From 9cbd8841feb73a4c34f201b9fe3b07e7be022dee Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 24 Sep 2026 13:11:12 +0200 Subject: [PATCH] Draw a walk's rows and variants through pathDraws, off the pin set and the walk's signature --- doc.go | 2 +- family.go | 2 +- graph.go | 2 +- hold.go | 21 +-------- holdfence.go | 2 +- path.go | 118 +++++++++++++++++++++++++++++++++++++-------------- path_test.go | 6 +-- pins.go | 25 ----------- record.go | 2 +- render.go | 4 +- template.go | 18 ++++---- todo.md | 8 ++-- 12 files changed, 110 insertions(+), 100 deletions(-) diff --git a/doc.go b/doc.go index ad963fd..cbd54d0 100644 --- a/doc.go +++ b/doc.go @@ -29,7 +29,7 @@ package fejkdata // `template.drawGroup` as the data spells it, `template.drawGroupKey` as a // render reads it, `holdSet`, `renderScope`. // - pin — fixing which row of a table the render uses, which the draw fences replay: -// `pinSet`, `pinSet.pin`, `pinSet.rowOf`. +// `pinSet`, `pinSet.pin`, `pathDraws.row`. // - family — a table and every table reaching it through a chain of // `table.parentT`, named by the root that chain ends at: `table.family`. // - whole — a read that lands on a `row`, so the row renders through its table's diff --git a/family.go b/family.go index c22474d..fad404e 100644 --- a/family.go +++ b/family.go @@ -33,7 +33,7 @@ func tableReadOf(head node, a arm, leaf node) *tableRead { return nil } tr := &tableRead{head: t, drawn: map[*table]bool{}} - _, _ = walkPath(nil, t, a.tail, pathWalk{pins: &tr.pins}) + _, _ = walkPath(t, a.tail, pathWalk{pins: &tr.pins}) written := a.name[:len(a.name)-len(joinSegments(a.tail))] cur := t tr.draws(cur) diff --git a/graph.go b/graph.go index 6b1794d..d0fcf79 100644 --- a/graph.go +++ b/graph.go @@ -178,7 +178,7 @@ func renderEdges(n node) []renderEdge { // already proved the tail resolves in every variant. func pathLeaves(n node, tail []string) []node { var out []node - _, _ = walkPath(nil, n, tail, pathWalk{ + _, _ = walkPath(n, tail, pathWalk{ choice: func(c *choice, _ []string) ([]node, error) { return c.items, nil }, leaf: func(n node) error { out = append(out, n); return nil }, }) diff --git a/hold.go b/hold.go index b12dc21..9ad3629 100644 --- a/hold.go +++ b/hold.go @@ -38,26 +38,7 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw if r, done := d.value[a.path]; done { return r } - leaf, err := walkPath(s, t.fields[a.key], a.tail, pathWalk{ - // Hold the draw at every level passed through, so two paths sharing a - // prefix share it. - choice: func(c *choice, rest []string) ([]node, error) { - key := a.key - if consumed := len(a.tail) - len(rest); consumed > 0 { - key = a.steps[consumed-1] - } - n, drew := d.variant[key] - if !drew { - n = drawn(s, c) - if d.variant == nil { - d.variant = map[string]node{} - } - d.variant[key] = n - } - return []node{n}, nil - }, - pins: &d.pins, - }) + leaf, err := walkPath(t.fields[a.key], a.tail, pathWalk{pins: &d.pins, draws: &pathDraws{s: s, held: d, a: &a}}) if err != nil { panic(fmt.Sprintf("fejkdata: %q: %v; a fence should have refused this at New", a.name, err)) } diff --git a/holdfence.go b/holdfence.go index 63380d3..562c915 100644 --- a/holdfence.go +++ b/holdfence.go @@ -106,7 +106,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) { - _, _ = walkPath(nil, n, tail, pathWalk{ + _, _ = walkPath(n, tail, pathWalk{ choice: func(c *choice, _ []string) ([]node, error) { cover(c, into, false); return nil, nil }, leaf: func(n node) error { cover(n, into, false); return nil }, }) diff --git a/path.go b/path.go index 22226c7..3fe216e 100644 --- a/path.go +++ b/path.go @@ -127,29 +127,40 @@ func names(segs []string) []string { // pathWalk is what one walk of a dotted path does at each kind of level: choice // returns the variants to continue into (none stops the walk), and where it is nil -// the walk draws one with s, takes the first where s is nil, or stops where pins is -// nil too; level runs at each template a segment descends +// the walk draws one with draws, takes the first where draws is nil, or stops where +// pins is nil too; level runs at each template a segment descends // into; leaf runs where the tail ends; pins is where the walk pins the table rows -// it selects and draws, nil skipping tables. A nil action is skipped. A render's -// walk sets only pins, so it allocates nothing. +// it selects and draws, nil skipping tables; draws is what draws the rows and +// variants the path reads, nil proving instead that the path resolves whichever +// are drawn. A nil action is skipped. A render's walk sets only pins and draws, so +// it allocates nothing. type pathWalk struct { choice func(c *choice, rest []string) ([]node, error) level func(t *template, rest []string) error leaf func(n node) error pins *pinSet + draws *pathDraws +} + +// pathDraws is what a walk draws with: the session, and, for a held read of a, the +// hold keeping the variant drawn at each level, so paths sharing a prefix share it. +// a is a pointer: held inline, its level keys take an eager hold set to the heap. +type pathDraws struct { + s *session + held *hold + a *arm } // walkPath descends tail from n and returns the node it ends at: a folder or // template by its next segment, a choice by w.choice, which consumes no segment, a // table by walkTable. A missing segment is an error, so no walk reaches past what -// the data holds. s draws rows and variants; where it is nil, only selectors pin. -// s is a parameter: a pathWalk field beside pins moves the hold maps to the heap. -func walkPath(s *session, n node, tail []string, w pathWalk) (node, error) { +// the data holds. +func walkPath(n node, tail []string, w pathWalk) (node, error) { if len(tail) == 0 { return n, w.atLeaf(n) } if t, isTable := n.(*table); isTable { - return walkTable(s, t, tail, w, false) + return walkTable(t, tail, w, false) } if isSelector(tail[0]) { return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(tail[0])) @@ -160,7 +171,7 @@ func walkPath(s *session, n node, tail []string, w pathWalk) (node, error) { if !ok { return nil, fmt.Errorf("no entry %q", tail[0]) } - return walkPath(s, child, tail[1:], w) + return walkPath(child, tail[1:], w) case *template: if w.level != nil { if err := w.level(n, tail); err != nil { @@ -171,9 +182,9 @@ func walkPath(s *session, n node, tail []string, w pathWalk) (node, error) { if !ok { return nil, fmt.Errorf("no field %q", tail[0]) } - return walkPath(s, child, tail[1:], w) + return walkPath(child, tail[1:], w) case *choice: - return walkChoice(s, n, tail, w) + return walkChoice(n, tail, w) case *column: return nil, fmt.Errorf("no field %q: %q is a column, and a cell holds no fields", tail[0], n.t.columns[n.i]) } @@ -187,20 +198,20 @@ func (w pathWalk) atLeaf(n node) error { return nil } -// walkChoice continues a walk into the variants w.choice returns, or into one drawn -// by s where the walk names no choice action. -func walkChoice(s *session, c *choice, tail []string, w pathWalk) (node, error) { +// walkChoice continues a walk into the variants w.choice returns, or into one +// w.draws draws where the walk names no choice action. +func walkChoice(c *choice, tail []string, w pathWalk) (node, error) { if w.choice == nil { if w.pins == nil { return nil, nil } - if err := carriedByAll(c, tail); err != nil { - return nil, err + if w.draws == nil { // a probe: every variant carries the tail, so any one proves it + if err := carriedByAll(c, tail); err != nil { + return nil, err + } + return walkPath(c.items[0], tail, w) } - if s == nil { // a probe: every variant carries the tail, so any one proves it - return walkPath(s, c.items[0], tail, w) - } - return walkPath(s, pick(s, c), tail, w) + return walkPath(w.draws.variant(c, tail), tail, w) } next, err := w.choice(c, tail) if err != nil { @@ -208,7 +219,7 @@ func walkChoice(s *session, c *choice, tail []string, w pathWalk) (node, error) } var last node for _, item := range next { - if last, err = walkPath(s, item, tail, w); err != nil { + if last, err = walkPath(item, tail, w); err != nil { return nil, err } } @@ -219,7 +230,7 @@ func walkChoice(s *session, c *choice, tail []string, w pathWalk) (node, error) // linked table by name. The walk reads a row wherever a segment follows or the // table was reached from another; a table reached whole, with no selector, is left // to a render's own draw. -func walkTable(s *session, t *table, tail []string, w pathWalk, descended bool) (node, error) { +func walkTable(t *table, tail []string, w pathWalk, descended bool) (node, error) { sel, tail, err := t.selector(tail) if err != nil { return nil, err @@ -231,19 +242,19 @@ func walkTable(s *session, t *table, tail []string, w pathWalk, descended bool) // A selector further down pins this table by ancestry, so the walk draws only // where none follows; drawing first could pick a row the selector is not inside. if w.pins != nil { - if err := readRow(s, w.pins, t, sel, (descended || len(tail) > 0) && !hasSelector(tail)); err != nil { + if err := readRow(w.pins, w.draws, t, sel, (descended || len(tail) > 0) && !hasSelector(tail)); err != nil { return nil, err } } switch { case len(tail) == 0 && sel == "" && !descended: - return walkPath(s, t, nil, w) + return walkPath(t, nil, w) case len(tail) == 0: - return walkPath(s, t.whole, nil, w) + return walkPath(t.whole, nil, w) case child != nil: - return walkTable(s, child, tail[1:], w, true) + return walkTable(child, tail[1:], w, true) } - return walkPath(s, column, tail[1:], w) + return walkPath(column, tail[1:], w) } // selector splits the selector a tail starts with from the rest of it. @@ -274,16 +285,59 @@ func (t *table) step(tail []string) (column node, child *table, err error) { // readRow pins the row a path reads of t: the one its selector names, or, where the // walk draws, one drawn where the path reads into the table. -func readRow(s *session, pins *pinSet, t *table, sel string, draw bool) error { +func readRow(p *pinSet, draws *pathDraws, t *table, sel string, draw bool) error { if sel != "" { - return pins.selectRow(t, sel) + return p.selectRow(t, sel) } - if draw && s != nil { - pins.rowOf(s, t) + if draw && draws != nil { + draws.row(p, t) } return nil } +// row is the render's row of t: the one pinned in p, else one drawn inside the +// nearest pinned ancestor — its parent drawn inside that first where the ancestor +// is further up — or over the whole table, and pinned with its ancestors. +func (d *pathDraws) row(p *pinSet, t *table) int { + if r, ok := p.pinned(t); ok { + return r + } + r := -1 + for a := t.parentT; a != nil && r < 0; a = a.parentT { + if _, ok := p.pinned(a); !ok { + continue + } + if t.parentT != a { + d.row(p, t.parentT) + } + pr, _ := p.pinned(t.parentT) + r = t.drawUnder(d.s, pr) + } + if r < 0 { + r = t.draw(d.s) + } + p.pin(t, r) + return r +} + +// 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 (d *pathDraws) variant(c *choice, rest []string) node { + if d.held == nil { + return pick(d.s, c) + } + key := d.a.levels[len(d.a.tail)-len(rest)] + n, drew := d.held.variant[key] + if !drew { + n = drawn(d.s, c) + if d.held.variant == nil { + d.held.variant = map[string]node{} + } + d.held.variant[key] = n + } + return n +} + // carriedByAll is the choice rule a path that must resolve on every call obeys: // the rest of the tail must be one every variant carries. func carriedByAll(c *choice, rest []string) error { @@ -315,7 +369,7 @@ func unreachableInChoice(c *choice, want string) error { // that validates here resolves on every render, and a typo is a New-time error. func checkPath(n node, tail []string, level string) error { var pins pinSet - _, err := walkPath(nil, n, tail, pathWalk{ + _, err := walkPath(n, tail, pathWalk{ choice: func(c *choice, rest []string) ([]node, error) { if err := carriedByAll(c, rest); err != nil { return nil, err diff --git a/path_test.go b/path_test.go index f4260d0..4a8ce24 100644 --- a/path_test.go +++ b/path_test.go @@ -13,14 +13,14 @@ func TestWalkPathStopsAtAMissingSegment(t *testing.T) { level: func(tm *template, rest []string) error { seen = append(seen, "level:"+rest[0]); return nil }, leaf: func(n node) error { seen = append(seen, "leaf"); return nil }, } - if _, err := walkPath(nil, n, []string{"a", "b"}, walk); err != nil { + if _, err := walkPath(n, []string{"a", "b"}, walk); err != nil { t.Fatalf("walkPath(a.b) = %v", err) } if want := []string{"level:a", "level:b", "leaf"}; !slices.Equal(seen, want) { t.Errorf("walk visited %v, want %v", seen, want) } seen = nil - _, err := walkPath(nil, n, []string{"a", "nope", "deeper"}, walk) + _, err := walkPath(n, []string{"a", "nope", "deeper"}, walk) if err == nil || !strings.Contains(err.Error(), `no field "nope"`) { t.Errorf("walkPath(a.nope.deeper) = %v, want the missing segment named", err) } @@ -32,7 +32,7 @@ func TestWalkPathStopsAtAMissingSegment(t *testing.T) { func TestWalkPathChoiceConsumesNoSegment(t *testing.T) { n := compiled(t, `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`) var leaves []node - _, err := walkPath(nil, n, []string{"f"}, pathWalk{ + _, err := walkPath(n, []string{"f"}, pathWalk{ choice: func(c *choice, rest []string) ([]node, error) { if len(rest) != 1 || rest[0] != "f" { t.Errorf("choice saw rest %v, want [f]", rest) diff --git a/pins.go b/pins.go index ca6b57c..3badda9 100644 --- a/pins.go +++ b/pins.go @@ -81,31 +81,6 @@ func (p *pinSet) each(fn func(t *table, r int)) { } } -// rowOf is the row of t: the one pinned, else one s draws inside the nearest pinned ancestor — its -// parent drawn inside that first where the ancestor is further up — or over the whole table, and -// pinned with its ancestors. -func (p *pinSet) rowOf(s *session, t *table) int { - if r, ok := p.pinned(t); ok { - return r - } - r := -1 - for a := t.parentT; a != nil && r < 0; a = a.parentT { - if _, ok := p.pinned(a); !ok { - continue - } - if t.parentT != a { - p.rowOf(s, t.parentT) - } - pr, _ := p.pinned(t.parentT) - r = t.drawUnder(s, pr) - } - if r < 0 { - r = t.draw(s) - } - p.pin(t, r) - return r -} - // clash is the table whose pinned row keeps row r of t out: t itself pinned to another row, or the // nearest ancestor pinned to a row r is not inside; nil where none does. func (p *pinSet) clash(t *table, r int) *table { diff --git a/record.go b/record.go index 9ef6a80..b7e1067 100644 --- a/record.go +++ b/record.go @@ -159,7 +159,7 @@ func tableRecord(s *session, t *table, tail []string, sc renderScope) (node, err } switch n := n.(type) { case *table: - sc.hold().pins.rowOf(s, n) + (&pathDraws{s: s}).row(&sc.hold().pins, n) return n, nil case *row: return n.t, nil diff --git a/render.go b/render.go index 3ff95ba..b0787ff 100644 --- a/render.go +++ b/render.go @@ -45,10 +45,10 @@ func (f *Generator) Fake(path string) (string, error) { 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. var probe pinSet - if _, err := walkPath(nil, root, segments, pathWalk{pins: &probe}); err != nil { + if _, err := walkPath(root, segments, pathWalk{pins: &probe}); err != nil { return nil, err } - return walkPath(s, root, segments, pathWalk{pins: &sc.hold().pins}) + return walkPath(root, segments, pathWalk{pins: &sc.hold().pins, draws: &pathDraws{s: s}}) } // render evaluates a compiled node to a string. compile validates every node up diff --git a/template.go b/template.go index c92edd5..7ec28be 100644 --- a/template.go +++ b/template.go @@ -273,11 +273,11 @@ func fieldTokens(format string) []string { // tail is what makes the arm a bound draw: its head is drawn once per expansion // (see compileOps). type arm struct { - name string // as written, for messages - key string - tail []string - steps []string // key per level passed through; the head and leaf hold their own - path string // key and tail, the one spelling every way of writing this read shares + name string // as written, for messages + key string + tail []string + levels []string // the key at each level the tail passes through, the head's first + path string // key and tail, the one spelling every way of writing this read shares } // splitArm splits one name into key and tail. refs maps a reference to what @@ -302,11 +302,11 @@ func splitArm(name string, refs map[string]refBinding) arm { } func pathArm(name, key string, segs []string) arm { - var steps []string - for i := 0; i < len(segs)-1; i++ { // every level except the leaf's own - steps = append(steps, key+"."+strings.Join(segs[:i+1], ".")) + levels := []string{key} + for i := 0; i < len(segs)-1; i++ { + levels = append(levels, key+"."+strings.Join(segs[:i+1], ".")) } - return arm{name: name, key: key, tail: segs, steps: steps, path: key + "." + strings.Join(segs, ".")} + return arm{name: name, key: key, tail: segs, levels: levels, path: key + "." + strings.Join(segs, ".")} } // splitArms splits a token body's '|' alternatives. diff --git a/todo.md b/todo.md index 4383a05..2a87b86 100644 --- a/todo.md +++ b/todo.md @@ -39,12 +39,12 @@ This round, in order: `operandReader`, `checkNoRepeatedRead` and `compileOps`, and function-versus-field is decided by `indexOutside(body, '(')` in two of them and by `funcCall` in the rest. - Name `pathWalk`'s modes: render, probe, check and cover are chosen today by which - callbacks are nil and whether `walkPath`'s session is, and `walkChoice` returns - `nil, nil` where none is set. + callbacks are nil and whether `draws` is, and `walkChoice` returns `nil, nil` where + none is set. - Have the check mode report the tables a read draws, so `tableRead.drawn` and `entered`'s `pinsAncestors` flag stop restating `walkTable`'s draw condition and - `rowOf`'s ancestor rule: a change to how a render draws rows still leaves the fence - proving the old rule. Hold a row rendered whole in `drawAt` beside `drawn`, so + `pathDraws.row`'s ancestor rule: a change to how a render draws rows still leaves the + fence proving the old rule. Hold a row rendered whole in `drawAt` beside `drawn`, so `pinSet` holds only what `pin` pins and `alternatives` needs no exception. Settle a cell of a municipality drawn whole beside a path pinning a region it is not in. Needs the item above.