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
4 changed files with 6 additions and 11 deletions
Showing only changes of commit b45a1edae0 - Show all commits
+3 -1
View File
@@ -709,7 +709,9 @@ tokens add cost in proportion to the output.
category, which both reference. That settled, a record's column fences run at `New`
too, so a category that loads renders as whichever shape is asked for, and a reference
reaching back into a category through another one is refused there as the overlap it
is.
is. Which reads those fences weigh differs on purpose: a record-only template's inert
format renders nothing, so a `drawGroup` on it can never matter and is refused, while
one on a rendering format can matter to a caller that bare-references it.
- **A record's column set is fixed before the first draw.** Only a category-level
template is a record: a path descending into a field, or naming a folder or a
choice, errors. A tail may pass through a choice whose variants carry different
+1 -2
View File
@@ -154,8 +154,7 @@ func (c *drawCheck) checkRecordDraws(path string, n node) error {
if !ok || !t.record {
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.
// A record-only template's format renders nothing, so weigh the columns, not the format.
columns := recordColumns(t)
reads := false
for _, name := range columns {
-3
View File
@@ -197,9 +197,6 @@ func checkRenders(s nodeScope) error {
return err
}
fence := &drawCheck{}
// 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 {
+2 -5
View File
@@ -51,11 +51,7 @@ func TestNoRenderAllocRegression(t *testing.T) {
}
}
// A record's fences read the compiled tree, so they belong to New, not to a draw.
// A reference path read across a repeat, and one read in a named draw group. The repeat
// prices both measures that keep a render's draw set off the heap — inlining expandAnew
// into render's loop, or keying a draw group's map with the scope's own string, costs an
// allocation an iteration — while the draw group holds the group path's own count.
// The repeat shape prices both escape measures: dropping either costs an alloc an iteration.
func TestNoReferenceAllocRegression(t *testing.T) {
word := `{"format":"{w}","w":["alpha","beta","gamma","delta"]}`
for _, s := range []struct {
@@ -78,6 +74,7 @@ func TestNoReferenceAllocRegression(t *testing.T) {
}
}
// A record's fences read the compiled tree, so they belong to New, not to a draw.
func TestNoRecordAllocRegression(t *testing.T) {
for _, s := range []struct{ name, json string }{
{"record 3 columns", `{"format":"","a":"x","b":"y","c":"z"}`},