From 32b9aa5c810712ad97f5b0a793823683ea42cd75 Mon Sep 17 00:00:00 2001 From: M Date: Sun, 30 Aug 2026 21:44:35 +0200 Subject: [PATCH] Aim a path token's render edge at what it renders --- README.md | 2 +- reference.go | 38 +++++++++++++++++++++++++++++++------- render.go | 4 +--- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 749039e..2b60e93 100644 --- a/README.md +++ b/README.md @@ -495,7 +495,7 @@ fakes.go Fakes, New, List, options, seeding node.go the node model and JSON -> node compilation render.go Fake and the recursive renderer (choices, format strings, paths, bound draws) template.go the {token} grammar: scanning, function and path tokens, validation -reference.go {..path} binding across the tree, and cycle detection +reference.go {..path} binding across the tree, the render graph, and the walks over it builtins.go the {name()} function registry and its implementations calc.go the {calc()} arithmetic evaluator: parser, eval, validation data.go data loading: folders/files -> namespace tree, multi-path merge diff --git a/reference.go b/reference.go index eaa7e76..ba5730c 100644 --- a/reference.go +++ b/reference.go @@ -59,12 +59,15 @@ func checkBoundLevelsHeld(root map[string]node) error { for _, head := range heads { held := map[node]bool{} cover(t.fields[head], held) + // One seen set across the edges: a node that cannot reach the level + // cannot reach it by another route either, so it is walked once here. + seen := map[node]bool{} for _, e := range renderEdges(t) { if splitArm(e.label).key == head { continue // a path token reading this level, which is the one route allowed } - if renders(e.to, held, map[node]bool{}) { - return fmt.Errorf("%s: {%s} renders a level that {%s} reads a path into; name the fields you want instead", path, e.label, t.bound[head]) + if renders(e.to, held, seen) { + return fmt.Errorf("%s: {%s} renders %q, which {%s} reads a path into; name the fields you want instead", path, e.label, head, t.bound[head]) } } } @@ -258,11 +261,13 @@ func renderEdges(n node) []renderEdge { case *template: var es []renderEdge for _, name := range append(fieldTokens(n.format), calcOperands(n.format)...) { - // A path token renders its head, so the edge is to the head — over- - // approximating the sub-field it descends to, which is what keeps the - // cycle walk conservative rather than blind. - if c, ok := n.fields[splitArm(name).key]; ok { - es = append(es, renderEdge{c, name}) + a := splitArm(name) + c, ok := n.fields[a.key] + if !ok { + continue + } + for _, leaf := range pathLeaves(c, a.tail) { + es = append(es, renderEdge{leaf, name}) } } return es @@ -271,6 +276,25 @@ func renderEdges(n node) []renderEdge { } } +// pathLeaves lists what a token's dotted tail renders. A path draws the levels it +// passes through but renders only what it lands on, so the leaf is the edge — a +// bare token, whose tail is empty, lands on the field itself. A choice on the way +// contributes every variant, since any of them may be the one drawn. checkPath has +// already proved the tail resolves in every variant, so the walk drops nothing. +func pathLeaves(n node, tail []string) []node { + if len(tail) == 0 { + return []node{n} + } + if c, ok := n.(*choice); ok { + var out []node + for _, it := range c.items { + out = append(out, pathLeaves(it, tail)...) + } + return out + } + return pathLeaves(child(n, tail[0]), tail[1:]) +} + // checkNoCycles rejects a reference cycle: a node whose rendering can reach itself // — directly, mutually, or through a chain — never terminates, so it must fail at // New rather than stack-overflow at render. It is a depth-first walk of the render diff --git a/render.go b/render.go index 74286d9..cb307e8 100644 --- a/render.go +++ b/render.go @@ -190,9 +190,7 @@ func resolve(s *session, arms []arm, t *template, bound *draws) string { bound.variant[a.key] = n } // Hold the draw at every level passed through, so two paths sharing a prefix - // share it. The leaf needs no hold: no other token can name it, since - // checkNoOverlap rejects a field or calc spelling and checkNotBound a - // reference one. + // share it. for i, seg := range a.tail { if i < len(a.steps) { held, drew := bound.variant[a.steps[i]]