packages/deriver/tests/Unit/Deriver/Evaluator/Execution/ObservationCollectorTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Execution;
6
7use Deriver\Deriver;
8use Deriver\Deriver\Candidate\ResultRef;
9use Deriver\Deriver\Candidate\Snapshot;
10use Deriver\Deriver\Candidate\Statistics;
11use Deriver\Deriver\Contributions;
12use Deriver\Deriver\DeriverConfig;
13use Deriver\Deriver\Evaluator\Builtin\Library;
14use Deriver\Deriver\Evaluator\Call\BoundInstruction;
15use Deriver\Deriver\Evaluator\Call\Evaluators;
16use Deriver\Deriver\Evaluator\Call\Extensions;
17use Deriver\Deriver\Evaluator\Call\Registrations;
18use Deriver\Deriver\Evaluator\Call\State\StateSlot;
19use Deriver\Deriver\Evaluator\Call\StateRegistry;
20use Deriver\Deriver\Evaluator\Domain\AbstractDomain;
21use Deriver\Deriver\Evaluator\Domain\DomainFact;
22use Deriver\Deriver\Evaluator\Domain\DomainLaws;
23use Deriver\Deriver\Evaluator\Domain\DomainOperations;
24use Deriver\Deriver\Evaluator\EvaluatorConfig;
25use Deriver\Deriver\Evaluator\Evidence\Frontier;
26use Deriver\Deriver\Evaluator\Execution\Call\ArgumentBinding;
27use Deriver\Deriver\Evaluator\Execution\Call\ArgumentOrder;
28use Deriver\Deriver\Evaluator\Execution\Call\CallableCheck;
29use Deriver\Deriver\Evaluator\Execution\Call\CallResolution;
30use Deriver\Deriver\Evaluator\Execution\Call\PassedArgument;
31use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Arguments;
32use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Modes;
33use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Resolution;
34use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Target;
35use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Transfer;
36use Deriver\Deriver\Evaluator\Execution\Completion\Alternative;
37use Deriver\Deriver\Evaluator\Execution\Completion\Assessment;
38use Deriver\Deriver\Evaluator\Execution\Completion\Completion;
39use Deriver\Deriver\Evaluator\Execution\Completion\CompletionKind;
40use Deriver\Deriver\Evaluator\Execution\Completion\Exceptional;
41use Deriver\Deriver\Evaluator\Execution\Completion\StorageSnapshot;
42use Deriver\Deriver\Evaluator\Execution\Context;
43use Deriver\Deriver\Evaluator\Execution\Control\ExceptionMatch;
44use Deriver\Deriver\Evaluator\Execution\Control\ObservationLimit;
45use Deriver\Deriver\Evaluator\Execution\Control\StateJoin;
46use Deriver\Deriver\Evaluator\Execution\Control\Unwinding;
47use Deriver\Deriver\Evaluator\Execution\Demand\Discovery;
48use Deriver\Deriver\Evaluator\Execution\Dependencies;
49use Deriver\Deriver\Evaluator\Execution\Evaluator;
50use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
51use Deriver\Deriver\Evaluator\Execution\InstructionTransfer;
52use Deriver\Deriver\Evaluator\Execution\Memory\Location;
53use Deriver\Deriver\Evaluator\Execution\Memory\Materialization;
54use Deriver\Deriver\Evaluator\Execution\Memory\Memory;
55use Deriver\Deriver\Evaluator\Execution\Memory\ReferenceConstraint;
56use Deriver\Deriver\Evaluator\Execution\Memory\Slots\SlotReference;
57use Deriver\Deriver\Evaluator\Execution\Memory\Slots\StateStorage;
58use Deriver\Deriver\Evaluator\Execution\Memory\StorageCapture;
59use Deriver\Deriver\Evaluator\Execution\ObservationCollector;
60use Deriver\Deriver\Evaluator\Execution\Operation\Conversions;
61use Deriver\Deriver\Evaluator\Execution\Operation\ScalarErrors;
62use Deriver\Deriver\Evaluator\Execution\State;
63use Deriver\Deriver\Evaluator\Execution\Summary\Evaluation;
64use Deriver\Deriver\Evaluator\Execution\Trace\Derivation;
65use Deriver\Deriver\Evaluator\Execution\Transfer\MemoryStep;
66use Deriver\Deriver\Evaluator\Execution\Transfer\PureStep;
67use Deriver\Deriver\Evaluator\Execution\Transfer\ReferenceAssignment;
68use Deriver\Deriver\Evaluator\Projection;
69use Deriver\Deriver\Evaluator\Request\Budget;
70use Deriver\Deriver\Evaluator\Request\EntryPoint;
71use Deriver\Deriver\Evaluator\Request\Observation\Observation;
72use Deriver\Deriver\Evaluator\Request\Query;
73use Deriver\Deriver\Evaluator\Request\QueryScope;
74use Deriver\Deriver\Evaluator\Request\ReturnQuery;
75use Deriver\Deriver\Evaluator\Request\StateQuery;
76use Deriver\Deriver\Evaluator\Request\TupleQuery;
77use Deriver\Deriver\Evaluator\Request\ValueQuery;
78use Deriver\Deriver\Evaluator\Scalar\Arithmetic;
79use Deriver\Deriver\Evaluator\Scalar\Identity;
80use Deriver\Deriver\Evaluator\Scalar\Increment;
81use Deriver\Deriver\Evaluator\Scalar\Operations;
82use Deriver\Deriver\Evaluator\TargetProfile;
83use Deriver\Deriver\Evaluator\Term;
84use Deriver\Deriver\Evaluator\TermKind;
85use Deriver\Deriver\Execution\DerivationResult;
86use Deriver\Deriver\Execution\QueryExecution;
87use Deriver\Deriver\Execution\ResultAssessment;
88use Deriver\Deriver\Identity\QueryEncoding;
89use Deriver\Deriver\Identity\ValueGraph;
90use Deriver\Deriver\Lowerer\AssignmentLowering;
91use Deriver\Deriver\Lowerer\CallLowering;
92use Deriver\Deriver\Lowerer\Control\DestructuringLowering;
93use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
94use Deriver\Deriver\Lowerer\Declaration\CallableSource;
95use Deriver\Deriver\Lowerer\Declaration\CallSiteIndex;
96use Deriver\Deriver\Lowerer\Declaration\Index;
97use Deriver\Deriver\Lowerer\Declaration\Indexer;
98use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
99use Deriver\Deriver\Lowerer\EffectInspection;
100use Deriver\Deriver\Lowerer\ExpressionLowering;
101use Deriver\Deriver\Lowerer\Graph\Argument;
102use Deriver\Deriver\Lowerer\Graph\BasicBlock;
103use Deriver\Deriver\Lowerer\Graph\CallableGraph;
104use Deriver\Deriver\Lowerer\Graph\ExpressionRef;
105use Deriver\Deriver\Lowerer\Graph\Instruction;
106use Deriver\Deriver\Lowerer\Graph\ObservationPhase;
107use Deriver\Deriver\Lowerer\Graph\PointRef;
108use Deriver\Deriver\Lowerer\Graph\Terminator;
109use Deriver\Deriver\Lowerer\Graph\TerminatorKind;
110use Deriver\Deriver\Lowerer\Hierarchy\Builtins;
111use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
112use Deriver\Deriver\Lowerer\InstructionEmitter;
113use Deriver\Deriver\Lowerer\Lowerer;
114use Deriver\Deriver\Lowerer\LoweringScope;
115use Deriver\Deriver\Lowerer\StatementLowering;
116use Deriver\Deriver\MicroEvaluatorConversion;
117use Deriver\Deriver\Parser\Node\File;
118use Deriver\Deriver\Parser\Parser;
119use Deriver\Deriver\Parser\ParserConfig;
120use Deriver\Deriver\Parser\TargetSyntax;
121use Deriver\Deriver\Providers\ProviderExpansion;
122use Deriver\Deriver\Resolver\AssignmentPatterns;
123use Deriver\Deriver\Resolver\ClassScope;
124use Deriver\Deriver\Resolver\ExpressionValidation;
125use Deriver\Deriver\Resolver\MagicContext;
126use Deriver\Deriver\Resolver\Resolver;
127use Deriver\Deriver\Selection\CallObservations;
128use Deriver\Deriver\Selection\QueryValidation;
129use Deriver\Deriver\SnapshotIdentity;
130use Deriver\Deriver\Sources\LineMap;
131use Deriver\Deriver\Sources\Path;
132use Deriver\Deriver\Sources\Source;
133use Deriver\Deriver\Sources\SourceRef;
134use Deriver\Deriver\Sources\Sources;
135use Deriver\Exception\ParserException;
136use Deriver\Exception\ResolutionException;
137use Override;
138use PHPUnit\Framework\Attributes\CoversClass;
139use PHPUnit\Framework\Attributes\DataProvider;
140use PHPUnit\Framework\Attributes\Small;
141use PHPUnit\Framework\Attributes\UsesClass;
142use PHPUnit\Framework\TestCase;
143
144#[UsesClass(Resolver::class)]
145#[UsesClass(ExpressionValidation::class)]
146#[UsesClass(ResolutionException::class)]
147#[UsesClass(ParserConfig::class)]
148#[UsesClass(ParserException::class)]
149#[CoversClass(ObservationCollector::class)]
150#[UsesClass(CallObservations::class)]
151#[UsesClass(QueryExecution::class)]
152#[UsesClass(QueryValidation::class)]
153#[UsesClass(ResultAssessment::class)]
154#[UsesClass(Deriver::class)]
155#[UsesClass(Argument::class)]
156#[UsesClass(BasicBlock::class)]
157#[UsesClass(CallableGraph::class)]
158#[UsesClass(CallableIdentity::class)]
159#[UsesClass(Instruction::class)]
160#[UsesClass(Terminator::class)]
161#[UsesClass(ArgumentBinding::class)]
162#[UsesClass(ArgumentOrder::class)]
163#[UsesClass(CallResolution::class)]
164#[UsesClass(CallableCheck::class)]
165#[UsesClass(Builtins::class)]
166#[UsesClass(Dispatch::class)]
167#[UsesClass(PassedArgument::class)]
168#[UsesClass(Arguments::class)]
169#[UsesClass(Modes::class)]
170#[UsesClass(Resolution::class)]
171#[UsesClass(Target::class)]
172#[UsesClass(Transfer::class)]
173#[UsesClass(Completion::class)]
174#[UsesClass(Context::class)]
175#[UsesClass(ExceptionMatch::class)]
176#[UsesClass(ObservationLimit::class)]
177#[UsesClass(StateJoin::class)]
178#[UsesClass(Unwinding::class)]
179#[UsesClass(Discovery::class)]
180#[UsesClass(Dependencies::class)]
181#[UsesClass(InstructionTransfer::class)]
182#[UsesClass(Evaluator::class)]
183#[UsesClass(SlotReference::class)]
184#[UsesClass(StateStorage::class)]
185#[UsesClass(Conversions::class)]
186#[UsesClass(ScalarErrors::class)]
187#[UsesClass(State::class)]
188#[UsesClass(Evaluation::class)]
189#[UsesClass(MemoryStep::class)]
190#[UsesClass(PureStep::class)]
191#[UsesClass(ReferenceAssignment::class)]
192#[UsesClass(Location::class)]
193#[UsesClass(Materialization::class)]
194#[UsesClass(Memory::class)]
195#[UsesClass(ReferenceConstraint::class)]
196#[UsesClass(StorageCapture::class)]
197#[UsesClass(Library::class)]
198#[UsesClass(DomainLaws::class)]
199#[UsesClass(DomainFact::class)]
200#[UsesClass(DomainOperations::class)]
201#[UsesClass(Extensions::class)]
202#[UsesClass(ProviderExpansion::class)]
203#[UsesClass(Evaluators::class)]
204#[UsesClass(StateRegistry::class)]
205#[UsesClass(StateSlot::class)]
206#[UsesClass(DeriverConfig::class)]
207#[UsesClass(EntryPoint::class)]
208#[UsesClass(Sources::class)]
209#[UsesClass(Path::class)]
210#[UsesClass(Snapshot::class)]
211#[UsesClass(Source::class)]
212#[UsesClass(TargetProfile::class)]
213#[UsesClass(Budget::class)]
214#[UsesClass(QueryScope::class)]
215#[UsesClass(ReturnQuery::class)]
216#[UsesClass(StateQuery::class)]
217#[UsesClass(TupleQuery::class)]
218#[UsesClass(ValueQuery::class)]
219#[UsesClass(ExpressionRef::class)]
220#[UsesClass(Observation::class)]
221#[UsesClass(PointRef::class)]
222#[UsesClass(ResultRef::class)]
223#[UsesClass(SourceRef::class)]
224#[UsesClass(Alternative::class)]
225#[UsesClass(Assessment::class)]
226#[UsesClass(Derivation::class)]
227#[UsesClass(DerivationResult::class)]
228#[UsesClass(Exceptional::class)]
229#[UsesClass(QueryEncoding::class)]
230#[UsesClass(ValueGraph::class)]
231#[UsesClass(Statistics::class)]
232#[UsesClass(StorageSnapshot::class)]
233#[UsesClass(Parser::class)]
234#[UsesClass(File::class)]
235#[UsesClass(AssignmentLowering::class)]
236#[UsesClass(CallLowering::class)]
237#[UsesClass(Lowerer::class)]
238#[UsesClass(DestructuringLowering::class)]
239#[UsesClass(EffectInspection::class)]
240#[UsesClass(ExpressionLowering::class)]
241#[UsesClass(InstructionEmitter::class)]
242#[UsesClass(LoweringScope::class)]
243#[UsesClass(StatementLowering::class)]
244#[UsesClass(CallSiteIndex::class)]
245#[UsesClass(CallableSource::class)]
246#[UsesClass(Indexer::class)]
247#[UsesClass(Index::class)]
248#[UsesClass(Composition::class)]
249#[UsesClass(LineMap::class)]
250#[UsesClass(MagicContext::class)]
251#[UsesClass(AssignmentPatterns::class)]
252#[UsesClass(ClassScope::class)]
253#[UsesClass(TargetSyntax::class)]
254#[UsesClass(Arithmetic::class)]
255#[UsesClass(Identity::class)]
256#[UsesClass(Increment::class)]
257#[UsesClass(Operations::class)]
258#[UsesClass(Projection::class)]
259#[UsesClass(Term::class)]
260#[Small]
261final class ObservationCollectorTest extends TestCase
262{
263    public function testInstructionPreservesTheSemanticContract(): void
264    {
265        $input = new Sources([new Source('fixture.php', '<?php $x=1; sink($x++, $x);')]);
266        $configuration = new DeriverConfig();
267        $contributions = new Contributions();
268        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
269        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
270        $contributions1 = $providers->contributions;
271        $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);
272        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
273        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
274        $snapshotId = $identity->id;
275        $input1 = $providers->input;
276        $parser = new Parser(new ParserConfig($target->version));
277        $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input1, false));
278        $program = new Index($snapshotId, $trees);
279        $session = new QueryExecution($program, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program->diagnostics())));
280        $observation = (new CallObservations($session->program, $session->evaluators))->find('sink')[0];
281        $result = $session->derive(new TupleQuery($observation->beforeInvocation(), ['before' => $observation->argument(0), 'after' => $observation->argument(1)]));
282        self::assertSame(1, $result->normalOutcomes[0]->values['before']->native());
283        self::assertSame(2, $result->normalOutcomes[0]->values['after']->native());
284        self::assertSame([], $result->frontiers);
285    }
286    public function testCompletionKeepsTheExceptionalStateAtTheThrow(): void
287    {
288        $source = '<?php function target(){return 1;}';
289        $budget = new Budget();
290        $configuration = new DeriverConfig();
291        $contributions = new Contributions();
292        $input = new Sources([new Source('fixture.php', $source)]);
293        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
294        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
295        $contributions1 = $providers->contributions;
296        $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);
297        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
298        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
299        $snapshotId = $identity->id;
300        $input1 = $providers->input;
301        $parser = new Parser(new ParserConfig($target->version));
302        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
303        $program1 = new Index($snapshotId, $trees);
304        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
305        $snapshotId1 = 'test';
306        $input2 = new Sources([new Source('fixture.php', $source)]);
307        $profile = $execution->configuration->target;
308        $parser1 = new Parser(new ParserConfig($profile->version));
309        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
310        $program = new Index($snapshotId1, $trees1);
311        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
312        $body = $context->program->callable('target');
313        self::assertNotNull($body);
314        $state = new State();
315        $state->memory->write($state->local('x'), Term::constant(2));
316        $state->completion = new Completion(CompletionKind::Throw, new Term(TermKind::Throwable, 'Error'));
317        (new ObservationCollector($context))->completion($body, $state);
318        self::assertSame([], $context->normal);
319        self::assertSame(2, $context->exceptional[0]->state['x']->native());
320    }
321    public function testProjectFollowsObjectIdentityWithoutDroppingSiblingFields(): void
322    {
323        $state = new State();
324        $state->memory->cells['object:one'] = Term::fromNative(['x' => ['nested' => 3], 'sibling' => 7]);
325        $source = '<?php function target(){return 1;}';
326        $budget = new Budget();
327        $configuration = new DeriverConfig();
328        $contributions = new Contributions();
329        $input = new Sources([new Source('fixture.php', $source)]);
330        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
331        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
332        $contributions1 = $providers->contributions;
333        $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);
334        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
335        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
336        $snapshotId = $identity->id;
337        $input1 = $providers->input;
338        $parser = new Parser(new ParserConfig($target->version));
339        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
340        $program1 = new Index($snapshotId, $trees);
341        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
342        $snapshotId1 = 'test';
343        $input2 = new Sources([new Source('fixture.php', $source)]);
344        $profile = $execution->configuration->target;
345        $parser1 = new Parser(new ParserConfig($profile->version));
346        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
347        $program = new Index($snapshotId1, $trees1);
348        $value = (new ObservationCollector(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators)))->project(new Term(TermKind::Object, 'one'), ['x', 'nested'], $state);
349        self::assertSame(3, $value->native());
350        self::assertSame(7, $state->memory->cells['object:one']->operands['sibling']->native());
351    }
352    /**
353     * @param Term $input Aggregate containing the selected field
354     * @param list<int|string> $path Requested field path
355     */
356    #[DataProvider('providerProjectedValues')]
357    public function testProjectSelectsTheRequestedValue(Term $input, array $path): void
358    {
359        $source = '<?php function target(){return 1;}';
360        $budget = new Budget();
361        $configuration = new DeriverConfig();
362        $contributions = new Contributions();
363        $input1 = new Sources([new Source('fixture.php', $source)]);
364        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
365        $providers = new ProviderExpansion($input1, array_map(Term::fromNative(...), $configuration->environment), $contributions);
366        $contributions1 = $providers->contributions;
367        $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);
368        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
369        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
370        $snapshotId = $identity->id;
371        $input2 = $providers->input;
372        $parser = new Parser(new ParserConfig($target->version));
373        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input2, false));
374        $program1 = new Index($snapshotId, $trees);
375        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
376        $snapshotId1 = 'test';
377        $input3 = new Sources([new Source('fixture.php', $source)]);
378        $profile = $execution->configuration->target;
379        $parser1 = new Parser(new ParserConfig($profile->version));
380        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input3, false));
381        $program = new Index($snapshotId1, $trees1);
382        $value = (new ObservationCollector(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators)))->project($input, $path, new State());
383        self::assertSame('selected', $value->native());
384    }
385    /**
386     * @return iterable<string,array{Term,list<int|string>}>
387     */
388    public static function providerProjectedValues(): iterable
389    {
390        $value = Term::constant('selected');
391        yield 'array field' => [Term::array(['keep' => $value]), ['keep']];
392        yield 'unrelated sibling' => [Term::array(['keep' => $value, 'sibling' => Term::constant('other')]), ['keep']];
393        yield 'nested array' => [Term::array(['outer' => Term::array(['keep' => $value])]), ['outer', 'keep']];
394        yield 'scalar identity' => [$value, []];
395    }
396    public function testProjectFollowsObjectAndReferenceHandles(): void
397    {
398        $state = new State();
399        $state->memory->cells['object:box'] = Term::array(['value' => Term::constant('selected')]);
400        $state->memory->cells['ref'] = Term::array(['value' => Term::constant('selected')]);
401        $source = '<?php function target(){return 1;}';
402        $budget = new Budget();
403        $configuration = new DeriverConfig();
404        $contributions = new Contributions();
405        $input = new Sources([new Source('fixture.php', $source)]);
406        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
407        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
408        $contributions1 = $providers->contributions;
409        $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);
410        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
411        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
412        $snapshotId = $identity->id;
413        $input1 = $providers->input;
414        $parser = new Parser(new ParserConfig($target->version));
415        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
416        $program1 = new Index($snapshotId, $trees);
417        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
418        $snapshotId1 = 'test';
419        $input2 = new Sources([new Source('fixture.php', $source)]);
420        $profile = $execution->configuration->target;
421        $parser1 = new Parser(new ParserConfig($profile->version));
422        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
423        $program = new Index($snapshotId1, $trees1);
424        $collector = new ObservationCollector(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators));
425        $object = $collector->project(new Term(TermKind::Object, 'box'), ['value'], $state);
426        $reference = $collector->project(new Term(TermKind::Cell, 'ref'), ['value'], $state);
427        self::assertSame('selected', $object->native());
428        self::assertSame('selected', $reference->native());
429    }
430    public function testProjectReadsAbstractSlots(): void
431    {
432        $config = new Contributions(stateSlots: [new StateSlot('example.cache', 'array')]);
433        $state = new State();
434        $state->memory->cells['evaluator:box'] = Term::array(['example.cache' => Term::fromNative(['keep' => 'selected'])]);
435        $source = '<?php function target(){return 1;}';
436        $budget = new Budget();
437        $configuration = new DeriverConfig();
438        $input = new Sources([new Source('fixture.php', $source)]);
439        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
440        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $config);
441        $contributions = $providers->contributions;
442        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions->evaluators, $contributions->providers, $contributions->domains, $contributions->intrinsics, $contributions->stateSlots), $configuration->standardEvaluators);
443        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
444        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
445        $snapshotId = $identity->id;
446        $input1 = $providers->input;
447        $parser = new Parser(new ParserConfig($target->version));
448        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
449        $program1 = new Index($snapshotId, $trees);
450        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
451        $snapshotId1 = 'test';
452        $input2 = new Sources([new Source('fixture.php', $source)]);
453        $profile = $execution->configuration->target;
454        $parser1 = new Parser(new ParserConfig($profile->version));
455        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
456        $program = new Index($snapshotId1, $trees1);
457        $value = (new ObservationCollector(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators)))->project(new Term(TermKind::Object, 'box'), ['keep'], $state, 'example.cache');
458        self::assertSame('selected', $value->native());
459    }
460    public function testProjectRetainsUnknownDomainAndMissingFieldDependencies(): void
461    {
462        $source = '<?php function target(){return 1;}';
463        $budget = new Budget();
464        $configuration = new DeriverConfig();
465        $contributions = new Contributions();
466        $input = new Sources([new Source('fixture.php', $source)]);
467        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
468        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
469        $contributions1 = $providers->contributions;
470        $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);
471        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
472        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
473        $snapshotId = $identity->id;
474        $input1 = $providers->input;
475        $parser = new Parser(new ParserConfig($target->version));
476        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
477        $program1 = new Index($snapshotId, $trees);
478        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
479        $snapshotId1 = 'test';
480        $input2 = new Sources([new Source('fixture.php', $source)]);
481        $profile = $execution->configuration->target;
482        $parser1 = new Parser(new ParserConfig($profile->version));
483        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
484        $program = new Index($snapshotId1, $trees1);
485        $collector = new ObservationCollector(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators));
486        $domain = new Term(TermKind::Domain, 'missing.domain');
487        $unknown = $collector->project($domain, ['field'], new State());
488        $array = Term::array(['public' => Term::constant(1)]);
489        $absent = $collector->project($array, ['absent'], new State());
490        self::assertSame(TermKind::Projection, $unknown->kind);
491        self::assertSame([$domain], $unknown->operands);
492        self::assertSame(TermKind::ArrayRead, $absent->kind);
493        self::assertSame($array, $absent->operands[0]);
494        self::assertSame('absent', $absent->operands[1]->native());
495    }
496    public function testProjectDelegatesRegisteredDomainProjection(): void
497    {
498        $config = new Contributions(domains: [new class () implements AbstractDomain {
499            /**
500             * @return string Domain identity
501             */
502            #[Override]
503            public function id(): string
504            {
505                return 'example.policy';
506            }
507            /**
508             * @return string Semantic version
509             */
510            #[Override]
511            public function version(): string
512            {
513                return '1';
514            }
515            /**
516             * @return DomainFact Empty set
517             */
518            #[Override]
519            public function bottom(): DomainFact
520            {
521                return new DomainFact($this->id(), new Term(TermKind::Bottom));
522            }
523            /**
524             * @return DomainFact All policy records
525             */
526            #[Override]
527            public function top(): DomainFact
528            {
529                return new DomainFact($this->id(), new Term(TermKind::Top));
530            }
531            /**
532             * @param DomainFact $a Contained fact
533             * @param DomainFact $b Containing fact
534             * @return bool Flat-lattice inclusion
535             */
536            #[Override]
537            public function lessOrEqual(DomainFact $a, DomainFact $b): bool
538            {
539                return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
540            }
541            /**
542             * @param DomainFact $a First possibility
543             * @param DomainFact $b Second possibility
544             * @return DomainFact Inclusive alternative
545             */
546            #[Override]
547            public function join(DomainFact $a, DomainFact $b): DomainFact
548            {
549                if ($this->lessOrEqual($a, $b)) {
550                    return $b;
551                }
552                return $this->lessOrEqual($b, $a) ? $a : $this->top();
553            }
554            /**
555             * @param DomainFact $previous Previous approximation
556             * @param DomainFact $next New approximation
557             * @return DomainFact Finite widening
558             */
559            #[Override]
560            public function widen(DomainFact $previous, DomainFact $next): DomainFact
561            {
562                return $this->join($previous, $next);
563            }
564            /**
565             * @param DomainFact $value Policy record
566             * @param Projection $projection Selected fields
567             * @return DomainFact Projected fact
568             */
569            #[Override]
570            public function project(DomainFact $value, Projection $projection): DomainFact
571            {
572                $term = $value->representation;
573                foreach ($projection->path as $key) {
574                    $term = $term->operands[$key] ?? new Term(TermKind::Top);
575                }
576                return new DomainFact($this->id(), $term);
577            }
578        }]);
579        $input = new Term(TermKind::Domain, 'example.policy', ['representation' => Term::fromNative(['keep' => 'selected'])]);
580        $source = '<?php function target(){return 1;}';
581        $budget = new Budget();
582        $configuration = new DeriverConfig();
583        $input1 = new Sources([new Source('fixture.php', $source)]);
584        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
585        $providers = new ProviderExpansion($input1, array_map(Term::fromNative(...), $configuration->environment), $config);
586        $contributions = $providers->contributions;
587        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions->evaluators, $contributions->providers, $contributions->domains, $contributions->intrinsics, $contributions->stateSlots), $configuration->standardEvaluators);
588        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
589        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
590        $snapshotId = $identity->id;
591        $input2 = $providers->input;
592        $parser = new Parser(new ParserConfig($target->version));
593        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input2, false));
594        $program1 = new Index($snapshotId, $trees);
595        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
596        $snapshotId1 = 'test';
597        $input3 = new Sources([new Source('fixture.php', $source)]);
598        $profile = $execution->configuration->target;
599        $parser1 = new Parser(new ParserConfig($profile->version));
600        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input3, false));
601        $program = new Index($snapshotId1, $trees1);
602        $value = (new ObservationCollector(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators)))->project($input, ['keep'], new State());
603        self::assertSame(TermKind::Domain, $value->kind);
604        self::assertSame('example.policy', $value->literal);
605        self::assertSame('selected', $value->operands['representation']->native());
606    }
607    /**
608     * @param Query $query Requested observation
609     * @param string $phase Executed phase
610     */
611    #[DataProvider('providerUnmatchedObservations')]
612    public function testInstructionIgnoresDifferentOwnersRegistersPointsAndPhases(Query $query, string $phase): void
613    {
614        $source1 = '<?php function target(){return 1;}';
615        $budget = new Budget();
616        $configuration = new DeriverConfig();
617        $contributions = new Contributions();
618        $input = new Sources([new Source('fixture.php', $source1)]);
619        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
620        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
621        $contributions1 = $providers->contributions;
622        $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);
623        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
624        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
625        $snapshotId = $identity->id;
626        $input1 = $providers->input;
627        $parser = new Parser(new ParserConfig($target->version));
628        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
629        $program1 = new Index($snapshotId, $trees);
630        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
631        $snapshotId1 = 'test';
632        $input2 = new Sources([new Source('fixture.php', $source1)]);
633        $profile = $execution->configuration->target;
634        $parser1 = new Parser(new ParserConfig($profile->version));
635        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
636        $program = new Index($snapshotId1, $trees1);
637        $fixture = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
638        $context = new Context($fixture->program, $query, $fixture->configuration, $fixture->evaluators);
639        $source = new SourceRef('test', 'fixture.php', 0, 1);
640        $body = new CallableGraph('target', [], [], $source);
641        $state = new State();
642        $state->registers['r1'] = Term::constant(7);
643        (new ObservationCollector($context))->instruction($body, new BoundInstruction('i', 'constant', $source, 'r1'), $state, ObservationPhase::from($phase));
644        self::assertSame([], $context->normal);
645        self::assertFalse($state->observed);
646        self::assertSame(CompletionKind::Normal, $state->completion->kind);
647    }
648    /**
649     * @return iterable<string,array{Query,string}>
650     */
651    public static function providerUnmatchedObservations(): iterable
652    {
653        $source = new SourceRef('test', 'fixture.php', 0, 1);
654        yield 'value before execution' => [new ValueQuery(new ExpressionRef($source, 'target', 'r1')), 'before'];
655        yield 'value at invocation' => [new ValueQuery(new ExpressionRef($source, 'target', 'r1')), 'invocation'];
656        yield 'value different owner' => [new ValueQuery(new ExpressionRef($source, 'other', 'r1')), 'after'];
657        yield 'value different register' => [new ValueQuery(new ExpressionRef($source, 'target', 'r2')), 'after'];
658        yield 'state different owner' => [new StateQuery(new PointRef($source, 'other', 'i', ObservationPhase::Before), 'x'), 'before'];
659        yield 'state different instruction' => [new StateQuery(new PointRef($source, 'target', 'other', ObservationPhase::Before), 'x'), 'before'];
660        yield 'state different phase' => [new StateQuery(new PointRef($source, 'target', 'i', ObservationPhase::Before), 'x'), 'after'];
661        yield 'return is not an instruction observation' => [new ReturnQuery('target'), 'after'];
662    }
663    /**
664     * @param QueryScope $scope Requested invocation scope
665     * @param string $completion \Deriver\Deriver\Evaluator\Execution\Completion\Completion expected after an observation
666     */
667    #[DataProvider('providerObservationScopes')]
668    public function testInstructionCapturesValueStateGuardAndEvidenceInTheRequestedScope(QueryScope $scope, string $completion): void
669    {
670        $source1 = '<?php function target(){return 1;}';
671        $budget = new Budget();
672        $configuration = new DeriverConfig();
673        $contributions = new Contributions();
674        $input = new Sources([new Source('fixture.php', $source1)]);
675        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
676        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
677        $contributions1 = $providers->contributions;
678        $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);
679        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
680        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
681        $snapshotId = $identity->id;
682        $input1 = $providers->input;
683        $parser = new Parser(new ParserConfig($target->version));
684        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
685        $program1 = new Index($snapshotId, $trees);
686        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
687        $snapshotId1 = 'test';
688        $input2 = new Sources([new Source('fixture.php', $source1)]);
689        $profile = $execution->configuration->target;
690        $parser1 = new Parser(new ParserConfig($profile->version));
691        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
692        $program = new Index($snapshotId1, $trees1);
693        $fixture = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
694        $source = new SourceRef('test', 'fixture.php', 0, 1);
695        $query = new ValueQuery(new ExpressionRef($source, 'target', 'r1'), scope: $scope);
696        $context = new Context($fixture->program, $query, $fixture->configuration, $fixture->evaluators);
697        $state = new State();
698        $state->registers['r1'] = Term::constant(7);
699        $state->guard = ['branch' => true];
700        $state->evidence = ['e1'];
701        $state->memory->write($state->local('x'), Term::constant(3));
702        (new ObservationCollector($context))->instruction(new CallableGraph('target', [], [], $source), new BoundInstruction('i', 'constant', $source, 'r1'), $state, ObservationPhase::After);
703        self::assertCount(1, $context->normal);
704        self::assertSame(7, $context->normal[0]->values['value']->native());
705        self::assertSame(3, $context->normal[0]->state['x']->native());
706        self::assertSame(['branch' => true], $context->normal[0]->guard);
707        self::assertSame(['e1'], $context->normal[0]->evidence);
708        self::assertTrue($state->observed);
709        self::assertSame(CompletionKind::from($completion), $state->completion->kind);
710        self::assertArrayHasKey('x', $context->normal[0]->storage->bindings);
711    }
712    /**
713     * @return iterable<string,array{QueryScope,string}>
714     */
715    public static function providerObservationScopes(): iterable
716    {
717        yield 'symbolic slice ends here' => [QueryScope::symbolic(), 'observed'];
718        yield 'entrypoint execution continues' => [QueryScope::fromEntrypoints([new EntryPoint('target')]), 'normal'];
719    }
720    /**
721     * @param Query $query Requested state or tuple
722     * @param array<string,int> $expected Captured fields
723     */
724    #[DataProvider('providerStateAndTupleQueries')]
725    public function testInstructionCapturesStateAndTupleValuesAtTheMatchingPoint(Query $query, array $expected): void
726    {
727        $source1 = '<?php function target(){return 1;}';
728        $budget = new Budget();
729        $configuration = new DeriverConfig();
730        $contributions = new Contributions();
731        $input = new Sources([new Source('fixture.php', $source1)]);
732        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
733        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
734        $contributions1 = $providers->contributions;
735        $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);
736        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
737        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
738        $snapshotId = $identity->id;
739        $input1 = $providers->input;
740        $parser = new Parser(new ParserConfig($target->version));
741        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
742        $program1 = new Index($snapshotId, $trees);
743        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
744        $snapshotId1 = 'test';
745        $input2 = new Sources([new Source('fixture.php', $source1)]);
746        $profile = $execution->configuration->target;
747        $parser1 = new Parser(new ParserConfig($profile->version));
748        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
749        $program = new Index($snapshotId1, $trees1);
750        $fixture = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
751        $context = new Context($fixture->program, $query, $fixture->configuration, $fixture->evaluators);
752        $source = new SourceRef('test', 'fixture.php', 0, 1);
753        $state = new State();
754        $state->memory->write($state->local('x'), Term::fromNative(['field' => 3]));
755        $state->memory->cells['ref'] = Term::constant(9);
756        $state->registers['r1'] = Term::constant(7);
757        $state->registers['r2'] = new Term(TermKind::Cell, 'ref');
758        (new ObservationCollector($context))->instruction(new CallableGraph('target', [], [], $source), new BoundInstruction('i', 'constant', $source, 'r1'), $state, ObservationPhase::Before);
759        self::assertCount(1, $context->normal);
760        self::assertSame($expected, array_map(static fn (Term $value): int|float|string|bool|null => $value->literal, $context->normal[0]->values));
761        self::assertSame(['field' => 3], $context->normal[0]->state['x']->native());
762    }
763    /**
764     * @return iterable<string,array{Query,array<string,int>}>
765     */
766    public static function providerStateAndTupleQueries(): iterable
767    {
768        $source = new SourceRef('test', 'fixture.php', 0, 1);
769        $point = new PointRef($source, 'target', 'i', ObservationPhase::Before);
770        $qualified = new PointRef($source, '\TARGET', 'i', ObservationPhase::Before);
771        yield 'qualified state' => [new StateQuery($qualified, 'x', new Projection(['field'])), ['state' => 3]];
772        yield 'qualified tuple' => [new TupleQuery($qualified, ['first' => new ExpressionRef($source, 'TARGET', 'r1')]), ['first' => 7]];
773        yield 'projected state' => [new StateQuery($point, 'x', new Projection(['field'])), ['state' => 3]];
774        yield 'correlated tuple' => [new TupleQuery($point, ['first' => new ExpressionRef($source, 'target', 'r1'), 'second' => new ExpressionRef($source, 'target', 'r2')]), ['first' => 7, 'second' => 9]];
775    }
776    /**
777     * @param string $completion Return or throw completion
778     * @param bool $observed Whether the requested point was reached
779     * @param string $entry Entry owning pre-observation errors
780     * @param array<string,int> $active Active call counts
781     * @param int $exceptions Number of externally visible exceptions
782     */
783    #[DataProvider('providerPreObservationCompletions')]
784    public function testCompletionExportsOnlyEscapingPreObservationExceptions(string $completion, bool $observed, string $entry, array $active, int $exceptions): void
785    {
786        $source1 = '<?php function target(){return 1;}';
787        $budget = new Budget();
788        $configuration = new DeriverConfig();
789        $contributions = new Contributions();
790        $input = new Sources([new Source('fixture.php', $source1)]);
791        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
792        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
793        $contributions1 = $providers->contributions;
794        $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);
795        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
796        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
797        $snapshotId = $identity->id;
798        $input1 = $providers->input;
799        $parser = new Parser(new ParserConfig($target->version));
800        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
801        $program1 = new Index($snapshotId, $trees);
802        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
803        $snapshotId1 = 'test';
804        $input2 = new Sources([new Source('fixture.php', $source1)]);
805        $profile = $execution->configuration->target;
806        $parser1 = new Parser(new ParserConfig($profile->version));
807        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
808        $program = new Index($snapshotId1, $trees1);
809        $fixture = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
810        $source = new SourceRef('test', 'fixture.php', 0, 1);
811        $query = new ValueQuery(new ExpressionRef($source, 'target', 'r1'));
812        $context = new Context($fixture->program, $query, $fixture->configuration, $fixture->evaluators);
813        $context->entrySymbol = $entry;
814        $context->active = $active;
815        $state = new State();
816        $state->observed = $observed;
817        $state->completion = new Completion(CompletionKind::from($completion), new Term(TermKind::Throwable, 'Error'));
818        (new ObservationCollector($context))->completion(new CallableGraph('target', [], [], $source), $state);
819        self::assertCount($exceptions, $context->exceptional);
820        self::assertSame([], $context->normal);
821    }
822    /**
823     * @return iterable<string,array{string,bool,string,array<string,int>,int}>
824     */
825    public static function providerPreObservationCompletions(): iterable
826    {
827        yield 'entry before point' => ['throw', false, 'target', ['target' => 1], 1];
828        yield 'entry no active count' => ['throw', false, 'target', [], 1];
829        yield 'point already reached' => ['throw', true, 'target', ['target' => 1], 0];
830        yield 'different entry' => ['throw', false, 'other', ['target' => 1], 0];
831        yield 'recursive entry' => ['throw', false, 'target', ['target' => 2], 0];
832        yield 'called within another entry' => ['throw', false, 'target', ['target' => 1, 'other' => 1], 0];
833        yield 'ordinary return' => ['return', false, 'target', ['target' => 1], 0];
834    }
835    /**
836     * @param string $symbol Requested function name
837     * @param QueryScope $scope Invocation scope
838     * @param int $active Recursive invocation count
839     * @param int $outcomes Expected visible returns
840     */
841    #[DataProvider('providerReturnCompletions')]
842    public function testCompletionSelectsReturnSymbolsAndScopeWithoutDuplicatingRecursiveRoots(string $symbol, QueryScope $scope, int $active, int $outcomes): void
843    {
844        $source = '<?php function target(){return 1;}';
845        $budget = new Budget();
846        $configuration = new DeriverConfig();
847        $contributions = new Contributions();
848        $input = new Sources([new Source('fixture.php', $source)]);
849        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
850        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
851        $contributions1 = $providers->contributions;
852        $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);
853        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
854        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
855        $snapshotId = $identity->id;
856        $input1 = $providers->input;
857        $parser = new Parser(new ParserConfig($target->version));
858        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
859        $program1 = new Index($snapshotId, $trees);
860        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
861        $snapshotId1 = 'test';
862        $input2 = new Sources([new Source('fixture.php', $source)]);
863        $profile = $execution->configuration->target;
864        $parser1 = new Parser(new ParserConfig($profile->version));
865        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
866        $program = new Index($snapshotId1, $trees1);
867        $fixture = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
868        $query = new ReturnQuery($symbol, $scope);
869        $context = new Context($fixture->program, $query, $fixture->configuration, $fixture->evaluators);
870        $context->active['target'] = $active;
871        $state = new State();
872        $state->completion = new Completion(CompletionKind::Return, Term::constant(8));
873        (new ObservationCollector($context))->completion(new CallableGraph('TaRgEt', [], [], new SourceRef('test', 'fixture.php', 0, 1)), $state);
874        self::assertCount($outcomes, $context->normal);
875        self::assertSame([], $context->exceptional);
876    }
877    /**
878     * @return iterable<string,array{string,QueryScope,int,int}>
879     */
880    public static function providerReturnCompletions(): iterable
881    {
882        yield 'case insensitive match' => ['TARGET', QueryScope::symbolic(), 1, 1];
883        yield 'other callable' => ['other', QueryScope::symbolic(), 1, 0];
884        yield 'recursive root suppressed' => ['target', QueryScope::symbolic(), 2, 0];
885        yield 'entrypoint nested return' => ['target', QueryScope::fromEntrypoints([new EntryPoint('target')]), 2, 1];
886    }
887    #[DataProvider('providerNamedOwnerVariants')]
888    public function testInstructionNormalizesNamedOwnersBeforeCapturingValues(string $owner): void
889    {
890        $source1 = '<?php function target(){return 1;}';
891        $budget = new Budget();
892        $configuration = new DeriverConfig();
893        $contributions = new Contributions();
894        $input = new Sources([new Source('fixture.php', $source1)]);
895        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
896        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
897        $contributions1 = $providers->contributions;
898        $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);
899        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
900        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
901        $snapshotId = $identity->id;
902        $input1 = $providers->input;
903        $parser = new Parser(new ParserConfig($target->version));
904        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
905        $program1 = new Index($snapshotId, $trees);
906        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
907        $snapshotId1 = 'test';
908        $input2 = new Sources([new Source('fixture.php', $source1)]);
909        $profile = $execution->configuration->target;
910        $parser1 = new Parser(new ParserConfig($profile->version));
911        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
912        $program = new Index($snapshotId1, $trees1);
913        $fixture = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
914        $source = new SourceRef('test', 'fixture.php', 0, 1);
915        $query = new ValueQuery(new ExpressionRef($source, $owner, 'r1'));
916        $context = new Context($fixture->program, $query, $fixture->configuration, $fixture->evaluators);
917        $state = new State();
918        $state->registers['r1'] = Term::constant(7);
919        (new ObservationCollector($context))->instruction(new CallableGraph('N\target', [], [], $source), new BoundInstruction('i', 'constant', $source, 'r1'), $state, ObservationPhase::After);
920        self::assertCount(1, $context->normal);
921        self::assertSame(7, $context->normal[0]->values['value']->native());
922        self::assertTrue($state->observed);
923        self::assertSame(CompletionKind::Observed, $state->completion->kind);
924    }
925    /**
926     * @return iterable<string,array{string}>
927     */
928    public static function providerNamedOwnerVariants(): iterable
929    {
930        yield 'declaration' => ['N\target'];
931        yield 'case variant' => ['n\TARGET'];
932        yield 'qualified variant' => ['\N\TARGET'];
933    }
934    public function testRepeatedRecognizesAControlFlowCycle(): void
935    {
936        $source1 = '<?php function target(){return 1;}';
937        $budget = new Budget();
938        $configuration = new DeriverConfig();
939        $contributions = new Contributions();
940        $input = new Sources([new Source('fixture.php', $source1)]);
941        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
942        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
943        $contributions1 = $providers->contributions;
944        $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);
945        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
946        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
947        $snapshotId = $identity->id;
948        $input1 = $providers->input;
949        $parser = new Parser(new ParserConfig($target->version));
950        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
951        $program1 = new Index($snapshotId, $trees);
952        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
953        $snapshotId1 = 'test';
954        $input2 = new Sources([new Source('fixture.php', $source1)]);
955        $profile = $execution->configuration->target;
956        $parser1 = new Parser(new ParserConfig($profile->version));
957        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
958        $program = new Index($snapshotId1, $trees1);
959        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
960        $source = new SourceRef('s', 'test.php', 0, 1);
961        $body = new CallableGraph('target', [], [new BasicBlock(0, [], new Terminator(TerminatorKind::Jump, targets: [0]))], $source);
962        self::assertTrue((new ObservationCollector($context))->repeated($body, 0));
963    }
964}
965