Give render and the draw fences one pin set

This commit is contained in:
2026-09-24 00:17:01 +02:00
committed by Lillem4n
parent 968d79399d
commit 3927dc9fa5
17 changed files with 279 additions and 284 deletions
+3 -2
View File
@@ -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 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 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 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 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 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 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 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 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 builtins.go the {name()} function registry and its implementations
+2 -1
View File
@@ -28,7 +28,8 @@ package fejkdata
// - draw group — reference draws held apart inside one render: // - draw group — reference draws held apart inside one render:
// `template.drawGroup` as the data spells it, `template.drawGroupKey` as a // `template.drawGroup` as the data spells it, `template.drawGroupKey` as a
// render reads it, `holdSet`, `renderScope`. // 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 // - family — a table and every table reaching it through a chain of
// `table.parentT`, named by the root that chain ends at: `table.family`. // `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 // - whole — a read that lands on a `row`, so the row renders through its table's
+3 -3
View File
@@ -180,7 +180,7 @@ type drawWalk struct {
type drawAt struct { type drawAt struct {
group string group string
route drawRoute 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 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 continue
} }
in := at 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) w.edge(n, e, in)
continue continue
} }
@@ -251,7 +251,7 @@ func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
w.reads = append(w.reads, pathRead{at, a, tr}) w.reads = append(w.reads, pathRead{at, a, tr})
} }
if tr != nil { 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) w.walk(e.to, at)
+11 -68
View File
@@ -2,7 +2,6 @@ package fejkdata
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
) )
@@ -12,7 +11,7 @@ import (
// it lands on a row rendered whole. // it lands on a row rendered whole.
type tableRead struct { type tableRead struct {
head *table head *table
pins hold pins pinSet
drawn map[*table]bool drawn map[*table]bool
sels []tableSel sels []tableSel
whole bool whole bool
@@ -34,7 +33,7 @@ func tableReadOf(head node, a arm, leaf node) *tableRead {
return nil return nil
} }
tr := &tableRead{head: t, drawn: map[*table]bool{}} 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))] written := a.name[:len(a.name)-len(joinSegments(a.tail))]
cur := t cur := t
tr.draws(cur) 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) { if o.tr == nil || o.at.group != r.at.group || alternatives(r.at, o.at) {
continue continue
} }
var d hold var d pinSet
if err := r.tr.replay(&d); err != nil { if err := r.tr.replay(&d); err != nil {
return conflict(r, err) 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. // 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 var err error
r.pins.each(func(t *table, row int) { r.pins.each(func(t *table, row int) {
if err == nil { 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. // 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 var found *table
pins.each(func(t *table, _ int) { pins.each(func(t *table, _ int) {
if found == nil && r.drawn[t] { if found == nil && r.drawn[t] {
@@ -226,69 +225,13 @@ func checkOwnFamily(t *template) error {
return nil 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. // alternatives reports whether two reads sit in different rows of one table.
func alternatives(a, b drawAt) bool { func alternatives(a, b drawAt) bool {
for _, p := range a.alt { found := false
if r, in := b.alt.rowOf(p.t); in && r != p.row { a.alt.each(func(t *table, r int) {
return true if br, in := b.alt.pinned(t); in && br != r {
found = true
} }
} })
return false return found
} }
+1 -1
View File
@@ -178,7 +178,7 @@ func renderEdges(n node) []renderEdge {
// already proved the tail resolves in every variant. // already proved the tail resolves in every variant.
func pathLeaves(n node, tail []string) []node { func pathLeaves(n node, tail []string) []node {
var out []node var out []node
_, _ = walkPath(n, tail, pathWalk{ _, _ = walkPath(nil, n, tail, pathWalk{
choice: func(c *choice, _ []string) ([]node, error) { return c.items, nil }, choice: func(c *choice, _ []string) ([]node, error) { return c.items, nil },
leaf: func(n node) error { out = append(out, n); return nil }, leaf: func(n node) error { out = append(out, n); return nil },
}) })
+9 -142
View File
@@ -2,26 +2,16 @@ package fejkdata
import ( import (
"fmt" "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, // 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 // 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 // spelling, so the same read written twice reads one draw, and the table rows its reads pinned. An
// its sibling names, and a render one per group for its reference paths. // expansion keeps one for its sibling names, and a render one per group for its reference paths.
type hold struct { type hold struct {
variant map[string]node variant map[string]node
value map[string]draw 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 pins pinSet
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
} }
// draw is what one read drew: its text, and whether it landed on a null. // draw is what one read drew: its text, and whether it landed on a null.
@@ -30,129 +20,6 @@ type draw struct {
null bool 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 // 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 // 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 // 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)} 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 { if r, done := d.value[a.path]; done {
return r 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 // Hold the draw at every level passed through, so two paths sharing a
// prefix share it. // prefix share it.
choice: func(c *choice, rest []string) ([]node, error) { 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 return []node{n}, nil
}, },
pins: d, pins: &d.pins,
}) })
if err != nil { if err != nil {
panic(fmt.Sprintf("fejkdata: %q: %v; a fence should have refused this at New", a.name, err)) 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, // 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 // the render's hold for its group, so its draw spans the render; for a sibling, or a
// reference read whole, held. // 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 { if isRef(a.key) && len(a.tail) > 0 {
return sc.hold(s) return sc.hold()
} }
return held return held
} }
@@ -221,7 +88,7 @@ func renderLeaf(s *session, n node, sc renderScope) draw {
} }
r := draw{text: render(s, n, sc)} r := draw{text: render(s, n, sc)}
if t, _ := n.(*template); t != nil && t.readsColumn != nil { 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 return r
} }
+1 -1
View File
@@ -106,7 +106,7 @@ func operandReader(t *template, head string) string {
// coverPath collects what holding one path pins: every choice level the path // coverPath collects what holding one path pins: every choice level the path
// passes through, whole, and the leaf it renders. // passes through, whole, and the leaf it renders.
func coverPath(n node, tail []string, into map[node]bool) { 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 }, 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 }, leaf: func(n node) error { cover(n, into, false); return nil },
}) })
+8 -13
View File
@@ -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 // 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. // stack for a render that reads through them; a zero holdSet makes them on its first read instead.
func eagerHoldSet(s *session) holdSet { func eagerHoldSet() holdSet {
return holdSet{unnamed: hold{variant: map[string]node{}, value: map[string]draw{}, s: s}} return holdSet{unnamed: hold{variant: map[string]node{}, value: map[string]draw{}}}
}
func lazyHoldSet(s *session) holdSet {
return holdSet{unnamed: hold{s: s}}
} }
// renderOnce renders n as one render, over a hold set of its own. // renderOnce renders n as one render, over a hold set of its own.
func renderOnce(s *session, n node) string { func renderOnce(s *session, n node) string {
set := lazyHoldSet(s) var set holdSet
return render(s, n, renderScope{set: &set}) return render(s, n, renderScope{set: &set})
} }
@@ -40,7 +36,7 @@ func renderOnce(s *session, n node) string {
// //
//go:noinline //go:noinline
func expandAnew(s *session, t *template) string { func expandAnew(s *session, t *template) string {
set := lazyHoldSet(s) var set holdSet
return expand(s, t, renderScope{set: &set}) return expand(s, t, renderScope{set: &set})
} }
@@ -52,9 +48,8 @@ func (sc renderScope) in(t *template) renderScope {
return sc return sc
} }
// hold is the set's hold for sc's draw group. The session is passed in rather than // hold is the set's hold for sc's draw group.
// read out of the set: copying it from there would leak the set's maps to the heap. func (sc renderScope) hold() *hold {
func (sc renderScope) hold(s *session) *hold {
if sc.group == "" { if sc.group == "" {
return &sc.set.unnamed return &sc.set.unnamed
} }
@@ -63,7 +58,7 @@ func (sc renderScope) hold(s *session) *hold {
if sc.set.named == nil { if sc.set.named == nil {
sc.set.named = map[string]*hold{} 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 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 return d
+29 -29
View File
@@ -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 // 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 // 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 // the walk draws one, or stops; level runs at each template a segment descends
// a segment descends into; leaf runs where the tail ends; pins is where the walk // into; leaf runs where the tail ends; pins is where the walk pins the table rows
// pins the table rows it selects and draws, nil skipping tables. A nil action is // it selects and draws, nil skipping tables. A nil action is skipped. A render's
// skipped. A render's walk sets only pins, so it allocates nothing. // walk sets only pins, so it allocates nothing.
type pathWalk struct { type pathWalk struct {
choice func(c *choice, rest []string) ([]node, error) choice func(c *choice, rest []string) ([]node, error)
level func(t *template, rest []string) error level func(t *template, rest []string) error
leaf func(n node) 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 // 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 // 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 // table by walkTable. A missing segment is an error, so no walk reaches past what
// the data holds. // the data holds. s draws rows and variants; nil, the walk only proves the path.
func walkPath(n node, tail []string, w pathWalk) (node, error) { func walkPath(s *session, n node, tail []string, w pathWalk) (node, error) {
if len(tail) == 0 { if len(tail) == 0 {
return n, w.atLeaf(n) return n, w.atLeaf(n)
} }
if t, isTable := n.(*table); isTable { if t, isTable := n.(*table); isTable {
return walkTable(t, tail, w, false) return walkTable(s, t, tail, w, false)
} }
if isSelector(tail[0]) { if isSelector(tail[0]) {
return nil, fmt.Errorf("%s is not a table, so it has no row to select", selectorOf(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 { if !ok {
return nil, fmt.Errorf("no entry %q", tail[0]) return nil, fmt.Errorf("no entry %q", tail[0])
} }
return walkPath(child, tail[1:], w) return walkPath(s, child, tail[1:], w)
case *template: case *template:
if w.level != nil { if w.level != nil {
if err := w.level(n, tail); err != 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 { if !ok {
return nil, fmt.Errorf("no field %q", tail[0]) return nil, fmt.Errorf("no field %q", tail[0])
} }
return walkPath(child, tail[1:], w) return walkPath(s, child, tail[1:], w)
case *choice: case *choice:
return walkChoice(n, tail, w) return walkChoice(s, n, tail, w)
case *column: 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]) 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 // 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. // by s where the walk names no choice action.
func walkChoice(c *choice, tail []string, w pathWalk) (node, error) { func walkChoice(s *session, c *choice, tail []string, w pathWalk) (node, error) {
if w.choice == nil { if w.choice == nil {
if w.pins == nil { if w.pins == nil {
return nil, 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 { if err := carriedByAll(c, tail); err != nil {
return nil, err return nil, err
} }
if w.pins.s == nil { // a probe: every variant carries the tail, so any one proves it if s == nil { // a probe: every variant carries the tail, so any one proves it
return walkPath(c.items[0], tail, w) 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) next, err := w.choice(c, tail)
if err != nil { if err != nil {
@@ -206,7 +206,7 @@ func walkChoice(c *choice, tail []string, w pathWalk) (node, error) {
} }
var last node var last node
for _, item := range next { 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 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 // 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 // table was reached from another; a table reached whole, with no selector, is left
// to a render's own draw. // 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) sel, tail, err := t.selector(tail)
if err != nil { if err != nil {
return nil, err 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 // 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. // where none follows; drawing first could pick a row the selector is not inside.
if w.pins != nil { 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 return nil, err
} }
} }
switch { switch {
case len(tail) == 0 && sel == "" && !descended: case len(tail) == 0 && sel == "" && !descended:
return walkPath(t, nil, w) return walkPath(s, t, nil, w)
case len(tail) == 0: case len(tail) == 0:
return walkPath(t.whole, nil, w) return walkPath(s, t.whole, nil, w)
case child != nil: 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. // 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 // 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. // 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 != "" { if sel != "" {
return d.selectRow(t, sel) return pins.selectRow(t, sel)
} }
if draw && d.s != nil { if draw && s != nil {
d.rowOf(t) pins.rowOf(s, t)
} }
return nil 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 // 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. // that validates here resolves on every render, and a typo is a New-time error.
func checkPath(n node, tail []string, level string) error { func checkPath(n node, tail []string, level string) error {
var pins hold var pins pinSet
_, err := walkPath(n, tail, pathWalk{ _, err := walkPath(nil, n, tail, pathWalk{
choice: func(c *choice, rest []string) ([]node, error) { choice: func(c *choice, rest []string) ([]node, error) {
if err := carriedByAll(c, rest); err != nil { if err := carriedByAll(c, rest); err != nil {
return nil, err return nil, err
+3 -3
View File
@@ -13,14 +13,14 @@ func TestWalkPathStopsAtAMissingSegment(t *testing.T) {
level: func(tm *template, rest []string) error { seen = append(seen, "level:"+rest[0]); return nil }, 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 }, 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) t.Fatalf("walkPath(a.b) = %v", err)
} }
if want := []string{"level:a", "level:b", "leaf"}; !slices.Equal(seen, want) { if want := []string{"level:a", "level:b", "leaf"}; !slices.Equal(seen, want) {
t.Errorf("walk visited %v, want %v", seen, want) t.Errorf("walk visited %v, want %v", seen, want)
} }
seen = nil 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"`) { if err == nil || !strings.Contains(err.Error(), `no field "nope"`) {
t.Errorf("walkPath(a.nope.deeper) = %v, want the missing segment named", err) 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) { func TestWalkPathChoiceConsumesNoSegment(t *testing.T) {
n := compiled(t, `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`) n := compiled(t, `[{"format":"{f}","f":"1"},{"format":"{f}","f":"2"}]`)
var leaves []node var leaves []node
_, err := walkPath(n, []string{"f"}, pathWalk{ _, err := walkPath(nil, n, []string{"f"}, pathWalk{
choice: func(c *choice, rest []string) ([]node, error) { choice: func(c *choice, rest []string) ([]node, error) {
if len(rest) != 1 || rest[0] != "f" { if len(rest) != 1 || rest[0] != "f" {
t.Errorf("choice saw rest %v, want [f]", rest) t.Errorf("choice saw rest %v, want [f]", rest)
+192
View File
@@ -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()
}
+4 -4
View File
@@ -130,7 +130,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err) return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
} }
set := eagerHoldSet(f.rand) set := eagerHoldSet()
sc := renderScope{set: &set} sc := renderScope{set: &set}
if t, isTable := n.(*table); isTable { if t, isTable := n.(*table); isTable {
if n, err = tableRecord(f.rand, t, tail, sc); err != nil { 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) { switch n := n.(type) {
case *table: case *table:
sc.hold(s).rowOf(n) sc.hold().pins.rowOf(s, n)
return n, nil return n, nil
case *row: case *row:
return n.t, nil return n.t, nil
@@ -204,7 +204,7 @@ type RecordTemplate struct {
func (t *RecordTemplate) Fake() *Record { func (t *RecordTemplate) Fake() *Record {
t.g.mu.Lock() t.g.mu.Lock()
defer t.g.mu.Unlock() defer t.g.mu.Unlock()
set := eagerHoldSet(t.g.rand) set := eagerHoldSet()
return renderRecord(t.g.rand, t.t, t.columns, renderScope{set: &set}) 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 { func renderRecord(s *session, t *template, columns []Column, sc renderScope) *Record {
sc = sc.in(t) sc = sc.in(t)
if t.table != nil { 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...)} r := &Record{columns: append([]Column(nil), columns...)}
for i := range r.columns { for i := range r.columns {
+6 -6
View File
@@ -23,7 +23,7 @@ func (f *Generator) Fake(path string) (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %w", err) return "", fmt.Errorf("fejkdata: %w", err)
} }
f.set = lazyHoldSet(f.rand) f.set = holdSet{}
sc := renderScope{set: &f.set} sc := renderScope{set: &f.set}
f.root.children = f.categories f.root.children = f.categories
n, err := descend(f.rand, &f.root, segments, sc) n, err := descend(f.rand, &f.root, segments, sc)
@@ -44,11 +44,11 @@ func (f *Generator) Fake(path string) (string, error) {
// every call. // every call.
func descend(s *session, root node, segments []string, sc renderScope) (node, 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. // Walked once without drawing first, so a path that fails moves no seeded stream.
var probe hold var probe pinSet
if _, err := walkPath(root, segments, pathWalk{pins: &probe}); err != nil { if _, err := walkPath(nil, root, segments, pathWalk{pins: &probe}); err != nil {
return nil, err 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 // 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) sc.t, sc.row = n, n.draw(s)
return expand(s, n.format, sc) return expand(s, n.format, sc)
case *row: 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) return expand(s, n.t.format, sc)
case *column: case *column:
if sc.t != n.t { 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 { if cell := n.t.cellNode(sc.row, n.i); cell != nil {
return render(s, cell, sc) return render(s, cell, sc)
+1 -1
View File
@@ -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. // 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) { func (s *structShape) fill(sess *session, v reflect.Value) {
if s.record != nil { if s.record != nil {
set := eagerHoldSet(sess) set := eagerHoldSet()
for i, c := range renderRecord(sess, s.record, s.columns, renderScope{set: &set}).columns { for i, c := range renderRecord(sess, s.record, s.columns, renderScope{set: &set}).columns {
setColumn(fieldAt(v, s.fields[i]), c) setColumn(fieldAt(v, s.fields[i]), c)
} }
+3 -3
View File
@@ -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 // 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. // naming several rows resolves inside the ancestors pinned.
func (t *table) find(sel string, d *hold) (int, error) { func (t *table) find(sel string, pins *pinSet) (int, error) {
if t.key < 0 && t.name < 0 { 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) 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] rows := t.indexed().byName[sel]
if len(rows) > 1 { if len(rows) > 1 {
rows = d.inside(t, rows) rows = pins.inside(t, rows)
} }
switch len(rows) { switch len(rows) {
case 1: case 1:
+1 -1
View File
@@ -316,7 +316,7 @@ func TestGrowIsALowerBound(t *testing.T) {
t.Fatalf("format %q did not compile to a template", format) t.Fatalf("format %q did not compile to a template", format)
} }
for i := 0; i < 50; i++ { 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 { 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) t.Errorf("format %q: expand emitted %d bytes, below grow %d", format, got, tmpl.grow)
} }
+2 -6
View File
@@ -22,10 +22,6 @@ by nothing but tests, whose premise lives only in the README's Decisions.
This round, in order: 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 - 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 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 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 `operandReader`, `checkNoRepeatedRead` and `compileOps`, and function-versus-field is
decided by `indexOutside(body, '(')` in two of them and by `funcCall` in the rest. 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 - 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. none is set.
- Register a transform once: `transforms` restates the `lowercase`, `uppercase` and - Register a transform once: `transforms` restates the `lowercase`, `uppercase` and
`ascii` entries in `builtins`, and `valueProof.template` classifies by the first while `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 - 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 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. 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. 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 - 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 to the heap anyway, so its two eager maps buy no stack and cost two allocations per