Draw groups: one reference draw per render, a drawGroup option, and a record's fences at load #16
@@ -131,9 +131,10 @@ columns that read a path into one category — `{/currency.code}` and
|
|||||||
record may not overlap — `{/cat.a}`, or a bare `{/cat}`, beside
|
record may not overlap — `{/cat.a}`, or a bare `{/cat}`, beside
|
||||||
`{/cat.a.b}` is refused, naming the fields to write instead, as
|
`{/cat.a.b}` is refused, naming the fields to write instead, as
|
||||||
[One draw, one spelling](#one-draw-one-spelling) refuses that pair inside a single
|
[One draw, one spelling](#one-draw-one-spelling) refuses that pair inside a single
|
||||||
format. A column may not reference the record it belongs to by any spelling: `{/users.first}`
|
format. Both fences run at load, so a category that loads renders as either shape. A
|
||||||
or a bare `{/users}` inside `users` describes a draw other than the columns beside
|
category never references itself — `{/users.first}` or a bare `{/users}` inside `users`
|
||||||
it, so put a value two columns share in its own category and reference that. A field hold, transform or
|
describes a draw other than the fields beside it — so read a sibling as a path, and put
|
||||||
|
a value two fields share in its own category and reference that. A field hold, transform or
|
||||||
operand ties fields together within one column as always (see
|
operand ties fields together within one column as always (see
|
||||||
[Correlated fields](#correlated-fields) and [Decisions](#decisions)).
|
[Correlated fields](#correlated-fields) and [Decisions](#decisions)).
|
||||||
|
|
||||||
@@ -447,9 +448,9 @@ a render of its own, in no group, so it draws anew, and a [draw group](#draw-gro
|
|||||||
draw apart. A bare reference names no field and makes its own picks each time —
|
draw apart. A bare reference names no field and makes its own picks each time —
|
||||||
`{/misc.uuid} {/misc.uuid}` is two draws — while the reference paths inside what it
|
`{/misc.uuid} {/misc.uuid}` is two draws — while the reference paths inside what it
|
||||||
renders still read the render's draws. Rejected at `New`: a path that is
|
renders still read the render's draws. Rejected at `New`: a path that is
|
||||||
unknown, names a folder, has no folder above, or reads a field not every variant
|
unknown, names a folder, has no folder above, reads a field not every variant
|
||||||
of a choice carries, and a reference that leads back to its own value, directly,
|
of a choice carries, or names the category the reference sits in, and a reference
|
||||||
mutually or through a chain.
|
that leads back to its own value, directly, mutually or through a chain.
|
||||||
|
|
||||||
### Draw group
|
### Draw group
|
||||||
|
|
||||||
@@ -690,6 +691,14 @@ tokens add cost in proportion to the output.
|
|||||||
different things — an operand pins the value its own render produced and stops at a
|
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
|
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.
|
rules and both scopes anyway, and tell them apart at every step.
|
||||||
|
- **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
|
||||||
|
the one spelling for a field of one's own. A value two fields share goes in its own
|
||||||
|
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.
|
||||||
- **A record's column set is fixed before the first draw.** Only a category-level
|
- **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
|
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
|
choice, errors. A tail may pass through a choice whose variants carry different
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ func (c *drawCheck) checkDraws(path string, n node) error {
|
|||||||
if !ok || !c.readsPath(t) {
|
if !ok || !c.readsPath(t) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
w := newDrawWalk(nil)
|
w := newDrawWalk()
|
||||||
for _, e := range renderEdges(t) {
|
for _, e := range renderEdges(t) {
|
||||||
w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}})
|
w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}})
|
||||||
}
|
}
|
||||||
@@ -143,6 +143,23 @@ func (c *drawCheck) checkDraws(path string, n node) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkRecordDraws fences the columns of a record — a template compiled at the top without a
|
||||||
|
// repeat — so a load proves the record view of it as well as the string view.
|
||||||
|
func (c *drawCheck) checkRecordDraws(path string, n node) error {
|
||||||
|
t, ok := n.(*template)
|
||||||
|
if !ok || !t.record {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
columns := recordColumns(t)
|
||||||
|
if len(columns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := checkColumnDraws(t, columns); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// readsPath reports whether rendering n reads a reference path, short of a repeat, which renders
|
// readsPath reports whether rendering n reads a reference path, short of a repeat, which renders
|
||||||
// over draws of its own.
|
// over draws of its own.
|
||||||
func (c *drawCheck) readsPath(n node) bool {
|
func (c *drawCheck) readsPath(n node) bool {
|
||||||
@@ -206,7 +223,7 @@ func refRead(n node, label string) (arm, node, bool) {
|
|||||||
|
|
||||||
// checkColumnDraws fences a record's columns as one render.
|
// checkColumnDraws fences a record's columns as one render.
|
||||||
func checkColumnDraws(t *template, columns []string) error {
|
func checkColumnDraws(t *template, columns []string) error {
|
||||||
w := newDrawWalk(t)
|
w := newDrawWalk()
|
||||||
for _, name := range columns {
|
for _, name := range columns {
|
||||||
w.walk(t.fields[name], drawAt{group: t.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}})
|
w.walk(t.fields[name], drawAt{group: t.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}})
|
||||||
}
|
}
|
||||||
@@ -214,14 +231,12 @@ func checkColumnDraws(t *template, columns []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// drawWalk gathers what one render reads by reference and what it draws afresh, each by draw group,
|
// drawWalk gathers what one render reads by reference and what it draws afresh, each by draw group,
|
||||||
// for check to compare. record is set for a record's columns, which may not read the record back.
|
// for check to compare.
|
||||||
type drawWalk struct {
|
type drawWalk struct {
|
||||||
record *template
|
|
||||||
reads []pathRead
|
reads []pathRead
|
||||||
read map[drawKey]bool
|
read map[drawKey]bool
|
||||||
fresh []freshDraw
|
fresh []freshDraw
|
||||||
seen map[drawVisit]bool
|
seen map[drawVisit]bool
|
||||||
err error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, how the render's root reached it, the
|
||||||
@@ -259,15 +274,15 @@ type drawKey struct {
|
|||||||
key any
|
key any
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDrawWalk(record *template) *drawWalk {
|
func newDrawWalk() *drawWalk {
|
||||||
return &drawWalk{record: record, read: map[drawKey]bool{}, seen: map[drawVisit]bool{}}
|
return &drawWalk{read: map[drawKey]bool{}, seen: map[drawVisit]bool{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, at.held}
|
||||||
if w.seen[v] || w.err != nil {
|
if w.seen[v] {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.seen[v] = true
|
w.seen[v] = true
|
||||||
@@ -287,13 +302,9 @@ 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)
|
a, target, reads := refRead(from, e.label)
|
||||||
switch {
|
if !reads {
|
||||||
case !reads:
|
|
||||||
w.walk(e.to, at)
|
w.walk(e.to, at)
|
||||||
return
|
return
|
||||||
case w.record != nil && target == node(w.record):
|
|
||||||
w.err = fmt.Errorf("%s reads {%s}, which points back at this record; a column cannot read another column — move the shared value into its own category and reference that", at.route.spelling, a.name)
|
|
||||||
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
|
||||||
@@ -306,9 +317,6 @@ func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
|
|||||||
// 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, reads compared in path order so
|
||||||
// which pair is reported does not vary.
|
// which pair is reported does not vary.
|
||||||
func (w *drawWalk) check() error {
|
func (w *drawWalk) check() error {
|
||||||
if w.err != nil {
|
|
||||||
return w.err
|
|
||||||
}
|
|
||||||
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 {
|
||||||
return w.reads[i].at.group < w.reads[j].at.group
|
return w.reads[i].at.group < w.reads[j].at.group
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ func checkRenders(s nodeScope) error {
|
|||||||
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return s((&valueProof{}).checkDatatype)
|
return s((&valueProof{}).checkDatatype)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,6 +164,6 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error {
|
|||||||
return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", path, name, rest)
|
return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", path, name, rest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return linkTemplateRefs(nil, path, t, root)
|
return linkTemplateRefs(nil, path, "", t, root)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -211,9 +211,6 @@ func recordOf(n node) (*template, []Column, error) {
|
|||||||
if !t.record {
|
if !t.record {
|
||||||
return nil, nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat)
|
return nil, nil, fmt.Errorf("carries repeat %d, which composes its format into one string; a record projects columns instead — drop the repeat and render the record again for more rows", t.repeat)
|
||||||
}
|
}
|
||||||
if err := checkColumnDraws(t, names); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
columns := make([]Column, len(names))
|
columns := make([]Column, len(names))
|
||||||
for i, name := range names {
|
for i, name := range names {
|
||||||
datatype, _ := columnDatatype(t.fields[name]) // checkColumns refused items that disagree wherever DataType is read
|
datatype, _ := columnDatatype(t.fields[name]) // checkColumns refused items that disagree wherever DataType is read
|
||||||
|
|||||||
+11
-6
@@ -72,15 +72,17 @@ func refSegments(name string, folder []string) ([]string, error) {
|
|||||||
// error, never a random render-time one.
|
// error, never a random render-time one.
|
||||||
func linkRefs(root map[string]node) error {
|
func linkRefs(root map[string]node) error {
|
||||||
return eachTemplate(root, func(folder []string, path string, t *template) error {
|
return eachTemplate(root, func(folder []string, path string, t *template) error {
|
||||||
t.keyDrawGroup(strings.Join(strings.Split(path, ".")[:len(folder)+1], "."))
|
category := strings.Join(strings.Split(path, ".")[:len(folder)+1], ".")
|
||||||
return linkTemplateRefs(folder, path, t, root)
|
t.keyDrawGroup(category)
|
||||||
|
return linkTemplateRefs(folder, path, category, t, root)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkTemplateRefs binds one template's references against root. A template with
|
// linkTemplateRefs binds one template's references against root, refusing one that names the
|
||||||
// none is left untouched, so an inline format that references nothing costs only
|
// category it sits in: a category is a unit, and a reference back into it describes a draw other
|
||||||
// the refTokens scan.
|
// than the fields beside it. A template with no reference is left untouched, so an inline format
|
||||||
func linkTemplateRefs(folder []string, path string, t *template, root map[string]node) error {
|
// that references nothing costs only the refTokens scan.
|
||||||
|
func linkTemplateRefs(folder []string, path, category string, t *template, root map[string]node) error {
|
||||||
names := refTokens(t.format)
|
names := refTokens(t.format)
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -99,6 +101,9 @@ func linkTemplateRefs(folder []string, path string, t *template, root map[string
|
|||||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||||
}
|
}
|
||||||
key := "/" + strings.Join(head, ".")
|
key := "/" + strings.Join(head, ".")
|
||||||
|
if category != "" && key == "/"+category {
|
||||||
|
return fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", path, name)
|
||||||
|
}
|
||||||
if err := checkPath(target, tail, key); err != nil {
|
if err := checkPath(target, tail, key); err != nil {
|
||||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user