packages/deriver/tests/Unit/Deriver/Evaluator/Evidence/ProvenanceTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Evidence;
6
7use Closure;
8use Deriver\Deriver\Evaluator\Enumeration\Choices;
9use Deriver\Deriver\Evaluator\Evidence\EvidenceKind;
10use Deriver\Deriver\Evaluator\Evidence\Node;
11use Deriver\Deriver\Evaluator\Evidence\Provenance as Subject;
12use Deriver\Deriver\Evaluator\Term;
13use PHPUnit\Framework\Attributes\CoversNothing;
14use PHPUnit\Framework\Attributes\DataProvider;
15use PHPUnit\Framework\TestCase;
16
17#[CoversNothing]
18final class ProvenanceTest extends TestCase
19{
20    /**
21     * @param Closure(Subject, Term): Term $transform
22     */
23    #[DataProvider('providerProofOperations')]
24    public function testEvidencePreservesSharedBranches(Term $value, Closure $transform): void
25    {
26        $proofs = new Subject();
27        $wrapped = $transform($proofs, $value);
28        self::assertCount(2, $wrapped->operands);
29        self::assertSame($wrapped->operands[0]->operands[0], $wrapped->operands[1]->operands[0]);
30        self::assertSame(['guard:branch:11' => true], $wrapped->operands[0]->attributes);
31        self::assertSame(['guard:branch:11' => false], $wrapped->operands[1]->attributes);
32        $leaf = array_reduce(range(0, 11), static fn (Term $node): Term => $node->operands[0]->operands[0], $wrapped);
33        self::assertSame(7, $leaf->literal);
34        self::assertNotNull($proofs->get($leaf));
35        self::assertNull($proofs->get($value));
36    }
37
38    /**
39     * @return iterable<string, array{Term, Closure(Subject, Term): Term}>
40     */
41    public static function providerProofOperations(): iterable
42    {
43        $value = Term::constant(7);
44        for ($depth = 0; $depth < 12; $depth++) {
45            $value = (new Choices())->make([[$value, ['branch:' . $depth => true]], [$value, ['branch:' . $depth => false]]]);
46        }
47        yield 'wrap' => [$value, static fn (Subject $proofs, Term $value): Term => $proofs->wrap($value, EvidenceKind::Operation)];
48        yield 'operation' => [$value, static fn (Subject $proofs, Term $value): Term => $proofs->operation($value, [Term::constant(1)], 'selection')];
49        yield 'evaluator' => [$value, static fn (Subject $proofs, Term $value): Term => $proofs->evaluator($value, new Node(EvidenceKind::EvaluatorApplication))];
50    }
51
52    public function testEqualValuesKeepTheirDistinctProofs(): void
53    {
54        $proofs = new Subject();
55        $left = $proofs->wrap(Term::constant(7), EvidenceKind::SourceDefinition, attributes: ['definition' => 'left']);
56        $right = $proofs->wrap(Term::constant(7), EvidenceKind::SourceDefinition, attributes: ['definition' => 'right']);
57        $wrapped = $proofs->wrap((new Choices())->make([[$left, ['x' => true]], [$right, ['x' => false]]]), EvidenceKind::Operation);
58
59        $leftResult = $wrapped->operands[0]->operands[0];
60        $rightResult = $wrapped->operands[1]->operands[0];
61        self::assertNotSame($leftResult, $rightResult);
62        self::assertSame($proofs->get($left), $proofs->get($leftResult)?->inputs['value']);
63        self::assertSame($proofs->get($right), $proofs->get($rightResult)?->inputs['value']);
64    }
65
66    public function testWrapPreservesAlternativeEvidence(): void
67    {
68        $proofs = new Subject();
69        $value = (new Choices())->make([[Term::constant(1),['x' => true]],[Term::constant(1),['x' => false]]]);
70        $wrapped = $proofs->wrap($value, EvidenceKind::Operation, attributes:['operation' => 'copy']);
71        self::assertCount(2, $wrapped->operands);
72        $evidence = $proofs->get($wrapped->operands[0]->operands[0]);
73        self::assertNotNull($evidence);
74        self::assertSame(EvidenceKind::Operation, $evidence->kind);
75    }
76
77    public function testAttachDoesNotChangeValueIdentity(): void
78    {
79        $proofs = new Subject();
80        $value = Term::constant(3);
81        $proof = new Node(EvidenceKind::Operation, attributes:['operation' => '+']);
82        self::assertSame((new \Deriver\Deriver\Evaluator\Scalar\Identity())->key($value), (new \Deriver\Deriver\Evaluator\Scalar\Identity())->key($proofs->attach($value, $proof)));
83    }
84
85    public function testInputsRetainsOperandRoles(): void
86    {
87        $proofs = new Subject();
88        self::assertSame(['operand:left'], array_keys($proofs->inputs(['left' => Term::constant(1)])));
89    }
90
91    public function testOperationRetainsTheResultAndItsInputs(): void
92    {
93        $proofs = new Subject();
94        $input = $proofs->wrap(Term::constant(1), EvidenceKind::SourceDefinition);
95        $result = $proofs->operation(Term::constant(2), [$input], '+');
96        self::assertNotNull($proofs->get($result));
97        self::assertSame($proofs->get($input), $proofs->get($result)->inputs['operand:0']);
98    }
99
100    public function testEvaluatorRetainsSelectionInputsAndOutput(): void
101    {
102        $proofs = new Subject();
103        $evaluator = new Node(EvidenceKind::EvaluatorApplication, attributes:['id' => 'one','version' => '1','operation' => 'invoke','name' => 'f']);
104        $value = $proofs->evaluator(Term::constant(7), $evaluator);
105        self::assertNotNull($proofs->get($value));
106        self::assertSame('one', $proofs->get($value)->attributes['id']);
107        self::assertArrayHasKey('output', $proofs->get($value)->inputs);
108    }
109
110    public function testGetKeepsAnalysesIsolatedAndDoesNotMutateValues(): void
111    {
112        $first = new Subject();
113        $second = new Subject();
114        $value = Term::constant(1);
115        $proof = new Node(EvidenceKind::SourceDefinition);
116        $derived = $first->attach($value, $proof);
117        self::assertSame($proof, $first->get($derived));
118        self::assertNull($first->get($value));
119        self::assertNull($second->get($derived));
120        self::assertFalse(property_exists($derived, 'evidence'));
121    }
122}
123