From bb70688f1d558e9208547260dc4b37b04b63ca2b Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 29 Sep 2026 11:20:09 +0200 Subject: [PATCH] Hold each name in one formatOps map, beside its first reader and path --- format.go | 34 ++++++++++++++++------------------ holdfence.go | 31 ++++++++++++++++--------------- node.go | 2 +- render.go | 2 +- todo.md | 3 --- 5 files changed, 34 insertions(+), 38 deletions(-) diff --git a/format.go b/format.go index 7cac363..2d25a8a 100644 --- a/format.go +++ b/format.go @@ -378,38 +378,36 @@ type op struct { } // formatOps is a compiled format: its ops, the size of its literal text, and the -// names it holds. pathHeads maps each head a path starts from to the first such path; -// held is every such head plus the fields an operand reads; holder maps each held -// name to the first reader holding it, for error messages. +// names it holds: every head a path starts from, plus the fields an operand reads. type formatOps struct { ops []op grow int - pathHeads map[string]string - held map[string]bool - holder map[string]string + held map[string]heldName heldLocal bool // some held name is not a reference path, so expand makes a hold } +// heldName is how a format first reaches a held name: the reader holding it, for +// error messages, and the first path starting from it, "" where none does. +type heldName struct { + holder string + path string +} + func (c *formatOps) holdName(a arm, label string) { if c.held == nil { - c.held = map[string]bool{} - c.holder = map[string]string{} + c.held = map[string]heldName{} } - c.held[a.head] = true if !isRef(a.head) || len(a.tail) == 0 { c.heldLocal = true } - if _, named := c.holder[a.head]; !named { - c.holder[a.head] = label + h, named := c.held[a.head] + if !named { + h.holder = label } - if len(a.tail) > 0 { - if c.pathHeads == nil { - c.pathHeads = map[string]string{} - } - if _, named := c.pathHeads[a.head]; !named { - c.pathHeads[a.head] = a.spelling - } + if len(a.tail) > 0 && h.path == "" { + h.path = a.spelling } + c.held[a.head] = h } func (c *formatOps) function(tok formatToken, refs map[string]refBinding) { diff --git a/holdfence.go b/holdfence.go index 534e547..ca30e1a 100644 --- a/holdfence.go +++ b/holdfence.go @@ -15,9 +15,9 @@ func heldCheck(path string, n node) error { if !ok || len(t.compiled.held) == 0 { return nil } - readers := pathHeadReaders(t.compiled.ops, t.compiled.pathHeads) + readers := pathHeadReaders(t.compiled) for _, name := range heldNames(t) { - if _, isPath := t.compiled.pathHeads[name]; isPath && isRef(name) { + if t.compiled.held[name].path != "" && isRef(name) { continue // held for the render: drawFence compares every read of it, by path, across the render and its groups } if err := checkNameHeld(t, name, readers); err != nil { @@ -36,8 +36,8 @@ func heldNames(t *template) []string { names = append(names, name) } sort.Slice(names, func(i, j int) bool { - _, pi := t.compiled.pathHeads[names[i]] - _, pj := t.compiled.pathHeads[names[j]] + pi := t.compiled.held[names[i]].path != "" + pj := t.compiled.held[names[j]].path != "" if pi != pj { return !pi } @@ -51,7 +51,7 @@ func heldNames(t *template) []string { // produces. func heldNodes(t *template, name string, readers []reader) map[node]bool { held := map[node]bool{} - if _, isPath := t.compiled.pathHeads[name]; !isPath { + if t.compiled.held[name].path == "" { operandDraw(t.head(name), held) return held } @@ -71,13 +71,13 @@ func checkNameHeld(t *template, name string, readers []reader) error { if len(held) == 0 { return nil // a fixed head holds nothing to reach } - reader, isPath := t.compiled.pathHeads[name] + reader := t.compiled.held[name].path seen := map[node]bool{} for _, e := range renderEdges(t) { if e.read.head == name || !renders(e.to, held, seen) { continue } - if isPath { + if reader != "" { return fmt.Errorf("%s renders %q, which {%s} reads a path into; name the fields you want instead", e.reached(), name, reader) } return fmt.Errorf("%s renders %q, which a {%s()} also reads; reach it one way so it is drawn once", e.reached(), name, operandReader(t, name)) @@ -175,8 +175,8 @@ func renders(n node, want, seen map[node]bool) bool { // disagree. Reads are compared in sorted order, so which pair is reported does not // depend on where the tokens sit. // docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held -func checkNoOverlap(ops []op, pathHeads map[string]string) error { - names := pathHeadReaders(ops, pathHeads) +func checkNoOverlap(c formatOps) error { + names := pathHeadReaders(c) // Stable over one format-order scan, so two readers of one name (a token and a // calc operand both naming "p") are reported as the format writes them. sort.SliceStable(names, func(i, j int) bool { return names[i].a.path < names[j].a.path }) @@ -199,16 +199,16 @@ type reader struct { // pathHeadReaders lists every way a format reaches a sibling path head, in the order the // format writes them. An operand renders its field, so it names a level exactly // as a token does; one scan finds both, which is what puts them in one order. -func pathHeadReaders(ops []op, pathHeads map[string]string) []reader { +func pathHeadReaders(c formatOps) []reader { var names []reader - for _, o := range ops { + for _, o := range c.ops { for _, a := range o.operands { - if _, isPathHead := pathHeads[a.head]; isPathHead && !isRef(a.head) { + if c.held[a.head].path != "" && !isRef(a.head) { names = append(names, reader{a, fmt.Sprintf("%s operand %q", o.fn, a.spelling)}) } } for _, a := range o.arms { - if _, isPathHead := pathHeads[a.head]; isPathHead && !isRef(a.head) { + if c.held[a.head].path != "" && !isRef(a.head) { names = append(names, reader{a, "token {" + a.spelling + "}"}) } } @@ -224,11 +224,12 @@ func checkNoRepeatedRead(c formatOps) error { count := map[string]int{} for _, o := range c.ops { for _, a := range o.arms { - if len(a.tail) > 0 || !c.held[a.head] { + h, held := c.held[a.head] + if len(a.tail) > 0 || !held { continue } if count[a.head]++; count[a.head] > 1 { - return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.spelling, c.holder[a.head], a.head, a.spelling) + return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.spelling, h.holder, a.head, a.spelling) } } } diff --git a/node.go b/node.go index ebb52b2..03542a1 100644 --- a/node.go +++ b/node.go @@ -229,7 +229,7 @@ func (t *template) fixedText() (string, bool) { // compiled reads. func compileFormat(toks []formatToken, refs map[string]refBinding) (formatOps, error) { c := compileOps(toks, refs) - if err := checkNoOverlap(c.ops, c.pathHeads); err != nil { + if err := checkNoOverlap(c); err != nil { return c, err } return c, checkNoRepeatedRead(c) diff --git a/render.go b/render.go index be3abb9..ce34353 100644 --- a/render.go +++ b/render.go @@ -164,7 +164,7 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) readV if sc.set.trace != nil { traceRead(sc.set.trace, t, sc, a) } - if !t.compiled.held[a.head] { + if _, expansionHolds := t.compiled.held[a.head]; !expansionHolds { if len(a.tail) > 0 { panic(internalError("%q reads a path into %q, which the expansion does not hold", a.spelling, a.head)) } diff --git a/todo.md b/todo.md index 793ffdf..aac1e3b 100644 --- a/todo.md +++ b/todo.md @@ -31,9 +31,6 @@ on 2026-09-29 a third time (Navigation 7.0, Locality 5.6, Shape 6.3, Self-suffic This round, in order: -- Replace `formatOps`' `held`, `holder` and `pathHeads` with one map from a held name - to its first reader and the path it starts, if any: four seats traced - `formatOps.holdName` writing four fields at once. - Give `arm` its head as written, set where `splitArm` splits, and build `pathProbe.readRow`'s selector spelling from it: six seats found the slice of `arm.spelling` by `len(joinSegments(tail))` resting on an invariant nothing states.