Answer stability round 1 and product-owner round 1: refuse a field binding read once from above its binder, word the self-read refusal plainly, say how a bound field's column keeps the pick, and ask item 96

This commit is contained in:
2026-10-05 09:54:38 +02:00
committed by Lillem4n
parent f4a05fbb36
commit 8d82759520
6 changed files with 42 additions and 13 deletions
+1 -3
View File
@@ -21,9 +21,7 @@ replacement, and each removed path, column or flag.
`{w} {uppercase(w)}` and `{/person.first} {/person.last}` are two draws each, which `{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}` `{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, 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 `{/sv_SE.person as p}{p.first}` beside `{p.sex}`.
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. - First release: the CLI, the library and the shipped data set.
- Table categories: a category JSON naming a `rows` TSV beside it, with the options - Table categories: a category JSON naming a `rows` TSV beside it, with the options
`key`, `name`, `weight` and `parent`; a path selects a row by key or name, `key`, `name`, `weight` and `parent`; a path selects a row by key or name,
+4 -3
View File
@@ -136,7 +136,9 @@ it as nothing.
A record is one render, so its columns read one pick of each [name](#names) its category 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 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. currency, while `{/currency.code}` and `{/currency.symbol}` in two columns may name two. A
field a name binds draws afresh as a column of its own, so write the column through the name,
`"where": "{p}"` beside `{place as p}`, for it to agree with the columns reading `p`.
A category never references itself: `{/users.first}` or a bare `{/users}` inside `users` 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 describes a draw other than the fields beside it. Read a sibling as a field, and put a
@@ -754,8 +756,7 @@ Refused at `New`, each error naming what to write instead:
and outside it; and outside it;
- a name that is a field or an option; - 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 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 - a read of a name inside the field it binds: read it outside the field;
field;
- a binding in a choice's item, which every other item would leave unbound; - 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, - 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 which the spelling it binds draws the same way: `{/word as w}{w}` is `{/word}`, and
+3 -1
View File
@@ -299,7 +299,9 @@ 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 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 `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), 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 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 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 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 already being filled is left alone, since filling it would never end. `New` cannot see a
+27 -5
View File
@@ -19,7 +19,7 @@ func checkNameReads(path string, t *template) error {
} }
return namedReads(t, func(o *op, a *arm) error { return namedReads(t, func(o *op, a *arm) error {
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) { 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, which would render itself; 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 it binds; read the name outside that field", t.site.label(path), o.body, a.named.name, a.named.ref)
} }
for _, leaf := range a.leaves { for _, leaf := range a.leaves {
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil { if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
@@ -58,15 +58,22 @@ func rendersInside(nodes []node, t *template) bool {
} }
// checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling // 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. Where the spelling // draws the same way without the name. A field stands where the reading template reaches the
// would be a CLI argument or tag of one reference alone, that entry point's own refusal then // binder through fields. Where the spelling would be a CLI argument or tag of one reference alone,
// names the bare path. // that entry point's own refusal then names the bare path.
func (b *nameBinding) checkUses() error { func (b *nameBinding) checkUses() error {
r := b.uses[0] r := b.uses[0]
if len(b.uses) > 1 || r.nested || b.bindsField() && r.in != b.binder { if len(b.uses) > 1 || r.nested {
return nil return nil
} }
spelling := b.ref spelling := b.ref
if b.bindsField() {
down, reaches := fieldPathTo(r.in, b.binder)
if !reaches {
return nil
}
spelling = joinSegments(append(down, b.ref))
}
switch { switch {
case strings.HasPrefix(r.tail, ".."): case strings.HasPrefix(r.tail, ".."):
spelling += r.tail spelling += r.tail
@@ -82,6 +89,21 @@ func (b *nameBinding) checkUses() error {
return fmt.Errorf("name %q is read once, so it keeps no pick for another read; write %s where it is read, and drop the token", b.name, spelling) return fmt.Errorf("name %q is read once, so it keeps no pick for another read; write %s where it is read, and drop the token", b.name, spelling)
} }
// fieldPathTo is the fields leading from t down to the template target, if t reaches it so.
func fieldPathTo(t, target *template) ([]string, bool) {
if t == target {
return nil, true
}
for _, name := range sortedNames(t.fields) {
if sub, isTemplate := t.fields[name].(*template); isTemplate {
if rest, reaches := fieldPathTo(sub, target); reaches {
return append([]string{name}, rest...), true
}
}
}
return nil, false
}
// calcReads reports whether a calc reads spelling as one operand. // calcReads reports whether a calc reads spelling as one operand.
func calcReads(spelling string) bool { func calcReads(spelling string) bool {
n, err := parseCalc(spelling) n, err := parseCalc(spelling)
+1
View File
@@ -120,6 +120,7 @@ func TestAFieldBindingIsRefusedWhereNoFieldIs(t *testing.T) {
`{"format":"{nope as p}{p}{p}","place":"x"}`: `no field "nope"`, `{"format":"{nope as p}{p}{p}","place":"x"}`: `no field "nope"`,
`{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`: `write {place.x} where it is read`, `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`: `write {place.x} where it is read`,
`{"format":"{place as p}{p}","place":["x","y"]}`: `write {place} where it is read`, `{"format":"{place as p}{p}","place":["x","y"]}`: `write {place} where it is read`,
`{"format":"{n}|{x}","x":{"format":"{a as n}{a}","a":["1","2"]}}`: `write {x.a} where it is read`,
`{"format":"{place as p}{calc(p * 2)}","place":["1","2"]}`: `write place where it is read`, `{"format":"{place as p}{calc(p * 2)}","place":["1","2"]}`: `write place where it is read`,
`{"format":"{place as place}{place}{place}","place":["x","y"]}`: `name "place" is a field of the root template too`, `{"format":"{place as place}{place}{place}","place":["x","y"]}`: `name "place" is a field of the root template too`,
} { } {
+6 -1
View File
@@ -6,7 +6,7 @@
`Bar = 9` `Bar = 9`
`Next ID = 96` `Next ID = 97`
| Goal | W | | Goal | W |
|---|---| |---|---|
@@ -72,6 +72,7 @@
| 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 | | 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 | | 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 | | 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 | | 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 |
| 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 | | 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 | | 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 |
@@ -233,6 +234,10 @@ 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. `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.
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.
### 5. Run the nine-seat comprehension panel after the draw restructure, and file what it names. ### 5. Run the nine-seat comprehension panel after the draw restructure, 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 draw restructure and item 90 ship with no scoring run, and item 5 scores them" suspends every scoring run and panel until then.