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