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