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