packages/deriver/tests/Unit/Deriver/Evaluator/Execution/Control/ExceptionMatchTest.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\Context;
16use Deriver\Deriver\Evaluator\Execution\Control\ExceptionMatch;
17use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
18use Deriver\Deriver\Evaluator\Request\Budget;
19use Deriver\Deriver\Evaluator\Request\QueryScope;
20use Deriver\Deriver\Evaluator\Request\ReturnQuery;
21use Deriver\Deriver\Evaluator\TargetProfile;
22use Deriver\Deriver\Evaluator\Term;
23use Deriver\Deriver\Evaluator\TermKind;
24use Deriver\Deriver\Execution\QueryExecution;
25use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
26use Deriver\Deriver\Lowerer\Declaration\CallableSource;
27use Deriver\Deriver\Lowerer\Declaration\ConstantSignatures;
28use Deriver\Deriver\Lowerer\Declaration\Index;
29use Deriver\Deriver\Lowerer\Declaration\Indexer;
30use Deriver\Deriver\Lowerer\Declaration\Metadata\ClassDeclaration;
31use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
32use Deriver\Deriver\Lowerer\Hierarchy\Builtins;
33use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
34use Deriver\Deriver\MicroEvaluatorConversion;
35use Deriver\Deriver\Parser\Node\File;
36use Deriver\Deriver\Parser\Parser;
37use Deriver\Deriver\Parser\ParserConfig;
38use Deriver\Deriver\Parser\TargetSyntax;
39use Deriver\Deriver\Providers\ProviderExpansion;
40use Deriver\Deriver\Resolver\AssignmentPatterns;
41use Deriver\Deriver\Resolver\ClassScope;
42use Deriver\Deriver\Resolver\ExpressionValidation;
43use Deriver\Deriver\Resolver\MagicContext;
44use Deriver\Deriver\Resolver\Resolver;
45use Deriver\Deriver\SnapshotIdentity;
46use Deriver\Deriver\Sources\LineMap;
47use Deriver\Deriver\Sources\Path;
48use Deriver\Deriver\Sources\Source;
49use Deriver\Deriver\Sources\Sources;
50use Deriver\Exception\ParserException;
51use Deriver\Exception\ResolutionException;
52use PHPUnit\Framework\Attributes\CoversClass;
53use PHPUnit\Framework\Attributes\DataProvider;
54use PHPUnit\Framework\Attributes\Small;
55use PHPUnit\Framework\Attributes\UsesClass;
56use PHPUnit\Framework\TestCase;
57
58#[UsesClass(Resolver::class)]
59#[UsesClass(ExpressionValidation::class)]
60#[UsesClass(ResolutionException::class)]
61#[UsesClass(ParserConfig::class)]
62#[UsesClass(ParserException::class)]
63#[CoversClass(ExceptionMatch::class)]
64#[UsesClass(CallableIdentity::class)]
65#[UsesClass(ClassDeclaration::class)]
66#[UsesClass(Builtins::class)]
67#[UsesClass(Dispatch::class)]
68#[UsesClass(Context::class)]
69#[UsesClass(Extensions::class)]
70#[UsesClass(Evaluators::class)]
71#[UsesClass(StateRegistry::class)]
72#[UsesClass(DeriverConfig::class)]
73#[UsesClass(Sources::class)]
74#[UsesClass(Path::class)]
75#[UsesClass(Source::class)]
76#[UsesClass(TargetProfile::class)]
77#[UsesClass(Budget::class)]
78#[UsesClass(QueryScope::class)]
79#[UsesClass(ReturnQuery::class)]
80#[UsesClass(Parser::class)]
81#[UsesClass(File::class)]
82#[UsesClass(ConstantSignatures::class)]
83#[UsesClass(CallableSource::class)]
84#[UsesClass(Indexer::class)]
85#[UsesClass(Index::class)]
86#[UsesClass(Composition::class)]
87#[UsesClass(LineMap::class)]
88#[UsesClass(MagicContext::class)]
89#[UsesClass(AssignmentPatterns::class)]
90#[UsesClass(ClassScope::class)]
91#[UsesClass(TargetSyntax::class)]
92#[UsesClass(Term::class)]
93#[Small]
94final class ExceptionMatchTest extends TestCase
95{
96    /**
97     * @param string $bound Input upper bound
98     * @param list<string> $catches Catch union
99     * @param bool $matched Whether the catch is feasible
100     * @param bool $remaining Whether an uncaught subset is feasible
101     */
102    #[DataProvider('providerBounds')]
103    public function testPartitionRetainsExactlyTheFeasibleSubsets(string $bound, array $catches, bool $matched, bool $remaining): void
104    {
105        $source = '<?php interface Tagged{}final class Box{}class ExternalProblem extends Missing{}class Problem extends RuntimeException implements Tagged{}';
106        $budget = new Budget();
107        $configuration = new DeriverConfig();
108        $contributions = new Contributions();
109        $input = new Sources([new Source('fixture.php', $source)]);
110        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
111        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
112        $contributions1 = $providers->contributions;
113        $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);
114        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
115        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
116        $snapshotId = $identity->id;
117        $input1 = $providers->input;
118        $parser = new Parser(new ParserConfig($target->version));
119        $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));
120        $program1 = new Index($snapshotId, $trees);
121        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
122        $snapshotId1 = 'test';
123        $input2 = new Sources([new Source('fixture.php', $source)]);
124        $profile = $execution->configuration->target;
125        $parser1 = new Parser(new ParserConfig($profile->version));
126        $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));
127        $program = new Index($snapshotId1, $trees1);
128        $types = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
129        [$caught, $rest] = $types->partition(Term::parameter('input', $bound), $catches);
130        self::assertSame($matched, $caught !== null);
131        self::assertSame($remaining, $rest !== null);
132    }
133    /**
134     * @return iterable<string, array{string, list<string>, bool, bool}>
135     */
136    public static function providerBounds(): iterable
137    {
138        yield 'definite root' => ['RuntimeException', ['Exception'], true, false];
139        yield 'disjoint roots' => ['Exception', ['Error'], false, true];
140        yield 'possible subclass' => ['Exception', ['RuntimeException'], true, true];
141        yield 'all throwable roots' => ['Throwable', ['Exception', 'Error'], true, false];
142        yield 'one throwable root' => ['Throwable', ['Error'], true, true];
143        yield 'union fully caught' => ['RuntimeException|TypeError', ['Exception', 'Error'], true, false];
144        yield 'mixed invalid subset' => ['mixed', ['Throwable'], true, true];
145        yield 'object invalid subset' => ['object', ['Throwable'], true, true];
146        yield 'int invalid' => ['int', ['Throwable'], false, true];
147        yield 'primitive union invalid' => ['null|array|string', ['Throwable'], false, true];
148        yield 'closure bound invalid' => ['Closure', ['Throwable'], false, true];
149        yield 'ordinary class invalid' => ['Box', ['Throwable'], false, true];
150        yield 'implemented interface' => ['Problem', ['Tagged'], true, false];
151        yield 'interface may be throwable' => ['Tagged', ['Throwable'], true, true];
152        yield 'intersection root' => ['Tagged&RuntimeException', ['Exception'], true, false];
153        yield 'intersection subtype' => ['Tagged&Exception', ['RuntimeException'], true, true];
154        yield 'intersection disjoint' => ['Tagged&Exception', ['Error'], false, true];
155        yield 'dnf partly caught' => ['(Tagged&Exception)|Error', ['Tagged'], true, true];
156        yield 'missing parent' => ['ExternalProblem', ['Throwable'], true, true];
157        yield 'unknown hierarchy' => ['VendorProblem', ['Exception'], true, true];
158        yield 'invokable exception possible' => ['callable', ['Throwable'], true, true];
159    }
160    public function testPartitionPreservesExclusionsAcrossRepeatedCatchClauses(): void
161    {
162        $source = '<?php function target(){return 1;}';
163        $budget = new Budget();
164        $configuration = new DeriverConfig();
165        $contributions = new Contributions();
166        $input1 = new Sources([new Source('fixture.php', $source)]);
167        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
168        $providers = new ProviderExpansion($input1, array_map(Term::fromNative(...), $configuration->environment), $contributions);
169        $contributions1 = $providers->contributions;
170        $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);
171        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
172        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
173        $snapshotId = $identity->id;
174        $input2 = $providers->input;
175        $parser = new Parser(new ParserConfig($target->version));
176        $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($input2, false));
177        $program1 = new Index($snapshotId, $trees);
178        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
179        $snapshotId1 = 'test';
180        $input3 = new Sources([new Source('fixture.php', $source)]);
181        $profile = $execution->configuration->target;
182        $parser1 = new Parser(new ParserConfig($profile->version));
183        $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($input3, false));
184        $program = new Index($snapshotId1, $trees1);
185        $types = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
186        $input = Term::parameter('input', 'Exception');
187        [$caught, $remaining] = $types->partition($input, ['RuntimeException']);
188        self::assertNotNull($caught);
189        self::assertSame('RuntimeException', $caught->attributes['type']);
190        self::assertSame($input, $caught->operands[0]);
191        self::assertNotNull($remaining);
192        [$shadowed, $rest] = $types->partition($remaining, ['OverflowException']);
193        self::assertNull($shadowed);
194        self::assertSame($remaining, $rest);
195        self::assertSame([$remaining, null], $types->partition($remaining, ['Exception']));
196    }
197    /**
198     * @param Term $value Evaluated throw operand
199     * @param bool $valid Whether the exact value is a Throwable
200     */
201    #[DataProvider('providerExactValues')]
202    public function testExactRecognizesRuntimeValuesAndClassIdentity(Term $value, bool $valid): void
203    {
204        $source = '<?php function target(){return 1;}';
205        $budget = new Budget();
206        $configuration = new DeriverConfig();
207        $contributions = new Contributions();
208        $input = new Sources([new Source('fixture.php', $source)]);
209        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
210        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
211        $contributions1 = $providers->contributions;
212        $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);
213        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
214        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
215        $snapshotId = $identity->id;
216        $input1 = $providers->input;
217        $parser = new Parser(new ParserConfig($target->version));
218        $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));
219        $program1 = new Index($snapshotId, $trees);
220        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
221        $snapshotId1 = 'test';
222        $input2 = new Sources([new Source('fixture.php', $source)]);
223        $profile = $execution->configuration->target;
224        $parser1 = new Parser(new ParserConfig($profile->version));
225        $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));
226        $program = new Index($snapshotId1, $trees1);
227        $types = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
228        self::assertSame($valid ? [$value, null] : [null, $value], $types->partition($value, ['Throwable']));
229    }
230    /**
231     * @return iterable<string, array{Term, bool}>
232     */
233    public static function providerExactValues(): iterable
234    {
235        yield 'integer' => [Term::constant(5), false];
236        yield 'null' => [Term::constant(null), false];
237        yield 'array' => [Term::array([]), false];
238        yield 'closure' => [new Term(TermKind::Closure, 'body'), false];
239        yield 'enum' => [new Term(TermKind::Enum, 'Mode::Ready'), false];
240        yield 'plain object' => [new Term(TermKind::Object, 'box', attributes: ['class' => 'stdClass']), false];
241        yield 'native error' => [new Term(TermKind::Throwable, 'TypeError'), true];
242        yield 'allocated exception' => [new Term(TermKind::Object, 'exception', attributes: ['class' => 'RuntimeException']), true];
243        yield 'uncertain throwable' => [new Term(TermKind::Throwable, 'Throwable', attributes: ['uncertain' => true]), true];
244    }
245    public function testRefineDeduplicatesBounds(): void
246    {
247        $input = new Term(TermKind::Parameter, 'item', attributes: ['type' => 'Throwable']);
248        $source = '<?php function target(){return 1;}';
249        $budget = new Budget();
250        $configuration = new DeriverConfig();
251        $contributions = new Contributions();
252        $input1 = new Sources([new Source('fixture.php', $source)]);
253        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
254        $providers = new ProviderExpansion($input1, array_map(Term::fromNative(...), $configuration->environment), $contributions);
255        $contributions1 = $providers->contributions;
256        $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);
257        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
258        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
259        $snapshotId = $identity->id;
260        $input2 = $providers->input;
261        $parser = new Parser(new ParserConfig($target->version));
262        $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($input2, false));
263        $program1 = new Index($snapshotId, $trees);
264        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
265        $snapshotId1 = 'test';
266        $input3 = new Sources([new Source('fixture.php', $source)]);
267        $profile = $execution->configuration->target;
268        $parser1 = new Parser(new ParserConfig($profile->version));
269        $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($input3, false));
270        $program = new Index($snapshotId1, $trees1);
271        $refined = (new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program))->refine($input, ['Error', 'Error'], ['', 'TypeError', 'TypeError']);
272        self::assertSame(['type' => 'Error', 'excluded-types' => 'TypeError'], $refined->attributes);
273        self::assertSame($input, $refined->operands[0]);
274    }
275    public function testArmsExpandsThrowableRootsAndPreservesIntersections(): void
276    {
277        $source = '<?php function target(){return 1;}';
278        $budget = new Budget();
279        $configuration = new DeriverConfig();
280        $contributions = new Contributions();
281        $input = new Sources([new Source('fixture.php', $source)]);
282        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
283        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
284        $contributions1 = $providers->contributions;
285        $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);
286        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
287        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
288        $snapshotId = $identity->id;
289        $input1 = $providers->input;
290        $parser = new Parser(new ParserConfig($target->version));
291        $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));
292        $program1 = new Index($snapshotId, $trees);
293        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
294        $snapshotId1 = 'test';
295        $input2 = new Sources([new Source('fixture.php', $source)]);
296        $profile = $execution->configuration->target;
297        $parser1 = new Parser(new ParserConfig($profile->version));
298        $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));
299        $program = new Index($snapshotId1, $trees1);
300        $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
301        self::assertSame(['Exception', 'Error', 'Tagged&Exception', 'null'], $match->arms('throwable|(Tagged&Exception)|null'));
302    }
303    public function testIntersectionRetainsIndependentInterfaceBounds(): void
304    {
305        $source = '<?php interface Tagged{}';
306        $budget = new Budget();
307        $configuration = new DeriverConfig();
308        $contributions = new Contributions();
309        $input = new Sources([new Source('fixture.php', $source)]);
310        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
311        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
312        $contributions1 = $providers->contributions;
313        $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);
314        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
315        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
316        $snapshotId = $identity->id;
317        $input1 = $providers->input;
318        $parser = new Parser(new ParserConfig($target->version));
319        $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));
320        $program1 = new Index($snapshotId, $trees);
321        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
322        $snapshotId1 = 'test';
323        $input2 = new Sources([new Source('fixture.php', $source)]);
324        $profile = $execution->configuration->target;
325        $parser1 = new Parser(new ParserConfig($profile->version));
326        $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));
327        $program = new Index($snapshotId1, $trees1);
328        $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
329        self::assertSame('Tagged&RuntimeException', $match->intersection('Tagged&Exception', 'RuntimeException'));
330        self::assertNull($match->intersection('RuntimeException', 'LogicException'));
331        self::assertSame('RuntimeException', $match->intersection('mixed', 'RuntimeException'));
332    }
333    public function testImpliesDistinguishesNecessaryAndPossibleSubclassRelations(): void
334    {
335        $source = '<?php function target(){return 1;}';
336        $budget = new Budget();
337        $configuration = new DeriverConfig();
338        $contributions = new Contributions();
339        $input = new Sources([new Source('fixture.php', $source)]);
340        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
341        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
342        $contributions1 = $providers->contributions;
343        $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);
344        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
345        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
346        $snapshotId = $identity->id;
347        $input1 = $providers->input;
348        $parser = new Parser(new ParserConfig($target->version));
349        $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));
350        $program1 = new Index($snapshotId, $trees);
351        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
352        $snapshotId1 = 'test';
353        $input2 = new Sources([new Source('fixture.php', $source)]);
354        $profile = $execution->configuration->target;
355        $parser1 = new Parser(new ParserConfig($profile->version));
356        $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));
357        $program = new Index($snapshotId1, $trees1);
358        $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
359        self::assertTrue($match->implies('Tagged&RuntimeException', 'Throwable'));
360        self::assertFalse($match->implies('Exception', 'RuntimeException'));
361    }
362    public function testOverlapsRetainsUnknownInterfacesAndRejectsSiblingClasses(): void
363    {
364        $source = '<?php interface Tagged{}';
365        $budget = new Budget();
366        $configuration = new DeriverConfig();
367        $contributions = new Contributions();
368        $input = new Sources([new Source('fixture.php', $source)]);
369        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
370        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
371        $contributions1 = $providers->contributions;
372        $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);
373        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
374        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
375        $snapshotId = $identity->id;
376        $input1 = $providers->input;
377        $parser = new Parser(new ParserConfig($target->version));
378        $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));
379        $program1 = new Index($snapshotId, $trees);
380        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
381        $snapshotId1 = 'test';
382        $input2 = new Sources([new Source('fixture.php', $source)]);
383        $profile = $execution->configuration->target;
384        $parser1 = new Parser(new ParserConfig($profile->version));
385        $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));
386        $program = new Index($snapshotId1, $trees1);
387        $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
388        self::assertTrue($match->overlaps('Tagged', 'Exception'));
389        self::assertTrue($match->overlaps('Exception', 'RuntimeException'));
390        self::assertFalse($match->overlaps('RuntimeException', 'LogicException'));
391        self::assertFalse($match->overlaps('int', 'Exception'));
392    }
393    public function testConcreteClassSeparatesSourceAndNativeClassesFromInterfaces(): void
394    {
395        $source = '<?php interface Tagged{}class Box{}';
396        $budget = new Budget();
397        $configuration = new DeriverConfig();
398        $contributions = new Contributions();
399        $input = new Sources([new Source('fixture.php', $source)]);
400        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
401        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
402        $contributions1 = $providers->contributions;
403        $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);
404        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
405        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
406        $snapshotId = $identity->id;
407        $input1 = $providers->input;
408        $parser = new Parser(new ParserConfig($target->version));
409        $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));
410        $program1 = new Index($snapshotId, $trees);
411        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
412        $snapshotId1 = 'test';
413        $input2 = new Sources([new Source('fixture.php', $source)]);
414        $profile = $execution->configuration->target;
415        $parser1 = new Parser(new ParserConfig($profile->version));
416        $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));
417        $program = new Index($snapshotId1, $trees1);
418        $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
419        self::assertTrue($match->concreteClass('Box'));
420        self::assertTrue($match->concreteClass('Exception'));
421        self::assertFalse($match->concreteClass('Throwable'));
422        self::assertFalse($match->concreteClass('Tagged'));
423        self::assertFalse($match->concreteClass('Missing'));
424    }
425    public function testExcludedRemovesAllSubclassesOfAnEarlierCatch(): void
426    {
427        $source = '<?php function target(){return 1;}';
428        $budget = new Budget();
429        $configuration = new DeriverConfig();
430        $contributions = new Contributions();
431        $input = new Sources([new Source('fixture.php', $source)]);
432        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
433        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
434        $contributions1 = $providers->contributions;
435        $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);
436        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
437        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
438        $snapshotId = $identity->id;
439        $input1 = $providers->input;
440        $parser = new Parser(new ParserConfig($target->version));
441        $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));
442        $program1 = new Index($snapshotId, $trees);
443        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
444        $snapshotId1 = 'test';
445        $input2 = new Sources([new Source('fixture.php', $source)]);
446        $profile = $execution->configuration->target;
447        $parser1 = new Parser(new ParserConfig($profile->version));
448        $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));
449        $program = new Index($snapshotId1, $trees1);
450        $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
451        self::assertTrue($match->excluded('OverflowException', ['', 'RuntimeException']));
452        self::assertFalse($match->excluded('LogicException', ['', 'RuntimeException']));
453    }
454    public function testPartitionRetainsUnknownParentsForAllocatedObjects(): void
455    {
456        $source = '<?php class Problem extends Missing{}';
457        $budget = new Budget();
458        $configuration = new DeriverConfig();
459        $contributions = new Contributions();
460        $input = new Sources([new Source('fixture.php', $source)]);
461        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
462        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
463        $contributions1 = $providers->contributions;
464        $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);
465        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
466        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
467        $snapshotId = $identity->id;
468        $input1 = $providers->input;
469        $parser = new Parser(new ParserConfig($target->version));
470        $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));
471        $program1 = new Index($snapshotId, $trees);
472        $execution = new QueryExecution($program1, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program1->diagnostics())));
473        $snapshotId1 = 'test';
474        $input2 = new Sources([new Source('fixture.php', $source)]);
475        $profile = $execution->configuration->target;
476        $parser1 = new Parser(new ParserConfig($profile->version));
477        $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));
478        $program = new Index($snapshotId1, $trees1);
479        $match = new ExceptionMatch((new Context($program, new ReturnQuery('target', budget: $budget), $execution->configuration, $execution->evaluators))->program);
480        [$caught, $remaining] = $match->partition(new Term(TermKind::Object, 'problem', attributes: ['class' => 'Problem']), ['Throwable']);
481        self::assertNotNull($caught);
482        self::assertNotNull($remaining);
483        self::assertSame('Problem&Exception|Problem&Error', $caught->attributes['type']);
484    }
485}
486