Identify a fence alternative by the table row rather than the cell, keep the outermost row through a reached table, and key reads and visits by it
Tests / vet + fmt + tests (pull_request) Successful in 1m8s
Tests / Gitea release from CHANGELOG.md (pull_request) Has been skipped

This commit is contained in:
2026-09-17 15:53:44 +02:00
parent 66cb5937cc
commit fb0480e741
5 changed files with 62 additions and 28 deletions
+9 -6
View File
@@ -962,12 +962,15 @@ renamed or retyped line is a major.
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.
- **The rows of a table are alternatives.** Only one row renders, so a cell in one
row and a cell in another never meet, and each may select its own row of another
table; the cells of one row, and whatever they reach, do meet, and so does the
format beside them. The family fence replays the reads of each row apart,
together with the reads outside any row. A choice's items get no such treatment
yet: two items selecting different rows are still refused.
- **A path is walked once without drawing before it is walked for real.** A path
that fails below its first level then moves no seeded stream, at the cost of one
draw-free walk per call, which allocates nothing.
- **`List` advertises direct descents only.** `region.municipality.locality` is
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
+20 -10
View File
@@ -271,17 +271,23 @@ type drawWalk struct {
}
// 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.
// the table row it entered, if any.
type drawAt struct {
group string
route drawRoute
cell node
alt rowAlt
}
// alternatives reports whether two reads sit in different cells of one column.
// rowAlt is one row of a table as an alternative: only one row renders, so reads in two rows of
// one table never meet, while reads in one row, across its columns and whatever they reach, do.
type rowAlt struct {
t *table
row int
}
// alternatives reports whether two reads sit in different rows of one table.
func alternatives(a, b drawAt) bool {
return a.cell != nil && b.cell != nil && a.cell != b.cell
return a.alt.t != nil && a.alt.t == b.alt.t && a.alt.row != b.alt.row
}
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
@@ -297,11 +303,13 @@ type pathRead struct {
type drawVisit struct {
n node
group string
alt rowAlt
}
type drawKey struct {
group string
path string
alt rowAlt
}
func newDrawWalk() *drawWalk {
@@ -311,7 +319,7 @@ func newDrawWalk() *drawWalk {
// walk follows what rendering n renders. A repeat renders over draws of its own, so the walk stops
// there.
func (w *drawWalk) walk(n node, at drawAt) {
v := drawVisit{n, at.group}
v := drawVisit{n, at.group, at.alt}
if w.seen[v] {
return
}
@@ -324,16 +332,18 @@ func (w *drawWalk) walk(n node, at drawAt) {
}
_, isColumn := n.(*column)
for _, e := range renderEdges(n) {
if isColumn {
at.cell = e.to
// The outermost row is kept: a cell reached through another row's cell renders with it.
edgeAt := at
if cell, _ := e.to.(*template); isColumn && at.alt.t == nil {
edgeAt.alt = rowAlt{cell.cellOf, cell.cellRow}
}
w.edge(n, e, at)
w.edge(n, e, edgeAt)
}
}
func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
if a, reads := refRead(from, e.label); reads {
if k := (drawKey{at.group, a.path}); !w.read[k] {
if k := (drawKey{at.group, a.path, at.alt}); !w.read[k] {
w.read[k] = true
w.reads = append(w.reads, pathRead{at, a, tableReadOf(from.(*template).fields[a.key], a, e.to)})
}
+31 -11
View File
@@ -243,28 +243,39 @@ func checkFamilies(reads []pathRead) error {
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.
// replayAlternatives replays the reads of each draw group into one draws per row
// alternative — the reads outside any row, then the row's own — so two rows of one
// table are never replayed together.
func replayAlternatives(reads []pathRead) error {
type alt struct {
group string
cell node
row rowAlt
}
var alts []alt
seen := map[alt]bool{}
outside, inside := map[string][]pathRead{}, map[alt][]pathRead{}
for _, r := range reads {
if a := (alt{r.at.group, r.at.cell}); r.tr != nil && !seen[a] {
seen[a] = true
if r.tr == nil {
continue
}
if r.at.alt.t == nil {
outside[r.at.group] = append(outside[r.at.group], r)
continue
}
a := alt{r.at.group, r.at.alt}
if inside[a] == nil {
alts = append(alts, a)
}
inside[a] = append(inside[a], r)
}
for group := range outside {
alts = append(alts, alt{group, rowAlt{}})
}
sort.Slice(alts, func(i, j int) bool {
return altOrder(alts[i].group, alts[i].row) < altOrder(alts[j].group, alts[j].row)
})
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
}
for _, r := range append(append([]pathRead(nil), outside[a.group]...), inside[a]...) {
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)
}
@@ -273,6 +284,15 @@ func replayAlternatives(reads []pathRead) error {
return nil
}
// altOrder sorts alternatives by group, the reads outside any row first, then by table and row,
// so which conflict is reported does not vary.
func altOrder(group string, r rowAlt) string {
if r.t == nil {
return group + "\x00"
}
return fmt.Sprintf("%s\x01%s\x00%08d", group, r.t.category, r.row)
}
// replay pins the read's rows into d, where they agree with the rows pinned before.
func (r *tableRead) replay(d *draws) error {
var err error
+1
View File
@@ -66,6 +66,7 @@ type template struct {
record bool // compiled at the top without a repeat, so its fields are record columns
table *table // the table whose format this is, whose columns are the fields
cellOf *table // the table whose cell this is
cellRow int // the row the cell sits in
drawGroup string // the draw group it draws in, as written; "" keeps its caller's
drawGroupKey string // its draw group keyed by its category once linked: what a render reads its reference paths under
}
+1 -1
View File
@@ -296,7 +296,7 @@ func (t *table) checkCells() error {
t.tokens = map[int]*template{}
}
t.tokens[i] = n.(*template)
t.tokens[i].cellOf = t
t.tokens[i].cellOf, t.tokens[i].cellRow = t, row
}
return nil
}