From 3927dc9fa59bdae19408255eec25c292d370330d Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 24 Sep 2026 00:17:01 +0200 Subject: [PATCH] Give render and the draw fences one pin set --- README.md | 5 +- doc.go | 3 +- drawfence.go | 6 +- family.go | 79 +++---------------- graph.go | 2 +- hold.go | 151 +++---------------------------------- holdfence.go | 2 +- holdset.go | 21 ++---- path.go | 58 +++++++------- path_test.go | 6 +- pins.go | 192 +++++++++++++++++++++++++++++++++++++++++++++++ record.go | 8 +- render.go | 12 +-- struct.go | 2 +- table.go | 6 +- template_test.go | 2 +- todo.md | 8 +- 17 files changed, 279 insertions(+), 284 deletions(-) create mode 100644 pins.go diff --git a/README.md b/README.md index 17bd83b..78ca477 100644 --- a/README.md +++ b/README.md @@ -1338,11 +1338,12 @@ record.go records: Record, the JSON/CSV/SQL serializers, and their entry p struct.go structs: FakeStruct, fake tags, and a field's Go type as its column's datatype inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format -hold.go the hold: one draw per expansion for paths and operands, and the rows it pins +hold.go the hold: one draw per expansion for paths and operands holdfence.go the load fences over the hold: the routes and spellings that read one held name two ways holdset.go one reference draw per render and group: the hold set a render reads its paths through drawfence.go the load fences over reference draws: a drawGroup's, a render's and a record's columns, and the walk over what a render reads -family.go a family of linked tables: what a reference path reads of one, the rows a walk enters, and the fences over paths into it +family.go a family of linked tables: what a reference path reads of one, and the fences over paths into it +pins.go the pin set: the table rows a render fixes, which the draw fences replay reference.go reference sigils, and binding references across the tree graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences builtins.go the {name()} function registry and its implementations diff --git a/doc.go b/doc.go index 1d8c2fe..ad963fd 100644 --- a/doc.go +++ b/doc.go @@ -28,7 +28,8 @@ package fejkdata // - draw group — reference draws held apart inside one render: // `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: `hold.pin`, `hold.rowOf`. +// - pin — fixing which row of a table the render uses, which the draw fences replay: +// `pinSet`, `pinSet.pin`, `pinSet.rowOf`. // - 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/drawfence.go b/drawfence.go index 7ea756b..824403e 100644 --- a/drawfence.go +++ b/drawfence.go @@ -180,7 +180,7 @@ type drawWalk struct { type drawAt struct { group string route drawRoute - alt rowSet + alt pinSet drawn *table // left out of the visit keys: only this table's own cells compare against it, which the own-family fence keeps true } @@ -233,7 +233,7 @@ func (w *drawWalk) walk(n node, at drawAt) { continue } in := at - in.alt = at.alt.enter(cell.cellOf, cell.cellRow, cell.cellOf != at.drawn) + in.alt = at.alt.entered(cell.cellOf, cell.cellRow, cell.cellOf != at.drawn) w.edge(n, e, in) continue } @@ -251,7 +251,7 @@ func (w *drawWalk) edge(from node, e renderEdge, at drawAt) { w.reads = append(w.reads, pathRead{at, a, tr}) } if tr != nil { - tr.pins.each(func(t *table, r int) { at.alt = at.alt.enter(t, r, true) }) + tr.pins.each(func(t *table, r int) { at.alt = at.alt.entered(t, r, true) }) } } w.walk(e.to, at) diff --git a/family.go b/family.go index 9403618..c89e2a6 100644 --- a/family.go +++ b/family.go @@ -2,7 +2,6 @@ package fejkdata import ( "fmt" - "sort" "strings" ) @@ -12,7 +11,7 @@ import ( // it lands on a row rendered whole. type tableRead struct { head *table - pins hold + pins pinSet drawn map[*table]bool sels []tableSel whole bool @@ -34,7 +33,7 @@ func tableReadOf(head node, a arm, leaf node) *tableRead { return nil } tr := &tableRead{head: t, drawn: map[*table]bool{}} - _, _ = walkPath(t, a.tail, pathWalk{pins: &tr.pins}) + _, _ = walkPath(nil, t, a.tail, pathWalk{pins: &tr.pins}) written := a.name[:len(a.name)-len(joinSegments(a.tail))] cur := t tr.draws(cur) @@ -126,7 +125,7 @@ func replayPairs(reads []pathRead) error { if o.tr == nil || o.at.group != r.at.group || alternatives(r.at, o.at) { continue } - var d hold + var d pinSet if err := r.tr.replay(&d); err != nil { return conflict(r, err) } @@ -143,7 +142,7 @@ func conflict(r pathRead, err error) error { } // replay pins the read's rows into d, where they agree with the rows pinned before. -func (r *tableRead) replay(d *hold) error { +func (r *tableRead) replay(d *pinSet) error { var err error r.pins.each(func(t *table, row int) { if err == nil { @@ -179,7 +178,7 @@ func checkFamilyPair(a, b pathRead) error { } // drawnOf is a table the read draws that pins holds a row of, if any. -func (r *tableRead) drawnOf(pins *hold) *table { +func (r *tableRead) drawnOf(pins *pinSet) *table { var found *table pins.each(func(t *table, _ int) { if found == nil && r.drawn[t] { @@ -226,69 +225,13 @@ func checkOwnFamily(t *template) error { return nil } -// rowSet stands in at load for the rows (*hold).pin holds at render: a path read enters a row with -// its ancestors, a whole read enters the drawn row alone. -type rowSet []tablePin - -func (s rowSet) rowOf(t *table) (int, bool) { - for _, p := range s { - if p.t == t { - return p.row, true - } - } - return 0, false -} - -// enter is s with row r of t, and with t's ancestors where the render pins that row rather than -// drawing it; the set is copied, since walks branch. -func (s rowSet) enter(t *table, r int, pins bool) rowSet { - if _, in := s.rowOf(t); in { - return s - } - out := append(make(rowSet, 0, len(s)+1), s...) - for { - out = append(out, tablePin{t, r}) - if !pins || t.parentT == nil { - break - } - t, r = t.parentT, t.parentRow(r) - if _, in := out.rowOf(t); in { - break - } - } - sort.Slice(out, func(i, j int) bool { return out[i].t.path < out[j].t.path }) - return out -} - -// key spells the set for a map, by the tables' identities. -func (s rowSet) key() string { - var b strings.Builder - for _, p := range s { - fmt.Fprintf(&b, "%p[%d]", p.t, p.row) - } - return b.String() -} - -// excludes reports whether a cell's row cannot render with the rows entered: another -// row of its table, or a row outside an entered ancestor's. -func (s rowSet) excludes(cell *template) bool { - for _, p := range s { - if p.t == cell.cellOf { - return p.row != cell.cellRow - } - if cell.cellOf.descends(p.t) && !cell.cellOf.under(cell.cellRow, p.t, p.row) { - return true - } - } - return false -} - // alternatives reports whether two reads sit in different rows of one table. func alternatives(a, b drawAt) bool { - for _, p := range a.alt { - if r, in := b.alt.rowOf(p.t); in && r != p.row { - return true + found := false + a.alt.each(func(t *table, r int) { + if br, in := b.alt.pinned(t); in && br != r { + found = true } - } - return false + }) + return found } diff --git a/graph.go b/graph.go index d0fcf79..6b1794d 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(n, tail, pathWalk{ + _, _ = walkPath(nil, 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 555e583..ecb7f2a 100644 --- a/hold.go +++ b/hold.go @@ -2,26 +2,16 @@ package fejkdata import ( "fmt" - "sort" ) -// tablePin is one table's pinned row. -type tablePin struct { - t *table - row int -} - // hold is what has already been drawn for held names: the variant each was drawn as, // so every path under it reads one row, and the draw each read made, by its one -// spelling, so the same read written twice reads one draw. An expansion keeps one for -// its sibling names, and a render one per group for its reference paths. +// spelling, so the same read written twice reads one draw, and the table rows its reads pinned. An +// expansion keeps one for its sibling names, and a render one per group for its reference paths. type hold struct { variant map[string]node value map[string]draw - pins [8]tablePin // the rows pinned, inline so a render over a country's five-deep geo tree stays off the heap - npins int - more map[*table]int // the rows pinned past the inline eight - s *session // what draws a row; nil where a walk only proves selectors + pins pinSet } // draw is what one read drew: its text, and whether it landed on a null. @@ -30,129 +20,6 @@ type draw struct { null bool } -// pinned is the row the render pinned for t, if any. -func (d *hold) pinned(t *table) (int, bool) { - for _, p := range d.pins[:d.npins] { - if p.t == t { - return p.row, true - } - } - r, ok := d.more[t] - return r, ok -} - -// mustRow is the row pinned for t, which the walk reaching a column pinned. -func (d *hold) mustRow(t *table) int { - r, ok := d.pinned(t) - if !ok { - panic(fmt.Sprintf("fejkdata: a column of %s is rendered with no row pinned", t.category)) - } - return r -} - -// pin pins row r of t, and the rows of t's ancestors it links to. -func (d *hold) pin(t *table, r int) { - for { - if _, done := d.pinned(t); done { - return - } - if d.npins < len(d.pins) { - d.pins[d.npins] = tablePin{t, r} - d.npins++ - } else { - if d.more == nil { - d.more = map[*table]int{} - } - d.more[t] = r - } - if t.parentT == nil { - return - } - t, r = t.parentT, t.parentRow(r) - } -} - -// each calls fn for every pinned row, in pin order, the spilled ones by table name. -func (d *hold) each(fn func(t *table, r int)) { - for _, p := range d.pins[:d.npins] { - fn(p.t, p.row) - } - spilled := make([]*table, 0, len(d.more)) - for t := range d.more { - spilled = append(spilled, t) - } - sort.Slice(spilled, func(i, j int) bool { return spilled[i].category < spilled[j].category }) - for _, t := range spilled { - fn(t, d.more[t]) - } -} - -// rowOf is the render's row of t: the one pinned, 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 *hold) rowOf(t *table) int { - if r, ok := d.pinned(t); ok { - return r - } - r := -1 - for a := t.parentT; a != nil && r < 0; a = a.parentT { - if _, ok := d.pinned(a); !ok { - continue - } - if t.parentT != a { - d.rowOf(t.parentT) - } - pr, _ := d.pinned(t.parentT) - r = t.drawUnder(d.s, pr) - } - if r < 0 { - r = t.draw(d.s) - } - d.pin(t, r) - return r -} - -// pinRow pins row r of t where it agrees with the rows pinned before it. -func (d *hold) pinRow(t *table, r int) error { - if pr, ok := d.pinned(t); ok && pr != r { - return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.path) - } - for a := t.parentT; a != nil; a = a.parentT { - if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) { - return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa)) - } - } - d.pin(t, r) - return nil -} - -// selectRow pins the row a selector names. -func (d *hold) selectRow(t *table, sel string) error { - r, err := t.find(sel, d) - if err != nil { - return err - } - return d.pinRow(t, r) -} - -// inside keeps the rows of t that sit inside every pinned ancestor. -func (d *hold) inside(t *table, rows []int) []int { - for a := t.parentT; a != nil; a = a.parentT { - pa, ok := d.pinned(a) - if !ok { - continue - } - var kept []int - for _, r := range rows { - if t.under(r, a, pa) { - kept = append(kept, r) - } - } - rows = kept - } - return rows -} - // readField renders one arm of a token. An arm's key is a sibling field or a // reference linkRefs bound into fields. A name the expansion holds — a level some // token addresses by dotted path, or a field an operand reads — is drawn once and @@ -167,11 +34,11 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw } return draw{text: render(s, t.fields[a.key], sc)} } - d := readHold(s, held, sc, a) + d := readHold(held, sc, a) if r, done := d.value[a.path]; done { return r } - leaf, err := walkPath(t.fields[a.key], a.tail, pathWalk{ + 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) { @@ -189,7 +56,7 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw } return []node{n}, nil }, - pins: d, + pins: &d.pins, }) if err != nil { panic(fmt.Sprintf("fejkdata: %q: %v; a fence should have refused this at New", a.name, err)) @@ -205,9 +72,9 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw // readHold is the hold a held read keeps its draw in: for a reference that reads a path, // the render's hold for its group, so its draw spans the render; for a sibling, or a // reference read whole, held. -func readHold(s *session, held *hold, sc renderScope, a arm) *hold { +func readHold(held *hold, sc renderScope, a arm) *hold { if isRef(a.key) && len(a.tail) > 0 { - return sc.hold(s) + return sc.hold() } return held } @@ -221,7 +88,7 @@ func renderLeaf(s *session, n node, sc renderScope) draw { } r := draw{text: render(s, n, sc)} if t, _ := n.(*template); t != nil && t.readsColumn != nil { - r.null = sc.in(t).hold(s).value[t.readsColumn.a.path].null + r.null = sc.in(t).hold().value[t.readsColumn.a.path].null } return r } diff --git a/holdfence.go b/holdfence.go index 562c915..63380d3 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(n, tail, pathWalk{ + _, _ = walkPath(nil, 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/holdset.go b/holdset.go index 46f648a..303509e 100644 --- a/holdset.go +++ b/holdset.go @@ -20,18 +20,14 @@ type renderScope struct { } // eagerHoldSet makes the unnamed group's maps where the set is declared, keeping them on that frame's -// stack for a render that reads through them; lazyHoldSet makes them on its first read instead. -func eagerHoldSet(s *session) holdSet { - return holdSet{unnamed: hold{variant: map[string]node{}, value: map[string]draw{}, s: s}} -} - -func lazyHoldSet(s *session) holdSet { - return holdSet{unnamed: hold{s: s}} +// stack for a render that reads through them; a zero holdSet makes them on its first read instead. +func eagerHoldSet() holdSet { + return holdSet{unnamed: hold{variant: map[string]node{}, value: map[string]draw{}}} } // renderOnce renders n as one render, over a hold set of its own. func renderOnce(s *session, n node) string { - set := lazyHoldSet(s) + var set holdSet return render(s, n, renderScope{set: &set}) } @@ -40,7 +36,7 @@ func renderOnce(s *session, n node) string { // //go:noinline func expandAnew(s *session, t *template) string { - set := lazyHoldSet(s) + var set holdSet return expand(s, t, renderScope{set: &set}) } @@ -52,9 +48,8 @@ func (sc renderScope) in(t *template) renderScope { return sc } -// hold is the set's hold for sc's draw group. The session is passed in rather than -// read out of the set: copying it from there would leak the set's maps to the heap. -func (sc renderScope) hold(s *session) *hold { +// hold is the set's hold for sc's draw group. +func (sc renderScope) hold() *hold { if sc.group == "" { return &sc.set.unnamed } @@ -63,7 +58,7 @@ func (sc renderScope) hold(s *session) *hold { if sc.set.named == nil { sc.set.named = map[string]*hold{} } - d = &hold{variant: map[string]node{}, value: map[string]draw{}, s: s} + d = &hold{variant: map[string]node{}, value: map[string]draw{}} sc.set.named[strings.Clone(sc.group)] = d // a key from sc would leak sc, and with it every render's hold set, to the heap } return d diff --git a/path.go b/path.go index 20fa50e..4b388d8 100644 --- a/path.go +++ b/path.go @@ -127,27 +127,27 @@ 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 the pins' session, or stops; 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. +// the walk draws one, or stops; 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. type pathWalk struct { choice func(c *choice, rest []string) ([]node, error) level func(t *template, rest []string) error leaf func(n node) error - pins *hold + pins *pinSet } // 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. -func walkPath(n node, tail []string, w pathWalk) (node, error) { +// the data holds. s draws rows and variants; nil, the walk only proves the path. +func walkPath(s *session, 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(t, tail, w, false) + return walkTable(s, 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])) @@ -158,7 +158,7 @@ func walkPath(n node, tail []string, w pathWalk) (node, error) { if !ok { return nil, fmt.Errorf("no entry %q", tail[0]) } - return walkPath(child, tail[1:], w) + return walkPath(s, child, tail[1:], w) case *template: if w.level != nil { if err := w.level(n, tail); err != nil { @@ -169,9 +169,9 @@ func walkPath(n node, tail []string, w pathWalk) (node, error) { if !ok { return nil, fmt.Errorf("no field %q", tail[0]) } - return walkPath(child, tail[1:], w) + return walkPath(s, child, tail[1:], w) case *choice: - return walkChoice(n, tail, w) + return walkChoice(s, 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]) } @@ -186,8 +186,8 @@ func (w pathWalk) atLeaf(n node) error { } // walkChoice continues a walk into the variants w.choice returns, or into one drawn -// by the pins' session where the walk names no choice action. -func walkChoice(c *choice, tail []string, w pathWalk) (node, error) { +// by s where the walk names no choice action. +func walkChoice(s *session, c *choice, tail []string, w pathWalk) (node, error) { if w.choice == nil { if w.pins == nil { return nil, nil @@ -195,10 +195,10 @@ func walkChoice(c *choice, tail []string, w pathWalk) (node, error) { if err := carriedByAll(c, tail); err != nil { return nil, err } - if w.pins.s == nil { // a probe: every variant carries the tail, so any one proves it - 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(pick(w.pins.s, c), tail, w) + return walkPath(s, pick(s, c), tail, w) } next, err := w.choice(c, tail) if err != nil { @@ -206,7 +206,7 @@ func walkChoice(c *choice, tail []string, w pathWalk) (node, error) { } var last node for _, item := range next { - if last, err = walkPath(item, tail, w); err != nil { + if last, err = walkPath(s, item, tail, w); err != nil { return nil, err } } @@ -217,7 +217,7 @@ func walkChoice(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(t *table, tail []string, w pathWalk, descended bool) (node, error) { +func walkTable(s *session, t *table, tail []string, w pathWalk, descended bool) (node, error) { sel, tail, err := t.selector(tail) if err != nil { return nil, err @@ -229,19 +229,19 @@ func walkTable(t *table, tail []string, w pathWalk, descended bool) (node, error // 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(w.pins, t, sel, (descended || len(tail) > 0) && !hasSelector(tail)); err != nil { + if err := readRow(s, w.pins, t, sel, (descended || len(tail) > 0) && !hasSelector(tail)); err != nil { return nil, err } } switch { case len(tail) == 0 && sel == "" && !descended: - return walkPath(t, nil, w) + return walkPath(s, t, nil, w) case len(tail) == 0: - return walkPath(t.whole, nil, w) + return walkPath(s, t.whole, nil, w) case child != nil: - return walkTable(child, tail[1:], w, true) + return walkTable(s, child, tail[1:], w, true) } - return walkPath(column, tail[1:], w) + return walkPath(s, column, tail[1:], w) } // selector splits the selector a tail starts with from the rest of it. @@ -272,12 +272,12 @@ 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(d *hold, t *table, sel string, draw bool) error { +func readRow(s *session, pins *pinSet, t *table, sel string, draw bool) error { if sel != "" { - return d.selectRow(t, sel) + return pins.selectRow(t, sel) } - if draw && d.s != nil { - d.rowOf(t) + if draw && s != nil { + pins.rowOf(s, t) } return nil } @@ -312,8 +312,8 @@ func unreachableInChoice(c *choice, want string) error { // carries a repeat or a drawGroup, which one draw of it could not apply. 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 { - var pins hold - _, err := walkPath(n, tail, pathWalk{ + var pins pinSet + _, err := walkPath(nil, 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 4a8ce24..f4260d0 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(n, []string{"a", "b"}, walk); err != nil { + if _, err := walkPath(nil, 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(n, []string{"a", "nope", "deeper"}, walk) + _, err := walkPath(nil, 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(n, []string{"f"}, pathWalk{ + _, err := walkPath(nil, 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 new file mode 100644 index 0000000..7014880 --- /dev/null +++ b/pins.go @@ -0,0 +1,192 @@ +package fejkdata + +import ( + "fmt" + "maps" + "sort" + "strings" +) + +// tablePin is one table's pinned row. +type tablePin struct { + t *table + row int +} + +// pinSet is the rows of tables fixed so far: by a render, by a path read replayed at load, and by +// a fence walk over the rows a render can enter. +type pinSet struct { + pins [8]tablePin // inline, so a render over a country's five-deep geo tree stays off the heap + n int + spill map[*table]int +} + +// pinned is the row pinned for t, if any. +func (p *pinSet) pinned(t *table) (int, bool) { + for _, q := range p.pins[:p.n] { + if q.t == t { + return q.row, true + } + } + r, ok := p.spill[t] + return r, ok +} + +// mustRow is the row pinned for t, which the walk reaching a column pinned. +func (p *pinSet) mustRow(t *table) int { + r, ok := p.pinned(t) + if !ok { + panic(fmt.Sprintf("fejkdata: a column of %s is rendered with no row pinned", t.category)) + } + return r +} + +// add pins row r of t alone. +func (p *pinSet) add(t *table, r int) { + if p.n < len(p.pins) { + p.pins[p.n] = tablePin{t, r} + p.n++ + return + } + if p.spill == nil { + p.spill = map[*table]int{} + } + p.spill[t] = r +} + +// pin pins row r of t, and the rows of t's ancestors it links to. +func (p *pinSet) pin(t *table, r int) { + for { + if _, done := p.pinned(t); done { + return + } + p.add(t, r) + if t.parentT == nil { + return + } + t, r = t.parentT, t.parentRow(r) + } +} + +// each calls fn for every pinned row, in pin order, the spilled ones by table name. +func (p *pinSet) each(fn func(t *table, r int)) { + for _, q := range p.pins[:p.n] { + fn(q.t, q.row) + } + spilled := make([]*table, 0, len(p.spill)) + for t := range p.spill { + spilled = append(spilled, t) + } + sort.Slice(spilled, func(i, j int) bool { return spilled[i].category < spilled[j].category }) + for _, t := range spilled { + fn(t, p.spill[t]) + } +} + +// 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 { + if pr, ok := p.pinned(t); ok && pr != r { + return t + } + for a := t.parentT; a != nil; a = a.parentT { + if pa, ok := p.pinned(a); ok && !t.under(r, a, pa) { + return a + } + } + return nil +} + +// pinRow pins row r of t where it agrees with the rows pinned before it. +func (p *pinSet) pinRow(t *table, r int) error { + switch a := p.clash(t, r); { + case a == t: + pr, _ := p.pinned(t) + return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.path) + case a != nil: + pa, _ := p.pinned(a) + return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa)) + } + p.pin(t, r) + return nil +} + +// selectRow pins the row a selector names. +func (p *pinSet) selectRow(t *table, sel string) error { + r, err := t.find(sel, p) + if err != nil { + return err + } + return p.pinRow(t, r) +} + +// inside keeps the rows of t that sit inside every pinned ancestor. +func (p *pinSet) inside(t *table, rows []int) []int { + for a := t.parentT; a != nil; a = a.parentT { + pa, ok := p.pinned(a) + if !ok { + continue + } + var kept []int + for _, r := range rows { + if t.under(r, a, pa) { + kept = append(kept, r) + } + } + rows = kept + } + return rows +} + +// entered is p with row r of t: pinned where the render pins it, alone where the render draws it +// to render whole. p is left as it was, since a fence walk branches. +func (p pinSet) entered(t *table, r int, pins bool) pinSet { + p.spill = maps.Clone(p.spill) + switch _, in := p.pinned(t); { + case pins: + p.pin(t, r) + case !in: + p.add(t, r) + } + return p +} + +// excludes reports whether a cell's row cannot render with the rows pinned. +func (p *pinSet) excludes(cell *template) bool { return p.clash(cell.cellOf, cell.cellRow) != nil } + +// key spells the set for a map, by the tables' identities in path order. +func (p *pinSet) key() string { + var pins []tablePin + p.each(func(t *table, r int) { pins = append(pins, tablePin{t, r}) }) + sort.Slice(pins, func(i, j int) bool { return pins[i].t.path < pins[j].t.path }) + var b strings.Builder + for _, q := range pins { + fmt.Fprintf(&b, "%p[%d]", q.t, q.row) + } + return b.String() +} diff --git a/record.go b/record.go index c98a892..9ef6a80 100644 --- a/record.go +++ b/record.go @@ -130,7 +130,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) { if err != nil { return nil, fmt.Errorf("fejkdata: %s: %w", path, err) } - set := eagerHoldSet(f.rand) + set := eagerHoldSet() sc := renderScope{set: &set} if t, isTable := n.(*table); isTable { if n, err = tableRecord(f.rand, t, tail, sc); err != nil { @@ -159,7 +159,7 @@ func tableRecord(s *session, t *table, tail []string, sc renderScope) (node, err } switch n := n.(type) { case *table: - sc.hold(s).rowOf(n) + sc.hold().pins.rowOf(s, n) return n, nil case *row: return n.t, nil @@ -204,7 +204,7 @@ type RecordTemplate struct { func (t *RecordTemplate) Fake() *Record { t.g.mu.Lock() defer t.g.mu.Unlock() - set := eagerHoldSet(t.g.rand) + set := eagerHoldSet() return renderRecord(t.g.rand, t.t, t.columns, renderScope{set: &set}) } @@ -265,7 +265,7 @@ func recordOf(n node) (*template, []Column, error) { func renderRecord(s *session, t *template, columns []Column, sc renderScope) *Record { sc = sc.in(t) if t.table != nil { - sc.t, sc.row = t.table, sc.hold(s).mustRow(t.table) + sc.t, sc.row = t.table, sc.hold().pins.mustRow(t.table) } r := &Record{columns: append([]Column(nil), columns...)} for i := range r.columns { diff --git a/render.go b/render.go index c51115c..3ff95ba 100644 --- a/render.go +++ b/render.go @@ -23,7 +23,7 @@ func (f *Generator) Fake(path string) (string, error) { if err != nil { return "", fmt.Errorf("fejkdata: %w", err) } - f.set = lazyHoldSet(f.rand) + f.set = holdSet{} sc := renderScope{set: &f.set} f.root.children = f.categories n, err := descend(f.rand, &f.root, segments, sc) @@ -44,11 +44,11 @@ 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. - var probe hold - if _, err := walkPath(root, segments, pathWalk{pins: &probe}); err != nil { + var probe pinSet + if _, err := walkPath(nil, root, segments, pathWalk{pins: &probe}); err != nil { return nil, err } - return walkPath(root, segments, pathWalk{pins: sc.hold(s)}) + return walkPath(s, root, segments, pathWalk{pins: &sc.hold().pins}) } // render evaluates a compiled node to a string. compile validates every node up @@ -64,11 +64,11 @@ func render(s *session, n node, sc renderScope) string { sc.t, sc.row = n, n.draw(s) return expand(s, n.format, sc) case *row: - sc.t, sc.row = n.t, sc.hold(s).mustRow(n.t) + sc.t, sc.row = n.t, sc.hold().pins.mustRow(n.t) return expand(s, n.t.format, sc) case *column: if sc.t != n.t { - sc.t, sc.row = n.t, sc.hold(s).mustRow(n.t) + sc.t, sc.row = n.t, sc.hold().pins.mustRow(n.t) } if cell := n.t.cellNode(sc.row, n.i); cell != nil { return render(s, cell, sc) diff --git a/struct.go b/struct.go index de3ef95..aeb5e2c 100644 --- a/struct.go +++ b/struct.go @@ -349,7 +349,7 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro // fill draws the record into v's tagged fields, then each nested struct as a record of its own. func (s *structShape) fill(sess *session, v reflect.Value) { if s.record != nil { - set := eagerHoldSet(sess) + set := eagerHoldSet() for i, c := range renderRecord(sess, s.record, s.columns, renderScope{set: &set}).columns { setColumn(fieldAt(v, s.fields[i]), c) } diff --git a/table.go b/table.go index 6060f07..df254cb 100644 --- a/table.go +++ b/table.go @@ -524,8 +524,8 @@ func (t *table) under(r int, a *table, pr int) bool { } // find is the row a selector names: by key first, then by name, where a name -// naming several rows resolves inside the ancestors pinned in d. -func (t *table) find(sel string, d *hold) (int, error) { +// naming several rows resolves inside the ancestors pinned. +func (t *table) find(sel string, pins *pinSet) (int, error) { if t.key < 0 && t.name < 0 { return 0, fmt.Errorf("%s has no key or name column to select a row by", t.path) } @@ -534,7 +534,7 @@ func (t *table) find(sel string, d *hold) (int, error) { } rows := t.indexed().byName[sel] if len(rows) > 1 { - rows = d.inside(t, rows) + rows = pins.inside(t, rows) } switch len(rows) { case 1: diff --git a/template_test.go b/template_test.go index bd5a9e3..f4741cf 100644 --- a/template_test.go +++ b/template_test.go @@ -316,7 +316,7 @@ func TestGrowIsALowerBound(t *testing.T) { t.Fatalf("format %q did not compile to a template", format) } for i := 0; i < 50; i++ { - set := lazyHoldSet(f.rand) + var set holdSet if got := len(expand(f.rand, tmpl, renderScope{set: &set})); got < tmpl.grow { t.Errorf("format %q: expand emitted %d bytes, below grow %d", format, got, tmpl.grow) } diff --git a/todo.md b/todo.md index a51120d..378d08e 100644 --- a/todo.md +++ b/todo.md @@ -22,10 +22,6 @@ by nothing but tests, whose premise lives only in the README's Decisions. This round, in order: -- Give render and the draw fences one pin model: a pin set `hold.pin`, `hold.rowOf` - and the fence's `rowSet` all use, so a change to how a render pins rows cannot leave - the fence proving the old rule, and `hold` stops being a proof scratchpad whenever its - session is nil. - Move the README's Decisions section to `docs/decisions.md`, leaving a one-line index of the titles in `AGENTS.md`, and drop the `AGENTS.md` line pointing decisions at the README. Give each fence a bare link to the entry it enforces: every seat needed @@ -46,7 +42,7 @@ 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 `pins.s` is, and `walkChoice` returns `nil, nil` where + callbacks are nil and whether its session is, and `walkChoice` returns `nil, nil` where none is set. - Register a transform once: `transforms` restates the `lowercase`, `uppercase` and `ascii` entries in `builtins`, and `valueProof.template` classifies by the first while @@ -287,7 +283,7 @@ because pairing a street with its exact postnummer rewrites shipped rows. - Hold every change the charter says owes a `CHANGELOG.md` entry to one in CI: the check covers `data` and `testdata/shipped_shape.txt` alone, where `AGENTS.md` adds a flag, an exit code, an exported name, a fence, a builtin and the lowest Go. -- Measure the allocation gate over the shipped `geo` trees too: `hold.pins [8]` is sized +- Measure the allocation gate over the shipped `geo` trees too: `pinSet.pins [8]` is sized for them, and `perf_test.go` checks a synthetic five-deep tree only. - Make a named draw group's hold lazy, `&hold{s: s}` in `renderScope.hold`: it escapes to the heap anyway, so its two eager maps buy no stack and cost two allocations per