packages/deriver/tests/Unit/Deriver/Evaluator/Execution/Summary/IsolationTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Execution\Summary;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\BoundInstruction;
10use Deriver\Deriver\Evaluator\Call\CallEvaluator;
11use Deriver\Deriver\Evaluator\Call\Evaluators;
12use Deriver\Deriver\Evaluator\Call\Extensions;
13use Deriver\Deriver\Evaluator\Call\Registrations;
14use Deriver\Deriver\Evaluator\Call\StateRegistry;
15use Deriver\Deriver\Evaluator\EvaluatorConfig;
16use Deriver\Deriver\Evaluator\Evidence\Frontier;
17use Deriver\Deriver\Evaluator\Execution\BatchObservations;
18use Deriver\Deriver\Evaluator\Execution\Context;
19use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
20use Deriver\Deriver\Evaluator\Execution\Summary\Isolation;
21use Deriver\Deriver\Evaluator\Request\Budget;
22use Deriver\Deriver\Evaluator\Request\QueryScope;
23use Deriver\Deriver\Evaluator\Request\ReturnQuery;
24use Deriver\Deriver\Evaluator\TargetProfile;
25use Deriver\Deriver\Evaluator\Term;
26use Deriver\Deriver\Evaluator\TermKind;
27use Deriver\Deriver\Execution\QueryExecution;
28use Deriver\Deriver\Lowerer\AssignmentLowering;
29use Deriver\Deriver\Lowerer\CallLowering;
30use Deriver\Deriver\Lowerer\Control\DestructuringLowering;
31use Deriver\Deriver\Lowerer\Control\ExceptionLowering;
32use Deriver\Deriver\Lowerer\Control\StaticLowering;
33use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
34use Deriver\Deriver\Lowerer\Declaration\CallableSource;
35use Deriver\Deriver\Lowerer\Declaration\ConstantSignatures;
36use Deriver\Deriver\Lowerer\Declaration\Index;
37use Deriver\Deriver\Lowerer\Declaration\Indexer;
38use Deriver\Deriver\Lowerer\Declaration\Metadata\ClassDeclaration;
39use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
40use Deriver\Deriver\Lowerer\EffectInspection;
41use Deriver\Deriver\Lowerer\ExpressionLowering;
42use Deriver\Deriver\Lowerer\Graph\BasicBlock;
43use Deriver\Deriver\Lowerer\Graph\CallableGraph;
44use Deriver\Deriver\Lowerer\Graph\CatchTarget;
45use Deriver\Deriver\Lowerer\Graph\ExceptionRegion;
46use Deriver\Deriver\Lowerer\Graph\Instruction;
47use Deriver\Deriver\Lowerer\Graph\Parameter;
48use Deriver\Deriver\Lowerer\Graph\Terminator;
49use Deriver\Deriver\Lowerer\Graph\TerminatorKind;
50use Deriver\Deriver\Lowerer\InstructionEmitter;
51use Deriver\Deriver\Lowerer\Lowerer;
52use Deriver\Deriver\Lowerer\LoweringScope;
53use Deriver\Deriver\Lowerer\StatementLowering;
54use Deriver\Deriver\MicroEvaluatorConversion;
55use Deriver\Deriver\Parser\Node\File;
56use Deriver\Deriver\Parser\Parser;
57use Deriver\Deriver\Parser\ParserConfig;
58use Deriver\Deriver\Parser\TargetSyntax;
59use Deriver\Deriver\Providers\ProviderExpansion;
60use Deriver\Deriver\Resolver\AssignmentPatterns;
61use Deriver\Deriver\Resolver\ClassScope;
62use Deriver\Deriver\Resolver\ExpressionValidation;
63use Deriver\Deriver\Resolver\MagicContext;
64use Deriver\Deriver\Resolver\Resolver;
65use Deriver\Deriver\SnapshotIdentity;
66use Deriver\Deriver\Sources\LineMap;
67use Deriver\Deriver\Sources\Path;
68use Deriver\Deriver\Sources\Source;
69use Deriver\Deriver\Sources\SourceRef;
70use Deriver\Deriver\Sources\Sources;
71use Deriver\Exception\ParserException;
72use Deriver\Exception\ResolutionException;
73use PHPUnit\Framework\Attributes\CoversClass;
74use PHPUnit\Framework\Attributes\DataProvider;
75use PHPUnit\Framework\Attributes\Small;
76use PHPUnit\Framework\Attributes\UsesClass;
77use PHPUnit\Framework\TestCase;
78
79/**
80 * @covers \Deriver\Deriver\Evaluator\Execution\Summary\Isolation
81 */
82#[UsesClass(Resolver::class)]
83#[UsesClass(ExpressionValidation::class)]
84#[UsesClass(ResolutionException::class)]
85#[UsesClass(ParserConfig::class)]
86#[UsesClass(ParserException::class)]
87#[CoversClass(Isolation::class)]
88#[UsesClass(BasicBlock::class)]
89#[UsesClass(CallableGraph::class)]
90#[UsesClass(CallableIdentity::class)]
91#[UsesClass(CatchTarget::class)]
92#[UsesClass(ClassDeclaration::class)]
93#[UsesClass(ExceptionRegion::class)]
94#[UsesClass(Instruction::class)]
95#[UsesClass(Parameter::class)]
96#[UsesClass(Terminator::class)]
97#[UsesClass(Context::class)]
98#[UsesClass(Extensions::class)]
99#[UsesClass(Evaluators::class)]
100#[UsesClass(StateRegistry::class)]
101#[UsesClass(DeriverConfig::class)]
102#[UsesClass(Sources::class)]
103#[UsesClass(Path::class)]
104#[UsesClass(Source::class)]
105#[UsesClass(TargetProfile::class)]
106#[UsesClass(Budget::class)]
107#[UsesClass(QueryScope::class)]
108#[UsesClass(ReturnQuery::class)]
109#[UsesClass(SourceRef::class)]
110#[UsesClass(Parser::class)]
111#[UsesClass(File::class)]
112#[UsesClass(AssignmentLowering::class)]
113#[UsesClass(CallLowering::class)]
114#[UsesClass(Lowerer::class)]
115#[UsesClass(DestructuringLowering::class)]
116#[UsesClass(ExceptionLowering::class)]
117#[UsesClass(StaticLowering::class)]
118#[UsesClass(EffectInspection::class)]
119#[UsesClass(ExpressionLowering::class)]
120#[UsesClass(InstructionEmitter::class)]
121#[UsesClass(LoweringScope::class)]
122#[UsesClass(StatementLowering::class)]
123#[UsesClass(ConstantSignatures::class)]
124#[UsesClass(CallableSource::class)]
125#[UsesClass(Indexer::class)]
126#[UsesClass(Index::class)]
127#[UsesClass(Composition::class)]
128#[UsesClass(LineMap::class)]
129#[UsesClass(MagicContext::class)]
130#[UsesClass(AssignmentPatterns::class)]
131#[UsesClass(ClassScope::class)]
132#[UsesClass(TargetSyntax::class)]
133#[UsesClass(Term::class)]
134#[Small]
135final class IsolationTest extends TestCase
136{
137    public function testCallableChecksTheWholeMutuallyRecursiveComponent(): void
138    {
139        $source = '<?php function target(){return hop();}function hop(){return target();}';
140        $budget = new Budget();
141        $configuration = new DeriverConfig();
142        $contributions = new Contributions();
143        $input = new Sources([new Source('fixture.php', $source)]);
144        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
145        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
146        $contributions1 = $providers->contributions;
147        $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);
148        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
149        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
150        $snapshotId = $identity->id;
151        $input1 = $providers->input;
152        $parser = new Parser(new ParserConfig($target->version));
153        $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));
154        $program1 = new Index($snapshotId, $trees);
155        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
156        $snapshotId1 = 'test';
157        $input2 = new Sources([new Source('fixture.php', $source)]);
158        $profile = $execution->configuration->target;
159        $parser1 = new Parser(new ParserConfig($profile->version));
160        $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));
161        $program = new Index($snapshotId1, $trees1);
162        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
163        $proof = new Isolation($context);
164        $symbol = 'target';
165        $fixtureCallable1 = $context->program->callable($symbol);
166        self::assertNotNull($fixtureCallable1);
167        self::assertTrue($proof->callable($fixtureCallable1));
168    }
169    public function testCallableRejectsTransitiveGlobalMutation(): void
170    {
171        $source = '<?php function target(){return hop();}function hop(){global $x;$x=2;return target();}';
172        $budget = new Budget();
173        $configuration = new DeriverConfig();
174        $contributions = new Contributions();
175        $input = new Sources([new Source('fixture.php', $source)]);
176        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
177        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
178        $contributions1 = $providers->contributions;
179        $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);
180        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
181        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
182        $snapshotId = $identity->id;
183        $input1 = $providers->input;
184        $parser = new Parser(new ParserConfig($target->version));
185        $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));
186        $program1 = new Index($snapshotId, $trees);
187        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
188        $snapshotId1 = 'test';
189        $input2 = new Sources([new Source('fixture.php', $source)]);
190        $profile = $execution->configuration->target;
191        $parser1 = new Parser(new ParserConfig($profile->version));
192        $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));
193        $program = new Index($snapshotId1, $trees1);
194        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
195        $symbol = 'target';
196        $fixtureCallable1 = $context->program->callable($symbol);
197        self::assertNotNull($fixtureCallable1);
198        self::assertFalse((new Isolation($context))->callable($fixtureCallable1));
199    }
200    public function testCallableRejectsReferencesAndUnknownCalls(): void
201    {
202        $source = '<?php function target(&$x){$x=2;}function unknown(){return missing();}';
203        $budget = new Budget();
204        $configuration = new DeriverConfig();
205        $contributions = new Contributions();
206        $input = new Sources([new Source('fixture.php', $source)]);
207        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
208        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
209        $contributions1 = $providers->contributions;
210        $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);
211        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
212        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
213        $snapshotId = $identity->id;
214        $input1 = $providers->input;
215        $parser = new Parser(new ParserConfig($target->version));
216        $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));
217        $program1 = new Index($snapshotId, $trees);
218        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
219        $snapshotId1 = 'test';
220        $input2 = new Sources([new Source('fixture.php', $source)]);
221        $profile = $execution->configuration->target;
222        $parser1 = new Parser(new ParserConfig($profile->version));
223        $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));
224        $program = new Index($snapshotId1, $trees1);
225        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
226        $proof = new Isolation($context);
227        $symbol = 'target';
228        $fixtureCallable1 = $context->program->callable($symbol);
229        self::assertNotNull($fixtureCallable1);
230        self::assertFalse($proof->callable($fixtureCallable1));
231        $symbol1 = 'unknown';
232        $fixtureCallable2 = $context->program->callable($symbol1);
233        self::assertNotNull($fixtureCallable2);
234        self::assertFalse($proof->callable($fixtureCallable2));
235    }
236    public function testValueRejectsReferencesHiddenInsideArrayCopies(): void
237    {
238        $source = '<?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', $source)]);
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 $source1): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->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', $source)]);
257        $profile = $execution->configuration->target;
258        $parser1 = new Parser(new ParserConfig($profile->version));
259        $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));
260        $program = new Index($snapshotId1, $trees1);
261        $proof = new Isolation(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators));
262        self::assertFalse($proof->value(Term::array([new Term(TermKind::Cell, 'shared')])));
263        self::assertFalse($proof->value(Term::parameter('unknown')));
264        self::assertTrue($proof->value(Term::array([Term::parameter('id', 'int')])));
265    }
266    public function testInstructionsRejectsExternalStateEvenIfNoReturnUsesIt(): void
267    {
268        $source = '<?php function target(){$unused=$_GET;return 1;}';
269        $budget = new Budget();
270        $configuration = new DeriverConfig();
271        $contributions = new Contributions();
272        $input = new Sources([new Source('fixture.php', $source)]);
273        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
274        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
275        $contributions1 = $providers->contributions;
276        $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);
277        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
278        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
279        $snapshotId = $identity->id;
280        $input1 = $providers->input;
281        $parser = new Parser(new ParserConfig($target->version));
282        $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));
283        $program1 = new Index($snapshotId, $trees);
284        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
285        $snapshotId1 = 'test';
286        $input2 = new Sources([new Source('fixture.php', $source)]);
287        $profile = $execution->configuration->target;
288        $parser1 = new Parser(new ParserConfig($profile->version));
289        $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));
290        $program = new Index($snapshotId1, $trees1);
291        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
292        $symbol = 'target';
293        $fixtureCallable1 = $context->program->callable($symbol);
294        self::assertNotNull($fixtureCallable1);
295        self::assertFalse((new Isolation($context))->instructions($fixtureCallable1, []));
296    }
297    public function testValuePreservesSharedGraphComplexity(): void
298    {
299        $depth = 64;
300        $leaf = Term::constant(1);
301        $leaf = array_reduce(array_fill(0, $depth, null), static fn (Term $leaf): Term => Term::array([$leaf, $leaf]), $leaf);
302        $value = $leaf;
303        $source = '<?php function target(){return 1;}';
304        $budget = new Budget();
305        $configuration = new DeriverConfig();
306        $contributions = new Contributions();
307        $input = new Sources([new Source('fixture.php', $source)]);
308        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
309        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
310        $contributions1 = $providers->contributions;
311        $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);
312        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
313        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
314        $snapshotId = $identity->id;
315        $input1 = $providers->input;
316        $parser = new Parser(new ParserConfig($target->version));
317        $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));
318        $program1 = new Index($snapshotId, $trees);
319        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
320        $snapshotId1 = 'test';
321        $input2 = new Sources([new Source('fixture.php', $source)]);
322        $profile = $execution->configuration->target;
323        $parser1 = new Parser(new ParserConfig($profile->version));
324        $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));
325        $program = new Index($snapshotId1, $trees1);
326        self::assertTrue((new Isolation(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators)))->value($value));
327    }
328    public function testCallableExcludesRuntimeErrorObjectAllocations(): void
329    {
330        $source = '<?php function target(){try{1/0;}catch(Error $e){return $e;}}';
331        $budget = new Budget();
332        $configuration = new DeriverConfig();
333        $contributions = new Contributions();
334        $input = new Sources([new Source('fixture.php', $source)]);
335        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
336        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
337        $contributions1 = $providers->contributions;
338        $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);
339        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
340        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
341        $snapshotId = $identity->id;
342        $input1 = $providers->input;
343        $parser = new Parser(new ParserConfig($target->version));
344        $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));
345        $program1 = new Index($snapshotId, $trees);
346        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
347        $snapshotId1 = 'test';
348        $input2 = new Sources([new Source('fixture.php', $source)]);
349        $profile = $execution->configuration->target;
350        $parser1 = new Parser(new ParserConfig($profile->version));
351        $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));
352        $program = new Index($snapshotId1, $trees1);
353        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
354        $body = $context->program->callable('target');
355        self::assertNotNull($body);
356        self::assertFalse((new Isolation($context))->callable($body));
357    }
358    public function testLocalRejectsReferenceBearingFrames(): void
359    {
360        $source = '<?php function target(&$x){return $x;}';
361        $budget = new Budget();
362        $configuration = new DeriverConfig();
363        $contributions = new Contributions();
364        $input = new Sources([new Source('fixture.php', $source)]);
365        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
366        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
367        $contributions1 = $providers->contributions;
368        $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);
369        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
370        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
371        $snapshotId = $identity->id;
372        $input1 = $providers->input;
373        $parser = new Parser(new ParserConfig($target->version));
374        $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));
375        $program1 = new Index($snapshotId, $trees);
376        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
377        $snapshotId1 = 'test';
378        $input2 = new Sources([new Source('fixture.php', $source)]);
379        $profile = $execution->configuration->target;
380        $parser1 = new Parser(new ParserConfig($profile->version));
381        $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));
382        $program = new Index($snapshotId1, $trees1);
383        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
384        $symbol = 'target';
385        $fixtureCallable1 = $context->program->callable($symbol);
386        self::assertNotNull($fixtureCallable1);
387        self::assertFalse((new Isolation($context))->local($fixtureCallable1));
388    }
389    public function testDependenciesStopsAnOptionalProofAtItsNodeBudget(): void
390    {
391        $source = '<?php function target(){return 1+2;}';
392        $budget = new Budget(nodes: 1);
393        $configuration = new DeriverConfig();
394        $contributions = new Contributions();
395        $input = new Sources([new Source('fixture.php', $source)]);
396        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
397        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
398        $contributions1 = $providers->contributions;
399        $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);
400        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
401        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
402        $snapshotId = $identity->id;
403        $input1 = $providers->input;
404        $parser = new Parser(new ParserConfig($target->version));
405        $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));
406        $program1 = new Index($snapshotId, $trees);
407        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
408        $snapshotId1 = 'test';
409        $input2 = new Sources([new Source('fixture.php', $source)]);
410        $profile = $execution->configuration->target;
411        $parser1 = new Parser(new ParserConfig($profile->version));
412        $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));
413        $program = new Index($snapshotId1, $trees1);
414        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
415        $work = 0;
416        $symbol = 'target';
417        $fixtureCallable1 = $context->program->callable($symbol);
418        self::assertNotNull($fixtureCallable1);
419        self::assertNull((new Isolation($context))->dependencies($fixtureCallable1, $work));
420        self::assertFalse($context->sealed);
421    }
422    public function testCallableChecksAnImpureSiblingAfterTraversingACycle(): void
423    {
424        $source = '<?php function target(){a();global $x;$x=1;}function a(){target();}';
425        $budget = new Budget();
426        $configuration = new DeriverConfig();
427        $contributions = new Contributions();
428        $input = new Sources([new Source('fixture.php', $source)]);
429        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
430        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
431        $contributions1 = $providers->contributions;
432        $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);
433        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
434        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
435        $snapshotId = $identity->id;
436        $input1 = $providers->input;
437        $parser = new Parser(new ParserConfig($target->version));
438        $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));
439        $program1 = new Index($snapshotId, $trees);
440        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
441        $snapshotId1 = 'test';
442        $input2 = new Sources([new Source('fixture.php', $source)]);
443        $profile = $execution->configuration->target;
444        $parser1 = new Parser(new ParserConfig($profile->version));
445        $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));
446        $program = new Index($snapshotId1, $trees1);
447        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
448        $symbol = 'target';
449        $fixtureCallable1 = $context->program->callable($symbol);
450        self::assertNotNull($fixtureCallable1);
451        self::assertFalse((new Isolation($context))->callable($fixtureCallable1));
452    }
453    public function testValueChecksDeepSharedInputsIteratively(): void
454    {
455        $depth = 2000;
456        $leaf = Term::constant(1);
457        $leaf = array_reduce(array_fill(0, $depth, null), static fn (Term $leaf): Term => Term::array([$leaf, $leaf]), $leaf);
458        $value = $leaf;
459        $source = '<?php function target(){return 1;}';
460        $budget = new Budget();
461        $configuration = new DeriverConfig();
462        $contributions = new Contributions();
463        $input = new Sources([new Source('fixture.php', $source)]);
464        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
465        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
466        $contributions1 = $providers->contributions;
467        $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);
468        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
469        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
470        $snapshotId = $identity->id;
471        $input1 = $providers->input;
472        $parser = new Parser(new ParserConfig($target->version));
473        $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));
474        $program1 = new Index($snapshotId, $trees);
475        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
476        $snapshotId1 = 'test';
477        $input2 = new Sources([new Source('fixture.php', $source)]);
478        $profile = $execution->configuration->target;
479        $parser1 = new Parser(new ParserConfig($profile->version));
480        $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));
481        $program = new Index($snapshotId1, $trees1);
482        self::assertTrue((new Isolation(new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators)))->value($value));
483    }
484    public function testValueAbandonsAnOversizedOptionalProof(): void
485    {
486        $depth = 10;
487        $leaf = Term::constant(1);
488        $leaf = array_reduce(array_fill(0, $depth, null), static fn (Term $leaf): Term => Term::array([$leaf, $leaf]), $leaf);
489        $value = $leaf;
490        $source = '<?php function target(){return 1;}';
491        $budget = new Budget(nodes: 2);
492        $configuration = new DeriverConfig();
493        $contributions = new Contributions();
494        $input = new Sources([new Source('fixture.php', $source)]);
495        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
496        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
497        $contributions1 = $providers->contributions;
498        $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);
499        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
500        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
501        $snapshotId = $identity->id;
502        $input1 = $providers->input;
503        $parser = new Parser(new ParserConfig($target->version));
504        $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));
505        $program1 = new Index($snapshotId, $trees);
506        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
507        $snapshotId1 = 'test';
508        $input2 = new Sources([new Source('fixture.php', $source)]);
509        $profile = $execution->configuration->target;
510        $parser1 = new Parser(new ParserConfig($profile->version));
511        $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));
512        $program = new Index($snapshotId1, $trees1);
513        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
514        self::assertFalse((new Isolation($context))->value($value));
515        self::assertSame([], $context->frontiers);
516    }
517    #[DataProvider('providerFrameIsolation')]
518    public function testLocalRequiresAnIndependentInvocationFrame(string $source, string $symbol, bool $expected): void
519    {
520        $budget = new Budget();
521        $configuration = new DeriverConfig();
522        $contributions = new Contributions();
523        $input = new Sources([new Source('fixture.php', $source)]);
524        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
525        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
526        $contributions1 = $providers->contributions;
527        $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);
528        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
529        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
530        $snapshotId = $identity->id;
531        $input1 = $providers->input;
532        $parser = new Parser(new ParserConfig($target->version));
533        $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));
534        $program1 = new Index($snapshotId, $trees);
535        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
536        $snapshotId1 = 'test';
537        $input2 = new Sources([new Source('fixture.php', $source)]);
538        $profile = $execution->configuration->target;
539        $parser1 = new Parser(new ParserConfig($profile->version));
540        $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));
541        $program = new Index($snapshotId1, $trees1);
542        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
543        $body = $context->program->callable($symbol);
544        self::assertNotNull($body);
545        self::assertSame($expected, (new Isolation($context))->local($body));
546    }
547    /**
548     * @return iterable<string,array{string,string,bool}>
549     */
550    public static function providerFrameIsolation(): iterable
551    {
552        yield 'ordinary function' => ['<?php function target($x){return $x;}', 'target', true];
553        yield 'reference return' => ['<?php function &target(){static $x;return $x;}', 'target', false];
554        yield 'reference parameter' => ['<?php function target($x,&$y){return $x;}', 'target', false];
555        yield 'instance method' => ['<?php class Box{function target(){return 1;}}', 'Box::target', false];
556        yield 'static method' => ['<?php class Box{static function target(){return 1;}}', 'Box::target', false];
557        yield 'catch with variable' => ['<?php function target(){try{return 1;}catch(Error $e){return 2;}}', 'target', false];
558        yield 'catch without variable' => ['<?php function target(){try{return 1;}catch(Error){return 2;}}', 'target', true];
559    }
560    public function testLocalRejectsCapturedVariablesScriptFramesAndModeledBodies(): void
561    {
562        $source1 = '<?php function target(){return 1;}';
563        $budget = new Budget();
564        $configuration = new DeriverConfig();
565        $contributions = new Contributions();
566        $input = new Sources([new Source('fixture.php', $source1)]);
567        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
568        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
569        $contributions1 = $providers->contributions;
570        $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);
571        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
572        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
573        $snapshotId = $identity->id;
574        $input1 = $providers->input;
575        $parser = new Parser(new ParserConfig($target->version));
576        $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));
577        $program1 = new Index($snapshotId, $trees);
578        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
579        $snapshotId1 = 'test';
580        $input2 = new Sources([new Source('fixture.php', $source1)]);
581        $profile = $execution->configuration->target;
582        $parser1 = new Parser(new ParserConfig($profile->version));
583        $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));
584        $program = new Index($snapshotId1, $trees1);
585        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
586        $source = new SourceRef('test', 'a.php', 0, 1);
587        $proof = new Isolation($context);
588        self::assertFalse($proof->local(new CallableGraph('closure:a', [], [], $source, captures: ['value' => false])));
589        self::assertFalse($proof->local(new CallableGraph('script:a.php', [], [], $source)));
590        $context->evaluators->evaluators['target'] = self::createStub(CallEvaluator::class);
591        self::assertFalse($proof->local(new CallableGraph('TARGET', [], [], $source)));
592    }
593    #[DataProvider('providerIsolatedValues')]
594    public function testValueRequiresEveryNestedOperandToBeIndependent(Term $value, bool $expected): void
595    {
596        $source = '<?php function target(){return 1;}';
597        $budget = new Budget();
598        $configuration = new DeriverConfig();
599        $contributions = new Contributions();
600        $input = new Sources([new Source('fixture.php', $source)]);
601        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
602        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
603        $contributions1 = $providers->contributions;
604        $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);
605        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
606        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
607        $snapshotId = $identity->id;
608        $input1 = $providers->input;
609        $parser = new Parser(new ParserConfig($target->version));
610        $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));
611        $program1 = new Index($snapshotId, $trees);
612        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
613        $snapshotId1 = 'test';
614        $input2 = new Sources([new Source('fixture.php', $source)]);
615        $profile = $execution->configuration->target;
616        $parser1 = new Parser(new ParserConfig($profile->version));
617        $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));
618        $program = new Index($snapshotId1, $trees1);
619        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
620        $proof = new Isolation($context);
621        self::assertSame($expected, $proof->value($value));
622        self::assertSame($expected, $proof->value(Term::array([$value, $value])));
623        self::assertSame([], $context->frontiers);
624        self::assertFalse($context->sealed);
625    }
626    /**
627     * @return iterable<string,array{Term,bool}>
628     */
629    public static function providerIsolatedValues(): iterable
630    {
631        yield 'null' => [Term::constant(null), true];
632        yield 'uninitialized' => [new Term(TermKind::Uninitialized), true];
633        yield 'runtime error token' => [new Term(TermKind::Throwable, 'Error'), true];
634        yield 'closed array' => [Term::fromNative(['x' => [1, 2]]), true];
635        yield 'open array' => [Term::array([], true), false];
636        yield 'cell' => [new Term(TermKind::Cell, 'shared'), false];
637        yield 'object with scalar-looking bound' => [new Term(TermKind::Object, 'a', attributes: ['type' => 'int']), false];
638        yield 'closure with scalar-looking bound' => [new Term(TermKind::Closure, 'a', attributes: ['type' => 'string']), false];
639        yield 'domain with scalar-looking bound' => [new Term(TermKind::Domain, 'a', attributes: ['type' => 'bool']), false];
640        yield 'scalar union' => [new Term(TermKind::Input, 'a', attributes: ['type' => 'int|float|string|bool|true|false|null']), true];
641        yield 'object union' => [new Term(TermKind::Input, 'a', attributes: ['type' => 'int|Box']), false];
642        yield 'missing bound' => [new Term(TermKind::Input, 'a'), false];
643        yield 'nonstring bound' => [new Term(TermKind::Input, 'a', attributes: ['type' => 12]), false];
644        yield 'unsafe operand under scalar expression' => [new Term(TermKind::Binary, '+', [new Term(TermKind::Cell, 'shared')], ['type' => 'int']), false];
645        yield 'safe scalar expression' => [new Term(TermKind::Binary, '+', [Term::constant(1), Term::constant(2)], ['type' => 'int']), true];
646    }
647    #[DataProvider('providerValueBudget')]
648    public function testValueCountsUniqueNodesAtTheExactProofBudget(int $limit, bool $expected): void
649    {
650        $leaf = Term::constant(1);
651        $value = Term::array([$leaf, $leaf]);
652        $source = '<?php function target(){return 1;}';
653        $budget = new Budget(nodes: $limit);
654        $configuration = new DeriverConfig();
655        $contributions = new Contributions();
656        $input = new Sources([new Source('fixture.php', $source)]);
657        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
658        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
659        $contributions1 = $providers->contributions;
660        $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);
661        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
662        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
663        $snapshotId = $identity->id;
664        $input1 = $providers->input;
665        $parser = new Parser(new ParserConfig($target->version));
666        $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));
667        $program1 = new Index($snapshotId, $trees);
668        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
669        $snapshotId1 = 'test';
670        $input2 = new Sources([new Source('fixture.php', $source)]);
671        $profile = $execution->configuration->target;
672        $parser1 = new Parser(new ParserConfig($profile->version));
673        $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));
674        $program = new Index($snapshotId1, $trees1);
675        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
676        self::assertSame($expected, (new Isolation($context))->value($value));
677        self::assertFalse($context->sealed);
678        self::assertSame([], $context->frontiers);
679    }
680    /**
681     * @return iterable<string,array{int,bool}>
682     */
683    public static function providerValueBudget(): iterable
684    {
685        yield 'one short' => [1, false];
686        yield 'exactly two unique nodes' => [2, true];
687        yield 'spare node' => [3, true];
688    }
689    public function testDependenciesIncludesDefaultInitializersAndEveryResolvedCallee(): void
690    {
691        $source = '<?php function first(){return 1;}function second(){return 2;}function target($a=3){$x=first();$y=second();return $x+$y;}';
692        $budget = new Budget();
693        $configuration = new DeriverConfig();
694        $contributions = new Contributions();
695        $input = new Sources([new Source('fixture.php', $source)]);
696        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
697        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
698        $contributions1 = $providers->contributions;
699        $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);
700        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
701        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
702        $snapshotId = $identity->id;
703        $input1 = $providers->input;
704        $parser = new Parser(new ParserConfig($target->version));
705        $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));
706        $program1 = new Index($snapshotId, $trees);
707        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
708        $snapshotId1 = 'test';
709        $input2 = new Sources([new Source('fixture.php', $source)]);
710        $profile = $execution->configuration->target;
711        $parser1 = new Parser(new ParserConfig($profile->version));
712        $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));
713        $program = new Index($snapshotId1, $trees1);
714        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
715        $body = $context->program->callable('target');
716        self::assertNotNull($body);
717        $work = 0;
718        $dependencies = (new Isolation($context))->dependencies($body, $work);
719        self::assertNotNull($dependencies);
720        self::assertCount(3, $dependencies);
721        self::assertSame($body->parameters[0]->default, $dependencies[0]);
722        self::assertSame(['first', 'second'], array_map(static fn (CallableGraph $callee): string => $callee->symbol, array_slice($dependencies, 1)));
723        self::assertGreaterThan(0, $work);
724    }
725    #[DataProvider('providerInstructionBoundary')]
726    public function testDependenciesHonorsTheRemainingInstructionBudget(int $alreadyUsed, bool $expected): void
727    {
728        $source1 = '<?php function target(){return 1;}';
729        $budget = new Budget(nodes: 2);
730        $configuration = new DeriverConfig();
731        $contributions = new Contributions();
732        $input = new Sources([new Source('fixture.php', $source1)]);
733        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
734        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
735        $contributions1 = $providers->contributions;
736        $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);
737        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
738        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
739        $snapshotId = $identity->id;
740        $input1 = $providers->input;
741        $parser = new Parser(new ParserConfig($target->version));
742        $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));
743        $program1 = new Index($snapshotId, $trees);
744        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
745        $snapshotId1 = 'test';
746        $input2 = new Sources([new Source('fixture.php', $source1)]);
747        $profile = $execution->configuration->target;
748        $parser1 = new Parser(new ParserConfig($profile->version));
749        $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));
750        $program = new Index($snapshotId1, $trees1);
751        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
752        $source = new SourceRef('test', 'a.php', 0, 1);
753        $body = new CallableGraph('target', [], [0 => new BasicBlock(0, [new BoundInstruction('i', 'constant', $source, 'v', constant: Term::constant(1))], new Terminator(TerminatorKind::Return, 'v'))], $source);
754        $work = $alreadyUsed;
755        self::assertSame($expected ? [] : null, (new Isolation($context))->dependencies($body, $work));
756        self::assertSame($alreadyUsed + 1, $work);
757        self::assertFalse($context->sealed);
758        self::assertSame([], $context->frontiers);
759    }
760    /**
761     * @return iterable<string,array{int,bool}>
762     */
763    public static function providerInstructionBoundary(): iterable
764    {
765        yield 'below limit' => [0, true];
766        yield 'exact limit' => [1, true];
767        yield 'over limit' => [2, false];
768    }
769    public function testInstructionsChecksTransitiveDependenciesAfterResolvingNames(): void
770    {
771        $source = '<?php function first(){return second();}function second(){global $x;return $x;}function target(){return first();}';
772        $budget = new Budget();
773        $configuration = new DeriverConfig();
774        $contributions = new Contributions();
775        $input = new Sources([new Source('fixture.php', $source)]);
776        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
777        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
778        $contributions1 = $providers->contributions;
779        $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);
780        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
781        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
782        $snapshotId = $identity->id;
783        $input1 = $providers->input;
784        $parser = new Parser(new ParserConfig($target->version));
785        $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));
786        $program1 = new Index($snapshotId, $trees);
787        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
788        $snapshotId1 = 'test';
789        $input2 = new Sources([new Source('fixture.php', $source)]);
790        $profile = $execution->configuration->target;
791        $parser1 = new Parser(new ParserConfig($profile->version));
792        $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));
793        $program = new Index($snapshotId1, $trees1);
794        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
795        $body = $context->program->callable('target');
796        self::assertNotNull($body);
797        self::assertFalse((new Isolation($context))->instructions($body, []));
798        self::assertFalse($context->sealed);
799    }
800    public function testObservedExcludesBatchReturnOwnersFromSummaryReplay(): void
801    {
802        $source = '<?php function target(){return 1;}';
803        $budget = new Budget();
804        $configuration = new DeriverConfig();
805        $contributions = new Contributions();
806        $input = new Sources([new Source('fixture.php', $source)]);
807        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
808        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
809        $contributions1 = $providers->contributions;
810        $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);
811        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
812        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
813        $snapshotId = $identity->id;
814        $input1 = $providers->input;
815        $parser = new Parser(new ParserConfig($target->version));
816        $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));
817        $program1 = new Index($snapshotId, $trees);
818        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
819        $snapshotId1 = 'test';
820        $input2 = new Sources([new Source('fixture.php', $source)]);
821        $profile = $execution->configuration->target;
822        $parser1 = new Parser(new ParserConfig($profile->version));
823        $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));
824        $program = new Index($snapshotId1, $trees1);
825        $context = new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators);
826        $body = $context->program->callable('target');
827        self::assertNotNull($body);
828        $proof = new Isolation($context);
829        self::assertFalse($proof->observed($body));
830        $context->batch = new BatchObservations([$context->query], $context);
831        self::assertTrue($proof->observed($body));
832        $context->batch = null;
833    }
834}
835