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 render shares one reference draw per category, per group
- A draw group name is local to its category - A draw group name is local to its category
- The expansion hold and the render's draws are two fences - 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 category never references itself, and a record's fences run at load
- A record's column set is fixed before the first draw - A record's column set is fixed before the first draw
- Null is a `null` item, not a rate - 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 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
format.go the {token} grammar: scanning, tokens, operands, validation, compiling a format 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 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 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 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`, // - draw — one pick from a choice, or one row taken from a table: `pick`,
// `resolveChoice`, `table.drawRow`. // `resolveChoice`, `table.drawRow`.
// - expansion — one render of one format: `expand`. // - 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` // `Generator.Fake`, `FakeRecord`, `Template.Fake` and `RecordTemplate.Fake`
// start one each, `FakeStruct` one per record, and `expandAnew` one per // start one each, `FakeStruct` one per record, and `expandAnew` one per
// iteration of a `template.repeat`. // iteration of a `template.repeat`.
// - hold — keeping one draw of a name, so every route to it reads that value: // - hold — one expansion keeping one draw of a name, so every route to it reads
// `formatOps.held`, `hold`, `groupHold`, `readHeld`. // that value: `formatOps.held`, `formatOps.keepsHold`, `heldCheck`.
// - draw group — reference draws held apart inside one render: // - 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 // `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: // - pin — fixing which row of a table the render uses, which the draw fences replay:
// `pinSet`, `pinSet.pin`, `table.drawIn`. // `pinSet`, `pinSet.pin`, `table.drawIn`.
// - family — a table and every table reaching it through a chain of // - 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 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. 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 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 set is declared kept a 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 set lives on the `Generator`, so there it 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. 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 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. 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 ## 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. a read drew.
## One name, one meaning ## One name, one meaning
+20 -20
View File
@@ -2,18 +2,18 @@ package fejkdata
import "strings" 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 // 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. // same read written twice reads one value.
type hold struct { type drawMemo struct {
variant map[string]node variant map[string]node
value map[string]readValue 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. // table rows those paths pinned.
type groupHold struct { type groupDraws struct {
hold memo drawMemo
pins pinSet 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. // 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) 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 // renderDraws is one render's reference draws: the unnamed draw group's, and each named one's;
// test's trace of its reads. // and a test's trace of its reads.
type holdSet struct { type renderDraws struct {
unnamed groupHold unnamed groupDraws
named map[string]*groupHold named map[string]*groupDraws
trace renderTrace 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 // 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 // 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 { type renderScope struct {
set *holdSet draws *renderDraws
group string group string
row renderedRow row renderedRow
} }
@@ -77,17 +77,17 @@ func (sc renderScope) rowOf(t *table) int {
return sc.row.index return sc.row.index
} }
func (sc renderScope) groupHold() *groupHold { func (sc renderScope) groupDraws() *groupDraws {
if sc.group == "" { 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 !drew {
if sc.set.named == nil { if sc.draws.named == nil {
sc.set.named = map[string]*groupHold{} sc.draws.named = map[string]*groupDraws{}
} }
d = &groupHold{} d = &groupDraws{}
sc.set.named[strings.Clone(sc.group)] = d sc.draws.named[strings.Clone(sc.group)] = d
} }
return d return d
} }
+2 -2
View File
@@ -386,7 +386,7 @@ type formatOps struct {
ops []op ops []op
grow int grow int
held map[string]firstReach 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 // 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{} c.held = map[string]firstReach{}
} }
if !isRef(a.head) || len(a.tail) == 0 { if !isRef(a.head) || len(a.tail) == 0 {
c.heldLocal = true c.keepsHold = true
} }
h, seen := c.held[a.head] h, seen := c.held[a.head]
if !seen { if !seen {
+1 -1
View File
@@ -771,7 +771,7 @@ func TestReadFieldPanicsOnAPathItCannotHold(t *testing.T) {
t.Fatal("not a template") t.Fatal("not a template")
} }
mustPanic(t, name, func() { 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 // 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 // for a sibling path, which never crosses a table, since a table is only a category. For a memoized
// read, held keeps the variant drawn at each level of a, so paths sharing a prefix share it. // 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 // 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. // as pathCheck does and draws the row the route asks for.
type pathDraw struct { type pathDraw struct {
s *generatorState s *generatorState
pins *pinSet pins *pinSet
held *hold memo *drawMemo
a *arm a *arm
} }
@@ -400,20 +400,20 @@ func (w *pathDraw) walk(n node, tail []string) (node, error) {
return n, nil return n, nil
} }
// variant is the variant of c the walk continues into: the one held for this level // 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 holds its draws; else one drawn afresh. // of the read, drawn once, where the walk has a memo; else one drawn afresh.
func (w *pathDraw) variant(c *choice, rest []string) node { func (w *pathDraw) variant(c *choice, rest []string) node {
if w.held == nil { if w.memo == nil {
return pick(w.s, c) return pick(w.s, c)
} }
key := w.a.levels[len(w.a.tail)-len(rest)] key := w.a.levels[len(w.a.tail)-len(rest)]
n, drew := w.held.variant[key] n, drew := w.memo.variant[key]
if !drew { if !drew {
n = resolveChoice(w.s, c) n = resolveChoice(w.s, c)
if w.held.variant == nil { if w.memo.variant == nil {
w.held.variant = map[string]node{} w.memo.variant = map[string]node{}
} }
w.held.variant[key] = n w.memo.variant[key] = n
} }
return 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) { func TestNoReferenceAllocRegression(t *testing.T) {
word := `{"format":"{w}","w":["alpha","beta","gamma","delta"]}` word := `{"format":"{w}","w":["alpha","beta","gamma","delta"]}`
for _, s := range []struct { for _, s := range []struct {
@@ -73,12 +73,12 @@ func TestNoReferenceAllocRegression(t *testing.T) {
t.Fatalf("New(%s): %v", s.name, err) t.Fatalf("New(%s): %v", s.name, err)
} }
if allocs := testing.AllocsPerRun(10000, func() { f.Fake("x") }); allocs > s.base*1.10 { 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) { func TestNoTableAllocRegression(t *testing.T) {
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{ f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{
"region.json": {Data: []byte(`{"format":"{name}","rows":"region.tsv","key":"code","weight":"population"}`)}, "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) { func renderRoot(s *generatorState, t *template, trace renderTrace) {
set := holdSet{trace: trace} draws := renderDraws{trace: trace}
sc := renderScope{set: &set} sc := renderScope{draws: &draws}
switch { switch {
case t.site.row == formatRow: case t.site.row == formatRow:
tbl := t.site.table tbl := t.site.table
render(s, tbl, sc) render(s, tbl, sc)
for r := 0; r < tbl.rowCount(); r++ { for r := 0; r < tbl.rowCount(); r++ {
set := holdSet{trace: trace} draws := renderDraws{trace: trace}
sc := renderScope{set: &set} sc := renderScope{draws: &draws}
sc.groupHold().pins.pin(tbl, r) sc.groupDraws().pins.pin(tbl, r)
render(s, tbl.rowNode, sc.at(tbl.rowNode, &sc.groupHold().pins)) render(s, tbl.rowNode, sc.at(tbl.rowNode, &sc.groupDraws().pins))
} }
case t.repeat > 1: case t.repeat > 1:
expand(s, t, sc) 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) { func renderRecordRoot(s *generatorState, t *template, columns []Column, trace renderTrace) {
set := holdSet{trace: trace} draws := renderDraws{trace: trace}
sc := renderScope{set: &set} sc := renderScope{draws: &draws}
if t.site.row == formatRow { 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) renderRecord(s, t, columns, sc)
} }
+6 -6
View File
@@ -130,8 +130,8 @@ 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)
} }
var set holdSet var draws renderDraws
sc := renderScope{set: &set} sc := renderScope{draws: &draws}
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 {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err) 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) { switch n := n.(type) {
case *table: case *table:
n.drawIn(s, &sc.groupHold().pins) n.drawIn(s, &sc.groupDraws().pins)
return n, nil return n, nil
case *tableRow: case *tableRow:
return n.t, nil return n.t, nil
@@ -204,8 +204,8 @@ 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()
var set holdSet var draws renderDraws
return renderRecord(t.g.rand, t.template, t.columns, renderScope{set: &set}) return renderRecord(t.g.rand, t.template, t.columns, renderScope{draws: &draws})
} }
// NewRecordTemplate compiles an inline record — a JSON object with a format and // 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 { func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record {
sc = sc.in(t) sc = sc.in(t)
if t.site.row == formatRow { 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...)} r := &Record{columns: append([]Column(nil), columns...)}
for i := range r.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 // 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. // drawing through it off the heap.
type rng interface { type rng interface {
IntN(n int) int IntN(n int) int
@@ -24,8 +24,8 @@ 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)
} }
var set holdSet var draws renderDraws
sc := renderScope{set: &set} sc := renderScope{draws: &draws}
n, err := descend(f.rand, &f.root, segments, sc) n, err := descend(f.rand, &f.root, segments, sc)
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %s: %w", path, err) 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 { if _, ok := n.(*folder); ok {
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path) 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 // 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 { if _, err := (&pathProbe{}).walk(root, segments); err != nil {
return nil, err 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 { func renderOnce(s *generatorState, n node) string {
var set holdSet var draws renderDraws
return render(s, n, renderScope{set: &set}) return render(s, n, renderScope{draws: &draws})
} }
// 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
// front, so rendering a compiled tree cannot fail. sc holds the reference draws the // 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. // 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 { func render(s *generatorState, n node, sc renderScope) string {
switch n := n.(type) { 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 // 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 //go:noinline
func expandAnew(s *generatorState, t *template) string { func expandAnew(s *generatorState, t *template) string {
var set holdSet var draws renderDraws
return expand(s, t, renderScope{set: &set}) return expand(s, t, renderScope{draws: &draws})
} }
// pick selects one item. Uniform choices are O(1); weighted choices are an // 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) b.Grow(t.compiled.grow)
// One draw per held name, for this expansion only: a nested template and each // 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 // repeat iteration get their own, since each is its own expansion. A reference
// path reads the render's hold in sc instead. // path reads its draw group's memo in sc instead.
var held *hold var hold *drawMemo
if t.compiled.heldLocal { if t.compiled.keepsHold {
held = &hold{ hold = &drawMemo{
variant: make(map[string]node, len(t.compiled.held)), variant: make(map[string]node, len(t.compiled.held)),
value: make(map[string]readValue, 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: case literalRun:
b.WriteString(o.lit) b.WriteString(o.lit)
case nameRead: 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: case builtinCall:
// Read before the call, so the value a calc computes is the value the // Read before the call, so the value a calc computes is the value the
// format showed. calcVars fixed the order op.operands holds. // 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 { if len(o.operands) > 0 {
operands = make([]string, len(o.operands)) operands = make([]string, len(o.operands))
for j, a := range 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 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 // 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 // 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 // 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. // 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 { if isRef(a.head) && len(a.tail) > 0 {
return readReference(s, t, sc, a) return readReference(s, t, sc, a)
} }
if sc.set.trace != nil { if sc.draws.trace != nil {
traceRead(sc.set.trace, t, sc, a) traceRead(sc.draws.trace, t, sc, a)
} }
if _, expansionHolds := t.compiled.held[a.head]; !expansionHolds { if _, expansionHolds := t.compiled.held[a.head]; !expansionHolds {
if len(a.tail) > 0 { 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)} 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)) 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. // spans the render.
func readReference(s *generatorState, t *template, sc renderScope, a arm) readValue { func readReference(s *generatorState, t *template, sc renderScope, a arm) readValue {
if sc.set.trace != nil { if sc.draws.trace != nil {
traceRead(sc.set.trace, t, sc, a) traceRead(sc.draws.trace, t, sc, a)
} }
group := sc.groupHold() group := sc.groupDraws()
return readHeld(s, t, &group.hold, &group.pins, sc, a) 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 { func readMemo(s *generatorState, t *template, memo *drawMemo, pins *pinSet, sc renderScope, a arm) readValue {
if r, done := held.value[a.path]; done { if r, done := memo.value[a.path]; done {
return r 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 { if pins != nil {
sc = sc.at(leaf, pins) sc = sc.at(leaf, pins)
} }
r := renderLeaf(s, leaf, sc) r := renderLeaf(s, leaf, sc)
if held.value == nil { if memo.value == nil {
held.value = map[string]readValue{} memo.value = map[string]readValue{}
} }
held.value[a.path] = r memo.value[a.path] = r
return 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) t.Fatalf("format %q did not compile to a template", format)
} }
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
var set holdSet var draws renderDraws
if got := len(expand(f.rand, tmpl, renderScope{set: &set})); got < tmpl.compiled.grow { 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) 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) }, "paths": func(n node) { paths(n) },
"prove": func(n node) { (&valueProof{}).prove(n) }, "prove": func(n node) { (&valueProof{}).prove(n) },
"render": func(n node) { "render": func(n node) {
var set holdSet var draws renderDraws
render(engine(1).rand, n, renderScope{set: &set, row: renderedRow{tbl, 0}}) render(engine(1).rand, n, renderScope{draws: &draws, row: renderedRow{tbl, 0}})
}, },
"renderEdges": func(n node) { renderEdges(n) }, "renderEdges": func(n node) { renderEdges(n) },
"stepInto": func(n node) { _, _ = stepInto(n, "x") }, "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. // 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) { func (s *structShape) fill(state *generatorState, v reflect.Value) {
if s.record != nil { if s.record != nil {
var set holdSet var draws renderDraws
for i, c := range renderRecord(state, s.record, s.columns, renderScope{set: &set}).columns { for i, c := range renderRecord(state, s.record, s.columns, renderScope{draws: &draws}).columns {
setColumn(fieldAt(v, s.fieldIndexes[i]), c) 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: 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 - Word the three overlap refusals apart, `checkNameHeld`'s, `checkNoOverlap`'s and
`overlapError`'s: three seats grepped one message to three fences. `overlapError`'s: three seats grepped one message to three fences.
- Compile a format in `format.go`: `linkTemplate` calls `compileFormat` in `node.go`, - Compile a format in `format.go`: `linkTemplate` calls `compileFormat` in `node.go`,