diff --git a/README.md b/README.md index 98b894f..ca16f9a 100644 --- a/README.md +++ b/README.md @@ -1015,22 +1015,19 @@ doc.go the package doc, and the vocabulary the package is written in fejkdata.go Generator, New, options, List 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/ -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 -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 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 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 -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 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 -holdfence.go the load fences over the hold: the routes and spellings that read one held name two ways -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 +draws.go the memo a named pick keeps its draws in, and the scope a render reads names and rows through +pins.go the pin set: the table rows one path or one named pick fixes 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 builtins.go the {name()} function registry and its implementations diff --git a/data.go b/data.go index cb31d96..18798d4 100644 --- a/data.go +++ b/data.go @@ -259,8 +259,10 @@ func (b binding) bind() error { if err := b.link(); err != nil { return err } - if err := b.scopeFence(); err != nil { - return err + if b.scopeFence != nil { + if err := b.scopeFence(); err != nil { + return err + } } if !b.typedByGo { 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 { 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) } diff --git a/doc.go b/doc.go index b3cdf6f..af6108c 100644 --- a/doc.go +++ b/doc.go @@ -17,30 +17,19 @@ package fejkdata // Vocabulary // // - 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`. -// - 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` -// start one each, `FakeStruct` one per record, and `expandAnew` one per -// 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. +// start one each, and `FakeStruct` one per record. // - 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 // repeat iteration: `nameBinding`, `nameScope`, `namedPick`, `pickFrame`. -// - memo — what a hold, a draw group or a named pick keeps its draws in, the -// variant drawn at each level and the value each path read: `drawMemo`, -// `readMemo`. -// - 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: +// - memo — what a named pick keeps its draws in, the variant drawn at each level +// and the value each path read: `drawMemo`. +// - pin — fixing which row of a table one path, or one named pick, uses: // `pinSet`, `pinSet.pin`, `table.drawIn`. // - family — a table and every table reaching it through a chain of -// `table.parentT`, named by the root that chain ends at: `table.familyRoot`. -// - 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`. +// `table.parentT`. // - fence — a load-time check, run over each category, template or struct as it // loads or compiles, so rendering a compiled tree cannot fail. diff --git a/drawfence.go b/drawfence.go deleted file mode 100644 index 415f8ba..0000000 --- a/drawfence.go +++ /dev/null @@ -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) -} diff --git a/draws.go b/draws.go index 3d31113..614dac8 100644 --- a/draws.go +++ b/draws.go @@ -1,11 +1,9 @@ package fejkdata -import "strings" - -// drawMemo is what a hold, a draw group or 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 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. +// 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 +// 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 { variant map[string]node value map[string]readValue @@ -30,39 +28,19 @@ func (m *drawMemo) stepDownPins(pins *pinSet, t *table, levels []string, at int) 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. type readValue struct { text string null bool } -// renderTrace is a test's view of the fields a render reads; nil outside a test. -type renderTrace func(group string, row renderedRow, a arm) - -// renderDraws is one render's reference draws: the unnamed draw group's, and each named one's; -// 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. +// renderScope is where a render reads: the frames of the name scopes 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 frames, moves to the heap. type renderScope struct { - draws *renderDraws - group string - row renderedRow + frames *frameStack + row renderedRow // base is the depth of the frame stack when the read rendering the category entered it, // below which no frame is the category's. base int @@ -77,14 +55,6 @@ type renderedRow struct { 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 // table's row or column, at the row pins holds for that table. func (sc renderScope) at(n node, pins *pinSet) renderScope { @@ -104,18 +74,3 @@ func (sc renderScope) rowOf(t *table) int { } 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 -} diff --git a/format.go b/format.go index 66fb88e..7d4b07f 100644 --- a/format.go +++ b/format.go @@ -247,7 +247,7 @@ func checkToken(t formatToken, fields map[string]node) ([]unboundRead, error) { } return checkReads(t, fields, true) case nameBind: - return nil, checkBind(t) + return nil, checkBind(t, fields) case nameRead: unbound, err := checkReads(t, fields, false) if err != nil { @@ -285,17 +285,14 @@ func checkReads(t formatToken, fields map[string]node, operands bool) ([]unbound return unbound, nil } -// checkBind proves a {ref as name} token is spelled once, binds a reference, and names a valid -// name that is no option. -func checkBind(t formatToken) error { +// checkBind proves a {x as name} token is spelled once, binds a reference or a path into a +// field, and names a valid name that is no option. +func checkBind(t formatToken, fields map[string]node) error { ref, name := t.boundRef, t.bound if trimmed := strings.TrimSpace(ref) + asWord + strings.TrimSpace(name); trimmed != t.body { return fmt.Errorf("token {%s}: write {%s}", t.body, trimmed) } - if !isRef(ref) { - 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 { + if err := checkBound(ref, fields); err != nil { return fmt.Errorf("token {%s}: %w", t.body, err) } if name == "" { @@ -310,6 +307,20 @@ func checkBind(t formatToken) error { 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. // 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 @@ -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 -// `template.head` resolves and the tail of a dotted path into it. A non-empty tail -// makes the arm a held draw: a reference path's for the render, any other for the -// expansion. +// `template.head` resolves and the tail of a dotted path into it. type arm struct { spelling string // as written, for messages head string @@ -388,7 +397,6 @@ type arm struct { path string // head and tail, the one path every way of writing this read shares steps []pathStep 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 named *nameBinding // namedRead: the binding of the name it reads through } @@ -397,16 +405,10 @@ type arm struct { type armKind uint8 const ( - freshRead armKind = iota // drawn afresh at every read - heldRead // drawn once per expansion, kept in its hold - refPathRead // drawn once per draw group, kept in its memo - namedRead // read through a name, kept in its pick + freshRead armKind = iota // drawn afresh at every read + 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 // linkRefs resolved it to; before linking, a reference is whole. func splitArm(name string, refs map[string]refBinding) arm { @@ -472,37 +474,10 @@ type op struct { operands []arm } -// formatOps is a compiled format: its ops, the size of its literal text, and the -// names its expansion holds: every head a path that is not a reference starts from, -// plus the fields an operand reads. +// formatOps is a compiled format: its ops, and the size of its literal text. type formatOps struct { ops []op 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) { @@ -511,10 +486,7 @@ func (c *formatOps) function(tok formatToken, refs map[string]refBinding, isName a := splitArm(operand, refs) if isName(a.head) { a.kind = namedRead - operands = append(operands, a) - continue } - c.holdName(a, fmt.Sprintf("%s operand %q", tok.fn, operand)) operands = append(operands, a) } 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) if isName(arms[i].head) { 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}) diff --git a/graph.go b/graph.go index a9cf92c..fa52949 100644 --- a/graph.go +++ b/graph.go @@ -2,6 +2,7 @@ package fejkdata import ( "fmt" + "slices" "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 -// name, reference, or choice index) for a readable cycle report. read is the linked -// 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. +// name, reference, or choice index) for a readable cycle report. type renderEdge struct { - to node - 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 + "}" + to node + label string } // renderEdges lists the children rendering n recurses into, mirroring render: a @@ -120,17 +106,11 @@ func renderEdges(n node) []renderEdge { return es case *template: 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 _, a := range o.operands { - add(a, o.fn) - } - for _, a := range o.arms { - add(a, "") + for _, a := range slices.Concat(o.operands, o.arms) { + for _, leaf := range a.leaves { + es = append(es, renderEdge{leaf, a.spelling}) + } } } return es diff --git a/holdfence.go b/holdfence.go deleted file mode 100644 index a605187..0000000 --- a/holdfence.go +++ /dev/null @@ -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 -} diff --git a/inline.go b/inline.go index 9beddcf..2fb596f 100644 --- a/inline.go +++ b/inline.go @@ -16,7 +16,7 @@ type Template struct { n node } -// Fake renders the template with one draw. +// Fake renders the template once. func (t *Template) Fake() string { t.g.mu.Lock() defer t.g.mu.Unlock() @@ -159,12 +159,6 @@ func bindInline(root *folder, n node, label string, typedByGo bool) error { scope: scope, link: func() error { return linkNodeRefs(scope, root.children) }, 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() } diff --git a/name.go b/name.go index 6e263c7..cf82e57 100644 --- a/name.go +++ b/name.go @@ -19,7 +19,8 @@ type nameScope struct { type nameBinding struct { 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 where string // the fields reaching the binding template, as a compile error spells them scope *nameScope @@ -33,11 +34,12 @@ type nameBinding struct { addressed map[string]string } -// nameUse is one read of a name: the path it reads into the name, "" for the name itself; whether -// a builtin reads it as an operand, and whether no reference can stand there; the draw group -// its template renders in; and whether it sits in a repeat nested inside the name's scope. +// nameUse is one read of a name: the path it reads into the name, "" for the name itself; the +// template reading it; whether a builtin reads it as an operand, and whether no reference can +// stand there; and whether it sits in a repeat nested inside the name's scope. type nameUse struct { - tail, group string + tail string + in *template 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 } 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 { 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 { 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 // format as compile reports them, and refuses a field spelling a name. scopes are every scope of // the category, which a refusal searches for a name bound where the read cannot see it. -func resolveReads(n node, where, group string, scopes []*nameScope) error { +func resolveReads(n node, where string, scopes []*nameScope) error { t, isTemplate := n.(*template) - if isTemplate && t.drawGroup != "" { - group = t.drawGroup - } - if err := eachContained(n, where, func(c node, where string) error { return resolveReads(c, where, group, scopes) }); err != nil { + if err := eachContained(n, where, func(c node, where string) error { return resolveReads(c, where, scopes) }); err != nil { return err } if !isTemplate { @@ -184,7 +183,7 @@ func resolveReads(n node, where, group string, scopes []*nameScope) error { if b == nil { 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 } @@ -221,15 +220,15 @@ func (t *template) isName(head string) bool { 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) { for _, tok := range t.tokens { if tok.kind != nameBind { continue } b := t.nameScope.bindings[tok.bound] - ref := t.link.refs[b.ref] - b.head, b.tail = t.link.refHeads[ref.head], ref.tail + a := splitArm(b.ref, t.link.refs) + 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 // every template is linked, so each binder has resolved that reference. 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 { return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.body, err) } 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 { @@ -275,7 +284,7 @@ func linkName(t *template, a *arm) error { return err } 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) for i := range a.levels { a.levels[i] = joinSegments(full[:i]) diff --git a/namefence.go b/namefence.go index b02d95b..6264eb5 100644 --- a/namefence.go +++ b/namefence.go @@ -3,6 +3,7 @@ package fejkdata import ( "fmt" "slices" + "strings" ) // 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 -// refuses reads of b in two draw groups, or inside a repeat, where what b names reads a reference path. +// checkUses refuses a binding read once at a spot the bound spelling can stand: that spelling +// draws the same way without the name. A field stands only beside its binding. func (b *nameBinding) checkUses() error { - if r := b.uses[0]; len(b.uses) == 1 && r.tail == "" && !r.nested && !r.noRef && len(b.tail) == 0 { - spelling := b.ref - 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{}) { + r := b.uses[0] + if len(b.uses) > 1 || r.nested || b.field && r.in != b.binder { return nil } - for _, u := range b.uses { - if u.nested { - 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) - } - if u.group != b.uses[0].group { - 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 := b.ref + switch { + case strings.HasPrefix(r.tail, ".."): + spelling += r.tail + case r.tail != "": + 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 { - if group == "" { - return "the unnamed one" - } - return fmt.Sprintf("%q", group) -} - -// 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 +// calcReads reports whether a calc reads spelling as one operand. +func calcReads(spelling string) bool { + n, err := parseCalc(spelling) + v, isVar := n.(calcVar) + return err == nil && isVar && string(v) == spelling } // 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 { - fresh := map[string]int{} + reads := map[string]int{} var into []arm for _, o := range t.compiled.ops { 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 { into = append(into, a) } - if a.kind == freshRead { - fresh[a.head]++ - } + reads[a.head]++ } } - for _, head := range sortedNames(fresh) { - if by, kept := b.addressed[join(key, head)]; kept && fresh[head] > 1 { + for _, head := range sortedNames(reads) { + 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) } } diff --git a/node.go b/node.go index 01fbf58..2b9e39c 100644 --- a/node.go +++ b/node.go @@ -53,9 +53,8 @@ type template struct { repeat int separator string 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 - isRecord bool // compiled at the top without a repeat, so its fields are record columns + 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 site tableSite 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 } -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. func (s tableSite) label(path string) string { 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. type templateLink struct { - refs map[string]refBinding // each reference the format reads -> what it resolves to - refHeads map[string]node // each refBinding.head -> the category it names - readsColumn *columnRead // set when the format is one reference alone reading a record's column - drawGroupKey string // its draw group keyed by its category: what a render reads its reference paths under + refs map[string]refBinding // each reference the format reads -> what it resolves to + refHeads map[string]node // each refBinding.head -> the category it names + readsColumn *columnRead // set when the format is one reference alone reading a record's column } func (*template) isNode() {} @@ -347,24 +341,20 @@ func compileTemplate(m map[string]any, pos position) (node, error) { if err != nil { 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) } toks, unbound, err := parseChecked(o.format, fields) if err != nil { return nil, err } - if err := checkNestedDrawGroup(fields, o.group); err != 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 + return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, isRecord: fieldPos == inColumn, unbound: unbound}, nil } // templateOptions is what a template object's option keys say. type templateOptions struct { datatype DataType format string - group string repeat int separator string 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 { return o, err } - if o.group, err = drawGroupOf(m, repeat); err != nil { - return o, err - } if sv, ok := m["separator"]; ok { if o.separator, ok = sv.(string); !ok { 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 } -// 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 // template objects carry weight; a present one must be finite and positive. func weightOf(raw any) (float64, error) { @@ -567,7 +505,7 @@ func checkPathNames(path string) error { func isOption(name string) bool { switch name { - case "datatype", "drawGroup", "format", "repeat", "separator", "weight": + case "datatype", "format", "repeat", "separator", "weight": return true } return false diff --git a/path.go b/path.go index ce7a242..700875b 100644 --- a/path.go +++ b/path.go @@ -281,24 +281,18 @@ const ( // 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 -// rows selected before it, and no level read carries a repeat or a drawGroup. It -// compiles the steps a draw takes, every leaf the path may render, and as cover the -// first choice it passes, else the leaf; level names the head in its errors. +// rows selected before it, and no level read carries a repeat. It compiles the steps +// a draw takes, and every leaf the path may render; level names the head in its errors. type pathCheck struct { pins pinSet level string tail []string steps []pathStep leaves []node - cover node } func (w *pathCheck) run(n node) (node, error) { - leaf, err := w.walk(n, w.tail) - if err == nil && w.cover == nil { - w.cover = leaf - } - return leaf, err + return w.walk(n, w.tail) } 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 switch x := n.(type) { case *choice: - if w.cover == nil { - w.cover = x - } return w.walkEvery(x, tail) case *table: 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 { - name := join(w.level, strings.Join(w.tail[:len(w.tail)-len(rest)], ".")) - switch { - case t.repeat > 1: + if t.repeat > 1 { + name := join(w.level, strings.Join(w.tail[:len(w.tail)-len(rest)], ".")) 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 } @@ -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; // 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 -// share it. It returns the leaf and the pins its row is in: a step down after a step up draws +// For a read through a name, memo keeps the variant drawn at each of levels, so paths sharing a +// 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. func drawSteps(s *generatorState, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) { var climbed *table diff --git a/pick.go b/pick.go index 7fbe12e..cab51a4 100644 --- a/pick.go +++ b/pick.go @@ -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 // 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 -// draw fences survey it in. +// pinned. type namedPick struct { named *nameBinding memo drawMemo @@ -24,44 +23,30 @@ func newPickFrame(scope *nameScope) *pickFrame { return f } -// frameStack is the frames of the name scopes rendering, innermost last. A render's draws point -// at it, and a repeat iteration's share it: holding the frames in the draws, or in a renderScope, -// would move every render's draws to the heap. +// frameStack is the frames of the name scopes rendering, innermost last. A render's scope points +// at it, and a repeat iteration's shares it: holding the frames in a renderScope would move every +// render's scope to the heap. type frameStack struct { frames []*pickFrame } -// pushFrame opens f until popFrames closes it, returning the mark popFrames takes. -func (d *renderDraws) pushFrame(f *pickFrame) int { - mark := d.depth() - if d.frameStack == nil { - d.frameStack = &frameStack{} - } - d.frameStack.frames = append(d.frameStack.frames, f) +// push opens f until pop closes it, returning the mark pop takes. +func (st *frameStack) push(f *pickFrame) int { + mark := len(st.frames) + st.frames = append(st.frames, f) return mark } -func (d *renderDraws) popFrames(mark int) { - if d.frameStack != nil { - d.frameStack.frames = d.frameStack.frames[:mark] - } -} +func (st *frameStack) pop(mark int) { st.frames = st.frames[:mark] } -func (d *renderDraws) depth() int { - if d.frameStack == nil { - return 0 - } - 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. +// enter starts a read landing on n: the read sees no frame opened before it, and gets a frame for +// each name scope around n, from memo where there is one, 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) { sc = sc.entering() for scope := scopeAround(n); scope != nil; scope = scope.up { if len(scope.order) > 0 { - sc.draws.pushFrame(memo.enteredFrame(scope)) + sc.frames.push(memo.enteredFrame(scope)) } } 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. func (sc renderScope) entering() renderScope { - sc.base, sc.pick = sc.draws.depth(), nil + sc.base, sc.pick = len(sc.frames.frames), nil return sc } @@ -89,6 +74,9 @@ func scopeAround(n node) *nameScope { } func (m *drawMemo) enteredFrame(scope *nameScope) *pickFrame { + if m == nil { + return newPickFrame(scope) + } f, ok := m.enteredFrames[scope] if !ok { f = newPickFrame(scope) @@ -106,16 +94,14 @@ func (sc renderScope) renderFrame(t *template) int { if t.ownNameScope == nil || sc.frameOf(t.ownNameScope) != nil { 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. func (sc renderScope) frameOf(scope *nameScope) *pickFrame { - if stack := sc.draws.frameStack; stack != nil { - for i := len(stack.frames) - 1; i >= sc.base; i-- { - if f := stack.frames[i]; f.scope == scope { - return f - } + for i := len(sc.frames.frames) - 1; i >= sc.base; i-- { + if f := sc.frames.frames[i]; f.scope == scope { + return f } } return nil @@ -131,9 +117,12 @@ func readName(s *generatorState, sc renderScope, a arm) readValue { return r } 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) r := p.renderAt(s, leaf, pins, a.path, sc) - sc.draws.popFrames(mark) + sc.frames.pop(mark) 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 -// 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 { if prefix == "" { return path diff --git a/pins.go b/pins.go index c7f4fd3..a4fd603 100644 --- a/pins.go +++ b/pins.go @@ -1,11 +1,6 @@ package fejkdata -import ( - "fmt" - "maps" - "sort" - "strings" -) +import "fmt" type tablePin struct { t *table @@ -73,33 +68,6 @@ func (p *pinSet) nearestPinned(t *table) *table { 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 // nearest ancestor pinned to a row r is not inside; nil where none does. func (p *pinSet) clash(t *table, r int) *table { @@ -164,27 +132,3 @@ func (p *pinSet) above(t *table) *pinSet { } 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 -} diff --git a/reads.go b/reads.go deleted file mode 100644 index 71f970b..0000000 --- a/reads.go +++ /dev/null @@ -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 -} diff --git a/record.go b/record.go index 27186a3..f319b30 100644 --- a/record.go +++ b/record.go @@ -20,9 +20,8 @@ type Column struct { } // 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 -// local to it, while a reference that reads a path is drawn once for the whole -// record, per group, and a pick bound by {ref as n} once for the whole record. +// listed in name order. Each {…} draws afresh, while a pick bound by {x as n} is +// drawn once for the whole record. type Record struct { columns []Column } @@ -131,10 +130,10 @@ func (f *Generator) FakeRecord(path string) (*Record, error) { if err != nil { return nil, fmt.Errorf("fejkdata: %s: %w", path, err) } - var draws renderDraws - sc := renderScope{draws: &draws} + var frames frameStack + sc := renderScope{frames: &frames} 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) } } 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 } -// tableRecord walks a path's tail from a table to the table whose row is the record, -// pinning in sc the rows it selects or draws. -func tableRecord(s *generatorState, t *table, tail []string, sc renderScope) (node, error) { +// tableRecord walks a path's tail from a table to the table whose row is the record, and that +// row, selected or drawn. +func tableRecord(s *generatorState, t *table, tail []string) (node, renderedRow, error) { n, pins, err := descend(s, t, tail) if err != nil { - return nil, err + return nil, renderedRow{}, err } - group := sc.groupDraws() - group.pins = pins switch n := n.(type) { case *table: - n.drawIn(s, &group.pins) - return n, nil + return n, renderedRow{n, n.drawIn(s, &pins)}, nil case *tableRow: - return n.t, nil + return n.t, renderedRow{n.t, pins.mustRow(n.t)}, nil 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 @@ -203,12 +199,12 @@ type RecordTemplate struct { 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 { t.g.mu.Lock() defer t.g.mu.Unlock() - var draws renderDraws - return renderRecord(t.g.rand, t.template, t.columns, renderScope{draws: &draws}) + var frames frameStack + return renderRecord(t.g.rand, t.template, t.columns, renderScope{frames: &frames}) } // 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 } -// renderRecord draws each column once, in the name order recordOf fixed, as one render -// over sc's draws; a table's columns read the row pinned there. +// renderRecord draws each column once, in the name order recordOf fixed, as one render, so the +// columns read one pick of each name; a table's columns read sc's row. func renderRecord(s *generatorState, t *template, columns []Column, sc renderScope) *Record { - sc = sc.in(t) if mark := sc.renderFrame(t); mark >= 0 { - defer sc.draws.popFrames(mark) - } - if t.site.isFormat() { - sc = sc.at(t.site.table.rowNode, &sc.groupDraws().pins) + defer sc.frames.pop(mark) } r := &Record{columns: append([]Column(nil), columns...)} for i := range r.columns { diff --git a/reference.go b/reference.go index 530f368..e37563c 100644 --- a/reference.go +++ b/reference.go @@ -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, // 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 -// spellings of one target are one draw. +// refHeads under its root path and its tail reads like a sibling path. func linkRefs(sites []categorySite, root map[string]node) error { if err := eachTemplate(sites, func(s categorySite, path string, t *template) error { 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)) } w := compilePath(head, a.tail) - a.steps, a.leaves, a.cover = w.steps, w.leaves, w.cover - switch _, held := t.compiled.held[a.head]; { - case a.isRefPath(): - a.kind = refPathRead - case held: - a.kind = heldRead - } + a.steps, a.leaves = w.steps, w.leaves } -// resolveLink binds every reference t reads, refusing one to t's own category, and keys -// t's draw group by that category, so a name is local to it: +// resolveLink binds every reference t reads, refusing one to t's own category: // docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load func (t *template) resolveLink(folder []string, path, category string, root map[string]node) (templateLink, error) { var link templateLink - if t.drawGroup != "" { - link.drawGroupKey = category + "/" + t.drawGroup - } names := refTokens(t.tokens) if len(names) == 0 { return link, nil @@ -169,8 +158,8 @@ func (t *template) resolveLink(folder []string, path, category string, root map[ return link, nil } -// columnRead is a record's column read by a format of that one reference alone, which is the -// column: it takes the column's datatype and null. +// columnRead is a record's column read by a format of that one reference or name read alone, +// which is the column: it takes the column's datatype and null. type columnRead struct { a arm column node @@ -182,11 +171,19 @@ func columnReadOf(t *template, link templateLink) *columnRead { return nil } a := splitArm(name, link.refs) - target, isTemplate := link.refHeads[a.head].(*template) - if !isTemplate || !target.isRecord || len(a.tail) != 1 { + if column := recordColumn(link.refHeads[a.head], a.tail); column != nil { + 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 &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. diff --git a/render.go b/render.go index d862e96..0a32091 100644 --- a/render.go +++ b/render.go @@ -25,8 +25,6 @@ func (f *Generator) Fake(path string) (string, error) { return "", fmt.Errorf("fejkdata: %w", err) } f.loadShippedAt(segments) - var draws renderDraws - sc := renderScope{draws: &draws} n, pins, err := descend(f.rand, &f.root, segments) if err != nil { 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 { return "", fmt.Errorf("fejkdata: %s names a folder, not a value", path) } - group := sc.groupDraws() - group.pins = pins - sc, _ = sc.at(n, &group.pins).enter(n, &group.memo) + var frames frameStack + sc, _ := renderScope{frames: &frames}.at(n, &pins).enter(n, 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 } -// 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 { - var draws renderDraws - return render(s, n, renderScope{draws: &draws}) + var frames frameStack + return render(s, n, renderScope{frames: &frames}) } // 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 -// render shares; each repeat iteration renders over draws of its own. +// front, so rendering a compiled tree cannot fail. // 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 { switch n := n.(type) { @@ -81,10 +77,9 @@ func render(s *generatorState, n node, sc renderScope) string { } return n.t.cell(row, n.i) case *template: - sc = sc.in(n) if n.repeat == 1 { if mark := sc.renderFrame(n); mark >= 0 { - defer sc.draws.popFrames(mark) + defer sc.frames.pop(mark) } if lit, fixed := n.fixedText(); fixed { 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 -// rendering t, where a name bound outside t keeps its pick. +// renderRepeat renders each iteration of t inside the name scopes rendering t, where a name bound +// outside t keeps its pick, and a name t binds picks again. func renderRepeat(s *generatorState, t *template, sc renderScope) string { var b strings.Builder 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 { 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() } -// 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 // O(log n) search over precomputed cumulative weights. 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 { var b strings.Builder 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 { o := &t.compiled.ops[i] switch o.kind { case literalRun: b.WriteString(o.lit) 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: - // 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 if len(o.operands) > 0 { operands = make([]string, len(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 @@ -168,19 +145,10 @@ func expand(s *generatorState, t *template, sc renderScope) string { return b.String() } -// readField renders one arm of a token. A field the expansion holds — a level some -// token addresses by a dotted path that is not a reference, or a field an operand -// reads — is drawn once and kept in hold, so {place.postal-code} and {place.locality} -// read one row, either read twice gives one value, and a shown operand is the operand -// 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) - } +// readField renders one arm of a token. A read of a name, or of a level a read of the name +// rendering addresses, is kept in that name's pick; every other read draws afresh, so {word} +// {word} draws twice and {p.a} {p.b} reads two draws of p. +func readField(s *generatorState, t *template, sc renderScope, a arm) readValue { switch { case a.kind == namedRead: 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) } sc.pick = nil - switch { - case a.kind == freshRead && isRef(a.head): - return readValue{text: render(s, t.head(a.head), sc.entering())} - 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) + if !isRef(a.head) { + leaf, _ := drawSteps(s, t.head(a.head), a.steps, nil, nil, a.levels) + return renderLeaf(s, leaf, sc) } + 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) - if mark >= 0 { - sc.draws.popFrames(mark) - } - if memo.value == nil { - memo.value = map[string]readValue{} - } - memo.value[a.path] = r + sc.frames.pop(mark) 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 // reference alone whose read drew null. 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} case *template: 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)} } -// resolveChoice resolves a choice to one variant, so a held head is a concrete node the -// rest of the expansion shares. Nested choices unwrap too: a draw is one value, not -// another set to pick from. +// resolveChoice resolves a choice to one variant. Nested choices unwrap too: a draw is one value, +// not another set to pick from. func resolveChoice(s *generatorState, n node) node { for c, ok := n.(*choice); ok; c, ok = n.(*choice) { n = pick(s, c) diff --git a/struct.go b/struct.go index a27388c..5a14374 100644 --- a/struct.go +++ b/struct.go @@ -95,6 +95,7 @@ type structFields struct { typ reflect.Type label string tags map[string]any + paths map[string]string // each field tagged with a path, and the path shape *structShape } @@ -104,11 +105,12 @@ func (sc *structCompile) compileShape(t reflect.Type, label string) (*structShap } sc.visiting[t] = true 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 { return nil, err } 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 { 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) { 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 { return fmt.Errorf("%s.%s: %w", c.label, sf.Name, err) } c.tags[sf.Name] = v + if path != "" { + c.paths[sf.Name] = path + } 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 -// as written, or a path as the reference {/path}. -func tagValue(sf reflect.StructField, tag string) (any, error) { +// as written, or a path as the reference {/path}, returning the path too. +func tagValue(sf reflect.StructField, tag string) (any, string, error) { if err := checkTaggedType(sf); err != nil { - return nil, err + return nil, "", err } inline, err := isTemplate(tag) switch { case err != nil: - return nil, err + return nil, "", err case inline: - return inputValue(tag) + v, err := inputValue(tag) + return v, "", err } 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. @@ -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 // only what its field's Go type can. func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]any) error { - tags["format"] = "" n, err := compile(tags) if err != nil { 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. func (s *structShape) fill(state *generatorState, v reflect.Value) { if s.record != nil { - var draws renderDraws - for i, c := range renderRecord(state, s.record, s.columns, renderScope{draws: &draws}).columns { + var frames frameStack + for i, c := range renderRecord(state, s.record, s.columns, renderScope{frames: &frames}).columns { setColumn(fieldAt(v, s.fieldIndexes[i]), c) } } diff --git a/table.go b/table.go index 97e256d..5e27ae3 100644 --- a/table.go +++ b/table.go @@ -11,7 +11,7 @@ import ( // 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 -// 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 { segment string // path's last segment 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])] } -// 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 // over the whole table, and pinned with its ancestors. 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) } - -// familyRoot is the table a chain of parents ends at. -func (t *table) familyRoot() *table { - for t.parentT != nil { - t = t.parentT - } - return t -}