Refuse a column pointing back at its record however it is spelled, and qualify the shared-draw claim to path reads
Tests / vet + fmt + tests (pull_request) Successful in 1m2s

This commit is contained in:
2026-09-04 09:47:55 +02:00
parent b99526aeb8
commit 85a4cd4475
2 changed files with 12 additions and 15 deletions
+4 -3
View File
@@ -123,9 +123,10 @@ string and a backslash as an escape.
A record written only to emit columns still needs a `format` — the grammar's one A record written only to emit columns still needs a `format` — the grammar's one
required key — so `"format": ""` carries the fields with an inert format: it required key — so `"format": ""` carries the fields with an inert format: it
renders nothing by `Fake`, and is compiled only so the tree's fences still run. renders nothing by `Fake`, and is compiled only so the tree's fences still run.
The columns are the point, and their facts stay together: two columns that The columns are the point, and their facts stay together: two columns that read a
reference one category — `{/currency.code}` and `{/currency.symbol}` — share one path into one category — `{/currency.code}` and `{/currency.symbol}` — share one
draw of it, so the record is internally consistent. That one draw is also why two draw of it, so the record is internally consistent. A bare `{/currency}` names no
field, so it keeps drawing on its own. That one draw is also why two
columns may not read overlapping reference *paths*`{/cat.a}` beside columns may not read overlapping reference *paths*`{/cat.a}` beside
`{/cat.a.b}` is refused, naming the fields to write instead, as `{/cat.a.b}` is refused, naming the fields to write instead, as
[One draw, one spelling](#one-draw-one-spelling) refuses that pair inside a single [One draw, one spelling](#one-draw-one-spelling) refuses that pair inside a single
+8 -12
View File
@@ -120,9 +120,8 @@ type recordShape struct {
err error err error
} }
// recordShapeOf fences a node once and remembers the answer. The fences read the // recordShapeOf fences a node once and remembers the answer. Callers hold the
// compiled tree, which New fixed, so a repeated Record call on one path pays them // generator's lock.
// once rather than per draw. Callers hold the generator's lock.
func (f *Generator) recordShapeOf(n node) recordShape { func (f *Generator) recordShapeOf(n node) recordShape {
if shape, done := f.records[n]; done { if shape, done := f.records[n]; done {
return shape return shape
@@ -225,10 +224,10 @@ type columnRef struct {
a arm a arm
} }
// columnRefs lists every reference read that reads a path, anywhere a column // columnRefs lists every reference that reads a path, anywhere a column renders,
// renders, following the same edges expand does. A read that lands back on the // following the same edges expand does. A reference landing back on the record
// record itself names a sibling column, which no draw of the record can answer // itself is reported rather than collected, whether it reads a path or the record
// for, so it is reported here rather than collected. // whole: either way the column describes a draw other than its neighbours'.
func columnRefs(t *template, columns []string) ([]columnRef, error) { func columnRefs(t *template, columns []string) ([]columnRef, error) {
var out []columnRef var out []columnRef
var err error var err error
@@ -241,11 +240,8 @@ func columnRefs(t *template, columns []string) ([]columnRef, error) {
} }
seen[n] = true seen[n] = true
if tm, ok := n.(*template); ok { if tm, ok := n.(*template); ok {
for _, r := range boundReaders(tm.format, tm.bound, tm.refs) { for _, ref := range refTokens(tm.format) {
a := splitArm(r.name, tm.refs) a := splitArm(ref, tm.refs)
if !isRef(a.key) {
continue
}
if tm.fields[a.key] == node(t) { if tm.fields[a.key] == node(t) {
err = fmt.Errorf("column %q reads {%s}, which points back at this record; a column cannot read another column — move the shared value into its own category and reference that", name, a.name) err = fmt.Errorf("column %q reads {%s}, which points back at this record; a column cannot read another column — move the shared value into its own category and reference that", name, a.name)
return return