Keep a name's frame with the read entering its category, refuse a binding in a choice's item, and read a name as a transform operand

This commit is contained in:
2026-10-03 02:05:02 +02:00
committed by Lillem4n
parent 40e7ab46f6
commit d0c477ec78
11 changed files with 198 additions and 130 deletions
+6 -5
View File
@@ -11,11 +11,12 @@ replacement, and each removed path, column or flag.
- A name keeps one pick: `{/misc.territory as t}` binds one row of `misc.territory` to `t` - A name keeps one pick: `{/misc.territory as t}` binds one row of `misc.territory` to `t`
and prints nothing, and `{t}`, `{t.capital}` and `{t.calling-code}` read that one row. and prints nothing, and `{t}`, `{t.capital}` and `{t.calling-code}` read that one row.
A name lives in the category binding it, so every field and record column of the A name lives in the category binding it, so every field and record column of the
category reads one pick, and a name a `repeat` binds picks again on every iteration. category reads one pick, a transform reads it as `{lowercase(t.name)}`, and a name a
Refused at `New`: a name bound twice in one category, or inside a `repeat` and outside `repeat` binds picks again on every iteration. Refused at `New`: a name bound twice in
it; a name that is a field or an option; a binding of anything but a reference; a one category, or inside a `repeat` and outside it; a name that is a field or an option;
binding nothing reads; a path through a name that selects a row; and a binding in a a binding of anything but a reference; a binding in a choice's item; a binding nothing
table. A field, column, category or folder name may not contain ` as `. reads; a path through a name that selects a row; and a binding in a table. A field,
column, category or folder name may not contain ` as `.
- 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,
+9 -6
View File
@@ -656,7 +656,7 @@ fail, except in a [typed column](#datatype), which must prove neither happens.
### Transforms ### Transforms
`{lowercase(x)}`, `{uppercase(x)}` and `{ascii(x)}` rewrite the value of `x` — a `{lowercase(x)}`, `{uppercase(x)}` and `{ascii(x)}` rewrite the value of `x` — a
field, a path or a `..path` — and nest. `ascii` folds Latin letters (`Åsa Öberg` 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` is held → `Asa Oberg`) and drops any other non-ASCII rune. `x` is held
([One draw, one spelling](#one-draw-one-spelling)), so an email built from a name ([One draw, one spelling](#one-draw-one-spelling)), so an email built from a name
matches the name beside it: matches the name beside it:
@@ -709,17 +709,20 @@ row:
``` ```
Renders e.g. `🇳🇴 Norway: capital Oslo, calling code +47`. Everything under a name is drawn Renders e.g. `🇳🇴 Norway: capital Oslo, calling code +47`. Everything under a name is drawn
once, so `{n}` and `{n.path}` agree, and a field `n` renders twice prints the same text twice. once, so `{n}` and `{n.path}` agree, a [transform](#transforms) of `n.path` reads the same
value, and a field `n` renders twice prints the same text twice. A name is a pick of its own:
`{t.name}` and `{/misc.territory.name}` beside it are two draws.
A name is drawn on its first read, and lives in the category binding it: any field of the A name is drawn on its first read, and lives in the category binding it: any field of the
category reads it, and a record's columns read one pick. Each render of the category picks category reads it, and a record's columns read one pick. Each render of the category picks
anew, and so does each bare reference to it. A name a `repeat` binds picks again on every anew, and so does each bare reference to it. A name a `repeat` binds picks again on every
iteration, and one bound outside the repeat keeps its pick on every line. iteration, and one bound outside the repeat keeps its pick on every line.
Rejected at load: a name bound twice in one category, or both inside a `repeat` and outside Rejected at load: a name bound twice in one category, or both inside a `repeat` and outside
it; a name that is a field or an option; a binding of anything but a reference; a binding it; a name that is a field or an option; a binding of anything but a reference; a binding in
nothing reads; a path through a name that selects a row, which the reference selects instead; a choice's item, which every other item would leave unbound; a binding nothing reads; a path
and a binding in a table's format or cell. A field, column, category or folder name may not through a name that selects a row, which the reference selects instead; and a binding in a
contain ` as `. table's format or cell. A field, column, category or folder name may not contain ` as `.
### Draw group ### Draw group
+10 -7
View File
@@ -894,10 +894,13 @@ a struct's fields as one.
2026-10-03, larv-review. Goals 5.3 and 5.6. 2026-10-03, larv-review. Goals 5.3 and 5.6.
`bindNames` scopes a name at compile, and a render keeps one `pickFrame` per scope. One `bindNames` scopes a name at compile, and a render keeps one `pickFrame` per scope. One
field binds a name where another reads it, `"first": "{/person as p}{p.first}"` beside field binds a name where another reads it, `"first": "{/person as p}{p.first}"` beside
`"last": "{p.last}"`, so the scope cannot be the template holding the binding; the category is the smallest unit holding every field and record column. The `"last": "{p.last}"`, so the scope cannot be the template holding the binding; the category
scope is lexical: a category referencing another never sees its names, so what a category is the smallest unit holding every field and record column. A choice's item binds no name,
renders never depends on who references it. A pick is drawn on its first read, so a record's since a read outside the item would see a binding another item leaves out. The scope is
columns, rendered in name order, read one pick whichever binds it. A read entering a category lexical: a read entering a category, by a reference or through a name, sees none of the
past its root, `Fake("cat.field")` or `{/cat.field}`, keeps the category's frame in the memo of frames rendering above it, so what a category renders never depends on who references it. A
that read: per render and group, as the read's own draw is. Valid while a name is read only pick is drawn on its first read, so a record's columns, rendered in name order, read one pick
inside the category binding it. whichever binds it. A read entering a category keeps the frame of each scope it enters in its
own memo, and the category's root renders that frame when the read lands on the root, so
`{n}` and `{n.path}` read one pick: per render and group for a reference path, per pick for a
path through a name, and per render for `Fake("cat.field")`.
+5 -1
View File
@@ -45,7 +45,11 @@ type renderScope struct {
draws *renderDraws draws *renderDraws
group string group string
row renderedRow row renderedRow
entry *drawMemo // the memo of the read that entered what renders // entry is the memo of the read that entered the category rendering, nil where none did;
// base, the depth of the frame stack when it entered, below which no frame is the
// category's.
entry *drawMemo
base int
// pick is the named pick what renders is part of, at pickKey, its path from the name; nil // pick is the named pick what renders is part of, at pickKey, its path from the name; nil
// outside one. // outside one.
pick *namedPick pick *namedPick
+13 -3
View File
@@ -247,7 +247,12 @@ func checkTokens(toks []formatToken, fields map[string]node) ([]unboundRead, err
func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) { func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
switch t.kind { switch t.kind {
case builtinCall: case builtinCall:
return nil, checkFunc(t, fields) err := checkFunc(t, fields)
var miss *fieldMiss
if errors.As(err, &miss) {
return []unboundRead{{miss.head, err}}, nil
}
return nil, err
case nameBind: case nameBind:
return nil, checkBind(t) return nil, checkBind(t)
case nameRead: case nameRead:
@@ -493,10 +498,15 @@ func (c *formatOps) holdName(a arm, label string) {
c.held[a.head] = h c.held[a.head] = h
} }
func (c *formatOps) function(tok formatToken, refs map[string]refBinding) { func (c *formatOps) function(tok formatToken, refs map[string]refBinding, isName func(string) bool) {
var operands []arm var operands []arm
for _, operand := range tok.names { for _, operand := range tok.names {
a := splitArm(operand, refs) a := splitArm(operand, refs)
if isName(a.head) {
a.kind = namedRead
operands = append(operands, a)
continue
}
c.holdName(a, fmt.Sprintf("%s operand %q", tok.fn, operand)) c.holdName(a, fmt.Sprintf("%s operand %q", tok.fn, operand))
operands = append(operands, a) operands = append(operands, a)
} }
@@ -528,7 +538,7 @@ func compileFormat(toks []formatToken, refs map[string]refBinding, isName func(h
c.grow += len(tok.lit) c.grow += len(tok.lit)
c.ops = append(c.ops, op{formatToken: tok}) c.ops = append(c.ops, op{formatToken: tok})
case builtinCall: case builtinCall:
c.function(tok, refs) c.function(tok, refs, isName)
case nameRead: case nameRead:
c.field(tok, refs, isName) c.field(tok, refs, isName)
} }
+113 -73
View File
@@ -40,34 +40,38 @@ func (sc *nameScope) lookup(name string) *nameBinding {
} }
// bindNames gives every template of a compiled category or inline template the scope its // bindNames gives every template of a compiled category or inline template the scope its
// names live in, and refuses a name bound twice along one chain of scopes, a name a field // names live in, and refuses a name bound twice along one chain of scopes, a binding in a
// spells too, a read no field or name answers, and a binding nothing reads. // choice's item, a name a field spells too, a read no field or name answers, and a binding
// nothing reads.
func bindNames(root node) error { func bindNames(root node) error {
var scopes []*nameScope top := &nameScope{owner: root}
var gather func(n node, scope *nameScope, where string) error scopes := []*nameScope{top}
gather = func(n node, scope *nameScope, where string) error { var gather func(n node, scope *nameScope, inChoice bool, where string) error
gather = func(n node, scope *nameScope, inChoice bool, where string) error {
if t, isTemplate := n.(*template); isTemplate { if t, isTemplate := n.(*template); isTemplate {
if t.repeat > 1 && n != root { if t.repeat > 1 && n != root {
scope = &nameScope{up: scope, owner: t} scope = &nameScope{up: scope, owner: t}
scopes = append(scopes, scope) scopes = append(scopes, scope)
inChoice = false
} }
t.scope = scope t.scope = scope
for _, tok := range t.tokens { if err := scope.bindAll(t, inChoice, where); err != nil {
if tok.kind != nameBind { return err
continue
}
if err := scope.bind(tok, t, where); err != nil {
return err
}
} }
} }
return eachContained(n, where, func(c node, where string) error { return gather(c, scope, where) }) _, isChoice := n.(*choice)
return eachContained(n, where, func(c node, where string) error { return gather(c, scope, inChoice || isChoice, where) })
} }
top := &nameScope{owner: root} if err := gather(root, top, false, ""); err != nil {
scopes = append(scopes, top)
if err := gather(root, top, ""); err != nil {
return err return err
} }
for _, sc := range scopes[1:] {
for _, b := range sc.order {
if outer := sc.up.lookup(b.name); outer != nil {
return fmt.Errorf("%stoken {%s}: name %q is bound outside this repeat already, by {%s}; rename one", b.where, b.body, b.name, outer.body)
}
}
}
if err := resolveReads(root, ""); err != nil { if err := resolveReads(root, ""); err != nil {
return err return err
} }
@@ -79,6 +83,37 @@ func bindNames(root node) error {
return nil return nil
} }
// bindAll binds every name t's tokens bind. A choice's item binds none: a read outside the item
// would find the name bound by an item that may never be drawn.
func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
for _, tok := range t.tokens {
if tok.kind != nameBind {
continue
}
if inChoice {
return fmt.Errorf("%stoken {%s}: a choice's item binds no name, since every other item would leave it unbound; bind it outside the choice", where, tok.body)
}
if b, twice := sc.bindings[tok.bound]; twice {
return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.body, tok.bound, sc.spelled(), b.body)
}
b := &nameBinding{name: tok.bound, ref: tok.names[0], body: tok.body, where: where, scope: sc, index: len(sc.order), binder: t}
if sc.bindings == nil {
sc.bindings = map[string]*nameBinding{}
}
sc.bindings[b.name] = b
sc.order = append(sc.order, b)
}
return nil
}
// spelled names the scope in an error.
func (sc *nameScope) spelled() string {
if sc.up == nil {
return "outside any repeat"
}
return "in one repeat"
}
// close refuses a binding nothing reads, and gives the owner of a scope binding names the scope. // close refuses a binding nothing reads, and gives the owner of a scope binding names the scope.
func (sc *nameScope) close() error { func (sc *nameScope) close() error {
for _, b := range sc.order { for _, b := range sc.order {
@@ -86,14 +121,8 @@ func (sc *nameScope) close() error {
return fmt.Errorf("%stoken {%s}: nothing reads name %q; drop the token", b.where, b.body, b.name) return fmt.Errorf("%stoken {%s}: nothing reads name %q; drop the token", b.where, b.body, b.name)
} }
} }
if len(sc.order) == 0 { if t, isTemplate := sc.owner.(*template); isTemplate && len(sc.order) > 0 {
return nil t.ownScope = sc
}
switch o := sc.owner.(type) {
case *template:
o.ownScope = sc
case *choice:
o.ownScope = sc
} }
return nil return nil
} }
@@ -113,22 +142,6 @@ func eachContained(n node, where string, fn func(c node, where string) error) er
return nil return nil
} }
func (sc *nameScope) bind(tok formatToken, binder *template, where string) error {
if b, twice := sc.bindings[tok.bound]; twice {
return fmt.Errorf("%stoken {%s}: name %q is bound twice in one scope, by {%s} too; rename one", where, tok.body, tok.bound, b.body)
}
if b := sc.up.lookup(tok.bound); b != nil {
return fmt.Errorf("%stoken {%s}: name %q is bound outside this repeat already, by {%s}; rename one", where, tok.body, tok.bound, b.body)
}
b := &nameBinding{name: tok.bound, ref: tok.names[0], body: tok.body, where: where, scope: sc, index: len(sc.order), binder: binder}
if sc.bindings == nil {
sc.bindings = map[string]*nameBinding{}
}
sc.bindings[b.name] = b
sc.order = append(sc.order, b)
return nil
}
// resolveReads answers each unbound read under n with a name its template sees, fields before the // resolveReads answers each unbound read under n with a name its template sees, fields before the
// format as compile reports them, and refuses a field spelling a name. // format as compile reports them, and refuses a field spelling a name.
func resolveReads(n node, where string) error { func resolveReads(n node, where string) error {
@@ -141,7 +154,11 @@ func resolveReads(n node, where string) error {
} }
for _, name := range sortedNames(t.fields) { for _, name := range sortedNames(t.fields) {
if b := t.scope.lookup(name); b != nil { if b := t.scope.lookup(name); b != nil {
return fmt.Errorf("%stoken {%s}: name %q is a field too; options, fields and names share one namespace, so rename one", b.where, b.body, name) holder := "the root template"
if where != "" {
holder = strings.TrimSuffix(where, ": ")
}
return fmt.Errorf("%stoken {%s}: name %q is a field of %s too; options, fields and names share one namespace, so rename one", b.where, b.body, name, holder)
} }
} }
for _, u := range t.unbound { for _, u := range t.unbound {
@@ -163,13 +180,15 @@ func (t *template) isName(head string) bool {
// every template is linked, so the binder has resolved that reference. // every template is linked, so the binder has resolved that reference.
func linkNames(path string, t *template) error { func linkNames(path string, t *template) error {
for i := range t.compiled.ops { for i := range t.compiled.ops {
for j := range t.compiled.ops[i].arms { o := &t.compiled.ops[i]
a := &t.compiled.ops[i].arms[j] for _, reads := range [][]arm{o.arms, o.operands} {
if a.kind != namedRead { for j := range reads {
continue if reads[j].kind != namedRead {
} continue
if err := linkName(t, a); err != nil { }
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), a.spelling, err) if err := linkName(t, &reads[j]); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
}
} }
} }
} }
@@ -223,23 +242,58 @@ type frameStack struct {
frames []*pickFrame frames []*pickFrame
} }
// pushFrame opens a frame of scope until popFrame closes it, returning the mark popFrame takes. // pushFrame opens f until popFrame closes it, returning the mark popFrame takes.
func (d *renderDraws) pushFrame(scope *nameScope) int { func (d *renderDraws) pushFrame(f *pickFrame) int {
mark := d.depth()
if d.frames == nil { if d.frames == nil {
d.frames = &frameStack{} d.frames = &frameStack{}
} }
mark := len(d.frames.frames) d.frames.frames = append(d.frames.frames, f)
d.frames.frames = append(d.frames.frames, newPickFrame(scope))
return mark return mark
} }
func (d *renderDraws) popFrame(mark int) { d.frames.frames = d.frames.frames[:mark] } func (d *renderDraws) popFrame(mark int) { d.frames.frames = d.frames.frames[:mark] }
// pickOf is b's pick in the frame rendering its scope. A read entering the scope past its owner, func (d *renderDraws) depth() int {
// as Fake("cat.field") or {/cat.field} do, keeps the frame in the memo of that read. if d.frames == nil {
return 0
}
return len(d.frames.frames)
}
// entering is sc as a read entering another category sees it: none of the frames rendering, and
// memo, which keeps the frame of each scope the read enters past its owner.
func (sc renderScope) entering(memo *drawMemo) renderScope {
sc.base, sc.entry, sc.pick = sc.draws.depth(), memo, nil
return sc
}
// frameOf is the frame a render of scope's owner opens: the one the read entering the category
// keeps, so {n} and {n.path} read one frame, else a fresh one.
func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
if sc.entry == nil {
return newPickFrame(scope)
}
return sc.entry.frameOf(scope)
}
func (m *drawMemo) frameOf(scope *nameScope) *pickFrame {
f, ok := m.frames[scope]
if !ok {
f = newPickFrame(scope)
if m.frames == nil {
m.frames = map[*nameScope]*pickFrame{}
}
m.frames[scope] = f
}
return f
}
// pickOf is b's pick: in the frame rendering its scope since the read entering the category, else
// in the frame that read keeps, as for Fake("cat.field") or {/cat.field}.
func (sc renderScope) pickOf(b *nameBinding) *namedPick { func (sc renderScope) pickOf(b *nameBinding) *namedPick {
if stack := sc.draws.frames; stack != nil { if stack := sc.draws.frames; stack != nil {
for i := len(stack.frames) - 1; i >= 0; i-- { for i := len(stack.frames) - 1; i >= sc.base; i-- {
if f := stack.frames[i]; f.scope == b.scope { if f := stack.frames[i]; f.scope == b.scope {
return &f.picks[b.index] return &f.picks[b.index]
} }
@@ -249,19 +303,12 @@ func (sc renderScope) pickOf(b *nameBinding) *namedPick {
if memo == nil { if memo == nil {
memo = &sc.groupDraws().memo memo = &sc.groupDraws().memo
} }
f, ok := memo.frames[b.scope] return &memo.frameOf(b.scope).picks[b.index]
if !ok {
f = newPickFrame(b.scope)
if memo.frames == nil {
memo.frames = map[*nameScope]*pickFrame{}
}
memo.frames[b.scope] = f
}
return &f.picks[b.index]
} }
func readName(s *generatorState, sc renderScope, a arm) readValue { func readName(s *generatorState, sc renderScope, a arm) readValue {
return readPicked(s, sc.pickOf(a.bind), a.bind.head, a.steps, a.levels, a.path, sc) p := sc.pickOf(a.bind)
return readPicked(s, p, a.bind.head, a.steps, a.levels, a.path, sc.entering(&p.memo))
} }
// drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one. // drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one.
@@ -297,19 +344,12 @@ func readPicked(s *generatorState, p *namedPick, head node, steps []pathStep, le
return r return r
} }
leaf := drawSteps(s, head, steps, &p.pins, &p.memo, levels) leaf := drawSteps(s, head, steps, &p.pins, &p.memo, levels)
mark := -1
if c, isChoice := leaf.(*choice); isChoice { if c, isChoice := leaf.(*choice); isChoice {
if c.ownScope != nil {
mark = sc.draws.pushFrame(c.ownScope)
}
leaf = p.memo.variantOf(s, c, key) leaf = p.memo.variantOf(s, c, key)
} }
sc = sc.at(leaf, &p.pins) sc = sc.at(leaf, &p.pins)
sc.pick, sc.pickKey, sc.entry = p, key, &p.memo sc.pick, sc.pickKey = p, key
r := renderLeaf(s, leaf, sc) r := renderLeaf(s, leaf, sc)
if mark >= 0 {
sc.draws.popFrame(mark)
}
if p.memo.value == nil { if p.memo.value == nil {
p.memo.value = map[string]readValue{} p.memo.value = map[string]readValue{}
} }
+3 -4
View File
@@ -25,10 +25,9 @@ func (*folder) isNode() {}
// relative dot paths every item can address, so carriedByAll and List both read the one // relative dot paths every item can address, so carriedByAll and List both read the one
// answer to what a path may reach through this choice. // answer to what a path may reach through this choice.
type choice struct { type choice struct {
items []node items []node
cum []float64 cum []float64
shared map[string]bool shared map[string]bool
ownScope *nameScope // set on a category's root binding names, by bindNames
} }
func (*choice) isNode() {} func (*choice) isNode() {}
+1 -2
View File
@@ -264,8 +264,7 @@ func recordOf(n node) (*template, []Column, error) {
func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record { func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record {
sc = sc.in(t) sc = sc.in(t)
if t.ownScope != nil { if t.ownScope != nil {
mark := sc.draws.pushFrame(t.ownScope) defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(t.ownScope)))
defer sc.draws.popFrame(mark)
} }
if t.site.isFormat() { if t.site.isFormat() {
sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins) sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins)
+3 -1
View File
@@ -100,7 +100,9 @@ func compileArms(t *template) {
for i := range t.compiled.ops { for i := range t.compiled.ops {
o := &t.compiled.ops[i] o := &t.compiled.ops[i]
for j := range o.operands { for j := range o.operands {
compileArm(t, &o.operands[j]) if o.operands[j].kind != namedRead {
compileArm(t, &o.operands[j])
}
} }
for j := range o.arms { for j := range o.arms {
if o.arms[j].kind != namedRead { if o.arms[j].kind != namedRead {
+28 -26
View File
@@ -61,10 +61,6 @@ func renderOnce(s *generatorState, n node) string {
func render(s *generatorState, n node, sc renderScope) string { func render(s *generatorState, n node, sc renderScope) string {
switch n := n.(type) { switch n := n.(type) {
case *choice: case *choice:
if n.ownScope != nil {
mark := sc.draws.pushFrame(n.ownScope)
defer sc.draws.popFrame(mark)
}
return render(s, pick(s, n), sc) return render(s, pick(s, n), sc)
case *nullItem: case *nullItem:
return "" return ""
@@ -83,38 +79,48 @@ func render(s *generatorState, n node, sc renderScope) string {
sc = sc.in(n) sc = sc.in(n)
if n.repeat == 1 { if n.repeat == 1 {
if n.ownScope != nil { if n.ownScope != nil {
mark := sc.draws.pushFrame(n.ownScope) defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(n.ownScope)))
defer sc.draws.popFrame(mark)
} }
if lit, fixed := n.fixedText(); fixed { if lit, fixed := n.fixedText(); fixed {
return lit return lit
} }
return expand(s, n, sc) return expand(s, n, sc)
} }
var b strings.Builder return renderRepeat(s, n, sc)
b.Grow(n.repeat * (n.compiled.grow + len(n.separator)))
for i := 0; i < n.repeat; i++ {
if i > 0 {
b.WriteString(n.separator)
}
b.WriteString(expandAnew(s, n, sc.draws.frames))
}
return b.String()
default: default:
panic(internalError("uncompiled node %T", n)) panic(internalError("uncompiled node %T", n))
} }
} }
// renderRepeat renders each iteration of t as a render of its own, inside the name scopes
// rendering t, where a name bound outside t keeps its pick: a scope sc's category was entered
// past keeps its frame in that read's memo, else in sc's render's.
func renderRepeat(s *generatorState, t *template, sc renderScope) string {
entry := sc.entry
if entry == nil {
entry = &sc.groupDraws().memo
}
var b strings.Builder
b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
for i := 0; i < t.repeat; i++ {
if i > 0 {
b.WriteString(t.separator)
}
b.WriteString(expandAnew(s, t, sc.draws.frames, sc.base, entry))
}
return b.String()
}
// expandAnew expands one repeat iteration of t as a render of its own, in no group, inside the // expandAnew expands one repeat iteration of t as a render of its own, in no group, inside the
// name scopes of frames. Inlined into render's loop, its draws would move to the heap. // name scopes of frames from base. Inlined into render's loop, its draws would move to the heap.
// //
//go:noinline //go:noinline
func expandAnew(s *generatorState, t *template, frames *frameStack) string { func expandAnew(s *generatorState, t *template, frames *frameStack, base int, entry *drawMemo) string {
draws := renderDraws{frames: frames} draws := renderDraws{frames: frames}
if t.ownScope != nil { if t.ownScope != nil {
defer draws.popFrame(draws.pushFrame(t.ownScope)) defer draws.popFrame(draws.pushFrame(newPickFrame(t.ownScope)))
} }
return expand(s, t, renderScope{draws: &draws}) return expand(s, t, renderScope{draws: &draws, base: base, entry: entry})
} }
// pick selects one item. Uniform choices are O(1); weighted choices are an // pick selects one item. Uniform choices are O(1); weighted choices are an
@@ -180,8 +186,9 @@ func readField(s *generatorState, t *template, hold *drawMemo, sc renderScope, a
return readName(s, sc, a) return readName(s, sc, a)
case sc.pick != nil && !isRef(a.head): case sc.pick != nil && !isRef(a.head):
return readUnder(s, t, sc, a) return readUnder(s, t, sc, a)
case a.kind == freshRead && isRef(a.head):
return readValue{text: render(s, t.head(a.head), sc.entering(nil))}
case a.kind == freshRead: case a.kind == freshRead:
sc.pick = nil
return readValue{text: render(s, t.head(a.head), sc)} return readValue{text: render(s, t.head(a.head), sc)}
} }
return readMemo(s, t, hold, nil, sc, a) return readMemo(s, t, hold, nil, sc, a)
@@ -203,9 +210,8 @@ func readMemo(s *generatorState, t *template, memo *drawMemo, pins *pinSet, sc r
} }
leaf := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels) leaf := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels)
if pins != nil { if pins != nil {
sc = sc.at(leaf, pins) sc = sc.at(leaf, pins).entering(memo)
} }
sc.pick, sc.entry = nil, memo
r := renderLeaf(s, leaf, sc) r := renderLeaf(s, leaf, sc)
if memo.value == nil { if memo.value == nil {
memo.value = map[string]readValue{} memo.value = map[string]readValue{}
@@ -228,10 +234,6 @@ func traceRead(trace renderTrace, t *template, sc renderScope, a arm) {
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one // 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. // reference alone whose read drew null.
func renderLeaf(s *generatorState, n node, sc renderScope) readValue { func renderLeaf(s *generatorState, n node, sc renderScope) readValue {
if c, isChoice := n.(*choice); isChoice && c.ownScope != nil {
mark := sc.draws.pushFrame(c.ownScope)
defer sc.draws.popFrame(mark)
}
n = resolveChoice(s, n) n = resolveChoice(s, n)
switch leaf := n.(type) { switch leaf := n.(type) {
case *nullItem: case *nullItem:
+7 -2
View File
@@ -6,7 +6,7 @@
`Bar = 9` `Bar = 9`
`Next ID = 71` `Next ID = 72`
| Goal | W | | Goal | W |
|---|---| |---|---|
@@ -65,6 +65,7 @@
| 12 | 0.1.0 | defect | **Strip the whitespace `data-import/territory.py` copies from its register.** | 1 | 1 | 5 | 7 | 4 | 20.6 | | 12 | 0.1.0 | defect | **Strip the whitespace `data-import/territory.py` copies from its register.** | 1 | 1 | 5 | 7 | 4 | 20.6 |
| 5 | 0.1.0 | | **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 | | **Run the nine-seat comprehension panel after the draw restructure, and file what it names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 6 | 0.1.0 | question | **Decide how the comprehension gate treats panel noise: identical code scored 5.7 and 5.4.** | 1 | 1 | 3 | 8 | 3 | 19.1 | | 6 | 0.1.0 | question | **Decide how the comprehension gate treats panel noise: identical code scored 5.7 and 5.4.** | 1 | 1 | 3 | 8 | 3 | 19.1 |
| 71 | 0.1.0 | | **Accept a name as a `calc` operand, so `{calc(n * 2)}` computes from the pick `{n}` prints.** | 3 | 4 | 5 | 7 | 5.3 | 17.2 |
| 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 | | 58 | 0.2.0 | | **Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.** | 3 | 7 | 8 | 9 | 1.1, 7 | 29.2 |
| 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 | | 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 |
| 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.7 | 19.1 | | 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.7 | 19.1 |
@@ -194,7 +195,7 @@ Shape: T = table, t = template, c = choice.
### 4. Draw every `{…}` afresh, keep a pick only by name, and delete `drawGroup` with the fences that held picks implicitly. ### 4. Draw every `{…}` afresh, keep a pick only by name, and delete `drawGroup` with the fences that held picks implicitly.
- Needs items 2 and 3. After it, `{/person} ({/person.first})` renders two people, `{w} {uppercase(w)}` renders `b G` where `w` is a choice of letters, and `{/city.name}, {/country[SE].name}` and `{/country[SE].name} / {/country[NO].name}` both load. - Needs items 2, 3 and 71. After it, `{/person} ({/person.first})` renders two people, `{w} {uppercase(w)}` renders `b G` where `w` is a choice of letters, and `{/city.name}, {/country[SE].name}` and `{/country[SE].name} / {/country[NO].name}` both load.
- Deletes the token-order rules (`drawsApart`, `readFold`, `branches`, the pairwise replay), the expansion hold fence, `drawGroup`, the own-family fence and the cell-reference fences, and revises the decisions that point here. The remaining draw checks are names, paths through names, cycles and the repeat cap. - Deletes the token-order rules (`drawsApart`, `readFold`, `branches`, the pairwise replay), the expansion hold fence, `drawGroup`, the own-family fence and the cell-reference fences, and revises the decisions that point here. The remaining draw checks are names, paths through names, cycles and the repeat cap.
- Fixes on the way: `{/sel}|{/sel}` panics out of `Fake` today where two draws land on rows whose cells select different rows of another table; it must render. - Fixes on the way: `{/sel}|{/sel}` panics out of `Fake` today where two draws land on rows whose cells select different rows of another table; it must render.
- Rewrites the README's References, Draw group, Correlated fields, Linked tables and "One draw, one spelling" sections; the Linked tables section says which pick's weights govern a family, since the bound row decides. - Rewrites the README's References, Draw group, Correlated fields, Linked tables and "One draw, one spelling" sections; the Linked tables section says which pick's weights govern a family, since the bound row decides.
@@ -251,6 +252,10 @@ Two nine-seat runs on `5cb919e`, same briefs, same model, 2026-09-30:
Both runs named the same hardest units, `drawsApart` and `readFold.reads`. Run 1 is the recorded score; run 2 reran the same code to measure noise. A spread of 0.3 is close to the whole 5.7–6.1 range the recorded scores span since 2026-09-20, so `AGENTS.md`'s "no merge lowers the last score" can block or pass a chunk by chance. Until this item is decided, that rule compares against 5.7, from 2026-09-30. Both runs named the same hardest units, `drawsApart` and `readFold.reads`. Run 1 is the recorded score; run 2 reran the same code to measure noise. A spread of 0.3 is close to the whole 5.7–6.1 range the recorded scores span since 2026-09-20, so `AGENTS.md`'s "no merge lowers the last score" can block or pass a chunk by chance. Until this item is decided, that rule compares against 5.7, from 2026-09-30.
### 71. Accept a name as a `calc` operand, so `{calc(n * 2)}` computes from the pick `{n}` prints.
`checkCalc` and the value proof read an operand as a sibling field, so `{calc(n * 2)}` is refused with `no field "n"` today. Item 4 stops an operand holding its field for the expansion, after which a name is the one way to show the operand a calc computes from, as the README's Computation example does.
### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go. ### 58. Ship prebuilt binaries, a container image, and packages for Homebrew, Scoop and the biggest Linux package managers, so the CLI needs no Go.
GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. A checkout build prints `devel` for `--version`; the binaries carry the stamped tag. v0.1.0 ships first, so it can be tested before it is packaged. GoReleaser attaches the binaries to the release the tag workflow publishes, and builds deb and rpm packages, an Alpine APK and an AUR package beside Homebrew and Scoop. A checkout build prints `devel` for `--version`; the binaries carry the stamped tag. v0.1.0 ships first, so it can be tested before it is packaged.