···11+package deltanet
22+33+import (
44+ "testing"
55+ "time"
66+)
77+88+// TestLMO_ErasureOfDivergingTerm tests that a diverging term (Omega) is erased
99+// if it is an argument to a function that ignores it (K combinator),
1010+// ensuring that the reduction strategy is Leftmost-Outermost (or at least Outermost).
1111+// Term: (\x. y) ((\z. z z) (\z. z z)) -> y
1212+func TestLMO_ErasureOfDivergingTerm(t *testing.T) {
1313+ net := NewNetwork()
1414+1515+ // Construct (\x. y)
1616+ // Abs Fan: 0=Result, 1=Body, 2=Var
1717+ abs := net.NewFan()
1818+ y := net.NewVar()
1919+ era := net.NewEraser()
2020+ net.LinkAt(abs, 1, y, 0, 0)
2121+ net.LinkAt(abs, 2, era, 0, 0)
2222+2323+ // Construct Omega: (\z. z z) (\z. z z)
2424+ buildSelfApp := func(depth uint64) Node {
2525+ // Abs Fan: 0=Result, 1=Body, 2=Var
2626+ abs := net.NewFan()
2727+2828+ // Body: z z (App)
2929+ // App Fan: 0=Fun, 1=Result, 2=Arg
3030+ app := net.NewFan()
3131+3232+ // Abs.1 (Body) -> App.1 (Result)
3333+ net.LinkAt(abs, 1, app, 1, depth)
3434+3535+ // Var z is shared: Replicator
3636+ // Rep: 0=Input, 1=Fun, 2=Arg
3737+ rep := net.NewReplicator(0, []int{0, 0})
3838+ // Rep.0 -> Abs.2 (Var)
3939+ net.LinkAt(rep, 0, abs, 2, depth)
4040+ // Rep.1 -> App.0 (Fun)
4141+ net.LinkAt(rep, 1, app, 0, depth)
4242+ // Rep.2 -> App.2 (Arg)
4343+ net.LinkAt(rep, 2, app, 2, depth)
4444+4545+ return abs
4646+ }
4747+4848+ omegaFun := buildSelfApp(1)
4949+ omegaArg := buildSelfApp(2)
5050+5151+ // Omega App: 0=Fun, 1=Result, 2=Arg
5252+ omegaApp := net.NewFan()
5353+ // OmegaApp.0 -> OmegaFun.0 (Redex!)
5454+ // Depth 1
5555+ net.LinkAt(omegaApp, 0, omegaFun, 0, 1)
5656+ // OmegaApp.2 -> OmegaArg.0
5757+ net.LinkAt(omegaApp, 2, omegaArg, 0, 2)
5858+5959+ // Main App: (\x. y) Omega
6060+ // App Fan: 0=Fun, 1=Result, 2=Arg
6161+ mainApp := net.NewFan()
6262+ // MainApp.0 -> Abs.0 (Redex!)
6363+ // Depth 0
6464+ net.LinkAt(mainApp, 0, abs, 0, 0)
6565+ // MainApp.2 -> OmegaApp.1 (Arg connects to Result of Omega)
6666+ net.LinkAt(mainApp, 2, omegaApp, 1, 1)
6767+6868+ // Root interface
6969+ root := net.NewVar()
7070+ // MainApp.1 -> Root
7171+ net.LinkAt(mainApp, 1, root, 0, 0)
7272+7373+ // Reduce
7474+ // Use a channel to detect timeout/hang
7575+ done := make(chan bool)
7676+ go func() {
7777+ net.ReduceToNormalForm()
7878+ done <- true
7979+ }()
8080+8181+ select {
8282+ case <-done:
8383+ // Success
8484+ case <-time.After(2 * time.Second):
8585+ t.Fatal("Reduction timed out - likely looping (LMO failure)")
8686+ }
8787+8888+ // Check result: Root should be connected to y
8989+ if !net.IsConnected(root, 0, y, 0) {
9090+ t.Errorf("Did not reduce to y")
9191+ }
9292+}
+89
pkg/deltanet/replicator_merge_test.go
···11+package deltanet
22+33+import (
44+ "testing"
55+ "time"
66+)
77+88+// TestReplicatorUnpairedMerge verifies that two consecutive unpaired replicators
99+// that satisfy the paper's constraint (0 <= lB - lA <= d) get merged by the
1010+// canonicalization pass. We construct a small net that should trigger a merge
1111+// and assert the network statistics and topology reflect the merge.
1212+func TestReplicatorUnpairedMerge(t *testing.T) {
1313+ net := NewNetwork()
1414+1515+ // Create two replicators A and B such that B is connected to an aux port of A.
1616+ // Choose levels and delta so that 0 <= lB - lA <= d
1717+ // A: level 3, deltas [2]
1818+ // B: level 5 so that B.Level == A.Level + delta (3 + 2 == 5)
1919+ a := net.NewReplicator(3, []int{2})
2020+ b := net.NewReplicator(5, []int{0})
2121+2222+ // Connect A aux 1 directly to B principal so reduceRepMerge can detect the pattern
2323+ net.Link(a, 1, b, 0)
2424+2525+ // Connect a principal to a Var (root area) so it's part of the net
2626+ root := net.NewVar()
2727+ net.Link(a, 0, root, 0)
2828+2929+ // Also attach a neighbor to B so merge has context
3030+ rightVar := net.NewVar()
3131+ net.Link(b, 1, rightVar, 0)
3232+3333+ // Run reduction which includes canonicalization passes
3434+ done := make(chan bool)
3535+ go func() {
3636+ net.ReduceToNormalForm()
3737+ done <- true
3838+ }()
3939+4040+ select {
4141+ case <-done:
4242+ // proceed
4343+ case <-time.After(2 * time.Second):
4444+ t.Fatal("Reduction timed out - likely a hang")
4545+ }
4646+4747+ // After reduction and canonicalization, there should be evidence of a merge.
4848+ // The implementation exposes a stat counter; check that at least one merge occurred.
4949+ stats := net.GetStats()
5050+ if stats.RepMerge == 0 {
5151+ t.Errorf("Expected at least one replicator merge, got 0")
5252+ }
5353+}
5454+5555+// TestAuxFanReplication ensures the aux-fan replication rule is applied in the
5656+// second phase and produces the expected topology: fan-out replicators eliminated
5757+// and appropriate copies created. We construct a fan connected to a replicator
5858+// and check that aux-fan replication statistic increases.
5959+func TestAuxFanReplicationStat(t *testing.T) {
6060+ net := NewNetwork()
6161+6262+ // Create a fan (application) and a replicator in the auxiliary position so
6363+ // that aux-fan replication should be triggered when reaching phase two.
6464+ fan := net.NewFan()
6565+ rep := net.NewReplicator(1, []int{0, 0})
6666+6767+ // Connect fan principal to rep principal to create active pair (fan.0 <-> rep.0)
6868+ net.Link(fan, 0, rep, 0)
6969+7070+ // Hook up aux ports to vars to keep structure
7171+ v1 := net.NewVar()
7272+ v2 := net.NewVar()
7373+ net.Link(fan, 1, v1, 0)
7474+ net.Link(fan, 2, v2, 0)
7575+7676+ r1 := net.NewVar()
7777+ r2 := net.NewVar()
7878+ net.Link(rep, 1, r1, 0)
7979+ net.Link(rep, 2, r2, 0)
8080+8181+ // Switch to phase 2 to trigger aux-fan replication behavior and reduce
8282+ net.SetPhase(2)
8383+ net.ReduceAll()
8484+8585+ stats := net.GetStats()
8686+ if stats.AuxFanRep == 0 {
8787+ t.Errorf("Expected aux-fan replication to have occurred, got 0")
8888+ }
8989+}
+21-21
pkg/lambda/parser.go
···104104 if p.current.Type == TokenLet {
105105 return p.parseLet()
106106 }
107107-107107+108108 // Try to parse an abstraction or application
109109 // Since application is left-associative and abstraction extends to the right,
110110 // we need to be careful.
111111 // Nix syntax: x: Body
112112 // App: M N
113113-113113+114114 // We parse a list of "atoms" and combine them as application.
115115 // If we see an identifier followed by colon, it's an abstraction.
116116 // But we need lookahead or backtracking.
117117 // Actually, `x: ...` starts with ident then colon.
118118 // `x y` starts with ident then ident.
119119-119119+120120 // Let's parse "Atom" first.
121121 // Atom ::= Ident | ( Term )
122122-122122+123123 // If current is Ident:
124124 // Check next token. If Colon, it's Abs.
125125 // Else, it's an Atom (Var), and we continue parsing more Atoms for App.
126126-126126+127127 if p.current.Type == TokenIdent {
128128 // Lookahead
129129 savePos := p.pos
130130 saveTok := p.current
131131-131131+132132 // Peek next
133133 p.next()
134134 if p.current.Type == TokenColon {
···141141 }
142142 return Abs{Arg: arg, Body: body}, nil
143143 }
144144-144144+145145 // Not an abstraction, backtrack
146146 p.pos = savePos
147147 p.current = saveTok
148148 }
149149-149149+150150 return p.parseApp()
151151}
152152···155155 if err != nil {
156156 return nil, err
157157 }
158158-158158+159159 for {
160160 if p.current.Type == TokenEOF || p.current.Type == TokenRParen || p.current.Type == TokenSemicolon || p.current.Type == TokenIn {
161161 break
···165165 // Usually lambda extends as far right as possible.
166166 // So `x y: z` parses as `x (y: z)`.
167167 // If we see Ident Colon, we should parse it as Abs and append to App.
168168-168168+169169 if p.current.Type == TokenIdent {
170170 // Check for colon
171171 savePos := p.pos
···190190 p.pos = savePos
191191 p.current = saveTok
192192 }
193193-193193+194194 right, err := p.parseAtom()
195195 if err != nil {
196196 // If we can't parse an atom, maybe we are done
···198198 }
199199 left = App{Fun: left, Arg: right}
200200 }
201201-201201+202202 return left, nil
203203}
204204···226226227227func (p *Parser) parseLet() (Term, error) {
228228 p.next() // consume 'let'
229229-229229+230230 // Parse bindings: x = M; y = N; ...
231231 type binding struct {
232232 name string
233233 val Term
234234 }
235235 var bindings []binding
236236-236236+237237 for {
238238 if p.current.Type != TokenIdent {
239239 return nil, fmt.Errorf("expected identifier in let binding")
240240 }
241241 name := p.current.Literal
242242 p.next()
243243-243243+244244 if p.current.Type != TokenEqual {
245245 return nil, fmt.Errorf("expected '='")
246246 }
247247 p.next()
248248-248248+249249 val, err := p.parseTerm()
250250 if err != nil {
251251 return nil, err
252252 }
253253-253253+254254 bindings = append(bindings, binding{name, val})
255255-255255+256256 if p.current.Type == TokenSemicolon {
257257 p.next()
258258 // Check if next is 'in' or another ident
···268268 return nil, fmt.Errorf("expected ';' or 'in'")
269269 }
270270 }
271271-271271+272272 body, err := p.parseTerm()
273273 if err != nil {
274274 return nil, err
275275 }
276276-276276+277277 // Desugar: let x=M; y=N in B -> (\x. (\y. B) N) M
278278 // We iterate backwards
279279 term := body
···284284 Arg: b.val,
285285 }
286286 }
287287-287287+288288 return term, nil
289289}
290290
+206-81
pkg/lambda/translate.go
···33import (
44 "fmt"
55 "github.com/vic/godnet/pkg/deltanet"
66+ "os"
67)
88+99+var deltaDebug = os.Getenv("DELTA_DEBUG") != ""
710811// Context for variables: name -> {Node, Port, Level}
912type varInfo struct {
···1619func ToDeltaNet(term Term, net *deltanet.Network) (deltanet.Node, int) {
1720 // We return the Node and Port index that represents the "root" of the term.
1821 // This port should be connected to the "parent".
1919-2222+2023 vars := make(map[string]*varInfo)
2121-2222- return buildTerm(term, net, vars, 0)
2424+2525+ return buildTerm(term, net, vars, 0, 0)
2326}
24272525-func buildTerm(term Term, net *deltanet.Network, vars map[string]*varInfo, level int) (deltanet.Node, int) {
2828+func buildTerm(term Term, net *deltanet.Network, vars map[string]*varInfo, level int, depth uint64) (deltanet.Node, int) {
2629 switch t := term.(type) {
2730 case Var:
2831 if info, ok := vars[t.Name]; ok {
2932 // Variable is bound
3030-3333+3134 if info.node.Type() == deltanet.NodeTypeReplicator {
3235 // Subsequent use
3336 // info.node is the Replicator.
···3740 oldDeltas := oldRep.Deltas()
3841 newDelta := level - (info.level + 1)
3942 newDeltas := append(oldDeltas, newDelta)
4040-4343+4144 newRep := net.NewReplicator(oldRep.Level(), newDeltas)
4242- 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)
4343-4545+ // 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)
4646+4447 // Move connections
4548 // Rep.0 -> Source
4649 sourceNode, sourcePort := net.GetLink(oldRep, 0)
4747- net.Link(newRep, 0, sourceNode, sourcePort)
4848-5050+ net.LinkAt(newRep, 0, sourceNode, sourcePort, depth)
5151+4952 // Move existing aux ports
5053 for i := 0; i < len(oldDeltas); i++ {
5154 // Get what oldRep.i+1 is connected to
5255 destNode, destPort := net.GetLink(oldRep, i+1)
5356 if destNode != nil {
5454- net.Link(newRep, i+1, destNode, destPort)
5757+ net.LinkAt(newRep, i+1, destNode, destPort, depth)
5558 }
5659 }
5757-6060+5861 // Update info
5962 info.node = newRep
6063 info.port = 0
6161-6464+6265 // Return new port
6366 return newRep, len(newDeltas) // Index is len (1-based? No, 0 is principal. 1..len)
6467 }
6565-6868+6669 linkNode, _ := net.GetLink(info.node, info.port)
6767-7070+6871 if linkNode.Type() == deltanet.NodeTypeEraser {
6972 // First use
7073 // Remove Eraser (linkNode)
7174 // In `deltanet`, `removeNode` is no-op, but we should disconnect.
7275 // Actually `Link` overwrites.
7373-7676+7477 // Create Replicator
7578 delta := level - (info.level + 1)
7676-7979+7780 repLevel := info.level + 1
7878-8181+7982 // Link Rep.0 to Source (info.node, info.port)
8083 rep := net.NewReplicator(repLevel, []int{delta})
8181- net.Link(rep, 0, info.node, info.port)
8282- 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)
8383-8484+ net.LinkAt(rep, 0, info.node, info.port, depth)
8585+ // 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)
8686+8487 // Update info to point to Rep
8588 info.node = rep
8689 info.port = 0 // Rep.0 is the input
8787-9090+8891 // Return Rep.1
8992 return rep, 1
9090-9393+9194 } else {
9295 // Should not happen if logic is correct (either Eraser or Replicator)
9396 panic(fmt.Sprintf("Unexpected node type on variable binding: %v", linkNode.Type()))
9497 }
9595-9898+9699 } else {
97100 // Free variable
98101 // Create Var node
···101104 // "Create free variable node... Create a replicator fan-in... link... return rep.1"
102105 // Level 0 for free vars.
103106 // Debug: record replicator parameters for free var
104104- fmt.Printf("ToDeltaNet: Free var '%s' at level=%d -> Rep(level=%d, deltas=%v)\n", t.Name, level, 0, []int{level - 1})
107107+ // fmt.Printf("ToDeltaNet: Free var '%s' at level=%d -> Rep(level=%d, deltas=%v)\n", t.Name, level, 0, []int{level - 1})
105108 rep := net.NewReplicator(0, []int{level - 1}) // level - (0 + 1) ?
106106- net.Link(rep, 0, v, 0)
107107-109109+ net.LinkAt(rep, 0, v, 0, depth)
110110+108111 // Register in vars so we can share it if used again
109112 vars[t.Name] = &varInfo{node: rep, port: 0, level: 0}
110110-113113+111114 return rep, 1
112115 }
113113-116116+114117 case Abs:
115118 // Create Fan
116119 fan := net.NewFan()
117120 // fan.0 is Result (returned)
118121 // fan.1 is Body
119122 // fan.2 is Var
120120-123123+121124 // Create Eraser for Var initially
122125 era := net.NewEraser()
123123- net.Link(era, 0, fan, 2)
124124-126126+ net.LinkAt(era, 0, fan, 2, depth)
127127+125128 // Register var
126129 // Save old var info if shadowing
127130 oldVar := vars[t.Arg]
128131 vars[t.Arg] = &varInfo{node: fan, port: 2, level: level}
129129-132132+130133 // Build Body
131131- bodyNode, bodyPort := buildTerm(t.Body, net, vars, level)
132132- net.Link(fan, 1, bodyNode, bodyPort)
133133-134134+ bodyNode, bodyPort := buildTerm(t.Body, net, vars, level, depth)
135135+ net.LinkAt(fan, 1, bodyNode, bodyPort, depth)
136136+134137 // Restore var
135138 if oldVar != nil {
136139 vars[t.Arg] = oldVar
137140 } else {
138141 delete(vars, t.Arg)
139142 }
140140-143143+141144 return fan, 0
142142-145145+143146 case App:
144147 // Create Fan
145148 fan := net.NewFan()
146149 // fan.0 is Function
147150 // fan.1 is Result (returned)
148151 // fan.2 is Argument
149149-152152+150153 // Build Function
151151- funNode, funPort := buildTerm(t.Fun, net, vars, level)
152152- net.Link(fan, 0, funNode, funPort)
153153-154154+ funNode, funPort := buildTerm(t.Fun, net, vars, level, depth)
155155+ net.LinkAt(fan, 0, funNode, funPort, depth)
156156+154157 // Build Argument (level + 1)
155155- argNode, argPort := buildTerm(t.Arg, net, vars, level+1)
156156- net.Link(fan, 2, argNode, argPort)
157157-158158+ argNode, argPort := buildTerm(t.Arg, net, vars, level+1, depth+1)
159159+ net.LinkAt(fan, 2, argNode, argPort, depth+1)
160160+158161 return fan, 1
159159-162162+160163 case Let:
161164 // Should have been desugared by parser, but if we encounter it:
162165 // let x = Val in Body -> (\x. Body) Val
···164167 Fun: Abs{Arg: t.Name, Body: t.Body},
165168 Arg: t.Val,
166169 }
167167- return buildTerm(desugared, net, vars, level)
168168-170170+ return buildTerm(desugared, net, vars, level, depth)
171171+169172 default:
170173 panic("Unknown term type")
171174 }
···179182 // We traverse from the root.
180183 // We need to track visited nodes to handle loops (though lambda terms shouldn't have loops unless we have recursion combinators).
181184 // But we also need to track bound variables.
182182-185185+183186 // Map from (NodeID, Port) to Variable Name for bound variables.
184187 // When we enter Abs at 0, we assign a name to Abs.2.
185185-188188+186189 bindings := make(map[uint64]string) // Key: Node ID of the binder (Fan), Value: Name
187187-190190+188191 // We need a name generator
189192 nameGen := 0
190193 nextName := func() string {
···192195 nameGen++
193196 return name
194197 }
195195-196196- return readTerm(net, rootNode, rootPort, bindings, nextName)
198198+199199+ visited := make(map[string]bool)
200200+ return readTerm(net, rootNode, rootPort, bindings, nextName, visited)
197201}
198202199199-func readTerm(net *deltanet.Network, node deltanet.Node, port int, bindings map[uint64]string, nextName func() string) Term {
203203+func readTerm(net *deltanet.Network, node deltanet.Node, port int, bindings map[uint64]string, nextName func() string, visited map[string]bool) Term {
200204 if node == nil {
201205 return Var{Name: "<nil>"}
202206 }
203203-207207+208208+ // Handle Phase 2 rotation
209209+ // Phys 0 -> Log 1
210210+ // Phys 1 -> Log 2
211211+ // Phys 2 -> Log 0
212212+ logicalPort := port
213213+ if net.Phase() == 2 && node.Type() == deltanet.NodeTypeFan {
214214+ switch port {
215215+ case 0:
216216+ logicalPort = 1
217217+ case 1:
218218+ logicalPort = 2
219219+ case 2:
220220+ logicalPort = 0
221221+ }
222222+ }
223223+224224+ key := fmt.Sprintf("%d:%d", node.ID(), port)
225225+ if visited[key] {
226226+ if deltaDebug {
227227+ fmt.Printf("readTerm: detected revisit %s -> returning <loop>\n", key)
228228+ }
229229+ return Var{Name: "<loop>"}
230230+ }
231231+ visited[key] = true
232232+ defer func() { delete(visited, key) }()
233233+234234+ if deltaDebug {
235235+ fmt.Printf("readTerm: nodeType=%v id=%d port=%d phase=%d\n", node.Type(), node.ID(), port, net.Phase())
236236+ }
237237+ if deltaDebug && node.Type() == deltanet.NodeTypeFan {
238238+ // Print where each physical port links to (nodeID:port)
239239+ for i := 0; i < 3; i++ {
240240+ n, p := net.GetLink(node, i)
241241+ if n != nil {
242242+ fmt.Printf(" Fan link[%d] -> %v id=%d port=%d\n", i, n.Type(), n.ID(), p)
243243+ } else {
244244+ fmt.Printf(" Fan link[%d] -> <nil>\n", i)
245245+ }
246246+ }
247247+ }
248248+204249 switch node.Type() {
205250 case deltanet.NodeTypeFan:
206206- if port == 0 {
251251+ if logicalPort == 0 {
207252 // Entering Abs at Result -> Abs
253253+ // Or Entering App at Fun -> App (Wait, App Result is 1)
254254+255255+ // If we enter at Logical 0:
256256+ // For Abs: 0 is Result. We are reading the Abs term.
257257+ // For App: 0 is Fun. We are reading the Function part of an App?
258258+ // No, if we are reading a term, we enter at its "Output" port.
259259+ // Abs Output is 0.
260260+ // App Output is 1.
261261+262262+ // So if LogicalPort == 0, it MUST be Abs.
208263 name := nextName()
209264 bindings[node.ID()] = name
210210-211211- body := readTerm(net, getLinkNode(net, node, 1), getLinkPort(net, node, 1), bindings, nextName)
265265+266266+ // Body is at Logical 1
267267+ // We need to find the PHYSICAL port for Logical 1.
268268+ // If Phase 2: Log 1 -> Phys 0.
269269+ // If Phase 1: Log 1 -> Phys 1.
270270+ bodyPortIdx := 1
271271+ if net.Phase() == 2 {
272272+ bodyPortIdx = 0
273273+ }
274274+275275+ body := readTerm(net, getLinkNode(net, node, bodyPortIdx), getLinkPort(net, node, bodyPortIdx), bindings, nextName, visited)
212276 return Abs{Arg: name, Body: body}
213213- } else if port == 1 {
214214- // Entering App at Result -> App
215215- fun := readTerm(net, getLinkNode(net, node, 0), getLinkPort(net, node, 0), bindings, nextName)
216216- arg := readTerm(net, getLinkNode(net, node, 2), getLinkPort(net, node, 2), bindings, nextName)
277277+278278+ } else if logicalPort == 1 {
279279+ // Entering at Logical 1.
280280+ // Abs: 1 is Body. (We are reading body? No, we enter at Output).
281281+ // App: 1 is Result. We are reading the App term.
282282+283283+ // So if LogicalPort == 1, it MUST be App.
284284+285285+ // Fun is at Logical 0.
286286+ // Arg is at Logical 2.
287287+288288+ funPortIdx := 0
289289+ argPortIdx := 2
290290+ if net.Phase() == 2 {
291291+ funPortIdx = 2
292292+ argPortIdx = 1
293293+ }
294294+295295+ funNode := getLinkNode(net, node, funPortIdx)
296296+ funP := getLinkPort(net, node, funPortIdx)
297297+ argNode := getLinkNode(net, node, argPortIdx)
298298+ argP := getLinkPort(net, node, argPortIdx)
299299+ if deltaDebug {
300300+ fmt.Printf(" App at Fan id=%d funLink=(%v id=%d port=%d) argLink=(%v id=%d port=%d)\n", node.ID(), funNode.Type(), funNode.ID(), funP, argNode.Type(), argNode.ID(), argP)
301301+ }
302302+ fun := readTerm(net, funNode, funP, bindings, nextName, visited)
303303+ arg := readTerm(net, argNode, argP, bindings, nextName, visited)
217304 return App{Fun: fun, Arg: arg}
305305+218306 } else {
219219- // Entering at 2?
220220- // This means we are traversing UP a variable binding?
221221- // Should not happen in normal term traversal unless we are debugging.
307307+ // Entering at Logical 2.
308308+ // Abs: 2 is Var.
309309+ // App: 2 is Arg.
310310+ // This means we are traversing UP a variable binding or argument?
311311+ // Should not happen when reading a term from root.
312312+ // Unless we are tracing a variable.
313313+ if name, ok := bindings[node.ID()]; ok {
314314+ return Var{Name: name}
315315+ }
222316 return Var{Name: "<binding>"}
223317 }
224224-318318+225319 case deltanet.NodeTypeReplicator:
226320 // We entered a Replicator.
227321 // If we entered at Aux port (>= 1), we are reading a variable usage.
228322 // We need to trace back to the source (Port 0).
323323+ if deltaDebug {
324324+ fmt.Printf(" Replicator(id=%d) Deltas=%v Level=%d entered at port=%d\n", node.ID(), node.Deltas(), node.Level(), port)
325325+ }
326326+229327 if port > 0 {
230328 sourceNode := getLinkNode(net, node, 0)
231329 sourcePort := getLinkPort(net, node, 0)
···233331 // Trace back until we hit a Fan.2 (Binder) or Var (Free)
234332 // If the source is a Fan (Abs/App), traceVariable will delegate
235333 // to readTerm to reconstruct the full subterm.
236236- return traceVariable(net, sourceNode, sourcePort, bindings, nextName)
334334+ return traceVariable(net, sourceNode, sourcePort, bindings, nextName, visited)
237335 } else {
238336 // Entered at 0?
239337 // Reading the value being shared?
···261359 // But `Rep` is connected to `x`'s binder.
262360 // So `M` connects to `Rep` aux port.
263361 // So we enter at aux.
264264-362362+265363 // What if `M` is `\y. y` and it is shared?
266364 // `Abs` (M) connects to `Rep.0`.
267365 // `Rep` aux ports connect to usages.
···274372 // If we read `M`, we start at `Abs.0`.
275373 // We don't start at `Rep`.
276374 // Unless `M` is *defined* as `Rep`? No.
277277-375375+278376 // Ah, `FromDeltaNet` takes `rootNode, rootPort`.
279377 // This is the "output" of the term.
280378 // If the term is `\x. x`, output is `Abs.0`.
281379 // If the term is `x`, output is `Rep` aux port (or `Abs.2`).
282380 // If the term is `M N`, output is `App.1`.
283283-381381+284382 // So we should never enter `Rep` at 0 during normal read-back of a term,
285383 // unless the term *itself* is being shared and we are reading the *source*?
286384 // But `rootNode` is the *result* of the reduction.
···288386 // If the result is `x` (free var), and it's shared?
289387 // `Var` -> `Rep.0`. `Rep.1` -> Output.
290388 // So Output is `Rep.1`. We enter at 1.
291291-389389+292390 // So entering at 0 should be rare/impossible for "Result".
391391+ if deltaDebug {
392392+ fmt.Printf(" Replicator(id=%d) entered at 0, returning <rep-0>\n", node.ID())
393393+ }
293394 return Var{Name: "<rep-0>"}
294395 }
295295-396396+296397 case deltanet.NodeTypeVar:
297398 // Free variable or wire
298399 // If it's a named var, return it.
···305406 // I can't modify `deltanet` package (user reverted).
306407 // So I can't store names in `Var` nodes.
307408 // I'll return "<free>" or generate a name.
409409+ if deltaDebug {
410410+ fmt.Printf(" Var node encountered (id=%d) -> <free>\n", node.ID())
411411+ }
308412 return Var{Name: "<free>"}
309309-413413+310414 case deltanet.NodeTypeEraser:
311415 return Var{Name: "<erased>"}
312312-416416+313417 default:
314418 return Var{Name: fmt.Sprintf("<? %v>", node.Type())}
315419 }
316420}
317421318318-func traceVariable(net *deltanet.Network, node deltanet.Node, port int, bindings map[uint64]string, nextName func() string) Term {
422422+func traceVariable(net *deltanet.Network, node deltanet.Node, port int, bindings map[uint64]string, nextName func() string, visited map[string]bool) Term {
319423 // Follow wires up through Replicators (entering at 0, leaving at 0?)
320424 // No, `Rep.0` connects to Source.
321425 // So if we are at `Rep`, we go to `Rep.0`'s link.
322322-426426+323427 currNode := node
324428 currPort := port
325325-429429+326430 for {
327431 if currNode == nil {
328432 return Var{Name: "<nil-trace>"}
329433 }
330434435435+ if deltaDebug {
436436+ fmt.Printf("traceVariable: at nodeType=%v id=%d port=%d phase=%d\n", currNode.Type(), currNode.ID(), currPort, net.Phase())
437437+ }
438438+331439 switch currNode.Type() {
332440 case deltanet.NodeTypeFan:
441441+ // Handle Rotation
442442+ logicalPort := currPort
443443+ if net.Phase() == 2 {
444444+ switch currPort {
445445+ case 0:
446446+ logicalPort = 1
447447+ case 1:
448448+ logicalPort = 2
449449+ case 2:
450450+ logicalPort = 0
451451+ }
452452+ }
453453+333454 // Hit a Fan.
334334- // If port 2, it's a binder.
335335- if currPort == 2 {
455455+ // If Logical 2, it's a binder (Abs Var).
456456+ if logicalPort == 2 {
336457 if name, ok := bindings[currNode.ID()]; ok {
337458 return Var{Name: name}
338459 }
339460 return Var{Name: "<unbound-fan>"}
340461 }
341341- // If port 0 or 1, reconstruct the full term (Abs or App)
342342- return readTerm(net, currNode, currPort, bindings, nextName)
462462+ // If Logical 0 or 1, reconstruct the full term (Abs or App)
463463+ // readTerm handles rotation internally based on port passed.
464464+ return readTerm(net, currNode, currPort, bindings, nextName, visited)
343465344466 case deltanet.NodeTypeReplicator:
345467 // Continue trace from Rep.0
346468 if currPort == 0 {
347469 return Var{Name: "<rep-trace-0>"}
470470+ }
471471+ if deltaDebug {
472472+ fmt.Printf(" traceVariable: traversing Replicator id=%d -> follow .0\n", currNode.ID())
348473 }
349474 nextNode, nextPort := net.GetLink(currNode, 0)
350475 currNode = nextNode
+64
pkg/lambda/translate_test.go
···11+package lambda
22+33+import (
44+ "github.com/vic/godnet/pkg/deltanet"
55+ "os"
66+ "testing"
77+)
88+99+// helper: roundtrip a term through ToDeltaNet -> FromDeltaNet (no reduction)
1010+func roundtrip(t *testing.T, term Term) Term {
1111+ net := deltanet.NewNetwork()
1212+ rootNode, rootPort := ToDeltaNet(term, net)
1313+ // ensure deterministic tiny timeout for network workers
1414+ // Ensure any pending interactions are processed (no-op if none)
1515+ net.ReduceAll()
1616+ res := FromDeltaNet(net, rootNode, rootPort)
1717+ return res
1818+}
1919+2020+func TestRoundtripIdentity(t *testing.T) {
2121+ orig := Abs{Arg: "x", Body: Var{Name: "x"}}
2222+ res := roundtrip(t, orig)
2323+ if _, ok := res.(Abs); !ok {
2424+ t.Fatalf("Identity roundtrip: expected Abs, got %T: %#v", res, res)
2525+ }
2626+}
2727+2828+func TestRoundtripNestedApp(t *testing.T) {
2929+ // (x. (y. (z. ((x y) z))))
3030+ orig := Abs{Arg: "x", Body: Abs{Arg: "y", Body: Abs{Arg: "z", Body: App{Fun: App{Fun: Var{Name: "x"}, Arg: Var{Name: "y"}}, Arg: Var{Name: "z"}}}}}
3131+ res := roundtrip(t, orig)
3232+ // We expect an Abs at top-level
3333+ if _, ok := res.(Abs); !ok {
3434+ t.Fatalf("NestedApp roundtrip: expected Abs, got %T: %#v", res, res)
3535+ }
3636+}
3737+3838+func TestRoundtripFreeVar(t *testing.T) {
3939+ orig := Var{Name: "a"}
4040+ res := roundtrip(t, orig)
4141+ // free variables lose name (deltanet doesn't store names) but structure should be Var
4242+ if _, ok := res.(Var); !ok {
4343+ t.Fatalf("FreeVar roundtrip: expected Var, got %T: %#v", res, res)
4444+ }
4545+}
4646+4747+func TestRoundtripSharedVar(t *testing.T) {
4848+ //
4949+ // (. (f f)) where f is bound and shared
5050+ orig := Abs{Arg: "f", Body: App{Fun: Var{Name: "f"}, Arg: Var{Name: "f"}}}
5151+ res := roundtrip(t, orig)
5252+ if _, ok := res.(Abs); !ok {
5353+ t.Fatalf("SharedVar roundtrip: expected Abs, got %T: %#v", res, res)
5454+ }
5555+}
5656+5757+func TestTranslatorDiagnostics(t *testing.T) {
5858+ // This ensures DELTA_DEBUG can be toggled without breaking behavior.
5959+ old := os.Getenv("DELTA_DEBUG")
6060+ defer os.Setenv("DELTA_DEBUG", old)
6161+ os.Setenv("DELTA_DEBUG", "1")
6262+ orig := Abs{Arg: "x", Body: Var{Name: "x"}}
6363+ _ = roundtrip(t, orig)
6464+}