Move a table's rows, links and pins into internal/rows, and walk every path through one per-step function, so Fake refuses a level carrying a repeat

This commit is contained in:
2026-10-05 21:33:30 +02:00
committed by Lillem4n
parent 05d9fcd8c1
commit 707cd6b203
22 changed files with 840 additions and 607 deletions
+2 -2
View File
@@ -111,7 +111,7 @@ In [docs/decisions.md](docs/decisions.md):
- `List` advertises direct descents only
- A path draws through its compiled steps
- A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read
- comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, and items 102 and 104 to 106
- The template engine stays the root package until item 107; what reads no engine type sits in `internal/`, and item 102 moves a table's rows there
- comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and items 104 to 106
- The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`
- Goal 5.1's `{…}` covers `{{`, `}}` and a lone `}`, so the goal names no escape
- Goal 7.2 holds while a first template can be written with no escape; a literal brace still takes `{{` or `}}`
+3
View File
@@ -12,6 +12,9 @@ replacement, and each removed path, column or flag.
`{/city[Oslo]..country.name}` renders `Norway`, and `.` after it steps down again,
drawing afresh. `New` refuses a `..` naming anything but the parent table, and a
selector after a `..`.
- A path given to `Fake` or the CLI may not read into a level carrying a
`repeat`, as a reference may not: `Fake("x.a")` where `x` carries `"repeat": 3` fails,
naming the level `x`.
- Every `{…}` draws afresh, and a name is the one way to keep a pick: `{p.a} {p.b}`,
`{w} {uppercase(w)}` and `{/person.first} {/person.last}` are two draws each, which
`{p as q}{q.a} {q.b}` and `{/person as n}{n.first} {n.last}` make one.
+2 -2
View File
@@ -954,7 +954,7 @@ 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
table.go tables: the rows TSV, its options and links, row selection and draws
table.go tables: their options, format and cells, and the link to the parent beside them
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, the memo a pick keeps, and the reads kept under it
@@ -965,7 +965,6 @@ record.go records: Record, the JSON/CSV/SQL serializers, and their entry p
struct.go structs: FakeStruct, fake tags as one record's columns, 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 a format's tokens checked against their fields, and compiled
pins.go the pin set: the table rows one path or one named pick fixes
reference.go resolving references across the tree
graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences
calc.go the {calc()} operand checks: never a number, and a constant zero divisor
@@ -980,6 +979,7 @@ internal/drawstate/ the seeded randomness and the {seq()} counters a generator d
internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, names, calc syntax, and whether an argument is a template or a path
internal/invariant/ the one phrase every package panics with when an invariant breaks
internal/proven/ what a proof knows of a value, and the bounds a calc takes from its operands
internal/rows/ a table's rows: the TSV, the options proved over it, the links between tables, row selection and draws, and the pin set one path or one named pick fixes
data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders, geo, misc
data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md)
docs/ the decision log, the register research behind the shipped data, and the survey of other libraries
+4 -4
View File
@@ -17,7 +17,7 @@ package fejkdata
// Vocabulary
//
// - draw — one pick from a choice, or one row taken from a table: `pick`,
// `resolveChoice`, `table.drawRow`. Every {…} draws afresh, bar a read of a name.
// `resolveChoice`, `renderScope.drawRowOf`. Every {…} draws afresh, bar a read of a name.
// - expansion — one render of one format: `expand`.
// - render — one walk of the tree from an entry point, over one `frameStack`:
// `Generator.Fake`, `FakeRecord`, `Template.Fake` and `RecordTemplate.Fake`
@@ -28,8 +28,8 @@ package fejkdata
// - 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`.
// `pinSet`, `namedPick.pins`, `table.drawStep`.
// - family — a table and every table reaching it through a chain of links
// `table.linkParent` makes.
// - fence — a load-time check, run over each category, template or struct as it
// loads or compiles, so rendering a compiled tree cannot fail.
+1 -1
View File
@@ -95,7 +95,7 @@ func vocabulary(t *testing.T, files []*ast.File) string {
}
// declaredSymbols is every name the package declares at the top level, a method and
// a struct field keyed under its type: pinSet.pin, tableRead.landsRow.
// a struct field keyed under its type: namedPick.pins, tableRead.landsRow.
func declaredSymbols(files []*ast.File) map[string]bool {
names := declaredFuncs(files)
for _, f := range files {
+15 -15
View File
@@ -793,14 +793,14 @@ the direct chain is the one a reader can predict from the tables' parents.
2026-09-30, larv-review on the comprehension round the maintainer approved on 2026-09-29;
replaces "The path walks are separate loops".
Goals 3 and 13: `drawSteps` draws every path from its steps, compiled at link by
`pathCheck` or, for a caller's path, by `probePath` into a stack buffer. The check and
the probe stay two loops: on 2026-09-29 one loop over all three walks, switching on a
mode field, leaked the check's leaves and errors with the draw's pins, arm and memo,
since Go tracks a struct's fields as one, and a repeat of a reference path rose from 66
to 106 allocations. `drawSteps` takes its state as parameters and a step holds no node,
so no benchmark gained an allocation. Valid while Go's escape analysis tracks
a struct's fields as one. Two loops over one rule work against DRY, and they already
disagree on a repeat level; `todo.md` item 102 walks both through one per-step function.
`pathCheck` or, for a caller's path, by `probePath` into a stack buffer. Both take each
step through `takeStep`, and differ only at a choice: the check walks every variant, the
probe the first. On 2026-09-29 one loop over all three walks, switching on a mode field,
leaked the check's leaves and errors with the draw's pins, arm and memo, since Go tracks a
struct's fields as one, and a repeat of a reference path rose from 66 to 106 allocations.
`drawSteps` and `takeStep` take their state as parameters and a step holds no node, so no
benchmark gained an allocation. Valid while Go's escape analysis tracks a struct's fields
as one.
## A name lives in the category binding it, or in the repeat binding it, and is drawn on its first read
@@ -825,11 +825,11 @@ category's frames, since the field sits inside it.
Valid while names are read only inside the category binding them.
## comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, and items 102 and 104 to 106
## comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and items 104 to 106
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat. The hardest units were the draw fences, which the change making every `{…}` draw afresh deleted. The restructure deletes or moves what the earlier chunks of the below-floor round touched. It is three things: the change making every `{…}` draw afresh; the moves of the draw state, the grammar, `DataType`, what a proof knows of a value, the builtins and the reading of data files into `internal/`; and the untangling of the engine's own structs, which is the change cutting a template's pointer to its table plus `todo.md` items 102 and 104 to 106. The maintainer approved these three, and `AGENTS.md`'s comprehension rule lets them merge below the floor. A scoring run before item 106 merges would score code about to go; for that reason the calc change accepting a name as an operand shipped unscored on 2026-10-05. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
2026-10-04, Lilleman auf Larv. Serves goal 3. The nine-seat panels in `todo.md`'s git history, 2026-09-22 to 2026-09-30, held the score between 5.2 and 6.1, with Locality lowest at every seat. The hardest units were the draw fences, which the change making every `{…}` draw afresh deleted. The restructure deletes or moves what the earlier chunks of the below-floor round touched. It is three things: the change making every `{…}` draw afresh; the moves of the draw state, the grammar, `DataType`, what a proof knows of a value, the builtins and the reading of data files into `internal/`; and the untangling of the engine's own structs, which is the change cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and `todo.md` items 104 to 106. The maintainer approved these three, and `AGENTS.md`'s comprehension rule lets them merge below the floor. A scoring run before item 106 merges would score code about to go; for that reason the calc change accepting a name as an operand shipped unscored on 2026-10-05. No scoring run or panel runs until item 5, which runs the nine-seat panel and larv-review's scoring run. The scoring run writes this entry's first scores. Valid until `todo.md` item 5 runs.
## The template engine stays the root package until item 107; what reads no engine type sits in `internal/`, and item 102 moves a table's rows there
## The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`
2026-10-05, Lilleman auf Larv. Serves goals 3.2 and 3.4. Two architect reviews found the
engine's types, `template`, `table`, `arm`, `op` and `nameBinding`, bound in one cycle
@@ -837,10 +837,10 @@ because several passes each write part of the same structs; the template languag
recursion reads few of those types. Moving the engine before items 104 to 106 give
each value one writer would spread that knot across packages, behind forwarding calls from
every public method. The draw state, the grammar, `DataType`, what a proof knows of a
value, the builtins and the reading of data files read no engine type, so each sits in a
package under `internal/`, behind the import list `imports_test.go` checks. Item
102 stops a table's rows reading engine types, moves them into `internal/rows`, and adds
that package to the list. Valid until `todo.md` item 107 moves the engine.
value, the builtins, the reading of data files and a table's rows read no engine type, so
each sits in a package under `internal/`, behind the import list `imports_test.go` checks.
A table's rows reach the engine table owning them as a payload of a type parameter. Valid
until `todo.md` item 107 moves the engine.
## Goal 5.1's `{…}` covers `{{`, `}}` and a lone `}`, so the goal names no escape
+4 -4
View File
@@ -168,11 +168,11 @@ func paths(n node) []string {
// tablePaths is a table's columns, then each table linked to it under its name: the
// direct descents, a step at a time.
func tablePaths(t *table) []string {
out := append([]string{""}, t.header...)
out := append([]string{""}, t.rows.Header()...)
sort.Strings(out[1:])
for _, name := range sortedNames(t.children) {
for _, p := range paths(t.children[name]) {
out = append(out, join(name, p))
for _, c := range t.rows.Children() {
for _, p := range paths(c.Payload()) {
out = append(out, join(c.Segment(), p))
}
}
return out
+2 -2
View File
@@ -68,7 +68,7 @@ func contained(n node) []namedNode {
return nil
}
var out []namedNode
for r := 0; r < n.t.rowCount(); r++ {
for r := 0; r < n.t.rows.Len(); r++ {
if cell := n.t.cellTemplate(r, n.i); cell != nil {
out = append(out, namedNode{node: cell, line: r + 2})
}
@@ -133,7 +133,7 @@ func renderEdges(n node) []renderEdge {
case *tableColumn:
var es []renderEdge
for _, c := range contained(n) {
es = append(es, renderEdge{to: c.node, label: n.t.header[n.i]})
es = append(es, renderEdge{to: c.node, label: n.name()})
}
return es
case *folder, *nullItem:
+10 -1
View File
@@ -1,7 +1,10 @@
// Package drawstate holds a generator's seeded randomness and its {seq()} counters.
package drawstate
import "math/rand/v2"
import (
"math/rand/v2"
"sort"
)
// State is one generator's draw state, kept across its renders.
type State struct {
@@ -18,6 +21,12 @@ func (s *State) IntN(n int) int { return s.rand.IntN(n) }
func (s *State) Float64() float64 { return s.rand.Float64() }
// Weighted draws an index into cum, a running sum of positive weights, with odds by weight.
func (s *State) Weighted(cum []float64) int {
x := s.Float64() * cum[len(cum)-1]
return min(sort.Search(len(cum), func(i int) bool { return cum[i] > x }), len(cum)-1) // x can round up to the total
}
// Seq advances the counter {seq(key)} reads, or {seq()} for an empty key, and returns it.
func (s *State) Seq(key string) uint64 {
s.counters[key]++
+12
View File
@@ -138,6 +138,18 @@ func HasSelector(segs []string) bool {
func SelectorOf(seg string) string { return seg[1 : len(seg)-1] }
// inSelector is what a selector may not contain: the path grammar reserves brackets and
// braces, and a quote or a bar would make the selector no path and no token arm.
const inSelector = `[]{}"|`
// UnspellableInSelector is the first character of s a selector cannot spell, "" where there is none.
func UnspellableInSelector(s string) string {
if i := strings.IndexAny(s, inSelector); i >= 0 {
return s[i : i+1]
}
return ""
}
// NameSegments is the segments of a path that are names, its selectors and ".." left out.
func NameSegments(segs []string) []string {
out := segs[:0:0]
+139
View File
@@ -0,0 +1,139 @@
package rows
import (
"fmt"
"github.com/larvit/fejkdata/internal/invariant"
)
type pin[P any] struct {
t *Table[P]
row int
}
// Pins is the table rows fixed so far.
type Pins[P any] struct {
inline [8]pin[P] // sized so a render over a country's five-deep geo tree stays off the heap
used int
spill map[*Table[P]]int
}
func (p *Pins[P]) Pinned(t *Table[P]) (int, bool) {
for _, q := range p.inline[:p.used] {
if q.t == t {
return q.row, true
}
}
r, ok := p.spill[t]
return r, ok
}
// MustRow is the row pinned for t, which the walk reaching a column pinned.
func (p *Pins[P]) MustRow(t *Table[P]) int {
r, ok := p.Pinned(t)
if !ok {
panic(invariant.Broken("a column of %s is rendered with no row pinned", t.segment))
}
return r
}
func (p *Pins[P]) add(t *Table[P], r int) {
if p.used < len(p.inline) {
p.inline[p.used] = pin[P]{t, r}
p.used++
return
}
if p.spill == nil {
p.spill = map[*Table[P]]int{}
}
p.spill[t] = r
}
// pin pins row r of t, and the rows of t's ancestors it links to. It checks nothing, so r must
// agree with the row p pins of nearestPinned(t): ask clash first, as PinRow does, or draw r inside
// it, as DrawIn does.
func (p *Pins[P]) pin(t *Table[P], r int) {
for stop := p.nearestPinned(t); t != stop; t = t.parent {
p.add(t, r)
if t.parent != stop {
r = t.parentRow(r)
}
}
}
// nearestPinned is the first of t and its ancestors p pins, where pinning a row of t stops; nil
// where none is.
func (p *Pins[P]) nearestPinned(t *Table[P]) *Table[P] {
for ; t != nil; t = t.parent {
if _, ok := p.Pinned(t); ok {
return t
}
}
return nil
}
// 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 *Pins[P]) clash(t *Table[P], r int) *Table[P] {
if pr, ok := p.Pinned(t); ok && pr != r {
return t
}
for a := t.parent; a != nil; a = a.parent {
if pa, ok := p.Pinned(a); ok && !t.under(r, a, pa) {
return a
}
}
return nil
}
// PinRow pins row r of t where it agrees with the rows pinned before it.
func (p *Pins[P]) PinRow(t *Table[P], r int) error {
switch a := p.clash(t, r); {
case a == t:
pr, _ := p.Pinned(t)
return fmt.Errorf("%s and %s are two rows of %s", t.selectorSpelling(pr), t.selectorSpelling(r), t.path)
case a != nil:
pa, _ := p.Pinned(a)
return fmt.Errorf("%s is not inside %s", t.selectorSpelling(r), a.selectorSpelling(pa))
}
p.pin(t, r)
return nil
}
// Select pins the row of t that sel names, and returns it.
func (p *Pins[P]) Select(t *Table[P], sel string) (int, error) {
r, err := t.find(sel, p)
if err != nil {
return 0, err
}
return r, p.PinRow(t, r)
}
// inside keeps the rows of t that sit inside every pinned ancestor.
func (p *Pins[P]) inside(t *Table[P], rows []int) []int {
for a := t.parent; a != nil; a = a.parent {
pa, ok := p.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
}
// Above is a pin set holding the rows p pins of t and its ancestors, and none below them.
func (p *Pins[P]) Above(t *Table[P]) *Pins[P] {
q := new(Pins[P])
for ; t != nil; t = t.parent {
if r, ok := p.Pinned(t); ok {
q.add(t, r)
}
}
return q
}
+508
View File
@@ -0,0 +1,508 @@
// Package rows holds a table's rows: the TSV they are read from, the options proved over
// them, the links between tables, and the rows a draw takes or a selector names.
package rows
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
)
// Table is the rows of one table category. P is what owns them, which a step to a linked
// table reaches through Payload.
type Table[P any] struct {
payload P
segment string // path's last segment
path string // the category's path from the data root, which a selector is written at
file string
options Columns
header []string
col map[string]int
cells []string // rows × columns, flat
keyIndex int // keyIndex through parentIndex are column indexes, -1 where the option is absent
nameIndex int
weightIndex int
parentIndex int
weights []float64 // nil when uniform
cum []float64 // cumulative weights, nil when uniform
byKey map[string]int
parent *Table[P]
children map[string]*Table[P]
once sync.Once
lookup rowLookup
}
// Columns names the column each option reads, "" where the option is absent.
type Columns struct {
Key, Name, Parent, Weight string
}
// rowLookup is what selection and linked draws look up, built on the first draw
// that needs it.
type rowLookup struct {
byName map[string][]int
rowsByParent map[string][]int
childCum map[string][]float64 // cumulative weights per parent key, nil when uniform
}
// Parse reads the TSV data, the rows of the table at path, and proves what the options
// claim of them. payload is what owns the rows.
func Parse[P any](payload P, segment, path, file, data string, o Columns) (*Table[P], error) {
t := &Table[P]{payload: payload, segment: segment, path: path, file: file, options: o, keyIndex: -1, nameIndex: -1, weightIndex: -1, parentIndex: -1}
if err := t.parseRows(data); err != nil {
return nil, fmt.Errorf("%s: %w", file, err)
}
if err := t.bindOptions(); err != nil {
return nil, err
}
if err := t.checkSelectorCells(); err != nil {
return nil, fmt.Errorf("%s: %w", file, err)
}
return t, nil
}
func (t *Table[P]) Payload() P { return t.payload }
func (t *Table[P]) Segment() string { return t.segment }
func (t *Table[P]) Path() string { return t.path }
func (t *Table[P]) File() string { return t.file }
func (t *Table[P]) Options() Columns { return t.options }
func (t *Table[P]) Header() []string { return t.header }
func (t *Table[P]) Parent() *Table[P] { return t.parent }
// LinkColumn is the index of the column holding the parent's key, -1 where there is none.
func (t *Table[P]) LinkColumn() int { return t.parentIndex }
func (t *Table[P]) Column(name string) (int, bool) {
i, ok := t.col[name]
return i, ok
}
func (t *Table[P]) Len() int { return len(t.cells) / len(t.header) }
func (t *Table[P]) Cell(row, col int) string { return t.cells[row*len(t.header)+col] }
// 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[P]) parseRows(data string) error {
data = strings.TrimSuffix(strings.TrimPrefix(data, "\xEF\xBB\xBF"), "\n")
header, rest, _ := strings.Cut(data, "\n")
header = strings.TrimSuffix(header, "\r")
if header == "" {
return fmt.Errorf("has no header line naming its columns")
}
t.header = strings.Split(header, "\t")
t.col = make(map[string]int, len(t.header))
for i, name := range t.header {
if err := grammar.CheckName(name); err != nil {
return fmt.Errorf("column %w", err)
}
if _, dup := t.col[name]; dup {
return fmt.Errorf("column %q is named twice", name)
}
t.col[name] = i
}
if header == data {
return fmt.Errorf("has no rows below its header; a table is at least two rows")
}
m := len(t.header)
t.cells = make([]string, 0, m*(strings.Count(rest, "\n")+1))
for line := 2; ; line++ {
row, more, found := strings.Cut(rest, "\n")
row = strings.TrimSuffix(row, "\r")
if err := t.appendRow(row, line); err != nil {
return err
}
if !found {
break
}
rest = more
}
if t.Len() < 2 {
return fmt.Errorf("has one row, which is a template; write it as one")
}
return nil
}
// appendRow splits one line into as many cells as the header has columns.
func (t *Table[P]) appendRow(row string, line int) error {
if row == "" {
return fmt.Errorf("line %d is empty; every line below the header is a row", line)
}
m := len(t.header)
for n := 0; n < m; n++ {
cell, next, tab := strings.Cut(row, "\t")
if !tab && n < m-1 || tab && n == m-1 {
return fmt.Errorf("line %d has %s cells than the %d columns", line, map[bool]string{true: "more", false: "fewer"}[tab], m)
}
t.cells = append(t.cells, cell)
row = next
}
return nil
}
// bindOptions resolves each option to its column and proves what it claims of the
// cells: a key is unique, a weight a positive number.
func (t *Table[P]) bindOptions() error {
o := t.options
for _, opt := range []struct {
name, value string
into *int
}{{"key", o.Key, &t.keyIndex}, {"name", o.Name, &t.nameIndex}, {"parent", o.Parent, &t.parentIndex}, {"weight", o.Weight, &t.weightIndex}} {
if opt.value == "" {
continue
}
i, ok := t.col[opt.value]
if !ok {
return fmt.Errorf("%s names no column %q of %s; the columns are %v", opt.name, opt.value, t.file, t.header)
}
*opt.into = i
}
if t.nameIndex >= 0 && t.keyIndex < 0 && t.parentIndex < 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, or a parent inside which each name is one row; add key")
}
if err := t.proveKeys(); err != nil {
return err
}
if err := t.proveNamesInsideParent(); err != nil {
return err
}
return t.sumWeights()
}
// proveNamesInsideParent proves a name without a key names one row inside its parent.
func (t *Table[P]) proveNamesInsideParent() error {
if t.keyIndex >= 0 || t.nameIndex < 0 {
return nil
}
inside := make(map[string]int, t.Len())
for r := 0; r < t.Len(); r++ {
k := t.Cell(r, t.parentIndex) + "\t" + t.Cell(r, t.nameIndex)
if first, dup := inside[k]; dup {
return fmt.Errorf("%s line %d: name %q repeats line %d inside %s %q; a name selects one row inside its parent; drop one, or add a key column", t.file, r+2, t.Cell(r, t.nameIndex), first+2, t.header[t.parentIndex], t.Cell(r, t.parentIndex))
}
inside[k] = r
}
return nil
}
// proveKeys proves every key names one row, and keeps the map a link is proved by.
func (t *Table[P]) proveKeys() error {
if t.keyIndex < 0 {
return nil
}
t.byKey = make(map[string]int, t.Len())
for r := 0; r < t.Len(); r++ {
k := t.Cell(r, t.keyIndex)
if k == "" {
return fmt.Errorf("%s line %d: the key is empty", t.file, r+2)
}
if first, dup := t.byKey[k]; dup {
return fmt.Errorf("%s line %d: key %q repeats line %d; a key selects one row", t.file, r+2, k, first+2)
}
t.byKey[k] = r
}
for r := 0; r < t.Len() && t.nameIndex >= 0; r++ {
if n := t.Cell(r, t.nameIndex); 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
}
// sumWeights proves every weight is a positive number, and keeps the weights and their
// running sum.
func (t *Table[P]) sumWeights() error {
if t.weightIndex < 0 {
return nil
}
t.weights, t.cum = make([]float64, t.Len()), make([]float64, t.Len())
total := 0.0
for r := range t.cum {
w, err := strconv.ParseFloat(t.Cell(r, t.weightIndex), 64)
if err != nil || math.IsInf(w, 0) || math.IsNaN(w) || w <= 0 {
return fmt.Errorf("%s line %d: weight %q is not a positive number", t.file, r+2, t.Cell(r, t.weightIndex))
}
total += w
t.weights[r], t.cum[r] = w, total
}
if math.IsInf(total, 0) {
return fmt.Errorf("%s: the weights sum past the largest number", t.file)
}
return nil
}
// checkSelectorCells refuses a key or name cell a selector cannot spell.
func (t *Table[P]) checkSelectorCells() error {
for r := 0; r < t.Len(); r++ {
for _, col := range []int{t.keyIndex, t.nameIndex} {
if col < 0 {
continue
}
if c := grammar.UnspellableInSelector(t.Cell(r, col)); c != "" {
return fmt.Errorf("line %d: %s %q contains %q, which a selector cannot spell", r+2, t.header[col], t.Cell(r, col), c)
}
}
}
return nil
}
// Link links t to p, the table its link column names, after proving p has a key, every
// link cell is one, and every row of p is linked to. sibling is the table beside t
// named name, nil where none is. It is the only writer of a table's parent and children.
// docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error
func (t *Table[P]) Link(p *Table[P], sibling func(name string) *Table[P]) error {
name := t.header[t.parentIndex]
if p.keyIndex < 0 {
return fmt.Errorf("parent %q has no key column to link to", name)
}
if err := t.checkAncestors(p, sibling); err != nil {
return err
}
linked := make(map[string]bool, p.Len())
for r := 0; r < t.Len(); r++ {
k := t.Cell(r, t.parentIndex)
if _, ok := p.byKey[k]; !ok {
return fmt.Errorf("%s line %d: %s %q is no key of %s", t.file, r+2, name, k, p.file)
}
linked[k] = true
}
for r := 0; r < p.Len(); r++ {
if k := p.Cell(r, p.keyIndex); !linked[k] {
return fmt.Errorf("%s links no row to %s %q; every %s row needs one, or drop line %d of %s", t.file, name, k, name, r+2, p.file)
}
}
t.parent = p
if p.children == nil {
p.children = map[string]*Table[P]{}
}
p.children[t.segment] = t
return nil
}
// checkAncestors refuses a cycle in the chain of parents starting at p, t's parent, and
// refuses an ancestor that has a column named like t.
func (t *Table[P]) checkAncestors(p *Table[P], sibling func(name string) *Table[P]) error {
var ancestors []*Table[P]
for q, seen := p, map[*Table[P]]bool{t: true}; q != nil; q = sibling(q.header[q.parentIndex]) {
if seen[q] {
return fmt.Errorf("parent cycle: %s reaches itself through its parents", q.segment)
}
seen[q] = true
ancestors = append(ancestors, q)
if q.parentIndex < 0 {
break
}
}
for _, q := range ancestors {
if _, clash := q.col[t.segment]; clash {
return fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.segment, q.segment, q.segment, t.segment)
}
}
return nil
}
// Children is the tables linked to t, by segment.
func (t *Table[P]) Children() []*Table[P] {
out := make([]*Table[P], 0, len(t.children))
for _, c := range t.children {
out = append(out, c)
}
sort.Slice(out, func(i, j int) bool { return out[i].segment < out[j].segment })
return out
}
// Descendant is the table named name among those linked to t, at any depth.
func (t *Table[P]) Descendant(name string) *Table[P] {
if c, ok := t.children[name]; ok {
return c
}
for _, c := range t.children {
if d := c.Descendant(name); d != nil {
return d
}
}
return nil
}
// StepUp proves a ".." after a row of t can step up: t has a parent, rest has no empty
// segment and names it, and no selector follows, since a row selected below the parent row
// could lie outside it.
func (t *Table[P]) StepUp(rest []string) error {
if t.parent == nil {
return fmt.Errorf(`%s has no parent table for ".." to step up to`, t.segment)
}
if err := grammar.CheckSegments(rest); err != nil {
return err
}
switch {
case len(rest) == 0 || rest[0] != t.parent.segment:
return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: %s`, t.segment, t.climbTo(rest))
case grammar.HasSelector(rest):
return fmt.Errorf(`no selector after "..": a row selected there could lie outside the row stepped up to; select from the table's path from the data root: %s`, grammar.JoinSegments(append([]string{t.parent.path}, rest[1:]...)))
}
return nil
}
// climbTo spells the ".." steps from t up to the ancestor rest names first, or the one step to
// t's parent where no ancestor is named.
func (t *Table[P]) climbTo(rest []string) string {
var b strings.Builder
for a := t.parent; a != nil; a = a.parent {
b.WriteString(".." + a.segment)
if len(rest) > 0 && a.segment == rest[0] {
return b.String()
}
}
return ".." + t.parent.segment
}
func (t *Table[P]) builtLookup() *rowLookup {
t.once.Do(func() {
if t.nameIndex >= 0 {
t.lookup.byName = make(map[string][]int, t.Len())
for r := 0; r < t.Len(); r++ {
n := t.Cell(r, t.nameIndex)
t.lookup.byName[n] = append(t.lookup.byName[n], r)
}
}
if t.parentIndex >= 0 {
t.lookup.rowsByParent = map[string][]int{}
for r := 0; r < t.Len(); r++ {
k := t.Cell(r, t.parentIndex)
t.lookup.rowsByParent[k] = append(t.lookup.rowsByParent[k], r)
}
if t.weights != nil {
t.lookup.childCum = make(map[string][]float64, len(t.lookup.rowsByParent))
for k, rows := range t.lookup.rowsByParent {
cum, total := make([]float64, len(rows)), 0.0
for i, r := range rows {
total += t.weights[r]
cum[i] = total
}
t.lookup.childCum[k] = cum
}
}
}
})
return &t.lookup
}
func (t *Table[P]) Draw(s *drawstate.State) int {
if t.cum == nil {
return s.IntN(t.Len())
}
return s.Weighted(t.cum)
}
// drawUnder picks a row among those linked to parent row pr.
func (t *Table[P]) drawUnder(s *drawstate.State, pr int) int {
lookup := t.builtLookup()
k := t.parent.Cell(pr, t.parent.keyIndex)
rows := lookup.rowsByParent[k]
if lookup.childCum == nil {
return rows[s.IntN(len(rows))]
}
return rows[s.Weighted(lookup.childCum[k])]
}
// 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[P]) DrawIn(s *drawstate.State, p *Pins[P]) int {
if r, ok := p.Pinned(t); ok {
return r
}
r := -1
for a := t.parent; a != nil && r < 0; a = a.parent {
if _, ok := p.Pinned(a); !ok {
continue
}
if t.parent != a {
t.parent.DrawIn(s, p)
}
pr, _ := p.Pinned(t.parent)
r = t.drawUnder(s, pr)
}
if r < 0 {
r = t.Draw(s)
}
p.pin(t, r)
return r
}
// parentRow is the row of t's parent that row r links to.
func (t *Table[P]) parentRow(r int) int { return t.parent.byKey[t.Cell(r, t.parentIndex)] }
// under reports whether row r of t sits inside row pr of ancestor a.
func (t *Table[P]) under(r int, a *Table[P], pr int) bool {
for c, row := t, r; c.parent != nil; c, row = c.parent, c.parentRow(row) {
if c.parent == a {
return c.parentRow(row) == pr
}
}
return false
}
// find is the row a selector names: by key first, then by name, where a name
// naming several rows resolves inside the ancestors pinned.
func (t *Table[P]) find(sel string, pins *Pins[P]) (int, error) {
if t.keyIndex < 0 && t.nameIndex < 0 {
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.path)
}
if r, ok := t.byKey[sel]; ok {
return r, nil
}
rows := t.builtLookup().byName[sel]
if len(rows) > 1 {
rows = pins.inside(t, rows)
}
switch len(rows) {
case 1:
return rows[0], nil
case 0:
return 0, fmt.Errorf("no row of %s has key or name %q", t.path, sel)
}
return 0, t.ambiguous(sel, rows)
}
// ambiguous names each row a selector could have meant: a keyed table by its keys,
// and one told apart only by its parent by the path that selects it.
func (t *Table[P]) ambiguous(sel string, rows []int) error {
spellings := make([]string, len(rows))
for i, r := range rows {
if t.keyIndex < 0 {
spellings[i] = t.selectorSpelling(r)
} else {
spellings[i] = t.Cell(r, t.keyIndex)
}
}
listed := strings.Join(spellings, ", ")
if t.keyIndex < 0 {
return fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %s", sel, len(rows), t.path, t.parent.path, listed)
}
inside := ""
if t.parent != nil {
inside = fmt.Sprintf(", or select it inside its %s", t.parent.path)
}
return fmt.Errorf("%q names %d rows of %s; select one by key, one of %s%s", sel, len(rows), t.path, listed, inside)
}
// selectorSpelling is how a path writes a selected row, for messages: by key, or
// by name inside its parent's row.
func (t *Table[P]) selectorSpelling(r int) string {
switch {
case t.keyIndex >= 0:
return t.path + "[" + t.Cell(r, t.keyIndex) + "]"
case t.nameIndex >= 0:
return t.parent.selectorSpelling(t.parentRow(r)) + "." + t.segment + "[" + t.Cell(r, t.nameIndex) + "]"
}
return fmt.Sprintf("%s line %d", t.file, r+2)
}
+60 -106
View File
@@ -16,7 +16,7 @@ func stepInto(n node, seg string) (node, error) {
return nil, fmt.Errorf("%s is not a table, so it has no row to select", grammar.SelectorOf(seg))
case seg == "..":
if c, ok := n.(*tableColumn); ok {
return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column; put the ".." right after the row`, c.t.header[c.i])
return nil, fmt.Errorf(`".." steps up from a table's row, and %q is a column; put the ".." right after the row`, c.name())
}
return nil, fmt.Errorf(`".." steps up only from a table's row; name any other level by its path from the data root`)
}
@@ -31,10 +31,10 @@ func stepInto(n node, seg string) (node, error) {
return child, nil
}
case *tableColumn:
if n.i == n.t.parentIndex {
return nil, fmt.Errorf("no field %q: %q is the link column, holding its parent's key; to read the row it links to, step up with ..%s.%s", seg, n.t.header[n.i], n.t.header[n.i], seg)
if n.i == n.t.rows.LinkColumn() {
return nil, fmt.Errorf("no field %q: %q is the link column, holding its parent's key; to read the row it links to, step up with ..%s.%s", seg, n.name(), n.name(), seg)
}
return nil, fmt.Errorf("no field %q: %q is a column, and a cell holds no fields", seg, n.t.header[n.i])
return nil, fmt.Errorf("no field %q: %q is a column, and a cell holds no fields", seg, n.name())
case *nullItem:
default:
panic(invariant.Broken("stepInto has no case for node %T", n))
@@ -63,10 +63,10 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) {
return tableRoute{}, err
}
if len(tail) > 0 && tail[0] == ".." {
if err := t.stepUp(tail[1:]); err != nil {
if err := t.rows.StepUp(tail[1:]); err != nil {
return tableRoute{}, err
}
return tableRoute{sel: sel, draw: sel == "", next: t.parentT, rest: tail[2:], descends: true, up: true}, nil
return tableRoute{sel: sel, draw: sel == "", next: t.rows.Parent().Payload(), rest: tail[2:], descends: true, up: true}, nil
}
column, child, err := t.step(tail)
if err != nil {
@@ -88,38 +88,6 @@ func (t *table) route(tail []string, descended bool) (tableRoute, error) {
return r, nil
}
// stepUp proves a ".." after a row of t can step up: t has a parent, rest has no empty
// segment and names it, and no selector follows, since a row selected below the parent row
// could lie outside it.
func (t *table) stepUp(rest []string) error {
if t.parentT == nil {
return fmt.Errorf(`%s has no parent table for ".." to step up to`, t.segment)
}
if err := grammar.CheckSegments(rest); err != nil {
return err
}
switch {
case len(rest) == 0 || rest[0] != t.parentT.segment:
return fmt.Errorf(`".." steps up from %s to its parent table, so name that next: %s`, t.segment, t.climbTo(rest))
case grammar.HasSelector(rest):
return fmt.Errorf(`no selector after "..": a row selected there could lie outside the row stepped up to; select from the table's path from the data root: %s`, grammar.JoinSegments(append([]string{t.parentT.path}, rest[1:]...)))
}
return nil
}
// climbTo spells the ".." steps from t up to the ancestor rest names first, or the one step to
// t's parent where no ancestor is named.
func (t *table) climbTo(rest []string) string {
var b strings.Builder
for a := t.parentT; a != nil; a = a.parentT {
b.WriteString(".." + a.segment)
if len(rest) > 0 && a.segment == rest[0] {
return b.String()
}
}
return ".." + t.parentT.segment
}
// pathStep is one step of a compiled path, taken at the node the steps before it
// reached, a choice there resolved first; at indexes the tail where it is taken, and
// only a table's step sits past the tail's end. A step holds no node, so a
@@ -161,46 +129,51 @@ func (w *pathCheck) run(n node) (node, error) {
func (w *pathCheck) walk(n node, tail []string) (node, error) {
for descended := false; len(tail) > 0 || descended; {
var err error
switch x := n.(type) {
case *choice:
return w.walkEvery(x, tail)
case *table:
var r tableRoute
if r, err = x.route(tail, descended); err != nil {
return nil, err
}
if w.steps, err = routeSteps(w.steps, &w.pins, x, r, len(w.tail)-len(tail)); err != nil {
return nil, err
}
n, tail, descended = r.next, r.rest, r.descends
continue
case *template:
if grammar.IsSelector(tail[0]) {
break
}
if err := w.enter(x, tail); err != nil {
return nil, err
}
if c, ok := n.(*choice); ok {
return w.walkEvery(c, tail)
}
if n, err = stepInto(n, tail[0]); err != nil {
var err error
if n, tail, descended, w.steps, err = takeStep(n, tail, descended, w.tail, w.level, w.steps, &w.pins); err != nil {
return nil, err
}
w.steps = append(w.steps, pathStep{kind: stepField, at: len(w.tail) - len(tail), name: tail[0]})
tail = tail[1:]
}
w.leaves = append(w.leaves, n)
return n, nil
}
// takeStep takes the step tail starts with from n, which is no choice, and appends what it takes
// to steps: a field, or the route a table passes. whole is the path tail ends, and level names its
// head in errors.
func takeStep(n node, tail []string, descended bool, whole []string, level string, steps []pathStep, pins *pinSet) (node, []string, bool, []pathStep, error) {
at := len(whole) - len(tail)
switch x := n.(type) {
case *table:
r, err := x.route(tail, descended)
if err != nil {
return nil, nil, false, steps, err
}
steps, err = routeSteps(steps, pins, x, r, at)
return r.next, r.rest, r.descends, steps, err
case *template:
if x.repeat > 1 && !grammar.IsSelector(tail[0]) {
return nil, nil, false, steps, fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", join(level, strings.Join(whole[:at], ".")))
}
}
next, err := stepInto(n, tail[0])
if err != nil {
return nil, nil, false, steps, err
}
return next, tail[1:], false, append(steps, pathStep{kind: stepField, at: at, name: tail[0]}), nil
}
// routeSteps appends the steps r takes past t, the first at at, pinning in pins the row
// it selects.
func routeSteps(steps []pathStep, pins *pinSet, t *table, r tableRoute, at int) ([]pathStep, error) {
if r.sel != "" {
if err := pins.selectRow(t, r.sel); err != nil {
row, err := pins.Select(t.rows, r.sel)
if err != nil {
return steps, err
}
row, _ := pins.pinned(t)
steps = append(steps, pathStep{kind: stepSelect, at: at, name: r.sel, row: row})
at++
}
@@ -213,14 +186,14 @@ func routeSteps(steps []pathStep, pins *pinSet, t *table, r tableRoute, at int)
case r.up:
steps = append(steps, pathStep{kind: stepParent, at: at})
case next != t:
steps = append(steps, pathStep{kind: stepChild, at: at, name: next.segment})
steps = append(steps, pathStep{kind: stepChild, at: at, name: next.rows.Segment()})
}
case *tableRow:
steps = append(steps, pathStep{kind: stepRow, at: at})
case *tableColumn:
steps = append(steps, pathStep{kind: stepColumn, at: at, name: t.header[next.i]})
steps = append(steps, pathStep{kind: stepColumn, at: at, name: next.name()})
default:
panic(invariant.Broken("%s: a route onto %T", t.segment, r.next))
panic(invariant.Broken("%s: a route onto %T", t.rows.Segment(), r.next))
}
return steps, nil
}
@@ -247,44 +220,24 @@ func (w *pathCheck) walkEvery(c *choice, tail []string) (node, error) {
return last, nil
}
func (w *pathCheck) enter(t *template, rest []string) error {
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)
}
return nil
}
// probePath proves a path resolves without drawing, appending to steps the steps a
// draw takes. Where pathCheck walks every variant of a choice, it walks the first,
// which carriedByAll lets stand for all; at a table it selects as pathCheck does.
// which carriedByAll lets stand for all.
func probePath(n node, tail []string, steps []pathStep) ([]pathStep, error) {
var pins pinSet
for full, descended := len(tail), false; len(tail) > 0 || descended; {
var err error
switch x := n.(type) {
case *choice:
if err := carriedByAll(x, tail); err != nil {
whole := tail
for descended := false; len(tail) > 0 || descended; {
if c, ok := n.(*choice); ok {
if err := carriedByAll(c, tail); err != nil {
return nil, err
}
n = x.items[0]
continue
case *table:
var r tableRoute
if r, err = x.route(tail, descended); err != nil {
return nil, err
}
if steps, err = routeSteps(steps, &pins, x, r, full-len(tail)); err != nil {
return nil, err
}
n, tail, descended = r.next, r.rest, r.descends
n = c.items[0]
continue
}
if n, err = stepInto(n, tail[0]); err != nil {
var err error
if n, tail, descended, steps, err = takeStep(n, tail, descended, whole, "", steps, &pins); err != nil {
return nil, err
}
steps = append(steps, pathStep{kind: stepField, at: full - len(tail), name: tail[0]})
tail = tail[1:]
}
return steps, nil
}
@@ -295,7 +248,7 @@ func probePath(n node, tail []string, steps []pathStep) ([]pathStep, error) {
// 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 *drawstate.State, n node, steps []pathStep, pins *pinSet, memo *drawMemo, levels []string) (node, *pinSet) {
var climbed *table
var climbed *rowsTable
for _, st := range steps {
if c, ok := n.(*choice); ok {
n = drawVariant(s, c, memo, levels, st.at)
@@ -313,7 +266,7 @@ func drawSteps(s *drawstate.State, n node, steps []pathStep, pins *pinSet, memo
}
switch {
case st.kind == stepParent:
climbed = t.parentT
climbed = t.rows.Parent()
case st.kind == stepChild && climbed != nil:
pins, climbed = memo.stepDownPins(pins, climbed, levels, st.at), nil
}
@@ -325,21 +278,21 @@ func drawSteps(s *drawstate.State, n node, steps []pathStep, pins *pinSet, memo
func (t *table) drawStep(s *drawstate.State, st pathStep, pins *pinSet) node {
switch st.kind {
case stepSelect:
if err := pins.pinRow(t, st.row); err != nil {
panic(invariant.Broken("%s[%s]: %v", t.segment, st.name, err))
if err := pins.PinRow(t.rows, st.row); err != nil {
panic(invariant.Broken("%s[%s]: %v", t.rows.Segment(), st.name, err))
}
return t
case stepDraw:
t.drawIn(s, pins)
t.rows.DrawIn(s, pins)
return t
case stepRow:
return t.rowNode
case stepColumn:
return t.formatTemplate.fields[st.name]
case stepChild:
return t.descendant(st.name)
return t.rows.Descendant(st.name).Payload()
case stepParent:
return t.parentT
return t.rows.Parent().Payload()
}
panic(invariant.Broken("drawStep has no case for step kind %d", st.kind))
}
@@ -357,7 +310,7 @@ func (t *table) selector(tail []string) (sel string, rest []string, err error) {
}
sel, rest = grammar.SelectorOf(tail[0]), tail[1:]
if len(rest) > 0 && grammar.IsSelector(rest[0]) {
return "", nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.segment, sel)
return "", nil, fmt.Errorf("%s[%s] is selected twice; one selector names its row", t.rows.Segment(), sel)
}
return sel, rest, nil
}
@@ -370,10 +323,11 @@ func (t *table) step(tail []string) (column node, child *table, err error) {
if column, ok := t.formatTemplate.fields[tail[0]]; ok {
return column, nil, nil
}
if child = t.descendant(tail[0]); child == nil {
return nil, nil, fmt.Errorf("no column or linked table %q in %s", tail[0], t.segment)
d := t.rows.Descendant(tail[0])
if d == nil {
return nil, nil, fmt.Errorf("no column or linked table %q in %s", tail[0], t.rows.Segment())
}
return nil, child, nil
return nil, d.Payload(), nil
}
// carriedByAll is the choice rule a path that must resolve on every call obeys:
+5 -5
View File
@@ -27,13 +27,13 @@ type drawMemo struct {
// stepDownPins is the pins a path steps down into after stepping up to t: kept in m where there
// is one, so every path stepping down there reads one draw.
func (m *drawMemo) stepDownPins(pins *pinSet, t *table, levels []string, at int) *pinSet {
func (m *drawMemo) stepDownPins(pins *pinSet, t *rowsTable, levels []string, at int) *pinSet {
if m == nil {
return pins.above(t)
return pins.Above(t)
}
p, ok := m.steppedDown[levels[at]]
if !ok {
p = pins.above(t)
p = pins.Above(t)
if m.steppedDown == nil {
m.steppedDown = map[string]*pinSet{}
}
@@ -162,9 +162,9 @@ func readName(s *drawstate.State, sc renderScope, a arm) readValue {
// drawRowOf draws the row a render of t reads: inside the pick's rows where t renders as part of one.
func (sc renderScope) drawRowOf(s *drawstate.State, t *table) int {
if sc.pick != nil {
return t.drawIn(s, &sc.pick.pins)
return t.rows.DrawIn(s, &sc.pick.pins)
}
return t.drawRow(s)
return t.rows.Draw(s)
}
// keeps reports whether a read of sc's name addresses the level a starts at.
+3 -3
View File
@@ -162,11 +162,11 @@ func tableRecord(s *drawstate.State, t *table, tail []string) (node, renderedRow
}
switch n := n.(type) {
case *table:
return n, renderedRow{n, n.drawIn(s, &pins)}, nil
return n, renderedRow{n, n.rows.DrawIn(s, &pins)}, nil
case *tableRow:
return n.t, renderedRow{n.t, pins.mustRow(n.t)}, nil
return n.t, renderedRow{n.t, pins.MustRow(n.t.rows)}, nil
case *tableColumn:
return nil, renderedRow{}, 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.name())
}
return n, renderedRow{}, nil
}
+2 -2
View File
@@ -71,7 +71,7 @@ func render(s *drawstate.State, n node, sc renderScope) string {
if cell := n.t.cellTemplate(row, n.i); cell != nil {
return render(s, cell, sc)
}
return n.t.cell(row, n.i)
return n.t.rows.Cell(row, n.i)
case *template:
if n.repeat == 1 {
if mark := sc.renderFrame(n); mark >= 0 {
@@ -114,7 +114,7 @@ func pick(s *drawstate.State, c *choice) node {
if c.cum == nil {
return c.items[s.IntN(len(c.items))]
}
return c.items[pickCum(s, c.cum)]
return c.items[s.Weighted(c.cum)]
}
func expand(s *drawstate.State, t *template, sc renderScope) string {
+3 -3
View File
@@ -34,9 +34,9 @@ type renderedRow struct {
func (sc renderScope) at(n node, pins *pinSet) renderScope {
switch n := n.(type) {
case *tableRow:
sc.row = renderedRow{n.t, pins.mustRow(n.t)}
sc.row = renderedRow{n.t, pins.MustRow(n.t.rows)}
case *tableColumn:
sc.row = renderedRow{n.t, pins.mustRow(n.t)}
sc.row = renderedRow{n.t, pins.MustRow(n.t.rows)}
}
return sc
}
@@ -44,7 +44,7 @@ func (sc renderScope) at(n node, pins *pinSet) renderScope {
// rowOf is the row of t its columns render from.
func (sc renderScope) rowOf(t *table) int {
if sc.row.t != t {
panic(invariant.Broken("a column of %s renders in a scope holding no row of it", t.segment))
panic(invariant.Broken("a column of %s renders in a scope holding no row of it", t.rows.Segment()))
}
return sc.row.index
}
+5 -7
View File
@@ -40,7 +40,7 @@ func shippedShape(f *Generator) string {
facts[prefix] = readsFact(n)
case *table:
facts[prefix] = "\tformat " + strconv.Quote(n.formatTemplate.format) + tableFacts(n) + readsFact(n)
for _, name := range n.header {
for _, name := range n.rows.Header() {
facts[join(prefix, name)] = "\tstring"
}
case *template:
@@ -69,12 +69,10 @@ func shippedShape(f *Generator) string {
// tableFacts names the columns a table's options read.
func tableFacts(t *table) string {
var b strings.Builder
for _, o := range []struct {
name string
col int
}{{"key", t.keyIndex}, {"name", t.nameIndex}, {"weight", t.weightIndex}, {"parent", t.parentIndex}} {
if o.col >= 0 {
b.WriteString("\t" + o.name + " " + t.header[o.col])
c := t.rows.Options()
for _, o := range []struct{ name, column string }{{"key", c.Key}, {"name", c.Name}, {"weight", c.Weight}, {"parent", c.Parent}} {
if o.column != "" {
b.WriteString("\t" + o.name + " " + o.column)
}
}
return b.String()
+2 -2
View File
@@ -69,8 +69,8 @@ func shippedIndexSource(whole *Generator) ([]byte, error) {
b.WriteString("// Code generated by go generate. DO NOT EDIT.\n\npackage fejkdata\n\nvar shippedIndex = map[string]shippedEntry{\n")
for _, s := range categorySites(&whole.root) {
fmt.Fprintf(&b, "%q: {", s.path)
if t, isTable := s.n.(*table); isTable && t.parentIndex >= 0 {
fmt.Fprintf(&b, "parent: %q, ", t.header[t.parentIndex])
if t, isTable := s.n.(*table); isTable && t.rows.Options().Parent != "" {
fmt.Fprintf(&b, "parent: %q, ", t.rows.Options().Parent)
}
ps := paths(s.n)
sort.Strings(ps)
+50 -435
View File
@@ -2,48 +2,26 @@ package fejkdata
import (
"fmt"
"math"
"sort"
"strconv"
"strings"
"sync"
"github.com/larvit/fejkdata/internal/drawstate"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/rows"
)
// 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 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
file string
rows *rowsTable
formatTemplate *template // fields are the column nodes
header []string
col map[string]int
rowNode *tableRow
cells []string // rows × columns, flat
cellTemplates map[int]*template
keyIndex int // keyIndex through parentIndex are column indexes, -1 where the option is absent
nameIndex int
weightIndex int
parentIndex int
cum []float64 // cumulative weights, nil when uniform
byKey map[string]int
parentT *table
children map[string]*table
once sync.Once
lookup rowLookup
}
// rowLookup is what selection and linked draws look up, built on the first draw
// that needs it.
type rowLookup struct {
byName map[string][]int
rowsByParent map[string][]int
childCum map[string][]float64 // cumulative weights per parent key, nil when uniform
}
type (
rowsTable = rows.Table[*table]
pinSet = rows.Pins[*table]
)
func (*table) isNode() {}
@@ -55,18 +33,18 @@ type tableColumn struct {
func (*tableColumn) isNode() {}
func (c *tableColumn) name() string { return c.t.rows.Header()[c.i] }
// tableRow is the row of a table the render pinned, rendered by the table's format.
type tableRow struct{ t *table }
func (*tableRow) isNode() {}
func (t *table) rowCount() int { return len(t.cells) / len(t.header) }
func (t *table) cell(row, col int) string { return t.cells[row*len(t.header)+col] }
// cellTemplate is what a cell renders: its compiled template where it carries tokens,
// else nil for its text.
func (t *table) cellTemplate(row, col int) *template { return t.cellTemplates[row*len(t.header)+col] }
func (t *table) cellTemplate(row, col int) *template {
return t.cellTemplates[row*len(t.rows.Header())+col]
}
// tableOptions are the keys a table object takes; every other key is refused.
var tableOptions = []string{"format", "key", "name", "parent", "rows", "weight"}
@@ -80,11 +58,6 @@ func isTableOption(name string) bool {
return false
}
// inSelector is what a key or name cell may not contain: the path grammar reserves
// brackets and braces, and a quote or a bar would make the selector no path and no
// token arm.
const inSelector = `[]{}"|`
func compileTable(m map[string]any, dir []string, name string, readRows func(file string) (string, error)) (*table, error) {
o, err := readTableOptions(m)
if err != nil {
@@ -94,11 +67,8 @@ func compileTable(m map[string]any, dir []string, name string, readRows func(fil
if err != nil {
return nil, err
}
t := &table{segment: name, path: categoryPath(dir, name), file: o.rows, keyIndex: -1, nameIndex: -1, weightIndex: -1, parentIndex: -1}
if err := t.parseRows(data); err != nil {
return nil, fmt.Errorf("%s: %w", o.rows, err)
}
if err := t.bindOptions(o); err != nil {
t := &table{}
if t.rows, err = rows.Parse(t, name, categoryPath(dir, name), o.rows, data, o.columns); err != nil {
return nil, err
}
if err := t.checkCells(); err != nil {
@@ -111,7 +81,8 @@ func compileTable(m map[string]any, dir []string, name string, readRows func(fil
}
type tableOptionValues struct {
format, keyColumn, nameColumn, parentColumn, rows, weightColumn string
format, rows string
columns rows.Columns
}
func readTableOptions(m map[string]any) (tableOptionValues, error) {
@@ -121,7 +92,7 @@ func readTableOptions(m map[string]any) (tableOptionValues, error) {
return o, fmt.Errorf("a table takes %s; %q is none of them", strings.Join(tableOptions, ", "), k)
}
}
for k, into := range map[string]*string{"format": &o.format, "key": &o.keyColumn, "name": &o.nameColumn, "parent": &o.parentColumn, "rows": &o.rows, "weight": &o.weightColumn} {
for k, into := range map[string]*string{"format": &o.format, "key": &o.columns.Key, "name": &o.columns.Name, "parent": &o.columns.Parent, "rows": &o.rows, "weight": &o.columns.Weight} {
v, ok := m[k]
if !ok {
continue
@@ -141,180 +112,30 @@ func readTableOptions(m map[string]any) (tableOptionValues, error) {
if !strings.HasSuffix(o.rows, ".tsv") || strings.Contains(o.rows, "/") {
return o, fmt.Errorf("rows names a .tsv file beside the category, not %q", o.rows)
}
if o.keyColumn != "" && o.keyColumn == o.nameColumn {
return o, fmt.Errorf("name names the key column %q, which a selector already reads; drop it", o.nameColumn)
if o.columns.Key != "" && o.columns.Key == o.columns.Name {
return o, fmt.Errorf("name names the key column %q, which a selector already reads; drop it", o.columns.Name)
}
return o, nil
}
// 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(strings.TrimPrefix(data, "\xEF\xBB\xBF"), "\n")
header, rest, _ := strings.Cut(data, "\n")
header = strings.TrimSuffix(header, "\r")
if header == "" {
return fmt.Errorf("has no header line naming its columns")
}
t.header = strings.Split(header, "\t")
t.col = make(map[string]int, len(t.header))
for i, name := range t.header {
if err := grammar.CheckName(name); err != nil {
return fmt.Errorf("column %w", err)
}
if _, dup := t.col[name]; dup {
return fmt.Errorf("column %q is named twice", name)
}
t.col[name] = i
}
if header == data {
return fmt.Errorf("has no rows below its header; a table is at least two rows")
}
m := len(t.header)
t.cells = make([]string, 0, m*(strings.Count(rest, "\n")+1))
for line := 2; ; line++ {
row, more, found := strings.Cut(rest, "\n")
row = strings.TrimSuffix(row, "\r")
if err := t.appendRow(row, line); err != nil {
return err
}
if !found {
break
}
rest = more
}
if t.rowCount() < 2 {
return fmt.Errorf("has one row, which is a template; write it as one")
}
return nil
}
// appendRow splits one line into as many cells as the header has columns.
func (t *table) appendRow(row string, line int) error {
if row == "" {
return fmt.Errorf("line %d is empty; every line below the header is a row", line)
}
m := len(t.header)
for n := 0; n < m; n++ {
cell, next, tab := strings.Cut(row, "\t")
if !tab && n < m-1 || tab && n == m-1 {
return fmt.Errorf("line %d has %s cells than the %d columns", line, map[bool]string{true: "more", false: "fewer"}[tab], m)
}
t.cells = append(t.cells, cell)
row = next
}
return nil
}
// bindOptions resolves each option to its column and proves what it claims of the
// cells: a key is unique, a weight a positive number.
func (t *table) bindOptions(o tableOptionValues) error {
for _, opt := range []struct {
name, value string
into *int
}{{"key", o.keyColumn, &t.keyIndex}, {"name", o.nameColumn, &t.nameIndex}, {"parent", o.parentColumn, &t.parentIndex}, {"weight", o.weightColumn, &t.weightIndex}} {
if opt.value == "" {
continue
}
i, ok := t.col[opt.value]
if !ok {
return fmt.Errorf("%s names no column %q of %s; the columns are %v", opt.name, opt.value, o.rows, t.header)
}
*opt.into = i
}
if t.nameIndex >= 0 && t.keyIndex < 0 && t.parentIndex < 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, or a parent inside which each name is one row; add key")
}
if err := t.proveKeys(); err != nil {
return err
}
if err := t.proveNamesInsideParent(); err != nil {
return err
}
return t.sumWeights()
}
// proveNamesInsideParent proves a name without a key names one row inside its parent.
func (t *table) proveNamesInsideParent() error {
if t.keyIndex >= 0 || t.nameIndex < 0 {
return nil
}
inside := make(map[string]int, t.rowCount())
for r := 0; r < t.rowCount(); r++ {
k := t.cell(r, t.parentIndex) + "\t" + t.cell(r, t.nameIndex)
if first, dup := inside[k]; dup {
return fmt.Errorf("%s line %d: name %q repeats line %d inside %s %q; a name selects one row inside its parent; drop one, or add a key column", t.file, r+2, t.cell(r, t.nameIndex), first+2, t.header[t.parentIndex], t.cell(r, t.parentIndex))
}
inside[k] = r
}
return nil
}
// proveKeys proves every key names one row, and keeps the map a link is proved by.
func (t *table) proveKeys() error {
if t.keyIndex < 0 {
return nil
}
t.byKey = make(map[string]int, t.rowCount())
for r := 0; r < t.rowCount(); r++ {
k := t.cell(r, t.keyIndex)
if k == "" {
return fmt.Errorf("%s line %d: the key is empty", t.file, r+2)
}
if first, dup := t.byKey[k]; dup {
return fmt.Errorf("%s line %d: key %q repeats line %d; a key selects one row", t.file, r+2, k, first+2)
}
t.byKey[k] = r
}
for r := 0; r < t.rowCount() && t.nameIndex >= 0; r++ {
if n := t.cell(r, t.nameIndex); 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
}
// sumWeights proves every weight is a positive number and builds the cumulative table.
func (t *table) sumWeights() error {
if t.weightIndex < 0 {
return nil
}
t.cum = make([]float64, t.rowCount())
total := 0.0
for r := range t.cum {
w, err := strconv.ParseFloat(t.cell(r, t.weightIndex), 64)
if err != nil || math.IsInf(w, 0) || math.IsNaN(w) || w <= 0 {
return fmt.Errorf("%s line %d: weight %q is not a positive number", t.file, r+2, t.cell(r, t.weightIndex))
}
total += w
t.cum[r] = total
}
if math.IsInf(total, 0) {
return fmt.Errorf("%s: the weights sum past the largest number", t.file)
}
return nil
}
// checkCells compiles every cell carrying a token, and fences what a selector reads.
// checkCells compiles every cell carrying a token.
func (t *table) checkCells() error {
for i, cell := range t.cells {
row, col := i/len(t.header), i%len(t.header)
if (col == t.keyIndex || col == t.nameIndex) && strings.ContainsAny(cell, inSelector) {
return fmt.Errorf("line %d: %s %q contains %q, which a selector cannot spell", row+2, t.header[col], cell, cell[strings.IndexAny(cell, inSelector):][:1])
header := t.rows.Header()
for row := 0; row < t.rows.Len(); row++ {
for col := range header {
cell := t.rows.Cell(row, col)
if strings.IndexByte(cell, '{') < 0 && strings.IndexByte(cell, '}') < 0 {
continue
}
n, err := compileCell(cell)
if err != nil {
return fmt.Errorf("line %d, %s: %w", row+2, header[col], err)
}
if t.cellTemplates == nil {
t.cellTemplates = map[int]*template{}
}
t.cellTemplates[row*len(header)+col] = n
}
if strings.IndexByte(cell, '{') < 0 && strings.IndexByte(cell, '}') < 0 {
continue
}
n, err := compileCell(cell)
if err != nil {
return fmt.Errorf("line %d, %s: %w", row+2, t.header[col], err)
}
if t.cellTemplates == nil {
t.cellTemplates = map[int]*template{}
}
t.cellTemplates[i] = n
}
return nil
}
@@ -351,13 +172,13 @@ func (t *table) compileRowFormat(format string) error {
}
for _, name := range tok.Arms {
a := splitArm(name, nil)
if _, ok := t.col[a.head]; !ok && !grammar.IsRef(a.head) && a.head != "" {
return fmt.Errorf("format names no column %q of %s; the columns are %v", a.head, t.file, t.header)
if _, ok := t.rows.Column(a.head); !ok && !grammar.IsRef(a.head) && a.head != "" {
return fmt.Errorf("format names no column %q of %s; the columns are %v", a.head, t.rows.File(), t.rows.Header())
}
}
}
fields := make(map[string]node, len(t.header))
for i, name := range t.header {
fields := make(map[string]node, len(t.rows.Header()))
for i, name := range t.rows.Header() {
fields[name] = &tableColumn{t, i}
}
unbound, err := checkTokens(toks, fields)
@@ -382,240 +203,34 @@ func refuseTableBinding(toks []grammar.Token) error {
return nil
}
// linkTables links each table to the parent beside it; it is the only writer of parentT and
// children.
// linkTables links each table to the parent beside it.
func linkTables(sites []categorySite) error {
for _, s := range sites {
t, isTable := s.n.(*table)
if !isTable || t.parentIndex < 0 {
if !isTable || t.rows.LinkColumn() < 0 {
continue
}
p, err := t.proveParent(s.in.children)
if err != nil {
if err := t.linkParent(s.in.children); err != nil {
return fmt.Errorf("%s: %w", s.path, err)
}
t.parentT = p
if p.children == nil {
p.children = map[string]*table{}
}
p.children[t.segment] = t
}
return nil
}
// proveParent is the table among siblings that t's link column names, proved: it has a key,
// every link cell is one, and every row of it is linked to; checkAncestors proves the rest.
// docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error
func (t *table) proveParent(siblings map[string]node) (*table, error) {
name := t.header[t.parentIndex]
// linkParent links t to the table among siblings that its link column names.
func (t *table) linkParent(siblings map[string]node) error {
name := t.rows.Options().Parent
p, ok := siblings[name].(*table)
switch {
case siblings[name] == nil:
return nil, fmt.Errorf("parent %q names no table beside it", name)
return fmt.Errorf("parent %q names no table beside it", name)
case !ok:
return nil, fmt.Errorf("parent %q is not a table; a link column reads a table's key", name)
case p.keyIndex < 0:
return nil, fmt.Errorf("parent %q has no key column to link to", name)
return fmt.Errorf("parent %q is not a table; a link column reads a table's key", name)
}
if err := t.checkAncestors(p, siblings); err != nil {
return nil, err
}
linked := make(map[string]bool, p.rowCount())
for r := 0; r < t.rowCount(); r++ {
k := t.cell(r, t.parentIndex)
if _, ok := p.byKey[k]; !ok {
return nil, fmt.Errorf("%s line %d: %s %q is no key of %s", t.file, r+2, name, k, p.file)
}
linked[k] = true
}
for r := 0; r < p.rowCount(); r++ {
if k := p.cell(r, p.keyIndex); !linked[k] {
return nil, fmt.Errorf("%s links no row to %s %q; every %s row needs one, or drop line %d of %s", t.file, name, k, name, r+2, p.file)
}
}
return p, nil
}
// checkAncestors refuses a cycle in the chain of parents starting at p, t's parent, and
// refuses an ancestor that has a column named like t.
func (t *table) checkAncestors(p *table, siblings map[string]node) error {
var ancestors []*table
for q, seen := p, map[*table]bool{t: true}; q != nil; q, _ = siblings[q.header[q.parentIndex]].(*table) {
if seen[q] {
return fmt.Errorf("parent cycle: %s reaches itself through its parents", q.segment)
}
seen[q] = true
ancestors = append(ancestors, q)
if q.parentIndex < 0 {
break
}
}
for _, q := range ancestors {
if _, clash := q.col[t.segment]; clash {
return fmt.Errorf("%q is named like a column of %q, its ancestor, so %s.%s could read either; rename one", t.segment, q.segment, q.segment, t.segment)
}
}
return nil
}
func (t *table) builtLookup() *rowLookup {
t.once.Do(func() {
if t.nameIndex >= 0 {
t.lookup.byName = make(map[string][]int, t.rowCount())
for r := 0; r < t.rowCount(); r++ {
n := t.cell(r, t.nameIndex)
t.lookup.byName[n] = append(t.lookup.byName[n], r)
}
}
if t.parentIndex >= 0 {
t.lookup.rowsByParent = map[string][]int{}
for r := 0; r < t.rowCount(); r++ {
k := t.cell(r, t.parentIndex)
t.lookup.rowsByParent[k] = append(t.lookup.rowsByParent[k], r)
}
if t.cum != nil {
t.lookup.childCum = make(map[string][]float64, len(t.lookup.rowsByParent))
for k, rows := range t.lookup.rowsByParent {
cum, total := make([]float64, len(rows)), 0.0
for i, r := range rows {
w, _ := strconv.ParseFloat(t.cell(r, t.weightIndex), 64) // sumWeights proved it
total += w
cum[i] = total
}
t.lookup.childCum[k] = cum
}
}
return t.rows.Link(p.rows, func(name string) *rowsTable {
if q, ok := siblings[name].(*table); ok {
return q.rows
}
return nil
})
return &t.lookup
}
func (t *table) drawRow(s *drawstate.State) int {
if t.cum == nil {
return s.IntN(t.rowCount())
}
return pickCum(s, t.cum)
}
func pickCum(s *drawstate.State, cum []float64) int {
x := s.Float64() * cum[len(cum)-1]
return min(sort.Search(len(cum), func(i int) bool { return cum[i] > x }), len(cum)-1) // x can round up to the total
}
// drawUnder picks a row among those linked to parent row pr.
func (t *table) drawUnder(s *drawstate.State, pr int) int {
lookup := t.builtLookup()
k := t.parentT.cell(pr, t.parentT.keyIndex)
rows := lookup.rowsByParent[k]
if lookup.childCum == nil {
return rows[s.IntN(len(rows))]
}
return rows[pickCum(s, lookup.childCum[k])]
}
// 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 *drawstate.State, p *pinSet) int {
if r, ok := p.pinned(t); ok {
return r
}
r := -1
for a := t.parentT; a != nil && r < 0; a = a.parentT {
if _, ok := p.pinned(a); !ok {
continue
}
if t.parentT != a {
t.parentT.drawIn(s, p)
}
pr, _ := p.pinned(t.parentT)
r = t.drawUnder(s, pr)
}
if r < 0 {
r = t.drawRow(s)
}
p.pin(t, r)
return r
}
// descendant is the table named name among those linked to t, at any depth.
func (t *table) descendant(name string) *table {
if c, ok := t.children[name]; ok {
return c
}
for _, c := range t.children {
if d := c.descendant(name); d != nil {
return d
}
}
return nil
}
// parentRow is the row of t's parent that row r links to.
func (t *table) parentRow(r int) int { return t.parentT.byKey[t.cell(r, t.parentIndex)] }
// under reports whether row r of t sits inside row pr of ancestor a.
func (t *table) under(r int, a *table, pr int) bool {
for c, row := t, r; c.parentT != nil; c, row = c.parentT, c.parentRow(row) {
if c.parentT == a {
return c.parentRow(row) == pr
}
}
return false
}
// find is the row a selector names: by key first, then by name, where a name
// naming several rows resolves inside the ancestors pinned.
func (t *table) find(sel string, pins *pinSet) (int, error) {
if t.keyIndex < 0 && t.nameIndex < 0 {
return 0, fmt.Errorf("%s has no key or name column to select a row by", t.path)
}
if r, ok := t.byKey[sel]; ok {
return r, nil
}
rows := t.builtLookup().byName[sel]
if len(rows) > 1 {
rows = pins.inside(t, rows)
}
switch len(rows) {
case 1:
return rows[0], nil
case 0:
return 0, fmt.Errorf("no row of %s has key or name %q", t.path, sel)
}
return 0, t.ambiguous(sel, rows)
}
// ambiguous names each row a selector could have meant: a keyed table by its keys,
// and one told apart only by its parent by the path that selects it.
func (t *table) ambiguous(sel string, rows []int) error {
spellings := make([]string, len(rows))
for i, r := range rows {
if t.keyIndex < 0 {
spellings[i] = t.selectorSpelling(r)
} else {
spellings[i] = t.cell(r, t.keyIndex)
}
}
listed := strings.Join(spellings, ", ")
if t.keyIndex < 0 {
return fmt.Errorf("%q names %d rows of %s; select it inside its %s, one of %s", sel, len(rows), t.path, t.parentT.path, listed)
}
inside := ""
if t.parentT != nil {
inside = fmt.Sprintf(", or select it inside its %s", t.parentT.path)
}
return fmt.Errorf("%q names %d rows of %s; select one by key, one of %s%s", sel, len(rows), t.path, listed, inside)
}
// selectorSpelling is how a path writes a selected row, for messages: by key, or
// by name inside its parent's row.
func (t *table) selectorSpelling(r int) string {
switch {
case t.keyIndex >= 0:
return t.path + "[" + t.cell(r, t.keyIndex) + "]"
case t.nameIndex >= 0:
return t.parentT.selectorSpelling(t.parentRow(r)) + "." + t.segment + "[" + t.cell(r, t.nameIndex) + "]"
}
return fmt.Sprintf("%s line %d", t.file, r+2)
}
+5 -10
View File
@@ -72,10 +72,9 @@
| 57 | 0.1.0 | | **Rewrite `CHANGELOG.md`'s `[Unreleased]` as what v0.1.0 holds.** | 1 | 2 | 6 | 6 | 1.1 | 22.5 |
| 34 | 0.1.0 | | **Add `{isin()}`, `{cusip()}`, `{aba()}` and `{vin()}`.** | 2 | 4 | 6 | 7 | 4 | 20.9 |
| 96 | 0.1.0 | | **Refuse two reads into one sibling, category or struct category that no name joins, `{place.postal-code} {place.locality}`, showing the spelling for one pick and the one for two.** | 4 | 4 | 7 | 7 | 5.6, 4 | 20.9 |
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel and larv-review's scoring run once items 102 and 104 to 106 have restructured the code, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 5 | 0.1.0 | decision | **Run the nine-seat comprehension panel and larv-review's scoring run once items 104 to 106 have restructured the code, and file what the panel names.** | 1 | 3 | 3 | 9 | 3 | 20.4 |
| 91 | 0.1.0 | | **Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.** | 3 | 4 | 6 | 7 | 4.1, 5.3 | 19.9 |
| 89 | 0.1.0 | | **Fail `New` without `WithSeed`, naming the option, and seed the CLI from the system.** | 3 | 3 | 6 | 7 | 10.1 | 16.0 |
| 102 | 0.1.0 | defect | **Move a table's rows and the links between tables into `internal/rows`, and walk every path through one per-step function, so `Fake` refuses a level carrying a `repeat` as the load check does.** | 5 | 6 | 4 | 8 | 3.2, 3.4, 5 | 15.9 |
| 106 | 0.1.0 | | **Settle each record's shape when its template links, so `Generator.records` goes.** | 2 | 2 | 2 | 7 | 3.2 | 14.1 |
| 110 | 0.1.0 | | **Refuse an empty segment in a path `Fake`, `FakeRecord` and the CLI take, as a reference does: `sv_SE.person.` reports `path has an empty segment`.** | 1 | 1 | 3 | 6 | 6 | 13.8 |
| 104 | 0.1.0 | decision | **Create a name scope with its owner in one place, and hand a template's name reads to the checks as a returned value, not appended to its bindings.** | 4 | 4 | 2 | 8 | 3.2 | 13.4 |
@@ -242,9 +241,9 @@ Nothing has shipped, so "no longer paths", "where it used to fail" and "where it
Asked of the maintainer by the product-owner pass of the chunk that made every `{…}` draw afresh. Since every `{…}` draws afresh, `{net} x {qty} = {calc(net * qty, 2)}`, `{place.postal-code} {place.locality}` and struct tags `sv_SE.address.street` beside `sv_SE.address.postal-code` load and may render a pair that disagrees, and nothing tells the author to bind a name (goal 4.1). Goal 5.6 asks such a template to fail, showing `{place as p}{p.postal-code} {p.locality}` for one place and two names for two. The decision "Every `{…}` draws afresh, and only a name keeps a pick" lists the draw checks as names, paths through names, cycles and the repeat cap, so this fence revises it. `{w} {w}`, one spelling twice, stays two draws. The maintainer answered yes on 2026-10-05, for reads inside one category or one struct; reads across categories, `{/city.name}, {/country.name}`, keep loading.
### 5. Run the nine-seat comprehension panel and larv-review's scoring run once items 102 and 104 to 106 have restructured the code, and file what the panel names.
### 5. Run the nine-seat comprehension panel and larv-review's scoring run once items 104 to 106 have restructured the code, and file what the panel names.
Until a panel scores 7.0 or above, only items 102 and 104 to 106 and the exceptions in `AGENTS.md`'s comprehension rule merge. The decision "comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, and items 102 and 104 to 106" suspends every scoring run and panel until this item runs.
Until a panel scores 7.0 or above, only items 104 to 106 and the exceptions in `AGENTS.md`'s comprehension rule merge. The decision "comprehension floor: every dimension and the overall at 7.0 or above; item 5 scores the restructure, which ships unscored: every `{…}` drawing afresh, the moves into `internal/`, cutting a template's pointer to its table, the move of a table's rows into `internal/rows`, and items 104 to 106" suspends every scoring run and panel until this item runs.
### 91. Accept a path through a name as a `calc` operand, so `{/misc.coordinate as c}{c}: {calc(c.lat * 60)}` computes from the coordinate `{c}` prints.
@@ -254,10 +253,6 @@ A calc operand parses as a bare identifier, so `{calc(c.lat * 60)}` fails with `
Goal 10.1 has the caller supply the seed, and today `New` without `WithSeed` seeds itself. This item ships in 0.1.0 beside item 77, so `New` breaks once before anyone depends on it. Mark `WithSeed` required in the Library section's options table. Its tests then stop swapping `randomBytes`, as `fejkdata_test.go` does today, which fails every `New` in the package while that test runs.
### 102. Move a table's rows and the links between tables into `internal/rows`, and walk every path through one per-step function, so `Fake` refuses a level carrying a `repeat` as the load check does.
`internal/rows` takes the rows, their key and parent proofs, the pins, the row draws and the lookups, and owns the parent and child links. Each rows table holds the engine table that owns it as a payload of a type parameter, set when the rows table is built, so a step to a relative reaches its format and cells with no side map, the links have one writer, and `internal/rows` imports no engine type. The rows code reads `CheckName` and the selector spelling from `internal/grammar`. The lookups reuse the weights `sumWeights` parsed; today `builtLookup` parses them again. Today `pathCheck.walk` and `probePath` are two loops over one rule for passing a choice, a table or a field, and they have drifted: `probePath` skips `pathCheck.enter`, so `Fake("x.a")` under `"repeat":3` renders one draw where the README's Correlated fields section refuses it; the refusal owes a `CHANGELOG.md` entry. The shared step takes its state as parameters, as `drawSteps` does: one loop switching on a mode raised a repeat of a reference path from 66 to 106 allocations (decision "A path draws through its compiled steps"). Technical principle: DRY.
### 106. Settle each record's shape when its template links, so `Generator.records` goes.
Needs item 105. Settling all 84 shipped record shapes took 0.09 ms on 2026-10-05, so the cache saves nothing. The error for `FakeRecord` on a node that is no record stays at call time. `bindingOfWhole` stops repeating a load-time search on every column of every record draw.
@@ -381,7 +376,7 @@ Require the path step to reach a sibling category.
### 107. Move the template engine into `internal/`, leaving the root the public API and its options.
Needs item 5. The engine returns column values and the root builds `Record`. Revises the decision "The template engine stays the root package until item 107; what reads no engine type sits in `internal/`, and item 102 moves a table's rows there", and, if the vocabulary moves, "The vocabulary sits below `doc.go`'s package clause, not in the package doc".
Needs item 5. The engine returns column values and the root builds `Record`. Revises the decision "The template engine stays the root package until item 107; what reads no engine type, a table's rows included, sits in `internal/`", and, if the vocabulary moves, "The vocabulary sits below `doc.go`'s package clause, not in the package doc".
### 112. Check a calc's field operands where its template links, beside its name operands, once item 36 reports every mistake at once.
@@ -469,7 +464,7 @@ Its doc says render failure, and it wraps what `NewTemplate` and `NewRecordTempl
### 116. Turn a row index into its rows file's line in one function.
Today `namedNode`'s cell line in `graph.go` and each site in `table.go` add the header line and the one-based count. Item 102 moves the rows into `internal/rows`; the function goes with them.
Today `namedNode`'s cell line in `graph.go` and each site in `internal/rows` add the header line and the one-based count.
### 56. Build on a manual run of `test.yml`.
+3 -3
View File
@@ -113,7 +113,7 @@ func (p *valueProof) prove(n node) proven.Facts {
case *table:
v = p.prove(n.rowNode)
case *tableRow:
v = proven.Unproven(fmt.Sprintf("%q renders a row of %s, which is composed text", n.t.formatTemplate.format, n.t.segment))
v = proven.Unproven(fmt.Sprintf("%q renders a row of %s, which is composed text", n.t.formatTemplate.format, n.t.rows.Segment()))
case *nullItem:
v = proven.Unproven(`it reads a null, which renders "" outside its own column`)
default:
@@ -126,12 +126,12 @@ func (p *valueProof) prove(n node) proven.Facts {
// proveCells proves a table column over every cell it may render.
func (p *valueProof) proveCells(c *tableColumn) proven.Facts {
var v proven.Facts
for r := 0; r < c.t.rowCount(); r++ {
for r := 0; r < c.t.rows.Len(); r++ {
var w proven.Facts
if cell := c.t.cellTemplate(r, c.i); cell != nil {
w = p.prove(cell)
} else {
w = proven.Literal(c.t.cell(r, c.i))
w = proven.Literal(c.t.rows.Cell(r, c.i))
}
if r == 0 {
v = w