diff --git a/README.md b/README.md index ec29b58..5dee75d 100644 --- a/README.md +++ b/README.md @@ -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 `--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 -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 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 @@ -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 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. +- **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 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 dc6292d..747eda5 100644 --- a/draw.go +++ b/draw.go @@ -270,10 +270,18 @@ type drawWalk struct { 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 { group string 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. @@ -314,7 +322,11 @@ func (w *drawWalk) walk(n node, at drawAt) { if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" { at.group = t.drawGroupKey } + _, isColumn := n.(*column) for _, e := range renderEdges(n) { + if isColumn { + at.cell = e.to + } w.edge(n, e, at) } } @@ -341,7 +353,7 @@ func (w *drawWalk) check() error { }) for i, level := range w.reads { 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 } if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) { diff --git a/family.go b/family.go index 6835f8a..34457ae 100644 --- a/family.go +++ b/family.go @@ -227,26 +227,47 @@ func (r *tableRead) selected(t *table) (tableSel, bool) { // 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. func checkFamilies(reads []pathRead) error { - shared := map[string]*draws{} for i, r := range reads { if r.tr == nil { continue } 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 } if err := checkFamilyPair(o, r); err != nil { return err } } - d := shared[r.at.group] - if d == nil { - d = &draws{} - shared[r.at.group] = d + } + 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) } - 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) + } + 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 nil diff --git a/path.go b/path.go index 683a4ab..5d85b8b 100644 --- a/path.go +++ b/path.go @@ -189,12 +189,15 @@ func (w pathWalk) atLeaf(n node) error { // by the pins' session where the walk names no choice action. func walkChoice(c *choice, tail []string, w pathWalk) (node, error) { if w.choice == nil { - if w.pins == nil || w.pins.s == nil { + if w.pins == nil { return nil, nil } if err := carriedByAll(c, tail); err != nil { 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) } next, err := w.choice(c, tail) diff --git a/render.go b/render.go index bda6ea5..97b84a0 100644 --- a/render.go +++ b/render.go @@ -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 // every call. 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)}) }