Fold the draw fence's pair checks into one loop over coRender and conflict, naming the lowest pin clash

This commit is contained in:
2026-09-30 07:55:37 +02:00
committed by Lillem4n
parent 4cba01fc7a
commit d34513336d
4 changed files with 73 additions and 93 deletions
+1 -1
View File
@@ -40,4 +40,4 @@ package fejkdata
// the `table` itself, so it draws a row apart from every pin: `branches.wholePins`. // the `table` itself, so it draws a row apart from every pin: `branches.wholePins`.
// - fence — a load-time check, so rendering a compiled tree cannot fail: `New` // - fence — a load-time check, so rendering a compiled tree cannot fail: `New`
// runs them over the data set, `NewTemplate` and `FakeStruct` over what those // runs them over the data set, `NewTemplate` and `FakeStruct` over what those
// compile. The draw fences are `heldCheck`, `drawFence` and `checkFamilies`. // compile. The draw fences are `heldCheck` and `drawFence`.
+66 -82
View File
@@ -20,21 +20,30 @@ type readSurvey struct {
clash *pinClash clash *pinClash
} }
// pinClash is the first read whose selectors clash with a row its route pinned. // pinClash is the read, lowest by how it is named, whose selectors clash with a row its route pinned.
type pinClash struct { type pinClash struct {
route readRoute route readRoute
spelling string spelling string
err error err error
} }
func (c *pinClash) named() string { return c.route.spelled(c.spelling) }
func (c *pinClash) before(o *pinClash) bool {
if c.named() != o.named() {
return c.named() < o.named()
}
return c.err.Error() < o.err.Error()
}
// survey is the fold's result as a render in group: every read not inside a nested draw group // survey is the fold's result as a render in group: every read not inside a nested draw group
// draws in it, and one read gathered by several routes is kept once. // draws in it, and one read gathered by several routes is kept once.
func survey(reads []pathRead, group string) *readSurvey { func survey(reads []pathRead, group string) *readSurvey {
s := &readSurvey{} s := &readSurvey{}
for _, r := range reads { for _, r := range reads {
if r.clash != nil { if r.clash != nil {
if s.clash == nil { if c := (&pinClash{r.route, r.a.spelling, r.clash}); s.clash == nil || c.before(s.clash) {
s.clash = &pinClash{r.route, r.a.spelling, r.clash} s.clash = c
} }
continue continue
} }
@@ -150,11 +159,10 @@ func repeats(n node) bool {
} }
// check refuses what one draw per reference path cannot answer for: a read selecting a row its route // check refuses what one draw per reference path cannot answer for: a read selecting a row its route
// pinned another of, a read of a level beside a path another read takes into it, and two reads of one // pinned another of, and any two reads that render together yet conflict.
// table family that select different rows.
func (s *readSurvey) check() error { func (s *readSurvey) check() error {
if c := s.clash; c != nil { if c := s.clash; c != nil {
return fmt.Errorf("%s: %w; read the family from a template beside it, or add the value as a column", c.route.spelled(c.spelling), c.err) return fmt.Errorf("%s: %w; read the family from a template beside it, or add the value as a column", c.named(), c.err)
} }
sort.SliceStable(s.reads, func(i, j int) bool { sort.SliceStable(s.reads, func(i, j int) bool {
if s.reads[i].group != s.reads[j].group { if s.reads[i].group != s.reads[j].group {
@@ -162,97 +170,73 @@ func (s *readSurvey) check() error {
} }
return s.reads[i].a.path < s.reads[j].a.path return s.reads[i].a.path < s.reads[j].a.path
}) })
for i, level := range s.reads { for i, a := range s.reads {
for _, into := range s.reads[i+1:] { for _, b := range s.reads[i+1:] {
if into.group != level.group || alternatives(level.branches, into.branches) { if !coRender(a, b) {
continue continue
} }
if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.landsRow) { if err := conflict(a, b); err != nil {
return overlapError(level.route, level.a.spelling, into)
}
}
}
return checkFamilies(s.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.
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.group != r.group || alternatives(o.branches, r.branches) || o.tr.headTable.familyRoot() != r.tr.headTable.familyRoot() {
continue
}
if err := checkFamilyPair(o, r); err != nil {
return err return err
} }
} }
} }
return replayPairs(reads) return nil
} }
// checkFamilyPair refuses a table read whole beside a path into its family, and a // coRender reports whether two reads can render together: in one draw group, and under no two
// table one read draws that the other pins, since which token renders first would // rows of one table, since one read renders one row of it.
// then decide the row. func coRender(a, b pathRead) bool {
// docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held return a.group == b.group && !a.branches.pins.differs(&b.branches.pins) && !a.branches.wholePins.differs(&b.branches.wholePins)
func checkFamilyPair(a, b pathRead) error { }
// conflict refuses two reads rendering together, a before b by path, that one draw per reference
// path cannot answer for. Pairs find every conflict a replay of all the reads would: a row clashes
// with one pinned table, and the read that pinned it holds that pin itself, with its ancestors.
func conflict(a, b pathRead) error {
for _, pair := range [][2]pathRead{{a, b}, {b, a}} { for _, pair := range [][2]pathRead{{a, b}, {b, a}} {
x, y := pair[0], pair[1] if err := drawsApart(pair[0], pair[1]); err != nil {
if len(x.a.tail) == 0 && len(y.a.tail) > 0 { return err
return overlapError(x.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.segment}, tail...)
}
return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup",
x.route.spelled(x.a.spelling), drawn.segment, y.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.route.spelled(x.a.spelling), drawn.segment, y.route.spelled(y.a.spelling))
} }
} }
if a.tr == nil || b.tr == nil {
return nil
}
d := a.tr.pins.clone()
if err := b.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", b.route.spelled(b.a.spelling), err)
}
return nil return nil
} }
// replayPairs replays, of every two reads that can render together, the later into the // drawsApart refuses x drawing apart from what y reads: a level beside a path y takes into it, a
// earlier's pins. Pairs find every conflict a full replay would: clash judges a row against one // table x reads whole beside a path y takes into its family, and a table x draws that y pins, since
// pinned table, and the read that pinned it holds that pin itself, since a read's pins carry // which token renders first would then decide the row. A level landing on a row renders the row its
// its rows' ancestors, so the pair of those two reads clashes the same way. // selectors pin, the one a path into it reads too.
func replayPairs(reads []pathRead) error { // docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held
for i, r := range reads { func drawsApart(x, y pathRead) error {
if r.tr == nil { if strings.HasPrefix(y.a.path, x.a.path+".") && !(x.tr != nil && x.tr.landsRow) {
continue return overlapError(x.route, x.a.spelling, y)
}
for _, o := range reads[i+1:] {
if o.tr == nil || o.group != r.group || alternatives(r.branches, o.branches) {
continue
}
d := r.tr.pins.clone()
if err := o.tr.replay(&d); err != nil {
return conflict(o, err)
}
}
} }
return nil if x.tr == nil || y.tr == nil || x.tr.headTable.familyRoot() != y.tr.headTable.familyRoot() {
} return nil
}
func conflict(r pathRead, err error) error { if len(x.a.tail) == 0 && len(y.a.tail) > 0 {
return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.route.spelled(r.a.spelling), err) return overlapError(x.route, x.a.spelling, y)
} }
drawn := x.tr.drawnOf(&y.tr.pins)
// alternatives reports whether two reads never render together: one read renders one row of a if drawn == nil {
// table, so reads under different rows the walks pinned, or under different rows of one whole return nil
// draw, never meet. }
func alternatives(a, b branches) bool { if s, ok := y.tr.selected(x.tr.headTable); ok && len(x.tr.sels) == 0 {
return a.pins.differs(&b.pins) || a.wholePins.differs(&b.wholePins) tail := x.a.tail
if s.t != x.tr.headTable {
tail = append([]string{x.tr.headTable.segment}, tail...)
}
return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup",
x.route.spelled(x.a.spelling), drawn.segment, y.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.route.spelled(x.a.spelling), drawn.segment, y.route.spelled(y.a.spelling))
} }
func overlapError(route readRoute, ref string, into pathRead) error { func overlapError(route readRoute, ref string, into pathRead) error {
+4 -2
View File
@@ -5,8 +5,10 @@ import "fmt"
// readRoute is how a render reaches a read: as its author spells it, and the root edge's label. // readRoute is how a render reaches a read: as its author spells it, and the root edge's label.
type readRoute struct{ spelling, label string } type readRoute struct{ spelling, label string }
// branches is the rows a read sits under: those pinned above it, and those of whole draws whose // branches is the rows a read sits under, where two reads under different rows of one table never
// cells it sits in. // render together. pins holds the rows pinned above it with their ancestors, which a pair of reads
// needs to find what a replay of all of them does; wholePins the rows of whole draws whose cells it
// sits in, without ancestors, since each whole draw draws apart from every other.
type branches struct { type branches struct {
pins pinSet pins pinSet
wholePins pinSet wholePins pinSet
+2 -8
View File
@@ -36,12 +36,6 @@ on 2026-09-29 a fifth time (Navigation 6.9, Locality 5.2, Shape 6.1, Self-suffic
This round, in order: This round, in order:
- Say at `branches` what its two sets hold, the rows pinned with their ancestors and the
rows of whole draws without, both load-bearing, and fold `check`, `checkFamilies`,
`replayPairs`, `checkFamilyPair` and `alternatives` into one pairwise loop over
`coRender` and `conflict`. Report a pin clash lowest by route and spelling, so which of
two is named stops following the walk, and say at `conflict` why a level landing on a
row may sit beside a path into it.
- Run the nine-seat panel as a checkpoint, and file its scores above: where Locality has - Run the nine-seat panel as a checkpoint, and file its scores above: where Locality has
not risen from 5.2, stop and bring the scores to the maintainer before the path steps. not risen from 5.2, stop and bring the scores to the maintainer before the path steps.
- Compile each path once at link into steps holding no pointer, with its leaves and - Compile each path once at link into steps holding no pointer, with its leaves and
@@ -310,8 +304,8 @@ by a `data-import/` script, as README goal 12 asks.
error names the category and the two columns but no row, so the author greps a error names the category and the two columns but no row, so the author greps a
register-sized file for the cell — and "name the fields you want instead" rewrites a register-sized file for the cell — and "name the fields you want instead" rewrites a
cell that is not the one to fix. cell that is not the one to fix.
- Spell a table one way across the family errors: `checkFamilyPair` names - Spell a table one way across the family errors: `drawsApart` names
`drawn.category`, `pinRow` names `t.path` and `mustRow` panics with `t.category`, so one `drawn.segment`, `pinRow` names `t.path` and `mustRow` panics with `t.category`, so one
table is `territory` and `misc.territory`, and the short spelling names no file where two table is `territory` and `misc.territory`, and the short spelling names no file where two
folders hold that name. folders hold that name.
- Let a table column carry a `datatype`, so `--format json` writes - Let a table column carry a `datatype`, so `--format json` writes