diff --git a/README.md b/README.md index 16f3129..0fab5ea 100644 --- a/README.md +++ b/README.md @@ -444,9 +444,10 @@ table another path in the group selects a row of, `{/city.name}` beside `{/country[SE].name}`, which the first token rendered would otherwise decide. `New` also refuses a link cell that is no key of the parent, a parent row no child links to, a chain of parents that closes, a table named like a column of any table above -it, and a cell or format of a table that references a table of its own family, since -a row rendered whole would draw the family apart from itself: read the family from a -template beside it, or add the value as a column. +it, and a cell or format of a table that references a table of its own family, through +any template, a `repeat` or a `drawGroup` included, since a row rendered whole would +draw the family apart from itself: read the family from a template beside it, or add +the value as a column. ### Options and fields @@ -969,6 +970,16 @@ renamed or retyped line is a major. they sit in two rows of one table, or in a row outside a selected ancestor's. A choice's items get no such treatment yet: two items selecting different rows are still refused. +- **A table never reaches its own family, by any route.** A `repeat` iteration and a + `drawGroup` each draw apart on purpose, but a row that lists three localities from + other regions is the output the family exists to prevent, so the own-family fence + walks through both rather than stopping where the draw fences do. +- **Tables carrying token cells stay small.** The family fence compares the reads of + every pair of rows that can render together, so a table whose every row's cell + selects a row of another table loads in time quadratic in its rows: about a + second at four thousand rows. No shipped table carries such cells, and a register + is a column set rather than a set of references, so the fence is left as it is + until a real data set needs the indexed form. - **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. diff --git a/draw.go b/draw.go index 0b45d39..b9cdd7d 100644 --- a/draw.go +++ b/draw.go @@ -137,16 +137,19 @@ func (c *drawCheck) checkDrawGroup(path string, n node) error { func (c *drawCheck) checkDraws(path string, n node) error { t, ok := n.(*template) - if !ok || !c.readsPath(t) { + if !ok { + return nil + } + if err := checkOwnFamily(t); err != nil { + return fmt.Errorf("%s: %w", path, err) + } + if !c.readsPath(t) { return nil } w := newDrawWalk() for _, e := range renderEdges(t) { w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}}) } - if err := w.checkOwnFamily(t); err != nil { - return fmt.Errorf("%s: %w", path, err) - } if err := w.check(); err != nil { return fmt.Errorf("%s: %w", path, err) } @@ -154,9 +157,10 @@ func (c *drawCheck) checkDraws(path string, n node) error { } // checkOwnFamily refuses a table's format or cell that reads, however many templates -// away, a table of its own family: a row rendered whole draws its row without -// pinning it, so the family would draw apart from the row being rendered. -func (w *drawWalk) checkOwnFamily(t *template) error { +// away and through a repeat or a draw group too, a table of its own family: a row +// rendered whole draws its row without pinning it, so the family would draw apart +// from the row being rendered, whichever draws the reaching template holds. +func checkOwnFamily(t *template) error { own := t.table if own == nil { own = t.cellOf @@ -164,10 +168,27 @@ func (w *drawWalk) checkOwnFamily(t *template) error { if own == nil { return nil } - for _, r := range w.reads { - if r.tr != nil && r.tr.head.family() == own.family() { - return fmt.Errorf("%s reads %s, a table of its own family, which a row of %s rendered whole would draw apart from; read the family from a template beside it, or add the value as a column", r.at.route.spelled(r.a.name), r.tr.head.category, own.category) + seen := map[node]bool{} + var find func(n node) (renderEdge, *table, bool) + find = func(n node) (renderEdge, *table, bool) { + if seen[n] { + return renderEdge{}, nil, false } + seen[n] = true + for _, e := range renderEdges(n) { + if a, isRef := refRead(n, e.label); isRef { + if head, isTable := n.(*template).fields[a.key].(*table); isTable && head.family() == own.family() { + return e, head, true + } + } + if e, head, found := find(e.to); found { + return e, head, true + } + } + return renderEdge{}, nil, false + } + if e, head, found := find(t); found { + return fmt.Errorf("%s reads %s, a table of its own family, which a row of %s rendered whole would draw apart from; read the family from a template beside it, or add the value as a column", e.reached(), head.category, own.category) } return nil }