diff --git a/README.md b/README.md index 5dee75d..f021e85 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/draw.go b/draw.go index 747eda5..c401d69 100644 --- a/draw.go +++ b/draw.go @@ -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)}) } diff --git a/family.go b/family.go index 34457ae..5af98f4 100644 --- a/family.go +++ b/family.go @@ -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 diff --git a/node.go b/node.go index bb2c65b..a89670d 100644 --- a/node.go +++ b/node.go @@ -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 } diff --git a/table.go b/table.go index f6de4d7..d101c21 100644 --- a/table.go +++ b/table.go @@ -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 }