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