packages/deriver/tests/Integration/FairEvaluationTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Integration;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\Evaluators;
10use Deriver\Deriver\Evaluator\Call\Registrations;
11use Deriver\Deriver\Evaluator\EvaluatorConfig;
12use Deriver\Deriver\Evaluator\Evidence\Frontier;
13use Deriver\Deriver\Evaluator\Index\DefinitionIndex;
14use Deriver\Deriver\Evaluator\Request\Budget;
15use Deriver\Deriver\Evaluator\Request\EntryPoint;
16use Deriver\Deriver\Evaluator\Request\QueryScope;
17use Deriver\Deriver\Evaluator\Request\ReturnQuery;
18use Deriver\Deriver\Evaluator\TargetProfile;
19use Deriver\Deriver\Evaluator\Term;
20use Deriver\Deriver\Lowerer\Declaration\Index;
21use Deriver\Deriver\MicroEvaluatorConversion;
22use Deriver\Deriver\Parser\Node\File;
23use Deriver\Deriver\Parser\Parser;
24use Deriver\Deriver\Parser\ParserConfig;
25use Deriver\Deriver\Providers\ProviderExpansion;
26use Deriver\Deriver\Publication\CandidateQuery;
27use Deriver\Deriver\Resolver\Resolver;
28use Deriver\Deriver\SnapshotIdentity;
29use Deriver\Deriver\Sources\Path;
30use Deriver\Deriver\Sources\Source;
31use Deriver\Deriver\Sources\Sources;
32use PHPUnit\Framework\Attributes\CoversNothing;
33use PHPUnit\Framework\Attributes\DataProvider;
34use PHPUnit\Framework\TestCase;
35
36#[CoversNothing]
37final class FairEvaluationTest extends TestCase
38{
39 #[DataProvider('providerBranches')]
40 public function testShortAlternativesSurviveAnExpensiveSiblingAndKeepItsRemainder(string $body): void
41 {
42 $source = 'function expensive($n){return expensive($n-1)+1;} function target($flag){' . $body . '}';
43 $configuration = new DeriverConfig();
44 $budget = new Budget(transfers: 80);
45 $contributions = new Contributions();
46 $prepared2Input = new Sources([new Source('candidate.php', '<?php ' . $source)]);
47 $prepared2Configuration = $configuration;
48 $prepared2Contributions = $contributions;
49 $prepared2Target = new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision);
50 $prepared2Providers = new ProviderExpansion($prepared2Input, array_map(Term::fromNative(...), $prepared2Configuration->environment), $prepared2Contributions);
51 $contributions1 = $prepared2Providers->contributions;
52 $prepared2Evaluators = new Evaluators(new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $prepared2Configuration->standardEvaluators);
53 $prepared2Settings = new EvaluatorConfig($prepared2Target, $prepared2Providers->environment, $prepared2Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared2Configuration->evaluators));
54 $prepared2Identity = new SnapshotIdentity($prepared2Providers->input, $prepared2Settings, $prepared2Evaluators, $prepared2Providers->manifest);
55 $snapshotId = $prepared2Identity->id;
56 $input = $prepared2Providers->input;
57 $parser = new Parser(new ParserConfig($prepared2Target->version));
58 $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));
59 $prepared2Program = new Index($snapshotId, $trees);
60 $prepared2Snapshot = $prepared2Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared2Program->diagnostics()));
61 $result = (new CandidateQuery(new DefinitionIndex($prepared2Program), $prepared2Settings, $prepared2Evaluators, $prepared2Snapshot))->derive(new ReturnQuery('target', budget: $budget));
62 $values = array_map(static fn ($candidate) => $candidate->result, iterator_to_array($result));
63 $complete = array_values(array_filter($values, static fn ($value): bool => !$value instanceof Term));
64 $partial = array_filter($values, static fn ($value): bool => $value instanceof Term);
65 self::assertSame([4], $complete);
66 self::assertNotEmpty($partial);
67 self::assertTrue($result->interrupted);
68 }
69 /**
70 * @return array<string, array{string}>
71 */
72 public static function providerBranches(): array
73 {
74 return ['return' => ['if($flag){return expensive(100);}return strlen("abcd");'], 'conditional expression' => ['return $flag ? expensive(100) : strlen("abcd");'], 'reversed branches' => ['return $flag ? strlen("abcd") : expensive(100);'], 'reaching assignment' => ['if($flag){$x=expensive(100);}else{$x=strlen("abcd");}return $x;']];
75 }
76 #[DataProvider('providerDependencies')]
77 public function testShortDependencyAlternativesSurviveAnExpensiveSibling(string $source): void
78 {
79 $source1 = 'function expensive($n){return expensive($n-1)+1;}' . $source;
80 $configuration = new DeriverConfig();
81 $budget = new Budget(transfers: 80);
82 $contributions = new Contributions();
83 $prepared2Input = new Sources([new Source('candidate.php', '<?php ' . $source1)]);
84 $prepared2Configuration = $configuration;
85 $prepared2Contributions = $contributions;
86 $prepared2Target = new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision);
87 $prepared2Providers = new ProviderExpansion($prepared2Input, array_map(Term::fromNative(...), $prepared2Configuration->environment), $prepared2Contributions);
88 $contributions1 = $prepared2Providers->contributions;
89 $prepared2Evaluators = new Evaluators(new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $prepared2Configuration->standardEvaluators);
90 $prepared2Settings = new EvaluatorConfig($prepared2Target, $prepared2Providers->environment, $prepared2Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared2Configuration->evaluators));
91 $prepared2Identity = new SnapshotIdentity($prepared2Providers->input, $prepared2Settings, $prepared2Evaluators, $prepared2Providers->manifest);
92 $snapshotId = $prepared2Identity->id;
93 $input = $prepared2Providers->input;
94 $parser = new Parser(new ParserConfig($prepared2Target->version));
95 $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($input, false));
96 $prepared2Program = new Index($snapshotId, $trees);
97 $prepared2Snapshot = $prepared2Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared2Program->diagnostics()));
98 $result = (new CandidateQuery(new DefinitionIndex($prepared2Program), $prepared2Settings, $prepared2Evaluators, $prepared2Snapshot))->derive(new ReturnQuery('target', budget: $budget));
99 $values = array_map(static fn ($candidate) => $candidate->result, iterator_to_array($result));
100 self::assertSame([4], array_values(array_filter($values, static fn ($value): bool => !$value instanceof Term)));
101 self::assertNotEmpty(array_filter($values, static fn ($value): bool => $value instanceof Term));
102 self::assertTrue($result->interrupted);
103 }
104 /**
105 * @return array<string, array{string}>
106 */
107 public static function providerDependencies(): array
108 {
109 return ['callers' => ['function target($x){return $x;}function a(){return target(expensive(100));}function b(){return target(strlen("abcd"));}'], 'dynamic dispatch' => ['function slow(){return expensive(100);}function fast(){return strlen("abcd");}function target($flag){$f=$flag?"slow":"fast";return $f();}'], 'conditional declarations' => ['if($flag){function target(){return expensive(100);}}else{function target(){return strlen("abcd");}}']];
110 }
111 public function testExplicitInputsReceiveWorkBeforeAnExpensiveInputFinishes(): void
112 {
113 $prepared1Input = new Sources([new Source('candidate.php', '<?php ' . 'function target($n){if($n<=0){return 1;}return target($n-1);}')]);
114 $prepared1Configuration = new DeriverConfig();
115 $prepared1Contributions = new Contributions();
116 $prepared1Target = new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision);
117 $prepared1Providers = new ProviderExpansion($prepared1Input, array_map(Term::fromNative(...), $prepared1Configuration->environment), $prepared1Contributions);
118 $contributions = $prepared1Providers->contributions;
119 $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);
120 $prepared1Settings = new EvaluatorConfig($prepared1Target, $prepared1Providers->environment, $prepared1Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared1Configuration->evaluators));
121 $prepared1Identity = new SnapshotIdentity($prepared1Providers->input, $prepared1Settings, $prepared1Evaluators, $prepared1Providers->manifest);
122 $snapshotId = $prepared1Identity->id;
123 $input = $prepared1Providers->input;
124 $parser = new Parser(new ParserConfig($prepared1Target->version));
125 $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));
126 $prepared1Program = new Index($snapshotId, $trees);
127 $prepared1Snapshot = $prepared1Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared1Program->diagnostics()));
128 $session = new CandidateQuery(new DefinitionIndex($prepared1Program), $prepared1Settings, $prepared1Evaluators, $prepared1Snapshot);
129 $result = $session->derive(new ReturnQuery('target', scope: QueryScope::fromEntrypoints([new EntryPoint('target', [Term::constant(100)]), new EntryPoint('target', [Term::constant(0)])]), budget: new Budget(transfers: 80)));
130 $values = array_map(static fn ($candidate) => $candidate->result, iterator_to_array($result));
131 self::assertSame([1], array_values(array_filter($values, static fn ($value): bool => !$value instanceof Term)));
132 self::assertNotEmpty(array_filter($values, static fn ($value): bool => $value instanceof Term));
133 self::assertTrue($result->interrupted);
134 }
135 public function testAnUnresolvedBranchDoesNotPreventAConcreteSibling(): void
136 {
137 $source = 'function target($flag){return $flag ? external() : strlen("abcd");}';
138 $configuration = new DeriverConfig();
139 $budget = new Budget();
140 $contributions = new Contributions();
141 $prepared2Input = new Sources([new Source('candidate.php', '<?php ' . $source)]);
142 $prepared2Configuration = $configuration;
143 $prepared2Contributions = $contributions;
144 $prepared2Target = new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision);
145 $prepared2Providers = new ProviderExpansion($prepared2Input, array_map(Term::fromNative(...), $prepared2Configuration->environment), $prepared2Contributions);
146 $contributions1 = $prepared2Providers->contributions;
147 $prepared2Evaluators = new Evaluators(new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $prepared2Configuration->standardEvaluators);
148 $prepared2Settings = new EvaluatorConfig($prepared2Target, $prepared2Providers->environment, $prepared2Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared2Configuration->evaluators));
149 $prepared2Identity = new SnapshotIdentity($prepared2Providers->input, $prepared2Settings, $prepared2Evaluators, $prepared2Providers->manifest);
150 $snapshotId = $prepared2Identity->id;
151 $input = $prepared2Providers->input;
152 $parser = new Parser(new ParserConfig($prepared2Target->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($input, false));
154 $prepared2Program = new Index($snapshotId, $trees);
155 $prepared2Snapshot = $prepared2Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared2Program->diagnostics()));
156 $result = (new CandidateQuery(new DefinitionIndex($prepared2Program), $prepared2Settings, $prepared2Evaluators, $prepared2Snapshot))->derive(new ReturnQuery('target', budget: $budget));
157 self::assertCount(2, $result);
158 self::assertInstanceOf(Term::class, $result[0]->result);
159 self::assertSame(4, $result[1]->result);
160 self::assertFalse($result->interrupted);
161 }
162 public function testWaitingDependenciesRetainCorrelationAcrossRepeatedReads(): void
163 {
164 $source = 'function target($flag){$x=$flag?1:2;return $x+$x;}';
165 $configuration = new DeriverConfig();
166 $budget = new Budget();
167 $contributions = new Contributions();
168 $prepared2Input = new Sources([new Source('candidate.php', '<?php ' . $source)]);
169 $prepared2Configuration = $configuration;
170 $prepared2Contributions = $contributions;
171 $prepared2Target = new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision);
172 $prepared2Providers = new ProviderExpansion($prepared2Input, array_map(Term::fromNative(...), $prepared2Configuration->environment), $prepared2Contributions);
173 $contributions1 = $prepared2Providers->contributions;
174 $prepared2Evaluators = new Evaluators(new TargetProfile($prepared2Configuration->phpVersion, $prepared2Configuration->integerBits, $prepared2Configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $prepared2Configuration->standardEvaluators);
175 $prepared2Settings = new EvaluatorConfig($prepared2Target, $prepared2Providers->environment, $prepared2Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared2Configuration->evaluators));
176 $prepared2Identity = new SnapshotIdentity($prepared2Providers->input, $prepared2Settings, $prepared2Evaluators, $prepared2Providers->manifest);
177 $snapshotId = $prepared2Identity->id;
178 $input = $prepared2Providers->input;
179 $parser = new Parser(new ParserConfig($prepared2Target->version));
180 $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));
181 $prepared2Program = new Index($snapshotId, $trees);
182 $prepared2Snapshot = $prepared2Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared2Program->diagnostics()));
183 $result = (new CandidateQuery(new DefinitionIndex($prepared2Program), $prepared2Settings, $prepared2Evaluators, $prepared2Snapshot))->derive(new ReturnQuery('target', budget: $budget));
184 $values = array_map(static fn ($candidate) => $candidate->result, iterator_to_array($result));
185 sort($values);
186 self::assertSame([2, 4], $values);
187 self::assertFalse($result->interrupted);
188 }
189}
190