Draw groups: one reference draw per render, a drawGroup option, and a record's fences at load #16

Merged
lilleman merged 14 commits from draw-groups into main 2026-09-16 10:45:45 +02:00
10 changed files with 400 additions and 167 deletions
Showing only changes of commit 246f99a319 - Show all commits
+57 -41
View File
@@ -124,11 +124,11 @@ string and a backslash as an escape.
A record written only to emit columns still needs a `format` — the grammar's one
required key — so `"format": ""` carries the fields with an inert format: it
renders nothing by `Fake`, and is compiled only so the tree's fences still run.
The columns are the point, and their facts stay together: two columns that read a
path into one category — `{/currency.code}` and `{/currency.symbol}` — share one
draw of it, so the record is internally consistent. A bare `{/currency}` names no
field, so it keeps drawing on its own. That one draw is also why two
columns may not read overlapping reference *paths* — `{/cat.a}` beside
The columns are the point, and their facts stay together: a record is one render, so
columns that read a path into one category — `{/currency.code}` and
`{/currency.symbol}` — read one draw of it ([References](#references)), and a
[group](#group) draws a column apart. That one draw is also why the columns of one
record may not overlap — `{/cat.a}`, or a bare `{/cat}` rendering it, beside
`{/cat.a.b}` is refused, naming the fields to write instead, as
[One draw, one spelling](#one-draw-one-spelling) refuses that pair inside a single
format. A column may not reference the record it belongs to by any spelling: `{/users.first}`
@@ -341,8 +341,8 @@ different datatypes.
### Options and fields
`format`, `weight`, `repeat`, `separator` and `datatype` are the only options; **any
other key is a field** (see [Decisions](#decisions)). An object that does nothing a
`format`, `weight`, `repeat`, `separator`, `datatype` and `group` are the only options;
**any other key is a field** (see [Decisions](#decisions)). An object that does nothing a
string can't — only a `format` — is rejected naming the string, as is a one-item
choice naming its item.
@@ -438,15 +438,36 @@ without naming `sv_SE`:
"Hej, {/en_US.person}!"
```
Renders e.g. `Hej, Pat Smith!`. A reference into a category is held like a
[correlated](#correlated-fields) path — `{.person.femalefirst} {.person.last}` name
one person, `{lowercase(.person.femalefirst)}` reads that same draw, and
`{.person.femalefirst}` beside `{/sv_SE.person.last}` in `sv_SE` is one person too — while a bare
`{/misc.uuid} {/misc.uuid}` is two draws. Rejected at `New`: a path that is
Renders e.g. `Hej, Pat Smith!`. A reference path into a category is held like a
[correlated](#correlated-fields) path, but for the whole render — one `Fake`, or one
record — rather than one format: `{.person.femalefirst} {.person.last}` name one
person, as do the same two references in sibling fields or a nested template, and
`{lowercase(.person.femalefirst)}` reads that same draw. Each `repeat` iteration is
a render of its own, so it draws anew, and a [group](#group) holds a draw apart. A
bare reference names no field and draws each time: `{/misc.uuid} {/misc.uuid}` is
two draws. Rejected at `New`: a path that is
unknown, names a folder, has no folder above, or reads a field not every variant
of a choice carries, and a reference that leads back to its own value, directly,
mutually or through a chain.
### Group
A template may carry `group` to hold its reference draws apart: every reference path
it renders, however deep, reads the draw of that group, and the templates naming one
group in a render read one draw. A nested `group` names another.
```json
{ "format": "{payer} pays {payee}; signed {signature}",
"payer": { "format": "{/sv_SE.person.femalefirst} {/sv_SE.person.last}", "group": "payer" },
"payee": "{/sv_SE.person.femalefirst} {/sv_SE.person.last}",
"signature": { "format": "{/sv_SE.person.last}", "group": "payer" } }
```
Renders e.g. `Sara Eriksson pays Ebba Lind; signed Eriksson`: the signature reads the
payer's draw, while the payee is drawn apart. Rejected at load: a `group` of `""` (the
default), one on a template that renders no reference path, and a path reading into a
level that carries one.
### Correlated fields
`{name.field}` reads a path into a sibling, which holds the sibling to one draw
@@ -478,12 +499,14 @@ The sub-fields stay addressable — `Fake("address.place.locality")` renders, an
### One draw, one spelling
A name any token reads as a path (`{p.first}`) or as an operand (`{calc(net * 2)}`,
`{uppercase(w)}`) is drawn **once per expansion**, and every other route to it —
a bare `{p}`, a second bare `{w}`, `{/cat.net}`, a nested template rendering
`{/cat.p.last}`, at any depth — is a load error naming the spelling to use. A
name nothing reads that way is drawn each time: `{word} {word}` differs. An
expansion is one render of one format, so each `repeat` iteration and each nested
template draws again.
`{uppercase(w)}`) is drawn **once per expansion**, a reference path (`{/cat.p.first}`)
**once per render** in its [group](#group), and every other route to either — a bare
`{p}`, a second bare `{w}`, `{/cat.net}`, a nested template rendering `{/cat.p.last}`
beside `{p.first}`, a bare `{/cat}` rendering what `{/cat.p.first}` reads, at any
depth — is a load error naming the spelling to use. A name nothing reads that way is
drawn each time: `{word} {word}` differs. An expansion is one render of one format, so
each nested template draws its own names again; a render is one `Fake` or one record,
and each `repeat` iteration is an expansion and a render of its own.
```text
token {p} renders a level that {p.first} reads a path into; name the fields you want instead
@@ -524,7 +547,7 @@ tokens add cost in proportion to the output.
## Decisions
- **Options and fields share one namespace.** `format`, `weight`, `repeat`,
`separator` and `datatype` are reserved; every other key is a field. Nesting fields under a
`separator`, `datatype` and `group` are reserved; every other key is a field. Nesting fields under a
key, or prefixing options, would tax every template to guard against a
misspelt option.
- **`{a|b}` stays beside nested choices.** `[[…], […]]` picks the same way, but
@@ -571,10 +594,10 @@ tokens add cost in proportion to the output.
to have would make `--seed 42` machine-dependent. Data still lives in `data/`
as JSON; `--data-path` layers over it.
- **A bare reference draws each time; a reference path is held.** `{/p} {/p}`
is two draws, as `{word} {word}` is, while `{/p.first}` beside a nested template
rendering `{/p.first}` is a load error: a bare token is by contract an
independent draw, a path pins its level, and any route into a pinned level from
another expansion could show another row.
is two draws, as `{word} {word}` is, while every `{/p.first}` in one render reads
one draw, and a bare `{/p}` rendering it beside them is a load error: a bare token
is by contract an independent draw, a path pins its level, and a fresh draw of a
pinned level could show another row.
- **Reference sigils follow the filesystem.** `/` is the root, `.` this file's
folder, `..` the folder above — what those spellings already mean to anyone who
has typed a path. A locale's files reach each other without naming the locale,
@@ -640,24 +663,16 @@ tokens add cost in proportion to the output.
see a caller's types, so the first `FakeStruct` for a type compiles its tags and the
answer, error included, is kept per type: a test's first call is its load, and no
`NewStruct` handle is needed, as the cache already compiles once.
- **A record shares one reference draw per category.** Two columns that reference
one category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of
it, so a record's facts agree the way a template's [correlated
fields](#correlated-fields) do. The draw is one per record, so it spans a
column's `repeat` and nested templates too (one record is one coherent unit);
a bare reference — `{/currency}`, no field — stays an independent draw every
time, the rule a format string already follows. Only references share: a sibling
field is local to its own column, so a `first` column does not silently bind to
a `first` in the column next to it.
The string view of that same template does not share. `Fake` renders each
sibling field as its own expansion, so a `{/currency.code}` field beside a
`{/currency.symbol}` field is two draws and may render `EUR $`; writing both
references in one `format` holds them together, as
[One draw, one spelling](#one-draw-one-spelling) says. The scope is what makes a
row coherent when the columns *are* the output, and there the caller cannot fall
back on one format string. Widening it to every render would change what `Fake`
has emitted since the start, for a correlation a single format already reaches.
- **A render shares one reference draw per category, per group.** Every reference
path into a category in one `Fake`, or one record, reads one draw of it, so a
value's facts agree across its fields, nested templates and columns alike —
`{/currency.code}` in one field and `{/currency.symbol}` in another name one
currency, whichever view renders them. A `repeat` iteration is a render of its
own, since repeating asks for another entity, and a [group](#group) names further
entities within one render, so a payer and a payee are two groups over one
`person` rather than two copies of it. Only references share: a sibling field is
local to its own expansion, so a `first` column does not silently bind to a
`first` in the column next to it.
- **A record's column set is fixed before the first draw.** Only a category-level
template is a record: a path descending into a field, or naming a folder or a
choice, errors. A tail may pass through a choice whose variants carry different
@@ -737,6 +752,7 @@ struct.go structs: FakeStruct, fake tags, and a field's Go type as its col
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
hold.go the hold: one draw per expansion for paths and operands, and its fences
draw.go one reference draw per render and group: draw sets, the group option, and its fence
reference.go reference sigils, and binding references across the tree
graph.go the render graph: edges, cycles, the repeat bound, tree walks
builtins.go the {name()} function registry and its implementations
+270
View File
@@ -0,0 +1,270 @@
package fejkdata
import (
"fmt"
"sort"
"strings"
)
// drawSet is one render's reference draws: the unnamed group's, and each named group's.
type drawSet struct {
unnamed draws
named map[string]*draws
}
// drawScope is where a render reads its reference paths: a draw set, in the group of the
// template rendering.
type drawScope struct {
set *drawSet
group string
}
// newDrawSet 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; a zero drawSet makes them on its first read instead.
func newDrawSet() drawSet {
return drawSet{unnamed: draws{variant: map[string]node{}, value: map[string]draw{}}}
}
// renderOnce renders n as one render, over draws of its own.
func renderOnce(s *session, n node) string {
var set drawSet
return render(s, n, drawScope{set: &set})
}
// in is the scope t renders in: the group it names, else its caller's.
func (sc drawScope) in(t *template) drawScope {
if t.drawGroup != "" {
sc.group = t.drawGroup
}
return sc
}
// draws is the set's draws for sc's group.
func (sc drawScope) draws() *draws {
if sc.group == "" {
return &sc.set.unnamed
}
d, drew := sc.set.named[sc.group]
if !drew {
if sc.set.named == nil {
sc.set.named = map[string]*draws{}
}
d = &draws{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 draw set, to the heap
}
return d
}
// groupOf reads a template's "group" (default "").
func groupOf(m map[string]any) (string, error) {
v, ok := m["group"]
if !ok {
return "", nil
}
name, ok := v.(string)
switch {
case !ok:
return "", fmt.Errorf("group must be a string, got %T", v)
case name == "":
return "", fmt.Errorf(`group "" is the default, so it has no effect; drop it`)
}
return name, nil
}
// drawCheck fences each template of a scope as a render of its own, remembering which nodes
// read a reference path.
type drawCheck struct {
reads map[node]bool
}
func (c *drawCheck) check(path string, n node) error {
t, ok := n.(*template)
switch {
case !ok:
return nil
case !c.readsPath(t) && t.drawGroup != "":
return fmt.Errorf("%s: group %q splits nothing, since nothing it renders reads a reference path; drop it", path, t.drawGroup)
case !c.readsPath(t):
return nil
}
w := newDrawWalk(nil)
for _, e := range renderEdges(t) {
w.edge(t, e, t.drawGroup, drawRoute{e.reached(), e.label}, "", false)
}
if err := w.check(); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
return nil
}
// readsPath reports whether rendering n reads a reference path, however deep.
func (c *drawCheck) readsPath(n node) bool {
if r, done := c.reads[n]; done {
return r
}
r := false
for _, e := range renderEdges(n) {
if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || c.readsPath(e.to) {
r = true
break
}
}
if c.reads == nil {
c.reads = map[node]bool{}
}
c.reads[n] = r
return r
}
// refRead is the reference an edge of n reads, and the node it is bound to; false when the
// edge reads none.
func refRead(n node, label string) (arm, node, bool) {
t, isTemplate := n.(*template)
if !isTemplate {
return arm{}, nil, false
}
a := splitArm(label, t.refs)
return a, t.fields[a.key], isRef(a.key)
}
// checkColumnDraws fences a record's columns as one render.
func checkColumnDraws(t *template, columns []string) error {
w := newDrawWalk(t)
for _, name := range columns {
w.walk(t.fields[name], t.drawGroup, drawRoute{spelling: fmt.Sprintf("column %q", name)}, "", false)
}
return w.check()
}
// drawWalk gathers what one render reads through its draws and what it draws afresh, each by
// group, for check to compare. record is set for a record's columns, which may not read the
// record back.
type drawWalk struct {
record *template
reads []pathRead
read map[drawKey]bool
fresh []freshDraw
seen map[drawVisit]bool
err error
}
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
type drawRoute struct{ spelling, label string }
type pathRead struct {
group string
route drawRoute
a arm
target node
}
type freshDraw struct {
group string
route drawRoute
via string
n node
}
type drawVisit struct {
n node
group string
held bool
}
type drawKey struct {
group string
key any
}
func newDrawWalk(record *template) *drawWalk {
return &drawWalk{record: record, read: map[drawKey]bool{}, seen: map[drawVisit]bool{}}
}
// walk follows what rendering n renders. held says n renders inside a reference path's draw, so
// what it draws belongs to that draw; a repeat renders over draws of its own, so the walk stops there.
func (w *drawWalk) walk(n node, group string, route drawRoute, via string, held bool) {
v := drawVisit{n, group, held}
if w.seen[v] || w.err != nil {
return
}
w.seen[v] = true
if !held {
w.fresh = append(w.fresh, freshDraw{group, route, via, n})
}
t, isTemplate := n.(*template)
if isTemplate && t.repeat > 1 {
return
}
if isTemplate && t.drawGroup != "" {
group = t.drawGroup
}
for _, e := range renderEdges(n) {
w.edge(n, e, group, route, via, held)
}
}
func (w *drawWalk) edge(from node, e renderEdge, group string, route drawRoute, via string, held bool) {
a, target, reads := refRead(from, e.label)
switch {
case !reads:
w.walk(e.to, group, route, via, held)
case w.record != nil && target == node(w.record):
w.err = fmt.Errorf("%s reads {%s}, which points back at this record; a column cannot read another column — move the shared value into its own category and reference that", route.spelling, a.name)
case len(a.tail) > 0:
if k := (drawKey{group, a.path}); !w.read[k] {
w.read[k] = true
w.reads = append(w.reads, pathRead{group, route, a, target})
}
w.walk(e.to, group, route, a.name, true)
default:
w.walk(e.to, group, route, a.name, false)
}
}
// check refuses what one draw per reference path cannot answer for: a path read into a level
// another read renders, and a node drawn afresh beside a path whose draw holds it.
func (w *drawWalk) check() error {
if w.err != nil {
return w.err
}
sort.SliceStable(w.reads, func(i, j int) bool {
if w.reads[i].group != w.reads[j].group {
return w.reads[i].group < w.reads[j].group
}
return w.reads[i].a.path < w.reads[j].a.path
})
pins := map[drawKey]pathRead{}
for i, level := range w.reads {
for _, into := range w.reads[i+1:] {
if into.group == level.group && strings.HasPrefix(into.a.path, level.a.path+".") {
return overlap(level.route, level.a.name, into)
}
}
held := map[node]bool{}
coverPath(level.target, level.a.tail, held)
for n := range held {
if _, pinned := pins[drawKey{level.group, n}]; !pinned {
pins[drawKey{level.group, n}] = level
}
}
}
for _, f := range w.fresh {
if r, pinned := pins[drawKey{f.group, f.n}]; pinned {
return overlap(f.route, f.via, r)
}
}
return nil
}
func overlap(route drawRoute, ref string, into pathRead) error {
return fmt.Errorf("%s renders a level that %s reads a path into; name the fields you want instead, or draw them apart with a group", route.spelled(ref), into.route.spelled(into.a.name))
}
// spelled names the route, and the reference it reaches a draw by where its root edge is not
// that reference.
func (r drawRoute) spelled(ref string) string {
if ref == "" || ref == r.label {
return r.spelling
}
return fmt.Sprintf("%s with {%s}", r.spelling, ref)
}
+3
View File
@@ -196,6 +196,9 @@ func checkRenders(s nodeScope) error {
if err := s(heldCheck); err != nil {
return err
}
if err := s((&drawCheck{}).check); err != nil {
return err
}
return s((&valueProof{}).checkDatatype)
}
+25 -16
View File
@@ -18,6 +18,9 @@ func heldCheck(path string, n node) error {
}
readers := boundReaders(t.format, t.bound, t.refs)
for _, head := range heldHeads(t) {
if _, isPath := t.bound[head]; isPath && isRef(head) {
continue // held for the whole render, which drawCheck fences
}
if err := checkHeadHeld(t, head, readers); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
@@ -252,9 +255,10 @@ func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding)
})
}
// draws is what an expansion has already drawn for its held names: the variant each
// was drawn as, so every path under it reads one row, and the draw each read made, by
// its one spelling, so the same read written twice reads one draw.
// draws is what has already been drawn for held names: the variant each was drawn as,
// so every path under it reads one row, and the draw each read made, by its one
// spelling, so the same read written twice reads one draw. An expansion keeps one for
// its sibling names, and a render one per group for its reference paths.
type draws struct {
variant map[string]node
value map[string]draw
@@ -273,14 +277,14 @@ type draw struct {
// gives one value, and a shown operand is the operand computed. Every other name is
// drawn afresh, so {word} {word} still draws twice. checkTokens, checkPath and
// linkRefs prove every step, so the walk cannot fail.
func readField(s *session, t *template, held, refScope *draws, a arm) draw {
func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw {
if !t.held[a.key] {
if len(a.tail) > 0 {
panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.name, a.key))
}
return draw{text: render(s, t.fields[a.key], refScope)}
return draw{text: render(s, t.fields[a.key], sc)}
}
d := readScope(held, refScope, a)
d := readScope(held, sc, a)
if r, done := d.value[a.path]; done {
return r
}
@@ -296,37 +300,42 @@ func readField(s *session, t *template, held, refScope *draws, a arm) draw {
n, drew := d.variant[key]
if !drew {
n = drawn(s, c)
if d.variant == nil {
d.variant = map[string]node{}
}
d.variant[key] = n
}
return []node{n}, nil
},
leaf: func(n node) error { r = renderLeaf(s, n, refScope); return nil },
leaf: func(n node) error { r = renderLeaf(s, n, sc); return nil },
})
if d.value == nil {
d.value = map[string]draw{}
}
d.value[a.path] = r
return r
}
// readScope is the draws a held read keeps its draw in: for a reference that reads a path,
// refScope, so its draw outlives this expansion; for a sibling, or a reference read whole, held.
func readScope(held, refScope *draws, a arm) *draws {
if isRef(a.key) && refScope != nil && len(a.tail) > 0 {
return refScope
// the render's draws for its group, so its draw spans the render; for a sibling, or a
// reference read whole, held.
func readScope(held *draws, sc drawScope, a arm) *draws {
if isRef(a.key) && len(a.tail) > 0 {
return sc.draws()
}
return held
}
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one
// reference alone whose read drew null.
func renderLeaf(s *session, n node, scope *draws) draw {
func renderLeaf(s *session, n node, sc drawScope) draw {
n = drawn(s, n)
if _, isNull := n.(*null); isNull {
return draw{null: true}
}
r := draw{text: render(s, n, scope)}
r := draw{text: render(s, n, sc)}
if t, _ := n.(*template); t != nil && t.readsColumn != nil {
if d := readScope(nil, scope, t.readsColumn.a); d != nil {
r.null = d.value[t.readsColumn.a.path].null
}
r.null = sc.in(t).draws().value[t.readsColumn.a.path].null
}
return r
}
+1 -1
View File
@@ -20,7 +20,7 @@ type Template struct {
func (t *Template) Fake() string {
t.g.mu.Lock()
defer t.g.mu.Unlock()
return render(t.g.rand, t.n, nil)
return renderOnce(t.g.rand, t.n)
}
// NewTemplate compiles an inline template — a format string or a JSON value — and
+8 -3
View File
@@ -62,6 +62,7 @@ type template struct {
fromString bool // written as a JSON string rather than an object
readsColumn *columnRead // set when the format is one reference alone reading a record's column
record bool // compiled at the top without a repeat, so its fields are record columns
drawGroup string // the group its render reads reference paths in; "" keeps its caller's
}
func (*template) isNode() {}
@@ -241,13 +242,13 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
if err != nil {
return nil, err
}
if len(fields) == 0 && o.repeat == 1 && !o.weighted && o.datatype == DataTypeString {
if len(fields) == 0 && o.repeat == 1 && !o.weighted && o.datatype == DataTypeString && o.group == "" {
return nil, fmt.Errorf("an object holding only a format is a string; write %q", o.format)
}
if err := checkTokens(o.format, fields); err != nil {
return nil, err
}
t := &template{format: o.format, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, record: fieldPos == inColumn}
t := &template{format: o.format, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, drawGroup: o.group, record: fieldPos == inColumn}
if err := t.compileFormat(); err != nil {
return nil, err
}
@@ -258,6 +259,7 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
type templateOptions struct {
datatype DataType
format string
group string
repeat int
separator string
weighted bool
@@ -278,6 +280,9 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) {
if o.datatype, err = datatypeOf(m, pos); err != nil {
return o, err
}
if o.group, err = groupOf(m); err != nil {
return o, err
}
if sv, ok := m["separator"]; ok {
if o.separator, ok = sv.(string); !ok {
return o, fmt.Errorf("separator must be a string, got %T", sv)
@@ -410,7 +415,7 @@ func checkPathNames(path string) error {
// field. These names can never be fields.
func isOption(name string) bool {
switch name {
case "datatype", "format", "repeat", "separator", "weight":
case "datatype", "format", "group", "repeat", "separator", "weight":
return true
}
return false
+9 -5
View File
@@ -89,9 +89,9 @@ func unreachableInChoice(c *choice, want string) error {
// checkPath proves a dotted tail resolves whichever way the draws go — a choice
// must carry the rest of the path in the set every variant shares — and that no
// level a path reads carries a repeat, 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.
// level a path reads carries a repeat or a group, which one draw of it could not
// apply. So a path that validates here resolves on every render, and a typo is a
// New-time error.
func checkPath(n node, tail []string, level string) error {
return walkPath(n, tail, pathWalk{
choice: func(c *choice, rest []string) ([]node, error) {
@@ -101,8 +101,12 @@ func checkPath(n node, tail []string, level string) error {
return c.items, nil
},
level: func(t *template, rest []string) error {
if t.repeat > 1 {
return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", join(level, strings.Join(tail[:len(tail)-len(rest)], ".")))
name := join(level, strings.Join(tail[:len(tail)-len(rest)], "."))
switch {
case t.repeat > 1:
return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", name)
case t.drawGroup != "":
return fmt.Errorf("the level %q carries a group, which a path reading into it cannot apply", name)
}
return nil
},
+6 -74
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"unicode"
"unicode/utf8"
@@ -23,7 +22,7 @@ type Column struct {
// Record is one record rendered from a template: every direct field is a column,
// listed in name order. Each column is its own expansion, so a sibling field is
// local to it, while a reference that reads a path is drawn once for the whole
// record.
// record, per group.
type Record struct {
columns []Column
}
@@ -212,7 +211,7 @@ func recordOf(n node) (*template, []Column, error) {
if !t.record {
return nil, nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat)
}
if err := checkColumnRefs(t, names); err != nil {
if err := checkColumnDraws(t, names); err != nil {
return nil, nil, err
}
columns := make([]Column, len(names))
@@ -223,80 +222,13 @@ func recordOf(n node) (*template, []Column, error) {
return t, columns, nil
}
// checkColumnRefs rejects the reference reads a record's shared draw cannot answer
// for: one column rendering a level another reads a path into, and a column
// reading the record back through its own path.
func checkColumnRefs(t *template, columns []string) error {
reads, err := columnRefs(t, columns)
if err != nil {
return err
}
sort.Slice(reads, func(i, j int) bool {
if reads[i].a.path != reads[j].a.path {
return reads[i].a.path < reads[j].a.path
}
return reads[i].column < reads[j].column
})
for i, level := range reads {
for _, into := range reads[i+1:] {
if strings.HasPrefix(into.a.path, level.a.path+".") {
return fmt.Errorf("column %q renders {%s}, a level column %q reads a path into with {%s}; name the fields you want instead",
level.column, level.a.name, into.column, into.a.name)
}
}
}
return nil
}
// columnRef is one held reference read, and the column whose render reaches it.
type columnRef struct {
column string
a arm
}
// columnRefs lists every reference that reads a path, anywhere a column renders,
// following the same edges expand does. A reference landing back on the record
// itself is reported rather than collected, whether it reads a path or the record
// whole: either way the column describes a draw other than its neighbours'.
func columnRefs(t *template, columns []string) ([]columnRef, error) {
var out []columnRef
var err error
for _, name := range columns {
seen := map[node]bool{}
var walk func(n node)
walk = func(n node) {
if n == nil || seen[n] || err != nil {
return
}
seen[n] = true
if tm, ok := n.(*template); ok {
for _, ref := range refTokens(tm.format) {
a := splitArm(ref, tm.refs)
if tm.fields[a.key] == node(t) {
err = fmt.Errorf("column %q reads {%s}, which points back at this record; a column cannot read another column — move the shared value into its own category and reference that", name, a.name)
return
}
if len(a.tail) > 0 {
out = append(out, columnRef{name, a})
}
}
}
for _, e := range renderEdges(n) {
walk(e.to)
}
}
walk(t.fields[name])
}
return out, err
}
// renderRecord draws each column once, in the name order recordOf fixed, over one
// reference scope shared across them.
// renderRecord draws each column once, in the name order recordOf fixed, as one render.
func renderRecord(s *session, t *template, columns []Column) *Record {
scope := &draws{variant: map[string]node{}, value: map[string]draw{}}
set := newDrawSet()
sc := drawScope{set: &set}.in(t)
r := &Record{columns: append([]Column(nil), columns...)}
for i := range r.columns {
column := renderLeaf(s, t.fields[r.columns[i].Name], scope)
column := renderLeaf(s, t.fields[r.columns[i].Name], sc)
r.columns[i].Value, r.columns[i].Null = column.text, column.null
}
return r
+21 -11
View File
@@ -27,7 +27,7 @@ func (f *Generator) Fake(path string) (string, error) {
if _, ok := n.(*group); ok {
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
}
return render(f.rand, n, nil), nil
return renderOnce(f.rand, n), nil
}
// descend walks named fields to the node a path names. It is the one render-side
@@ -50,20 +50,21 @@ func descend(s *session, root node, segments []string) (node, error) {
}
// render evaluates a compiled node to a string. compile validates every node up
// front, so rendering a compiled tree cannot fail. refScope carries the draws a
// reference shares beyond its own expansion; nil keeps every reference local.
func render(s *session, n node, refScope *draws) string {
// front, so rendering a compiled tree cannot fail. sc holds the reference draws the
// render shares; each repeat iteration renders over draws of its own.
func render(s *session, n node, sc drawScope) string {
switch n := n.(type) {
case *choice:
return render(s, pick(s, n), refScope)
return render(s, pick(s, n), sc)
case *null:
return ""
case *template:
sc = sc.in(n)
if n.repeat == 1 {
if n.fixed {
return n.lit
}
return expand(s, n, refScope)
return expand(s, n, sc)
}
var b strings.Builder
b.Grow(n.repeat * (n.grow + len(n.separator)))
@@ -71,7 +72,7 @@ func render(s *session, n node, refScope *draws) string {
if i > 0 {
b.WriteString(n.separator)
}
b.WriteString(expand(s, n, refScope))
b.WriteString(expandAnew(s, n, sc.group))
}
return b.String()
default:
@@ -79,6 +80,15 @@ func render(s *session, n node, refScope *draws) string {
}
}
// expandAnew expands one repeat iteration of t as a render of its own, in group. Inlined into
// render's loop, its draw set would move to the heap.
//
//go:noinline
func expandAnew(s *session, t *template, group string) string {
var set drawSet
return expand(s, t, drawScope{set: &set, group: group})
}
// pick selects one item. Uniform choices are O(1); weighted choices are an
// O(log n) search over precomputed cumulative weights. compile guarantees a
// non-empty choice and a finite positive total, so the index is always in range.
@@ -93,12 +103,12 @@ func pick(r rng, c *choice) node {
// expand renders a template's compiled ops. compile validated every token, so this
// cannot fail.
func expand(s *session, t *template, refScope *draws) string {
func expand(s *session, t *template, sc drawScope) string {
var b strings.Builder
b.Grow(t.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
// reads the caller's scope instead, whenever one was supplied.
// path reads the render's draws in sc instead.
var held *draws
if len(t.held) > 0 {
held = &draws{
@@ -112,7 +122,7 @@ func expand(s *session, t *template, refScope *draws) string {
case 'l':
b.WriteString(o.lit)
case 'f':
b.WriteString(readField(s, t, held, refScope, o.arms[s.IntN(len(o.arms))]).text)
b.WriteString(readField(s, t, held, sc, o.arms[s.IntN(len(o.arms))]).text)
case 'b':
// Read before the call, so the value a calc computes is the value the
// format showed. calcVars fixed the order op.operands holds.
@@ -120,7 +130,7 @@ func expand(s *session, t *template, refScope *draws) string {
if len(o.operands) > 0 {
operands = make([]string, len(o.operands))
for j, a := range o.operands {
operands[j] = readField(s, t, held, refScope, a).text
operands[j] = readField(s, t, held, sc, a).text
}
}
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
-16
View File
@@ -2,22 +2,6 @@
## Before v0.1.0
The record API lands first, so the data update can use it.
### Record API
- Draw groups — references into one category share one draw per render (one
record, or one `Fake`) in both views; each `repeat` iteration draws anew, and a
bare reference draws each time. An option naming a draw group splits a render
into several entities. Replaces the Decision "A record shares one reference draw
per category". Each expectation becomes a test:
- `first`, `last` and `email` reading `person` → one person
- `from_first`/`from_last` grouped `from`, `to_first`/`to_last` grouped `to` → two people
- `host`, plus `guests` repeated 3 times → four people
- `code` and `symbol` sibling fields reading `currency`, as `{code} {symbol}` → a matching pair
- `{a} & {b}`, each reading `person` → one person, or two when `a` and `b` name different groups
- two bare `{/sv_SE.word}` → two words
### Data
- Major data update. Shipped categories render as records with their building