From 0f645d70921a41d57aea995afefa50e9182317ec Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 16 Sep 2026 10:29:23 +0200 Subject: [PATCH] Delete the fresh-draw fence the self-reference rule made unreachable, and pay for the draw walks only where data binds a reference --- README.md | 13 +++++--- draw.go | 97 +++++++++++++++---------------------------------------- graph.go | 23 +++++++++---- 3 files changed, 52 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 349cd02..da3d8fa 100644 --- a/README.md +++ b/README.md @@ -522,7 +522,9 @@ token {w} is repeated, and uppercase operand "w" holds "w" to one draw per expan ### 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 list's length, a weighted one O(log n), and long formats, deep nesting and many 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 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 - 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 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, @@ -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 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 - them on the first read instead — which the allocation gate prices at two heap - allocations, paid only by a render that shares a draw. + them on the first read instead, at two heap allocations for a render that does share + 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 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 diff --git a/draw.go b/draw.go index 0acb893..e7c466f 100644 --- a/draw.go +++ b/draw.go @@ -118,7 +118,6 @@ type drawCheck struct { memo map[readsMemo]bool } -// readsMemo is one answer reads has given: for a node, and for each place it stops. type readsMemo struct { n node stopAtGroup bool @@ -155,8 +154,14 @@ 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. columns := recordColumns(t) - if len(columns) == 0 { + reads := false + for _, name := range columns { + reads = reads || c.readsPath(t.fields[name]) + } + if !reads { return nil } if err := checkColumnDraws(t, columns); err != nil { @@ -182,7 +187,7 @@ func (c *drawCheck) reads(n node, stopAtGroup bool) bool { } r := false 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 break } @@ -201,18 +206,17 @@ func repeats(n node) bool { func grouped(n node) bool { 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 -// reads none. -func refRead(n node, label string) (arm, node, bool) { +// refRead is the reference an edge of n reads; false when the edge reads none. +func refRead(n node, label string) (arm, bool) { t, isTemplate := n.(*template) if !isTemplate { - return arm{}, nil, false + return arm{}, false } 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. @@ -224,22 +228,17 @@ func checkColumnDraws(t *template, columns []string) error { return w.check() } -// drawWalk gathers what one render reads by reference and what it draws afresh, each by draw group, -// for check to compare. +// drawWalk gathers the references one render reads, by draw group, for check to compare. type drawWalk struct { reads []pathRead read map[drawKey]bool - fresh []freshDraw seen map[drawVisit]bool } -// drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, the -// reference it last crossed, and whether it renders inside a reference path's draw. +// drawAt is where a walk stands: the draw group it draws in, and how the render's root reached it. type drawAt struct { group string 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. @@ -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. type pathRead struct { - at drawAt - a arm - target node -} - -type freshDraw struct { at drawAt - n node + a arm } type drawVisit struct { n node group string - held bool } type drawKey struct { group string - key any + path string } 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 // there. func (w *drawWalk) walk(n node, at drawAt) { - v := drawVisit{n, at.group, at.held} + v := drawVisit{n, at.group} if w.seen[v] { return } w.seen[v] = true - if !at.held { - w.fresh = append(w.fresh, freshDraw{at, n}) - } if repeats(n) { return } @@ -295,21 +284,18 @@ func (w *drawWalk) walk(n node, at drawAt) { } func (w *drawWalk) edge(from node, e renderEdge, at drawAt) { - a, target, reads := refRead(from, e.label) - if !reads { - w.walk(e.to, at) - return + if a, reads := refRead(from, e.label); reads { + if k := (drawKey{at.group, a.path}); !w.read[k] { + w.read[k] = true + 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) } -// check refuses what one draw per reference path cannot answer for, reads compared in path order so -// which pair is reported does not vary. +// check refuses what one draw per reference path cannot answer for: a read of a level beside a path +// another read takes into it. Reads are compared in path order, so which pair is reported does not +// vary. func (w *drawWalk) check() error { sort.SliceStable(w.reads, func(i, j int) bool { 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 }) - 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 _, into := range w.reads[i+1:] { 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 } -// 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 { 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)) } diff --git a/graph.go b/graph.go index a32a965..b1dd371 100644 --- a/graph.go +++ b/graph.go @@ -197,14 +197,25 @@ func checkRenders(s nodeScope) error { return err } 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 } - if err := s(fence.checkDraws); err != nil { - return err - } - if err := s(fence.checkRecordDraws); err != nil { - return err + if refs { + if err := s(fence.checkDraws); err != nil { + return err + } + if err := s(fence.checkRecordDraws); err != nil { + return err + } } return s((&valueProof{}).checkDatatype) }