packages/deriver/tests/Unit/Deriver/Evaluator/Call/Plan/PlanFootprintsTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Call\Plan;
6
7use Deriver\Deriver\Evaluator\Call\Plan\Action;
8use Deriver\Deriver\Evaluator\Call\Plan\CallArgument;
9use Deriver\Deriver\Evaluator\Call\Plan\Expression;
10use Deriver\Deriver\Evaluator\Call\Plan\Location\LocationRef;
11use Deriver\Deriver\Evaluator\Call\Plan\PlanFootprints;
12use Deriver\Deriver\Evaluator\Call\Plan\PlanValidation;
13use Deriver\Deriver\Evaluator\Declaration\Parameter;
14use Deriver\Deriver\Evaluator\Declaration\Signature;
15use Deriver\Deriver\Evaluator\Term;
16use PHPUnit\Framework\Attributes\CoversClass;
17use PHPUnit\Framework\Attributes\Small;
18use PHPUnit\Framework\Attributes\UsesClass;
19use PHPUnit\Framework\TestCase;
20
21/**
22 * @covers \Deriver\Deriver\Evaluator\Call\Plan\PlanFootprints
23 */
24#[CoversClass(PlanFootprints::class)]
25#[UsesClass(LocationRef::class)]
26#[UsesClass(PlanValidation::class)]
27#[UsesClass(Action::class)]
28#[UsesClass(CallArgument::class)]
29#[UsesClass(Expression::class)]
30#[UsesClass(Parameter::class)]
31#[UsesClass(Signature::class)]
32#[UsesClass(Term::class)]
33#[Small]
34final class PlanFootprintsTest extends TestCase
35{
36    public function testActionRejectsReferenceResultOnAllocation(): void
37    {
38        $validation = new PlanValidation();
39        (new PlanFootprints($validation))->action(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Allocate, referenceResult: true));
40        self::assertContains('invalid-reference-result', $validation->errors);
41    }
42    public function testLocationRecordsContainerReadBeforeElementWrite(): void
43    {
44        $validation = new PlanValidation();
45        (new PlanFootprints($validation))->location(LocationRef::element(LocationRef::state('domain.items'), Expression::literal(Term::constant('key'))), true);
46        self::assertSame(['domain.items'], $validation->reads);
47        self::assertSame(['domain.items'], $validation->writes);
48    }
49    public function testLocationRejectsContradictoryFields(): void
50    {
51        $validation = new PlanValidation();
52        (new PlanFootprints($validation))->location(new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Parameter, 'x', parent: LocationRef::parameter('other')), true);
53        self::assertSame(['invalid-location:parameter'], $validation->errors);
54    }
55    public function testExternalDistinguishesReferenceParametersAndLocals(): void
56    {
57        $validation = new PlanValidation();
58        $validation->signature = new Signature([new Parameter('value', byReference: true), new Parameter('copy')]);
59        $footprints = new PlanFootprints($validation);
60        self::assertTrue($footprints->external('value'));
61        self::assertFalse($footprints->external('copy'));
62        self::assertFalse($footprints->external('result'));
63    }
64    public function testShapeRejectsAnUnknownKindEvenWhenItHasAName(): void
65    {
66        $footprints = new PlanFootprints(new PlanValidation());
67        self::assertFalse($footprints->shape(new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Parameter, 'name', receiver: Expression::receiver())));
68        self::assertTrue($footprints->shape(LocationRef::parameter('name')));
69    }
70    public function testCompletionRequiresAStorageReturnForReferenceSignatures(): void
71    {
72        $validation = new PlanValidation();
73        $validation->signature = new Signature(byReference: true);
74        (new PlanFootprints($validation))->completion(Action::returns(Expression::literal(Term::constant(1))));
75        self::assertContains('reference-return-requires-location', $validation->errors);
76    }
77
78    /**
79     * @param LocationRef $location
80     * @param bool $expected
81     */
82    #[\PHPUnit\Framework\Attributes\DataProvider('providerLocationShapes')]
83    public function testShapeRequiresTheFieldsOfItsOwnAddressKind(LocationRef $location, bool $expected): void
84    {
85        self::assertSame($expected, (new PlanFootprints(new PlanValidation()))->shape($location));
86    }
87
88    /**
89     * @return list<array{LocationRef, bool}>
90     */
91    public static function providerLocationShapes(): array
92    {
93        $value = Expression::parameter('value');
94        $parent = LocationRef::parameter('array');
95        return [
96            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Parameter), false],
97            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Parameter, 'x', receiver: $value), false],
98            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Parameter, 'x', key: $value), false],
99            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, receiver: $value), false],
100            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, 'example.slot'), false],
101            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, 'example.slot', $value), true],
102            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, 'example.slot', $value, $parent), false],
103            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, 'example.slot', $value, key: $value), false],
104            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Element), false],
105            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Element, parent: $parent), true],
106            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Element, 'name', parent: $parent), false],
107            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Element, receiver: $value, parent: $parent), false],
108            [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Element, parent: $parent, key: $value), true],
109        ];
110    }
111
112    public function testExternalKeepsUndeclaredLocalsPrivateAndLegacyParametersExternal(): void
113    {
114        $footprints = new PlanFootprints(new PlanValidation());
115        self::assertTrue($footprints->external('value'));
116        self::assertFalse($footprints->external('@result'));
117    }
118
119    public function testLocationTracksReadOnlyNestedReferencesAndAppendWrites(): void
120    {
121        $validation = new PlanValidation();
122        $footprints = new PlanFootprints($validation);
123        $footprints->location(LocationRef::element(LocationRef::state('example.items')), false);
124        $footprints->location(LocationRef::parameter('input'), false);
125        $footprints->location(LocationRef::parameter('@temporary'), true);
126        self::assertSame(['example.items'], $validation->reads);
127        self::assertSame([], $validation->writes);
128        self::assertSame([], $validation->errors);
129        $footprints->location(LocationRef::parameter('input'), true);
130        self::assertSame(['parameter:input'], $validation->writes);
131    }
132
133    public function testLocationValidatesKeyExpressionsBeforeCompilation(): void
134    {
135        $validation = new PlanValidation();
136        (new PlanFootprints($validation))->location(LocationRef::element(LocationRef::parameter('items'), new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Cast)), false);
137        self::assertSame(['expression-arity-or-opcode:cast'], $validation->errors);
138    }
139
140    public function testLocationEnforcesTheExactPlanNodeAllowance(): void
141    {
142        $validation = new PlanValidation();
143        $validation->nodes = 19999;
144        $footprints = new PlanFootprints($validation);
145        $footprints->location(LocationRef::parameter('input'), false);
146        self::assertSame([], $validation->errors);
147        $footprints->location(LocationRef::parameter('input'), false);
148        self::assertSame(['plan-node-budget'], $validation->errors);
149    }
150
151    public function testActionKeepsInvocationArgumentsAndTheirReferenceEffects(): void
152    {
153        $validation = new PlanValidation();
154        $arguments = [new CallArgument(LocationRef::parameter('value')), new CallArgument(new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::State, 'example.slot', [Expression::receiver()]))];
155        (new PlanFootprints($validation))->action(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Invoke, arguments: $arguments, referenceResult: true));
156        self::assertSame(['parameter:value'], $validation->writes);
157        self::assertSame(['example.slot'], $validation->reads);
158        self::assertSame([], $validation->errors);
159    }
160
161    public function testActionRejectsNamedUnpackAndBranchesOnAnOrdinaryEffect(): void
162    {
163        $validation = new PlanValidation();
164        $value = Expression::parameter('value');
165        $footprints = new PlanFootprints($validation);
166        $footprints->action(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Invoke, arguments: [new CallArgument($value, 'named', true)], no: [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Return)]));
167        self::assertSame(['unexpected-action-branches', 'named-unpack'], $validation->errors);
168    }
169
170    public function testActionRejectsArgumentsAndLocationsOnUnrelatedActions(): void
171    {
172        $validation = new PlanValidation();
173        $value = Expression::parameter('value');
174        $footprints = new PlanFootprints($validation);
175        $footprints->action(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Return, arguments: [new CallArgument($value)]));
176        $footprints->action(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Return, locations: [LocationRef::parameter('@local')]));
177        self::assertSame(['action-location-or-arguments:return', 'action-location-or-arguments:return'], $validation->errors);
178    }
179
180    public function testCompletionKeepsOnlyDeclaredReferenceReturnsAndHavocThrows(): void
181    {
182        $validation = new PlanValidation();
183        $footprints = new PlanFootprints($validation);
184        $footprints->completion(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Havoc, mayThrow: true));
185        $footprints->completion(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Invoke, referenceResult: true));
186        self::assertSame([], $validation->errors);
187        $footprints->completion(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Return, mayThrow: true));
188        $validation->signature = new Signature();
189        $footprints->completion(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::ReturnReference));
190        self::assertSame(['invalid-havoc-completion', 'undeclared-reference-return'], $validation->errors);
191    }
192}
193