Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
16 changed files with 339 additions and 313 deletions
Showing only changes of commit cd7f0a04dc - Show all commits
+1
View File
@@ -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
+3 -3
View File
@@ -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
}
+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
// 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
}
+65 -32
View File
@@ -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
}
+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
// 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)
}
+69 -106
View File
@@ -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
}
+7 -16
View File
@@ -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
+11 -9
View File
@@ -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() {}
+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
// 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.
+70 -70
View File
@@ -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
}
+89 -43
View File
@@ -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
}
+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 {
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)}
+2 -2
View File
@@ -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"
+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.
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)
}
}
+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.
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.
+3 -3
View File
@@ -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