Draw groups: one reference draw per render, a drawGroup option, and a record's fences at load #16
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -249,23 +248,16 @@ type drawRoute struct{ spelling, label string }
|
|||||||
type pathRead struct {
|
type pathRead struct {
|
||||||
at drawAt
|
at drawAt
|
||||||
a arm
|
a arm
|
||||||
target node
|
|
||||||
}
|
|
||||||
|
|
||||||
type freshDraw struct {
|
|
||||||
at drawAt
|
|
||||||
n node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|
||||||
w.walk(e.to, at)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if k := (drawKey{at.group, a.path}); !w.read[k] {
|
if k := (drawKey{at.group, a.path}); !w.read[k] {
|
||||||
w.read[k] = true
|
w.read[k] = true
|
||||||
w.reads = append(w.reads, pathRead{at, a, target})
|
w.reads = append(w.reads, pathRead{at, a})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,15 +197,26 @@ 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 refs {
|
||||||
if err := s(fence.checkDraws); err != nil {
|
if err := s(fence.checkDraws); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s(fence.checkRecordDraws); err != nil {
|
if err := s(fence.checkRecordDraws); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return s((&valueProof{}).checkDatatype)
|
return s((&valueProof{}).checkDatatype)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user