From cd7f0a04dc72c5c13ec21d6edca41898f88b2f3e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 5 Oct 2026 22:21:10 +0200 Subject: [PATCH] Link each template in steps that each return what they build, create a name scope with its owner in one place, and settle each record's columns at load, so Generator.records goes --- data.go | 1 + datatype.go | 6 +- fejkdata.go | 3 +- format.go | 97 +++++++++++++++++++--------- inline.go | 18 ++---- name.go | 175 ++++++++++++++++++++------------------------------ namefence.go | 23 ++----- node.go | 20 +++--- pick.go | 5 +- record.go | 140 ++++++++++++++++++++-------------------- reference.go | 132 ++++++++++++++++++++++++------------- render.go | 9 +-- shape_test.go | 4 +- struct.go | 11 ++-- todo.md | 2 +- valueproof.go | 6 +- 16 files changed, 339 insertions(+), 313 deletions(-) diff --git a/data.go b/data.go index 21a6138..0e927f3 100644 --- a/data.go +++ b/data.go @@ -148,6 +148,7 @@ func (b binding) bind() error { if err := checkNoCycles(b.scope); err != nil { return err } + settleRecords(b.scope) if !b.typedByGo { if err := checkColumns(b.scope); err != nil { return err diff --git a/datatype.go b/datatype.go index 1e58d36..94027fa 100644 --- a/datatype.go +++ b/datatype.go @@ -67,7 +67,7 @@ func checkColumns(s nodeScope) error { checked[column] = true items, _ := columnItems(column) for _, it := range items { - if r := it.link.readsColumn; r != nil && r.category != "" { + if r := it.readsColumn; r != nil && r.category != "" { if err := check(r.category, r.field, r.column); err != nil { return err } @@ -110,8 +110,8 @@ func columnDatatype(n node) DataType { // itemDatatype is the datatype a column item declares, else that of the column it reads. func itemDatatype(t *template) DataType { - if t.datatype == DataTypeString && t.link.readsColumn != nil { - return columnDatatype(t.link.readsColumn.column) + if t.datatype == DataTypeString && t.readsColumn != nil { + return columnDatatype(t.readsColumn.column) } return t.datatype } diff --git a/fejkdata.go b/fejkdata.go index 8a8eb42..64ea95a 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -28,12 +28,11 @@ var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithD // It is safe for concurrent use; a seeded sequence is reproducible only when drawn // from one goroutine. type Generator struct { - // mu guards drawState, records, structs and root, which gains a shipped category on the + // mu guards drawState, structs and root, which gains a shipped category on the // first call reaching it. mu sync.Mutex drawState *drawstate.State root folder // the categories as the node a path walks from, owned here so a walk allocates none - records map[node]recordShape structs map[reflect.Type]structResult } diff --git a/format.go b/format.go index b2e4cd6..ddee4f5 100644 --- a/format.go +++ b/format.go @@ -6,6 +6,7 @@ import ( "github.com/larvit/fejkdata/internal/builtinfunc" "github.com/larvit/fejkdata/internal/grammar" + "github.com/larvit/fejkdata/internal/invariant" ) // checkFunc validates a call at compile time: a known builtin, its args, and, for calc, the @@ -224,7 +225,7 @@ const ( ) // splitArm splits one name into head and tail. refs maps a reference to what -// linkRefs resolved it to; before linking, a reference is whole. +// resolveLink resolved it to; before linking, a reference is whole. func splitArm(name string, refs map[string]refBinding) arm { if grammar.IsRef(name) { b, linked := refs[name] @@ -278,43 +279,75 @@ type formatOps struct { grow int } -func (c *formatOps) function(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) { - var operands []arm - for _, operand := range builtinfunc.Operands(tok.Fn, tok.Args) { - a := splitArm(operand, refs) - if isName(a.head) { - a.kind = namedRead - } - operands = append(operands, a) - } - c.ops = append(c.ops, op{Token: tok, call: builtinfunc.Prep(tok.Fn, tok.Args), operands: operands}) -} - -func (c *formatOps) field(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) { - arms := make([]arm, len(tok.Arms)) - for i, name := range tok.Arms { - arms[i] = splitArm(name, refs) - if isName(arms[i].head) { - arms[i].kind = namedRead - } - } - c.ops = append(c.ops, op{Token: tok, arms: arms}) -} - -// compileFormat compiles a parsed format; a {ref as n} token compiles to no op. Call -// checkTokens first: it is what proves every token valid. -func compileFormat(toks []grammar.Token, refs map[string]refBinding, isName func(head string) bool) formatOps { +// compileFormat compiles t's parsed format against its link and its names' targets; a {ref as n} +// token compiles to no op. checkTokens proved every token valid. +func compileFormat(t *template, targets map[*nameBinding]nameTarget) (formatOps, error) { var c formatOps - for _, tok := range toks { + for _, tok := range t.tokens { + o := op{Token: tok} + var err error switch tok.Kind { case grammar.LiteralRun: c.grow += len(tok.Lit) - c.ops = append(c.ops, op{Token: tok}) case grammar.BuiltinCall: - c.function(tok, refs, isName) + o.call = builtinfunc.Prep(tok.Fn, tok.Args) + o.operands, err = t.compileArms(builtinfunc.Operands(tok.Fn, tok.Args), targets) case grammar.NameRead: - c.field(tok, refs, isName) + o.arms, err = t.compileArms(tok.Arms, targets) + case grammar.NameBind: + continue } + if err != nil { + return c, fmt.Errorf("token {%s}: %w", tok.Body, err) + } + c.ops = append(c.ops, o) } - return c + return c, nil +} + +func (t *template) compileArms(names []string, targets map[*nameBinding]nameTarget) ([]arm, error) { + if len(names) == 0 { + return nil, nil + } + arms := make([]arm, len(names)) + for i, name := range names { + a, err := t.compileArm(name, targets) + if err != nil { + return nil, err + } + arms[i] = a + } + return arms, nil +} + +// compileArm compiles one read: a path from the head it names, or from what the name it reads +// through binds. +func (t *template) compileArm(name string, targets map[*nameBinding]nameTarget) (arm, error) { + a := splitArm(name, t.link.refs) + if !t.isName(a.head) { + head := t.head(a.head) + if head == nil { + panic(invariant.Broken("{%s} reads a head nothing bound", a.spelling)) + } + w := compilePath(head, a.tail) + a.steps, a.leaves = w.steps, w.leaves + return a, nil + } + b := t.nameScope.lookup(a.head) + if grammar.HasSelector(a.tail) { + return a, fmt.Errorf("a path through name %q may not select a row; read it directly, {%s.%s}, or bind the row to a name of its own", a.head, b.ref, grammar.JoinSegments(a.tail)) + } + target := targets[b] + full := append(target.tail[:len(target.tail):len(target.tail)], a.tail...) + if err := checkPathResolves(target.head, full, a.head); err != nil { + return a, err + } + w := compilePath(target.head, full) + a.kind, a.named, a.steps, a.leaves = namedRead, b, w.steps, w.leaves + a.levels = make([]string, len(full)+1) + for i := range a.levels { + a.levels[i] = grammar.JoinSegments(full[:i]) + } + a.path = grammar.JoinSegments(full) + return a, nil } diff --git a/inline.go b/inline.go index 9c7203a..20ebce6 100644 --- a/inline.go +++ b/inline.go @@ -122,26 +122,16 @@ func refuseFolderRefs(scope nodeScope) error { }) } -// linkNodeRefs binds the references in an inline node's templates against the -// loaded tree. +// 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 { - return linkTemplate(nil, label, "", t, root) + ts = append(ts, linkSite{t: t, label: label}) } return nil }); err != nil { return err } - for _, pass := range linkPasses { - if err := scope(func(label string, m node) error { - if t, ok := m.(*template); ok { - return pass(label, t) - } - return nil - }); err != nil { - return err - } - } - return nil + return linkTemplates(ts, root) } diff --git a/name.go b/name.go index ce91ad0..970677c 100644 --- a/name.go +++ b/name.go @@ -20,6 +20,7 @@ type nameScope struct { } type nameBinding struct { + // Filled by `bindNames`, from the compiled category: name string ref string // what it binds, as written: a reference, or a path into a field of binder body string @@ -27,9 +28,10 @@ type nameBinding struct { scope *nameScope index int binder *template - uses []nameUse - head node - tail []string + + // Filled by `linkTemplates`, from the assembled tree: + head node + tail []string // 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 @@ -61,17 +63,16 @@ func (sc *nameScope) lookup(name string) *nameBinding { // choice's item, a name a field spells too, a read no field or name answers, and a binding // read by nothing. func bindNames(root node) error { - top := &nameScope{owner: root} - scopes := []*nameScope{top} + var scopes []*nameScope 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); n == root || isTemplate && t.repeat > 1 { + scope = &nameScope{up: scope, owner: n} + scopes = append(scopes, scope) + inChoice = false + } switch t := n.(type) { case *template: - if t.repeat > 1 && n != root { - scope = &nameScope{up: scope, owner: t} - scopes = append(scopes, scope) - inChoice = false - } t.nameScope = scope if err := scope.bindAll(t, inChoice, where); err != nil { return err @@ -82,7 +83,7 @@ func bindNames(root node) error { _, isChoice := n.(*choice) return eachContained(n, where, func(c node, where string) error { return gather(c, scope, inChoice || isChoice, where) }) } - if err := gather(root, top, false, ""); err != nil { + if err := gather(root, nil, false, ""); err != nil { return err } for _, sc := range scopes[1:] { @@ -92,15 +93,11 @@ func bindNames(root node) error { } } } - if err := resolveReads(root, "", scopes); err != nil { + reads, err := resolveReads(root, scopes) + if err != nil { return err } - for _, sc := range scopes { - if err := sc.settle(); err != nil { - return err - } - } - return nil + return checkNameUses(scopes, reads) } // bindAll binds every name t's tokens bind. @@ -133,16 +130,30 @@ func (sc *nameScope) spelled() string { return "in one repeat" } -// settle refuses a binding read by nothing, and hands a scope binding names to its owner, which -// renders a frame of it. -func (sc *nameScope) settle() error { - for _, b := range sc.order { - if len(b.uses) == 0 { - return fmt.Errorf("%stoken {%s}: nothing reads name %q; drop the token", b.where, b.body, b.name) +// 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) + } } } - if t, isTemplate := sc.owner.(*template); isTemplate && len(sc.order) > 0 { - t.ownNameScope = sc + 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 { + if sc := t.nameScope; sc != nil && sc.owner == node(t) && len(sc.order) > 0 { + return sc } return nil } @@ -162,34 +173,40 @@ func eachContained(n node, where string, fn func(c node, where string) error) er 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. scopes are every scope of -// the category, which a refusal searches for a name bound where the read cannot see it. -func resolveReads(n node, where string, scopes []*nameScope) error { - t, isTemplate := n.(*template) - if err := eachContained(n, where, func(c node, where string) error { return resolveReads(c, where, scopes) }); err != nil { - return err - } - if !isTemplate { +// 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 read cannot see it. +func resolveReads(root node, scopes []*nameScope) (map[*nameBinding][]nameUse, error) { + reads := map[*nameBinding][]nameUse{} + var resolve func(n node, where string) error + resolve = func(n node, where string) error { + t, isTemplate := n.(*template) + if err := eachContained(n, where, resolve); err != nil { + return err + } + if !isTemplate { + return nil + } + for _, name := range sortedNames(t.fields) { + if b := t.nameScope.lookup(name); b != nil { + 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 { + b := t.nameScope.lookup(u.head) + 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}) + } return nil } - for _, name := range sortedNames(t.fields) { - if b := t.nameScope.lookup(name); b != nil { - 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 { - b := t.nameScope.lookup(u.head) - if b == nil { - return unresolved(where, u, t.nameScope, scopes) - } - b.uses = append(b.uses, nameUse{tail: u.tail, in: t, operand: u.operand, noRef: u.noRef, nested: t.nameScope != b.scope}) - } - return nil + return reads, resolve(root, "") } // unresolved is the refusal of u, a read no field or name answers: naming the repeat binding the @@ -224,18 +241,6 @@ func (t *template) isName(head string) bool { return !grammar.IsRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil } -// linkBindings resolves what each of t's bindings binds to a head node and a tail path. -func linkBindings(t *template) { - for _, tok := range t.tokens { - if tok.Kind != grammar.NameBind { - continue - } - b := t.nameScope.bindings[tok.Bound] - a := splitArm(b.ref, t.link.refs) - b.head, b.tail = t.head(a.head), a.tail - } -} - // 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 { for i := range t.compiled.ops { @@ -252,45 +257,3 @@ func namedReads(t *template, fn func(o *op, a *arm) error) error { } return nil } - -// linkPasses run in order once every template is linked, each over every template before the next -// starts: a pass reads what the one before filled in. -var linkPasses = []func(label string, t *template) error{linkNames, linkColumnRead, checkNameReads, checkCalcNames} - -// linkNames compiles t's reads of a name as paths from the head its binding resolved to, once every -// template is linked, so each binder has resolved what it binds. -func linkNames(label string, t *template) error { - return namedReads(t, func(o *op, a *arm) error { - if err := linkName(t, a); err != nil { - return fmt.Errorf("%s: token {%s}: %w", label, o.Body, err) - } - return nil - }) -} - -func linkName(t *template, a *arm) error { - b := t.nameScope.lookup(a.head) - if grammar.HasSelector(a.tail) { - return fmt.Errorf("a path through name %q may not select a row; read it directly, {%s.%s}, or bind the row to a name of its own", a.head, b.ref, grammar.JoinSegments(a.tail)) - } - full := append(b.tail[:len(b.tail):len(b.tail)], a.tail...) - if err := checkPathResolves(b.head, full, a.head); err != nil { - return err - } - w := compilePath(b.head, full) - a.named, a.steps, a.leaves = b, w.steps, w.leaves - a.levels = make([]string, len(full)+1) - for i := range a.levels { - a.levels[i] = grammar.JoinSegments(full[:i]) - } - a.path = grammar.JoinSegments(full) - if b.addressed == nil { - b.addressed = map[string]string{} - } - for _, key := range append(a.levels[len(b.tail):], a.path) { - if _, seen := b.addressed[key]; !seen { - b.addressed[key] = a.spelling - } - } - return nil -} diff --git a/namefence.go b/namefence.go index 420e1b0..163f86e 100644 --- a/namefence.go +++ b/namefence.go @@ -8,18 +8,9 @@ import ( "github.com/larvit/fejkdata/internal/grammar" ) -// 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. +// 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 { - for _, tok := range t.tokens { - if tok.Kind != grammar.NameBind { - continue - } - if err := t.nameScope.bindings[tok.Bound].checkUses(); err != nil { - return fmt.Errorf("%s: token {%s}: %w", label, tok.Body, err) - } - } 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) @@ -60,13 +51,13 @@ func rendersInside(nodes []node, t *template) bool { return false } -// checkUses refuses a binding read 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() error { - r := b.uses[0] - if len(b.uses) > 1 || r.nested { +func (b *nameBinding) checkUses(uses []nameUse) error { + r := uses[0] + if len(uses) > 1 || r.nested { return nil } spelling := b.ref diff --git a/node.go b/node.go index edf134e..26b1a69 100644 --- a/node.go +++ b/node.go @@ -47,7 +47,7 @@ func (*nullItem) isNode() {} // template renders a format string, substituting {tokens} from fields. A bare // JSON string is a template with no fields. repeat (default 1) renders that format // that many times and joins the results with separator (default ""), each render -// an independent pick. Every format compiles in `linkTemplate`. +// an independent pick. Every format compiles in `linkTemplates`. type template struct { // Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`: format string @@ -61,19 +61,21 @@ type template struct { unbound []unboundRead // the heads its tokens read that no field holds // Filled by `bindNames`, from the compiled category: - nameScope *nameScope // where its tokens look a name up - ownNameScope *nameScope // the scope it renders a frame of: a category's, on its root, or a repeat's, per iteration + nameScope *nameScope // where its tokens look a name up - // Filled by `linkTemplate`, from the assembled tree: - link templateLink - compiled formatOps + // Filled by `linkTemplates`, from the assembled tree: + link templateLink + compiled formatOps + readsColumn *columnRead // set when the format is one reference or name read alone reading a record's column + + // Filled by `settleRecords`, once the load refused every cycle: + columns []recordColumn // a record's, where it has fields, in name order } // templateLink is what a template resolves to in the assembled tree. type templateLink struct { - refs map[string]refBinding // each reference the format reads -> what it resolves to - refHeads map[string]node // each refBinding.head -> the category it names - readsColumn *columnRead // set when the format is one reference or name read alone reading a record's column + refs map[string]refBinding // each reference the format reads -> what it resolves to + refHeads map[string]node // each refBinding.head -> the category it names } func (*template) isNode() {} diff --git a/pick.go b/pick.go index f86be6d..a5e997a 100644 --- a/pick.go +++ b/pick.go @@ -124,10 +124,11 @@ func (m *drawMemo) enteredFrame(scope *nameScope) *pickFrame { // renderFrame opens a fresh frame of t's scope, where t binds names and no read entering the // category opened one, returning the mark that closes it, or -1. func (sc renderScope) renderFrame(t *template) int { - if t.ownNameScope == nil || sc.frameOf(t.ownNameScope) != nil { + own := t.ownScope() + if own == nil || sc.frameOf(own) != nil { return -1 } - return sc.frames.push(newPickFrame(t.ownNameScope)) + return sc.frames.push(newPickFrame(own)) } // frameOf is the frame of scope rendering since the read entering the category, nil where none is. diff --git a/record.go b/record.go index 97fab4a..831727c 100644 --- a/record.go +++ b/record.go @@ -142,15 +142,15 @@ func (f *Generator) FakeRecord(path string) (*Record, error) { } else if len(tail) > 0 { return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0]) } - shape := f.recordShapeOf(n) - if errors.Is(shape.err, ErrNoColumns) { + record, err := recordOf(n) + if errors.Is(err, ErrNoColumns) { ns := grammar.NameSegments(segments) - return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, shape.err, ns[len(ns)-1], path) + return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, err, ns[len(ns)-1], path) } - if shape.err != nil { - return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err) + if err != nil { + return nil, fmt.Errorf("fejkdata: %s %w", path, err) } - return renderRecord(f.drawState, shape.template, shape.columns, sc), nil + return renderRecord(f.drawState, record, sc), nil } // tableRecord walks a path's tail from a table to the table whose row is the record, and that @@ -171,35 +171,11 @@ func tableRecord(s *drawstate.State, t *table, tail []string) (node, renderedRow return n, renderedRow{}, nil } -// recordShape is what recordOf settled about a node: the template to project, its -// columns, or why it is not a record. -type recordShape struct { - template *template - columns []Column - err error -} - -// recordShapeOf fences a node once and remembers the answer. Callers hold the -// generator's lock. -func (f *Generator) recordShapeOf(n node) recordShape { - if shape, done := f.records[n]; done { - return shape - } - t, columns, err := recordOf(n) - shape := recordShape{template: t, columns: columns, err: err} - if f.records == nil { - f.records = map[node]recordShape{} - } - f.records[n] = shape - return shape -} - // RecordTemplate is an inline record compiled, referenced and validated once, // ready to render many times with [RecordTemplate.Fake]. type RecordTemplate struct { g *Generator template *template - columns []Column } // Fake renders the record, one pick of each name across its columns. @@ -207,7 +183,7 @@ func (t *RecordTemplate) Fake() *Record { t.g.mu.Lock() defer t.g.mu.Unlock() var frames frameStack - return renderRecord(t.g.drawState, t.template, t.columns, renderScope{frames: &frames}) + return renderRecord(t.g.drawState, t.template, renderScope{frames: &frames}) } // NewRecordTemplate compiles an inline record — a JSON object with a format and @@ -217,11 +193,11 @@ func (f *Generator) NewRecordTemplate(input string) (*RecordTemplate, error) { if err != nil { return nil, err } - tm, columns, err := recordOf(t.n) + record, err := recordOf(t.n) if err != nil { return nil, fmt.Errorf("fejkdata: an inline record %w", err) } - return &RecordTemplate{g: f, template: tm, columns: columns}, nil + return &RecordTemplate{g: f, template: record}, nil } // FakeRecordTemplate compiles and renders an inline record in one call. @@ -237,63 +213,87 @@ func (f *Generator) FakeRecordTemplate(input string) (*Record, error) { // the record to write instead. var ErrNoColumns = errors.New("has no fields, so no columns") -// recordOf is the fence both record entry points pass. The columns come back with -// the template, fixed for every draw the caller goes on to make. -func recordOf(n node) (*template, []Column, error) { +// recordOf is the fence both record entry points pass: the record template n is, whose columns +// its link fixed for every draw. +func recordOf(n node) (*template, error) { if tb, isTable := n.(*table); isTable { n = tb.formatTemplate } t, ok := n.(*template) if !ok { - return nil, nil, errors.New("names a choice, not a template; a record is a template whose fields are its columns") + return nil, errors.New("names a choice, not a template; a record is a template whose fields are its columns") } - names := sortedNames(t.fields) - if len(names) == 0 { - return nil, nil, ErrNoColumns + if len(t.fields) == 0 { + return nil, ErrNoColumns } if !t.isRecord { - return nil, 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) + 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) } - columns := make([]Column, len(names)) - for i, name := range names { - columns[i] = Column{Name: name, DataType: columnDatatype(t.fields[name])} - } - return t, columns, nil + return t, nil } -// renderRecord draws each column once, in the name order recordOf fixed, as one render, so the -// columns read one pick of each name; a table's columns read sc's row. -func renderRecord(s *drawstate.State, t *template, columns []Column, sc renderScope) *Record { +// 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. +type recordColumn struct { + Column + field node + whole *nameBinding +} + +// settleRecords fixes the columns of every record in scope, whose datatypes a cycle would walk +// forever, so bind refuses the scope's cycles first. +func settleRecords(scope nodeScope) { + _ = scope(func(_ string, n node) error { + if t, isTemplate := n.(*template); isTemplate { + t.columns = recordColumns(t) + } + return nil + }) +} + +// recordColumns is t's columns in name order where t is a record with fields, else nil. +func recordColumns(t *template) []recordColumn { + if !t.isRecord || len(t.fields) == 0 { + return nil + } + whole := map[node]*nameBinding{} + for _, tok := range t.tokens { + if tok.Kind != grammar.NameBind { + continue + } + if b := t.nameScope.bindings[tok.Bound]; b.bindsField() && len(b.tail) == 0 && whole[b.head] == nil { + whole[b.head] = b + } + } + names := sortedNames(t.fields) + 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]} + } + return columns +} + +// renderRecord draws each column of t once, in name order, as one render, so the columns read one +// pick of each name; a table's columns read sc's row. +func renderRecord(s *drawstate.State, t *template, sc renderScope) *Record { if mark := sc.renderFrame(t); mark >= 0 { defer sc.frames.pop(mark) } - r := &Record{columns: append([]Column(nil), columns...)} - for i := range r.columns { - field := t.fields[r.columns[i].Name] + r := &Record{columns: make([]Column, len(t.columns))} + for i, c := range t.columns { var column readValue - if b := bindingOfWhole(t, field); b != nil { - column = readName(s, sc, arm{kind: namedRead, named: b, levels: wholeLevels}) + if c.whole != nil { + column = readName(s, sc, arm{kind: namedRead, named: c.whole, levels: wholeLevels}) } else { - column = renderLeaf(s, field, sc) + column = renderLeaf(s, c.field, sc) } + r.columns[i] = c.Column r.columns[i].Value, r.columns[i].Null = column.text, column.null } return r } -// wholeLevels is the levels of a read of a whole name, as linkName compiles {n}. +// wholeLevels is the levels of a read of a whole name, as compileArm compiles {n}. var wholeLevels = []string{""} - -// bindingOfWhole is the name t binds to the whole of field, whose pick the field's column renders -// so the row agrees with the columns reading the name; nil where none does. -func bindingOfWhole(t *template, field node) *nameBinding { - if t.ownNameScope == nil { - return nil - } - for _, b := range t.ownNameScope.order { - if b.bindsField() && b.binder == t && len(b.tail) == 0 && b.head == field { - return b - } - } - return nil -} diff --git a/reference.go b/reference.go index bba3c92..0c440ce 100644 --- a/reference.go +++ b/reference.go @@ -2,10 +2,10 @@ package fejkdata import ( "fmt" + "slices" "strings" "github.com/larvit/fejkdata/internal/grammar" - "github.com/larvit/fejkdata/internal/invariant" ) // refBinding is what a reference resolves to: the head its category is held @@ -35,59 +35,106 @@ func refSegments(name string, folder []string) ([]string, error) { return append(append([]string{}, base...), segs...), nil } -// linkRefs binds every reference and compiles every format, once all data is merged, -// so a reference sees the override-resolved tree. A reference's head binds into -// refHeads under its root path and its tail reads like a sibling path. +// 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 { - return linkTemplate(s.dir, label, s.path, t, root) + ts = append(ts, linkSite{t: t, folder: s.dir, category: s.path, label: label}) + return nil }); err != nil { return err } - for _, pass := range linkPasses { - if err := eachTemplate(sites, func(_ categorySite, label string, t *template) error { return pass(label, t) }); err != nil { + 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 } -// linkTemplate resolves t's references in category, "" for an inline template, and -// compiles its format against them. -func linkTemplate(folder []string, label, category string, t *template, root map[string]node) error { - link, err := t.resolveLink(folder, label, category, root) - if err != nil { - return err - } - t.link, t.compiled = link, compileFormat(t.tokens, link.refs, t.isName) - linkBindings(t) - compileArms(t) - return nil -} - -func compileArms(t *template) { - for i := range t.compiled.ops { - o := &t.compiled.ops[i] - for j := range o.operands { - if o.operands[j].kind != namedRead { - compileArm(t, &o.operands[j]) - } - } - for j := range o.arms { - if o.arms[j].kind != namedRead { - compileArm(t, &o.arms[j]) +// 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 } -func compileArm(t *template, a *arm) { - head := t.head(a.head) - if head == nil { - panic(invariant.Broken("{%s} reads a head nothing bound", a.spelling)) +// 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 + } + } + } + } } - w := compilePath(head, a.tail) - a.steps, a.leaves = w.steps, w.leaves + return keys } // resolveLink binds every reference t reads, refusing one to t's own category: @@ -132,9 +179,9 @@ type columnRead struct { column node } -// linkColumnRead sets the record's column t's format reads, where the format only reads one -// reference or name, once the names are linked. -func linkColumnRead(_ string, t *template) error { +// 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 @@ -152,9 +199,8 @@ func linkColumnRead(_ string, t *template) error { case !grammar.IsRef(a.head): return nil } - target, isTemplate := head.(*template) - if isTemplate && target.isRecord && len(tail) == 1 { - t.link.readsColumn = &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]} + 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 } diff --git a/render.go b/render.go index 76328b8..0d2f35d 100644 --- a/render.go +++ b/render.go @@ -93,15 +93,16 @@ func render(s *drawstate.State, n node, sc renderScope) string { func renderRepeat(s *drawstate.State, t *template, sc renderScope) string { var b strings.Builder b.Grow(t.repeat * (t.compiled.grow + len(t.separator))) + own := t.ownScope() for i := 0; i < t.repeat; i++ { if i > 0 { b.WriteString(t.separator) } - if t.ownNameScope == nil { + if own == nil { b.WriteString(expand(s, t, sc)) continue } - mark := sc.frames.push(newPickFrame(t.ownNameScope)) + mark := sc.frames.push(newPickFrame(own)) b.WriteString(expand(s, t, sc)) sc.frames.pop(mark) } @@ -172,8 +173,8 @@ func renderLeaf(s *drawstate.State, n node, sc renderScope) readValue { case *nullItem: return readValue{null: true} case *template: - if leaf.link.readsColumn != nil { - return readField(s, leaf, sc, leaf.link.readsColumn.a) + if leaf.readsColumn != nil { + return readField(s, leaf, sc, leaf.readsColumn.a) } } return readValue{text: render(s, n, sc)} diff --git a/shape_test.go b/shape_test.go index d478c16..65b27e2 100644 --- a/shape_test.go +++ b/shape_test.go @@ -45,8 +45,8 @@ func shippedShape(f *Generator) string { } case *template: facts[prefix] = "\tformat " + strconv.Quote(n.format) + readsFact(n) - if _, columns, err := recordOf(n); err == nil { - for _, c := range columns { + 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 += " null" diff --git a/struct.go b/struct.go index 7b1d199..98bea52 100644 --- a/struct.go +++ b/struct.go @@ -70,7 +70,6 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) { // index path each column fills, and the struct fields carrying tags of their own. type structShape struct { record *template - columns []Column fieldIndexes [][]int nested []nestedStruct } @@ -266,20 +265,20 @@ func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, if err := bindInline(root, n, label, true); err != nil { return err } - record, columns, err := recordOf(n) + record, err := recordOf(n) if err != nil { return fmt.Errorf("%s: %w", label, err) } proof := &valueProof{} - s.fieldIndexes = make([][]int, len(columns)) - for i, c := range columns { + 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 { return err } s.fieldIndexes[i] = sf.Index } - s.record, s.columns = record, columns + s.record = record return nil } @@ -324,7 +323,7 @@ func (k columnKind) holds(v proven.Facts) bool { func (s *structShape) fill(state *drawstate.State, v reflect.Value) { if s.record != nil { var frames frameStack - for i, c := range renderRecord(state, s.record, s.columns, renderScope{frames: &frames}).columns { + for i, c := range renderRecord(state, s.record, renderScope{frames: &frames}).columns { setColumn(fieldAt(v, s.fieldIndexes[i]), c) } } diff --git a/todo.md b/todo.md index 039bc7d..279d372 100644 --- a/todo.md +++ b/todo.md @@ -247,7 +247,7 @@ Until a panel scores 7.0 or above, only items 104 to 106 and the exceptions in ` ### 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. -A calc operand parses as a bare identifier, so `{calc(c.lat * 60)}` fails with `unexpected ".lat * 60"`, and a second binding of `/misc.coordinate.lat` would draw another coordinate. Resolve the path through `linkName`'s leaves, as `operandNodes` does for a bare name. +A calc operand parses as a bare identifier, so `{calc(c.lat * 60)}` fails with `unexpected ".lat * 60"`, and a second binding of `/misc.coordinate.lat` would draw another coordinate. Resolve the path through `compileArm`'s leaves, as `operandNodes` does for a bare name. ### 89. Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system. diff --git a/valueproof.go b/valueproof.go index 020e296..209a1d9 100644 --- a/valueproof.go +++ b/valueproof.go @@ -24,7 +24,7 @@ func (p *valueProof) checkDatatype(label string, n node) error { if !ok || t.datatype == DataTypeString { return nil } - if r := t.link.readsColumn; r != nil && columnDatatype(r.column) == t.datatype { + if r := t.readsColumn; r != nil && columnDatatype(r.column) == t.datatype { return fmt.Errorf(`%s: %s takes datatype %s from the column it reads; drop "datatype"`, label, t.format, t.datatype) } if reason := p.proveColumnItem(t).Not[t.datatype]; reason != "" { @@ -66,10 +66,10 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro // proveColumnItem proves a column item: what it renders, or, when it is a column it reads, that column. func (p *valueProof) proveColumnItem(t *template) proven.Facts { - if t.link.readsColumn == nil { + if t.readsColumn == nil { return p.prove(t) } - return p.proveColumn(t.link.readsColumn.column) + return p.proveColumn(t.readsColumn.column) } // proveColumn proves a column over what its items draw, a null item marking it null rather than