packages/deriver/tests/Unit/Deriver/Lowerer/LowererTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Lowerer;
6
7use Deriver\Deriver;
8use Deriver\Deriver\Candidate;
9use Deriver\Deriver\Candidate\ResultRef;
10use Deriver\Deriver\Candidate\Snapshot;
11use Deriver\Deriver\Candidate\Statistics;
12use Deriver\Deriver\Contributions;
13use Deriver\Deriver\DeriverConfig;
14use Deriver\Deriver\Evaluator\Call\Evaluators;
15use Deriver\Deriver\Evaluator\Call\Extensions;
16use Deriver\Deriver\Evaluator\Call\Provider\DispatchDecision;
17use Deriver\Deriver\Evaluator\Call\Registrations;
18use Deriver\Deriver\Evaluator\Call\StateRegistry;
19use Deriver\Deriver\Evaluator\EvaluatorConfig;
20use Deriver\Deriver\Evaluator\Evidence\Frontier;
21use Deriver\Deriver\Evaluator\Execution\Call\Allocation;
22use Deriver\Deriver\Evaluator\Execution\Call\ArgumentBinding;
23use Deriver\Deriver\Evaluator\Execution\Call\ArgumentOrder;
24use Deriver\Deriver\Evaluator\Execution\Call\CallableCheck;
25use Deriver\Deriver\Evaluator\Execution\Call\CallExecutor;
26use Deriver\Deriver\Evaluator\Execution\Call\CallResolution;
27use Deriver\Deriver\Evaluator\Execution\Call\Creation\Access;
28use Deriver\Deriver\Evaluator\Execution\Call\Member\Invocation;
29use Deriver\Deriver\Evaluator\Execution\Call\MethodInvocation;
30use Deriver\Deriver\Evaluator\Execution\Call\Native\Invocation as NativeInvocation;
31use Deriver\Deriver\Evaluator\Execution\Call\Native\Properties;
32use Deriver\Deriver\Evaluator\Execution\Call\Native\Signatures;
33use Deriver\Deriver\Evaluator\Execution\Call\ParameterBinding;
34use Deriver\Deriver\Evaluator\Execution\Call\PassedArgument;
35use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Arguments;
36use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Creation;
37use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Methods;
38use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Modes;
39use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Resolution;
40use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Target;
41use Deriver\Deriver\Evaluator\Execution\Call\Preparation\Transfer;
42use Deriver\Deriver\Evaluator\Execution\Call\TypeBinding;
43use Deriver\Deriver\Evaluator\Execution\Call\TypeCheck;
44use Deriver\Deriver\Evaluator\Execution\Completion\Alternative;
45use Deriver\Deriver\Evaluator\Execution\Completion\Assessment;
46use Deriver\Deriver\Evaluator\Execution\Completion\Completion;
47use Deriver\Deriver\Evaluator\Execution\Completion\StorageSnapshot;
48use Deriver\Deriver\Evaluator\Execution\Constant\ClassNames;
49use Deriver\Deriver\Evaluator\Execution\Context;
50use Deriver\Deriver\Evaluator\Execution\Control\ExceptionChain;
51use Deriver\Deriver\Evaluator\Execution\Control\ExceptionMatch;
52use Deriver\Deriver\Evaluator\Execution\Control\Handler;
53use Deriver\Deriver\Evaluator\Execution\Control\ObservationLimit;
54use Deriver\Deriver\Evaluator\Execution\Control\StateJoin;
55use Deriver\Deriver\Evaluator\Execution\Control\Unwinding;
56use Deriver\Deriver\Evaluator\Execution\Demand\Discovery;
57use Deriver\Deriver\Evaluator\Execution\Dependencies;
58use Deriver\Deriver\Evaluator\Execution\Evaluator;
59use Deriver\Deriver\Evaluator\Execution\EvaluatorConfig as ExecutionEvaluatorConfig;
60use Deriver\Deriver\Evaluator\Execution\InstructionTransfer;
61use Deriver\Deriver\Evaluator\Execution\Memory\Location;
62use Deriver\Deriver\Evaluator\Execution\Memory\Materialization;
63use Deriver\Deriver\Evaluator\Execution\Memory\Memory;
64use Deriver\Deriver\Evaluator\Execution\Memory\ReferenceConstraint;
65use Deriver\Deriver\Evaluator\Execution\Memory\Slots\SlotReference;
66use Deriver\Deriver\Evaluator\Execution\Memory\Slots\StateStorage;
67use Deriver\Deriver\Evaluator\Execution\Memory\StorageCapture;
68use Deriver\Deriver\Evaluator\Execution\ObservationCollector;
69use Deriver\Deriver\Evaluator\Execution\Offset\Address;
70use Deriver\Deriver\Evaluator\Execution\Offset\Path;
71use Deriver\Deriver\Evaluator\Execution\Offset\Reader;
72use Deriver\Deriver\Evaluator\Execution\Offset\Transfer as OffsetTransfer;
73use Deriver\Deriver\Evaluator\Execution\Operation\Conversions;
74use Deriver\Deriver\Evaluator\Execution\Operation\ScalarErrors;
75use Deriver\Deriver\Evaluator\Execution\State;
76use Deriver\Deriver\Evaluator\Execution\Summary\Evaluation;
77use Deriver\Deriver\Evaluator\Execution\Summary\Isolation;
78use Deriver\Deriver\Evaluator\Execution\Trace\Derivation;
79use Deriver\Deriver\Evaluator\Execution\Transfer\CallableTransfer;
80use Deriver\Deriver\Evaluator\Execution\Transfer\MemoryStep;
81use Deriver\Deriver\Evaluator\Execution\Transfer\ObjectAccess;
82use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyAccessCheck;
83use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyLookup;
84use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyReference;
85use Deriver\Deriver\Evaluator\Execution\Transfer\PropertySlot;
86use Deriver\Deriver\Evaluator\Execution\Transfer\PropertyTransfer;
87use Deriver\Deriver\Evaluator\Execution\Transfer\PureStep;
88use Deriver\Deriver\Evaluator\Execution\Transfer\ReferenceAssignment;
89use Deriver\Deriver\Evaluator\Index\DefinitionIndex;
90use Deriver\Deriver\Evaluator\Projection;
91use Deriver\Deriver\Evaluator\Request\Budget;
92use Deriver\Deriver\Evaluator\Request\EntryPoint;
93use Deriver\Deriver\Evaluator\Request\Observation\Observation;
94use Deriver\Deriver\Evaluator\Request\QueryScope;
95use Deriver\Deriver\Evaluator\Request\ReturnQuery;
96use Deriver\Deriver\Evaluator\Request\ValueQuery;
97use Deriver\Deriver\Evaluator\Scalar\Arithmetic;
98use Deriver\Deriver\Evaluator\Scalar\Arrays;
99use Deriver\Deriver\Evaluator\Scalar\Comparison;
100use Deriver\Deriver\Evaluator\Scalar\Identity;
101use Deriver\Deriver\Evaluator\Scalar\NumericString;
102use Deriver\Deriver\Evaluator\Scalar\Operations;
103use Deriver\Deriver\Evaluator\TargetProfile;
104use Deriver\Deriver\Evaluator\Term;
105use Deriver\Deriver\Evaluator\TermKind;
106use Deriver\Deriver\Execution\DerivationResult;
107use Deriver\Deriver\Execution\QueryExecution;
108use Deriver\Deriver\Execution\ResultAssessment;
109use Deriver\Deriver\Identity\QueryEncoding;
110use Deriver\Deriver\Identity\ValueGraph;
111use Deriver\Deriver\Lowerer\AggregateLowering;
112use Deriver\Deriver\Lowerer\AssignmentLowering;
113use Deriver\Deriver\Lowerer\CallLowering;
114use Deriver\Deriver\Lowerer\Control\DestructuringLowering;
115use Deriver\Deriver\Lowerer\Control\ExceptionLowering;
116use Deriver\Deriver\Lowerer\Declaration\CallableIdentity;
117use Deriver\Deriver\Lowerer\Declaration\CallableSource;
118use Deriver\Deriver\Lowerer\Declaration\CallSiteIndex;
119use Deriver\Deriver\Lowerer\Declaration\ConstantSignatures;
120use Deriver\Deriver\Lowerer\Declaration\Index;
121use Deriver\Deriver\Lowerer\Declaration\Indexer;
122use Deriver\Deriver\Lowerer\Declaration\Metadata\ClassDeclaration;
123use Deriver\Deriver\Lowerer\Declaration\Metadata\PropertyDeclaration;
124use Deriver\Deriver\Lowerer\Declaration\Traits\Composition;
125use Deriver\Deriver\Lowerer\Declaration\Traits\LexicalConstants;
126use Deriver\Deriver\Lowerer\Declaration\Traits\Members;
127use Deriver\Deriver\Lowerer\EffectInspection;
128use Deriver\Deriver\Lowerer\ExpressionLowering;
129use Deriver\Deriver\Lowerer\Graph\Argument;
130use Deriver\Deriver\Lowerer\Graph\BasicBlock;
131use Deriver\Deriver\Lowerer\Graph\CallableGraph;
132use Deriver\Deriver\Lowerer\Graph\CatchTarget;
133use Deriver\Deriver\Lowerer\Graph\ExceptionRegion;
134use Deriver\Deriver\Lowerer\Graph\ExpressionRef;
135use Deriver\Deriver\Lowerer\Graph\Instruction;
136use Deriver\Deriver\Lowerer\Graph\Parameter;
137use Deriver\Deriver\Lowerer\Graph\Terminator;
138use Deriver\Deriver\Lowerer\Hierarchy\Access as HierarchyAccess;
139use Deriver\Deriver\Lowerer\Hierarchy\Builtins;
140use Deriver\Deriver\Lowerer\Hierarchy\Dispatch;
141use Deriver\Deriver\Lowerer\InstructionEmitter;
142use Deriver\Deriver\Lowerer\Lowerer;
143use Deriver\Deriver\Lowerer\LoweringScope;
144use Deriver\Deriver\Lowerer\StatementLowering;
145use Deriver\Deriver\MicroEvaluatorConversion;
146use Deriver\Deriver\Parser\Node\File;
147use Deriver\Deriver\Parser\Parser;
148use Deriver\Deriver\Parser\ParserConfig;
149use Deriver\Deriver\Parser\TargetSyntax;
150use Deriver\Deriver\Providers\ProviderExpansion;
151use Deriver\Deriver\Publication\CandidateQuery;
152use Deriver\Deriver\Resolver\AssignmentPatterns;
153use Deriver\Deriver\Resolver\ClassScope;
154use Deriver\Deriver\Resolver\ExpressionValidation;
155use Deriver\Deriver\Resolver\MagicContext;
156use Deriver\Deriver\Resolver\Resolver;
157use Deriver\Deriver\Selection\CallObservations;
158use Deriver\Deriver\Selection\QueryValidation;
159use Deriver\Deriver\SnapshotIdentity;
160use Deriver\Deriver\Sources\LineMap;
161use Deriver\Deriver\Sources\Path as SourcesPath;
162use Deriver\Deriver\Sources\Source;
163use Deriver\Deriver\Sources\SourceRef;
164use Deriver\Deriver\Sources\Sources;
165use Deriver\Exception\ParserException;
166use Deriver\Exception\ResolutionException;
167use JsonException;
168use PhpParser\Node\Expr\ArrowFunction;
169use PhpParser\Node\Expr\BinaryOp\Plus;
170use PhpParser\Node\Expr\Variable;
171use PhpParser\Node\Identifier;
172use PhpParser\Node\IntersectionType;
173use PhpParser\Node\Name;
174use PhpParser\Node\NullableType;
175use PhpParser\Node\Param;
176use PhpParser\Node\Scalar\Int_;
177use PhpParser\Node\Stmt\Function_;
178use PHPUnit\Framework\Attributes\CoversClass;
179use PHPUnit\Framework\Attributes\DataProvider;
180use PHPUnit\Framework\Attributes\Small;
181use PHPUnit\Framework\Attributes\UsesClass;
182use PHPUnit\Framework\TestCase;
183use WeakMap;
184
185#[UsesClass(Resolver::class)]
186#[UsesClass(ExpressionValidation::class)]
187#[UsesClass(ResolutionException::class)]
188#[UsesClass(ParserConfig::class)]
189#[UsesClass(ParserException::class)]
190#[CoversClass(Lowerer::class)]
191#[UsesClass(CallObservations::class)]
192#[UsesClass(QueryExecution::class)]
193#[UsesClass(QueryValidation::class)]
194#[UsesClass(ResultAssessment::class)]
195#[UsesClass(Deriver::class)]
196#[UsesClass(Argument::class)]
197#[UsesClass(BasicBlock::class)]
198#[UsesClass(CallableGraph::class)]
199#[UsesClass(CallableIdentity::class)]
200#[UsesClass(CatchTarget::class)]
201#[UsesClass(ClassDeclaration::class)]
202#[UsesClass(ExceptionRegion::class)]
203#[UsesClass(Instruction::class)]
204#[UsesClass(Parameter::class)]
205#[UsesClass(PropertyDeclaration::class)]
206#[UsesClass(Terminator::class)]
207#[UsesClass(Allocation::class)]
208#[UsesClass(ArgumentBinding::class)]
209#[UsesClass(ArgumentOrder::class)]
210#[UsesClass(CallExecutor::class)]
211#[UsesClass(CallResolution::class)]
212#[UsesClass(CallableCheck::class)]
213#[UsesClass(Access::class)]
214#[UsesClass(Builtins::class)]
215#[UsesClass(Dispatch::class)]
216#[UsesClass(HierarchyAccess::class)]
217#[UsesClass(Invocation::class)]
218#[UsesClass(MethodInvocation::class)]
219#[UsesClass(NativeInvocation::class)]
220#[UsesClass(Properties::class)]
221#[UsesClass(Signatures::class)]
222#[UsesClass(ParameterBinding::class)]
223#[UsesClass(PassedArgument::class)]
224#[UsesClass(Arguments::class)]
225#[UsesClass(Creation::class)]
226#[UsesClass(Methods::class)]
227#[UsesClass(Modes::class)]
228#[UsesClass(Resolution::class)]
229#[UsesClass(Target::class)]
230#[UsesClass(Transfer::class)]
231#[UsesClass(TypeBinding::class)]
232#[UsesClass(TypeCheck::class)]
233#[UsesClass(Completion::class)]
234#[UsesClass(ClassNames::class)]
235#[UsesClass(Context::class)]
236#[UsesClass(ExceptionChain::class)]
237#[UsesClass(ExceptionMatch::class)]
238#[UsesClass(Handler::class)]
239#[UsesClass(ObservationLimit::class)]
240#[UsesClass(StateJoin::class)]
241#[UsesClass(Unwinding::class)]
242#[UsesClass(Discovery::class)]
243#[UsesClass(Dependencies::class)]
244#[UsesClass(InstructionTransfer::class)]
245#[UsesClass(Evaluator::class)]
246#[UsesClass(SlotReference::class)]
247#[UsesClass(StateStorage::class)]
248#[UsesClass(ObservationCollector::class)]
249#[UsesClass(Address::class)]
250#[UsesClass(Path::class)]
251#[UsesClass(Reader::class)]
252#[UsesClass(OffsetTransfer::class)]
253#[UsesClass(Conversions::class)]
254#[UsesClass(ScalarErrors::class)]
255#[UsesClass(State::class)]
256#[UsesClass(Evaluation::class)]
257#[UsesClass(Isolation::class)]
258#[UsesClass(CallableTransfer::class)]
259#[UsesClass(MemoryStep::class)]
260#[UsesClass(ObjectAccess::class)]
261#[UsesClass(PropertyAccessCheck::class)]
262#[UsesClass(PropertyLookup::class)]
263#[UsesClass(PropertyReference::class)]
264#[UsesClass(PropertySlot::class)]
265#[UsesClass(PropertyTransfer::class)]
266#[UsesClass(PureStep::class)]
267#[UsesClass(ReferenceAssignment::class)]
268#[UsesClass(Location::class)]
269#[UsesClass(Materialization::class)]
270#[UsesClass(Memory::class)]
271#[UsesClass(ReferenceConstraint::class)]
272#[UsesClass(StorageCapture::class)]
273#[UsesClass(DispatchDecision::class)]
274#[UsesClass(Extensions::class)]
275#[UsesClass(ProviderExpansion::class)]
276#[UsesClass(Evaluators::class)]
277#[UsesClass(StateRegistry::class)]
278#[UsesClass(DeriverConfig::class)]
279#[UsesClass(EntryPoint::class)]
280#[UsesClass(Sources::class)]
281#[UsesClass(SourcesPath::class)]
282#[UsesClass(Snapshot::class)]
283#[UsesClass(Source::class)]
284#[UsesClass(TargetProfile::class)]
285#[UsesClass(Budget::class)]
286#[UsesClass(QueryScope::class)]
287#[UsesClass(ReturnQuery::class)]
288#[UsesClass(ValueQuery::class)]
289#[UsesClass(ExpressionRef::class)]
290#[UsesClass(Observation::class)]
291#[UsesClass(ResultRef::class)]
292#[UsesClass(SourceRef::class)]
293#[UsesClass(Alternative::class)]
294#[UsesClass(Assessment::class)]
295#[UsesClass(Derivation::class)]
296#[UsesClass(DerivationResult::class)]
297#[UsesClass(QueryEncoding::class)]
298#[UsesClass(ValueGraph::class)]
299#[UsesClass(Statistics::class)]
300#[UsesClass(StorageSnapshot::class)]
301#[UsesClass(Parser::class)]
302#[UsesClass(File::class)]
303#[UsesClass(AggregateLowering::class)]
304#[UsesClass(AssignmentLowering::class)]
305#[UsesClass(CallLowering::class)]
306#[UsesClass(DestructuringLowering::class)]
307#[UsesClass(ExceptionLowering::class)]
308#[UsesClass(EffectInspection::class)]
309#[UsesClass(ExpressionLowering::class)]
310#[UsesClass(InstructionEmitter::class)]
311#[UsesClass(LoweringScope::class)]
312#[UsesClass(StatementLowering::class)]
313#[UsesClass(ConstantSignatures::class)]
314#[UsesClass(CallSiteIndex::class)]
315#[UsesClass(CallableSource::class)]
316#[UsesClass(Indexer::class)]
317#[UsesClass(Index::class)]
318#[UsesClass(Composition::class)]
319#[UsesClass(LexicalConstants::class)]
320#[UsesClass(Members::class)]
321#[UsesClass(LineMap::class)]
322#[UsesClass(MagicContext::class)]
323#[UsesClass(AssignmentPatterns::class)]
324#[UsesClass(ClassScope::class)]
325#[UsesClass(TargetSyntax::class)]
326#[UsesClass(Arithmetic::class)]
327#[UsesClass(Arrays::class)]
328#[UsesClass(Comparison::class)]
329#[UsesClass(Identity::class)]
330#[UsesClass(NumericString::class)]
331#[UsesClass(Operations::class)]
332#[UsesClass(Projection::class)]
333#[UsesClass(Term::class)]
334#[Small]
335final class LowererTest extends TestCase
336{
337    public function testCompilePreservesTheSemanticContract(): void
338    {
339        $source = '<?php function target() {$x="first";$a=fn()=>$x;$b=function()use(&$x){return $x;};$x="second";return [$a(),$b()];}';
340        $symbol = 'target';
341        $input = new Sources([new Source('fixture.php', $source)]);
342        $configuration = new DeriverConfig();
343        $contributions = new Contributions();
344        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
345        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
346        $contributions1 = $providers->contributions;
347        $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);
348        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
349        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
350        $snapshotId = $identity->id;
351        $input1 = $providers->input;
352        $parser = new Parser(new ParserConfig($target->version));
353        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
354        $program = new Index($snapshotId, $trees);
355        $result = (new QueryExecution($program, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program->diagnostics()))))->derive(new ReturnQuery($symbol));
356        self::assertSame(['first', 'second'], $result->normalOutcomes[0]->values['return']->native());
357        self::assertSame([], $result->frontiers);
358        self::assertSame([], $result->exceptionalOutcomes);
359    }
360    public function testParametersCapturesReferenceAndDefaultExpressionSeparately(): void
361    {
362        $source1 = '<?php function target(int &$n = 2, ...$rest) {}';
363        $snapshotId = 'test';
364        $input = new Sources([new Source('fixture.php', $source1)]);
365        $profile = new TargetProfile();
366        $parser = new Parser(new ParserConfig($profile->version));
367        $trees = array_map(static fn (Source $source2): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source2->path), $source2->contents, $source2->declarationsOnly))), iterator_to_array($input, false));
368        $index = new Index($snapshotId, $trees);
369        $source = $index->declarations['target'];
370        self::assertInstanceOf(Function_::class, $source->node);
371        $parameters = (new Lowerer($index))->parameters($source->node, $source);
372        self::assertTrue($parameters[0]->byReference);
373        self::assertNotNull($parameters[0]->default);
374        self::assertSame(2, $parameters[0]->default->blocks[0]->instructions[0]->constant?->native());
375        self::assertTrue($parameters[1]->variadic);
376    }
377    public function testExpressionProducesAnIndependentReturnGraph(): void
378    {
379        $source = '<?php function target(){return 1;}';
380        $snapshotId = 'test';
381        $input = new Sources([new Source('fixture.php', $source)]);
382        $profile = new TargetProfile();
383        $parser = new Parser(new ParserConfig($profile->version));
384        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
385        $compiler = new Lowerer(new Index($snapshotId, $trees));
386        $body = $compiler->expression(new Int_(7), 'fixture.php', 'default:test');
387        self::assertSame('default:test', $body->symbol);
388        self::assertSame(7, $body->blocks[0]->instructions[0]->constant?->native());
389        self::assertSame($body->blocks[0]->instructions[0]->result, $body->blocks[0]->terminator->operand);
390    }
391    public function testTypePreservesNullableUnionsAndIntersections(): void
392    {
393        $source = '<?php function target(){return 1;}';
394        $snapshotId = 'test';
395        $input = new Sources([new Source('fixture.php', $source)]);
396        $profile = new TargetProfile();
397        $parser = new Parser(new ParserConfig($profile->version));
398        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
399        $compiler = new Lowerer(new Index($snapshotId, $trees));
400        self::assertSame('int|null', $compiler->type(new NullableType(new Identifier('int'))));
401        self::assertSame('A&B', $compiler->type(new IntersectionType([new Name('A'), new Name('B')])));
402        self::assertSame('mixed', $compiler->type(null));
403    }
404    public function testCapturesExcludesArrowParameters(): void
405    {
406        $arrow = new ArrowFunction(['params' => [new Param(new Variable('x'))], 'expr' => new Plus(new Variable('x'), new Variable('outside'))]);
407        $source = '<?php function target(){return 1;}';
408        $snapshotId = 'test';
409        $input = new Sources([new Source('fixture.php', $source)]);
410        $profile = new TargetProfile();
411        $parser = new Parser(new ParserConfig($profile->version));
412        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
413        self::assertSame(['outside' => false], (new Lowerer(new Index($snapshotId, $trees)))->captures($arrow));
414    }
415    public function testFreeVariablesDeduplicatesLexicalReads(): void
416    {
417        $node = new Plus(new Variable('x'), new Variable('x'));
418        $source = '<?php function target(){return 1;}';
419        $snapshotId = 'test';
420        $input = new Sources([new Source('fixture.php', $source)]);
421        $profile = new TargetProfile();
422        $parser = new Parser(new ParserConfig($profile->version));
423        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input, false));
424        self::assertSame(['x' => false], (new Lowerer(new Index($snapshotId, $trees)))->freeVariables($node));
425    }
426    public function testCompileOmitsAnExternalImplementationButRetainsItsSignature(): void
427    {
428        $snapshotId = 'test';
429        $input = new Sources([new Source('stub.php', '<?php function sample(int &$value): int {return 99;}', true)]);
430        $profile = new TargetProfile();
431        $parser = new Parser(new ParserConfig($profile->version));
432        $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input, false));
433        $index = new Index($snapshotId, $trees);
434        $body = (new Lowerer($index))->compile($index->declarations['sample']);
435        self::assertTrue($body->external);
436        self::assertTrue($body->parameters[0]->byReference);
437        self::assertSame(['external-body'], array_column($body->blocks[0]->instructions, 'operation'));
438    }
439    /**
440     * @param string $source Trusted default initializer fixture
441     * @param string $expectedJson Independently observed PHP 8.3 result
442     * @throws JsonException If expected fixture data cannot be decoded
443     */
444    #[DataProvider('providerInitializers')]
445    public function testParametersPreservesTheDeclaringScalarModeForNewDefaults(string $source, string $expectedJson): void
446    {
447        $symbol = 'target';
448        $input = new Sources([new Source('fixture.php', $source)]);
449        $configuration = new DeriverConfig();
450        $contributions = new Contributions();
451        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
452        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
453        $contributions1 = $providers->contributions;
454        $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);
455        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
456        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
457        $snapshotId = $identity->id;
458        $input1 = $providers->input;
459        $parser = new Parser(new ParserConfig($target->version));
460        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
461        $program = new Index($snapshotId, $trees);
462        $result = (new QueryExecution($program, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program->diagnostics()))))->derive(new ReturnQuery($symbol));
463        self::assertSame([], $result->frontiers);
464        self::assertSame([], $result->exceptionalOutcomes);
465        self::assertCount(1, $result->normalOutcomes);
466        self::assertSame(json_decode($expectedJson, true, 512, JSON_THROW_ON_ERROR), $result->normalOutcomes[0]->values['return']->native());
467    }
468    /**
469     * @return array<string,array{string,string}>
470     */
471    public static function providerInitializers(): array
472    {
473        return ['strict function default' => ['<?php declare(strict_types=1);class Box{function __construct(public int $value){}}function helper($box=new Box("7")){return $box->value;}function target(){try{return helper();}catch(TypeError $e){return "error";}}', '"error"'], 'weak function default' => ['<?php class Box{function __construct(public int $value){}}function helper($box=new Box("7")){return $box->value;}function target(){return helper();}', '7'], 'strict method default' => ['<?php declare(strict_types=1);class Box{function __construct(public int $value){}}class Factory{function make($box=new Box("7")){return $box->value;}}function target(){try{$f=new Factory;return $f->make();}catch(TypeError $e){return "error";}}', '"error"'], 'strict static method default' => ['<?php declare(strict_types=1);class Box{function __construct(public int $value){}}class Factory{static function make($box=new Box("7")){return $box->value;}}function target(){try{return Factory::make();}catch(TypeError $e){return "error";}}', '"error"'], 'strict closure default' => ['<?php declare(strict_types=1);class Box{function __construct(public int $value){}}function target(){$f=function($box=new Box("7")){return $box->value;};try{return $f();}catch(TypeError $e){return "error";}}', '"error"'], 'strict arrow default' => ['<?php declare(strict_types=1);class Box{function __construct(public int $value){}}function target(){$f=fn($box=new Box("7"))=>$box->value;try{return $f();}catch(TypeError $e){return "error";}}', '"error"'], 'strict promoted default' => ['<?php declare(strict_types=1);class Box{function __construct(public int $value){}}class Holder{function __construct(public Box $box=new Box("7")){}}function target(){try{$h=new Holder;return $h->box->value;}catch(TypeError $e){return "error";}}', '"error"'], 'supplied argument skips invalid default' => ['<?php declare(strict_types=1);class Box{function __construct(public int $value){}}function helper($box=new Box("7")){return $box->value;}function target(){return helper(new Box(9));}', '9'], 'fresh default each invocation' => ['<?php class Box{}function helper($box=new Box){return $box;}function target(){$a=helper();$b=helper();return $a!==$b;}', 'true'], 'default fails before constructor body' => ['<?php declare(strict_types=1);class Box{function __construct(int $value){$GLOBALS["calls"]++;}}function helper($box=new Box("7")){return 9;}function target(){$GLOBALS["calls"]=0;try{helper();}catch(TypeError $e){return $GLOBALS["calls"];}return 99;}', '0']];
474    }
475    /**
476     * @param string $library Captured declaration file
477     * @param string $application Captured caller file
478     * @param string $expectedJson PHP 8.3 observation
479     * @throws JsonException If captured fixture data cannot be encoded
480     */
481    #[DataProvider('providerInitializerFiles')]
482    public function testParametersUsesTheDeclarationFileModeAcrossCallerFiles(string $library, string $application, string $expectedJson): void
483    {
484        $prepared1Input = new Sources([new Source('library.php', $library), new Source('app.php', $application)]);
485        $prepared1Configuration = new DeriverConfig();
486        $prepared1Contributions = new Contributions();
487        $prepared1Target = new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision);
488        $prepared1Providers = new ProviderExpansion($prepared1Input, array_map(Term::fromNative(...), $prepared1Configuration->environment), $prepared1Contributions);
489        $contributions = $prepared1Providers->contributions;
490        $prepared1Evaluators = new Evaluators(new TargetProfile($prepared1Configuration->phpVersion, $prepared1Configuration->integerBits, $prepared1Configuration->floatPrecision), new Registrations($contributions->evaluators, $contributions->providers, $contributions->domains, $contributions->intrinsics, $contributions->stateSlots), $prepared1Configuration->standardEvaluators);
491        $prepared1Settings = new EvaluatorConfig($prepared1Target, $prepared1Providers->environment, $prepared1Configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $prepared1Configuration->evaluators));
492        $prepared1Identity = new SnapshotIdentity($prepared1Providers->input, $prepared1Settings, $prepared1Evaluators, $prepared1Providers->manifest);
493        $snapshotId = $prepared1Identity->id;
494        $input = $prepared1Providers->input;
495        $parser = new Parser(new ParserConfig($prepared1Target->version));
496        $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input, false));
497        $prepared1Program = new Index($snapshotId, $trees);
498        $prepared1Snapshot = $prepared1Identity->snapshot(array_map(Frontier::fromDiagnostic(...), $prepared1Program->diagnostics()));
499        $session = new CandidateQuery(new DefinitionIndex($prepared1Program), $prepared1Settings, $prepared1Evaluators, $prepared1Snapshot);
500        $result = $session->derive(new ReturnQuery('target'));
501        $frontiers = array_reduce(iterator_to_array($result, false), static function (array $frontiers, Candidate $candidate1): array {
502            if ($candidate1->result instanceof Term) {
503                $graph = $candidate1->result;
504                $snapshot = $candidate1->evidence[0]->snapshot;
505                $pending = [$graph];
506                $seen = new WeakMap();
507                $frontiers1 = [];
508                while ($pending !== []) {
509                    $node = array_pop($pending);
510                    if (isset($seen[$node])) {
511                        continue;
512                    }
513                    $seen[$node] = true;
514                    array_push($pending, ...array_values($node->operands));
515                    $reason = $node->attributes['reason'] ?? null;
516                    if (!is_string($reason)) {
517                        continue;
518                    }
519                    $source1 = new SourceRef($snapshot->id, (string) ($node->attributes['source'] ?? ''), (int) ($node->attributes['start'] ?? 0), (int) ($node->attributes['end'] ?? $node->attributes['start'] ?? 0));
520                    $identity = (string) ($node->attributes['identity'] ?? (new Identity())->key($node));
521                    $frontiers1[$identity] = new Frontier($reason, $source1, (string) $node->literal, ['value'], [$identity], $node, $reason);
522                }
523                array_push($frontiers, ...array_values($frontiers1));
524            }
525            return $frontiers;
526        }, []);
527        self::assertSame([], $frontiers);
528        self::assertSame([], array_filter(iterator_to_array($result, false), static fn ($candidate): bool => $candidate->type_name === 'never'));
529        self::assertCount(1, $result);
530        self::assertSame(json_decode($expectedJson, true, 512, JSON_THROW_ON_ERROR), $result[0]->result);
531    }
532    /**
533     * @return array<string,array{string,string,string}>
534     */
535    public static function providerInitializerFiles(): array
536    {
537        $result = [];
538        foreach ([0, 1] as $declaration) {
539            foreach ([0, 1] as $caller) {
540                $library = '<?php declare(strict_types=' . $declaration . ');class Box{function __construct(public int $value){}}function helper($box=new Box("7")){return $box->value;}';
541                $application = '<?php declare(strict_types=' . $caller . ');function target(){try{return helper();}catch(TypeError $e){return "error";}}';
542                $result['declaration ' . $declaration . ' caller ' . $caller] = [$library, $application, $declaration === 1 ? '"error"' : '7'];
543            }
544        }
545        return $result;
546    }
547    /**
548     * @param string $source Trusted promoted-property fixture
549     * @param string $expectedJson PHP 8.3 observation
550     * @throws JsonException If fixture data cannot be decoded
551     */
552    #[DataProvider('providerPromotedReferences')]
553    public function testPromotionsPreserveAliasesAndReadonlyErrors(string $source, string $expectedJson): void
554    {
555        $symbol = 'target';
556        $input = new Sources([new Source('fixture.php', $source)]);
557        $configuration = new DeriverConfig();
558        $contributions = new Contributions();
559        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
560        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
561        $contributions1 = $providers->contributions;
562        $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);
563        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
564        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
565        $snapshotId = $identity->id;
566        $input1 = $providers->input;
567        $parser = new Parser(new ParserConfig($target->version));
568        $trees = array_map(static fn (Source $source1): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source1->path), $source1->contents, $source1->declarationsOnly))), iterator_to_array($input1, false));
569        $program = new Index($snapshotId, $trees);
570        $result = (new QueryExecution($program, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program->diagnostics()))))->derive(new ReturnQuery($symbol));
571        self::assertSame([], $result->frontiers);
572        self::assertSame([], $result->exceptionalOutcomes);
573        self::assertCount(1, $result->normalOutcomes);
574        self::assertSame(json_decode($expectedJson, true, 512, JSON_THROW_ON_ERROR), $result->normalOutcomes[0]->values['return']->native());
575    }
576    /**
577     * @return array<string,array{string,string}>
578     */
579    public static function providerPromotedReferences(): array
580    {
581        return ['caller write reaches property' => ['<?php class Box{function __construct(public int &$value){}}function target(){$x=1;$b=new Box($x);$x=2;return $b->value;}', '2'], 'property write reaches caller' => ['<?php class Box{function __construct(public int &$value){}}function target(){$x=1;$b=new Box($x);$b->value=2;return $x;}', '2'], 'constructor write reaches property' => ['<?php class Box{function __construct(public int &$value){$value=3;}}function target(){$x=1;$b=new Box($x);return [$x,$b->value];}', '[3,3]'], 'private promotion keeps alias' => ['<?php class Box{function __construct(private int &$value){}function read(){return $this->value;}}function target(){$x=1;$b=new Box($x);$x=4;return $b->read();}', '4'], 'protected promotion keeps alias' => ['<?php class Box{function __construct(protected int &$value){}function read(){return $this->value;}}function target(){$x=1;$b=new Box($x);$x=4;return $b->read();}', '4'], 'clone keeps shared reference' => ['<?php class Box{function __construct(public int &$value){}}function target(){$x=1;$a=new Box($x);$b=clone $a;$x=5;return [$a->value,$b->value];}', '[5,5]'], 'reference enforces live property type' => ['<?php class Box{function __construct(public int &$value){}}function target(){$x=1;$b=new Box($x);try{$x=[];}catch(TypeError $e){return [$x,$b->value];}return 99;}', '[1,1]'], 'unset removes property constraint' => ['<?php class Box{function __construct(public int &$value){}}function target(){$x=1;$b=new Box($x);unset($b->value);$x=[];return $x;}', '[]'], 'by value promotion stays independent' => ['<?php class Box{function __construct(public int $value){}}function target(){$x=1;$b=new Box($x);$x=2;return $b->value;}', '1'], 'reference readonly promotion rejected' => ['<?php class Box{function __construct(public readonly int &$value){}}function target(){$x=1;try{$b=new Box($x);return 99;}catch(Error $e){return $x;}}', '1'], 'reference readonly class rejected' => ['<?php readonly class Box{function __construct(public int &$value){}}function target(){$x=1;try{$b=new Box($x);return 99;}catch(Error $e){return $x;}}', '1'], 'array promotion keeps alias' => ['<?php class Box{function __construct(public array &$items){}}function target(){$x=[1];$b=new Box($x);$b->items[]=2;return $x;}', '[1,2]'], 'reference default aliases parameter and property' => ['<?php class Box{function __construct(public int &$value=1){$value=2;}}function target(){$b=new Box;return $b->value;}', '2'], 'reference named argument' => ['<?php class Box{function __construct(public int &$value){}}function target(){$x=1;$b=new Box(value:$x);$x=6;return $b->value;}', '6'], 'weak coercion precedes readonly failure' => ['<?php class Box{function __construct(public readonly int &$value){}}function target(){$x="1";try{$b=new Box($x);return 99;}catch(Error $e){return $x;}}', '1'], 'readonly promotion cannot reinitialize' => ['<?php class Box{function __construct(public readonly int $x){}}function target(){$b=new Box(1);try{$b->__construct(2);}catch(Error $e){return $b->x;}return 99;}', '1'], 'all parameter checks precede promotion' => ['<?php class Box{function __construct(public int $x,string $y){}}function target(){$b=new Box(1,"a");try{$b->__construct(2,[]);}catch(TypeError $e){return $b->x;}return 99;}', '1'], 'all coercions precede readonly failure' => ['<?php class Box{function __construct(public readonly int &$x,int &$y){}}function target(){$x=1;$y="2";try{new Box($x,$y);}catch(Error $e){return $y;}return 99;}', '2'], 'repeated promotion rebinds reference' => ['<?php class Box{function __construct(public int &$x){}}function target(){$x=1;$y=2;$b=new Box($x);$b->__construct($y);$x=3;$y=4;return [$x,$b->x];}', '[3,4]'], 'promotion reads latest coerced shared argument' => ['<?php class Box{function __construct(public &$x,float &$y){}}function target(){$v=1;$b=new Box($v,$v);return [$b->x,$v];}', '[1.0,1.0]'], 'parent constructor private promotion' => ['<?php class A{function __construct(private int &$x){}function read(){return $this->x;}}class B extends A{}function target(){$x=1;$b=new B($x);$x=9;return $b->read();}', '9'], 'trait constructor reference promotion' => ['<?php trait T{function __construct(private int &$x){}function read(){return $this->x;}}class B{use T;}function target(){$x=1;$b=new B($x);$x=9;return $b->read();}', '9']];
582    }
583    /**
584     * @param string $reference \Deriver\Deriver\Lowerer\Graph\Parameter passing syntax
585     * @param TermKind $expectedKind Expected selected value category
586     * @param int|string $expectedLiteral Preserved symbolic identity or updated value
587     */
588    #[DataProvider('providerSymbolicPromotions')]
589    public function testPromotionsInitializeSymbolicConstructorObservations(string $reference, TermKind $expectedKind, int|string $expectedLiteral): void
590    {
591        $input = new Sources([new Source('fixture.php', '<?php function sink($v){}class Box{function __construct(public int ' . $reference . '$value){$value=2;sink($this->value);}}')]);
592        $configuration = new DeriverConfig();
593        $contributions = new Contributions();
594        $target = new TargetProfile($configuration->phpVersion, $configuration->integerBits, $configuration->floatPrecision);
595        $providers = new ProviderExpansion($input, array_map(Term::fromNative(...), $configuration->environment), $contributions);
596        $contributions1 = $providers->contributions;
597        $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);
598        $settings = new EvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators, array_map((new MicroEvaluatorConversion())->convert(...), $configuration->evaluators));
599        $identity = new SnapshotIdentity($providers->input, $settings, $evaluators, $providers->manifest);
600        $snapshotId = $identity->id;
601        $input1 = $providers->input;
602        $parser = new Parser(new ParserConfig($target->version));
603        $trees = array_map(static fn (Source $source): File => (new Resolver())->resolve($parser->parse(new Source(SourcesPath::normalize($source->path), $source->contents, $source->declarationsOnly))), iterator_to_array($input1, false));
604        $program = new Index($snapshotId, $trees);
605        $session = new QueryExecution($program, new ExecutionEvaluatorConfig($target, $providers->environment, $configuration->standardEvaluators), $evaluators, $identity->snapshot(array_map(Frontier::fromDiagnostic(...), $program->diagnostics())));
606        $result = $session->derive(new ValueQuery((new CallObservations($session->program, $session->evaluators))->find('sink')[0]->argument(0)));
607        self::assertSame([], $result->frontiers);
608        self::assertSame([], $result->exceptionalOutcomes);
609        $values = array_column(array_column($result->normalOutcomes, 'values'), 'value');
610        self::assertNotEmpty($values);
611        self::assertSame(array_fill(0, count($values), $expectedKind), array_column($values, 'kind'));
612        self::assertSame([$expectedLiteral], array_values(array_unique(array_column($values, 'literal'))));
613    }
614    /**
615     * @return array<string,array{string,TermKind,int|string}>
616     */
617    public static function providerSymbolicPromotions(): array
618    {
619        return ['by value' => ['', TermKind::Parameter, 'value'], 'by reference' => ['&', TermKind::Constant, 2]];
620    }
621}
622