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

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Execution\Control;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\Evaluators;
10use Deriver\Deriver\Evaluator\Call\Extensions;
11use Deriver\Deriver\Evaluator\Call\Registrations;
12use Deriver\Deriver\Evaluator\Call\StateRegistry;
13use Deriver\Deriver\Evaluator\Domain\Lattice;
14use Deriver\Deriver\Evaluator\EvaluatorConfig;
15use Deriver\Deriver\Evaluator\Evidence\Frontier;
16use Deriver\Deriver\Evaluator\Execution\Completion\Alternative;
17use Deriver\Deriver\Evaluator\Execution\Completion\Exceptional;
18use Deriver\Deriver\Evaluator\Execution\Completion\StorageSnapshot;
19use Deriver\Deriver\Evaluator\Execution\Context;
20use Deriver\Deriver\Evaluator\Execution\Control\ObservationLimit;
21use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
22use Deriver\Deriver\Evaluator\Request\Budget;
23use Deriver\Deriver\Evaluator\Request\QueryScope;
24use Deriver\Deriver\Evaluator\Request\ReturnQuery;
25use Deriver\Deriver\Evaluator\Scalar\Identity;
26use Deriver\Deriver\Evaluator\TargetProfile;
27use Deriver\Deriver\Evaluator\Term;
28use Deriver\Deriver\Evaluator\TermKind;
29use Deriver\Deriver\Execution\QueryExecution;
30use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
31use Deriver\Deriver\Lowerer\Declaration\CallableSource;
32use Deriver\Deriver\Lowerer\Declaration\Index;
33use Deriver\Deriver\Lowerer\Declaration\Indexer;
34use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
35use Deriver\Deriver\MicroEvaluatorConversion;
36use Deriver\Deriver\Parser\Node\File;
37use Deriver\Deriver\Parser\Parser;
38use Deriver\Deriver\Parser\ParserConfig;
39use Deriver\Deriver\Parser\TargetSyntax;
40use Deriver\Deriver\Providers\ProviderExpansion;
41use Deriver\Deriver\Resolver\AssignmentPatterns;
42use Deriver\Deriver\Resolver\ClassScope;
43use Deriver\Deriver\Resolver\ExpressionValidation;
44use Deriver\Deriver\Resolver\MagicContext;
45use Deriver\Deriver\Resolver\Resolver;
46use Deriver\Deriver\SnapshotIdentity;
47use Deriver\Deriver\Sources\LineMap;
48use Deriver\Deriver\Sources\Path;
49use Deriver\Deriver\Sources\Source;
50use Deriver\Deriver\Sources\SourceRef;
51use Deriver\Deriver\Sources\Sources;
52use Deriver\Exception\ParserException;
53use Deriver\Exception\ResolutionException;
54use PHPUnit\Framework\Attributes\CoversClass;
55use PHPUnit\Framework\Attributes\Small;
56use PHPUnit\Framework\Attributes\UsesClass;
57use PHPUnit\Framework\TestCase;
58
59/**
60 * @covers \Deriver\Deriver\Evaluator\Execution\Control\ObservationLimit
61 */
62#[UsesClass(Resolver::class)]
63#[UsesClass(ExpressionValidation::class)]
64#[UsesClass(ResolutionException::class)]
65#[UsesClass(ParserConfig::class)]
66#[UsesClass(ParserException::class)]
67#[CoversClass(ObservationLimit::class)]
68#[UsesClass(CallableIdentity::class)]
69#[UsesClass(Context::class)]
70#[UsesClass(Extensions::class)]
71#[UsesClass(Evaluators::class)]
72#[UsesClass(StateRegistry::class)]
73#[UsesClass(DeriverConfig::class)]
74#[UsesClass(Sources::class)]
75#[UsesClass(Path::class)]
76#[UsesClass(Source::class)]
77#[UsesClass(TargetProfile::class)]
78#[UsesClass(Budget::class)]
79#[UsesClass(QueryScope::class)]
80#[UsesClass(ReturnQuery::class)]
81#[UsesClass(SourceRef::class)]
82#[UsesClass(Alternative::class)]
83#[UsesClass(Frontier::class)]
84#[UsesClass(StorageSnapshot::class)]
85#[UsesClass(Parser::class)]
86#[UsesClass(File::class)]
87#[UsesClass(CallableSource::class)]
88#[UsesClass(Indexer::class)]
89#[UsesClass(Index::class)]
90#[UsesClass(Composition::class)]
91#[UsesClass(LineMap::class)]
92#[UsesClass(MagicContext::class)]
93#[UsesClass(AssignmentPatterns::class)]
94#[UsesClass(ClassScope::class)]
95#[UsesClass(TargetSyntax::class)]
96#[UsesClass(Identity::class)]
97#[UsesClass(Lattice::class)]
98#[UsesClass(Term::class)]
99#[Small]
100final class ObservationLimitTest extends TestCase
101{
102    public function testEnforceRetainsValuesOutsideTheFirstPartition(): void
103    {
104        $source = '<?php function target(){return 1;}';
105        $budget = new Budget(partitions: 2);
106        $configuration = new DeriverConfig();
107        $contributions = new Contributions();
108        $input = new Sources([new Source('fixture.php', $source)]);
109        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
110        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
111        $contributions1 = $providers->contributions;
112        $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);
113        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
114        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
115        $snapshotId = $identity->id;
116        $input1 = $providers->input;
117        $parser = new Parser(new ParserConfig($target->version));
118        $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));
119        $program1 = new Index($snapshotId, $trees);
120        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
121        $snapshotId1 = 'test';
122        $input2 = new Sources([new Source('fixture.php', $source)]);
123        $profile = $execution->configuration->target;
124        $parser1 = new Parser(new ParserConfig($profile->version));
125        $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));
126        $program = new Index($snapshotId1, $trees1);
127        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
128        $context->normal = [new Alternative(['return' => Term::constant(1)]), new Alternative(['return' => Term::constant(2)]), new Alternative(['return' => Term::constant(99)])];
129        (new ObservationLimit($context))->enforce(new SourceRef('test', 'fixture.php', 0, 1));
130        self::assertCount(2, $context->normal);
131        self::assertSame(1, $context->normal[0]->values['return']->native());
132        self::assertTrue((new Lattice())->contains($context->normal[1]->values['return'], Term::constant(2)));
133        self::assertTrue((new Lattice())->contains($context->normal[1]->values['return'], Term::constant(99)));
134        self::assertCount(2, $context->frontiers);
135    }
136    public function testDistinctMergesOnlyIdenticalValuesStateAndStorage(): void
137    {
138        $source1 = '<?php function target(){return 1;}';
139        $budget = new Budget();
140        $configuration = new DeriverConfig();
141        $contributions = new Contributions();
142        $input = new Sources([new Source('fixture.php', $source1)]);
143        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
144        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
145        $contributions1 = $providers->contributions;
146        $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);
147        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
148        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
149        $snapshotId = $identity->id;
150        $input1 = $providers->input;
151        $parser = new Parser(new ParserConfig($target->version));
152        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
153        $program1 = new Index($snapshotId, $trees);
154        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
155        $snapshotId1 = 'test';
156        $input2 = new Sources([new Source('fixture.php', $source1)]);
157        $profile = $execution->configuration->target;
158        $parser1 = new Parser(new ParserConfig($profile->version));
159        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
160        $program = new Index($snapshotId1, $trees1);
161        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
162        $limit = new ObservationLimit($context);
163        $source = new SourceRef('test', 'fixture.php', 0, 1);
164        $outcomes = [new Alternative(['v' => Term::constant(1)], ['g' => true]), new Alternative(['v' => Term::constant(1)], ['g' => true]), new Alternative(['v' => Term::constant(1)], state: ['x' => Term::constant(2)])];
165        $distinct = $limit->distinct($outcomes, $source);
166        self::assertCount(2, $distinct);
167        self::assertSame(['g' => true], $distinct[0]->guard);
168        self::assertSame([], $context->frontiers);
169    }
170    public function testDistinctExceptionsKeepsDifferentExceptionClasses(): void
171    {
172        $source1 = '<?php function target(){return 1;}';
173        $budget = new Budget();
174        $configuration = new DeriverConfig();
175        $contributions = new Contributions();
176        $input = new Sources([new Source('fixture.php', $source1)]);
177        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
178        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
179        $contributions1 = $providers->contributions;
180        $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);
181        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
182        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
183        $snapshotId = $identity->id;
184        $input1 = $providers->input;
185        $parser = new Parser(new ParserConfig($target->version));
186        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
187        $program1 = new Index($snapshotId, $trees);
188        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
189        $snapshotId1 = 'test';
190        $input2 = new Sources([new Source('fixture.php', $source1)]);
191        $profile = $execution->configuration->target;
192        $parser1 = new Parser(new ParserConfig($profile->version));
193        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
194        $program = new Index($snapshotId1, $trees1);
195        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
196        $source = new SourceRef('test', 'fixture.php', 0, 1);
197        $outcomes = [new Exceptional(new Term(TermKind::Throwable, 'Error'), ['g' => true]), new Exceptional(new Term(TermKind::Throwable, 'Error'), ['g' => false]), new Exceptional(new Term(TermKind::Throwable, 'TypeError'))];
198        $distinct = (new ObservationLimit($context))->distinctExceptions($outcomes, $source);
199        self::assertSame(['Error', 'TypeError'], array_map(static fn (Exceptional $outcome): string|int|float|bool|null => $outcome->exception->literal, $distinct));
200        self::assertSame([], $distinct[0]->guard);
201        self::assertSame(['CORRELATION_RELAXED'], array_column(array_values($context->frontiers), 'code'));
202    }
203    public function testKeySeparatesValuesStateAndStorage(): void
204    {
205        $source = '<?php function target(){return 1;}';
206        $budget = new Budget();
207        $configuration = new DeriverConfig();
208        $contributions = new Contributions();
209        $input = new Sources([new Source('fixture.php', $source)]);
210        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
211        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
212        $contributions1 = $providers->contributions;
213        $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);
214        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
215        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
216        $snapshotId = $identity->id;
217        $input1 = $providers->input;
218        $parser = new Parser(new ParserConfig($target->version));
219        $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));
220        $program1 = new Index($snapshotId, $trees);
221        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
222        $snapshotId1 = 'test';
223        $input2 = new Sources([new Source('fixture.php', $source)]);
224        $profile = $execution->configuration->target;
225        $parser1 = new Parser(new ParserConfig($profile->version));
226        $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));
227        $program = new Index($snapshotId1, $trees1);
228        $limit = new ObservationLimit(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators));
229        $base = $limit->key(['v' => Term::constant(1)], [], new StorageSnapshot());
230        self::assertSame($base, $limit->key(['v' => Term::constant(1)], [], new StorageSnapshot()));
231        self::assertNotSame($base, $limit->key(['v' => Term::constant('1')], [], new StorageSnapshot()));
232        self::assertNotSame($base, $limit->key([], ['v' => Term::constant(1)], new StorageSnapshot()));
233        self::assertNotSame($base, $limit->key(['v' => Term::constant(1)], [], new StorageSnapshot(cells: ['c' => Term::constant(1)])));
234    }
235    public function testGuardKeepsSharedEntriesAndReportsRelaxation(): void
236    {
237        $source = new SourceRef('test', 'fixture.php', 0, 1);
238        $source1 = '<?php function target(){return 1;}';
239        $budget = new Budget();
240        $configuration = new DeriverConfig();
241        $contributions = new Contributions();
242        $input = new Sources([new Source('fixture.php', $source1)]);
243        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
244        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
245        $contributions1 = $providers->contributions;
246        $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);
247        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
248        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
249        $snapshotId = $identity->id;
250        $input1 = $providers->input;
251        $parser = new Parser(new ParserConfig($target->version));
252        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input1, false));
253        $program1 = new Index($snapshotId, $trees);
254        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
255        $snapshotId1 = 'test';
256        $input2 = new Sources([new Source('fixture.php', $source1)]);
257        $profile = $execution->configuration->target;
258        $parser1 = new Parser(new ParserConfig($profile->version));
259        $trees1 = array_map(static fn (Source $source3): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source3->path), $source3->contents, $source3->declarationsOnly))), iterator_to_array($input2, false));
260        $program = new Index($snapshotId1, $trees1);
261        $equal = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
262        self::assertSame(['a' => true], (new ObservationLimit($equal))->guard(['a' => true], ['a' => true], $source));
263        self::assertSame([], $equal->frontiers);
264        $source4 = '<?php function target(){return 1;}';
265        $budget1 = new Budget();
266        $configuration1 = new DeriverConfig();
267        $contributions2 = new Contributions();
268        $input3 = new Sources([new Source('fixture.php', $source4)]);
269        $target1 = new TargetProfile($configuration1->phpVersion, $configuration1->integerBits, $configuration1->floatPrecision);
270        $providers1 = new ProviderExpansion($input3, array_map(Term::fromNative(...), $configuration1->environment), $contributions2);
271        $contributions3 = $providers1->contributions;
272        $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);
273        $settings1 = new EvaluatorConfig($target1, $providers1->environment, $configuration1->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration1->evaluators));
274        $identity1 = new SnapshotIdentity($providers1->input, $settings1, $evaluators1, $providers1->manifest);
275        $snapshotId2 = $identity1->id;
276        $input4 = $providers1->input;
277        $parser2 = new Parser(new ParserConfig($target1->version));
278        $trees2 = array_map(static fn (Source $source5): File => (new Resolver())->resolve($parser2->parse(new Source(Path::normalize($source5->path), $source5->contents, $source5->declarationsOnly))), iterator_to_array($input4, false));
279        $program3 = new Index($snapshotId2, $trees2);
280        $execution1 = new QueryExecution($program3, new ExecutionEvaluatorConfig($target1, $providers1->environment, $configuration1->standardEvaluators), $evaluators1, $identity1->snapshot(array_map(Frontier::fromDiagnostic(...), $program3->diagnostics())));
281        $snapshotId3 = 'test';
282        $input5 = new Sources([new Source('fixture.php', $source4)]);
283        $profile1 = $execution1->configuration->target;
284        $parser3 = new Parser(new ParserConfig($profile1->version));
285        $trees3 = array_map(static fn (Source $source6): File => (new Resolver())->resolve($parser3->parse(new Source(Path::normalize($source6->path), $source6->contents, $source6->declarationsOnly))), iterator_to_array($input5, false));
286        $program2 = new Index($snapshotId3, $trees3);
287        $different = new Context($program2, new ReturnQuery('target', budget: $budget1), $execution1->configuration, $execution1->evaluators);
288        self::assertSame(['a' => true], (new ObservationLimit($different))->guard(['a' => true, 'b' => true], ['a' => true, 'b' => false], $source));
289        self::assertSame(['CORRELATION_RELAXED'], array_column(array_values($different->frontiers), 'code'));
290    }
291    public function testEnforceMergesRepeatedOutcomesBeforeJoining(): void
292    {
293        $source = '<?php function target(){return 1;}';
294        $budget = new Budget(partitions: 2);
295        $configuration = new DeriverConfig();
296        $contributions = new Contributions();
297        $input = new Sources([new Source('fixture.php', $source)]);
298        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
299        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
300        $contributions1 = $providers->contributions;
301        $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);
302        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
303        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
304        $snapshotId = $identity->id;
305        $input1 = $providers->input;
306        $parser = new Parser(new ParserConfig($target->version));
307        $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));
308        $program1 = new Index($snapshotId, $trees);
309        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
310        $snapshotId1 = 'test';
311        $input2 = new Sources([new Source('fixture.php', $source)]);
312        $profile = $execution->configuration->target;
313        $parser1 = new Parser(new ParserConfig($profile->version));
314        $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));
315        $program = new Index($snapshotId1, $trees1);
316        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
317        $context->normal = [new Alternative(['return' => Term::constant(1)], ['a' => true, 'b' => true], evidence: ['x']), new Alternative(['return' => Term::constant(2)]), new Alternative(['return' => Term::constant(1)], ['a' => true, 'b' => false], evidence: ['y'])];
318        (new ObservationLimit($context))->enforce(new SourceRef('test', 'fixture.php', 0, 1));
319        self::assertSame([1, 2], array_map(static fn (Alternative $outcome): string|int|float|bool|null => $outcome->values['return']->literal, $context->normal));
320        self::assertSame(['a' => true], $context->normal[0]->guard);
321        self::assertSame(['x', 'y'], $context->normal[0]->evidence);
322        self::assertSame(['CORRELATION_RELAXED'], array_column(array_values($context->frontiers), 'code'));
323    }
324    public function testEnforceKeepsIdenticalOutcomesWithinTheBudget(): void
325    {
326        $source = '<?php function target(){return 1;}';
327        $budget = new Budget(partitions: 2);
328        $configuration = new DeriverConfig();
329        $contributions = new Contributions();
330        $input = new Sources([new Source('fixture.php', $source)]);
331        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
332        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
333        $contributions1 = $providers->contributions;
334        $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);
335        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
336        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
337        $snapshotId = $identity->id;
338        $input1 = $providers->input;
339        $parser = new Parser(new ParserConfig($target->version));
340        $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));
341        $program1 = new Index($snapshotId, $trees);
342        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
343        $snapshotId1 = 'test';
344        $input2 = new Sources([new Source('fixture.php', $source)]);
345        $profile = $execution->configuration->target;
346        $parser1 = new Parser(new ParserConfig($profile->version));
347        $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));
348        $program = new Index($snapshotId1, $trees1);
349        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
350        $context->normal = [new Alternative(['return' => Term::constant(1)]), new Alternative(['return' => Term::constant(1)])];
351        (new ObservationLimit($context))->enforce(new SourceRef('test', 'fixture.php', 0, 1));
352        self::assertCount(2, $context->normal);
353        self::assertSame([], $context->frontiers);
354    }
355    public function testJoinPreservesEqualFieldsAndIncludesAbsentFields(): void
356    {
357        $source = '<?php function target(){return 1;}';
358        $budget = new Budget();
359        $configuration = new DeriverConfig();
360        $contributions = new Contributions();
361        $input = new Sources([new Source('fixture.php', $source)]);
362        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
363        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
364        $contributions1 = $providers->contributions;
365        $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);
366        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
367        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
368        $snapshotId = $identity->id;
369        $input1 = $providers->input;
370        $parser = new Parser(new ParserConfig($target->version));
371        $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));
372        $program1 = new Index($snapshotId, $trees);
373        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
374        $snapshotId1 = 'test';
375        $input2 = new Sources([new Source('fixture.php', $source)]);
376        $profile = $execution->configuration->target;
377        $parser1 = new Parser(new ParserConfig($profile->version));
378        $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));
379        $program = new Index($snapshotId1, $trees1);
380        $join = new ObservationLimit(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators));
381        $values = $join->join([['stable' => Term::constant('x'), 'optional' => Term::constant(2)], ['stable' => Term::constant('x')]]);
382        self::assertSame('x', $values['stable']->native());
383        self::assertSame('mixed', $values['optional']->attributes['type']);
384    }
385    public function testBoundaryRecordsBudgetAndCorrelationSeparately(): void
386    {
387        $source = '<?php function target(){return 1;}';
388        $budget = new Budget();
389        $configuration = new DeriverConfig();
390        $contributions = new Contributions();
391        $input = new Sources([new Source('fixture.php', $source)]);
392        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
393        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
394        $contributions1 = $providers->contributions;
395        $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);
396        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
397        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
398        $snapshotId = $identity->id;
399        $input1 = $providers->input;
400        $parser = new Parser(new ParserConfig($target->version));
401        $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));
402        $program1 = new Index($snapshotId, $trees);
403        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
404        $snapshotId1 = 'test';
405        $input2 = new Sources([new Source('fixture.php', $source)]);
406        $profile = $execution->configuration->target;
407        $parser1 = new Parser(new ParserConfig($profile->version));
408        $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));
409        $program = new Index($snapshotId1, $trees1);
410        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
411        (new ObservationLimit($context))->boundary(new SourceRef('test', 'fixture.php', 0, 1));
412        self::assertSame(['BUDGET_EXCEEDED', 'CORRELATION_RELAXED'], array_column(array_values($context->frontiers), 'code'));
413    }
414    public function testStorageWidensDifferentHeapFieldsAndKeepsCommonAliases(): void
415    {
416        $a = new StorageSnapshot(['x' => new Term(TermKind::Cell, 'a')], ['a' => Term::constant(1)]);
417        $b = new StorageSnapshot(['x' => new Term(TermKind::Cell, 'a')], ['a' => Term::constant(2)]);
418        $source = '<?php function target(){return 1;}';
419        $budget = new Budget();
420        $configuration = new DeriverConfig();
421        $contributions = new Contributions();
422        $input = new Sources([new Source('fixture.php', $source)]);
423        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
424        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
425        $contributions1 = $providers->contributions;
426        $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);
427        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
428        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
429        $snapshotId = $identity->id;
430        $input1 = $providers->input;
431        $parser = new Parser(new ParserConfig($target->version));
432        $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));
433        $program1 = new Index($snapshotId, $trees);
434        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
435        $snapshotId1 = 'test';
436        $input2 = new Sources([new Source('fixture.php', $source)]);
437        $profile = $execution->configuration->target;
438        $parser1 = new Parser(new ParserConfig($profile->version));
439        $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));
440        $program = new Index($snapshotId1, $trees1);
441        $joined = (new ObservationLimit(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators)))->storage([$a, $b]);
442        self::assertSame('a', $joined->bindings['x']->literal);
443        self::assertNotSame(TermKind::Constant, $joined->cells['a']->kind);
444    }
445}
446