Answer architecture round 1: refuse a name read inside the field it binds, check every scope's cycles, build a column read in one pass, drop the implicit struct pick for names across tags, and state the fresh-draw rule once

This commit is contained in:
2026-10-05 09:39:18 +02:00
committed by Lillem4n
parent a45edf8fb5
commit 1a15a139c8
23 changed files with 257 additions and 230 deletions
-1
View File
@@ -37,7 +37,6 @@ In [docs/decisions.md](docs/decisions.md):
- `{a|b}` stays beside nested choices
- Flags follow getopt_long
- An argument is a template by its shape, not by a flag
- An inline template skips the cycle fence
- An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
- A padded JSON argument is rejected, not trimmed
- `FakeTemplate` and `NewTemplate` both stay
+2 -2
View File
@@ -20,8 +20,8 @@ replacement, and each removed path, column or flag.
- Every `{…}` draws afresh, and a name is the one way to keep a pick: `{p.a} {p.b}`,
`{w} {uppercase(w)}` and `{/person.first} {/person.last}` are two draws each, which
`{p as q}{q.a} {q.b}` and `{/person as n}{n.first} {n.last}` make one. `{x as n}`
binds a sibling field as well as a reference, and a struct's path tags into one
category read one pick of it. `drawGroup` is no longer an option, and `New` no longer
binds a sibling field as well as a reference, and a struct's tags share its names,
`{/sv_SE.person as p}{p.first}` beside `{p.sex}`. `drawGroup` is no longer an option, and `New` no longer
refuses a read beside a path into what it reads, or reads of one table family that
could disagree.
- First release: the CLI, the library and the shipped data set.
+28 -26
View File
@@ -136,8 +136,7 @@ it as nothing.
A record is one render, so its columns read one pick of each [name](#names) its category
binds: `"code": "{/currency as c}{c.code}"` beside `"symbol": "{c.symbol}"` is one
currency. Every other `{…}` draws afresh, so `{/currency.code}` and `{/currency.symbol}`
in two columns may name two currencies.
currency, while `{/currency.code}` and `{/currency.symbol}` in two columns may name two.
A category never references itself: `{/users.first}` or a bare `{/users}` inside `users`
describes a draw other than the fields beside it. Read a sibling as a field, and put a
@@ -337,12 +336,12 @@ a column of one record, its tag a path or an inline template — told apart by
[datatype](#datatype): a string, bool, integer or float kind, or a pointer to one,
which a [`null`](#null) item leaves nil. An integer stays within int64 whatever its
kind, and a value the kind cannot hold, such as `{int(0,300)}` in a `uint8`, is refused
naming a kind that holds it. Path tags reading into one category, two or more of them
and none selecting a row, read one pick of it, as a record's columns read one name:
`sv_SE.person.first` and `sv_SE.person.sex` describe one person, while an inline template
in a tag draws afresh. The fields an embedded struct promotes are columns of the
same record; a named struct field, or a pointer to one, fills from its own tags as a
record of its own, so it picks apart from its parent. `fake:"-"` leaves a
naming a kind that holds it. The tags are a record's columns, so they share its
[names](#names): `First` tagged `{/sv_SE.person as p}{p.first}` and `Sex` tagged `{p.sex}`
describe one person, while two path tags into `sv_SE.person` are two draws. The fields an
embedded struct promotes are columns of the same record; a named struct field, or a
pointer to one, fills from its own tags as a record of its own, so it picks apart from its
parent. `fake:"-"` leaves a
struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep
their values, and so does a pointer back to a struct already being filled; a type
whose fields reach more than 1024 structs is refused, naming `fake:"-"` to cut it. The
@@ -371,13 +370,16 @@ Every character is literal except a `{…}` token:
| Token | Renders |
|-------|---------|
| `{name}` | the sibling field `name`, or the pick bound to the name ([Names](#names)) |
| `{x as name}` | nothing: binds one pick of `x`, a reference or a sibling field, to `name` ([Names](#names)) |
| `{x as name}` | nothing: binds one pick of `x`, a reference or a path into a sibling field, to `name` ([Names](#names)) |
| `{name.field}` | `field` of a fresh draw of the sibling `name`, or of the pick bound to it ([Correlated fields](#correlated-fields)) |
| `{a\|b}` | one of the named fields, even odds |
| `{fn(args)}` | a builtin ([Functions](#functions)) |
| `{/path}`, `{.path}`, `{..path}` | a node reached from the data root, this file's folder, or the folder above ([References](#references)) |
| `{{`, `}}` | a literal `{` or `}` |
Each `{…}` makes a new pick, so `{w} {w}`, `{p.a} {p.b}` and `{/person.first}
{/person.last}` are two draws each. A [name](#names) keeps one pick for every read of it.
```json
"100 Main St, Apt {int(1,9)}{upper(1)} — tel {digits(3)}-{digits(4)}"
```
@@ -444,8 +446,8 @@ order.id: datatype integer: {digits(3)} prints text, not an integer
order.id: datatype integer: "1{digits(2)}" is not one value; write one literal or one {float()}, {int()}, {seq()} or {calc()}, or read one
```
A column of one reference alone to another record's column — `"score": "{/src.score}"`
— is that column: it takes the column's datatype and is null where the column is, and
A column of one reference or name read alone to another record's column —
`"score": "{/src.score}"`, or `"score": "{s.score}"` with `s` bound to `/src` — is that column: it takes the column's datatype and is null where the column is, and
a struct field tagged `src.score` is nil there. A `datatype` of its own types the
column's values where they prove it, and one restating the datatype it takes is refused.
Any other read renders the column's text, a null as `""`.
@@ -668,8 +670,7 @@ literals, sibling fields and [names](#names), each rendered then read as a numbe
a second argument rounds to that many decimals. A hyphen is always subtraction, so a
hyphenated field or name can't be an operand.
Each `{…}` draws afresh, so a calc computes from the values its format shows only where
both read one [name](#names):
A calc computes from the values its format shows where both read one [name](#names):
```json
{ "format": "{net as n}{qty as q}{n} x {q} = {calc(n * q, 2)}", "net": ["19.99", "5.00"], "qty": ["3", "7"] }
@@ -687,8 +688,8 @@ fail, except in a [typed column](#datatype), which must prove neither happens.
`{lowercase(x)}`, `{uppercase(x)}` and `{ascii(x)}` rewrite the value of `x` — a
field, a path, a `..path` or a [name](#names) — and nest. `ascii` folds Latin letters (`Åsa Öberg`
→ `Asa Oberg`) and drops any other non-ASCII rune. `x` draws afresh like any `{…}`, so an
email matches the name beside it where both read one name:
→ `Asa Oberg`) and drops any other non-ASCII rune. An email
matches the name beside it where both read one name:
```json
{ "format": "{person as p}{p.first} {p.last} <{lowercase(ascii(p.first))}.{lowercase(ascii(p.last))}@example.com>",
@@ -714,9 +715,8 @@ without naming `sv_SE`:
"Hej, {/en_US.person}!"
```
Renders e.g. `Hej, Pat Smith!`. Each reference draws afresh, a path into a category
too: `{.person.first} {.person.last}` may name two people, and `{/misc.uuid} {/misc.uuid}`
is two draws. Bind the person to a [name](#names) to read one: `{.person as p}{p.first}
Renders e.g. `Hej, Pat Smith!`. `{.person.first} {.person.last}` may name two people, as
`{/misc.uuid} {/misc.uuid}` is two draws ([Format string](#format-string)). Bind the person to a [name](#names) to read one: `{.person as p}{p.first}
{p.last}`. Rejected at `New`: a path that is
unknown, names a folder, has no folder above, reads a field not every variant
of a choice carries, or names the category the reference sits in, and a reference
@@ -724,8 +724,8 @@ that leads back to its own value, directly, mutually or through a chain.
### Names
`{x as n}` binds one pick of `x`, a reference or a sibling field, to the name `n` and prints
nothing. `{n}` then
`{x as n}` binds one pick of `x`, a reference or a path into a sibling field, to the name `n`
and prints nothing. `{n}` then
prints that pick, and `{n.path}` reads a path through it, so every read of `n` describes one
row:
@@ -754,6 +754,8 @@ Refused at `New`, each error naming what to write instead:
and outside it;
- a name that is a field or an option;
- a binding of anything but a reference or a path into a field of the template binding it;
- a read of a name inside the field it binds, which would render itself: read it outside the
field;
- a binding in a choice's item, which every other item would leave unbound;
- a binding nothing reads, or one read once outside a `repeat` nested where it is bound,
which the spelling it binds draws the same way: `{/word as w}{w}` is `{/word}`, and
@@ -766,8 +768,8 @@ Refused at `New`, each error naming what to write instead:
### Correlated fields
`{name.field}` reads a path into a sibling, drawing it afresh as any `{…}` does. Several
tokens read one draw of it through a [name](#names): bind the sibling, and every read of
`{name.field}` reads a path into a sibling. Several tokens read one draw of it through a
[name](#names): bind the sibling, and every read of
the name reads that one row — a locality and the postal code that really covers it:
```json
@@ -780,8 +782,8 @@ the name reads that one row — a locality and the postal code that really cover
```
Renders e.g. `Kungsgatan 35` / `176 99 Stockholm`, never a Stockholm code beside
Tranås; each row's `weight` says how often it appears. A name keeps every level its
reads pass: `{p.geo.town.name} {p.geo.town.zip}` share the town. Every variant of a choice
Tranås; each row's `weight` says how often it appears. `{p.geo.town.name}
{p.geo.town.zip}` share the town too, as a name keeps every level its reads pass. Every variant of a choice
on the path must carry the rest of it, so a row missing a field is named at load:
```text
@@ -952,14 +954,14 @@ node.go the node model and JSON -> node compilation
table.go tables: the rows TSV, its options and links, row selection and draws
name.go names: {x as n} bindings, the scopes they live in, and the reads they answer
namefence.go the load fences over names
pick.go a render's named picks: the frames of the name scopes rendering, and the reads kept under a name
pick.go a render's named picks: the frames of the name scopes rendering, the memo a pick keeps, and the reads kept under it
scope.go the scope a render reads names and rows through
path.go the dotted-path walks with their selectors, and proving a path resolves
render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
struct.go structs: FakeStruct, fake tags, the one pick a struct's path tags into one category read, 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 named pick keeps its draws in, and the scope a render reads names and rows through
pins.go the pin set: the table rows one path or one named pick fixes
reference.go reference sigils, and resolving references across the tree
graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences
+13 -14
View File
@@ -57,30 +57,29 @@ func BenchmarkRepeat(b *testing.B) {
benchPath(b, dir, "many")
}
// BenchmarkHeld measures a format that holds a head and reads two paths from it —
// what a correlated pair costs against BenchmarkUnheld, the same output drawn from
// two independent fields.
func BenchmarkHeld(b *testing.B) {
dir := tmpData(b, "addr", `{"format":"{place.postal-code} {place.locality}","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`)
// BenchmarkNamed measures a format that binds a field to a name and reads two paths through
// it — what a correlated pair costs against BenchmarkUnnamed, the same output drawn from two
// independent fields.
func BenchmarkNamed(b *testing.B) {
dir := tmpData(b, "addr", `{"format":"{place as p}{p.postal-code} {p.locality}","place":[{"format":"{locality}","locality":"Stockholm","postal-code":"1{digits(2)} {digits(2)}"},{"format":"{locality}","locality":"Tranås","postal-code":"573 {digits(2)}"}]}`)
benchPath(b, dir, "addr")
}
func BenchmarkUnheld(b *testing.B) {
func BenchmarkUnnamed(b *testing.B) {
dir := tmpData(b, "addr", `{"format":"{postal-code} {locality}","postal-code":["1{digits(2)} {digits(2)}","573 {digits(2)}"],"locality":["Stockholm","Tranås"]}`)
benchPath(b, dir, "addr")
}
// BenchmarkHeldDeep reads two paths through an intermediate level, the shape the
// depth question is about.
func BenchmarkHeldDeep(b *testing.B) {
dir := tmpData(b, "addr", `{"format":"{p.addr.city} {p.addr.zip}","p":{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":"1{digits(2)} {digits(2)}"}}}`)
// BenchmarkNamedDeep reads two paths through a name and an intermediate level.
func BenchmarkNamedDeep(b *testing.B) {
dir := tmpData(b, "addr", `{"format":"{p as q}{q.addr.city} {q.addr.zip}","p":{"format":"{addr}","addr":{"format":"{city}","city":["Stockholm","Tranås"],"zip":"1{digits(2)} {digits(2)}"}}}`)
benchPath(b, dir, "addr")
}
// BenchmarkHeldWide reads ten paths from one row: the point where the maps an
// expansion holds outgrow a single bucket.
func BenchmarkHeldWide(b *testing.B) {
dir := tmpData(b, "row", `{"format":"{r.a}{r.b}{r.c}{r.d}{r.e}{r.f}{r.g}{r.h}{r.i}{r.j}","r":[
// BenchmarkNamedWide reads ten paths through one name: the point where the maps a pick keeps
// outgrow a single bucket.
func BenchmarkNamedWide(b *testing.B) {
dir := tmpData(b, "row", `{"format":"{r as s}{s.a}{s.b}{s.c}{s.d}{s.e}{s.f}{s.g}{s.h}{s.i}{s.j}","r":[
{"format":"x","a":"1","b":"2","c":"3","d":"4","e":"5","f":"6","g":"7","h":"8","i":"9","j":"0"},
{"format":"y","a":"A","b":"B","c":"C","d":"D","e":"E","f":"F","g":"G","h":"H","i":"I","j":"J"}]}`)
benchPath(b, dir, "row")
+6 -11
View File
@@ -232,9 +232,8 @@ func inlineScope(n node, label string) nodeScope {
// binding is one scope's way through bind, the one load pipeline; its fields are where
// scopes differ.
type binding struct {
scope nodeScope
link func() error
scopeFence func() error
scope nodeScope
link func() error
// Set by a caller that proves the columns against their Go types itself.
typedByGo bool
}
@@ -251,7 +250,6 @@ func categoryBinding(sites []categorySite, root map[string]node) binding {
}
return linkRefs(sites, root)
},
scopeFence: func() error { return checkNoCycles(sites) },
}
}
@@ -259,10 +257,8 @@ func (b binding) bind() error {
if err := b.link(); err != nil {
return err
}
if b.scopeFence != nil {
if err := b.scopeFence(); err != nil {
return err
}
if err := checkNoCycles(b.scope); err != nil {
return err
}
if !b.typedByGo {
if err := checkColumns(b.scope); err != nil {
@@ -274,9 +270,8 @@ func (b binding) bind() error {
// checkNodeFences runs the per-node fences every binding needs over a scope, each
// over the whole scope before the next, so which of several broken nodes is reported
// does not depend on the walk. Its walks recurse unguarded, so the scope's cycles are
// refused first: categories by checkNoCycles, and an inline node closes none, since nothing
// references it.
// does not depend on the walk. Its walks recurse unguarded, so bind refuses the scope's cycles
// first.
func checkNodeFences(s nodeScope) error {
mem := renderCounts{}
if err := s(func(path string, n node) error { return repeatCheck(path, n, mem) }); err != nil {
+6 -6
View File
@@ -330,8 +330,8 @@ func TestShippedSwedishAddress(t *testing.T) {
}
// TestShippedAddressIsOneNamedPick pins that an address's street, postal code,
// locality and region are one place in the geo tables, read through a name or a struct's
// tags, and that a reference beside the name is another address.
// locality and region are one place in the geo tables, read through a name in a template or
// across a struct's tags, and that a reference beside the name is another address.
func TestShippedAddressIsOneNamedPick(t *testing.T) {
f := newGenerator(t, "data", WithSeed(3))
for _, c := range []struct{ locale, country, template, largest string }{
@@ -354,16 +354,16 @@ func TestShippedAddressIsOneNamedPick(t *testing.T) {
}
if c.country == "SE" {
var row struct {
Street string `fake:"sv_SE.address.street"`
PostalCode string `fake:"sv_SE.address.postal-code"`
Locality string `fake:"sv_SE.address.locality"`
Street string `fake:"{/sv_SE.address as a}{a.street}"`
PostalCode string `fake:"{a.postal-code}"`
Locality string `fake:"{a.locality}"`
}
for i := 0; i < 200; i++ {
if err := f.FakeStruct(&row); err != nil {
t.Fatal(err)
}
if got := row.Street + "|" + row.PostalCode + "|" + row.Locality + "|"; !places[got] {
t.Fatalf("sv_SE.address: struct tags %q are no street, postal code and locality of one place", got)
t.Fatalf("sv_SE.address: struct tags reading one name, %q, are no street, postal code and locality of one place", got)
}
}
}
+1 -1
View File
@@ -77,7 +77,7 @@ func checkColumns(s nodeScope) error {
items, _ := columnItems(column)
for _, it := range items {
if r := it.link.readsColumn; r != nil {
if err := check(r.a.head[1:], r.a.tail[0], r.column); err != nil {
if err := check(r.category, r.field, r.column); err != nil {
return err
}
}
+4 -10
View File
@@ -73,13 +73,6 @@ record. A folder-relative `{.name}` or `{..name}` is refused naming `{/name}`, s
inline template sits in no folder. `IsTemplate` exports the rule, so the CLI, struct
tags and any other caller read one.
## An inline template skips the cycle fence
2026-09-03, Lilleman auf Larv.
Each load proves what it loads acyclic, an inline node is a finite tree of its own, and
nothing in the tree can reference it, so no render of it reaches itself.
## An inline template that does not compile is misuse (exit 2), including a reference that resolves to nothing
2026-09-24, Lilleman auf Larv.
@@ -305,9 +298,8 @@ not agree on one among themselves; each must only hold that type.
The fields an embedded struct promotes are the struct's own — `e.First`, as
`encoding/json` and SQL mappers read them — so they are columns of its record and share
its picks: path tags into one category, two or more of them and none selecting a row, read
one pick of it, as a category's record columns read one pick of each name (goal 4.1), while
an inline template in a tag draws afresh as any `{…}` does; a tagged field that another field hides is refused, not dropped. A named
its names: one tag may bind `{/person as p}{p.first}` and another read `{p.sex}`, while a
path tag draws afresh as `{/path}` does (goal 4.1); a tagged field that another field hides is refused, not dropped. A named
struct field is another entity and a record of its own. `fake:"-"` leaves a struct
field, embedded or named, unfilled, so no name may be `-`; a pointer back to a struct
already being filled is left alone, since filling it would never end. `New` cannot see a
@@ -474,6 +466,8 @@ table, `street`, `car` or `useragent`, carries no key and is only drawn.
id by the postort's name, unique across Sweden as the key it replaces, give a new
postort the next unused integer, and retire a removed or renamed one's id for good.
The code still takes a `name` option and selects by it; `todo.md` item 41 brings it to this decision.
## A parent row with no child row is a load error
2026-09-17, Lilleman auf Larv.
+10 -2
View File
@@ -166,14 +166,22 @@ func repeatCheck(path string, n node, mem renderCounts) error {
// graph (renderEdges). Every node is a root: a field its parent's format never
// renders is still reachable by dot path, so a cycle in one would otherwise reach
// render and be fatal there.
func checkNoCycles(sites []categorySite) error {
func checkNoCycles(s nodeScope) error {
const (
grey = 1
black = 2
)
inScope := map[node]bool{}
_ = s(func(_ string, n node) error {
inScope[n] = true
return nil
})
color := map[node]int{}
var visit func(n node, path string) error
visit = func(n node, path string) error {
if !inScope[n] {
return nil // loaded before this scope and proven then, so it never reaches back into it
}
switch color[n] {
case grey:
return fmt.Errorf("reference cycle: %s", path)
@@ -189,5 +197,5 @@ func checkNoCycles(sites []categorySite) error {
color[n] = black
return nil
}
return sitesScope(sites)(func(path string, n node) error { return visit(n, path) })
return s(func(path string, n node) error { return visit(n, path) })
}
+1 -1
View File
@@ -194,7 +194,7 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
}); err != nil {
return err
}
for _, pass := range namePasses {
for _, pass := range linkPasses {
if err := scope(func(path string, m node) error {
if t, ok := m.(*template); ok {
return pass(path, t)
+9 -17
View File
@@ -20,7 +20,6 @@ type nameScope struct {
type nameBinding struct {
name string
ref string // what it binds, as written: a reference, or a path into a field of binder
field bool // ref is a path into a field, so a read of the name stays in the category
body string
where string // the fields reaching the binding template, as a compile error spells them
scope *nameScope
@@ -43,6 +42,9 @@ type nameUse struct {
operand, noRef, nested bool
}
// bindsField reports whether b binds a path into a field, so a read of it stays in the category.
func (b *nameBinding) bindsField() bool { return !isRef(b.ref) }
func (sc *nameScope) lookup(name string) *nameBinding {
for ; sc != nil; sc = sc.up {
if b, ok := sc.bindings[name]; ok {
@@ -111,7 +113,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.boundRef, field: !isRef(tok.boundRef), 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{}
}
@@ -249,29 +251,19 @@ func namedReads(t *template, fn func(o *op, a *arm) error) error {
return nil
}
// namePasses run in order, each over every linked template before the next starts: a pass reads
// what the one before filled in.
var namePasses = []func(path string, t *template) error{linkNames, checkNameReads, checkCalcNames}
// linkPasses run in order once every template is linked, each over every template before the next
// starts: a pass reads what the one before filled in.
var linkPasses = []func(path string, t *template) error{linkNames, linkColumnRead, checkNameReads, checkCalcNames}
// 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 {
if err := namedReads(t, func(o *op, a *arm) 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
}); err != nil {
return err
}
if ops := t.compiled.ops; t.repeat == 1 && len(ops) == 1 && ops[0].kind == nameRead && len(ops[0].arms) == 1 && ops[0].arms[0].kind == namedRead {
a := ops[0].arms[0]
full := append(a.named.tail[:len(a.named.tail):len(a.named.tail)], a.tail...)
if column := recordColumn(a.named.head, full); column != nil {
t.link.readsColumn = &columnRead{a: a, column: column}
}
}
return nil
})
}
func linkName(t *template, a *arm) error {
+44 -3
View File
@@ -363,16 +363,57 @@ func TestANameIsATransformOperand(t *testing.T) {
}
}
func TestOneRenderReadsOneNameAcrossDrawGroups(t *testing.T) {
func TestOneRenderReadsOneNameAcrossNestedTemplates(t *testing.T) {
dir := writeData(t, map[string]string{
"word": `["a","b","c","d","e","f","g","h"]`,
"other": `{"format":"{x}","x":["1","2"]}`,
"cat": `{"format":"{/word as w}{a}","a":{"format":"{w}|{b}","b":{"format":"{w}{/other.x}","drawGroup":"g"}}}`,
"cat": `{"format":"{/word as w}{a}","a":{"format":"{w}|{b}","b":"{w}{/other.x}"}}`,
})
f := newGenerator(t, dir, WithSeed(59))
for i := 0; i < 50; i++ {
if got := fake(t, f, "cat.a"); got[0] != got[2] {
t.Fatalf("cat.a = %q, want one pick of w in both draw groups", got)
t.Fatalf("cat.a = %q, want one pick of w in both templates", got)
}
}
}
func TestAFieldBindingReadInsideItsFieldIsRefused(t *testing.T) {
f := engine(1)
for src, want := range map[string]string{
`{"format":"{x as n}{n}{n}","x":"{n}"}`: `name "n" is read inside "x", the field it binds`,
`{"format":"{x as n}{n.a}{n.a}","x":{"format":"{a}","a":"{n}"}}`: `name "n" is read inside "x", the field it binds`,
`{"format":"{x as n}{y as m}{n}{n}{m}{m}","x":"{m}","y":"{n}"}`: "cycle",
} {
if _, err := f.NewTemplate(src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("NewTemplate(%s) = %v, want it refused naming %s", src, err, want)
}
}
}
func TestANameReadAloneIsTheRecordColumnItReads(t *testing.T) {
dir := writeData(t, map[string]string{
"src": `{"format":"","score":[null,{"format":"{int(1,9)}","datatype":"integer"}],"code":["1","2"]}`,
"row": `{"format":"","a":"{/src.score as s}{s}","b":"{s}","c":"{/src as r}{r.score}","d":"{r.code}"}`,
})
f := newGenerator(t, dir, WithSeed(3))
nulls := 0
for i := 0; i < 100; i++ {
r, err := f.FakeRecord("row")
if err != nil {
t.Fatal(err)
}
c := r.Columns()
if c[0].DataType != DataTypeInteger || c[2].DataType != DataTypeInteger || c[0].Value != c[1].Value || c[0].Null != c[1].Null {
t.Fatalf("%s: want a, b and c integer columns of src.score, a and b one pick", r.JSON())
}
if c[0].Null {
nulls++
}
}
if nulls == 0 || nulls == 100 {
t.Errorf("a was null %d times in 100 records, want both outcomes", nulls)
}
if _, err := f.NewRecordTemplate(`{"format":"","a":"{/src.score as s}{s}","b":"{s}"}`); err != nil {
t.Errorf("NewRecordTemplate = %v, want a name-read column accepted inline", err)
}
}
+17 -1
View File
@@ -1,6 +1,7 @@
package fejkdata
import (
"errors"
"fmt"
"slices"
"strings"
@@ -18,6 +19,9 @@ func checkNameReads(path string, t *template) error {
}
}
return namedReads(t, func(o *op, a *arm) error {
if a.named.bindsField() && rendersInside(a.named.head, t) {
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field it binds, which would render itself; read the name outside that field", t.site.label(path), o.body, a.named.name, a.named.ref)
}
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)
@@ -27,11 +31,23 @@ func checkNameReads(path string, t *template) error {
})
}
// rendersInside reports whether t sits in what n contains.
func rendersInside(n node, t *template) bool {
return eachNode(n, "", func(_ string, m node) error {
if m == t {
return errFound
}
return nil
}) != nil
}
var errFound = errors.New("found")
// checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling
// draws the same way without the name. A field stands only beside its binding.
func (b *nameBinding) checkUses() error {
r := b.uses[0]
if len(b.uses) > 1 || r.nested || b.field && r.in != b.binder {
if len(b.uses) > 1 || r.nested || b.bindsField() && r.in != b.binder {
return nil
}
spelling := b.ref
+1 -1
View File
@@ -92,7 +92,7 @@ func (s tableSite) label(path string) string {
type templateLink struct {
refs map[string]refBinding // each reference the format reads -> what it resolves to
refHeads map[string]node // each refBinding.head -> the category it names
readsColumn *columnRead // set when the format is one reference alone reading a record's column
readsColumn *columnRead // set when the format is one reference or name read alone reading a record's column
}
func (*template) isNode() {}
+1 -2
View File
@@ -159,8 +159,7 @@ func TestNoStructAllocRegression(t *testing.T) {
if err := f.FakeStruct(&v); err != nil {
t.Fatal(err)
}
// The three tags read one pick of x: its frame and memo cost two allocations.
const base = 7.0
const base = 5.0
if allocs := testing.AllocsPerRun(10000, func() { f.FakeStruct(&v) }); allocs > base*1.10 {
t.Errorf("FakeStruct: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); compiling the type per call is the usual cause", allocs, base*1.10, base)
}
+29 -1
View File
@@ -9,6 +9,34 @@ type namedPick struct {
pins pinSet
}
// drawMemo is what a named pick has drawn: the variant each level was drawn as, so every path
// under it reads one variant; the value each read produced, by its path, so the same read written
// twice reads one value; the frames a read opens for the name scopes around where it lands
// (enter); and the rows drawn by each step down after a "..", by level.
type drawMemo struct {
variant map[string]node
value map[string]readValue
enteredFrames map[*nameScope]*pickFrame
steppedDown map[string]*pinSet
}
// stepDownPins is the pins a path steps down into after stepping up to t: kept in m where there
// is one, so every path stepping down there reads one draw.
func (m *drawMemo) stepDownPins(pins *pinSet, t *table, levels []string, at int) *pinSet {
if m == nil {
return pins.above(t)
}
p, ok := m.steppedDown[levels[at]]
if !ok {
p = pins.above(t)
if m.steppedDown == nil {
m.steppedDown = map[string]*pinSet{}
}
m.steppedDown[levels[at]] = p
}
return p
}
// pickFrame is one render of a name scope: a pick per binding, each drawn on its first read.
type pickFrame struct {
scope *nameScope
@@ -117,7 +145,7 @@ func readName(s *generatorState, sc renderScope, a arm) readValue {
return r
}
leaf, pins := p.draw(s, a.named.head, a.steps, a.levels, a.path)
if a.named.field {
if a.named.bindsField() {
return p.renderAt(s, leaf, pins, a.path, sc)
}
sc, mark := sc.enter(leaf, &p.memo)
View File
+31 -31
View File
@@ -76,7 +76,7 @@ func linkRefs(sites []categorySite, root map[string]node) error {
}); err != nil {
return err
}
for _, pass := range namePasses {
for _, pass := range linkPasses {
if err := eachTemplate(sites, func(_ categorySite, path string, t *template) error { return pass(path, t) }); err != nil {
return err
}
@@ -98,9 +98,6 @@ func linkTemplate(folder []string, path, category string, t *template, root map[
}
func compileArms(t *template) {
if r := t.link.readsColumn; r != nil {
compileArm(t, &r.a)
}
for i := range t.compiled.ops {
o := &t.compiled.ops[i]
for j := range o.operands {
@@ -154,46 +151,41 @@ func (t *template) resolveLink(folder []string, path, category string, root map[
link.refHeads[head] = target
link.refs[name] = refBinding{head, tail}
}
link.readsColumn = columnReadOf(t, link)
return link, nil
}
// columnRead is a record's column read by a format of that one reference or name read alone,
// which is the column: it takes the column's datatype and null.
// which is the column: it takes the column's datatype and null. category and field name it.
type columnRead struct {
a arm
column node
a arm
category, field string
column node
}
func columnReadOf(t *template, link templateLink) *columnRead {
name, lone := loneRef(t.tokens)
if !lone || t.repeat != 1 {
// linkColumnRead sets the record's column t's format reads, where it is one reference or name
// read alone, once the names are linked.
func linkColumnRead(_ string, t *template) error {
ops := t.compiled.ops
if t.repeat != 1 || len(ops) != 1 || ops[0].kind != nameRead || len(ops[0].arms) != 1 {
return nil
}
a := splitArm(name, link.refs)
if column := recordColumn(link.refHeads[a.head], a.tail); column != nil {
return &columnRead{a: a, column: column}
a := ops[0].arms[0]
head, category, tail := t.head(a.head), a.head, a.tail
switch {
case a.kind == namedRead && !a.named.bindsField():
ref := a.named.binder.link.refs[a.named.ref]
head, category = a.named.head, ref.head
tail = append(ref.tail[:len(ref.tail):len(ref.tail)], a.tail...)
case a.kind == namedRead || !isRef(a.head):
return nil
}
target, isTemplate := head.(*template)
if isTemplate && target.isRecord && len(tail) == 1 {
t.link.readsColumn = &columnRead{a: a, category: category[1:], field: tail[0], column: target.fields[tail[0]]}
}
return nil
}
// recordColumn is the column tail names in head, nil where head is no record or tail no column of it.
func recordColumn(head node, tail []string) node {
target, isTemplate := head.(*template)
if !isTemplate || !target.isRecord || len(tail) != 1 {
return nil
}
return target.fields[tail[0]]
}
// loneRef is the reference a format of one reference token and nothing else reads.
func loneRef(toks []formatToken) (string, bool) {
if len(toks) != 1 || toks[0].kind != nameRead || len(toks[0].names) != 1 || !isRef(toks[0].names[0]) {
return "", false
}
return toks[0].names[0], true
}
// eachTemplate calls fn once per template of the categories, with the category it sits
// in and the dot path reaching it.
func eachTemplate(sites []categorySite, fn func(s categorySite, path string, t *template) error) error {
@@ -259,3 +251,11 @@ func refTokens(toks []formatToken) []string {
}
return refs
}
// loneRef is the reference a format of one reference token and nothing else reads.
func loneRef(toks []formatToken) (string, bool) {
if len(toks) != 1 || toks[0].kind != nameRead || len(toks[0].names) != 1 || !isRef(toks[0].names[0]) {
return "", false
}
return toks[0].names[0], true
}
-28
View File
@@ -1,33 +1,5 @@
package fejkdata
// drawMemo is what a named pick has drawn: the variant each level was drawn as, so every path
// under it reads one variant; the value each read produced, by its path, so the same read written
// twice reads one value; the frames a read opens for the name scopes around where it lands
// (enter); and the rows drawn by each step down after a "..", by level.
type drawMemo struct {
variant map[string]node
value map[string]readValue
enteredFrames map[*nameScope]*pickFrame
steppedDown map[string]*pinSet
}
// stepDownPins is the pins a path steps down into after stepping up to t: kept in m where there
// is one, so every path stepping down there reads one draw.
func (m *drawMemo) stepDownPins(pins *pinSet, t *table, levels []string, at int) *pinSet {
if m == nil {
return pins.above(t)
}
p, ok := m.steppedDown[levels[at]]
if !ok {
p = pins.above(t)
if m.steppedDown == nil {
m.steppedDown = map[string]*pinSet{}
}
m.steppedDown[levels[at]] = p
}
return p
}
// readValue is what one read drew: its text, and whether it landed on a null.
type readValue struct {
text string
+10 -44
View File
@@ -95,7 +95,6 @@ type structFields struct {
typ reflect.Type
label string
tags map[string]any
paths map[string]string // each field tagged with a path, and the path
shape *structShape
}
@@ -105,12 +104,11 @@ func (sc *structCompile) compileShape(t reflect.Type, label string) (*structShap
}
sc.visiting[t] = true
defer delete(sc.visiting, t)
c := &structFields{structCompile: sc, typ: t, label: label, tags: map[string]any{}, paths: map[string]string{}, shape: &structShape{}}
c := &structFields{structCompile: sc, typ: t, label: label, tags: map[string]any{}, shape: &structShape{}}
if err := c.gatherFields(t, nil); err != nil {
return nil, err
}
if len(c.tags) > 0 {
bindShared(sc.root, c.tags, c.paths)
if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil {
return nil, err
}
@@ -174,14 +172,11 @@ func (c *structFields) addTag(sf reflect.StructField, tag string) error {
if visible, ok := c.typ.FieldByName(sf.Name); !ok || !slices.Equal(visible.Index, sf.Index) {
return fmt.Errorf("%s.%s: hidden by another field named %s, so its fake tag cannot fill it; rename one", c.label, fieldPath(c.typ, sf.Index), sf.Name)
}
v, path, err := tagValue(sf, tag)
v, err := tagValue(sf, tag)
if err != nil {
return fmt.Errorf("%s.%s: %w", c.label, sf.Name, err)
}
c.tags[sf.Name] = v
if path != "" {
c.paths[sf.Name] = path
}
return nil
}
@@ -220,52 +215,22 @@ func (c *structFields) nest(sf reflect.StructField, elem reflect.Type) error {
}
// tagValue reads a field's fake tag as the value its column compiles from: an inline template
// as written, or a path as the reference {/path}, returning the path too.
func tagValue(sf reflect.StructField, tag string) (any, string, error) {
// as written, or a path as the reference {/path}.
func tagValue(sf reflect.StructField, tag string) (any, error) {
if err := checkTaggedType(sf); err != nil {
return nil, "", err
return nil, err
}
inline, err := isTemplate(tag)
switch {
case err != nil:
return nil, "", err
return nil, err
case inline:
v, err := inputValue(tag)
return v, "", err
return inputValue(tag)
}
if err := checkPathNames(tag); err != nil {
return nil, "", err
return nil, err
}
return "{/" + tag + "}", tag, nil
}
// bindShared has the fields whose paths read into one category, two or more of them and none
// selecting a row, read one pick of it through a name, as a record's columns read one pick.
func bindShared(root *folder, tags map[string]any, paths map[string]string) {
byCategory := map[string][]string{}
for _, field := range sortedNames(paths) {
segs, err := splitPath(paths[field])
if err != nil || hasSelector(segs) {
continue
}
if _, i := folderAt(root, segs); i < len(segs) {
category := strings.Join(segs[:i+1], ".")
byCategory[category] = append(byCategory[category], field)
}
}
var format strings.Builder
for _, category := range sortedNames(byCategory) {
fields := byCategory[category]
if len(fields) < 2 {
continue
}
name := strings.ReplaceAll(category, ".", "-")
fmt.Fprintf(&format, "{/%s as %s}", category, name)
for _, field := range fields {
tags[field] = "{" + name + strings.TrimPrefix(paths[field], category) + "}"
}
}
tags["format"] = format.String()
return "{/" + tag + "}", nil
}
// checkTaggedType rejects a tagged field no column can fill.
@@ -288,6 +253,7 @@ func checkTaggedType(sf reflect.StructField) error {
// compileRecord compiles the tagged fields of t as one record, and proves each column holds
// only what its field's Go type can.
func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]any) error {
tags["format"] = ""
n, err := compile(tags)
if err != nil {
return fmt.Errorf("%s: %w", label, err)
+28 -22
View File
@@ -8,18 +8,18 @@ import (
)
type structPlace struct {
City string `fake:"place.city"`
Zip string `fake:"place.zip"`
City string `fake:"{/place as p}{p.city}"`
Zip string `fake:"{p.zip}"`
}
type structUser struct {
Active bool `fake:"[\"true\",\"false\"]"`
Age uint8 `fake:"{int(18,99)}"`
Email string `fake:"{lowercase(/person.first)}@example.com"`
First string `fake:"person.first"`
Email string `fake:"{lowercase(p.first)}@example.com"`
First string `fake:"{/person as p}{p.first}"`
Home structPlace
ID int64 `fake:"{seq()}"`
Last string `fake:"person.last"`
Last string `fake:"{p.last}"`
Level uint8 `fake:"{float(0,255,0)}"`
Nick *string `fake:"[null,\"bo\"]"`
Note string
@@ -31,7 +31,7 @@ type structUser struct {
}
type structGiven struct {
First string `fake:"person.first"`
First string `fake:"{/person as p}{p.first}"`
}
type StructFamily struct {
@@ -41,7 +41,7 @@ type StructFamily struct {
type structEmployee struct {
structGiven
*StructFamily
Email string `fake:"{lowercase(/person.first)}@example.com"`
Email string `fake:"{lowercase(p.first)}@example.com"`
}
type structLink struct {
@@ -118,25 +118,28 @@ func TestFakeStructFieldTaggedWithAColumnIsThatColumn(t *testing.T) {
}
}
func digitOrNil(s string) bool {
return s == "nil" || len(s) == 1 && s >= "1" && s <= "9"
}
func TestFakeStructPathTagsReadOnePickOfTheirCategory(t *testing.T) {
func TestFakeStructPathTagsDrawAfreshAndANameKeepsOnePick(t *testing.T) {
f := structData(t)
var v struct {
Leg string `fake:"trip.leg"`
To string `fake:"trip.leg.to"`
Name string `fake:"person"`
Last string `fake:"person.last"`
A string `fake:"trip.leg"`
B string `fake:"trip.leg"`
Leg string `fake:"{/trip as r}{r.leg}"`
To string `fake:"{r.leg.to}"`
Name string `fake:"{/person as p}{p}"`
Last string `fake:"{p.last}"`
}
apart := false
for i := 0; i < 100; i++ {
if err := f.FakeStruct(&v); err != nil {
t.Fatal(err)
}
if v.Leg != v.To || v.Name != "Ada "+v.Last && v.Name != "Bo "+v.Last {
t.Fatalf("%+v, want the tags into trip one leg, and into person one person", v)
t.Fatalf("%+v, want the reads of r one leg, and of p one person", v)
}
apart = apart || v.A != v.B
}
if !apart {
t.Error("two tags of trip.leg drew one leg in 100 fills, want each path tag a draw of its own")
}
}
@@ -157,8 +160,8 @@ func TestFakeStructFillsTaggedFields(t *testing.T) {
switch {
case !reflect.DeepEqual(u, twin):
t.Fatalf("same seed diverged: %+v != %+v", u, twin)
case people[u.First] != u.Last || u.Email != "ada@example.com" && u.Email != "bo@example.com":
t.Fatalf("person fields %q %q %q, want one person across the path tags, and the template its own draw", u.First, u.Last, u.Email)
case people[u.First] != u.Last || u.Email != strings.ToLower(u.First)+"@example.com":
t.Fatalf("person fields %q %q %q, want one person read through p across the struct", u.First, u.Last, u.Email)
case zips[u.Home.City] != u.Home.Zip || u.Work == nil || zips[u.Work.City] != u.Work.Zip:
t.Fatalf("places %+v, %+v, want each nested struct one place, the pointer allocated", u.Home, u.Work)
case u.ID != int64(i+1) || u.Age < 18 || u.Age > 99 || u.Score < 0 || u.Score > 1 || u.Rank == nil || (*u.Rank != 1 && *u.Rank != 2):
@@ -180,14 +183,13 @@ func TestFakeStructFillsTaggedFields(t *testing.T) {
func TestFakeStructFillsEmbeddedFieldsIntoItsRecord(t *testing.T) {
f := structData(t)
people := map[string]string{"Ada": "Lovelace", "Bo": "Ek"}
for i := 0; i < 100; i++ {
var e structEmployee
if err := f.FakeStruct(&e); err != nil {
t.Fatal(err)
}
if e.StructFamily == nil || people[e.First] != e.Last {
t.Fatalf("%+v, %+v: want the promoted fields one person, the embedded pointer allocated", e, e.StructFamily)
if e.StructFamily == nil || e.Last != "Lovelace" && e.Last != "Ek" || e.Email != strings.ToLower(e.First)+"@example.com" {
t.Fatalf("%+v, %+v: want a promoted field's name read by the struct's own, the embedded pointer allocated", e, e.StructFamily)
}
}
var skipped struct {
@@ -351,3 +353,7 @@ func TestFakeStructBoundsTheStructsATypeReaches(t *testing.T) {
t.Errorf("a type reaching 1025 structs, the last embedded: FakeStruct = %v, want it refused naming the cap and fake:\"-\"", err)
}
}
func digitOrNil(s string) bool {
return s == "nil" || len(s) == 1 && s >= "1" && s <= "9"
}
+4 -4
View File
@@ -591,7 +591,7 @@ func TestTableFences(t *testing.T) {
"a whole table's format selecting another row": map[string]string{"x.json": `{"format":"{v} {/y[1].w}","rows":"x.tsv","key":"code"}`, "x.tsv": "code\tv\n1\ta\n2\tb\n", "y.json": `{"format":"{w}","rows":"y.tsv","key":"k"}`, "y.tsv": "k\tw\n1\tm\n2\tn\n", "r.json": `"{/x} {/y[2].w}"`},
"a table reaching its family through a template": with(geo(), map[string]string{"addr.json": `"{/locality.name}"`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}),
"a table reaching its family through a repeat": with(geo(), map[string]string{"addr.json": `{"format":"{/locality.name} ","repeat":3}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}),
"a selected row reaching its family's other row": with(geo(), map[string]string{"a.json": `"{/region[01].note}"`, "addr.json": `{"format":"{/region[12].name}","drawGroup":"g"}`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}),
"a selected row reaching its family's other row": with(geo(), map[string]string{"a.json": `"{/region[01].note}"`, "addr.json": `"{/region[12].name}"`, "region.tsv": "code\tname\tpopulation\ttimezone\tnote\n01\tStockholms län\t2400000\tEurope/Stockholm\t{/addr}\n12\tSkåne län\t1400000\tEurope/Stockholm\t-\n14\tVästra Götalands län\t1750000\tEurope/Stockholm\t-\n"}),
"a cell repeating a reference": with(base, map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv"}`, "t.tsv": "a\nx\n{/region} {/region} {uppercase(/region)}\n"}),
}
for name, files := range accepted {
@@ -899,9 +899,9 @@ func TestShippedTables(t *testing.T) {
}
}
var row struct {
Idempotent bool `fake:"misc.httpmethod.idempotent"`
Method string `fake:"misc.httpmethod.method"`
Safe bool `fake:"misc.httpmethod.safe"`
Idempotent bool `fake:"{/misc.httpmethod as m}{m.idempotent}"`
Method string `fake:"{m.method}"`
Safe bool `fake:"{m.safe}"`
}
if err := newGenerator(t, "data", WithSeed(1)).FakeStruct(&row); err != nil {
t.Fatalf("FakeStruct into a bool = %v, want the register's yes and no shipped as true and false", err)
+12 -2
View File
@@ -6,7 +6,7 @@
`Bar = 9`
`Next ID = 94`
`Next ID = 96`
| Goal | W |
|---|---|
@@ -82,7 +82,7 @@
| 81 | 0.2.0 | decision | **Refuse at load every `calc` operand not proven numeric and every divisor not proven nonzero, in a string column too.** | 4 | 4 | 6 | 8 | 6.1, 4 | 20.6 |
| 36 | 0.2.0 | | **Report every mistake a load finds in one error, as the decision "A load reports every mistake at once" states.** | 5 | 6 | 7 | 8 | 6 | 19.5 |
| 88 | 0.2.0 | | **Add a 12-digit `YYYYMMDDNNNC` form of `sv_SE.personnummer` and `sv_SE.samordningsnummer`, the form Skatteverket's testpersonnummer series and many systems store.** | 2 | 3 | 6 | 6 | 4 | 19.4 |
| 41 | 0.2.0 | | **Key every table by one key column, as the decision "A table selects by one key column, a code, never a free-form name" states.** | 6 | 7 | 7 | 8 | 5.5 | 19.1 |
| 41 | 0.2.0 | decision | **Key every table by one key column, as the decision "A table selects by one key column, a code, never a free-form name" states.** | 6 | 7 | 7 | 8 | 5.5 | 19.1 |
| 52 | 0.2.0 | | **Name the file a link error comes from beside its category path, `sub.x (/d/a/sub/x.json): …`.** | 1 | 2 | 5 | 7 | 6 | 19.0 |
| 28 | 0.2.0 | decision | **Accept a middle name, and draw a `personnummer` whose sex matches a sex selected through a named pick.** | 4 | 4 | 6 | 7 | 4.1 | 18.9 |
| 24 | 0.2.0 | defect | **Stop `misc.territory[EH].tld` rendering `.eh`, the one shipped TLD `misc.tld` does not hold.** | 1 | 1 | 4 | 7 | 4 | 18.6 |
@@ -104,6 +104,7 @@
| 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 |
| 22 | 0.2.0 | | **Give `url` and `email` a path that draws only domains nobody can register, keeping the wide set as the default.** | 3 | 4 | 7 | 8 | 16 | 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, 10 | 14.0 |
| 94 | 0.2.0 | | **Refuse a name whose every read passes one deeper level, naming the binding of that level: `{place as p}{p.geo.town.name} {p.geo.town.zip}` is `{place.geo.town as t}{t.name} {t.zip}`.** | 3 | 3 | 3 | 5 | 5.7 | 10.2 |
| 14 | 0.3.0 | | **Spell `misc.creditcard`'s digit runs `{digits(n)}`.** | 2 | 2 | 4 | 6 | 5.7 | 15.1 |
| 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 |
@@ -127,6 +128,7 @@
| 37 | 0.3.0 | | **Rename `newRand` to `newGeneratorState` and `Generator.rand` to `state`.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 |
| 43 | 0.3.0 | | **Call `templateError` a compile failure in its doc.** | 1 | 1 | 1 | 5 | 3.3 | 9.8 |
| 82 | 0.3.0 | principle | **Pass `newRand` its entropy source, so no test swaps the package's `randomBytes`.** | 1 | 1 | 1 | 5 | 3.2 | 9.8 |
| 95 | 0.3.0 | principle | **Hold a struct's columns and the field each fills in one slice.** | 1 | 2 | 1 | 5 | 3.2 | 9.5 |
| 19 | 0.3.0 | | **Fill the 398 Swedish localities weighted 200 from SCB småorter.** | 2 | 4 | 4 | 6 | 14.1 | 9.2 |
| 56 | 0.3.0 | defect | **Build on a manual run of `test.yml`.** | 1 | 1 | 1 | 4 | 1 | 8.8 |
| 83 | 0.3.0 | principle | **Read a name binding's head and tail from its binder's link, and delete `linkBindings`.** | 4 | 3 | 1 | 6 | 3.2 | 8.1 |
@@ -356,6 +358,10 @@ Require the path step to reach a sibling category.
`readTableOptions` and `loadDir` return on the first in Go's map order.
### 94. Refuse a name whose every read passes one deeper level, naming the binding of that level: `{place as p}{p.geo.town.name} {p.geo.town.zip}` is `{place.geo.town as t}{t.name} {t.zip}`.
Both spellings load and draw alike, against goal 5.7. Widen the read-once refusal in `namefence.go` to the longest prefix every read of a name shares.
### 14. Spell `misc.creditcard`'s digit runs `{digits(n)}`.
A `"repeat": 14` over `{digits(1)}` draws every 14-digit run with the odds `{digits(14)}` does. Both load, since neither has a part that changes nothing; the shipped data writes the obvious one. Re-pin seeded output in its own commit.
@@ -432,6 +438,10 @@ Its doc says render failure, and it wraps what `NewTemplate` and `NewRecordTempl
`fejkdata_test.go` swaps the global `randomBytes` to fail seeding, so every `New` in the package reads the failing source while that test runs. Technical principle: functional core, imperative shell.
### 95. Hold a struct's columns and the field each fills in one slice.
`structShape.columns` and `structShape.fieldIndexes` pair up by index alone, against the technical principle "one owner per value".
### 56. Build on a manual run of `test.yml`.
`workflow_dispatch` leaves `github.event.before` empty, so the diff compares `HEAD` with itself and skips `docker build`.