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:
+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`
|
- 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,
|
||||||
|
|||||||
@@ -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
@@ -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")`.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {}
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user