Name the render's draws apart from the expansion hold: drawMemo, groupDraws, renderDraws

This commit is contained in:
2026-09-29 14:38:36 +02:00
committed by Lillem4n
parent 40f1129c23
commit b99966fe19
15 changed files with 106 additions and 107 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ In [docs/decisions.md](docs/decisions.md):
- A render shares one reference draw per category, per group
- A draw group name is local to its category
- The expansion hold and the render's draws are two fences
- A hold set makes its draw maps on the first read
- A render's draws make their maps on the first read
- A category never references itself, and a record's fences run at load
- A record's column set is fixed before the first draw
- Null is a `null` item, not a rate
+1 -1
View File
@@ -883,7 +883,7 @@ 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
format.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
hold.go the hold: one draw per held name; and the hold set and scope a render reads its reference paths through
draws.go the memo a hold or draw group keeps its draws in; and a render's draws and the scope it reads its reference paths through
holdfence.go the load fences over the hold: the routes and spellings that read one held name two ways
drawfence.go the load fences over reference draws: a drawGroup's, and a render's and a record's columns through the survey
readsurvey.go the load-time survey of what a render reads, and what a path reads of a table family, and its verdict on them; and the fence on a table reaching its own family
+7 -5
View File
@@ -19,15 +19,17 @@ package fejkdata
// - draw — one pick from a choice, or one row taken from a table: `pick`,
// `resolveChoice`, `table.drawRow`.
// - expansion — one render of one format: `expand`.
// - render — what owns one `holdSet`, and so what one reference draw spans:
// - render — what owns one `renderDraws`, and so what one reference draw spans:
// `Generator.Fake`, `FakeRecord`, `Template.Fake` and `RecordTemplate.Fake`
// start one each, `FakeStruct` one per record, and `expandAnew` one per
// iteration of a `template.repeat`.
// - hold — keeping one draw of a name, so every route to it reads that value:
// `formatOps.held`, `hold`, `groupHold`, `readHeld`.
// - draw group — reference draws held apart inside one render:
// - hold — one expansion keeping one draw of a name, so every route to it reads
// that value: `formatOps.held`, `formatOps.keepsHold`, `heldCheck`.
// - memo — what a hold or a draw group keeps its draws in, the variant drawn at
// each level and the value each path read: `drawMemo`, `readMemo`.
// - draw group — reference draws kept apart inside one render:
// `template.drawGroup` as the data spells it, `templateLink.drawGroupKey` as a
// render reads it, `holdSet`, `renderScope`.
// render reads it, `renderDraws`, `groupDraws`, `renderScope`.
// - pin — fixing which row of a table the render uses, which the draw fences replay:
// `pinSet`, `pinSet.pin`, `table.drawIn`.
// - family — a table and every table reaching it through a chain of
+5 -5
View File
@@ -242,11 +242,11 @@ groups. They pin different things — an operand pins the value its own render p
and stops at a reference, a path pins every level it passes through — so one walk would
carry both rules and both scopes anyway, and tell them apart at every step.
## A hold set makes its draw maps on the first read
## A render's draws make their maps on the first read
Every render starts from an empty hold set, its draw groups included, since goal 2 prices
a reader meeting two spellings of one empty set. Making the maps where the set is declared kept a
record's on its frame's stack, but a `Fake`'s set lives on the `Generator`, so there it
Every render starts from empty draws, its draw groups included, since goal 2 prices
a reader meeting two spellings of one empty set. Making the maps where the draws are declared kept a
record's on its frame's stack, but a `Fake`'s draws live on the `Generator`, so there it
cost the cheapest render two heap allocations and 240→390 ns, measured 2026-09-28.
Lazily, a render reading a reference path pays those two allocations, a record or struct
included, and one reading none pays nothing; goal 11 holds either way.
@@ -304,7 +304,7 @@ writes as the load check proved it; a `Value any` would hand every caller a type
## A unit takes the stem of what it is, and a file the stem of the units it holds
Goal 2 wants a name to reach one unit: `hold` is what a draw is kept in, `readValue` what
Goal 2 wants a name to reach one unit: `drawMemo` is what a draw is kept in, `readValue` what
a read drew.
## One name, one meaning
+20 -20
View File
@@ -2,18 +2,18 @@ package fejkdata
import "strings"
// hold is what one expansion has drawn for the names it holds: the variant each was drawn as, so
// drawMemo is what a hold or a draw group has drawn: the variant each level was drawn as, so
// every path under it reads one variant; and the value each read produced, by its path, so the
// same read written twice reads one value.
type hold struct {
type drawMemo struct {
variant map[string]node
value map[string]readValue
}
// groupHold is what one render's draw group has drawn for its reference paths: a hold, and the
// groupDraws is what one render's draw group has drawn for its reference paths: a memo, and the
// table rows those paths pinned.
type groupHold struct {
hold
type groupDraws struct {
memo drawMemo
pins pinSet
}
@@ -26,20 +26,20 @@ type readValue struct {
// renderTrace is a test's view of the fields a render reads; nil outside a test.
type renderTrace func(group string, row renderedRow, a arm)
// holdSet is one render's reference draws: the unnamed draw group's, and each named one's; and a
// test's trace of its reads.
type holdSet struct {
unnamed groupHold
named map[string]*groupHold
// renderDraws is one render's reference draws: the unnamed draw group's, and each named one's;
// and a test's trace of its reads.
type renderDraws struct {
unnamed groupDraws
named map[string]*groupDraws
trace renderTrace
}
// renderScope is where a render reads its reference paths: a hold set, in the draw group of the
// renderScope is where a render reads its reference paths: its reference draws, in the draw group of the
// template rendering; and row, the row of the table rendering, which its columns read. It passes
// by value, and what is read from it reaches a map key, an interface or a func value only as a
// copy; else sc, and with it every render's hold set, moves to the heap.
// copy; else sc, and with it every render's draws, move to the heap.
type renderScope struct {
set *holdSet
draws *renderDraws
group string
row renderedRow
}
@@ -77,17 +77,17 @@ func (sc renderScope) rowOf(t *table) int {
return sc.row.index
}
func (sc renderScope) groupHold() *groupHold {
func (sc renderScope) groupDraws() *groupDraws {
if sc.group == "" {
return &sc.set.unnamed
return &sc.draws.unnamed
}
d, drew := sc.set.named[sc.group]
d, drew := sc.draws.named[sc.group]
if !drew {
if sc.set.named == nil {
sc.set.named = map[string]*groupHold{}
if sc.draws.named == nil {
sc.draws.named = map[string]*groupDraws{}
}
d = &groupHold{}
sc.set.named[strings.Clone(sc.group)] = d
d = &groupDraws{}
sc.draws.named[strings.Clone(sc.group)] = d
}
return d
}
+2 -2
View File
@@ -386,7 +386,7 @@ type formatOps struct {
ops []op
grow int
held map[string]firstReach
heldLocal bool // some held name is not a reference path, so expand makes a hold
keepsHold bool // some held name is not a reference path, so expand makes a hold
}
// firstReach is how a format first reaches a held name: the reader holding it, for
@@ -401,7 +401,7 @@ func (c *formatOps) holdName(a arm, label string) {
c.held = map[string]firstReach{}
}
if !isRef(a.head) || len(a.tail) == 0 {
c.heldLocal = true
c.keepsHold = true
}
h, seen := c.held[a.head]
if !seen {
+1 -1
View File
@@ -771,7 +771,7 @@ func TestReadFieldPanicsOnAPathItCannotHold(t *testing.T) {
t.Fatal("not a template")
}
mustPanic(t, name, func() {
readField(engine(1).rand, tm, nil, renderScope{set: &holdSet{}}, a)
readField(engine(1).rand, tm, nil, renderScope{draws: &renderDraws{}}, a)
})
}
}
+10 -10
View File
@@ -348,14 +348,14 @@ func (w *pathProbe) readRow(t *table, tail []string, r tableRoute) error {
}
// pathDraw draws the rows and variants a proved path reads, pinning the rows in pins; pins is nil
// for a sibling path, which never crosses a table, since a table is only a category. For a held
// read, held keeps the variant drawn at each level of a, so paths sharing a prefix share it.
// for a sibling path, which never crosses a table, since a table is only a category. For a memoized
// read, memo keeps the variant drawn at each level of a, so paths sharing a prefix share it.
// Where pathCheck walks every variant of a choice, it walks the one drawn; at a table it selects
// as pathCheck does and draws the row the route asks for.
type pathDraw struct {
s *generatorState
pins *pinSet
held *hold
memo *drawMemo
a *arm
}
@@ -400,20 +400,20 @@ func (w *pathDraw) walk(n node, tail []string) (node, error) {
return n, nil
}
// 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.
// variant is the variant of c the walk continues into: the one memo keeps for this level
// of the read, drawn once, where the walk has a memo; else one drawn afresh.
func (w *pathDraw) variant(c *choice, rest []string) node {
if w.held == nil {
if w.memo == nil {
return pick(w.s, c)
}
key := w.a.levels[len(w.a.tail)-len(rest)]
n, drew := w.held.variant[key]
n, drew := w.memo.variant[key]
if !drew {
n = resolveChoice(w.s, c)
if w.held.variant == nil {
w.held.variant = map[string]node{}
if w.memo.variant == nil {
w.memo.variant = map[string]node{}
}
w.held.variant[key] = n
w.memo.variant[key] = n
}
return n
}
+3 -3
View File
@@ -55,7 +55,7 @@ func TestNoRenderAllocRegression(t *testing.T) {
}
}
// The repeat shape prices what keeps a hold set off the heap: a copy dropped costs an alloc an iteration.
// The repeat shape prices what keeps a render's draws off the heap: a copy dropped costs an alloc an iteration.
func TestNoReferenceAllocRegression(t *testing.T) {
word := `{"format":"{w}","w":["alpha","beta","gamma","delta"]}`
for _, s := range []struct {
@@ -73,12 +73,12 @@ func TestNoReferenceAllocRegression(t *testing.T) {
t.Fatalf("New(%s): %v", s.name, err)
}
if allocs := testing.AllocsPerRun(10000, func() { f.Fake("x") }); allocs > s.base*1.10 {
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a hold set reaching the heap is the usual cause", s.name, allocs, s.base*1.10, s.base)
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); a render's draws reaching the heap is the usual cause", s.name, allocs, s.base*1.10, s.base)
}
}
}
// A table read pins a row in the render's hold and reads its cells in place.
// A table read pins a row in the render's draws and reads its cells in place.
func TestNoTableAllocRegression(t *testing.T) {
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{
"region.json": {Data: []byte(`{"format":"{name}","rows":"region.tsv","key":"code","weight":"population"}`)},
+9 -9
View File
@@ -77,17 +77,17 @@ func spellPins(p *pinSet) string {
}
func renderRoot(s *generatorState, t *template, trace renderTrace) {
set := holdSet{trace: trace}
sc := renderScope{set: &set}
draws := renderDraws{trace: trace}
sc := renderScope{draws: &draws}
switch {
case t.site.row == formatRow:
tbl := t.site.table
render(s, tbl, sc)
for r := 0; r < tbl.rowCount(); r++ {
set := holdSet{trace: trace}
sc := renderScope{set: &set}
sc.groupHold().pins.pin(tbl, r)
render(s, tbl.rowNode, sc.at(tbl.rowNode, &sc.groupHold().pins))
draws := renderDraws{trace: trace}
sc := renderScope{draws: &draws}
sc.groupDraws().pins.pin(tbl, r)
render(s, tbl.rowNode, sc.at(tbl.rowNode, &sc.groupDraws().pins))
}
case t.repeat > 1:
expand(s, t, sc)
@@ -97,10 +97,10 @@ func renderRoot(s *generatorState, t *template, trace renderTrace) {
}
func renderRecordRoot(s *generatorState, t *template, columns []Column, trace renderTrace) {
set := holdSet{trace: trace}
sc := renderScope{set: &set}
draws := renderDraws{trace: trace}
sc := renderScope{draws: &draws}
if t.site.row == formatRow {
t.site.table.drawIn(s, &sc.groupHold().pins)
t.site.table.drawIn(s, &sc.groupDraws().pins)
}
renderRecord(s, t, columns, sc)
}
+6 -6
View File
@@ -130,8 +130,8 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
if err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
}
var set holdSet
sc := renderScope{set: &set}
var draws renderDraws
sc := renderScope{draws: &draws}
if t, isTable := n.(*table); isTable {
if n, err = tableRecord(f.rand, t, tail, sc); err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
@@ -159,7 +159,7 @@ func tableRecord(s *generatorState, t *table, tail []string, sc renderScope) (no
}
switch n := n.(type) {
case *table:
n.drawIn(s, &sc.groupHold().pins)
n.drawIn(s, &sc.groupDraws().pins)
return n, nil
case *tableRow:
return n.t, nil
@@ -204,8 +204,8 @@ type RecordTemplate struct {
func (t *RecordTemplate) Fake() *Record {
t.g.mu.Lock()
defer t.g.mu.Unlock()
var set holdSet
return renderRecord(t.g.rand, t.template, t.columns, renderScope{set: &set})
var draws renderDraws
return renderRecord(t.g.rand, t.template, t.columns, renderScope{draws: &draws})
}
// NewRecordTemplate compiles an inline record — a JSON object with a format and
@@ -264,7 +264,7 @@ func recordOf(n node) (*template, []Column, error) {
func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record {
sc = sc.in(t)
if t.site.row == formatRow {
sc = sc.at(t.site.table.rowNode, &sc.groupHold().pins)
sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins)
}
r := &Record{columns: append([]Column(nil), columns...)}
for i := range r.columns {
+35 -35
View File
@@ -6,7 +6,7 @@ import (
)
// rng is the randomness a builtin sample draws from; *rand.Rand satisfies it. The
// render path takes the concrete *generatorState instead, which keeps the hold set of the walk
// render path takes the concrete *generatorState instead, which keeps the draws of the walk
// drawing through it off the heap.
type rng interface {
IntN(n int) int
@@ -24,8 +24,8 @@ func (f *Generator) Fake(path string) (string, error) {
if err != nil {
return "", fmt.Errorf("fejkdata: %w", err)
}
var set holdSet
sc := renderScope{set: &set}
var draws renderDraws
sc := renderScope{draws: &draws}
n, err := descend(f.rand, &f.root, segments, sc)
if err != nil {
return "", fmt.Errorf("fejkdata: %s: %w", path, err)
@@ -33,7 +33,7 @@ func (f *Generator) Fake(path string) (string, error) {
if _, ok := n.(*folder); ok {
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
}
return render(f.rand, n, sc.at(n, &sc.groupHold().pins)), nil
return render(f.rand, n, sc.at(n, &sc.groupDraws().pins)), nil
}
// descend walks a caller's path to the node it names, pinning in sc the rows it
@@ -43,18 +43,18 @@ func descend(s *generatorState, root node, segments []string, sc renderScope) (n
if _, err := (&pathProbe{}).walk(root, segments); err != nil {
return nil, err
}
return drawPath(root, segments, "", &pathDraw{s: s, pins: &sc.groupHold().pins}), nil
return drawPath(root, segments, "", &pathDraw{s: s, pins: &sc.groupDraws().pins}), nil
}
// renderOnce renders n as one render, over a hold set of its own.
// renderOnce renders n as one render, over draws of its own.
func renderOnce(s *generatorState, n node) string {
var set holdSet
return render(s, n, renderScope{set: &set})
var draws renderDraws
return render(s, n, renderScope{draws: &draws})
}
// render evaluates a compiled node to a string. compile validates every node up
// front, so rendering a compiled tree cannot fail. sc holds the reference draws the
// render shares; each repeat iteration renders over a hold set of its own.
// render shares; each repeat iteration renders over draws of its own.
// A child this switch renders is one renderEdges must list too, or the fences miss it.
func render(s *generatorState, n node, sc renderScope) string {
switch n := n.(type) {
@@ -96,12 +96,12 @@ func render(s *generatorState, n node, sc renderScope) string {
}
// expandAnew expands one repeat iteration of t as a render of its own, in no group. Inlined into
// render's loop, its hold set would move to the heap.
// render's loop, its draws would move to the heap.
//
//go:noinline
func expandAnew(s *generatorState, t *template) string {
var set holdSet
return expand(s, t, renderScope{set: &set})
var draws renderDraws
return expand(s, t, renderScope{draws: &draws})
}
// pick selects one item. Uniform choices are O(1); weighted choices are an
@@ -119,10 +119,10 @@ func expand(s *generatorState, t *template, sc renderScope) string {
b.Grow(t.compiled.grow)
// One draw per held name, for this expansion only: a nested template and each
// repeat iteration get their own, since each is its own expansion. A reference
// path reads the render's hold in sc instead.
var held *hold
if t.compiled.heldLocal {
held = &hold{
// path reads its draw group's memo in sc instead.
var hold *drawMemo
if t.compiled.keepsHold {
hold = &drawMemo{
variant: make(map[string]node, len(t.compiled.held)),
value: make(map[string]readValue, len(t.compiled.held)),
}
@@ -133,7 +133,7 @@ func expand(s *generatorState, t *template, sc renderScope) string {
case literalRun:
b.WriteString(o.lit)
case nameRead:
b.WriteString(readField(s, t, held, sc, o.arms[s.IntN(len(o.arms))]).text)
b.WriteString(readField(s, t, hold, sc, o.arms[s.IntN(len(o.arms))]).text)
case builtinCall:
// Read before the call, so the value a calc computes is the value the
// format showed. calcVars fixed the order op.operands holds.
@@ -141,7 +141,7 @@ func expand(s *generatorState, t *template, sc renderScope) string {
if len(o.operands) > 0 {
operands = make([]string, len(o.operands))
for j, a := range o.operands {
operands[j] = readField(s, t, held, sc, a).text
operands[j] = readField(s, t, hold, sc, a).text
}
}
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
@@ -152,15 +152,15 @@ func expand(s *generatorState, t *template, sc renderScope) string {
// readField renders one arm of a token. A name the expansion holds — a level some
// token addresses by a dotted path that is not a reference, or a field an operand
// reads — is drawn once and kept in held, so {place.postal-code} and {place.locality}
// reads — is drawn once and kept in hold, so {place.postal-code} and {place.locality}
// read one row, either read twice gives one value, and a shown operand is the operand
// computed. Every other name is drawn afresh, so {word} {word} still draws twice.
func readField(s *generatorState, t *template, held *hold, sc renderScope, a arm) readValue {
func readField(s *generatorState, t *template, hold *drawMemo, sc renderScope, a arm) readValue {
if isRef(a.head) && len(a.tail) > 0 {
return readReference(s, t, sc, a)
}
if sc.set.trace != nil {
traceRead(sc.set.trace, t, sc, a)
if sc.draws.trace != nil {
traceRead(sc.draws.trace, t, sc, a)
}
if _, expansionHolds := t.compiled.held[a.head]; !expansionHolds {
if len(a.tail) > 0 {
@@ -168,35 +168,35 @@ func readField(s *generatorState, t *template, held *hold, sc renderScope, a arm
}
return readValue{text: render(s, t.head(a.head), sc)}
}
if held == nil {
if hold == nil {
panic(internalError("%q reads a name the expansion holds, with no hold to keep it in", a.spelling))
}
return readHeld(s, t, held, nil, sc, a)
return readMemo(s, t, hold, nil, sc, a)
}
// readReference reads a reference path, kept in the render's hold for its group, so its draw
// readReference reads a reference path, kept in its draw group's memo, so its draw
// spans the render.
func readReference(s *generatorState, t *template, sc renderScope, a arm) readValue {
if sc.set.trace != nil {
traceRead(sc.set.trace, t, sc, a)
if sc.draws.trace != nil {
traceRead(sc.draws.trace, t, sc, a)
}
group := sc.groupHold()
return readHeld(s, t, &group.hold, &group.pins, sc, a)
group := sc.groupDraws()
return readMemo(s, t, &group.memo, &group.pins, sc, a)
}
func readHeld(s *generatorState, t *template, held *hold, pins *pinSet, sc renderScope, a arm) readValue {
if r, done := held.value[a.path]; done {
func readMemo(s *generatorState, t *template, memo *drawMemo, pins *pinSet, sc renderScope, a arm) readValue {
if r, done := memo.value[a.path]; done {
return r
}
leaf := drawPath(t.head(a.head), a.tail, a.head, &pathDraw{s: s, held: held, pins: pins, a: &a})
leaf := drawPath(t.head(a.head), a.tail, a.head, &pathDraw{s: s, memo: memo, pins: pins, a: &a})
if pins != nil {
sc = sc.at(leaf, pins)
}
r := renderLeaf(s, leaf, sc)
if held.value == nil {
held.value = map[string]readValue{}
if memo.value == nil {
memo.value = map[string]readValue{}
}
held.value[a.path] = r
memo.value[a.path] = r
return r
}
+4 -4
View File
@@ -42,8 +42,8 @@ func TestGrowIsALowerBound(t *testing.T) {
t.Fatalf("format %q did not compile to a template", format)
}
for i := 0; i < 50; i++ {
var set holdSet
if got := len(expand(f.rand, tmpl, renderScope{set: &set})); got < tmpl.compiled.grow {
var draws renderDraws
if got := len(expand(f.rand, tmpl, renderScope{draws: &draws})); got < tmpl.compiled.grow {
t.Errorf("format %q: expand emitted %d bytes, below grow %d", format, got, tmpl.compiled.grow)
}
}
@@ -112,8 +112,8 @@ func TestNodeSwitchesHandleEveryKind(t *testing.T) {
"paths": func(n node) { paths(n) },
"prove": func(n node) { (&valueProof{}).prove(n) },
"render": func(n node) {
var set holdSet
render(engine(1).rand, n, renderScope{set: &set, row: renderedRow{tbl, 0}})
var draws renderDraws
render(engine(1).rand, n, renderScope{draws: &draws, row: renderedRow{tbl, 0}})
},
"renderEdges": func(n node) { renderEdges(n) },
"stepInto": func(n node) { _, _ = stepInto(n, "x") },
+2 -2
View File
@@ -351,8 +351,8 @@ 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(state *generatorState, v reflect.Value) {
if s.record != nil {
var set holdSet
for i, c := range renderRecord(state, s.record, s.columns, renderScope{set: &set}).columns {
var draws renderDraws
for i, c := range renderRecord(state, s.record, s.columns, renderScope{draws: &draws}).columns {
setColumn(fieldAt(v, s.fieldIndexes[i]), c)
}
}
-3
View File
@@ -34,9 +34,6 @@ ranking `readSurvey.walk` and `edge` hardest and the unit they would least modif
This round, in order:
- Give the expansion hold and the render's group hold two names: `groupHold` embeds
`hold`, `held`, `heldLocal` and `holdSet` share its stem, and four seats could not
tell which fence guards which.
- Word the three overlap refusals apart, `checkNameHeld`'s, `checkNoOverlap`'s and
`overlapError`'s: three seats grepped one message to three fences.
- Compile a format in `format.go`: `linkTemplate` calls `compileFormat` in `node.go`,