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

This commit is contained in:
2026-10-05 22:21:10 +02:00
committed by Lillem4n
parent cdc82a99c6
commit cd7f0a04dc
16 changed files with 339 additions and 313 deletions
+1
View File
@@ -148,6 +148,7 @@ func (b binding) bind() error {
if err := checkNoCycles(b.scope); err != nil { if err := checkNoCycles(b.scope); err != nil {
return err return err
} }
settleRecords(b.scope)
if !b.typedByGo { if !b.typedByGo {
if err := checkColumns(b.scope); err != nil { if err := checkColumns(b.scope); err != nil {
return err return err
+3 -3
View File
@@ -67,7 +67,7 @@ func checkColumns(s nodeScope) error {
checked[column] = true checked[column] = true
items, _ := columnItems(column) items, _ := columnItems(column)
for _, it := range items { 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 { if err := check(r.category, r.field, r.column); err != nil {
return err 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. // itemDatatype is the datatype a column item declares, else that of the column it reads.
func itemDatatype(t *template) DataType { func itemDatatype(t *template) DataType {
if t.datatype == DataTypeString && t.link.readsColumn != nil { if t.datatype == DataTypeString && t.readsColumn != nil {
return columnDatatype(t.link.readsColumn.column) return columnDatatype(t.readsColumn.column)
} }
return t.datatype return t.datatype
} }
+1 -2
View File
@@ -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 // It is safe for concurrent use; a seeded sequence is reproducible only when drawn
// from one goroutine. // from one goroutine.
type Generator struct { 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. // first call reaching it.
mu sync.Mutex mu sync.Mutex
drawState *drawstate.State drawState *drawstate.State
root folder // the categories as the node a path walks from, owned here so a walk allocates none 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 structs map[reflect.Type]structResult
} }
+65 -32
View File
@@ -6,6 +6,7 @@ import (
"github.com/larvit/fejkdata/internal/builtinfunc" "github.com/larvit/fejkdata/internal/builtinfunc"
"github.com/larvit/fejkdata/internal/grammar" "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 // 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 // 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 { func splitArm(name string, refs map[string]refBinding) arm {
if grammar.IsRef(name) { if grammar.IsRef(name) {
b, linked := refs[name] b, linked := refs[name]
@@ -278,43 +279,75 @@ type formatOps struct {
grow int grow int
} }
func (c *formatOps) function(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) { // compileFormat compiles t's parsed format against its link and its names' targets; a {ref as n}
var operands []arm // token compiles to no op. checkTokens proved every token valid.
for _, operand := range builtinfunc.Operands(tok.Fn, tok.Args) { func compileFormat(t *template, targets map[*nameBinding]nameTarget) (formatOps, error) {
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 {
var c formatOps var c formatOps
for _, tok := range toks { for _, tok := range t.tokens {
o := op{Token: tok}
var err error
switch tok.Kind { switch tok.Kind {
case grammar.LiteralRun: case grammar.LiteralRun:
c.grow += len(tok.Lit) c.grow += len(tok.Lit)
c.ops = append(c.ops, op{Token: tok})
case grammar.BuiltinCall: 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: 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)
} }
return c c.ops = append(c.ops, o)
}
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
} }
+4 -14
View File
@@ -122,26 +122,16 @@ func refuseFolderRefs(scope nodeScope) error {
}) })
} }
// linkNodeRefs binds the references in an inline node's templates against the // linkNodeRefs links the templates of an inline node against the loaded tree.
// loaded tree.
func linkNodeRefs(scope nodeScope, root map[string]node) error { func linkNodeRefs(scope nodeScope, root map[string]node) error {
var ts []linkSite
if err := scope(func(label string, m node) error { if err := scope(func(label string, m node) error {
if t, ok := m.(*template); ok { if t, ok := m.(*template); ok {
return linkTemplate(nil, label, "", t, root) ts = append(ts, linkSite{t: t, label: label})
} }
return nil return nil
}); err != nil { }); err != nil {
return err return err
} }
for _, pass := range linkPasses { return linkTemplates(ts, root)
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
} }
+44 -81
View File
@@ -20,6 +20,7 @@ type nameScope struct {
} }
type nameBinding struct { type nameBinding struct {
// Filled by `bindNames`, from the compiled category:
name string name string
ref string // what it binds, as written: a reference, or a path into a field of binder ref string // what it binds, as written: a reference, or a path into a field of binder
body string body string
@@ -27,7 +28,8 @@ type nameBinding struct {
scope *nameScope scope *nameScope
index int index int
binder *template binder *template
uses []nameUse
// Filled by `linkTemplates`, from the assembled tree:
head node head node
tail []string tail []string
// addressed is every key a read of the name lands on or passes, the spelling of the // addressed is every key a read of the name lands on or passes, the spelling of the
@@ -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 // choice's item, a name a field spells too, a read no field or name answers, and a binding
// read by nothing. // read by nothing.
func bindNames(root node) error { func bindNames(root node) error {
top := &nameScope{owner: root} var scopes []*nameScope
scopes := []*nameScope{top}
var gather func(n node, scope *nameScope, inChoice bool, 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 { gather = func(n node, scope *nameScope, inChoice bool, where string) error {
switch t := n.(type) { if t, isTemplate := n.(*template); n == root || isTemplate && t.repeat > 1 {
case *template: scope = &nameScope{up: scope, owner: n}
if t.repeat > 1 && n != root {
scope = &nameScope{up: scope, owner: t}
scopes = append(scopes, scope) scopes = append(scopes, scope)
inChoice = false inChoice = false
} }
switch t := n.(type) {
case *template:
t.nameScope = scope t.nameScope = scope
if err := scope.bindAll(t, inChoice, where); err != nil { if err := scope.bindAll(t, inChoice, where); err != nil {
return err return err
@@ -82,7 +83,7 @@ func bindNames(root node) error {
_, isChoice := n.(*choice) _, isChoice := n.(*choice)
return eachContained(n, where, func(c node, where string) error { return gather(c, scope, inChoice || isChoice, where) }) 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 return err
} }
for _, sc := range scopes[1:] { 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 return err
} }
for _, sc := range scopes { return checkNameUses(scopes, reads)
if err := sc.settle(); err != nil {
return err
}
}
return nil
} }
// bindAll binds every name t's tokens bind. // bindAll binds every name t's tokens bind.
@@ -133,16 +130,30 @@ func (sc *nameScope) spelled() string {
return "in one repeat" return "in one repeat"
} }
// settle refuses a binding read by nothing, and hands a scope binding names to its owner, which // checkNameUses refuses a binding read by nothing, then each binding checkUses refuses.
// renders a frame of it. func checkNameUses(scopes []*nameScope, reads map[*nameBinding][]nameUse) error {
func (sc *nameScope) settle() error { for _, sc := range scopes {
for _, b := range sc.order { for _, b := range sc.order {
if len(b.uses) == 0 { if len(reads[b]) == 0 {
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 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 return nil
} }
@@ -162,12 +173,16 @@ func eachContained(n node, where string, fn func(c node, where string) error) er
return nil return nil
} }
// resolveReads answers each unbound read under n with a name its template sees, fields before the // resolveReads answers each unbound read under root with a name its template sees, fields before
// format as compile reports them, and refuses a field spelling a name. scopes are every scope of // the format as compile reports them, returning every binding's reads, and refuses a field spelling
// the category, which a refusal searches for a name bound where the read cannot see it. // a name. scopes are every scope of the category, which a refusal searches for a name bound where
func resolveReads(n node, where string, scopes []*nameScope) error { // 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) t, isTemplate := n.(*template)
if err := eachContained(n, where, func(c node, where string) error { return resolveReads(c, where, scopes) }); err != nil { if err := eachContained(n, where, resolve); err != nil {
return err return err
} }
if !isTemplate { if !isTemplate {
@@ -187,9 +202,11 @@ func resolveReads(n node, where string, scopes []*nameScope) error {
if b == nil { if b == nil {
return unresolved(where, u, t.nameScope, scopes) 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}) reads[b] = append(reads[b], nameUse{tail: u.tail, in: t, operand: u.operand, noRef: u.noRef, nested: t.nameScope != b.scope})
} }
return nil return nil
}
return reads, resolve(root, "")
} }
// unresolved is the refusal of u, a read no field or name answers: naming the repeat binding the // 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 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. // 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 { func namedReads(t *template, fn func(o *op, a *arm) error) error {
for i := range t.compiled.ops { for i := range t.compiled.ops {
@@ -252,45 +257,3 @@ func namedReads(t *template, fn func(o *op, a *arm) error) error {
} }
return nil 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
}
+7 -16
View File
@@ -8,18 +8,9 @@ import (
"github.com/larvit/fejkdata/internal/grammar" "github.com/larvit/fejkdata/internal/grammar"
) )
// checkNameReads refuses each binding of t that checkUses refuses, a read of a name inside the // checkNameReads refuses a read of a name inside the field bound to it, and a read {n} beside {n.w}
// field bound to it, and a read {n} beside {n.w} where n's pick renders w twice: the pick keeps one // where n's pick renders w twice: the pick keeps one draw of w.
// draw of w.
func checkNameReads(label string, t *template) error { 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 { return namedReads(t, func(o *op, a *arm) error {
if a.named.bindsField() && rendersInside(compilePath(a.named.head, a.named.tail).leaves, t) { 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) 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 return false
} }
// checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling // checkUses refuses a binding whose uses read it once at a spot the bound spelling can stand: that
// draws the same way without the name. A bound field can stand in only where the reading // 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 // 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. // of one reference alone, that entry point's own refusal then names the bare path.
func (b *nameBinding) checkUses() error { func (b *nameBinding) checkUses(uses []nameUse) error {
r := b.uses[0] r := uses[0]
if len(b.uses) > 1 || r.nested { if len(uses) > 1 || r.nested {
return nil return nil
} }
spelling := b.ref spelling := b.ref
+6 -4
View File
@@ -47,7 +47,7 @@ func (*nullItem) isNode() {}
// template renders a format string, substituting {tokens} from fields. A bare // 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 // 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 // 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 { type template struct {
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`: // Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
format string format string
@@ -62,18 +62,20 @@ type template struct {
// Filled by `bindNames`, from the compiled category: // Filled by `bindNames`, from the compiled category:
nameScope *nameScope // where its tokens look a name up 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
// Filled by `linkTemplate`, from the assembled tree: // Filled by `linkTemplates`, from the assembled tree:
link templateLink link templateLink
compiled formatOps 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. // templateLink is what a template resolves to in the assembled tree.
type templateLink struct { type templateLink struct {
refs map[string]refBinding // each reference the format reads -> what it resolves to refs map[string]refBinding // each reference the format reads -> what it resolves to
refHeads map[string]node // each refBinding.head -> the category it names 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
} }
func (*template) isNode() {} func (*template) isNode() {}
+3 -2
View File
@@ -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 // 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. // category opened one, returning the mark that closes it, or -1.
func (sc renderScope) renderFrame(t *template) int { 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 -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. // frameOf is the frame of scope rendering since the read entering the category, nil where none is.
+70 -70
View File
@@ -142,15 +142,15 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
} else if len(tail) > 0 { } 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]) 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) record, err := recordOf(n)
if errors.Is(shape.err, ErrNoColumns) { if errors.Is(err, ErrNoColumns) {
ns := grammar.NameSegments(segments) 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 { if err != nil {
return nil, fmt.Errorf("fejkdata: %s %w", path, shape.err) 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 // 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 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, // RecordTemplate is an inline record compiled, referenced and validated once,
// ready to render many times with [RecordTemplate.Fake]. // ready to render many times with [RecordTemplate.Fake].
type RecordTemplate struct { type RecordTemplate struct {
g *Generator g *Generator
template *template template *template
columns []Column
} }
// Fake renders the record, one pick of each name across its columns. // 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() t.g.mu.Lock()
defer t.g.mu.Unlock() defer t.g.mu.Unlock()
var frames frameStack 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 // 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 { if err != nil {
return nil, err return nil, err
} }
tm, columns, err := recordOf(t.n) record, err := recordOf(t.n)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: an inline record %w", err) 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. // 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. // the record to write instead.
var ErrNoColumns = errors.New("has no fields, so no columns") var ErrNoColumns = errors.New("has no fields, so no columns")
// recordOf is the fence both record entry points pass. The columns come back with // recordOf is the fence both record entry points pass: the record template n is, whose columns
// the template, fixed for every draw the caller goes on to make. // its link fixed for every draw.
func recordOf(n node) (*template, []Column, error) { func recordOf(n node) (*template, error) {
if tb, isTable := n.(*table); isTable { if tb, isTable := n.(*table); isTable {
n = tb.formatTemplate n = tb.formatTemplate
} }
t, ok := n.(*template) t, ok := n.(*template)
if !ok { 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(t.fields) == 0 {
if len(names) == 0 { return nil, ErrNoColumns
return nil, nil, ErrNoColumns
} }
if !t.isRecord { 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)) return t, nil
for i, name := range names {
columns[i] = Column{Name: name, DataType: columnDatatype(t.fields[name])}
}
return t, columns, nil
} }
// renderRecord draws each column once, in the name order recordOf fixed, as one render, so the // recordColumn is one column of a record template: what it writes, the field drawing it, and the
// columns read one pick of each name; a table's columns read sc's row. // name the template binds to that whole field, whose pick the column renders so the row agrees
func renderRecord(s *drawstate.State, t *template, columns []Column, sc renderScope) *Record { // 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 { if mark := sc.renderFrame(t); mark >= 0 {
defer sc.frames.pop(mark) defer sc.frames.pop(mark)
} }
r := &Record{columns: append([]Column(nil), columns...)} r := &Record{columns: make([]Column, len(t.columns))}
for i := range r.columns { for i, c := range t.columns {
field := t.fields[r.columns[i].Name]
var column readValue var column readValue
if b := bindingOfWhole(t, field); b != nil { if c.whole != nil {
column = readName(s, sc, arm{kind: namedRead, named: b, levels: wholeLevels}) column = readName(s, sc, arm{kind: namedRead, named: c.whole, levels: wholeLevels})
} else { } 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 r.columns[i].Value, r.columns[i].Null = column.text, column.null
} }
return r 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{""} 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
}
+87 -41
View File
@@ -2,10 +2,10 @@ package fejkdata
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
"github.com/larvit/fejkdata/internal/grammar" "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 // 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 return append(append([]string{}, base...), segs...), nil
} }
// linkRefs binds every reference and compiles every format, once all data is merged, // linkRefs links every template of the categories, once all data is merged, so a reference sees
// so a reference sees the override-resolved tree. A reference's head binds into // the override-resolved tree.
// refHeads under its root path and its tail reads like a sibling path.
func linkRefs(sites []categorySite, root map[string]node) error { func linkRefs(sites []categorySite, root map[string]node) error {
var ts []linkSite
if err := eachTemplate(sites, func(s categorySite, label string, t *template) error { 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 { }); err != nil {
return err return err
} }
for _, pass := range linkPasses { return linkTemplates(ts, root)
if err := eachTemplate(sites, func(_ categorySite, label string, t *template) error { return pass(label, t) }); err != nil {
return err
}
}
return nil
} }
// linkTemplate resolves t's references in category, "" for an inline template, and // linkSite is a template to link, with the folder and category it sits in, "" for an inline
// compiles its format against them. // template, and eachNode's label for it.
func linkTemplate(folder []string, label, category string, t *template, root map[string]node) error { type linkSite struct {
link, err := t.resolveLink(folder, label, category, root) 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 { if err != nil {
return err return err
} }
t.link, t.compiled = link, compileFormat(t.tokens, link.refs, t.isName) s.t.link = link
linkBindings(t) }
compileArms(t) 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 return nil
} }
func compileArms(t *template) { // nameTargets resolves what each binding of ts binds, from its binder's link.
for i := range t.compiled.ops { func nameTargets(ts []linkSite) map[*nameBinding]nameTarget {
o := &t.compiled.ops[i] targets := map[*nameBinding]nameTarget{}
for j := range o.operands { for _, s := range ts {
if o.operands[j].kind != namedRead { for _, tok := range s.t.tokens {
compileArm(t, &o.operands[j]) 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}
for j := range o.arms {
if o.arms[j].kind != namedRead {
compileArm(t, &o.arms[j])
} }
} }
} }
return targets
} }
func compileArm(t *template, a *arm) { // addressedKeys is every key the reads of each name in ts land on or pass, from the name, with the
head := t.head(a.head) // spelling of the first read reaching it.
if head == nil { func addressedKeys(ts []linkSite) map[*nameBinding]map[string]string {
panic(invariant.Broken("{%s} reads a head nothing bound", a.spelling)) 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
} }
w := compilePath(head, a.tail) if keys[a.named] == nil {
a.steps, a.leaves = w.steps, w.leaves 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: // resolveLink binds every reference t reads, refusing one to t's own category:
@@ -132,9 +179,9 @@ type columnRead struct {
column node column node
} }
// linkColumnRead sets the record's column t's format reads, where the format only reads one // columnReadOf is the record's column t's format reads, where the format only reads one reference
// reference or name, once the names are linked. // or name, and nil where it does not.
func linkColumnRead(_ string, t *template) error { func columnReadOf(t *template) *columnRead {
ops := t.compiled.ops ops := t.compiled.ops
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.NameRead || len(ops[0].arms) != 1 { if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.NameRead || len(ops[0].arms) != 1 {
return nil return nil
@@ -152,9 +199,8 @@ func linkColumnRead(_ string, t *template) error {
case !grammar.IsRef(a.head): case !grammar.IsRef(a.head):
return nil return nil
} }
target, isTemplate := head.(*template) if target, isTemplate := head.(*template); isTemplate && target.isRecord && len(tail) == 1 {
if isTemplate && target.isRecord && len(tail) == 1 { return &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
t.link.readsColumn = &columnRead{a: a, category: category, field: tail[0], column: target.fields[tail[0]]}
} }
return nil return nil
} }
+5 -4
View File
@@ -93,15 +93,16 @@ func render(s *drawstate.State, n node, sc renderScope) string {
func renderRepeat(s *drawstate.State, t *template, sc renderScope) string { func renderRepeat(s *drawstate.State, t *template, sc renderScope) string {
var b strings.Builder var b strings.Builder
b.Grow(t.repeat * (t.compiled.grow + len(t.separator))) b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
own := t.ownScope()
for i := 0; i < t.repeat; i++ { for i := 0; i < t.repeat; i++ {
if i > 0 { if i > 0 {
b.WriteString(t.separator) b.WriteString(t.separator)
} }
if t.ownNameScope == nil { if own == nil {
b.WriteString(expand(s, t, sc)) b.WriteString(expand(s, t, sc))
continue continue
} }
mark := sc.frames.push(newPickFrame(t.ownNameScope)) mark := sc.frames.push(newPickFrame(own))
b.WriteString(expand(s, t, sc)) b.WriteString(expand(s, t, sc))
sc.frames.pop(mark) sc.frames.pop(mark)
} }
@@ -172,8 +173,8 @@ func renderLeaf(s *drawstate.State, n node, sc renderScope) readValue {
case *nullItem: case *nullItem:
return readValue{null: true} return readValue{null: true}
case *template: case *template:
if leaf.link.readsColumn != nil { if leaf.readsColumn != nil {
return readField(s, leaf, sc, leaf.link.readsColumn.a) return readField(s, leaf, sc, leaf.readsColumn.a)
} }
} }
return readValue{text: render(s, n, sc)} return readValue{text: render(s, n, sc)}
+2 -2
View File
@@ -45,8 +45,8 @@ func shippedShape(f *Generator) string {
} }
case *template: case *template:
facts[prefix] = "\tformat " + strconv.Quote(n.format) + readsFact(n) facts[prefix] = "\tformat " + strconv.Quote(n.format) + readsFact(n)
if _, columns, err := recordOf(n); err == nil { if record, err := recordOf(n); err == nil {
for _, c := range columns { for _, c := range record.columns {
fact := "\t" + c.DataType.String() fact := "\t" + c.DataType.String()
if _, nullable := columnItems(n.fields[c.Name]); nullable { if _, nullable := columnItems(n.fields[c.Name]); nullable {
fact += " null" fact += " null"
+5 -6
View File
@@ -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. // index path each column fills, and the struct fields carrying tags of their own.
type structShape struct { type structShape struct {
record *template record *template
columns []Column
fieldIndexes [][]int fieldIndexes [][]int
nested []nestedStruct 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 { if err := bindInline(root, n, label, true); err != nil {
return err return err
} }
record, columns, err := recordOf(n) record, err := recordOf(n)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", label, err) return fmt.Errorf("%s: %w", label, err)
} }
proof := &valueProof{} proof := &valueProof{}
s.fieldIndexes = make([][]int, len(columns)) s.fieldIndexes = make([][]int, len(record.columns))
for i, c := range columns { for i, c := range record.columns {
sf, _ := t.FieldByName(c.Name) sf, _ := t.FieldByName(c.Name)
if err := proof.checkField(label+"."+c.Name, sf.Type, record.fields[c.Name]); err != nil { if err := proof.checkField(label+"."+c.Name, sf.Type, record.fields[c.Name]); err != nil {
return err return err
} }
s.fieldIndexes[i] = sf.Index s.fieldIndexes[i] = sf.Index
} }
s.record, s.columns = record, columns s.record = record
return nil return nil
} }
@@ -324,7 +323,7 @@ func (k columnKind) holds(v proven.Facts) bool {
func (s *structShape) fill(state *drawstate.State, v reflect.Value) { func (s *structShape) fill(state *drawstate.State, v reflect.Value) {
if s.record != nil { if s.record != nil {
var frames frameStack 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) setColumn(fieldAt(v, s.fieldIndexes[i]), c)
} }
} }
+1 -1
View File
@@ -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. ### 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. ### 89. Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.
+3 -3
View File
@@ -24,7 +24,7 @@ func (p *valueProof) checkDatatype(label string, n node) error {
if !ok || t.datatype == DataTypeString { if !ok || t.datatype == DataTypeString {
return nil 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) 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 != "" { 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. // 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 { func (p *valueProof) proveColumnItem(t *template) proven.Facts {
if t.link.readsColumn == nil { if t.readsColumn == nil {
return p.prove(t) 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 // proveColumn proves a column over what its items draw, a null item marking it null rather than