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