diff --git a/README.md b/README.md index 95ef384..df9ea93 100644 --- a/README.md +++ b/README.md @@ -312,8 +312,8 @@ ok, err := fejkdata.IsTemplate(arg) // an inline template by its shape, el `New` refuses a mistake in the data, and `NewTemplate` one in the template. On a loaded generator: -- `Fake` fails only for a path that names nothing or could name two things, with the - same error every call. +- `Fake` fails only for a path that names nothing, could name two things, or reads one + draw of a level carrying a `repeat`, with the same error every call. - `FakeStruct` fails only for a non-struct argument or a type its tags do not describe, with the same error every call. - `Template.Fake` cannot fail. diff --git a/data-import/httpmethod.py b/data-import/httpmethod.py index efcd8ab..0919eef 100644 --- a/data-import/httpmethod.py +++ b/data-import/httpmethod.py @@ -23,7 +23,7 @@ CACHE = Path(__file__).resolve().parent / "cache" COLUMNS = ["idempotent", "method", "safe"] CORE = re.compile(r"\[RFC9110, Section 9\.3\.|\[RFC5789, Section 2\]") BOOLEAN = {"yes": "true", "no": "false"} -SELECTOR = '[]{}"|' # mirrors inSelector in table.go +SELECTOR = '[]{}"|' # mirrors inSelector in internal/grammar/path.go EXPECTED = 9 diff --git a/data-import/protocol.py b/data-import/protocol.py index bc06769..c4cb73d 100644 --- a/data-import/protocol.py +++ b/data-import/protocol.py @@ -21,7 +21,7 @@ OUT = Path(__file__).resolve().parent.parent / "data" / "misc" / "protocol.tsv" CACHE = Path(__file__).resolve().parent / "cache" COLUMNS = ["keyword", "name", "number"] SKIP = ("Reserved", "deprecated") -SELECTOR = '[]{}"|' # mirrors inSelector in table.go +SELECTOR = '[]{}"|' # mirrors inSelector in internal/grammar/path.go def rows(text): diff --git a/fejkdata.go b/fejkdata.go index f3495e1..491a515 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -171,7 +171,7 @@ func tablePaths(t *table) []string { out := append([]string{""}, t.rows.Header()...) sort.Strings(out[1:]) for _, c := range t.rows.Children() { - for _, p := range paths(c.Payload()) { + for _, p := range paths(c.Owner()) { out = append(out, join(c.Segment(), p)) } } diff --git a/internal/drawstate/drawstate_test.go b/internal/drawstate/drawstate_test.go index 1225951..67142dc 100644 --- a/internal/drawstate/drawstate_test.go +++ b/internal/drawstate/drawstate_test.go @@ -38,3 +38,16 @@ func TestCountersCountFromOneEach(t *testing.T) { t.Fatalf("a new state's Seq = %d, want 1", got) } } + +func TestWeightedDrawsByWeight(t *testing.T) { + s := New(1) + counts := make([]int, 3) + for i := 0; i < 30000; i++ { + counts[s.Weighted([]float64{1, 2, 6})]++ + } + for i, want := range []float64{1.0 / 6, 1.0 / 6, 4.0 / 6} { + if share := float64(counts[i]) / 30000; share < want-0.02 || share > want+0.02 { + t.Errorf("index %d drew a share of %.3f, want about %.3f", i, share, want) + } + } +} diff --git a/internal/rows/pins.go b/internal/rows/pins.go index c7bb991..6e9d5ce 100644 --- a/internal/rows/pins.go +++ b/internal/rows/pins.go @@ -6,19 +6,19 @@ import ( "github.com/larvit/fejkdata/internal/invariant" ) -type pin[P any] struct { - t *Table[P] +type pin[O any] struct { + t *Table[O] 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 +type Pins[O any] struct { + inline [8]pin[O] // sized so a render over a country's five-deep geo tree stays off the heap used int - spill map[*Table[P]]int + spill map[*Table[O]]int } -func (p *Pins[P]) Pinned(t *Table[P]) (int, bool) { +func (p *Pins[O]) Pinned(t *Table[O]) (int, bool) { for _, q := range p.inline[:p.used] { if q.t == t { return q.row, true @@ -28,23 +28,23 @@ func (p *Pins[P]) Pinned(t *Table[P]) (int, bool) { 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 { +// MustRow is the row pinned for t, which the caller pinned. +func (p *Pins[O]) MustRow(t *Table[O]) int { r, ok := p.Pinned(t) if !ok { - panic(invariant.Broken("a column of %s is rendered with no row pinned", t.segment)) + panic(invariant.Broken("no row of %s is pinned", t.segment)) } return r } -func (p *Pins[P]) add(t *Table[P], r int) { +func (p *Pins[O]) add(t *Table[O], r int) { if p.used < len(p.inline) { - p.inline[p.used] = pin[P]{t, r} + p.inline[p.used] = pin[O]{t, r} p.used++ return } if p.spill == nil { - p.spill = map[*Table[P]]int{} + p.spill = map[*Table[O]]int{} } p.spill[t] = r } @@ -52,7 +52,7 @@ func (p *Pins[P]) add(t *Table[P], r int) { // 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) { +func (p *Pins[O]) pin(t *Table[O], r int) { for stop := p.nearestPinned(t); t != stop; t = t.parent { p.add(t, r) if t.parent != stop { @@ -63,7 +63,7 @@ func (p *Pins[P]) pin(t *Table[P], r int) { // 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] { +func (p *Pins[O]) nearestPinned(t *Table[O]) *Table[O] { for ; t != nil; t = t.parent { if _, ok := p.Pinned(t); ok { return t @@ -74,7 +74,7 @@ func (p *Pins[P]) nearestPinned(t *Table[P]) *Table[P] { // 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] { +func (p *Pins[O]) clash(t *Table[O], r int) *Table[O] { if pr, ok := p.Pinned(t); ok && pr != r { return t } @@ -87,7 +87,7 @@ func (p *Pins[P]) clash(t *Table[P], r int) *Table[P] { } // 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 { +func (p *Pins[O]) PinRow(t *Table[O], r int) error { switch a := p.clash(t, r); { case a == t: pr, _ := p.Pinned(t) @@ -101,7 +101,7 @@ func (p *Pins[P]) PinRow(t *Table[P], r int) error { } // Select pins the row of t that sel names, and returns it. -func (p *Pins[P]) Select(t *Table[P], sel string) (int, error) { +func (p *Pins[O]) Select(t *Table[O], sel string) (int, error) { r, err := t.find(sel, p) if err != nil { return 0, err @@ -110,7 +110,7 @@ func (p *Pins[P]) Select(t *Table[P], sel string) (int, error) { } // inside keeps the rows of t that sit inside every pinned ancestor. -func (p *Pins[P]) inside(t *Table[P], rows []int) []int { +func (p *Pins[O]) inside(t *Table[O], rows []int) []int { for a := t.parent; a != nil; a = a.parent { pa, ok := p.Pinned(a) if !ok { @@ -128,8 +128,8 @@ func (p *Pins[P]) inside(t *Table[P], rows []int) []int { } // 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]) +func (p *Pins[O]) Above(t *Table[O]) *Pins[O] { + q := new(Pins[O]) for ; t != nil; t = t.parent { if r, ok := p.Pinned(t); ok { q.add(t, r) diff --git a/internal/rows/rows.go b/internal/rows/rows.go index 129003c..053b427 100644 --- a/internal/rows/rows.go +++ b/internal/rows/rows.go @@ -14,14 +14,13 @@ import ( "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 +// Table is the rows of one table category, and O is what owns them. +type Table[O any] struct { + owner O 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 + options Options header []string col map[string]int cells []string // rows × columns, flat @@ -32,14 +31,14 @@ type Table[P any] struct { weights []float64 // nil when uniform cum []float64 // cumulative weights, nil when uniform byKey map[string]int - parent *Table[P] - children map[string]*Table[P] + parent *Table[O] + children map[string]*Table[O] once sync.Once lookup rowLookup } -// Columns names the column each option reads, "" where the option is absent. -type Columns struct { +// Options names the column each option reads, "" where the option is absent. +type Options struct { Key, Name, Parent, Weight string } @@ -52,9 +51,9 @@ type rowLookup struct { } // 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} +// claim of them. +func Parse[O any](owner O, segment, path, file, data string, o Options) (*Table[O], error) { + t := &Table[O]{owner: owner, 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) } @@ -67,29 +66,25 @@ func Parse[P any](payload P, segment, path, file, data string, o Columns) (*Tabl 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 } +func (t *Table[O]) Owner() O { return t.owner } +func (t *Table[O]) Segment() string { return t.segment } +func (t *Table[O]) File() string { return t.file } +func (t *Table[O]) Options() Options { return t.options } +func (t *Table[O]) Header() []string { return t.header } +func (t *Table[O]) Parent() *Table[O] { 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) { +func (t *Table[O]) 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[O]) 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] } +func (t *Table[O]) 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 { +func (t *Table[O]) 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") @@ -130,7 +125,7 @@ func (t *Table[P]) parseRows(data string) error { } // appendRow splits one line into as many cells as the header has columns. -func (t *Table[P]) appendRow(row string, line int) error { +func (t *Table[O]) appendRow(row string, line int) error { if row == "" { return fmt.Errorf("line %d is empty; every line below the header is a row", line) } @@ -148,8 +143,11 @@ func (t *Table[P]) appendRow(row string, line int) error { // 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 { +func (t *Table[O]) bindOptions() error { o := t.options + if o.Key != "" && o.Key == o.Name { + return fmt.Errorf("name names the key column %q, which a selector already reads; drop it", o.Name) + } for _, opt := range []struct { name, value string into *int @@ -176,7 +174,7 @@ func (t *Table[P]) bindOptions() error { } // proveNamesInsideParent proves a name without a key names one row inside its parent. -func (t *Table[P]) proveNamesInsideParent() error { +func (t *Table[O]) proveNamesInsideParent() error { if t.keyIndex >= 0 || t.nameIndex < 0 { return nil } @@ -192,7 +190,7 @@ func (t *Table[P]) proveNamesInsideParent() error { } // proveKeys proves every key names one row, and keeps the map a link is proved by. -func (t *Table[P]) proveKeys() error { +func (t *Table[O]) proveKeys() error { if t.keyIndex < 0 { return nil } @@ -219,7 +217,7 @@ func (t *Table[P]) proveKeys() error { // sumWeights proves every weight is a positive number, and keeps the weights and their // running sum. -func (t *Table[P]) sumWeights() error { +func (t *Table[O]) sumWeights() error { if t.weightIndex < 0 { return nil } @@ -240,7 +238,7 @@ func (t *Table[P]) sumWeights() error { } // checkSelectorCells refuses a key or name cell a selector cannot spell. -func (t *Table[P]) checkSelectorCells() error { +func (t *Table[O]) checkSelectorCells() error { for r := 0; r < t.Len(); r++ { for _, col := range []int{t.keyIndex, t.nameIndex} { if col < 0 { @@ -258,7 +256,7 @@ func (t *Table[P]) checkSelectorCells() error { // 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 { +func (t *Table[O]) Link(p *Table[O], sibling func(name string) *Table[O]) error { name := t.header[t.parentIndex] if p.keyIndex < 0 { return fmt.Errorf("parent %q has no key column to link to", name) @@ -281,7 +279,7 @@ func (t *Table[P]) Link(p *Table[P], sibling func(name string) *Table[P]) error } t.parent = p if p.children == nil { - p.children = map[string]*Table[P]{} + p.children = map[string]*Table[O]{} } p.children[t.segment] = t return nil @@ -289,9 +287,9 @@ func (t *Table[P]) Link(p *Table[P], sibling func(name string) *Table[P]) error // 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]) { +func (t *Table[O]) checkAncestors(p *Table[O], sibling func(name string) *Table[O]) error { + var ancestors []*Table[O] + for q, seen := p, map[*Table[O]]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) } @@ -310,8 +308,8 @@ func (t *Table[P]) checkAncestors(p *Table[P], sibling func(name string) *Table[ } // Children is the tables linked to t, by segment. -func (t *Table[P]) Children() []*Table[P] { - out := make([]*Table[P], 0, len(t.children)) +func (t *Table[O]) Children() []*Table[O] { + out := make([]*Table[O], 0, len(t.children)) for _, c := range t.children { out = append(out, c) } @@ -320,7 +318,7 @@ func (t *Table[P]) Children() []*Table[P] { } // Descendant is the table named name among those linked to t, at any depth. -func (t *Table[P]) Descendant(name string) *Table[P] { +func (t *Table[O]) Descendant(name string) *Table[O] { if c, ok := t.children[name]; ok { return c } @@ -332,10 +330,10 @@ func (t *Table[P]) Descendant(name string) *Table[P] { return nil } -// StepUp proves a ".." after a row of t can step up: t has a parent, rest has no empty +// ProveStepUp 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 { +func (t *Table[O]) ProveStepUp(rest []string) error { if t.parent == nil { return fmt.Errorf(`%s has no parent table for ".." to step up to`, t.segment) } @@ -353,7 +351,7 @@ func (t *Table[P]) StepUp(rest []string) error { // 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 { +func (t *Table[O]) climbTo(rest []string) string { var b strings.Builder for a := t.parent; a != nil; a = a.parent { b.WriteString(".." + a.segment) @@ -364,7 +362,7 @@ func (t *Table[P]) climbTo(rest []string) string { return ".." + t.parent.segment } -func (t *Table[P]) builtLookup() *rowLookup { +func (t *Table[O]) builtLookup() *rowLookup { t.once.Do(func() { if t.nameIndex >= 0 { t.lookup.byName = make(map[string][]int, t.Len()) @@ -395,7 +393,7 @@ func (t *Table[P]) builtLookup() *rowLookup { return &t.lookup } -func (t *Table[P]) Draw(s *drawstate.State) int { +func (t *Table[O]) Draw(s *drawstate.State) int { if t.cum == nil { return s.IntN(t.Len()) } @@ -403,7 +401,7 @@ func (t *Table[P]) Draw(s *drawstate.State) int { } // drawUnder picks a row among those linked to parent row pr. -func (t *Table[P]) drawUnder(s *drawstate.State, pr int) int { +func (t *Table[O]) drawUnder(s *drawstate.State, pr int) int { lookup := t.builtLookup() k := t.parent.Cell(pr, t.parent.keyIndex) rows := lookup.rowsByParent[k] @@ -413,10 +411,10 @@ func (t *Table[P]) drawUnder(s *drawstate.State, pr int) int { 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 +// DrawIn is the row of t inside the rows p pins: 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 { +func (t *Table[O]) DrawIn(s *drawstate.State, p *Pins[O]) int { if r, ok := p.Pinned(t); ok { return r } @@ -439,10 +437,10 @@ func (t *Table[P]) DrawIn(s *drawstate.State, p *Pins[P]) int { } // 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)] } +func (t *Table[O]) 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 { +func (t *Table[O]) under(r int, a *Table[O], 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 @@ -453,7 +451,7 @@ func (t *Table[P]) under(r int, a *Table[P], 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. -func (t *Table[P]) find(sel string, pins *Pins[P]) (int, error) { +func (t *Table[O]) find(sel string, pins *Pins[O]) (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) } @@ -475,7 +473,7 @@ func (t *Table[P]) find(sel string, pins *Pins[P]) (int, error) { // 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 { +func (t *Table[O]) ambiguous(sel string, rows []int) error { spellings := make([]string, len(rows)) for i, r := range rows { if t.keyIndex < 0 { @@ -497,7 +495,7 @@ func (t *Table[P]) ambiguous(sel string, rows []int) error { // 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 { +func (t *Table[O]) selectorSpelling(r int) string { switch { case t.keyIndex >= 0: return t.path + "[" + t.Cell(r, t.keyIndex) + "]" diff --git a/internal/rows/rows_test.go b/internal/rows/rows_test.go new file mode 100644 index 0000000..2728616 --- /dev/null +++ b/internal/rows/rows_test.go @@ -0,0 +1,130 @@ +package rows + +import ( + "strings" + "testing" + + "github.com/larvit/fejkdata/internal/drawstate" +) + +func parse(t *testing.T, segment, data string, o Options) *Table[string] { + t.Helper() + tb, err := Parse(segment, segment, "g."+segment, segment+".tsv", data, o) + if err != nil { + t.Fatalf("Parse(%s): %v", segment, err) + } + return tb +} + +// family is a country table with two cities linked to it, each with a name only unique inside its country. +func family(t *testing.T) (country, city *Table[string]) { + t.Helper() + country = parse(t, "country", "code\tname\nSE\tSweden\nNO\tNorway\n", Options{Key: "code"}) + city = parse(t, "city", "country\tname\nSE\tBorg\nNO\tBorg\nSE\tLund\n", Options{Name: "name", Parent: "country"}) + if err := city.Link(country, func(string) *Table[string] { return nil }); err != nil { + t.Fatalf("Link: %v", err) + } + return country, city +} + +func TestParseRefuses(t *testing.T) { + for _, c := range []struct { + name, data string + o Options + want string + }{ + {"no header", "", Options{}, "no header line"}, + {"one row", "a\n1\n", Options{}, "has one row"}, + {"a short row", "a\tb\n1\t2\n3\n", Options{}, "line 3 has fewer cells"}, + {"a repeated key", "k\nx\nx\n", Options{Key: "k"}, `key "x" repeats line 2`}, + {"the key named as name", "k\nx\ny\n", Options{Key: "k", Name: "k"}, "name names the key column"}, + {"a weight of zero", "k\tw\nx\t1\ny\t0\n", Options{Weight: "w"}, `weight "0" is not a positive number`}, + {"a key a selector cannot spell", "k\nx\ny|z\n", Options{Key: "k"}, `contains "|"`}, + {"an option naming no column", "k\nx\ny\n", Options{Key: "nope"}, `key names no column "nope"`}, + } { + if _, err := Parse("o", "t", "g.t", "t.tsv", c.data, c.o); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("%s: Parse = %v, want %q", c.name, err, c.want) + } + } +} + +func TestLinkRefusesAParentRowNoChildLinksTo(t *testing.T) { + country := parse(t, "country", "code\nSE\nNO\n", Options{Key: "code"}) + city := parse(t, "city", "country\tname\nSE\tLund\nSE\tBorg\n", Options{Parent: "country"}) + if err := city.Link(country, func(string) *Table[string] { return nil }); err == nil || !strings.Contains(err.Error(), `country "NO"`) { + t.Errorf("Link = %v, want the unlinked row NO named", err) + } + if city.Parent() != nil { + t.Error("a refused link set the parent") + } +} + +func TestOwnerIsWhatParseWasGiven(t *testing.T) { + if _, city := family(t); city.Parent().Owner() != "country" { + t.Errorf("Owner = %q, want country", city.Parent().Owner()) + } +} + +func TestSelectResolvesANameInsideThePinnedParent(t *testing.T) { + country, city := family(t) + var p Pins[string] + if _, err := p.Select(city, "Borg"); err == nil || !strings.Contains(err.Error(), "names 2 rows") { + t.Fatalf("Select(Borg) with no country pinned = %v, want it ambiguous", err) + } + if _, err := p.Select(country, "NO"); err != nil { + t.Fatal(err) + } + r, err := p.Select(city, "Borg") + if err != nil || r != 1 { + t.Fatalf("Select(Borg) inside NO = %d, %v, want row 1", r, err) + } + if err := p.PinRow(city, 2); err == nil || !strings.Contains(err.Error(), "two rows of g.city") { + t.Errorf("PinRow(Lund) over Borg = %v, want a clash", err) + } +} + +func TestPinRowRefusesARowOutsideThePinnedParent(t *testing.T) { + country, city := family(t) + var p Pins[string] + if err := p.PinRow(country, 1); err != nil { + t.Fatal(err) + } + if err := p.PinRow(city, 2); err == nil || !strings.Contains(err.Error(), "is not inside g.country[NO]") { + t.Errorf("PinRow(Lund) inside NO = %v, want it refused", err) + } +} + +func TestDrawInDrawsInsideThePinnedParentAndPinsItsAncestors(t *testing.T) { + country, city := family(t) + s := drawstate.New(1) + for i := 0; i < 50; i++ { + var p Pins[string] + if err := p.PinRow(country, 0); err != nil { + t.Fatal(err) + } + if r := city.DrawIn(s, &p); city.Cell(r, 0) != "SE" { + t.Fatalf("DrawIn inside SE drew a city of %s", city.Cell(r, 0)) + } + var q Pins[string] + r := city.DrawIn(s, &q) + if pr := q.MustRow(country); country.Cell(pr, 0) != city.Cell(r, 0) { + t.Fatalf("DrawIn pinned country %s above a city of %s", country.Cell(pr, 0), city.Cell(r, 0)) + } + if above := q.Above(country); func() bool { _, ok := above.Pinned(city); return ok }() { + t.Fatal("Above(country) kept the city's pin") + } + } +} + +func TestProveStepUpNamesTheParent(t *testing.T) { + country, city := family(t) + if err := city.ProveStepUp([]string{"country", "name"}); err != nil { + t.Errorf("ProveStepUp(country.name) = %v", err) + } + if err := city.ProveStepUp([]string{"name"}); err == nil || !strings.Contains(err.Error(), "..country") { + t.Errorf("ProveStepUp(name) = %v, want ..country named", err) + } + if err := country.ProveStepUp([]string{"x"}); err == nil { + t.Error("ProveStepUp from a table with no parent passed") + } +} diff --git a/path.go b/path.go index fd9fa29..37509de 100644 --- a/path.go +++ b/path.go @@ -31,7 +31,7 @@ func stepInto(n node, seg string) (node, error) { return child, nil } case *tableColumn: - if n.i == n.t.rows.LinkColumn() { + if n.name() == n.t.rows.Options().Parent { 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.name()) @@ -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.rows.StepUp(tail[1:]); err != nil { + if err := t.rows.ProveStepUp(tail[1:]); err != nil { return tableRoute{}, err } - return tableRoute{sel: sel, draw: sel == "", next: t.rows.Parent().Payload(), rest: tail[2:], descends: true, up: true}, nil + return tableRoute{sel: sel, draw: sel == "", next: t.rows.Parent().Owner(), rest: tail[2:], descends: true, up: true}, nil } column, child, err := t.step(tail) if err != nil { @@ -128,42 +128,56 @@ 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; { - if c, ok := n.(*choice); ok { - return w.walkEvery(c, tail) + at := pathPos{n: n, tail: tail} + for at.more() { + if c, ok := at.n.(*choice); ok { + return w.walkEvery(c, at.tail) } var err error - if n, tail, descended, w.steps, err = takeStep(n, tail, descended, w.tail, w.level, w.steps, &w.pins); err != nil { + if w.steps, err = takeStep(&at, w.tail, w.level, w.steps, &w.pins); err != nil { return nil, err } } - w.leaves = append(w.leaves, n) - return n, nil + w.leaves = append(w.leaves, at.n) + return at.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) { +// pathPos is where a walk stands: the node reached, the tail left to walk from it, and whether +// the step into it came from a row of an ancestor table, so a table there reads a row even +// where the tail is empty. +type pathPos struct { + n node + tail []string + descended bool +} + +func (p pathPos) more() bool { return len(p.tail) > 0 || p.descended } + +// takeStep moves at past the step its tail starts with, from its node, which is no choice, and +// appends what it takes to steps: a field, or the route a table passes. whole is the path at's +// tail ends, and level names its head in errors. +func takeStep(at *pathPos, whole []string, level string, steps []pathStep, pins *pinSet) ([]pathStep, error) { + i := len(whole) - len(at.tail) + switch x := at.n.(type) { case *table: - r, err := x.route(tail, descended) + r, err := x.route(at.tail, at.descended) if err != nil { - return nil, nil, false, steps, err + return steps, err } - steps, err = routeSteps(steps, pins, x, r, at) - return r.next, r.rest, r.descends, steps, err + *at = pathPos{r.next, r.rest, r.descends} + return routeSteps(steps, pins, x, r, i) 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], "."))) + if x.repeat > 1 && !grammar.IsSelector(at.tail[0]) { + return steps, fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", join(level, strings.Join(whole[:i], "."))) } } - next, err := stepInto(n, tail[0]) + next, err := stepInto(at.n, at.tail[0]) if err != nil { - return nil, nil, false, steps, err + return steps, err } - return next, tail[1:], false, append(steps, pathStep{kind: stepField, at: at, name: tail[0]}), nil + steps = append(steps, pathStep{kind: stepField, at: i, name: at.tail[0]}) + *at = pathPos{n: next, tail: at.tail[1:]} + return steps, nil } // routeSteps appends the steps r takes past t, the first at at, pinning in pins the row @@ -225,17 +239,16 @@ func (w *pathCheck) walkEvery(c *choice, tail []string) (node, error) { // which carriedByAll lets stand for all. func probePath(n node, tail []string, steps []pathStep) ([]pathStep, error) { var pins pinSet - whole := tail - for descended := false; len(tail) > 0 || descended; { - if c, ok := n.(*choice); ok { - if err := carriedByAll(c, tail); err != nil { + for at := (pathPos{n: n, tail: tail}); at.more(); { + if c, ok := at.n.(*choice); ok { + if err := carriedByAll(c, at.tail); err != nil { return nil, err } - n = c.items[0] + at.n = c.items[0] continue } var err error - if n, tail, descended, steps, err = takeStep(n, tail, descended, whole, "", steps, &pins); err != nil { + if steps, err = takeStep(&at, tail, "", steps, &pins); err != nil { return nil, err } } @@ -290,9 +303,9 @@ func (t *table) drawStep(s *drawstate.State, st pathStep, pins *pinSet) node { case stepColumn: return t.formatTemplate.fields[st.name] case stepChild: - return t.rows.Descendant(st.name).Payload() + return t.rows.Descendant(st.name).Owner() case stepParent: - return t.rows.Parent().Payload() + return t.rows.Parent().Owner() } panic(invariant.Broken("drawStep has no case for step kind %d", st.kind)) } @@ -327,7 +340,7 @@ func (t *table) step(tail []string) (column node, child *table, err error) { if d == nil { return nil, nil, fmt.Errorf("no column or linked table %q in %s", tail[0], t.rows.Segment()) } - return nil, d.Payload(), nil + return nil, d.Owner(), nil } // carriedByAll is the choice rule a path that must resolve on every call obeys: diff --git a/table.go b/table.go index 16cdfec..f6c5004 100644 --- a/table.go +++ b/table.go @@ -15,12 +15,12 @@ type table struct { rows *rowsTable formatTemplate *template // fields are the column nodes rowNode *tableRow - cellTemplates map[int]*template + cellTemplates map[int]*template // by cellIndex } type ( - rowsTable = rows.Table[*table] pinSet = rows.Pins[*table] + rowsTable = rows.Table[*table] ) func (*table) isNode() {} @@ -43,9 +43,11 @@ func (*tableRow) isNode() {} // 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.rows.Header())+col] + return t.cellTemplates[t.cellIndex(row, col)] } +func (t *table) cellIndex(row, col int) int { return 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"} @@ -68,7 +70,7 @@ func compileTable(m map[string]any, dir []string, name string, readRows func(fil return nil, err } t := &table{} - if t.rows, err = rows.Parse(t, name, categoryPath(dir, name), o.rows, data, o.columns); err != nil { + if t.rows, err = rows.Parse(t, name, categoryPath(dir, name), o.rows, data, o.options); err != nil { return nil, err } if err := t.checkCells(); err != nil { @@ -82,7 +84,7 @@ func compileTable(m map[string]any, dir []string, name string, readRows func(fil type tableOptionValues struct { format, rows string - columns rows.Columns + options rows.Options } func readTableOptions(m map[string]any) (tableOptionValues, error) { @@ -92,7 +94,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.columns.Key, "name": &o.columns.Name, "parent": &o.columns.Parent, "rows": &o.rows, "weight": &o.columns.Weight} { + for k, into := range map[string]*string{"format": &o.format, "key": &o.options.Key, "name": &o.options.Name, "parent": &o.options.Parent, "rows": &o.rows, "weight": &o.options.Weight} { v, ok := m[k] if !ok { continue @@ -112,9 +114,6 @@ 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.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 } @@ -134,7 +133,7 @@ func (t *table) checkCells() error { if t.cellTemplates == nil { t.cellTemplates = map[int]*template{} } - t.cellTemplates[row*len(header)+col] = n + t.cellTemplates[t.cellIndex(row, col)] = n } } return nil @@ -207,7 +206,7 @@ func refuseTableBinding(toks []grammar.Token) error { func linkTables(sites []categorySite) error { for _, s := range sites { t, isTable := s.n.(*table) - if !isTable || t.rows.LinkColumn() < 0 { + if !isTable || t.rows.Options().Parent == "" { continue } if err := t.linkParent(s.in.children); err != nil { @@ -227,8 +226,8 @@ func (t *table) linkParent(siblings map[string]node) error { case !ok: return fmt.Errorf("parent %q is not a table; a link column reads a table's key", name) } - return t.rows.Link(p.rows, func(name string) *rowsTable { - if q, ok := siblings[name].(*table); ok { + return t.rows.Link(p.rows, func(sibling string) *rowsTable { + if q, ok := siblings[sibling].(*table); ok { return q.rows } return nil diff --git a/todo.md b/todo.md index e654c40..d14c233 100644 --- a/todo.md +++ b/todo.md @@ -416,11 +416,11 @@ An `int64` or `float64` field unproven in range is told to become itself, and a ### 51. Spell a table one way across the errors that name it. -`pinRow` names `t.path` and `mustRow` panics with `t.category`, so one table is `territory` and `misc.territory`, and the short spelling names no file where two folders hold that name. +In `internal/rows`, `Pins.PinRow` names `t.path` and `Pins.MustRow` panics with `t.segment`, so one table is `territory` and `misc.territory`, and the short spelling names no file where two folders hold that name. ### 115. Spell a rows file's line one way in every error, naming the file. -Today a cell reads `g.t.b, line 3` (`namedNode.labelIn`), a cell check `t.tsv: line 3, b` (`checkCells`) and a link `t.tsv line 3` (`proveParent`). A reference cycle found from a table's format through a cell names no line at all: `reference cycle: g.t -> format -> b -> b -> /h -> …`, where the column-to-cell step in `renderEdges` repeats the column name. +Today a cell reads `g.t.b, line 3` (`namedNode.labelIn`), a cell check `t.tsv: line 3, b` (`checkCells`) and a link `t.tsv line 3` (`rows.Table.Link`). A reference cycle found from a table's format through a cell names no line at all: `reference cycle: g.t -> format -> b -> b -> /h -> …`, where the column-to-cell step in `renderEdges` repeats the column name. ### 10. Test that every node kind reaches each switch over node kinds.