Draw groups: one reference draw per render, a drawGroup option, and a record's fences at load #16

Merged
lilleman merged 14 commits from draw-groups into main 2026-09-16 10:45:45 +02:00
3 changed files with 52 additions and 81 deletions
Showing only changes of commit 0f645d7092 - Show all commits
+9 -4
View File
@@ -522,7 +522,9 @@ token {w} is repeated, and uppercase operand "w" holds "w" to one draw per expan
### Performance ### Performance
Each file is parsed, validated and weight-indexed once, in `New`. A `Fake` call Each file is parsed, validated and weight-indexed once, in `New`. Proving the draw
fences adds one pass over the loaded tree, and walks what a render reads only where
data binds a reference, so a set that binds none pays for the pass alone. A `Fake` call
then costs about what its output costs: an unweighted pick is O(1) whatever the then costs about what its output costs: an unweighted pick is O(1) whatever the
list's length, a weighted one O(log n), and long formats, deep nesting and many list's length, a weighted one O(log n), and long formats, deep nesting and many
tokens add cost in proportion to the output. tokens add cost in proportion to the output.
@@ -604,7 +606,9 @@ tokens add cost in proportion to the output.
is two draws, as `{word} {word}` is, while every `{/p.first}` in one render reads is two draws, as `{word} {word}` is, while every `{/p.first}` in one render reads
one draw, and a bare `{/p}` beside them is a load error: a bare token one draw, and a bare `{/p}` beside them is a load error: a bare token
is by contract an independent draw, a path pins its level, and a fresh draw of a is by contract an independent draw, a path pins its level, and a fresh draw of a
pinned level could show another row. pinned level could show another row. A builtin's operand holds what it reads for
its expansion, references included, so `{uppercase(/p)} {/p}` is one draw — the
rule every operand follows.
- **Reference sigils follow the filesystem.** `/` is the root, `.` this file's - **Reference sigils follow the filesystem.** `/` is the root, `.` this file's
folder, `..` the folder above — what those spellings already mean to anyone who folder, `..` the folder above — what those spellings already mean to anyone who
has typed a path. A locale's files reach each other without naming the locale, has typed a path. A locale's files reach each other without naming the locale,
@@ -695,8 +699,9 @@ tokens add cost in proportion to the output.
columns always read through the render's draws, so making the maps where the set is columns always read through the render's draws, so making the maps where the set is
declared keeps them on that frame's stack. A `Fake` often reads no reference path at declared keeps them on that frame's stack. A `Fake` often reads no reference path at
all, and making them anyway cost about a fifth of the cheapest render, so it makes all, and making them anyway cost about a fifth of the cheapest render, so it makes
them on the first read instead — which the allocation gate prices at two heap them on the first read instead, at two heap allocations for a render that does share
allocations, paid only by a render that shares a draw. a draw. The allocation gate over a repeat of a reference path and over a named draw
group prices that, and pins the two measures that keep a draw set off the heap.
- **A category never references itself, and a record's fences run at load.** A category - **A category never references itself, and a record's fences run at load.** A category
is one unit: a reference back into it — `{/users.first}` inside `users` — describes a is one unit: a reference back into it — `{/users.first}` inside `users` — describes a
draw other than the fields beside it, so `New` refuses it and the sibling path stays draw other than the fields beside it, so `New` refuses it and the sibling path stays
+26 -71
View File
@@ -118,7 +118,6 @@ type drawCheck struct {
memo map[readsMemo]bool memo map[readsMemo]bool
} }
// readsMemo is one answer reads has given: for a node, and for each place it stops.
type readsMemo struct { type readsMemo struct {
n node n node
stopAtGroup bool stopAtGroup bool
@@ -155,8 +154,14 @@ func (c *drawCheck) checkRecordDraws(path string, n node) error {
if !ok || !t.record { if !ok || !t.record {
return nil return nil
} }
// A record's columns are its fields, which its format need not render at all, so the
// reads to weigh are theirs rather than the template's own.
columns := recordColumns(t) columns := recordColumns(t)
if len(columns) == 0 { reads := false
for _, name := range columns {
reads = reads || c.readsPath(t.fields[name])
}
if !reads {
return nil return nil
} }
if err := checkColumnDraws(t, columns); err != nil { if err := checkColumnDraws(t, columns); err != nil {
@@ -182,7 +187,7 @@ func (c *drawCheck) reads(n node, stopAtGroup bool) bool {
} }
r := false r := false
for _, e := range renderEdges(n) { for _, e := range renderEdges(n) {
if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || (!repeats(e.to) && !(stopAtGroup && grouped(e.to)) && c.reads(e.to, stopAtGroup)) { if a, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || (!repeats(e.to) && !(stopAtGroup && grouped(e.to)) && c.reads(e.to, stopAtGroup)) {
r = true r = true
break break
} }
@@ -201,18 +206,17 @@ func repeats(n node) bool {
func grouped(n node) bool { func grouped(n node) bool {
t, isTemplate := n.(*template) t, isTemplate := n.(*template)
return isTemplate && t.drawGroup != "" return isTemplate && t.drawGroupKey != ""
} }
// refRead is the reference an edge of n reads, and the node it is bound to; false when the edge // refRead is the reference an edge of n reads; false when the edge reads none.
// reads none. func refRead(n node, label string) (arm, bool) {
func refRead(n node, label string) (arm, node, bool) {
t, isTemplate := n.(*template) t, isTemplate := n.(*template)
if !isTemplate { if !isTemplate {
return arm{}, nil, false return arm{}, false
} }
a := splitArm(label, t.refs) a := splitArm(label, t.refs)
return a, t.fields[a.key], isRef(a.key) return a, isRef(a.key)
} }
// checkColumnDraws fences a record's columns as one render. // checkColumnDraws fences a record's columns as one render.
@@ -224,22 +228,17 @@ func checkColumnDraws(t *template, columns []string) error {
return w.check() return w.check()
} }
// drawWalk gathers what one render reads by reference and what it draws afresh, each by draw group, // drawWalk gathers the references one render reads, by draw group, for check to compare.
// for check to compare.
type drawWalk struct { type drawWalk struct {
reads []pathRead reads []pathRead
read map[drawKey]bool read map[drawKey]bool
fresh []freshDraw
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, the // drawAt is where a walk stands: the draw group it draws in, and how the render's root reached it.
// reference it last crossed, and whether it renders inside a reference path's draw.
type drawAt struct { type drawAt struct {
group string group string
route drawRoute route drawRoute
via string
held bool
} }
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label. // drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
@@ -247,25 +246,18 @@ type drawRoute struct{ spelling, label string }
// pathRead is one reference a render reads: a path, or a bare reference with no tail. // pathRead is one reference a render reads: a path, or a bare reference with no tail.
type pathRead struct { type pathRead struct {
at drawAt
a arm
target node
}
type freshDraw struct {
at drawAt at drawAt
n node a arm
} }
type drawVisit struct { type drawVisit struct {
n node n node
group string group string
held bool
} }
type drawKey struct { type drawKey struct {
group string group string
key any path string
} }
func newDrawWalk() *drawWalk { func newDrawWalk() *drawWalk {
@@ -275,14 +267,11 @@ func newDrawWalk() *drawWalk {
// walk follows what rendering n renders. A repeat renders over draws of its own, so the walk stops // walk follows what rendering n renders. A repeat renders over draws of its own, so the walk stops
// there. // there.
func (w *drawWalk) walk(n node, at drawAt) { func (w *drawWalk) walk(n node, at drawAt) {
v := drawVisit{n, at.group, at.held} v := drawVisit{n, at.group}
if w.seen[v] { if w.seen[v] {
return return
} }
w.seen[v] = true w.seen[v] = true
if !at.held {
w.fresh = append(w.fresh, freshDraw{at, n})
}
if repeats(n) { if repeats(n) {
return return
} }
@@ -295,21 +284,18 @@ func (w *drawWalk) walk(n node, at drawAt) {
} }
func (w *drawWalk) edge(from node, e renderEdge, at drawAt) { func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
a, target, reads := refRead(from, e.label) if a, reads := refRead(from, e.label); reads {
if !reads { if k := (drawKey{at.group, a.path}); !w.read[k] {
w.walk(e.to, at) w.read[k] = true
return w.reads = append(w.reads, pathRead{at, a})
}
} }
if k := (drawKey{at.group, a.path}); !w.read[k] {
w.read[k] = true
w.reads = append(w.reads, pathRead{at, a, target})
}
at.via, at.held = a.name, len(a.tail) > 0
w.walk(e.to, at) w.walk(e.to, at)
} }
// check refuses what one draw per reference path cannot answer for, reads compared in path order so // check refuses what one draw per reference path cannot answer for: a read of a level beside a path
// which pair is reported does not vary. // another read takes into it. Reads are compared in path order, so which pair is reported does not
// vary.
func (w *drawWalk) check() error { func (w *drawWalk) check() error {
sort.SliceStable(w.reads, func(i, j int) bool { sort.SliceStable(w.reads, func(i, j int) bool {
if w.reads[i].at.group != w.reads[j].at.group { if w.reads[i].at.group != w.reads[j].at.group {
@@ -317,14 +303,6 @@ func (w *drawWalk) check() error {
} }
return w.reads[i].a.path < w.reads[j].a.path return w.reads[i].a.path < w.reads[j].a.path
}) })
if err := w.checkOverlaps(); err != nil {
return err
}
return w.checkFreshDraws()
}
// checkOverlaps refuses a read of a level beside a path another read takes into it.
func (w *drawWalk) checkOverlaps() error {
for i, level := range w.reads { for i, level := range w.reads {
for _, into := range w.reads[i+1:] { for _, into := range w.reads[i+1:] {
if into.at.group == level.at.group && strings.HasPrefix(into.a.path, level.a.path+".") { if into.at.group == level.at.group && strings.HasPrefix(into.a.path, level.a.path+".") {
@@ -335,29 +313,6 @@ func (w *drawWalk) checkOverlaps() error {
return nil return nil
} }
// checkFreshDraws refuses a node drawn afresh where a reference path's draw holds it.
func (w *drawWalk) checkFreshDraws() error {
pins := map[drawKey]pathRead{}
for _, r := range w.reads {
if len(r.a.tail) == 0 {
continue
}
held := map[node]bool{}
coverPath(r.target, r.a.tail, held)
for n := range held {
if _, pinned := pins[drawKey{r.at.group, n}]; !pinned {
pins[drawKey{r.at.group, n}] = r
}
}
}
for _, f := range w.fresh {
if r, pinned := pins[drawKey{f.at.group, f.n}]; pinned {
return overlapError(f.at.route, f.at.via, r)
}
}
return nil
}
func overlapError(route drawRoute, ref string, into pathRead) error { 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.name)) 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.name))
} }
+17 -6
View File
@@ -197,14 +197,25 @@ func checkRenders(s nodeScope) error {
return err return err
} }
fence := &drawCheck{} fence := &drawCheck{}
if err := s(fence.checkDrawGroup); err != nil { // One pass refuses a draw group that splits nothing and notes whether the scope binds a
// reference at all: weighing what a render's draws hold walks every node it renders, and
// with nothing bound there is no draw to share, so the shipped set pays for neither walk.
refs := false
if err := s(func(path string, n node) error {
if t, ok := n.(*template); ok && len(t.refs) > 0 {
refs = true
}
return fence.checkDrawGroup(path, n)
}); err != nil {
return err return err
} }
if err := s(fence.checkDraws); err != nil { if refs {
return err if err := s(fence.checkDraws); err != nil {
} return err
if err := s(fence.checkRecordDraws); err != nil { }
return err if err := s(fence.checkRecordDraws); err != nil {
return err
}
} }
return s((&valueProof{}).checkDatatype) return s((&valueProof{}).checkDatatype)
} }