diff --git a/README.md b/README.md index de34fa8..bf21f7c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/draw.go b/draw.go new file mode 100644 index 0000000..43f323d --- /dev/null +++ b/draw.go @@ -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) +} diff --git a/graph.go b/graph.go index 962a51c..5e6acc0 100644 --- a/graph.go +++ b/graph.go @@ -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) } diff --git a/hold.go b/hold.go index 16b00a8..82b8951 100644 --- a/hold.go +++ b/hold.go @@ -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 } diff --git a/inline.go b/inline.go index 23b592b..8da83e1 100644 --- a/inline.go +++ b/inline.go @@ -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 diff --git a/node.go b/node.go index 604ea0c..e1dc797 100644 --- a/node.go +++ b/node.go @@ -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 diff --git a/path.go b/path.go index 687d9b9..ea4fbe1 100644 --- a/path.go +++ b/path.go @@ -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 }, diff --git a/record.go b/record.go index b630526..5c64142 100644 --- a/record.go +++ b/record.go @@ -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 diff --git a/render.go b/render.go index ec12628..71a5902 100644 --- a/render.go +++ b/render.go @@ -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 diff --git a/todo.md b/todo.md index 1b0f4a6..2d65711 100644 --- a/todo.md +++ b/todo.md @@ -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