Split hold and draw so a file name says load or render
This commit is contained in:
@@ -1328,8 +1328,10 @@ record.go records: Record, the JSON/CSV/SQL serializers, and their entry p
|
|||||||
struct.go structs: FakeStruct, fake tags, and a field's Go type as its column's datatype
|
struct.go structs: FakeStruct, fake tags, and a field's Go type as its column's datatype
|
||||||
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
|
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
|
||||||
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
|
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
|
||||||
hold.go the hold: one draw per expansion for paths and operands, the rows a render pins, and its fences
|
hold.go the hold at render: one draw per expansion for paths and operands, and the rows it pins
|
||||||
draw.go one reference draw per render and group: draw sets, the group option, and its fence
|
holdfence.go the load fences over the hold: the routes and spellings that read one held name two ways
|
||||||
|
draw.go one reference draw per render and group: the draw set a render reads its paths through
|
||||||
|
drawfence.go the load fences over reference draws: the drawGroup option, and the walk over a render's reads
|
||||||
family.go a family of linked tables: what a reference path reads of one, and the fence over paths into it
|
family.go a family of linked tables: what a reference path reads of one, and the fence over paths into it
|
||||||
reference.go reference sigils, and binding references across the tree
|
reference.go reference sigils, and binding references across the tree
|
||||||
graph.go the render graph: edges, cycles, the repeat bound, tree walks
|
graph.go the render graph: edges, cycles, the repeat bound, tree walks
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package fejkdata
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,421 +55,3 @@ func (sc drawScope) draws(s *session) *draws {
|
|||||||
}
|
}
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawGroupOf reads a template's "drawGroup" (default ""), which a repeat cannot carry: each
|
|
||||||
// iteration renders in no draw group.
|
|
||||||
func drawGroupOf(m map[string]any, repeat int) (string, error) {
|
|
||||||
v, ok := m["drawGroup"]
|
|
||||||
if !ok {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
name, ok := v.(string)
|
|
||||||
switch {
|
|
||||||
case !ok:
|
|
||||||
return "", fmt.Errorf("drawGroup must be a string, got %T", v)
|
|
||||||
case name == "":
|
|
||||||
return "", fmt.Errorf(`drawGroup "" is the default, so it has no effect; drop it`)
|
|
||||||
case repeat > 1:
|
|
||||||
return "", fmt.Errorf("drawGroup %q on a repeat names nothing, since each iteration is a render of its own; drop it", name)
|
|
||||||
}
|
|
||||||
return name, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// keyDrawGroup keys t's draw group by the category t sits in, "" for an inline template, so a name
|
|
||||||
// is local to its category.
|
|
||||||
func (t *template) keyDrawGroup(category string) {
|
|
||||||
if t.drawGroup != "" {
|
|
||||||
t.drawGroupKey = category + "/" + t.drawGroup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNestedDrawGroup refuses a template beneath one drawing in group that names group again,
|
|
||||||
// short of a repeat or another draw group.
|
|
||||||
func checkNestedDrawGroup(fields map[string]node, group string) error {
|
|
||||||
if group == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var walk func(path string, n node) error
|
|
||||||
walk = func(path string, n node) error {
|
|
||||||
t, isTemplate := n.(*template)
|
|
||||||
switch {
|
|
||||||
case isTemplate && t.drawGroup == group:
|
|
||||||
return fmt.Errorf("%q names drawGroup %q, the draw group this template draws in already; drop it", path, group)
|
|
||||||
case isTemplate && (t.drawGroup != "" || t.repeat > 1):
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, c := range contained(n) {
|
|
||||||
if err := walk(join(path, c.name), c.node); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, name := range sortedNames(fields) {
|
|
||||||
if err := walk(name, fields[name]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawCheck fences each template of a scope as a render of its own, remembering which nodes read a
|
|
||||||
// reference path.
|
|
||||||
type drawCheck struct {
|
|
||||||
memo map[readsMemo]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type readsMemo struct {
|
|
||||||
n node
|
|
||||||
stopAtGroup bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkDrawGroup refuses a draw group that splits nothing: one whose render reads every reference
|
|
||||||
// path inside a repeat or a nested draw group, which draw apart from it whatever it names.
|
|
||||||
func (c *drawCheck) checkDrawGroup(path string, n node) error {
|
|
||||||
if t, ok := n.(*template); ok && t.drawGroup != "" && !c.splitsDraws(t) {
|
|
||||||
return fmt.Errorf("%s: drawGroup %q splits nothing, since nothing it renders reads a reference path outside a repeat or a nested drawGroup; drop it", path, t.drawGroup)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *drawCheck) checkDraws(path string, n node) error {
|
|
||||||
t, ok := n.(*template)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := checkOwnFamily(t); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
|
||||||
}
|
|
||||||
if !c.readsPath(t) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
w := newDrawWalk()
|
|
||||||
for _, e := range renderEdges(t) {
|
|
||||||
w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}})
|
|
||||||
}
|
|
||||||
if err := w.check(); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkOwnFamily refuses a table's format or cell that reads, however many templates
|
|
||||||
// away and through a repeat or a draw group too, a table of its own family: a row
|
|
||||||
// rendered whole draws its row without pinning it, so the family would draw apart
|
|
||||||
// from the row being rendered, whichever draws the reaching template holds.
|
|
||||||
func checkOwnFamily(t *template) error {
|
|
||||||
own := t.table
|
|
||||||
if own == nil {
|
|
||||||
own = t.cellOf
|
|
||||||
}
|
|
||||||
if own == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
seen := map[node]bool{}
|
|
||||||
var find func(n node) (renderEdge, *table, bool)
|
|
||||||
find = func(n node) (renderEdge, *table, bool) {
|
|
||||||
if seen[n] {
|
|
||||||
return renderEdge{}, nil, false
|
|
||||||
}
|
|
||||||
seen[n] = true
|
|
||||||
for _, e := range renderEdges(n) {
|
|
||||||
if a, isRef := refRead(n, e.label); isRef {
|
|
||||||
if head, isTable := n.(*template).fields[a.key].(*table); isTable && head.family() == own.family() {
|
|
||||||
return e, head, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e, head, found := find(e.to); found {
|
|
||||||
return e, head, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return renderEdge{}, nil, false
|
|
||||||
}
|
|
||||||
if e, head, found := find(t); found {
|
|
||||||
return fmt.Errorf("%s reads %s, a table of its own family, which a row of %s rendered whole would draw apart from; read the family from a template beside it, or add the value as a column", e.reached(), head.category, own.category)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
// A record-only template's format renders nothing, so weigh the columns, not the format.
|
|
||||||
columns := recordColumns(t)
|
|
||||||
reads := false
|
|
||||||
for _, name := range columns {
|
|
||||||
reads = reads || c.readsPath(t.fields[name])
|
|
||||||
}
|
|
||||||
if !reads {
|
|
||||||
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 { return c.reads(n, false) }
|
|
||||||
|
|
||||||
// splitsDraws reports whether rendering n reads a reference path that n's own draw group answers
|
|
||||||
// for: one outside a repeat and outside a nested draw group, which hold their own draws.
|
|
||||||
func (c *drawCheck) splitsDraws(n node) bool { return c.reads(n, true) }
|
|
||||||
|
|
||||||
// reads walks what rendering n renders for a reference path, stopping at a repeat — and at a nested
|
|
||||||
// draw group when stopAtGroup — since each holds draws of its own.
|
|
||||||
func (c *drawCheck) reads(n node, stopAtGroup bool) bool {
|
|
||||||
k := readsMemo{n, stopAtGroup}
|
|
||||||
if r, done := c.memo[k]; done {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
r := false
|
|
||||||
for _, e := range renderEdges(n) {
|
|
||||||
if a, isRef := refRead(n, e.label); (isRef && (len(a.tail) > 0 || readsTable(n, a))) || (!repeats(e.to) && !(stopAtGroup && grouped(e.to)) && c.reads(e.to, stopAtGroup)) {
|
|
||||||
r = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c.memo == nil {
|
|
||||||
c.memo = map[readsMemo]bool{}
|
|
||||||
}
|
|
||||||
c.memo[k] = r
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func repeats(n node) bool {
|
|
||||||
t, isTemplate := n.(*template)
|
|
||||||
return isTemplate && t.repeat > 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func grouped(n node) bool {
|
|
||||||
t, isTemplate := n.(*template)
|
|
||||||
return isTemplate && t.drawGroupKey != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// readsTable reports whether a reference of n names a table, whose draw the render's
|
|
||||||
// group answers for even when read whole.
|
|
||||||
func readsTable(n node, a arm) bool {
|
|
||||||
t, isTemplate := n.(*template)
|
|
||||||
if !isTemplate {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, isTable := t.fields[a.key].(*table)
|
|
||||||
return isTable
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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{}, false
|
|
||||||
}
|
|
||||||
a := splitArm(label, t.refs)
|
|
||||||
return a, isRef(a.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkColumnDraws fences a record's columns as one render.
|
|
||||||
func checkColumnDraws(t *template, columns []string) error {
|
|
||||||
w := newDrawWalk()
|
|
||||||
for _, name := range columns {
|
|
||||||
w.walk(t.fields[name], drawAt{group: t.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}})
|
|
||||||
}
|
|
||||||
return w.check()
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawWalk gathers the references one render reads, by draw group, for check to compare.
|
|
||||||
type drawWalk struct {
|
|
||||||
reads []pathRead
|
|
||||||
read map[drawKey]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
|
|
||||||
// table rows it entered, and the table a whole read draws a row of.
|
|
||||||
type drawAt struct {
|
|
||||||
group string
|
|
||||||
route drawRoute
|
|
||||||
alt rowSet
|
|
||||||
drawn *table // left out of the visit keys: only this table's own cells compare against it, which the own-family fence keeps true
|
|
||||||
}
|
|
||||||
|
|
||||||
// rowSet stands in at load for the rows (*draws).pin holds at render: a path read enters a row with
|
|
||||||
// its ancestors, a whole read enters the drawn row alone.
|
|
||||||
type rowSet []tablePin
|
|
||||||
|
|
||||||
func (s rowSet) rowOf(t *table) (int, bool) {
|
|
||||||
for _, p := range s {
|
|
||||||
if p.t == t {
|
|
||||||
return p.row, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// enter is s with row r of t, and with t's ancestors where the render pins that row rather than
|
|
||||||
// drawing it; the set is copied, since walks branch.
|
|
||||||
func (s rowSet) enter(t *table, r int, pins bool) rowSet {
|
|
||||||
if _, in := s.rowOf(t); in {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
out := append(make(rowSet, 0, len(s)+1), s...)
|
|
||||||
for {
|
|
||||||
out = append(out, tablePin{t, r})
|
|
||||||
if !pins || t.parentT == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
t, r = t.parentT, t.parentRow(r)
|
|
||||||
if _, in := out.rowOf(t); in {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Slice(out, func(i, j int) bool { return out[i].t.path < out[j].t.path })
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// key spells the set for a map, by the tables' identities.
|
|
||||||
func (s rowSet) key() string {
|
|
||||||
var b strings.Builder
|
|
||||||
for _, p := range s {
|
|
||||||
fmt.Fprintf(&b, "%p[%d]", p.t, p.row)
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// alternatives reports whether two reads sit in different rows of one table.
|
|
||||||
func alternatives(a, b drawAt) bool {
|
|
||||||
for _, p := range a.alt {
|
|
||||||
if r, in := b.alt.rowOf(p.t); in && r != p.row {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
|
|
||||||
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
|
|
||||||
tr *tableRead // set where the reference names a table
|
|
||||||
}
|
|
||||||
|
|
||||||
type drawVisit struct {
|
|
||||||
n node
|
|
||||||
group string
|
|
||||||
alt string
|
|
||||||
}
|
|
||||||
|
|
||||||
type drawKey struct {
|
|
||||||
group string
|
|
||||||
path string
|
|
||||||
alt string
|
|
||||||
}
|
|
||||||
|
|
||||||
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.alt.key()}
|
|
||||||
if w.seen[v] {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.seen[v] = true
|
|
||||||
if repeats(n) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" {
|
|
||||||
at.group = t.drawGroupKey
|
|
||||||
}
|
|
||||||
if t, isTable := n.(*table); isTable {
|
|
||||||
at.drawn = t
|
|
||||||
}
|
|
||||||
for _, e := range renderEdges(n) {
|
|
||||||
if cell, isCell := e.to.(*template); isCell && cell.cellOf != nil {
|
|
||||||
if at.alt.excludes(cell) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
in := at
|
|
||||||
in.alt = at.alt.enter(cell.cellOf, cell.cellRow, cell.cellOf != at.drawn)
|
|
||||||
w.edge(n, e, in)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
w.edge(n, e, at)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// excludes reports whether a cell's row cannot render with the rows entered: another
|
|
||||||
// row of its table, or a row outside an entered ancestor's.
|
|
||||||
func (s rowSet) excludes(cell *template) bool {
|
|
||||||
for _, p := range s {
|
|
||||||
if p.t == cell.cellOf {
|
|
||||||
return p.row != cell.cellRow
|
|
||||||
}
|
|
||||||
if cell.cellOf.descends(p.t) && !cell.cellOf.under(cell.cellRow, p.t, p.row) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// edge records the reference an edge reads, then walks on with every row the read
|
|
||||||
// pins entered, so a selected row renders only its own cells.
|
|
||||||
func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
|
|
||||||
if a, reads := refRead(from, e.label); reads {
|
|
||||||
tr := tableReadOf(from.(*template).fields[a.key], a, e.to)
|
|
||||||
if k := (drawKey{at.group, a.path, at.alt.key()}); !w.read[k] {
|
|
||||||
w.read[k] = true
|
|
||||||
w.reads = append(w.reads, pathRead{at, a, tr})
|
|
||||||
}
|
|
||||||
if tr != nil {
|
|
||||||
tr.pins.each(func(t *table, r int) { at.alt = at.alt.enter(t, r, true) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.walk(e.to, at)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check refuses what one draw per reference path cannot answer for: a read of a level beside a path
|
|
||||||
// another read takes into it, and two reads of one table family that select different rows. 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 {
|
|
||||||
return w.reads[i].at.group < w.reads[j].at.group
|
|
||||||
}
|
|
||||||
return w.reads[i].a.path < w.reads[j].a.path
|
|
||||||
})
|
|
||||||
for i, level := range w.reads {
|
|
||||||
for _, into := range w.reads[i+1:] {
|
|
||||||
if into.at.group != level.at.group || alternatives(level.at, into.at) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) {
|
|
||||||
return overlapError(level.at.route, level.a.name, into)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return checkFamilies(w.reads)
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// spelled names the route, and the reference it reaches a draw by where its root edge is not that
|
|
||||||
// reference.
|
|
||||||
func (r drawRoute) spelled(ref string) string {
|
|
||||||
if ref == "" || ref == r.label {
|
|
||||||
return r.spelling
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s with {%s}", r.spelling, ref)
|
|
||||||
}
|
|
||||||
|
|||||||
+425
@@ -0,0 +1,425 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// drawGroupOf reads a template's "drawGroup" (default ""), which a repeat cannot carry: each
|
||||||
|
// iteration renders in no draw group.
|
||||||
|
func drawGroupOf(m map[string]any, repeat int) (string, error) {
|
||||||
|
v, ok := m["drawGroup"]
|
||||||
|
if !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
name, ok := v.(string)
|
||||||
|
switch {
|
||||||
|
case !ok:
|
||||||
|
return "", fmt.Errorf("drawGroup must be a string, got %T", v)
|
||||||
|
case name == "":
|
||||||
|
return "", fmt.Errorf(`drawGroup "" is the default, so it has no effect; drop it`)
|
||||||
|
case repeat > 1:
|
||||||
|
return "", fmt.Errorf("drawGroup %q on a repeat names nothing, since each iteration is a render of its own; drop it", name)
|
||||||
|
}
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyDrawGroup keys t's draw group by the category t sits in, "" for an inline template, so a name
|
||||||
|
// is local to its category.
|
||||||
|
func (t *template) keyDrawGroup(category string) {
|
||||||
|
if t.drawGroup != "" {
|
||||||
|
t.drawGroupKey = category + "/" + t.drawGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNestedDrawGroup refuses a template beneath one drawing in group that names group again,
|
||||||
|
// short of a repeat or another draw group.
|
||||||
|
func checkNestedDrawGroup(fields map[string]node, group string) error {
|
||||||
|
if group == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var walk func(path string, n node) error
|
||||||
|
walk = func(path string, n node) error {
|
||||||
|
t, isTemplate := n.(*template)
|
||||||
|
switch {
|
||||||
|
case isTemplate && t.drawGroup == group:
|
||||||
|
return fmt.Errorf("%q names drawGroup %q, the draw group this template draws in already; drop it", path, group)
|
||||||
|
case isTemplate && (t.drawGroup != "" || t.repeat > 1):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, c := range contained(n) {
|
||||||
|
if err := walk(join(path, c.name), c.node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, name := range sortedNames(fields) {
|
||||||
|
if err := walk(name, fields[name]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawCheck fences each template of a scope as a render of its own, remembering which nodes read a
|
||||||
|
// reference path.
|
||||||
|
type drawCheck struct {
|
||||||
|
memo map[readsMemo]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type readsMemo struct {
|
||||||
|
n node
|
||||||
|
stopAtGroup bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDrawGroup refuses a draw group that splits nothing: one whose render reads every reference
|
||||||
|
// path inside a repeat or a nested draw group, which draw apart from it whatever it names.
|
||||||
|
func (c *drawCheck) checkDrawGroup(path string, n node) error {
|
||||||
|
if t, ok := n.(*template); ok && t.drawGroup != "" && !c.splitsDraws(t) {
|
||||||
|
return fmt.Errorf("%s: drawGroup %q splits nothing, since nothing it renders reads a reference path outside a repeat or a nested drawGroup; drop it", path, t.drawGroup)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *drawCheck) checkDraws(path string, n node) error {
|
||||||
|
t, ok := n.(*template)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := checkOwnFamily(t); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
|
}
|
||||||
|
if !c.readsPath(t) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w := newDrawWalk()
|
||||||
|
for _, e := range renderEdges(t) {
|
||||||
|
w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}})
|
||||||
|
}
|
||||||
|
if err := w.check(); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOwnFamily refuses a table's format or cell that reads, however many templates
|
||||||
|
// away and through a repeat or a draw group too, a table of its own family: a row
|
||||||
|
// rendered whole draws its row without pinning it, so the family would draw apart
|
||||||
|
// from the row being rendered, whichever draws the reaching template holds.
|
||||||
|
func checkOwnFamily(t *template) error {
|
||||||
|
own := t.table
|
||||||
|
if own == nil {
|
||||||
|
own = t.cellOf
|
||||||
|
}
|
||||||
|
if own == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
seen := map[node]bool{}
|
||||||
|
var find func(n node) (renderEdge, *table, bool)
|
||||||
|
find = func(n node) (renderEdge, *table, bool) {
|
||||||
|
if seen[n] {
|
||||||
|
return renderEdge{}, nil, false
|
||||||
|
}
|
||||||
|
seen[n] = true
|
||||||
|
for _, e := range renderEdges(n) {
|
||||||
|
if a, isRef := refRead(n, e.label); isRef {
|
||||||
|
if head, isTable := n.(*template).fields[a.key].(*table); isTable && head.family() == own.family() {
|
||||||
|
return e, head, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e, head, found := find(e.to); found {
|
||||||
|
return e, head, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return renderEdge{}, nil, false
|
||||||
|
}
|
||||||
|
if e, head, found := find(t); found {
|
||||||
|
return fmt.Errorf("%s reads %s, a table of its own family, which a row of %s rendered whole would draw apart from; read the family from a template beside it, or add the value as a column", e.reached(), head.category, own.category)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// A record-only template's format renders nothing, so weigh the columns, not the format.
|
||||||
|
columns := recordColumns(t)
|
||||||
|
reads := false
|
||||||
|
for _, name := range columns {
|
||||||
|
reads = reads || c.readsPath(t.fields[name])
|
||||||
|
}
|
||||||
|
if !reads {
|
||||||
|
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 { return c.reads(n, false) }
|
||||||
|
|
||||||
|
// splitsDraws reports whether rendering n reads a reference path that n's own draw group answers
|
||||||
|
// for: one outside a repeat and outside a nested draw group, which hold their own draws.
|
||||||
|
func (c *drawCheck) splitsDraws(n node) bool { return c.reads(n, true) }
|
||||||
|
|
||||||
|
// reads walks what rendering n renders for a reference path, stopping at a repeat — and at a nested
|
||||||
|
// draw group when stopAtGroup — since each holds draws of its own.
|
||||||
|
func (c *drawCheck) reads(n node, stopAtGroup bool) bool {
|
||||||
|
k := readsMemo{n, stopAtGroup}
|
||||||
|
if r, done := c.memo[k]; done {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
r := false
|
||||||
|
for _, e := range renderEdges(n) {
|
||||||
|
if a, isRef := refRead(n, e.label); (isRef && (len(a.tail) > 0 || readsTable(n, a))) || (!repeats(e.to) && !(stopAtGroup && grouped(e.to)) && c.reads(e.to, stopAtGroup)) {
|
||||||
|
r = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.memo == nil {
|
||||||
|
c.memo = map[readsMemo]bool{}
|
||||||
|
}
|
||||||
|
c.memo[k] = r
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func repeats(n node) bool {
|
||||||
|
t, isTemplate := n.(*template)
|
||||||
|
return isTemplate && t.repeat > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func grouped(n node) bool {
|
||||||
|
t, isTemplate := n.(*template)
|
||||||
|
return isTemplate && t.drawGroupKey != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// readsTable reports whether a reference of n names a table, whose draw the render's
|
||||||
|
// group answers for even when read whole.
|
||||||
|
func readsTable(n node, a arm) bool {
|
||||||
|
t, isTemplate := n.(*template)
|
||||||
|
if !isTemplate {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, isTable := t.fields[a.key].(*table)
|
||||||
|
return isTable
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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{}, false
|
||||||
|
}
|
||||||
|
a := splitArm(label, t.refs)
|
||||||
|
return a, isRef(a.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkColumnDraws fences a record's columns as one render.
|
||||||
|
func checkColumnDraws(t *template, columns []string) error {
|
||||||
|
w := newDrawWalk()
|
||||||
|
for _, name := range columns {
|
||||||
|
w.walk(t.fields[name], drawAt{group: t.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}})
|
||||||
|
}
|
||||||
|
return w.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawWalk gathers the references one render reads, by draw group, for check to compare.
|
||||||
|
type drawWalk struct {
|
||||||
|
reads []pathRead
|
||||||
|
read map[drawKey]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
|
||||||
|
// table rows it entered, and the table a whole read draws a row of.
|
||||||
|
type drawAt struct {
|
||||||
|
group string
|
||||||
|
route drawRoute
|
||||||
|
alt rowSet
|
||||||
|
drawn *table // left out of the visit keys: only this table's own cells compare against it, which the own-family fence keeps true
|
||||||
|
}
|
||||||
|
|
||||||
|
// rowSet stands in at load for the rows (*draws).pin holds at render: a path read enters a row with
|
||||||
|
// its ancestors, a whole read enters the drawn row alone.
|
||||||
|
type rowSet []tablePin
|
||||||
|
|
||||||
|
func (s rowSet) rowOf(t *table) (int, bool) {
|
||||||
|
for _, p := range s {
|
||||||
|
if p.t == t {
|
||||||
|
return p.row, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// enter is s with row r of t, and with t's ancestors where the render pins that row rather than
|
||||||
|
// drawing it; the set is copied, since walks branch.
|
||||||
|
func (s rowSet) enter(t *table, r int, pins bool) rowSet {
|
||||||
|
if _, in := s.rowOf(t); in {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
out := append(make(rowSet, 0, len(s)+1), s...)
|
||||||
|
for {
|
||||||
|
out = append(out, tablePin{t, r})
|
||||||
|
if !pins || t.parentT == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t, r = t.parentT, t.parentRow(r)
|
||||||
|
if _, in := out.rowOf(t); in {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].t.path < out[j].t.path })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// key spells the set for a map, by the tables' identities.
|
||||||
|
func (s rowSet) key() string {
|
||||||
|
var b strings.Builder
|
||||||
|
for _, p := range s {
|
||||||
|
fmt.Fprintf(&b, "%p[%d]", p.t, p.row)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// alternatives reports whether two reads sit in different rows of one table.
|
||||||
|
func alternatives(a, b drawAt) bool {
|
||||||
|
for _, p := range a.alt {
|
||||||
|
if r, in := b.alt.rowOf(p.t); in && r != p.row {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawRoute is how a render reaches a draw: as its author spells it, and the root edge's label.
|
||||||
|
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
|
||||||
|
tr *tableRead // set where the reference names a table
|
||||||
|
}
|
||||||
|
|
||||||
|
type drawVisit struct {
|
||||||
|
n node
|
||||||
|
group string
|
||||||
|
alt string
|
||||||
|
}
|
||||||
|
|
||||||
|
type drawKey struct {
|
||||||
|
group string
|
||||||
|
path string
|
||||||
|
alt string
|
||||||
|
}
|
||||||
|
|
||||||
|
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.alt.key()}
|
||||||
|
if w.seen[v] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.seen[v] = true
|
||||||
|
if repeats(n) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" {
|
||||||
|
at.group = t.drawGroupKey
|
||||||
|
}
|
||||||
|
if t, isTable := n.(*table); isTable {
|
||||||
|
at.drawn = t
|
||||||
|
}
|
||||||
|
for _, e := range renderEdges(n) {
|
||||||
|
if cell, isCell := e.to.(*template); isCell && cell.cellOf != nil {
|
||||||
|
if at.alt.excludes(cell) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
in := at
|
||||||
|
in.alt = at.alt.enter(cell.cellOf, cell.cellRow, cell.cellOf != at.drawn)
|
||||||
|
w.edge(n, e, in)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
w.edge(n, e, at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// excludes reports whether a cell's row cannot render with the rows entered: another
|
||||||
|
// row of its table, or a row outside an entered ancestor's.
|
||||||
|
func (s rowSet) excludes(cell *template) bool {
|
||||||
|
for _, p := range s {
|
||||||
|
if p.t == cell.cellOf {
|
||||||
|
return p.row != cell.cellRow
|
||||||
|
}
|
||||||
|
if cell.cellOf.descends(p.t) && !cell.cellOf.under(cell.cellRow, p.t, p.row) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// edge records the reference an edge reads, then walks on with every row the read
|
||||||
|
// pins entered, so a selected row renders only its own cells.
|
||||||
|
func (w *drawWalk) edge(from node, e renderEdge, at drawAt) {
|
||||||
|
if a, reads := refRead(from, e.label); reads {
|
||||||
|
tr := tableReadOf(from.(*template).fields[a.key], a, e.to)
|
||||||
|
if k := (drawKey{at.group, a.path, at.alt.key()}); !w.read[k] {
|
||||||
|
w.read[k] = true
|
||||||
|
w.reads = append(w.reads, pathRead{at, a, tr})
|
||||||
|
}
|
||||||
|
if tr != nil {
|
||||||
|
tr.pins.each(func(t *table, r int) { at.alt = at.alt.enter(t, r, true) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.walk(e.to, at)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check refuses what one draw per reference path cannot answer for: a read of a level beside a path
|
||||||
|
// another read takes into it, and two reads of one table family that select different rows. 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 {
|
||||||
|
return w.reads[i].at.group < w.reads[j].at.group
|
||||||
|
}
|
||||||
|
return w.reads[i].a.path < w.reads[j].a.path
|
||||||
|
})
|
||||||
|
for i, level := range w.reads {
|
||||||
|
for _, into := range w.reads[i+1:] {
|
||||||
|
if into.at.group != level.at.group || alternatives(level.at, into.at) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(into.a.path, level.a.path+".") && !(level.tr != nil && level.tr.whole) {
|
||||||
|
return overlapError(level.at.route, level.a.name, into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checkFamilies(w.reads)
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// spelled names the route, and the reference it reaches a draw by where its root edge is not that
|
||||||
|
// reference.
|
||||||
|
func (r drawRoute) spelled(ref string) string {
|
||||||
|
if ref == "" || ref == r.label {
|
||||||
|
return r.spelling
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s with {%s}", r.spelling, ref)
|
||||||
|
}
|
||||||
@@ -3,258 +3,8 @@ package fejkdata
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// heldCheck rejects every route to a held sibling name except the ones that read its
|
|
||||||
// draw. An expansion holds one draw of that name; anything else that renders it draws
|
|
||||||
// again, and the two disagree. checkNoOverlap settles the spellings within one format
|
|
||||||
// (a token, an operand); this settles the rest — a reference, whether it sits in that
|
|
||||||
// format or in anything the format renders, however deep. A reference path is held
|
|
||||||
// for the whole render instead, which drawCheck fences.
|
|
||||||
func heldCheck(path string, n node) error {
|
|
||||||
t, ok := n.(*template)
|
|
||||||
if !ok || len(t.held) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
readers := boundReaders(t.format, t.bound, t.refs)
|
|
||||||
for _, head := range heldHeads(t) {
|
|
||||||
if _, isPath := t.bound[head]; isPath && isRef(head) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := checkHeadHeld(t, head, readers); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// heldHeads lists a template's held names, operand heads first, then paths, each
|
|
||||||
// in name order: a level read both ways is reported by the operand's fence, and
|
|
||||||
// which overlap is reported does not vary.
|
|
||||||
func heldHeads(t *template) []string {
|
|
||||||
heads := make([]string, 0, len(t.held))
|
|
||||||
for head := range t.held {
|
|
||||||
heads = append(heads, head)
|
|
||||||
}
|
|
||||||
sort.Slice(heads, func(i, j int) bool {
|
|
||||||
_, pi := t.bound[heads[i]]
|
|
||||||
_, pj := t.bound[heads[j]]
|
|
||||||
if pi != pj {
|
|
||||||
return !pi
|
|
||||||
}
|
|
||||||
return heads[i] < heads[j]
|
|
||||||
})
|
|
||||||
return heads
|
|
||||||
}
|
|
||||||
|
|
||||||
// pinned collects what the hold of head answers for: a path pins the levels it
|
|
||||||
// passes through and the leaf it lands on, an operand exactly the value its render
|
|
||||||
// produces.
|
|
||||||
func pinned(t *template, head string, readers []reader) map[node]bool {
|
|
||||||
held := map[node]bool{}
|
|
||||||
if _, isPath := t.bound[head]; !isPath {
|
|
||||||
operandDraw(t.fields[head], held)
|
|
||||||
return held
|
|
||||||
}
|
|
||||||
for _, r := range readers {
|
|
||||||
if a := splitArm(r.name, t.refs); a.key == head {
|
|
||||||
coverPath(t.fields[head], a.tail, held)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return held
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkHeadHeld rejects every route to what head's hold pins except the readers
|
|
||||||
// holding it. One seen set across the edges: a node that cannot reach the level
|
|
||||||
// cannot reach it by another route either, so it is walked once.
|
|
||||||
func checkHeadHeld(t *template, head string, readers []reader) error {
|
|
||||||
held := pinned(t, head, readers)
|
|
||||||
if len(held) == 0 {
|
|
||||||
return nil // a fixed head holds nothing to reach
|
|
||||||
}
|
|
||||||
reader, isPath := t.bound[head]
|
|
||||||
seen := map[node]bool{}
|
|
||||||
for _, e := range renderEdges(t) {
|
|
||||||
if splitArm(e.label, t.refs).key == head || !renders(e.to, held, seen) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isPath {
|
|
||||||
return fmt.Errorf("%s renders %q, which {%s} reads a path into; name the fields you want instead", e.reached(), head, reader)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s renders %q, which a {%s()} also reads; reach it one way so it is drawn once", e.reached(), head, operandReader(t, head))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// operandReader names the builtin whose operand holds head.
|
|
||||||
func operandReader(t *template, head string) string {
|
|
||||||
fn := ""
|
|
||||||
_ = eachToken(t.format, func(tok ftoken) error {
|
|
||||||
if tok.kind != 'b' || fn != "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if name, _, isFunc := funcCall(tok.body); isFunc {
|
|
||||||
for _, operand := range tokenOperands(tok.body) {
|
|
||||||
if splitArm(operand, t.refs).key == head {
|
|
||||||
fn = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return fn
|
|
||||||
}
|
|
||||||
|
|
||||||
// coverPath collects what holding one path pins: every choice level the path
|
|
||||||
// passes through, whole, and the leaf it renders.
|
|
||||||
func coverPath(n node, tail []string, into map[node]bool) {
|
|
||||||
_, _ = walkPath(n, tail, pathWalk{
|
|
||||||
choice: func(c *choice, _ []string) ([]node, error) { cover(c, into, false); return nil, nil },
|
|
||||||
leaf: func(n node) error { cover(n, into, false); return nil },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// cover collects a level and everything contained in it. A fixed string outside a
|
|
||||||
// choice is left out — it cannot disagree with itself — but inside one each
|
|
||||||
// variant carries its own, so there it counts.
|
|
||||||
func cover(n node, into map[node]bool, inChoice bool) {
|
|
||||||
if isFixed(n) && !inChoice {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
into[n] = true
|
|
||||||
_, isChoice := n.(*choice)
|
|
||||||
for _, c := range contained(n) {
|
|
||||||
cover(c.node, into, inChoice || isChoice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// operandDraw collects what one held draw of an operand answers for: the operand
|
|
||||||
// and what rendering it settles inside itself. The builtin renders its operand
|
|
||||||
// whole, so that draw fixes every value the render produced, and a second route to
|
|
||||||
// any of them disagrees with it.
|
|
||||||
//
|
|
||||||
// The walk stops at a reference edge, which is where the operand's own value ends
|
|
||||||
// and a shared source begins: two names referencing one category are two draws, the
|
|
||||||
// same rule {word} {word} follows.
|
|
||||||
func operandDraw(n node, into map[node]bool) {
|
|
||||||
if isFixed(n) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if into[n] {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
into[n] = true
|
|
||||||
for _, e := range renderEdges(n) {
|
|
||||||
if isRef(e.label) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
operandDraw(e.to, into)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFixed is a string that varies nothing: fixed text with no fields to read into.
|
|
||||||
func isFixed(n node) bool {
|
|
||||||
t, ok := n.(*template)
|
|
||||||
return ok && t.fixed && len(t.fields) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// renders reports whether rendering n can reach anything in want, following the
|
|
||||||
// same edges expand does. seen keeps a node shared by several routes from being
|
|
||||||
// walked twice; checkNoCycles has already proved the graph is a DAG, so the walk
|
|
||||||
// ends.
|
|
||||||
func renders(n node, want, seen map[node]bool) bool {
|
|
||||||
if want[n] {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if seen[n] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
seen[n] = true
|
|
||||||
for _, e := range renderEdges(n) {
|
|
||||||
if renders(e.to, want, seen) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNoOverlap rejects a format that both renders a sibling level and reads a path
|
|
||||||
// into it — {p} beside {p.first}, {p.addr} beside {p.addr.city}. The path reads the
|
|
||||||
// level's held draw while rendering the level expands it afresh, so their values would
|
|
||||||
// disagree. Reads are compared in sorted order, so which pair is reported does not
|
|
||||||
// depend on where the tokens sit.
|
|
||||||
func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error {
|
|
||||||
names := boundReaders(format, bound, refs)
|
|
||||||
// Stable over one format-order scan, so two readers of one name (a token and a
|
|
||||||
// calc operand both naming "p") are reported as the format writes them.
|
|
||||||
sort.SliceStable(names, func(i, j int) bool { return names[i].path < names[j].path })
|
|
||||||
for i, level := range names {
|
|
||||||
for _, path := range names[i+1:] {
|
|
||||||
if strings.HasPrefix(path.path, level.path+".") {
|
|
||||||
return fmt.Errorf("%s renders a level that {%s} reads a path into; name the fields you want instead", level.label, path.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// reader is one way a format reaches a bound field: as written, by its one
|
|
||||||
// spelling, and how to name it.
|
|
||||||
type reader struct{ name, path, label string }
|
|
||||||
|
|
||||||
// boundReaders lists every way a format reaches a bound sibling field, in the order the
|
|
||||||
// format writes them. An operand renders its field, so it names a level exactly
|
|
||||||
// as a token does; one scan finds both, which is what puts them in one order.
|
|
||||||
func boundReaders(format string, bound map[string]string, refs map[string]refBinding) []reader {
|
|
||||||
var names []reader
|
|
||||||
_ = eachToken(format, func(t ftoken) error {
|
|
||||||
if t.kind != 'b' {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if fn, _, isFunc := funcCall(t.body); isFunc {
|
|
||||||
for _, operand := range tokenOperands(t.body) {
|
|
||||||
a := splitArm(operand, refs)
|
|
||||||
if _, isBound := bound[a.key]; isBound && !isRef(a.key) {
|
|
||||||
names = append(names, reader{a.name, a.path, fmt.Sprintf("%s operand %q", fn, operand)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, a := range splitArms(t.body, refs) {
|
|
||||||
if _, isBound := bound[a.key]; isBound && !isRef(a.key) {
|
|
||||||
names = append(names, reader{a.name, a.path, "token {" + a.name + "}"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside
|
|
||||||
// {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The
|
|
||||||
// error names the single-token spelling.
|
|
||||||
func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding) error {
|
|
||||||
count := map[string]int{}
|
|
||||||
return eachToken(format, func(t ftoken) error {
|
|
||||||
if t.kind != 'b' {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, _, isFunc := funcCall(t.body); isFunc {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, a := range splitArms(t.body, refs) {
|
|
||||||
if len(a.tail) > 0 || !c.held[a.key] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if count[a.key]++; count[a.key] > 1 {
|
|
||||||
return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.name, c.holder[a.key], a.key, a.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// tablePin is one table's pinned row.
|
// tablePin is one table's pinned row.
|
||||||
type tablePin struct {
|
type tablePin struct {
|
||||||
t *table
|
t *table
|
||||||
|
|||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// heldCheck rejects every route to a held sibling name except the ones that read its
|
||||||
|
// draw. An expansion holds one draw of that name; anything else that renders it draws
|
||||||
|
// again, and the two disagree. checkNoOverlap settles the spellings within one format
|
||||||
|
// (a token, an operand); this settles the rest — a reference, whether it sits in that
|
||||||
|
// format or in anything the format renders, however deep. A reference path is held
|
||||||
|
// for the whole render instead, which drawCheck fences.
|
||||||
|
func heldCheck(path string, n node) error {
|
||||||
|
t, ok := n.(*template)
|
||||||
|
if !ok || len(t.held) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
readers := boundReaders(t.format, t.bound, t.refs)
|
||||||
|
for _, head := range heldHeads(t) {
|
||||||
|
if _, isPath := t.bound[head]; isPath && isRef(head) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := checkHeadHeld(t, head, readers); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// heldHeads lists a template's held names, operand heads first, then paths, each
|
||||||
|
// in name order: a level read both ways is reported by the operand's fence, and
|
||||||
|
// which overlap is reported does not vary.
|
||||||
|
func heldHeads(t *template) []string {
|
||||||
|
heads := make([]string, 0, len(t.held))
|
||||||
|
for head := range t.held {
|
||||||
|
heads = append(heads, head)
|
||||||
|
}
|
||||||
|
sort.Slice(heads, func(i, j int) bool {
|
||||||
|
_, pi := t.bound[heads[i]]
|
||||||
|
_, pj := t.bound[heads[j]]
|
||||||
|
if pi != pj {
|
||||||
|
return !pi
|
||||||
|
}
|
||||||
|
return heads[i] < heads[j]
|
||||||
|
})
|
||||||
|
return heads
|
||||||
|
}
|
||||||
|
|
||||||
|
// pinned collects what the hold of head answers for: a path pins the levels it
|
||||||
|
// passes through and the leaf it lands on, an operand exactly the value its render
|
||||||
|
// produces.
|
||||||
|
func pinned(t *template, head string, readers []reader) map[node]bool {
|
||||||
|
held := map[node]bool{}
|
||||||
|
if _, isPath := t.bound[head]; !isPath {
|
||||||
|
operandDraw(t.fields[head], held)
|
||||||
|
return held
|
||||||
|
}
|
||||||
|
for _, r := range readers {
|
||||||
|
if a := splitArm(r.name, t.refs); a.key == head {
|
||||||
|
coverPath(t.fields[head], a.tail, held)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return held
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkHeadHeld rejects every route to what head's hold pins except the readers
|
||||||
|
// holding it. One seen set across the edges: a node that cannot reach the level
|
||||||
|
// cannot reach it by another route either, so it is walked once.
|
||||||
|
func checkHeadHeld(t *template, head string, readers []reader) error {
|
||||||
|
held := pinned(t, head, readers)
|
||||||
|
if len(held) == 0 {
|
||||||
|
return nil // a fixed head holds nothing to reach
|
||||||
|
}
|
||||||
|
reader, isPath := t.bound[head]
|
||||||
|
seen := map[node]bool{}
|
||||||
|
for _, e := range renderEdges(t) {
|
||||||
|
if splitArm(e.label, t.refs).key == head || !renders(e.to, held, seen) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isPath {
|
||||||
|
return fmt.Errorf("%s renders %q, which {%s} reads a path into; name the fields you want instead", e.reached(), head, reader)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s renders %q, which a {%s()} also reads; reach it one way so it is drawn once", e.reached(), head, operandReader(t, head))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// operandReader names the builtin whose operand holds head.
|
||||||
|
func operandReader(t *template, head string) string {
|
||||||
|
fn := ""
|
||||||
|
_ = eachToken(t.format, func(tok ftoken) error {
|
||||||
|
if tok.kind != 'b' || fn != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if name, _, isFunc := funcCall(tok.body); isFunc {
|
||||||
|
for _, operand := range tokenOperands(tok.body) {
|
||||||
|
if splitArm(operand, t.refs).key == head {
|
||||||
|
fn = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// coverPath collects what holding one path pins: every choice level the path
|
||||||
|
// passes through, whole, and the leaf it renders.
|
||||||
|
func coverPath(n node, tail []string, into map[node]bool) {
|
||||||
|
_, _ = walkPath(n, tail, pathWalk{
|
||||||
|
choice: func(c *choice, _ []string) ([]node, error) { cover(c, into, false); return nil, nil },
|
||||||
|
leaf: func(n node) error { cover(n, into, false); return nil },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cover collects a level and everything contained in it. A fixed string outside a
|
||||||
|
// choice is left out — it cannot disagree with itself — but inside one each
|
||||||
|
// variant carries its own, so there it counts.
|
||||||
|
func cover(n node, into map[node]bool, inChoice bool) {
|
||||||
|
if isFixed(n) && !inChoice {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
into[n] = true
|
||||||
|
_, isChoice := n.(*choice)
|
||||||
|
for _, c := range contained(n) {
|
||||||
|
cover(c.node, into, inChoice || isChoice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// operandDraw collects what one held draw of an operand answers for: the operand
|
||||||
|
// and what rendering it settles inside itself. The builtin renders its operand
|
||||||
|
// whole, so that draw fixes every value the render produced, and a second route to
|
||||||
|
// any of them disagrees with it.
|
||||||
|
//
|
||||||
|
// The walk stops at a reference edge, which is where the operand's own value ends
|
||||||
|
// and a shared source begins: two names referencing one category are two draws, the
|
||||||
|
// same rule {word} {word} follows.
|
||||||
|
func operandDraw(n node, into map[node]bool) {
|
||||||
|
if isFixed(n) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if into[n] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
into[n] = true
|
||||||
|
for _, e := range renderEdges(n) {
|
||||||
|
if isRef(e.label) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
operandDraw(e.to, into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFixed is a string that varies nothing: fixed text with no fields to read into.
|
||||||
|
func isFixed(n node) bool {
|
||||||
|
t, ok := n.(*template)
|
||||||
|
return ok && t.fixed && len(t.fields) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// renders reports whether rendering n can reach anything in want, following the
|
||||||
|
// same edges expand does. seen keeps a node shared by several routes from being
|
||||||
|
// walked twice; checkNoCycles has already proved the graph is a DAG, so the walk
|
||||||
|
// ends.
|
||||||
|
func renders(n node, want, seen map[node]bool) bool {
|
||||||
|
if want[n] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if seen[n] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
seen[n] = true
|
||||||
|
for _, e := range renderEdges(n) {
|
||||||
|
if renders(e.to, want, seen) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNoOverlap rejects a format that both renders a sibling level and reads a path
|
||||||
|
// into it — {p} beside {p.first}, {p.addr} beside {p.addr.city}. The path reads the
|
||||||
|
// level's held draw while rendering the level expands it afresh, so their values would
|
||||||
|
// disagree. Reads are compared in sorted order, so which pair is reported does not
|
||||||
|
// depend on where the tokens sit.
|
||||||
|
func checkNoOverlap(format string, bound map[string]string, refs map[string]refBinding) error {
|
||||||
|
names := boundReaders(format, bound, refs)
|
||||||
|
// Stable over one format-order scan, so two readers of one name (a token and a
|
||||||
|
// calc operand both naming "p") are reported as the format writes them.
|
||||||
|
sort.SliceStable(names, func(i, j int) bool { return names[i].path < names[j].path })
|
||||||
|
for i, level := range names {
|
||||||
|
for _, path := range names[i+1:] {
|
||||||
|
if strings.HasPrefix(path.path, level.path+".") {
|
||||||
|
return fmt.Errorf("%s renders a level that {%s} reads a path into; name the fields you want instead", level.label, path.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// reader is one way a format reaches a bound field: as written, by its one
|
||||||
|
// spelling, and how to name it.
|
||||||
|
type reader struct{ name, path, label string }
|
||||||
|
|
||||||
|
// boundReaders lists every way a format reaches a bound sibling field, in the order the
|
||||||
|
// format writes them. An operand renders its field, so it names a level exactly
|
||||||
|
// as a token does; one scan finds both, which is what puts them in one order.
|
||||||
|
func boundReaders(format string, bound map[string]string, refs map[string]refBinding) []reader {
|
||||||
|
var names []reader
|
||||||
|
_ = eachToken(format, func(t ftoken) error {
|
||||||
|
if t.kind != 'b' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if fn, _, isFunc := funcCall(t.body); isFunc {
|
||||||
|
for _, operand := range tokenOperands(t.body) {
|
||||||
|
a := splitArm(operand, refs)
|
||||||
|
if _, isBound := bound[a.key]; isBound && !isRef(a.key) {
|
||||||
|
names = append(names, reader{a.name, a.path, fmt.Sprintf("%s operand %q", fn, operand)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, a := range splitArms(t.body, refs) {
|
||||||
|
if _, isBound := bound[a.key]; isBound && !isRef(a.key) {
|
||||||
|
names = append(names, reader{a.name, a.path, "token {" + a.name + "}"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNoRepeatedRead rejects a bare token repeated on a held name: {w} {w} beside
|
||||||
|
// {uppercase(w)} would read one draw twice, where {w} {w} alone draws twice. The
|
||||||
|
// error names the single-token spelling.
|
||||||
|
func checkNoRepeatedRead(format string, c formatOps, refs map[string]refBinding) error {
|
||||||
|
count := map[string]int{}
|
||||||
|
return eachToken(format, func(t ftoken) error {
|
||||||
|
if t.kind != 'b' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, _, isFunc := funcCall(t.body); isFunc {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, a := range splitArms(t.body, refs) {
|
||||||
|
if len(a.tail) > 0 || !c.held[a.key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if count[a.key]++; count[a.key] > 1 {
|
||||||
|
return fmt.Errorf("token {%s} is repeated, and %s holds %q to one draw per expansion; write {%s} once", a.name, c.holder[a.key], a.key, a.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -21,13 +21,12 @@ name says whether a unit runs at `New` or at `Fake`.
|
|||||||
|
|
||||||
This round, in order:
|
This round, in order:
|
||||||
|
|
||||||
- Split `hold.go` and `draw.go` so each file holds one phase, the load-time fences
|
|
||||||
apart from the render-time machinery, changing no logic.
|
|
||||||
- Move `nodeScope`, `checkScope` and `checkRenders` out of `graph.go` and refresh the
|
- Move `nodeScope`, `checkScope` and `checkRenders` out of `graph.go` and refresh the
|
||||||
README Layout lines for `graph.go` and `data.go`: the order every load fence runs in
|
README Layout lines for `graph.go` and `data.go`: the order every load fence runs in
|
||||||
is the load path's spine, and no file name carries it.
|
is the load path's spine, and no file name carries it.
|
||||||
- Name the `draw` stem apart: the free `pinned` in `hold.go` is not `(*draws).pinned`
|
- Name the `draw` stem apart: the free `pinned` in `holdfence.go` is not
|
||||||
beside it, `compileFormat` is a method on both `*template` and `*table`, and
|
`(*draws).pinned` in `hold.go`, `compileFormat` is a method on both `*template` and
|
||||||
|
`*table`, and
|
||||||
`draws`, `drawSet`, `drawScope`, `drawWalk`, `drawAt`, `drawKey`, `drawVisit` and
|
`draws`, `drawSet`, `drawScope`, `drawWalk`, `drawAt`, `drawKey`, `drawVisit` and
|
||||||
`drawRoute` no longer tell each other apart.
|
`drawRoute` no longer tell each other apart.
|
||||||
- Spell one fresh draw set one way: `newDrawSet` exists, and `render.go` and `draw.go`
|
- Spell one fresh draw set one way: `newDrawSet` exists, and `render.go` and `draw.go`
|
||||||
|
|||||||
Reference in New Issue
Block a user