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