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