From 047237f361c49caca869557462e8ce318f69259d Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Thu, 17 Sep 2026 12:54:24 +0200 Subject: [PATCH] Replay each path's selectors through the walk for the family fence, require a key for a name, refuse a name shadowing a key, a table reading its own family and an ancestor's column clash, add the row node and family.go, and pin eight rows inline --- CHANGELOG.md | 6 +- README.md | 37 +++++-- draw.go | 214 +---------------------------------- family.go | 300 ++++++++++++++++++++++++++++++++++++++++++++++++++ fejkdata.go | 6 +- graph.go | 8 +- hold.go | 15 +-- path.go | 13 ++- record.go | 5 +- reference.go | 19 ++++ render.go | 12 +- table.go | 42 ++++--- table_test.go | 14 +-- value.go | 5 +- 14 files changed, 419 insertions(+), 277 deletions(-) create mode 100644 family.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 997eabd..4bf9871 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,11 @@ replacement, and each removed path, column or flag. `key`, `name`, `weight` and `parent`; a path selects a row by key or name, `misc.country[SE]`, and descends to a linked table by name; linked tables draw consistently within one render and draw group. `rows` is an option, so no - template may carry a field of that name. + template may carry a field of that name. Refused at `New`: a `name` without a + `key`, a name spelling another row's key, a table named like a column of any + table above it, a table whose format or cell references its own family, and, + within one render and draw group, a path drawing a table another path selects a + row of, or two paths pinning different rows of one table. - `New` refuses a root choice of templates sharing one format and one set of string fields, naming the rows TSV to write instead. - `misc.country`, `misc.currency`, `misc.language`, `misc.httpstatus` and diff --git a/README.md b/README.md index 5d86fe1..ec29b58 100644 --- a/README.md +++ b/README.md @@ -382,8 +382,10 @@ is refused, since a cell has no sibling. `New` proves the header, the options an cell token, and refuses a TSV no category names, a key that is empty or repeats, a weight that is not a positive number, and a key or name holding `[`, `]`, `{`, `}`, `"` or `|`, which a selector cannot spell; the rows are indexed on the first draw that -selects one. The table's options are its own — `rows`, `key`, `name`, `weight` and -`parent` — so a column may be named `name`, as one usually is. +selects one. A `name` needs a `key`, since a name naming several rows is reported by +their keys, and a name spelling another row's key is refused, since the key would +select first and the name never. The table's options are its own — `rows`, `key`, +`name`, `weight` and `parent` — so a column may be named `name`, as one usually is. A choice of templates sharing one format and one set of string fields is a table written by hand, and `New` refuses it in a data file naming the TSV to write; an @@ -393,9 +395,8 @@ inline template has no file beside it, so there it stays a choice. `[key]` or `[name]` after a table's name selects one row: `misc.country[SE]` and `misc.country[Sweden]` name one row, and `misc.country[SE].capital` reads its column. -A key wins over a name that spells the same, and a name naming several rows is an -error listing their keys, unless a row selected before it settles which -([Linked tables](#linked-tables)). A selector is part of the path, so it works +A name naming several rows is an error listing their keys, unless a row selected +before it settles which ([Linked tables](#linked-tables)). A selector is part of the path, so it works wherever a path does: `Fake`, `FakeRecord`, a `{/misc.country[SE].capital}` reference and a struct tag. A dot inside the brackets belongs to the key or name, so `city[St. Louis]` selects it. A path starts with a name, and `[` still opens a JSON @@ -436,9 +437,14 @@ tables in one render and group selects the same rows: one that selects none besi one that does is refused naming the spelling that does, `{/country[SE].city.name}` beside `{/country[SE].name}`, and two selecting different rows are refused naming a `drawGroup` to draw them apart in. A bare `{/city}` beside a path into its family is -refused too, since a bare reference draws each time. `New` also refuses a link cell -that is no key of the parent, a parent row no child links to, a chain of parents that -closes, and a child named like one of its parent's columns. +refused too, since a bare reference draws each time, and so is a path that draws a +table another path in the group selects a row of, `{/city.name}` beside +`{/country[SE].name}`, which the first token rendered would otherwise decide. `New` +also refuses a link cell that is no key of the parent, a parent row no child links +to, a chain of parents that closes, a table named like a column of any table above +it, and a cell or format of a table that references a table of its own family, since +a row rendered whole would draw the family apart from itself: read the family from a +template beside it, or add the value as a column. ### Options and fields @@ -907,8 +913,18 @@ renamed or retyped line is a major. - **A selector is bracketed, and a dot inside it is literal.** `municipality[0180]` reads as selection to anyone who has indexed an array, and `[St. Louis]` keeps a name whole where a colon or a dot-separated spelling could not; zsh needs the - brackets quoted, which the README's examples show. A key wins over a name that - spells the same, since a key names one row by contract and a name may not. + brackets quoted, which the README's examples show. A name that spells another + row's key is refused at load rather than shadowed: a key names one row by + contract, so the name could never select its own, and the check is one lookup per + row against the key index that already exists. +- **`parent` names the link column and the table alike.** One word says both, so a + child table sits in its parent's folder and links on a column of the parent's + name; the geo plan wants exactly that, and a table needing another folder or + another column name would be asking for a second spelling. +- **After a row, a path names a column or a linked table.** `locality[Lund].address`, + a template beside the family under a selected row, is refused today; admitting it + later is additive, since a refused spelling gains a meaning and no accepted one + changes, so the door stays open for the address records the plan describes. - **A table read into is pinned; a table rendered whole draws afresh.** A path into a table pins its row for the render and group, as a reference path pins its level, and a bare `{/city}` draws each time, as a bare reference does; so a bare table @@ -1015,6 +1031,7 @@ inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplat template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format hold.go the hold: one draw per expansion for paths and operands, and its fences draw.go one reference draw per render and group: draw sets, the group option, and its fence +family.go a family of linked tables: the rows a render pins, and the fence over paths into one family reference.go reference sigils, and binding references across the tree graph.go the render graph: edges, cycles, the repeat bound, tree walks builtins.go the {name()} function registry and its implementations diff --git a/draw.go b/draw.go index 06b917f..ebc1111 100644 --- a/draw.go +++ b/draw.go @@ -21,110 +21,6 @@ type drawScope struct { row int } -// pinned is the row the render pinned for t, if any. -func (d *draws) pinned(t *table) (int, bool) { - for _, p := range d.pins[:d.npins] { - if p.t == t { - return p.row, true - } - } - r, ok := d.more[t] - return r, ok -} - -// mustRow is the row pinned for t, which the walk reaching a column pinned. -func (d *draws) mustRow(t *table) int { - r, ok := d.pinned(t) - if !ok { - panic(fmt.Sprintf("fejkdata: a column of %s is rendered with no row pinned", t.category)) - } - return r -} - -// pin pins row r of t, and the rows of t's ancestors it links to. -func (d *draws) pin(t *table, r int) { - for { - if _, done := d.pinned(t); done { - return - } - if d.npins < len(d.pins) { - d.pins[d.npins] = tablePin{t, r} - d.npins++ - } else { - if d.more == nil { - d.more = map[*table]int{} - } - d.more[t] = r - } - if t.parentT == nil { - return - } - t, r = t.parentT, t.parentRow(r) - } -} - -// rowOf is the render's row of t: the one pinned, 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 (d *draws) rowOf(t *table) int { - if r, ok := d.pinned(t); ok { - return r - } - r := -1 - for a := t.parentT; a != nil && r < 0; a = a.parentT { - if _, ok := d.pinned(a); !ok { - continue - } - if t.parentT != a { - d.rowOf(t.parentT) - } - pr, _ := d.pinned(t.parentT) - r = t.drawUnder(d.s, pr) - } - if r < 0 { - r = t.draw(d.s) - } - d.pin(t, r) - return r -} - -// selectRow pins the row a selector names, refusing one outside the rows pinned -// before it. -func (d *draws) selectRow(t *table, sel string) error { - r, err := t.find(sel, d) - if err != nil { - return err - } - if pr, ok := d.pinned(t); ok && pr != r { - return fmt.Errorf("%s is not %s, the row already drawn", t.selectorSpelling(r), t.selectorSpelling(pr)) - } - for a := t.parentT; a != nil; a = a.parentT { - if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) { - return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa)) - } - } - d.pin(t, r) - return nil -} - -// inside keeps the rows of t that sit inside every pinned ancestor. -func (d *draws) inside(t *table, rows []int) []int { - for a := t.parentT; a != nil; a = a.parentT { - pa, ok := d.pinned(a) - if !ok { - continue - } - var kept []int - for _, r := range rows { - if t.under(r, a, pa) { - kept = append(kept, r) - } - } - rows = kept - } - return rows -} - // newDrawSet makes the unnamed group's maps where the set is declared, keeping them on that frame's // stack for a render that reads through them; a zero drawSet makes them on its first read instead. func newDrawSet(s *session) drawSet { @@ -368,78 +264,6 @@ type pathRead struct { tr *tableRead // set where the reference names a table } -// tableRead is what a reference reads of a table family: the table named, the rows -// its selectors pin along the way, and whether it lands on a row rendered whole. -type tableRead struct { - head *table - sels []tableSel - whole bool -} - -// tableSel is one selector on the way: the table it selects a row of, and the -// selector's spelling up to its closing bracket. -type tableSel struct { - t *table - row int - spelling string -} - -// tableReadOf reads what a reference path does of a table, replaying its selectors -// over a walk of its own — checkPath proved each names a row — so two paths naming -// one row by key and by name compare equal. -func tableReadOf(head node, a arm, leaf node) *tableRead { - t, isTable := head.(*table) - if !isTable { - return nil - } - tr := &tableRead{head: t} - var pins draws - cur, seen := t, a.name - for _, seg := range a.tail { - switch { - case isSelector(seg): - end := strings.Index(seen, "]") + 1 - _ = pins.selectRow(cur, selectorOf(seg)) - row, _ := pins.pinned(cur) - tr.sels = append(tr.sels, tableSel{cur, row, a.name[:len(a.name)-len(seen)+end]}) - seen = seen[end:] - case cur.children[seg] != nil: - cur = cur.children[seg] - } - } - c, isColumn := leaf.(*column) - tr.whole = isColumn && c.i < 0 - return tr -} - -// family is the table a chain of parents ends at. -func (t *table) family() *table { - for t.parentT != nil { - t = t.parentT - } - return t -} - -// selected is the selector in r that pins t, or the nearest ancestor of t it pins. -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 -} - -func (r *tableRead) selection() string { - parts := make([]string, len(r.sels)) - for i, s := range r.sels { - parts[i] = fmt.Sprintf("%s[%d]", s.t.category, s.row) - } - return strings.Join(parts, " ") -} - type drawVisit struct { n node group string @@ -501,45 +325,9 @@ func (w *drawWalk) check() error { if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) { return overlapError(level.at.route, level.a.name, into) } - if err := checkFamily(level, into); err != nil { - return err - } } } - return nil -} - -// checkFamily refuses two reads of one table family in one group that cannot read one consistent -// draw: a table rendered whole beside a path into the family, and two paths selecting different rows. -func checkFamily(a, b pathRead) error { - if a.tr == nil || b.tr == nil || a.tr.head.family() != b.tr.head.family() { - return nil - } - for _, pair := range [][2]pathRead{{a, b}, {b, a}} { - bare, path := pair[0], pair[1] - if len(bare.a.tail) == 0 && len(path.a.tail) > 0 { - return overlapError(bare.at.route, bare.a.name, path) - } - } - if len(a.a.tail) == 0 || len(b.a.tail) == 0 || a.tr.selection() == b.tr.selection() { - return nil - } - for _, pair := range [][2]pathRead{{a, b}, {b, a}} { - selected, plain := pair[0], pair[1] - if len(plain.tr.sels) > 0 { - continue - } - if s, ok := selected.tr.selected(plain.tr.head); ok { - tail := plain.a.tail - if s.t != plain.tr.head { - tail = append([]string{plain.tr.head.category}, tail...) - } - return fmt.Errorf("%s reads %s without the row %s selects; write {%s.%s}, or draw them apart with a drawGroup", - plain.at.route.spelled(plain.a.name), plain.tr.head.category, selected.at.route.spelled(selected.a.name), s.spelling, strings.Join(tail, ".")) - } - } - return fmt.Errorf("%s and %s select different rows of one table family; select the same rows in both, or draw them apart with a drawGroup", - a.at.route.spelled(a.a.name), b.at.route.spelled(b.a.name)) + return checkFamilies(w.reads) } func overlapError(route drawRoute, ref string, into pathRead) error { diff --git a/family.go b/family.go new file mode 100644 index 0000000..6835f8a --- /dev/null +++ b/family.go @@ -0,0 +1,300 @@ +package fejkdata + +import ( + "fmt" + "sort" + "strings" +) + +// tablePin is one table's pinned row. +type tablePin struct { + t *table + row int +} + +// pinned is the row the render pinned for t, if any. +func (d *draws) pinned(t *table) (int, bool) { + for _, p := range d.pins[:d.npins] { + if p.t == t { + return p.row, true + } + } + r, ok := d.more[t] + return r, ok +} + +// mustRow is the row pinned for t, which the walk reaching a column pinned. +func (d *draws) mustRow(t *table) int { + r, ok := d.pinned(t) + if !ok { + panic(fmt.Sprintf("fejkdata: a column of %s is rendered with no row pinned", t.category)) + } + return r +} + +// pin pins row r of t, and the rows of t's ancestors it links to. +func (d *draws) pin(t *table, r int) { + for { + if _, done := d.pinned(t); done { + return + } + if d.npins < len(d.pins) { + d.pins[d.npins] = tablePin{t, r} + d.npins++ + } else { + if d.more == nil { + d.more = map[*table]int{} + } + d.more[t] = r + } + if t.parentT == nil { + return + } + t, r = t.parentT, t.parentRow(r) + } +} + +// each calls fn for every pinned row, in pin order, the spilled ones by table name. +func (d *draws) each(fn func(t *table, r int)) { + for _, p := range d.pins[:d.npins] { + fn(p.t, p.row) + } + spilled := make([]*table, 0, len(d.more)) + for t := range d.more { + spilled = append(spilled, t) + } + sort.Slice(spilled, func(i, j int) bool { return spilled[i].category < spilled[j].category }) + for _, t := range spilled { + fn(t, d.more[t]) + } +} + +// rowOf is the render's row of t: the one pinned, 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 (d *draws) rowOf(t *table) int { + if r, ok := d.pinned(t); ok { + return r + } + r := -1 + for a := t.parentT; a != nil && r < 0; a = a.parentT { + if _, ok := d.pinned(a); !ok { + continue + } + if t.parentT != a { + d.rowOf(t.parentT) + } + pr, _ := d.pinned(t.parentT) + r = t.drawUnder(d.s, pr) + } + if r < 0 { + r = t.draw(d.s) + } + d.pin(t, r) + return r +} + +// pinRow pins row r of t where it agrees with the rows pinned before it. +func (d *draws) pinRow(t *table, r int) error { + if pr, ok := d.pinned(t); ok && pr != r { + return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.category) + } + for a := t.parentT; a != nil; a = a.parentT { + if pa, ok := d.pinned(a); ok && !t.under(r, a, pa) { + return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa)) + } + } + d.pin(t, r) + return nil +} + +// selectRow pins the row a selector names. +func (d *draws) selectRow(t *table, sel string) error { + r, err := t.find(sel, d) + if err != nil { + return err + } + return d.pinRow(t, r) +} + +// inside keeps the rows of t that sit inside every pinned ancestor. +func (d *draws) inside(t *table, rows []int) []int { + for a := t.parentT; a != nil; a = a.parentT { + pa, ok := d.pinned(a) + if !ok { + continue + } + var kept []int + for _, r := range rows { + if t.under(r, a, pa) { + kept = append(kept, r) + } + } + rows = kept + } + return rows +} + +// 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 row rendered whole. +type tableRead struct { + head *table + pins draws + drawn map[*table]bool + sels []tableSel + whole 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 replays a reference's selectors through the walk a render uses, so +// two paths pinning one row by different routes compare equal. checkPath proved +// each selector names a row. +func tableReadOf(head node, a arm, leaf node) *tableRead { + t, isTable := head.(*table) + if !isTable { + return nil + } + tr := &tableRead{head: t, drawn: map[*table]bool{}} + _, _ = walkPath(t, a.tail, pathWalk{pins: &tr.pins}) + written := a.name[:len(a.name)-len(joinSegments(a.tail))] + cur := t + tr.draws(cur) + for i, seg := range a.tail { + switch d := cur.descendant(seg); { + case isSelector(seg): + tr.sels = append(tr.sels, tableSel{cur, written + joinSegments(a.tail[:i+1])}) + case d != nil: + cur = d + tr.draws(cur) + } + } + _, tr.whole = leaf.(*row) + return tr +} + +// draws marks t and its ancestors drawn, up to the first the read pins. +func (r *tableRead) draws(t *table) { + for ; t != nil; t = t.parentT { + if _, pinned := r.pins.pinned(t); pinned { + return + } + r.drawn[t] = true + } +} + +// joinSegments spells segments as a path: a selector attaches to the name before it. +func joinSegments(segs []string) string { + var b strings.Builder + for _, s := range segs { + if b.Len() > 0 && !isSelector(s) { + b.WriteByte('.') + } + b.WriteString(s) + } + return b.String() +} + +// family is the table a chain of parents ends at. +func (t *table) family() *table { + for t.parentT != nil { + t = t.parentT + } + return t +} + +// 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 +} + +// checkFamilies refuses reads of one table family in one draw group that cannot +// read one consistent draw: a table rendered whole beside a path into the family, +// a table one read draws that another pins, and two reads pinning different rows. +// The reads come sorted by group and path, so which pair is reported does not vary. +func checkFamilies(reads []pathRead) error { + shared := map[string]*draws{} + for i, r := range reads { + if r.tr == nil { + continue + } + for _, o := range reads[:i] { + if o.tr == nil || o.at.group != r.at.group || o.tr.head.family() != r.tr.head.family() { + continue + } + if err := checkFamilyPair(o, r); err != nil { + return err + } + } + d := shared[r.at.group] + if d == nil { + d = &draws{} + shared[r.at.group] = d + } + if err := r.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", r.at.route.spelled(r.a.name), err) + } + } + return nil +} + +// replay pins the read's rows into d, where they agree with the rows pinned before. +func (r *tableRead) replay(d *draws) error { + var err error + r.pins.each(func(t *table, row int) { + if err == nil { + err = d.pinRow(t, row) + } + }) + return err +} + +// checkFamilyPair refuses a table read whole beside a path into its family, and a +// table one read draws that the other pins, since which token renders first would +// then decide the row. +func checkFamilyPair(a, b pathRead) error { + for _, pair := range [][2]pathRead{{a, b}, {b, a}} { + x, y := pair[0], pair[1] + if len(x.a.tail) == 0 && len(y.a.tail) > 0 { + return overlapError(x.at.route, x.a.name, y) + } + if drawn := x.tr.drawnOf(&y.tr.pins); drawn != nil { + if s, ok := y.tr.selected(x.tr.head); ok && len(x.tr.sels) == 0 { + tail := x.a.tail + if s.t != x.tr.head { + tail = append([]string{x.tr.head.category}, tail...) + } + return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup", + x.at.route.spelled(x.a.name), drawn.category, y.at.route.spelled(y.a.name), s.spelling, joinSegments(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.at.route.spelled(x.a.name), drawn.category, y.at.route.spelled(y.a.name)) + } + } + return nil +} + +// drawnOf is a table the read draws that pins holds a row of, if any. +func (r *tableRead) drawnOf(pins *draws) *table { + var found *table + pins.each(func(t *table, _ int) { + if found == nil && r.drawn[t] { + found = t + } + }) + return found +} diff --git a/fejkdata.go b/fejkdata.go index 68374ca..4a32629 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -46,7 +46,7 @@ type Generator struct { mu sync.Mutex rand *session categories map[string]node - root *folder // the categories as the node a path walks from + root folder // the categories as the node a path walks from, owned here so a walk allocates none set drawSet // one Fake's draws, owned here so a walk pinning rows keeps them off the heap records map[node]recordShape structs map[reflect.Type]structResult @@ -125,7 +125,7 @@ func New(opts ...Option) (*Generator, error) { if err != nil { return nil, fmt.Errorf("fejkdata: %w", err) } - return &Generator{rand: rng, categories: cats, root: &folder{children: cats}}, nil + return &Generator{rand: rng, categories: cats}, nil } // List returns the sorted dotted paths Fake can render: every category, the dotted @@ -170,7 +170,7 @@ func paths(n node) []string { return []string{""} case *table: return tablePaths(n) - case *column: + case *column, *row: return []string{""} case *choice: out := []string{""} diff --git a/graph.go b/graph.go index 47c5870..08358f2 100644 --- a/graph.go +++ b/graph.go @@ -63,9 +63,6 @@ func contained(n node) []namedNode { case *table: return append([]namedNode{{node: n.format}}, named(n.fields)...) case *column: - if n.i < 0 { - return nil - } var out []namedNode for r := 0; r < n.t.rows(); r++ { if cell := n.t.cellNode(r, n.i); cell != nil { @@ -162,10 +159,9 @@ func renderEdges(n node) []renderEdge { return es case *table: return []renderEdge{{to: n.format, label: "format"}} + case *row: + return []renderEdge{{to: n.t.format, label: "format"}} case *column: - if n.i < 0 { - return []renderEdge{{to: n.t.format, label: "format"}} - } var es []renderEdge for _, c := range contained(n) { es = append(es, renderEdge{to: c.node, label: n.t.columns[n.i]}) diff --git a/hold.go b/hold.go index e0136b1..3a82baf 100644 --- a/hold.go +++ b/hold.go @@ -262,18 +262,12 @@ func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding) type draws struct { variant map[string]node value map[string]draw - pins [4]tablePin // the rows pinned, inline so a render pinning a few tables stays off the heap + pins [8]tablePin // the rows pinned, inline so a render over a country's five-deep geo tree stays off the heap npins int - more map[*table]int // the rows pinned past the inline four + more map[*table]int // the rows pinned past the inline eight s *session // what draws a row; nil where a walk only proves selectors } -// tablePin is one table's pinned row. -type tablePin struct { - t *table - row int -} - // draw is what one read drew: its text, and whether it landed on a null. type draw struct { text string @@ -298,7 +292,7 @@ func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw { if r, done := d.value[a.path]; done { return r } - leaf, _ := walkPath(t.fields[a.key], a.tail, pathWalk{ + leaf, err := walkPath(t.fields[a.key], a.tail, pathWalk{ // Hold the draw at every level passed through, so two paths sharing a // prefix share it. choice: func(c *choice, rest []string) ([]node, error) { @@ -318,6 +312,9 @@ func readField(s *session, t *template, held *draws, sc drawScope, a arm) draw { }, pins: d, }) + if err != nil { + panic(fmt.Sprintf("fejkdata: %q: %v; a fence should have refused this at New", a.name, err)) + } r := renderLeaf(s, leaf, sc) if d.value == nil { d.value = map[string]draw{} diff --git a/path.go b/path.go index 9f7fe52..184ebec 100644 --- a/path.go +++ b/path.go @@ -102,6 +102,15 @@ func splitOutside(s string, c byte) []string { // isSelector reports whether a segment is a [key or name] rather than a name. func isSelector(seg string) bool { return strings.HasPrefix(seg, "[") } +func hasSelector(segs []string) bool { + for _, s := range segs { + if isSelector(s) { + return true + } + } + return false +} + // selectorOf is the key or name a selector segment holds. func selectorOf(seg string) string { return seg[1 : len(seg)-1] } @@ -214,8 +223,10 @@ func walkTable(t *table, tail []string, w pathWalk, descended bool) (node, error return nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.category, sel) } } + // A selector further down pins this table by ancestry, so the walk draws only + // where none follows; drawing first could pick a row the selector is not inside. if w.pins != nil { - if err := readRow(w.pins, t, sel, descended || len(tail) > 0); err != nil { + if err := readRow(w.pins, t, sel, (descended || len(tail) > 0) && !hasSelector(tail)); err != nil { return nil, err } } diff --git a/record.go b/record.go index 4a6a47a..97514af 100644 --- a/record.go +++ b/record.go @@ -157,10 +157,9 @@ func tableRecord(s *session, t *table, tail []string, sc drawScope) (node, error case *table: sc.draws(s).rowOf(n) return n, nil + case *row: + return n.t, nil case *column: - if n.i < 0 { - return n.t, nil - } return nil, fmt.Errorf("descends into %q, a column; a record is a table's row", n.t.columns[n.i]) } return n, nil diff --git a/reference.go b/reference.go index a50a6cd..474ab88 100644 --- a/reference.go +++ b/reference.go @@ -109,6 +109,9 @@ func linkTemplateRefs(folder []string, path, category string, t *template, root if category != "" && key == "/"+category { return fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", path, name) } + if err := checkNotOwnFamily(root, category, target); err != nil { + return fmt.Errorf("%s: reference {%s}: %w", path, name, err) + } if err := checkPath(target, tail, key); err != nil { return fmt.Errorf("%s: reference {%s}: %w", path, name, err) } @@ -122,6 +125,22 @@ func linkTemplateRefs(folder []string, path, category string, t *template, root return nil } +// checkNotOwnFamily refuses a reference from a table's format or cell into a table of +// its own family: a table rendered whole draws its row without pinning it, so the +// family would draw apart from the row being rendered. +func checkNotOwnFamily(root map[string]node, category string, target node) error { + if category == "" { + return nil + } + _, own, _, err := resolveCategory(root, strings.Split(category, ".")) + from, isTable := own.(*table) + into, targetIsTable := target.(*table) + if err != nil || !isTable || !targetIsTable || from.family() != into.family() { + return nil + } + return fmt.Errorf("a cell or format of %s reads %s, a table of its own family, which a row of %s rendered whole would draw apart from; read the family from a template beside it, or add the value as a column", from.category, into.category, from.category) +} + // 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. type columnRead struct { diff --git a/render.go b/render.go index 1b119cc..bda6ea5 100644 --- a/render.go +++ b/render.go @@ -25,10 +25,8 @@ func (f *Generator) Fake(path string) (string, error) { } f.set = drawSet{unnamed: draws{s: f.rand}} sc := drawScope{set: &f.set} - if f.root == nil { - f.root = &folder{children: f.categories} - } - n, err := descend(f.rand, f.root, segments, sc) + f.root.children = f.categories + n, err := descend(f.rand, &f.root, segments, sc) if err != nil { return "", fmt.Errorf("fejkdata: %s: %w", path, err) } @@ -60,13 +58,13 @@ func render(s *session, n node, sc drawScope) string { case *table: sc.t, sc.row = n, n.draw(s) return expand(s, n.format, sc) + case *row: + sc.t, sc.row = n.t, sc.draws(s).mustRow(n.t) + return expand(s, n.t.format, sc) case *column: if sc.t != n.t { sc.t, sc.row = n.t, sc.draws(s).mustRow(n.t) } - if n.i < 0 { - return expand(s, n.t.format, sc) - } if cell := n.t.cellNode(sc.row, n.i); cell != nil { return render(s, cell, sc) } diff --git a/table.go b/table.go index 3be9f98..78726e8 100644 --- a/table.go +++ b/table.go @@ -19,7 +19,7 @@ type table struct { columns []string col map[string]int fields map[string]node // column nodes, the format's fields - whole *column // the pinned row rendered by the format + whole *row // the pinned row rendered by the format cells []string // rows × columns, flat tokens map[int]*template key int // column index, or -1 @@ -44,8 +44,7 @@ type tableIndex struct { func (*table) isNode() {} -// column is one column of a table, rendered as the cell of the row the render -// pinned; i < 0 is the whole row rendered by the table's format. +// column is one column of a table, rendered as the cell of the row the render pinned. type column struct { t *table i int @@ -53,6 +52,11 @@ type column struct { func (*column) isNode() {} +// row is the row of a table the render pinned, rendered by the table's format. +type row struct{ t *table } + +func (*row) isNode() {} + func (t *table) rows() int { return len(t.cells) / len(t.columns) } func (t *table) cell(row, col int) string { return t.cells[row*len(t.columns)+col] } @@ -144,7 +148,7 @@ func readTableOptions(m map[string]any) (tableOptionValues, error) { // parseRows reads the TSV: the header line names the columns, each following line // is a row of as many cells. Cells are substrings of data, so the file is held once. func (t *table) parseRows(data string) error { - data = strings.TrimSuffix(data, "\n") + data = strings.TrimSuffix(strings.TrimPrefix(data, "\xEF\xBB\xBF"), "\n") header, rest, _ := strings.Cut(data, "\n") if data == "" { return fmt.Errorf("has no header line naming its columns") @@ -202,6 +206,9 @@ func (t *table) bindOptions(o tableOptionValues) error { } *opt.into = i } + if t.name >= 0 && t.key < 0 { + return fmt.Errorf("name selects a row as a key does, and lists the rows it matches by their keys, so it needs a key column; add key") + } if err := t.indexKeys(); err != nil { return err } @@ -224,6 +231,13 @@ func (t *table) indexKeys() error { } t.byKey[k] = r } + for r := 0; r < t.rows() && t.name >= 0; r++ { + if n := t.cell(r, t.name); t.byKey[n] != r { + if other, isKey := t.byKey[n]; isKey { + return fmt.Errorf("%s line %d: name %q is the key of line %d, which a selector reads first, so the name could never select this row", t.file, r+2, n, other+2) + } + } + } return nil } @@ -282,7 +296,7 @@ func (t *table) compileFormat(format string) error { return err } t.format = &template{format: format, fields: t.fields, repeat: 1, record: true, table: t} - t.whole = &column{t, -1} + t.whole = &row{t} return t.format.compileFormat() } @@ -300,7 +314,6 @@ func linkTables(root map[string]node) error { return err } case *table: - n.category = name if n.parent < 0 { continue } @@ -325,17 +338,21 @@ func (t *table) linkParent(path string, siblings map[string]node) error { case p.key < 0: return fmt.Errorf("parent %q has no key column to link to", name) } + var ancestors []*table for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.columns[q.parent]].(*table) { if seen[q] { return fmt.Errorf("parent cycle: %s reaches itself through its parents", q.category) } seen[q] = true + ancestors = append(ancestors, q) if q.parent < 0 { break } } - if _, clash := p.col[t.category]; clash { - return fmt.Errorf("%q is named like a column of its parent %q, so %s.%s could read either; rename one", t.category, name, name, t.category) + for _, q := range ancestors { + if _, clash := q.col[t.category]; clash { + return fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.category, q.category, q.category, t.category) + } } linked := make(map[string]bool, p.rows()) for r := 0; r < t.rows(); r++ { @@ -450,8 +467,8 @@ func (t *table) under(r int, a *table, pr int) bool { // find is the row a selector names: by key first, then by name, where a name // naming several rows resolves inside the ancestors pinned in d. func (t *table) find(sel string, d *draws) (int, error) { - if t.key < 0 && t.name < 0 { - return 0, fmt.Errorf("%s has no key or name column to select a row by", t.category) + if t.key < 0 { + return 0, fmt.Errorf("%s has no key column to select a row by", t.category) } if r, ok := t.byKey[sel]; ok { return r, nil @@ -479,8 +496,5 @@ func (t *table) find(sel string, d *draws) (int, error) { // selectorSpelling is how a path writes a selected row, for messages. func (t *table) selectorSpelling(r int) string { - if t.key >= 0 { - return t.category + "[" + t.cell(r, t.key) + "]" - } - return t.category + "[" + t.cell(r, t.name) + "]" + return t.category + "[" + t.cell(r, t.key) + "]" } diff --git a/table_test.go b/table_test.go index 9535688..a8fef79 100644 --- a/table_test.go +++ b/table_test.go @@ -337,11 +337,11 @@ func TestTableSelectionFences(t *testing.T) { // table are refused at load rather than found at render. func TestTableFamilyFenceReplaysPins(t *testing.T) { accepted := map[string]string{ - "skip-level selectors": `"{/region[12].locality[L4].name}|{/region[12].name}"`, - "descendant then ancestor": `"{/locality[L4].name}|{/region[12].name}"`, - "two selected levels": `"{/municipality[1281].name}|{/locality[L4].name}"`, - "nested prefixes": `"{/region[12].municipality[1281].name}|{/municipality[1281].locality[L4].name}"`, - "selected above a draw": `"{/region[12].locality[L4].name}|{/region[12].municipality.code}"`, + "skip-level selectors": `"{/region[12].locality[L4].name}|{/region[12].name}"`, + "descendant then ancestor": `"{/locality[L4].name}|{/region[12].name}"`, + "two selected levels": `"{/municipality[1281].name}|{/locality[L4].name}"`, + "nested prefixes": `"{/region[12].municipality[1281].name}|{/municipality[1281].locality[L4].name}"`, + "a draw under the selection": `"{/region[12].name}|{/region[12].municipality.locality[L4].name}"`, } for name, json := range accepted { f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": json}))), WithSeed(1)) @@ -358,7 +358,7 @@ func TestTableFamilyFenceReplaysPins(t *testing.T) { } rejected := map[string]struct{ json, want string }{ "two rows of one table": {`"{/region[12].locality[L4].name} {/region[12].locality[L7].name}"`, "drawGroup"}, - "a row outside a selected ancestor": {`"{/region[14].name} {/locality[L4].name}"`, "not inside"}, + "a row outside a selected ancestor": {`"{/region[14].name} {/locality[L4].name}"`, "drawGroup"}, } for name, c := range rejected { _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, with(geo(), map[string]string{"x.json": c.json})))) @@ -392,7 +392,7 @@ func TestTableFences(t *testing.T) { "a brace in a name": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\tx{1}\ny\ty\n"}, `"{"`}, "name without a key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","name":"a"}`, "t.tsv": "a\nx\nx\n"}, "key"}, "a name that is another row's key": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","key":"a","name":"n"}`, "t.tsv": "a\tn\nx\ty\ny\tz\n"}, `"y"`}, - "a cell reading its family": {with(geo(), map[string]string{"locality.tsv": "code\tname\tmunicipality\nL1\t{/municipality.code}\t0180\nL2\tSolna\t0184\nL3\tMalmö\t1280\nL4\tLund\t1281\nL5\tGöteborg\t1480\n"}), "family"}, + "a cell reading its family": {with(geo(), map[string]string{"locality.tsv": "code\tname\tmunicipality\tnote\nL1\tStockholm\t0180\t{/municipality.code}\nL2\tSolna\t0184\t-\nL3\tMalmö\t1280\t-\nL4\tLund\t1281\t-\nL5\tGöteborg\t1480\t-\n"}), "family"}, "a format reading its family": {with(geo(), map[string]string{"locality.json": `{"format":"{name} {/region.name}","rows":"locality.tsv","key":"code","name":"name","parent":"municipality"}`}), "family"}, "a descendant named like an ancestor's column": {with(geo(), map[string]string{"region.tsv": "code\tname\tpopulation\tlocality\n01\tStockholms län\t2400000\tx\n12\tSkåne län\t1400000\ty\n14\tVästra Götalands län\t1750000\tz\n"}), `"locality"`}, "weight not a number": {map[string]string{"t.json": `{"format":"{a}","rows":"t.tsv","weight":"w"}`, "t.tsv": "a\tw\nx\tmany\ny\t2\n"}, `"many"`}, diff --git a/value.go b/value.go index 3e184a1..c6f78ca 100644 --- a/value.go +++ b/value.go @@ -87,6 +87,8 @@ func (p *valueProof) of(n node) proven { v = p.template(n) case *column: v = p.cells(n) + case *row: + v = unproven(fmt.Sprintf("%q renders a row of %s, which is composed text", n.t.format.format, n.t.category)) default: v = unproven(`it reads a null, which renders "" outside its own column`) } @@ -96,9 +98,6 @@ func (p *valueProof) of(n node) proven { // cells proves a table column over every cell it may render. func (p *valueProof) cells(c *column) proven { - if c.i < 0 { - return unproven(fmt.Sprintf("%q renders a row of %s, which is composed text", c.t.format.format, c.t.category)) - } var v proven for r := 0; r < c.t.rows(); r++ { var w proven