packages/deriver/tests/Unit/Deriver/Evaluator/Call/Plan/PlanLocationsTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Call\Plan;
6
7use Deriver\Deriver\Evaluator\Call\Plan\Expression;
8use Deriver\Deriver\Evaluator\Call\Plan\Location\LocationRef;
9use Deriver\Deriver\Evaluator\Call\Plan\PlanCompiler;
10use Deriver\Deriver\Evaluator\Call\Plan\PlanLocations;
11use Deriver\Deriver\Evaluator\Term;
12use Deriver\Deriver\Sources\SourceRef;
13use Deriver\Exception\InvalidInputException;
14use PHPUnit\Framework\Attributes\CoversClass;
15use PHPUnit\Framework\Attributes\DataProvider;
16use PHPUnit\Framework\Attributes\Small;
17use PHPUnit\Framework\Attributes\UsesClass;
18use PHPUnit\Framework\TestCase;
19
20/**
21 * @covers \Deriver\Deriver\Evaluator\Call\Plan\PlanLocations
22 */
23#[CoversClass(PlanLocations::class)]
24#[UsesClass(\Deriver\Deriver\Lowerer\Graph\Instruction::class)]
25#[UsesClass(LocationRef::class)]
26#[UsesClass(PlanCompiler::class)]
27#[UsesClass(Expression::class)]
28#[UsesClass(SourceRef::class)]
29#[UsesClass(Term::class)]
30#[Small]
31final class PlanLocationsTest extends TestCase
32{
33 public function testAddressPreservesAppendWithoutReadingStorage(): void
34 {
35 $compiler = new PlanCompiler(new SourceRef('test', 'evaluator:test', 0, 1));
36 $register = (new PlanLocations($compiler))->address(LocationRef::element(LocationRef::parameter('items')));
37 self::assertSame('m1', $register);
38 self::assertSame(['local', 'element-address'], array_column($compiler->instructions[0], 'operation'));
39 self::assertSame(['m0', ''], $compiler->instructions[0][1]->operands);
40 }
41 public function testAddressRejectsMalformedDescriptors(): void
42 {
43 $compiler = new PlanCompiler(new SourceRef('test', 'evaluator:test', 0, 1));
44 $this->expectException(InvalidInputException::class);
45 (new PlanLocations($compiler))->address(new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State));
46 }
47 public function testFromExpressionRecognizesOnlyAddressableExpressions(): void
48 {
49 $compiler = new PlanCompiler(new SourceRef('test', 'evaluator:test', 0, 1));
50 $locations = new PlanLocations($compiler);
51 self::assertSame(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, $locations->fromExpression(Expression::state('domain.slot'))?->kind);
52 self::assertNull($locations->fromExpression(Expression::literal(Term::constant(1))));
53 }
54
55 #[DataProvider('providerIncompleteLocations')]
56 public function testAddressRejectsMissingLocationComponents(LocationRef $location): void
57 {
58 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:location', 0, 1));
59 $this->expectException(InvalidInputException::class);
60 $this->expectExceptionMessage('EVALUATOR_CONTRACT_VIOLATION: invalid location '.$location->kind->value);
61 (new PlanLocations($compiler))->address($location);
62 }
63
64 /**
65 * @return iterable<string,array{LocationRef}>
66 */
67 public static function providerIncompleteLocations(): iterable
68 {
69 yield 'state missing receiver' => [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, 'domain.slot')];
70 yield 'element missing parent' => [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::Element, key:Expression::literal(Term::constant(0)))];
71 yield 'state missing receiver with other fields' => [new LocationRef(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, 'slot', parent: LocationRef::parameter('items'), key: Expression::literal(Term::constant(0)))];
72 }
73
74 public function testAddressBindsNestedElementsToTheirParentsAndEvaluatesKeysInOrder(): void
75 {
76 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:location', 0, 1));
77 $location = LocationRef::element(LocationRef::element(LocationRef::parameter('items'), Expression::literal(Term::constant('outer'))), Expression::literal(Term::constant(7)));
78 self::assertSame('m4', (new PlanLocations($compiler))->address($location));
79 self::assertSame(['local','constant','element-address','constant','element-address'], array_column($compiler->instructions[0], 'operation'));
80 self::assertSame('items', $compiler->instructions[0][0]->name);
81 self::assertSame('outer', (new \Deriver\Deriver\Evaluator\Expression\LiteralEvaluator())->instruction($compiler->instructions[0][1])->literal);
82 self::assertSame(7, (new \Deriver\Deriver\Evaluator\Expression\LiteralEvaluator())->instruction($compiler->instructions[0][3])->literal);
83 self::assertSame(['m0','m1'], $compiler->instructions[0][2]->operands);
84 self::assertSame(['m2','m3'], $compiler->instructions[0][4]->operands);
85 }
86
87 public function testAddressPreservesAnExplicitStateReceiverAndSlot(): void
88 {
89 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:location', 0, 1));
90 $receiver = new Term(\Deriver\Deriver\Evaluator\TermKind::Object, 'receiver');
91 self::assertSame('m1', (new PlanLocations($compiler))->address(LocationRef::state('custom.value', Expression::literal($receiver))));
92 self::assertSame($receiver, (new \Deriver\Deriver\Evaluator\Expression\LiteralEvaluator())->instruction($compiler->instructions[0][0]));
93 self::assertSame('evaluator-state-address', $compiler->instructions[0][1]->operation);
94 self::assertSame(['m0'], $compiler->instructions[0][1]->operands);
95 self::assertSame('custom.value', $compiler->instructions[0][1]->name);
96 }
97
98 public function testFromExpressionRetainsExistingLocationIdentityWithoutEmittingInstructions(): void
99 {
100 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:location', 0, 1));
101 $location = LocationRef::element(LocationRef::parameter('items'));
102 $locations = new PlanLocations($compiler);
103 self::assertSame($location, $locations->fromExpression($location));
104 self::assertSame($location, $locations->fromExpression(Expression::read($location)));
105 self::assertSame([0 => []], $compiler->instructions);
106 }
107
108 public function testFromExpressionDefaultsMissingStateReceiverToThis(): void
109 {
110 $compiler = new PlanCompiler(new SourceRef('snapshot', 'evaluator:location', 0, 1));
111 $location = (new PlanLocations($compiler))->fromExpression(new Expression(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::State, 'cache.value'));
112 self::assertNotNull($location);
113 self::assertSame(\Deriver\Deriver\Evaluator\Call\Plan\Location\LocationKind::State, $location->kind);
114 self::assertSame('cache.value', $location->name);
115 self::assertNotNull($location->receiver);
116 self::assertSame(\Deriver\Deriver\Evaluator\Call\Plan\ExpressionKind::Parameter, $location->receiver->operation);
117 self::assertSame('this', $location->receiver->name);
118 }
119}
120