+15
.github/FUNDING.yml
+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
+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
+12
.tangled/workflows/mirror.yml
+201
LICENSE
+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
README.md
···
1
+
# GoD-Net - A Golang implementation of Delta Interaction Nets.
+148
cmd/gentests/helper/reduction.go
+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
+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
+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
+
}
+165
pkg/deltanet/canonical_test.go
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+
}