A Golang runtime and compilation backend for Delta Interaction Nets.
1package lambda
2
3import (
4 "fmt"
5 "testing"
6
7 "github.com/vic/godnet/pkg/deltanet"
8)
9
10// TestIdentityFunction tests the simplest lambda term: (λx. x)
11// Paper context: "In λ L-calculus, variables appear exactly once."
12// This tests the basic fan annihilation rule where an abstraction immediately
13// applies to an argument, verifying the core β-reduction mechanism.
14func TestIdentityFunction(t *testing.T) {
15 // Paper: "The only interaction rule in Δ L-Nets is fan annihilation,
16 // which expresses β-reduction."
17
18 n := deltanet.NewNetwork()
19 n.EnableTrace(100)
20
21 // Parse (λx. x)
22 term, err := Parse("(x: x)")
23 if err != nil {
24 t.Fatalf("Parse error: %v", err)
25 }
26
27 root, port, varNames := ToDeltaNet(term, n)
28 output := n.NewVar()
29 n.Link(root, port, output, 0)
30
31 // Reduce to normal form
32 n.ReduceToNormalForm()
33
34 // Identity function is already in normal form (no active pairs)
35 // Result should be the abstraction itself
36 resultNode, resultPort := n.GetLink(output, 0)
37 result := FromDeltaNet(n, resultNode, resultPort, varNames)
38
39 t.Logf("Identity function: (λx. x) → %v", result)
40
41 // Verify no reductions occurred (already in normal form)
42 stats := n.GetStats()
43 if stats.TotalReductions > 0 {
44 t.Logf("Note: %d reductions (may include canonicalization)", stats.TotalReductions)
45 }
46}
47
48// TestKCombinator tests the K combinator: (λx. λy. x)
49// Paper: "In Δ A-Nets (affine), applying an abstraction which doesn't use
50// its bound variable results in an eraser becoming connected to a parent port."
51// This tests erasure interaction rules and the canonicalization step.
52func TestKCombinator(t *testing.T) {
53 // Paper: "As an extreme example, applying an abstraction which doesn't use
54 // its bound variable to an argument which only uses globally-free variables
55 // produces a subnet which is disjointed from the root."
56
57 n := deltanet.NewNetwork()
58 n.EnableTrace(100)
59
60 // Parse K combinator applied twice: (((λx. λy. x) a) b) → a
61 term, err := Parse("(((x: (y: x)) a) b)")
62 if err != nil {
63 t.Fatalf("Parse error: %v", err)
64 }
65
66 root, port, varNames := ToDeltaNet(term, n)
67 output := n.NewVar()
68 n.Link(root, port, output, 0)
69
70 t.Logf("VarNames map: %v", varNames)
71
72 // Reduce to normal form
73 n.ReduceToNormalForm()
74
75 // Get what output is connected to after reduction
76 resultNode, resultPort := n.GetLink(output, 0)
77 t.Logf("Output connected to: %v id=%d port=%d", resultNode.Type(), resultNode.ID(), resultPort)
78
79 result := FromDeltaNet(n, resultNode, resultPort, varNames)
80 t.Logf("K combinator: (λx. λy. x) a b → %v", result)
81
82 // Paper: "fan annihilations are applied in leftmost-outermost order,
83 // with the final erasure canonicalization step ensuring perfect confluence"
84 stats := n.GetStats()
85 t.Logf("Reductions: %d fan-fan, %d erasures", stats.FanAnnihilation, stats.Erasure)
86
87 // Result should be 'a' (second argument 'b' is erased)
88 if v, ok := result.(Var); !ok || v.Name != "a" {
89 t.Errorf("Expected variable 'a', got %v", result)
90 }
91}
92
93// TestSCombinator tests the S combinator: (λx. λy. λz. (x z) (y z))
94// Paper: "In Δ I-Nets (relevant), replicators are needed to express optimal
95// parallel reduction involving sharing."
96// This tests fan-replicator commutation and replicator-replicator interactions.
97func TestSCombinator(t *testing.T) {
98 // Paper: "When a replicator interacts with a fan, the replicator travels
99 // through and out of the fan's two auxiliary ports, resulting in two exact
100 // copies of the replicator."
101
102 n := deltanet.NewNetwork()
103 n.EnableTrace(100)
104
105 // Parse S combinator with arguments: S K K e
106 // Where S = λx.λy.λz.(x z)(y z), K = λa.λb.a
107 term, err := Parse("((((x: (y: (z: ((x z) (y z))))) (a: (b: a))) (c: (d: c))) e)")
108 if err != nil {
109 t.Fatalf("Parse error: %v", err)
110 }
111
112 root, port, varNames := ToDeltaNet(term, n)
113 output := n.NewVar()
114 n.Link(root, port, output, 0)
115
116 // Reduce to normal form
117 n.ReduceToNormalForm()
118
119 resultNode, resultPort := n.GetLink(output, 0)
120 result := FromDeltaNet(n, resultNode, resultPort, varNames)
121 t.Logf("S combinator: S K K e → %v", result)
122
123 stats := n.GetStats()
124 t.Logf("Reductions: %d fan-fan, %d rep-rep, %d fan-rep commutations",
125 stats.FanAnnihilation, stats.RepAnnihilation, stats.FanRepCommutation)
126
127 // S K K is equivalent to I (identity), so S K K e → e
128 if v, ok := result.(Var); !ok || v.Name != "e" {
129 t.Errorf("Expected variable 'e', got %v", result)
130 }
131}
132
133// TestSharing tests sharing of subterms through replicators
134// Paper: "Each instance of a replicator incorporates information that in
135// previous models was spread across multiple agents, such as indexed fans
136// and delimiters. This consolidation of information enables simplifications
137// that were previously unfeasible."
138func TestSharing(t *testing.T) {
139 // Paper: "Instead of making use of delimiters, sharing is expressed through
140 // a single agent type which allows any number of auxiliary ports, called
141 // a *replicator*."
142
143 n := deltanet.NewNetwork()
144 n.EnableTrace(100)
145
146 // Parse (λf. f (f x)) (λy. y)
147 // The argument (λy. y) is shared between two applications
148 term, err := Parse("((f: (f (f x))) (y: y))")
149 if err != nil {
150 t.Fatalf("Parse error: %v", err)
151 }
152
153 root, port, varNames := ToDeltaNet(term, n)
154 output := n.NewVar()
155 n.Link(root, port, output, 0)
156
157 // Reduce to normal form
158 n.ReduceToNormalForm()
159
160 resultNode, resultPort := n.GetLink(output, 0)
161 result := FromDeltaNet(n, resultNode, resultPort, varNames)
162 t.Logf("Sharing test: (λf. f (f x)) (λy. y) → %v", result)
163
164 stats := n.GetStats()
165 t.Logf("Reductions: %d total, %d fan-rep commutations",
166 stats.TotalReductions, stats.FanRepCommutation)
167
168 // Paper: "no reduction operation is applied which is rendered unnecessary
169 // later, and no reduction operation which is necessary is applied more
170 // than once."
171 // This is the optimality guarantee through sharing
172}
173
174// TestChurchNumerals tests Church encoding of natural numbers
175// Paper: Church numerals demonstrate the power of λ-calculus encoding
176// and stress-test the interaction system with nested applications.
177func TestChurchNumerals(t *testing.T) {
178 // Church numeral 0: λf. λx. x
179 // Church numeral 1: λf. λx. f x
180 // Church numeral 2: λf. λx. f (f x)
181
182 tests := []struct {
183 name string
184 input string
185 desc string
186 }{
187 {
188 name: "Zero",
189 input: "(f: (x: x))",
190 desc: "Church numeral 0 applies f zero times",
191 },
192 {
193 name: "One",
194 input: "(f: (x: (f x)))",
195 desc: "Church numeral 1 applies f once",
196 },
197 {
198 name: "Two",
199 input: "(f: (x: (f (f x))))",
200 desc: "Church numeral 2 applies f twice",
201 },
202 }
203
204 for _, tt := range tests {
205 t.Run(tt.name, func(t *testing.T) {
206 n := deltanet.NewNetwork()
207 term, err := Parse(tt.input)
208 if err != nil {
209 t.Fatalf("Parse error: %v", err)
210 }
211
212 root, port, _ := ToDeltaNet(term, n)
213 output := n.NewVar()
214 n.Link(root, port, output, 0)
215
216 // Church numerals are already in normal form (no active pairs to reduce)
217 // so we don't call ReduceToNormalForm()
218 t.Logf("%s: %s (already in normal form)", tt.name, tt.desc)
219 })
220 }
221}
222
223// TestBooleans tests Church booleans and boolean operations
224// Paper: Boolean operations demonstrate conditional evaluation and
225// the interaction between abstraction, application, and erasure.
226func TestBooleans(t *testing.T) {
227 // Church true: λx. λy. x (K combinator)
228 // Church false: λx. λy. y (K I combinator)
229 // NOT: λb. b false true
230
231 tests := []struct {
232 name string
233 input string
234 expected string
235 }{
236 {
237 name: "NOT true",
238 input: "((b: ((b (x: (y: y))) (x: (y: x)))) (x: (y: x)))",
239 expected: "false",
240 },
241 {
242 name: "NOT false",
243 input: "((b: ((b (x: (y: y))) (x: (y: x)))) (x: (y: y)))",
244 expected: "true",
245 },
246 {
247 name: "AND true true",
248 input: "(((a: (b: ((a b) a))) (x: (y: x))) (x: (y: x)))",
249 expected: "true",
250 },
251 {
252 name: "AND true false",
253 input: "(((a: (b: ((a b) a))) (x: (y: x))) (x: (y: y)))",
254 expected: "false",
255 },
256 }
257
258 for _, tt := range tests {
259 t.Run(tt.name, func(t *testing.T) {
260 n := deltanet.NewNetwork()
261 term, err := Parse(tt.input)
262 if err != nil {
263 t.Fatalf("Parse error: %v", err)
264 }
265
266 root, port, varNames := ToDeltaNet(term, n)
267 output := n.NewVar()
268 n.Link(root, port, output, 0)
269
270 n.ReduceToNormalForm()
271
272 resultNode, resultPort := n.GetLink(output, 0)
273 result := FromDeltaNet(n, resultNode, resultPort, varNames)
274 t.Logf("%s → %v", tt.name, result)
275
276 // Paper: "in the Δ A-Nets system, fan annihilations are applied in
277 // leftmost-outermost order, with the final erasure canonicalization
278 // step ensuring perfect confluence"
279 stats := n.GetStats()
280 t.Logf("Reductions: %d fan-fan, %d erasures", stats.FanAnnihilation, stats.Erasure)
281 })
282 }
283}
284
285// TestPairs tests Church pairs (tuples)
286// Paper: Pairs demonstrate product types in λ-calculus and stress-test
287// the interaction between multiple abstractions and applications.
288func TestPairs(t *testing.T) {
289 // Paper: Testing structural operations like pair construction and projection
290
291 // pair = λx. λy. λf. f x y
292 // fst = λp. p (λx. λy. x)
293 // snd = λp. p (λx. λy. y)
294
295 tests := []struct {
296 name string
297 input string
298 desc string
299 }{
300 {
301 name: "fst",
302 input: "((p: (p (x: (y: x)))) ((x: (y: (f: ((f x) y)))) a b))",
303 desc: "First projection of pair (a, b) → a",
304 },
305 {
306 name: "snd",
307 input: "((p: (p (x: (y: y)))) ((x: (y: (f: ((f x) y)))) a b))",
308 desc: "Second projection of pair (a, b) → b",
309 },
310 }
311
312 for _, tt := range tests {
313 t.Run(tt.name, func(t *testing.T) {
314 n := deltanet.NewNetwork()
315 term, err := Parse(tt.input)
316 if err != nil {
317 t.Fatalf("Parse error: %v", err)
318 }
319
320 root, port, varNames := ToDeltaNet(term, n)
321 output := n.NewVar()
322 n.Link(root, port, output, 0)
323
324 n.ReduceToNormalForm()
325
326 resultNode, resultPort := n.GetLink(output, 0)
327 result := FromDeltaNet(n, resultNode, resultPort, varNames)
328 t.Logf("%s: %s → %v", tt.name, tt.desc, result)
329
330 stats := n.GetStats()
331 t.Logf("Reductions: %d total", stats.TotalReductions)
332 })
333 }
334}
335
336// TestLetBinding tests let-expressions (syntactic sugar for application)
337// Paper: Let-bindings demonstrate how syntactic conveniences translate
338// to the core calculus through β-reduction.
339func TestLetBinding(t *testing.T) {
340 // let x = v in e ≡ (λx. e) v
341
342 tests := []struct {
343 name string
344 input string
345 desc string
346 }{
347 {
348 name: "let simple",
349 input: "((x: (x x)) (y: y))",
350 desc: "let x = (λy. y) in (x x) → (λy. y)",
351 },
352 {
353 name: "let id",
354 input: "((x: x) a)",
355 desc: "let x = a in x → a",
356 },
357 }
358
359 for _, tt := range tests {
360 t.Run(tt.name, func(t *testing.T) {
361 n := deltanet.NewNetwork()
362 term, err := Parse(tt.input)
363 if err != nil {
364 t.Fatalf("Parse error: %v", err)
365 }
366
367 root, port, varNames := ToDeltaNet(term, n)
368 output := n.NewVar()
369 n.Link(root, port, output, 0)
370
371 n.ReduceToNormalForm()
372
373 resultNode, resultPort := n.GetLink(output, 0)
374 result := FromDeltaNet(n, resultNode, resultPort, varNames)
375 t.Logf("%s: %s → %v", tt.name, tt.desc, result)
376 })
377 }
378}
379
380// TestComplexSharing tests complex sharing patterns
381// Paper: "The additional degrees of freedom in Δ-Nets allow it to realize
382// optimal reduction in the manner envisioned by Lévy, i.e., no reduction
383// operation is applied which is rendered unnecessary later, and no reduction
384// operation which is necessary is applied more than once."
385func TestComplexSharing(t *testing.T) {
386 n := deltanet.NewNetwork()
387 n.EnableTrace(100)
388
389 // Complex term with nested sharing
390 // (λf. (f (f (λx. x)))) (λg. (g (λy. y)))
391 term, err := Parse("((f: (f (f (x: x)))) (g: (g (y: y))))")
392 if err != nil {
393 t.Fatalf("Parse error: %v", err)
394 }
395
396 root, port, varNames := ToDeltaNet(term, n)
397 output := n.NewVar()
398 n.Link(root, port, output, 0)
399
400 // Reduce to normal form
401 n.ReduceToNormalForm()
402
403 resultNode, resultPort := n.GetLink(output, 0)
404 result := FromDeltaNet(n, resultNode, resultPort, varNames)
405 t.Logf("Complex sharing: → %v", result)
406
407 stats := n.GetStats()
408 t.Logf("Optimality metrics:")
409 t.Logf(" Total reductions: %d", stats.TotalReductions)
410 t.Logf(" Fan annihilations: %d", stats.FanAnnihilation)
411 t.Logf(" Fan-rep commutations: %d", stats.FanRepCommutation)
412 t.Logf(" Rep-rep interactions: %d", stats.RepAnnihilation+stats.RepCommutation)
413
414 // Paper: "This consolidation of information enables simplifications that
415 // were previously unfeasible, and leads to constant memory usage"
416}
417
418// TestNestedApplications tests deeply nested application chains
419// Paper: Nested applications test the depth-based priority system
420// and verify that leftmost-outermost ordering is maintained.
421func TestNestedApplications(t *testing.T) {
422 // Paper: "The reduction order which guarantees that replicator merges
423 // happen as early as possible, minimizing the total number of reductions,
424 // is a sequential leftmost-outermost order"
425
426 n := deltanet.NewNetwork()
427 n.EnableTrace(100)
428
429 // Nested applications: (((f a) b) c)
430 term, err := Parse("((((f: f) (a: a)) (b: b)) c)")
431 if err != nil {
432 t.Fatalf("Parse error: %v", err)
433 }
434
435 root, port, varNames := ToDeltaNet(term, n)
436 output := n.NewVar()
437 n.Link(root, port, output, 0)
438
439 n.ReduceToNormalForm()
440
441 resultNode, resultPort := n.GetLink(output, 0)
442 result := FromDeltaNet(n, resultNode, resultPort, varNames)
443 t.Logf("Nested applications: → %v", result)
444
445 // Verify leftmost-outermost ordering through trace
446 trace := n.TraceSnapshot()
447 t.Logf("Trace length: %d interactions", len(trace))
448
449 for i, ev := range trace {
450 t.Logf(" Step %d: %v (A: %v#%d, B: %v#%d)", i, ev.Rule, ev.AType, ev.AID, ev.BType, ev.BID)
451 }
452
453 // Paper: "In order to ensure that no reduction operations are applied in
454 // a subnet that is later going to be erased, a sequential leftmost-outermost
455 // reduction order needs to be followed."
456}
457
458// TestFreeVariables tests handling of free (global) variables
459// Paper: Free variables demonstrate the interface between the λ-term
460// and its environment, represented by Var nodes in the net.
461func TestFreeVariables(t *testing.T) {
462 // Paper: "The free-variable fragment contains a single-port (non-agent) node,
463 // which is represented by the name of the associated free variable in the
464 // λ-term."
465
466 tests := []struct {
467 name string
468 input string
469 desc string
470 }{
471 {
472 name: "free_1",
473 input: "x",
474 desc: "Single free variable",
475 },
476 {
477 name: "free_app",
478 input: "(f a)",
479 desc: "Application with free variables",
480 },
481 }
482
483 for _, tt := range tests {
484 t.Run(tt.name, func(t *testing.T) {
485 n := deltanet.NewNetwork()
486 term, err := Parse(tt.input)
487 if err != nil {
488 t.Fatalf("Parse error: %v", err)
489 }
490
491 root, port, varNames := ToDeltaNet(term, n)
492 output := n.NewVar()
493 n.Link(root, port, output, 0)
494
495 n.ReduceToNormalForm()
496
497 resultNode, resultPort := n.GetLink(output, 0)
498 result := FromDeltaNet(n, resultNode, resultPort, varNames)
499 t.Logf("%s: %s → %v", tt.name, tt.desc, result)
500
501 // Free variables remain as interface nodes
502 })
503 }
504}
505
506// TestMixedFeatures tests terms combining multiple features
507// Paper: Real-world terms combine linear, affine, and relevant features,
508// exercising all subsystems (Δ L, Δ A, Δ I) simultaneously in Δ K.
509func TestMixedFeatures(t *testing.T) {
510 // Paper: "the Δ-Nets core interaction system decomposes perfectly into
511 // three overlapping subsystems, each analogous to a substructure λ-calculus."
512
513 n := deltanet.NewNetwork()
514 n.EnableTrace(100)
515
516 // Mixed term: (λf. λx. f (f x)) (λy. λz. y) a b
517 // Combines: sharing (f used twice), erasure (z unused), free vars (a, b)
518 term, err := Parse("((((f: (x: (f (f x)))) (y: (z: y))) a) b)")
519 if err != nil {
520 t.Fatalf("Parse error: %v", err)
521 }
522
523 root, port, varNames := ToDeltaNet(term, n)
524 output := n.NewVar()
525 n.Link(root, port, output, 0)
526
527 n.ReduceToNormalForm()
528
529 resultNode, resultPort := n.GetLink(output, 0)
530 result := FromDeltaNet(n, resultNode, resultPort, varNames)
531 t.Logf("Mixed features: → %v", result)
532
533 stats := n.GetStats()
534 t.Logf("Mixed system statistics:")
535 t.Logf(" Δ L (linear): %d fan annihilations", stats.FanAnnihilation)
536 t.Logf(" Δ A (affine): %d erasures", stats.Erasure)
537 t.Logf(" Δ I (relevant): %d fan-rep commutations", stats.FanRepCommutation)
538 t.Logf(" Total: %d reductions", stats.TotalReductions)
539
540 // Paper: "The full Δ-Nets system may also be referred to as Δ K-Nets"
541 // and handles all four calculi: λ L, λ A, λ I, λ K
542}
543
544// TestNonNormalizingTerm tests that non-normalizing terms maintain constant memory
545// Paper: "This consolidation of information enables simplifications that were
546// previously unfeasible, and leads to constant memory usage in the reduction
547// of (λx. x x)(λy. y y), for example."
548func TestNonNormalizingTerm(t *testing.T) {
549 // Paper: The classic diverging term Ω = (λx. x x)(λx. x x)
550 // In Delta-Nets, this should use constant memory despite infinite reduction
551
552 n := deltanet.NewNetwork()
553
554 // Parse Ω
555 term, err := Parse("((x: (x x)) (y: (y y)))")
556 if err != nil {
557 t.Fatalf("Parse error: %v", err)
558 }
559
560 root, port, _ := ToDeltaNet(term, n)
561 output := n.NewVar()
562 n.Link(root, port, output, 0)
563
564 // Get initial counts
565 initialActive := n.ActiveNodeCount()
566 initialTotal := n.NodeCount()
567
568 // Reduce for several steps (not to completion as it diverges)
569 maxSteps := uint64(1000)
570 performedSteps := n.ReduceWithLimit(maxSteps)
571
572 finalActive := n.ActiveNodeCount()
573 finalTotal := n.NodeCount()
574 activeGrowth := finalActive - initialActive
575 totalGrowth := finalTotal - initialTotal
576
577 t.Logf("Non-normalizing term: performed %d reductions", performedSteps)
578 t.Logf("Active nodes: initial=%d, final=%d, growth=%d",
579 initialActive, finalActive, activeGrowth)
580 t.Logf("Total nodes (including dead): initial=%d, final=%d, growth=%d",
581 initialTotal, finalTotal, totalGrowth)
582
583 // Paper: "leads to constant memory usage"
584 // With garbage collection of dead nodes, active memory should remain bounded.
585 // Dead nodes are periodically removed from the registry during ReduceWithLimit.
586
587 if performedSteps > 0 {
588 activeGrowthRate := float64(activeGrowth) / float64(performedSteps)
589 t.Logf("Active growth rate: %.4f nodes per reduction", activeGrowthRate)
590
591 // Verify active memory stays bounded (near-constant)
592 // With GC, active nodes should not grow linearly
593 if activeGrowthRate > 0.5 {
594 t.Errorf("Active growth rate too high: %.4f nodes/reduction (expected < 0.5)",
595 activeGrowthRate)
596 }
597
598 // Total nodes may grow temporarily but should be cleaned by GC
599 totalGrowthRate := float64(totalGrowth) / float64(performedSteps)
600 t.Logf("Total growth rate: %.4f nodes per reduction", totalGrowthRate)
601
602 if totalGrowthRate > 1.0 {
603 t.Errorf("Total growth rate too high: %.4f (expected < 1.0 with GC)",
604 totalGrowthRate)
605 }
606 }
607}
608
609// TestOptimalityExample tests the example from the paper demonstrating
610// optimal reduction without unnecessary operations
611// Paper: Term from Section 1 that has no optimal strategy in standard
612// λ-calculus but is optimally reduced in Delta-Nets
613func TestOptimalityExample(t *testing.T) {
614 // Paper: "The Delta-Nets algorithm solves the longstanding enigma of
615 // optimal λ-calculi reduction with groundbreaking clarity."
616
617 n := deltanet.NewNetwork()
618 n.EnableTrace(100)
619
620 // Complex term from paper demonstrating optimality
621 // ((λg. g (g λx. x)) (λh. (λf. f (f λz. z)) (λw. h (w λy. y))))
622 term, err := Parse("((g: (g (g (x: x)))) (h: (((f: (f (f (z: z)))) (w: (h (w (y: y))))))))")
623 if err != nil {
624 t.Fatalf("Parse error: %v", err)
625 }
626
627 root, port, varNames := ToDeltaNet(term, n)
628 output := n.NewVar()
629 n.Link(root, port, output, 0)
630
631 n.ReduceToNormalForm()
632
633 resultNode, resultPort := n.GetLink(output, 0)
634 result := FromDeltaNet(n, resultNode, resultPort, varNames)
635 t.Logf("Optimality example: → %v", result)
636
637 stats := n.GetStats()
638 t.Logf("Optimal reduction statistics:")
639 t.Logf(" Total reductions: %d", stats.TotalReductions)
640 t.Logf(" Fan annihilations: %d", stats.FanAnnihilation)
641 t.Logf(" Rep annihilations: %d", stats.RepAnnihilation)
642 t.Logf(" Fan-rep commutations: %d", stats.FanRepCommutation)
643 t.Logf(" Rep commutations: %d", stats.RepCommutation)
644
645 // Paper: "no reduction operation is applied which is rendered unnecessary
646 // later, and no reduction operation which is necessary is applied more
647 // than once."
648 // This is verified by comparing reduction counts with theoretical minimum
649}
650
651// normalizeForComparison normalizes a term for structural comparison
652// by renaming bound variables consistently
653func normalizeForComparison(t Term) string {
654 bindings := make(map[string]string)
655 var idx int
656 var walk func(Term) Term
657 walk = func(tt Term) Term {
658 switch v := tt.(type) {
659 case Var:
660 if name, ok := bindings[v.Name]; ok {
661 return Var{Name: name}
662 }
663 return Var{Name: "<free>"}
664 case Abs:
665 canon := fmt.Sprintf("x%d", idx)
666 idx++
667 old, had := bindings[v.Arg]
668 bindings[v.Arg] = canon
669 body := walk(v.Body)
670 if had {
671 bindings[v.Arg] = old
672 } else {
673 delete(bindings, v.Arg)
674 }
675 return Abs{Arg: canon, Body: body}
676 case App:
677 return App{Fun: walk(v.Fun), Arg: walk(v.Arg)}
678 default:
679 return tt
680 }
681 }
682 return fmt.Sprintf("%s", walk(t))
683}