diff --git a/README.md b/README.md index d39b9aa..e9e5301 100644 --- a/README.md +++ b/README.md @@ -879,8 +879,7 @@ format.go the {token} grammar: scanning, tokens, operands, validation, com hold.go the hold: one draw per held name, and reading a field through it holdfence.go the load fences over the hold: the routes and spellings that read one held name two ways holdset.go one reference draw per render and group: the hold set a render reads its paths through -drawfence.go the load fences over reference draws: a drawGroup's, a render's and a record's columns, and the walk over what a render reads -family.go a family of linked tables: what a reference path reads of one, and the fences over paths into it +drawfence.go the load fences over reference draws: a drawGroup's, a render's, a record's columns and a table family's, and the walk over what a render reads pins.go the pin set: the table rows a render fixes, which the draw fences replay reference.go reference sigils, and binding references across the tree graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences diff --git a/drawfence.go b/drawfence.go index f8c5dd7..f54da29 100644 --- a/drawfence.go +++ b/drawfence.go @@ -309,6 +309,90 @@ func (w *drawWalk) check() error { return checkFamilies(w.reads) } +// checkFamilies refuses reads of one table family in one draw group that cannot +// read one consistent draw: a table rendered whole beside a path into the family, +// 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 { + 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 || alternatives(o.at, r.at) || o.tr.headTable.family() != r.tr.headTable.family() { + continue + } + if err := checkFamilyPair(o, r); err != nil { + return err + } + } + } + return replayPairs(reads) +} + +// checkFamilyPair refuses a table read whole beside a path into its family, and a +// table one read draws that the other pins, since which token renders first would +// then decide the row. +// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held +func checkFamilyPair(a, b pathRead) error { + for _, pair := range [][2]pathRead{{a, b}, {b, a}} { + x, y := pair[0], pair[1] + if len(x.a.tail) == 0 && len(y.a.tail) > 0 { + return overlapError(x.at.route, x.a.spelling, y) + } + if drawn := x.tr.drawnOf(&y.tr.pins); drawn != nil { + if s, ok := y.tr.selected(x.tr.headTable); ok && len(x.tr.sels) == 0 { + tail := x.a.tail + if s.t != x.tr.headTable { + tail = append([]string{x.tr.headTable.category}, tail...) + } + return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup", + x.at.route.spelled(x.a.spelling), drawn.category, y.at.route.spelled(y.a.spelling), s.spelling, joinSegments(tail)) + } + return fmt.Errorf("%s draws %s, which %s selects a row of; select that row in both, or draw them apart with a drawGroup", + x.at.route.spelled(x.a.spelling), drawn.category, y.at.route.spelled(y.a.spelling)) + } + } + return nil +} + +// replayPairs replays every two reads that can render together into one draws, the earlier +// read first. Pairs find every conflict a full replay would: clash judges a row against one +// pinned table, and the read that pinned it holds that pin itself, since a read's pins carry +// its rows' ancestors, so the pair of those two reads clashes the same way. +func replayPairs(reads []pathRead) error { + for i, r := range reads { + if r.tr == nil { + continue + } + for _, o := range reads[i+1:] { + if o.tr == nil || o.at.group != r.at.group || alternatives(r.at, o.at) { + continue + } + var d pinSet + if err := r.tr.replay(&d); err != nil { + return conflict(r, err) + } + if err := o.tr.replay(&d); err != nil { + return conflict(o, err) + } + } + } + return nil +} + +func conflict(r pathRead, err error) error { + 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.spelling), err) +} + +// alternatives reports whether two reads never render together: one read renders one row of a +// table, so reads under different rows the walks pinned, or under different rows of one whole +// draw, never meet. +// docs/decisions.md#the-rows-of-a-table-are-alternatives +func alternatives(a, b drawAt) bool { + return a.pins.differs(&b.pins) || a.wholePins.differs(&b.wholePins) +} + func overlapError(route drawRoute, ref string, into pathRead) error { return fmt.Errorf("%s renders a level that %s reads a path into; name the fields you want instead, or draw them apart with a drawGroup", route.spelled(ref), into.at.route.spelled(into.a.spelling)) } @@ -321,3 +405,119 @@ func (r drawRoute) spelled(ref string) string { } return fmt.Sprintf("%s with {%s}", r.spelling, ref) } + +// tableRead is what a reference path reads of a table family: the table its head +// names, the rows its selectors pin, the tables it draws — those it walks with no +// row pinned, and their unpinned ancestors — each selector's spelling, and whether +// it lands on a `tableRow`. +type tableRead struct { + headTable *table + pins pinSet + drawn map[*table]bool + sels []tableSel + landsRow bool +} + +// tableSel is one selector on the way: the table it selects a row of, and the path +// as written up to and including it. +type tableSel struct { + t *table + spelling string +} + +// tableReadOf replays a reference path through pathProbe, so two paths pinning one +// row by different routes compare equal. checkPath proved +// each selector names a row. +func tableReadOf(head node, a arm, leaf node) *tableRead { + t, isTable := head.(*table) + if !isTable { + return nil + } + probe := &pathProbe{drawn: map[*table]bool{}} + _, _ = probe.walk(t, a.tail) + tr := &tableRead{headTable: t, pins: probe.pins, drawn: probe.drawn} + written := a.spelling[:len(a.spelling)-len(joinSegments(a.tail))] + cur := t + for i, seg := range a.tail { + switch d := cur.descendant(seg); { + case isSelector(seg): + tr.sels = append(tr.sels, tableSel{cur, written + joinSegments(a.tail[:i+1])}) + case d != nil: + cur = d + } + } + _, tr.landsRow = leaf.(*tableRow) + return tr +} + +// selected is the selector in r on t, or on the nearest ancestor of t it selects. +func (r *tableRead) selected(t *table) (tableSel, bool) { + for ; t != nil; t = t.parentT { + for _, s := range r.sels { + if s.t == t { + return s, true + } + } + } + return tableSel{}, false +} + +// drawnOf is a table the read draws that pins holds a row of, if any. +func (r *tableRead) drawnOf(pins *pinSet) *table { + var found *table + pins.each(func(t *table, _ int) { + if found == nil && r.drawn[t] { + found = t + } + }) + return found +} + +// replay pins the read's rows into d, where they agree with the rows pinned before. +func (r *tableRead) replay(d *pinSet) error { + var err error + r.pins.each(func(t *table, row int) { + if err == nil { + err = d.pinRow(t, row) + } + }) + return err +} + +// checkOwnFamily refuses a table's format or cell that reads, however many templates +// away and through a repeat or a draw group too, a table of its own family: a whole +// read draws its row without pinning it, so the family would draw apart from the row +// it renders, whichever draws the reaching template holds. +// docs/decisions.md#a-table-never-reaches-its-own-family-by-any-route +func checkOwnFamily(t *template) error { + own := t.table + if own == nil { + own = t.cellOf + } + if own == nil { + return nil + } + 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).head(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 bare read of %s would draw apart from the row it renders; read the family from a template beside it, or add the value as a column", e.reached(), head.category, own.category) + } + return nil +} diff --git a/family.go b/family.go deleted file mode 100644 index f941f8a..0000000 --- a/family.go +++ /dev/null @@ -1,226 +0,0 @@ -package fejkdata - -import ( - "fmt" - "strings" -) - -// tableRead is what a reference path reads of a table family: the table its head -// names, the rows its selectors pin, the tables it draws — those it walks with no -// row pinned, and their unpinned ancestors — each selector's spelling, and whether -// it lands on a `tableRow`. -type tableRead struct { - headTable *table - pins pinSet - drawn map[*table]bool - sels []tableSel - landsRow bool -} - -// tableSel is one selector on the way: the table it selects a row of, and the path -// as written up to and including it. -type tableSel struct { - t *table - spelling string -} - -// tableReadOf replays a reference path through pathProbe, so two paths pinning one -// row by different routes compare equal. checkPath proved -// each selector names a row. -func tableReadOf(head node, a arm, leaf node) *tableRead { - t, isTable := head.(*table) - if !isTable { - return nil - } - probe := &pathProbe{drawn: map[*table]bool{}} - _, _ = probe.walk(t, a.tail) - tr := &tableRead{headTable: t, pins: probe.pins, drawn: probe.drawn} - written := a.spelling[:len(a.spelling)-len(joinSegments(a.tail))] - cur := t - for i, seg := range a.tail { - switch d := cur.descendant(seg); { - case isSelector(seg): - tr.sels = append(tr.sels, tableSel{cur, written + joinSegments(a.tail[:i+1])}) - case d != nil: - cur = d - } - } - _, tr.landsRow = leaf.(*tableRow) - return tr -} - -// joinSegments spells segments as a path: a selector attaches to the name before it. -func joinSegments(segs []string) string { - var b strings.Builder - for _, s := range segs { - if b.Len() > 0 && !isSelector(s) { - b.WriteByte('.') - } - b.WriteString(s) - } - return b.String() -} - -// family is the table a chain of parents ends at. -func (t *table) family() *table { - for t.parentT != nil { - t = t.parentT - } - return t -} - -// selected is the selector in r on t, or on the nearest ancestor of t it selects. -func (r *tableRead) selected(t *table) (tableSel, bool) { - for ; t != nil; t = t.parentT { - for _, s := range r.sels { - if s.t == t { - return s, true - } - } - } - return tableSel{}, false -} - -// checkFamilies refuses reads of one table family in one draw group that cannot -// read one consistent draw: a table rendered whole beside a path into the family, -// 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 { - 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 || alternatives(o.at, r.at) || o.tr.headTable.family() != r.tr.headTable.family() { - continue - } - if err := checkFamilyPair(o, r); err != nil { - return err - } - } - } - return replayPairs(reads) -} - -// replayPairs replays every two reads that can render together into one draws, the earlier -// read first. Pairs find every conflict a full replay would: clash judges a row against one -// pinned table, and the read that pinned it holds that pin itself, since a read's pins carry -// its rows' ancestors, so the pair of those two reads clashes the same way. -func replayPairs(reads []pathRead) error { - for i, r := range reads { - if r.tr == nil { - continue - } - for _, o := range reads[i+1:] { - if o.tr == nil || o.at.group != r.at.group || alternatives(r.at, o.at) { - continue - } - var d pinSet - if err := r.tr.replay(&d); err != nil { - return conflict(r, err) - } - if err := o.tr.replay(&d); err != nil { - return conflict(o, err) - } - } - } - return nil -} - -func conflict(r pathRead, err error) error { - 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.spelling), err) -} - -// replay pins the read's rows into d, where they agree with the rows pinned before. -func (r *tableRead) replay(d *pinSet) error { - var err error - r.pins.each(func(t *table, row int) { - if err == nil { - err = d.pinRow(t, row) - } - }) - return err -} - -// checkFamilyPair refuses a table read whole beside a path into its family, and a -// table one read draws that the other pins, since which token renders first would -// then decide the row. -// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held -func checkFamilyPair(a, b pathRead) error { - for _, pair := range [][2]pathRead{{a, b}, {b, a}} { - x, y := pair[0], pair[1] - if len(x.a.tail) == 0 && len(y.a.tail) > 0 { - return overlapError(x.at.route, x.a.spelling, y) - } - if drawn := x.tr.drawnOf(&y.tr.pins); drawn != nil { - if s, ok := y.tr.selected(x.tr.headTable); ok && len(x.tr.sels) == 0 { - tail := x.a.tail - if s.t != x.tr.headTable { - tail = append([]string{x.tr.headTable.category}, tail...) - } - return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup", - x.at.route.spelled(x.a.spelling), drawn.category, y.at.route.spelled(y.a.spelling), s.spelling, joinSegments(tail)) - } - return fmt.Errorf("%s draws %s, which %s selects a row of; select that row in both, or draw them apart with a drawGroup", - x.at.route.spelled(x.a.spelling), drawn.category, y.at.route.spelled(y.a.spelling)) - } - } - return nil -} - -// drawnOf is a table the read draws that pins holds a row of, if any. -func (r *tableRead) drawnOf(pins *pinSet) *table { - var found *table - pins.each(func(t *table, _ int) { - if found == nil && r.drawn[t] { - found = t - } - }) - return found -} - -// checkOwnFamily refuses a table's format or cell that reads, however many templates -// away and through a repeat or a draw group too, a table of its own family: a whole -// read draws its row without pinning it, so the family would draw apart from the row -// it renders, whichever draws the reaching template holds. -// docs/decisions.md#a-table-never-reaches-its-own-family-by-any-route -func checkOwnFamily(t *template) error { - own := t.table - if own == nil { - own = t.cellOf - } - if own == nil { - return nil - } - 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).head(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 bare read of %s would draw apart from the row it renders; read the family from a template beside it, or add the value as a column", e.reached(), head.category, own.category) - } - return nil -} - -// alternatives reports whether two reads never render together: one read renders one row of a -// table, so reads under different rows the walks pinned, or under different rows of one whole -// draw, never meet. -// docs/decisions.md#the-rows-of-a-table-are-alternatives -func alternatives(a, b drawAt) bool { - return a.pins.differs(&b.pins) || a.wholePins.differs(&b.wholePins) -} diff --git a/path.go b/path.go index 4966a32..17f9640 100644 --- a/path.go +++ b/path.go @@ -456,3 +456,15 @@ func checkPath(n node, tail []string, level string) error { _, err := (&pathCheck{level: level, tail: tail}).run(n) return err } + +// joinSegments spells segments as a path: a selector attaches to the name before it. +func joinSegments(segs []string) string { + var b strings.Builder + for _, s := range segs { + if b.Len() > 0 && !isSelector(s) { + b.WriteByte('.') + } + b.WriteString(s) + } + return b.String() +} diff --git a/table.go b/table.go index d577ba0..f11c4d9 100644 --- a/table.go +++ b/table.go @@ -613,3 +613,11 @@ func (t *table) selectorSpelling(r int) string { } return fmt.Sprintf("%s line %d", t.file, r+2) } + +// family is the table a chain of parents ends at. +func (t *table) family() *table { + for t.parentT != nil { + t = t.parentT + } + return t +} diff --git a/todo.md b/todo.md index 2876deb..3b58261 100644 --- a/todo.md +++ b/todo.md @@ -21,9 +21,6 @@ Self-sufficiency 5.6), every seat capped by Locality, and all nine naming This round, in order: -- Gather the family fence beside `drawWalk.check`: `checkFamilies`, `replayPairs`, - `checkFamilyPair` and `tableRead.replay` sit in `family.go` and `pins.go`, and one - rule spans seven files. - Compile every format in one phase: a template holding a reference has nil `ops` until `linkTemplateRefs`, so a check reading `ops` before link skips it silently. - Run `New`, `NewTemplate` and `FakeStruct` through one bind pipeline that names @@ -44,7 +41,7 @@ This round, in order: path `Fake` accepts, which a selector is too. Delete the dead `table.descends` and the redundant `string(key)` in `checkNoRepeatedItem`. - Delete the decision "The rows of a table are alternatives": it defines `alternatives`, - which its comment in `family.go` already does. Point the README's "which pairs are + which its comment in `drawfence.go` already does. Point the README's "which pairs are weighed against each other" at that comment's rule, stated in the README's Table section, where it sends a reader to the log for the definition today. - Run the nine-seat panel again and file what it names here.