packages/deriver/tests/Unit/Deriver/Evaluator/Index/BodyIndexTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Index;
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\Evaluator;
12use Deriver\Deriver\Evaluator\EvaluatorConfig;
13use Deriver\Deriver\Evaluator\Evidence\Frontier;
14use Deriver\Deriver\Evaluator\Index\BodyIndex;
15use Deriver\Deriver\Evaluator\Index\DefinitionIndex;
16use Deriver\Deriver\Evaluator\Request\Budget;
17use Deriver\Deriver\Evaluator\Request\ReturnQuery;
18use Deriver\Deriver\Evaluator\Scope\Context;
19use Deriver\Deriver\Evaluator\Scope\Frame;
20use Deriver\Deriver\Evaluator\TargetProfile;
21use Deriver\Deriver\Evaluator\Term;
22use Deriver\Deriver\Lowerer\Declaration\Index;
23use Deriver\Deriver\MicroEvaluatorConversion;
24use Deriver\Deriver\Parser\Node\File;
25use Deriver\Deriver\Parser\Parser;
26use Deriver\Deriver\Parser\ParserConfig;
27use Deriver\Deriver\Providers\ProviderExpansion;
28use Deriver\Deriver\Publication\CandidateQuery;
29use Deriver\Deriver\Resolver\Resolver;
30use Deriver\Deriver\SnapshotIdentity;
31use Deriver\Deriver\Sources\Path;
32use Deriver\Deriver\Sources\Source;
33use Deriver\Deriver\Sources\Sources;
34use PHPUnit\Framework\Attributes\CoversNothing;
35use PHPUnit\Framework\TestCase;
36
37#[CoversNothing]
38final class BodyIndexTest extends TestCase
39{
40    public function testReachableBlocksIncludesExceptionHandlerEntries(): void
41    {
42        $tree = (new Resolver())->resolve((new Parser(new ParserConfig('8.3')))->parse(new Source('handlers.php', '<?php function target(){try{throw new Exception();}catch(Exception $e){return 1;}finally{return 2;}}')));
43        $index = new DefinitionIndex(new Index('handlers', [$tree]));
44        $graph = $index->graph('target');
45        self::assertNotNull($graph);
46        $region = $graph->body->regions[0];
47        self::assertNotNull($region->finally);
48        self::assertArrayHasKey($region->catches[0]->block, $graph->reachableBlocks());
49        self::assertArrayHasKey($region->finally, $graph->reachableBlocks());
50    }
51
52    public function testReturnsIndexesAllReturnOriginsWithoutEvaluatingThem(): void
53    {
54        $source = 'function target($flag){if($flag){return 1;}return external();}';
55        $configuration = new DeriverConfig();
56        $budget = new Budget();
57        $contributions = new Contributions();
58        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
59        $providers = new ProviderExpansion(new Sources([new Source('candidate.php', '<?php ' . $source)]), array_map(Term::fromNative(...), $configuration->environment), $contributions);
60        $contributions1 = $providers->contributions;
61        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
62        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
63        $snapshotId = 'test';
64        $input = $providers->input;
65        $parser = new Parser(new ParserConfig($target->version));
66        $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));
67        $program = new Index($snapshotId, $trees);
68        $e = new Evaluator(new Context(new DefinitionIndex($program), $settings, $evaluators, $budget));
69        $symbol = 'target';
70        $graph = $e->context->index->graph($symbol);
71        self::assertNotNull($graph);
72        $f = new Frame($graph, 'source:' . $symbol);
73        self::assertCount(2, $f->graph->returns());
74        self::assertSame(0, $e->context->bodyExpansions);
75    }
76    public function testReturnsPreservesTheDefinitionBeforeFinally(): void
77    {
78        $source = 'function target(){try{return 1;}finally{return 2;}}';
79        $configuration = new DeriverConfig();
80        $budget = new Budget();
81        $contributions = new Contributions();
82        $prepared1Input = new Sources([new Source('candidate.php', '<?php ' . $source)]);
83        $prepared1Configuration = $configuration;
84        $prepared1Contributions = $contributions;
85        $prepared1Target = new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision);
86        $prepared1Providers = new ProviderExpansion($prepared1Input, array_map(Term::fromNative(...), $prepared1Configuration->environment), $prepared1Contributions);
87        $contributions1 = $prepared1Providers->contributions;
88        $prepared1Evaluators = new Evaluators(new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $prepared1Configuration->standardEvaluators);
89        $prepared1Settings = new EvaluatorConfig($prepared1Target, $prepared1Providers->environment, $prepared1Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared1Configuration->evaluators));
90        $prepared1Identity = new SnapshotIdentity($prepared1Providers->input, $prepared1Settings, $prepared1Evaluators, $prepared1Providers->manifest);
91        $snapshotId = $prepared1Identity->id;
92        $input = $prepared1Providers->input;
93        $parser = new Parser(new ParserConfig($prepared1Target->version));
94        $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));
95        $prepared1Program = new Index($snapshotId, $trees);
96        $prepared1Snapshot = $prepared1Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared1Program->diagnostics()));
97        self::assertSame(2, (new CandidateQuery(new DefinitionIndex($prepared1Program), $prepared1Settings, $prepared1Evaluators, $prepared1Snapshot))->derive(new ReturnQuery('target', budget: $budget))[0]->result);
98    }
99    public function testWritesIncludesEveryDirectStorageMutation(): void
100    {
101        $source = 'function target(){$x=1;$x+=2;$x++;unset($x);}';
102        $configuration = new DeriverConfig();
103        $budget = new Budget();
104        $contributions = new Contributions();
105        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
106        $providers = new ProviderExpansion(new Sources([new Source('candidate.php', '<?php ' . $source)]), array_map(Term::fromNative(...), $configuration->environment), $contributions);
107        $contributions1 = $providers->contributions;
108        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
109        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
110        $snapshotId = 'test';
111        $input = $providers->input;
112        $parser = new Parser(new ParserConfig($target->version));
113        $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));
114        $program = new Index($snapshotId, $trees);
115        $evaluator = new Evaluator(new Context(new DefinitionIndex($program), $settings, $evaluators, $budget));
116        $symbol = 'target';
117        $graph = $evaluator->context->index->graph($symbol);
118        self::assertNotNull($graph);
119        $frame = new Frame($graph, 'source:' . $symbol);
120        $operation = 'write';
121        $instruction = array_values(array_filter($frame->graph->definitions, static fn ($instruction): bool => $instruction->operation === $operation))[0] ?? null;
122        self::assertNotNull($instruction);
123        self::assertTrue(BodyIndex::writes($instruction));
124        $operation1 = 'compound';
125        $instruction1 = array_values(array_filter($frame->graph->definitions, static fn ($instruction1): bool => $instruction1->operation === $operation1))[0] ?? null;
126        self::assertNotNull($instruction1);
127        self::assertTrue(BodyIndex::writes($instruction1));
128        $operation2 = 'increment';
129        $instruction2 = array_values(array_filter($frame->graph->definitions, static fn ($instruction2): bool => $instruction2->operation === $operation2))[0] ?? null;
130        self::assertNotNull($instruction2);
131        self::assertTrue(BodyIndex::writes($instruction2));
132        $operation3 = 'unset';
133        $instruction3 = array_values(array_filter($frame->graph->definitions, static fn ($instruction3): bool => $instruction3->operation === $operation3))[0] ?? null;
134        self::assertNotNull($instruction3);
135        self::assertTrue(BodyIndex::writes($instruction3));
136        $operation4 = 'local';
137        $instruction4 = array_values(array_filter($frame->graph->definitions, static fn ($instruction4): bool => $instruction4->operation === $operation4))[0] ?? null;
138        self::assertNotNull($instruction4);
139        self::assertFalse(BodyIndex::writes($instruction4));
140    }
141    public function testRootFindsThePropertyBehindAnElementMutation(): void
142    {
143        $source = 'class Box{public $a=[1];function target(){$this->a[0]+=2;}}';
144        $configuration = new DeriverConfig();
145        $budget = new Budget();
146        $contributions = new Contributions();
147        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
148        $providers = new ProviderExpansion(new Sources([new Source('candidate.php', '<?php ' . $source)]), array_map(Term::fromNative(...), $configuration->environment), $contributions);
149        $contributions1 = $providers->contributions;
150        $evaluators = new Evaluators(new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision), new Registrations($contributions1->evaluators, $contributions1->providers, $contributions1->domains, $contributions1->intrinsics, $contributions1->stateSlots), $configuration->standardEvaluators);
151        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
152        $snapshotId = 'test';
153        $input = $providers->input;
154        $parser = new Parser(new ParserConfig($target->version));
155        $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));
156        $program = new Index($snapshotId, $trees);
157        $evaluator = new Evaluator(new Context(new DefinitionIndex($program), $settings, $evaluators, $budget));
158        $symbol = 'Box::target';
159        $graph = $evaluator->context->index->graph($symbol);
160        self::assertNotNull($graph);
161        $frame = new Frame($graph, 'source:' . $symbol);
162        $operation = 'compound';
163        $instruction = array_values(array_filter($frame->graph->definitions, static fn ($instruction): bool => $instruction->operation === $operation))[0] ?? null;
164        self::assertNotNull($instruction);
165        self::assertSame('field-address', $frame->graph->root($instruction)?->operation);
166    }
167}
168