packages/deriver/src/Deriver/Evaluator/Execution/Memory/Memory.php
1<?php
2
3declare(strict_types=1);
4
5namespace Deriver\Deriver\Evaluator\Execution\Memory;
6
7use Deriver\Deriver\Evaluator\Scalar\Arrays;
8use Deriver\Deriver\Evaluator\Term;
9
10/**
11 * Copy-on-write cells, object records, globals, and static storage.
12 * @visibility Deriver\Deriver\Evaluator
13 * @visibility Deriver\Deriver\Evaluator\Execution
14 */
15final class Memory
16{
17 /**
18 * @var array<string, Term> Versioned storage roots.
19 */
20 public array $cells = [];
21 /**
22 * @var array<string, string> Runtime object class identities.
23 */
24 public array $classes = [];
25 /**
26 * @var array<string, array<string, string>> Declared property types by storage root and slot.
27 */
28 public array $propertyTypes = [];
29 /**
30 * @var array<string, int> Cell write versions.
31 */
32 public array $versions = [];
33 /**
34 * @var array<string, string> Last explained write by memory root.
35 */
36 public array $writers = [];
37 /**
38 * @var array<string, array<int|string, true>> Readonly slots eligible for one write during clone.
39 */
40 public array $cloneWrites = [];
41 /**
42 * @var array<string, \Deriver\Deriver\Evaluator\Call\State\StateSlot> Registered abstract object lifecycle contracts.
43 */
44 public array $slotContracts = [];
45 /**
46 * Allocation and evaluation event sequence.
47 */
48 public int $sequence = 0;
49 /**
50 * @var array<string, LiveArray> Active live foreach cursors shared with nested source calls.
51 */
52 public array $liveArrays = [];
53 /**
54 * Cause of possible writes to shared roots not yet materialized in this path.
55 */
56 public ?string $unknownShared = null;
57
58 /**
59 * Allocates a distinct cell or object event within an execution path.
60 * @param string $prefix Allocation category
61 * @return string Fresh identity
62 */
63 public function fresh(string $prefix): string
64 {
65 return $prefix . ':' . $this->sequence++;
66 }
67
68 /**
69 * Allocates a cell with an explicit initialization state.
70 * @param Term $value Initial value
71 * @return Location New cell address
72 */
73 public function allocate(Term $value): Location
74 {
75 $id = $this->fresh('cell');
76 $this->cells[$id] = $value;
77 $this->versions[$id] = 0;
78 return new Location($id);
79 }
80
81 /**
82 * Reads storage while following reference cells.
83 * @param Location $location Address
84 * @return Term Current value or an explicit uninitialized state
85 */
86 public function read(Location $location): Term
87 {
88 if ($location->unknown) {
89 return Term::opaque('UNKNOWN_LOCATION');
90 }
91 $value = $this->dereference($this->cells[$location->root] ?? new Term(\Deriver\Deriver\Evaluator\TermKind::Uninitialized));
92 foreach ($location->path as $key) {
93 if ($value->kind !== \Deriver\Deriver\Evaluator\TermKind::Array) {
94 if ($value->kind === \Deriver\Deriver\Evaluator\TermKind::Uninitialized) {
95 return $value;
96 }
97 $value = new Term(\Deriver\Deriver\Evaluator\TermKind::ArrayRead, operands: [$value, Term::constant($key)]);
98 continue;
99 }
100 $value = $this->element($value, $key);
101 }
102 return $value;
103 }
104
105 /**
106 * Selects and dereferences an array slot.
107 * @param Term $array Array shape
108 * @param int|string $key Normalized key
109 * @return Term Dereferenced entry or explicit absence
110 */
111 public function element(Term $array, int|string $key): Term
112 {
113 return $this->dereference($array->operands[$key] ?? (($array->attributes['open'] ?? false) === true ? Term::opaque('UNKNOWN_ARRAY_KEY') : new Term(\Deriver\Deriver\Evaluator\TermKind::Uninitialized)));
114 }
115
116 /**
117 * Follows reference cells with cycle detection.
118 * @param Term $value Stored value
119 * @return Term Referenced value
120 */
121 public function dereference(Term $value): Term
122 {
123 $seen = [];
124 while ($value->kind === \Deriver\Deriver\Evaluator\TermKind::Cell && is_string($value->literal)) {
125 if (isset($seen[$value->literal])) {
126 return new Term(\Deriver\Deriver\Evaluator\TermKind::Opaque, 'CYCLIC_REFERENCE', attributes: ['type' => 'mixed', 'dependencyCoverage' => 'partial']);
127 }
128 $seen[$value->literal] = true;
129 $value = $this->cells[$value->literal] ?? new Term(\Deriver\Deriver\Evaluator\TermKind::Uninitialized);
130 }
131 return $value;
132 }
133
134 /**
135 * Writes through an address, preserving unrelated keys and reference elements.
136 * @param Location $location Destination
137 * @param Term $value New value
138 * @param bool $replacement Whether this operation replaces the entire selected array
139 */
140 public function write(Location $location, Term $value, bool $replacement = true): void
141 {
142 if ($location->unknown) {
143 $this->cells[$location->root] = Term::opaque('UNKNOWN_WRITE', dependencies: [$value]);
144 } else {
145 $this->writePath($location->root, $location->path, $value, replacement: $replacement);
146 }
147 $this->versions[$location->root] = ($this->versions[$location->root] ?? 0) + 1;
148 }
149
150 /**
151 * Updates a path within one storage root.
152 * @param string $root Root cell identity
153 * @param list<int|string> $path Ordered array keys
154 * @param Term $value Assigned value
155 * @param bool $rebind Whether to replace a final reference slot
156 * @param bool $replacement Whether a root write represents whole-array assignment
157 */
158 public function writePath(string $root, array $path, Term $value, bool $rebind = false, bool $replacement = true): void
159 {
160 $before = $this->cells[$root] ?? new Term(\Deriver\Deriver\Evaluator\TermKind::Uninitialized);
161 if ($before->kind === \Deriver\Deriver\Evaluator\TermKind::Cell && is_string($before->literal)) {
162 $this->writePath($before->literal, $path, $value, $rebind, $replacement);
163 return;
164 }
165 $this->cells[$root] = $this->replace($before, $path, $value, $rebind, $replacement);
166 $this->synchronize($root, $before, $this->cells[$root], $replacement && $path === []);
167 }
168
169 /**
170 * Rebuilds only the affected part of a persistent array value.
171 * @param Term $before Previous aggregate
172 * @param list<int|string> $path Remaining path
173 * @param Term $value Assigned value
174 * @param bool $rebind Whether to replace a final reference slot
175 * @param bool $replacement Whether a root write represents whole-array assignment
176 * @return Term Updated aggregate
177 */
178 public function replace(Term $before, array $path, Term $value, bool $rebind = false, bool $replacement = true): Term
179 {
180 if ($path === [] && $rebind) {
181 return $value;
182 }
183 if ($before->kind === \Deriver\Deriver\Evaluator\TermKind::Cell && is_string($before->literal)) {
184 $this->writePath($before->literal, $path, $value, $rebind, $replacement);
185 return $before;
186 }
187 if ($path === []) {
188 return $value;
189 }
190 $key = array_shift($path);
191 $entries = $before->kind === \Deriver\Deriver\Evaluator\TermKind::Array ? $before->operands : [];
192 $entry = $this->replace($entries[$key] ?? new Term(\Deriver\Deriver\Evaluator\TermKind::Uninitialized), $path, $value, $rebind, $replacement);
193 $base = $before->kind === \Deriver\Deriver\Evaluator\TermKind::Array ? $before : Term::array([], $before->kind !== \Deriver\Deriver\Evaluator\TermKind::Uninitialized && !($before->kind === \Deriver\Deriver\Evaluator\TermKind::Constant && $before->literal === null));
194 return (new Arrays())->set($base, Term::constant($key), $entry);
195 }
196
197 /**
198 * Materializes a reference cell only when an address escapes by reference.
199 * A global variable's slot then holds only a pointer to the shared cell, so unsetting the name leaves other references intact.
200 * @param Location $location Escaping address
201 * @return string Shared cell identity
202 */
203 public function reference(Location $location): string
204 {
205 if (!$location->unknown && $this->read($location)->kind === \Deriver\Deriver\Evaluator\TermKind::Uninitialized) {
206 $this->write($location, Term::constant(null));
207 }
208 if ($location->path === []) {
209 $root = $location->root;
210 $seen = [];
211 while (($this->cells[$root] ?? null)?->kind === \Deriver\Deriver\Evaluator\TermKind::Cell && is_string($this->cells[$root]->literal) && !isset($seen[$root])) {
212 $seen[$root] = true;
213 $root = $this->cells[$root]->literal;
214 }
215 if (!$location->unknown && str_starts_with($root, 'global:')) {
216 $cell = $this->allocate($this->cells[$root] ?? Term::constant(null));
217 $this->cells[$root] = new Term(\Deriver\Deriver\Evaluator\TermKind::Cell, $cell->root);
218 return $cell->root;
219 }
220 return $root;
221 }
222 $raw = $this->raw($location);
223 if ($raw->kind === \Deriver\Deriver\Evaluator\TermKind::Cell && is_string($raw->literal)) {
224 return $raw->literal;
225 }
226 $cell = $this->allocate($this->read($location));
227 $this->write($location, new Term(\Deriver\Deriver\Evaluator\TermKind::Cell, $cell->root));
228 return $cell->root;
229 }
230
231 /**
232 * Reads the final slot without dereferencing its contents.
233 * @param Location $location Address
234 * @return Term Raw stored slot
235 */
236 public function raw(Location $location): Term
237 {
238 $value = $this->cells[$location->root] ?? new Term(\Deriver\Deriver\Evaluator\TermKind::Uninitialized);
239 foreach ($location->path as $key) {
240 $value = $this->dereference($value)->operands[$key] ?? new Term(\Deriver\Deriver\Evaluator\TermKind::Uninitialized);
241 }
242 return $value;
243 }
244
245 /**
246 * Removes an array entry without changing the referenced cell it held.
247 * @param Location $location Removed address
248 */
249 public function remove(Location $location): void
250 {
251 if ($location->path === []) {
252 unset($this->cells[$location->root]);
253 return;
254 }
255 $path = $location->path;
256 $key = array_pop($path);
257 $parent = new Location($location->root, $path);
258 $before = $this->read($parent);
259 $entries = $before->operands;
260 unset($entries[$key]);
261 $this->write($parent, new Term(\Deriver\Deriver\Evaluator\TermKind::Array, operands: $entries, attributes: [...$before->attributes, 'next' => (new Arrays())->next($before)]), replacement: false);
262 }
263
264 /**
265 * Updates active by-reference cursors after one actual storage mutation.
266 * @param string $root Mutated reference cell
267 * @param Term $before Previous stored array
268 * @param Term $after New stored array
269 * @param bool $replacement Whether the entire referenced array was assigned
270 */
271 public function synchronize(string $root, Term $before, Term $after, bool $replacement): void
272 {
273 foreach ($this->liveArrays as $id => $cursor) {
274 if ($cursor->location->root === $root) {
275 $this->liveArrays[$id] = $cursor->changed($before, $after, $replacement);
276 }
277 }
278 }
279
280 /**
281 * Dereferences array elements for public observations with a finite cycle boundary.
282 * @param Term $value Observed value
283 * @param int $depth Structural depth
284 * @return Term Observable immutable value
285 */
286 public function materialize(Term $value, int $depth = 0): Term
287 {
288 return (new Materialization($this))->read($value, $depth);
289 }
290}
291