Answer architecture round 1: the read-once check back at link over uses from the compiled arms, every link step taking its inputs as arguments in link.go, a record column holding its name and datatype, and items 104 to 106 dropped from the plan with the decisions citing them reworded

This commit is contained in:
2026-10-05 22:30:40 +02:00
committed by Lillem4n
parent cd7f0a04dc
commit 0993d86f22
18 changed files with 314 additions and 285 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ In [docs/decisions.md](docs/decisions.md):
- `List` advertises direct descents only
- A path draws through its compiled steps
- A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read
- comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and items 104 to 106
- comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and the link in steps that each return what they build
- The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`
- Goal 5.1's `{…}` covers `{{`, `}}` and a lone `}`, so the goal names no escape
- Goal 7.2 holds while a first template can be written with no escape; a literal brace still takes `{{` or `}}`
+1
View File
@@ -967,6 +967,7 @@ struct.go structs: FakeStruct, fake tags as one record's columns, and a fi
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
format.go a format's tokens checked against their fields, and compiled
reference.go resolving references across the tree
link.go linking a template to the assembled tree, in steps: references, names' targets, compiled formats, a name's keys and reads, column reads, and the checks over them
graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences
calc.go the {calc()} operand checks: never a number, and a constant zero divisor
datatype.go column datatypes: DataType, where datatype and null may sit, a column's datatype
+1 -1
View File
@@ -136,7 +136,7 @@ func categoryBinding(sites []categorySite, root map[string]node) binding {
if err := linkTables(sites); err != nil {
return err
}
return linkRefs(sites, root)
return linkCategories(sites, root)
},
}
}
+5 -5
View File
@@ -825,18 +825,18 @@ category's frames, since the field sits inside it.
Valid while names are read only inside the category binding them.
## comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and items 104 to 106
## comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and the link in steps that each return what they build
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat. The hardest units were the draw fences, which the change making every `{…}` draw afresh deleted. The restructure deletes or moves what the earlier chunks of the below-floor round touched. It is three things: the change making every `{…}` draw afresh; the moves of the draw state, the grammar, `DataType`, what a proof knows of a value, the builtins and the reading of data files into `internal/`; and the untangling of the engine's own structs, which is the change cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and `todo.md` items 104 to 106. The maintainer approved these three, and `AGENTS.md`'s comprehension rule lets them merge below the floor. A scoring run before item 106 merges would score code about to go; for that reason the calc change accepting a name as an operand shipped unscored on 2026-10-05. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat. The hardest units were the draw fences, which the change making every `{…}` draw afresh deleted. The restructure deletes or moves what the earlier chunks of the below-floor round touched. It is three things: the change making every `{…}` draw afresh; the moves of the draw state, the grammar, `DataType`, what a proof knows of a value, the builtins and the reading of data files into `internal/`; and the untangling of the engine's own structs, which is the change cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and the link in steps that each return what they build, with a name scope created beside its owner and each record's columns settled at load. The maintainer approved these three, and `AGENTS.md`'s comprehension rule lets them merge below the floor. A scoring run before the link restructure merged would have scored code about to go; for that reason the calc change accepting a name as an operand shipped unscored on 2026-10-05. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
## The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`
2026-10-05, Lilleman auf Larv. Serves goals 3.2 and 3.4. Two architect reviews found the
engine's types, `template`, `table`, `arm`, `op` and `nameBinding`, bound in one cycle
because several passes each write part of the same structs; the template language's
recursion reads few of those types. Moving the engine before items 104 to 106 give
each value one writer would spread that knot across packages, behind forwarding calls from
every public method. The draw state, the grammar, `DataType`, what a proof knows of a
recursion reads few of those types. Moving the engine before the link gave each value
one writer would have spread that knot across packages, behind forwarding calls from every
public method. The draw state, the grammar, `DataType`, what a proof knows of a
value, the builtins, the reading of data files and a table's rows read no engine type, so
each sits in a package under `internal/`, behind the import list `imports_test.go` checks.
A rows table holds the engine table owning it as a value of a type parameter, so
+1 -1
View File
@@ -118,7 +118,7 @@ func linked(t *testing.T, s string) (node, error) {
if err != nil {
return nil, err
}
return n, linkNodeRefs(inlineScope(n, "template"), nil)
return n, linkInline(inlineScope(n, "template"), nil)
}
// compiled parses, compiles and links a JSON fragment into a node.
+5 -9
View File
@@ -32,13 +32,10 @@ func parseChecked(format string, fields map[string]node) ([]grammar.Token, []unb
}
// unboundRead is a token reading a head no field holds, which only a name bound around the
// template can answer; err is the refusal where none does. tail is the path read into the name,
// "" for the name itself, operand marks a builtin's read of it, noRef marks such a read where no
// reference can stand, and body is the token's.
// template can answer; err is the refusal where none does, and body is the token's.
type unboundRead struct {
head, tail, body string
operand, noRef bool
err error
head, body string
err error
}
// checkTokens proves every token's grammar, functions and field paths, and returns the reads
@@ -86,7 +83,7 @@ func checkReads(t grammar.Token, fields map[string]node, operands bool) ([]unbou
if _, _, err := grammar.RefShape(name); err != nil {
return nil, fmt.Errorf("token {%s}: %w", t.Body, err)
}
continue // its target is checked at New (see linkRefs)
continue // its target is checked at New (see linkCategories)
}
missing, err := checkArm(name, fields, !operands && len(names) == 1)
if err == nil {
@@ -96,8 +93,7 @@ func checkReads(t grammar.Token, fields map[string]node, operands bool) ([]unbou
if !missing {
return nil, err
}
a := splitArm(name, nil)
unbound = append(unbound, unboundRead{head: a.head, tail: grammar.JoinSegments(a.tail), body: t.Body, operand: operands, noRef: operands && builtinfunc.NoRefOperands(t.Fn), err: err})
unbound = append(unbound, unboundRead{head: splitArm(name, nil).head, body: t.Body, err: err})
}
return unbound, nil
}
+1 -15
View File
@@ -96,7 +96,7 @@ func bindInline(root *folder, n node, label string, typedByGo bool) error {
loadShipped(root, unloadedReads(root, nil, scope))
return binding{
scope: scope,
link: func() error { return linkNodeRefs(scope, root.children) },
link: func() error { return linkInline(scope, root.children) },
typedByGo: typedByGo,
}.bind()
}
@@ -121,17 +121,3 @@ func refuseFolderRefs(scope nodeScope) error {
return nil
})
}
// linkNodeRefs links the templates of an inline node against the loaded tree.
func linkNodeRefs(scope nodeScope, root map[string]node) error {
var ts []linkSite
if err := scope(func(label string, m node) error {
if t, ok := m.(*template); ok {
ts = append(ts, linkSite{t: t, label: label})
}
return nil
}); err != nil {
return err
}
return linkTemplates(ts, root)
}
+189
View File
@@ -0,0 +1,189 @@
package fejkdata
import (
"fmt"
"github.com/larvit/fejkdata/internal/builtinfunc"
"github.com/larvit/fejkdata/internal/grammar"
)
// linkSite is a template to link, with the folder and category it sits in, "" for an inline
// template, and eachNode's label for it.
type linkSite struct {
t *template
folder []string
category, label string
}
// linkCategories links every template of the categories, once all data is merged, so a reference
// sees the override-resolved tree.
func linkCategories(sites []categorySite, root map[string]node) error {
var ts []linkSite
for _, s := range sites {
if err := eachNode(s.n, s.path, func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
ts = append(ts, linkSite{t: t, folder: s.dir, category: s.path, label: label})
}
return nil
}); err != nil {
return err
}
}
return linkTemplates(ts, root)
}
// linkInline links the templates of an inline node against the loaded tree.
func linkInline(scope nodeScope, root map[string]node) error {
var ts []linkSite
if err := scope(func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
ts = append(ts, linkSite{t: t, label: label})
}
return nil
}); err != nil {
return err
}
return linkTemplates(ts, root)
}
// nameTarget is what a binding resolves to in the assembled tree: the node its head names and the
// path it reads into that node.
type nameTarget struct {
head node
tail []string
}
// linkedNames is what the link resolved about the names of a set of templates: each binding's
// target, the keys its reads address, and its reads.
type linkedNames struct {
targets map[*nameBinding]nameTarget
addressed map[*nameBinding]map[string]string
uses map[*nameBinding][]nameUse
}
// linkTemplates links ts in steps, each over every template before the next starts, each
// returning what it builds and taking what an earlier step built as an argument: the references,
// the names' targets, the compiled formats, the keys and reads of each name, and the column each
// format reads. The bindings take their targets and keys once every check has passed.
func linkTemplates(ts []linkSite, root map[string]node) error {
for _, s := range ts {
link, err := s.t.resolveLink(s.folder, s.label, s.category, root)
if err != nil {
return err
}
s.t.link = link
}
targets := nameTargets(ts)
for _, s := range ts {
compiled, err := compileFormat(s.t, targets)
if err != nil {
return fmt.Errorf("%s: %w", s.label, err)
}
s.t.compiled = compiled
}
names := linkedNames{targets: targets, addressed: addressedKeys(ts, targets), uses: nameUses(ts)}
for _, s := range ts {
s.t.readsColumn = columnReadOf(s.t, targets)
}
for _, s := range ts {
if err := checkNameReads(s.label, s.t, names); err != nil {
return err
}
}
for _, s := range ts {
if err := checkCalcNames(s.label, s.t); err != nil {
return err
}
}
for b, target := range targets {
b.nameTarget, b.addressed = target, names.addressed[b]
}
return nil
}
// nameTargets resolves what each binding of ts binds, from its binder's link.
func nameTargets(ts []linkSite) map[*nameBinding]nameTarget {
targets := map[*nameBinding]nameTarget{}
for _, s := range ts {
for _, tok := range s.t.tokens {
if tok.Kind == grammar.NameBind {
a := splitArm(tok.BoundRef, s.t.link.refs)
targets[s.t.nameScope.bindings[tok.Bound]] = nameTarget{head: s.t.head(a.head), tail: a.tail}
}
}
}
return targets
}
// addressedKeys is every key the reads of each name in ts land on or pass, from the name, with the
// spelling of the first read reaching it.
func addressedKeys(ts []linkSite, targets map[*nameBinding]nameTarget) map[*nameBinding]map[string]string {
keys := map[*nameBinding]map[string]string{}
for _, s := range ts {
for _, r := range namedReads(s.t) {
b := r.a.named
if keys[b] == nil {
keys[b] = map[string]string{}
}
for _, key := range append(r.a.levels[len(targets[b].tail):], r.a.path) {
if _, seen := keys[b][key]; !seen {
keys[b][key] = r.a.spelling
}
}
}
}
return keys
}
// nameUses is every read of each name in ts.
func nameUses(ts []linkSite) map[*nameBinding][]nameUse {
uses := map[*nameBinding][]nameUse{}
for _, s := range ts {
for _, r := range namedReads(s.t) {
b := r.a.named
uses[b] = append(uses[b], nameUse{
tail: grammar.JoinSegments(r.a.tail),
in: s.t,
operand: r.operand,
noRef: r.operand && builtinfunc.NoRefOperands(r.o.Fn),
nested: s.t.nameScope != b.scope,
})
}
}
return uses
}
// columnRead is a record's column read by a format that only reads one reference or name,
// which is the column: it takes the column's datatype and null. category and field name it;
// category is "" for a column of the reading template's own record, which checkColumns reaches
// on its own.
type columnRead struct {
a arm
category, field string
column node
}
// columnReadOf is the record's column t's format reads, where the format only reads one reference
// or name, and nil where it does not.
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
ops := t.compiled.ops
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.NameRead || len(ops[0].arms) != 1 {
return nil
}
a := ops[0].arms[0]
head, category, tail := t.head(a.head), a.head[min(1, len(a.head)):], a.tail
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
head, category = b.binder, ""
tail = append(append([]string{splitArm(b.ref, nil).head}, targets[b].tail...), a.tail...)
case a.kind == namedRead:
head, category = targets[b].head, b.binder.link.refs[b.ref].head[1:]
tail = append(targets[b].tail[:len(targets[b].tail):len(targets[b].tail)], a.tail...)
case !grammar.IsRef(a.head):
return nil
}
if target, isTemplate := head.(*template); isTemplate && target.isRecord && len(tail) == 1 {
return &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
}
return nil
}
+42 -40
View File
@@ -29,9 +29,8 @@ type nameBinding struct {
index int
binder *template
// Filled by `linkTemplates`, from the assembled tree:
head node
tail []string
// Filled by `linkTemplates`, once every check of the link passed:
nameTarget
// addressed is every key a read of the name lands on or passes, the spelling of the
// first read reaching it beside it; a pick keeps the draws at these keys, and only these.
addressed map[string]string
@@ -93,11 +92,23 @@ func bindNames(root node) error {
}
}
}
reads, err := resolveReads(root, scopes)
read, err := resolveReads(root, scopes)
if err != nil {
return err
}
return checkNameUses(scopes, reads)
return checkRead(scopes, read)
}
// checkRead refuses a binding of scopes that read does not hold.
func checkRead(scopes []*nameScope, read map[*nameBinding]bool) error {
for _, sc := range scopes {
for _, b := range sc.order {
if !read[b] {
return fmt.Errorf("%stoken {%s}: nothing reads name %q; drop the token", b.where, b.body, b.name)
}
}
}
return nil
}
// bindAll binds every name t's tokens bind.
@@ -130,25 +141,6 @@ func (sc *nameScope) spelled() string {
return "in one repeat"
}
// checkNameUses refuses a binding read by nothing, then each binding checkUses refuses.
func checkNameUses(scopes []*nameScope, reads map[*nameBinding][]nameUse) error {
for _, sc := range scopes {
for _, b := range sc.order {
if len(reads[b]) == 0 {
return fmt.Errorf("%stoken {%s}: nothing reads name %q; drop the token", b.where, b.body, b.name)
}
}
}
for _, sc := range scopes {
for _, b := range sc.order {
if err := b.checkUses(reads[b]); err != nil {
return fmt.Errorf("%stoken {%s}: %w", b.where, b.body, err)
}
}
}
return nil
}
// ownScope is the scope t renders a frame of, nil where that scope binds no name: a category's,
// on its root, or a repeat's, per iteration.
func (t *template) ownScope() *nameScope {
@@ -174,11 +166,11 @@ func eachContained(n node, where string, fn func(c node, where string) error) er
}
// resolveReads answers each unbound read under root with a name its template sees, fields before
// the format as compile reports them, returning every binding's reads, and refuses a field spelling
// a name. scopes are every scope of the category, which a refusal searches for a name bound where
// the format as compile reports them, returning every binding read, and refuses a field spelling a
// name. scopes are every scope of the category, which a refusal searches for a name bound where
// the read cannot see it.
func resolveReads(root node, scopes []*nameScope) (map[*nameBinding][]nameUse, error) {
reads := map[*nameBinding][]nameUse{}
func resolveReads(root node, scopes []*nameScope) (map[*nameBinding]bool, error) {
read := map[*nameBinding]bool{}
var resolve func(n node, where string) error
resolve = func(n node, where string) error {
t, isTemplate := n.(*template)
@@ -202,11 +194,11 @@ func resolveReads(root node, scopes []*nameScope) (map[*nameBinding][]nameUse, e
if b == nil {
return unresolved(where, u, t.nameScope, scopes)
}
reads[b] = append(reads[b], nameUse{tail: u.tail, in: t, operand: u.operand, noRef: u.noRef, nested: t.nameScope != b.scope})
read[b] = true
}
return nil
}
return reads, resolve(root, "")
return read, resolve(root, "")
}
// unresolved is the refusal of u, a read no field or name answers: naming the repeat binding the
@@ -241,19 +233,29 @@ func (t *template) isName(head string) bool {
return !grammar.IsRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil
}
// namedReads calls fn with every read of a name t's format makes, and the token holding it.
func namedReads(t *template, fn func(o *op, a *arm) error) error {
// namedReadAt is one read of a name in a format: the op holding it, the compiled arm, and whether a
// builtin reads it as an operand.
type namedReadAt struct {
o *op
a *arm
operand bool
}
// namedReads is every read of a name t's format makes, in format order.
func namedReads(t *template) []namedReadAt {
var out []namedReadAt
for i := range t.compiled.ops {
o := &t.compiled.ops[i]
for _, reads := range [][]arm{o.arms, o.operands} {
for j := range reads {
if reads[j].kind == namedRead {
if err := fn(o, &reads[j]); err != nil {
return err
}
}
for j := range o.arms {
if o.arms[j].kind == namedRead {
out = append(out, namedReadAt{o: o, a: &o.arms[j]})
}
}
for j := range o.operands {
if o.operands[j].kind == namedRead {
out = append(out, namedReadAt{o: o, a: &o.operands[j], operand: true})
}
}
}
return nil
return out
}
+4
View File
@@ -188,6 +188,10 @@ func TestNameErrors(t *testing.T) {
`no field "word"; a binding names a field, or a reference`},
{"read once through a field", map[string]string{"card": `{"format":"{place as p}{p.x}","place":{"format":"{x}","x":["a","b"]}}`},
`name "p" is read once, so it keeps no pick for another read; write {place.x} where it is read`},
{"read once through a path that does not resolve", map[string]string{"word": `{"format":"{w}","w":["a","b"]}`, "card": `"{/word as a}{a.zz}"`},
`no field "zz"`},
{"read once through a field path that does not resolve", map[string]string{"card": `{"format":"{place as p}{p.zz}","place":{"format":"{x}","x":["a","b"]}}`},
`no field "zz"`},
{"read by nothing", map[string]string{"word": `["a","b"]`, "card": `"{/word as w}"`},
`nothing reads name "w"`},
{"read in a repeat", map[string]string{"nm.json": `{"format":"{first}","rows":"nm.tsv"}`, "nm.tsv": "first\tlast\nAda\tByron\nBo\tEk\n", "person": `{"format":"{first} {last}","first":"{/nm.first}","last":"{/nm.last}"}`, "card": `{"format":"{/person as p}{each}","each":{"format":"{p.first}","repeat":2}}`},
+33 -21
View File
@@ -8,20 +8,31 @@ import (
"github.com/larvit/fejkdata/internal/grammar"
)
// checkNameReads refuses a read of a name inside the field bound to it, and a read {n} beside {n.w}
// where n's pick renders w twice: the pick keeps one draw of w.
func checkNameReads(label string, t *template) error {
return namedReads(t, func(o *op, a *arm) error {
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) {
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", label, o.Body, a.named.name, a.named.ref)
// checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the
// field bound to it, and a read {n} beside {n.w} where n's pick renders w twice: the pick keeps one
// draw of w.
func checkNameReads(label string, t *template, names linkedNames) error {
for _, tok := range t.tokens {
if tok.Kind != grammar.NameBind {
continue
}
for _, leaf := range a.leaves {
if err := a.named.checkOnce(a.spelling, leaf, a.path); err != nil {
return fmt.Errorf("%s: token {%s}: %w", label, o.Body, err)
b := t.nameScope.bindings[tok.Bound]
if err := b.checkUses(names.uses[b]); err != nil {
return fmt.Errorf("%s: token {%s}: %w", label, tok.Body, err)
}
}
for _, r := range namedReads(t) {
b, target := r.a.named, names.targets[r.a.named]
if b.bindsField() && rendersInside(compilePath(target.head, target.tail).leaves, t) {
return fmt.Errorf("%s: token {%s}: name %q is read inside %q, the field bound to it; read the name outside that field", label, r.o.Body, b.name, b.ref)
}
for _, leaf := range r.a.leaves {
if err := checkOnce(names.addressed[b], r.a.spelling, leaf, r.a.path); err != nil {
return fmt.Errorf("%s: token {%s}: %w", label, r.o.Body, err)
}
}
return nil
})
}
return nil
}
// rendersInside reports whether t sits in what one of nodes contains.
@@ -51,8 +62,8 @@ func rendersInside(nodes []node, t *template) bool {
return false
}
// checkUses refuses a binding whose uses read it once at a spot the bound spelling can stand: that
// spelling draws the same way without the name. A bound field can stand in only where the reading
// checkUses refuses a binding whose uses read it once at a spot the bound spelling can stand:
// that spelling draws the same way without the name. A bound field can stand in only where the reading
// template reaches the binder through fields. Where the spelling would be a CLI argument or tag
// of one reference alone, that entry point's own refusal then names the bare path.
func (b *nameBinding) checkUses(uses []nameUse) error {
@@ -105,22 +116,23 @@ func calcReads(spelling string) bool {
return err == nil && isVar && v.Name == spelling
}
// checkOnce walks n, rendering at key under b's pick, into each level a read of b addresses.
func (b *nameBinding) checkOnce(read string, n node, key string) error {
// checkOnce walks n, rendering at key under a pick, into each level a read of the name addresses:
// the keys of addressed.
func checkOnce(addressed map[string]string, read string, n node, key string) error {
switch n := n.(type) {
case *choice:
for _, item := range n.items {
if err := b.checkOnce(read, item, key); err != nil {
if err := checkOnce(addressed, read, item, key); err != nil {
return err
}
}
case *template:
return b.checkTemplateOnce(read, n, key)
return checkTemplateOnce(addressed, read, n, key)
}
return nil
}
func (b *nameBinding) checkTemplateOnce(read string, t *template, key string) error {
func checkTemplateOnce(addressed map[string]string, read string, t *template, key string) error {
reads := map[string]int{}
var into []arm
for _, o := range t.compiled.ops {
@@ -128,20 +140,20 @@ func (b *nameBinding) checkTemplateOnce(read string, t *template, key string) er
if grammar.IsRef(a.head) || a.kind == namedRead {
continue
}
if _, kept := b.addressed[join(key, a.path)]; kept {
if _, kept := addressed[join(key, a.path)]; kept {
into = append(into, a)
}
reads[a.head]++
}
}
for _, head := range sortedNames(reads) {
if by, kept := b.addressed[join(key, head)]; kept && reads[head] > 1 {
if by, kept := addressed[join(key, head)]; kept && reads[head] > 1 {
return fmt.Errorf("{%s} renders field %q twice, so {%s} cannot say which draw it reads; drop {%s} or {%s}", read, head, by, read, by)
}
}
for _, a := range into {
for _, leaf := range a.leaves {
if err := b.checkOnce(read, leaf, join(key, a.path)); err != nil {
if err := checkOnce(addressed, read, leaf, join(key, a.path)); err != nil {
return err
}
}
+1 -1
View File
@@ -107,7 +107,7 @@ func TestEveryFormatCompilesAtLink(t *testing.T) {
t.Errorf("%s compiled before link", format)
}
}
if err := linkNodeRefs(inlineScope(n, "t"), map[string]node{"w": w}); err != nil {
if err := linkInline(inlineScope(n, "t"), map[string]node{"w": w}); err != nil {
t.Fatal(err)
}
for format, has := range compiledByPath() {
+16 -12
View File
@@ -10,6 +10,7 @@ import (
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/invariant"
)
// Column is one rendered column of a record. Value is the rendered text, which a
@@ -214,7 +215,7 @@ func (f *Generator) FakeRecordTemplate(input string) (*Record, error) {
var ErrNoColumns = errors.New("has no fields, so no columns")
// recordOf is the fence both record entry points pass: the record template n is, whose columns
// its link fixed for every draw.
// settleRecords fixed at load for every draw.
func recordOf(n node) (*template, error) {
if tb, isTable := n.(*table); isTable {
n = tb.formatTemplate
@@ -229,16 +230,20 @@ func recordOf(n node) (*template, error) {
if !t.isRecord {
return nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat)
}
if len(t.columns) != len(t.fields) {
panic(invariant.Broken("record %q was reached before its load settled its columns", t.format))
}
return t, nil
}
// recordColumn is one column of a record template: what it writes, the field drawing it, and the
// name the template binds to that whole field, whose pick the column renders so the row agrees
// with the columns reading the name.
// recordColumn is one column of a record template: its name and datatype, the field drawing it,
// and the name the template binds to that whole field, whose pick the column renders so the row
// agrees with the columns reading the name.
type recordColumn struct {
Column
field node
whole *nameBinding
name string
datatype DataType
field node
boundWhole *nameBinding
}
// settleRecords fixes the columns of every record in scope, whose datatypes a cycle would walk
@@ -270,7 +275,7 @@ func recordColumns(t *template) []recordColumn {
columns := make([]recordColumn, len(names))
for i, name := range names {
field := t.fields[name]
columns[i] = recordColumn{Column: Column{Name: name, DataType: columnDatatype(field)}, field: field, whole: whole[field]}
columns[i] = recordColumn{name: name, datatype: columnDatatype(field), field: field, boundWhole: whole[field]}
}
return columns
}
@@ -284,13 +289,12 @@ func renderRecord(s *drawstate.State, t *template, sc renderScope) *Record {
r := &Record{columns: make([]Column, len(t.columns))}
for i, c := range t.columns {
var column readValue
if c.whole != nil {
column = readName(s, sc, arm{kind: namedRead, named: c.whole, levels: wholeLevels})
if c.boundWhole != nil {
column = readName(s, sc, arm{kind: namedRead, named: c.boundWhole, levels: wholeLevels})
} else {
column = renderLeaf(s, c.field, sc)
}
r.columns[i] = c.Column
r.columns[i].Value, r.columns[i].Null = column.text, column.null
r.columns[i] = Column{Name: c.name, DataType: c.datatype, Value: column.text, Null: column.null}
}
return r
}
+5
View File
@@ -550,6 +550,11 @@ func TestEveryRecordCarriesItsShapeOnceLoaded(t *testing.T) {
if len(tm.columns) != len(tm.fields) {
t.Errorf("%s: %d record columns, want one per field, %d", label, len(tm.columns), len(tm.fields))
}
for _, c := range tm.columns {
if c.field != tm.fields[c.name] || c.datatype != columnDatatype(c.field) {
t.Errorf("%s: column %q holds field %v of datatype %s, want field %q's", label, c.name, c.field, c.datatype, c.name)
}
}
return nil
}); err != nil {
t.Fatal(err)
-155
View File
@@ -2,7 +2,6 @@ package fejkdata
import (
"fmt"
"slices"
"strings"
"github.com/larvit/fejkdata/internal/grammar"
@@ -35,108 +34,6 @@ func refSegments(name string, folder []string) ([]string, error) {
return append(append([]string{}, base...), segs...), nil
}
// linkRefs links every template of the categories, once all data is merged, so a reference sees
// the override-resolved tree.
func linkRefs(sites []categorySite, root map[string]node) error {
var ts []linkSite
if err := eachTemplate(sites, func(s categorySite, label string, t *template) error {
ts = append(ts, linkSite{t: t, folder: s.dir, category: s.path, label: label})
return nil
}); err != nil {
return err
}
return linkTemplates(ts, root)
}
// linkSite is a template to link, with the folder and category it sits in, "" for an inline
// template, and eachNode's label for it.
type linkSite struct {
t *template
folder []string
category, label string
}
// nameTarget is what a binding resolves to in the assembled tree: the node its head names and the
// path it reads into that node.
type nameTarget struct {
head node
tail []string
}
// linkTemplates links ts in steps, each over every template before the next starts, each
// returning what it builds: the references, the names' targets, the compiled formats, the keys
// each name's reads address, and the column each format reads.
func linkTemplates(ts []linkSite, root map[string]node) error {
for _, s := range ts {
link, err := s.t.resolveLink(s.folder, s.label, s.category, root)
if err != nil {
return err
}
s.t.link = link
}
targets := nameTargets(ts)
for _, s := range ts {
compiled, err := compileFormat(s.t, targets)
if err != nil {
return fmt.Errorf("%s: %w", s.label, err)
}
s.t.compiled = compiled
}
addressed := addressedKeys(ts)
for b, target := range targets {
b.head, b.tail, b.addressed = target.head, target.tail, addressed[b]
}
for _, s := range ts {
s.t.readsColumn = columnReadOf(s.t)
}
for _, check := range []func(label string, t *template) error{checkNameReads, checkCalcNames} {
for _, s := range ts {
if err := check(s.label, s.t); err != nil {
return err
}
}
}
return nil
}
// nameTargets resolves what each binding of ts binds, from its binder's link.
func nameTargets(ts []linkSite) map[*nameBinding]nameTarget {
targets := map[*nameBinding]nameTarget{}
for _, s := range ts {
for _, tok := range s.t.tokens {
if tok.Kind == grammar.NameBind {
a := splitArm(tok.BoundRef, s.t.link.refs)
targets[s.t.nameScope.bindings[tok.Bound]] = nameTarget{head: s.t.head(a.head), tail: a.tail}
}
}
}
return targets
}
// addressedKeys is every key the reads of each name in ts land on or pass, from the name, with the
// spelling of the first read reaching it.
func addressedKeys(ts []linkSite) map[*nameBinding]map[string]string {
keys := map[*nameBinding]map[string]string{}
for _, s := range ts {
for _, o := range s.t.compiled.ops {
for _, a := range slices.Concat(o.arms, o.operands) {
if a.kind != namedRead {
continue
}
if keys[a.named] == nil {
keys[a.named] = map[string]string{}
}
for _, key := range append(a.levels[len(a.levels)-len(a.tail)-1:], a.path) {
if _, seen := keys[a.named][key]; !seen {
keys[a.named][key] = a.spelling
}
}
}
}
}
return keys
}
// resolveLink binds every reference t reads, refusing one to t's own category:
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
func (t *template) resolveLink(folder []string, label, category string, root map[string]node) (templateLink, error) {
@@ -169,58 +66,6 @@ func (t *template) resolveLink(folder []string, label, category string, root map
return link, nil
}
// columnRead is a record's column read by a format that only reads one reference or name,
// which is the column: it takes the column's datatype and null. category and field name it;
// category is "" for a column of the reading template's own record, which checkColumns reaches
// on its own.
type columnRead struct {
a arm
category, field string
column node
}
// columnReadOf is the record's column t's format reads, where the format only reads one reference
// or name, and nil where it does not.
func columnReadOf(t *template) *columnRead {
ops := t.compiled.ops
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.NameRead || len(ops[0].arms) != 1 {
return nil
}
a := ops[0].arms[0]
head, category, tail := t.head(a.head), a.head[min(1, len(a.head)):], a.tail
switch b := a.named; {
case a.kind == namedRead && b.bindsField():
head, category = b.binder, ""
tail = append(append([]string{splitArm(b.ref, nil).head}, b.tail...), a.tail...)
case a.kind == namedRead:
ref := b.binder.link.refs[b.ref]
head, category = b.head, ref.head[1:]
tail = append(ref.tail[:len(ref.tail):len(ref.tail)], a.tail...)
case !grammar.IsRef(a.head):
return nil
}
if target, isTemplate := head.(*template); isTemplate && target.isRecord && len(tail) == 1 {
return &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
}
return nil
}
// eachTemplate calls fn once per template of the categories, with the category it sits
// in and eachNode's label for it.
func eachTemplate(sites []categorySite, fn func(s categorySite, label string, t *template) error) error {
for _, s := range sites {
if err := eachNode(s.n, s.path, func(label string, n node) error {
if t, isTemplate := n.(*template); isTemplate {
return fn(s, label, t)
}
return nil
}); err != nil {
return err
}
}
return nil
}
// resolveCategory walks a dotted path through the folders to the category it
// names, returning that category's segments, the node, and the tail left to read into it.
func resolveCategory(root map[string]node, segments []string) (categorySegs []string, target node, tail []string, err error) {
+3 -3
View File
@@ -47,11 +47,11 @@ func shippedShape(f *Generator) string {
facts[prefix] = "\tformat " + strconv.Quote(n.format) + readsFact(n)
if record, err := recordOf(n); err == nil {
for _, c := range record.columns {
fact := "\t" + c.DataType.String()
if _, nullable := columnItems(n.fields[c.Name]); nullable {
fact := "\t" + c.datatype.String()
if _, nullable := columnItems(c.field); nullable {
fact += " null"
}
facts[join(prefix, c.Name)] = fact
facts[join(prefix, c.name)] = fact
}
}
}
+2 -2
View File
@@ -272,8 +272,8 @@ func (s *structShape) compileRecord(root *folder, t reflect.Type, label string,
proof := &valueProof{}
s.fieldIndexes = make([][]int, len(record.columns))
for i, c := range record.columns {
sf, _ := t.FieldByName(c.Name)
if err := proof.checkField(label+"."+c.Name, sf.Type, record.fields[c.Name]); err != nil {
sf, _ := t.FieldByName(c.name)
if err := proof.checkField(label+"."+c.name, sf.Type, c.field); err != nil {
return err
}
s.fieldIndexes[i] = sf.Index
+4 -19
View File
@@ -72,14 +72,11 @@
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
| 34 | 0.1.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 6 | 7 | 4 | 20.9 |
| 96 | 0.1.0 | | **Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel and larv-review's scoring run once items 104 to 106 have restructured the code, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel and larv-review's scoring run on the restructured code, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 91 | 0.1.0 | | **Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.** | 3 | 4 | 6 | 7 | 4.1, 5.3 | 19.9 |
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
| 106 | 0.1.0 | | **Settle each record's shape when its template links, so `Generator.records` goes.** | 2 | 2 | 2 | 7 | 3.2 | 14.1 |
| 110 | 0.1.0 | | **Refuse an empty segment in a path `Fake`, `FakeRecord` and the CLI take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
| 104 | 0.1.0 | decision | **Create a name scope with its owner in one place, and hand a template's name reads to the checks as a returned value, not appended to its bindings.** | 4 | 4 | 2 | 8 | 3.2 | 13.4 |
| 79 | 0.1.0 | decision | **Beside a `--data-path`, load in `New` the user's categories, the shipped ones they override or read, and the shipped ones whose reads reach an overridden one; load the rest on first reach.** | 6 | 6 | 7 | 8 | 13 | 12.9 |
| 105 | 0.1.0 | principle | **Link each template in steps that each return what they build — its references, its bindings' heads and addressed keys, its compiled arms, its column read — so no step writes into what an earlier one built.** | 6 | 7 | 2 | 9 | 3.2 | 12.4 |
| 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 |
| 113 | 0.2.0 | defect | **Draw `{iban(CC)}` with its country's national check digits, and SE's with a real bank's id, so a validator checking more than mod 97 accepts it.** | 3 | 5 | 7 | 9 | 4 | 25.0 |
| 26 | 0.2.0 | defect | **Spell `sv_SE.personnummer` with the `+` Skatteverket uses from the year the holder turns 100, judged by a date the caller gives.** | 4 | 6 | 7 | 8 | 4 | 22.1 |
@@ -241,9 +238,9 @@ Nothing has shipped, so "no longer paths", "where it used to fail" and "where it
Asked of the maintainer by the product-owner pass of the chunk that made every `{…}` draw afresh. Since every `{…}` draws afresh, `{net} x {qty} = {calc(net * qty, 2)}`, `{place.postal-code} {place.locality}` and struct tags `sv_SE.address.street` beside `sv_SE.address.postal-code` load and may render a pair that disagrees, and nothing tells the author to bind a name (goal 4.1). Goal 5.6 asks such a template to fail, showing `{place as p}{p.postal-code} {p.locality}` for one place and two names for two. The decision "Every `{…}` draws afresh, and only a name keeps a pick" lists the draw checks as names, paths through names, cycles and the repeat cap, so this fence revises it. `{w} {w}`, one spelling twice, stays two draws. The maintainer answered yes on 2026-10-05, for reads inside one category or one struct; reads across categories, `{/city.name}, {/country.name}`, keep loading.
### 5. Run the nine-seat comprehension panel and larv-review's scoring run once items 104 to 106 have restructured the code, and file what the panel names.
### 5. Run the nine-seat comprehension panel and larv-review's scoring run on the restructured code, and file what the panel names.
Until a panel scores 7.0 or above, only items 104 to 106 and the exceptions in `AGENTS.md`'s comprehension rule merge. The decision "comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and items 104 to 106" suspends every scoring run and panel until this item runs.
Until a panel scores 7.0 or above, only the exceptions in `AGENTS.md`'s comprehension rule merge. The decision "comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and the link in steps that each return what they build" suspends every scoring run and panel until this item runs.
### 91. Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.
@@ -253,28 +250,16 @@ A calc operand parses as a bare identifier, so `{calc(c.lat * 60)}` fails with `
Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` seeds itself. This item ships in 0.1.0 beside item 77, so `New` breaks once before anyone depends on it. Mark `WithSeed` required in the Library section's options table. Its tests then stop swapping `randomBytes`, as `fejkdata_test.go` does today, which fails every `New` in the package while that test runs.
### 106. Settle each record's shape when its template links, so `Generator.records` goes.
Needs item 105. Settling all 84 shipped record shapes took 0.09 ms on 2026-10-05, so the cache saves nothing. The error for `FakeRecord` on a node that is no record stays at call time. `bindingOfWhole` stops repeating a load-time search on every column of every record draw.
### 110. Refuse an empty segment in a path `Fake`, `FakeRecord` and the CLI take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.
Today `fejkdata 'sv_SE.person.'` reports `no field ""`, while `{/sv_SE.person.}` reports `path has an empty segment`. `grammar.CheckSegments` is the check a reference, a sibling path and a step up `..` run.
### 104. Create a name scope with its owner in one place, and hand a template's name reads to the checks as a returned value, not appended to its bindings.
The two-way link `nameScope.owner` ↔ `template.ownNameScope` is written by a third party, `settle`; `resolveReads` appends `uses` to a binding that only the load checks read.
### 79. Beside a `--data-path`, load in `New` the user's categories, the shipped ones they override or read, and the shipped ones whose reads reach an overridden one; load the rest on first reach.
Needs item 77, which makes an override fail unless it says it replaces.
The decision "With only the shipped set, a category loads on the first call reaching it; beside a `--data-path`, every category loads in `New`" parses the whole shipped set on every `--data-path` run, about 45 ms today and seconds once items 61 and 78 land. Goal 6 needs only the user's categories, the shipped ones they override, the shipped ones they read, and the shipped ones whose reads reach an overridden category, so the index would carry each category's reads.
### 105. Link each template in steps that each return what they build — its references, its bindings' heads and addressed keys, its compiled arms, its column read — so no step writes into what an earlier one built.
Needs item 104. `linkBindings` writes `nameBinding.head` and `tail` in `linkRefs`'s first pass and `linkName` reads them in its second, so swapping the passes hands `linkName` a nil head. `linkPasses` does the same: `linkColumnRead` reads the `arm.named` that `linkNames` wrote and writes `link.readsColumn` into the link `linkTemplate` built, and `checkNameReads` reads the arms both filled. `arm` is filled in three places, `compileFormat`, `compileArms` and `linkName`, and `splitArm` runs before the link and again after it, so five readers of an earlier panel traced three phases. Technical principle: one owner per value.
### 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.
@@ -460,7 +445,7 @@ Its doc says render failure, and it wraps what `NewTemplate` and `NewRecordTempl
### 95. Hold a struct's columns and the field each fills in one slice.
`structShape.columns` and `structShape.fieldIndexes` pair up by index alone, against the technical principle "one owner per value".
`structShape.fieldIndexes` pairs with its record template's `columns` by index alone, against the technical principle "one owner per value".
### 116. Turn a row index into its rows file's line in one function.