diff --git a/README.md b/README.md index 5d7c637..316ca77 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/draw.go b/draw.go index 272d91b..a592dc5 100644 --- a/draw.go +++ b/draw.go @@ -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 diff --git a/graph.go b/graph.go index 8c96e90..a32a965 100644 --- a/graph.go +++ b/graph.go @@ -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) } diff --git a/inline.go b/inline.go index eb8e735..f37a007 100644 --- a/inline.go +++ b/inline.go @@ -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) }) } diff --git a/record.go b/record.go index 5c64142..748338c 100644 --- a/record.go +++ b/record.go @@ -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 diff --git a/reference.go b/reference.go index 31ce49f..2fea914 100644 --- a/reference.go +++ b/reference.go @@ -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) }