Answer stability round 3, product-owner round 3 and prose round 1: a column reading a bound column takes its datatype and null, a column bound in part draws afresh, and plainer wording across the README, decisions, plan and errors

This commit is contained in:
2026-10-05 10:04:11 +02:00
committed by Lillem4n
parent dfde34556e
commit f89ffd1b73
17 changed files with 121 additions and 85 deletions
+3 -3
View File
@@ -63,7 +63,7 @@ In [docs/decisions.md](docs/decisions.md):
- A record's column set is fixed before the first draw
- Null is a `null` item, not a rate
- A typed column holds one value, not composed text
- A column of one reference or name read alone is the column it reads
- A column that only reads one reference or name is the column it reads
- A typed column's calc is refused unless proven
- `Column` carries text, not a Go value
- A unit takes the stem of what it is, and a file the stem of the units it holds
@@ -74,7 +74,7 @@ In [docs/decisions.md](docs/decisions.md):
- `parent` names the link column and the table alike
- After a row, a path names a column or a linked table
- A selected row is fixed, not drawn
- `..` steps up to the parent row, and a step down after it draws afresh, or once per pick
- `..` steps up to the parent row, and a step down after it draws afresh, or once per name's pick
- A table selects by one key column, a code, never a free-form name
- A parent row with no child row is a load error
- The choice-of-rows fence guards a data file's root, and requires string fields
@@ -111,4 +111,4 @@ In [docs/decisions.md](docs/decisions.md):
- `List` advertises direct descents only
- A path draws through its compiled steps
- A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read
- comprehension floor: every dimension and the overall at 7.0 or above; the draw restructure and item 90 ship with no scoring run, and item 5 scores them
- comprehension floor: every dimension and the overall at 7.0 or above; the change making every `{…}` draw afresh and item 90 ship with no scoring run, and item 5 scores them
+8 -8
View File
@@ -12,16 +12,16 @@ replacement, and each removed path, column or flag.
`{/city[Oslo]..country.name}` renders `Norway`, and `.` after it steps down again,
drawing afresh. `New` refuses a `..` naming anything but the parent table, and a
selector after a `..`.
- Names: `{/misc.territory as t}` binds one row of `misc.territory` to `t` and prints
nothing; `{t}`, `{t.capital}` and `{lowercase(t.name)}` read that one row. A name lives
in the category binding it, and a `repeat` binding it picks again on every iteration.
A name is a `{calc()}` operand: `{/misc.coordinate.lat as lat}{calc(lat * 60)}`. What
`New` refuses is listed under [Names](README.md#names).
- 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 tags share its names,
`{/sv_SE.person as p}{p.first}` beside `{p.sex}`.
`{p as q}{q.a} {q.b}` and `{/person as n}{n.first} {n.last}` make one.
`{/misc.territory as t}` binds one row of `misc.territory` to `t` and prints nothing;
`{t}`, `{t.capital}` and `{lowercase(t.name)}` read that one row. A name binds a
reference, a sibling field or a path into one, lives in the category binding it, and
picks again on every iteration of a `repeat` binding it. A struct's tags share its
names, `{/sv_SE.person as p}{p.first}` beside `{p.sex}`. A name is a `{calc()}`
operand: `{/misc.coordinate.lat as lat}{calc(lat * 60)}`. What `New` refuses is listed
under [Names](README.md#names).
- 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,
+22 -21
View File
@@ -137,12 +137,14 @@ 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, while `{/currency.code}` and `{/currency.symbol}` in two columns may name two. A
field the format binds whole, `{place as p}`, is a column holding that pick, so it agrees with
the columns reading `p`.
column the format binds to a name, as `{place as p}` binds `place`, holds that same pick, so it
agrees with the columns reading `p`; a column bound only in part, `{place.town as t}`, draws
afresh, so bind it whole and read `{p.town}`.
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
value two fields share in its own category, which each binds to a name
value two fields share in its own category, bind it to a name in one field, and read the
name in both
([Decisions](docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load)).
## Data
@@ -342,9 +344,8 @@ 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
pointer to one, fills from its own tags as a record of its own, so its names and picks are
its own. `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
first call for a type compiles its tags and reports what they get wrong, with the same
@@ -372,15 +373,15 @@ 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 path into a sibling field, to `name` ([Names](#names)) |
| `{x as name}` | nothing: binds one pick of `x`, a reference, a sibling field or a path into one, 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.
Each `{…}` draws afresh, so `{w} {w}`, `{p.a} {p.b}` and `{/person.first} {/person.last}`
are two draws each. Only 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)}"
@@ -448,7 +449,7 @@ 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 or name read alone to another record's column —
A column whose format is one read of another record's column and nothing else —
`"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.
@@ -569,7 +570,7 @@ country by its weights, then a city inside it by theirs. Two paths are two draws
[name](#names) agree instead, and the table it binds decides whose weights govern:
`{/city as c}` draws a city by city weight and `{c..country.name}` is its country, while
`{/country as k}` draws a country by country weight and `{k.city.name}` a city inside it.
A name keeps every row its reads pin, so `{k.city.name}` and `{k.city.population}` describe
A name keeps every row its reads draw, so `{k.city.name}` and `{k.city.population}` describe
one city, and `{k.name}` its country.
`New` refuses a link cell that is no key of the parent, a parent row no child links to, a
@@ -717,8 +718,8 @@ without naming `sv_SE`:
"Hej, {/en_US.person}!"
```
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}
Renders e.g. `Hej, Pat Smith!`. Each reference draws afresh ([Format string](#format-string)),
so `{.person.first} {.person.last}` may name two people. 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
@@ -726,8 +727,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 path into a sibling field, to the name `n`
and prints nothing. `{n}` then
`{x as n}` binds one pick of `x`, a reference, a sibling field or a path into one, 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:
@@ -739,7 +740,7 @@ Renders e.g. `🇳🇴 Norway: capital Oslo, calling code +47`. A name keeps wha
lands on and every level it passes, so `{n}` and `{n.path}` agree, and so does a
[transform](#transforms) of `n.path`. Everything else under the name draws as it would
anywhere: if `n`'s category has the format `{w}-{w}`, it draws `w` twice, and `{n.w}` beside
`{n}` is refused, since it could read either draw. A field binds the same way, so
`{n}` is refused, since it could read either draw. A sibling field is bound the same way, so
`{place as p}{p.postal-code} {p.locality}` reads one place ([Correlated fields](#correlated-fields)).
A name is a pick of its own: `{t.name}` and `{/misc.territory.name}` beside it are two draws,
@@ -756,12 +757,12 @@ 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: read it outside the field;
- a read of a name inside the field bound to it: read it outside that 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
`{/region as r}{r.name}` is `{/region.name}`; a calc reads neither a reference nor a
path, so there one read is not refused;
- a binding nothing reads, or one read only once, unless that read sits in a `repeat`
nested where it is bound: write what it binds there instead, `{/word}` for
`{/word as w}{w}` and `{/region.name}` for `{/region as r}{r.name}`; a calc cannot read a
reference or a path, so a calc's one read of a name bound to either is not refused;
- a path through a name that selects a row;
- `{n}` beside `{n.w}` where `n`'s category reads `w` twice, as in `{w}-{w}` or
`{w.a} {w.b}`;
+1 -1
View File
@@ -76,7 +76,7 @@ func checkColumns(s nodeScope) error {
checked[column] = true
items, _ := columnItems(column)
for _, it := range items {
if r := it.link.readsColumn; r != nil {
if r := it.link.readsColumn; r != nil && r.category != "" {
if err := check(r.category, r.field, r.column); err != nil {
return err
}
+18 -17
View File
@@ -128,19 +128,18 @@ Works against goal 2.3 until `todo.md` item 77 ends it.
## Every `{…}` draws afresh, and only a name keeps a pick
2026-10-05, larv-review on the draw restructure the maintainer planned on 2026-10-03.
2026-10-05, larv-review on the change the maintainer planned on 2026-10-03.
Serves goals 5.2 and 4.1: each `{…}` makes a new pick, and values that belong together come
from the one pick a name keeps.
- `{p.a} {p.b}`, `{w} {uppercase(w)}`, `{/p.first} {/p.last}` and `{/p} {/p}` are two draws
each, and `{p as q}{q.a} {q.b}` is one. Nothing is held per format or per render, so no
fence guards a hold: the load checks on draws are names, paths through names, cycles and
the repeat cap.
- A name binds a reference or a path into a field of the template binding it, `{place as
each, and `{p as q}{q.a} {q.b}` is one. Nothing is kept per format or per render, so the
only load checks on draws are names, paths through names, cycles and the repeat cap.
- A name binds a reference, or a field of the template binding it or a path into one, `{place as
p}`: an inline template, and a choice of rows inside one category, have no reference to
bind, and goal 4.1 holds there too.
- A name read once is refused where the spelling it binds stands in, and the error names
that spelling (goal 5.7): `{/x as n}{n.y}` is `{/x.y}`. A calc reads no reference or
- A name read once is refused where writing what it binds in its place draws the same, and
the error names that spelling (goal 5.7): `{/x as n}{n.y}` is `{/x.y}`. A calc reads no reference or
path, and a read inside a nested repeat keeps one pick on every line, so neither is
refused.
@@ -299,9 +298,11 @@ 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 names: one tag may bind `{/person as p}{p.first}` and another read `{p.sex}` (goal 4.1),
while a path tag draws afresh as `{/path}` does (goal 5.2). A tag binding a name only an
embedding type reads is refused on the embedded type alone, as any name read once is, so the
embedder binds it in a tag of its own; a tagged field that another field hides is refused, not dropped. A named
while a path tag draws afresh as `{/path}` does (goal 5.2).
A tag binding a name only an embedding type reads is refused on the embedded type alone, as
any name read once is, so the embedder binds it in a tag of its own. 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
@@ -316,7 +317,7 @@ handle is needed, as the cache already compiles once.
A category is one unit: a reference back into it — `{/users.first}` inside `users` —
describes a draw other than the fields beside it, so `New` refuses it and the sibling
field stays the one spelling for a field of one's own. A value two fields share goes in its
own category, which each binds to a name (goal 4.1). That settled, a record's column fences
own category, which one field binds to a name and both read (goal 4.1). That settled, a record's column fences
run at `New` too, so a category that loads renders as a value and as a record alike.
## A record's column set is fixed before the first draw
@@ -345,7 +346,7 @@ Its bounds come from a literal or a call's arguments, so a load error names a re
value, a range check is one comparison, and `1{digits(2)}` is a second spelling of
`{int(100,199)}`.
## A column of one reference or name read alone is the column it reads
## A column that only reads one reference or name is the column it reads
2026-09-15, Lilleman auf Larv.
@@ -435,10 +436,10 @@ accepted one changes, so the door stays open for the address records the plan de
Decided 2026-09-27 by the maintainer, for goals 4 and 5; valid while a selector names
exactly one row.
Selected rows never conflict: `{/misc.territory[SE].capital} / {/misc.territory[FI].capital}`
Each selector reads the row it names: `{/misc.territory[SE].capital} / {/misc.territory[FI].capital}`
renders `Stockholm / Helsinki`, and two items of one choice may select different rows.
## `..` steps up to the parent row, and a step down after it draws afresh, or once per pick
## `..` steps up to the parent row, and a step down after it draws afresh, or once per name's pick
2026-10-05, larv-review in PR #158. Serves goal 5.4, which has `..` go up one level, and goal
5.2, which has each `{…}` make a new pick.
@@ -810,7 +811,7 @@ it.
The scope is lexical. A read entering a category, by a reference, through a name or from
`Fake`, sees no frame (one render of a scope's picks) opened above it, so a category renders
the same whoever references it. It opens the frames around where it lands: a read through a
the same whoever references it. Such a read opens the frames around where it lands: a read through a
name from its pick's memo, so `{n}` and `{n.path}` read one pick of each name inside, and a
reference or `Fake` fresh ones.
@@ -820,6 +821,6 @@ category's frames, since the field sits inside it.
Valid while names are read only inside the category binding them.
## comprehension floor: every dimension and the overall at 7.0 or above; the draw restructure and item 90 ship with no scoring run, and item 5 scores them
## comprehension floor: every dimension and the overall at 7.0 or above; the change making every `{…}` draw afresh and item 90 ship with no scoring run, and item 5 scores them
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat, and the hardest units were the draw fences the restructure deleted. The draw restructure, shipped 2026-10-05, and `todo.md` item 90 delete or move what the earlier chunks of the below-floor round touched, so a scoring run before item 90 merges would score code about to go; the calc change accepting a name as an operand shipped unscored on 2026-10-05 for that reason. The two finish the round `AGENTS.md`'s comprehension rule lets merge below the floor, a restructure the maintainer approved. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat, and the hardest units were the draw fences that change deleted. The change making every `{…}` draw afresh and `todo.md` item 90 delete or move what the earlier chunks of the below-floor round touched, so a scoring run before item 90 merges would score code about to go; the calc change accepting a name as an operand shipped unscored on 2026-10-05 for that reason. The two finish the round `AGENTS.md`'s comprehension rule lets merge below the floor, a restructure the maintainer approved. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
+1 -1
View File
@@ -316,7 +316,7 @@ func checkBound(ref string, fields map[string]node) error {
}
missing, err := checkArm(ref, fields, false)
if missing {
return fmt.Errorf("%w; a name binds a pick of a field, or of a reference, which starts with /, . or ..", err)
return fmt.Errorf("%w; a binding names a field, or a reference, which starts with /, . or ..", err)
}
return err
}
+3 -3
View File
@@ -222,7 +222,7 @@ func (t *template) isName(head string) bool {
return !isRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil
}
// linkBindings gives each binding t's tokens make the head and tail what it binds resolved to.
// linkBindings resolves what each of t's bindings binds to a head node and a tail path.
func linkBindings(t *template) {
for _, tok := range t.tokens {
if tok.kind != nameBind {
@@ -255,8 +255,8 @@ func namedReads(t *template, fn func(o *op, a *arm) error) error {
// 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.
// linkNames compiles t's reads of a name as paths from the head its binding resolved to, once every
// template is linked, so each binder has resolved what it binds.
func linkNames(path string, t *template) error {
return namedReads(t, func(o *op, a *arm) error {
if err := linkName(t, a); err != nil {
+3 -3
View File
@@ -185,7 +185,7 @@ func TestNameErrors(t *testing.T) {
{"an option", map[string]string{"word": `["a","b"]`, "card": `"{/word as format}{format}"`},
`"format" is an option and can never be a name`},
{"no reference or field", map[string]string{"card": `{"format":"{word as w}{w}{w}","words":["a","b"]}`},
`no field "word"; a name binds a pick of a field, or of a reference`},
`no field "word"; a binding names a field, or a reference`},
{"read once through a field", map[string]string{"card": `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`},
`name "p" is read once, so it keeps no pick for another read; write {place.x} where it is read`},
{"read by nothing", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}"`},
@@ -380,8 +380,8 @@ func TestOneRenderReadsOneNameAcrossNestedTemplates(t *testing.T) {
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}{n}{n}","x":"{n}"}`: `name "n" is read inside "x", the field bound to it`,
`{"format":"{x as n}{n.a}{n.a}","x":{"format":"{a}","a":"{n}"}}`: `name "n" is read inside "x", the field bound to it`,
`{"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) {
+6 -5
View File
@@ -6,8 +6,9 @@ import (
"strings"
)
// checkNameReads refuses each binding of t that checkUses refuses, and a read such as {n} whose
// pick renders twice a field another read, {n.w}, reads once: the pick keeps one draw of it.
// checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the
// field bound to it, and a read {n} beside {n.w} where n's pick renders w twice: the pick keeps one
// draw of w.
func checkNameReads(path string, t *template) error {
for _, tok := range t.tokens {
if tok.kind != nameBind {
@@ -19,7 +20,7 @@ func checkNameReads(path string, t *template) error {
}
return namedReads(t, func(o *op, a *arm) error {
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) {
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field it binds; read the name outside that field", t.site.label(path), o.body, a.named.name, a.named.ref)
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; 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 {
@@ -58,8 +59,8 @@ func rendersInside(nodes []node, t *template) bool {
}
// 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 where the reading template reaches the
// binder through fields. Where the spelling would be a CLI argument or tag of one reference alone,
// draws the same way without the name. A bound field can stand in only where the reading template
// reaches the binder through fields. Where the spelling would be a CLI argument or tag of one reference alone,
// that entry point's own refusal then names the bare path.
func (b *nameBinding) checkUses() error {
r := b.uses[0]
+2 -2
View File
@@ -183,8 +183,8 @@ func readUnder(s *generatorState, t *template, sc renderScope, a arm) readValue
return p.renderAt(s, leaf, pins, key, sc)
}
// underKey is the key of path under the pick key prefix. It never returns prefix itself, which a
// memo would keep: anything a renderScope holds reaching the heap moves every render's scope there.
// underKey is the key of path under the pick key prefix. It never returns prefix itself: prefix is
// sc.pickKey, and a memo keeping it would move every render's scope to the heap.
func underKey(prefix, path string) string {
if prefix == "" {
return path
+25
View File
@@ -307,3 +307,28 @@ func TestAFieldBoundWholeIsItsPickAsAColumn(t *testing.T) {
t.Errorf("place drew only %v in 100 records, want both", seen)
}
}
func TestAColumnReadingABoundColumnIsThatColumn(t *testing.T) {
f := newGenerator(t, writeData(t, map[string]string{
"row": `{"format":"{place as p}","place":[null,{"format":"{int(1,9)}","datatype":"integer"}],"a":"{p}","z":"{p}"}`,
}), WithSeed(1))
nulls := 0
for i := 0; i < 100; i++ {
r, err := f.FakeRecord("row")
if err != nil {
t.Fatal(err)
}
c := r.Columns()
for _, col := range c {
if col.DataType != DataTypeInteger || col.Null != c[0].Null || col.Value != c[0].Value {
t.Fatalf("%s: want a, place and z one integer pick, null alike", r.JSON())
}
}
if c[0].Null {
nulls++
}
}
if nulls == 0 || nulls == 100 {
t.Errorf("the pick was null %d times in 100 records, want both outcomes", nulls)
}
}
+4 -1
View File
@@ -269,7 +269,7 @@ func renderRecord(s *generatorState, t *template, columns []Column, sc renderSco
field := t.fields[r.columns[i].Name]
var column readValue
if b := bindingOfWhole(t, field); b != nil {
column = readName(s, sc, arm{kind: namedRead, named: b, levels: []string{""}})
column = readName(s, sc, arm{kind: namedRead, named: b, levels: wholeLevels})
} else {
column = renderLeaf(s, field, sc)
}
@@ -278,6 +278,9 @@ func renderRecord(s *generatorState, t *template, columns []Column, sc renderSco
return r
}
// wholeLevels is the levels of a read of a whole name, as linkName compiles {n}.
var wholeLevels = []string{""}
// bindingOfWhole is the name t binds to the whole of field, whose pick the field's column renders
// so the row agrees with the columns reading the name; nil where none does.
func bindingOfWhole(t *template, field node) *nameBinding {
+13 -8
View File
@@ -155,7 +155,9 @@ func (t *template) resolveLink(folder []string, path, category string, root map[
}
// 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. category and field name it.
// which is the column: it takes the column's datatype and null. category and field name it;
// category is "" for a column of the reading template's own record, which checkColumns reaches
// on its own.
type columnRead struct {
a arm
category, field string
@@ -170,18 +172,21 @@ func linkColumnRead(_ string, t *template) error {
return nil
}
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
head, category, tail := t.head(a.head), a.head[min(1, len(a.head)):], a.tail
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
head, category = b.binder, ""
tail = append(append([]string{splitArm(b.ref, nil).head}, b.tail...), a.tail...)
case a.kind == namedRead:
ref := b.binder.link.refs[b.ref]
head, category = b.head, ref.head[1:]
tail = append(ref.tail[:len(ref.tail):len(ref.tail)], a.tail...)
case a.kind == namedRead || !isRef(a.head):
case !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]]}
t.link.readsColumn = &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
}
return nil
}
+2 -2
View File
@@ -168,8 +168,8 @@ func readField(s *generatorState, t *template, sc renderScope, a arm) readValue
return r
}
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one
// reference alone whose read drew null.
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column that only
// reads one reference or name whose read drew null.
func renderLeaf(s *generatorState, n node, sc renderScope) readValue {
n = resolveChoice(s, n)
switch leaf := n.(type) {
+2 -2
View File
@@ -27,8 +27,8 @@ type renderedRow struct {
index int
}
// at is the scope n, the leaf of a path that pinned its rows in pins, renders in: where n is a
// table's row or column, at the row pins holds for that table.
// at returns the scope n renders in, n being the leaf of a path whose rows sit in pins; where n
// is a table's row or column, the scope carries the row pins holds for that table.
func (sc renderScope) at(n node, pins *pinSet) renderScope {
switch n := n.(type) {
case *tableRow:
+7 -7
View File
@@ -72,8 +72,8 @@
| 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, 15, 14 | 25.9 |
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
| 34 | 0.1.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 6 | 7 | 4 | 20.9 |
| 96 | 0.1.0 | question | **Refuse two reads of one sibling, one category or one struct category by different spellings, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel after the draw restructure, and file what it names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 96 | 0.1.0 | question | **Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel after every `{…}` draws afresh and the code is restructured, and file what it names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 91 | 0.1.0 | | **Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.** | 3 | 4 | 6 | 7 | 4.1, 5.3 | 19.9 |
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
| 90 | 0.1.0 | decision | **Restructure the code into folders named for what they hold: the public API stays in the root package, and the implementation moves into `internal/` packages.** | 6 | 9 | 3 | 9 | 3.2, 3.4 | 13.9 |
@@ -234,13 +234,13 @@ Nothing has shipped, so "no longer paths", "where it used to fail" and "where it
`isin`: Luhn over letters expanded to digits. `aba`: 3-7-1 weights. `vin`: position 9 over the whole; a sample taking the WMI, since the check sits mid-string.
### 96. Refuse two reads of one sibling, one category or one struct category by different spellings, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.
### 96. Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.
Asked of the maintainer by the product-owner pass of item 4's chunk. Since every `{…}` draws afresh, `{net} x {qty} = {calc(net * qty, 2)}`, `{place.postal-code} {place.locality}` and struct tags `sv_SE.address.street` beside `sv_SE.address.postal-code` load and may render a pair that disagrees, and nothing tells the author to bind a name (goal 4.1). Goal 5.6 asks such a template to fail, showing `{place as p}{p.postal-code} {p.locality}` for one place and two names for two. Item 4 planned the draw checks as names, paths through names, cycles and the repeat cap, so the fence would revise the decision "Every `{…}` draws afresh, and only a name keeps a pick". `{w} {w}`, one spelling twice, stays two draws. Open: whether reads across categories, `{/city.name}, {/country.name}`, count.
Asked of the maintainer by the product-owner pass of the chunk that made every `{…}` draw afresh. Since every `{…}` draws afresh, `{net} x {qty} = {calc(net * qty, 2)}`, `{place.postal-code} {place.locality}` and struct tags `sv_SE.address.street` beside `sv_SE.address.postal-code` load and may render a pair that disagrees, and nothing tells the author to bind a name (goal 4.1). Goal 5.6 asks such a template to fail, showing `{place as p}{p.postal-code} {p.locality}` for one place and two names for two. The decision "Every `{…}` draws afresh, and only a name keeps a pick" lists the draw checks as names, paths through names, cycles and the repeat cap, so this fence revises it. `{w} {w}`, one spelling twice, stays two draws. Open: whether reads across categories, `{/city.name}, {/country.name}`, count.
### 5. Run the nine-seat comprehension panel after the draw restructure, and file what it names.
### 5. Run the nine-seat comprehension panel after every `{…}` draws afresh and the code is restructured, and file what it names.
Needs item 90. The decision "comprehension floor: every dimension and the overall at 7.0 or above; the draw restructure and item 90 ship with no scoring run, and item 5 scores them" suspends every scoring run and panel until then.
Needs item 90. The decision "comprehension floor: every dimension and the overall at 7.0 or above; the change making every `{…}` draw afresh and item 90 ship with no scoring run, and item 5 scores them" suspends every scoring run and panel until then.
### 91. Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.
@@ -301,7 +301,7 @@ With several `--data-path` layers the author has to work out which directory won
### 28. Accept a middle name, and draw a `personnummer` whose sex matches a sex selected through a named pick.
Today `personnummer` draws its own sex, so a person bound to `{/sv_SE.sex[f] as s}` needs the template the README's Data section shows to agree with a number.
Today `personnummer` draws its own sex, so a name and a number agree only through the README Data section's template, which reads both through one bound `sex`.
### 24. Stop `misc.territory[EH].tld` rendering `.eh`, the one shipped TLD `misc.tld` does not hold.
+1 -1
View File
@@ -29,7 +29,7 @@ type valueProof struct {
// checkDatatype rejects a typed column item some render of which is not text of its datatype,
// and one restating the datatype of the column it is.
// docs/decisions.md#a-column-of-one-reference-alone-is-the-column-it-reads
// docs/decisions.md#a-column-that-only-reads-one-reference-or-name-is-the-column-it-reads
func (p *valueProof) checkDatatype(path string, n node) error {
t, ok := n.(*template)
if !ok || t.datatype == DataTypeString {