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