packages/deriver/tests/Unit/Deriver/Evaluator/Call/Plan/PlanActionsTest.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\PlanActions;
11use Deriver\Deriver\Evaluator\Call\Plan\PlanCompiler;
12use Deriver\Deriver\Evaluator\Term;
13use Deriver\Deriver\Sources\SourceRef;
14use Deriver\Exception\InvalidInputException;
15use PHPUnit\Framework\Attributes\CoversClass;
16use PHPUnit\Framework\Attributes\DataProvider;
17use PHPUnit\Framework\Attributes\Small;
18use PHPUnit\Framework\Attributes\UsesClass;
19use PHPUnit\Framework\TestCase;
20
21/**
22 * @covers \Deriver\Deriver\Evaluator\Call\Plan\PlanActions
23 */
24#[CoversClass(PlanActions::class)]
25#[UsesClass(\Deriver\Deriver\Lowerer\Graph\Instruction::class)]
26#[UsesClass(\Deriver\Deriver\Lowerer\Graph\Terminator::class)]
27#[UsesClass(LocationRef::class)]
28#[UsesClass(PlanCompiler::class)]
29#[UsesClass(\Deriver\Deriver\Evaluator\Call\Plan\PlanInvocations::class)]
30#[UsesClass(\Deriver\Deriver\Evaluator\Call\Plan\PlanLocations::class)]
31#[UsesClass(Action::class)]
32#[UsesClass(Expression::class)]
33#[UsesClass(SourceRef::class)]
34#[UsesClass(Term::class)]
35#[Small]
36final class PlanActionsTest extends TestCase
37{
38 public function testApplyReturnsAReferenceThroughCoreMemory(): void
39 {
40 $compiler = new PlanCompiler(new SourceRef('test', 'evaluator:test', 0, 1));
41 self::assertTrue((new PlanActions($compiler))->apply(Action::returnReference(LocationRef::parameter('value'))));
42 self::assertSame(['local', 'reference'], array_column($compiler->instructions[0], 'operation'));
43 self::assertSame(\Deriver\Deriver\Lowerer\Graph\TerminatorKind::Return, $compiler->terminators[0]->kind);
44 }
45 public function testApplyRejectsMalformedStorageTargets(): void
46 {
47 $compiler = new PlanCompiler(new SourceRef('test', 'evaluator:test', 0, 1));
48 $this->expectException(InvalidInputException::class);
49 (new PlanActions($compiler))->apply(new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::LocationWrite, locations: [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State)]));
50 }
51
52 /**
53 * @param list<string> $operations Expected control graph operations
54 */
55 #[DataProvider('providerCompletionActions')]
56 public function testApplyBindsCompletionToItsExactValue(Action $action, string $kind, string $operand, array $operations): void
57 {
58 $source = new SourceRef('snapshot', 'evaluator:completion', 7, 9);
59 $compiler = new PlanCompiler($source);
60 self::assertTrue((new PlanActions($compiler))->apply($action));
61 self::assertSame($operations, array_column($compiler->instructions[0], 'operation'));
62 self::assertSame(\Deriver\Deriver\Lowerer\Graph\TerminatorKind::from($kind), $compiler->terminators[0]->kind);
63 self::assertSame($operand, $compiler->terminators[0]->operand);
64 self::assertSame([], $compiler->terminators[0]->targets);
65 }
66
67 /**
68 * @return iterable<string,array{Action,string,string,list<string>}>
69 */
70 public static function providerCompletionActions(): iterable
71 {
72 yield 'return value' => [Action::returns(Expression::literal(Term::constant(17))),'return','m0',['constant']];
73 yield 'throw value' => [Action::throws(Expression::literal(new Term(\Deriver\Deriver\Evaluator\TermKind::Throwable, 'Error'))),'throw','m0',['constant']];
74 yield 'return reference' => [Action::returnReference(LocationRef::parameter('input')),'return','m1',['local','reference']];
75 yield 'bare return' => [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::Return),'return','',[]];
76 }
77
78 /**
79 * @param list<string> $operations Expected storage graph operations
80 */
81 #[DataProvider('providerOrderedWrites')]
82 public function testApplyPreservesTheWriteTargetAndOperandOrder(Action $action, array $operations, int $address, int $value, string $target): void
83 {
84 $source = new SourceRef('snapshot', 'evaluator:write', 0, 8);
85 $compiler = new PlanCompiler($source);
86 self::assertFalse((new PlanActions($compiler))->apply($action));
87 $instructions = $compiler->instructions[0];
88 self::assertSame($operations, array_column($instructions, 'operation'));
89 self::assertSame($target, $instructions[$address]->name);
90 self::assertSame(47, (new \Deriver\Deriver\Evaluator\Expression\LiteralEvaluator())->instruction($instructions[$value])->literal);
91 self::assertSame([$instructions[$address]->result,$instructions[$value]->result], $instructions[count($instructions) - 1]->operands);
92 self::assertSame($source, $instructions[count($instructions) - 1]->source);
93 self::assertSame([], $compiler->terminators);
94 }
95
96 /**
97 * @return iterable<string,array{Action,list<string>,int,int,string}>
98 */
99 public static function providerOrderedWrites(): iterable
100 {
101 $value = Expression::literal(Term::constant(47));
102 yield 'receiver state' => [Action::write('cache.value', $value, Expression::literal(new Term(\Deriver\Deriver\Evaluator\TermKind::Object, 'receiver'))),['constant','constant','evaluator-state-address','write'],2,1,'cache.value'];
103 yield 'reference parameter' => [new Action(\Deriver\Deriver\Evaluator\Call\Plan\ActionKind::WriteParameter, [$value], 'output'),['constant','local','write'],1,0,'output'];
104 yield 'explicit location' => [Action::assign(LocationRef::parameter('output'), $value),['local','constant','write'],0,1,'output'];
105 }
106
107 public function testApplyConnectsAStateAddressToItsReceiver(): void
108 {
109 $receiver = new Term(\Deriver\Deriver\Evaluator\TermKind::Object, 'object-identity');
110 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:write', 0, 8));
111 self::assertFalse((new PlanActions($compiler))->apply(Action::write('cache.value', Expression::literal(Term::constant(1)), Expression::literal($receiver))));
112 self::assertSame($receiver, (new \Deriver\Deriver\Evaluator\Expression\LiteralEvaluator())->instruction($compiler->instructions[0][0]));
113 self::assertSame(['m0'], $compiler->instructions[0][2]->operands);
114 }
115
116 #[DataProvider('providerHavocActions')]
117 public function testApplyPreservesEveryHavocLocationAndExceptionalPossibility(bool $mayThrow): void
118 {
119 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:havoc', 0, 8));
120 self::assertFalse((new PlanActions($compiler))->apply(Action::havoc([LocationRef::parameter('first'),LocationRef::parameter('second')], 'remote write', $mayThrow)));
121 self::assertSame(['local','local','evaluator-havoc'], array_column($compiler->instructions[0], 'operation'));
122 self::assertSame(['first','second','remote write'], array_column($compiler->instructions[0], 'name'));
123 self::assertSame(['m0','m1'], $compiler->instructions[0][2]->operands);
124 self::assertSame(['may-throw' => $mayThrow], $compiler->instructions[0][2]->attributes);
125 self::assertSame([], $compiler->terminators);
126 }
127
128 /**
129 * @return iterable<string,array{bool}>
130 */
131 public static function providerHavocActions(): iterable
132 {
133 yield 'normal only' => [false];
134 yield 'possibly exceptional' => [true];
135 }
136
137 public function testApplyAliasesTheDestinationToTheSourceCell(): void
138 {
139 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:alias', 0, 8));
140 self::assertFalse((new PlanActions($compiler))->apply(Action::alias(LocationRef::parameter('destination'), LocationRef::parameter('source'))));
141 self::assertSame(['destination','source',''], array_column($compiler->instructions[0], 'name'));
142 self::assertSame(['local','local','alias'], array_column($compiler->instructions[0], 'operation'));
143 self::assertSame(['m0','m1'], $compiler->instructions[0][2]->operands);
144 }
145
146 public function testApplyKeepsBothChoiceCompletionsAndTheContinuationSeparate(): void
147 {
148 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:choice', 0, 8));
149 $action = Action::choice(Expression::literal(Term::parameter('flag', 'bool')), [Action::returns(Expression::literal(Term::constant(1)))], [Action::throws(Expression::literal(new Term(\Deriver\Deriver\Evaluator\TermKind::Throwable, 'Error')))]);
150 self::assertFalse((new PlanActions($compiler))->apply($action));
151 self::assertSame(3, $compiler->current);
152 self::assertSame(\Deriver\Deriver\Lowerer\Graph\TerminatorKind::Branch, $compiler->terminators[0]->kind);
153 self::assertSame('m0', $compiler->terminators[0]->operand);
154 self::assertSame([1,2], $compiler->terminators[0]->targets);
155 self::assertSame(\Deriver\Deriver\Lowerer\Graph\TerminatorKind::Return, $compiler->terminators[1]->kind);
156 self::assertSame('m1', $compiler->terminators[1]->operand);
157 self::assertSame(\Deriver\Deriver\Lowerer\Graph\TerminatorKind::Throw, $compiler->terminators[2]->kind);
158 self::assertSame('m2', $compiler->terminators[2]->operand);
159 self::assertSame([], $compiler->instructions[3]);
160 }
161
162 #[DataProvider('providerInvocationActions')]
163 public function testApplyRoutesEachInvocationThroughPreparedArgumentBinding(Action $action, string $operation): void
164 {
165 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:call', 0, 8));
166 self::assertFalse((new PlanActions($compiler))->apply($action));
167 self::assertSame(['constant','call-prepare',$operation,'local','write'], array_column($compiler->instructions[0], 'operation'));
168 self::assertSame($operation, $compiler->instructions[0][1]->attributes['call-operation']);
169 self::assertSame(['prepared' => 'm1'], $compiler->instructions[0][2]->attributes);
170 self::assertSame('@result', $compiler->instructions[0][3]->name);
171 self::assertSame(['m3','m2'], $compiler->instructions[0][4]->operands);
172 self::assertSame([], $compiler->terminators);
173 }
174
175 /**
176 * @return iterable<string,array{Action,string}>
177 */
178 public static function providerInvocationActions(): iterable
179 {
180 $target = Expression::literal(Term::constant('target'));
181 yield 'callback' => [Action::callback('@result', $target),'invoke'];
182 yield 'invoke' => [Action::invoke('@result', $target),'invoke'];
183 yield 'allocate' => [Action::allocate('@result', $target),'new'];
184 }
185}
186