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