packages/deriver/src/Deriver/Evaluator/Execution/InstructionTransfer.php

1<?php
2
3declare(strict_types=1);
4
5namespace Deriver\Deriver\Evaluator\Execution;
6
7use Deriver\Deriver\Evaluator\Builtin\TypePredicates;
8use Deriver\Deriver\Evaluator\Execution\Call\CallExecutor;
9use Deriver\Deriver\Evaluator\Execution\Call\Closure\Capture;
10use Deriver\Deriver\Evaluator\Execution\Call\Contract\Effects;
11use Deriver\Deriver\Evaluator\Execution\Call\Contract\ExternalBody;
12use Deriver\Deriver\Evaluator\Execution\Call\PassedArgument;
13use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Arguments;
14use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Transfer;
15use Deriver\Deriver\Evaluator\Execution\Call\UnknownCall;
16use Deriver\Deriver\Evaluator\Execution\Control\Handler;
17use Deriver\Deriver\Evaluator\Execution\Control\IterationStep;
18use Deriver\Deriver\Evaluator\Execution\Memory\Slots\SlotReference;
19use Deriver\Deriver\Evaluator\Execution\Memory\Slots\StateStorage;
20use Deriver\Deriver\Evaluator\Execution\Offset\Protocol;
21use Deriver\Deriver\Evaluator\Execution\Offset\ProtocolAccess;
22use Deriver\Deriver\Evaluator\Execution\Offset\Reader;
23use Deriver\Deriver\Evaluator\Execution\Operation\Conversions;
24use Deriver\Deriver\Evaluator\Execution\Operation\ScalarErrors;
25use Deriver\Deriver\Evaluator\Execution\Transfer\CallableTransfer;
26use Deriver\Deriver\Evaluator\Execution\Transfer\CompoundAssignment;
27use Deriver\Deriver\Evaluator\Execution\Transfer\ConstantTransfer;
28use Deriver\Deriver\Evaluator\Execution\Transfer\MemoryStep;
29use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyReference;
30use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyTransfer;
31use Deriver\Deriver\Evaluator\Execution\Transfer\PureStep;
32use Deriver\Deriver\Evaluator\Execution\Transfer\ReferenceAssignment;
33use Deriver\Deriver\Evaluator\Scalar\Arrays;
34use Deriver\Deriver\Lowerer\Graph\CallableGraph;
35use Deriver\Deriver\Lowerer\Graph\Instruction;
36
37/**
38 * Connects shared instruction semantics to the abstract machine.
39 * @visibility namespace
40 */
41final class InstructionTransfer
42{
43    /**
44     * @param Evaluator $machine Calling evaluator
45     */
46    public function __construct(public readonly Evaluator $machine)
47    {
48    }
49
50    /**
51     * Dispatches one instruction to its semantic operation.
52     * @param CallableGraph $callable Current callable
53     * @param Instruction $instruction \Deriver\Deriver\Lowerer\Graph\Instruction
54     * @param State $state Input path
55     * @return list<State> Result paths
56     */
57    public function apply(CallableGraph $callable, Instruction $instruction, State $state): array
58    {
59        $op = $instruction->operation;
60        $prepared = $this->preparation($callable, $instruction, $state);
61        if ($prepared !== null) {
62            return $prepared;
63        }
64        if ($op === 'compound') {
65            return (new CompoundAssignment($this->machine))->apply($callable, $instruction, $state);
66        }
67        if (in_array($op, ['unsupported', 'symbol-table-boundary', 'uncertain-order'], true)) {
68            return $this->boundary($callable, $instruction, $state);
69        }
70        $storage = $this->storage($callable, $instruction, $state);
71        if ($storage !== null) {
72            return $storage;
73        }
74        $conversion = (new Conversions($this->machine))->apply($callable, $instruction, $state);
75        if ($conversion !== null) {
76            return $conversion;
77        }
78        $context = $this->machine->context;
79        if ($op === 'array-read') {
80            return $this->offsetRead($callable, $instruction, $state);
81        }
82        if (in_array($op, ['constant-fetch', 'class-constant'], true)) {
83            return (new ConstantTransfer($this->machine))->apply($callable, $instruction, $state);
84        }
85        if (in_array($op, ['invoke', 'invoke-method', 'invoke-static', 'new', 'clone', 'intrinsic'], true)) {
86            return (new CallExecutor($this->machine))->instruction($callable, $instruction, $state);
87        }
88        if (in_array($op, ['local', 'dynamic-local', 'element-address', 'field-address', 'static-address', 'unsupported-address', 'returned-address', 'read', 'read-silent', 'write', 'alias', 'reference', 'increment', 'unset', 'global', 'static-local', 'static-initialized'], true)) {
89            $value = (new MemoryStep($context))->evaluate($callable, $instruction, $state);
90        } elseif (in_array($op, ['iterator', 'iterate', 'iterator-key', 'iterator-value', 'iterator-address', 'iterator-release'], true)) {
91            $value = (new IterationStep($context))->evaluate($instruction, $state);
92        } elseif ($op === 'array-unpack') {
93            return $this->unpack($instruction, $state);
94        } elseif ($op === 'array-set') {
95            $array = $state->value($instruction->operands[0]);
96            $key = $instruction->operands[1] === '' ? null : (new Reader($context))->key($state->value($instruction->operands[1]), $instruction);
97            $item = $state->registers[$instruction->operands[2]] ?? \Deriver\Deriver\Evaluator\Term::opaque('UNCOMPUTED_REGISTER');
98            $value = (new Arrays())->set($array, $key, $item);
99        } elseif (in_array($op, ['closure', 'callable', 'callable-method', 'instanceof'], true)) {
100            $value = (new CallableTransfer($context))->evaluate($callable, $instruction, $state);
101        } else {
102            $value = $this->other($callable, $instruction, $state);
103        }
104        $state->registers[$instruction->result] = $value;
105        return (new ScalarErrors($context))->paths($instruction, $state);
106    }
107
108    /**
109     * Unpacks arrays while preserving invalid-type errors and unknown iterator effects.
110     * @param Instruction $instruction Array construction step
111     * @param State $state Current path
112     * @return list<State> Normal and possible exceptional paths
113     */
114    public function unpack(Instruction $instruction, State $state): array
115    {
116        $array = $state->value($instruction->operands[0]);
117        $item = $state->value($instruction->operands[2]);
118        if ((new TypePredicates())->apply('is_array', $item)->literal === true) {
119            $state->registers[$instruction->result] = (new Arrays())->merge($array, $item);
120            if ($state->registers[$instruction->result]->kind === \Deriver\Deriver\Evaluator\TermKind::ArrayMerge && !(new Arrays())->appendable($array)) {
121                $this->machine->context->frontier('WIDENED', $instruction->source, 'symbolic-unpack-append', [$array, $item]);
122                $exception = $state->fork();
123                $exception->completion = new Completion\Completion(Completion\CompletionKind::Throw, new \Deriver\Deriver\Evaluator\Term(\Deriver\Deriver\Evaluator\TermKind::Throwable, 'Error'));
124                return [$state, $exception];
125            }
126            return [$state];
127        }
128        if ($item->kind === \Deriver\Deriver\Evaluator\TermKind::Constant || $item->kind === \Deriver\Deriver\Evaluator\TermKind::Closure) {
129            $state->completion = new Completion\Completion(Completion\CompletionKind::Throw, new \Deriver\Deriver\Evaluator\Term(\Deriver\Deriver\Evaluator\TermKind::Throwable, $item->kind === \Deriver\Deriver\Evaluator\TermKind::Closure ? 'TypeError' : 'Error'));
130            return [$state];
131        }
132        return (new UnknownCall($this->machine->context))->apply($state, $instruction, [new PassedArgument($item)], null, 'UNSUPPORTED_LANGUAGE_FEATURE');
133    }
134
135    /**
136     * Resolves call preparation and evaluator state instructions before ordinary value transfer.
137     * @param CallableGraph $callable Executing graph
138     * @param Instruction $instruction Current instruction
139     * @param State $state Current path
140     * @return list<State>|null Specialized result, or null for ordinary instructions
141     */
142    public function preparation(CallableGraph $callable, Instruction $instruction, State $state): ?array
143    {
144        $op = $instruction->operation;
145        if ($op === 'iterator-value') {
146            return (new IterationStep($this->machine->context))->candidates($instruction, $state);
147        }
148        if ($op === 'external-body') {
149            return (new ExternalBody($this->machine->context))->apply($callable, $instruction, $state);
150        }
151        if ($op === 'evaluator-havoc') {
152            return (new Effects($this->machine->context))->apply($instruction, $state);
153        }
154        if ($op === 'evaluator-state-address') {
155            $state->registers[$instruction->result] = (new StateStorage($this->machine->context))->address($instruction, $state);
156            return [$state];
157        }
158        if ($op === 'call-prepare') {
159            return (new Transfer($this->machine))->apply($callable, $instruction, $state);
160        }
161        if ($op === 'callable' || $op === 'callable-method') {
162            return (new Capture($this->machine))->apply($callable, $instruction, $state);
163        }
164        if ($op === 'argument') {
165            return (new Arguments($this->machine))->apply($callable, $instruction, $state);
166        }
167        return null;
168    }
169
170    /**
171     * Applies property, offset, and shared-reference checks before ordinary storage operations.
172     * @param CallableGraph $callable Executing callable
173     * @param Instruction $instruction Pending operation
174     * @param State $state Input path
175     * @return list<State>|null Checked paths or null when ordinary transfer may proceed
176     */
177    public function storage(CallableGraph $callable, Instruction $instruction, State $state): ?array
178    {
179        $op = $instruction->operation;
180        $slot = (new SlotReference($this->machine->context))->apply($callable, $instruction, $state);
181        if ($slot !== null) {
182            return $slot;
183        }
184        $source = $instruction->operands[1] ?? '';
185        if ($op === 'alias' && ($instruction->attributes['source-prepared'] ?? false) !== true && (isset($state->properties[$source]) || isset($state->offsets[$source]))) {
186            return (new PropertyReference($this->machine))->alias($callable, $instruction, $state);
187        }
188        $property = $state->properties[$instruction->operands[0] ?? ''] ?? null;
189        if ($property !== null && in_array($op, ['read', 'read-silent', 'write', 'increment', 'reference', 'alias', 'unset'], true)) {
190            return (new PropertyTransfer($this->machine))->apply($callable, $instruction, $state, $property);
191        }
192        if (isset($state->offsets[$instruction->operands[0] ?? '']) && in_array($op, ['read', 'read-silent', 'write', 'increment', 'reference', 'alias', 'unset'], true)) {
193            return (new Offset\Transfer($this->machine))->apply($callable, $instruction, $state);
194        }
195        if ($op === 'reference' && ($state->addresses[$instruction->operands[0] ?? '']->unknown ?? true)) {
196            return $this->boundary($callable, new \Deriver\Deriver\Evaluator\Call\BoundInstruction($instruction->id, 'unsupported', $instruction->source, $instruction->result, name: 'unknown-reference-location'), $state);
197        }
198        $referenceWrite = (new ReferenceAssignment($this->machine->context))->apply($callable, $instruction, $state);
199        if ($referenceWrite !== null) {
200            return $referenceWrite;
201        }
202        return null;
203    }
204
205    /**
206     * Reads offsets on computed values, including silent tests and ArrayAccess results.
207     * @param CallableGraph $callable Executing graph
208     * @param Instruction $instruction Computed offset read
209     * @param State $state Evaluated container and key
210     * @return list<State> Read results and exceptions
211     */
212    public function offsetRead(CallableGraph $callable, Instruction $instruction, State $state): array
213    {
214        $container = $state->value($instruction->operands[0]);
215        $key = $state->value($instruction->operands[1]);
216        $reader = new Reader($this->machine->context);
217        $silent = ($instruction->attributes['silent'] ?? false) === true;
218        if ($container->kind === \Deriver\Deriver\Evaluator\TermKind::Object && !$reader->plainObject($container)) {
219            $read = new \Deriver\Deriver\Evaluator\Call\BoundInstruction($instruction->id, $silent ? 'read-silent' : 'array-read', $instruction->source, $instruction->result, $instruction->operands, attributes: $instruction->attributes);
220            return (new Protocol($this->machine))->apply($callable, $read, $state, new ProtocolAccess($container, $key));
221        }
222        return (new Offset\Transfer($this->machine))->result($state, $instruction, $reader->read($container, $key, $instruction, $state, $silent));
223    }
224
225    /**
226     * Preserves normal and exceptional exits when arbitrary boundary effects are possible.
227     * @param CallableGraph $callable Executing callable
228     * @param Instruction $instruction Unsupported or unordered operation
229     * @param State $state Input path
230     * @return list<State> Inclusive normal and unknown-throwable paths after conservative effects
231     */
232    public function boundary(CallableGraph $callable, Instruction $instruction, State $state): array
233    {
234        $state->registers[$instruction->result] = $this->other($callable, $instruction, $state);
235        $exception = $state->fork();
236        $exception->completion = new Completion\Completion(Completion\CompletionKind::Throw, new \Deriver\Deriver\Evaluator\Term(\Deriver\Deriver\Evaluator\TermKind::Throwable, 'Throwable', attributes: ['uncertain' => true]));
237        return [$state, $exception];
238    }
239
240    /**
241     * Applies exception and boundary instructions before pure evaluation.
242     * @param CallableGraph $callable Current callable
243     * @param Instruction $instruction \Deriver\Deriver\Lowerer\Graph\Instruction
244     * @param State $state Current path
245     * @return \Deriver\Deriver\Evaluator\Term Result value
246     */
247    public function other(CallableGraph $callable, Instruction $instruction, State $state): \Deriver\Deriver\Evaluator\Term
248    {
249        $context = $this->machine->context;
250        if ($instruction->operation === 'enter-try') {
251            $region = $instruction->attributes['region'] ?? 0;
252            if (is_int($region) && isset($callable->regions[$region])) {
253                $state->handlers[] = new Handler($callable->regions[$region]);
254            }
255            return \Deriver\Deriver\Evaluator\Term::constant(null);
256        }
257        if ($instruction->operation === 'throw') {
258            $value = $state->value($instruction->operands[0]);
259            $state->completion = new Completion\Completion(Completion\CompletionKind::Throw, $value);
260            return $value;
261        }
262        if (in_array($instruction->operation, ['unsupported', 'symbol-table-boundary', 'uncertain-order'], true)) {
263            $reason = $instruction->operation === 'uncertain-order' ? 'UNSPECIFIED_EVALUATION_ORDER' : ($instruction->operation === 'symbol-table-boundary' ? $instruction->name : 'UNSUPPORTED_LANGUAGE_FEATURE');
264            $values = array_map($state->value(...), $instruction->operands);
265            if (($instruction->attributes['effects'] ?? '') === 'reachable') {
266                (new Havoc())->call($state, $values, [], $reason);
267            } else {
268                (new Havoc())->all($state, $reason);
269            }
270            return $context->frontier($reason, $instruction->source, $instruction->name, $values, (string) ($instruction->attributes['type'] ?? 'mixed'));
271        }
272        return (new PureStep($context))->evaluate($callable, $instruction, $state);
273    }
274}
275