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