From 6700a8ad9f8c172fe79b51030714d9ad49e3fefe Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 21 Sep 2026 18:38:03 +0200 Subject: [PATCH] Split hold and draw so a file name says load or render --- README.md | 6 +- draw.go | 420 -------------------------------------------------- drawfence.go | 425 +++++++++++++++++++++++++++++++++++++++++++++++++++ hold.go | 250 ------------------------------ holdfence.go | 256 +++++++++++++++++++++++++++++++ todo.md | 7 +- 6 files changed, 688 insertions(+), 676 deletions(-) create mode 100644 drawfence.go create mode 100644 holdfence.go diff --git a/README.md b/README.md index 8d343b0..19a5df8 100644 --- a/README.md +++ b/README.md @@ -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 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 -hold.go the hold: one draw per expansion for paths and operands, the rows a render pins, and its fences -draw.go one reference draw per render and group: draw sets, the group option, and its fence +hold.go the hold at render: one draw per expansion for paths and operands, and the rows it pins +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 reference.go reference sigils, and binding references across the tree graph.go the render graph: edges, cycles, the repeat bound, tree walks diff --git a/draw.go b/draw.go index daa1e61..0fdb478 100644 --- a/draw.go +++ b/draw.go @@ -1,8 +1,6 @@ package fejkdata import ( - "fmt" - "sort" "strings" ) @@ -57,421 +55,3 @@ func (sc drawScope) draws(s *session) *draws { } 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) -} diff --git a/drawfence.go b/drawfence.go new file mode 100644 index 0000000..4e51eaf --- /dev/null +++ b/drawfence.go @@ -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) +} diff --git a/hold.go b/hold.go index 4b89c82..a54e183 100644 --- a/hold.go +++ b/hold.go @@ -3,258 +3,8 @@ 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 - }) -} - // tablePin is one table's pinned row. type tablePin struct { t *table diff --git a/holdfence.go b/holdfence.go new file mode 100644 index 0000000..f1b114a --- /dev/null +++ b/holdfence.go @@ -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 + }) +} diff --git a/todo.md b/todo.md index c7de840..bd4356c 100644 --- a/todo.md +++ b/todo.md @@ -21,13 +21,12 @@ name says whether a unit runs at `New` or at `Fake`. 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 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. -- Name the `draw` stem apart: the free `pinned` in `hold.go` is not `(*draws).pinned` - beside it, `compileFormat` is a method on both `*template` and `*table`, and +- Name the `draw` stem apart: the free `pinned` in `holdfence.go` is not + `(*draws).pinned` in `hold.go`, `compileFormat` is a method on both `*template` and + `*table`, and `draws`, `drawSet`, `drawScope`, `drawWalk`, `drawAt`, `drawKey`, `drawVisit` and `drawRoute` no longer tell each other apart. - Spell one fresh draw set one way: `newDrawSet` exists, and `render.go` and `draw.go`