packages/deriver/tests/Integration/FiniteCandidatesTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Integration;
6
7use Deriver\Deriver\BranchEvaluators;
8use Deriver\Deriver\Candidate\Snapshot;
9use Deriver\Deriver\EvaluationOperation;
10use Deriver\Deriver\Evaluator\Call\Evaluators;
11use Deriver\Deriver\Evaluator\EvaluatorConfig;
12use Deriver\Deriver\Evaluator\Index\DefinitionIndex;
13use Deriver\Deriver\Evaluator\Request\ReturnQuery;
14use Deriver\Deriver\Evaluator\TargetProfile;
15use Deriver\Deriver\Evaluator\Term;
16use Deriver\Deriver\Evaluator\TermKind;
17use Deriver\Deriver\Lowerer\Declaration\Index;
18use Deriver\Deriver\MicroEvaluator;
19use Deriver\Deriver\MicroEvaluatorConversion;
20use Deriver\Deriver\Parser\Parser;
21use Deriver\Deriver\Parser\ParserConfig;
22use Deriver\Deriver\Publication\CandidateQuery;
23use Deriver\Deriver\Resolver\Resolver;
24use Deriver\Deriver\Sources\Source;
25use PHPUnit\Framework\Attributes\CoversNothing;
26use PHPUnit\Framework\Attributes\DataProvider;
27use PHPUnit\Framework\TestCase;
28
29#[CoversNothing]
30final class FiniteCandidatesTest extends TestCase
31{
32 /**
33 * @return iterable<string, array{string, list<scalar|null>, bool}>
34 */
35 public static function providerClosedDomains(): iterable
36 {
37 yield 'declared boolean' => ['function subject($x):bool{return $x?false:($x?true:expensive());}', [false,true], false];
38 yield 'typed predicate return' => ['function foo($x):bool{return $x?false:($x?true:expensive());}function subject($x){if(foo($x))return 1;return 2;}', [1,2], true];
39 yield 'typed predicate argument' => ['function foo(bool $x):bool{return $x;}function subject($x){if(foo($x?false:($x?true:expensive())))return 1;return 2;}', [1,2], true];
40 yield 'weak boolean conversions' => ['function subject($x):bool{return $x?0:($x?"yes":expensive());}', [false,true], false];
41 yield 'nullable boolean' => ['function subject($x):?bool{return $x?null:($x?false:($x?true:expensive()));}', [null,false,true], false];
42 yield 'boolean storage' => ['function subject($x):bool{$v=$x?false:($x?true:expensive());return $v;}', [false,true], false];
43 yield 'boolean helper' => ['function helper($x){return $x?false:($x?true:expensive());}function subject($x):bool{return helper($x);}', [false,true], false];
44 yield 'enum cases' => ['enum State{case A;case B;}function subject($x):State{return $x?State::A:($x?State::B:expensive());}', ['State::A','State::B'], false];
45 yield 'backed enum cases' => ['enum State:string{case A="a";case B="b";}function subject($x):State{return $x?State::A:($x?State::B:expensive());}', ['State::A','State::B'], false];
46 yield 'enum helper' => ['enum State{case A;case B;}function helper($x){return $x?State::A:($x?State::B:expensive());}function subject($x):State{return helper($x);}', ['State::A','State::B'], false];
47 yield 'nullable enum' => ['enum State{case A;case B;}function subject($x):?State{return $x?null:($x?State::A:($x?State::B:expensive()));}', [null,'State::A','State::B'], false];
48 }
49
50 /**
51 * @param list<scalar|null> $expected
52 */
53 #[DataProvider('providerClosedDomains')]
54 public function testStopsAtCompleteDomain(string $source, array $expected, bool $pruning): void
55 {
56 $derive = static function (string $source, MicroEvaluator ...$evaluators): \Deriver\Deriver\Candidate\CandidateCollection {
57 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', '<?php ' . $source)));
58 $program = new Index('fixture', [$tree]);
59 $target = new TargetProfile();
60 $settings = new EvaluatorConfig($target, evaluators: array_map((new MicroEvaluatorConversion())->convert(...), array_values($evaluators)));
61 return (new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('fixture', [], [], $target, 'environment')))->derive(new ReturnQuery('subject'));
62 };
63 $opened = 0;
64 $evaluators = [...($pruning ? BranchEvaluators::truth() : []), new MicroEvaluator('expensive', '1', EvaluationOperation::Function, 'expensive', static function () use (&$opened): Term {
65 ++$opened;
66 return Term::constant(false);
67 })];
68 $result = $derive($source, ...$evaluators);
69 $values = array_map(static fn ($candidate) => $candidate->result instanceof Term && $candidate->result->kind === TermKind::Enum ? $candidate->result->literal : $candidate->result, iterator_to_array($result));
70 self::assertEqualsCanonicalizing($expected, $values);
71 self::assertSame(0, $opened);
72 self::assertFalse($result->interrupted);
73 }
74
75 public function testATypeBoundDoesNotInventMissingWitnesses(): void
76 {
77 $derive = static function (string $source, MicroEvaluator ...$evaluators): \Deriver\Deriver\Candidate\CandidateCollection {
78 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', '<?php ' . $source)));
79 $program = new Index('fixture', [$tree]);
80 $target = new TargetProfile();
81 $settings = new EvaluatorConfig($target, evaluators: array_map((new MicroEvaluatorConversion())->convert(...), array_values($evaluators)));
82 return (new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('fixture', [], [], $target, 'environment')))->derive(new ReturnQuery('subject'));
83 };
84 $opened = 0;
85 $source = 'function subject($x):bool{return $x?false:expensive();}';
86 $result = $derive($source, ...[new MicroEvaluator('expensive', '1', EvaluationOperation::Function, 'expensive', static function () use (&$opened): Term {
87 ++$opened;
88 return Term::constant(true);
89 })]);
90 self::assertSame(1, $opened);
91 self::assertEqualsCanonicalizing([false,true], array_map(static fn ($candidate) => $candidate->result, iterator_to_array($result)));
92 $singleton = $derive('function subject():bool{return false;}');
93 self::assertCount(1, $singleton);
94 self::assertFalse($singleton[0]->result);
95 }
96
97 public function testTypedTruthStillRejectsInvalidStrictInputs(): void
98 {
99 $derive = static function (string $source, MicroEvaluator ...$evaluators): \Deriver\Deriver\Candidate\CandidateCollection {
100 $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('fixture.php', '<?php ' . $source)));
101 $program = new Index('fixture', [$tree]);
102 $target = new TargetProfile();
103 $settings = new EvaluatorConfig($target, evaluators: array_map((new MicroEvaluatorConversion())->convert(...), array_values($evaluators)));
104 return (new CandidateQuery(new DefinitionIndex($program), $settings, new Evaluators($target), new Snapshot('fixture', [], [], $target, 'environment')))->derive(new ReturnQuery('subject'));
105 };
106 $result = $derive('declare(strict_types=1);function foo(bool $x):bool{return $x;}function subject(){return foo(1);}', ...BranchEvaluators::truth());
107 self::assertNotEmpty(iterator_to_array($result));
108 self::assertInstanceOf(Term::class, $result[0]->result);
109 self::assertSame(TermKind::NoValue, $result[0]->result->kind);
110 }
111
112}
113