packages/deriver/tests/Unit/Deriver/Evaluator/DemandTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator;
6
7use Deriver\Deriver\Candidate;
8use Deriver\Deriver\Candidate\CandidateStatus;
9use Deriver\Deriver\Contributions;
10use Deriver\Deriver\DeriverConfig;
11use Deriver\Deriver\Evaluator\Call\Evaluators;
12use Deriver\Deriver\Evaluator\Call\Registrations;
13use Deriver\Deriver\Evaluator\EvaluatorConfig;
14use Deriver\Deriver\Evaluator\Evidence\Frontier;
15use Deriver\Deriver\Evaluator\Index\DefinitionIndex;
16use Deriver\Deriver\Evaluator\Projection;
17use Deriver\Deriver\Evaluator\Request\Budget;
18use Deriver\Deriver\Evaluator\Request\EntryPoint;
19use Deriver\Deriver\Evaluator\Request\QueryScope;
20use Deriver\Deriver\Evaluator\Request\ReturnQuery;
21use Deriver\Deriver\Evaluator\Request\TupleQuery;
22use Deriver\Deriver\Evaluator\Request\ValueQuery;
23use Deriver\Deriver\Evaluator\Scalar\Identity;
24use Deriver\Deriver\Evaluator\TargetProfile;
25use Deriver\Deriver\Evaluator\Term;
26use Deriver\Deriver\Lowerer\Declaration\Index;
27use Deriver\Deriver\MicroEvaluatorConversion;
28use Deriver\Deriver\Parser\Node\File;
29use Deriver\Deriver\Parser\Parser;
30use Deriver\Deriver\Parser\ParserConfig;
31use Deriver\Deriver\Providers\ProviderExpansion;
32use Deriver\Deriver\Publication\CandidateQuery;
33use Deriver\Deriver\Resolver\Resolver;
34use Deriver\Deriver\Selection\CallObservations;
35use Deriver\Deriver\SnapshotIdentity;
36use Deriver\Deriver\Sources\Path;
37use Deriver\Deriver\Sources\Source;
38use Deriver\Deriver\Sources\SourceRef;
39use Deriver\Deriver\Sources\Sources;
40use PHPUnit\Framework\Attributes\CoversNothing;
41use PHPUnit\Framework\TestCase;
42use WeakMap;
43
44#[CoversNothing]
45final class DemandTest extends TestCase
46{
47 public function testFramesKeepExplicitBindingsOutOfOtherQueries(): void
48 {
49 $prepared1Input = new Sources([new Source('candidate.php', '<?php ' . 'function target($x){observe($x);}')]);
50 $prepared1Configuration = new DeriverConfig();
51 $prepared1Contributions = new Contributions();
52 $prepared1Target = new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision);
53 $prepared1Providers = new ProviderExpansion($prepared1Input, array_map(Term::fromNative(...), $prepared1Configuration->environment), $prepared1Contributions);
54 $contributions = $prepared1Providers->contributions;
55 $prepared1Evaluators = new Evaluators(new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision), new Registrations($contributions->evaluators, $contributions->providers, $contributions->domains, $contributions->intrinsics, $contributions->stateSlots), $prepared1Configuration->standardEvaluators);
56 $prepared1Settings = new EvaluatorConfig($prepared1Target, $prepared1Providers->environment, $prepared1Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared1Configuration->evaluators));
57 $prepared1Identity = new SnapshotIdentity($prepared1Providers->input, $prepared1Settings, $prepared1Evaluators, $prepared1Providers->manifest);
58 $snapshotId = $prepared1Identity->id;
59 $input = $prepared1Providers->input;
60 $parser = new Parser(new ParserConfig($prepared1Target->version));
61 $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input, false));
62 $prepared1Program = new Index($snapshotId, $trees);
63 $prepared1Snapshot = $prepared1Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared1Program->diagnostics()));
64 $s = new CandidateQuery(new DefinitionIndex($prepared1Program), $prepared1Settings, $prepared1Evaluators, $prepared1Snapshot);
65 $site = (new CallObservations($prepared1Program, $prepared1Evaluators))->find('observe')[0];
66 $scope = QueryScope::fromEntrypoints([new EntryPoint('target', [Term::constant(3)])]);
67 $r = $s->derive(new ValueQuery($site->argument(0), scope: $scope));
68 $slot = 'value';
69 self::assertSame([3], array_map(static fn (Term $value) => $value->native(), array_map(static fn ($candidate): Term => $candidate->result instanceof Term ? $candidate->result : Term::fromNative($candidate->result), iterator_to_array($r, false))));
70 $result = $s->derive(new ValueQuery((new CallObservations($prepared1Program, $prepared1Evaluators))->find('observe')[0]->argument(0)));
71 $frontiers = array_reduce(iterator_to_array($result, false), static function (array $frontiers, Candidate $candidate1): array {
72 if ($candidate1->result instanceof Term) {
73 $graph = $candidate1->result;
74 $snapshot = $candidate1->evidence[0]->snapshot;
75 $pending = [$graph];
76 $seen = new WeakMap();
77 $frontiers1 = [];
78 while ($pending !== []) {
79 $node = array_pop($pending);
80 if (isset($seen[$node])) {
81 continue;
82 }
83 $seen[$node] = true;
84 array_push($pending, ...array_values($node->operands));
85 $reason = $node->attributes['reason'] ?? null;
86 if (!is_string($reason)) {
87 continue;
88 }
89 $source1 = new SourceRef($snapshot->id, (string) ($node->attributes['source'] ?? ''), (int) ($node->attributes['start'] ?? 0), (int) ($node->attributes['end'] ?? $node->attributes['start'] ?? 0));
90 $identity = (string) ($node->attributes['identity'] ?? (new Identity())->key($node));
91 $frontiers1[$identity] = new Frontier($reason, $source1, (string) $node->literal, ['value'], [$identity], $node, $reason);
92 }
93 array_push($frontiers, ...array_values($frontiers1));
94 }
95 return $frontiers;
96 }, []);
97 self::assertNotEmpty($frontiers);
98 }
99 public function testObserveProjectsADemandedArrayEntry(): void
100 {
101 $prepared2Input = new Sources([new Source('candidate.php', '<?php ' . 'function target(){observe([1,2]);}')]);
102 $prepared2Configuration = new DeriverConfig();
103 $prepared2Contributions = new Contributions();
104 $prepared2Target = new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision);
105 $prepared2Providers = new ProviderExpansion($prepared2Input, array_map(Term::fromNative(...), $prepared2Configuration->environment), $prepared2Contributions);
106 $contributions = $prepared2Providers->contributions;
107 $prepared2Evaluators = new Evaluators(new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision), new Registrations($contributions->evaluators, $contributions->providers, $contributions->domains, $contributions->intrinsics, $contributions->stateSlots), $prepared2Configuration->standardEvaluators);
108 $prepared2Settings = new EvaluatorConfig($prepared2Target, $prepared2Providers->environment, $prepared2Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared2Configuration->evaluators));
109 $prepared2Identity = new SnapshotIdentity($prepared2Providers->input, $prepared2Settings, $prepared2Evaluators, $prepared2Providers->manifest);
110 $snapshotId = $prepared2Identity->id;
111 $input = $prepared2Providers->input;
112 $parser = new Parser(new ParserConfig($prepared2Target->version));
113 $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input, false));
114 $prepared2Program = new Index($snapshotId, $trees);
115 $prepared2Snapshot = $prepared2Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared2Program->diagnostics()));
116 $s = new CandidateQuery(new DefinitionIndex($prepared2Program), $prepared2Settings, $prepared2Evaluators, $prepared2Snapshot);
117 $r = $s->derive(new ValueQuery((new CallObservations($prepared2Program, $prepared2Evaluators))->find('observe')[0]->argument(0), new Projection([1])));
118 $slot = 'value';
119 self::assertSame([2], array_map(static fn (Term $value) => $value->native(), array_map(static fn ($candidate): Term => $candidate->result instanceof Term ? $candidate->result : Term::fromNative($candidate->result), iterator_to_array($r, false))));
120 }
121 public function testTupleKeepsTheSelectedNames(): void
122 {
123 $prepared3Input = new Sources([new Source('candidate.php', '<?php ' . 'function target(){observe(1,2);}')]);
124 $prepared3Configuration = new DeriverConfig();
125 $prepared3Contributions = new Contributions();
126 $prepared3Target = new TargetProfile($prepared3Configuration->phpVersion, $prepared3Configuration->integerBits, $prepared3Configuration->floatPrecision);
127 $prepared3Providers = new ProviderExpansion($prepared3Input, array_map(Term::fromNative(...), $prepared3Configuration->environment), $prepared3Contributions);
128 $contributions = $prepared3Providers->contributions;
129 $prepared3Evaluators = new Evaluators(new TargetProfile($prepared3Configuration->phpVersion, $prepared3Configuration->integerBits, $prepared3Configuration->floatPrecision), new Registrations($contributions->evaluators, $contributions->providers, $contributions->domains, $contributions->intrinsics, $contributions->stateSlots), $prepared3Configuration->standardEvaluators);
130 $prepared3Settings = new EvaluatorConfig($prepared3Target, $prepared3Providers->environment, $prepared3Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared3Configuration->evaluators));
131 $prepared3Identity = new SnapshotIdentity($prepared3Providers->input, $prepared3Settings, $prepared3Evaluators, $prepared3Providers->manifest);
132 $snapshotId = $prepared3Identity->id;
133 $input = $prepared3Providers->input;
134 $parser = new Parser(new ParserConfig($prepared3Target->version));
135 $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input, false));
136 $prepared3Program = new Index($snapshotId, $trees);
137 $prepared3Snapshot = $prepared3Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared3Program->diagnostics()));
138 $s = new CandidateQuery(new DefinitionIndex($prepared3Program), $prepared3Settings, $prepared3Evaluators, $prepared3Snapshot);
139 $site = (new CallObservations($prepared3Program, $prepared3Evaluators))->find('observe')[0];
140 $r = $s->derive(new TupleQuery($site->beforeInvocation(), ['left' => $site->argument(0), 'right' => $site->argument(1)]));
141 self::assertIsArray($r[0]->result);
142 self::assertSame(['left', 'right'], array_keys($r[0]->result));
143 }
144 public function testReturnsRetainsConditionalImplementations(): void
145 {
146 $source = 'if($flag){function target(){return 1;}}else{function target(){return 2;}}';
147 $configuration = new DeriverConfig();
148 $budget = new Budget();
149 $contributions = new Contributions();
150 $prepared7Input = new Sources([new Source('candidate.php', '<?php ' . $source)]);
151 $prepared7Configuration = $configuration;
152 $prepared7Contributions = $contributions;
153 $prepared7Target = new TargetProfile($prepared7Configuration->phpVersion, $prepared7Configuration->integerBits, $prepared7Configuration->floatPrecision);
154 $prepared7Providers = new ProviderExpansion($prepared7Input, array_map(Term::fromNative(...), $prepared7Configuration->environment), $prepared7Contributions);
155 $contributions1 = $prepared7Providers->contributions;
156 $prepared7Evaluators = new Evaluators(new TargetProfile($prepared7Configuration->phpVersion, $prepared7Configuration->integerBits, $prepared7Configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $prepared7Configuration->standardEvaluators);
157 $prepared7Settings = new EvaluatorConfig($prepared7Target, $prepared7Providers->environment, $prepared7Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared7Configuration->evaluators));
158 $prepared7Identity = new SnapshotIdentity($prepared7Providers->input, $prepared7Settings, $prepared7Evaluators, $prepared7Providers->manifest);
159 $snapshotId = $prepared7Identity->id;
160 $input = $prepared7Providers->input;
161 $parser = new Parser(new ParserConfig($prepared7Target->version));
162 $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($input, false));
163 $prepared7Program = new Index($snapshotId, $trees);
164 $prepared7Snapshot = $prepared7Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared7Program->diagnostics()));
165 $result = (new CandidateQuery(new DefinitionIndex($prepared7Program), $prepared7Settings, $prepared7Evaluators, $prepared7Snapshot))->derive(new ReturnQuery('target', budget: $budget));
166 self::assertSame([1, 2], array_column(iterator_to_array($result, false), 'result'));
167 }
168 public function testProjectAppliesThePathOfTheTargetToTheSelectedValue(): void
169 {
170 $prepared4Input = new Sources([new Source('candidate.php', '<?php ' . 'function target(){observe([\'a\' => [1, 2]]);}')]);
171 $prepared4Configuration = new DeriverConfig();
172 $prepared4Contributions = new Contributions();
173 $prepared4Target = new TargetProfile($prepared4Configuration->phpVersion, $prepared4Configuration->integerBits, $prepared4Configuration->floatPrecision);
174 $prepared4Providers = new ProviderExpansion($prepared4Input, array_map(Term::fromNative(...), $prepared4Configuration->environment), $prepared4Contributions);
175 $contributions = $prepared4Providers->contributions;
176 $prepared4Evaluators = new Evaluators(new TargetProfile($prepared4Configuration->phpVersion, $prepared4Configuration->integerBits, $prepared4Configuration->floatPrecision), new Registrations($contributions->evaluators, $contributions->providers, $contributions->domains, $contributions->intrinsics, $contributions->stateSlots), $prepared4Configuration->standardEvaluators);
177 $prepared4Settings = new EvaluatorConfig($prepared4Target, $prepared4Providers->environment, $prepared4Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared4Configuration->evaluators));
178 $prepared4Identity = new SnapshotIdentity($prepared4Providers->input, $prepared4Settings, $prepared4Evaluators, $prepared4Providers->manifest);
179 $snapshotId = $prepared4Identity->id;
180 $input = $prepared4Providers->input;
181 $parser = new Parser(new ParserConfig($prepared4Target->version));
182 $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input, false));
183 $prepared4Program = new Index($snapshotId, $trees);
184 $prepared4Snapshot = $prepared4Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared4Program->diagnostics()));
185 $s = new CandidateQuery(new DefinitionIndex($prepared4Program), $prepared4Settings, $prepared4Evaluators, $prepared4Snapshot);
186 $r = $s->derive(new ValueQuery((new CallObservations($prepared4Program, $prepared4Evaluators))->find('observe')[0]->argument(0), new Projection(['a', 1])));
187 $slot = 'value';
188 self::assertSame([2], array_map(static fn (Term $value) => $value->native(), array_map(static fn ($candidate): Term => $candidate->result instanceof Term ? $candidate->result : Term::fromNative($candidate->result), iterator_to_array($r, false))));
189 }
190 public function testSelectKeepsAMissingSymbolAsAnUnexpandedReference(): void
191 {
192 $prepared5Input = new Sources([new Source('candidate.php', '<?php ' . 'function target(){return 1;}')]);
193 $prepared5Configuration = new DeriverConfig();
194 $prepared5Contributions = new Contributions();
195 $prepared5Target = new TargetProfile($prepared5Configuration->phpVersion, $prepared5Configuration->integerBits, $prepared5Configuration->floatPrecision);
196 $prepared5Providers = new ProviderExpansion($prepared5Input, array_map(Term::fromNative(...), $prepared5Configuration->environment), $prepared5Contributions);
197 $contributions = $prepared5Providers->contributions;
198 $prepared5Evaluators = new Evaluators(new TargetProfile($prepared5Configuration->phpVersion, $prepared5Configuration->integerBits, $prepared5Configuration->floatPrecision), new Registrations($contributions->evaluators, $contributions->providers, $contributions->domains, $contributions->intrinsics, $contributions->stateSlots), $prepared5Configuration->standardEvaluators);
199 $prepared5Settings = new EvaluatorConfig($prepared5Target, $prepared5Providers->environment, $prepared5Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared5Configuration->evaluators));
200 $prepared5Identity = new SnapshotIdentity($prepared5Providers->input, $prepared5Settings, $prepared5Evaluators, $prepared5Providers->manifest);
201 $snapshotId = $prepared5Identity->id;
202 $input = $prepared5Providers->input;
203 $parser = new Parser(new ParserConfig($prepared5Target->version));
204 $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(Path::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input, false));
205 $prepared5Program = new Index($snapshotId, $trees);
206 $prepared5Snapshot = $prepared5Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared5Program->diagnostics()));
207 $r = (new CandidateQuery(new DefinitionIndex($prepared5Program), $prepared5Settings, $prepared5Evaluators, $prepared5Snapshot))->derive(new ReturnQuery('target'));
208 $slot = 'value';
209 self::assertSame([1], array_map(static fn (Term $value) => $value->native(), array_map(static fn ($candidate): Term => $candidate->result instanceof Term ? $candidate->result : Term::fromNative($candidate->result), iterator_to_array($r, false))));
210 $prepared6Input = new Sources([new Source('candidate.php', '<?php ' . 'function target(){return missing();}')]);
211 $prepared6Configuration = new DeriverConfig();
212 $prepared6Contributions = new Contributions();
213 $prepared6Target = new TargetProfile($prepared6Configuration->phpVersion, $prepared6Configuration->integerBits, $prepared6Configuration->floatPrecision);
214 $prepared6Providers = new ProviderExpansion($prepared6Input, array_map(Term::fromNative(...), $prepared6Configuration->environment), $prepared6Contributions);
215 $contributions1 = $prepared6Providers->contributions;
216 $prepared6Evaluators = new Evaluators(new TargetProfile($prepared6Configuration->phpVersion, $prepared6Configuration->integerBits, $prepared6Configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $prepared6Configuration->standardEvaluators);
217 $prepared6Settings = new EvaluatorConfig($prepared6Target, $prepared6Providers->environment, $prepared6Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared6Configuration->evaluators));
218 $prepared6Identity = new SnapshotIdentity($prepared6Providers->input, $prepared6Settings, $prepared6Evaluators, $prepared6Providers->manifest);
219 $snapshotId1 = $prepared6Identity->id;
220 $input1 = $prepared6Providers->input;
221 $parser1 = new Parser(new ParserConfig($prepared6Target->version));
222 $trees1 = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
223 $prepared6Program = new Index($snapshotId1, $trees1);
224 $prepared6Snapshot = $prepared6Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared6Program->diagnostics()));
225 $missing = (new CandidateQuery(new DefinitionIndex($prepared6Program), $prepared6Settings, $prepared6Evaluators, $prepared6Snapshot))->derive(new ReturnQuery('target'));
226 self::assertSame(CandidateStatus::Partials, $missing[0]->type);
227 } public function testPathObservationPrunesAnExcludedArgumentSite(): void
228 {
229 $source = 'function foo($x){return $x;}function bar(){if(false){foo(7);}}';
230 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', '<?php ' . $source)));
231 $index = new DefinitionIndex(new Index('fixture', [$tree]));
232 $settings = new EvaluatorConfig(evaluators: array_map((new MicroEvaluatorConversion())->convert(...), \Deriver\Deriver\BranchEvaluators::truth()));
233 $engine = new \Deriver\Deriver\Evaluator\Evaluator(new \Deriver\Deriver\Evaluator\Scope\Context($index, $settings, new Evaluators($settings->target), new Budget()));
234 [$caller, $call] = (new \Deriver\Deriver\Evaluator\Invocation\Path\Callers($index))->sites('foo', 'bar')[0];
235 $engine->context->callPath = new \Deriver\Deriver\Evaluator\Request\CallPath(['bar'], true);
236 $target = new \Deriver\Deriver\Evaluator\Target('bar', \Deriver\Deriver\Evaluator\TargetKind::Value, $call->arguments[0]->register, register: $call->arguments[0]->register);
237 $value = (new \Deriver\Deriver\Evaluator\Demand($engine))->pathObservation(new \Deriver\Deriver\Evaluator\Scope\Frame($caller, 'bar'), $target);
238 self::assertSame(\Deriver\Deriver\Evaluator\TermKind::Choice, $value->kind);
239 self::assertSame([], $value->operands);
240 }
241
242
243}
244