Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
+6
-5
@@ -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`
|
||||
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
|
||||
category reads one pick, and a name a `repeat` binds picks again on every iteration.
|
||||
Refused at `New`: a name bound twice in one category, or inside a `repeat` and outside
|
||||
it; a name that is a field or an option; a binding of anything but a reference; a
|
||||
binding nothing 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 `.
|
||||
category reads one pick, a transform reads it as `{lowercase(t.name)}`, and a name a
|
||||
`repeat` binds picks again on every iteration. Refused at `New`: a name bound twice in
|
||||
one category, or inside a `repeat` and outside it; a name that is a field or an option;
|
||||
a binding of anything but a reference; a binding in a choice's item; a binding nothing
|
||||
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.
|
||||
- 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,
|
||||
|
||||
@@ -656,7 +656,7 @@ fail, except in a [typed column](#datatype), which must prove neither happens.
|
||||
### Transforms
|
||||
|
||||
`{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
|
||||
([One draw, one spelling](#one-draw-one-spelling)), so an email built from a name
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
it; a name that is a field or an option; a binding of anything but a reference; a binding
|
||||
nothing reads; a path through a name that selects a row, which the reference selects instead;
|
||||
and a binding in a table's format or cell. A field, column, category or folder name may not
|
||||
contain ` as `.
|
||||
it; a name that is a field or an option; a binding of anything but a reference; a binding in
|
||||
a choice's item, which every other item would leave unbound; a binding nothing reads; a path
|
||||
through a name that selects a row, which the reference selects instead; and a binding in a
|
||||
table's format or cell. A field, column, category or folder name may not contain ` as `.
|
||||
|
||||
### Draw group
|
||||
|
||||
|
||||
+10
-7
@@ -894,10 +894,13 @@ a struct's fields as one.
|
||||
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
|
||||
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
|
||||
scope is lexical: a category referencing another never sees its names, so what a category
|
||||
renders never depends on who references it. A pick is drawn on its first read, so a record's
|
||||
columns, rendered in name order, read one pick whichever binds it. A read entering a category
|
||||
past its root, `Fake("cat.field")` or `{/cat.field}`, keeps the category's frame in the memo of
|
||||
that read: per render and group, as the read's own draw is. Valid while a name is read only
|
||||
inside the category binding it.
|
||||
`"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. A choice's item binds no name,
|
||||
since a read outside the item would see a binding another item leaves out. The scope is
|
||||
lexical: a read entering a category, by a reference or through a name, sees none of the
|
||||
frames rendering above it, so what a category renders never depends on who references it. A
|
||||
pick is drawn on its first read, so a record's columns, rendered in name order, read one pick
|
||||
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")`.
|
||||
|
||||
@@ -45,7 +45,11 @@ type renderScope struct {
|
||||
draws *renderDraws
|
||||
group string
|
||||
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
|
||||
// outside one.
|
||||
pick *namedPick
|
||||
|
||||
@@ -247,7 +247,12 @@ func checkTokens(toks []formatToken, fields map[string]node) ([]unboundRead, err
|
||||
func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
|
||||
switch t.kind {
|
||||
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:
|
||||
return nil, checkBind(t)
|
||||
case nameRead:
|
||||
@@ -493,10 +498,15 @@ func (c *formatOps) holdName(a arm, label string) {
|
||||
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
|
||||
for _, operand := range tok.names {
|
||||
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))
|
||||
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.ops = append(c.ops, op{formatToken: tok})
|
||||
case builtinCall:
|
||||
c.function(tok, refs)
|
||||
c.function(tok, refs, isName)
|
||||
case nameRead:
|
||||
c.field(tok, refs, isName)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// names live in, and refuses a name bound twice along one chain of scopes, a name a field
|
||||
// spells too, a read no field or name answers, and a binding nothing reads.
|
||||
// names live in, and refuses a name bound twice along one chain of scopes, a binding in a
|
||||
// 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 {
|
||||
var scopes []*nameScope
|
||||
var gather func(n node, scope *nameScope, where string) error
|
||||
gather = func(n node, scope *nameScope, where string) error {
|
||||
top := &nameScope{owner: root}
|
||||
scopes := []*nameScope{top}
|
||||
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.repeat > 1 && n != root {
|
||||
scope = &nameScope{up: scope, owner: t}
|
||||
scopes = append(scopes, scope)
|
||||
inChoice = false
|
||||
}
|
||||
t.scope = scope
|
||||
for _, tok := range t.tokens {
|
||||
if tok.kind != nameBind {
|
||||
continue
|
||||
}
|
||||
if err := scope.bind(tok, t, where); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := scope.bindAll(t, inChoice, 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}
|
||||
scopes = append(scopes, top)
|
||||
if err := gather(root, top, ""); err != nil {
|
||||
if err := gather(root, top, false, ""); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -79,6 +83,37 @@ func bindNames(root node) error {
|
||||
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.
|
||||
func (sc *nameScope) close() error {
|
||||
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)
|
||||
}
|
||||
}
|
||||
if len(sc.order) == 0 {
|
||||
return nil
|
||||
}
|
||||
switch o := sc.owner.(type) {
|
||||
case *template:
|
||||
o.ownScope = sc
|
||||
case *choice:
|
||||
o.ownScope = sc
|
||||
if t, isTemplate := sc.owner.(*template); isTemplate && len(sc.order) > 0 {
|
||||
t.ownScope = sc
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -113,22 +142,6 @@ func eachContained(n node, where string, fn func(c node, where string) error) er
|
||||
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
|
||||
// format as compile reports them, and refuses a field spelling a name.
|
||||
func resolveReads(n node, where string) error {
|
||||
@@ -141,7 +154,11 @@ func resolveReads(n node, where string) error {
|
||||
}
|
||||
for _, name := range sortedNames(t.fields) {
|
||||
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 {
|
||||
@@ -163,13 +180,15 @@ func (t *template) isName(head string) bool {
|
||||
// every template is linked, so the binder has resolved that reference.
|
||||
func linkNames(path string, t *template) error {
|
||||
for i := range t.compiled.ops {
|
||||
for j := range t.compiled.ops[i].arms {
|
||||
a := &t.compiled.ops[i].arms[j]
|
||||
if a.kind != namedRead {
|
||||
continue
|
||||
}
|
||||
if err := linkName(t, a); err != nil {
|
||||
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), a.spelling, err)
|
||||
o := &t.compiled.ops[i]
|
||||
for _, reads := range [][]arm{o.arms, o.operands} {
|
||||
for j := range reads {
|
||||
if reads[j].kind != namedRead {
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// pushFrame opens a frame of scope until popFrame closes it, returning the mark popFrame takes.
|
||||
func (d *renderDraws) pushFrame(scope *nameScope) int {
|
||||
// pushFrame opens f until popFrame closes it, returning the mark popFrame takes.
|
||||
func (d *renderDraws) pushFrame(f *pickFrame) int {
|
||||
mark := d.depth()
|
||||
if d.frames == nil {
|
||||
d.frames = &frameStack{}
|
||||
}
|
||||
mark := len(d.frames.frames)
|
||||
d.frames.frames = append(d.frames.frames, newPickFrame(scope))
|
||||
d.frames.frames = append(d.frames.frames, f)
|
||||
return 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,
|
||||
// as Fake("cat.field") or {/cat.field} do, keeps the frame in the memo of that read.
|
||||
func (d *renderDraws) depth() int {
|
||||
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 {
|
||||
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 {
|
||||
return &f.picks[b.index]
|
||||
}
|
||||
@@ -249,19 +303,12 @@ func (sc renderScope) pickOf(b *nameBinding) *namedPick {
|
||||
if memo == nil {
|
||||
memo = &sc.groupDraws().memo
|
||||
}
|
||||
f, ok := memo.frames[b.scope]
|
||||
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]
|
||||
return &memo.frameOf(b.scope).picks[b.index]
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -297,19 +344,12 @@ func readPicked(s *generatorState, p *namedPick, head node, steps []pathStep, le
|
||||
return r
|
||||
}
|
||||
leaf := drawSteps(s, head, steps, &p.pins, &p.memo, levels)
|
||||
mark := -1
|
||||
if c, isChoice := leaf.(*choice); isChoice {
|
||||
if c.ownScope != nil {
|
||||
mark = sc.draws.pushFrame(c.ownScope)
|
||||
}
|
||||
leaf = p.memo.variantOf(s, c, key)
|
||||
}
|
||||
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)
|
||||
if mark >= 0 {
|
||||
sc.draws.popFrame(mark)
|
||||
}
|
||||
if p.memo.value == nil {
|
||||
p.memo.value = map[string]readValue{}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,9 @@ func (*folder) isNode() {}
|
||||
// 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.
|
||||
type choice struct {
|
||||
items []node
|
||||
cum []float64
|
||||
shared map[string]bool
|
||||
ownScope *nameScope // set on a category's root binding names, by bindNames
|
||||
items []node
|
||||
cum []float64
|
||||
shared map[string]bool
|
||||
}
|
||||
|
||||
func (*choice) isNode() {}
|
||||
|
||||
@@ -264,8 +264,7 @@ func recordOf(n node) (*template, []Column, error) {
|
||||
func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record {
|
||||
sc = sc.in(t)
|
||||
if t.ownScope != nil {
|
||||
mark := sc.draws.pushFrame(t.ownScope)
|
||||
defer sc.draws.popFrame(mark)
|
||||
defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(t.ownScope)))
|
||||
}
|
||||
if t.site.isFormat() {
|
||||
sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins)
|
||||
|
||||
+3
-1
@@ -100,7 +100,9 @@ func compileArms(t *template) {
|
||||
for i := range t.compiled.ops {
|
||||
o := &t.compiled.ops[i]
|
||||
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 {
|
||||
if o.arms[j].kind != namedRead {
|
||||
|
||||
@@ -61,10 +61,6 @@ func renderOnce(s *generatorState, n node) string {
|
||||
func render(s *generatorState, n node, sc renderScope) string {
|
||||
switch n := n.(type) {
|
||||
case *choice:
|
||||
if n.ownScope != nil {
|
||||
mark := sc.draws.pushFrame(n.ownScope)
|
||||
defer sc.draws.popFrame(mark)
|
||||
}
|
||||
return render(s, pick(s, n), sc)
|
||||
case *nullItem:
|
||||
return ""
|
||||
@@ -83,38 +79,48 @@ func render(s *generatorState, n node, sc renderScope) string {
|
||||
sc = sc.in(n)
|
||||
if n.repeat == 1 {
|
||||
if n.ownScope != nil {
|
||||
mark := sc.draws.pushFrame(n.ownScope)
|
||||
defer sc.draws.popFrame(mark)
|
||||
defer sc.draws.popFrame(sc.draws.pushFrame(sc.frameOf(n.ownScope)))
|
||||
}
|
||||
if lit, fixed := n.fixedText(); fixed {
|
||||
return lit
|
||||
}
|
||||
return expand(s, n, sc)
|
||||
}
|
||||
var b strings.Builder
|
||||
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()
|
||||
return renderRepeat(s, n, sc)
|
||||
default:
|
||||
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
|
||||
// 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
|
||||
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}
|
||||
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
|
||||
@@ -180,8 +186,9 @@ func readField(s *generatorState, t *template, hold *drawMemo, sc renderScope, a
|
||||
return readName(s, sc, a)
|
||||
case sc.pick != nil && !isRef(a.head):
|
||||
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:
|
||||
sc.pick = nil
|
||||
return readValue{text: render(s, t.head(a.head), sc)}
|
||||
}
|
||||
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)
|
||||
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)
|
||||
if memo.value == nil {
|
||||
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
|
||||
// reference alone whose read drew null.
|
||||
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)
|
||||
switch leaf := n.(type) {
|
||||
case *nullItem:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 71`
|
||||
`Next ID = 72`
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -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.
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -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.
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user