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 `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, 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 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 - `New` refuses a root choice of templates sharing one format and one set of string
fields, naming the rows TSV to write instead. fields, naming the rows TSV to write instead.
- `misc.territory`, `misc.currency`, `misc.language`, `misc.httpstatus` and - `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 - **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 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 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 format beside them. The rule the fence applies is that two reads meet unless they
they sit in two rows of one table, or in a row outside a selected ancestor's. A 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 choice's items get no such treatment yet: two items selecting different rows
are still refused. are still refused.
- **A table never reaches its own family, by any route.** A `repeat` iteration and a - **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 seen map[drawVisit]bool
} }
// drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, and // drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, the
// the table rows it entered. // table rows it entered, and the table a whole read draws a row of.
type drawAt struct { type drawAt struct {
group string group string
route drawRoute route drawRoute
alt rowSet alt rowSet
drawn *table
} }
// rowSet is the rows a walk entered, one per table: only one row of a table renders, so reads in // 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 // 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 // reach, do. It stands in for the rows the render pins, so a row a path read pins enters with the
// TestRowSetSimulatesPinning holds the two to one answer. // ancestors (*draws).pin pins alongside it, while a table read whole is drawn a row of and pins
// none.
type rowSet []tablePin type rowSet []tablePin
func (s rowSet) rowOf(t *table) (int, bool) { func (s rowSet) rowOf(t *table) (int, bool) {
@@ -314,24 +316,24 @@ func (s rowSet) rowOf(t *table) (int, bool) {
return 0, false 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 // enter is s with row r of t, where t is not in it yet, and, where pins says the render pins that
// is copied, since walks branch. // 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) rowSet { func (s rowSet) enter(t *table, r int, pins bool) rowSet {
if _, in := s.rowOf(t); in { if _, in := s.rowOf(t); in {
return s return s
} }
out := append(make(rowSet, 0, len(s)+1), s...) out := append(make(rowSet, 0, len(s)+1), s...)
for { for {
if _, in := out.rowOf(t); in {
break
}
out = append(out, tablePin{t, r}) out = append(out, tablePin{t, r})
if t.parentT == nil { if !pins || t.parentT == nil {
break break
} }
t, r = t.parentT, t.parentRow(r) 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 return out
} }
@@ -394,12 +396,15 @@ func (w *drawWalk) walk(n node, at drawAt) {
if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" { if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" {
at.group = t.drawGroupKey at.group = t.drawGroupKey
} }
if t, isTable := n.(*table); isTable {
at.drawn = t
}
for _, e := range renderEdges(n) { for _, e := range renderEdges(n) {
if cell, isCell := e.to.(*template); isCell && cell.cellOf != nil { if cell, isCell := e.to.(*template); isCell && cell.cellOf != nil {
if at.alt.excludes(cell) { if at.alt.excludes(cell) {
continue 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 continue
} }
w.edge(n, e, at) 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}) w.reads = append(w.reads, pathRead{at, a, tr})
} }
if tr != nil { 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) w.walk(e.to, at)
+34 -27
View File
@@ -964,9 +964,9 @@ func TestAmbiguousNameNamesARunnablePath(t *testing.T) {
} }
} }
// TestRowSetSimulatesPinning proves the load-time walk's rowSet answers what // TestRowSetSimulatesPinning proves the walk's row set answers what the render's pinning
// (*draws).pin and pinned answer at render time: the rows one entry brings with it, // answers: a path read pins a row and the ancestors it links to, a whole read draws a row
// the cells that entry rules out, and which two entries are alternatives. // and pins none.
func TestRowSetSimulatesPinning(t *testing.T) { func TestRowSetSimulatesPinning(t *testing.T) {
f := newGenerator(t, writeFiles(t, geo()), WithSeed(1)) f := newGenerator(t, writeFiles(t, geo()), WithSeed(1))
var tables []*table var tables []*table
@@ -979,24 +979,22 @@ func TestRowSetSimulatesPinning(t *testing.T) {
} }
for _, entered := range tables { for _, entered := range tables {
for row := 0; row < entered.rows(); row++ { 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 var pinned draws
held := 0
pinned.pin(entered, row) 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) { pinned.each(func(tbl *table, r int) {
got, in := s.rowOf(tbl) held++
if !in || got != r { if got, in := s.rowOf(tbl); !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)) 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)
}
for _, cand := range tables { for _, cand := range tables {
for cr := 0; cr < cand.rows(); cr++ { for cr := 0; cr < cand.rows(); cr++ {
var beside draws var beside draws
@@ -1005,28 +1003,37 @@ func assertRowSetAgrees(t *testing.T, s rowSet, pinned draws, entered *table, ro
if got := s.excludes(&template{cellOf: cand, cellRow: cr}); got != refused { 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) t.Errorf("excludes(%s) = %v beside %s, but pinRow refuses it = %v", cand.selectorSpelling(cr), got, entered.selectorSpelling(row), refused)
} }
a := drawAt{alt: s} if got := alternatives(drawAt{alt: s}, drawAt{alt: rowSet(nil).enter(cand, cr, true)}); got != refused {
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) 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 // TestCellReadsMeetWhereTheRenderPairsTheRows pins which cells' reads are one render's to
// cell of region 01 and a cell of a municipality of region 12 never render together, // reconcile: a path read pins the row's ancestors, so a cell of region 01 and a cell of a
// so the reads inside them are not one render's to reconcile. // municipality of region 12 never render together, while a whole read draws its row and pins
func TestCellsOfRowsThatNeverMeetAreNotCompared(t *testing.T) { // no ancestor, so those same two cells do.
files := with(geo(), map[string]string{ func TestCellReadsMeetWhereTheRenderPairsTheRows(t *testing.T) {
"addr.json": `{"format":"{city}","city":"Lund"}`, 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", "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", "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 { if err != nil {
t.Fatalf("New = %v, want the two cells accepted", err) t.Fatalf("New = %v, want the two cells accepted", err)
} }
fake(t, f, "x") 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)
}
} }