diff --git a/AGENTS.md b/AGENTS.md index 691412c..439b0ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,3 +110,4 @@ In [docs/decisions.md](docs/decisions.md): - The US given names come from a mirror of the SSA file - `List` advertises direct descents only - Each entry point to `drawCheck.hasRead` says what its caller gets +- The four path walks are four loops diff --git a/docs/decisions.md b/docs/decisions.md index 31f3626..faf3af2 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -683,3 +683,12 @@ the direct chain is the one a reader can predict from the tables' parents. 2026-09-25, larv-review; approved 2026-09-25 by lilleman. Goal 2: a reader lands on `readsPath` or `splitsDraws` from its caller, so each doc states where that walk stops, though `hasRead` states the same stop rule. Valid while the two differ in where they stop. + +## The four path walks are four loops + +2026-09-28, larv-review on systems-architect's finding; awaiting approval. Goals 2 and +11: `pathCheck`, `pathCover`, `pathProbe` and `pathDraw` each loop over `stepInto` and +`table.route` themselves. One walk calling them through an interface or a type parameter +leaks the walker and the pins it holds, an allocation per reference read. Valid while Go's +escape analysis treats a call through an interface or a type parameter as leaking its +receiver. diff --git a/family.go b/family.go index af1ac51..0051deb 100644 --- a/family.go +++ b/family.go @@ -32,7 +32,7 @@ func tableReadOf(head node, a arm, leaf node) *tableRead { if !isTable { return nil } - probe := &probeWalk{} + probe := &pathProbe{drawn: map[*table]bool{}} _, _ = probe.walk(t, a.tail) tr := &tableRead{headTable: t, pins: probe.pins, drawn: probe.drawn} written := a.spelling[:len(a.spelling)-len(joinSegments(a.tail))] diff --git a/graph.go b/graph.go index c128b0b..25c40ce 100644 --- a/graph.go +++ b/graph.go @@ -164,7 +164,7 @@ func renderEdges(n node) []renderEdge { // contributes every variant, since any of them may be the one drawn. checkPath has // already proved the tail resolves in every variant. func pathLeaves(n node, tail []string) []node { - w := &proveWalk{tail: tail} + w := &pathCheck{tail: tail} _, _ = w.walk(n, tail) return w.leaves } diff --git a/hold.go b/hold.go index 9c20384..ec51a05 100644 --- a/hold.go +++ b/hold.go @@ -44,7 +44,7 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw if len(a.tail) > 0 { sc.pins = &d.pins } - r := renderLeaf(s, drawPath(t.head(a.key), a.tail, a.key, &renderWalk{s: s, pins: &d.pins, held: d, a: &a}), sc) + r := renderLeaf(s, drawPath(t.head(a.key), a.tail, a.key, &pathDraw{s: s, pins: &d.pins, held: d, a: &a}), sc) if d.value == nil { d.value = map[string]draw{} } diff --git a/holdfence.go b/holdfence.go index 27b35c4..cbbd04f 100644 --- a/holdfence.go +++ b/holdfence.go @@ -100,7 +100,7 @@ func operandReader(t *template, head string) string { // coverPath collects what holding one path pins: every choice level the path // passes through, whole, and the leaf it renders. func coverPath(n node, tail []string, into map[node]bool) { - _, _ = (&coverWalk{into: into}).walk(n, tail) + _, _ = (&pathCover{into: into}).walk(n, tail) } // cover collects a level and everything contained in it. A fixed string outside a diff --git a/path.go b/path.go index 87a4aee..02ab643 100644 --- a/path.go +++ b/path.go @@ -185,18 +185,18 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) { return r, nil } -// proveWalk proves a path resolves whichever way the draws go: every variant of a +// pathCheck proves a path resolves whichever way the draws go: every variant of a // choice carries the rest of it, and is walked, a selector names a row inside the // rows selected before it, and no level read carries a repeat or a drawGroup. It // collects every leaf the path may render; level and tail name a refused level. -type proveWalk struct { +type pathCheck struct { pins pinSet level string tail []string leaves []node } -func (w *proveWalk) walk(n node, tail []string) (node, error) { +func (w *pathCheck) walk(n node, tail []string) (node, error) { for descended := false; len(tail) > 0 || descended; { var err error switch x := n.(type) { @@ -228,7 +228,7 @@ func (w *proveWalk) walk(n node, tail []string) (node, error) { return n, nil } -func (w *proveWalk) walkEvery(c *choice, tail []string) (node, error) { +func (w *pathCheck) walkEvery(c *choice, tail []string) (node, error) { if err := carriedByAll(c, tail); err != nil { return nil, err } @@ -242,7 +242,7 @@ func (w *proveWalk) walkEvery(c *choice, tail []string) (node, error) { return last, nil } -func (w *proveWalk) enter(t *template, rest []string) error { +func (w *pathCheck) enter(t *template, rest []string) error { name := join(w.level, strings.Join(w.tail[:len(w.tail)-len(rest)], ".")) switch { case t.repeat > 1: @@ -253,11 +253,10 @@ func (w *proveWalk) enter(t *template, rest []string) error { return nil } -// coverWalk collects into what holding a path pins: every choice level it passes -// through, whole, and the leaf it renders. It stops at a choice and reads no row. -type coverWalk struct{ into map[node]bool } +// pathCover is coverPath's walk: it stops at a choice and reads no row. +type pathCover struct{ into map[node]bool } -func (w *coverWalk) walk(n node, tail []string) (node, error) { +func (w *pathCover) walk(n node, tail []string) (node, error) { for descended := false; len(tail) > 0 || descended; { var err error switch x := n.(type) { @@ -281,15 +280,15 @@ func (w *coverWalk) walk(n node, tail []string) (node, error) { return n, nil } -// probeWalk proves a path as proveWalk does, walking one variant of a choice, so a +// pathProbe proves a path resolves, walking one variant of a choice, so a // path that resolves resolves whichever variants and rows are drawn. It draws // nothing, marking in drawn the tables a draw would pin. -type probeWalk struct { +type pathProbe struct { pins pinSet drawn map[*table]bool } -func (w *probeWalk) walk(n node, tail []string) (node, error) { +func (w *pathProbe) walk(n node, tail []string) (node, error) { for descended := false; len(tail) > 0 || descended; { var err error switch x := n.(type) { @@ -318,26 +317,23 @@ func (w *probeWalk) walk(n node, tail []string) (node, error) { return n, nil } -func (w *probeWalk) readRow(t *table, r tableRoute) error { +func (w *pathProbe) readRow(t *table, r tableRoute) error { if r.sel != "" { return w.pins.selectRow(t, r.sel) } if !r.draw { return nil } - if w.drawn == nil { - w.drawn = map[*table]bool{} - } for stop := w.pins.nearestPinned(t); t != stop; t = t.parentT { w.drawn[t] = true } return nil } -// renderWalk draws the rows and variants a proved path reads, pinning them in pins. +// pathDraw draws the rows and variants a proved path reads, pinning them in pins. // For a held read, held keeps the variant drawn at each level of a, so paths // sharing a prefix share it. -type renderWalk struct { +type pathDraw struct { s *session pins *pinSet held *hold @@ -346,7 +342,7 @@ type renderWalk struct { // drawPath walks w over a path proved first, by a probe or by New's fences, so the // walk cannot fail. head is what n is reached by, for the panic. -func drawPath(n node, tail []string, head string, w *renderWalk) node { +func drawPath(n node, tail []string, head string, w *pathDraw) node { leaf, err := w.walk(n, tail) if err != nil { panic(fmt.Sprintf("fejkdata: %s: %v; the path should have been proved before it was drawn", join(head, joinSegments(tail)), err)) @@ -354,7 +350,7 @@ func drawPath(n node, tail []string, head string, w *renderWalk) node { return leaf } -func (w *renderWalk) walk(n node, tail []string) (node, error) { +func (w *pathDraw) walk(n node, tail []string) (node, error) { for descended := false; len(tail) > 0 || descended; { var err error switch x := n.(type) { @@ -387,7 +383,7 @@ func (w *renderWalk) walk(n node, tail []string) (node, error) { // variant is the variant of c the walk continues into: the one held for this level // of the read, drawn once, where the walk holds its draws; else one drawn afresh. -func (w *renderWalk) variant(c *choice, rest []string) node { +func (w *pathDraw) variant(c *choice, rest []string) node { if w.held == nil { return pick(w.s, c) } @@ -453,9 +449,9 @@ func unreachableInChoice(c *choice, want string) error { return fmt.Errorf("not every variant of this %d-way choice carries %q; all carry %v", len(c.items), want, offered) } -// checkPath proves a dotted tail as proveWalk does, so a path that validates here +// checkPath proves a dotted tail as pathCheck does, so a path that validates here // resolves on every render, and a typo is a New-time error. func checkPath(n node, tail []string, level string) error { - _, err := (&proveWalk{level: level, tail: tail}).walk(n, tail) + _, err := (&pathCheck{level: level, tail: tail}).walk(n, tail) return err } diff --git a/render.go b/render.go index bc25236..d5a1b20 100644 --- a/render.go +++ b/render.go @@ -44,10 +44,10 @@ func (f *Generator) Fake(path string) (string, error) { // every call. func descend(s *session, root node, segments []string, sc renderScope) (node, error) { // Walked once without drawing first, so a path that fails moves no seeded stream. - if _, err := (&probeWalk{}).walk(root, segments); err != nil { + if _, err := (&pathProbe{drawn: map[*table]bool{}}).walk(root, segments); err != nil { return nil, err } - return drawPath(root, segments, "", &renderWalk{s: s, pins: &sc.hold().pins}), nil + return drawPath(root, segments, "", &pathDraw{s: s, pins: &sc.hold().pins}), nil } // render evaluates a compiled node to a string. compile validates every node up