Delete the fresh-draw fence the self-reference rule made unreachable, and pay for the draw walks only where data binds a reference
Tests / vet + fmt + tests (pull_request) Successful in 1m6s
Tests / vet + fmt + tests (pull_request) Successful in 1m6s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user