packages/deriver/tests/Unit/Deriver/Evaluator/Execution/Control/ExceptionChainTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Execution\Control;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\Evaluators;
10use Deriver\Deriver\Evaluator\Call\Extensions;
11use Deriver\Deriver\Evaluator\Call\Registrations;
12use Deriver\Deriver\Evaluator\Call\StateRegistry;
13use Deriver\Deriver\Evaluator\EvaluatorConfig;
14use Deriver\Deriver\Evaluator\Evidence\Frontier;
15use Deriver\Deriver\Evaluator\Execution\Call\Native\Properties;
16use Deriver\Deriver\Evaluator\Execution\Call\Native\Signatures;
17use Deriver\Deriver\Evaluator\Execution\Completion\Completion;
18use Deriver\Deriver\Evaluator\Execution\Completion\CompletionKind;
19use Deriver\Deriver\Evaluator\Execution\Context;
20use Deriver\Deriver\Evaluator\Execution\Control\ExceptionChain;
21use Deriver\Deriver\Evaluator\Execution\Control\Handler;
22use Deriver\Deriver\Evaluator\Execution\Control\HandlerPhase;
23use Deriver\Deriver\Evaluator\Execution\Control\Unwinding;
24use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
25use Deriver\Deriver\Evaluator\Execution\Memory\Location;
26use Deriver\Deriver\Evaluator\Execution\Memory\Memory;
27use Deriver\Deriver\Evaluator\Execution\State;
28use Deriver\Deriver\Evaluator\Request\Budget;
29use Deriver\Deriver\Evaluator\Request\QueryScope;
30use Deriver\Deriver\Evaluator\Request\ReturnQuery;
31use Deriver\Deriver\Evaluator\Scalar\Arrays;
32use Deriver\Deriver\Evaluator\Scalar\Operations;
33use Deriver\Deriver\Evaluator\TargetProfile;
34use Deriver\Deriver\Evaluator\Term;
35use Deriver\Deriver\Evaluator\TermKind;
36use Deriver\Deriver\Execution\QueryExecution;
37use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
38use Deriver\Deriver\Lowerer\Declaration\CallableSource;
39use Deriver\Deriver\Lowerer\Declaration\ConstantSignatures;
40use Deriver\Deriver\Lowerer\Declaration\Index;
41use Deriver\Deriver\Lowerer\Declaration\Indexer;
42use Deriver\Deriver\Lowerer\Declaration\Metadata\ClassDeclaration;
43use Deriver\Deriver\Lowerer\Declaration\Metadata\PropertyDeclaration;
44use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
45use Deriver\Deriver\Lowerer\Graph\ExceptionRegion;
46use Deriver\Deriver\Lowerer\Hierarchy\Builtins;
47use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
48use Deriver\Deriver\MicroEvaluatorConversion;
49use Deriver\Deriver\Parser\Node\File;
50use Deriver\Deriver\Parser\Parser;
51use Deriver\Deriver\Parser\ParserConfig;
52use Deriver\Deriver\Parser\TargetSyntax;
53use Deriver\Deriver\Providers\ProviderExpansion;
54use Deriver\Deriver\Resolver\AssignmentPatterns;
55use Deriver\Deriver\Resolver\ClassScope;
56use Deriver\Deriver\Resolver\ExpressionValidation;
57use Deriver\Deriver\Resolver\MagicContext;
58use Deriver\Deriver\Resolver\Resolver;
59use Deriver\Deriver\SnapshotIdentity;
60use Deriver\Deriver\Sources\LineMap;
61use Deriver\Deriver\Sources\Path;
62use Deriver\Deriver\Sources\Source;
63use Deriver\Deriver\Sources\Sources;
64use Deriver\Exception\ParserException;
65use Deriver\Exception\ResolutionException;
66use PHPUnit\Framework\Attributes\CoversClass;
67use PHPUnit\Framework\Attributes\DataProvider;
68use PHPUnit\Framework\Attributes\Small;
69use PHPUnit\Framework\Attributes\UsesClass;
70use PHPUnit\Framework\TestCase;
71
72#[UsesClass(Resolver::class)]
73#[UsesClass(ExpressionValidation::class)]
74#[UsesClass(ResolutionException::class)]
75#[UsesClass(ParserConfig::class)]
76#[UsesClass(ParserException::class)]
77#[CoversClass(ExceptionChain::class)]
78#[UsesClass(CallableIdentity::class)]
79#[UsesClass(ClassDeclaration::class)]
80#[UsesClass(ExceptionRegion::class)]
81#[UsesClass(PropertyDeclaration::class)]
82#[UsesClass(Builtins::class)]
83#[UsesClass(Dispatch::class)]
84#[UsesClass(Properties::class)]
85#[UsesClass(Signatures::class)]
86#[UsesClass(Completion::class)]
87#[UsesClass(Context::class)]
88#[UsesClass(Handler::class)]
89#[UsesClass(Unwinding::class)]
90#[UsesClass(State::class)]
91#[UsesClass(Location::class)]
92#[UsesClass(Memory::class)]
93#[UsesClass(Extensions::class)]
94#[UsesClass(Evaluators::class)]
95#[UsesClass(StateRegistry::class)]
96#[UsesClass(DeriverConfig::class)]
97#[UsesClass(Sources::class)]
98#[UsesClass(Path::class)]
99#[UsesClass(Source::class)]
100#[UsesClass(TargetProfile::class)]
101#[UsesClass(Budget::class)]
102#[UsesClass(QueryScope::class)]
103#[UsesClass(ReturnQuery::class)]
104#[UsesClass(Parser::class)]
105#[UsesClass(File::class)]
106#[UsesClass(ConstantSignatures::class)]
107#[UsesClass(CallableSource::class)]
108#[UsesClass(Indexer::class)]
109#[UsesClass(Index::class)]
110#[UsesClass(Composition::class)]
111#[UsesClass(LineMap::class)]
112#[UsesClass(MagicContext::class)]
113#[UsesClass(AssignmentPatterns::class)]
114#[UsesClass(ClassScope::class)]
115#[UsesClass(TargetSyntax::class)]
116#[UsesClass(Arrays::class)]
117#[UsesClass(Operations::class)]
118#[UsesClass(Term::class)]
119#[Small]
120final class ExceptionChainTest extends TestCase
121{
122    public function testUnwindLinksNativeFailuresWhilePreservingTheThrownObject(): void
123    {
124        $source = '<?php function target(){return 1;}';
125        $budget = new Budget();
126        $configuration = new DeriverConfig();
127        $contributions = new Contributions();
128        $input = new Sources([new Source('fixture.php', $source)]);
129        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
130        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
131        $contributions1 = $providers->contributions;
132        $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);
133        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
134        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
135        $snapshotId = $identity->id;
136        $input1 = $providers->input;
137        $parser = new Parser(new ParserConfig($target->version));
138        $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($input1, false));
139        $program2 = new Index($snapshotId, $trees);
140        $execution = new QueryExecution($program2, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program2->diagnostics())));
141        $snapshotId1 = 'test';
142        $input2 = new Sources([new Source('fixture.php', $source)]);
143        $profile = $execution->configuration->target;
144        $parser1 = new Parser(new ParserConfig($profile->version));
145        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
146        $program1 = new Index($snapshotId1, $trees1);
147        $program = (new Context($program1, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program;
148        $state = new State();
149        $unwinding = new Unwinding($program);
150        $original = $unwinding->capture($state, new Term(TermKind::Throwable, 'RuntimeException'));
151        $replacement = $unwinding->capture($state, new Term(TermKind::Throwable, 'Error'));
152        $state->completion = new Completion(CompletionKind::Throw, $replacement);
153        $handler = new Handler(new ExceptionRegion([], 5, 6), HandlerPhase::Finally, new Completion(CompletionKind::Throw, $original));
154        (new ExceptionChain($program))->unwind($state, $handler);
155        self::assertSame($replacement, $state->completion->value);
156        self::assertSame($original, $state->memory->read(new Location('object:' . $replacement->literal, ['Error::previous'])));
157    }
158    /**
159     * @param string $phase \Deriver\Deriver\Evaluator\Execution\Control\Handler phase
160     * @param string $saved Saved completion kind
161     * @param string $current Current completion kind
162     */
163    #[DataProvider('providerUnchangedCompletions')]
164    public function testUnwindLeavesUnrelatedCompletionsUnchanged(string $phase, string $saved, string $current): void
165    {
166        $state = new State();
167        $completion = new Completion(CompletionKind::from($current), Term::constant(1), 9, 2);
168        $state->completion = $completion;
169        $handler = new Handler(new ExceptionRegion([], 5, 6), HandlerPhase::from($phase), new Completion(CompletionKind::from($saved), Term::constant(2)));
170        $source = '<?php function target(){return 1;}';
171        $budget = new Budget();
172        $configuration = new DeriverConfig();
173        $contributions = new Contributions();
174        $input = new Sources([new Source('fixture.php', $source)]);
175        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
176        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
177        $contributions1 = $providers->contributions;
178        $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);
179        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
180        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
181        $snapshotId = $identity->id;
182        $input1 = $providers->input;
183        $parser = new Parser(new ParserConfig($target->version));
184        $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($input1, false));
185        $program1 = new Index($snapshotId, $trees);
186        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
187        $snapshotId1 = 'test';
188        $input2 = new Sources([new Source('fixture.php', $source)]);
189        $profile = $execution->configuration->target;
190        $parser1 = new Parser(new ParserConfig($profile->version));
191        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
192        $program = new Index($snapshotId1, $trees1);
193        (new ExceptionChain((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program))->unwind($state, $handler);
194        self::assertSame($completion, $state->completion);
195        self::assertSame([], $state->memory->cells);
196    }
197    /**
198     * @return iterable<string, array{string, string, string}>
199     */
200    public static function providerUnchangedCompletions(): iterable
201    {
202        yield 'try' => ['try', 'throw', 'throw'];
203        yield 'catch' => ['catch', 'throw', 'throw'];
204        yield 'saved return' => ['finally', 'return', 'throw'];
205        yield 'saved jump' => ['finally', 'jump', 'throw'];
206        yield 'new return' => ['finally', 'throw', 'return'];
207        yield 'new jump' => ['finally', 'throw', 'jump'];
208    }
209    public function testAppendPreservesAnExistingChainAndRejectsCycles(): void
210    {
211        $source = '<?php function target(){return 1;}';
212        $budget = new Budget();
213        $configuration = new DeriverConfig();
214        $contributions = new Contributions();
215        $input = new Sources([new Source('fixture.php', $source)]);
216        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
217        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
218        $contributions1 = $providers->contributions;
219        $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);
220        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
221        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
222        $snapshotId = $identity->id;
223        $input1 = $providers->input;
224        $parser = new Parser(new ParserConfig($target->version));
225        $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($input1, false));
226        $program2 = new Index($snapshotId, $trees);
227        $execution = new QueryExecution($program2, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program2->diagnostics())));
228        $snapshotId1 = 'test';
229        $input2 = new Sources([new Source('fixture.php', $source)]);
230        $profile = $execution->configuration->target;
231        $parser1 = new Parser(new ParserConfig($profile->version));
232        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
233        $program1 = new Index($snapshotId1, $trees1);
234        $program = (new Context($program1, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program;
235        $state = new State();
236        $unwinding = new Unwinding($program);
237        $a = $unwinding->capture($state, new Term(TermKind::Throwable, 'RuntimeException'));
238        $b = $unwinding->capture($state, new Term(TermKind::Throwable, 'LogicException'));
239        $c = $unwinding->capture($state, new Term(TermKind::Throwable, 'Error'));
240        $chain = new ExceptionChain($program);
241        $chain->append($state, $a, $b);
242        $chain->append($state, $a, $c);
243        $chain->append($state, $c, $a);
244        $chain->append($state, $a, $b);
245        self::assertSame($b, $state->memory->read(new Location('object:' . $a->literal, ['Exception::previous'])));
246        self::assertSame($c, $state->memory->read(new Location('object:' . $b->literal, ['Exception::previous'])));
247        self::assertNull($state->memory->read(new Location('object:' . $c->literal, ['Error::previous']))->native());
248    }
249    public function testAncestorsTerminatesOnCyclesAndUnknownObjects(): void
250    {
251        $state = new State();
252        $a = new Term(TermKind::Object, 'a', attributes: ['class' => 'Exception']);
253        $b = new Term(TermKind::Object, 'b', attributes: ['class' => 'Error']);
254        $state->memory->write(new Location('object:a', ['Exception::previous']), $b);
255        $state->memory->write(new Location('object:b', ['Error::previous']), $a);
256        $source = '<?php function target(){return 1;}';
257        $budget = new Budget();
258        $configuration = new DeriverConfig();
259        $contributions = new Contributions();
260        $input = new Sources([new Source('fixture.php', $source)]);
261        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
262        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
263        $contributions1 = $providers->contributions;
264        $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);
265        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
266        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
267        $snapshotId = $identity->id;
268        $input1 = $providers->input;
269        $parser = new Parser(new ParserConfig($target->version));
270        $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($input1, false));
271        $program1 = new Index($snapshotId, $trees);
272        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
273        $snapshotId1 = 'test';
274        $input2 = new Sources([new Source('fixture.php', $source)]);
275        $profile = $execution->configuration->target;
276        $parser1 = new Parser(new ParserConfig($profile->version));
277        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
278        $program = new Index($snapshotId1, $trees1);
279        $chain = new ExceptionChain((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
280        self::assertSame(['a' => true, 'b' => true], $chain->ancestors($state, $a));
281        self::assertSame(['unknown' => true], $chain->ancestors($state, new Term(TermKind::Object, 'unknown')));
282        self::assertSame([], $chain->ancestors($state, Term::constant(null)));
283    }
284    public function testLocationResolvesBothNativeFamiliesAndSourceInheritance(): void
285    {
286        $source = '<?php class Problem extends RuntimeException{}';
287        $budget = new Budget();
288        $configuration = new DeriverConfig();
289        $contributions = new Contributions();
290        $input = new Sources([new Source('fixture.php', $source)]);
291        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
292        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
293        $contributions1 = $providers->contributions;
294        $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);
295        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
296        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
297        $snapshotId = $identity->id;
298        $input1 = $providers->input;
299        $parser = new Parser(new ParserConfig($target->version));
300        $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($input1, false));
301        $program1 = new Index($snapshotId, $trees);
302        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
303        $snapshotId1 = 'test';
304        $input2 = new Sources([new Source('fixture.php', $source)]);
305        $profile = $execution->configuration->target;
306        $parser1 = new Parser(new ParserConfig($profile->version));
307        $trees1 = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser1->parse(new Source(Path::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input2, false));
308        $program = new Index($snapshotId1, $trees1);
309        $chain = new ExceptionChain((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
310        self::assertEquals(new Location('object:a', ['Exception::previous']), $chain->location(new Term(TermKind::Object, 'a', attributes: ['class' => 'Problem'])));
311        self::assertEquals(new Location('object:b', ['Error::previous']), $chain->location(new Term(TermKind::Object, 'b', attributes: ['class' => 'Error'])));
312        self::assertNull($chain->location(new Term(TermKind::Object, 'unknown')));
313    }
314}
315