packages/deriver/tests/Integration/CandidateControlTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Integration;
6
7use Deriver\Deriver\Candidate\Snapshot;
8use Deriver\Deriver\Evaluation;
9use Deriver\Deriver\EvaluationOperation;
10use Deriver\Deriver\Evaluator\Call\Evaluators;
11use Deriver\Deriver\Evaluator\EvaluatorConfig;
12use Deriver\Deriver\Evaluator\Evidence\EvidenceKind;
13use Deriver\Deriver\Evaluator\Index\DefinitionIndex;
14use Deriver\Deriver\Evaluator\Request\ReturnQuery;
15use Deriver\Deriver\Evaluator\TargetProfile;
16use Deriver\Deriver\Evaluator\Term;
17use Deriver\Deriver\Evaluator\TermKind;
18use Deriver\Deriver\Lowerer\Declaration\Index;
19use Deriver\Deriver\MicroEvaluator;
20use Deriver\Deriver\MicroEvaluatorConversion;
21use Deriver\Deriver\Parser\Parser;
22use Deriver\Deriver\Parser\ParserConfig;
23use Deriver\Deriver\Publication\CandidateQuery;
24use Deriver\Deriver\Resolver\Resolver;
25use Deriver\Deriver\Sources\Source;
26use PHPUnit\Framework\Attributes\CoversNothing;
27use PHPUnit\Framework\Attributes\DataProvider;
28use PHPUnit\Framework\TestCase;
29
30#[CoversNothing]
31final class CandidateControlTest extends TestCase
32{
33 /**
34 * @return iterable<string, array{string, list<int|null>}>
35 */
36 public static function providerBranches(): iterable
37 {
38 yield 'independent ternaries' => ['function subject($x){$a=$x?1:2;$b=$x?10:20;return $a+$b;} function caller(){return subject(condition());}', [11,12,21,22]];
39 yield 'constant condition' => ['function subject(){return false?1:2;}', [1,2]];
40 yield 'if' => ['function subject(){if(condition()){return 7;}return 9;}', [7,9]];
41 yield 'elseif' => ['function subject(){if(condition()){return 1;}elseif(condition()){return 2;}return 3;}', [1,2,3]];
42 yield 'match' => ['function subject(){return match(condition()){caseValue()=>1,default=>2};}', [1,2]];
43 yield 'switch' => ['function subject(){switch(condition()){case caseValue():return 1;default:return 2;}}', [1,2]];
44 yield 'while' => ['function subject(){while(condition()){return 7;}return 9;}', [7,9]];
45 yield 'for' => ['function subject(){for(start();condition();advance()){return 7;}return 9;}', [7,9]];
46 yield 'foreach' => ['function subject(){foreach(source() as $v){return 7;}return 9;}', [7,9]];
47 yield 'while overwrite' => ['function subject(){$v=1;while(condition()){$v=7;}return $v;}', [1,7]];
48 yield 'for overwrite' => ['function subject(){$v=1;for(start();condition();advance()){$v=7;}return $v;}', [1,7]];
49 yield 'foreach overwrite' => ['function subject(){$v=1;foreach(source() as $item){$v=7;}return $v;}', [1,7]];
50 yield 'do overwrite' => ['function subject(){$v=1;do{$v=7;}while(condition());return $v;}', [7]];
51 yield 'nested overwrite' => ['function subject(){$v=1;while(condition()){for(start();condition();advance()){$v=7;}}return $v;}', [1,7]];
52 yield 'foreach values' => ['function subject(){foreach([3,7] as $v){return $v;}return 9;}', [3,7,9]];
53 yield 'foreach last value' => ['function subject(){$v=1;foreach([3,7] as $v){}return $v;}', [1,3,7]];
54 yield 'for initializer value' => ['function subject(){for($v=7;condition();advance()){return $v;}return 9;}', [7,9]];
55 yield 'value assigned in condition' => ['function subject(){while($v=7){return $v;}return 9;}', [7,9]];
56 yield 'break excludes an unreachable update' => ['function subject(){$v=0;while(true){$v=7;break;}return $v;}', [0,7]];
57 yield 'loop in finally' => ['function subject(){try{return 9;}finally{$v=1;while(condition()){$v=7;}return $v;}}', [1,7]];
58 yield 'loop in catch' => ['function subject(){try{throw new Exception();}catch(Exception $e){$v=1;while(condition()){$v=7;}return $v;}}', [1,7]];
59 yield 'dependent loop storage' => ['function subject(){$x=1;$y=2;while(condition()){$x=$y;$y=7;}return $x;}', [1,2,7]];
60 }
61
62 /**
63 * @param list<int|null> $expected
64 */
65 #[DataProvider('providerBranches')]
66 public function testCandidatesDoNotDemandPredicates(string $source, array $expected): void
67 {
68 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('candidate-control.php', '<?php ' . $source)));
69 $program = new Index('candidates', [$tree]);
70 $target = new TargetProfile();
71 $settings = new EvaluatorConfig($target, evaluators: []);
72 $query = new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('candidates', [], [], $target, 'environment'));
73 $result = $query->derive(new ReturnQuery('subject'));
74 $values = array_map(static fn ($candidate) => $candidate->result, iterator_to_array($result));
75 sort($values);
76 self::assertSame($expected, $values);
77 self::assertFalse($result->interrupted);
78 self::assertSame(1, $program->graphCount(), 'Neither predicate functions nor callers supplying only predicates are compiled.');
79 self::assertSame([], $result->statistics->expandedBodies);
80 $nodes = array_merge(...array_map(static fn ($candidate): array => array_merge(...array_map(static fn ($alternative): array => $alternative->nodes(), $candidate->evidence)), iterator_to_array($result)));
81 self::assertNotContains(EvidenceKind::Unexpanded, array_column($nodes, 'kind'), 'Complete candidates do not acquire unresolved predicate dependencies.');
82 }
83
84 /**
85 * @return array<string, array{int, string, string}>
86 */
87 public static function providerValueBranches(): array
88 {
89 return ['left' => [0, 'left', 'right'], 'right' => [1, 'right', 'left']];
90 }
91
92 #[DataProvider('providerValueBranches')]
93 public function testEvidenceContainsOnlyTheSelectedValueBranch(int $offset, string $selected, string $unselected): void
94 {
95 $source = 'function subject(){return condition()?left():right();} function left(){return 1;} function right(){return 2;}';
96 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('candidate-control.php', '<?php ' . $source)));
97 $program = new Index('candidates', [$tree]);
98 $target = new TargetProfile();
99 $settings = new EvaluatorConfig($target, evaluators: []);
100 $query = new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('candidates', [], [], $target, 'environment'));
101 $result = $query->derive(new ReturnQuery('subject'));
102 self::assertCount(2, $result);
103 $nodes = array_merge(...array_map(static fn ($alternative): array => $alternative->nodes(), $result[$offset]->evidence));
104 $owners = array_column(array_column($nodes, 'attributes'), 'owner');
105 self::assertContains($selected, $owners);
106 self::assertNotContains($unselected, $owners);
107 self::assertNotContains('condition', $owners);
108 }
109
110 public function testLoopCarriedValuesRetainAnUpdateWithoutDeterminingIterationCounts(): void
111 {
112 $source = 'function subject(){$v=1;while(condition()){$v+=2;}return $v;}';
113 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('candidate-control.php', '<?php ' . $source)));
114 $program = new Index('candidates', [$tree]);
115 $target = new TargetProfile();
116 $settings = new EvaluatorConfig($target, evaluators: []);
117 $query = new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('candidates', [], [], $target, 'environment'));
118 $result = $query->derive(new ReturnQuery('subject'));
119 self::assertCount(1, $result);
120 $term = $result[0]->result;
121 self::assertInstanceOf(Term::class, $term);
122 self::assertSame(TermKind::Recurrence, $term->kind);
123 self::assertSame('LOOP_DEPENDENCY', $term->attributes['reason']);
124 self::assertSame(1, $term->operands[0]->native());
125 self::assertSame(TermKind::Binary, $term->operands[1]->kind);
126 self::assertSame('+', $term->operands[1]->literal);
127 self::assertSame(2, $term->operands[1]->operands[1]->native());
128 self::assertSame(1, $program->graphCount());
129 self::assertFalse($result->interrupted);
130 }
131
132 public function testForeachKeysStayPairedWithTheirValues(): void
133 {
134 $source = 'function subject(){foreach(["a"=>3,"b"=>7] as $k=>$v){return [$k,$v];}return [];}';
135 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('candidate-control.php', '<?php ' . $source)));
136 $program = new Index('candidates', [$tree]);
137 $target = new TargetProfile();
138 $settings = new EvaluatorConfig($target, evaluators: []);
139 $query = new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('candidates', [], [], $target, 'environment'));
140 $result = $query->derive(new ReturnQuery('subject'));
141 $values = array_map(static fn ($candidate) => $candidate->result, iterator_to_array($result));
142 self::assertEqualsCanonicalizing([['a',3],['b',7],[]], $values);
143 }
144
145 public function testAnApplicationEvaluatorCanChooseToEvaluateAConditional(): void
146 {
147 $evaluator = new MicroEvaluator('app.conditional', '1', EvaluationOperation::Conditional, '', static fn (Evaluation $evaluation): Term => $evaluation->input($evaluation->input(2)->native() === true ? 0 : 1));
148 $source = 'function subject(){return false?1:2;}';
149 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('candidate-control.php', '<?php ' . $source)));
150 $program = new Index('candidates', [$tree]);
151 $target = new TargetProfile();
152 $settings = new EvaluatorConfig($target, evaluators: [(new MicroEvaluatorConversion())->convert($evaluator)]);
153 $query = new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('candidates', [], [], $target, 'environment'));
154 $result = $query->derive(new ReturnQuery('subject'));
155 self::assertCount(1, $result);
156 self::assertSame(2, $result[0]->result);
157 self::assertSame(1, $result->statistics->evaluatorApplications);
158 }
159
160 public function testUnusedTypedPredicateArgumentsDoNotOpenTheirValues(): void
161 {
162 $source = 'function subject(){return helper(condition());} function helper(bool $flag){return $flag?1:2;}';
163 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('candidate-control.php', '<?php ' . $source)));
164 $program = new Index('candidates', [$tree]);
165 $target = new TargetProfile();
166 $settings = new EvaluatorConfig($target, evaluators: []);
167 $query = new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('candidates', [], [], $target, 'environment'));
168 $result = $query->derive(new ReturnQuery('subject'));
169 self::assertSame([1,2], array_map(static fn ($candidate) => $candidate->result, iterator_to_array($result)));
170 self::assertSame(['helper' => 1], $result->statistics->expandedBodies);
171 self::assertSame([], $result->statistics->expandedReferences);
172 }
173
174}
175