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
|
||||
`{/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
|
||||
format. A column may not reference the record it belongs to by any spelling: `{/users.first}`
|
||||
or a bare `{/users}` inside `users` describes a draw other than the columns beside
|
||||
it, so put a value two columns share in its own category and reference that. A field hold, transform or
|
||||
format. Both fences run at load, so a category that loads renders as either shape. A
|
||||
category never references itself — `{/users.first}` or a bare `{/users}` inside `users`
|
||||
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
|
||||
[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 —
|
||||
`{/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
|
||||
unknown, names a folder, has no folder above, or reads a field not every variant
|
||||
of a choice carries, and a reference that leads back to its own value, directly,
|
||||
mutually or through a chain.
|
||||
unknown, names a folder, has no folder above, reads a field not every variant
|
||||
of a choice carries, or names the category the reference sits in, and a reference
|
||||
that leads back to its own value, directly, mutually or through a chain.
|
||||
|
||||
### 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
|
||||
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 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
|
||||
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
|
||||
|
||||
@@ -133,7 +133,7 @@ func (c *drawCheck) checkDraws(path string, n node) error {
|
||||
if !ok || !c.readsPath(t) {
|
||||
return nil
|
||||
}
|
||||
w := newDrawWalk(nil)
|
||||
w := newDrawWalk()
|
||||
for _, e := range renderEdges(t) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// over draws of its own.
|
||||
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.
|
||||
func checkColumnDraws(t *template, columns []string) error {
|
||||
w := newDrawWalk(t)
|
||||
w := newDrawWalk()
|
||||
for _, name := range columns {
|
||||
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,
|
||||
// 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 {
|
||||
record *template
|
||||
reads []pathRead
|
||||
read map[drawKey]bool
|
||||
fresh []freshDraw
|
||||
seen map[drawVisit]bool
|
||||
err error
|
||||
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
|
||||
@@ -259,15 +274,15 @@ type drawKey struct {
|
||||
key any
|
||||
}
|
||||
|
||||
func newDrawWalk(record *template) *drawWalk {
|
||||
return &drawWalk{record: record, read: map[drawKey]bool{}, seen: map[drawVisit]bool{}}
|
||||
func newDrawWalk() *drawWalk {
|
||||
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
|
||||
// there.
|
||||
func (w *drawWalk) walk(n node, at drawAt) {
|
||||
v := drawVisit{n, at.group, at.held}
|
||||
if w.seen[v] || w.err != nil {
|
||||
if w.seen[v] {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
a, target, reads := refRead(from, e.label)
|
||||
switch {
|
||||
case !reads:
|
||||
if !reads {
|
||||
w.walk(e.to, at)
|
||||
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] {
|
||||
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
|
||||
// which pair is reported does not vary.
|
||||
func (w *drawWalk) check() error {
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
sort.SliceStable(w.reads, func(i, j int) bool {
|
||||
if 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 {
|
||||
return err
|
||||
}
|
||||
if err := s(fence.checkRecordDraws); err != nil {
|
||||
return err
|
||||
}
|
||||
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 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 {
|
||||
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))
|
||||
for i, name := range names {
|
||||
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.
|
||||
func linkRefs(root map[string]node) error {
|
||||
return eachTemplate(root, func(folder []string, path string, t *template) error {
|
||||
t.keyDrawGroup(strings.Join(strings.Split(path, ".")[:len(folder)+1], "."))
|
||||
return linkTemplateRefs(folder, path, t, root)
|
||||
category := strings.Join(strings.Split(path, ".")[:len(folder)+1], ".")
|
||||
t.keyDrawGroup(category)
|
||||
return linkTemplateRefs(folder, path, category, t, root)
|
||||
})
|
||||
}
|
||||
|
||||
// linkTemplateRefs binds one template's references against root. A template with
|
||||
// none is left untouched, so an inline format that references nothing costs only
|
||||
// the refTokens scan.
|
||||
func linkTemplateRefs(folder []string, path string, t *template, root map[string]node) error {
|
||||
// linkTemplateRefs binds one template's references against root, refusing one that names the
|
||||
// category it sits in: a category is a unit, and a reference back into it describes a draw other
|
||||
// than the fields beside it. A template with no reference is left untouched, so an inline format
|
||||
// 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)
|
||||
if len(names) == 0 {
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
return fmt.Errorf("%s: reference {%s}: %w", path, name, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user