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