Draw every {…} afresh, keep a pick only by name, and delete drawGroup with the hold and draw fences

This commit is contained in:
2026-10-05 09:05:35 +02:00
committed by Lillem4n
parent 5b23ca7706
commit 6aebeebb72
21 changed files with 271 additions and 1443 deletions
+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
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
+4 -26
View File
@@ -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)
}
+7 -18
View File
@@ -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.
-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
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
}
+23 -55
View File
@@ -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})
+8 -28
View File
@@ -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
-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
}
// 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()
}
+28 -19
View File
@@ -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])
+27 -57
View File
@@ -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)
}
}
+8 -70
View File
@@ -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
+7 -19
View File
@@ -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
+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
// 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
+1 -57
View File
@@ -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
}
-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,
// 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 {
+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,
// 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.
+32 -103
View File
@@ -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)
+47 -12
View File
@@ -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)
}
}
+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
// 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
}