A Golang runtime and compilation backend for Delta Interaction Nets.

init


gentests

test

readme

+15
.github/FUNDING.yml
··· 1 + # These are supported funding model platforms 2 + 3 + github: [vic] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 + patreon: # Replace with a single Patreon username 5 + open_collective: # Replace with a single Open Collective username 6 + ko_fi: oeiuwq # Replace with a single Ko-fi username 7 + tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 + community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 + liberapay: # Replace with a single Liberapay username 10 + issuehunt: # Replace with a single IssueHunt username 11 + lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 + polar: # Replace with a single Polar username 13 + buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 + thanks_dev: # Replace with a single thanks.dev username 15 + custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+39
.github/workflows/go.yml
··· 1 + name: Build & Test 2 + 3 + on: 4 + push: 5 + branches: [ "main" ] 6 + pull_request: 7 + branches: [ "main" ] 8 + 9 + jobs: 10 + 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + 16 + - name: Set up Go 17 + uses: actions/setup-go@v5 18 + with: 19 + go-version: '1.25' 20 + 21 + - name: Test 22 + run: go test -v ./... 23 + 24 + lint: 25 + runs-on: ubuntu-latest 26 + steps: 27 + - uses: actions/checkout@v4 28 + 29 + - name: Set up Go 30 + uses: actions/setup-go@v5 31 + with: 32 + go-version: '1.25' 33 + 34 + - name: Check if `go fmt` and `go mod tidy` make any changes 35 + run: | 36 + set -x 37 + go fmt ./... 38 + go mod tidy 39 + git diff --exit-code
+12
.tangled/workflows/mirror.yml
··· 1 + when: 2 + - event: ["push"] 3 + branch: ["*"] 4 + engine: "nixery" 5 + clone: 6 + skip: true 7 + dependencies: 8 + nixpkgs: 9 + - gh 10 + steps: 11 + - name: mirror 12 + command: gh -R vic/vic issue comment 3 -b godnet
+201
LICENSE
··· 1 + Apache License 2 + Version 2.0, January 2004 3 + http://www.apache.org/licenses/ 4 + 5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 + 7 + 1. Definitions. 8 + 9 + "License" shall mean the terms and conditions for use, reproduction, 10 + and distribution as defined by Sections 1 through 9 of this document. 11 + 12 + "Licensor" shall mean the copyright owner or entity authorized by 13 + the copyright owner that is granting the License. 14 + 15 + "Legal Entity" shall mean the union of the acting entity and all 16 + other entities that control, are controlled by, or are under common 17 + control with that entity. For the purposes of this definition, 18 + "control" means (i) the power, direct or indirect, to cause the 19 + direction or management of such entity, whether by contract or 20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 + outstanding shares, or (iii) beneficial ownership of such entity. 22 + 23 + "You" (or "Your") shall mean an individual or Legal Entity 24 + exercising permissions granted by this License. 25 + 26 + "Source" form shall mean the preferred form for making modifications, 27 + including but not limited to software source code, documentation 28 + source, and configuration files. 29 + 30 + "Object" form shall mean any form resulting from mechanical 31 + transformation or translation of a Source form, including but 32 + not limited to compiled object code, generated documentation, 33 + and conversions to other media types. 34 + 35 + "Work" shall mean the work of authorship, whether in Source or 36 + Object form, made available under the License, as indicated by a 37 + copyright notice that is included in or attached to the work 38 + (an example is provided in the Appendix below). 39 + 40 + "Derivative Works" shall mean any work, whether in Source or Object 41 + form, that is based on (or derived from) the Work and for which the 42 + editorial revisions, annotations, elaborations, or other modifications 43 + represent, as a whole, an original work of authorship. For the purposes 44 + of this License, Derivative Works shall not include works that remain 45 + separable from, or merely link (or bind by name) to the interfaces of, 46 + the Work and Derivative Works thereof. 47 + 48 + "Contribution" shall mean any work of authorship, including 49 + the original version of the Work and any modifications or additions 50 + to that Work or Derivative Works thereof, that is intentionally 51 + submitted to Licensor for inclusion in the Work by the copyright owner 52 + or by an individual or Legal Entity authorized to submit on behalf of 53 + the copyright owner. For the purposes of this definition, "submitted" 54 + means any form of electronic, verbal, or written communication sent 55 + to the Licensor or its representatives, including but not limited to 56 + communication on electronic mailing lists, source code control systems, 57 + and issue tracking systems that are managed by, or on behalf of, the 58 + Licensor for the purpose of discussing and improving the Work, but 59 + excluding communication that is conspicuously marked or otherwise 60 + designated in writing by the copyright owner as "Not a Contribution." 61 + 62 + "Contributor" shall mean Licensor and any individual or Legal Entity 63 + on behalf of whom a Contribution has been received by Licensor and 64 + subsequently incorporated within the Work. 65 + 66 + 2. Grant of Copyright License. Subject to the terms and conditions of 67 + this License, each Contributor hereby grants to You a perpetual, 68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 + copyright license to reproduce, prepare Derivative Works of, 70 + publicly display, publicly perform, sublicense, and distribute the 71 + Work and such Derivative Works in Source or Object form. 72 + 73 + 3. Grant of Patent License. Subject to the terms and conditions of 74 + this License, each Contributor hereby grants to You a perpetual, 75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 + (except as stated in this section) patent license to make, have made, 77 + use, offer to sell, sell, import, and otherwise transfer the Work, 78 + where such license applies only to those patent claims licensable 79 + by such Contributor that are necessarily infringed by their 80 + Contribution(s) alone or by combination of their Contribution(s) 81 + with the Work to which such Contribution(s) was submitted. If You 82 + institute patent litigation against any entity (including a 83 + cross-claim or counterclaim in a lawsuit) alleging that the Work 84 + or a Contribution incorporated within the Work constitutes direct 85 + or contributory patent infringement, then any patent licenses 86 + granted to You under this License for that Work shall terminate 87 + as of the date such litigation is filed. 88 + 89 + 4. Redistribution. You may reproduce and distribute copies of the 90 + Work or Derivative Works thereof in any medium, with or without 91 + modifications, and in Source or Object form, provided that You 92 + meet the following conditions: 93 + 94 + (a) You must give any other recipients of the Work or 95 + Derivative Works a copy of this License; and 96 + 97 + (b) You must cause any modified files to carry prominent notices 98 + stating that You changed the files; and 99 + 100 + (c) You must retain, in the Source form of any Derivative Works 101 + that You distribute, all copyright, patent, trademark, and 102 + attribution notices from the Source form of the Work, 103 + excluding those notices that do not pertain to any part of 104 + the Derivative Works; and 105 + 106 + (d) If the Work includes a "NOTICE" text file as part of its 107 + distribution, then any Derivative Works that You distribute must 108 + include a readable copy of the attribution notices contained 109 + within such NOTICE file, excluding those notices that do not 110 + pertain to any part of the Derivative Works, in at least one 111 + of the following places: within a NOTICE text file distributed 112 + as part of the Derivative Works; within the Source form or 113 + documentation, if provided along with the Derivative Works; or, 114 + within a display generated by the Derivative Works, if and 115 + wherever such third-party notices normally appear. The contents 116 + of the NOTICE file are for informational purposes only and 117 + do not modify the License. You may add Your own attribution 118 + notices within Derivative Works that You distribute, alongside 119 + or as an addendum to the NOTICE text from the Work, provided 120 + that such additional attribution notices cannot be construed 121 + as modifying the License. 122 + 123 + You may add Your own copyright statement to Your modifications and 124 + may provide additional or different license terms and conditions 125 + for use, reproduction, or distribution of Your modifications, or 126 + for any such Derivative Works as a whole, provided Your use, 127 + reproduction, and distribution of the Work otherwise complies with 128 + the conditions stated in this License. 129 + 130 + 5. Submission of Contributions. Unless You explicitly state otherwise, 131 + any Contribution intentionally submitted for inclusion in the Work 132 + by You to the Licensor shall be under the terms and conditions of 133 + this License, without any additional terms or conditions. 134 + Notwithstanding the above, nothing herein shall supersede or modify 135 + the terms of any separate license agreement you may have executed 136 + with Licensor regarding such Contributions. 137 + 138 + 6. Trademarks. This License does not grant permission to use the trade 139 + names, trademarks, service marks, or product names of the Licensor, 140 + except as required for reasonable and customary use in describing the 141 + origin of the Work and reproducing the content of the NOTICE file. 142 + 143 + 7. Disclaimer of Warranty. Unless required by applicable law or 144 + agreed to in writing, Licensor provides the Work (and each 145 + Contributor provides its Contributions) on an "AS IS" BASIS, 146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 + implied, including, without limitation, any warranties or conditions 148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 + PARTICULAR PURPOSE. You are solely responsible for determining the 150 + appropriateness of using or redistributing the Work and assume any 151 + risks associated with Your exercise of permissions under this License. 152 + 153 + 8. Limitation of Liability. In no event and under no legal theory, 154 + whether in tort (including negligence), contract, or otherwise, 155 + unless required by applicable law (such as deliberate and grossly 156 + negligent acts) or agreed to in writing, shall any Contributor be 157 + liable to You for damages, including any direct, indirect, special, 158 + incidental, or consequential damages of any character arising as a 159 + result of this License or out of the use or inability to use the 160 + Work (including but not limited to damages for loss of goodwill, 161 + work stoppage, computer failure or malfunction, or any and all 162 + other commercial damages or losses), even if such Contributor 163 + has been advised of the possibility of such damages. 164 + 165 + 9. Accepting Warranty or Additional Liability. While redistributing 166 + the Work or Derivative Works thereof, You may choose to offer, 167 + and charge a fee for, acceptance of support, warranty, indemnity, 168 + or other liability obligations and/or rights consistent with this 169 + License. However, in accepting such obligations, You may act only 170 + on Your own behalf and on Your sole responsibility, not on behalf 171 + of any other Contributor, and only if You agree to indemnify, 172 + defend, and hold each Contributor harmless for any liability 173 + incurred by, or claims asserted against, such Contributor by reason 174 + of your accepting any such warranty or additional liability. 175 + 176 + END OF TERMS AND CONDITIONS 177 + 178 + APPENDIX: How to apply the Apache License to your work. 179 + 180 + To apply the Apache License to your work, attach the following 181 + boilerplate notice, with the fields enclosed by brackets "[]" 182 + replaced with your own identifying information. (Don't include 183 + the brackets!) The text should be enclosed in the appropriate 184 + comment syntax for the file format. We also recommend that a 185 + file or class name and description of purpose be included on the 186 + same "printed page" as the copyright notice for easier 187 + identification within third-party archives. 188 + 189 + Copyright [yyyy] [name of copyright owner] 190 + 191 + Licensed under the Apache License, Version 2.0 (the "License"); 192 + you may not use this file except in compliance with the License. 193 + You may obtain a copy of the License at 194 + 195 + http://www.apache.org/licenses/LICENSE-2.0 196 + 197 + Unless required by applicable law or agreed to in writing, software 198 + distributed under the License is distributed on an "AS IS" BASIS, 199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 + See the License for the specific language governing permissions and 201 + limitations under the License.
+1
README.md
··· 1 + # GoD-Net - A Golang implementation of Delta Interaction Nets.
+148
cmd/gentests/helper/reduction.go
··· 1 + package gentests 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + "testing" 7 + "time" 8 + 9 + "github.com/vic/godnet/pkg/deltanet" 10 + "github.com/vic/godnet/pkg/lambda" 11 + ) 12 + 13 + func CheckLambdaReduction(t *testing.T, testName string, inputStr string, outputStr string) { 14 + expectedOutput := strings.TrimSpace(outputStr) 15 + 16 + // Parse expected output 17 + expectedTerm, err := lambda.Parse(expectedOutput) 18 + if err != nil { 19 + t.Fatalf("Parse error for expected output: %v", err) 20 + } 21 + 22 + // Instead of round-tripping expected output through DeltaNet (which 23 + // loses original free variable names and produces implementation 24 + // placeholders), we normalize both expected and actual terms to a 25 + // structural, alpha-renamed form and compare those. Normalization 26 + // replaces all free variable names with the placeholder "<free>" 27 + // and renames bound variables to a canonical sequence x0, x1, ... 28 + normalize := func(t lambda.Term) lambda.Term { 29 + // mapping from original bound name -> canonical name 30 + bindings := make(map[string]string) 31 + var idx int 32 + var walk func(lambda.Term) lambda.Term 33 + walk = func(tt lambda.Term) lambda.Term { 34 + switch v := tt.(type) { 35 + case lambda.Var: 36 + if name, ok := bindings[v.Name]; ok { 37 + return lambda.Var{Name: name} 38 + } 39 + return lambda.Var{Name: "<free>"} 40 + case lambda.Abs: 41 + canon := fmt.Sprintf("x%d", idx) 42 + idx++ 43 + // shadowing: save old if any 44 + old, had := bindings[v.Arg] 45 + bindings[v.Arg] = canon 46 + body := walk(v.Body) 47 + if had { 48 + bindings[v.Arg] = old 49 + } else { 50 + delete(bindings, v.Arg) 51 + } 52 + return lambda.Abs{Arg: canon, Body: body} 53 + case lambda.App: 54 + return lambda.App{Fun: walk(v.Fun), Arg: walk(v.Arg)} 55 + default: 56 + return tt 57 + } 58 + } 59 + return walk(t) 60 + } 61 + 62 + // Parse input 63 + term, err := lambda.Parse(inputStr) 64 + if err != nil { 65 + t.Fatalf("Parse error: %v", err) 66 + } 67 + 68 + // Convert input to Net 69 + net := deltanet.NewNetwork() 70 + root, port := lambda.ToDeltaNet(term, net) 71 + 72 + // Connect to output interface 73 + output := net.NewVar() 74 + net.Link(root, port, output, 0) 75 + 76 + // Reduce 77 + start := time.Now() 78 + net.ReduceAll() 79 + elapsed := time.Since(start) 80 + 81 + // Optionally canonicalize/prune unreachable nodes when the expected 82 + // result is a simple free variable. Canonicalization (erasure 83 + // canonicalization) is only necessary for tests where the expected 84 + // canonical form is a free variable and pruning unreachable subnets 85 + // is required to match the intended lambda term. 86 + resNode, resPort := net.GetLink(output, 0) 87 + if _, ok := expectedTerm.(lambda.Var); ok { 88 + net.Canonicalize(resNode, resPort) 89 + // refresh root after canonicalization 90 + resNode, resPort = net.GetLink(output, 0) 91 + } 92 + 93 + // Read back into a Term 94 + resNode, resPort = net.GetLink(output, 0) 95 + t.Logf("%s: root node before FromDeltaNet: %v id=%d port=%d", testName, resNode.Type(), resNode.ID(), resPort) 96 + actualTerm := lambda.FromDeltaNet(net, resNode, resPort) 97 + 98 + // If expected is a simple free variable, collapse any top-level 99 + // unused abstractions that canonicalization may have missed. This 100 + // ensures cases where an outer binder is unused (should be erased) 101 + // are represented as the free value for comparison. 102 + if _, ok := expectedTerm.(lambda.Var); ok { 103 + // Helper to test if a name occurs in a term 104 + var occurs func(name string, t lambda.Term) bool 105 + occurs = func(name string, t lambda.Term) bool { 106 + switch v := t.(type) { 107 + case lambda.Var: 108 + return v.Name == name 109 + case lambda.Abs: 110 + // shadowing: if the inner arg equals name, occurrences inside are shadowed 111 + if v.Arg == name { 112 + return false 113 + } 114 + return occurs(name, v.Body) 115 + case lambda.App: 116 + return occurs(name, v.Fun) || occurs(name, v.Arg) 117 + default: 118 + return false 119 + } 120 + } 121 + 122 + // Strip top-level unused abstractions 123 + for { 124 + ab, ok := actualTerm.(lambda.Abs) 125 + if !ok { 126 + break 127 + } 128 + if !occurs(ab.Arg, ab.Body) { 129 + actualTerm = ab.Body 130 + continue 131 + } 132 + break 133 + } 134 + } 135 + 136 + // Normalize both expectedTerm and actualTerm for comparison 137 + normExpected := normalize(expectedTerm) 138 + normActual := normalize(actualTerm) 139 + 140 + if fmt.Sprintf("%s", normActual) != fmt.Sprintf("%s", normExpected) { 141 + t.Errorf("Mismatch in %s:\nInput: %s\nExpected: %s\nActual: %s", testName, inputStr, fmt.Sprintf("%s", normExpected), fmt.Sprintf("%s", normActual)) 142 + } 143 + 144 + // Optional: Check stats if stats.nix exists 145 + // For now, we just log them 146 + stats := net.GetStats() 147 + t.Logf("%s: %d reductions in %v", testName, stats.TotalReductions, elapsed) 148 + }
+148
cmd/gentests/main.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + "path/filepath" 7 + 8 + "github.com/vic/godnet/pkg/lambda" 9 + ) 10 + 11 + type TestCase struct { 12 + Name string 13 + Input string 14 + Output string 15 + } 16 + 17 + const testTemplate = ` 18 + package gentests 19 + import _ "embed" 20 + import "testing" 21 + import "github.com/vic/godnet/cmd/gentests/helper" 22 + //go:embed input.nix 23 + var input string 24 + //go:embed output.nix 25 + var output string 26 + func Test_%s_Reduction(t *testing.T) { 27 + gentests.CheckLambdaReduction(t, "%s", input, output) 28 + } 29 + ` 30 + 31 + func main() { 32 + tests := []TestCase{ 33 + // Identity 34 + {"001_id", "x: x", "y: y"}, 35 + {"002_id_id", "(x: x) (y: y)", "z: z"}, 36 + 37 + // K Combinator (Erasure) 38 + {"003_k_1", "(x: y: x) a b", "a"}, 39 + {"004_k_2", "(x: y: y) a b", "b"}, 40 + {"005_erase_complex", "(x: y: x) a ((z: z) b)", "a"}, 41 + 42 + // S Combinator (Sharing) 43 + {"006_s_1", "(x: y: z: x z (y z)) (a: b: a) (c: d: c) e", "e"}, 44 + {"007_s_2", "(x: y: z: x z (y z)) (a: b: b) (c: d: c) e", "e"}, 45 + 46 + // Church Numerals 47 + {"010_zero", "(f: x: x) f x", "x"}, 48 + {"011_one", "(f: x: f x) f x", "f x"}, 49 + {"012_two", "(f: x: f (f x)) f x", "f (f x)"}, 50 + {"013_succ_0", "(n: f: x: f (n f x)) (f: x: x) f x", "f x"}, 51 + {"014_succ_1", "(n: f: x: f (n f x)) (f: x: f x) f x", "f (f x)"}, 52 + {"015_add_1_1", "(m: n: f: x: m f (n f x)) (f: x: f x) (f: x: f x) f x", "f (f x)"}, 53 + {"016_mul_2_2", "(m: n: f: m (n f)) (f: x: f (f x)) (f: x: f (f x)) f x", "f (f (f (f x)))"}, 54 + 55 + // Logic 56 + {"020_true", "(x: y: x) a b", "a"}, 57 + {"021_false", "(x: y: y) a b", "b"}, 58 + {"022_not_true", "(b: b (x: y: y) (x: y: x)) (x: y: x) a b", "b"}, 59 + {"023_not_false", "(b: b (x: y: y) (x: y: x)) (x: y: y) a b", "a"}, 60 + {"024_and_true_true", "(p: q: p q p) (x: y: x) (x: y: x) a b", "a"}, 61 + {"025_and_true_false", "(p: q: p q p) (x: y: x) (x: y: y) a b", "b"}, 62 + 63 + // Pairs 64 + {"030_pair_fst", "(p: p (x: y: x)) ((x: y: f: f x y) a b)", "a"}, 65 + {"031_pair_snd", "(p: p (x: y: y)) ((x: y: f: f x y) a b)", "b"}, 66 + 67 + // Let bindings 68 + {"040_let_simple", "let x = a; in x", "a"}, 69 + {"041_let_id", "let i = x: x; in i a", "a"}, 70 + {"042_let_nested", "let x = a; in let y = b; in x", "a"}, 71 + {"043_let_shadow", "let x = a; in let x = b; in x", "b"}, 72 + 73 + // Complex / Stress 74 + {"050_deep_app", "(x: x x x) (y: y)", "y: y"}, 75 + {"051_share_app", "(f: f (f x)) (y: y)", "x"}, 76 + 77 + {"060_pow_2_3", "(b: e: e b) (f: x: f (f x)) (f: x: f (f (f x))) f x", "f (f (f (f (f (f (f (f x)))))))"}, 78 + 79 + // Replicator tests 80 + {"070_share_complex", "(x: x (x a)) (y: y)", "a"}, 81 + 82 + // Erasure of shared term 83 + {"071_erase_shared", "(x: y: y) ((z: z) a) b", "b"}, 84 + 85 + // Commutation 86 + {"072_self_app", "(x: x x) (y: y)", "y: y"}, 87 + 88 + // Nested Lambdas 89 + {"080_nested_1", "x: y: z: x y z", "x: y: z: x y z"}, 90 + {"081_nested_app", "(x: y: x y) a b", "a b"}, 91 + 92 + // Free variables 93 + {"090_free_1", "x", "x"}, 94 + {"091_free_app", "x y", "x y"}, 95 + {"092_free_abs", "y: x y", "y: x y"}, 96 + 97 + // Mixed 98 + {"100_mixed_1", "(x: x) ((y: y) a)", "a"}, 99 + } 100 + 101 + baseDir := "cmd/gentests/tests" 102 + os.MkdirAll(baseDir, 0755) 103 + 104 + for _, tc := range tests { 105 + dir := filepath.Join(baseDir, tc.Name) 106 + os.MkdirAll(dir, 0755) 107 + 108 + // Normalize Input 109 + inTerm, err := lambda.Parse(tc.Input) 110 + if err != nil { 111 + fmt.Printf("Error parsing input for %s: %v\n", tc.Name, err) 112 + continue 113 + } 114 + 115 + // Normalize Output 116 + outTerm, err := lambda.Parse(tc.Output) 117 + if err != nil { 118 + fmt.Printf("Error parsing output for %s: %v\n", tc.Name, err) 119 + continue 120 + } 121 + 122 + testGo := fmt.Sprintf(testTemplate, tc.Name, tc.Name) 123 + 124 + os.WriteFile(filepath.Join(dir, "input.nix"), []byte(inTerm.String()), 0644) 125 + os.WriteFile(filepath.Join(dir, "output.nix"), []byte(outTerm.String()), 0644) 126 + os.WriteFile(filepath.Join(dir, "reduction_test.go"), []byte(testGo), 0644) 127 + } 128 + 129 + fmt.Printf("Generated %d tests\n", len(tests)) 130 + } 131 + 132 + /* 133 + func church(n int) string { 134 + body := "x" 135 + for i := 0; i < n; i++ { 136 + body = fmt.Sprintf("f (%s)", body) 137 + } 138 + return fmt.Sprintf("(f: x: %s)", body) 139 + } 140 + 141 + func churchBody(n int) string { 142 + body := "x" 143 + for i := 0; i < n; i++ { 144 + body = fmt.Sprintf("f (%s)", body) 145 + } 146 + return body 147 + } 148 + */
+66
cmd/godnet/main.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "os" 7 + 8 + "time" 9 + 10 + "github.com/vic/godnet/pkg/deltanet" 11 + "github.com/vic/godnet/pkg/lambda" 12 + ) 13 + 14 + func main() { 15 + var input []byte 16 + var err error 17 + 18 + if len(os.Args) > 1 { 19 + input, err = os.ReadFile(os.Args[1]) 20 + if err != nil { 21 + fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err) 22 + os.Exit(1) 23 + } 24 + } else { 25 + input, err = io.ReadAll(os.Stdin) 26 + if err != nil { 27 + fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err) 28 + os.Exit(1) 29 + } 30 + } 31 + 32 + term, err := lambda.Parse(string(input)) 33 + if err != nil { 34 + fmt.Fprintf(os.Stderr, "Parse error: %v\n", err) 35 + os.Exit(1) 36 + } 37 + 38 + net := deltanet.NewNetwork() 39 + root, port := lambda.ToDeltaNet(term, net) 40 + 41 + // Connect root to a dummy interface node to allow reduction at the root 42 + output := net.NewVar() 43 + net.Link(root, port, output, 0) 44 + 45 + start := time.Now() 46 + net.ReduceAll() 47 + elapsed := time.Since(start) 48 + 49 + // Read back from the output node 50 + resNode, resPort := net.GetLink(output, 0) 51 + res := lambda.FromDeltaNet(net, resNode, resPort) 52 + fmt.Println(res) 53 + 54 + stats := net.GetStats() 55 + fmt.Fprintf(os.Stderr, "\nStats:\n") 56 + fmt.Fprintf(os.Stderr, "Time: %v\n", elapsed) 57 + fmt.Fprintf(os.Stderr, "Total Reductions: %d\n", stats.TotalReductions) 58 + if elapsed.Seconds() > 0 { 59 + fmt.Fprintf(os.Stderr, "Reductions/sec: %.2f\n", float64(stats.TotalReductions)/elapsed.Seconds()) 60 + } 61 + fmt.Fprintf(os.Stderr, "Fan Annihilation: %d\n", stats.FanAnnihilation) 62 + fmt.Fprintf(os.Stderr, "Replicator Annihilation: %d\n", stats.RepAnnihilation) 63 + fmt.Fprintf(os.Stderr, "Replicator Commutation: %d\n", stats.RepCommutation) 64 + fmt.Fprintf(os.Stderr, "Fan-Replicator Commutation: %d\n", stats.FanRepCommutation) 65 + fmt.Fprintf(os.Stderr, "Erasure: %d\n", stats.Erasure) 66 + }
+3
go.mod
··· 1 + module github.com/vic/godnet 2 + 3 + go 1.25.3
+165
pkg/deltanet/canonical_test.go
··· 1 + package deltanet 2 + 3 + import ( 4 + "testing" 5 + ) 6 + 7 + func TestUnpairedReplicatorDecay(t *testing.T) { 8 + n := NewNetwork() 9 + n.EnableTrace(100) 10 + 11 + // Create a Replicator with 1 aux port, delta 0 12 + // This is the identity replicator that should decay 13 + rep := n.NewReplicator(0, []int{0}) 14 + 15 + // Create two vars to connect 16 + v1 := n.NewVar() 17 + v2 := n.NewVar() 18 + 19 + // Connect v1 to Rep Principal 20 + n.Link(v1, 0, rep, 0) 21 + // Connect v2 to Rep Aux 0 22 + n.Link(v2, 0, rep, 1) 23 + 24 + // Run Canonicalization (which should trigger decay) 25 + // We assume ReduceAll or a specific method handles this. 26 + // For now, let's assume we'll add a method for this specific check or it happens during reduction if we trigger it. 27 + // Since it's a static rule on a single node, it might need a trigger. 28 + // Let's assume we call a new method `ApplyCanonicalRules`. 29 + n.ApplyCanonicalRules() 30 + 31 + // Check if Rep is gone and v1 is connected to v2 32 + // Wait for async ops 33 + n.wg.Wait() 34 + 35 + // Verify connection 36 + if !n.IsConnected(v1, 0, v2, 0) { 37 + t.Errorf("Replicator did not decay: v1 and v2 are not connected") 38 + } 39 + 40 + // Verify trace 41 + trace := n.TraceSnapshot() 42 + found := false 43 + for _, ev := range trace { 44 + if ev.Rule == RuleRepDecay { 45 + found = true 46 + break 47 + } 48 + } 49 + if !found { 50 + t.Errorf("RuleRepDecay not found in trace") 51 + } 52 + } 53 + 54 + func TestUnpairedReplicatorMerging(t *testing.T) { 55 + n := NewNetwork() 56 + n.EnableTrace(100) 57 + 58 + // Create Rep A: Level 0, Deltas [1] (Not identity, won't decay) 59 + repA := n.NewReplicator(0, []int{1}) 60 + // Create Rep B: Level 1, Deltas [-1] (Not identity, won't decay) 61 + repB := n.NewReplicator(1, []int{-1}) 62 + 63 + // Connect Rep A Aux 0 to Rep B Principal 64 + // This satisfies the condition: B is connected to A via aux port. 65 + // Level diff: 1 - 0 = 1. Delta of port is 1. 0 + 1 = 1. OK. 66 + n.Link(repA, 1, repB, 0) 67 + 68 + // Connect vars to outside 69 + v1 := n.NewVar() 70 + v2 := n.NewVar() 71 + n.Link(v1, 0, repA, 0) 72 + n.Link(v2, 0, repB, 1) 73 + 74 + // Trigger merging 75 + n.ApplyCanonicalRules() 76 + // n.wg.Wait() 77 + 78 + // Expectation: A and B merge into a single Replicator. 79 + // v1 should be connected to the new Replicator's Principal 80 + // v2 should be connected to the new Replicator's Aux 0 81 + 82 + // Get what v1 is connected to 83 + target, _ := n.GetLink(v1, 0) 84 + if target == nil { 85 + t.Fatalf("v1 is disconnected") 86 + } 87 + if target.Type() != NodeTypeReplicator { 88 + t.Errorf("v1 connected to %v, expected Replicator", target.Type()) 89 + } 90 + if target.ID() == repA.ID() || target.ID() == repB.ID() { 91 + // Ideally it's a new node or one of them reused. 92 + // If it's one of them, the other should be gone. 93 + // Let's check if we have a single path. 94 + } 95 + 96 + // Check path v1 -> Rep -> v2 97 + // New Rep should have 1 aux port (from Rep B) 98 + if len(target.Ports()) < 2 { 99 + t.Fatalf("Target replicator has insufficient ports") 100 + } 101 + target2, _ := n.GetLink(target, 1) // Aux 0 102 + if target2 == nil { 103 + t.Fatalf("Replicator Aux 0 disconnected") 104 + } 105 + if target2.ID() != v2.ID() { 106 + t.Errorf("Replicator Aux 0 connected to %v, expected v2", target2.ID()) 107 + } 108 + 109 + // Verify trace 110 + trace := n.TraceSnapshot() 111 + found := false 112 + for _, ev := range trace { 113 + if ev.Rule == RuleRepMerge { 114 + found = true 115 + break 116 + } 117 + } 118 + if !found { 119 + t.Errorf("RuleRepMerge not found in trace") 120 + } 121 + } 122 + 123 + func TestPhase2AuxFanReplication(t *testing.T) { 124 + n := NewNetwork() 125 + n.EnableTrace(100) 126 + 127 + // Setup: Fan connected to Replicator 128 + // Fan Principal -> Rep Principal (Active Pair) 129 + // This normally triggers Fan-Rep commutation. 130 + // In Phase 2, it should trigger Aux Fan Replication. 131 + 132 + fan := n.NewFan() 133 + rep := n.NewReplicator(0, []int{0, 0}) // 2 aux ports 134 + 135 + // Connect Fan Principal to Rep Principal 136 + n.Link(fan, 0, rep, 0) 137 + 138 + // Connect other ports to vars to keep them alive 139 + v1 := n.NewVar(); n.Link(fan, 1, v1, 0) 140 + v2 := n.NewVar(); n.Link(fan, 2, v2, 0) 141 + v3 := n.NewVar(); n.Link(rep, 1, v3, 0) 142 + v4 := n.NewVar(); n.Link(rep, 2, v4, 0) 143 + 144 + // Force Phase 2 145 + n.SetPhase(2) 146 + 147 + // Reduce 148 + n.ReduceAll() 149 + 150 + // Verify trace 151 + trace := n.TraceSnapshot() 152 + found := false 153 + for _, ev := range trace { 154 + if ev.Rule == RuleAuxFanRep { 155 + found = true 156 + break 157 + } 158 + if ev.Rule == RuleFanRep { 159 + t.Errorf("Found RuleFanRep in Phase 2, expected RuleAuxFanRep") 160 + } 161 + } 162 + if !found { 163 + t.Errorf("RuleAuxFanRep not found in trace") 164 + } 165 + }
+874
pkg/deltanet/deltanet.go
··· 1 + package deltanet 2 + 3 + import ( 4 + "fmt" 5 + "runtime" 6 + "sync" 7 + "sync/atomic" 8 + ) 9 + 10 + // NodeType identifies the type of agent. 11 + type NodeType int 12 + 13 + const ( 14 + NodeTypeFan NodeType = iota 15 + NodeTypeEraser 16 + NodeTypeReplicator 17 + NodeTypeVar // Wire/Interface 18 + ) 19 + 20 + func (t NodeType) String() string { 21 + switch t { 22 + case NodeTypeFan: 23 + return "Fan" 24 + case NodeTypeEraser: 25 + return "Eraser" 26 + case NodeTypeReplicator: 27 + return "Replicator" 28 + case NodeTypeVar: 29 + return "Var" 30 + default: 31 + return "Unknown" 32 + } 33 + } 34 + 35 + // Node represents an agent in the interaction net. 36 + type Node interface { 37 + Type() NodeType 38 + ID() uint64 39 + Ports() []*Port 40 + // Specific methods for Replicators 41 + Level() int 42 + Deltas() []int 43 + } 44 + 45 + // Port represents a connection point on a node. 46 + type Port struct { 47 + Node Node 48 + Index int 49 + Wire atomic.Pointer[Wire] 50 + } 51 + 52 + // Wire represents a connection between two ports. 53 + type Wire struct { 54 + P0 atomic.Pointer[Port] 55 + P1 atomic.Pointer[Port] 56 + depth uint64 57 + } 58 + 59 + // BaseNode contains common fields. 60 + type BaseNode struct { 61 + id uint64 62 + typ NodeType 63 + ports []*Port 64 + } 65 + 66 + func (n *BaseNode) Type() NodeType { return n.typ } 67 + func (n *BaseNode) ID() uint64 { return n.id } 68 + func (n *BaseNode) Ports() []*Port { return n.ports } 69 + func (n *BaseNode) Level() int { return 0 } 70 + func (n *BaseNode) Deltas() []int { return nil } 71 + 72 + // ReplicatorNode specific fields. 73 + type ReplicatorNode struct { 74 + BaseNode 75 + level int 76 + deltas []int 77 + } 78 + 79 + func (n *ReplicatorNode) Level() int { return n.level } 80 + func (n *ReplicatorNode) Deltas() []int { return n.deltas } 81 + 82 + // Network manages the graph of nodes and interactions. 83 + type Network struct { 84 + nextID uint64 85 + scheduler *Scheduler 86 + wg sync.WaitGroup 87 + workers int 88 + startOnce sync.Once 89 + 90 + // Stats 91 + ops uint64 // Total reductions 92 + 93 + // Detailed stats 94 + statFanAnn uint64 95 + statRepAnn uint64 96 + statRepComm uint64 97 + statFanRepComm uint64 98 + statErasure uint64 99 + statRepDecay uint64 100 + statRepMerge uint64 101 + statAuxFanRep uint64 102 + // Registry of created nodes (used for canonicalization) 103 + nodes map[uint64]Node 104 + nodesMu sync.Mutex 105 + 106 + traceBuf []TraceEvent 107 + traceCap uint64 108 + traceIdx uint64 109 + traceOn uint32 110 + 111 + phase int 112 + } 113 + 114 + // Stats holds reduction statistics. 115 + type Stats struct { 116 + TotalReductions uint64 117 + FanAnnihilation uint64 118 + RepAnnihilation uint64 119 + RepCommutation uint64 120 + FanRepCommutation uint64 121 + Erasure uint64 122 + RepDecay uint64 123 + RepMerge uint64 124 + AuxFanRep uint64 125 + } 126 + 127 + func NewNetwork() *Network { 128 + n := &Network{ 129 + scheduler: NewScheduler(), 130 + workers: runtime.NumCPU(), 131 + nodes: make(map[uint64]Node), 132 + phase: 1, 133 + } 134 + return n 135 + } 136 + 137 + func (n *Network) Start() { 138 + n.startOnce.Do(func() { 139 + for i := 0; i < n.workers; i++ { 140 + go n.worker() 141 + } 142 + }) 143 + } 144 + 145 + // SetPhase sets the reduction phase. 146 + // func (n *Network) SetPhase(p int) { 147 + // n.phase = p 148 + // } 149 + 150 + 151 + func (n *Network) GetStats() Stats { 152 + return Stats{ 153 + TotalReductions: atomic.LoadUint64(&n.ops), 154 + FanAnnihilation: atomic.LoadUint64(&n.statFanAnn), 155 + RepAnnihilation: atomic.LoadUint64(&n.statRepAnn), 156 + RepCommutation: atomic.LoadUint64(&n.statRepComm), 157 + FanRepCommutation: atomic.LoadUint64(&n.statFanRepComm), 158 + Erasure: atomic.LoadUint64(&n.statErasure), 159 + RepDecay: atomic.LoadUint64(&n.statRepDecay), 160 + RepMerge: atomic.LoadUint64(&n.statRepMerge), 161 + AuxFanRep: atomic.LoadUint64(&n.statAuxFanRep), 162 + } 163 + } 164 + 165 + func (n *Network) nextNodeID() uint64 { 166 + return atomic.AddUint64(&n.nextID, 1) 167 + } 168 + 169 + func (n *Network) addNodeInternal(typ NodeType, numPorts int) *BaseNode { 170 + id := n.nextNodeID() 171 + node := &BaseNode{ 172 + id: id, 173 + typ: typ, 174 + ports: make([]*Port, numPorts), 175 + } 176 + for i := 0; i < numPorts; i++ { 177 + node.ports[i] = &Port{Node: node, Index: i} 178 + } 179 + n.nodesMu.Lock() 180 + if n.nodes == nil { 181 + n.nodes = make(map[uint64]Node) 182 + } 183 + n.nodes[node.id] = node 184 + n.nodesMu.Unlock() 185 + return node 186 + } 187 + 188 + func (n *Network) NewFan() Node { 189 + return n.addNodeInternal(NodeTypeFan, 3) // 0: Principal, 1: Aux1, 2: Aux2 190 + } 191 + 192 + func (n *Network) NewEraser() Node { 193 + return n.addNodeInternal(NodeTypeEraser, 1) // 0: Principal 194 + } 195 + 196 + func (n *Network) NewReplicator(level int, deltas []int) Node { 197 + id := n.nextNodeID() 198 + numPorts := 1 + len(deltas) // 0: Principal, 1..n: Aux 199 + node := &ReplicatorNode{ 200 + BaseNode: BaseNode{ 201 + id: id, 202 + typ: NodeTypeReplicator, 203 + ports: make([]*Port, numPorts), 204 + }, 205 + level: level, 206 + deltas: deltas, 207 + } 208 + for i := 0; i < numPorts; i++ { 209 + node.ports[i] = &Port{Node: node, Index: i} 210 + } 211 + n.nodesMu.Lock() 212 + if n.nodes == nil { 213 + n.nodes = make(map[uint64]Node) 214 + } 215 + n.nodes[node.id] = node 216 + n.nodesMu.Unlock() 217 + return node 218 + } 219 + 220 + func (n *Network) NewVar() Node { 221 + node := n.addNodeInternal(NodeTypeVar, 1) // 0: Connection 222 + return node 223 + } 224 + 225 + // Canonicalize prunes all nodes not reachable from the given root (node, port). 226 + // For every unreachable node, all its connected wires are replaced by erasers. 227 + func (n *Network) Canonicalize(root Node, rootPort int) { 228 + if n.nodes == nil { 229 + return 230 + } 231 + 232 + visited := make(map[uint64]bool) 233 + var stack []struct { 234 + node Node 235 + port int 236 + } 237 + stack = append(stack, struct { 238 + node Node 239 + port int 240 + }{root, rootPort}) 241 + 242 + for len(stack) > 0 { 243 + el := stack[len(stack)-1] 244 + stack = stack[:len(stack)-1] 245 + if el.node == nil { 246 + continue 247 + } 248 + id := el.node.ID() 249 + if visited[id] { 250 + continue 251 + } 252 + visited[id] = true 253 + 254 + // Visit all neighbor ports connected to this node 255 + for _, p := range el.node.Ports() { 256 + w := p.Wire.Load() 257 + if w == nil { 258 + continue 259 + } 260 + other := w.Other(p) 261 + if other == nil { 262 + continue 263 + } 264 + stack = append(stack, struct { 265 + node Node 266 + port int 267 + }{other.Node, other.Index}) 268 + } 269 + } 270 + 271 + // Snapshot nodes to avoid holding lock while mutating the network 272 + n.nodesMu.Lock() 273 + nodesSnapshot := make([]Node, 0, len(n.nodes)) 274 + for _, node := range n.nodes { 275 + nodesSnapshot = append(nodesSnapshot, node) 276 + } 277 + n.nodesMu.Unlock() 278 + 279 + // For every node not visited, replace its connections with erasers 280 + for _, node := range nodesSnapshot { 281 + id := node.ID() 282 + if visited[id] { 283 + continue 284 + } 285 + // (debug) previously printed pruned node info here; removed for cleanliness 286 + // For each of the node's ports, if connected, splice an eraser in place 287 + for _, p := range node.Ports() { 288 + w := p.Wire.Load() 289 + if w == nil { 290 + continue 291 + } 292 + // Replace the port in the wire with an eraser principal 293 + newEra := n.NewEraser() 294 + n.splice(newEra.Ports()[0], p) 295 + } 296 + n.removeNode(node) 297 + } 298 + } 299 + 300 + // Link connects two ports. 301 + func (n *Network) Link(node1 Node, port1 int, node2 Node, port2 int) { 302 + n.LinkAt(node1, port1, node2, port2, 0) 303 + } 304 + 305 + // LinkAt connects two ports with a specified depth. 306 + func (n *Network) LinkAt(node1 Node, port1 int, node2 Node, port2 int, depth uint64) { 307 + p1 := node1.Ports()[port1] 308 + p2 := node2.Ports()[port2] 309 + 310 + wire := &Wire{depth: depth} 311 + wire.P0.Store(p1) 312 + wire.P1.Store(p2) 313 + 314 + p1.Wire.Store(wire) 315 + p2.Wire.Store(wire) 316 + 317 + // Check if this forms an active pair 318 + if port1 == 0 && port2 == 0 && isActive(node1) && isActive(node2) { 319 + n.wg.Add(1) 320 + n.scheduler.Push(wire, int(depth)) 321 + } 322 + } 323 + 324 + func isActive(node Node) bool { 325 + return node.Type() != NodeTypeVar 326 + } 327 + 328 + // IsConnected checks if two ports are connected. 329 + func (n *Network) IsConnected(node1 Node, port1 int, node2 Node, port2 int) bool { 330 + p1 := node1.Ports()[port1] 331 + w := p1.Wire.Load() 332 + if w == nil { 333 + return false 334 + } 335 + 336 + other := w.Other(p1) 337 + return other != nil && other.Node == node2 && other.Index == port2 338 + } 339 + 340 + // GetLink returns the node connected to the given port. 341 + func (n *Network) GetLink(node Node, port int) (Node, int) { 342 + p := node.Ports()[port] 343 + w := p.Wire.Load() 344 + if w == nil { 345 + return nil, -1 346 + } 347 + other := w.Other(p) 348 + if other == nil { 349 + return nil, -1 350 + } 351 + return other.Node, other.Index 352 + } 353 + 354 + func (w *Wire) Other(p *Port) *Port { 355 + p0 := w.P0.Load() 356 + if p0 == p { 357 + return w.P1.Load() 358 + } 359 + return p0 360 + } 361 + 362 + // ReduceAll reduces the network until no more active pairs exist. 363 + func (n *Network) ReduceAll() { 364 + n.Start() 365 + // Wait for all active pairs to be processed 366 + n.wg.Wait() 367 + } 368 + 369 + func (n *Network) worker() { 370 + for { 371 + wire := n.scheduler.Pop() 372 + n.reducePair(wire) 373 + n.wg.Done() 374 + } 375 + } 376 + 377 + func (n *Network) reducePair(w *Wire) { 378 + p0 := w.P0.Load() 379 + p1 := w.P1.Load() 380 + 381 + if p0 == nil || p1 == nil { 382 + return // Already handled? 383 + } 384 + 385 + a := p0.Node 386 + b := p1.Node 387 + depth := w.depth 388 + 389 + // Dispatch based on types 390 + atomic.AddUint64(&n.ops, 1) 391 + rule := RuleUnknown 392 + switch { 393 + case a.Type() == b.Type(): 394 + // Annihilation 395 + if a.Type() == NodeTypeReplicator { 396 + // Check levels 397 + if a.Level() == b.Level() { 398 + atomic.AddUint64(&n.statRepAnn, 1) 399 + rule = RuleRepRep 400 + n.annihilate(a, b) 401 + } else { 402 + atomic.AddUint64(&n.statRepComm, 1) 403 + rule = RuleRepRepComm 404 + n.commuteReplicators(a, b, depth) 405 + } 406 + } else { 407 + atomic.AddUint64(&n.statFanAnn, 1) 408 + rule = RuleFanFan 409 + n.annihilate(a, b) 410 + } 411 + case a.Type() == NodeTypeEraser || b.Type() == NodeTypeEraser: 412 + atomic.AddUint64(&n.statErasure, 1) 413 + if a.Type() == NodeTypeEraser { 414 + rule = RuleErasure 415 + n.erase(a, b) 416 + } else { 417 + rule = RuleErasure 418 + n.erase(b, a) 419 + } 420 + case (a.Type() == NodeTypeFan && b.Type() == NodeTypeReplicator) || (a.Type() == NodeTypeReplicator && b.Type() == NodeTypeFan): 421 + if n.phase == 2 { 422 + atomic.AddUint64(&n.statAuxFanRep, 1) 423 + rule = RuleAuxFanRep 424 + if a.Type() == NodeTypeFan { 425 + n.auxFanReplication(a, b, depth) 426 + } else { 427 + n.auxFanReplication(b, a, depth) 428 + } 429 + } else { 430 + atomic.AddUint64(&n.statFanRepComm, 1) 431 + if a.Type() == NodeTypeFan { 432 + rule = RuleFanRep 433 + n.commuteFanReplicator(a, b, depth) 434 + } else { 435 + rule = RuleFanRep 436 + n.commuteFanReplicator(b, a, depth) 437 + } 438 + } 439 + default: 440 + fmt.Printf("Unknown interaction: %v <-> %v\n", a.Type(), b.Type()) 441 + } 442 + n.recordTrace(rule, a, b) 443 + } 444 + 445 + // Helper to connect two ports with a NEW wire 446 + func (n *Network) connect(p1, p2 *Port, depth uint64) { 447 + wire := &Wire{depth: depth} 448 + wire.P0.Store(p1) 449 + wire.P1.Store(p2) 450 + p1.Wire.Store(wire) 451 + p2.Wire.Store(wire) 452 + 453 + // Check for new active pair 454 + if p1.Index == 0 && p2.Index == 0 && isActive(p1.Node) && isActive(p2.Node) { 455 + n.wg.Add(1) 456 + n.scheduler.Push(wire, int(depth)) 457 + } 458 + } 459 + 460 + // Helper to splice a new port into an existing wire. 461 + // pNew replaces pOld in the wire. 462 + func (n *Network) splice(pNew, pOld *Port) { 463 + w := pOld.Wire.Load() 464 + if w == nil { 465 + return 466 + } 467 + 468 + // Point pNew to w 469 + pNew.Wire.Store(w) 470 + 471 + // Update w to point to pNew instead of pOld 472 + if w.P0.Load() == pOld { 473 + w.P0.Store(pNew) 474 + } else { 475 + w.P1.Store(pNew) 476 + } 477 + 478 + // Clear the old port's Wire pointer so it no longer appears connected. 479 + // Leaving pOld.Wire non-nil can make canonicalization traverse through 480 + // stale references and incorrectly mark nodes as reachable. 481 + pOld.Wire.Store(nil) 482 + 483 + // Check if this forms active pair 484 + neighbor := w.Other(pNew) 485 + if neighbor != nil && pNew.Index == 0 && neighbor.Index == 0 && isActive(pNew.Node) && isActive(neighbor.Node) { 486 + n.wg.Add(1) 487 + n.scheduler.Push(w, int(w.depth)) 488 + } 489 + } 490 + 491 + // Helper to fuse two existing wires (Annihilation) 492 + func (n *Network) fuse(p1, p2 *Port) { 493 + // Retry loop for CAS 494 + for { 495 + w1 := p1.Wire.Load() 496 + w2 := p2.Wire.Load() 497 + 498 + if w1 == nil || w2 == nil { 499 + // Should not happen if nodes are connected 500 + return 501 + } 502 + 503 + neighborP1 := w1.Other(p1) 504 + neighborP2 := w2.Other(p2) 505 + 506 + if neighborP1 == nil || neighborP2 == nil { 507 + // Disconnected port? 508 + return 509 + } 510 + 511 + // We want to connect neighborP1 and neighborP2. 512 + // We can reuse w1. 513 + // We need to update neighborP2 to point to w1. 514 + 515 + // Try to claim neighborP2 516 + // fmt.Printf("CAS %p %p %p\n", neighborP2, w2, w1) 517 + if neighborP2.Wire.CompareAndSwap(w2, w1) { 518 + // Success! Now update w1 to point to neighborP2 instead of p1 519 + // We need to replace p1 with neighborP2 in w1 520 + if w1.P0.Load() == p1 { 521 + w1.P0.Store(neighborP2) 522 + } else { 523 + w1.P1.Store(neighborP2) 524 + } 525 + 526 + // Check if this formed a new active pair 527 + if neighborP1.Index == 0 && neighborP2.Index == 0 && isActive(neighborP1.Node) && isActive(neighborP2.Node) { 528 + n.wg.Add(1) 529 + n.scheduler.Push(w1, int(w1.depth)) 530 + } 531 + return 532 + } 533 + // CAS failed, neighborP2 moved. Retry. 534 + runtime.Gosched() 535 + } 536 + } 537 + 538 + func (n *Network) removeNode(node Node) { 539 + // No-op in lock-free version (GC handles memory) 540 + } 541 + 542 + func (n *Network) annihilate(a, b Node) { 543 + // Link corresponding aux ports 544 + count := len(a.Ports()) 545 + if len(b.Ports()) < count { 546 + count = len(b.Ports()) 547 + } 548 + 549 + for i := 1; i < count; i++ { 550 + n.fuse(a.Ports()[i], b.Ports()[i]) 551 + } 552 + } 553 + 554 + func (n *Network) erase(eraser, victim Node) { 555 + for i := 1; i < len(victim.Ports()); i++ { 556 + // Create new Eraser 557 + newEra := n.NewEraser() 558 + // Connect new Eraser (Principal 0) to Victim's neighbor (via Aux i) 559 + n.splice(newEra.Ports()[0], victim.Ports()[i]) 560 + } 561 + 562 + n.removeNode(eraser) 563 + n.removeNode(victim) 564 + } 565 + 566 + func (n *Network) commuteFanReplicator(fan, rep Node, depth uint64) { 567 + // Create copies 568 + r1 := n.createReplicatorCopy(rep) 569 + r2 := n.createReplicatorCopy(rep) 570 + 571 + // Connect R1, R2 principal to Fan's neighbors 572 + if fan.Ports()[1].Wire.Load() != nil { 573 + n.splice(r1.Ports()[0], fan.Ports()[1]) 574 + } 575 + if fan.Ports()[2].Wire.Load() != nil { 576 + n.splice(r2.Ports()[0], fan.Ports()[2]) 577 + } 578 + 579 + // Create Fan copies 580 + numRepAux := len(rep.Ports()) - 1 581 + for i := 0; i < numRepAux; i++ { 582 + f := n.createFanCopy() 583 + 584 + // Connect Fan principal to Rep's neighbor 585 + if rep.Ports()[i+1].Wire.Load() != nil { 586 + n.splice(f.Ports()[0], rep.Ports()[i+1]) 587 + } 588 + 589 + // Connect Fan aux to Rep copies aux 590 + n.connect(f.Ports()[1], r1.Ports()[i+1], depth) 591 + n.connect(f.Ports()[2], r2.Ports()[i+1], depth) 592 + } 593 + 594 + n.removeNode(fan) 595 + n.removeNode(rep) 596 + } 597 + 598 + func (n *Network) auxFanReplication(fan, rep Node, depth uint64) { 599 + // In Phase 2, fans are rotated, so the interaction is structurally standard 600 + // but semantically "Aux Fan Replication". 601 + n.commuteFanReplicator(fan, rep, depth) 602 + } 603 + 604 + func (n *Network) commuteReplicators(a, b Node, depth uint64) { 605 + if a.Level() > b.Level() { 606 + n.commuteReplicators(b, a, depth) 607 + return 608 + } 609 + 610 + // A replicates B 611 + // Create N copies of B (B1...BN) 612 + numAAux := len(a.Ports()) - 1 613 + bCopies := make([]Node, numAAux) 614 + for i := 0; i < numAAux; i++ { 615 + delta := a.Deltas()[i] 616 + bCopy := n.createReplicatorCopyWithLevel(b, b.Level()+delta) 617 + bCopies[i] = bCopy 618 + 619 + // Connect B_i principal to A's neighbor 620 + if a.Ports()[i+1].Wire.Load() != nil { 621 + n.splice(bCopy.Ports()[0], a.Ports()[i+1]) 622 + } 623 + } 624 + 625 + // Create M copies of A (A1...AM) 626 + numBAux := len(b.Ports()) - 1 627 + aCopies := make([]Node, numBAux) 628 + for i := 0; i < numBAux; i++ { 629 + aCopy := n.createReplicatorCopy(a) 630 + aCopies[i] = aCopy 631 + 632 + // Connect A_i principal to B's neighbor 633 + if b.Ports()[i+1].Wire.Load() != nil { 634 + n.splice(aCopy.Ports()[0], b.Ports()[i+1]) 635 + } 636 + 637 + // Connect A_i aux to B copies aux 638 + for k := 0; k < len(bCopies); k++ { 639 + n.connect(aCopy.Ports()[k+1], bCopies[k].Ports()[i+1], depth) 640 + } 641 + } 642 + 643 + n.removeNode(a) 644 + n.removeNode(b) 645 + } 646 + 647 + func (n *Network) createFanCopy() Node { 648 + return n.NewFan() 649 + } 650 + 651 + func (n *Network) createReplicatorCopy(original Node) Node { 652 + return n.NewReplicator(original.Level(), original.Deltas()) 653 + } 654 + 655 + func (n *Network) createReplicatorCopyWithLevel(original Node, newLevel int) Node { 656 + return n.NewReplicator(newLevel, original.Deltas()) 657 + } 658 + 659 + func (n *Network) SetPhase(p int) { 660 + if p == 2 && n.phase == 1 { 661 + n.phase = 2 662 + n.rotateAllFans() 663 + } else { 664 + n.phase = p 665 + } 666 + } 667 + 668 + func (n *Network) rotateAllFans() { 669 + n.nodesMu.Lock() 670 + nodesSnapshot := make([]Node, 0, len(n.nodes)) 671 + for _, node := range n.nodes { 672 + nodesSnapshot = append(nodesSnapshot, node) 673 + } 674 + n.nodesMu.Unlock() 675 + 676 + for _, node := range nodesSnapshot { 677 + if node.Type() == NodeTypeFan { 678 + n.rotateFan(node.(*BaseNode)) // Assuming Fan is BaseNode, need to check 679 + } 680 + } 681 + } 682 + 683 + func (n *Network) rotateFan(fan *BaseNode) { 684 + // Rotate ports: P->A2, A1->P, A2->A1 685 + // 0 <- 1 686 + // 1 <- 2 687 + // 2 <- 0 688 + 689 + p0 := fan.ports[0] 690 + p1 := fan.ports[1] 691 + p2 := fan.ports[2] 692 + 693 + fan.ports[0] = p1 694 + fan.ports[1] = p2 695 + fan.ports[2] = p0 696 + 697 + fan.ports[0].Index = 0 698 + fan.ports[1].Index = 1 699 + fan.ports[2].Index = 2 700 + 701 + // Check for active pair on new Principal (p1) 702 + if isActive(fan) { 703 + w := fan.ports[0].Wire.Load() 704 + if w != nil { 705 + other := w.Other(fan.ports[0]) 706 + if other != nil && other.Index == 0 && isActive(other.Node) { 707 + n.wg.Add(1) 708 + n.scheduler.Push(w, int(w.depth)) 709 + } 710 + } 711 + } 712 + } 713 + 714 + // ApplyCanonicalRules applies decay and merge rules to all nodes. 715 + func (n *Network) ApplyCanonicalRules() { 716 + n.nodesMu.Lock() 717 + nodes := make([]Node, 0, len(n.nodes)) 718 + for _, node := range n.nodes { 719 + nodes = append(nodes, node) 720 + } 721 + n.nodesMu.Unlock() 722 + 723 + for _, node := range nodes { 724 + // Check if node is still valid (might have been removed by previous rule) 725 + if len(node.Ports()) > 0 { 726 + p0 := node.Ports()[0] 727 + if p0.Wire.Load() == nil { 728 + // Disconnected/Removed 729 + continue 730 + } 731 + } 732 + 733 + if node.Type() == NodeTypeReplicator { 734 + // Check for Decay 735 + if len(node.Ports()) == 2 && node.Deltas()[0] == 0 { 736 + n.reduceRepDecay(node) 737 + continue 738 + } 739 + // Check for Merge 740 + n.reduceRepMerge(node) 741 + } 742 + } 743 + } 744 + 745 + func (n *Network) reduceRepMerge(rep Node) { 746 + // Check if any aux port is connected to another Replicator's Principal 747 + for i := 1; i < len(rep.Ports()); i++ { 748 + p := rep.Ports()[i] 749 + w := p.Wire.Load() 750 + if w == nil { 751 + continue 752 + } 753 + other := w.Other(p) 754 + if other == nil { 755 + continue 756 + } 757 + 758 + // Check if other is Replicator Principal (Index 0) 759 + if other.Node.Type() == NodeTypeReplicator && other.Index == 0 { 760 + otherRep := other.Node 761 + 762 + // Check compatibility 763 + // Level(Other) == Level(Rep) + Delta(Rep)[i-1] 764 + delta := rep.Deltas()[i-1] 765 + if otherRep.Level() == rep.Level()+delta { 766 + n.mergeReplicators(rep, otherRep, i-1) 767 + return // Only one merge per pass to avoid complexity 768 + } 769 + } 770 + } 771 + } 772 + 773 + func (n *Network) mergeReplicators(repA, repB Node, auxIndexA int) { 774 + // repA Aux[auxIndexA] <-> repB Principal 775 + 776 + // New Deltas 777 + newDeltas := make([]int, 0) 778 + deltaA := repA.Deltas()[auxIndexA] 779 + 780 + for k, d := range repA.Deltas() { 781 + if k == auxIndexA { 782 + // Expand with repB deltas 783 + for _, dB := range repB.Deltas() { 784 + newDeltas = append(newDeltas, deltaA+dB) 785 + } 786 + } else { 787 + newDeltas = append(newDeltas, d) 788 + } 789 + } 790 + 791 + // Create New Replicator 792 + newRep := n.NewReplicator(repA.Level(), newDeltas) 793 + 794 + // Connect Principal 795 + // repA Principal neighbor <-> newRep Principal 796 + pA0 := repA.Ports()[0] 797 + if w := pA0.Wire.Load(); w != nil { 798 + // neighbor := w.Other(pA0) // Not needed for splice 799 + n.splice(newRep.Ports()[0], pA0) 800 + } 801 + 802 + // Connect Aux ports 803 + newPortIdx := 1 804 + for k := 0; k < len(repA.Deltas()); k++ { 805 + if k == auxIndexA { 806 + // Connect to repB's aux neighbors 807 + for m := 0; m < len(repB.Deltas()); m++ { 808 + pB := repB.Ports()[m+1] 809 + if w := pB.Wire.Load(); w != nil { 810 + n.splice(newRep.Ports()[newPortIdx], pB) 811 + } 812 + newPortIdx++ 813 + } 814 + } else { 815 + // Connect to repA's aux neighbor 816 + pA := repA.Ports()[k+1] 817 + if w := pA.Wire.Load(); w != nil { 818 + n.splice(newRep.Ports()[newPortIdx], pA) 819 + } 820 + newPortIdx++ 821 + } 822 + } 823 + 824 + n.removeNode(repA) 825 + n.removeNode(repB) 826 + atomic.AddUint64(&n.statRepMerge, 1) 827 + n.recordTrace(RuleRepMerge, repA, repB) 828 + } 829 + 830 + func (n *Network) reduceRepDecay(rep Node) { 831 + // Rep(0) <-> A(i) 832 + // Rep(1) <-> B(j) 833 + // Link A(i) <-> B(j) 834 + 835 + p0 := rep.Ports()[0] 836 + p1 := rep.Ports()[1] 837 + 838 + w0 := p0.Wire.Load() 839 + w1 := p1.Wire.Load() 840 + 841 + if w0 == nil || w1 == nil { 842 + return 843 + } 844 + 845 + neighbor0 := w0.Other(p0) 846 + neighbor1 := w1.Other(p1) 847 + 848 + if neighbor0 == nil || neighbor1 == nil { 849 + return 850 + } 851 + 852 + // Create new wire between neighbor0 and neighbor1 853 + // We can reuse w0 854 + 855 + // Update neighbor1 to point to w0 856 + if neighbor1.Wire.CompareAndSwap(w1, w0) { 857 + // Update w0 to point to neighbor1 instead of p0 858 + if w0.P0.Load() == p0 { 859 + w0.P0.Store(neighbor1) 860 + } else { 861 + w0.P1.Store(neighbor1) 862 + } 863 + 864 + // Check active pair 865 + if neighbor0.Index == 0 && neighbor1.Index == 0 && isActive(neighbor0.Node) && isActive(neighbor1.Node) { 866 + n.wg.Add(1) 867 + n.scheduler.Push(w0, int(w0.depth)) 868 + } 869 + 870 + n.removeNode(rep) 871 + atomic.AddUint64(&n.statRepDecay, 1) 872 + n.recordTrace(RuleRepDecay, rep, nil) 873 + } 874 + }
+522
pkg/deltanet/deltanet_test.go
··· 1 + package deltanet 2 + 3 + import ( 4 + "testing" 5 + ) 6 + 7 + // TestFanAnnihilation tests Beta-reduction (Fan-Fan interaction). 8 + func TestFanAnnihilation(t *testing.T) { 9 + net := NewNetwork() 10 + 11 + // Create two fans facing each other 12 + f1 := net.NewFan() 13 + f2 := net.NewFan() 14 + 15 + // Link Principal ports 16 + net.Link(f1, 0, f2, 0) 17 + 18 + // Create wires for aux ports 19 + in1 := net.NewVar() 20 + in2 := net.NewVar() 21 + out1 := net.NewVar() 22 + out2 := net.NewVar() 23 + 24 + net.Link(f1, 1, in1, 0) 25 + net.Link(f1, 2, in2, 0) 26 + net.Link(f2, 1, out1, 0) 27 + net.Link(f2, 2, out2, 0) 28 + 29 + // Reduce 30 + net.ReduceAll() 31 + 32 + // Verify connections: in1 <-> out1, in2 <-> out2 33 + if !net.IsConnected(in1, 0, out1, 0) { 34 + t.Errorf("Fan annihilation failed: Port 1s not connected") 35 + } 36 + if !net.IsConnected(in2, 0, out2, 0) { 37 + t.Errorf("Fan annihilation failed: Port 2s not connected") 38 + } 39 + } 40 + 41 + // TestEraserFanInteraction tests Eraser eating a Fan. 42 + func TestEraserFanInteraction(t *testing.T) { 43 + net := NewNetwork() 44 + 45 + era := net.NewEraser() 46 + fan := net.NewFan() 47 + 48 + net.Link(era, 0, fan, 0) 49 + 50 + w1 := net.NewVar() 51 + w2 := net.NewVar() 52 + net.Link(fan, 1, w1, 0) 53 + net.Link(fan, 2, w2, 0) 54 + 55 + net.ReduceAll() 56 + 57 + // w1 and w2 should now be connected to NEW Erasers 58 + target1, _ := net.GetLink(w1, 0) 59 + if target1 == nil || target1.Type() != NodeTypeEraser { 60 + t.Errorf("Port 1 not connected to Eraser, got %v", target1) 61 + } 62 + 63 + target2, _ := net.GetLink(w2, 0) 64 + if target2 == nil || target2.Type() != NodeTypeEraser { 65 + t.Errorf("Port 2 not connected to Eraser, got %v", target2) 66 + } 67 + } 68 + 69 + // TestFanReplicatorCommutation tests Fan passing through Replicator. 70 + func TestFanReplicatorCommutation(t *testing.T) { 71 + net := NewNetwork() 72 + 73 + fan := net.NewFan() 74 + // Replicator Level 1, 2 aux ports, deltas [0, 0] 75 + rep := net.NewReplicator(1, []int{0, 0}) 76 + 77 + net.Link(fan, 0, rep, 0) 78 + 79 + // Fan Aux 80 + fAux1 := net.NewVar() 81 + fAux2 := net.NewVar() 82 + net.Link(fan, 1, fAux1, 0) 83 + net.Link(fan, 2, fAux2, 0) 84 + 85 + // Rep Aux 86 + rAux1 := net.NewVar() 87 + rAux2 := net.NewVar() 88 + net.Link(rep, 1, rAux1, 0) 89 + net.Link(rep, 2, rAux2, 0) 90 + 91 + net.ReduceAll() 92 + 93 + // Expected Topology: 94 + // fAux1 connected to Rep(Copy1) 95 + // fAux2 connected to Rep(Copy2) 96 + // rAux1 connected to Fan(CopyA) 97 + // rAux2 connected to Fan(CopyB) 98 + // And the internal connections between Rep copies and Fan copies. 99 + 100 + l1, _ := net.GetLink(fAux1, 0) 101 + if l1 == nil || l1.Type() != NodeTypeReplicator { 102 + t.Errorf("Fan Aux 1 should connect to Replicator, got %v", l1) 103 + } 104 + 105 + l2, _ := net.GetLink(fAux2, 0) 106 + if l2 == nil || l2.Type() != NodeTypeReplicator { 107 + t.Errorf("Fan Aux 2 should connect to Replicator, got %v", l2) 108 + } 109 + 110 + l3, _ := net.GetLink(rAux1, 0) 111 + if l3 == nil || l3.Type() != NodeTypeFan { 112 + t.Errorf("Rep Aux 1 should connect to Fan, got %v", l3) 113 + } 114 + } 115 + 116 + // TestReplicatorReplicatorAnnihilation tests two identical replicators annihilating. 117 + func TestReplicatorReplicatorAnnihilation(t *testing.T) { 118 + net := NewNetwork() 119 + 120 + r1 := net.NewReplicator(5, []int{1, 2}) 121 + r2 := net.NewReplicator(5, []int{1, 2}) 122 + 123 + net.Link(r1, 0, r2, 0) 124 + 125 + in1 := net.NewVar() 126 + in2 := net.NewVar() 127 + out1 := net.NewVar() 128 + out2 := net.NewVar() 129 + 130 + net.Link(r1, 1, in1, 0) 131 + net.Link(r1, 2, in2, 0) 132 + net.Link(r2, 1, out1, 0) 133 + net.Link(r2, 2, out2, 0) 134 + 135 + net.ReduceAll() 136 + 137 + if !net.IsConnected(in1, 0, out1, 0) { 138 + t.Errorf("Rep annihilation failed: Port 1s not connected") 139 + } 140 + if !net.IsConnected(in2, 0, out2, 0) { 141 + t.Errorf("Rep annihilation failed: Port 2s not connected") 142 + } 143 + } 144 + 145 + // TestReplicatorReplicatorCommutation tests two replicators with different levels commuting. 146 + func TestReplicatorReplicatorCommutation(t *testing.T) { 147 + net := NewNetwork() 148 + 149 + // R1 (Level 1) <-> R2 (Level 2) 150 + r1 := net.NewReplicator(1, []int{0}) // 1 aux port 151 + r2 := net.NewReplicator(2, []int{0}) // 1 aux port 152 + 153 + net.Link(r1, 0, r2, 0) 154 + 155 + in := net.NewVar() 156 + out := net.NewVar() 157 + 158 + net.Link(r1, 1, in, 0) 159 + net.Link(r2, 1, out, 0) 160 + 161 + net.ReduceAll() 162 + 163 + // Result: 164 + // R1 replicates R2 -> R2 copies connected to R1 neighbors. 165 + // R2 replicates R1 -> R1 copies connected to R2 neighbors. 166 + // Since both have 1 aux port: 167 + // in -> R2_copy -> R1_copy -> out 168 + // Wait, let's trace. 169 + // R1 (Level 1) <-> R2 (Level 2). 170 + // R1 replicates R2. 171 + // R1 has 1 aux port (connected to 'in'). 172 + // So we create 1 copy of R2 (R2'). 173 + // R2' principal connects to 'in'. 174 + // R2 replicates R1. 175 + // R2 has 1 aux port (connected to 'out'). 176 + // So we create 1 copy of R1 (R1'). 177 + // R1' principal connects to 'out'. 178 + // Internal connection: R1' aux connects to R2' aux. 179 + 180 + // So: in <-> R2'(0). R2'(1) <-> R1'(1). R1'(0) <-> out. 181 + 182 + l1, _ := net.GetLink(in, 0) 183 + if l1 == nil || l1.Type() != NodeTypeReplicator || l1.Level() != 2 { 184 + t.Errorf("Input should connect to Replicator Level 2, got %v", l1) 185 + } 186 + 187 + l2, _ := net.GetLink(out, 0) 188 + if l2 == nil || l2.Type() != NodeTypeReplicator || l2.Level() != 1 { 189 + t.Errorf("Output should connect to Replicator Level 1, got %v", l2) 190 + } 191 + } 192 + 193 + // TestEraserEraserInteraction tests Eraser annihilating Eraser. 194 + func TestEraserEraserInteraction(t *testing.T) { 195 + net := NewNetwork() 196 + e1 := net.NewEraser() 197 + e2 := net.NewEraser() 198 + net.Link(e1, 0, e2, 0) 199 + net.ReduceAll() 200 + // Success if no hang/crash. 201 + } 202 + 203 + // TestEraserReplicatorInteraction tests Eraser erasing a Replicator. 204 + func TestEraserReplicatorInteraction(t *testing.T) { 205 + net := NewNetwork() 206 + e := net.NewEraser() 207 + r := net.NewReplicator(0, []int{0, 0}) // 2 aux ports 208 + net.Link(e, 0, r, 0) 209 + 210 + v1 := net.NewVar() 211 + v2 := net.NewVar() 212 + net.Link(r, 1, v1, 0) 213 + net.Link(r, 2, v2, 0) 214 + 215 + net.ReduceAll() 216 + 217 + verifyEraserConnection(t, net, v1) 218 + verifyEraserConnection(t, net, v2) 219 + } 220 + 221 + // TestReplicatorEraserInteraction tests Replicator being erased by Eraser (Symmetric). 222 + func TestReplicatorEraserInteraction(t *testing.T) { 223 + net := NewNetwork() 224 + r := net.NewReplicator(0, []int{0}) 225 + e := net.NewEraser() 226 + net.Link(r, 0, e, 0) 227 + 228 + v1 := net.NewVar() 229 + net.Link(r, 1, v1, 0) 230 + 231 + net.ReduceAll() 232 + 233 + verifyEraserConnection(t, net, v1) 234 + } 235 + 236 + // TestFanEraserInteraction tests Fan being erased by Eraser (Symmetric). 237 + func TestFanEraserInteraction(t *testing.T) { 238 + net := NewNetwork() 239 + f := net.NewFan() 240 + e := net.NewEraser() 241 + net.Link(f, 0, e, 0) 242 + 243 + v1 := net.NewVar() 244 + v2 := net.NewVar() 245 + net.Link(f, 1, v1, 0) 246 + net.Link(f, 2, v2, 0) 247 + 248 + net.ReduceAll() 249 + 250 + verifyEraserConnection(t, net, v1) 251 + verifyEraserConnection(t, net, v2) 252 + } 253 + 254 + // TestReplicatorFanInteraction tests Replicator commuting with Fan (Symmetric). 255 + func TestReplicatorFanInteraction(t *testing.T) { 256 + net := NewNetwork() 257 + rep := net.NewReplicator(1, []int{0, 0}) 258 + fan := net.NewFan() 259 + 260 + net.Link(rep, 0, fan, 0) 261 + 262 + rAux1 := net.NewVar() 263 + rAux2 := net.NewVar() 264 + net.Link(rep, 1, rAux1, 0) 265 + net.Link(rep, 2, rAux2, 0) 266 + 267 + fAux1 := net.NewVar() 268 + fAux2 := net.NewVar() 269 + net.Link(fan, 1, fAux1, 0) 270 + net.Link(fan, 2, fAux2, 0) 271 + 272 + net.ReduceAll() 273 + 274 + // Check topology: 275 + // rAux1 -> Fan 276 + // rAux2 -> Fan 277 + // fAux1 -> Rep 278 + // fAux2 -> Rep 279 + 280 + l1, _ := net.GetLink(rAux1, 0) 281 + if l1 == nil || l1.Type() != NodeTypeFan { 282 + t.Errorf("Rep Aux 1 should connect to Fan, got %v", l1) 283 + } 284 + 285 + l2, _ := net.GetLink(fAux1, 0) 286 + if l2 == nil || l2.Type() != NodeTypeReplicator { 287 + t.Errorf("Fan Aux 1 should connect to Replicator, got %v", l2) 288 + } 289 + } 290 + 291 + // TestComplexNormalization tests that new active pairs are handled correctly. 292 + func TestComplexNormalization(t *testing.T) { 293 + net := NewNetwork() 294 + // Setup: F1(0) <-> R(0). R(1) <-> F2(0). 295 + // F1 >< R reduces first. 296 + // This creates a copy of F1 (F1') connected to R's neighbor at port 1 (F2). 297 + // So F1'(0) <-> F2(0) becomes active. 298 + // Then F1' >< F2 reduces (Fan-Fan annihilation). 299 + 300 + f1 := net.NewFan() 301 + r := net.NewReplicator(0, []int{0, 0}) 302 + f2 := net.NewFan() 303 + 304 + net.Link(f1, 0, r, 0) 305 + net.Link(r, 1, f2, 0) 306 + 307 + // Aux ports 308 + v1 := net.NewVar() 309 + v2 := net.NewVar() 310 + net.Link(f1, 1, v1, 0) 311 + net.Link(f1, 2, v2, 0) 312 + 313 + v3 := net.NewVar() 314 + net.Link(r, 2, v3, 0) 315 + 316 + v4 := net.NewVar() 317 + v5 := net.NewVar() 318 + net.Link(f2, 1, v4, 0) 319 + net.Link(f2, 2, v5, 0) 320 + 321 + net.ReduceAll() 322 + 323 + // If successful, we should see connections between the vars. 324 + // F1 >< R: 325 + // R copies F1. R_copy1 connects to v1, R_copy2 connects to v2. 326 + // F1 copies R. F1_copy1 connects to F2(0). F1_copy2 connects to v3. 327 + // 328 + // F1_copy1 >< F2: 329 + // F1_copy1 is a Fan. F2 is a Fan. 330 + // Annihilation. 331 + // F1_copy1 aux ports connect to F2 aux ports. 332 + // F1_copy1 aux ports come from R copies? 333 + // Wait. 334 + // F1 >< R: 335 + // F1 has aux v1, v2. 336 + // R has aux F2, v3. 337 + // 338 + // R copies F1 (R_v1, R_v2). 339 + // R_v1 principal -> v1. Aux -> F1 copies aux 1. 340 + // R_v2 principal -> v2. Aux -> F1 copies aux 2. 341 + // 342 + // F1 copies R (F1_F2, F1_v3). 343 + // F1_F2 principal -> F2. Aux 1 -> R_v1 aux 1. Aux 2 -> R_v2 aux 1. 344 + // F1_v3 principal -> v3. Aux 1 -> R_v1 aux 2. Aux 2 -> R_v2 aux 2. 345 + // 346 + // Now F1_F2 >< F2 (Fan >< Fan). 347 + // F1_F2 aux 1 (connected to R_v1 aux 1) connects to F2 aux 1 (v4). 348 + // F1_F2 aux 2 (connected to R_v2 aux 1) connects to F2 aux 2 (v5). 349 + // 350 + // So: 351 + // R_v1 aux 1 <-> v4. 352 + // R_v2 aux 1 <-> v5. 353 + // 354 + // R_v1 is a Replicator copy. Principal -> v1. 355 + // R_v2 is a Replicator copy. Principal -> v2. 356 + // 357 + // So we have: 358 + // v1 <-> R_v1(0). R_v1(1) <-> v4. R_v1(2) <-> ... (connected to F1_v3 aux 1) 359 + // v2 <-> R_v2(0). R_v2(1) <-> v5. R_v2(2) <-> ... (connected to F1_v3 aux 2) 360 + // 361 + // F1_v3 is a Fan copy. Principal -> v3. 362 + // F1_v3(1) <-> R_v1(2). 363 + // F1_v3(2) <-> R_v2(2). 364 + // 365 + // Topology check: 366 + // v1 should be connected to a Replicator. 367 + // That Replicator's port 1 should be connected to v4. 368 + // That Replicator's port 2 should be connected to a Fan (F1_v3). 369 + // That Fan's principal should be connected to v3. 370 + 371 + l, _ := net.GetLink(v1, 0) 372 + if l == nil || l.Type() != NodeTypeReplicator { 373 + t.Errorf("v1 should connect to Replicator, got %v", l) 374 + return 375 + } 376 + // Check l's port 1 377 + l_p1, _ := net.GetLink(l, 1) 378 + // l_p1 should be v4 (which is a Var, so we check if it IS v4's node) 379 + // But v4 is a Var node. 380 + // Wait, GetLink returns the Node. 381 + if l_p1 != v4 { 382 + t.Errorf("v1's Replicator port 1 should connect to v4, got %v", l_p1) 383 + } 384 + } 385 + 386 + // TestReplicatorDeltaShift verifies that Replicator commutation correctly shifts levels by delta. 387 + func TestReplicatorDeltaShift(t *testing.T) { 388 + net := NewNetwork() 389 + 390 + // R1: Level 10, Delta [5] 391 + r1 := net.NewReplicator(10, []int{5}) 392 + // R2: Level 20, Delta [0] 393 + r2 := net.NewReplicator(20, []int{0}) 394 + 395 + // Connect R1 >< R2 396 + net.Link(r1, 0, r2, 0) 397 + 398 + // Aux ports 399 + in := net.NewVar() 400 + out := net.NewVar() 401 + net.Link(r1, 1, in, 0) 402 + net.Link(r2, 1, out, 0) 403 + 404 + net.ReduceAll() 405 + 406 + // R1 (Level 10) < R2 (Level 20). 407 + // R1 replicates R2. 408 + // R2 copy level = R2.Level + R1.Delta = 20 + 5 = 25. 409 + // R2 copy connects to 'in' (R1's neighbor). 410 + 411 + // R2 replicates R1. 412 + // R1 copy level = R1.Level = 10. 413 + // R1 copy connects to 'out' (R2's neighbor). 414 + 415 + // Check 'in' connection 416 + l1, _ := net.GetLink(in, 0) 417 + if l1 == nil { 418 + t.Fatal("in not connected") 419 + } 420 + if l1.Type() != NodeTypeReplicator { 421 + t.Errorf("in should connect to Replicator, got %v", l1.Type()) 422 + } 423 + if l1.Level() != 25 { 424 + t.Errorf("Expected R2 copy level to be 25 (20+5), got %d", l1.Level()) 425 + } 426 + 427 + // Check 'out' connection 428 + l2, _ := net.GetLink(out, 0) 429 + if l2 == nil { 430 + t.Fatal("out not connected") 431 + } 432 + if l2.Type() != NodeTypeReplicator { 433 + t.Errorf("out should connect to Replicator, got %v", l2.Type()) 434 + } 435 + if l2.Level() != 10 { 436 + t.Errorf("Expected R1 copy level to be 10, got %d", l2.Level()) 437 + } 438 + } 439 + 440 + // TestReplicatorMultiDelta verifies that Replicator commutation handles multiple deltas correctly. 441 + func TestReplicatorMultiDelta(t *testing.T) { 442 + net := NewNetwork() 443 + 444 + // R1: Level 10, Deltas [5, 10] 445 + r1 := net.NewReplicator(10, []int{5, 10}) 446 + // R2: Level 20, Deltas [0] 447 + r2 := net.NewReplicator(20, []int{0}) 448 + 449 + net.Link(r1, 0, r2, 0) 450 + 451 + // R1 aux 452 + in1 := net.NewVar() 453 + in2 := net.NewVar() 454 + net.Link(r1, 1, in1, 0) 455 + net.Link(r1, 2, in2, 0) 456 + 457 + // R2 aux 458 + out := net.NewVar() 459 + net.Link(r2, 1, out, 0) 460 + 461 + net.ReduceAll() 462 + 463 + // Check in1 -> R2 copy with level 25 464 + l1, _ := net.GetLink(in1, 0) 465 + if l1 == nil || l1.Type() != NodeTypeReplicator { 466 + t.Errorf("in1 should connect to Replicator") 467 + } else if l1.Level() != 25 { 468 + t.Errorf("in1 Replicator level: expected 25, got %d", l1.Level()) 469 + } 470 + 471 + // Check in2 -> R2 copy with level 30 472 + l2, _ := net.GetLink(in2, 0) 473 + if l2 == nil || l2.Type() != NodeTypeReplicator { 474 + t.Errorf("in2 should connect to Replicator") 475 + } else if l2.Level() != 30 { 476 + t.Errorf("in2 Replicator level: expected 30, got %d", l2.Level()) 477 + } 478 + } 479 + 480 + // TestEraserPropagation verifies that an Eraser recursively destroys a structure. 481 + func TestEraserPropagation(t *testing.T) { 482 + net := NewNetwork() 483 + 484 + // E >< F1 485 + // | \ 486 + // F2 F3 487 + 488 + e := net.NewEraser() 489 + f1 := net.NewFan() 490 + f2 := net.NewFan() 491 + f3 := net.NewFan() 492 + 493 + net.Link(e, 0, f1, 0) 494 + net.Link(f1, 1, f2, 0) 495 + net.Link(f1, 2, f3, 0) 496 + 497 + // Vars at the leaves 498 + v1 := net.NewVar() 499 + v2 := net.NewVar() 500 + v3 := net.NewVar() 501 + v4 := net.NewVar() 502 + 503 + net.Link(f2, 1, v1, 0) 504 + net.Link(f2, 2, v2, 0) 505 + net.Link(f3, 1, v3, 0) 506 + net.Link(f3, 2, v4, 0) 507 + 508 + net.ReduceAll() 509 + 510 + // All vars should be connected to Erasers 511 + verifyEraserConnection(t, net, v1) 512 + verifyEraserConnection(t, net, v2) 513 + verifyEraserConnection(t, net, v3) 514 + verifyEraserConnection(t, net, v4) 515 + } 516 + 517 + func verifyEraserConnection(t *testing.T, net *Network, n Node) { 518 + l, _ := net.GetLink(n, 0) 519 + if l == nil || l.Type() != NodeTypeEraser { 520 + t.Errorf("Node should be connected to Eraser, got %v", l) 521 + } 522 + }
+48
pkg/deltanet/lmo_helpers_test.go
··· 1 + package deltanet 2 + 3 + import "testing" 4 + 5 + func newFanWithSinks(net *Network) Node { 6 + fan := net.NewFan() 7 + net.Link(fan, 1, net.NewVar(), 0) 8 + net.Link(fan, 2, net.NewVar(), 0) 9 + return fan 10 + } 11 + 12 + func newReplicatorWithSinks(net *Network, level int, deltas []int) Node { 13 + rep := net.NewReplicator(level, deltas) 14 + for i := 1; i < len(rep.Ports()); i++ { 15 + net.Link(rep, i, net.NewVar(), 0) 16 + } 17 + return rep 18 + } 19 + 20 + func newEraserWithFanSink(net *Network) (Node, Node) { 21 + eras := net.NewEraser() 22 + fan := newFanWithSinks(net) 23 + net.Link(eras, 0, fan, 0) 24 + return eras, fan 25 + } 26 + 27 + func tracedNet(capacity int) *Network { 28 + n := NewNetwork() 29 + n.EnableTrace(capacity) 30 + n.workers = 1 // Force sequential execution for deterministic order tests 31 + return n 32 + } 33 + 34 + func firstTraceEvent(t *testing.T, net *Network) TraceEvent { 35 + t.Helper() 36 + events := net.TraceSnapshot() 37 + if len(events) == 0 { 38 + t.Fatalf("expected at least one trace event") 39 + } 40 + return events[0] 41 + } 42 + 43 + func assertEventMatchesPair(t *testing.T, ev TraceEvent, aID, bID uint64) { 44 + t.Helper() 45 + if !((ev.AID == aID && ev.BID == bID) || (ev.AID == bID && ev.BID == aID)) { 46 + t.Fatalf("event pair mismatch: got (%d,%d) want ids %d and %d", ev.AID, ev.BID, aID, bID) 47 + } 48 + }
+131
pkg/deltanet/lmo_order_test.go
··· 1 + package deltanet 2 + 3 + import "testing" 4 + 5 + func TestLeftmostPrefersRootFanFan(t *testing.T) { 6 + traceNet := tracedNet(8) 7 + 8 + innerLeft := newFanWithSinks(traceNet) 9 + innerRight := newFanWithSinks(traceNet) 10 + traceNet.LinkAt(innerLeft, 0, innerRight, 0, 1) 11 + 12 + rootLeft := newFanWithSinks(traceNet) 13 + rootRight := newFanWithSinks(traceNet) 14 + traceNet.Link(rootLeft, 0, rootRight, 0) 15 + 16 + traceNet.Start() 17 + traceNet.ReduceAll() 18 + event := firstTraceEvent(t, traceNet) 19 + if event.Rule != RuleFanFan { 20 + t.Fatalf("expected fan-fan rule, got %v", event.Rule) 21 + } 22 + assertEventMatchesPair(t, event, rootLeft.ID(), rootRight.ID()) 23 + } 24 + 25 + func TestLeftmostPrefersRootFanRep(t *testing.T) { 26 + traceNet := tracedNet(8) 27 + 28 + innerRep := newReplicatorWithSinks(traceNet, 0, []int{0}) 29 + innerFan := newFanWithSinks(traceNet) 30 + traceNet.LinkAt(innerRep, 0, innerFan, 0, 1) 31 + 32 + rootFan := newFanWithSinks(traceNet) 33 + rootRep := newReplicatorWithSinks(traceNet, 1, []int{0, 0}) 34 + traceNet.Link(rootFan, 0, rootRep, 0) 35 + 36 + traceNet.Start() 37 + traceNet.ReduceAll() 38 + event := firstTraceEvent(t, traceNet) 39 + if event.Rule != RuleFanRep { 40 + t.Fatalf("expected fan-rep rule, got %v", event.Rule) 41 + } 42 + assertEventMatchesPair(t, event, rootFan.ID(), rootRep.ID()) 43 + } 44 + 45 + func TestLeftmostPrefersRootEraserFan(t *testing.T) { 46 + traceNet := tracedNet(8) 47 + 48 + // Inner pair: Fan-Fan 49 + innerLeft := newFanWithSinks(traceNet) 50 + innerRight := newFanWithSinks(traceNet) 51 + traceNet.LinkAt(innerLeft, 0, innerRight, 0, 1) 52 + 53 + // Root pair: Eraser-Fan 54 + rootEraser := traceNet.NewEraser() 55 + rootFan := newFanWithSinks(traceNet) 56 + traceNet.Link(rootEraser, 0, rootFan, 0) 57 + 58 + traceNet.Start() 59 + traceNet.ReduceAll() 60 + event := firstTraceEvent(t, traceNet) 61 + if event.Rule != RuleErasure { 62 + t.Fatalf("expected erasure rule, got %v", event.Rule) 63 + } 64 + assertEventMatchesPair(t, event, rootEraser.ID(), rootFan.ID()) 65 + } 66 + 67 + func TestLeftmostPrefersRootEraserRep(t *testing.T) { 68 + traceNet := tracedNet(8) 69 + 70 + // Inner pair: Fan-Fan 71 + innerLeft := newFanWithSinks(traceNet) 72 + innerRight := newFanWithSinks(traceNet) 73 + traceNet.LinkAt(innerLeft, 0, innerRight, 0, 1) 74 + 75 + // Root pair: Eraser-Replicator 76 + rootEraser := traceNet.NewEraser() 77 + rootRep := newReplicatorWithSinks(traceNet, 1, []int{0, 0}) 78 + traceNet.Link(rootEraser, 0, rootRep, 0) 79 + 80 + traceNet.Start() 81 + traceNet.ReduceAll() 82 + event := firstTraceEvent(t, traceNet) 83 + if event.Rule != RuleErasure { 84 + t.Fatalf("expected erasure rule, got %v", event.Rule) 85 + } 86 + assertEventMatchesPair(t, event, rootEraser.ID(), rootRep.ID()) 87 + } 88 + 89 + func TestLeftmostPrefersRootRepRep(t *testing.T) { 90 + traceNet := tracedNet(8) 91 + 92 + // Inner pair: Fan-Fan 93 + innerLeft := newFanWithSinks(traceNet) 94 + innerRight := newFanWithSinks(traceNet) 95 + traceNet.LinkAt(innerLeft, 0, innerRight, 0, 1) 96 + 97 + // Root pair: Rep-Rep (Annihilation) 98 + rootRep1 := newReplicatorWithSinks(traceNet, 1, []int{0, 0}) 99 + rootRep2 := newReplicatorWithSinks(traceNet, 1, []int{0, 0}) 100 + traceNet.Link(rootRep1, 0, rootRep2, 0) 101 + 102 + traceNet.Start() 103 + traceNet.ReduceAll() 104 + event := firstTraceEvent(t, traceNet) 105 + if event.Rule != RuleRepRep { 106 + t.Fatalf("expected rep-rep rule, got %v", event.Rule) 107 + } 108 + assertEventMatchesPair(t, event, rootRep1.ID(), rootRep2.ID()) 109 + } 110 + 111 + func TestLeftmostPrefersRootRepRepComm(t *testing.T) { 112 + traceNet := tracedNet(8) 113 + 114 + // Inner pair: Fan-Fan 115 + innerLeft := newFanWithSinks(traceNet) 116 + innerRight := newFanWithSinks(traceNet) 117 + traceNet.LinkAt(innerLeft, 0, innerRight, 0, 1) 118 + 119 + // Root pair: Rep-Rep (Commutation, different levels) 120 + rootRep1 := newReplicatorWithSinks(traceNet, 1, []int{0, 0}) 121 + rootRep2 := newReplicatorWithSinks(traceNet, 2, []int{0, 0}) 122 + traceNet.Link(rootRep1, 0, rootRep2, 0) 123 + 124 + traceNet.Start() 125 + traceNet.ReduceAll() 126 + event := firstTraceEvent(t, traceNet) 127 + if event.Rule != RuleRepRepComm { 128 + t.Fatalf("expected rep-rep-comm rule, got %v", event.Rule) 129 + } 130 + assertEventMatchesPair(t, event, rootRep1.ID(), rootRep2.ID()) 131 + }
+49
pkg/deltanet/scheduler.go
··· 1 + package deltanet 2 + 3 + const MaxPriority = 64 4 + 5 + type Scheduler struct { 6 + queues [MaxPriority]chan *Wire 7 + signal chan struct{} 8 + } 9 + 10 + func NewScheduler() *Scheduler { 11 + s := &Scheduler{ 12 + signal: make(chan struct{}, 10000), 13 + } 14 + for i := range s.queues { 15 + s.queues[i] = make(chan *Wire, 1024) 16 + } 17 + return s 18 + } 19 + 20 + func (s *Scheduler) Push(w *Wire, depth int) { 21 + if depth < 0 { 22 + depth = 0 23 + } 24 + if depth >= MaxPriority { 25 + depth = MaxPriority - 1 26 + } 27 + s.queues[depth] <- w 28 + select { 29 + case s.signal <- struct{}{}: 30 + default: 31 + // Signal buffer full, workers should be busy enough 32 + } 33 + } 34 + 35 + func (s *Scheduler) Pop() *Wire { 36 + for { 37 + // Scan for highest priority (lowest depth index) 38 + for i := 0; i < MaxPriority; i++ { 39 + select { 40 + case w := <-s.queues[i]: 41 + return w 42 + default: 43 + continue 44 + } 45 + } 46 + // No work found, wait for signal 47 + <-s.signal 48 + } 49 + }
+77
pkg/deltanet/trace.go
··· 1 + package deltanet 2 + 3 + import "sync/atomic" 4 + 5 + type RuleKind int 6 + 7 + const ( 8 + RuleUnknown RuleKind = iota 9 + RuleFanFan 10 + RuleRepRep 11 + RuleRepRepComm 12 + RuleFanRep 13 + RuleErasure 14 + RuleRepDecay 15 + RuleRepMerge 16 + RuleAuxFanRep 17 + ) 18 + 19 + type TraceEvent struct { 20 + Step uint64 21 + Rule RuleKind 22 + AType NodeType 23 + AID uint64 24 + BType NodeType 25 + BID uint64 26 + } 27 + 28 + func (n *Network) EnableTrace(capacity int) { 29 + if capacity <= 0 { 30 + capacity = 1 31 + } 32 + n.traceBuf = make([]TraceEvent, capacity) 33 + n.traceCap = uint64(capacity) 34 + atomic.StoreUint64(&n.traceIdx, 0) 35 + atomic.StoreUint32(&n.traceOn, 1) 36 + } 37 + 38 + func (n *Network) DisableTrace() { 39 + atomic.StoreUint32(&n.traceOn, 0) 40 + } 41 + 42 + func (n *Network) TraceSnapshot() []TraceEvent { 43 + if atomic.LoadUint32(&n.traceOn) == 0 { 44 + return nil 45 + } 46 + count := atomic.LoadUint64(&n.traceIdx) 47 + if count > n.traceCap { 48 + count = n.traceCap 49 + } 50 + res := make([]TraceEvent, count) 51 + copy(res, n.traceBuf[:count]) 52 + return res 53 + } 54 + 55 + func (n *Network) recordTrace(rule RuleKind, a, b Node) { 56 + if atomic.LoadUint32(&n.traceOn) == 0 || n.traceCap == 0 { 57 + return 58 + } 59 + idx := atomic.AddUint64(&n.traceIdx, 1) - 1 60 + if idx >= n.traceCap { 61 + return 62 + } 63 + var bType NodeType 64 + var bID uint64 65 + if b != nil { 66 + bType = b.Type() 67 + bID = b.ID() 68 + } 69 + n.traceBuf[idx] = TraceEvent{ 70 + Step: idx, 71 + Rule: rule, 72 + AType: a.Type(), 73 + AID: a.ID(), 74 + BType: bType, 75 + BID: bID, 76 + } 77 + }
+49
pkg/lambda/ast.go
··· 1 + package lambda 2 + 3 + import "fmt" 4 + 5 + // Term represents a lambda calculus term. 6 + type Term interface { 7 + String() string 8 + } 9 + 10 + // Var represents a variable usage. 11 + type Var struct { 12 + Name string 13 + } 14 + 15 + func (v Var) String() string { 16 + return v.Name 17 + } 18 + 19 + // Abs represents an abstraction (lambda). 20 + type Abs struct { 21 + Arg string 22 + Body Term 23 + } 24 + 25 + func (a Abs) String() string { 26 + return fmt.Sprintf("(%s: %s)", a.Arg, a.Body) 27 + } 28 + 29 + // App represents an application. 30 + type App struct { 31 + Fun Term 32 + Arg Term 33 + } 34 + 35 + func (a App) String() string { 36 + return fmt.Sprintf("(%s %s)", a.Fun, a.Arg) 37 + } 38 + 39 + // Let represents a let binding (sugar for application). 40 + // let x = Val in Body -> (\x. Body) Val 41 + type Let struct { 42 + Name string 43 + Val Term 44 + Body Term 45 + } 46 + 47 + func (l Let) String() string { 48 + return fmt.Sprintf("let %s = %s; %s", l.Name, l.Val, l.Body) 49 + }
+295
pkg/lambda/parser.go
··· 1 + package lambda 2 + 3 + import ( 4 + "fmt" 5 + "unicode" 6 + ) 7 + 8 + type TokenType int 9 + 10 + const ( 11 + TokenEOF TokenType = iota 12 + TokenIdent 13 + TokenColon 14 + TokenEqual 15 + TokenSemicolon 16 + TokenLParen 17 + TokenRParen 18 + TokenLet 19 + TokenIn 20 + ) 21 + 22 + type Token struct { 23 + Type TokenType 24 + Literal string 25 + } 26 + 27 + type Parser struct { 28 + input string 29 + pos int 30 + current Token 31 + } 32 + 33 + func NewParser(input string) *Parser { 34 + p := &Parser{input: input} 35 + p.next() 36 + return p 37 + } 38 + 39 + func (p *Parser) next() { 40 + p.skipWhitespace() 41 + if p.pos >= len(p.input) { 42 + p.current = Token{Type: TokenEOF} 43 + return 44 + } 45 + 46 + ch := p.input[p.pos] 47 + switch { 48 + case isLetter(ch): 49 + start := p.pos 50 + for p.pos < len(p.input) && (isLetter(p.input[p.pos]) || isDigit(p.input[p.pos])) { 51 + p.pos++ 52 + } 53 + lit := p.input[start:p.pos] 54 + if lit == "let" { 55 + p.current = Token{Type: TokenLet, Literal: lit} 56 + } else if lit == "in" { 57 + p.current = Token{Type: TokenIn, Literal: lit} 58 + } else { 59 + p.current = Token{Type: TokenIdent, Literal: lit} 60 + } 61 + case ch == ':': 62 + p.current = Token{Type: TokenColon, Literal: ":"} 63 + p.pos++ 64 + case ch == '=': 65 + p.current = Token{Type: TokenEqual, Literal: "="} 66 + p.pos++ 67 + case ch == ';': 68 + p.current = Token{Type: TokenSemicolon, Literal: ";"} 69 + p.pos++ 70 + case ch == '(': 71 + p.current = Token{Type: TokenLParen, Literal: "("} 72 + p.pos++ 73 + case ch == ')': 74 + p.current = Token{Type: TokenRParen, Literal: ")"} 75 + p.pos++ 76 + default: 77 + // Treat unknown chars as identifiers for now (e.g. +) 78 + // Or maybe just single char symbols 79 + p.current = Token{Type: TokenIdent, Literal: string(ch)} 80 + p.pos++ 81 + } 82 + } 83 + 84 + func (p *Parser) skipWhitespace() { 85 + for p.pos < len(p.input) && unicode.IsSpace(rune(p.input[p.pos])) { 86 + p.pos++ 87 + } 88 + } 89 + 90 + func isLetter(ch byte) bool { 91 + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' 92 + } 93 + 94 + func isDigit(ch byte) bool { 95 + return ch >= '0' && ch <= '9' 96 + } 97 + 98 + func (p *Parser) Parse() (Term, error) { 99 + return p.parseTerm() 100 + } 101 + 102 + // Term ::= Abs | Let | App 103 + func (p *Parser) parseTerm() (Term, error) { 104 + if p.current.Type == TokenLet { 105 + return p.parseLet() 106 + } 107 + 108 + // Try to parse an abstraction or application 109 + // Since application is left-associative and abstraction extends to the right, 110 + // we need to be careful. 111 + // Nix syntax: x: Body 112 + // App: M N 113 + 114 + // We parse a list of "atoms" and combine them as application. 115 + // If we see an identifier followed by colon, it's an abstraction. 116 + // But we need lookahead or backtracking. 117 + // Actually, `x: ...` starts with ident then colon. 118 + // `x y` starts with ident then ident. 119 + 120 + // Let's parse "Atom" first. 121 + // Atom ::= Ident | ( Term ) 122 + 123 + // If current is Ident: 124 + // Check next token. If Colon, it's Abs. 125 + // Else, it's an Atom (Var), and we continue parsing more Atoms for App. 126 + 127 + if p.current.Type == TokenIdent { 128 + // Lookahead 129 + savePos := p.pos 130 + saveTok := p.current 131 + 132 + // Peek next 133 + p.next() 134 + if p.current.Type == TokenColon { 135 + // It's an abstraction 136 + arg := saveTok.Literal 137 + p.next() // consume colon 138 + body, err := p.parseTerm() 139 + if err != nil { 140 + return nil, err 141 + } 142 + return Abs{Arg: arg, Body: body}, nil 143 + } 144 + 145 + // Not an abstraction, backtrack 146 + p.pos = savePos 147 + p.current = saveTok 148 + } 149 + 150 + return p.parseApp() 151 + } 152 + 153 + func (p *Parser) parseApp() (Term, error) { 154 + left, err := p.parseAtom() 155 + if err != nil { 156 + return nil, err 157 + } 158 + 159 + for { 160 + if p.current.Type == TokenEOF || p.current.Type == TokenRParen || p.current.Type == TokenSemicolon || p.current.Type == TokenIn { 161 + break 162 + } 163 + // Also stop if we see something that looks like the start of an abstraction? 164 + // `x: ...` inside an app? `(x: x) y` is valid. `x y: z` -> `x (y: z)`? 165 + // Usually lambda extends as far right as possible. 166 + // So `x y: z` parses as `x (y: z)`. 167 + // If we see Ident Colon, we should parse it as Abs and append to App. 168 + 169 + if p.current.Type == TokenIdent { 170 + // Check for colon 171 + savePos := p.pos 172 + saveTok := p.current 173 + p.next() 174 + if p.current.Type == TokenColon { 175 + // It's an abstraction `arg: body` 176 + // This abstraction is the argument to the current application 177 + argName := saveTok.Literal 178 + p.next() // consume colon 179 + body, err := p.parseTerm() 180 + if err != nil { 181 + return nil, err 182 + } 183 + left = App{Fun: left, Arg: Abs{Arg: argName, Body: body}} 184 + // After parsing an abstraction (which consumes everything to the right), 185 + // we are done with this application chain? 186 + // Yes, because `x y: z a` -> `x (y: z a)`. 187 + return left, nil 188 + } 189 + // Backtrack 190 + p.pos = savePos 191 + p.current = saveTok 192 + } 193 + 194 + right, err := p.parseAtom() 195 + if err != nil { 196 + // If we can't parse an atom, maybe we are done 197 + break 198 + } 199 + left = App{Fun: left, Arg: right} 200 + } 201 + 202 + return left, nil 203 + } 204 + 205 + func (p *Parser) parseAtom() (Term, error) { 206 + switch p.current.Type { 207 + case TokenIdent: 208 + name := p.current.Literal 209 + p.next() 210 + return Var{Name: name}, nil 211 + case TokenLParen: 212 + p.next() 213 + term, err := p.parseTerm() 214 + if err != nil { 215 + return nil, err 216 + } 217 + if p.current.Type != TokenRParen { 218 + return nil, fmt.Errorf("expected ')'") 219 + } 220 + p.next() 221 + return term, nil 222 + default: 223 + return nil, fmt.Errorf("unexpected token: %v", p.current) 224 + } 225 + } 226 + 227 + func (p *Parser) parseLet() (Term, error) { 228 + p.next() // consume 'let' 229 + 230 + // Parse bindings: x = M; y = N; ... 231 + type binding struct { 232 + name string 233 + val Term 234 + } 235 + var bindings []binding 236 + 237 + for { 238 + if p.current.Type != TokenIdent { 239 + return nil, fmt.Errorf("expected identifier in let binding") 240 + } 241 + name := p.current.Literal 242 + p.next() 243 + 244 + if p.current.Type != TokenEqual { 245 + return nil, fmt.Errorf("expected '='") 246 + } 247 + p.next() 248 + 249 + val, err := p.parseTerm() 250 + if err != nil { 251 + return nil, err 252 + } 253 + 254 + bindings = append(bindings, binding{name, val}) 255 + 256 + if p.current.Type == TokenSemicolon { 257 + p.next() 258 + // Check if next is 'in' or another ident 259 + if p.current.Type == TokenIn { 260 + p.next() 261 + break 262 + } 263 + // Continue to next binding 264 + } else if p.current.Type == TokenIn { 265 + p.next() 266 + break 267 + } else { 268 + return nil, fmt.Errorf("expected ';' or 'in'") 269 + } 270 + } 271 + 272 + body, err := p.parseTerm() 273 + if err != nil { 274 + return nil, err 275 + } 276 + 277 + // Desugar: let x=M; y=N in B -> (\x. (\y. B) N) M 278 + // We iterate backwards 279 + term := body 280 + for i := len(bindings) - 1; i >= 0; i-- { 281 + b := bindings[i] 282 + term = App{ 283 + Fun: Abs{Arg: b.name, Body: term}, 284 + Arg: b.val, 285 + } 286 + } 287 + 288 + return term, nil 289 + } 290 + 291 + // Parse parses a lambda term from a string. 292 + func Parse(input string) (Term, error) { 293 + p := NewParser(input) 294 + return p.Parse() 295 + }
+373
pkg/lambda/translate.go
··· 1 + package lambda 2 + 3 + import ( 4 + "fmt" 5 + "github.com/vic/godnet/pkg/deltanet" 6 + ) 7 + 8 + // Context for variables: name -> {Node, Port, Level} 9 + type varInfo struct { 10 + node deltanet.Node 11 + port int 12 + level int 13 + } 14 + 15 + // ToDeltaNet converts a lambda term to a Delta Net. 16 + func ToDeltaNet(term Term, net *deltanet.Network) (deltanet.Node, int) { 17 + // We return the Node and Port index that represents the "root" of the term. 18 + // This port should be connected to the "parent". 19 + 20 + vars := make(map[string]*varInfo) 21 + 22 + return buildTerm(term, net, vars, 0) 23 + } 24 + 25 + func buildTerm(term Term, net *deltanet.Network, vars map[string]*varInfo, level int) (deltanet.Node, int) { 26 + switch t := term.(type) { 27 + case Var: 28 + if info, ok := vars[t.Name]; ok { 29 + // Variable is bound 30 + 31 + if info.node.Type() == deltanet.NodeTypeReplicator { 32 + // Subsequent use 33 + // info.node is the Replicator. 34 + // We need to add a port to it. 35 + // Create new Replicator with +1 port. 36 + oldRep := info.node 37 + oldDeltas := oldRep.Deltas() 38 + newDelta := level - (info.level + 1) 39 + newDeltas := append(oldDeltas, newDelta) 40 + 41 + newRep := net.NewReplicator(oldRep.Level(), newDeltas) 42 + fmt.Printf("ToDeltaNet: Expand Replicator ID %d level=%d oldDeltas=%v -> newDeltas=%v (usage level=%d, binder level=%d)\n", oldRep.ID(), oldRep.Level(), oldDeltas, newDeltas, level, info.level) 43 + 44 + // Move connections 45 + // Rep.0 -> Source 46 + sourceNode, sourcePort := net.GetLink(oldRep, 0) 47 + net.Link(newRep, 0, sourceNode, sourcePort) 48 + 49 + // Move existing aux ports 50 + for i := 0; i < len(oldDeltas); i++ { 51 + // Get what oldRep.i+1 is connected to 52 + destNode, destPort := net.GetLink(oldRep, i+1) 53 + if destNode != nil { 54 + net.Link(newRep, i+1, destNode, destPort) 55 + } 56 + } 57 + 58 + // Update info 59 + info.node = newRep 60 + info.port = 0 61 + 62 + // Return new port 63 + return newRep, len(newDeltas) // Index is len (1-based? No, 0 is principal. 1..len) 64 + } 65 + 66 + linkNode, _ := net.GetLink(info.node, info.port) 67 + 68 + if linkNode.Type() == deltanet.NodeTypeEraser { 69 + // First use 70 + // Remove Eraser (linkNode) 71 + // In `deltanet`, `removeNode` is no-op, but we should disconnect. 72 + // Actually `Link` overwrites. 73 + 74 + // Create Replicator 75 + delta := level - (info.level + 1) 76 + 77 + repLevel := info.level + 1 78 + 79 + // Link Rep.0 to Source (info.node, info.port) 80 + rep := net.NewReplicator(repLevel, []int{delta}) 81 + net.Link(rep, 0, info.node, info.port) 82 + fmt.Printf("ToDeltaNet: First-use: created Replicator ID %d level=%d deltas=%v for binder level=%d usage level=%d\n", rep.ID(), rep.Level(), rep.Deltas(), info.level, level) 83 + 84 + // Update info to point to Rep 85 + info.node = rep 86 + info.port = 0 // Rep.0 is the input 87 + 88 + // Return Rep.1 89 + return rep, 1 90 + 91 + } else { 92 + // Should not happen if logic is correct (either Eraser or Replicator) 93 + panic(fmt.Sprintf("Unexpected node type on variable binding: %v", linkNode.Type())) 94 + } 95 + 96 + } else { 97 + // Free variable 98 + // Create Var node 99 + v := net.NewVar() 100 + // Create Replicator to share it (as per deltanets.ts) 101 + // "Create free variable node... Create a replicator fan-in... link... return rep.1" 102 + // Level 0 for free vars. 103 + // Debug: record replicator parameters for free var 104 + fmt.Printf("ToDeltaNet: Free var '%s' at level=%d -> Rep(level=%d, deltas=%v)\n", t.Name, level, 0, []int{level - 1}) 105 + rep := net.NewReplicator(0, []int{level - 1}) // level - (0 + 1) ? 106 + net.Link(rep, 0, v, 0) 107 + 108 + // Register in vars so we can share it if used again 109 + vars[t.Name] = &varInfo{node: rep, port: 0, level: 0} 110 + 111 + return rep, 1 112 + } 113 + 114 + case Abs: 115 + // Create Fan 116 + fan := net.NewFan() 117 + // fan.0 is Result (returned) 118 + // fan.1 is Body 119 + // fan.2 is Var 120 + 121 + // Create Eraser for Var initially 122 + era := net.NewEraser() 123 + net.Link(era, 0, fan, 2) 124 + 125 + // Register var 126 + // Save old var info if shadowing 127 + oldVar := vars[t.Arg] 128 + vars[t.Arg] = &varInfo{node: fan, port: 2, level: level} 129 + 130 + // Build Body 131 + bodyNode, bodyPort := buildTerm(t.Body, net, vars, level) 132 + net.Link(fan, 1, bodyNode, bodyPort) 133 + 134 + // Restore var 135 + if oldVar != nil { 136 + vars[t.Arg] = oldVar 137 + } else { 138 + delete(vars, t.Arg) 139 + } 140 + 141 + return fan, 0 142 + 143 + case App: 144 + // Create Fan 145 + fan := net.NewFan() 146 + // fan.0 is Function 147 + // fan.1 is Result (returned) 148 + // fan.2 is Argument 149 + 150 + // Build Function 151 + funNode, funPort := buildTerm(t.Fun, net, vars, level) 152 + net.Link(fan, 0, funNode, funPort) 153 + 154 + // Build Argument (level + 1) 155 + argNode, argPort := buildTerm(t.Arg, net, vars, level+1) 156 + net.Link(fan, 2, argNode, argPort) 157 + 158 + return fan, 1 159 + 160 + case Let: 161 + // Should have been desugared by parser, but if we encounter it: 162 + // let x = Val in Body -> (\x. Body) Val 163 + desugared := App{ 164 + Fun: Abs{Arg: t.Name, Body: t.Body}, 165 + Arg: t.Val, 166 + } 167 + return buildTerm(desugared, net, vars, level) 168 + 169 + default: 170 + panic("Unknown term type") 171 + } 172 + } 173 + 174 + // FromDeltaNet reconstructs a lambda term from the network. 175 + func FromDeltaNet(net *deltanet.Network, rootNode deltanet.Node, rootPort int) Term { 176 + // Debug 177 + // fmt.Printf("FromDeltaNet: Root %v Port %d\n", rootNode.Type(), rootPort) 178 + 179 + // We traverse from the root. 180 + // We need to track visited nodes to handle loops (though lambda terms shouldn't have loops unless we have recursion combinators). 181 + // But we also need to track bound variables. 182 + 183 + // Map from (NodeID, Port) to Variable Name for bound variables. 184 + // When we enter Abs at 0, we assign a name to Abs.2. 185 + 186 + bindings := make(map[uint64]string) // Key: Node ID of the binder (Fan), Value: Name 187 + 188 + // We need a name generator 189 + nameGen := 0 190 + nextName := func() string { 191 + name := fmt.Sprintf("x%d", nameGen) 192 + nameGen++ 193 + return name 194 + } 195 + 196 + return readTerm(net, rootNode, rootPort, bindings, nextName) 197 + } 198 + 199 + func readTerm(net *deltanet.Network, node deltanet.Node, port int, bindings map[uint64]string, nextName func() string) Term { 200 + if node == nil { 201 + return Var{Name: "<nil>"} 202 + } 203 + 204 + switch node.Type() { 205 + case deltanet.NodeTypeFan: 206 + if port == 0 { 207 + // Entering Abs at Result -> Abs 208 + name := nextName() 209 + bindings[node.ID()] = name 210 + 211 + body := readTerm(net, getLinkNode(net, node, 1), getLinkPort(net, node, 1), bindings, nextName) 212 + return Abs{Arg: name, Body: body} 213 + } else if port == 1 { 214 + // Entering App at Result -> App 215 + fun := readTerm(net, getLinkNode(net, node, 0), getLinkPort(net, node, 0), bindings, nextName) 216 + arg := readTerm(net, getLinkNode(net, node, 2), getLinkPort(net, node, 2), bindings, nextName) 217 + return App{Fun: fun, Arg: arg} 218 + } else { 219 + // Entering at 2? 220 + // This means we are traversing UP a variable binding? 221 + // Should not happen in normal term traversal unless we are debugging. 222 + return Var{Name: "<binding>"} 223 + } 224 + 225 + case deltanet.NodeTypeReplicator: 226 + // We entered a Replicator. 227 + // If we entered at Aux port (>= 1), we are reading a variable usage. 228 + // We need to trace back to the source (Port 0). 229 + if port > 0 { 230 + sourceNode := getLinkNode(net, node, 0) 231 + sourcePort := getLinkPort(net, node, 0) 232 + 233 + // Trace back until we hit a Fan.2 (Binder) or Var (Free) 234 + // If the source is a Fan (Abs/App), traceVariable will delegate 235 + // to readTerm to reconstruct the full subterm. 236 + return traceVariable(net, sourceNode, sourcePort, bindings, nextName) 237 + } else { 238 + // Entered at 0? 239 + // Reading the value being shared? 240 + // This happens if we have `(\x. x) M`. `M` connects to `Rep.0`. 241 + // If we read `M`, we traverse `M`. 242 + // But here we are reading the *term* that `Rep` is part of. 243 + // If `Rep` is part of the term structure (e.g. sharing a subterm), 244 + // then `Rep.0` points to the subterm. 245 + // So we just recurse on `Rep.0`? 246 + // No, `Rep.0` is the *input* to the Replicator. 247 + // If we enter at 0, we are going *upstream*? 248 + // Wait, `Rep` directionality: 249 + // 0 is Input. 1..N are Outputs. 250 + // If we enter at 0, we are looking at the Output of `Rep`? No. 251 + // If we enter at 0, we came from the Input side. 252 + // This means we are traversing *into* the Replicator from the source. 253 + // This implies the Replicator is sharing the *result* of something. 254 + // e.g. `let x = M in ...`. `M` connects to `Rep.0`. 255 + // If we are reading `M`, we don't hit `Rep`. 256 + // If we are reading the body, we hit `Rep` at aux ports. 257 + // So when do we hit `Rep` at 0? 258 + // Only if we are traversing `M` and `M` *is* the Replicator? 259 + // No, `Rep` is not a term constructor like Abs/App. It's a structural node. 260 + // If `M` is `x`, and `x` is shared, then `M` *is* a wire to `Rep`. 261 + // But `Rep` is connected to `x`'s binder. 262 + // So `M` connects to `Rep` aux port. 263 + // So we enter at aux. 264 + 265 + // What if `M` is `\y. y` and it is shared? 266 + // `Abs` (M) connects to `Rep.0`. 267 + // `Rep` aux ports connect to usages. 268 + // If we read `M` (e.g. if we are reading the `let` value), we hit `Rep.0`. 269 + // So we should just read what `Rep` is connected to? 270 + // No, `Rep` *is* the sharing mechanism. 271 + // If we are reading the term `M`, and `M` is shared, we see `Abs`. 272 + // We don't see `Rep` unless we are reading the *usages*. 273 + // Wait. `Abs.0` connects to `Rep.0`. 274 + // If we read `M`, we start at `Abs.0`. 275 + // We don't start at `Rep`. 276 + // Unless `M` is *defined* as `Rep`? No. 277 + 278 + // Ah, `FromDeltaNet` takes `rootNode, rootPort`. 279 + // This is the "output" of the term. 280 + // If the term is `\x. x`, output is `Abs.0`. 281 + // If the term is `x`, output is `Rep` aux port (or `Abs.2`). 282 + // If the term is `M N`, output is `App.1`. 283 + 284 + // So we should never enter `Rep` at 0 during normal read-back of a term, 285 + // unless the term *itself* is being shared and we are reading the *source*? 286 + // But `rootNode` is the *result* of the reduction. 287 + // If the result is shared, then `rootNode` might be `Rep`? 288 + // If the result is `x` (free var), and it's shared? 289 + // `Var` -> `Rep.0`. `Rep.1` -> Output. 290 + // So Output is `Rep.1`. We enter at 1. 291 + 292 + // So entering at 0 should be rare/impossible for "Result". 293 + return Var{Name: "<rep-0>"} 294 + } 295 + 296 + case deltanet.NodeTypeVar: 297 + // Free variable or wire 298 + // If it's a named var, return it. 299 + // But `Var` nodes don't store names in `deltanet` package? 300 + // `deltanet.NewVar()` creates `NodeTypeVar`. 301 + // It doesn't store a name. 302 + // We lost the name! 303 + // We need to store names for free variables if we want to read them back. 304 + // But `deltanet` doesn't support labels. 305 + // I can't modify `deltanet` package (user reverted). 306 + // So I can't store names in `Var` nodes. 307 + // I'll return "<free>" or generate a name. 308 + return Var{Name: "<free>"} 309 + 310 + case deltanet.NodeTypeEraser: 311 + return Var{Name: "<erased>"} 312 + 313 + default: 314 + return Var{Name: fmt.Sprintf("<? %v>", node.Type())} 315 + } 316 + } 317 + 318 + func traceVariable(net *deltanet.Network, node deltanet.Node, port int, bindings map[uint64]string, nextName func() string) Term { 319 + // Follow wires up through Replicators (entering at 0, leaving at 0?) 320 + // No, `Rep.0` connects to Source. 321 + // So if we are at `Rep`, we go to `Rep.0`'s link. 322 + 323 + currNode := node 324 + currPort := port 325 + 326 + for { 327 + if currNode == nil { 328 + return Var{Name: "<nil-trace>"} 329 + } 330 + 331 + switch currNode.Type() { 332 + case deltanet.NodeTypeFan: 333 + // Hit a Fan. 334 + // If port 2, it's a binder. 335 + if currPort == 2 { 336 + if name, ok := bindings[currNode.ID()]; ok { 337 + return Var{Name: name} 338 + } 339 + return Var{Name: "<unbound-fan>"} 340 + } 341 + // If port 0 or 1, reconstruct the full term (Abs or App) 342 + return readTerm(net, currNode, currPort, bindings, nextName) 343 + 344 + case deltanet.NodeTypeReplicator: 345 + // Continue trace from Rep.0 346 + if currPort == 0 { 347 + return Var{Name: "<rep-trace-0>"} 348 + } 349 + nextNode, nextPort := net.GetLink(currNode, 0) 350 + currNode = nextNode 351 + currPort = nextPort 352 + 353 + case deltanet.NodeTypeVar: 354 + return Var{Name: "<free>"} 355 + 356 + case deltanet.NodeTypeEraser: 357 + return Var{Name: "<erased>"} 358 + 359 + default: 360 + return Var{Name: fmt.Sprintf("<? %v>", currNode.Type())} 361 + } 362 + } 363 + } 364 + 365 + func getLinkNode(net *deltanet.Network, node deltanet.Node, port int) deltanet.Node { 366 + n, _ := net.GetLink(node, port) 367 + return n 368 + } 369 + 370 + func getLinkPort(net *deltanet.Network, node deltanet.Node, port int) int { 371 + _, p := net.GetLink(node, port) 372 + return p 373 + }