Compile each path's steps, leaves and cover once at link, deleting pathCover, coverPath and pathLeaves
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user