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