packages/deriver/tests/Unit/Deriver/Evaluator/Call/Plan/PlanValidationTest.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\Expression;
9use Deriver\Deriver\Evaluator\Call\Plan\Location\LocationRef;
10use Deriver\Deriver\Evaluator\Call\Plan\PlanValidation;
11use Deriver\Deriver\Evaluator\Call\Plan\SemanticPlan;
12use Deriver\Deriver\Evaluator\Call\State\StateSlot;
13use Deriver\Deriver\Evaluator\Declaration\Signature;
14use Deriver\Deriver\Evaluator\Term;
15use PHPUnit\Framework\Attributes\CoversClass;
16use PHPUnit\Framework\Attributes\Small;
17use PHPUnit\Framework\Attributes\UsesClass;
18use PHPUnit\Framework\TestCase;
19
20/**
21 * @covers \Deriver\Deriver\Evaluator\Call\Plan\PlanValidation
22 */
23#[CoversClass(PlanValidation::class)]
24#[UsesClass(LocationRef::class)]
25#[UsesClass(\Deriver\Deriver\Evaluator\Call\Plan\PlanFootprints::class)]
26#[UsesClass(Action::class)]
27#[UsesClass(Expression::class)]
28#[UsesClass(SemanticPlan::class)]
29#[UsesClass(Signature::class)]
30#[UsesClass(StateSlot::class)]
31#[UsesClass(Term::class)]
32#[Small]
33final class PlanValidationTest extends TestCase
34{
35 public function testValidateRejectsUndeclaredStateWrites(): void
36 {
37 $plan = new SemanticPlan([Action::write('example.slot', Expression::literal(Term::constant(1)))]);
38 self::assertContains('undeclared-state-write', (new PlanValidation())->validate($plan));
39 }
40 public function testActionsRejectsSurplusOperandsBeforeTheyReachTheSolver(): void
41 {
42 $validation = new PlanValidation();
43 $validation->actions([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Return, [Expression::parameter('first'), Expression::parameter('second')])]);
44 self::assertSame(['action-arity-or-opcode:return'], $validation->errors);
45 }
46 public function testExpressionRejectsMissingLiteralPayloads(): void
47 {
48 $validation = new PlanValidation();
49 $validation->expression(new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Constant));
50 self::assertSame(['missing-constant'], $validation->errors);
51 }
52 public function testValidateRejectsMissingLocationBeforeCompilation(): void
53 {
54 $plan = new SemanticPlan([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::LocationWrite, [Expression::literal(Term::constant(1))])]);
55 self::assertContains('action-location-or-arguments:location-write', (new PlanValidation())->validate($plan));
56 }
57 public function testExpressionRejectsMissingLocationPayload(): void
58 {
59 $validation = new PlanValidation();
60 $validation->expression(new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::LocationRead));
61 self::assertContains('missing-location', $validation->errors);
62 }
63 public function testCompletesRequiresAllReferenceReturnBranches(): void
64 {
65 $validation = new PlanValidation();
66 $return = Action::returnReference(LocationRef::parameter('value'));
67 $condition = Expression::parameter('condition');
68 self::assertFalse($validation->completes([Action::choice($condition, [$return], [])]));
69 self::assertTrue($validation->completes([Action::choice($condition, [$return], [$return])]));
70 }
71
72 /**
73 * @param Action $action
74 * @param list<string> $writes
75 */
76 #[\PHPUnit\Framework\Attributes\DataProvider('providerValidActions')]
77 public function testValidateAcceptsSupportedActionsWithTheirDeclaredEffects(Action $action, array $writes): void
78 {
79 self::assertSame([], (new PlanValidation())->validate(new SemanticPlan([$action], writes: $writes)));
80 }
81
82 /**
83 * @return list<array{Action, list<string>}>
84 */
85 public static function providerValidActions(): array
86 {
87 $value = Expression::literal(Term::constant(1));
88 $local = LocationRef::parameter('@value');
89 return [
90 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Return, [$value]), []],
91 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Throw, [$value]), []],
92 [Action::write('example.slot', $value), ['example.slot']],
93 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::WriteParameter, [$value], 'value'), ['parameter:value']],
94 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::WriteParameter, [$value], '@local'), []],
95 [Action::choice($value, [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::WriteParameter, [$value], 'yes')], [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::WriteParameter, [$value], 'no')]), ['parameter:yes', 'parameter:no']],
96 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Callback, [$value]), []],
97 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Callback, [$value, $value]), []],
98 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Invoke, [$value]), []],
99 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Allocate, [$value]), []],
100 [Action::assign($local, $value), []],
101 [Action::alias($local, $local), []],
102 [Action::returnReference($local), []],
103 [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Havoc, locations: [$local], mayThrow: true), []],
104 ];
105 }
106
107 /**
108 * @param string $operation
109 */
110 #[\PHPUnit\Framework\Attributes\DataProvider('providerRequiredOperands')]
111 public function testActionsRejectsMissingRequiredOperands(string $operation): void
112 {
113 $validation = new PlanValidation();
114 $validation->actions([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::from($operation))]);
115 self::assertSame(['action-arity-or-opcode:' . $operation], $validation->errors);
116 }
117
118 /**
119 * @return list<array{string}>
120 */
121 public static function providerRequiredOperands(): array
122 {
123 return [['return'], ['throw'], ['state-write'], ['write-parameter'], ['choice'], ['callback'], ['invoke'], ['allocate'], ['location-write']];
124 }
125
126 /**
127 * @param Expression $expression
128 * @param list<string> $reads
129 */
130 #[\PHPUnit\Framework\Attributes\DataProvider('providerValidExpressions')]
131 public function testExpressionChecksNestedOperandsAndRecordsStateReads(Expression $expression, array $reads): void
132 {
133 $validation = new PlanValidation();
134 $validation->expression($expression);
135 self::assertSame([], $validation->errors);
136 self::assertSame($reads, $validation->reads);
137 }
138
139 /**
140 * @return list<array{Expression, list<string>}>
141 */
142 public static function providerValidExpressions(): array
143 {
144 $input = Expression::parameter('value');
145 return [
146 [$input, []], [Expression::literal(Term::constant(null)), []],
147 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::External, 'env:key'), []],
148 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::State, 'example.slot', [$input]), ['example.slot']],
149 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Binary, '+', [$input, $input]), []],
150 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Unary, '!', [$input]), []],
151 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Cast, 'int', [$input]), []],
152 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::ArrayRead, operands: [$input, $input]), []],
153 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::ArraySet, operands: [$input, $input, $input]), []],
154 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Intrinsic, 'example.empty'), []],
155 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Intrinsic, 'example.pair', [$input, $input]), []],
156 [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::LocationRead, location: LocationRef::state('example.slot')), ['example.slot']],
157 ];
158 }
159
160 public function testExpressionRejectsMalformedNestedOperands(): void
161 {
162 $validation = new PlanValidation();
163 $validation->expression(new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Intrinsic, 'example', [new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Binary)]));
164 $validation->expression(new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Cast));
165 self::assertSame(['expression-arity-or-opcode:binary', 'expression-arity-or-opcode:cast'], $validation->errors);
166 }
167
168 public function testValidateChecksReferenceCompletionAndBothKindsOfDeclaredFootprint(): void
169 {
170 $read = new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::State, 'example.slot', [Expression::receiver()]);
171 $plan = new SemanticPlan([Action::returns($read)]);
172 self::assertSame(['undeclared-state-read'], (new PlanValidation())->validate($plan));
173 self::assertSame(['missing-reference-completion'], (new PlanValidation())->validate(new SemanticPlan([]), new Signature(byReference: true)));
174 self::assertSame([], (new PlanValidation())->validate(new SemanticPlan([Action::returns($read)], reads: ['example.slot'])));
175 }
176
177 public function testValidateRequiresRegisteredStateSlotsAndKeepsParameterEffectsSeparate(): void
178 {
179 $plan = new SemanticPlan([Action::write('example.slot', Expression::parameter('value'))], writes: ['example.slot']);
180 self::assertSame(['unregistered-state-slot:example.slot'], (new PlanValidation())->validate($plan, state: []));
181 self::assertSame([], (new PlanValidation())->validate($plan, state: ['example.slot' => new StateSlot('example.slot')]));
182 $reference = new SemanticPlan([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::WriteParameter, [Expression::parameter('value')], 'value')], writes: ['parameter:value']);
183 self::assertSame([], (new PlanValidation())->validate($reference, state: []));
184 }
185
186 public function testActionsEnforcesTheExactPlanNodeAllowance(): void
187 {
188 $validation = new PlanValidation();
189 $validation->nodes = 19999;
190 $validation->actions([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Havoc)]);
191 self::assertSame([], $validation->errors);
192 self::assertSame(20000, $validation->nodes);
193 $validation->actions([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Havoc)]);
194 self::assertSame(['plan-node-budget'], $validation->errors);
195 }
196
197 public function testExpressionEnforcesTheExactPlanNodeAllowance(): void
198 {
199 $validation = new PlanValidation();
200 $validation->nodes = 19999;
201 $validation->expression(Expression::parameter('value'));
202 self::assertSame([], $validation->errors);
203 $validation->expression(Expression::parameter('value'));
204 self::assertSame(['plan-node-budget'], $validation->errors);
205 }
206
207 public function testCompletesRetainsFallthroughAndAcceptsExplicitTermination(): void
208 {
209 $validation = new PlanValidation();
210 self::assertFalse($validation->completes([]));
211 self::assertFalse($validation->completes([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Havoc)]));
212 self::assertTrue($validation->completes([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Return)]));
213 self::assertTrue($validation->completes([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Throw)]));
214 $validation->nodes = 19999;
215 self::assertTrue($validation->completes([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::ReturnReference)]));
216 self::assertFalse($validation->completes([new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::ReturnReference)]));
217 self::assertSame(['plan-node-budget'], $validation->errors);
218 }
219}
220