A Golang runtime and compilation backend for Delta Interaction Nets.
1package lambda
2
3import (
4 "testing"
5
6 "github.com/vic/godnet/pkg/deltanet"
7)
8
9// TestOptimalityProperty verifies that Delta-Nets achieves optimal reduction
10// as claimed in the paper: "no reduction operation is applied which is rendered
11// unnecessary later, and no reduction operation which is necessary is applied
12// more than once."
13//
14// This means every normalizing term should reduce in the theoretical minimum
15// number of steps. Perfect confluence guarantees that "every normalizing
16// interaction order produces the same result in the same number of interactions."
17//
18// For each test case, we document the theoretical minimum based on:
19// 1. Beta-reductions needed in lambda calculus
20// 2. Additional fan/eraser interactions from the Delta-Net encoding
21// 3. The paper's claim that Delta-Nets matches Lévy's optimal reduction
22func TestOptimalityProperty(t *testing.T) {
23 tests := []struct {
24 name string
25 input string
26 expectedReductions uint64
27 description string
28 }{
29 {
30 name: "identity",
31 input: "(x: x)",
32 expectedReductions: 0,
33 description: "No reductions needed - already in normal form",
34 },
35 {
36 name: "id_id",
37 input: "((x: x) (y: y))",
38 expectedReductions: 4,
39 description: "1 beta reduction + 3 structural (fan annihilation from encoding)",
40 },
41 {
42 name: "K_combinator_1arg",
43 input: "(((x: (y: x)) a) b)",
44 expectedReductions: 2,
45 description: "K a b → a: 2 beta reductions + erasure of b",
46 },
47 {
48 name: "K_combinator_2args",
49 input: "((x: (y: x)) a)",
50 expectedReductions: 1,
51 description: "K a → λy.a: 1 beta reduction (optimal encoding)",
52 },
53 {
54 name: "church_zero",
55 input: "(((f: (x: x)) f) x)",
56 expectedReductions: 2,
57 description: "Church zero application: (λf.λx.x) f x → x (optimal)",
58 },
59 {
60 name: "church_one",
61 input: "(((f: (x: (f x))) f) x)",
62 expectedReductions: 2,
63 description: "Church one application: reduces to (f x)",
64 },
65 {
66 name: "church_two",
67 input: "(((f: (x: (f (f x)))) f) x)",
68 expectedReductions: 2,
69 description: "Church two application: reduces to (f (f x))",
70 },
71 {
72 name: "simple_let",
73 input: "((x: x) y)",
74 expectedReductions: 1,
75 description: "Simple beta reduction: (λx.x) y → y",
76 },
77 {
78 name: "boolean_true",
79 input: "((((x: (y: x)) a) b) c)",
80 expectedReductions: 2,
81 description: "True combinator applied: returns first argument",
82 },
83 {
84 name: "boolean_false",
85 input: "((((x: (y: y)) a) b) c)",
86 expectedReductions: 2,
87 description: "False combinator applied: returns second argument",
88 },
89 }
90
91 for _, tt := range tests {
92 t.Run(tt.name, func(t *testing.T) {
93 term, err := Parse(tt.input)
94 if err != nil {
95 t.Fatalf("Parse error: %v", err)
96 }
97
98 net := deltanet.NewNetwork()
99 root, port, _ := ToDeltaNet(term, net)
100
101 output := net.NewVar()
102 net.Link(root, port, output, 0)
103
104 net.ReduceToNormalForm()
105
106 stats := net.GetStats()
107
108 if stats.TotalReductions != tt.expectedReductions {
109 t.Errorf("%s: Expected %d reductions (optimal), got %d\nDescription: %s\nDelta: %+d",
110 tt.name,
111 tt.expectedReductions,
112 stats.TotalReductions,
113 tt.description,
114 int64(stats.TotalReductions)-int64(tt.expectedReductions))
115 } else {
116 t.Logf("%s: ✓ Optimal - %d reductions\n %s",
117 tt.name,
118 stats.TotalReductions,
119 tt.description)
120 }
121 })
122 }
123}
124
125// TestOptimalityComplexCases tests more complex terms where manual calculation
126// of optimal reduction count is harder. These serve as regression tests - if
127// counts change, we need to verify the change is correct.
128//
129// Note: The exact reduction count can vary slightly depending on how the
130// let-bindings are encoded. The important property is that we don't exceed
131// the theoretical minimum significantly. All tests below use let-bindings
132// which add some overhead compared to direct application.
133func TestOptimalityComplexCases(t *testing.T) {
134 tests := []struct {
135 name string
136 input string
137 maxReductions uint64 // Maximum acceptable reductions
138 description string
139 }{
140 {
141 name: "succ_zero",
142 input: "((succ: (n: (f: (x: (f ((n f) x)))))) (f: (x: x)))",
143 maxReductions: 10,
144 description: "Successor of zero - with let-binding overhead",
145 },
146 {
147 name: "not_true",
148 input: "((not: (b: ((b (x: (y: y))) (x: (y: x))))) (x: (y: x)))",
149 maxReductions: 12,
150 description: "Boolean NOT applied to TRUE",
151 },
152 {
153 name: "not_false",
154 input: "((not: (b: ((b (x: (y: y))) (x: (y: x))))) (x: (y: y)))",
155 maxReductions: 12,
156 description: "Boolean NOT applied to FALSE",
157 },
158 {
159 name: "and_true_true",
160 input: "((and: (a: (b: ((a b) (x: (y: y)))))) ((x: (y: x)) (x: (y: x))))",
161 maxReductions: 18,
162 description: "Boolean AND of TRUE and TRUE",
163 },
164 {
165 name: "and_true_false",
166 input: "((and: (a: (b: ((a b) (x: (y: y)))))) ((x: (y: x)) (x: (y: y))))",
167 maxReductions: 18,
168 description: "Boolean AND of TRUE and FALSE",
169 },
170 {
171 name: "pair_fst",
172 input: "((pair: ((pair (x: (y: x))) a)) b)",
173 maxReductions: 5,
174 description: "Extract first element from pair",
175 },
176 {
177 name: "pair_snd",
178 input: "((pair: ((pair (x: (y: y))) a)) b)",
179 maxReductions: 5,
180 description: "Extract second element from pair",
181 },
182 {
183 name: "s_combinator_1arg",
184 input: "((((x: (y: (z: ((x z) (y z))))) (a: a)) (b: b)) (c: c))",
185 maxReductions: 25,
186 description: "S combinator with identity functions: S I I c → c c",
187 },
188 {
189 name: "s_combinator_partial",
190 input: "(((x: (y: (z: ((x z) (y z))))) (a: a)) (b: b))",
191 maxReductions: 12,
192 description: "S combinator partially applied",
193 },
194 }
195
196 for _, tt := range tests {
197 t.Run(tt.name, func(t *testing.T) {
198 term, err := Parse(tt.input)
199 if err != nil {
200 t.Fatalf("Parse error: %v", err)
201 }
202
203 net := deltanet.NewNetwork()
204 root, port, _ := ToDeltaNet(term, net)
205
206 output := net.NewVar()
207 net.Link(root, port, output, 0)
208
209 net.ReduceToNormalForm()
210
211 stats := net.GetStats()
212
213 if stats.TotalReductions > tt.maxReductions {
214 t.Errorf("%s: Exceeded maximum %d reductions, got %d (excess: +%d)\nDescription: %s",
215 tt.name,
216 tt.maxReductions,
217 stats.TotalReductions,
218 stats.TotalReductions-tt.maxReductions,
219 tt.description)
220 } else {
221 t.Logf("%s: ✓ %d reductions (≤ %d max)\n %s",
222 tt.name,
223 stats.TotalReductions,
224 tt.maxReductions,
225 tt.description)
226 }
227 })
228 }
229}
230
231// TestOptimalityInvariant tests that the reduction count is invariant
232// across multiple runs and different reduction orders (due to perfect confluence)
233func TestOptimalityInvariant(t *testing.T) {
234 input := "((x: x) (y: y))"
235
236 term, err := Parse(input)
237 if err != nil {
238 t.Fatalf("Parse error: %v", err)
239 }
240
241 // Run reduction multiple times
242 var counts []uint64
243 for i := 0; i < 5; i++ {
244 net := deltanet.NewNetwork()
245 root, port, _ := ToDeltaNet(term, net)
246
247 output := net.NewVar()
248 net.Link(root, port, output, 0)
249
250 net.ReduceToNormalForm()
251
252 stats := net.GetStats()
253 counts = append(counts, stats.TotalReductions)
254 }
255
256 // All counts should be identical due to perfect confluence
257 first := counts[0]
258 for i, count := range counts {
259 if count != first {
260 t.Errorf("Run %d: got %d reductions, expected %d (same as run 0)",
261 i, count, first)
262 }
263 }
264
265 t.Logf("Perfect confluence verified: all 5 runs produced exactly %d reductions", first)
266}