diff --git a/README.md b/README.md index 316ca77..349cd02 100644 --- a/README.md +++ b/README.md @@ -602,7 +602,7 @@ tokens add cost in proportion to the output. as JSON; `--data-path` layers over it. - **A bare reference draws each time; a reference path is held.** `{/p} {/p}` is two draws, as `{word} {word}` is, while every `{/p.first}` in one render reads - one draw, and a bare `{/p}` rendering it 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 pinned level could show another row. - **Reference sigils follow the filesystem.** `/` is the root, `.` this file's @@ -691,6 +691,12 @@ tokens add cost in proportion to the output. different things — an operand pins the value its own render produced and stops at a reference, a path pins every level it passes through — so one walk would carry both rules and both scopes anyway, and tell them apart at every step. +- **A record makes its draw maps up front, a `Fake` on its first read.** A record's + 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. - **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 a592dc5..0acb893 100644 --- a/draw.go +++ b/draw.go @@ -115,8 +115,13 @@ func checkNestedDrawGroup(fields map[string]node, group string) error { // drawCheck fences each template of a scope as a render of its own, remembering which nodes read a // reference path. type drawCheck struct { - reads map[node]bool - splits map[node]bool + 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 } // checkDrawGroup refuses a draw group that splits nothing: one whose render reads every reference @@ -162,41 +167,30 @@ func (c *drawCheck) checkRecordDraws(path string, n node) error { // readsPath reports whether rendering n reads a reference path, short of a repeat, which renders // over draws of its own. -func (c *drawCheck) readsPath(n node) bool { - if r, done := c.reads[n]; done { - return r - } - r := false - for _, e := range renderEdges(n) { - if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || (!repeats(e.to) && c.readsPath(e.to)) { - r = true - break - } - } - if c.reads == nil { - c.reads = map[node]bool{} - } - c.reads[n] = r - return r -} +func (c *drawCheck) readsPath(n node) bool { return c.reads(n, false) } // splitsDraws reports whether rendering n reads a reference path that n's own draw group answers // for: one outside a repeat and outside a nested draw group, which hold their own draws. -func (c *drawCheck) splitsDraws(n node) bool { - if r, done := c.splits[n]; done { +func (c *drawCheck) splitsDraws(n node) bool { return c.reads(n, true) } + +// reads walks what rendering n renders for a reference path, stopping at a repeat — and at a nested +// draw group when stopAtGroup — since each holds draws of its own. +func (c *drawCheck) reads(n node, stopAtGroup bool) bool { + k := readsMemo{n, stopAtGroup} + if r, done := c.memo[k]; done { return r } r := false for _, e := range renderEdges(n) { - if a, _, isRef := refRead(n, e.label); (isRef && len(a.tail) > 0) || (!repeats(e.to) && !grouped(e.to) && c.splitsDraws(e.to)) { + 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 } } - if c.splits == nil { - c.splits = map[node]bool{} + if c.memo == nil { + c.memo = map[readsMemo]bool{} } - c.splits[n] = r + c.memo[k] = r return r } diff --git a/path.go b/path.go index a48b80e..cb5e444 100644 --- a/path.go +++ b/path.go @@ -89,7 +89,7 @@ func unreachableInChoice(c *choice, want string) error { // checkPath proves a dotted tail resolves whichever way the draws go — a choice // must carry the rest of the path in the set every variant shares — and that no -// level a path reads carries a repeat or a group, which one draw of it could not +// level a path reads carries a repeat or a drawGroup, which one draw of it could not // apply. So a path that validates here resolves on every render, and a typo is a // New-time error. func checkPath(n node, tail []string, level string) error {