A Golang runtime and compilation backend for Delta Interaction Nets.
at main 7.9 kB view raw
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}