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