Close the row set over ancestors only where the render pins the row

This commit is contained in:
2026-09-21 16:39:19 +02:00
committed by Lillem4n
parent 42fd728156
commit 4d45e3ebdc
4 changed files with 70 additions and 55 deletions
+3 -1
View File
@@ -18,7 +18,9 @@ replacement, and each removed path, column or flag.
`parent`, a name repeating inside one parent row, a name spelling another row's key, a table named like a column of any
table above it, a table whose format or cell references its own family, and,
within one render and draw group, a path drawing a table another path selects a
row of, or two paths pinning different rows of one table.
row of, or two paths pinning different rows of one table. Two reads are held to one
draw only where a render can pin both their rows: a path read pins a row's ancestors
with it, while a table read whole draws a row and pins none.
- `New` refuses a root choice of templates sharing one format and one set of string
fields, naming the rows TSV to write instead.
- `misc.territory`, `misc.currency`, `misc.language`, `misc.httpstatus` and
+3 -2
View File
@@ -1140,8 +1140,9 @@ the Development section below, and who ships a register the four above then draw
- **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 rule the fence applies is that two reads meet unless
they sit in two rows of one table, or in a row outside a selected ancestor's. A
format beside them. The rule the fence applies is that two reads meet unless they
sit in two rows of one table, or in rows no render pins at once: a path read pins a
row's ancestors with it, while a table read whole draws a row and pins none. 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
+19 -14
View File
@@ -291,18 +291,20 @@ type drawWalk struct {
seen map[drawVisit]bool
}
// drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, and
// the table rows it entered.
// drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, the
// table rows it entered, and the table a whole read draws a row of.
type drawAt struct {
group string
route drawRoute
alt rowSet
drawn *table
}
// rowSet is the rows a walk entered, one per table: only one row of a table renders, so reads in
// two rows of one table never meet, while reads in one row, across its columns and whatever they
// reach, do. It simulates what the render pins: enter is (*draws).pin and rowOf is its pinned, and
// TestRowSetSimulatesPinning holds the two to one answer.
// reach, do. It stands in for the rows the render pins, so a row a path read pins enters with the
// ancestors (*draws).pin pins alongside it, while a table read whole is drawn a row of and pins
// none.
type rowSet []tablePin
func (s rowSet) rowOf(t *table) (int, bool) {
@@ -314,24 +316,24 @@ func (s rowSet) rowOf(t *table) (int, bool) {
return 0, false
}
// enter is s with row r of t and the ancestor rows it links to, where t is not in it yet; the set
// is copied, since walks branch.
func (s rowSet) enter(t *table, r int) rowSet {
// enter is s with row r of t, where t is not in it yet, and, where pins says the render pins that
// row rather than draws it, the ancestor rows it links to. The set is copied, since walks branch.
func (s rowSet) enter(t *table, r int, pins bool) rowSet {
if _, in := s.rowOf(t); in {
return s
}
out := append(make(rowSet, 0, len(s)+1), s...)
for {
if _, in := out.rowOf(t); in {
break
}
out = append(out, tablePin{t, r})
if t.parentT == nil {
if !pins || t.parentT == nil {
break
}
t, r = t.parentT, t.parentRow(r)
if _, in := out.rowOf(t); in {
break
}
}
sort.Slice(out, func(i, j int) bool { return out[i].t.category < out[j].t.category })
sort.Slice(out, func(i, j int) bool { return out[i].t.path < out[j].t.path })
return out
}
@@ -394,12 +396,15 @@ func (w *drawWalk) walk(n node, at drawAt) {
if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" {
at.group = t.drawGroupKey
}
if t, isTable := n.(*table); isTable {
at.drawn = t
}
for _, e := range renderEdges(n) {
if cell, isCell := e.to.(*template); isCell && cell.cellOf != nil {
if at.alt.excludes(cell) {
continue
}
w.edge(n, e, drawAt{at.group, at.route, at.alt.enter(cell.cellOf, cell.cellRow)})
w.edge(n, e, drawAt{at.group, at.route, at.alt.enter(cell.cellOf, cell.cellRow, cell.cellOf != at.drawn), at.drawn})
continue
}
w.edge(n, e, at)
@@ -430,7 +435,7 @@ func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
w.reads = append(w.reads, pathRead{at, a, tr})
}
if tr != nil {
tr.pins.each(func(t *table, r int) { at.alt = at.alt.enter(t, r) })
tr.pins.each(func(t *table, r int) { at.alt = at.alt.enter(t, r, true) })
}
}
w.walk(e.to, at)
+45 -38
View File
@@ -964,9 +964,9 @@ func TestAmbiguousNameNamesARunnablePath(t *testing.T) {
}
}
// TestRowSetSimulatesPinning proves the load-time walk's rowSet answers what
// (*draws).pin and pinned answer at render time: the rows one entry brings with it,
// the cells that entry rules out, and which two entries are alternatives.
// TestRowSetSimulatesPinning proves the walk's row set answers what the render's pinning
// answers: a path read pins a row and the ancestors it links to, a whole read draws a row
// and pins none.
func TestRowSetSimulatesPinning(t *testing.T) {
f := newGenerator(t, writeFiles(t, geo()), WithSeed(1))
var tables []*table
@@ -979,54 +979,61 @@ func TestRowSetSimulatesPinning(t *testing.T) {
}
for _, entered := range tables {
for row := 0; row < entered.rows(); row++ {
s := rowSet(nil).enter(entered, row)
if drawn := rowSet(nil).enter(entered, row, false); len(drawn) != 1 || drawn[0] != (tablePin{entered, row}) {
t.Errorf("a drawn %s enters %v, want its own row alone", entered.selectorSpelling(row), drawn)
}
s := rowSet(nil).enter(entered, row, true)
var pinned draws
held := 0
pinned.pin(entered, row)
assertRowSetAgrees(t, s, pinned, entered, row, tables)
}
}
}
// assertRowSetAgrees compares the set entered with row of entered against the same
// row pinned, over every row of tables.
func assertRowSetAgrees(t *testing.T, s rowSet, pinned draws, entered *table, row int, tables []*table) {
t.Helper()
pinned.each(func(tbl *table, r int) {
got, in := s.rowOf(tbl)
if !in || got != r {
t.Errorf("entering %s leaves %s unset in the row set, though pinning it pins %s", entered.selectorSpelling(row), tbl.path, tbl.selectorSpelling(r))
}
})
for _, cand := range tables {
for cr := 0; cr < cand.rows(); cr++ {
var beside draws
beside.pin(entered, row)
refused := beside.pinRow(cand, cr) != nil
if got := s.excludes(&template{cellOf: cand, cellRow: cr}); got != refused {
t.Errorf("excludes(%s) = %v beside %s, but pinRow refuses it = %v", cand.selectorSpelling(cr), got, entered.selectorSpelling(row), refused)
pinned.each(func(tbl *table, r int) {
held++
if got, in := s.rowOf(tbl); !in || got != r {
t.Errorf("entering %s leaves %s unset, though pinning it pins %s", entered.selectorSpelling(row), tbl.path, tbl.selectorSpelling(r))
}
})
if held != len(s) {
t.Errorf("entering %s holds %d rows, though pinning it pins %d", entered.selectorSpelling(row), len(s), held)
}
a := drawAt{alt: s}
b := drawAt{alt: rowSet(nil).enter(cand, cr)}
if got := alternatives(a, b); got != refused {
t.Errorf("alternatives(%s, %s) = %v, but pinning both refuses = %v", entered.selectorSpelling(row), cand.selectorSpelling(cr), got, refused)
for _, cand := range tables {
for cr := 0; cr < cand.rows(); cr++ {
var beside draws
beside.pin(entered, row)
refused := beside.pinRow(cand, cr) != nil
if got := s.excludes(&template{cellOf: cand, cellRow: cr}); got != refused {
t.Errorf("excludes(%s) = %v beside %s, but pinRow refuses it = %v", cand.selectorSpelling(cr), got, entered.selectorSpelling(row), refused)
}
if got := alternatives(drawAt{alt: s}, drawAt{alt: rowSet(nil).enter(cand, cr, true)}); got != refused {
t.Errorf("alternatives(%s, %s) = %v, but pinning both refuses = %v", entered.selectorSpelling(row), cand.selectorSpelling(cr), got, refused)
}
}
}
}
}
}
// TestCellsOfRowsThatNeverMeetAreNotCompared pins what the entered ancestors buy: a
// cell of region 01 and a cell of a municipality of region 12 never render together,
// so the reads inside them are not one render's to reconcile.
func TestCellsOfRowsThatNeverMeetAreNotCompared(t *testing.T) {
files := with(geo(), map[string]string{
"addr.json": `{"format":"{city}","city":"Lund"}`,
// TestCellReadsMeetWhereTheRenderPairsTheRows pins which cells' reads are one render's to
// reconcile: a path read pins the row's ancestors, so a cell of region 01 and a cell of a
// municipality of region 12 never render together, while a whole read draws its row and pins
// no ancestor, so those same two cells do.
func TestCellReadsMeetWhereTheRenderPairsTheRows(t *testing.T) {
cells := with(geo(), map[string]string{
"addr.json": `{"format":"{city}","city":["Lund","Malmö"]}`,
"municipality.tsv": "code\tname\tregion\tpopulation\tnote\n0180\tStockholm\t01\t980000\t-\n0184\tSolna\t01\t85000\t-\n1280\tMalmö\t12\t360000\t{/addr.city}\n1281\tLund\t12\t130000\t-\n1480\tGöteborg\t14\t590000\t-\n",
"region.tsv": "code\tname\tpopulation\tnote\n01\tStockholms län\t2400000\t{/addr}\n12\tSkåne län\t1400000\t-\n14\tVästra Götalands län\t1750000\t-\n",
"x.json": `"{/region.note} {/municipality.note}"`,
})
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, files)), WithSeed(1))
paths := with(cells, map[string]string{"x.json": `"{/region.note} {/municipality.note}"`})
f, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, paths)), WithSeed(1))
if err != nil {
t.Fatalf("New = %v, want the two cells accepted", err)
}
fake(t, f, "x")
whole := with(cells, map[string]string{
"municipality.json": `{"format":"{name}={note}","rows":"municipality.tsv","key":"code","name":"name","parent":"region","weight":"population"}`,
"region.json": `{"format":"{name}={note}","rows":"region.tsv","key":"code","name":"name","weight":"population"}`,
"x.json": `"{/region}|{/municipality}"`,
})
if _, err := New(WithoutShippedData(), WithDataPath(writeFiles(t, whole)), WithSeed(1)); err == nil || !strings.Contains(err.Error(), "reads a path into") {
t.Fatalf("New = %v, want the two drawn rows held to one draw of addr", err)
}
}