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