From f89ffd1b7335d389565f020121bc0fa89017dbe2 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 10:04:11 +0200 Subject: [PATCH] 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 --- AGENTS.md | 6 +++--- CHANGELOG.md | 16 ++++++++-------- README.md | 43 ++++++++++++++++++++++--------------------- datatype.go | 2 +- docs/decisions.md | 35 ++++++++++++++++++----------------- format.go | 2 +- name.go | 6 +++--- name_test.go | 6 +++--- namefence.go | 11 ++++++----- pick.go | 4 ++-- pick_test.go | 25 +++++++++++++++++++++++++ record.go | 5 ++++- reference.go | 21 +++++++++++++-------- render.go | 4 ++-- scope.go | 4 ++-- todo.md | 14 +++++++------- valueproof.go | 2 +- 17 files changed, 121 insertions(+), 85 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9b87031..70eee98 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f52532..fbe2b12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/README.md b/README.md index 01b21b5..d7b252e 100644 --- a/README.md +++ b/README.md @@ -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}`; diff --git a/datatype.go b/datatype.go index 79f4b45..4c3bdb1 100644 --- a/datatype.go +++ b/datatype.go @@ -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 } diff --git a/docs/decisions.md b/docs/decisions.md index a98d6f0..3cecbc0 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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. diff --git a/format.go b/format.go index 7d4b07f..7f6914f 100644 --- a/format.go +++ b/format.go @@ -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 } diff --git a/name.go b/name.go index 2958304..cf027c8 100644 --- a/name.go +++ b/name.go @@ -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 { diff --git a/name_test.go b/name_test.go index 1cc0eb8..db52443 100644 --- a/name_test.go +++ b/name_test.go @@ -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) { diff --git a/namefence.go b/namefence.go index 7dae4d3..14d7509 100644 --- a/namefence.go +++ b/namefence.go @@ -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] diff --git a/pick.go b/pick.go index 92d8c44..e844400 100644 --- a/pick.go +++ b/pick.go @@ -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 diff --git a/pick_test.go b/pick_test.go index 096d60e..a24d4b1 100644 --- a/pick_test.go +++ b/pick_test.go @@ -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) + } +} diff --git a/record.go b/record.go index 8d26e7a..41ad086 100644 --- a/record.go +++ b/record.go @@ -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 { diff --git a/reference.go b/reference.go index af1ecca..62947e0 100644 --- a/reference.go +++ b/reference.go @@ -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 } diff --git a/render.go b/render.go index 0a32091..607e94a 100644 --- a/render.go +++ b/render.go @@ -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) { diff --git a/scope.go b/scope.go index fe47556..0420b85 100644 --- a/scope.go +++ b/scope.go @@ -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: diff --git a/todo.md b/todo.md index b85d3a1..5cbf13f 100644 --- a/todo.md +++ b/todo.md @@ -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. diff --git a/valueproof.go b/valueproof.go index e4e89f9..d353ea0 100644 --- a/valueproof.go +++ b/valueproof.go @@ -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 {