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
21 changed files with 271 additions and 1443 deletions
Showing only changes of commit 6aebeebb72 - Show all commits
+5 -8
View File
@@ -1015,22 +1015,19 @@ doc.go the package doc, and the vocabulary the package is written in
fejkdata.go Generator, New, options, List fejkdata.go Generator, New, options, List
shipped.go the embedded data set, and loading a shipped category on the first call reaching it shipped.go the embedded data set, and loading a shipped category on the first call reaching it
shippedindex.go the index of shipped categories, generated from data/ shippedindex.go the index of shipped categories, generated from data/
node.go the node model and JSON -> node compilation, with its fence on a nested drawGroup node.go the node model and JSON -> node compilation
table.go tables: the rows TSV, its options and links, row selection and draws table.go tables: the rows TSV, its options and links, row selection and draws
name.go names: {ref as n} bindings, the scopes they live in, and the reads they answer name.go names: {x as n} bindings, the scopes they live in, and the reads they answer
namefence.go the load fences over names namefence.go the load fences over names
pick.go a render's named picks: the frames of the name scopes rendering, and the reads kept under a name pick.go a render's named picks: the frames of the name scopes rendering, and the reads kept under a name
path.go the dotted-path walks with their selectors, and proving a path resolves path.go the dotted-path walks with their selectors, and proving a path resolves
render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
struct.go structs: FakeStruct, fake tags, and a field's Go type as its column's datatype struct.go structs: FakeStruct, fake tags, the one pick a struct's path tags into one category read, and a field's Go type as its column's datatype
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
format.go the {token} grammar: scanning, tokens, operands, validation, compiling a format format.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
draws.go the memo a hold, draw group or named pick keeps its draws in; and a render's draws and the scope it reads its reference paths and names through draws.go the memo a named pick keeps its draws in, and the scope a render reads names and rows through
holdfence.go the load fences over the hold: the routes and spellings that read one held name two ways pins.go the pin set: the table rows one path or one named pick fixes
drawfence.go the load fences over reference draws and their refusals: a drawGroup's, a render's and a record's columns judged on what they read, and a table reaching its own family
reads.go the load-time fold of what a render reads, and what a path reads of a table family
pins.go the pin set: the table rows a render fixes, which the draw fences replay
reference.go reference sigils, and resolving references across the tree reference.go reference sigils, and resolving references across the tree
graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences
builtins.go the {name()} function registry and its implementations builtins.go the {name()} function registry and its implementations
+4 -26
View File
@@ -259,8 +259,10 @@ func (b binding) bind() error {
if err := b.link(); err != nil { if err := b.link(); err != nil {
return err return err
} }
if err := b.scopeFence(); err != nil { if b.scopeFence != nil {
return err if err := b.scopeFence(); err != nil {
return err
}
} }
if !b.typedByGo { if !b.typedByGo {
if err := checkColumns(b.scope); err != nil { if err := checkColumns(b.scope); err != nil {
@@ -280,29 +282,5 @@ func checkNodeFences(s nodeScope) error {
if err := s(func(path string, n node) error { return repeatCheck(path, n, mem) }); err != nil { if err := s(func(path string, n node) error { return repeatCheck(path, n, mem) }); err != nil {
return err return err
} }
if err := s(heldCheck); err != nil {
return err
}
fence := &drawFence{fold: newReadFold()}
refs := false
if err := s(func(path string, n node) error {
if t, ok := n.(*template); ok && len(t.link.refs) > 0 {
refs = true
}
return fence.checkDrawGroup(path, n)
}); err != nil {
return err
}
if refs {
if err := s(checkOwnFamily); err != nil {
return err
}
if err := s(fence.checkDraws); err != nil {
return err
}
if err := s(fence.checkRecordDraws); err != nil {
return err
}
}
return s((&valueProof{}).checkDatatype) return s((&valueProof{}).checkDatatype)
} }
+7 -18
View File
@@ -17,30 +17,19 @@ package fejkdata
// Vocabulary // Vocabulary
// //
// - draw — one pick from a choice, or one row taken from a table: `pick`, // - draw — one pick from a choice, or one row taken from a table: `pick`,
// `resolveChoice`, `table.drawRow`. // `resolveChoice`, `table.drawRow`. Every {…} draws afresh, bar a read of a name.
// - expansion — one render of one format: `expand`. // - expansion — one render of one format: `expand`.
// - render — what owns one `renderDraws`, and so what one reference draw spans: // - render — one walk of the tree from an entry point, over one `frameStack`:
// `Generator.Fake`, `FakeRecord`, `Template.Fake` and `RecordTemplate.Fake` // `Generator.Fake`, `FakeRecord`, `Template.Fake` and `RecordTemplate.Fake`
// start one each, `FakeStruct` one per record, and `expandAnew` one per // start one each, and `FakeStruct` one per record.
// iteration of a `template.repeat`.
// - hold — keeping one draw of a field, so every route to it reads that value:
// an expansion holds a sibling path's head and an operand's field, its draw
// group a reference path's: `formatOps.held` and `heldCheck` an expansion's,
// `readReference` a draw group's.
// - named pick — the draw a token such as {/person as p} binds to a name, drawn // - named pick — the draw a token such as {/person as p} binds to a name, drawn
// on its first read and kept while its scope renders: a category, or one // on its first read and kept while its scope renders: a category, or one
// repeat iteration: `nameBinding`, `nameScope`, `namedPick`, `pickFrame`. // repeat iteration: `nameBinding`, `nameScope`, `namedPick`, `pickFrame`.
// - memo — what a hold, a draw group or a named pick keeps its draws in, the // - memo — what a named pick keeps its draws in, the variant drawn at each level
// variant drawn at each level and the value each path read: `drawMemo`, // and the value each path read: `drawMemo`.
// `readMemo`. // - pin — fixing which row of a table one path, or one named pick, uses:
// - draw group — reference draws kept apart inside one render:
// `template.drawGroup` as the data spells it, `templateLink.drawGroupKey` as a
// render reads it, `renderDraws`, `groupDraws`, `renderScope`.
// - pin — fixing which row of a table the render uses, which the fences replay:
// `pinSet`, `pinSet.pin`, `table.drawIn`. // `pinSet`, `pinSet.pin`, `table.drawIn`.
// - family — a table and every table reaching it through a chain of // - family — a table and every table reaching it through a chain of
// `table.parentT`, named by the root that chain ends at: `table.familyRoot`. // `table.parentT`.
// - whole — a read of a table with no selector and no descent, {/city}, landing on
// the `table` itself, so it draws a row apart from every pin: `branches.wholePins`.
// - fence — a load-time check, run over each category, template or struct as it // - fence — a load-time check, run over each category, template or struct as it
// loads or compiles, so rendering a compiled tree cannot fail. // loads or compiles, so rendering a compiled tree cannot fail.
-252
View File
@@ -1,252 +0,0 @@
package fejkdata
import (
"fmt"
"sort"
"strings"
)
// drawFence fences each template of a scope as a render of its own, over one fold of what every
// node reads.
// docs/decisions.md#a-render-shares-one-reference-draw-per-category-per-group
// docs/decisions.md#the-expansion-hold-and-the-renders-draws-are-two-fences
type drawFence struct {
fold *readFold
}
// readSurvey is what one render reads, gathered at load for check to compare.
type readSurvey struct {
reads []pathRead
clash *pinClash
}
// pinClash is the read, lowest by how it is named, whose selectors clash with a row its route pinned.
type pinClash struct {
route readRoute
spelling string
err error
}
func (c *pinClash) named() string { return c.route.spelled(c.spelling) }
func (c *pinClash) before(o *pinClash) bool {
if c.named() != o.named() {
return c.named() < o.named()
}
return c.err.Error() < o.err.Error()
}
// survey is the fold's result as a render in group: every read not inside a nested draw group
// draws in it, and one read gathered by several routes is kept once.
func survey(reads []pathRead, group string) *readSurvey {
s := &readSurvey{}
for _, r := range reads {
if r.clash != nil {
if c := (&pinClash{r.route, r.a.spelling, r.clash}); s.clash == nil || c.before(s.clash) {
s.clash = c
}
continue
}
if r.group == "" {
r.group = group
}
s.reads = append(s.reads, r)
}
s.reads = distinct(s.reads)
return s
}
// checkDrawGroup refuses a draw group that splits nothing: one whose render reads every reference
// path inside a repeat or a nested draw group, which draw apart from it whatever it names.
func (f *drawFence) checkDrawGroup(path string, n node) error {
t, ok := n.(*template)
if !ok || t.drawGroup == "" {
return nil
}
for _, r := range f.fold.rootReads(t) {
if r.group == "" && r.draws() {
return nil
}
}
return fmt.Errorf("%s: drawGroup %q splits nothing, since nothing it renders reads a reference path outside a repeat or a nested drawGroup; drop it", path, t.drawGroup)
}
func (f *drawFence) checkDraws(path string, n node) error {
t, ok := n.(*template)
if !ok {
return nil
}
reads := f.fold.rootReads(t)
if !anyDraws(reads) {
return nil
}
if err := survey(reads, t.link.drawGroupKey).check(); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
return nil
}
// checkRecordDraws fences the columns of a record — a template compiled at the top without a
// repeat — so a load proves the record view of it as well as the string view.
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
func (f *drawFence) checkRecordDraws(path string, n node) error {
t, ok := n.(*template)
if !ok || !t.isRecord {
return nil
}
reads := f.fold.columnReads(t, sortedNames(t.fields))
if !anyDraws(reads) {
return nil
}
if err := survey(reads, t.link.drawGroupKey).check(); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
return nil
}
// checkOwnFamily refuses a table's format or cell that reads, however many templates
// away and through a repeat or a draw group too, a table of its own family: a whole
// read draws its row without pinning it, so the family would draw apart from the row
// it renders, whichever draws the reaching template holds.
// docs/decisions.md#a-table-never-reaches-its-own-family-by-any-route
func checkOwnFamily(path string, n node) error {
t, isTemplate := n.(*template)
if !isTemplate || t.site.table == nil {
return nil
}
own := t.site.table
seen := map[node]bool{}
var find func(n node) (renderEdge, *table, bool)
find = func(n node) (renderEdge, *table, bool) {
if seen[n] {
return renderEdge{}, nil, false
}
seen[n] = true
for _, e := range renderEdges(n) {
if e.readsRef() {
if familyTable, isTable := n.(*template).head(e.read.head).(*table); isTable && familyTable.familyRoot() == own.familyRoot() {
return e, familyTable, true
}
}
if e, familyTable, found := find(e.to); found {
return e, familyTable, true
}
}
return renderEdge{}, nil, false
}
if e, familyTable, found := find(t); found {
return fmt.Errorf("%s: %s reads %s, a table of its own family, which a bare read of %s would draw apart from the row it renders; read the family from a template beside it, or add the value as a column", path, e.reached(), familyTable.segment, own.segment)
}
return nil
}
// draws reports whether a read's draw is its group's to answer for: a path, or a table read whole,
// whose row the group draws.
func (r pathRead) draws() bool { return len(r.a.tail) > 0 || r.tr != nil }
func anyDraws(reads []pathRead) bool {
for _, r := range reads {
if r.draws() {
return true
}
}
return false
}
func repeats(n node) bool {
t, isTemplate := n.(*template)
return isTemplate && t.repeat > 1
}
// check refuses what one draw per reference path cannot answer for: a read selecting a row its route
// pinned another of, and any two reads that render together yet conflict.
func (s *readSurvey) check() error {
if c := s.clash; c != nil {
return fmt.Errorf("%s: %w; read the family from a template beside it, or add the value as a column", c.named(), c.err)
}
sort.SliceStable(s.reads, func(i, j int) bool {
if s.reads[i].group != s.reads[j].group {
return s.reads[i].group < s.reads[j].group
}
return s.reads[i].a.path < s.reads[j].a.path
})
for i, a := range s.reads {
for _, b := range s.reads[i+1:] {
if !coRender(a, b) {
continue
}
if err := conflict(a, b); err != nil {
return err
}
}
}
return nil
}
// coRender reports whether two reads can render together: in one draw group, and under no two
// rows of one table, since one read renders one row of it.
func coRender(a, b pathRead) bool {
return a.group == b.group && !a.branches.pins.differs(&b.branches.pins) && !a.branches.wholePins.differs(&b.branches.wholePins)
}
// conflict refuses two reads rendering together, a before b by path, that one draw per reference
// path cannot answer for. Pairs find every conflict a replay of all the reads would: a row clashes
// with one pinned table, and the read that pinned it holds that pin itself, with its ancestors.
func conflict(a, b pathRead) error {
for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
if err := drawsApart(pair[0], pair[1]); err != nil {
return err
}
}
if a.tr == nil || b.tr == nil {
return nil
}
d := a.tr.pins.clone()
if err := b.tr.replay(&d); err != nil {
return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", b.route.spelled(b.a.spelling), err)
}
return nil
}
// drawsApart refuses x drawing apart from what y reads: a level beside a path y takes into it, a
// table x reads whole beside a path y takes into its family, and a table x draws that y pins, since
// which token renders first would then decide the row. A level landing on a row renders the row its
// selectors pin, the one a path into it reads too.
// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held
func drawsApart(x, y pathRead) error {
if strings.HasPrefix(y.a.path, x.a.path+".") && !(x.tr != nil && x.tr.landsRow) {
return overlapError(x.route, x.a.spelling, y)
}
if x.tr == nil || y.tr == nil || x.tr.headTable.familyRoot() != y.tr.headTable.familyRoot() {
return nil
}
if len(x.a.tail) == 0 && len(y.a.tail) > 0 {
return overlapError(x.route, x.a.spelling, y)
}
drawn := x.tr.drawnOf(&y.tr.pins)
if drawn == nil {
return nil
}
if s, ok := y.tr.selected(x.tr.headTable); ok && len(x.tr.sels) == 0 {
tail := x.a.tail
if s.t != x.tr.headTable {
tail = append([]string{x.tr.headTable.segment}, tail...)
}
return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s}, or draw them apart with a drawGroup",
x.route.spelled(x.a.spelling), drawn.segment, y.route.spelled(y.a.spelling), joinSegments(append([]string{s.spelling}, tail...)))
}
return fmt.Errorf("%s draws %s, which %s selects a row of; select that row in both, or draw them apart with a drawGroup",
x.route.spelled(x.a.spelling), drawn.segment, y.route.spelled(y.a.spelling))
}
func overlapError(route readRoute, ref string, into pathRead) error {
apart := ""
switch {
case route.spelling == into.route.spelling:
case isRef(into.route.label):
apart = fmt.Sprintf(", or move {%s} into a field with a drawGroup", into.route.label)
default:
apart = fmt.Sprintf(", or give %s a drawGroup", into.route.spelling)
}
return fmt.Errorf("%s renders its own draw of what %s reads a path through; name the fields you want instead%s", route.spelled(ref), into.route.spelled(into.a.spelling), apart)
}
+10 -55
View File
@@ -1,11 +1,9 @@
package fejkdata package fejkdata
import "strings" // drawMemo is what a named pick has drawn: the variant each level was drawn as, so every path
// under it reads one variant; the value each read produced, by its path, so the same read written
// drawMemo is what a hold, a draw group or a named pick has drawn: the variant each level was // twice reads one value; the frames a read opens for the name scopes around where it lands
// drawn as, so every path under it reads one variant; the value each read produced, by its path, // (enter); and the rows drawn by each step down after a "..", by level.
// so the same read written twice reads one value; the frames a read opens for the name
// scopes around where it lands (enter); and the rows drawn by each step down after a "..", by level.
type drawMemo struct { type drawMemo struct {
variant map[string]node variant map[string]node
value map[string]readValue value map[string]readValue
@@ -30,39 +28,19 @@ func (m *drawMemo) stepDownPins(pins *pinSet, t *table, levels []string, at int)
return p return p
} }
// groupDraws is what one render's draw group has drawn for its reference paths: a memo, and the
// table rows those paths pinned.
type groupDraws struct {
memo drawMemo
pins pinSet
}
// readValue is what one read drew: its text, and whether it landed on a null. // readValue is what one read drew: its text, and whether it landed on a null.
type readValue struct { type readValue struct {
text string text string
null bool null bool
} }
// renderTrace is a test's view of the fields a render reads; nil outside a test. // renderScope is where a render reads: the frames of the name scopes rendering; and row, the row
type renderTrace func(group string, row renderedRow, a arm) // of the table rendering, which its columns read. It passes by value, and what is read from it
// reaches a map key, an interface or a func value only as a copy; else sc, and with it every
// renderDraws is one render's reference draws: the unnamed draw group's, and each named one's; // render's frames, moves to the heap.
// a test's trace of its reads; and the frames of the name scopes rendering.
type renderDraws struct {
unnamed groupDraws
named map[string]*groupDraws
trace renderTrace
frameStack *frameStack
}
// renderScope is where a render reads its reference paths: its reference draws, in the draw group of the
// template rendering; and row, the row of the table rendering, which its columns read. It passes
// by value, and what is read from it reaches a map key, an interface or a func value only as a
// copy; else sc, and with it every render's draws, moves to the heap.
type renderScope struct { type renderScope struct {
draws *renderDraws frames *frameStack
group string row renderedRow
row renderedRow
// base is the depth of the frame stack when the read rendering the category entered it, // base is the depth of the frame stack when the read rendering the category entered it,
// below which no frame is the category's. // below which no frame is the category's.
base int base int
@@ -77,14 +55,6 @@ type renderedRow struct {
index int index int
} }
// in is the scope t renders in: its draw group where it names one, else its caller's.
func (sc renderScope) in(t *template) renderScope {
if t.link.drawGroupKey != "" {
sc.group = t.link.drawGroupKey
}
return sc
}
// at is the scope n, the leaf of a path that pinned its rows in pins, renders in: where n is a // at is the scope n, the leaf of a path that pinned its rows in pins, renders in: where n is a
// table's row or column, at the row pins holds for that table. // table's row or column, at the row pins holds for that table.
func (sc renderScope) at(n node, pins *pinSet) renderScope { func (sc renderScope) at(n node, pins *pinSet) renderScope {
@@ -104,18 +74,3 @@ func (sc renderScope) rowOf(t *table) int {
} }
return sc.row.index return sc.row.index
} }
func (sc renderScope) groupDraws() *groupDraws {
if sc.group == "" {
return &sc.draws.unnamed
}
d, drew := sc.draws.named[sc.group]
if !drew {
if sc.draws.named == nil {
sc.draws.named = map[string]*groupDraws{}
}
d = &groupDraws{}
sc.draws.named[strings.Clone(sc.group)] = d
}
return d
}
+23 -55
View File
@@ -247,7 +247,7 @@ func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) {
} }
return checkReads(t, fields, true) return checkReads(t, fields, true)
case nameBind: case nameBind:
return nil, checkBind(t) return nil, checkBind(t, fields)
case nameRead: case nameRead:
unbound, err := checkReads(t, fields, false) unbound, err := checkReads(t, fields, false)
if err != nil { if err != nil {
@@ -285,17 +285,14 @@ func checkReads(t formatToken, fields map[string]node, operands bool) ([]unbound
return unbound, nil return unbound, nil
} }
// checkBind proves a {ref as name} token is spelled once, binds a reference, and names a valid // checkBind proves a {x as name} token is spelled once, binds a reference or a path into a
// name that is no option. // field, and names a valid name that is no option.
func checkBind(t formatToken) error { func checkBind(t formatToken, fields map[string]node) error {
ref, name := t.boundRef, t.bound ref, name := t.boundRef, t.bound
if trimmed := strings.TrimSpace(ref) + asWord + strings.TrimSpace(name); trimmed != t.body { if trimmed := strings.TrimSpace(ref) + asWord + strings.TrimSpace(name); trimmed != t.body {
return fmt.Errorf("token {%s}: write {%s}", t.body, trimmed) return fmt.Errorf("token {%s}: write {%s}", t.body, trimmed)
} }
if !isRef(ref) { if err := checkBound(ref, fields); err != nil {
return fmt.Errorf("token {%s}: %q is no reference; a name binds a pick of a reference, which starts with /, . or ..", t.body, ref)
}
if _, _, err := refShape(ref); err != nil {
return fmt.Errorf("token {%s}: %w", t.body, err) return fmt.Errorf("token {%s}: %w", t.body, err)
} }
if name == "" { if name == "" {
@@ -310,6 +307,20 @@ func checkBind(t formatToken) error {
return nil return nil
} }
// checkBound proves what a binding picks is a reference or a path into a field of the binding
// template.
func checkBound(ref string, fields map[string]node) error {
if isRef(ref) {
_, _, err := refShape(ref)
return err
}
missing, err := checkArm(ref, fields, false)
if missing {
return fmt.Errorf("%w; a name binds a pick of a field, or of a reference, which starts with /, . or ..", err)
}
return err
}
// checkArm validates one sibling name or path against a template's fields. // checkArm validates one sibling name or path against a template's fields.
// wholeToken says the name is the token's entire body, so {/name} would render // wholeToken says the name is the token's entire body, so {/name} would render
// the same value and can be offered as the reference spelling. missing says no // the same value and can be offered as the reference spelling. missing says no
@@ -376,9 +387,7 @@ func checkNoRepeatedArm(body string, names []string) error {
} }
// arm is one alternative of a {a|b} token or one operand, split into the head // arm is one alternative of a {a|b} token or one operand, split into the head
// `template.head` resolves and the tail of a dotted path into it. A non-empty tail // `template.head` resolves and the tail of a dotted path into it.
// makes the arm a held draw: a reference path's for the render, any other for the
// expansion.
type arm struct { type arm struct {
spelling string // as written, for messages spelling string // as written, for messages
head string head string
@@ -388,7 +397,6 @@ type arm struct {
path string // head and tail, the one path every way of writing this read shares path string // head and tail, the one path every way of writing this read shares
steps []pathStep steps []pathStep
leaves []node // every node the path may land on, one per variant it passes leaves []node // every node the path may land on, one per variant it passes
cover node // what holding the path pins: the first choice it passes, else its leaf
kind armKind kind armKind
named *nameBinding // namedRead: the binding of the name it reads through named *nameBinding // namedRead: the binding of the name it reads through
} }
@@ -397,16 +405,10 @@ type arm struct {
type armKind uint8 type armKind uint8
const ( const (
freshRead armKind = iota // drawn afresh at every read freshRead armKind = iota // drawn afresh at every read
heldRead // drawn once per expansion, kept in its hold namedRead // read through a name, kept in its pick
refPathRead // drawn once per draw group, kept in its memo
namedRead // read through a name, kept in its pick
) )
func (a arm) isRefPath() bool {
return isRef(a.head) && len(a.tail) > 0
}
// 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. // linkRefs 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 {
@@ -472,37 +474,10 @@ type op struct {
operands []arm operands []arm
} }
// formatOps is a compiled format: its ops, the size of its literal text, and the // formatOps is a compiled format: its ops, and the size of its literal text.
// names its expansion holds: every head a path that is not a reference starts from,
// plus the fields an operand reads.
type formatOps struct { type formatOps struct {
ops []op ops []op
grow int grow int
held map[string]firstReach
}
// firstReach is how a format first reaches a held name: the reader holding it, for
// error messages, and the first path starting from it, "" where none does.
type firstReach struct {
holder string
path string
}
func (c *formatOps) holdName(a arm, label string) {
if a.isRefPath() {
return
}
if c.held == nil {
c.held = map[string]firstReach{}
}
h, seen := c.held[a.head]
if !seen {
h.holder = label
}
if len(a.tail) > 0 && h.path == "" {
h.path = a.spelling
}
c.held[a.head] = h
} }
func (c *formatOps) function(tok formatToken, refs map[string]refBinding, isName func(string) bool) { func (c *formatOps) function(tok formatToken, refs map[string]refBinding, isName func(string) bool) {
@@ -511,10 +486,7 @@ func (c *formatOps) function(tok formatToken, refs map[string]refBinding, isName
a := splitArm(operand, refs) a := splitArm(operand, refs)
if isName(a.head) { if isName(a.head) {
a.kind = namedRead a.kind = namedRead
operands = append(operands, a)
continue
} }
c.holdName(a, fmt.Sprintf("%s operand %q", tok.fn, operand))
operands = append(operands, a) operands = append(operands, a)
} }
c.ops = append(c.ops, op{formatToken: tok, call: builtins[tok.fn].prep(tok.args), operands: operands}) c.ops = append(c.ops, op{formatToken: tok, call: builtins[tok.fn].prep(tok.args), operands: operands})
@@ -526,10 +498,6 @@ func (c *formatOps) field(tok formatToken, refs map[string]refBinding, isName fu
arms[i] = splitArm(name, refs) arms[i] = splitArm(name, refs)
if isName(arms[i].head) { if isName(arms[i].head) {
arms[i].kind = namedRead arms[i].kind = namedRead
continue
}
if len(arms[i].tail) > 0 {
c.holdName(arms[i], "token {"+arms[i].spelling+"}")
} }
} }
c.ops = append(c.ops, op{formatToken: tok, arms: arms}) c.ops = append(c.ops, op{formatToken: tok, arms: arms})
+8 -28
View File
@@ -2,6 +2,7 @@ package fejkdata
import ( import (
"fmt" "fmt"
"slices"
"sort" "sort"
) )
@@ -86,25 +87,10 @@ func sortedNames[V any](m map[string]V) []string {
} }
// renderEdge is a child a node renders into, labelled by what reaches it (a field // renderEdge is a child a node renders into, labelled by what reaches it (a field
// name, reference, or choice index) for a readable cycle report. read is the linked // name, reference, or choice index) for a readable cycle report.
// arm a template's edge reads, zero on any other. operand names the builtin when the
// label is its operand rather than a token, so an error can name it the way the
// author wrote it.
type renderEdge struct { type renderEdge struct {
to node to node
label string label string
read arm
operand string
}
func (e renderEdge) readsRef() bool { return isRef(e.read.head) }
// reached names an edge as the author spelled it.
func (e renderEdge) reached() string {
if e.operand != "" {
return fmt.Sprintf("%s operand %q", e.operand, e.label)
}
return "{" + e.label + "}"
} }
// renderEdges lists the children rendering n recurses into, mirroring render: a // renderEdges lists the children rendering n recurses into, mirroring render: a
@@ -120,17 +106,11 @@ func renderEdges(n node) []renderEdge {
return es return es
case *template: case *template:
var es []renderEdge var es []renderEdge
add := func(a arm, operand string) {
for _, leaf := range a.leaves {
es = append(es, renderEdge{leaf, a.spelling, a, operand})
}
}
for _, o := range n.compiled.ops { for _, o := range n.compiled.ops {
for _, a := range o.operands { for _, a := range slices.Concat(o.operands, o.arms) {
add(a, o.fn) for _, leaf := range a.leaves {
} es = append(es, renderEdge{leaf, a.spelling})
for _, a := range o.arms { }
add(a, "")
} }
} }
return es return es
-243
View File
@@ -1,243 +0,0 @@
package fejkdata
import (
"fmt"
"sort"
"strings"
)
// heldCheck rejects a format reading one held draw two ways, and every route to a held
// name except the readers of its draw. An expansion holds one draw of that name;
// anything else that renders it draws again, and the two disagree.
// docs/decisions.md#the-expansion-hold-and-the-renders-draws-are-two-fences
func heldCheck(path string, n node) error {
t, ok := n.(*template)
if !ok || len(t.compiled.held) == 0 {
return nil
}
if err := checkTemplateHold(t); err != nil {
return fmt.Errorf("%s: %w", t.site.label(path), err)
}
return nil
}
func checkTemplateHold(t *template) error {
if err := checkNoOverlap(t.compiled); err != nil {
return err
}
if err := checkNoRepeatedRead(t.compiled); err != nil {
return err
}
readers := pathHeadReaders(t.compiled)
for _, name := range heldNames(t) {
if err := checkNameHeld(t, name, readers); err != nil {
return err
}
}
return nil
}
// heldNames lists a template's held names, operands' first, then paths', each
// in name order: a level read both ways is reported by the operand's fence.
func heldNames(t *template) []string {
names := make([]string, 0, len(t.compiled.held))
for name := range t.compiled.held {
names = append(names, name)
}
sort.Slice(names, func(i, j int) bool {
pi := t.compiled.held[names[i]].path != ""
pj := t.compiled.held[names[j]].path != ""
if pi != pj {
return !pi
}
return names[i] < names[j]
})
return names
}
// heldNodes is what the hold of name answers for: a path pins the levels it
// passes through and the leaf it lands on, an operand exactly the value its render
// produces.
func heldNodes(t *template, name string, readers []reader) map[node]bool {
held := map[node]bool{}
if t.compiled.held[name].path == "" {
operandDraw(t.head(name), held)
return held
}
for _, r := range readers {
if r.a.head == name {
cover(r.a.cover, held, false)
}
}
return held
}
// checkNameHeld rejects every route to what name's hold pins except the readers
// holding it. One seen set across the edges: a node that cannot reach the level
// cannot reach it by another route either, so it is walked once.
func checkNameHeld(t *template, name string, readers []reader) error {
held := heldNodes(t, name, readers)
if len(held) == 0 {
return nil // a fixed head holds nothing to reach
}
path := t.compiled.held[name].path
seen := map[node]bool{}
for _, e := range renderEdges(t) {
if e.read.head == name || !renders(e.to, held, seen) {
continue
}
if path != "" {
return fmt.Errorf("%s reaches %q by a second route, where {%s} holds it to one draw; name the fields you want instead", e.reached(), name, path)
}
call, operand := operandReader(t, name)
if e.operand != "" {
return fmt.Errorf("%s renders %q, which {%s} holds to one draw for this expansion; stop %s reaching %q", e.reached(), name, call, e.reached(), name)
}
return fmt.Errorf("%s renders %q, which {%s} holds to one draw for this expansion; drop %s, or write {%s} in this format to read that draw", e.reached(), name, call, e.reached(), operand)
}
return nil
}
// operandReader spells the call whose operand holds name, and the operand, as written.
func operandReader(t *template, name string) (call, operand string) {
for _, o := range t.compiled.ops {
for _, a := range o.operands {
if a.head == name && !a.isRefPath() {
return o.body, a.spelling
}
}
}
return "", ""
}
// cover collects a level and everything contained in it. A fixed string outside a
// choice is left out — it cannot disagree with itself — but inside one each
// variant carries its own, so there it counts.
func cover(n node, into map[node]bool, inChoice bool) {
if isFixed(n) && !inChoice {
return
}
into[n] = true
_, isChoice := n.(*choice)
for _, c := range contained(n) {
cover(c.node, into, inChoice || isChoice)
}
}
// operandDraw collects what one held draw of an operand answers for: the operand
// and what rendering it settles inside itself. The builtin renders its operand
// whole, so that draw fixes every value the render produced, and a second route to
// any of them disagrees with it.
//
// The walk stops at a reference edge, which is where the operand's own value ends
// and a shared source begins: two names referencing one category are two draws, the
// same rule {word} {word} follows.
func operandDraw(n node, into map[node]bool) {
if isFixed(n) {
return
}
if into[n] {
return
}
into[n] = true
for _, e := range renderEdges(n) {
if e.readsRef() {
continue
}
operandDraw(e.to, into)
}
}
// isFixed is a string that varies nothing: fixed text with no fields to read into.
func isFixed(n node) bool {
t, ok := n.(*template)
if !ok {
return false
}
_, fixed := t.fixedText()
return fixed && len(t.fields) == 0
}
// renders reports whether rendering n can reach anything in want, following the
// same edges expand does.
func renders(n node, want, seen map[node]bool) bool {
if want[n] {
return true
}
if seen[n] {
return false
}
seen[n] = true
for _, e := range renderEdges(n) {
if renders(e.to, want, seen) {
return true
}
}
return false
}
// checkNoOverlap rejects a format that both renders a sibling level and reads a path
// into it — {p} beside {p.first}, {p.addr} beside {p.addr.city}. The path reads the
// level's held draw while rendering the level expands it afresh, so their values would
// disagree.
// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held
func checkNoOverlap(c formatOps) error {
names := pathHeadReaders(c)
// Stable over one format-order scan, so two readers of one name (a token and a
// calc operand both naming "p") are reported as the format writes them.
sort.SliceStable(names, func(i, j int) bool { return names[i].a.path < names[j].a.path })
for i, level := range names {
for _, path := range names[i+1:] {
if strings.HasPrefix(path.a.path, level.a.path+".") {
return fmt.Errorf("%s renders a level that {%s} reads a path into; name the fields you want instead", level.label, path.a.spelling)
}
}
}
return nil
}
// reader is one way a format reaches a path head, and how to name it.
type reader struct {
a arm
label string
}
// pathHeadReaders lists every way a format reaches a sibling path head, in the order the
// format writes them. An operand renders its field, so it names a level exactly
// as a token does; one scan finds both, which is what puts them in one order.
func pathHeadReaders(c formatOps) []reader {
var names []reader
for _, o := range c.ops {
for _, a := range o.operands {
if c.held[a.head].path != "" {
names = append(names, reader{a, fmt.Sprintf("%s operand %q", o.fn, a.spelling)})
}
}
for _, a := range o.arms {
if c.held[a.head].path != "" {
names = append(names, reader{a, "token {" + a.spelling + "}"})
}
}
}
return names
}
// checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside
// {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The
// error names the single-token spelling.
// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held
func checkNoRepeatedRead(c formatOps) error {
count := map[string]int{}
for _, o := range c.ops {
for _, a := range o.arms {
h, held := c.held[a.head]
if len(a.tail) > 0 || !held {
continue
}
if count[a.head]++; count[a.head] > 1 {
return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.spelling, h.holder, a.head, a.spelling)
}
}
}
return nil
}
+1 -7
View File
@@ -16,7 +16,7 @@ type Template struct {
n node n node
} }
// Fake renders the template with one draw. // Fake renders the template once.
func (t *Template) Fake() string { func (t *Template) Fake() string {
t.g.mu.Lock() t.g.mu.Lock()
defer t.g.mu.Unlock() defer t.g.mu.Unlock()
@@ -159,12 +159,6 @@ func bindInline(root *folder, n node, label string, typedByGo bool) error {
scope: scope, scope: scope,
link: func() error { return linkNodeRefs(scope, root.children) }, link: func() error { return linkNodeRefs(scope, root.children) },
typedByGo: typedByGo, typedByGo: typedByGo,
scopeFence: func() error {
if t, isTemplate := n.(*template); isTemplate && t.drawGroup != "" {
return fmt.Errorf("%s: drawGroup %q names nothing, since nothing can reference an inline template; drop it", label, t.drawGroup)
}
return nil
},
}.bind() }.bind()
} }
+28 -19
View File
@@ -19,7 +19,8 @@ type nameScope struct {
type nameBinding struct { type nameBinding struct {
name string name string
ref string ref string // what it binds, as written: a reference, or a path into a field of binder
field bool // ref is a path into a field, so a read of the name stays in the category
body string body string
where string // the fields reaching the binding template, as a compile error spells them where string // the fields reaching the binding template, as a compile error spells them
scope *nameScope scope *nameScope
@@ -33,11 +34,12 @@ type nameBinding struct {
addressed map[string]string addressed map[string]string
} }
// nameUse is one read of a name: the path it reads into the name, "" for the name itself; whether // nameUse is one read of a name: the path it reads into the name, "" for the name itself; the
// a builtin reads it as an operand, and whether no reference can stand there; the draw group // template reading it; whether a builtin reads it as an operand, and whether no reference can
// its template renders in; and whether it sits in a repeat nested inside the name's scope. // stand there; and whether it sits in a repeat nested inside the name's scope.
type nameUse struct { type nameUse struct {
tail, group string tail string
in *template
operand, noRef, nested bool operand, noRef, nested bool
} }
@@ -86,7 +88,7 @@ func bindNames(root node) error {
} }
} }
} }
if err := resolveReads(root, "", "", scopes); err != nil { if err := resolveReads(root, "", scopes); err != nil {
return err return err
} }
for _, sc := range scopes { for _, sc := range scopes {
@@ -109,7 +111,7 @@ func (sc *nameScope) bindAll(t *template, inChoice bool, where string) error {
if b, twice := sc.bindings[tok.bound]; twice { if b, twice := sc.bindings[tok.bound]; twice {
return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.body, tok.bound, sc.spelled(), b.body) return fmt.Errorf("%stoken {%s}: name %q is bound twice %s, by {%s} too; rename one", where, tok.body, tok.bound, sc.spelled(), b.body)
} }
b := &nameBinding{name: tok.bound, ref: tok.boundRef, body: tok.body, where: where, scope: sc, index: len(sc.order), binder: t} b := &nameBinding{name: tok.bound, ref: tok.boundRef, field: !isRef(tok.boundRef), body: tok.body, where: where, scope: sc, index: len(sc.order), binder: t}
if sc.bindings == nil { if sc.bindings == nil {
sc.bindings = map[string]*nameBinding{} sc.bindings = map[string]*nameBinding{}
} }
@@ -159,12 +161,9 @@ func eachContained(n node, where string, fn func(c node, where string) error) er
// resolveReads answers each unbound read under n with a name its template sees, fields before the // resolveReads answers each unbound read under n with a name its template sees, fields before the
// format as compile reports them, and refuses a field spelling a name. scopes are every scope of // 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. // the category, which a refusal searches for a name bound where the read cannot see it.
func resolveReads(n node, where, group string, scopes []*nameScope) error { func resolveReads(n node, where string, scopes []*nameScope) error {
t, isTemplate := n.(*template) t, isTemplate := n.(*template)
if isTemplate && t.drawGroup != "" { if err := eachContained(n, where, func(c node, where string) error { return resolveReads(c, where, scopes) }); err != nil {
group = t.drawGroup
}
if err := eachContained(n, where, func(c node, where string) error { return resolveReads(c, where, group, scopes) }); err != nil {
return err return err
} }
if !isTemplate { if !isTemplate {
@@ -184,7 +183,7 @@ func resolveReads(n node, where, group 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, group: group, operand: u.operand, noRef: u.noRef, nested: t.nameScope != b.scope}) 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 nil
} }
@@ -221,15 +220,15 @@ func (t *template) isName(head string) bool {
return !isRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil return !isRef(head) && t.fields[head] == nil && t.nameScope.lookup(head) != nil
} }
// linkBindings gives each binding t's tokens make the head and tail its reference resolved to. // linkBindings gives each binding t's tokens make the head and tail what it binds resolved to.
func linkBindings(t *template) { func linkBindings(t *template) {
for _, tok := range t.tokens { for _, tok := range t.tokens {
if tok.kind != nameBind { if tok.kind != nameBind {
continue continue
} }
b := t.nameScope.bindings[tok.bound] b := t.nameScope.bindings[tok.bound]
ref := t.link.refs[b.ref] a := splitArm(b.ref, t.link.refs)
b.head, b.tail = t.link.refHeads[ref.head], ref.tail b.head, b.tail = t.head(a.head), a.tail
} }
} }
@@ -257,12 +256,22 @@ var namePasses = []func(path string, t *template) error{linkNames, checkNameRead
// linkNames compiles t's reads of a name as paths from the head its binding's reference names, once // linkNames compiles t's reads of a name as paths from the head its binding's reference names, once
// every template is linked, so each binder has resolved that reference. // every template is linked, so each binder has resolved that reference.
func linkNames(path string, t *template) error { func linkNames(path string, t *template) error {
return namedReads(t, func(o *op, a *arm) error { if err := namedReads(t, func(o *op, a *arm) error {
if err := linkName(t, a); err != nil { if err := linkName(t, a); err != nil {
return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err) return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err)
} }
return nil return nil
}) }); err != nil {
return err
}
if ops := t.compiled.ops; t.repeat == 1 && len(ops) == 1 && ops[0].kind == nameRead && len(ops[0].arms) == 1 && ops[0].arms[0].kind == namedRead {
a := ops[0].arms[0]
full := append(a.named.tail[:len(a.named.tail):len(a.named.tail)], a.tail...)
if column := recordColumn(a.named.head, full); column != nil {
t.link.readsColumn = &columnRead{a: a, column: column}
}
}
return nil
} }
func linkName(t *template, a *arm) error { func linkName(t *template, a *arm) error {
@@ -275,7 +284,7 @@ func linkName(t *template, a *arm) error {
return err return err
} }
w := compilePath(b.head, full) w := compilePath(b.head, full)
a.named, a.steps, a.leaves, a.cover = b, w.steps, w.leaves, w.cover a.named, a.steps, a.leaves = b, w.steps, w.leaves
a.levels = make([]string, len(full)+1) a.levels = make([]string, len(full)+1)
for i := range a.levels { for i := range a.levels {
a.levels[i] = joinSegments(full[:i]) a.levels[i] = joinSegments(full[:i])
+27 -57
View File
@@ -3,6 +3,7 @@ package fejkdata
import ( import (
"fmt" "fmt"
"slices" "slices"
"strings"
) )
// checkNameReads refuses each binding of t that checkUses refuses, and a read such as {n} whose // checkNameReads refuses each binding of t that checkUses refuses, and a read such as {n} whose
@@ -26,63 +27,34 @@ func checkNameReads(path string, t *template) error {
}) })
} }
// checkUses refuses a binding of a category read once whole where a reference could stand. It // checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling
// refuses reads of b in two draw groups, or inside a repeat, where what b names reads a reference path. // draws the same way without the name. A field stands only beside its binding.
func (b *nameBinding) checkUses() error { func (b *nameBinding) checkUses() error {
if r := b.uses[0]; len(b.uses) == 1 && r.tail == "" && !r.nested && !r.noRef && len(b.tail) == 0 { r := b.uses[0]
spelling := b.ref if len(b.uses) > 1 || r.nested || b.field && r.in != b.binder {
if !r.operand {
spelling = "{" + spelling + "}"
}
return fmt.Errorf("name %q is read once, whole, which the bare reference draws the same way; write %s where it is read, and drop the token", b.name, spelling)
}
if !readsHeld(b.head, map[node]bool{}) {
return nil return nil
} }
for _, u := range b.uses { spelling := b.ref
if u.nested { switch {
return fmt.Errorf("name %q is read inside a repeat, and what it names reads a reference path, which each iteration draws apart; bind the name inside the repeat, or bind a second name there", b.name) case strings.HasPrefix(r.tail, ".."):
} spelling += r.tail
if u.group != b.uses[0].group { case r.tail != "":
return fmt.Errorf("name %q is read in two draw groups, %s and %s, and what it names reads a reference path, which each draw group draws apart; read the name in one draw group, or bind a name in each", b.name, groupSpelling(b.uses[0].group), groupSpelling(u.group)) spelling += "." + r.tail
}
} }
return nil if r.noRef && !calcReads(spelling) {
return nil
}
if !r.operand {
spelling = "{" + spelling + "}"
}
return fmt.Errorf("name %q is read once, so it keeps no pick for another read; write %s where it is read, and drop the token", b.name, spelling)
} }
func groupSpelling(group string) string { // calcReads reports whether a calc reads spelling as one operand.
if group == "" { func calcReads(spelling string) bool {
return "the unnamed one" n, err := parseCalc(spelling)
} v, isVar := n.(calcVar)
return fmt.Sprintf("%q", group) return err == nil && isVar && string(v) == spelling
}
// readsHeld reports whether rendering n, or any field under it, can read a reference path.
func readsHeld(n node, seen map[node]bool) bool {
if seen[n] {
return false
}
seen[n] = true
if t, isTemplate := n.(*template); isTemplate {
for _, o := range t.compiled.ops {
for _, a := range slices.Concat(o.arms, o.operands) {
if a.kind == refPathRead {
return true
}
}
}
}
for _, c := range contained(n) {
if readsHeld(c.node, seen) {
return true
}
}
for _, e := range renderEdges(n) {
if readsHeld(e.to, seen) {
return true
}
}
return false
} }
// checkOnce walks n, rendering at key under b's pick, into each level a read of b addresses. // checkOnce walks n, rendering at key under b's pick, into each level a read of b addresses.
@@ -101,7 +73,7 @@ func (b *nameBinding) checkOnce(read string, n node, key string) error {
} }
func (b *nameBinding) checkTemplateOnce(read string, t *template, key string) error { func (b *nameBinding) checkTemplateOnce(read string, t *template, key string) error {
fresh := map[string]int{} reads := map[string]int{}
var into []arm var into []arm
for _, o := range t.compiled.ops { for _, o := range t.compiled.ops {
for _, a := range slices.Concat(o.arms, o.operands) { for _, a := range slices.Concat(o.arms, o.operands) {
@@ -111,13 +83,11 @@ func (b *nameBinding) checkTemplateOnce(read string, t *template, key string) er
if _, kept := b.addressed[join(key, a.path)]; kept { if _, kept := b.addressed[join(key, a.path)]; kept {
into = append(into, a) into = append(into, a)
} }
if a.kind == freshRead { reads[a.head]++
fresh[a.head]++
}
} }
} }
for _, head := range sortedNames(fresh) { for _, head := range sortedNames(reads) {
if by, kept := b.addressed[join(key, head)]; kept && fresh[head] > 1 { if by, kept := b.addressed[join(key, head)]; kept && reads[head] > 1 {
return fmt.Errorf("{%s} renders field %q twice, so {%s} cannot say which draw it reads; drop {%s} or {%s}", read, head, by, read, by) return fmt.Errorf("{%s} renders field %q twice, so {%s} cannot say which draw it reads; drop {%s} or {%s}", read, head, by, read, by)
} }
} }
+8 -70
View File
@@ -53,9 +53,8 @@ type template struct {
repeat int repeat int
separator string separator string
datatype DataType datatype DataType
drawGroup string // the draw group it draws in, as written; "" keeps its caller's fromString bool // written as a JSON string rather than an object
fromString bool // written as a JSON string rather than an object isRecord bool // compiled at the top without a repeat, so its fields are record columns
isRecord bool // compiled at the top without a repeat, so its fields are record columns
site tableSite site tableSite
unbound []unboundRead // the heads its tokens read that no field holds unbound []unboundRead // the heads its tokens read that no field holds
@@ -81,10 +80,6 @@ func (s tableSite) isCell() bool {
return s.table != nil && s.row != formatRow return s.table != nil && s.row != formatRow
} }
func (s tableSite) isFormat() bool {
return s.table != nil && s.row == formatRow
}
// label names the template at path in an error, a cell by its line in the rows file. // label names the template at path in an error, a cell by its line in the rows file.
func (s tableSite) label(path string) string { func (s tableSite) label(path string) string {
if s.isCell() { if s.isCell() {
@@ -95,10 +90,9 @@ func (s tableSite) label(path string) string {
// 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 alone reading a record's column readsColumn *columnRead // set when the format is one reference alone reading a record's column
drawGroupKey string // its draw group keyed by its category: what a render reads its reference paths under
} }
func (*template) isNode() {} func (*template) isNode() {}
@@ -347,24 +341,20 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(fields) == 0 && o.repeat == 1 && !o.weighted && o.datatype == DataTypeString && o.group == "" { if len(fields) == 0 && o.repeat == 1 && !o.weighted && o.datatype == DataTypeString {
return nil, fmt.Errorf("an object holding only a format is a string; write %q", o.format) return nil, fmt.Errorf("an object holding only a format is a string; write %q", o.format)
} }
toks, unbound, err := parseChecked(o.format, fields) toks, unbound, err := parseChecked(o.format, fields)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := checkNestedDrawGroup(fields, o.group); err != nil { return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, isRecord: fieldPos == inColumn, unbound: unbound}, nil
return nil, err
}
return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, drawGroup: o.group, isRecord: fieldPos == inColumn, unbound: unbound}, nil
} }
// templateOptions is what a template object's option keys say. // templateOptions is what a template object's option keys say.
type templateOptions struct { type templateOptions struct {
datatype DataType datatype DataType
format string format string
group string
repeat int repeat int
separator string separator string
weighted bool weighted bool
@@ -385,9 +375,6 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) {
if o.datatype, err = datatypeOf(m, pos); err != nil { if o.datatype, err = datatypeOf(m, pos); err != nil {
return o, err return o, err
} }
if o.group, err = drawGroupOf(m, repeat); err != nil {
return o, err
}
if sv, ok := m["separator"]; ok { if sv, ok := m["separator"]; ok {
if o.separator, ok = sv.(string); !ok { if o.separator, ok = sv.(string); !ok {
return o, fmt.Errorf("separator must be a string, got %T", sv) return o, fmt.Errorf("separator must be a string, got %T", sv)
@@ -445,55 +432,6 @@ func repeatOf(m map[string]any) (int, error) {
return int(r), nil return int(r), nil
} }
// checkNestedDrawGroup refuses a template beneath one drawing in group that names group again,
// short of a repeat or another draw group.
func checkNestedDrawGroup(fields map[string]node, group string) error {
if group == "" {
return nil
}
var walk func(path string, n node) error
walk = func(path string, n node) error {
t, isTemplate := n.(*template)
switch {
case isTemplate && t.drawGroup == group:
return fmt.Errorf("%q names drawGroup %q, the draw group this template draws in already; drop it", path, group)
case isTemplate && (t.drawGroup != "" || t.repeat > 1):
return nil
}
for _, c := range contained(n) {
if err := walk(join(path, c.name), c.node); err != nil {
return err
}
}
return nil
}
for _, name := range sortedNames(fields) {
if err := walk(name, fields[name]); err != nil {
return err
}
}
return nil
}
// drawGroupOf reads a template's "drawGroup" (default ""), which a repeat cannot carry: each
// iteration renders in no draw group.
func drawGroupOf(m map[string]any, repeat int) (string, error) {
v, ok := m["drawGroup"]
if !ok {
return "", nil
}
name, ok := v.(string)
switch {
case !ok:
return "", fmt.Errorf("drawGroup must be a string, got %T", v)
case name == "":
return "", fmt.Errorf(`drawGroup "" is the default, so it has no effect; drop it`)
case repeat > 1:
return "", fmt.Errorf("drawGroup %q on a repeat names nothing, since each iteration is a render of its own; drop it", name)
}
return name, nil
}
// weightOf reads a node's "weight" (default 1) from its raw JSON form. Only // weightOf reads a node's "weight" (default 1) from its raw JSON form. Only
// template objects carry weight; a present one must be finite and positive. // template objects carry weight; a present one must be finite and positive.
func weightOf(raw any) (float64, error) { func weightOf(raw any) (float64, error) {
@@ -567,7 +505,7 @@ func checkPathNames(path string) error {
func isOption(name string) bool { func isOption(name string) bool {
switch name { switch name {
case "datatype", "drawGroup", "format", "repeat", "separator", "weight": case "datatype", "format", "repeat", "separator", "weight":
return true return true
} }
return false return false
+7 -19
View File
@@ -281,24 +281,18 @@ const (
// pathCheck proves a path resolves whichever way the draws go: every variant of a // pathCheck proves a path resolves whichever way the draws go: every variant of a
// choice carries the rest of it, and is walked, a selector names a row inside the // choice carries the rest of it, and is walked, a selector names a row inside the
// rows selected before it, and no level read carries a repeat or a drawGroup. It // rows selected before it, and no level read carries a repeat. It compiles the steps
// compiles the steps a draw takes, every leaf the path may render, and as cover the // a draw takes, and every leaf the path may render; level names the head in its errors.
// first choice it passes, else the leaf; level names the head in its errors.
type pathCheck struct { type pathCheck struct {
pins pinSet pins pinSet
level string level string
tail []string tail []string
steps []pathStep steps []pathStep
leaves []node leaves []node
cover node
} }
func (w *pathCheck) run(n node) (node, error) { func (w *pathCheck) run(n node) (node, error) {
leaf, err := w.walk(n, w.tail) return w.walk(n, w.tail)
if err == nil && w.cover == nil {
w.cover = leaf
}
return leaf, err
} }
func (w *pathCheck) walk(n node, tail []string) (node, error) { func (w *pathCheck) walk(n node, tail []string) (node, error) {
@@ -306,9 +300,6 @@ func (w *pathCheck) walk(n node, tail []string) (node, error) {
var err error var err error
switch x := n.(type) { switch x := n.(type) {
case *choice: case *choice:
if w.cover == nil {
w.cover = x
}
return w.walkEvery(x, tail) return w.walkEvery(x, tail)
case *table: case *table:
var r tableRoute var r tableRoute
@@ -393,12 +384,9 @@ func (w *pathCheck) walkEvery(c *choice, tail []string) (node, error) {
} }
func (w *pathCheck) enter(t *template, rest []string) error { func (w *pathCheck) enter(t *template, rest []string) error {
name := join(w.level, strings.Join(w.tail[:len(w.tail)-len(rest)], ".")) if t.repeat > 1 {
switch { name := join(w.level, strings.Join(w.tail[:len(w.tail)-len(rest)], "."))
case t.repeat > 1:
return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", name) return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", name)
case t.drawGroup != "":
return fmt.Errorf("the level %q carries a drawGroup, which a path reading into it cannot apply", name)
} }
return nil return nil
} }
@@ -439,8 +427,8 @@ func probePath(n node, tail []string, steps []pathStep) ([]pathStep, error) {
// drawSteps draws the rows and variants a path's steps read from n, pinning the rows in pins; // drawSteps draws the rows and variants a path's steps read from n, pinning the rows in pins;
// pins is nil for a sibling path, which never crosses a table, since a table is only a category. // pins is nil for a sibling path, which never crosses a table, since a table is only a category.
// For a memoized read, memo keeps the variant drawn at each of levels, so paths sharing a prefix // For a read through a name, memo keeps the variant drawn at each of levels, so paths sharing a
// share it. It returns the leaf and the pins its row is in: a step down after a step up draws // prefix share it. It returns the leaf and the pins its row is in: a step down after a step up draws
// afresh, into pins of its own, which memo shares with every path stepping down there. // afresh, into pins of its own, which memo shares with every path stepping down there.
func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) { func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) {
var climbed *table var climbed *table
+26 -37
View File
@@ -2,8 +2,7 @@ package fejkdata
// namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the // namedPick is one draw of a name: the variant drawn at each level a read of it addresses, the
// value each read there produced, keyed by the path from the name, and the table rows they // value each read there produced, keyed by the path from the name, and the table rows they
// pinned. A reference read under it draws in the draw group of the read of the name, which the // pinned.
// draw fences survey it in.
type namedPick struct { type namedPick struct {
named *nameBinding named *nameBinding
memo drawMemo memo drawMemo
@@ -24,44 +23,30 @@ func newPickFrame(scope *nameScope) *pickFrame {
return f return f
} }
// frameStack is the frames of the name scopes rendering, innermost last. A render's draws point // frameStack is the frames of the name scopes rendering, innermost last. A render's scope points
// at it, and a repeat iteration's share it: holding the frames in the draws, or in a renderScope, // at it, and a repeat iteration's shares it: holding the frames in a renderScope would move every
// would move every render's draws to the heap. // render's scope to the heap.
type frameStack struct { type frameStack struct {
frames []*pickFrame frames []*pickFrame
} }
// pushFrame opens f until popFrames closes it, returning the mark popFrames takes. // push opens f until pop closes it, returning the mark pop takes.
func (d *renderDraws) pushFrame(f *pickFrame) int { func (st *frameStack) push(f *pickFrame) int {
mark := d.depth() mark := len(st.frames)
if d.frameStack == nil { st.frames = append(st.frames, f)
d.frameStack = &frameStack{}
}
d.frameStack.frames = append(d.frameStack.frames, f)
return mark return mark
} }
func (d *renderDraws) popFrames(mark int) { func (st *frameStack) pop(mark int) { st.frames = st.frames[:mark] }
if d.frameStack != nil {
d.frameStack.frames = d.frameStack.frames[:mark]
}
}
func (d *renderDraws) depth() int { // enter starts a read landing on n: the read sees no frame opened before it, and gets a frame for
if d.frameStack == nil { // each name scope around n, from memo where there is one, so reads sharing memo read one pick of
return 0 // each name. It returns the mark that closes those frames.
}
return len(d.frameStack.frames)
}
// enter starts a read landing on n: the read sees no frame opened before it, and gets from memo a
// frame for each name scope around n, so reads sharing memo read one pick of each name. It
// returns the mark that closes those frames.
func (sc renderScope) enter(n node, memo *drawMemo) (renderScope, int) { func (sc renderScope) enter(n node, memo *drawMemo) (renderScope, int) {
sc = sc.entering() sc = sc.entering()
for scope := scopeAround(n); scope != nil; scope = scope.up { for scope := scopeAround(n); scope != nil; scope = scope.up {
if len(scope.order) > 0 { if len(scope.order) > 0 {
sc.draws.pushFrame(memo.enteredFrame(scope)) sc.frames.push(memo.enteredFrame(scope))
} }
} }
return sc, sc.base return sc, sc.base
@@ -69,7 +54,7 @@ func (sc renderScope) enter(n node, memo *drawMemo) (renderScope, int) {
// entering is sc as a read entering a category sees it: no frame opened before it, and no pick. // entering is sc as a read entering a category sees it: no frame opened before it, and no pick.
func (sc renderScope) entering() renderScope { func (sc renderScope) entering() renderScope {
sc.base, sc.pick = sc.draws.depth(), nil sc.base, sc.pick = len(sc.frames.frames), nil
return sc return sc
} }
@@ -89,6 +74,9 @@ func scopeAround(n node) *nameScope {
} }
func (m *drawMemo) enteredFrame(scope *nameScope) *pickFrame { func (m *drawMemo) enteredFrame(scope *nameScope) *pickFrame {
if m == nil {
return newPickFrame(scope)
}
f, ok := m.enteredFrames[scope] f, ok := m.enteredFrames[scope]
if !ok { if !ok {
f = newPickFrame(scope) f = newPickFrame(scope)
@@ -106,16 +94,14 @@ func (sc renderScope) renderFrame(t *template) int {
if t.ownNameScope == nil || sc.frameOf(t.ownNameScope) != nil { if t.ownNameScope == nil || sc.frameOf(t.ownNameScope) != nil {
return -1 return -1
} }
return sc.draws.pushFrame(newPickFrame(t.ownNameScope)) return sc.frames.push(newPickFrame(t.ownNameScope))
} }
// 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.
func (sc renderScope) frameOf(scope *nameScope) *pickFrame { func (sc renderScope) frameOf(scope *nameScope) *pickFrame {
if stack := sc.draws.frameStack; stack != nil { for i := len(sc.frames.frames) - 1; i >= sc.base; i-- {
for i := len(stack.frames) - 1; i >= sc.base; i-- { if f := sc.frames.frames[i]; f.scope == scope {
if f := stack.frames[i]; f.scope == scope { return f
return f
}
} }
} }
return nil return nil
@@ -131,9 +117,12 @@ func readName(s *generatorState, sc renderScope, a arm) readValue {
return r return r
} }
leaf, pins := p.draw(s, a.named.head, a.steps, a.levels, a.path) leaf, pins := p.draw(s, a.named.head, a.steps, a.levels, a.path)
if a.named.field {
return p.renderAt(s, leaf, pins, a.path, sc)
}
sc, mark := sc.enter(leaf, &p.memo) sc, mark := sc.enter(leaf, &p.memo)
r := p.renderAt(s, leaf, pins, a.path, sc) r := p.renderAt(s, leaf, pins, a.path, sc)
sc.draws.popFrames(mark) sc.frames.pop(mark)
return r return r
} }
@@ -167,7 +156,7 @@ func readUnder(s *generatorState, t *template, sc renderScope, a arm) readValue
} }
// underKey is the key of path under the pick key prefix. It never returns prefix itself, which a // underKey is the key of path under the pick key prefix. It never returns prefix itself, which a
// memo would keep: anything a renderScope holds reaching the heap moves every render's draws there. // memo would keep: anything a renderScope holds reaching the heap moves every render's scope there.
func underKey(prefix, path string) string { func underKey(prefix, path string) string {
if prefix == "" { if prefix == "" {
return path return path
+1 -57
View File
@@ -1,11 +1,6 @@
package fejkdata package fejkdata
import ( import "fmt"
"fmt"
"maps"
"sort"
"strings"
)
type tablePin struct { type tablePin struct {
t *table t *table
@@ -73,33 +68,6 @@ func (p *pinSet) nearestPinned(t *table) *table {
return nil return nil
} }
// each calls fn for every pinned row, in pin order, the spilled ones by path.
func (p *pinSet) each(fn func(t *table, r int)) {
for _, q := range p.inline[:p.used] {
fn(q.t, q.row)
}
spilled := make([]*table, 0, len(p.spill))
for t := range p.spill {
spilled = append(spilled, t)
}
sort.Slice(spilled, func(i, j int) bool { return spilled[i].path < spilled[j].path })
for _, t := range spilled {
fn(t, p.spill[t])
}
}
// key spells the pinned rows, so two sets pinning the same rows share one.
func (p *pinSet) key() string {
var pins []tablePin
p.each(func(t *table, r int) { pins = append(pins, tablePin{t, r}) })
sort.Slice(pins, func(i, j int) bool { return pins[i].t.path < pins[j].t.path })
var b strings.Builder
for _, q := range pins {
fmt.Fprintf(&b, "%q%d", q.t.path, q.row)
}
return b.String()
}
// clash is the table whose pinned row keeps row r of t out: t itself pinned to another row, or the // clash is the table whose pinned row keeps row r of t out: t itself pinned to another row, or the
// nearest ancestor pinned to a row r is not inside; nil where none does. // nearest ancestor pinned to a row r is not inside; nil where none does.
func (p *pinSet) clash(t *table, r int) *table { func (p *pinSet) clash(t *table, r int) *table {
@@ -164,27 +132,3 @@ func (p *pinSet) above(t *table) *pinSet {
} }
return q return q
} }
// clone is a copy of p that pins apart from it; a plain copy shares the spill map.
func (p pinSet) clone() pinSet {
p.spill = maps.Clone(p.spill)
return p
}
// entered is p with row r of t pinned. p is left as it was, since a fence walk branches.
func (p pinSet) entered(t *table, r int) pinSet {
p = p.clone()
p.pin(t, r)
return p
}
// differs reports whether p and q hold different rows of one table.
func (p *pinSet) differs(q *pinSet) bool {
found := false
p.each(func(t *table, r int) {
if qr, in := q.pinned(t); in && qr != r {
found = true
}
})
return found
}
-321
View File
@@ -1,321 +0,0 @@
package fejkdata
import "fmt"
// readRoute is how a render reaches a read: as its author spells it, and the root edge's label.
type readRoute struct{ spelling, label string }
// branches is the rows a read sits under, where two reads under different rows of one table never
// render together. pins holds the rows pinned above it with their ancestors, which a pair of reads
// needs to find what a replay of all of them does; wholePins the rows of whole draws whose cells it
// sits in, without ancestors, since each whole draw draws apart from every other.
type branches struct {
pins pinSet
wholePins pinSet
}
// pathRead is one reference a render reads, a path or a bare reference with no tail: in its draw
// group ("" until the root names one), by the route the render's root reached it, under its
// branches.
type pathRead struct {
group string
route readRoute
branches branches
a arm
tr *tableRead // set where the reference names a table
clash error // set where its selectors clash with a row its route pinned
}
type readKey struct {
group string
path string
pins string
wholePins string
clash bool
}
// readFold gathers, once per node, the reference reads rendering it makes.
type readFold struct {
memo map[node][]pathRead
}
func newReadFold() *readFold { return &readFold{memo: map[node][]pathRead{}} }
// rootReads is what rendering t as a render of its own reads, each read named by the edge of t
// that reaches it, and in "" where no template below t names a draw group.
func (f *readFold) rootReads(t *template) []pathRead {
var reads []pathRead
for _, e := range renderEdges(t) {
reads = append(reads, f.viaEdge(t, e, readRoute{e.reached(), e.label})...)
}
return reads
}
// columnReads is what rendering t's columns as one record reads.
func (f *readFold) columnReads(t *template, columns []string) []pathRead {
var reads []pathRead
for _, name := range columns {
reads = append(reads, routed(f.reads(t.fields[name]), readRoute{spelling: fmt.Sprintf("column %q", name)})...)
}
return reads
}
// reads is what rendering n reads, short of a repeat, which renders over draws of its own: each
// read with the rows pinned above it inside n, and in its draw group where a template inside n
// names one.
func (f *readFold) reads(n node) []pathRead {
if r, done := f.memo[n]; done {
return r
}
var out []pathRead
switch n := n.(type) {
case *choice:
for _, it := range n.items {
out = append(out, f.reads(it)...)
}
case *template:
if repeats(n) {
break
}
for _, e := range renderEdges(n) {
out = append(out, f.viaEdge(n, e, readRoute{})...)
}
if n.link.drawGroupKey != "" {
for i := range out {
if out[i].group == "" {
out[i].group = n.link.drawGroupKey
}
}
}
case *table:
out = f.rowReads(n, func(b *branches, row int) bool {
b.wholePins = b.wholePins.clone()
b.wholePins.add(n, row)
return true
})
case *tableRow:
out = f.rowReads(n.t, func(b *branches, row int) bool { return b.enter(n.t, row) })
case *tableColumn:
out = f.cellReads(n, func(b *branches, row int) bool { return b.enter(n.t, row) })
case *folder, *nullItem:
default:
panic(internalError("reads has no case for node %T", n))
}
out = distinct(out)
f.memo[n] = out
return out
}
// distinct keeps one of every read gathered by several routes, so a diamond of templates
// contributes each read once however many routes reach it.
func distinct(reads []pathRead) []pathRead {
seen := make(map[readKey]bool, len(reads))
out := reads[:0:0]
for _, r := range reads {
k := readKey{r.group, r.a.path, r.branches.pins.key(), r.branches.wholePins.key(), r.clash != nil}
if !seen[k] {
seen[k] = true
out = append(out, r)
}
}
return out
}
// viaEdge is what rendering e from n reads: the reference e reads, then what its leaf renders
// under the rows that reference pins.
func (f *readFold) viaEdge(n node, e renderEdge, route readRoute) []pathRead {
if !e.readsRef() {
return routed(f.reads(e.to), route)
}
a := e.read
tr := tableReadOf(n.(*template).head(a.head), a, e.to)
out := []pathRead{{route: route, a: a, tr: tr}}
below := f.reads(e.to)
if tr != nil {
below = pinBelow(below, tr)
}
return append(out, routed(below, route)...)
}
// pinBelow is the reads below a table read, once its selected rows are pinned above them: a read
// under a cell those rows keep out is dropped, since that cell never renders on this route, and
// a read whose own selectors clash with them carries that clash.
func pinBelow(reads []pathRead, tr *tableRead) []pathRead {
kept := reads[:0:0]
for _, r := range reads {
pins := r.branches.pins.clone()
if err := tr.replayBelow(&pins); err != nil {
continue
}
if r.tr != nil && r.clash == nil {
check := pins.clone()
r.clash = r.tr.replay(&check)
}
r.branches.pins = pins
kept = append(kept, r)
}
return kept
}
// rowReads is what the rows of t read: the cells of the columns its format renders, each tagged
// with its row, and the format's other reads, which every row shares. A format reaching its own
// cells through another category is checkOwnFamily's refusal, which runs before any survey of
// these reads.
func (f *readFold) rowReads(t *table, tag func(*branches, int) bool) []pathRead {
var out []pathRead
for _, e := range renderEdges(t.formatTemplate) {
if c, isColumn := e.to.(*tableColumn); isColumn && !e.readsRef() {
out = append(out, f.cellReads(c, tag)...)
continue
}
out = append(out, f.viaEdge(t.formatTemplate, e, readRoute{})...)
}
return out
}
// cellReads is what the cells of column c read, each tagged with its row.
func (f *readFold) cellReads(c *tableColumn, tag func(*branches, int) bool) []pathRead {
var out []pathRead
for r := 0; r < c.t.rowCount(); r++ {
cell := c.t.cellTemplate(r, c.i)
if cell == nil {
continue
}
for _, read := range f.reads(cell) {
if tag(&read.branches, r) {
out = append(out, read)
}
}
}
return out
}
// enter pins row r of t above a read, or reports that the rows already pinned keep it out.
func (b *branches) enter(t *table, r int) bool {
if b.pins.clash(t, r) != nil {
return false
}
b.pins = b.pins.entered(t, r)
return true
}
// routed names the route the render's root reaches reads by, where none is named yet.
func routed(reads []pathRead, route readRoute) []pathRead {
if route == (readRoute{}) {
return reads
}
out := make([]pathRead, len(reads))
for i, r := range reads {
if r.route == (readRoute{}) {
r.route = route
}
out[i] = r
}
return out
}
// spelled names the route, and the reference it reaches a read by where its root edge is not that
// reference.
func (r readRoute) spelled(ref string) string {
if ref == "" || ref == r.label {
return r.spelling
}
return fmt.Sprintf("%s with {%s}", r.spelling, ref)
}
// tableRead is what a reference path reads of a table family: the table its head
// names, the rows its selectors pin, the tables it draws — those it walks with no
// row pinned, and their unpinned ancestors — each selector's spelling, and whether
// it lands on a `tableRow`.
type tableRead struct {
headTable *table
pins pinSet
leafPins *pinSet // the rows the leaf renders in: pins, without the rows below the row a ".." stepped up to
drawn map[*table]bool
sels []tableSel
landsRow bool
}
// tableSel is one selector on the way: the table it selects a row of, and the path
// as written up to and including it.
type tableSel struct {
t *table
spelling string
}
// tableReadOf follows a reference path's steps for what it takes of a table family, so
// two paths pinning one row by different routes compare equal.
func tableReadOf(head node, a arm, leaf node) *tableRead {
t, isTable := head.(*table)
if !isTable {
return nil
}
r := &tableRead{headTable: t}
r.leafPins = &r.pins
_, r.landsRow = leaf.(*tableRow)
var climbed *table
for _, st := range a.steps {
switch st.kind {
case stepParent:
t, climbed = t.parentT, t.parentT
case stepChild:
if climbed != nil {
r.leafPins, climbed = r.leafPins.above(climbed), nil
}
t = t.descendant(st.name)
case stepSelect:
r.pins.pin(t, st.row)
r.sels = append(r.sels, tableSel{t, joinSegments(append([]string{a.writtenHead}, a.tail[:st.at+1]...))})
case stepDraw:
if r.leafPins != &r.pins {
continue // a step down after a step up draws apart from the render's pins
}
if r.drawn == nil {
r.drawn = map[*table]bool{}
}
for d, stop := t, r.pins.nearestPinned(t); d != stop; d = d.parentT {
r.drawn[d] = true
}
}
}
return r
}
// selected is the selector in r on t, or on the nearest ancestor of t it selects.
func (r *tableRead) selected(t *table) (tableSel, bool) {
for ; t != nil; t = t.parentT {
for _, s := range r.sels {
if s.t == t {
return s, true
}
}
}
return tableSel{}, false
}
// drawnOf is a table the read draws that pins holds a row of, if any.
func (r *tableRead) drawnOf(pins *pinSet) *table {
var found *table
pins.each(func(t *table, _ int) {
if found == nil && r.drawn[t] {
found = t
}
})
return found
}
// replay pins the read's rows into d, where they agree with the rows pinned before.
func (r *tableRead) replay(d *pinSet) error { return replayPins(&r.pins, d) }
// replayBelow pins into d the rows the read's leaf renders in.
func (r *tableRead) replayBelow(d *pinSet) error { return replayPins(r.leafPins, d) }
func replayPins(from, d *pinSet) error {
var err error
from.each(func(t *table, row int) {
if err == nil {
err = d.pinRow(t, row)
}
})
return err
}
+19 -27
View File
@@ -20,9 +20,8 @@ type Column struct {
} }
// Record is one record rendered from a template: every direct field is a column, // Record is one record rendered from a template: every direct field is a column,
// listed in name order. Each column is its own expansion, so a sibling field is // listed in name order. Each {…} draws afresh, while a pick bound by {x as n} is
// local to it, while a reference that reads a path is drawn once for the whole // drawn once for the whole record.
// record, per group, and a pick bound by {ref as n} once for the whole record.
type Record struct { type Record struct {
columns []Column columns []Column
} }
@@ -131,10 +130,10 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err) return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
} }
var draws renderDraws var frames frameStack
sc := renderScope{draws: &draws} sc := renderScope{frames: &frames}
if t, isTable := n.(*table); isTable { if t, isTable := n.(*table); isTable {
if n, err = tableRecord(f.rand, t, tail, sc); err != nil { if n, sc.row, err = tableRecord(f.rand, t, tail); err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err) return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
} }
} else if len(tail) > 0 { } else if len(tail) > 0 {
@@ -151,25 +150,22 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
return renderRecord(f.rand, shape.template, shape.columns, sc), nil return renderRecord(f.rand, shape.template, shape.columns, sc), nil
} }
// tableRecord walks a path's tail from a table to the table whose row is the record, // tableRecord walks a path's tail from a table to the table whose row is the record, and that
// pinning in sc the rows it selects or draws. // row, selected or drawn.
func tableRecord(s *generatorState, t *table, tail []string, sc renderScope) (node, error) { func tableRecord(s *generatorState, t *table, tail []string) (node, renderedRow, error) {
n, pins, err := descend(s, t, tail) n, pins, err := descend(s, t, tail)
if err != nil { if err != nil {
return nil, err return nil, renderedRow{}, err
} }
group := sc.groupDraws()
group.pins = pins
switch n := n.(type) { switch n := n.(type) {
case *table: case *table:
n.drawIn(s, &group.pins) return n, renderedRow{n, n.drawIn(s, &pins)}, nil
return n, nil
case *tableRow: case *tableRow:
return n.t, nil return n.t, renderedRow{n.t, pins.mustRow(n.t)}, nil
case *tableColumn: case *tableColumn:
return nil, fmt.Errorf("descends into %q, a column; a record is a table's row", n.t.header[n.i]) return nil, renderedRow{}, fmt.Errorf("descends into %q, a column; a record is a table's row", n.t.header[n.i])
} }
return n, nil return n, renderedRow{}, nil
} }
// recordShape is what recordOf settled about a node: the template to project, its // recordShape is what recordOf settled about a node: the template to project, its
@@ -203,12 +199,12 @@ type RecordTemplate struct {
columns []Column columns []Column
} }
// Fake renders the record with one draw. // Fake renders the record, one pick of each name across its columns.
func (t *RecordTemplate) Fake() *Record { func (t *RecordTemplate) Fake() *Record {
t.g.mu.Lock() t.g.mu.Lock()
defer t.g.mu.Unlock() defer t.g.mu.Unlock()
var draws renderDraws var frames frameStack
return renderRecord(t.g.rand, t.template, t.columns, renderScope{draws: &draws}) return renderRecord(t.g.rand, t.template, t.columns, 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
@@ -262,15 +258,11 @@ func recordOf(n node) (*template, []Column, error) {
return t, columns, nil return t, columns, nil
} }
// renderRecord draws each column once, in the name order recordOf fixed, as one render // renderRecord draws each column once, in the name order recordOf fixed, as one render, so the
// over sc's draws; a table's columns read the row pinned there. // columns read one pick of each name; a table's columns read sc's row.
func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record { func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record {
sc = sc.in(t)
if mark := sc.renderFrame(t); mark >= 0 { if mark := sc.renderFrame(t); mark >= 0 {
defer sc.draws.popFrames(mark) defer sc.frames.pop(mark)
}
if t.site.isFormat() {
sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins)
} }
r := &Record{columns: append([]Column(nil), columns...)} r := &Record{columns: append([]Column(nil), columns...)}
for i := range r.columns { for i := range r.columns {
+16 -19
View File
@@ -69,8 +69,7 @@ func refSegments(name string, folder []string) ([]string, error) {
// linkRefs binds every reference and compiles every format, once all data is merged, // 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 // 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, so two // refHeads under its root path and its tail reads like a sibling path.
// spellings of one target are one draw.
func linkRefs(sites []categorySite, root map[string]node) error { func linkRefs(sites []categorySite, root map[string]node) error {
if err := eachTemplate(sites, func(s categorySite, path string, t *template) error { if err := eachTemplate(sites, func(s categorySite, path string, t *template) error {
return linkTemplate(s.dir, path, s.path, t, root) return linkTemplate(s.dir, path, s.path, t, root)
@@ -123,23 +122,13 @@ func compileArm(t *template, a *arm) {
panic(internalError("{%s} reads a head nothing bound", a.spelling)) panic(internalError("{%s} reads a head nothing bound", a.spelling))
} }
w := compilePath(head, a.tail) w := compilePath(head, a.tail)
a.steps, a.leaves, a.cover = w.steps, w.leaves, w.cover a.steps, a.leaves = w.steps, w.leaves
switch _, held := t.compiled.held[a.head]; {
case a.isRefPath():
a.kind = refPathRead
case held:
a.kind = heldRead
}
} }
// resolveLink binds every reference t reads, refusing one to t's own category, and keys // resolveLink binds every reference t reads, refusing one to t's own category:
// t's draw group by that category, so a name is local to it:
// docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load // docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load
func (t *template) resolveLink(folder []string, path, category string, root map[string]node) (templateLink, error) { func (t *template) resolveLink(folder []string, path, category string, root map[string]node) (templateLink, error) {
var link templateLink var link templateLink
if t.drawGroup != "" {
link.drawGroupKey = category + "/" + t.drawGroup
}
names := refTokens(t.tokens) names := refTokens(t.tokens)
if len(names) == 0 { if len(names) == 0 {
return link, nil return link, nil
@@ -169,8 +158,8 @@ func (t *template) resolveLink(folder []string, path, category string, root map[
return link, nil return link, nil
} }
// columnRead is a record's column read by a format of that one reference alone, which is the // columnRead is a record's column read by a format of that one reference or name read alone,
// column: it takes the column's datatype and null. // which is the column: it takes the column's datatype and null.
type columnRead struct { type columnRead struct {
a arm a arm
column node column node
@@ -182,11 +171,19 @@ func columnReadOf(t *template, link templateLink) *columnRead {
return nil return nil
} }
a := splitArm(name, link.refs) a := splitArm(name, link.refs)
target, isTemplate := link.refHeads[a.head].(*template) if column := recordColumn(link.refHeads[a.head], a.tail); column != nil {
if !isTemplate || !target.isRecord || len(a.tail) != 1 { return &columnRead{a: a, column: column}
}
return nil
}
// recordColumn is the column tail names in head, nil where head is no record or tail no column of it.
func recordColumn(head node, tail []string) node {
target, isTemplate := head.(*template)
if !isTemplate || !target.isRecord || len(tail) != 1 {
return nil return nil
} }
return &columnRead{a: a, column: target.fields[a.tail[0]]} return target.fields[tail[0]]
} }
// loneRef is the reference a format of one reference token and nothing else reads. // loneRef is the reference a format of one reference token and nothing else reads.
+32 -103
View File
@@ -25,8 +25,6 @@ func (f *Generator) Fake(path string) (string, error) {
return "", fmt.Errorf("fejkdata: %w", err) return "", fmt.Errorf("fejkdata: %w", err)
} }
f.loadShippedAt(segments) f.loadShippedAt(segments)
var draws renderDraws
sc := renderScope{draws: &draws}
n, pins, err := descend(f.rand, &f.root, segments) n, pins, err := descend(f.rand, &f.root, segments)
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %s: %w", path, err) return "", fmt.Errorf("fejkdata: %s: %w", path, err)
@@ -34,9 +32,8 @@ func (f *Generator) Fake(path string) (string, error) {
if _, ok := n.(*folder); ok { if _, ok := n.(*folder); ok {
return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path) return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path)
} }
group := sc.groupDraws() var frames frameStack
group.pins = pins sc, _ := renderScope{frames: &frames}.at(n, &pins).enter(n, nil)
sc, _ = sc.at(n, &group.pins).enter(n, &group.memo)
return render(f.rand, n, sc), nil return render(f.rand, n, sc), nil
} }
@@ -53,15 +50,14 @@ func descend(s *generatorState, root node, segments []string) (node, pinSet, err
return n, *leafPins, nil return n, *leafPins, nil
} }
// renderOnce renders n as one render, over draws of its own. // renderOnce renders n as one render, over frames of its own.
func renderOnce(s *generatorState, n node) string { func renderOnce(s *generatorState, n node) string {
var draws renderDraws var frames frameStack
return render(s, n, renderScope{draws: &draws}) return render(s, n, renderScope{frames: &frames})
} }
// render evaluates a compiled node to a string. compile validates every node up // render evaluates a compiled node to a string. compile validates every node up
// front, so rendering a compiled tree cannot fail. sc holds the reference draws the // front, so rendering a compiled tree cannot fail.
// render shares; each repeat iteration renders over draws of its own.
// A child this switch renders is one renderEdges must list too, or the fences miss it. // A child this switch renders is one renderEdges must list too, or the fences miss it.
func render(s *generatorState, n node, sc renderScope) string { func render(s *generatorState, n node, sc renderScope) string {
switch n := n.(type) { switch n := n.(type) {
@@ -81,10 +77,9 @@ func render(s *generatorState, n node, sc renderScope) string {
} }
return n.t.cell(row, n.i) return n.t.cell(row, n.i)
case *template: case *template:
sc = sc.in(n)
if n.repeat == 1 { if n.repeat == 1 {
if mark := sc.renderFrame(n); mark >= 0 { if mark := sc.renderFrame(n); mark >= 0 {
defer sc.draws.popFrames(mark) defer sc.frames.pop(mark)
} }
if lit, fixed := n.fixedText(); fixed { if lit, fixed := n.fixedText(); fixed {
return lit return lit
@@ -97,8 +92,8 @@ func render(s *generatorState, n node, sc renderScope) string {
} }
} }
// renderRepeat renders each iteration of t as a render of its own, inside the name scopes // renderRepeat renders each iteration of t inside the name scopes rendering t, where a name bound
// rendering t, where a name bound outside t keeps its pick. // outside t keeps its pick, and a name t binds picks again.
func renderRepeat(s *generatorState, t *template, sc renderScope) string { func renderRepeat(s *generatorState, 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)))
@@ -106,23 +101,17 @@ func renderRepeat(s *generatorState, t *template, sc renderScope) string {
if i > 0 { if i > 0 {
b.WriteString(t.separator) b.WriteString(t.separator)
} }
b.WriteString(expandAnew(s, t, sc.draws.frameStack, sc.base)) if t.ownNameScope == nil {
b.WriteString(expand(s, t, sc))
continue
}
mark := sc.frames.push(newPickFrame(t.ownNameScope))
b.WriteString(expand(s, t, sc))
sc.frames.pop(mark)
} }
return b.String() return b.String()
} }
// expandAnew expands one repeat iteration of t as a render of its own, in no group, inside the
// name scopes of stack from base. Inlined into renderRepeat's loop, its draws would move to the heap.
//
//go:noinline
func expandAnew(s *generatorState, t *template, stack *frameStack, base int) string {
draws := renderDraws{frameStack: stack}
if t.ownNameScope != nil {
defer draws.popFrames(draws.pushFrame(newPickFrame(t.ownNameScope)))
}
return expand(s, t, renderScope{draws: &draws, base: base})
}
// pick selects one item. Uniform choices are O(1); weighted choices are an // pick selects one item. Uniform choices are O(1); weighted choices are an
// O(log n) search over precomputed cumulative weights. // O(log n) search over precomputed cumulative weights.
func pick(s *generatorState, c *choice) node { func pick(s *generatorState, c *choice) node {
@@ -135,31 +124,19 @@ func pick(s *generatorState, c *choice) node {
func expand(s *generatorState, t *template, sc renderScope) string { func expand(s *generatorState, t *template, sc renderScope) string {
var b strings.Builder var b strings.Builder
b.Grow(t.compiled.grow) b.Grow(t.compiled.grow)
// One draw per held name, for this expansion only: a nested template and each
// repeat iteration get their own, since each is its own expansion. A reference
// path reads its draw group's memo in sc instead.
var hold *drawMemo
if len(t.compiled.held) > 0 {
hold = &drawMemo{
variant: make(map[string]node, len(t.compiled.held)),
value: make(map[string]readValue, len(t.compiled.held)),
}
}
for i := range t.compiled.ops { for i := range t.compiled.ops {
o := &t.compiled.ops[i] o := &t.compiled.ops[i]
switch o.kind { switch o.kind {
case literalRun: case literalRun:
b.WriteString(o.lit) b.WriteString(o.lit)
case nameRead: case nameRead:
b.WriteString(readField(s, t, hold, sc, o.arms[s.IntN(len(o.arms))]).text) b.WriteString(readField(s, t, sc, o.arms[s.IntN(len(o.arms))]).text)
case builtinCall: case builtinCall:
// Read before the call, so the value a calc computes is the value the
// format showed. calcVars fixed the order op.operands holds.
var operands []string var operands []string
if len(o.operands) > 0 { if len(o.operands) > 0 {
operands = make([]string, len(o.operands)) operands = make([]string, len(o.operands))
for j, a := range o.operands { for j, a := range o.operands {
operands[j] = readField(s, t, hold, sc, a).text operands[j] = readField(s, t, sc, a).text
} }
} }
b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far b.WriteString(o.call(s, b.String(), operands)) // b.String() is the output so far
@@ -168,19 +145,10 @@ func expand(s *generatorState, t *template, sc renderScope) string {
return b.String() return b.String()
} }
// readField renders one arm of a token. A field the expansion holds — a level some // readField renders one arm of a token. A read of a name, or of a level a read of the name
// token addresses by a dotted path that is not a reference, or a field an operand // rendering addresses, is kept in that name's pick; every other read draws afresh, so {word}
// reads — is drawn once and kept in hold, so {place.postal-code} and {place.locality} // {word} draws twice and {p.a} {p.b} reads two draws of p.
// read one row, either read twice gives one value, and a shown operand is the operand func readField(s *generatorState, t *template, sc renderScope, a arm) readValue {
// computed. A field a read of a name addresses is kept in that name's pick.
// Every other field is drawn afresh, so {word} {word} still draws twice.
func readField(s *generatorState, t *template, hold *drawMemo, sc renderScope, a arm) readValue {
if a.kind == refPathRead {
return readReference(s, t, sc, a)
}
if sc.draws.trace != nil {
traceRead(sc.draws.trace, t, sc, a)
}
switch { switch {
case a.kind == namedRead: case a.kind == namedRead:
return readName(s, sc, a) return readName(s, sc, a)
@@ -188,56 +156,18 @@ func readField(s *generatorState, t *template, hold *drawMemo, sc renderScope, a
return readUnder(s, t, sc, a) return readUnder(s, t, sc, a)
} }
sc.pick = nil sc.pick = nil
switch { if !isRef(a.head) {
case a.kind == freshRead && isRef(a.head): leaf, _ := drawSteps(s, t.head(a.head), a.steps, nil, nil, a.levels)
return readValue{text: render(s, t.head(a.head), sc.entering())} return renderLeaf(s, leaf, sc)
case a.kind == freshRead:
return readValue{text: render(s, t.head(a.head), sc)}
}
return readMemo(s, t, hold, nil, sc, a)
}
// readReference reads a reference path, kept in its draw group's memo, so its draw
// spans the render.
func readReference(s *generatorState, t *template, sc renderScope, a arm) readValue {
if sc.draws.trace != nil {
traceRead(sc.draws.trace, t, sc, a)
}
group := sc.groupDraws()
return readMemo(s, t, &group.memo, &group.pins, sc, a)
}
func readMemo(s *generatorState, t *template, memo *drawMemo, pins *pinSet, sc renderScope, a arm) readValue {
if r, done := memo.value[a.path]; done {
return r
}
leaf, leafPins := drawSteps(s, t.head(a.head), a.steps, pins, memo, a.levels)
mark := -1
if pins != nil {
sc, mark = sc.at(leaf, leafPins).enter(leaf, memo)
} }
var pins pinSet
leaf, leafPins := drawSteps(s, t.head(a.head), a.steps, &pins, nil, a.levels)
sc, mark := sc.at(leaf, leafPins).enter(leaf, nil)
r := renderLeaf(s, leaf, sc) r := renderLeaf(s, leaf, sc)
if mark >= 0 { sc.frames.pop(mark)
sc.draws.popFrames(mark)
}
if memo.value == nil {
memo.value = map[string]readValue{}
}
memo.value[a.path] = r
return r return r
} }
func traceRead(trace renderTrace, t *template, sc renderScope, a arm) {
var row renderedRow
switch {
case t.site.isCell():
row = renderedRow{t.site.table, t.site.row}
case t.site.isFormat():
row = renderedRow{t.site.table, sc.rowOf(t.site.table)}
}
trace(strings.Clone(sc.group), row, a)
}
// renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one // renderLeaf draws and renders what a read lands on: null on a null item, or on a column of one
// reference alone whose read drew null. // reference alone whose read drew null.
func renderLeaf(s *generatorState, n node, sc renderScope) readValue { func renderLeaf(s *generatorState, n node, sc renderScope) readValue {
@@ -247,15 +177,14 @@ func renderLeaf(s *generatorState, n node, sc renderScope) readValue {
return readValue{null: true} return readValue{null: true}
case *template: case *template:
if leaf.link.readsColumn != nil { if leaf.link.readsColumn != nil {
return readReference(s, leaf, sc.in(leaf), leaf.link.readsColumn.a) return readField(s, leaf, sc, leaf.link.readsColumn.a)
} }
} }
return readValue{text: render(s, n, sc)} return readValue{text: render(s, n, sc)}
} }
// resolveChoice resolves a choice to one variant, so a held head is a concrete node the // resolveChoice resolves a choice to one variant. Nested choices unwrap too: a draw is one value,
// rest of the expansion shares. Nested choices unwrap too: a draw is one value, not // not another set to pick from.
// another set to pick from.
func resolveChoice(s *generatorState, n node) node { func resolveChoice(s *generatorState, n node) node {
for c, ok := n.(*choice); ok; c, ok = n.(*choice) { for c, ok := n.(*choice); ok; c, ok = n.(*choice) {
n = pick(s, c) n = pick(s, c)
+47 -12
View File
@@ -95,6 +95,7 @@ type structFields struct {
typ reflect.Type typ reflect.Type
label string label string
tags map[string]any tags map[string]any
paths map[string]string // each field tagged with a path, and the path
shape *structShape shape *structShape
} }
@@ -104,11 +105,12 @@ func (sc *structCompile) compileShape(t reflect.Type, label string) (*structShap
} }
sc.visiting[t] = true sc.visiting[t] = true
defer delete(sc.visiting, t) defer delete(sc.visiting, t)
c := &structFields{structCompile: sc, typ: t, label: label, tags: map[string]any{}, shape: &structShape{}} c := &structFields{structCompile: sc, typ: t, label: label, tags: map[string]any{}, paths: map[string]string{}, shape: &structShape{}}
if err := c.gatherFields(t, nil); err != nil { if err := c.gatherFields(t, nil); err != nil {
return nil, err return nil, err
} }
if len(c.tags) > 0 { if len(c.tags) > 0 {
bindShared(sc.root, c.tags, c.paths)
if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil { if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil {
return nil, err return nil, err
} }
@@ -172,11 +174,14 @@ func (c *structFields) addTag(sf reflect.StructField, tag string) error {
if visible, ok := c.typ.FieldByName(sf.Name); !ok || !slices.Equal(visible.Index, sf.Index) { if visible, ok := c.typ.FieldByName(sf.Name); !ok || !slices.Equal(visible.Index, sf.Index) {
return fmt.Errorf("%s.%s: hidden by another field named %s, so its fake tag cannot fill it; rename one", c.label, fieldPath(c.typ, sf.Index), sf.Name) return fmt.Errorf("%s.%s: hidden by another field named %s, so its fake tag cannot fill it; rename one", c.label, fieldPath(c.typ, sf.Index), sf.Name)
} }
v, err := tagValue(sf, tag) v, path, err := tagValue(sf, tag)
if err != nil { if err != nil {
return fmt.Errorf("%s.%s: %w", c.label, sf.Name, err) return fmt.Errorf("%s.%s: %w", c.label, sf.Name, err)
} }
c.tags[sf.Name] = v c.tags[sf.Name] = v
if path != "" {
c.paths[sf.Name] = path
}
return nil return nil
} }
@@ -215,22 +220,53 @@ func (c *structFields) nest(sf reflect.StructField, elem reflect.Type) error {
} }
// tagValue reads a field's fake tag as the value its column compiles from: an inline template // tagValue reads a field's fake tag as the value its column compiles from: an inline template
// as written, or a path as the reference {/path}. // as written, or a path as the reference {/path}, returning the path too.
func tagValue(sf reflect.StructField, tag string) (any, error) { func tagValue(sf reflect.StructField, tag string) (any, string, error) {
if err := checkTaggedType(sf); err != nil { if err := checkTaggedType(sf); err != nil {
return nil, err return nil, "", err
} }
inline, err := isTemplate(tag) inline, err := isTemplate(tag)
switch { switch {
case err != nil: case err != nil:
return nil, err return nil, "", err
case inline: case inline:
return inputValue(tag) v, err := inputValue(tag)
return v, "", err
} }
if err := checkPathNames(tag); err != nil { if err := checkPathNames(tag); err != nil {
return nil, err return nil, "", err
} }
return "{/" + tag + "}", nil return "{/" + tag + "}", tag, nil
}
// bindShared has the fields whose paths read into one category, two or more of them and none
// selecting a row, read one pick of it through a name, as a record's columns read one pick:
// docs/decisions.md#a-structs-path-tags-into-one-category-read-one-pick-of-it
func bindShared(root *folder, tags map[string]any, paths map[string]string) {
byCategory := map[string][]string{}
for _, field := range sortedNames(paths) {
segs, err := splitPath(paths[field])
if err != nil || hasSelector(segs) {
continue
}
if _, i := folderAt(root, segs); i < len(segs) {
category := strings.Join(segs[:i+1], ".")
byCategory[category] = append(byCategory[category], field)
}
}
var format strings.Builder
for _, category := range sortedNames(byCategory) {
fields := byCategory[category]
if len(fields) < 2 {
continue
}
name := strings.ReplaceAll(category, ".", "-")
fmt.Fprintf(&format, "{/%s as %s}", category, name)
for _, field := range fields {
tags[field] = "{" + name + strings.TrimPrefix(paths[field], category) + "}"
}
}
tags["format"] = format.String()
} }
// checkTaggedType rejects a tagged field no column can fill. // checkTaggedType rejects a tagged field no column can fill.
@@ -253,7 +289,6 @@ func checkTaggedType(sf reflect.StructField) error {
// compileRecord compiles the tagged fields of t as one record, and proves each column holds // compileRecord compiles the tagged fields of t as one record, and proves each column holds
// only what its field's Go type can. // only what its field's Go type can.
func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]any) error { func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]any) error {
tags["format"] = ""
n, err := compile(tags) n, err := compile(tags)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", label, err) return fmt.Errorf("%s: %w", label, err)
@@ -318,8 +353,8 @@ func (k columnKind) holds(v proven) bool {
// fill draws the record into v's tagged fields, then each nested struct as a record of its own. // fill draws the record into v's tagged fields, then each nested struct as a record of its own.
func (s *structShape) fill(state *generatorState, v reflect.Value) { func (s *structShape) fill(state *generatorState, v reflect.Value) {
if s.record != nil { if s.record != nil {
var draws renderDraws var frames frameStack
for i, c := range renderRecord(state, s.record, s.columns, renderScope{draws: &draws}).columns { for i, c := range renderRecord(state, s.record, s.columns, renderScope{frames: &frames}).columns {
setColumn(fieldAt(v, s.fieldIndexes[i]), c) setColumn(fieldAt(v, s.fieldIndexes[i]), c)
} }
} }
+2 -10
View File
@@ -11,7 +11,7 @@ import (
// table is a category whose rows come from a TSV beside it: the header names the // table is a category whose rows come from a TSV beside it: the header names the
// columns, each row is one draw, and the format renders the drawn row. A column is a // columns, each row is one draw, and the format renders the drawn row. A column is a
// cell of the row a render pinned; a cell carrying tokens compiles to a string node. // cell of the row a path pinned; a cell carrying tokens compiles to a string node.
type table struct { type table struct {
segment string // path's last segment segment string // path's last segment
path string // the category's path from the data root, which a selector is written at path string // the category's path from the data root, which a selector is written at
@@ -493,7 +493,7 @@ func (t *table) drawUnder(s *generatorState, pr int) int {
return rows[pickCum(s, lookup.childCum[k])] return rows[pickCum(s, lookup.childCum[k])]
} }
// drawIn is the render's row of t: the one pinned in p, else one drawn inside the nearest // drawIn is the row of t a path or a named pick reads: the one pinned in p, else one drawn inside the nearest
// pinned ancestor — its parent drawn inside that first where the ancestor is further up — or // pinned ancestor — its parent drawn inside that first where the ancestor is further up — or
// over the whole table, and pinned with its ancestors. // over the whole table, and pinned with its ancestors.
func (t *table) drawIn(s *generatorState, p *pinSet) int { func (t *table) drawIn(s *generatorState, p *pinSet) int {
@@ -599,11 +599,3 @@ func (t *table) selectorSpelling(r int) string {
} }
return fmt.Sprintf("%s line %d", t.file, r+2) return fmt.Sprintf("%s line %d", t.file, r+2)
} }
// familyRoot is the table a chain of parents ends at.
func (t *table) familyRoot() *table {
for t.parentT != nil {
t = t.parentT
}
return t
}