Treat a column's cells as alternatives in the family fence, probe a path before drawing, and record that two categories may name one TSV
Tests / vet + fmt + tests (pull_request) Successful in 1m6s
Tests / Gitea release from CHANGELOG.md (pull_request) Has been skipped

This commit is contained in:
2026-09-17 15:39:07 +02:00
parent 1ab3bbce30
commit d8c348d07b
5 changed files with 65 additions and 12 deletions
+13 -1
View File
@@ -378,7 +378,9 @@ numbers that skews the draw, and `parent` the table a column links to
columns are the [record](#records)'s columns, so `--format csv` writes the rows and columns are the [record](#records)'s columns, so `--format csv` writes the rows and
`--list` shows `country.alpha2`. A cell is a string node: `1{digits(2)} {digits(2)}` `--list` shows `country.alpha2`. A cell is a string node: `1{digits(2)} {digits(2)}`
in a cell draws digits and `{/misc.uuid}` reads a reference, while `{name}` in a cell in a cell draws digits and `{/misc.uuid}` reads a reference, while `{name}` in a cell
is refused, since a cell has no sibling. `New` proves the header, the options and every is refused, since a cell has no sibling. Each cell may select its own row of another
table, `{/misc.currency[SEK].symbol}` on one row and `{/misc.currency[EUR].symbol}`
on the next, since only one row renders. `New` proves the header, the options and every
cell token, and refuses a TSV no category names, a key that is empty or repeats, a cell token, and refuses a TSV no category names, a key that is empty or repeats, a
weight that is not a positive number, and a key or name holding `[`, `]`, `{`, `}`, weight that is not a positive number, and a key or name holding `[`, `]`, `{`, `}`,
`"` or `|`, which a selector cannot spell; the rows are indexed on the first draw that `"` or `|`, which a selector cannot spell; the rows are indexed on the first draw that
@@ -956,6 +958,16 @@ renamed or retyped line is a major.
against the parent's keys and a key's uniqueness is a data mistake, so both are against the parent's keys and a key's uniqueness is a data mistake, so both are
load-time; the name index and the per-parent child lists serve only a draw or a load-time; the name index and the per-parent child lists serve only a draw or a
selection, so they wait for the first one, keeping `New` linear in the bytes read. selection, so they wait for the first one, keeping `New` linear in the bytes read.
- **Two categories may name one TSV.** Each is a view of the file with its own
format and options, at the cost of holding the rows twice, which is what a
category over a register with two natural formats asks for; a TSV nothing names
stays a load error, since that one is a file forgotten rather than shared.
- **The cells of one column are alternatives.** Only one row renders, so two cells
selecting different rows of another table never meet, as two items of a choice
never do; the family fence replays the reads of each cell apart, together with
the reads outside any cell. A path is walked once without drawing before it is
walked for real, so a path that fails below its first level moves no seeded
stream.
- **`List` advertises direct descents only.** `region.municipality.locality` is - **`List` advertises direct descents only.** `region.municipality.locality` is
listed, and `region.locality` resolves too but is not: the set of every descent listed, and `region.locality` resolves too but is not: the set of every descent
through a chain of five tables is every subsequence of it, and the direct chain is through a chain of five tables is every subsequence of it, and the direct chain is
+14 -2
View File
@@ -270,10 +270,18 @@ type drawWalk struct {
seen map[drawVisit]bool seen map[drawVisit]bool
} }
// drawAt is where a walk stands: the draw group it draws in, and how the render's root reached it. // drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, and
// the table cell it entered, if any: the cells of one column are alternatives, as a choice's items
// are, so reads in two of them never meet.
type drawAt struct { type drawAt struct {
group string group string
route drawRoute route drawRoute
cell node
}
// alternatives reports whether two reads sit in different cells of one column.
func alternatives(a, b drawAt) bool {
return a.cell != nil && b.cell != nil && a.cell != b.cell
} }
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label. // drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
@@ -314,7 +322,11 @@ func (w *drawWalk) walk(n node, at drawAt) {
if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" { if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" {
at.group = t.drawGroupKey at.group = t.drawGroupKey
} }
_, isColumn := n.(*column)
for _, e := range renderEdges(n) { for _, e := range renderEdges(n) {
if isColumn {
at.cell = e.to
}
w.edge(n, e, at) w.edge(n, e, at)
} }
} }
@@ -341,7 +353,7 @@ func (w *drawWalk) check() error {
}) })
for i, level := range w.reads { for i, level := range w.reads {
for _, into := range w.reads[i+1:] { for _, into := range w.reads[i+1:] {
if into.at.group != level.at.group { if into.at.group != level.at.group || alternatives(level.at, into.at) {
continue continue
} }
if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) { if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) {
+28 -7
View File
@@ -227,28 +227,49 @@ func (r *tableRead) selected(t *table) (tableSel, bool) {
// a table one read draws that another pins, and two reads pinning different rows. // a table one read draws that another pins, and two reads pinning different rows.
// The reads come sorted by group and path, so which pair is reported does not vary. // The reads come sorted by group and path, so which pair is reported does not vary.
func checkFamilies(reads []pathRead) error { func checkFamilies(reads []pathRead) error {
shared := map[string]*draws{}
for i, r := range reads { for i, r := range reads {
if r.tr == nil { if r.tr == nil {
continue continue
} }
for _, o := range reads[:i] { for _, o := range reads[:i] {
if o.tr == nil || o.at.group != r.at.group || o.tr.head.family() != r.tr.head.family() { if o.tr == nil || o.at.group != r.at.group || alternatives(o.at, r.at) || o.tr.head.family() != r.tr.head.family() {
continue continue
} }
if err := checkFamilyPair(o, r); err != nil { if err := checkFamilyPair(o, r); err != nil {
return err return err
} }
} }
d := shared[r.at.group]
if d == nil {
d = &draws{}
shared[r.at.group] = d
} }
if err := r.tr.replay(d); err != nil { return replayAlternatives(reads)
}
// replayAlternatives replays the reads of each draw group into one draws per cell
// alternative — the reads outside any cell, then the cell's own — so two cells of
// one column are never replayed together.
func replayAlternatives(reads []pathRead) error {
type alt struct {
group string
cell node
}
var alts []alt
seen := map[alt]bool{}
for _, r := range reads {
if a := (alt{r.at.group, r.at.cell}); r.tr != nil && !seen[a] {
seen[a] = true
alts = append(alts, a)
}
}
for _, a := range alts {
var d draws
for _, r := range reads {
if r.tr == nil || r.at.group != a.group || r.at.cell != nil && r.at.cell != a.cell {
continue
}
if err := r.tr.replay(&d); err != nil {
return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.at.route.spelled(r.a.name), err) return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.at.route.spelled(r.a.name), err)
} }
} }
}
return nil return nil
} }
+4 -1
View File
@@ -189,12 +189,15 @@ func (w pathWalk) atLeaf(n node) error {
// by the pins' session where the walk names no choice action. // by the pins' session where the walk names no choice action.
func walkChoice(c *choice, tail []string, w pathWalk) (node, error) { func walkChoice(c *choice, tail []string, w pathWalk) (node, error) {
if w.choice == nil { if w.choice == nil {
if w.pins == nil || w.pins.s == nil { if w.pins == nil {
return nil, nil return nil, nil
} }
if err := carriedByAll(c, tail); err != nil { if err := carriedByAll(c, tail); err != nil {
return nil, err return nil, err
} }
if w.pins.s == nil { // a probe: every variant carries the tail, so any one proves it
return walkPath(c.items[0], tail, w)
}
return walkPath(pick(w.pins.s, c), tail, w) return walkPath(pick(w.pins.s, c), tail, w)
} }
next, err := w.choice(c, tail) next, err := w.choice(c, tail)
+5
View File
@@ -43,6 +43,11 @@ func (f *Generator) Fake(path string) (string, error) {
// carries before a variant is picked — a path that resolves at all resolves on // carries before a variant is picked — a path that resolves at all resolves on
// every call. // every call.
func descend(s *session, root node, segments []string, sc drawScope) (node, error) { func descend(s *session, root node, segments []string, sc drawScope) (node, error) {
// Walked once without drawing first, so a path that fails moves no seeded stream.
var probe draws
if _, err := walkPath(root, segments, pathWalk{pins: &probe}); err != nil {
return nil, err
}
return walkPath(root, segments, pathWalk{pins: sc.draws(s)}) return walkPath(root, segments, pathWalk{pins: sc.draws(s)})
} }