Compile each path's steps, leaves and cover once at link, deleting pathCover, coverPath and pathLeaves

This commit is contained in:
2026-09-30 17:09:50 +02:00
committed by Lillem4n
parent 0f58784a1b
commit 6b9755b3ef
10 changed files with 149 additions and 103 deletions
+1 -1
View File
@@ -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
+3 -2
View File
@@ -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
+3
View File
@@ -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 {
+1 -14
View File
@@ -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 {
+1 -7
View File
@@ -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.
+93 -65
View File
@@ -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
+21 -7
View File
@@ -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.
+23
View File
@@ -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
+2 -6
View File
@@ -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.
+1 -1
View File
@@ -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":