Keep in a pick only what reads of its name address, look frames up on the stack alone, and refuse a name read once whole

This commit is contained in:
2026-10-03 02:36:12 +02:00
committed by Lillem4n
parent 685e7bb27c
commit b1aed332e7
15 changed files with 455 additions and 231 deletions
+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
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
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 a choice's item; a binding nothing
reads; a path through a name that selects a row; and a binding in a table. A field,
column, category or folder name may not contain ` as `.
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 a
choice's item; a binding nothing reads, or one read once whole, which the reference
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.
- 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,
+17 -11
View File
@@ -708,12 +708,16 @@ row:
"{/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
once, so `{n}` and `{n.path}` agree, a [transform](#transforms) of `n.path` reads the same
value, and a field `n` renders twice prints the same text twice. A name is a pick of its own:
`{t.name}` and `{/misc.territory.name}` beside it are two draws. A reference path read under a
name draws in the draw group of the read of the name, so give the reads of two names bound to
one category a [draw group](#draw-group) each to draw what it references apart.
Renders e.g. `🇳🇴 Norway: capital Oslo, calling code +47`. A name keeps each level a
`{n.path}` reads, and what it lands on, so `{n}` and `{n.path}` agree, and a
[transform](#transforms) of `n.path` reads the same value. Everything else under the name draws
as it would anywhere: `{w}-{w}` in the category `n` names draws `w` twice, so `{n.w}` beside
`{n}` is refused there, since it could read either draw.
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
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.
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
a choice's item, which every other item would leave unbound; a binding nothing reads; a path
through a name that selects a row, which the reference selects instead; and a binding in a
table's format or cell. A field, column, category or folder name may not contain ` as `.
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 a choice's item, which every other item would leave unbound; a
binding nothing reads, or one read once whole outside a `repeat`, which the reference spells;
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
@@ -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
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
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
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
+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
// group a reference path's: `formatOps.held` and `heldCheck` an expansion'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
// render of the scope it is bound in, a category or a repeat iteration, and drawn on its
// first read: `nameBinding`, `nameScope`, `namedPick`, `pickFrame`.
// - named pick — the draw a token such as {/person as p} binds to a name. It is drawn on its
// first read and kept while its scope renders: a category, or one repeat iteration.
// `nameBinding`, `nameScope`, `namedPick`, `pickFrame`.
// - 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`.
// - 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
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
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
`"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,
since a read outside the item would see a binding another item leaves out. The scope is
lexical: a read entering a category, by a reference or through a name, sees none of the
frames rendering above it, so what a category renders never depends on who references it. A
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
own memo, and the category's root renders that frame when the read lands on the root, so
`{n}` and `{n.path}` read one pick: per render and group for a reference path, per pick for a
path through a name, and per render for `Fake("cat.field")`. 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; a pick
with draw groups of its own would draw where no fence looks.
since a read outside the item would see a binding another item leaves out. A pick is drawn on
its first read, so a record's columns, rendered in name order, read one pick whichever binds
it.
The scope is lexical: a read entering a category, by a reference, through a name or from
`Fake`, sees none of the frames rendering above it, so what a category renders never depends
on who references it. That read opens the frame of each scope around what it lands on, taken
from its own memo, so every read entering by one memo, `{n}` and `{n.path}` alike, reads one
pick: per render and group for a reference path, per pick for a path through a name, per
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 {
variant map[string]node
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
@@ -34,7 +34,7 @@ type renderDraws struct {
unnamed groupDraws
named map[string]*groupDraws
trace renderTrace
frames *frameStack
frameStack *frameStack
}
// 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
group string
row renderedRow
// entry is the memo of the read that entered the category rendering, nil where none did;
// base, the depth of the frame stack when it entered, below which no frame is the
// category's.
entry *drawMemo
// base is the depth of the frame stack when the read rendering the category entered it,
// below which no frame is the category's.
base int
// pick is the named pick what renders is part of, at pickKey, its path from the name; nil
// outside one.
+50 -46
View File
@@ -1,7 +1,6 @@
package fejkdata
import (
"errors"
"fmt"
"strings"
)
@@ -64,8 +63,9 @@ type formatToken struct {
body string // the braces' content, as written
fn string // builtinCall
args []string // builtinCall
names []string // nameRead: the '|' arms; builtinCall: the operands its builtin reads; nameBind: the reference it binds
bound string // nameBind: the name
names []string // nameRead: the '|' arms; builtinCall: the operands its builtin reads
// nameBind: the reference it binds, and the name.
boundRef, bound string
}
type tokenKind uint8
@@ -98,7 +98,7 @@ func parseFormat(format string) ([]formatToken, error) {
}
flush()
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
}
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
// 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 {
head string
whole bool
operand bool
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,
// 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.
@@ -247,32 +242,16 @@ func checkTokens(toks []formatToken, fields map[string]node) ([]unboundRead, err
func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
switch t.kind {
case builtinCall:
err := checkFunc(t, fields)
var miss *fieldMiss
if errors.As(err, &miss) {
return []unboundRead{{miss.head, err}}, nil
}
if err := checkFunc(t, fields); err != nil {
return nil, err
}
return checkReads(t, fields, true)
case nameBind:
return nil, checkBind(t)
case nameRead:
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)
}
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
}
unbound, err := checkReads(t, fields, false)
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
// 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
}
// 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
// could read as something else.
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 {
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.
// 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
// holds is a *fieldMiss.
func checkArm(name string, fields map[string]node, wholeToken bool) error {
// the same value and can be offered as the reference spelling. missing says no
// field holds the head, which a name may yet answer.
func checkArm(name string, fields map[string]node, wholeToken bool) (missing bool, err error) {
a := splitArm(name, nil)
if err := checkSegments(a); err != nil {
return err
return false, err
}
field, ok := fields[a.head]
if !ok {
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) {
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 {
hint := ""
if wholeToken && hintableRef(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 {
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
@@ -383,7 +387,7 @@ type arm struct {
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
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.
+8 -3
View File
@@ -181,10 +181,15 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
}); err != nil {
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 {
return linkNames(path, t)
return pass(path, t)
}
return nil
}); err != nil {
return err
}
}
return nil
})
}
+255 -88
View File
@@ -2,6 +2,7 @@ package fejkdata
import (
"fmt"
"sort"
"strings"
)
@@ -16,7 +17,8 @@ type nameScope struct {
}
// 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 {
name string
ref string
@@ -25,9 +27,18 @@ type nameBinding struct {
scope *nameScope
index int
binder *template
read bool
uses []nameUse
head node
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 {
@@ -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
// 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
// nothing reads.
// read by nothing, or once whole, which its reference spells.
func bindNames(root node) error {
top := &nameScope{owner: root}
scopes := []*nameScope{top}
var 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 {
scope = &nameScope{up: scope, owner: t}
scopes = append(scopes, scope)
inChoice = false
}
t.scope = scope
t.nameScope = scope
if err := scope.bindAll(t, inChoice, where); err != nil {
return err
}
case *choice:
t.nameScope = scope
}
_, isChoice := n.(*choice)
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
}
for _, sc := range scopes {
if err := sc.close(); err != nil {
if err := sc.settle(); err != nil {
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 {
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 {
sc.bindings = map[string]*nameBinding{}
}
@@ -114,15 +128,24 @@ func (sc *nameScope) spelled() string {
return "in one repeat"
}
// close refuses a binding nothing reads, and gives the owner of a scope binding names the scope.
func (sc *nameScope) close() error {
// settle refuses a binding read by nothing, or read once whole, outside a repeat, where its
// 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 {
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)
}
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 {
t.ownScope = sc
t.ownNameScope = sc
}
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
// format as compile reports them, and refuses a field spelling a name.
func resolveReads(n node, where string) error {
if err := eachContained(n, where, resolveReads); err != nil {
// format as compile reports them, and refuses a field spelling a name. scopes are every scope of
// the category, which a refusal searches for a name bound where the read cannot see it.
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
}
t, isTemplate := n.(*template)
@@ -153,7 +177,7 @@ func resolveReads(n node, where string) error {
return nil
}
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"
if where != "" {
holder = strings.TrimSuffix(where, ": ")
@@ -162,32 +186,52 @@ func resolveReads(n node, where string) error {
}
}
for _, u := range t.unbound {
b := t.scope.lookup(u.head)
b := t.nameScope.lookup(u.head)
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
}
// 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.
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
// every template is linked, so the binder has resolved that reference.
func linkNames(path string, t *template) error {
// linkBindings gives each binding t's tokens make the head and tail its reference resolved to.
func linkBindings(t *template) {
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 {
o := &t.compiled.ops[i]
for _, reads := range [][]arm{o.arms, o.operands} {
for j := range reads {
if reads[j].kind != namedRead {
continue
if reads[j].kind == namedRead {
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
}
func linkName(t *template, a *arm) error {
b := t.scope.lookup(a.head)
if b.head == nil {
ref := b.binder.link.refs[b.ref]
b.head, b.tail = b.binder.link.refHeads[ref.head], ref.tail
// linkNames compiles t's reads of a name as paths from the head its binding's reference names, once
// every template is linked, so each binder has resolved that reference.
func linkNames(path string, t *template) error {
return namedReads(t, func(o *op, a *arm) error {
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) {
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
}
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)
for i := range a.levels {
a.levels[i] = joinSegments(full[:i])
}
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
}
// namedPick is one draw of a name: the variant drawn at each level under it, the value each
// path through it read, 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.
// checkNameReads refuses a read of a name rendering, under the pick, a field it renders twice where
// a path through the name reads one draw of it: the pick keeps one draw there, and the two
// renders would disagree with it.
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 {
named *nameBinding
memo drawMemo
pins pinSet
}
@@ -234,7 +359,11 @@ type pickFrame struct {
}
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
@@ -244,79 +373,104 @@ type frameStack struct {
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 {
mark := d.depth()
if d.frames == nil {
d.frames = &frameStack{}
if d.frameStack == nil {
d.frameStack = &frameStack{}
}
d.frames.frames = append(d.frames.frames, f)
d.frameStack.frames = append(d.frameStack.frames, f)
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 {
if d.frames == nil {
if d.frameStack == nil {
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
// memo, which keeps the frame of each scope the read enters past its owner.
func (sc renderScope) entering(memo *drawMemo) renderScope {
sc.base, sc.entry, sc.pick = sc.draws.depth(), memo, nil
return sc
// enter is sc as a read entering a category sees it, landing on n: none of the frames rendering,
// and, from memo, the frame of each scope binding names around n, so every read entering with
// memo reads one pick of each name. It returns the mark that closes those frames.
func (sc renderScope) enter(n node, memo *drawMemo) (renderScope, int) {
sc.base, sc.pick = sc.draws.depth(), nil
for scope := scopeAround(n); scope != nil; scope = scope.up {
if len(scope.order) > 0 {
sc.draws.pushFrame(memo.enteredFrame(scope))
}
}
return sc, sc.base
}
// frameOf is the frame a render of scope's owner opens: the one the read entering the category
// 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)
// scopeAround is the innermost name scope a render of n reads names in, short of the frames n
// 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 sc.entry.frameOf(scope)
return n.nameScope
case *choice:
return n.nameScope
}
return nil
}
func (m *drawMemo) frameOf(scope *nameScope) *pickFrame {
f, ok := m.frames[scope]
func (m *drawMemo) enteredFrame(scope *nameScope) *pickFrame {
f, ok := m.enteredFrames[scope]
if !ok {
f = newPickFrame(scope)
if m.frames == nil {
m.frames = map[*nameScope]*pickFrame{}
if m.enteredFrames == nil {
m.enteredFrames = map[*nameScope]*pickFrame{}
}
m.frames[scope] = f
m.enteredFrames[scope] = f
}
return f
}
// pickOf is b's pick: in the frame rendering its scope since the read entering the category, else
// in the frame that read keeps, as for Fake("cat.field") or {/cat.field}.
func (sc renderScope) pickOf(b *nameBinding) *namedPick {
if stack := sc.draws.frames; stack != nil {
for i := len(stack.frames) - 1; i >= sc.base; i-- {
if f := stack.frames[i]; f.scope == b.scope {
return &f.picks[b.index]
// renderFrame opens a fresh frame of t's scope, where t binds names and no read entering the
// category opened one, returning the mark that closes it, or -1.
func (sc renderScope) renderFrame(t *template) int {
if t.ownNameScope == nil || sc.frameOf(t.ownNameScope) != nil {
return -1
}
}
}
return &sc.entryMemo().frameOf(b.scope).picks[b.index]
return sc.draws.pushFrame(newPickFrame(t.ownNameScope))
}
// entryMemo is the memo of the read that entered the category rendering, else the unnamed draw
// group's of the render: one memo for a render begun below a category's root, whatever draw group
// its templates name.
func (sc renderScope) entryMemo() *drawMemo {
if sc.entry != nil {
return sc.entry
// frameOf is the frame of scope rendering since the read entering the category, nil where none is.
func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
if stack := sc.draws.frameStack; stack != nil {
for i := len(stack.frames) - 1; i >= sc.base; i-- {
if f := stack.frames[i]; f.scope == scope {
return f
}
return &sc.draws.unnamed.memo
}
}
return nil
}
func readName(s *generatorState, sc renderScope, a arm) readValue {
p := sc.pickOf(a.bind)
return readPicked(s, p, a.bind.head, a.steps, a.levels, a.path, sc.entering(&p.memo))
f := sc.frameOf(a.named.scope)
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.
@@ -327,14 +481,25 @@ func (sc renderScope) drawRowOf(s *generatorState, t *table) int {
return t.drawRow(s)
}
// readUnder reads a of t, which renders as part of the pick sc.pick at sc.pickKey: once per pick,
// by its path from the name.
// keeps reports whether the pick sc renders part of keeps a, a read of t: whether a read of the
// 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 {
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))
for i, l := range a.levels {
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
@@ -346,15 +511,17 @@ func underKey(prefix, path string) string {
return prefix + "." + path
}
// readPicked draws the path key names under pick p, once, and renders what it lands on as part of p.
func readPicked(s *generatorState, p *namedPick, head node, steps []pathStep, levels []string, key string, sc renderScope) readValue {
if r, done := p.memo.value[key]; done {
return r
}
// draw draws the path key names under p, its variant at key kept too.
func (p *namedPick) draw(s *generatorState, head node, steps []pathStep, levels []string, key string) node {
leaf := drawSteps(s, head, steps, &p.pins, &p.memo, levels)
if c, isChoice := leaf.(*choice); isChoice {
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.pick, sc.pickKey = p, key
r := renderLeaf(s, leaf, sc)
+3 -2
View File
@@ -28,6 +28,7 @@ type choice struct {
items []node
cum []float64
shared map[string]bool
nameScope *nameScope // where its items look a name up, set by bindNames
}
func (*choice) isNode() {}
@@ -56,8 +57,8 @@ type template struct {
unbound []unboundRead // the heads its tokens read that no field holds
// Filled by `bindNames`, from the compiled category:
scope *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
nameScope *nameScope // where its tokens look a name up
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:
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.
func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record {
sc = sc.in(t)
if t.ownScope != nil {
defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(t.ownScope)))
if mark := sc.renderFrame(t); mark >= 0 {
defer sc.draws.popFrames(mark)
}
if t.site.isFormat() {
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 {
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
@@ -89,6 +92,7 @@ func linkTemplate(folder []string, path, category string, t *template, root map[
return err
}
t.link, t.compiled = link, compileFormat(t.tokens, link.refs, t.isName)
linkBindings(t)
compileArms(t)
return nil
}
@@ -253,12 +257,16 @@ func resolveCategory(root map[string]node, segments []string) (categorySegs []st
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 {
var refs []string
seen := map[string]bool{}
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] {
seen[name] = true
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 {
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
@@ -78,8 +80,8 @@ func render(s *generatorState, n node, sc renderScope) string {
case *template:
sc = sc.in(n)
if n.repeat == 1 {
if n.ownScope != nil {
defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(n.ownScope)))
if mark := sc.renderFrame(n); mark >= 0 {
defer sc.draws.popFrames(mark)
}
if lit, fixed := n.fixedText(); fixed {
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
// 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 {
entry := sc.entryMemo()
var b strings.Builder
b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
for i := 0; i < t.repeat; i++ {
if i > 0 {
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()
}
// 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
func expandAnew(s *generatorState, t *template, frames *frameStack, base int, entry *drawMemo) string {
draws := renderDraws{frames: frames}
if t.ownScope != nil {
defer draws.popFrame(draws.pushFrame(newPickFrame(t.ownScope)))
func expandAnew(s *generatorState, t *template, stack *frameStack, base int) string {
draws := renderDraws{frameStack: stack}
if t.ownNameScope != nil {
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
@@ -180,10 +181,14 @@ func readField(s *generatorState, t *template, hold *drawMemo, sc renderScope, a
switch {
case a.kind == namedRead:
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)
}
sc.pick = nil
switch {
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:
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
}
leaf := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels)
mark := -1
if pins != nil {
sc = sc.at(leaf, pins).entering(memo)
sc, mark = sc.at(leaf, pins).enter(leaf, memo)
}
r := renderLeaf(s, leaf, sc)
if mark >= 0 {
sc.draws.popFrames(mark)
}
if memo.value == nil {
memo.value = map[string]readValue{}
}
+2 -2
View File
@@ -345,8 +345,8 @@ func (t *table) compileRowFormat(format string) error {
if err != nil {
return err
}
if len(unbound) > 0 {
return unbound[0].err
if err := checkTableToks(nil, unbound); err != nil {
return err
}
t.formatTemplate = &template{format: format, tokens: toks, fields: fields, repeat: 1, isRecord: true, site: tableSite{t, formatRow}}
t.rowNode = &tableRow{t}
+25 -5
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 72`
`Next ID = 76`
| 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 |
| 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 |
| 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 |
| 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 |
@@ -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 |
| 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 |
| 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 |
| 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 |
@@ -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 |
| 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 |
| 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 |
| 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 |
@@ -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 |
| 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 |
| 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 |
| 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 |
@@ -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.
- `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.
- 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.
- 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.
- 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.
### 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.
### 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.
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.
### 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.
`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.
### 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.
`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,17 +43,12 @@ func unwrapTransform(arg string) (leaf string, chain []func(string) string, err
arg = args[0]
}
}
func transformArg(fields map[string]node, a []string) error {
leaf, _, err := unwrapTransform(a[0])
if err != nil {
// transformArg proves a transform's chain; checkReads proves the operand it reads.
func transformArg(_ map[string]node, a []string) error {
_, _, err := unwrapTransform(a[0])
return err
}
if isRef(leaf) {
_, _, err := refShape(leaf)
return err
}
return checkArm(leaf, fields, false)
}
func transformOperand(a []string) []string {
if len(a) != 1 {
return nil