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