Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
15 changed files with 455 additions and 231 deletions
Showing only changes of commit b1aed332e7 - Show all commits
+6 -4
View File
@@ -13,10 +13,12 @@ replacement, and each removed path, column or flag.
A name lives in the category binding it, so every field and record column of the A name lives in the category binding it, so every field and record column of the
category reads one pick, a transform reads it as `{lowercase(t.name)}`, and a name a category reads one pick, a transform reads it as `{lowercase(t.name)}`, and a name a
`repeat` binds picks again on every iteration. Refused at `New`: a name bound twice `repeat` binds picks again on every iteration. Refused at `New`: a name bound twice
outside any `repeat`, twice in one `repeat`, or both inside a `repeat` and outside it; a name that is a field or an option; outside any `repeat`, twice in one `repeat`, or both inside a `repeat` and outside it; a
a binding of anything but a reference; a binding in a choice's item; a binding nothing name that is a field or an option; a binding of anything but a reference; a binding in a
reads; a path through a name that selects a row; and a binding in a table. A field, choice's item; a binding nothing reads, or one read once whole, which the reference
column, category or folder name may not contain ` as `. spells; a path through a name that selects a row; a read of a name rendering a field
twice where a path through the name reads it; and a binding in a table. A field, column,
category or folder name may not contain ` as `.
- First release: the CLI, the library and the shipped data set. - First release: the CLI, the library and the shipped data set.
- Table categories: a category JSON naming a `rows` TSV beside it, with the options - Table categories: a category JSON naming a `rows` TSV beside it, with the options
`key`, `name`, `weight` and `parent`; a path selects a row by key or name, `key`, `name`, `weight` and `parent`; a path selects a row by key or name,
+17 -11
View File
@@ -708,12 +708,16 @@ row:
"{/misc.territory as t}{t.flag} {t}: capital {t.capital}, calling code +{t.calling-code}" "{/misc.territory as t}{t.flag} {t}: capital {t.capital}, calling code +{t.calling-code}"
``` ```
Renders e.g. `🇳🇴 Norway: capital Oslo, calling code +47`. Everything under a name is drawn Renders e.g. `🇳🇴 Norway: capital Oslo, calling code +47`. A name keeps each level a
once, so `{n}` and `{n.path}` agree, a [transform](#transforms) of `n.path` reads the same `{n.path}` reads, and what it lands on, so `{n}` and `{n.path}` agree, and a
value, and a field `n` renders twice prints the same text twice. A name is a pick of its own: [transform](#transforms) of `n.path` reads the same value. Everything else under the name draws
`{t.name}` and `{/misc.territory.name}` beside it are two draws. A reference path read under a as it would anywhere: `{w}-{w}` in the category `n` names draws `w` twice, so `{n.w}` beside
name draws in the draw group of the read of the name, so give the reads of two names bound to `{n}` is refused there, since it could read either draw.
one category a [draw group](#draw-group) each to draw what it references apart.
A name is a pick of its own: `{t.name}` and `{/misc.territory.name}` beside it are two draws. A
reference path the picked category reads is still held for the render as usual, so two names
bound to `sv_SE.person`, whose fields read `{.first-name.name}`, print one person unless their
reads sit in two [draw groups](#draw-group).
A name is drawn on its first read, and lives in the category binding it: any field of the A name is drawn on its first read, and lives in the category binding it: any field of the
category reads it, and a record's columns read one pick. Each render of the category picks category reads it, and a record's columns read one pick. Each render of the category picks
@@ -721,10 +725,12 @@ anew, and so does each bare reference to it. A name a `repeat` binds picks again
iteration, and one bound outside the repeat keeps its pick on every line. iteration, and one bound outside the repeat keeps its pick on every line.
Rejected at load: a name bound twice outside any `repeat`, twice in one `repeat`, or both Rejected at load: a name bound twice outside any `repeat`, twice in one `repeat`, or both
inside a `repeat` and outside it; a name that is a field or an option; a binding of anything but a reference; a binding in inside a `repeat` and outside it; a name that is a field or an option; a binding of anything
a choice's item, which every other item would leave unbound; a binding nothing reads; a path but a reference; a binding in a choice's item, which every other item would leave unbound; a
through a name that selects a row, which the reference selects instead; and a binding in a binding nothing reads, or one read once whole outside a `repeat`, which the reference spells;
table's format or cell. A field, column, category or folder name may not contain ` as `. a path through a name that selects a row, which the reference selects instead; a read of a name
rendering a field twice where a path through the name reads it; and a binding in a table's
format or cell. A field, column, category or folder name may not contain ` as `.
### Draw group ### Draw group
@@ -926,7 +932,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
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 draws.go the memo a hold, draw group or named pick keeps its draws in; and a render's draws and the scope it reads its reference paths and names 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 and their refusals: a drawGroup's, a render's and a record's columns judged on what they read, and a table reaching its own family drawfence.go the load fences over reference draws and their refusals: a drawGroup's, a render's and a record's columns judged on what they read, and a table reaching its own family
reads.go the load-time fold of what a render reads, and what a path reads of a table family reads.go the load-time fold of what a render reads, and what a path reads of a table family
+3 -3
View File
@@ -27,9 +27,9 @@ package fejkdata
// an expansion holds a sibling path's head and an operand's field, its draw // an expansion holds a sibling path's head and an operand's field, its draw
// group a reference path's: `formatOps.held` and `heldCheck` an expansion's, // group a reference path's: `formatOps.held` and `heldCheck` an expansion's,
// `readReference` a draw group's. // `readReference` a draw group's.
// - named pick — one draw of a reference a binding token, {ref as n}, gives a name, kept for one // - named pick — the draw a token such as {/person as p} binds to a name. It is drawn on its
// render of the scope it is bound in, a category or a repeat iteration, and drawn on its // first read and kept while its scope renders: a category, or one repeat iteration.
// first read: `nameBinding`, `nameScope`, `namedPick`, `pickFrame`. // `nameBinding`, `nameScope`, `namedPick`, `pickFrame`.
// - memo — what a hold, a draw group or a named pick keeps its draws in, the variant drawn at // - memo — what a hold, a draw group or a named pick keeps its draws in, the variant drawn at
// each level and the value each path read: `drawMemo`, `readMemo`. // each level and the value each path read: `drawMemo`, `readMemo`.
// - draw group — reference draws kept apart inside one render: // - draw group — reference draws kept apart inside one render:
+19 -10
View File
@@ -662,6 +662,8 @@ same four columns as `geo.SE.address`, each a reference into it, and the format
twice; a column is spelled the same in both, `street-number`, so the two never disagree twice; a column is spelled the same in both, `street-number`, so the two never disagree
on a name. on a name.
`todo.md` item 3 replaces it.
## A postort's kommun comes from its name, its tätort or its codes, never from distance ## A postort's kommun comes from its name, its tätort or its codes, never from distance
2026-09-24, Lilleman auf Larv. 2026-09-24, Lilleman auf Larv.
@@ -896,13 +898,20 @@ a struct's fields as one.
field binds a name where another reads it, `"first": "{/person as p}{p.first}"` beside field binds a name where another reads it, `"first": "{/person as p}{p.first}"` beside
`"last": "{p.last}"`, so the scope cannot be the template holding the binding; the category `"last": "{p.last}"`, so the scope cannot be the template holding the binding; the category
is the smallest unit holding every field and record column. A choice's item binds no name, is the smallest unit holding every field and record column. A choice's item binds no name,
since a read outside the item would see a binding another item leaves out. The scope is since a read outside the item would see a binding another item leaves out. A pick is drawn on
lexical: a read entering a category, by a reference or through a name, sees none of the its first read, so a record's columns, rendered in name order, read one pick whichever binds
frames rendering above it, so what a category renders never depends on who references it. A it.
pick is drawn on its first read, so a record's columns, rendered in name order, read one pick
whichever binds it. A read entering a category keeps the frame of each scope it enters in its The scope is lexical: a read entering a category, by a reference, through a name or from
own memo, and the category's root renders that frame when the read lands on the root, so `Fake`, sees none of the frames rendering above it, so what a category renders never depends
`{n}` and `{n.path}` read one pick: per render and group for a reference path, per pick for a on who references it. That read opens the frame of each scope around what it lands on, taken
path through a name, and per render for `Fake("cat.field")`. A reference read under a pick from its own memo, so every read entering by one memo, `{n}` and `{n.path}` alike, reads one
draws in the draw group of the read of the name, the group the draw fences survey it in; a pick pick: per render and group for a reference path, per pick for a path through a name, per
with draw groups of its own would draw where no fence looks. render for `Fake("cat.field")`. A frame is looked up on the stack of frames rendering, and
nowhere else.
A pick keeps only the levels a read of the name addresses, so a template draws the same way
under a name as anywhere else (goal 5.2). A reference read under a pick draws in the draw group
of the read of the name, the group the draw fences survey it in, so two names bound to one
category share what it reads by reference path. `todo.md` item 4 ends that: once every `{…}`
draws afresh, nothing is held for the render.
+4 -6
View File
@@ -9,7 +9,7 @@ import "strings"
type drawMemo struct { type drawMemo struct {
variant map[string]node variant map[string]node
value map[string]readValue value map[string]readValue
frames map[*nameScope]*pickFrame enteredFrames map[*nameScope]*pickFrame
} }
// groupDraws is what one render's draw group has drawn for its reference paths: a memo, and the // groupDraws is what one render's draw group has drawn for its reference paths: a memo, and the
@@ -34,7 +34,7 @@ type renderDraws struct {
unnamed groupDraws unnamed groupDraws
named map[string]*groupDraws named map[string]*groupDraws
trace renderTrace trace renderTrace
frames *frameStack frameStack *frameStack
} }
// renderScope is where a render reads its reference paths: its reference draws, in the draw group of the // renderScope is where a render reads its reference paths: its reference draws, in the draw group of the
@@ -45,10 +45,8 @@ type renderScope struct {
draws *renderDraws draws *renderDraws
group string group string
row renderedRow row renderedRow
// entry is the memo of the read that entered the category rendering, nil where none did; // base is the depth of the frame stack when the read rendering the category entered it,
// base, the depth of the frame stack when it entered, below which no frame is the // below which no frame is the category's.
// category's.
entry *drawMemo
base int base int
// pick is the named pick what renders is part of, at pickKey, its path from the name; nil // pick is the named pick what renders is part of, at pickKey, its path from the name; nil
// outside one. // outside one.
+50 -46
View File
@@ -1,7 +1,6 @@
package fejkdata package fejkdata
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
) )
@@ -64,8 +63,9 @@ type formatToken struct {
body string // the braces' content, as written body string // the braces' content, as written
fn string // builtinCall fn string // builtinCall
args []string // builtinCall args []string // builtinCall
names []string // nameRead: the '|' arms; builtinCall: the operands its builtin reads; nameBind: the reference it binds names []string // nameRead: the '|' arms; builtinCall: the operands its builtin reads
bound string // nameBind: the name // nameBind: the reference it binds, and the name.
boundRef, bound string
} }
type tokenKind uint8 type tokenKind uint8
@@ -98,7 +98,7 @@ func parseFormat(format string) ([]formatToken, error) {
} }
flush() flush()
if ref, name, binds := cutOutside(u.body, asWord); binds && indexOutside(u.body, '(') < 0 { if ref, name, binds := cutOutside(u.body, asWord); binds && indexOutside(u.body, '(') < 0 {
toks = append(toks, formatToken{kind: nameBind, body: u.body, names: []string{ref}, bound: name}) toks = append(toks, formatToken{kind: nameBind, body: u.body, boundRef: ref, bound: name})
return nil return nil
} }
if indexOutside(u.body, '(') < 0 { if indexOutside(u.body, '(') < 0 {
@@ -215,20 +215,15 @@ func parseChecked(format string, fields map[string]node) ([]formatToken, []unbou
} }
// unboundRead is a token reading a head no field holds, which only a name bound around the // unboundRead is a token reading a head no field holds, which only a name bound around the
// template can answer; err is the refusal where none does. // template can answer; err is the refusal where none does. whole is a read of the name itself,
// no path into it, and operand a builtin's read of it.
type unboundRead struct { type unboundRead struct {
head string head string
whole bool
operand bool
err error err error
} }
// fieldMiss is a head no field of the template holds.
type fieldMiss struct {
head string
err error
}
func (m *fieldMiss) Error() string { return m.err.Error() }
// checkTokens proves every parsed token names an existing field or a known function, // checkTokens proves every parsed token names an existing field or a known function,
// so a typo'd or dangling reference is a New-time error. A head no field holds comes // so a typo'd or dangling reference is a New-time error. A head no field holds comes
// back unbound, for bindNames to look up among the names. // back unbound, for bindNames to look up among the names.
@@ -247,32 +242,16 @@ func checkTokens(toks []formatToken, fields map[string]node) ([]unboundRead, err
func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) { func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
switch t.kind { switch t.kind {
case builtinCall: case builtinCall:
err := checkFunc(t, fields) if err := checkFunc(t, fields); err != nil {
var miss *fieldMiss
if errors.As(err, &miss) {
return []unboundRead{{miss.head, err}}, nil
}
return nil, err return nil, err
}
return checkReads(t, fields, true)
case nameBind: case nameBind:
return nil, checkBind(t) return nil, checkBind(t)
case nameRead: case nameRead:
var unbound []unboundRead unbound, err := checkReads(t, fields, false)
for _, name := range t.names {
if isRef(name) {
if _, _, err := refShape(name); err != nil {
return nil, fmt.Errorf("token {%s}: %w", t.body, err)
}
continue // its target is checked at New (see linkRefs)
}
err := checkArm(name, fields, len(t.names) == 1)
var miss *fieldMiss
if errors.As(err, &miss) {
unbound = append(unbound, unboundRead{miss.head, fmt.Errorf("token {%s}: %w", t.body, err)})
continue
}
if err != nil { if err != nil {
return nil, fmt.Errorf("token {%s}: %w", t.body, err) return nil, err
}
} }
// Last, so an arm broken on its own terms is reported as that: a repeat is // Last, so an arm broken on its own terms is reported as that: a repeat is
// the consequence of such a mistake, not the mistake itself. // the consequence of such a mistake, not the mistake itself.
@@ -281,10 +260,35 @@ func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
return nil, nil return nil, nil
} }
// checkReads proves each name t reads, an arm or an operand, is a reference or a path into a
// field, returning those whose head no field holds.
func checkReads(t formatToken, fields map[string]node, operands bool) ([]unboundRead, error) {
var unbound []unboundRead
for _, name := range t.names {
if isRef(name) {
if _, _, err := refShape(name); err != nil {
return nil, fmt.Errorf("token {%s}: %w", t.body, err)
}
continue // its target is checked at New (see linkRefs)
}
missing, err := checkArm(name, fields, !operands && len(t.names) == 1)
if err == nil {
continue
}
err = fmt.Errorf("token {%s}: %w", t.body, err)
if !missing {
return nil, err
}
a := splitArm(name, nil)
unbound = append(unbound, unboundRead{head: a.head, whole: len(a.tail) == 0, operand: operands, err: err})
}
return unbound, nil
}
// checkBind proves a {ref as name} token binds a reference to a name no other token // checkBind proves a {ref as name} token binds a reference to a name no other token
// could read as something else. // could read as something else.
func checkBind(t formatToken) error { func checkBind(t formatToken) error {
ref, name := t.names[0], t.bound ref, name := t.boundRef, t.bound
if trimmed := strings.TrimSpace(ref) + asWord + strings.TrimSpace(name); trimmed != t.body { if trimmed := strings.TrimSpace(ref) + asWord + strings.TrimSpace(name); trimmed != t.body {
return fmt.Errorf("token {%s}: write {%s}", t.body, trimmed) return fmt.Errorf("token {%s}: write {%s}", t.body, trimmed)
} }
@@ -305,34 +309,34 @@ func checkBind(t formatToken) error {
// checkArm validates one sibling name or path against a template's fields. // checkArm validates one sibling name or path against a template's fields.
// wholeToken says the name is the token's entire body, so {/name} would render // wholeToken says the name is the token's entire body, so {/name} would render
// the same value and can be offered as the reference spelling. A head no field // the same value and can be offered as the reference spelling. missing says no
// holds is a *fieldMiss. // field holds the head, which a name may yet answer.
func checkArm(name string, fields map[string]node, wholeToken bool) error { func checkArm(name string, fields map[string]node, wholeToken bool) (missing bool, err error) {
a := splitArm(name, nil) a := splitArm(name, nil)
if err := checkSegments(a); err != nil { if err := checkSegments(a); err != nil {
return err return false, err
} }
field, ok := fields[a.head] field, ok := fields[a.head]
if !ok { if !ok {
if a.head == "" { if a.head == "" {
return fmt.Errorf("a name is never empty, so this token can name no field") return false, fmt.Errorf("a name is never empty, so this token can name no field")
} }
if isOption(a.head) { if isOption(a.head) {
return fmt.Errorf("%q is an option and can never be a field", a.head) return false, fmt.Errorf("%q is an option and can never be a field", a.head)
} }
if len(fields) == 0 { if len(fields) == 0 {
hint := "" hint := ""
if wholeToken && hintableRef(name) { if wholeToken && hintableRef(name) {
hint = fmt.Sprintf(" — write {/%s} to reference the data", name) hint = fmt.Sprintf(" — write {/%s} to reference the data", name)
} }
return &fieldMiss{a.head, fmt.Errorf("no field %q; a token names a sibling field, and this template has none%s", a.head, hint)} return true, fmt.Errorf("no field %q; a token names a sibling field, and this template has none%s", a.head, hint)
} }
return &fieldMiss{a.head, fmt.Errorf("no field %q", a.head)} return true, fmt.Errorf("no field %q", a.head)
} }
if err := checkPathResolves(field, a.tail, a.head); err != nil { if err := checkPathResolves(field, a.tail, a.head); err != nil {
return fmt.Errorf("field %q: %w", a.head, err) return false, fmt.Errorf("field %q: %w", a.head, err)
} }
return nil return false, nil
} }
// hintableRef reports whether {/name} is a reference the grammar accepts, so the // hintableRef reports whether {/name} is a reference the grammar accepts, so the
@@ -383,7 +387,7 @@ type arm struct {
leaves []node // every node the path may land on, one per variant it passes leaves []node // every node the path may land on, one per variant it passes
cover node // what holding the path pins: the first choice it passes, else its leaf cover node // what holding the path pins: the first choice it passes, else its leaf
kind armKind kind armKind
bind *nameBinding // namedRead: the binding of the name it reads through named *nameBinding // namedRead: the binding of the name it reads through
} }
// armKind is how expand reads an arm, fixed at compile. // armKind is how expand reads an arm, fixed at compile.
+8 -3
View File
@@ -181,10 +181,15 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
}); err != nil { }); err != nil {
return err return err
} }
return scope(func(path string, m node) error { for _, pass := range []func(string, *template) error{linkNames, checkNameReads} {
if err := scope(func(path string, m node) error {
if t, ok := m.(*template); ok { if t, ok := m.(*template); ok {
return linkNames(path, t) return pass(path, t)
}
return nil
}); err != nil {
return err
}
} }
return nil return nil
})
} }
+257 -90
View File
@@ -2,6 +2,7 @@ package fejkdata
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
) )
@@ -16,7 +17,8 @@ type nameScope struct {
} }
// nameBinding is one {ref as name} token: the scope it binds name in, its index among // nameBinding is one {ref as name} token: the scope it binds name in, its index among
// that scope's bindings, and, once linked, the head node and tail its reference reads. // that scope's bindings, its uses, and, once linked, the head node and tail its
// reference reads and the keys its reads address.
type nameBinding struct { type nameBinding struct {
name string name string
ref string ref string
@@ -25,9 +27,18 @@ type nameBinding struct {
scope *nameScope scope *nameScope
index int index int
binder *template binder *template
read bool uses []nameUse
head node head node
tail []string tail []string
// addressed is every key a read of the name lands on or passes, the spelling of the
// first read reaching it beside it; a pick keeps the draws at these keys, and only these.
addressed map[string]string
}
// nameUse is one read of a name: whole, with no path into it, an operand of a builtin, or in a
// repeat nested inside the name's scope.
type nameUse struct {
whole, operand, nested bool
} }
func (sc *nameScope) lookup(name string) *nameBinding { func (sc *nameScope) lookup(name string) *nameBinding {
@@ -42,22 +53,25 @@ func (sc *nameScope) lookup(name string) *nameBinding {
// bindNames gives every template of a compiled category or inline template the scope its // bindNames gives every template of a compiled category or inline template the scope its
// names live in, and refuses a name bound twice along one chain of scopes, a binding in a // names live in, and refuses a name bound twice along one chain of scopes, a binding in a
// choice's item, a name a field spells too, a read no field or name answers, and a binding // choice's item, a name a field spells too, a read no field or name answers, and a binding
// nothing reads. // read by nothing, or once whole, which its reference spells.
func bindNames(root node) error { func bindNames(root node) error {
top := &nameScope{owner: root} top := &nameScope{owner: root}
scopes := []*nameScope{top} scopes := []*nameScope{top}
var gather func(n node, scope *nameScope, inChoice bool, where string) error var gather func(n node, scope *nameScope, inChoice bool, where string) error
gather = func(n node, scope *nameScope, inChoice bool, where string) error { gather = func(n node, scope *nameScope, inChoice bool, where string) error {
if t, isTemplate := n.(*template); isTemplate { switch t := n.(type) {
case *template:
if t.repeat > 1 && n != root { if t.repeat > 1 && n != root {
scope = &nameScope{up: scope, owner: t} scope = &nameScope{up: scope, owner: t}
scopes = append(scopes, scope) scopes = append(scopes, scope)
inChoice = false inChoice = false
} }
t.scope = scope t.nameScope = scope
if err := scope.bindAll(t, inChoice, where); err != nil { if err := scope.bindAll(t, inChoice, where); err != nil {
return err return err
} }
case *choice:
t.nameScope = scope
} }
_, isChoice := n.(*choice) _, isChoice := n.(*choice)
return eachContained(n, where, func(c node, where string) error { return gather(c, scope, inChoice || isChoice, where) }) return eachContained(n, where, func(c node, where string) error { return gather(c, scope, inChoice || isChoice, where) })
@@ -72,11 +86,11 @@ func bindNames(root node) error {
} }
} }
} }
if err := resolveReads(root, ""); err != nil { if err := resolveReads(root, "", scopes); err != nil {
return err return err
} }
for _, sc := range scopes { for _, sc := range scopes {
if err := sc.close(); err != nil { if err := sc.settle(); err != nil {
return err return err
} }
} }
@@ -96,7 +110,7 @@ func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
if b, twice := sc.bindings[tok.bound]; twice { if b, twice := sc.bindings[tok.bound]; twice {
return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.body, tok.bound, sc.spelled(), b.body) return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.body, tok.bound, sc.spelled(), b.body)
} }
b := &nameBinding{name: tok.bound, ref: tok.names[0], body: tok.body, where: where, scope: sc, index: len(sc.order), binder: t} b := &nameBinding{name: tok.bound, ref: tok.boundRef, body: tok.body, where: where, scope: sc, index: len(sc.order), binder: t}
if sc.bindings == nil { if sc.bindings == nil {
sc.bindings = map[string]*nameBinding{} sc.bindings = map[string]*nameBinding{}
} }
@@ -114,15 +128,24 @@ func (sc *nameScope) spelled() string {
return "in one repeat" return "in one repeat"
} }
// close refuses a binding nothing reads, and gives the owner of a scope binding names the scope. // settle refuses a binding read by nothing, or read once whole, outside a repeat, where its
func (sc *nameScope) close() error { // reference spells the same pick; and hands a scope binding names to its owner, which renders a
// frame of it.
func (sc *nameScope) settle() error {
for _, b := range sc.order { for _, b := range sc.order {
if !b.read { if len(b.uses) == 0 {
return fmt.Errorf("%stoken {%s}: nothing reads name %q; drop the token", b.where, b.body, b.name) return fmt.Errorf("%stoken {%s}: nothing reads name %q; drop the token", b.where, b.body, b.name)
} }
if r := b.uses[0]; len(b.uses) == 1 && r.whole && !r.nested {
spelling := "{" + b.ref + "}"
if r.operand {
spelling = b.ref
}
return fmt.Errorf("%stoken {%s}: name %q is read once, so it keeps nothing; write %s where it is read, and drop the token", b.where, b.body, b.name, spelling)
}
} }
if t, isTemplate := sc.owner.(*template); isTemplate && len(sc.order) > 0 { if t, isTemplate := sc.owner.(*template); isTemplate && len(sc.order) > 0 {
t.ownScope = sc t.ownNameScope = sc
} }
return nil return nil
} }
@@ -143,9 +166,10 @@ func eachContained(n node, where string, fn func(c node, where string) error) er
} }
// resolveReads answers each unbound read under n with a name its template sees, fields before the // resolveReads answers each unbound read under n with a name its template sees, fields before the
// format as compile reports them, and refuses a field spelling a name. // format as compile reports them, and refuses a field spelling a name. scopes are every scope of
func resolveReads(n node, where string) error { // the category, which a refusal searches for a name bound where the read cannot see it.
if err := eachContained(n, where, resolveReads); err != nil { func resolveReads(n node, where string, scopes []*nameScope) error {
if err := eachContained(n, where, func(c node, where string) error { return resolveReads(c, where, scopes) }); err != nil {
return err return err
} }
t, isTemplate := n.(*template) t, isTemplate := n.(*template)
@@ -153,7 +177,7 @@ func resolveReads(n node, where string) error {
return nil return nil
} }
for _, name := range sortedNames(t.fields) { for _, name := range sortedNames(t.fields) {
if b := t.scope.lookup(name); b != nil { if b := t.nameScope.lookup(name); b != nil {
holder := "the root template" holder := "the root template"
if where != "" { if where != "" {
holder = strings.TrimSuffix(where, ": ") holder = strings.TrimSuffix(where, ": ")
@@ -162,32 +186,52 @@ func resolveReads(n node, where string) error {
} }
} }
for _, u := range t.unbound { for _, u := range t.unbound {
b := t.scope.lookup(u.head) b := t.nameScope.lookup(u.head)
if b == nil { if b == nil {
return fmt.Errorf("%s%w", where, u.err) return fmt.Errorf("%s%w%s", where, u.err, boundElsewhere(u.head, scopes))
} }
b.read = true b.uses = append(b.uses, nameUse{whole: u.whole, operand: u.operand, nested: t.nameScope != b.scope})
} }
return nil return nil
} }
// boundElsewhere names the repeat binding name, for a read outside it.
func boundElsewhere(name string, scopes []*nameScope) string {
for _, sc := range scopes {
if b, ok := sc.bindings[name]; ok {
return fmt.Sprintf("; name %q is bound inside the repeat at %s, which a read outside the repeat cannot see", name, strings.TrimSuffix(b.where, ": "))
}
}
return ""
}
// isName reports whether a head t reads is a name: neither a reference nor a field. // isName reports whether a head t reads is a name: neither a reference nor a field.
func (t *template) isName(head string) bool { func (t *template) isName(head string) bool {
return !isRef(head) && t.fields[head] == nil && t.scope.lookup(head) != nil return !isRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil
} }
// linkNames compiles t's reads of a name as paths from the head its binding's reference names, once // linkBindings gives each binding t's tokens make the head and tail its reference resolved to.
// every template is linked, so the binder has resolved that reference. func linkBindings(t *template) {
func linkNames(path string, t *template) error { for _, tok := range t.tokens {
if tok.kind != nameBind {
continue
}
b := t.nameScope.bindings[tok.bound]
ref := t.link.refs[b.ref]
b.head, b.tail = t.link.refHeads[ref.head], ref.tail
}
}
// namedReads calls fn with every read of a name t's format makes, and the token holding it.
func namedReads(t *template, fn func(o *op, a *arm) error) error {
for i := range t.compiled.ops { for i := range t.compiled.ops {
o := &t.compiled.ops[i] o := &t.compiled.ops[i]
for _, reads := range [][]arm{o.arms, o.operands} { for _, reads := range [][]arm{o.arms, o.operands} {
for j := range reads { for j := range reads {
if reads[j].kind != namedRead { if reads[j].kind == namedRead {
continue if err := fn(o, &reads[j]); err != nil {
return err
} }
if err := linkName(t, &reads[j]); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
} }
} }
} }
@@ -195,12 +239,19 @@ func linkNames(path string, t *template) error {
return nil return nil
} }
func linkName(t *template, a *arm) error { // linkNames compiles t's reads of a name as paths from the head its binding's reference names, once
b := t.scope.lookup(a.head) // every template is linked, so each binder has resolved that reference.
if b.head == nil { func linkNames(path string, t *template) error {
ref := b.binder.link.refs[b.ref] return namedReads(t, func(o *op, a *arm) error {
b.head, b.tail = b.binder.link.refHeads[ref.head], ref.tail if err := linkName(t, a); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
} }
return nil
})
}
func linkName(t *template, a *arm) error {
b := t.nameScope.lookup(a.head)
if hasSelector(a.tail) { if hasSelector(a.tail) {
return fmt.Errorf("a path through name %q selects no row; select it in the reference %q binds", a.head, a.head) return fmt.Errorf("a path through name %q selects no row; select it in the reference %q binds", a.head, a.head)
} }
@@ -209,20 +260,94 @@ func linkName(t *template, a *arm) error {
return err return err
} }
w := compilePath(b.head, full) w := compilePath(b.head, full)
a.bind, a.steps, a.leaves, a.cover = b, w.steps, w.leaves, w.cover a.named, a.steps, a.leaves, a.cover = b, w.steps, w.leaves, w.cover
a.levels = make([]string, len(full)+1) a.levels = make([]string, len(full)+1)
for i := range a.levels { for i := range a.levels {
a.levels[i] = joinSegments(full[:i]) a.levels[i] = joinSegments(full[:i])
} }
a.path = joinSegments(full) a.path = joinSegments(full)
if b.addressed == nil {
b.addressed = map[string]string{}
}
for _, key := range append(a.levels[len(b.tail):], a.path) {
if _, seen := b.addressed[key]; !seen {
b.addressed[key] = a.spelling
}
}
return nil return nil
} }
// namedPick is one draw of a name: the variant drawn at each level under it, the value each // checkNameReads refuses a read of a name rendering, under the pick, a field it renders twice where
// path through it read, keyed by the path from the name, and the table rows they pinned. A // a path through the name reads one draw of it: the pick keeps one draw there, and the two
// reference read under it draws in the draw group of the read of the name, which the draw // renders would disagree with it.
// fences survey it in. func checkNameReads(path string, t *template) error {
return namedReads(t, func(o *op, a *arm) error {
for _, leaf := range a.leaves {
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
}
}
return nil
})
}
// checkOnce walks n, rendering at key under b's pick, into each level a read of b addresses.
func (b *nameBinding) checkOnce(read string, n node, key string) error {
switch n := n.(type) {
case *choice:
for _, item := range n.items {
if err := b.checkOnce(read, item, key); err != nil {
return err
}
}
case *template:
return b.checkTemplateOnce(read, n, key)
}
return nil
}
func (b *nameBinding) checkTemplateOnce(read string, t *template, key string) error {
fresh := map[string]int{}
var into []arm
for _, o := range t.compiled.ops {
for _, a := range append(o.arms[:len(o.arms):len(o.arms)], o.operands...) {
if isRef(a.head) || a.kind == namedRead {
continue
}
if _, kept := b.addressed[join(key, a.path)]; kept {
into = append(into, a)
}
if a.kind == freshRead {
fresh[a.head]++
}
}
}
heads := make([]string, 0, len(fresh))
for head := range fresh {
heads = append(heads, head)
}
sort.Strings(heads)
for _, head := range heads {
if by, kept := b.addressed[join(key, head)]; kept && fresh[head] > 1 {
return fmt.Errorf("{%s} renders field %q twice, so {%s} cannot say which draw it reads; drop one of them", read, head, by)
}
}
for _, a := range into {
for _, leaf := range a.leaves {
if err := b.checkOnce(read, leaf, join(key, a.path)); err != nil {
return err
}
}
}
return nil
}
// namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the
// value each read there produced, keyed by the path from the name, and the table rows they
// pinned. A reference read under it draws in the draw group of the read of the name, which the
// draw fences survey it in.
type namedPick struct { type namedPick struct {
named *nameBinding
memo drawMemo memo drawMemo
pins pinSet pins pinSet
} }
@@ -234,7 +359,11 @@ type pickFrame struct {
} }
func newPickFrame(scope *nameScope) *pickFrame { func newPickFrame(scope *nameScope) *pickFrame {
return &pickFrame{scope: scope, picks: make([]namedPick, len(scope.order))} f := &pickFrame{scope: scope, picks: make([]namedPick, len(scope.order))}
for i, b := range scope.order {
f.picks[i].named = b
}
return f
} }
// frameStack is the frames of the name scopes rendering, innermost last. A render's draws point // frameStack is the frames of the name scopes rendering, innermost last. A render's draws point
@@ -244,79 +373,104 @@ type frameStack struct {
frames []*pickFrame frames []*pickFrame
} }
// pushFrame opens f until popFrame closes it, returning the mark popFrame takes. // pushFrame opens f until popFrames closes it, returning the mark popFrames takes.
func (d *renderDraws) pushFrame(f *pickFrame) int { func (d *renderDraws) pushFrame(f *pickFrame) int {
mark := d.depth() mark := d.depth()
if d.frames == nil { if d.frameStack == nil {
d.frames = &frameStack{} d.frameStack = &frameStack{}
} }
d.frames.frames = append(d.frames.frames, f) d.frameStack.frames = append(d.frameStack.frames, f)
return mark return mark
} }
func (d *renderDraws) popFrame(mark int) { d.frames.frames = d.frames.frames[:mark] } func (d *renderDraws) popFrames(mark int) {
if d.frameStack != nil {
d.frameStack.frames = d.frameStack.frames[:mark]
}
}
func (d *renderDraws) depth() int { func (d *renderDraws) depth() int {
if d.frames == nil { if d.frameStack == nil {
return 0 return 0
} }
return len(d.frames.frames) return len(d.frameStack.frames)
} }
// entering is sc as a read entering another category sees it: none of the frames rendering, and // enter is sc as a read entering a category sees it, landing on n: none of the frames rendering,
// memo, which keeps the frame of each scope the read enters past its owner. // and, from memo, the frame of each scope binding names around n, so every read entering with
func (sc renderScope) entering(memo *drawMemo) renderScope { // memo reads one pick of each name. It returns the mark that closes those frames.
sc.base, sc.entry, sc.pick = sc.draws.depth(), memo, nil func (sc renderScope) enter(n node, memo *drawMemo) (renderScope, int) {
return sc sc.base, sc.pick = sc.draws.depth(), nil
} for scope := scopeAround(n); scope != nil; scope = scope.up {
if len(scope.order) > 0 {
// frameOf is the frame a render of scope's owner opens: the one the read entering the category sc.draws.pushFrame(memo.enteredFrame(scope))
// keeps, so {n} and {n.path} read one frame, else a fresh one.
func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
if sc.entry == nil {
return newPickFrame(scope)
} }
return sc.entry.frameOf(scope) }
return sc, sc.base
} }
func (m *drawMemo) frameOf(scope *nameScope) *pickFrame { // scopeAround is the innermost name scope a render of n reads names in, short of the frames n
f, ok := m.frames[scope] // renders itself, one per iteration of a repeat.
func scopeAround(n node) *nameScope {
switch n := n.(type) {
case *template:
if n.repeat > 1 && n.nameScope != nil && n.nameScope.owner == n {
return n.nameScope.up
}
return n.nameScope
case *choice:
return n.nameScope
}
return nil
}
func (m *drawMemo) enteredFrame(scope *nameScope) *pickFrame {
f, ok := m.enteredFrames[scope]
if !ok { if !ok {
f = newPickFrame(scope) f = newPickFrame(scope)
if m.frames == nil { if m.enteredFrames == nil {
m.frames = map[*nameScope]*pickFrame{} m.enteredFrames = map[*nameScope]*pickFrame{}
} }
m.frames[scope] = f m.enteredFrames[scope] = f
} }
return f return f
} }
// pickOf is b's pick: in the frame rendering its scope since the read entering the category, else // renderFrame opens a fresh frame of t's scope, where t binds names and no read entering the
// in the frame that read keeps, as for Fake("cat.field") or {/cat.field}. // category opened one, returning the mark that closes it, or -1.
func (sc renderScope) pickOf(b *nameBinding) *namedPick { func (sc renderScope) renderFrame(t *template) int {
if stack := sc.draws.frames; stack != nil { if t.ownNameScope == nil || sc.frameOf(t.ownNameScope) != nil {
for i := len(stack.frames) - 1; i >= sc.base; i-- { return -1
if f := stack.frames[i]; f.scope == b.scope {
return &f.picks[b.index]
} }
} return sc.draws.pushFrame(newPickFrame(t.ownNameScope))
}
return &sc.entryMemo().frameOf(b.scope).picks[b.index]
} }
// entryMemo is the memo of the read that entered the category rendering, else the unnamed draw // frameOf is the frame of scope rendering since the read entering the category, nil where none is.
// group's of the render: one memo for a render begun below a category's root, whatever draw group func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
// its templates name. if stack := sc.draws.frameStack; stack != nil {
func (sc renderScope) entryMemo() *drawMemo { for i := len(stack.frames) - 1; i >= sc.base; i-- {
if sc.entry != nil { if f := stack.frames[i]; f.scope == scope {
return sc.entry return f
} }
return &sc.draws.unnamed.memo }
}
return nil
} }
func readName(s *generatorState, sc renderScope, a arm) readValue { func readName(s *generatorState, sc renderScope, a arm) readValue {
p := sc.pickOf(a.bind) f := sc.frameOf(a.named.scope)
return readPicked(s, p, a.bind.head, a.steps, a.levels, a.path, sc.entering(&p.memo)) if f == nil {
panic(internalError("name %q is read where no frame of its scope renders", a.named.name))
}
p := &f.picks[a.named.index]
if r, done := p.memo.value[a.path]; done {
return r
}
leaf := p.draw(s, a.named.head, a.steps, a.levels, a.path)
sc, mark := sc.enter(leaf, &p.memo)
r := p.renderAt(s, leaf, a.path, sc)
sc.draws.popFrames(mark)
return r
} }
// drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one. // drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one.
@@ -327,14 +481,25 @@ func (sc renderScope) drawRowOf(s *generatorState, t *table) int {
return t.drawRow(s) return t.drawRow(s)
} }
// readUnder reads a of t, which renders as part of the pick sc.pick at sc.pickKey: once per pick, // keeps reports whether the pick sc renders part of keeps a, a read of t: whether a read of the
// by its path from the name. // name addresses the level a starts at.
func (sc renderScope) keeps(a arm) bool {
_, kept := sc.pick.named.addressed[underKey(sc.pickKey, a.head)]
return kept
}
// readUnder reads a of t, which renders as part of the pick sc.pick at sc.pickKey, where a read of
// the name addresses it: once per pick, by its path from the name.
func readUnder(s *generatorState, t *template, sc renderScope, a arm) readValue { func readUnder(s *generatorState, t *template, sc renderScope, a arm) readValue {
p, key := sc.pick, underKey(sc.pickKey, a.path)
if r, done := p.memo.value[key]; done {
return r
}
levels := make([]string, len(a.levels)) levels := make([]string, len(a.levels))
for i, l := range a.levels { for i, l := range a.levels {
levels[i] = underKey(sc.pickKey, l) levels[i] = underKey(sc.pickKey, l)
} }
return readPicked(s, sc.pick, t.head(a.head), a.steps, levels, underKey(sc.pickKey, a.path), sc) return p.renderAt(s, p.draw(s, t.head(a.head), a.steps, levels, key), key, sc)
} }
// underKey is the key of path under the pick key prefix. It never returns prefix itself, which a // underKey is the key of path under the pick key prefix. It never returns prefix itself, which a
@@ -346,15 +511,17 @@ func underKey(prefix, path string) string {
return prefix + "." + path return prefix + "." + path
} }
// readPicked draws the path key names under pick p, once, and renders what it lands on as part of p. // draw draws the path key names under p, its variant at key kept too.
func readPicked(s *generatorState, p *namedPick, head node, steps []pathStep, levels []string, key string, sc renderScope) readValue { func (p *namedPick) draw(s *generatorState, head node, steps []pathStep, levels []string, key string) node {
if r, done := p.memo.value[key]; done {
return r
}
leaf := drawSteps(s, head, steps, &p.pins, &p.memo, levels) leaf := drawSteps(s, head, steps, &p.pins, &p.memo, levels)
if c, isChoice := leaf.(*choice); isChoice { if c, isChoice := leaf.(*choice); isChoice {
leaf = p.memo.variantOf(s, c, key) leaf = p.memo.variantOf(s, c, key)
} }
return leaf
}
// renderAt renders leaf, drawn at key, as part of p, and keeps what it rendered.
func (p *namedPick) renderAt(s *generatorState, leaf node, key string, sc renderScope) readValue {
sc = sc.at(leaf, &p.pins) sc = sc.at(leaf, &p.pins)
sc.pick, sc.pickKey = p, key sc.pick, sc.pickKey = p, key
r := renderLeaf(s, leaf, sc) r := renderLeaf(s, leaf, sc)
+3 -2
View File
@@ -28,6 +28,7 @@ type choice struct {
items []node items []node
cum []float64 cum []float64
shared map[string]bool shared map[string]bool
nameScope *nameScope // where its items look a name up, set by bindNames
} }
func (*choice) isNode() {} func (*choice) isNode() {}
@@ -56,8 +57,8 @@ type template struct {
unbound []unboundRead // the heads its tokens read that no field holds unbound []unboundRead // the heads its tokens read that no field holds
// Filled by `bindNames`, from the compiled category: // Filled by `bindNames`, from the compiled category:
scope *nameScope // where its tokens look a name up nameScope *nameScope // where its tokens look a name up
ownScope *nameScope // the scope it renders a frame of: a category's, on its root, or a repeat's, per iteration ownNameScope *nameScope // the scope it renders a frame of: a category's, on its root, or a repeat's, per iteration
// Filled by `linkTemplate`, from the assembled tree: // Filled by `linkTemplate`, from the assembled tree:
link templateLink link templateLink
+2 -2
View File
@@ -263,8 +263,8 @@ func recordOf(n node) (*template, []Column, error) {
// over sc's draws; a table's columns read the row pinned there. // over sc's draws; a table's columns read the row pinned there.
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.ownScope != nil { if mark := sc.renderFrame(t); mark >= 0 {
defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(t.ownScope))) defer sc.draws.popFrames(mark)
} }
if t.site.isFormat() { if t.site.isFormat() {
sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins) sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins)
+11 -3
View File
@@ -78,7 +78,10 @@ func linkRefs(root map[string]node) error {
}); err != nil { }); err != nil {
return err return err
} }
return eachTemplate(root, func(_ []string, path string, t *template) error { return linkNames(path, t) }) if err := eachTemplate(root, func(_ []string, path string, t *template) error { return linkNames(path, t) }); err != nil {
return err
}
return eachTemplate(root, func(_ []string, path string, t *template) error { return checkNameReads(path, t) })
} }
// linkTemplate resolves t's references in category, "" for an inline template, and // linkTemplate resolves t's references in category, "" for an inline template, and
@@ -89,6 +92,7 @@ func linkTemplate(folder []string, path, category string, t *template, root map[
return err return err
} }
t.link, t.compiled = link, compileFormat(t.tokens, link.refs, t.isName) t.link, t.compiled = link, compileFormat(t.tokens, link.refs, t.isName)
linkBindings(t)
compileArms(t) compileArms(t)
return nil return nil
} }
@@ -253,12 +257,16 @@ func resolveCategory(root map[string]node, segments []string) (categorySegs []st
return segments[:i], n, segments[i:], nil return segments[:i], n, segments[i:], nil
} }
// refTokens returns the reference names a format reads, as tokens or as operands. // refTokens returns the reference names a format reads, as tokens or as operands, or binds.
func refTokens(toks []formatToken) []string { func refTokens(toks []formatToken) []string {
var refs []string var refs []string
seen := map[string]bool{} seen := map[string]bool{}
for _, tok := range toks { for _, tok := range toks {
for _, name := range tok.names { names := tok.names
if tok.kind == nameBind {
names = []string{tok.boundRef}
}
for _, name := range names {
if isRef(name) && !seen[name] { if isRef(name) && !seen[name] {
seen[name] = true seen[name] = true
refs = append(refs, name) refs = append(refs, name)
+24 -15
View File
@@ -33,7 +33,9 @@ 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.groupDraws().pins)), nil group := sc.groupDraws()
sc, _ = sc.at(n, &group.pins).enter(n, &group.memo)
return render(f.rand, n, sc), 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
@@ -78,8 +80,8 @@ func render(s *generatorState, n node, sc renderScope) string {
case *template: case *template:
sc = sc.in(n) sc = sc.in(n)
if n.repeat == 1 { if n.repeat == 1 {
if n.ownScope != nil { if mark := sc.renderFrame(n); mark >= 0 {
defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(n.ownScope))) defer sc.draws.popFrames(mark)
} }
if lit, fixed := n.fixedText(); fixed { if lit, fixed := n.fixedText(); fixed {
return lit return lit
@@ -93,30 +95,29 @@ func render(s *generatorState, n node, sc renderScope) string {
} }
// renderRepeat renders each iteration of t as a render of its own, inside the name scopes // renderRepeat renders each iteration of t as a render of its own, inside the name scopes
// rendering t, where a name bound outside t keeps its pick in the frame sc.entryMemo keeps. // rendering t, where a name bound outside t keeps its pick.
func renderRepeat(s *generatorState, t *template, sc renderScope) string { func renderRepeat(s *generatorState, t *template, sc renderScope) string {
entry := sc.entryMemo()
var b strings.Builder var b strings.Builder
b.Grow(t.repeat * (t.compiled.grow + len(t.separator))) b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
for i := 0; i < t.repeat; i++ { for i := 0; i < t.repeat; i++ {
if i > 0 { if i > 0 {
b.WriteString(t.separator) b.WriteString(t.separator)
} }
b.WriteString(expandAnew(s, t, sc.draws.frames, sc.base, entry)) b.WriteString(expandAnew(s, t, sc.draws.frameStack, sc.base))
} }
return b.String() return b.String()
} }
// expandAnew expands one repeat iteration of t as a render of its own, in no group, inside the // expandAnew expands one repeat iteration of t as a render of its own, in no group, inside the
// name scopes of frames from base. Inlined into render's loop, its draws would move to the heap. // name scopes of stack from base. Inlined into render's loop, its draws would move to the heap.
// //
//go:noinline //go:noinline
func expandAnew(s *generatorState, t *template, frames *frameStack, base int, entry *drawMemo) string { func expandAnew(s *generatorState, t *template, stack *frameStack, base int) string {
draws := renderDraws{frames: frames} draws := renderDraws{frameStack: stack}
if t.ownScope != nil { if t.ownNameScope != nil {
defer draws.popFrame(draws.pushFrame(newPickFrame(t.ownScope))) defer draws.popFrames(draws.pushFrame(newPickFrame(t.ownNameScope)))
} }
return expand(s, t, renderScope{draws: &draws, base: base, entry: entry}) return expand(s, t, renderScope{draws: &draws, base: base})
} }
// 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
@@ -180,10 +181,14 @@ func readField(s *generatorState, t *template, hold *drawMemo, sc renderScope, a
switch { switch {
case a.kind == namedRead: case a.kind == namedRead:
return readName(s, sc, a) return readName(s, sc, a)
case sc.pick != nil && !isRef(a.head): case sc.pick != nil && !isRef(a.head) && sc.keeps(a):
return readUnder(s, t, sc, a) return readUnder(s, t, sc, a)
}
sc.pick = nil
switch {
case a.kind == freshRead && isRef(a.head): case a.kind == freshRead && isRef(a.head):
return readValue{text: render(s, t.head(a.head), sc.entering(nil))} sc.base = sc.draws.depth()
return readValue{text: render(s, t.head(a.head), sc)}
case a.kind == freshRead: case a.kind == freshRead:
return readValue{text: render(s, t.head(a.head), sc)} return readValue{text: render(s, t.head(a.head), sc)}
} }
@@ -205,10 +210,14 @@ func readMemo(s *generatorState, t *template, memo *drawMemo, pins *pinSet, sc r
return r return r
} }
leaf := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels) leaf := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels)
mark := -1
if pins != nil { if pins != nil {
sc = sc.at(leaf, pins).entering(memo) sc, mark = sc.at(leaf, pins).enter(leaf, memo)
} }
r := renderLeaf(s, leaf, sc) r := renderLeaf(s, leaf, sc)
if mark >= 0 {
sc.draws.popFrames(mark)
}
if memo.value == nil { if memo.value == nil {
memo.value = map[string]readValue{} memo.value = map[string]readValue{}
} }
+2 -2
View File
@@ -345,8 +345,8 @@ func (t *table) compileRowFormat(format string) error {
if err != nil { if err != nil {
return err return err
} }
if len(unbound) > 0 { if err := checkTableToks(nil, unbound); err != nil {
return unbound[0].err return err
} }
t.formatTemplate = &template{format: format, tokens: toks, fields: fields, repeat: 1, isRecord: true, site: tableSite{t, formatRow}} t.formatTemplate = &template{format: format, tokens: toks, fields: fields, repeat: 1, isRecord: true, site: tableSite{t, formatRow}}
t.rowNode = &tableRow{t} t.rowNode = &tableRow{t}
+25 -5
View File
@@ -6,7 +6,7 @@
`Bar = 9` `Bar = 9`
`Next ID = 72` `Next ID = 76`
| Goal | W | | Goal | W |
|---|---| |---|---|
@@ -55,7 +55,7 @@
| 61 | 0.1.0 | | **Add a locale, with its `geo/` tree, for each of the ten most-spoken languages and each Nordic country.** | 4 | 9 | 9 | 10 | 2.1 | 30.8 | | 61 | 0.1.0 | | **Add a locale, with its `geo/` tree, for each of the ten most-spoken languages and each Nordic country.** | 4 | 9 | 9 | 10 | 2.1 | 30.8 |
| 16 | 0.1.0 | | **Add the remaining locale categories: company, phone, finance, vehicle, words.** | 3 | 8 | 8 | 9 | 2.2, 14 | 28.1 | | 16 | 0.1.0 | | **Add the remaining locale categories: company, phone, finance, vehicle, words.** | 3 | 8 | 8 | 9 | 2.2, 14 | 28.1 |
| 15 | 0.1.0 | | **Add the remaining `misc` tables and templates, one row of its detail's table per chunk.** | 3 | 9 | 8 | 8 | 2, 14, 13 | 25.9 | | 15 | 0.1.0 | | **Add the remaining `misc` tables and templates, one row of its detail's table per chunk.** | 3 | 9 | 8 | 8 | 2, 14, 13 | 25.9 |
| 3 | 0.1.0 | | **Rewrite the shipped categories so facts that belong together come from one named pick.** | 5 | 5 | 8 | 9 | 4.1 | 25.0 | | 3 | 0.1.0 | decision | **Rewrite the shipped categories so facts that belong together come from one named pick.** | 5 | 5 | 8 | 9 | 4.1 | 25.0 |
| 4 | 0.1.0 | decision | **Draw every `{…}` afresh, keep a pick only by name, and delete `drawGroup` with the fences that held picks implicitly.** | 7 | 8 | 8 | 10 | 5.2, 4.1 | 24.0 | | 4 | 0.1.0 | decision | **Draw every `{…}` afresh, keep a pick only by name, and delete `drawGroup` with the fences that held picks implicitly.** | 7 | 8 | 8 | 10 | 5.2, 4.1 | 24.0 |
| 25 | 0.1.0 | defect | **Draw `en_US.phone`'s `exch` as a NANP central office code, and assert the rule in `TestShippedUSPhone`.** | 2 | 2 | 6 | 8 | 4 | 23.1 | | 25 | 0.1.0 | defect | **Draw `en_US.phone`'s `exch` as a NANP central office code, and assert the rule in `TestShippedUSPhone`.** | 2 | 2 | 6 | 8 | 4 | 23.1 |
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 | | 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
@@ -88,6 +88,8 @@
| 32 | 0.2.0 | | **Add `misc.browser` as the parent of `misc.useragent`, so `misc.browser[Chrome].useragent` resolves.** | 3 | 3 | 4 | 6 | 4.1 | 14.4 | | 32 | 0.2.0 | | **Add `misc.browser` as the parent of `misc.useragent`, so `misc.browser[Chrome].useragent` resolves.** | 3 | 3 | 4 | 6 | 4.1 | 14.4 |
| 27 | 0.2.0 | | **Merge `en_US.ip` and `sv_SE.ip`, today byte-identical, into one `misc.ip`.** | 3 | 2 | 4 | 6 | 5.7 | 14.1 | | 27 | 0.2.0 | | **Merge `en_US.ip` and `sv_SE.ip`, today byte-identical, into one `misc.ip`.** | 3 | 2 | 4 | 6 | 5.7 | 14.1 |
| 46 | 0.2.0 | | **Report the same error every load for a table with two bad options, and for a folder with two unnamed rows files.** | 2 | 2 | 3 | 7 | 6, 9 | 14.0 | | 46 | 0.2.0 | | **Report the same error every load for a table with two bad options, and for a folder with two unnamed rows files.** | 2 | 2 | 3 | 7 | 6, 9 | 14.0 |
| 73 | 0.2.0 | question | **Decide whether the default embed's partial Swedish and US place tables stand against goal 13.1, or have the goals say which goal wins.** | 1 | 1 | 3 | 6 | 13.1 | 9.6 |
| 74 | 0.2.0 | question | **Decide whether `misc.timezone` may drop the 17 zones of territories `misc.territory` leaves out, against goal 13.1, or have the goals say which goal wins.** | 1 | 1 | 3 | 6 | 13.1 | 9.6 |
| 14 | 0.3.0 | | **Spell `misc.creditcard`'s digit runs `{digits(n)}`, and refuse a repeat over a lone `{digits(1)}` at `New`, naming that spelling.** | 3 | 3 | 4 | 6 | 5.7 | 13.9 | | 14 | 0.3.0 | | **Spell `misc.creditcard`'s digit runs `{digits(n)}`, and refuse a repeat over a lone `{digits(1)}` at `New`, naming that spelling.** | 3 | 3 | 4 | 6 | 5.7 | 13.9 |
| 38 | 0.3.0 | defect | **Compare `calcParser.binary`'s operator as a rune.** | 1 | 1 | 3 | 6 | 6 | 13.8 | | 38 | 0.3.0 | defect | **Compare `calcParser.binary`'s operator as a rune.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
| 45 | 0.3.0 | | **Name the node a selector follows in `stepInto`'s refusal.** | 1 | 1 | 3 | 6 | 6 | 13.8 | | 45 | 0.3.0 | | **Name the node a selector follows in `stepInto`'s refusal.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
@@ -99,6 +101,7 @@
| 11 | 0.3.0 | | **Test that every `geo` tree holds the five table names and the `address` columns that port across countries.** | 1 | 2 | 3 | 5 | 4 | 13.0 | | 11 | 0.3.0 | | **Test that every `geo` tree holds the five table names and the `address` columns that port across countries.** | 1 | 2 | 3 | 5 | 4 | 13.0 |
| 35 | 0.3.0 | | **Add `{base64url(n)}` for JWT shapes.** | 1 | 2 | 4 | 5 | 8 | 13.0 | | 35 | 0.3.0 | | **Add `{base64url(n)}` for JWT shapes.** | 1 | 2 | 4 | 5 | 8 | 13.0 |
| 21 | 0.3.0 | | **List `title` in both locales, and `sv_SE`'s `birth-number`, in the README's Data list of what each locale carries.** | 1 | 1 | 4 | 6 | 11 | 12.8 | | 21 | 0.3.0 | | **List `title` in both locales, and `sv_SE`'s `birth-number`, in the README's Data list of what each locale carries.** | 1 | 1 | 4 | 6 | 11 | 12.8 |
| 75 | 0.3.0 | | **Run every README `sh` example as a test, and compare each "Renders" line to a seeded render.** | 2 | 5 | 4 | 7 | 11.2 | 11.8 |
| 23 | 0.3.0 | | **Give every other shipped category that could reach something real a path that never does.** | 3 | 6 | 6 | 7 | 15 | 11.7 | | 23 | 0.3.0 | | **Give every other shipped category that could reach something real a path that never does.** | 3 | 6 | 6 | 7 | 15 | 11.7 |
| 8 | 0.3.0 | | **Move `binding`, `bind` and `checkNodeFences` out of `data.go` into a file of their own.** | 1 | 2 | 2 | 5 | 3.1 | 11.5 | | 8 | 0.3.0 | | **Move `binding`, `bind` and `checkNodeFences` out of `data.go` into a file of their own.** | 1 | 2 | 2 | 5 | 3.1 | 11.5 |
| 9 | 0.3.0 | | **Move `table.route`, `selector`, `step` and `drawStep` from `path.go` to `table.go`.** | 1 | 2 | 2 | 5 | 3.1 | 11.5 | | 9 | 0.3.0 | | **Move `table.route`, `selector`, `step` and `drawStep` from `path.go` to `table.go`.** | 1 | 2 | 2 | 5 | 3.1 | 11.5 |
@@ -106,6 +109,7 @@
| 40 | 0.3.0 | | **Parse `defaultTable`'s path with the library's grammar.** | 2 | 2 | 3 | 5 | 5.7 | 11.5 | | 40 | 0.3.0 | | **Parse `defaultTable`'s path with the library's grammar.** | 2 | 2 | 3 | 5 | 5.7 | 11.5 |
| 33 | 0.3.0 | | **Group `data/misc` into folders where a group name makes a path easier to guess.** | 4 | 5 | 4 | 5 | 5 | 10.8 | | 33 | 0.3.0 | | **Group `data/misc` into folders where a group name makes a path easier to guess.** | 4 | 5 | 4 | 5 | 5 | 10.8 |
| 47 | 0.3.0 | defect | **Refuse a category naming a hidden rows file, `"rows": ".x.tsv"`, as a rows file that is not there.** | 1 | 1 | 2 | 5 | 6 | 10.2 | | 47 | 0.3.0 | defect | **Refuse a category naming a hidden rows file, `"rows": ".x.tsv"`, as a rows file that is not there.** | 1 | 1 | 2 | 5 | 6 | 10.2 |
| 72 | 0.3.0 | principle | **Fail the merge gate when test coverage falls below the last recorded figure.** | 2 | 3 | 2 | 5 | 3 | 10.2 |
| 7 | 0.3.0 | | **Fill `arm` in one place.** | 4 | 4 | 2 | 6 | 3.2 | 9.8 | | 7 | 0.3.0 | | **Fill `arm` in one place.** | 4 | 4 | 2 | 6 | 3.2 | 9.8 |
| 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 13.1 | 9.8 | | 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 13.1 | 9.8 |
| 37 | 0.3.0 | | **Rename `newRand` to `newGeneratorState` and `Generator.rand` to `state`.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 | | 37 | 0.3.0 | | **Rename `newRand` to `newGeneratorState` and `Generator.rand` to `state`.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 |
@@ -189,16 +193,16 @@ Shape: T = table, t = template, c = choice.
### 3. Rewrite the shipped categories so facts that belong together come from one named pick. ### 3. Rewrite the shipped categories so facts that belong together come from one named pick.
- `sv_SE.person` and `en_US.person` bind the first name and read sex and title through it: `"first": "{.first-name as n}{n.name}"`, `"sex": "{n..sex.name}"`, a title from `{n..sex.title.name}`. - `sv_SE.person` and `en_US.person` bind the first name and read sex and title through it: `"first": "{.first-name as n}{n.name}"`, `"sex": "{n..sex.name}"`, a title from `{n..sex.title.name}`.
- `geo.SE.address` and `geo.US.address` bind the street and read locality, postal code and region through it; the locale `address` categories bind `/geo.XX.address` once. - `geo.SE.address` and `geo.US.address` bind the street and read locality, postal code and region through it; the locale `address` categories bind `/geo.XX.address` once and print it whole as `{a}`, so the country record's format appears once, and the decision "A locale's `address` restates its country record's format" is revised.
- Needs item 2. Every shipped value must still pass its consumer's check before and after item 4. - Needs item 2. Every shipped value must still pass its consumer's check before and after item 4.
- Re-pin seeded output in its own commit. - Re-pin seeded output in its own commit.
### 4. Draw every `{…}` afresh, keep a pick only by name, and delete `drawGroup` with the fences that held picks implicitly. ### 4. Draw every `{…}` afresh, keep a pick only by name, and delete `drawGroup` with the fences that held picks implicitly.
- Needs items 2, 3 and 71. After it, `{/person} ({/person.first})` renders two people, `{w} {uppercase(w)}` renders `b G` where `w` is a choice of letters, and `{/city.name}, {/country[SE].name}` and `{/country[SE].name} / {/country[NO].name}` both load. - Needs items 2, 3 and 71. After it, `{/person} ({/person.first})` renders two people, as does `{/sv_SE.person as a}{/sv_SE.person as b}{a} & {b}`, `{w} {uppercase(w)}` renders `b G` where `w` is a choice of letters, and `{/city.name}, {/country[SE].name}` and `{/country[SE].name} / {/country[NO].name}` both load.
- Deletes the token-order rules (`drawsApart`, `readFold`, `branches`, the pairwise replay), the expansion hold fence, `drawGroup`, the own-family fence and the cell-reference fences, and revises the decisions that point here. The remaining draw checks are names, paths through names, cycles and the repeat cap. - Deletes the token-order rules (`drawsApart`, `readFold`, `branches`, the pairwise replay), the expansion hold fence, `drawGroup`, the own-family fence and the cell-reference fences, and revises the decisions that point here. The remaining draw checks are names, paths through names, cycles and the repeat cap.
- Fixes on the way: `{/sel}|{/sel}` panics out of `Fake` today where two draws land on rows whose cells select different rows of another table; it must render. - Fixes on the way: `{/sel}|{/sel}` panics out of `Fake` today where two draws land on rows whose cells select different rows of another table; it must render.
- Rewrites the README's References, Draw group, Correlated fields, Linked tables and "One draw, one spelling" sections; the Linked tables section says which pick's weights govern a family, since the bound row decides. - Rewrites the README's References, Draw group, Correlated fields, Linked tables, Names and "One draw, one spelling" sections; the Linked tables section says which pick's weights govern a family, since the bound row decides.
- Re-pin seeded output and `testdata/shipped_shape.txt` in their own commits; the CHANGELOG names the grammar change. - Re-pin seeded output and `testdata/shipped_shape.txt` in their own commits; the CHANGELOG names the grammar change.
### 25. Draw `en_US.phone`'s `exch` as a NANP central office code, and assert the rule in `TestShippedUSPhone`. ### 25. Draw `en_US.phone`'s `exch` as a NANP central office code, and assert the rule in `TestShippedUSPhone`.
@@ -333,6 +337,14 @@ Require the path step to reach a sibling category.
`readTableOptions` and `loadDir` return on the first in Go's map order. `readTableOptions` and `loadDir` return on the first in Go's map order.
### 73. Decide whether the default embed's partial Swedish and US place tables stand against goal 13.1, or have the goals say which goal wins.
The decision "The default embed holds every Swedish postort the import can place and give a street-delivery code and a street, and the US places of 25,000 or more" ships less than its registers hold, and names no item that ends it. Item 60 ships the full registers as packs, beside the partial default.
### 74. Decide whether `misc.timezone` may drop the 17 zones of territories `misc.territory` leaves out, against goal 13.1, or have the goals say which goal wins.
The decision "`misc.territory` is the spine, and a `misc` table naming a territory links to it" serves goal 4.1 and drops those tzdb rows, and names no item that ends it.
### 14. Spell `misc.creditcard`'s digit runs `{digits(n)}`, and refuse a repeat over a lone `{digits(1)}` at `New`, naming that spelling. ### 14. Spell `misc.creditcard`'s digit runs `{digits(n)}`, and refuse a repeat over a lone `{digits(1)}` at `New`, naming that spelling.
Both render the same run. Both render the same run.
@@ -369,6 +381,10 @@ Needs item 4; recount the switches then. Today ten, from `render` to `columnItem
Nothing checks it, and a new country breaks it silently. Nothing checks it, and a new country breaks it silently.
### 75. Run every README `sh` example as a test, and compare each "Renders" line to a seeded render.
`readme_test.go` loads and renders each `json` block, but runs no `sh` example (CLI, Records, Table, Linked tables) and compares no "Renders e.g." output, so goal 11.2 is not met.
### 23. Give every other shipped category that could reach something real a path that never does. ### 23. Give every other shipped category that could reach something real a path that never does.
`phone` draws live PTS and NANP ranges — PTS's five fiction series are the Swedish inert set, sourced in [research-sources-se.md](docs/research/research-sources-se.md), and the NANP one still wants a source — and `bankgiro`, `plusgiro` and `routing` draw live prefixes. `phone` draws live PTS and NANP ranges — PTS's five fiction series are the Swedish inert set, sourced in [research-sources-se.md](docs/research/research-sources-se.md), and the NANP one still wants a source — and `bankgiro`, `plusgiro` and `routing` draw live prefixes.
@@ -389,6 +405,10 @@ The CLI counts brackets of its own, so a change to the selector grammar desyncs
`loadDir` indexes hidden TSVs and skips them in two places, so one loads where its comment says a hidden file is never data. `loadDir` indexes hidden TSVs and skips them in two places, so one loads where its comment says a hidden file is never data.
### 72. Fail the merge gate when test coverage falls below the last recorded figure.
`technical-principles.md` holds that test coverage should not decline. `docker compose run --rm cover` prints the figure, and neither the `Dockerfile` gate nor `.github/workflows/test.yml` checks it.
### 7. Fill `arm` in one place. ### 7. Fill `arm` in one place.
`splitArm` runs before the link and again after it, and `compileArm` finishes it from `reference.go`, so five seats traced three phases. Needs item 4. `splitArm` runs before the link and again after it, and `compileArm` finishes it from `reference.go`, so five seats traced three phases. Needs item 4.
+4 -9
View File
@@ -43,16 +43,11 @@ func unwrapTransform(arg string) (leaf string, chain []func(string) string, err
arg = args[0] arg = args[0]
} }
} }
func transformArg(fields map[string]node, a []string) error {
leaf, _, err := unwrapTransform(a[0]) // transformArg proves a transform's chain; checkReads proves the operand it reads.
if err != nil { func transformArg(_ map[string]node, a []string) error {
_, _, err := unwrapTransform(a[0])
return err return err
}
if isRef(leaf) {
_, _, err := refShape(leaf)
return err
}
return checkArm(leaf, fields, false)
} }
func transformOperand(a []string) []string { func transformOperand(a []string) []string {
if len(a) != 1 { if len(a) != 1 {