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