packages/deriver/tests/Unit/Deriver/Evaluator/Call/Plan/PlanInvocationsTest.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\PlanCompiler;
12use Deriver\Deriver\Evaluator\Call\Plan\PlanInvocations;
13use Deriver\Deriver\Evaluator\Term;
14use Deriver\Deriver\Sources\SourceRef;
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\PlanInvocations
23 */
24#[CoversClass(PlanInvocations::class)]
25#[UsesClass(\Deriver\Deriver\Lowerer\Graph\Argument::class)]
26#[UsesClass(\Deriver\Deriver\Lowerer\Graph\Instruction::class)]
27#[UsesClass(LocationRef::class)]
28#[UsesClass(PlanCompiler::class)]
29#[UsesClass(\Deriver\Deriver\Evaluator\Call\Plan\PlanLocations::class)]
30#[UsesClass(Action::class)]
31#[UsesClass(CallArgument::class)]
32#[UsesClass(Expression::class)]
33#[UsesClass(SourceRef::class)]
34#[UsesClass(Term::class)]
35#[Small]
36final class PlanInvocationsTest extends TestCase
37{
38 public function testApplyPreparesCallBeforeItsArguments(): void
39 {
40 $compiler = new PlanCompiler(new SourceRef('test', 'evaluator:test', 0, 1));
41 (new PlanInvocations($compiler))->apply(Action::invoke('@result', Expression::literal(Term::constant('consume')), [new CallArgument(LocationRef::parameter('value'))], true));
42 self::assertSame(['constant', 'call-prepare', 'local', 'argument', 'invoke', 'local', 'returned-address', 'alias'], array_column($compiler->instructions[0], 'operation'));
43 self::assertTrue($compiler->instructions[0][3]->attributes['address']);
44 }
45 public function testArgumentsPreservesNamedUnpackAndLocationMetadata(): void
46 {
47 $compiler = new PlanCompiler(new SourceRef('test', 'evaluator:test', 0, 1));
48 $arguments = (new PlanInvocations($compiler))->arguments([new CallArgument(LocationRef::parameter('items'), unpack: true)], 'prepared');
49 self::assertTrue($arguments[0]->unpack);
50 self::assertSame($arguments[0]->register, $arguments[0]->location);
51 self::assertTrue($compiler->instructions[0][1]->attributes['unpack-variable']);
52 }
53
54 /**
55 * @param list<string> $operations Expected argument evaluation order
56 */
57 #[DataProvider('providerArgumentLocations')]
58 public function testArgumentsDistinguishesAddressableStorageFromComputedValues(Expression|LocationRef $input, array $operations, bool $address, bool $unpackVariable): void
59 {
60 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:arguments', 0, 4));
61 $arguments = (new PlanInvocations($compiler))->arguments([new CallArgument($input, 'items', true)], 'signature');
62 $instructions = $compiler->instructions[0];
63 $instruction = $instructions[count($instructions) - 1];
64 self::assertSame($operations, array_column($instructions, 'operation'));
65 self::assertSame(['signature',$instructions[count($instructions) - 2]->result], $instruction->operands);
66 self::assertSame(['address' => $address,'argument-name' => 'items','unpack' => true,'unpack-variable' => $unpackVariable,'temporary' => false], $instruction->attributes);
67 self::assertSame([], $instruction->arguments);
68 self::assertCount(1, $arguments);
69 self::assertSame($instruction->result, $arguments[0]->register);
70 self::assertSame($instruction->result, $arguments[0]->location);
71 self::assertSame('items', $arguments[0]->name);
72 self::assertTrue($arguments[0]->unpack);
73 }
74
75 /**
76 * @return iterable<string,array{Expression|LocationRef,list<string>,bool,bool}>
77 */
78 public static function providerArgumentLocations(): iterable
79 {
80 yield 'parameter location' => [LocationRef::parameter('value'),['local','argument'],true,true];
81 yield 'parameter expression' => [Expression::parameter('value'),['local','argument'],true,true];
82 yield 'state expression' => [Expression::state('cache.value'),['local','read','evaluator-state-address','argument'],true,false];
83 yield 'state location' => [LocationRef::state('cache.value'),['local','read','evaluator-state-address','argument'],true,false];
84 yield 'element location' => [LocationRef::element(LocationRef::parameter('value'), Expression::literal(Term::constant('key'))),['local','constant','element-address','argument'],true,false];
85 yield 'explicit read' => [Expression::read(LocationRef::parameter('value')),['local','argument'],true,true];
86 yield 'computed value' => [Expression::literal(Term::constant(4)),['constant','argument'],false,false];
87 }
88
89 public function testArgumentsSuppliesEarlierBindingsToEachLaterArgument(): void
90 {
91 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:arguments', 0, 4));
92 $arguments = (new PlanInvocations($compiler))->arguments([new CallArgument(Expression::literal(Term::constant(1))),new CallArgument(Expression::parameter('second'), 'other')], 'signature');
93 self::assertCount(2, $arguments);
94 self::assertSame([], $compiler->instructions[0][1]->arguments);
95 self::assertSame([$arguments[0]], $compiler->instructions[0][3]->arguments);
96 self::assertSame(['signature','m2'], $compiler->instructions[0][3]->operands);
97 self::assertSame('other', $arguments[1]->name);
98 self::assertFalse($arguments[1]->unpack);
99 self::assertSame('m3', $arguments[1]->location);
100 }
101
102 public function testApplyPreservesCallbackArgumentOrderAndResultBinding(): void
103 {
104 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:callback', 0, 4));
105 $target = Term::constant('callback');
106 (new PlanInvocations($compiler))->apply(Action::callback('result', Expression::literal($target), [Expression::literal(Term::constant(11)),Expression::literal(Term::constant(29))]));
107 $instructions = $compiler->instructions[0];
108 self::assertSame(['constant','call-prepare','constant','argument','constant','argument','invoke','local','write'], array_column($instructions, 'operation'));
109 self::assertSame($target, (new \Deriver\Deriver\Evaluator\Expression\LiteralEvaluator())->instruction($instructions[0]));
110 self::assertSame(11, (new \Deriver\Deriver\Evaluator\Expression\LiteralEvaluator())->instruction($instructions[2])->literal);
111 self::assertSame(29, (new \Deriver\Deriver\Evaluator\Expression\LiteralEvaluator())->instruction($instructions[4])->literal);
112 self::assertSame(['m0'], $instructions[6]->operands);
113 self::assertSame(['m3','m5'], array_column($instructions[6]->arguments, 'register'));
114 self::assertSame(['prepared' => 'm1'], $instructions[6]->attributes);
115 self::assertSame('result', $instructions[7]->name);
116 self::assertSame(['m7','m6'], $instructions[8]->operands);
117 }
118
119 public function testApplyRetainsTheReturnedCellWhenAReferenceResultIsRequested(): void
120 {
121 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:reference', 0, 4));
122 (new PlanInvocations($compiler))->apply(Action::invoke('alias', Expression::literal(Term::constant('byReference')), byReference:true));
123 $instructions = $compiler->instructions[0];
124 self::assertSame(['constant','call-prepare','invoke','local','returned-address','alias'], array_column($instructions, 'operation'));
125 self::assertSame(['m2'], $instructions[4]->operands);
126 self::assertSame(['m3','m4'], $instructions[5]->operands);
127 self::assertSame('alias', $instructions[3]->name);
128 }
129}
130