packages/deriver/tests/Unit/Deriver/Evaluator/Execution/Offset/PathTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Execution\Offset;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\BoundInstruction;
10use Deriver\Deriver\Evaluator\Call\Evaluators;
11use Deriver\Deriver\Evaluator\Call\Extensions;
12use Deriver\Deriver\Evaluator\Call\Registrations;
13use Deriver\Deriver\Evaluator\Call\StateRegistry;
14use Deriver\Deriver\Evaluator\EvaluatorConfig;
15use Deriver\Deriver\Evaluator\Evidence\Frontier;
16use Deriver\Deriver\Evaluator\Execution\Call\TypeBinding;
17use Deriver\Deriver\Evaluator\Execution\Call\TypeCheck;
18use Deriver\Deriver\Evaluator\Execution\Completion\Completion;
19use Deriver\Deriver\Evaluator\Execution\Context;
20use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
21use Deriver\Deriver\Evaluator\Execution\Memory\Location;
22use Deriver\Deriver\Evaluator\Execution\Memory\Memory;
23use Deriver\Deriver\Evaluator\Execution\Memory\ReferenceConstraint;
24use Deriver\Deriver\Evaluator\Execution\Offset\Address;
25use Deriver\Deriver\Evaluator\Execution\Offset\Path;
26use Deriver\Deriver\Evaluator\Execution\Offset\ProtocolAccess;
27use Deriver\Deriver\Evaluator\Execution\Offset\Reader;
28use Deriver\Deriver\Evaluator\Execution\Offset\StringAccess;
29use Deriver\Deriver\Evaluator\Execution\Offset\Strings;
30use Deriver\Deriver\Evaluator\Execution\State;
31use Deriver\Deriver\Evaluator\Execution\Transfer\ReferenceAssignment;
32use Deriver\Deriver\Evaluator\Request\Budget;
33use Deriver\Deriver\Evaluator\Request\QueryScope;
34use Deriver\Deriver\Evaluator\Request\ReturnQuery;
35use Deriver\Deriver\Evaluator\Scalar\Arrays;
36use Deriver\Deriver\Evaluator\Scalar\IntegerConversion;
37use Deriver\Deriver\Evaluator\Scalar\Operations;
38use Deriver\Deriver\Evaluator\TargetProfile;
39use Deriver\Deriver\Evaluator\Term;
40use Deriver\Deriver\Evaluator\TermKind;
41use Deriver\Deriver\Execution\QueryExecution;
42use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
43use Deriver\Deriver\Lowerer\Declaration\CallableSource;
44use Deriver\Deriver\Lowerer\Declaration\Index;
45use Deriver\Deriver\Lowerer\Declaration\Indexer;
46use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
47use Deriver\Deriver\Lowerer\ExpressionLowering;
48use Deriver\Deriver\Lowerer\Graph\BasicBlock;
49use Deriver\Deriver\Lowerer\Graph\CallableGraph;
50use Deriver\Deriver\Lowerer\Graph\Instruction;
51use Deriver\Deriver\Lowerer\Graph\Terminator;
52use Deriver\Deriver\Lowerer\Hierarchy\Builtins;
53use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
54use Deriver\Deriver\Lowerer\InstructionEmitter;
55use Deriver\Deriver\Lowerer\Lowerer;
56use Deriver\Deriver\Lowerer\LoweringScope;
57use Deriver\Deriver\Lowerer\StatementLowering;
58use Deriver\Deriver\MicroEvaluatorConversion;
59use Deriver\Deriver\Parser\Node\File;
60use Deriver\Deriver\Parser\Parser;
61use Deriver\Deriver\Parser\ParserConfig;
62use Deriver\Deriver\Parser\TargetSyntax;
63use Deriver\Deriver\Providers\ProviderExpansion;
64use Deriver\Deriver\Resolver\AssignmentPatterns;
65use Deriver\Deriver\Resolver\ClassScope;
66use Deriver\Deriver\Resolver\ExpressionValidation;
67use Deriver\Deriver\Resolver\MagicContext;
68use Deriver\Deriver\Resolver\Resolver;
69use Deriver\Deriver\SnapshotIdentity;
70use Deriver\Deriver\Sources\LineMap;
71use Deriver\Deriver\Sources\Path as SourcesPath;
72use Deriver\Deriver\Sources\Source;
73use Deriver\Deriver\Sources\SourceRef;
74use Deriver\Deriver\Sources\Sources;
75use Deriver\Exception\ParserException;
76use Deriver\Exception\ResolutionException;
77use PHPUnit\Framework\Attributes\CoversClass;
78use PHPUnit\Framework\Attributes\DataProvider;
79use PHPUnit\Framework\Attributes\Small;
80use PHPUnit\Framework\Attributes\UsesClass;
81use PHPUnit\Framework\TestCase;
82
83/**
84 * @covers \Deriver\Deriver\Evaluator\Execution\Offset\Path
85 */
86#[UsesClass(Resolver::class)]
87#[UsesClass(ExpressionValidation::class)]
88#[UsesClass(ResolutionException::class)]
89#[UsesClass(ParserConfig::class)]
90#[UsesClass(ParserException::class)]
91#[CoversClass(Path::class)]
92#[UsesClass(BasicBlock::class)]
93#[UsesClass(CallableGraph::class)]
94#[UsesClass(CallableIdentity::class)]
95#[UsesClass(Instruction::class)]
96#[UsesClass(Terminator::class)]
97#[UsesClass(Builtins::class)]
98#[UsesClass(Dispatch::class)]
99#[UsesClass(TypeBinding::class)]
100#[UsesClass(TypeCheck::class)]
101#[UsesClass(Completion::class)]
102#[UsesClass(Context::class)]
103#[UsesClass(Address::class)]
104#[UsesClass(ProtocolAccess::class)]
105#[UsesClass(Reader::class)]
106#[UsesClass(StringAccess::class)]
107#[UsesClass(Strings::class)]
108#[UsesClass(State::class)]
109#[UsesClass(ReferenceAssignment::class)]
110#[UsesClass(Location::class)]
111#[UsesClass(Memory::class)]
112#[UsesClass(ReferenceConstraint::class)]
113#[UsesClass(Extensions::class)]
114#[UsesClass(Evaluators::class)]
115#[UsesClass(StateRegistry::class)]
116#[UsesClass(DeriverConfig::class)]
117#[UsesClass(Sources::class)]
118#[UsesClass(SourcesPath::class)]
119#[UsesClass(Source::class)]
120#[UsesClass(TargetProfile::class)]
121#[UsesClass(Budget::class)]
122#[UsesClass(QueryScope::class)]
123#[UsesClass(ReturnQuery::class)]
124#[UsesClass(SourceRef::class)]
125#[UsesClass(Frontier::class)]
126#[UsesClass(Parser::class)]
127#[UsesClass(File::class)]
128#[UsesClass(Lowerer::class)]
129#[UsesClass(ExpressionLowering::class)]
130#[UsesClass(InstructionEmitter::class)]
131#[UsesClass(LoweringScope::class)]
132#[UsesClass(StatementLowering::class)]
133#[UsesClass(CallableSource::class)]
134#[UsesClass(Indexer::class)]
135#[UsesClass(Index::class)]
136#[UsesClass(Composition::class)]
137#[UsesClass(LineMap::class)]
138#[UsesClass(MagicContext::class)]
139#[UsesClass(AssignmentPatterns::class)]
140#[UsesClass(ClassScope::class)]
141#[UsesClass(TargetSyntax::class)]
142#[UsesClass(Arrays::class)]
143#[UsesClass(IntegerConversion::class)]
144#[UsesClass(Operations::class)]
145#[UsesClass(Term::class)]
146#[Small]
147final class PathTest extends TestCase
148{
149 public function testChainPreservesTheOriginalContainerAndNestedKeys(): void
150 {
151 $source = '<?php function target(){return 1;}';
152 $budget = new Budget();
153 $configuration = new DeriverConfig();
154 $contributions = new Contributions();
155 $input = new Sources([new Source('fixture.php', $source)]);
156 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
157 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
158 $contributions1 = $providers->contributions;
159 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
160 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
161 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
162 $snapshotId = $identity->id;
163 $input1 = $providers->input;
164 $parser = new Parser(new ParserConfig($target->version));
165 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
166 $program1 = new Index($snapshotId, $trees);
167 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
168 $snapshotId1 = 'test';
169 $input2 = new Sources([new Source('fixture.php', $source)]);
170 $profile = $execution->configuration->target;
171 $parser1 = new Parser(new ParserConfig($profile->version));
172 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
173 $program = new Index($snapshotId1, $trees1);
174 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
175 $body = $context->program->callable('target');
176 self::assertNotNull($body);
177 $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
178 $state = new State();
179 $base = $state->memory->allocate(Term::constant(null));
180 $state->addresses['base'] = $base;
181 $state->offsets['slot'] = new Address('base', Term::constant('x'));
182 $path = new Path($context);
183 $state->offsets['nested'] = new Address('slot', Term::constant('y'));
184 $chain = $path->chain($state, 'nested');
185 self::assertSame($base, $chain['base']);
186 self::assertSame(['slot', 'nested'], $chain['offsets']);
187 }
188 public function testReadDoesNotAutovivifyNullContainers(): void
189 {
190 $source = '<?php function target(){return 1;}';
191 $budget = new Budget();
192 $configuration = new DeriverConfig();
193 $contributions = new Contributions();
194 $input = new Sources([new Source('fixture.php', $source)]);
195 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
196 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
197 $contributions1 = $providers->contributions;
198 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
199 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
200 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
201 $snapshotId = $identity->id;
202 $input1 = $providers->input;
203 $parser = new Parser(new ParserConfig($target->version));
204 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
205 $program1 = new Index($snapshotId, $trees);
206 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
207 $snapshotId1 = 'test';
208 $input2 = new Sources([new Source('fixture.php', $source)]);
209 $profile = $execution->configuration->target;
210 $parser1 = new Parser(new ParserConfig($profile->version));
211 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
212 $program = new Index($snapshotId1, $trees1);
213 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
214 $body = $context->program->callable('target');
215 self::assertNotNull($body);
216 $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
217 $state = new State();
218 $base = $state->memory->allocate(Term::constant(null));
219 $state->addresses['base'] = $base;
220 $state->offsets['slot'] = new Address('base', Term::constant('x'));
221 $path = new Path($context);
222 $read = new BoundInstruction('read', 'read', $body->source, 'result', ['slot']);
223 $result = $path->read($state, $read);
224 self::assertInstanceOf(Term::class, $result);
225 self::assertNull($result->native());
226 self::assertNull($state->memory->read($base)->native());
227 }
228 public function testLocateRetainsArrayCreationBeforeAnInvalidKey(): void
229 {
230 $source = '<?php function target(){return 1;}';
231 $budget = new Budget();
232 $configuration = new DeriverConfig();
233 $contributions = new Contributions();
234 $input = new Sources([new Source('fixture.php', $source)]);
235 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
236 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
237 $contributions1 = $providers->contributions;
238 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
239 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
240 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
241 $snapshotId = $identity->id;
242 $input1 = $providers->input;
243 $parser = new Parser(new ParserConfig($target->version));
244 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
245 $program1 = new Index($snapshotId, $trees);
246 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
247 $snapshotId1 = 'test';
248 $input2 = new Sources([new Source('fixture.php', $source)]);
249 $profile = $execution->configuration->target;
250 $parser1 = new Parser(new ParserConfig($profile->version));
251 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
252 $program = new Index($snapshotId1, $trees1);
253 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
254 $body = $context->program->callable('target');
255 self::assertNotNull($body);
256 $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
257 $state = new State();
258 $base = $state->memory->allocate(Term::constant(null));
259 $state->addresses['base'] = $base;
260 $state->offsets['slot'] = new Address('base', Term::constant('x'));
261 $path = new Path($context);
262 $state->offsets['slot'] = new Address('base', Term::array([]));
263 $result = $path->locate($body, $state, $instruction);
264 self::assertInstanceOf(Term::class, $result);
265 self::assertSame('TypeError', $result->literal);
266 self::assertSame([], $state->memory->read($base)->native());
267 }
268 public function testPrepareRejectsScalarMutationWithoutChangingTheScalar(): void
269 {
270 $source = '<?php function target(){return 1;}';
271 $budget = new Budget();
272 $configuration = new DeriverConfig();
273 $contributions = new Contributions();
274 $input = new Sources([new Source('fixture.php', $source)]);
275 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
276 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
277 $contributions1 = $providers->contributions;
278 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
279 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
280 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
281 $snapshotId = $identity->id;
282 $input1 = $providers->input;
283 $parser = new Parser(new ParserConfig($target->version));
284 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
285 $program1 = new Index($snapshotId, $trees);
286 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
287 $snapshotId1 = 'test';
288 $input2 = new Sources([new Source('fixture.php', $source)]);
289 $profile = $execution->configuration->target;
290 $parser1 = new Parser(new ParserConfig($profile->version));
291 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
292 $program = new Index($snapshotId1, $trees1);
293 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
294 $body = $context->program->callable('target');
295 self::assertNotNull($body);
296 $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
297 $state = new State();
298 $base = $state->memory->allocate(Term::constant(null));
299 $state->addresses['base'] = $base;
300 $state->offsets['slot'] = new Address('base', Term::constant('x'));
301 $path = new Path($context);
302 $state->memory->write($base, Term::constant(4));
303 self::assertSame('Error', $path->prepare($body, $state, $base, $instruction, true)->literal);
304 self::assertSame(4, $state->memory->read($base)->native());
305 }
306 public function testCreateChecksDirectPropertyTypesBeforeArrayCreation(): void
307 {
308 $source = '<?php function target(){return 1;}';
309 $budget = new Budget();
310 $configuration = new DeriverConfig();
311 $contributions = new Contributions();
312 $input = new Sources([new Source('fixture.php', $source)]);
313 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
314 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
315 $contributions1 = $providers->contributions;
316 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
317 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
318 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
319 $snapshotId = $identity->id;
320 $input1 = $providers->input;
321 $parser = new Parser(new ParserConfig($target->version));
322 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
323 $program1 = new Index($snapshotId, $trees);
324 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
325 $snapshotId1 = 'test';
326 $input2 = new Sources([new Source('fixture.php', $source)]);
327 $profile = $execution->configuration->target;
328 $parser1 = new Parser(new ParserConfig($profile->version));
329 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
330 $program = new Index($snapshotId1, $trees1);
331 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
332 $body = $context->program->callable('target');
333 self::assertNotNull($body);
334 $instruction = new BoundInstruction('offset', 'write', $body->source, 'result', ['slot', 'rhs']);
335 $state = new State();
336 $base = $state->memory->allocate(Term::constant(null));
337 $state->addresses['base'] = $base;
338 $state->offsets['slot'] = new Address('base', Term::constant('x'));
339 $path = new Path($context);
340 $state->memory->cells['object'] = Term::array(['field' => Term::constant(null)]);
341 $state->memory->propertyTypes['object']['field'] = 'int|null';
342 $address = new Location('object', ['field']);
343 self::assertSame('TypeError', $path->create($body, $state, $address, Term::constant(null), $instruction)->literal);
344 self::assertNull($state->memory->read($address)->native());
345 }
346 public function testChainRetainsUnknownBasesAndDirectLocations(): void
347 {
348 $source = '<?php function target(){return 1;}';
349 $budget = new Budget();
350 $configuration = new DeriverConfig();
351 $contributions = new Contributions();
352 $input = new Sources([new Source('fixture.php', $source)]);
353 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
354 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
355 $contributions1 = $providers->contributions;
356 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
357 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
358 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
359 $snapshotId = $identity->id;
360 $input1 = $providers->input;
361 $parser = new Parser(new ParserConfig($target->version));
362 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
363 $program1 = new Index($snapshotId, $trees);
364 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
365 $snapshotId1 = 'test';
366 $input2 = new Sources([new Source('fixture.php', $source)]);
367 $profile = $execution->configuration->target;
368 $parser1 = new Parser(new ParserConfig($profile->version));
369 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
370 $program = new Index($snapshotId1, $trees1);
371 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
372 $state = new State();
373 $path = new Path($context);
374 $missing = $path->chain($state, 'absent');
375 $known = new Location('root', ['existing']);
376 $state->addresses['known'] = $known;
377 self::assertTrue($missing['base']->unknown);
378 self::assertSame([], $missing['offsets']);
379 self::assertSame(['base' => $known, 'offsets' => []], $path->chain($state, 'known'));
380 }
381 /**
382 * @param Term $container Root value
383 * @param string $operation Read mode
384 * @param string $kind Expected value category
385 * @param mixed $literal Expected payload
386 * @param list<string> $reasons Expected diagnostics
387 */
388 #[DataProvider('providerReadCases')]
389 public function testReadStopsAtErrorsAndHonorsSilentAbsence(Term $container, string $operation, string $kind, mixed $literal, array $reasons): void
390 {
391 $source = '<?php function target(){return 1;}';
392 $budget = new Budget();
393 $configuration = new DeriverConfig();
394 $contributions = new Contributions();
395 $input = new Sources([new Source('fixture.php', $source)]);
396 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
397 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
398 $contributions1 = $providers->contributions;
399 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
400 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
401 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
402 $snapshotId = $identity->id;
403 $input1 = $providers->input;
404 $parser = new Parser(new ParserConfig($target->version));
405 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
406 $program1 = new Index($snapshotId, $trees);
407 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
408 $snapshotId1 = 'test';
409 $input2 = new Sources([new Source('fixture.php', $source)]);
410 $profile = $execution->configuration->target;
411 $parser1 = new Parser(new ParserConfig($profile->version));
412 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
413 $program = new Index($snapshotId1, $trees1);
414 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
415 $body = $context->program->callable('target');
416 self::assertNotNull($body);
417 $state = new State();
418 $base = $state->memory->allocate($container);
419 $state->addresses['base'] = $base;
420 $state->offsets['first'] = new Address('base', Term::constant('x'));
421 $state->offsets['second'] = new Address('first', Term::constant('y'));
422 $result = (new Path($context))->read($state, new BoundInstruction('read', $operation, $body->source, 'result', ['second']));
423 self::assertInstanceOf(Term::class, $result);
424 self::assertSame($kind, $result->kind->value);
425 self::assertSame($literal, $result->literal);
426 self::assertSame($reasons, array_column($context->frontiers, 'code'));
427 self::assertSame($container, $state->memory->read($base));
428 }
429 /**
430 * @return iterable<string, array{Term, string, string, mixed, list<string>}>
431 */
432 public static function providerReadCases(): iterable
433 {
434 yield 'typed absence throws before offsets' => [new Term(TermKind::Uninitialized, attributes: ['type' => 'array']), 'read', 'throwable', 'Error', []];
435 yield 'typed silent absence' => [new Term(TermKind::Uninitialized, attributes: ['type' => 'array']), 'read-silent', 'constant', null, []];
436 yield 'untyped absence warns' => [new Term(TermKind::Uninitialized), 'read', 'constant', null, ['PHP_WARNING']];
437 yield 'mixed absence warns' => [new Term(TermKind::Uninitialized, attributes: ['type' => 'mixed']), 'read', 'constant', null, ['PHP_WARNING']];
438 yield 'nested array' => [Term::array(['x' => Term::array(['y' => Term::constant(19)])]), 'read', 'constant', 19, []];
439 yield 'string invalid key stops' => [Term::constant('text'), 'read', 'throwable', 'TypeError', []];
440 yield 'unknown container stops' => [Term::parameter('input'), 'read', 'opaque', 'OFFSET_OPERATION', []];
441 yield 'plain object stops' => [new Term(TermKind::Object, 'one', attributes: ['class' => 'stdClass']), 'read', 'throwable', 'Error', []];
442 }
443 public function testReadDefersAnOverloadedElementWithTheRemainingChain(): void
444 {
445 $source = '<?php function target(){return 1;}';
446 $budget = new Budget();
447 $configuration = new DeriverConfig();
448 $contributions = new Contributions();
449 $input = new Sources([new Source('fixture.php', $source)]);
450 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
451 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
452 $contributions1 = $providers->contributions;
453 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
454 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
455 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
456 $snapshotId = $identity->id;
457 $input1 = $providers->input;
458 $parser = new Parser(new ParserConfig($target->version));
459 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
460 $program1 = new Index($snapshotId, $trees);
461 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
462 $snapshotId1 = 'test';
463 $input2 = new Sources([new Source('fixture.php', $source)]);
464 $profile = $execution->configuration->target;
465 $parser1 = new Parser(new ParserConfig($profile->version));
466 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
467 $program = new Index($snapshotId1, $trees1);
468 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
469 $body = $context->program->callable('target');
470 self::assertNotNull($body);
471 $receiver = new Term(TermKind::Object, 'bag', attributes: ['class' => 'UnknownBag']);
472 $key = Term::constant('original');
473 $state = new State();
474 $state->addresses['base'] = $state->memory->allocate(Term::array(['first' => $receiver]));
475 $state->offsets['first'] = new Address('base', Term::constant('first'));
476 $state->offsets['second'] = new Address('first', $key);
477 $state->offsets['third'] = new Address('second', Term::constant(3));
478 $result = (new Path($context))->read($state, new BoundInstruction('read', 'read', $body->source, 'result', ['third']));
479 self::assertInstanceOf(ProtocolAccess::class, $result);
480 self::assertSame($receiver, $result->receiver);
481 self::assertSame($key, $result->key);
482 self::assertSame(['third'], $result->remaining);
483 }
484 /**
485 * @param Term $key Original key
486 * @param int|string $normalized PHP array key
487 * @param list<string> $reasons Expected diagnostics
488 */
489 #[DataProvider('providerArrayKeys')]
490 public function testLocateNormalizesArrayKeysAndRetainsTheOriginalRoot(Term $key, int|string $normalized, array $reasons): void
491 {
492 $source = '<?php function target(){return 1;}';
493 $budget = new Budget();
494 $configuration = new DeriverConfig();
495 $contributions = new Contributions();
496 $input = new Sources([new Source('fixture.php', $source)]);
497 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
498 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
499 $contributions1 = $providers->contributions;
500 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
501 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
502 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
503 $snapshotId = $identity->id;
504 $input1 = $providers->input;
505 $parser = new Parser(new ParserConfig($target->version));
506 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
507 $program1 = new Index($snapshotId, $trees);
508 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
509 $snapshotId1 = 'test';
510 $input2 = new Sources([new Source('fixture.php', $source)]);
511 $profile = $execution->configuration->target;
512 $parser1 = new Parser(new ParserConfig($profile->version));
513 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
514 $program = new Index($snapshotId1, $trees1);
515 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
516 $body = $context->program->callable('target');
517 self::assertNotNull($body);
518 $state = new State();
519 $base = $state->memory->allocate(Term::array(['outer' => Term::array([])]));
520 $state->addresses['base'] = new Location($base->root, ['outer']);
521 $state->offsets['offset'] = new Address('base', $key);
522 $result = (new Path($context))->locate($body, $state, new BoundInstruction('write', 'write', $body->source, 'result', ['offset', 'value']));
523 self::assertInstanceOf(Location::class, $result);
524 self::assertSame($base->root, $result->root);
525 self::assertSame(['outer', $normalized], $result->path);
526 self::assertSame($result, $state->addresses['offset']);
527 self::assertSame($key, $state->offsets['offset']->key);
528 self::assertSame(['outer' => []], $state->memory->read($base)->native());
529 self::assertSame($reasons, array_column($context->frontiers, 'code'));
530 }
531 /**
532 * @return iterable<string, array{Term, int|string, list<string>}>
533 */
534 public static function providerArrayKeys(): iterable
535 {
536 yield 'integer' => [Term::constant(4), 4, []];
537 yield 'negative integer' => [Term::constant(-2), -2, []];
538 yield 'string' => [Term::constant('name'), 'name', []];
539 yield 'numeric string' => [Term::constant('4'), 4, []];
540 yield 'leading zero string' => [Term::constant('04'), '04', []];
541 yield 'null' => [Term::constant(null), '', []];
542 yield 'true' => [Term::constant(true), 1, []];
543 yield 'false' => [Term::constant(false), 0, []];
544 yield 'integer float' => [Term::constant(2.0), 2, []];
545 yield 'fractional float' => [Term::constant(2.5), 2, ['PHP_WARNING']];
546 }
547 public function testLocateAppendStoresTheResolvedKeyForSubsequentReads(): void
548 {
549 $source = '<?php function target(){return 1;}';
550 $budget = new Budget();
551 $configuration = new DeriverConfig();
552 $contributions = new Contributions();
553 $input = new Sources([new Source('fixture.php', $source)]);
554 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
555 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
556 $contributions1 = $providers->contributions;
557 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
558 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
559 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
560 $snapshotId = $identity->id;
561 $input1 = $providers->input;
562 $parser = new Parser(new ParserConfig($target->version));
563 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
564 $program1 = new Index($snapshotId, $trees);
565 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
566 $snapshotId1 = 'test';
567 $input2 = new Sources([new Source('fixture.php', $source)]);
568 $profile = $execution->configuration->target;
569 $parser1 = new Parser(new ParserConfig($profile->version));
570 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
571 $program = new Index($snapshotId1, $trees1);
572 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
573 $body = $context->program->callable('target');
574 self::assertNotNull($body);
575 $state = new State();
576 $base = $state->memory->allocate(Term::array([4 => Term::constant('existing')]));
577 $state->addresses['base'] = $base;
578 $state->offsets['append'] = new Address('base', null);
579 $path = new Path($context);
580 $result = $path->locate($body, $state, new BoundInstruction('write', 'write', $body->source, 'result', ['append', 'value']));
581 self::assertInstanceOf(Location::class, $result);
582 $state->memory->write($result, Term::constant('new'));
583 $read = $path->read($state, new BoundInstruction('read', 'read', $body->source, 'read', ['append']));
584 self::assertInstanceOf(Term::class, $read);
585 self::assertSame('new', $read->native());
586 self::assertSame([5], $result->path);
587 self::assertSame('base', $state->offsets['append']->parent);
588 self::assertSame(5, $state->offsets['append']->key?->native());
589 self::assertSame([4 => 'existing', 5 => 'new'], $state->memory->read($base)->native());
590 }
591 public function testLocatePreservesIntermediateArraysWhenTheLastKeyFails(): void
592 {
593 $source = '<?php function target(){return 1;}';
594 $budget = new Budget();
595 $configuration = new DeriverConfig();
596 $contributions = new Contributions();
597 $input = new Sources([new Source('fixture.php', $source)]);
598 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
599 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
600 $contributions1 = $providers->contributions;
601 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
602 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
603 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
604 $snapshotId = $identity->id;
605 $input1 = $providers->input;
606 $parser = new Parser(new ParserConfig($target->version));
607 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
608 $program1 = new Index($snapshotId, $trees);
609 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
610 $snapshotId1 = 'test';
611 $input2 = new Sources([new Source('fixture.php', $source)]);
612 $profile = $execution->configuration->target;
613 $parser1 = new Parser(new ParserConfig($profile->version));
614 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
615 $program = new Index($snapshotId1, $trees1);
616 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
617 $body = $context->program->callable('target');
618 self::assertNotNull($body);
619 $state = new State();
620 $base = $state->memory->allocate(Term::constant(null));
621 $state->addresses['base'] = $base;
622 $state->offsets['first'] = new Address('base', Term::constant('created'));
623 $state->offsets['second'] = new Address('first', Term::array([]));
624 $result = (new Path($context))->locate($body, $state, new BoundInstruction('write', 'write', $body->source, 'result', ['second', 'value']));
625 self::assertInstanceOf(Term::class, $result);
626 self::assertSame('TypeError', $result->literal);
627 self::assertSame(['created' => []], $state->memory->read($base)->native());
628 self::assertSame(['created'], $state->addresses['first']->path);
629 self::assertArrayNotHasKey('second', $state->addresses);
630 }
631 public function testLocateUnsetDoesNotCreateMissingIntermediateArrays(): void
632 {
633 $source = '<?php function target(){return 1;}';
634 $budget = new Budget();
635 $configuration = new DeriverConfig();
636 $contributions = new Contributions();
637 $input = new Sources([new Source('fixture.php', $source)]);
638 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
639 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
640 $contributions1 = $providers->contributions;
641 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
642 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
643 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
644 $snapshotId = $identity->id;
645 $input1 = $providers->input;
646 $parser = new Parser(new ParserConfig($target->version));
647 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
648 $program1 = new Index($snapshotId, $trees);
649 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
650 $snapshotId1 = 'test';
651 $input2 = new Sources([new Source('fixture.php', $source)]);
652 $profile = $execution->configuration->target;
653 $parser1 = new Parser(new ParserConfig($profile->version));
654 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
655 $program = new Index($snapshotId1, $trees1);
656 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
657 $body = $context->program->callable('target');
658 self::assertNotNull($body);
659 $state = new State();
660 $base = $state->memory->allocate(Term::array([]));
661 $state->addresses['base'] = $base;
662 $state->offsets['first'] = new Address('base', Term::constant('absent'));
663 $state->offsets['second'] = new Address('first', Term::constant('nested'));
664 $result = (new Path($context))->locate($body, $state, new BoundInstruction('unset', 'unset', $body->source, 'result', ['second']));
665 self::assertInstanceOf(Term::class, $result);
666 self::assertNull($result->native());
667 self::assertSame([], $state->memory->read($base)->native());
668 self::assertSame([], $context->frontiers);
669 self::assertArrayNotHasKey('second', $state->addresses);
670 }
671 public function testLocateReturnsStringStorageWithoutCoercingTheOriginalKey(): void
672 {
673 $source = '<?php function target(){return 1;}';
674 $budget = new Budget();
675 $configuration = new DeriverConfig();
676 $contributions = new Contributions();
677 $input = new Sources([new Source('fixture.php', $source)]);
678 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
679 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
680 $contributions1 = $providers->contributions;
681 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
682 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
683 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
684 $snapshotId = $identity->id;
685 $input1 = $providers->input;
686 $parser = new Parser(new ParserConfig($target->version));
687 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
688 $program1 = new Index($snapshotId, $trees);
689 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
690 $snapshotId1 = 'test';
691 $input2 = new Sources([new Source('fixture.php', $source)]);
692 $profile = $execution->configuration->target;
693 $parser1 = new Parser(new ParserConfig($profile->version));
694 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
695 $program = new Index($snapshotId1, $trees1);
696 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
697 $body = $context->program->callable('target');
698 self::assertNotNull($body);
699 $state = new State();
700 $base = $state->memory->allocate(Term::array(['text' => Term::constant('abc')]));
701 $key = Term::constant('-1');
702 $state->addresses['base'] = $base;
703 $state->offsets['first'] = new Address('base', Term::constant('text'));
704 $state->offsets['second'] = new Address('first', $key);
705 $result = (new Path($context))->locate($body, $state, new BoundInstruction('write', 'write', $body->source, 'result', ['second', 'value']));
706 self::assertInstanceOf(StringAccess::class, $result);
707 self::assertSame($base->root, $result->container->root);
708 self::assertSame(['text'], $result->container->path);
709 self::assertSame($key, $result->key);
710 self::assertSame(['text' => 'abc'], $state->memory->read($base)->native());
711 }
712 /**
713 * @param Term|null $key Evaluated offset or append
714 * @param string $kind Expected outcome category
715 * @param string $literal Expected error or boundary
716 */
717 #[DataProvider('providerNestedStringKeys')]
718 public function testLocateRejectsNestedStringMutationWithoutChangingStorage(?Term $key, string $kind, string $literal): void
719 {
720 $source = '<?php function target(){return 1;}';
721 $budget = new Budget();
722 $configuration = new DeriverConfig();
723 $contributions = new Contributions();
724 $input = new Sources([new Source('fixture.php', $source)]);
725 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
726 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
727 $contributions1 = $providers->contributions;
728 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
729 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
730 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
731 $snapshotId = $identity->id;
732 $input1 = $providers->input;
733 $parser = new Parser(new ParserConfig($target->version));
734 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
735 $program1 = new Index($snapshotId, $trees);
736 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
737 $snapshotId1 = 'test';
738 $input2 = new Sources([new Source('fixture.php', $source)]);
739 $profile = $execution->configuration->target;
740 $parser1 = new Parser(new ParserConfig($profile->version));
741 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
742 $program = new Index($snapshotId1, $trees1);
743 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
744 $body = $context->program->callable('target');
745 self::assertNotNull($body);
746 $state = new State();
747 $base = $state->memory->allocate(Term::constant('abc'));
748 $state->addresses['base'] = $base;
749 $state->offsets['first'] = new Address('base', $key);
750 $state->offsets['second'] = new Address('first', Term::constant(0));
751 $result = (new Path($context))->locate($body, $state, new BoundInstruction('write', 'write', $body->source, 'result', ['second', 'value']));
752 self::assertInstanceOf(Term::class, $result);
753 self::assertSame($kind, $result->kind->value);
754 self::assertSame($literal, $result->literal);
755 self::assertSame('abc', $state->memory->read($base)->native());
756 self::assertArrayNotHasKey('first', $state->addresses);
757 }
758 /**
759 * @return iterable<string, array{Term|null, string, string}>
760 */
761 public static function providerNestedStringKeys(): iterable
762 {
763 yield 'valid byte' => [Term::constant(0), 'throwable', 'Error'];
764 yield 'invalid byte key' => [Term::constant('bad'), 'throwable', 'TypeError'];
765 yield 'append' => [null, 'throwable', 'Error'];
766 yield 'unknown key' => [Term::parameter('key'), 'opaque', 'OFFSET_OPERATION'];
767 }
768 public function testLocatePreservesOverloadedReceiversAndUnevaluatedSuffixes(): void
769 {
770 $source = '<?php function target(){return 1;}';
771 $budget = new Budget();
772 $configuration = new DeriverConfig();
773 $contributions = new Contributions();
774 $input = new Sources([new Source('fixture.php', $source)]);
775 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
776 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
777 $contributions1 = $providers->contributions;
778 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
779 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
780 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
781 $snapshotId = $identity->id;
782 $input1 = $providers->input;
783 $parser = new Parser(new ParserConfig($target->version));
784 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
785 $program1 = new Index($snapshotId, $trees);
786 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
787 $snapshotId1 = 'test';
788 $input2 = new Sources([new Source('fixture.php', $source)]);
789 $profile = $execution->configuration->target;
790 $parser1 = new Parser(new ParserConfig($profile->version));
791 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
792 $program = new Index($snapshotId1, $trees1);
793 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
794 $body = $context->program->callable('target');
795 self::assertNotNull($body);
796 $state = new State();
797 $receiver = new Term(TermKind::Object, 'bag', attributes: ['class' => 'UnknownBag']);
798 $key = Term::array(['uncoerced' => Term::constant(1)]);
799 $state->addresses['base'] = $state->memory->allocate($receiver);
800 $state->offsets['first'] = new Address('base', $key);
801 $state->offsets['second'] = new Address('first', Term::constant('remaining'));
802 $result = (new Path($context))->locate($body, $state, new BoundInstruction('write', 'write', $body->source, 'result', ['second', 'value']));
803 self::assertInstanceOf(ProtocolAccess::class, $result);
804 self::assertSame($receiver, $result->receiver);
805 self::assertSame($key, $result->key);
806 self::assertSame(['second'], $result->remaining);
807 self::assertArrayNotHasKey('first', $state->addresses);
808 }
809 public function testLocateRetainsUnknownKeyDependencies(): void
810 {
811 $source = '<?php function target(){return 1;}';
812 $budget = new Budget();
813 $configuration = new DeriverConfig();
814 $contributions = new Contributions();
815 $input = new Sources([new Source('fixture.php', $source)]);
816 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
817 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
818 $contributions1 = $providers->contributions;
819 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
820 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
821 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
822 $snapshotId = $identity->id;
823 $input1 = $providers->input;
824 $parser = new Parser(new ParserConfig($target->version));
825 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
826 $program1 = new Index($snapshotId, $trees);
827 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
828 $snapshotId1 = 'test';
829 $input2 = new Sources([new Source('fixture.php', $source)]);
830 $profile = $execution->configuration->target;
831 $parser1 = new Parser(new ParserConfig($profile->version));
832 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
833 $program = new Index($snapshotId1, $trees1);
834 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
835 $body = $context->program->callable('target');
836 self::assertNotNull($body);
837 $state = new State();
838 $container = Term::array([]);
839 $key = new Term(TermKind::Parameter, 'key');
840 $state->addresses['base'] = $state->memory->allocate($container);
841 $state->offsets['offset'] = new Address('base', $key);
842 $result = (new Path($context))->locate($body, $state, new BoundInstruction('write', 'write', $body->source, 'result', ['offset', 'value']));
843 self::assertInstanceOf(Term::class, $result);
844 self::assertSame('OFFSET_OPERATION', $result->literal);
845 self::assertSame([$container, $key], $result->operands);
846 self::assertArrayNotHasKey('offset', $state->addresses);
847 }
848 /**
849 * @param Term $container Previous storage
850 * @param bool $create Whether creation is permitted
851 * @param string $kind Expected category
852 * @param mixed $literal Expected payload
853 * @param bool $created Whether an empty array replaces storage
854 * @param list<string> $reasons Expected diagnostics
855 */
856 #[DataProvider('providerContainers')]
857 public function testPrepareChangesOnlyLegalAutovivifiedContainers(Term $container, bool $create, string $kind, mixed $literal, bool $created, array $reasons): void
858 {
859 $source = '<?php function target(){return 1;}';
860 $budget = new Budget();
861 $configuration = new DeriverConfig();
862 $contributions = new Contributions();
863 $input = new Sources([new Source('fixture.php', $source)]);
864 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
865 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
866 $contributions1 = $providers->contributions;
867 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
868 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
869 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
870 $snapshotId = $identity->id;
871 $input1 = $providers->input;
872 $parser = new Parser(new ParserConfig($target->version));
873 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
874 $program1 = new Index($snapshotId, $trees);
875 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
876 $snapshotId1 = 'test';
877 $input2 = new Sources([new Source('fixture.php', $source)]);
878 $profile = $execution->configuration->target;
879 $parser1 = new Parser(new ParserConfig($profile->version));
880 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
881 $program = new Index($snapshotId1, $trees1);
882 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
883 $body = $context->program->callable('target');
884 self::assertNotNull($body);
885 $state = new State();
886 $base = $state->memory->allocate($container);
887 $result = (new Path($context))->prepare($body, $state, $base, new BoundInstruction('offset', 'write', $body->source), $create);
888 self::assertSame($kind, $result->kind->value);
889 self::assertSame($literal, $result->literal);
890 self::assertEquals($created ? Term::array([]) : $container, $state->memory->read($base));
891 self::assertSame($reasons, array_column($context->frontiers, 'code'));
892 }
893 /**
894 * @return iterable<string, array{Term, bool, string, mixed, bool, list<string>}>
895 */
896 public static function providerContainers(): iterable
897 {
898 yield 'array' => [Term::array([]), true, 'array', null, false, []];
899 yield 'string' => [Term::constant('abc'), true, 'constant', 'abc', false, []];
900 yield 'null created' => [Term::constant(null), true, 'array', null, true, []];
901 yield 'false created' => [Term::constant(false), true, 'array', null, true, ['PHP_WARNING']];
902 yield 'uninitialized created' => [new Term(TermKind::Uninitialized), true, 'array', null, true, []];
903 yield 'null unset' => [Term::constant(null), false, 'constant', null, false, []];
904 yield 'false unset' => [Term::constant(false), false, 'constant', null, false, ['PHP_WARNING']];
905 yield 'uninitialized unset' => [new Term(TermKind::Uninitialized), false, 'constant', null, false, []];
906 yield 'true' => [Term::constant(true), true, 'throwable', 'Error', false, []];
907 yield 'integer zero' => [Term::constant(0), true, 'throwable', 'Error', false, []];
908 yield 'float' => [Term::constant(1.5), true, 'throwable', 'Error', false, []];
909 yield 'plain object' => [new Term(TermKind::Object, 'one', attributes: ['class' => 'stdClass']), true, 'throwable', 'Error', false, []];
910 yield 'closure' => [new Term(TermKind::Closure, 'one'), true, 'throwable', 'Error', false, []];
911 yield 'enum' => [new Term(TermKind::Enum, 'A'), true, 'throwable', 'Error', false, []];
912 yield 'possible ArrayAccess' => [new Term(TermKind::Object, 'bag'), true, 'object', 'bag', false, []];
913 yield 'symbolic' => [Term::parameter('input'), true, 'opaque', 'OFFSET_OPERATION', false, []];
914 }
915 public function testCreateAppliesTheTypesOfPropertiesSharingAnEscapedReference(): void
916 {
917 $source = '<?php function target(){return 1;}';
918 $budget = new Budget();
919 $configuration = new DeriverConfig();
920 $contributions = new Contributions();
921 $input = new Sources([new Source('fixture.php', $source)]);
922 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
923 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
924 $contributions1 = $providers->contributions;
925 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
926 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
927 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
928 $snapshotId = $identity->id;
929 $input1 = $providers->input;
930 $parser = new Parser(new ParserConfig($target->version));
931 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
932 $program1 = new Index($snapshotId, $trees);
933 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
934 $snapshotId1 = 'test';
935 $input2 = new Sources([new Source('fixture.php', $source)]);
936 $profile = $execution->configuration->target;
937 $parser1 = new Parser(new ParserConfig($profile->version));
938 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
939 $program = new Index($snapshotId1, $trees1);
940 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
941 $body = $context->program->callable('target');
942 self::assertNotNull($body);
943 $state = new State();
944 $shared = $state->memory->allocate(Term::constant(null));
945 $state->memory->cells['object'] = Term::array(['field' => new Term(TermKind::Cell, $shared->root)]);
946 $state->memory->propertyTypes['object']['field'] = 'int|null';
947 $result = (new Path($context))->create($body, $state, $shared, Term::constant(null), new BoundInstruction('offset', 'write', $body->source));
948 self::assertSame(TermKind::Throwable, $result->kind);
949 self::assertSame('TypeError', $result->literal);
950 self::assertNull($state->memory->read($shared)->native());
951 self::assertNull($state->memory->read(new Location('object', ['field']))->native());
952 }
953 public function testCreateAllowsNullableArrayProperties(): void
954 {
955 $source = '<?php function target(){return 1;}';
956 $budget = new Budget();
957 $configuration = new DeriverConfig();
958 $contributions = new Contributions();
959 $input = new Sources([new Source('fixture.php', $source)]);
960 $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
961 $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
962 $contributions1 = $providers->contributions;
963 $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
964 $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
965 $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
966 $snapshotId = $identity->id;
967 $input1 = $providers->input;
968 $parser = new Parser(new ParserConfig($target->version));
969 $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
970 $program1 = new Index($snapshotId, $trees);
971 $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
972 $snapshotId1 = 'test';
973 $input2 = new Sources([new Source('fixture.php', $source)]);
974 $profile = $execution->configuration->target;
975 $parser1 = new Parser(new ParserConfig($profile->version));
976 $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
977 $program = new Index($snapshotId1, $trees1);
978 $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
979 $body = $context->program->callable('target');
980 self::assertNotNull($body);
981 $state = new State();
982 $state->memory->cells['object'] = Term::array(['field' => Term::constant(null)]);
983 $state->memory->propertyTypes['object']['field'] = 'array|null';
984 $address = new Location('object', ['field']);
985 $result = (new Path($context))->create($body, $state, $address, Term::constant(null), new BoundInstruction('offset', 'write', $body->source));
986 self::assertSame([], $result->native());
987 self::assertSame([], $state->memory->read($address)->native());
988 self::assertSame(['object' => ['field' => 'array|null']], $state->memory->propertyTypes);
989 }
990}
991