packages/deriver/tests/Unit/Deriver/Evaluator/Execution/Control/StateJoinTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Execution\Control;
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\Call\Evaluators;
14use Deriver\Deriver\Evaluator\Call\Extensions;
15use Deriver\Deriver\Evaluator\Call\Registrations;
16use Deriver\Deriver\Evaluator\Call\StateRegistry;
17use Deriver\Deriver\Evaluator\Domain\Lattice;
18use Deriver\Deriver\Evaluator\EvaluatorConfig;
19use Deriver\Deriver\Evaluator\Evidence\Frontier;
20use Deriver\Deriver\Evaluator\Execution\Call\ArgumentBinding;
21use Deriver\Deriver\Evaluator\Execution\Call\ArgumentOrder;
22use Deriver\Deriver\Evaluator\Execution\Call\ParameterBinding;
23use Deriver\Deriver\Evaluator\Execution\Call\TypeBinding;
24use Deriver\Deriver\Evaluator\Execution\Call\TypeCheck;
25use Deriver\Deriver\Evaluator\Execution\Completion\Alternative;
26use Deriver\Deriver\Evaluator\Execution\Completion\Assessment;
27use Deriver\Deriver\Evaluator\Execution\Completion\Completion;
28use Deriver\Deriver\Evaluator\Execution\Completion\CompletionKind;
29use Deriver\Deriver\Evaluator\Execution\Completion\StorageSnapshot;
30use Deriver\Deriver\Evaluator\Execution\Constraints;
31use Deriver\Deriver\Evaluator\Execution\Context;
32use Deriver\Deriver\Evaluator\Execution\Control\ObservationLimit;
33use Deriver\Deriver\Evaluator\Execution\Control\PathJoin;
34use Deriver\Deriver\Evaluator\Execution\Control\ResidualPaths;
35use Deriver\Deriver\Evaluator\Execution\Control\StateJoin;
36use Deriver\Deriver\Evaluator\Execution\Control\Unwinding;
37use Deriver\Deriver\Evaluator\Execution\Demand\Cell;
38use Deriver\Deriver\Evaluator\Execution\Demand\Components;
39use Deriver\Deriver\Evaluator\Execution\Demand\Discovery;
40use Deriver\Deriver\Evaluator\Execution\Demand\Key;
41use Deriver\Deriver\Evaluator\Execution\Demand\Table;
42use Deriver\Deriver\Evaluator\Execution\Dependencies;
43use Deriver\Deriver\Evaluator\Execution\Evaluator;
44use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
45use Deriver\Deriver\Evaluator\Execution\Havoc;
46use Deriver\Deriver\Evaluator\Execution\InstructionTransfer;
47use Deriver\Deriver\Evaluator\Execution\Memory\Location;
48use Deriver\Deriver\Evaluator\Execution\Memory\Materialization;
49use Deriver\Deriver\Evaluator\Execution\Memory\Memory;
50use Deriver\Deriver\Evaluator\Execution\Memory\ReferenceConstraint;
51use Deriver\Deriver\Evaluator\Execution\Memory\Slots\SlotReference;
52use Deriver\Deriver\Evaluator\Execution\Memory\StorageCapture;
53use Deriver\Deriver\Evaluator\Execution\ObservationCollector;
54use Deriver\Deriver\Evaluator\Execution\Operation\Conversions;
55use Deriver\Deriver\Evaluator\Execution\Operation\ScalarErrors;
56use Deriver\Deriver\Evaluator\Execution\State;
57use Deriver\Deriver\Evaluator\Execution\Summary\CompletionRecord;
58use Deriver\Deriver\Evaluator\Execution\Summary\Evaluation;
59use Deriver\Deriver\Evaluator\Execution\Summary\Invocation;
60use Deriver\Deriver\Evaluator\Execution\Summary\Isolation;
61use Deriver\Deriver\Evaluator\Execution\Trace\Derivation;
62use Deriver\Deriver\Evaluator\Execution\Transfer\MemoryStep;
63use Deriver\Deriver\Evaluator\Execution\Transfer\PureStep;
64use Deriver\Deriver\Evaluator\Execution\Transfer\ReferenceAssignment;
65use Deriver\Deriver\Evaluator\Request\Budget;
66use Deriver\Deriver\Evaluator\Request\EntryPoint;
67use Deriver\Deriver\Evaluator\Request\QueryScope;
68use Deriver\Deriver\Evaluator\Request\ReturnQuery;
69use Deriver\Deriver\Evaluator\Scalar\Arrays;
70use Deriver\Deriver\Evaluator\Scalar\Identity;
71use Deriver\Deriver\Evaluator\Scalar\Operations;
72use Deriver\Deriver\Evaluator\Scalar\StringPrefix;
73use Deriver\Deriver\Evaluator\TargetProfile;
74use Deriver\Deriver\Evaluator\Term;
75use Deriver\Deriver\Evaluator\TermKind;
76use Deriver\Deriver\Execution\DerivationResult;
77use Deriver\Deriver\Execution\QueryExecution;
78use Deriver\Deriver\Execution\ResultAssessment;
79use Deriver\Deriver\Identity\QueryEncoding;
80use Deriver\Deriver\Identity\ValueGraph;
81use Deriver\Deriver\Lowerer\AggregateLowering;
82use Deriver\Deriver\Lowerer\AssignmentLowering;
83use Deriver\Deriver\Lowerer\Control\DestructuringLowering;
84use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
85use Deriver\Deriver\Lowerer\Declaration\CallableSource;
86use Deriver\Deriver\Lowerer\Declaration\Index;
87use Deriver\Deriver\Lowerer\Declaration\Indexer;
88use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
89use Deriver\Deriver\Lowerer\ExpressionLowering;
90use Deriver\Deriver\Lowerer\Graph\BasicBlock;
91use Deriver\Deriver\Lowerer\Graph\CallableGraph;
92use Deriver\Deriver\Lowerer\Graph\Instruction;
93use Deriver\Deriver\Lowerer\Graph\Parameter;
94use Deriver\Deriver\Lowerer\Graph\Terminator;
95use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
96use Deriver\Deriver\Lowerer\InstructionEmitter;
97use Deriver\Deriver\Lowerer\Lowerer;
98use Deriver\Deriver\Lowerer\LoweringScope;
99use Deriver\Deriver\Lowerer\StatementLowering;
100use Deriver\Deriver\MicroEvaluatorConversion;
101use Deriver\Deriver\Parser\Node\File;
102use Deriver\Deriver\Parser\Parser;
103use Deriver\Deriver\Parser\ParserConfig;
104use Deriver\Deriver\Parser\TargetSyntax;
105use Deriver\Deriver\Providers\ProviderExpansion;
106use Deriver\Deriver\Resolver\AssignmentPatterns;
107use Deriver\Deriver\Resolver\ClassScope;
108use Deriver\Deriver\Resolver\ExpressionValidation;
109use Deriver\Deriver\Resolver\MagicContext;
110use Deriver\Deriver\Resolver\Resolver;
111use Deriver\Deriver\Selection\QueryValidation;
112use Deriver\Deriver\SnapshotIdentity;
113use Deriver\Deriver\Sources\LineMap;
114use Deriver\Deriver\Sources\Path;
115use Deriver\Deriver\Sources\Source;
116use Deriver\Deriver\Sources\SourceRef;
117use Deriver\Deriver\Sources\Sources;
118use Deriver\Exception\ParserException;
119use Deriver\Exception\ResolutionException;
120use PHPUnit\Framework\Attributes\CoversClass;
121use PHPUnit\Framework\Attributes\Small;
122use PHPUnit\Framework\Attributes\UsesClass;
123use PHPUnit\Framework\TestCase;
124
125#[UsesClass(Resolver::class)]
126#[UsesClass(ExpressionValidation::class)]
127#[UsesClass(ResolutionException::class)]
128#[UsesClass(ParserConfig::class)]
129#[UsesClass(ParserException::class)]
130#[CoversClass(StateJoin::class)]
131#[UsesClass(QueryExecution::class)]
132#[UsesClass(QueryValidation::class)]
133#[UsesClass(ResultAssessment::class)]
134#[UsesClass(Deriver::class)]
135#[UsesClass(Constraints::class)]
136#[UsesClass(BasicBlock::class)]
137#[UsesClass(CallableGraph::class)]
138#[UsesClass(CallableIdentity::class)]
139#[UsesClass(Instruction::class)]
140#[UsesClass(Parameter::class)]
141#[UsesClass(Terminator::class)]
142#[UsesClass(ArgumentBinding::class)]
143#[UsesClass(ArgumentOrder::class)]
144#[UsesClass(Dispatch::class)]
145#[UsesClass(ParameterBinding::class)]
146#[UsesClass(TypeBinding::class)]
147#[UsesClass(TypeCheck::class)]
148#[UsesClass(Completion::class)]
149#[UsesClass(Context::class)]
150#[UsesClass(ObservationLimit::class)]
151#[UsesClass(ResidualPaths::class)]
152#[UsesClass(Unwinding::class)]
153#[UsesClass(Cell::class)]
154#[UsesClass(Components::class)]
155#[UsesClass(Discovery::class)]
156#[UsesClass(Key::class)]
157#[UsesClass(Table::class)]
158#[UsesClass(Dependencies::class)]
159#[UsesClass(Havoc::class)]
160#[UsesClass(InstructionTransfer::class)]
161#[UsesClass(Evaluator::class)]
162#[UsesClass(SlotReference::class)]
163#[UsesClass(ObservationCollector::class)]
164#[UsesClass(Conversions::class)]
165#[UsesClass(ScalarErrors::class)]
166#[UsesClass(State::class)]
167#[UsesClass(CompletionRecord::class)]
168#[UsesClass(Evaluation::class)]
169#[UsesClass(Invocation::class)]
170#[UsesClass(Isolation::class)]
171#[UsesClass(MemoryStep::class)]
172#[UsesClass(PureStep::class)]
173#[UsesClass(ReferenceAssignment::class)]
174#[UsesClass(Location::class)]
175#[UsesClass(Materialization::class)]
176#[UsesClass(Memory::class)]
177#[UsesClass(ReferenceConstraint::class)]
178#[UsesClass(StorageCapture::class)]
179#[UsesClass(Extensions::class)]
180#[UsesClass(ProviderExpansion::class)]
181#[UsesClass(Evaluators::class)]
182#[UsesClass(StateRegistry::class)]
183#[UsesClass(DeriverConfig::class)]
184#[UsesClass(EntryPoint::class)]
185#[UsesClass(Sources::class)]
186#[UsesClass(Path::class)]
187#[UsesClass(Snapshot::class)]
188#[UsesClass(Source::class)]
189#[UsesClass(TargetProfile::class)]
190#[UsesClass(Budget::class)]
191#[UsesClass(QueryScope::class)]
192#[UsesClass(ReturnQuery::class)]
193#[UsesClass(ResultRef::class)]
194#[UsesClass(SourceRef::class)]
195#[UsesClass(Alternative::class)]
196#[UsesClass(Assessment::class)]
197#[UsesClass(Derivation::class)]
198#[UsesClass(DerivationResult::class)]
199#[UsesClass(Frontier::class)]
200#[UsesClass(QueryEncoding::class)]
201#[UsesClass(ValueGraph::class)]
202#[UsesClass(Statistics::class)]
203#[UsesClass(StorageSnapshot::class)]
204#[UsesClass(Parser::class)]
205#[UsesClass(File::class)]
206#[UsesClass(AggregateLowering::class)]
207#[UsesClass(AssignmentLowering::class)]
208#[UsesClass(Lowerer::class)]
209#[UsesClass(DestructuringLowering::class)]
210#[UsesClass(ExpressionLowering::class)]
211#[UsesClass(InstructionEmitter::class)]
212#[UsesClass(LoweringScope::class)]
213#[UsesClass(StatementLowering::class)]
214#[UsesClass(CallableSource::class)]
215#[UsesClass(Indexer::class)]
216#[UsesClass(Index::class)]
217#[UsesClass(Composition::class)]
218#[UsesClass(LineMap::class)]
219#[UsesClass(MagicContext::class)]
220#[UsesClass(AssignmentPatterns::class)]
221#[UsesClass(ClassScope::class)]
222#[UsesClass(TargetSyntax::class)]
223#[UsesClass(Arrays::class)]
224#[UsesClass(Identity::class)]
225#[UsesClass(Operations::class)]
226#[UsesClass(PathJoin::class)]
227#[UsesClass(Lattice::class)]
228#[UsesClass(StringPrefix::class)]
229#[UsesClass(Term::class)]
230#[Small]
231final class StateJoinTest extends TestCase
232{
233    public function testLimitPreservesTheSemanticContract(): void
234    {
235        $source = '<?php function target(bool $admin){if($admin){$table="admins";$column="admin_id";}else{$table="users";$column="user_id";}return [$table,$column];}';
236        $symbol = 'target';
237        $input = new Sources([new Source('fixture.php', $source)]);
238        $configuration = new DeriverConfig();
239        $contributions = new Contributions();
240        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
241        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
242        $contributions1 = $providers->contributions;
243        $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);
244        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
245        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
246        $snapshotId = $identity->id;
247        $input1 = $providers->input;
248        $parser = new Parser(new ParserConfig($target->version));
249        $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));
250        $program = new Index($snapshotId, $trees);
251        $result = (new QueryExecution($program, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program->diagnostics()))))->derive(new ReturnQuery($symbol));
252        self::assertCount(2, $result->normalOutcomes);
253        self::assertSame(['admins', 'admin_id'], $result->normalOutcomes[0]->values['return']->native());
254        self::assertSame(['users', 'user_id'], $result->normalOutcomes[1]->values['return']->native());
255        self::assertNotSame($result->normalOutcomes[0]->guard, $result->normalOutcomes[1]->guard);
256    }
257    public function testCompletedIncludesLateReturnsAfterThePartitionLimit(): void
258    {
259        $source = '<?php function target(){return 1;}';
260        $budget = new Budget(partitions: 1);
261        $configuration = new DeriverConfig();
262        $contributions = new Contributions();
263        $input = new Sources([new Source('fixture.php', $source)]);
264        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
265        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
266        $contributions1 = $providers->contributions;
267        $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);
268        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
269        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
270        $snapshotId = $identity->id;
271        $input1 = $providers->input;
272        $parser = new Parser(new ParserConfig($target->version));
273        $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));
274        $program1 = new Index($snapshotId, $trees);
275        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
276        $snapshotId1 = 'test';
277        $input2 = new Sources([new Source('fixture.php', $source)]);
278        $profile = $execution->configuration->target;
279        $parser1 = new Parser(new ParserConfig($profile->version));
280        $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));
281        $program = new Index($snapshotId1, $trees1);
282        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
283        $body = $context->program->callable('target');
284        self::assertNotNull($body);
285        $a = new State();
286        $a->completion = new Completion(CompletionKind::Return, Term::constant(1));
287        $b = new State();
288        $b->completion = new Completion(CompletionKind::Return, Term::constant('last'));
289        $states = (new StateJoin($context))->completed([$a, $b], $body);
290        self::assertCount(1, $states);
291        self::assertSame(TermKind::Abstract, $states[0]->completion->value?->kind);
292        self::assertTrue((new Lattice())->contains($states[0]->completion->value, Term::constant(1)));
293        self::assertTrue((new Lattice())->contains($states[0]->completion->value, Term::constant('last')));
294        self::assertCount(2, $context->frontiers);
295    }
296    public function testCompletedHavocsLateExceptionsThatCannotBeJoined(): void
297    {
298        $source = '<?php function target(){return 1;}';
299        $budget = new Budget(partitions: 1);
300        $configuration = new DeriverConfig();
301        $contributions = new Contributions();
302        $input = new Sources([new Source('fixture.php', $source)]);
303        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
304        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
305        $contributions1 = $providers->contributions;
306        $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);
307        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
308        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
309        $snapshotId = $identity->id;
310        $input1 = $providers->input;
311        $parser = new Parser(new ParserConfig($target->version));
312        $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));
313        $program1 = new Index($snapshotId, $trees);
314        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
315        $snapshotId1 = 'test';
316        $input2 = new Sources([new Source('fixture.php', $source)]);
317        $profile = $execution->configuration->target;
318        $parser1 = new Parser(new ParserConfig($profile->version));
319        $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));
320        $program = new Index($snapshotId1, $trees1);
321        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
322        $body = $context->program->callable('target');
323        self::assertNotNull($body);
324        $a = new State();
325        $a->completion = new Completion(CompletionKind::Throw, new Term(TermKind::Throwable, 'LogicException'));
326        $b = new State();
327        $b->completion = new Completion(CompletionKind::Throw, new Term(TermKind::Throwable, 'RuntimeException'));
328        $states = (new StateJoin($context))->completed([$a, $b], $body);
329        self::assertCount(1, $states);
330        self::assertSame(['Throwable', true], [$states[0]->completion->value?->literal, $states[0]->completion->value?->attributes['uncertain'] ?? null]);
331    }
332    public function testLimitGroupsIndependentProgramPointsBeforeApplyingTheBudget(): void
333    {
334        $source = '<?php function target(){return 1;}';
335        $budget = new Budget(partitions: 2);
336        $configuration = new DeriverConfig();
337        $contributions = new Contributions();
338        $input = new Sources([new Source('fixture.php', $source)]);
339        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
340        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
341        $contributions1 = $providers->contributions;
342        $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);
343        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
344        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
345        $snapshotId = $identity->id;
346        $input1 = $providers->input;
347        $parser = new Parser(new ParserConfig($target->version));
348        $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));
349        $program1 = new Index($snapshotId, $trees);
350        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
351        $snapshotId1 = 'test';
352        $input2 = new Sources([new Source('fixture.php', $source)]);
353        $profile = $execution->configuration->target;
354        $parser1 = new Parser(new ParserConfig($profile->version));
355        $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));
356        $program = new Index($snapshotId1, $trees1);
357        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
358        $symbol = 'target';
359        $body = $context->program->callable($symbol);
360        self::assertNotNull($body);
361        $a = new State();
362        $a->block = 1;
363        $b = new State();
364        $b->block = 2;
365        $c = new State();
366        $c->block = 1;
367        $states = (new StateJoin($context))->limit([$a, $b, $c], $body);
368        self::assertSame([$a, $c, $b], $states);
369        self::assertSame([], $context->frontiers);
370    }
371    public function testLimitKeepsClustersWithinTheBudgetAndSealsTheStorageOfTheRest(): void
372    {
373        $source = '<?php function target(){return 1;}';
374        $budget = new Budget(partitions: 1);
375        $configuration = new DeriverConfig();
376        $contributions = new Contributions();
377        $input = new Sources([new Source('fixture.php', $source)]);
378        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
379        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
380        $contributions1 = $providers->contributions;
381        $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);
382        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
383        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
384        $snapshotId = $identity->id;
385        $input1 = $providers->input;
386        $parser = new Parser(new ParserConfig($target->version));
387        $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));
388        $program1 = new Index($snapshotId, $trees);
389        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
390        $snapshotId1 = 'test';
391        $input2 = new Sources([new Source('fixture.php', $source)]);
392        $profile = $execution->configuration->target;
393        $parser1 = new Parser(new ParserConfig($profile->version));
394        $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));
395        $program = new Index($snapshotId1, $trees1);
396        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
397        $symbol = 'target';
398        $body = $context->program->callable($symbol);
399        self::assertNotNull($body);
400        $a = new State();
401        $a->observed = true;
402        $a->guard = ['first' => true];
403        $a->locals['a'] = new Location('a');
404        $a->memory->cells['a'] = Term::constant(1);
405        $b = new State();
406        $b->observed = false;
407        $b->locals['b'] = new Location('b');
408        $b->memory->cells['b'] = Term::constant(2);
409        $c = new State();
410        $c->observed = true;
411        $states = (new StateJoin($context))->limit([$a, $b, $c], $body);
412        self::assertCount(3, $states);
413        self::assertSame($a, $states[0]);
414        self::assertSame([CompletionKind::Normal, CompletionKind::Return, CompletionKind::Throw], array_map(static fn ($state) => $state->completion->kind, $states));
415        self::assertFalse($states[1]->observed);
416        self::assertFalse($states[2]->observed);
417        self::assertSame([], $states[1]->guard);
418        self::assertSame([], $states[1]->constraints);
419        self::assertSame(['b'], array_keys($states[1]->locals));
420        self::assertSame([TermKind::Opaque], array_column($states[1]->memory->cells, 'kind'));
421        self::assertSame('BUDGET_EXCEEDED', $states[1]->memory->unknownShared);
422        self::assertSame('Throwable', $states[2]->completion->value?->literal);
423        self::assertSame(1, $a->memory->cells['a']->native());
424        self::assertSame(2, $b->memory->cells['b']->native());
425        self::assertContains('CORRELATION_RELAXED', array_column($context->frontiers, 'code'));
426    }
427    public function testLimitJoinsExcessPathsThatDifferOnlyInPlainValues(): void
428    {
429        $source = '<?php function target(){return 1;}';
430        $budget = new Budget(partitions: 2);
431        $configuration = new DeriverConfig();
432        $contributions = new Contributions();
433        $input = new Sources([new Source('fixture.php', $source)]);
434        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
435        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
436        $contributions1 = $providers->contributions;
437        $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);
438        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
439        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
440        $snapshotId = $identity->id;
441        $input1 = $providers->input;
442        $parser = new Parser(new ParserConfig($target->version));
443        $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));
444        $program1 = new Index($snapshotId, $trees);
445        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
446        $snapshotId1 = 'test';
447        $input2 = new Sources([new Source('fixture.php', $source)]);
448        $profile = $execution->configuration->target;
449        $parser1 = new Parser(new ParserConfig($profile->version));
450        $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));
451        $program = new Index($snapshotId1, $trees1);
452        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
453        $symbol = 'target';
454        $body = $context->program->callable($symbol);
455        self::assertNotNull($body);
456        $base = new State();
457        $base->block = 1;
458        $base->memory->write($base->local('sql'), Term::constant(''));
459        $paths = array_map(static function (string $sql) use ($base): State {
460            $path = $base->fork();
461            $path->memory->write($path->local('sql'), Term::constant($sql));
462            return $path;
463        }, ['SELECT a', 'SELECT b', 'SELECT c']);
464        $states = (new StateJoin($context))->limit($paths, $body);
465        self::assertCount(2, $states);
466        self::assertSame($paths[0], $states[0]);
467        self::assertSame(CompletionKind::Normal, $states[1]->completion->kind);
468        $joined = $states[1]->memory->read($states[1]->locals['sql']);
469        self::assertSame('SELECT ', $joined->operands[0]->native());
470        self::assertSame(['BUDGET_EXCEEDED', 'CORRELATION_RELAXED'], array_column(array_values($context->frontiers), 'code'));
471    }
472    public function testLimitPassesCompletedResidualsThrough(): void
473    {
474        $source = '<?php function target(){return 1;}';
475        $budget = new Budget(partitions: 1);
476        $configuration = new DeriverConfig();
477        $contributions = new Contributions();
478        $input = new Sources([new Source('fixture.php', $source)]);
479        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
480        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
481        $contributions1 = $providers->contributions;
482        $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);
483        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
484        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
485        $snapshotId = $identity->id;
486        $input1 = $providers->input;
487        $parser = new Parser(new ParserConfig($target->version));
488        $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));
489        $program1 = new Index($snapshotId, $trees);
490        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
491        $snapshotId1 = 'test';
492        $input2 = new Sources([new Source('fixture.php', $source)]);
493        $profile = $execution->configuration->target;
494        $parser1 = new Parser(new ParserConfig($profile->version));
495        $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));
496        $program = new Index($snapshotId1, $trees1);
497        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
498        $symbol = 'target';
499        $body = $context->program->callable($symbol);
500        self::assertNotNull($body);
501        $a = new State();
502        $a->completion = new Completion(CompletionKind::Return, Term::constant(1));
503        $b = new State();
504        $b->completion = new Completion(CompletionKind::Throw, new Term(TermKind::Throwable, 'Throwable'));
505        self::assertSame([$a, $b], (new StateJoin($context))->limit([$a, $b], $body));
506        self::assertSame([], $context->frontiers);
507    }
508    public function testClustersJoinCompatiblePathsAndKeepOtherStructures(): void
509    {
510        $source = '<?php function target(){return 1;}';
511        $budget = new Budget();
512        $configuration = new DeriverConfig();
513        $contributions = new Contributions();
514        $input = new Sources([new Source('fixture.php', $source)]);
515        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
516        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
517        $contributions1 = $providers->contributions;
518        $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);
519        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
520        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
521        $snapshotId = $identity->id;
522        $input1 = $providers->input;
523        $parser = new Parser(new ParserConfig($target->version));
524        $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));
525        $program1 = new Index($snapshotId, $trees);
526        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
527        $snapshotId1 = 'test';
528        $input2 = new Sources([new Source('fixture.php', $source)]);
529        $profile = $execution->configuration->target;
530        $parser1 = new Parser(new ParserConfig($profile->version));
531        $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));
532        $program = new Index($snapshotId1, $trees1);
533        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
534        $first = Term::constant('x');
535        $second = Term::constant('y');
536        $base = new State();
537        $base->block = 2;
538        $base->previous = 1;
539        $base->memory->write($base->local('table'), Term::constant('kept'));
540        $base->local('sql');
541        $a1 = $base->fork();
542        $b1 = $base->fork();
543        $a1->memory->write($a1->local('sql'), $first);
544        $b1->memory->write($b1->local('sql'), $second);
545        [$a, $b] = [$a1, $b1];
546        $first1 = Term::constant('z');
547        $second1 = Term::constant('z');
548        $base1 = new State();
549        $base1->block = 2;
550        $base1->previous = 1;
551        $base1->memory->write($base1->local('table'), Term::constant('kept'));
552        $base1->local('sql');
553        $a2 = $base1->fork();
554        $b2 = $base1->fork();
555        $a2->memory->write($a2->local('sql'), $first1);
556        $b2->memory->write($b2->local('sql'), $second1);
557        [$c] = [$a2, $b2];
558        $c->locals['extra'] = new Location('extra');
559        $clusters = (new StateJoin($context))->clusters([$a, $c, $b]);
560        self::assertCount(2, $clusters);
561        self::assertSame('WIDENED', $clusters[0]->memory->read($clusters[0]->locals['sql'])->literal);
562        self::assertSame($c, $clusters[1]);
563    }
564    public function testSealKeepsTheRootsOfEverySealedPath(): void
565    {
566        $source = '<?php function target(){return 1;}';
567        $budget = new Budget();
568        $configuration = new DeriverConfig();
569        $contributions = new Contributions();
570        $input = new Sources([new Source('fixture.php', $source)]);
571        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
572        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
573        $contributions1 = $providers->contributions;
574        $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);
575        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
576        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
577        $snapshotId = $identity->id;
578        $input1 = $providers->input;
579        $parser = new Parser(new ParserConfig($target->version));
580        $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));
581        $program1 = new Index($snapshotId, $trees);
582        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
583        $snapshotId1 = 'test';
584        $input2 = new Sources([new Source('fixture.php', $source)]);
585        $profile = $execution->configuration->target;
586        $parser1 = new Parser(new ParserConfig($profile->version));
587        $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));
588        $program = new Index($snapshotId1, $trees1);
589        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
590        $symbol = 'target';
591        $body = $context->program->callable($symbol);
592        self::assertNotNull($body);
593        $a = new State();
594        $a->locals['a'] = new Location('a');
595        $a->memory->cells['a'] = Term::constant(1);
596        $b = new State();
597        $b->locals['b'] = new Location('b');
598        $b->memory->cells['b'] = Term::constant(2);
599        $paths = (new StateJoin($context))->seal([$a, $b], $body);
600        self::assertSame(['return', 'throw'], array_map(static fn (State $state): string => $state->completion->kind->value, $paths));
601        self::assertSame(['a', 'b'], array_keys($paths[0]->locals));
602        self::assertSame([TermKind::Opaque, TermKind::Opaque], array_column($paths[0]->memory->cells, 'kind'));
603    }
604    public function testEncodeSharesTermsAndDistinguishesValues(): void
605    {
606        $source = '<?php function target(){return 1;}';
607        $budget = new Budget();
608        $configuration = new DeriverConfig();
609        $contributions = new Contributions();
610        $input = new Sources([new Source('fixture.php', $source)]);
611        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
612        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
613        $contributions1 = $providers->contributions;
614        $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);
615        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
616        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
617        $snapshotId = $identity->id;
618        $input1 = $providers->input;
619        $parser = new Parser(new ParserConfig($target->version));
620        $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));
621        $program1 = new Index($snapshotId, $trees);
622        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
623        $snapshotId1 = 'test';
624        $input2 = new Sources([new Source('fixture.php', $source)]);
625        $profile = $execution->configuration->target;
626        $parser1 = new Parser(new ParserConfig($profile->version));
627        $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));
628        $program = new Index($snapshotId1, $trees1);
629        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
630        $join = new StateJoin($context);
631        $depth = 40;
632        $leaf = Term::constant(1);
633        $leaf = array_reduce(array_fill(0, $depth, null), static fn (Term $leaf): Term => Term::array([$leaf, $leaf]), $leaf);
634        $shared = $leaf;
635        self::assertSame($join->encode(new Location('x'), $context->identity), $join->encode(new Location('x'), $context->identity));
636        self::assertNotSame($join->encode(new Location('x'), $context->identity), $join->encode(new Location('y'), $context->identity));
637        $state = new State();
638        $state->registers['r'] = $shared;
639        self::assertSame($join->encode($state, $context->identity), $join->encode($state->fork(), $context->identity));
640    }
641    public function testCandidateKeysSeparateStatesWithDifferentStorage(): void
642    {
643        $source = '<?php function target(){return 1;}';
644        $budget = new Budget();
645        $configuration = new DeriverConfig();
646        $contributions = new Contributions();
647        $input = new Sources([new Source('fixture.php', $source)]);
648        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
649        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
650        $contributions1 = $providers->contributions;
651        $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);
652        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
653        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
654        $snapshotId = $identity->id;
655        $input1 = $providers->input;
656        $parser = new Parser(new ParserConfig($target->version));
657        $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));
658        $program1 = new Index($snapshotId, $trees);
659        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
660        $snapshotId1 = 'test';
661        $input2 = new Sources([new Source('fixture.php', $source)]);
662        $profile = $execution->configuration->target;
663        $parser1 = new Parser(new ParserConfig($profile->version));
664        $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));
665        $program = new Index($snapshotId1, $trees1);
666        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
667        $join = new StateJoin($context);
668        $a = new State();
669        $a->registers['order'] = Term::parameter('order', 'bool');
670        $a->memory->write($a->local('x'), Term::constant(1));
671        $b = $a->fork();
672        $b->guard[$context->identity->key(Term::parameter('order', 'bool'))] = true;
673        self::assertSame($join->candidateKey($a, 'order', $context->identity), $join->candidateKey($b, 'order', $context->identity));
674        $b->memory->write($b->local('x'), Term::constant(2));
675        self::assertNotSame($join->candidateKey($a, 'order', $context->identity), $join->candidateKey($b, 'order', $context->identity));
676        self::assertNotSame($join->orderKey($a, 'order'), $join->orderKey($b, 'order'));
677    }
678    public function testCompletedKeepsNormalAndExceptionalGroupsIndependentAtTheExactLimit(): void
679    {
680        $source = '<?php function target(){return 1;}';
681        $budget = new Budget(partitions: 1);
682        $configuration = new DeriverConfig();
683        $contributions = new Contributions();
684        $input = new Sources([new Source('fixture.php', $source)]);
685        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
686        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
687        $contributions1 = $providers->contributions;
688        $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);
689        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
690        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
691        $snapshotId = $identity->id;
692        $input1 = $providers->input;
693        $parser = new Parser(new ParserConfig($target->version));
694        $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));
695        $program1 = new Index($snapshotId, $trees);
696        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
697        $snapshotId1 = 'test';
698        $input2 = new Sources([new Source('fixture.php', $source)]);
699        $profile = $execution->configuration->target;
700        $parser1 = new Parser(new ParserConfig($profile->version));
701        $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));
702        $program = new Index($snapshotId1, $trees1);
703        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
704        $symbol = 'target';
705        $body = $context->program->callable($symbol);
706        self::assertNotNull($body);
707        $normal = new State();
708        $normal->completion = new Completion(CompletionKind::Return, Term::constant(3));
709        $error = new State();
710        $error->completion = new Completion(CompletionKind::Throw, new Term(TermKind::Throwable, 'Error'));
711        $states = (new StateJoin($context))->completed([$normal, $error], $body);
712        self::assertSame([$normal, $error], $states);
713        self::assertSame([], $context->frontiers);
714    }
715    public function testCompletedRetainsUnobservedExceptionalStateFromLaterPartitions(): void
716    {
717        $source = '<?php function target(){return 1;}';
718        $budget = new Budget(partitions: 1);
719        $configuration = new DeriverConfig();
720        $contributions = new Contributions();
721        $input = new Sources([new Source('fixture.php', $source)]);
722        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
723        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
724        $contributions1 = $providers->contributions;
725        $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);
726        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
727        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
728        $snapshotId = $identity->id;
729        $input1 = $providers->input;
730        $parser = new Parser(new ParserConfig($target->version));
731        $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));
732        $program1 = new Index($snapshotId, $trees);
733        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
734        $snapshotId1 = 'test';
735        $input2 = new Sources([new Source('fixture.php', $source)]);
736        $profile = $execution->configuration->target;
737        $parser1 = new Parser(new ParserConfig($profile->version));
738        $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));
739        $program = new Index($snapshotId1, $trees1);
740        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
741        $symbol = 'target';
742        $body = $context->program->callable($symbol);
743        self::assertNotNull($body);
744        $first = new State();
745        $first->completion = new Completion(CompletionKind::Throw, new Term(TermKind::Throwable, 'Error'));
746        $first->observed = true;
747        $first->guard = ['first' => true];
748        $first->constraints = ['n' => ['min' => 1, 'max' => null, 'equal' => null, 'excluded' => []]];
749        $first->locals['a'] = new Location('first');
750        $first->memory->cells['first'] = Term::constant(1);
751        $last = new State();
752        $last->completion = new Completion(CompletionKind::Throw, new Term(TermKind::Throwable, 'RuntimeException'));
753        $last->locals['z'] = new Location('last');
754        $last->memory->cells['last'] = Term::constant(2);
755        $states = (new StateJoin($context))->completed([$first, $last], $body);
756        self::assertCount(1, $states);
757        self::assertSame(CompletionKind::Throw, $states[0]->completion->kind);
758        self::assertSame('Throwable', $states[0]->completion->value?->literal);
759        self::assertSame(['uncertain' => true], $states[0]->completion->value->attributes);
760        self::assertFalse($states[0]->observed);
761        self::assertSame([], $states[0]->guard);
762        self::assertSame([], $states[0]->constraints);
763        self::assertSame(['a', 'z'], array_keys($states[0]->locals));
764        self::assertSame(['first', 'last'], array_keys($states[0]->memory->cells));
765        self::assertSame([TermKind::Opaque, TermKind::Opaque], array_column($states[0]->memory->cells, 'kind'));
766        self::assertSame('BUDGET_EXCEEDED', $states[0]->memory->unknownShared);
767        self::assertTrue($first->observed);
768        self::assertSame(1, $first->memory->cells['first']->native());
769        self::assertSame(2, $last->memory->cells['last']->native());
770    }
771    public function testOrderKeyIgnoresEvidenceMultiplicityButRetainsScalarTypes(): void
772    {
773        $source = '<?php function target(){return 1;}';
774        $budget = new Budget();
775        $configuration = new DeriverConfig();
776        $contributions = new Contributions();
777        $input = new Sources([new Source('fixture.php', $source)]);
778        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
779        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
780        $contributions1 = $providers->contributions;
781        $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);
782        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
783        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
784        $snapshotId = $identity->id;
785        $input1 = $providers->input;
786        $parser = new Parser(new ParserConfig($target->version));
787        $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));
788        $program1 = new Index($snapshotId, $trees);
789        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
790        $snapshotId1 = 'test';
791        $input2 = new Sources([new Source('fixture.php', $source)]);
792        $profile = $execution->configuration->target;
793        $parser1 = new Parser(new ParserConfig($profile->version));
794        $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));
795        $program = new Index($snapshotId1, $trees1);
796        $join = new StateJoin(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators));
797        $a = new State();
798        $a->registers['order'] = Term::parameter('order', 'bool');
799        $a->registers['value'] = Term::constant(1);
800        $a->evidence = ['a', 'b', 'a'];
801        $b = $a->fork();
802        $b->evidence = ['b', 'a'];
803        self::assertSame($join->orderKey($a, 'order'), $join->orderKey($b, 'order'));
804        $b->registers['value'] = Term::constant('1');
805        self::assertNotSame($join->orderKey($a, 'order'), $join->orderKey($b, 'order'));
806    }
807    public function testFingerprintIgnoresObjectSharingAndKeepsArrayOrder(): void
808    {
809        $source = '<?php function target(){return 1;}';
810        $budget = new Budget();
811        $configuration = new DeriverConfig();
812        $contributions = new Contributions();
813        $input = new Sources([new Source('fixture.php', $source)]);
814        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
815        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
816        $contributions1 = $providers->contributions;
817        $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);
818        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
819        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
820        $snapshotId = $identity->id;
821        $input1 = $providers->input;
822        $parser = new Parser(new ParserConfig($target->version));
823        $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));
824        $program1 = new Index($snapshotId, $trees);
825        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
826        $snapshotId1 = 'test';
827        $input2 = new Sources([new Source('fixture.php', $source)]);
828        $profile = $execution->configuration->target;
829        $parser1 = new Parser(new ParserConfig($profile->version));
830        $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));
831        $program = new Index($snapshotId1, $trees1);
832        $join = new StateJoin(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators));
833        $value = Term::constant(1);
834        $a = new State();
835        $a->registers = ['a' => $value, 'b' => $value];
836        $b = new State();
837        $b->registers = ['a' => Term::constant(1), 'b' => Term::constant(1)];
838        self::assertSame($join->fingerprint($a), $join->fingerprint($b));
839        $a->registers['array'] = Term::fromNative(['a' => 1, 'b' => 2]);
840        $b->registers['array'] = Term::fromNative(['b' => 2, 'a' => 1]);
841        self::assertNotSame($join->fingerprint($a), $join->fingerprint($b));
842    }
843    public function testOrdersCoalescesPureCallsWithoutDroppingEffectAlternatives(): void
844    {
845        $source = '<?php function target(){$a=[1,2,3,4,5,6,7,8,9,10];for($i=0;$i<count($a);$i++){}return $i;}';
846        $symbol = 'target';
847        $input = new Sources([new Source('fixture.php', $source)]);
848        $configuration = new DeriverConfig();
849        $contributions = new Contributions();
850        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
851        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
852        $contributions1 = $providers->contributions;
853        $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);
854        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
855        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
856        $snapshotId = $identity->id;
857        $input1 = $providers->input;
858        $parser = new Parser(new ParserConfig($target->version));
859        $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));
860        $program = new Index($snapshotId, $trees);
861        $pure = (new QueryExecution($program, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program->diagnostics()))))->derive(new ReturnQuery($symbol));
862        self::assertCount(1, $pure->normalOutcomes);
863        self::assertSame(10, $pure->normalOutcomes[0]->values['return']->native());
864        self::assertSame([], $pure->frontiers);
865        $source2 = '<?php function target(){$i=1;return $i+++$i;}';
866        $symbol1 = 'target';
867        $input2 = new Sources([new Source('fixture.php', $source2)]);
868        $configuration1 = new DeriverConfig();
869        $contributions2 = new Contributions();
870        $target1 = new TargetProfile($configuration1->phpVersion, $configuration1->integerBits, $configuration1->floatPrecision);
871        $providers1 = new ProviderExpansion($input2, array_map(Term::fromNative(...), $configuration1->environment), $contributions2);
872        $contributions3 = $providers1->contributions;
873        $evaluators1 = new Evaluators(new TargetProfile($configuration1->phpVersion, $configuration1->integerBits, $configuration1->floatPrecision), new Registrations($contributions3->evaluators, $contributions3->providers, $contributions3->domains, $contributions3->intrinsics, $contributions3->stateSlots), $configuration1->standardEvaluators);
874        $settings1 = new EvaluatorConfig($target1, $providers1->environment, $configuration1->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration1->evaluators));
875        $identity1 = new SnapshotIdentity($providers1->input, $settings1, $evaluators1, $providers1->manifest);
876        $snapshotId1 = $identity1->id;
877        $input3 = $providers1->input;
878        $parser1 = new Parser(new ParserConfig($target1->version));
879        $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($input3, false));
880        $program1 = new Index($snapshotId1, $trees1);
881        $effects = (new QueryExecution($program1, new ExecutionEvaluatorConfig($target1, $providers1->environment, $configuration1->standardEvaluators), $evaluators1, $identity1->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics()))))->derive(new ReturnQuery($symbol1));
882        self::assertEqualsCanonicalizing([2, 3], array_map(static fn ($outcome) => $outcome->values['return']->native(), $effects->normalOutcomes));
883    }
884}
885