From 6b9755b3efcd9440c49a767c3275c93f0330d271 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 30 Sep 2026 17:09:50 +0200 Subject: [PATCH] Compile each path's steps, leaves and cover once at link, deleting pathCover, coverPath and pathLeaves --- AGENTS.md | 2 +- docs/decisions.md | 5 +- format.go | 3 + graph.go | 15 +---- holdfence.go | 8 +-- path.go | 158 +++++++++++++++++++++++++++------------------- reads.go | 28 ++++++-- reference.go | 23 +++++++ todo.md | 8 +-- valueproof.go | 2 +- 10 files changed, 149 insertions(+), 103 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f834c41..24ee4ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,4 +116,4 @@ In [docs/decisions.md](docs/decisions.md): - The Swedish ids draw Skatteverket's test series - The US given names come from a mirror of the SSA file - `List` advertises direct descents only -- The four path walks are four loops +- The path walks are separate loops diff --git a/docs/decisions.md b/docs/decisions.md index bc66c4d..2ab0cd1 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -842,11 +842,12 @@ import drops them by name rather than by a rank a regeneration would move. the set of every descent through a chain of five tables is every subsequence of it, and the direct chain is the one a reader can predict from the tables' parents. -## The four path walks are four loops +## The path walks are separate loops 2026-09-28, larv-review on systems-architect's finding; approved 2026-09-28 by lilleman. Re-tested 2026-09-29 by collapsing them into one loop switching on a mode field. -Goals 2 and 11: `pathCheck`, `pathCover`, `pathProbe` and `pathDraw` stay four loops. +Goals 2 and 11: `pathCheck`, `pathProbe` and `pathDraw` stay separate loops; `pathCover` +became the cover `pathCheck` compiles, on 2026-09-30. One loop fails `perf_test.go`: the check's leaves, the probe's selectors and the errors naming a level leak the walker's contents, and Go tracks a struct's fields as one, so the draw's pins, arm and memo leak with them. Held by pointer, a repeat of a reference diff --git a/format.go b/format.go index abc146d..825f52b 100644 --- a/format.go +++ b/format.go @@ -312,6 +312,9 @@ type arm struct { tail []string levels []string // the path at each level the tail passes through, the head first path string // head and tail, the one path every way of writing this read shares + steps []pathStep + leaves []node // every node the path may land on, one per variant it passes + cover node // what holding the path pins: the first choice it passes, else its leaf } func (a arm) isRefPath() bool { diff --git a/graph.go b/graph.go index 4992dc9..88ee6fe 100644 --- a/graph.go +++ b/graph.go @@ -131,11 +131,7 @@ func renderEdges(n node) []renderEdge { case *template: var es []renderEdge add := func(a arm, operand string) { - c := n.head(a.head) - if c == nil { - return - } - for _, leaf := range pathLeaves(c, a.tail) { + for _, leaf := range a.leaves { es = append(es, renderEdge{leaf, a.spelling, a, operand}) } } @@ -165,15 +161,6 @@ func renderEdges(n node) []renderEdge { } } -// pathLeaves lists what a token's dotted tail renders: 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. -func pathLeaves(n node, tail []string) []node { - w := &pathCheck{tail: tail} - _, _ = w.run(n) - return w.leaves -} - type renderCounts map[node]int func (m renderCounts) renderCount(n node) int { diff --git a/holdfence.go b/holdfence.go index a86e9a3..fe19439 100644 --- a/holdfence.go +++ b/holdfence.go @@ -53,7 +53,7 @@ func heldNodes(t *template, name string, readers []reader) map[node]bool { } for _, r := range readers { if r.a.head == name { - coverPath(t.head(name), r.a.tail, held) + cover(r.a.cover, held, false) } } return held @@ -97,12 +97,6 @@ func operandReader(t *template, name string) (call, operand string) { return "", "" } -// coverPath collects what holding one path pins: the first choice level it passes, -// whole, else the leaf it renders; it reads no row. -func coverPath(n node, tail []string, into map[node]bool) { - _, _ = (&pathCover{into: into}).walk(n, tail) -} - // cover collects a level and everything contained in it. A fixed string outside a // choice is left out — it cannot disagree with itself — but inside one each // variant carries its own, so there it counts. diff --git a/path.go b/path.go index 3ce85a2..9659ce8 100644 --- a/path.go +++ b/path.go @@ -186,34 +186,70 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) { return r, nil } +// pathStep is one step of a compiled path, taken at the node the steps before it +// reached; at indexes the tail segment the step consumes. A step holds no pointer, so a +// caller's steps stay on its stack. +type pathStep struct { + kind stepKind + at int + name string // stepField: the field; stepSelect: the selector; stepColumn, stepChild: the column or table stepped to + row int // stepSelect: the row the selector names +} + +type stepKind uint8 + +const ( + stepField stepKind = iota + 1 // into a field of the template at this level, a choice there resolved first + stepSelect // pin the selected row of the table + stepDraw // draw a row of the table inside the pins + stepRow // land on the table's row node + stepColumn // into a column of the table + stepChild // into a table linked to the table +) + // 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 names the head in its errors. +// compiles the steps a draw takes, every leaf the path may render, and as cover the +// first choice it passes, else the leaf; level names the head in its errors. type pathCheck struct { pins pinSet level string tail []string + steps []pathStep leaves []node + cover node } -func (w *pathCheck) run(n node) (node, error) { return w.walk(n, w.tail) } +func (w *pathCheck) run(n node) (node, error) { + leaf, err := w.walk(n, w.tail) + if err == nil && w.cover == nil { + w.cover = leaf + } + return leaf, err +} + +func (w *pathCheck) emit(st pathStep, tail []string) { + st.at = len(w.tail) - len(tail) + w.steps = append(w.steps, st) +} func (w *pathCheck) walk(n node, tail []string) (node, error) { for descended := false; len(tail) > 0 || descended; { var err error switch x := n.(type) { case *choice: + if w.cover == nil { + w.cover = x + } return w.walkEvery(x, tail) case *table: var r tableRoute if r, err = x.route(tail, descended); err != nil { return nil, err } - if r.sel != "" { - if err := w.pins.selectRow(x, r.sel); err != nil { - return nil, err - } + if err := w.tableSteps(x, r, tail); err != nil { + return nil, err } n, tail, descended = r.next, r.rest, r.descends continue @@ -228,23 +264,58 @@ func (w *pathCheck) walk(n node, tail []string) (node, error) { if n, err = stepInto(n, tail[0]); err != nil { return nil, err } + w.emit(pathStep{kind: stepField, name: tail[0]}, tail) tail = tail[1:] } w.leaves = append(w.leaves, n) return n, nil } +func (w *pathCheck) tableSteps(t *table, r tableRoute, tail []string) error { + if r.sel != "" { + if err := w.pins.selectRow(t, r.sel); err != nil { + return err + } + row, _ := w.pins.pinned(t) + w.emit(pathStep{kind: stepSelect, name: r.sel, row: row}, tail) + } + if r.draw { + w.emit(pathStep{kind: stepDraw}, tail) + } + switch next := r.next.(type) { + case *table: + if next != t { + w.emit(pathStep{kind: stepChild, name: next.segment}, tail) + } + case *tableRow: + w.emit(pathStep{kind: stepRow}, tail) + case *tableColumn: + w.emit(pathStep{kind: stepColumn, name: t.header[next.i]}, tail) + default: + panic(internalError("%s: a route onto %T", t.segment, r.next)) + } + return nil +} + +// walkEvery walks every variant and keeps the first one's steps: every variant carries +// the rest of the path, so each compiles to the same steps past the choice. func (w *pathCheck) walkEvery(c *choice, tail []string) (node, error) { if err := carriedByAll(c, tail); err != nil { return nil, err } var last node - for _, item := range c.items { + before, first := w.steps[:len(w.steps):len(w.steps)], w.steps + for i, item := range c.items { var err error + w.steps = before if last, err = w.walk(item, tail); err != nil { return nil, err } + if i == 0 { + first = w.steps + } } + w.steps = first return last, nil } @@ -259,45 +330,10 @@ func (w *pathCheck) enter(t *template, rest []string) error { return nil } -// pathCover is coverPath's walk. Where pathCheck walks every variant of a choice, it -// covers the first choice it reaches whole and stops; at a table it follows the route -// and selects no row. -type pathCover struct{ into map[node]bool } - -func (w *pathCover) walk(n node, tail []string) (node, error) { - for descended := false; len(tail) > 0 || descended; { - var err error - switch x := n.(type) { - case *choice: - cover(x, w.into, false) - return x, nil - case *table: - var r tableRoute - if r, err = x.route(tail, descended); err != nil { - return nil, err - } - n, tail, descended = r.next, r.rest, r.descends - continue - } - if n, err = stepInto(n, tail[0]); err != nil { - return nil, err - } - tail = tail[1:] - } - cover(n, w.into, false) - return n, nil -} - // pathProbe proves a path resolves without drawing. Where pathCheck walks every // variant of a choice, it walks the first, which carriedByAll lets stand for all; at a -// table it selects as pathCheck does. Probing an arm, it records what the read takes: -// in drawn the tables a draw would pin, and in sels each selector's table and spelling. -type pathProbe struct { - pins pinSet - a *arm - drawn map[*table]bool - sels []tableSel -} +// table it selects as pathCheck does. +type pathProbe struct{ pins pinSet } func (w *pathProbe) walk(n node, tail []string) (node, error) { for descended := false; len(tail) > 0 || descended; { @@ -314,8 +350,10 @@ func (w *pathProbe) walk(n node, tail []string) (node, error) { if r, err = x.route(tail, descended); err != nil { return nil, err } - if err := w.readRow(x, tail, r); err != nil { - return nil, err + if r.sel != "" { + if err := w.pins.selectRow(x, r.sel); err != nil { + return nil, err + } } n, tail, descended = r.next, r.rest, r.descends continue @@ -328,25 +366,6 @@ func (w *pathProbe) walk(n node, tail []string) (node, error) { return n, nil } -func (w *pathProbe) readRow(t *table, tail []string, r tableRoute) error { - switch { - case r.sel != "": - if w.a != nil { - walked := len(w.a.tail) - len(tail) + 1 - w.sels = append(w.sels, tableSel{t, joinSegments(append([]string{w.a.writtenHead}, w.a.tail[:walked]...))}) - } - return w.pins.selectRow(t, r.sel) - case r.draw && w.a != 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 -} - // pathDraw draws the rows and variants a proved path reads, pinning the rows in pins; pins is nil // for a sibling path, which never crosses a table, since a table is only a category. For a memoized // read, memo keeps the variant drawn at each level of a, so paths sharing a prefix share it. @@ -471,6 +490,15 @@ func checkPath(n node, tail []string, level string) error { return err } +// compilePath compiles a path checkPath proved. +func compilePath(n node, tail []string) pathCheck { + w := pathCheck{tail: tail} + if _, err := w.run(n); err != nil { + panic(internalError("%s: %v; the path should have been proved before it was compiled", joinSegments(tail), err)) + } + return w +} + // joinSegments spells segments as a path: a selector attaches to the name before it. func joinSegments(segs []string) string { var b strings.Builder diff --git a/reads.go b/reads.go index a2ffdc5..e8bf049 100644 --- a/reads.go +++ b/reads.go @@ -242,18 +242,32 @@ type tableSel struct { spelling string } -// tableReadOf replays a reference path through pathProbe, so two paths pinning one -// row by different routes compare equal. checkPath proved -// each selector names a row. +// tableReadOf follows a reference path's steps for what it takes of a table family, so +// two paths pinning one row by different routes compare equal. func tableReadOf(head node, a arm, leaf node) *tableRead { t, isTable := head.(*table) if !isTable { return nil } - probe := &pathProbe{a: &a} - _, _ = probe.walk(t, a.tail) - _, landsRow := leaf.(*tableRow) - return &tableRead{headTable: t, pins: probe.pins, drawn: probe.drawn, sels: probe.sels, landsRow: landsRow} + r := &tableRead{headTable: t} + _, r.landsRow = leaf.(*tableRow) + for _, st := range a.steps { + switch st.kind { + case stepSelect: + r.pins.pin(t, st.row) + r.sels = append(r.sels, tableSel{t, joinSegments(append([]string{a.writtenHead}, a.tail[:st.at+1]...))}) + case stepDraw: + if r.drawn == nil { + r.drawn = map[*table]bool{} + } + for d, stop := t, r.pins.nearestPinned(t); d != stop; d = d.parentT { + r.drawn[d] = true + } + case stepChild: + t = t.descendant(st.name) + } + } + return r } // selected is the selector in r on t, or on the nearest ancestor of t it selects. diff --git a/reference.go b/reference.go index 861104e..833b0f7 100644 --- a/reference.go +++ b/reference.go @@ -93,9 +93,32 @@ func linkTemplate(folder []string, path, category string, t *template, root map[ return fmt.Errorf("%s: %w", path, err) } t.link, t.compiled = link, compiled + compileArms(t) return nil } +func compileArms(t *template) { + if r := t.link.readsColumn; r != nil { + compileArm(t, &r.a) + } + for i := range t.compiled.ops { + o := &t.compiled.ops[i] + for j := range o.operands { + compileArm(t, &o.operands[j]) + } + for j := range o.arms { + compileArm(t, &o.arms[j]) + } + } +} + +func compileArm(t *template, a *arm) { + if head := t.head(a.head); head != nil { + w := compilePath(head, a.tail) + a.steps, a.leaves, a.cover = w.steps, w.leaves, w.cover + } +} + // resolveLink binds every reference t reads, refusing one to t's own category, and keys // t's draw group by that category, so a name is local to it: // docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load diff --git a/todo.md b/todo.md index c750647..db711ec 100644 --- a/todo.md +++ b/todo.md @@ -40,13 +40,9 @@ maintainer chose on 2026-09-30 to go on with the path steps regardless. This round, in order: -- Compile each path once at link into steps holding no pointer, with its leaves and - cover, and have `renderEdges`, `heldNodes`, `valueProof` and `tableReadOf` read them, - deleting `pathCover`, `coverPath` and `pathLeaves`. Prototyped green, allocations - identical. - Draw every path through one interpreter over those steps, `drawSteps`, with `descend` - probing into a stack buffer, deleting `pathDraw`, and replace the decision "The four - path walks are four loops": the prototype held `perf_test.go` once a step held no + probing into a stack buffer, deleting `pathDraw`, and replace the decision "The path + walks are separate loops": the prototype held `perf_test.go` once a step held no pointer, the probe returned its slice and the interpreter took `levels`. - Fold `checkNoOverlap` and `checkNoRepeatedRead` into `heldCheck`, so `compileFormat` compiles and nothing more. diff --git a/valueproof.go b/valueproof.go index 9c46f5b..0ac6217 100644 --- a/valueproof.go +++ b/valueproof.go @@ -164,7 +164,7 @@ func (p *valueProof) proveTemplate(t *template) proven { case o.kind == nameRead: var leaves []node for _, a := range o.arms { - leaves = append(leaves, pathLeaves(t.head(a.head), a.tail)...) + leaves = append(leaves, a.leaves...) } return p.proveUnion(leaves) case name == "calc":