package fejkdata import ( "fmt" "sort" "strings" ) // 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. // docs/decisions.md#a-render-shares-one-reference-draw-per-category-per-group // docs/decisions.md#the-expansion-hold-and-the-renders-draws-are-two-fences type drawCheck struct { memo map[hasReadMemo]bool } type hasReadMemo 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 } if err := renderDraws(t).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 whole // read draws its row without pinning it, so the family would draw apart from the row // it renders, whichever draws the reaching template holds. // docs/decisions.md#a-table-never-reaches-its-own-family-by-any-route func checkOwnFamily(t *template) error { own := t.table if own == nil { own = t.cell.table } 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 e.readsRef() { if head, isTable := n.(*template).head(e.read.key).(*table); isTable && head.familyRoot() == own.familyRoot() { 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 bare read of %s would draw apart from the row it renders; read the family from a template beside it, or add the value as a column", e.reached(), head.category, own.category) } return nil } func renderDraws(t *template) *drawWalk { w := newDrawWalk() for _, e := range renderEdges(t) { w.edge(t, e, drawAt{group: t.link.drawGroupKey, route: drawRoute{e.reached(), e.label}}) } return w } // 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. // docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load func (c *drawCheck) checkRecordDraws(path string, n node) error { t, ok := n.(*template) if !ok || !t.isRecord { return nil } // A record-only template's format renders nothing, so weigh the columns, not the format. columns := sortedNames(t.fields) 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.hasRead(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.hasRead(n, true) } // hasRead 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) hasRead(n node, stopAtGroup bool) bool { k := hasReadMemo{n, stopAtGroup} if r, done := c.memo[k]; done { return r } r := false for _, e := range renderEdges(n) { if readsOnEdge(n, e) || (walksInto(e.to, stopAtGroup) && c.hasRead(e.to, stopAtGroup)) { r = true break } } if c.memo == nil { c.memo = map[hasReadMemo]bool{} } c.memo[k] = r return r } func readsOnEdge(n node, e renderEdge) bool { return e.readsRef() && (len(e.read.tail) > 0 || readsTable(n, e.read)) } func walksInto(to node, stopAtGroup bool) bool { return !repeats(to) && !(stopAtGroup && grouped(to)) } 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.link.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.head(a.key).(*table) return isTable } // checkColumnDraws fences a record's columns as one render. func checkColumnDraws(t *template, columns []string) error { return columnDraws(t, columns).check() } func columnDraws(t *template, columns []string) *drawWalk { w := newDrawWalk() for _, name := range columns { w.walk(t.fields[name], drawAt{group: t.link.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}}) } return w } // drawWalk gathers the references one render reads, by draw group, for check to compare. type drawWalk struct { reads []pathRead read map[readKey]bool seen map[nodeVisit]bool pinIDs map[pinsLink]pinsID } // drawAt is where a walk stands: the draw group it draws in, how the render's root reached it, the // table rows it pinned, the table a whole read draws a row of, and the rows of whole draws whose // cells the walk is in. type drawAt struct { group string route drawRoute pins pinSet wholeTable *table wholePins pinSet } // 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 nodeVisit struct { n node group string wholeTable *table pins pinsID wholePins pinsID } type readKey struct { group string path string pins pinsID wholePins pinsID } // pinsID names a pin set within one walk: two sets pinning the same rows share one. type pinsID int // pinsLink is a set's last pin, in path order, and the id of the set before it. type pinsLink struct { prev pinsID pin tablePin } func newDrawWalk() *drawWalk { return &drawWalk{read: map[readKey]bool{}, seen: map[nodeVisit]bool{}, pinIDs: map[pinsLink]pinsID{}} } func (w *drawWalk) pinsID(p *pinSet) pinsID { var pins []tablePin p.each(func(t *table, r int) { pins = append(pins, tablePin{t, r}) }) sort.Slice(pins, func(i, j int) bool { return pins[i].t.path < pins[j].t.path }) id := pinsID(0) for _, q := range pins { l := pinsLink{id, q} next, ok := w.pinIDs[l] if !ok { next = pinsID(len(w.pinIDs) + 1) w.pinIDs[l] = next } id = next } return id } // walk follows what rendering n renders. A repeat renders over draws of its own, so the walk stops // there. A cell whose row the pins keep out has no case: a pinned table renders its pinned row // alone and an unpinned one draws a row inside its nearest pinned ancestor's, so that cell never // renders on this route. func (w *drawWalk) walk(n node, at drawAt) { v := nodeVisit{n, at.group, at.wholeTable, w.pinsID(&at.pins), w.pinsID(&at.wholePins)} if w.seen[v] { return } w.seen[v] = true if repeats(n) { return } if t, isTemplate := n.(*template); isTemplate && t.link.drawGroupKey != "" { at.group = t.link.drawGroupKey } if t, isTable := n.(*table); isTable { at.wholeTable = t } for _, e := range renderEdges(n) { to, isTemplate := e.to.(*template) cell := cellSite{} if isTemplate { cell = to.cell } switch { case cell.table == nil: w.edge(n, e, at) case cell.table == at.wholeTable: in := at in.wholePins = at.wholePins.clone() in.wholePins.add(cell.table, cell.row) w.edge(n, e, in) case at.pins.clash(cell.table, cell.row) == nil: in := at in.pins = at.pins.entered(cell.table, cell.row) w.edge(n, e, in) } } } // 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 e.readsRef() { a := e.read tr := tableReadOf(from.(*template).head(a.key), a, e.to) if k := (readKey{at.group, a.path, w.pinsID(&at.pins), w.pinsID(&at.wholePins)}); !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.pins = at.pins.entered(t, r) }) } } 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.landsRow) { return overlapError(level.at.route, level.a.spelling, into) } } } return checkFamilies(w.reads) } // checkFamilies refuses reads of one table family in one draw group that cannot // read one consistent draw: a table rendered whole beside a path into the family, // a table one read draws that another pins, and two reads pinning different rows. // The reads come sorted by group and path, so which pair is reported does not vary. func checkFamilies(reads []pathRead) error { for i, r := range reads { if r.tr == nil { continue } for _, o := range reads[:i] { if o.tr == nil || o.at.group != r.at.group || alternatives(o.at, r.at) || o.tr.headTable.familyRoot() != r.tr.headTable.familyRoot() { continue } if err := checkFamilyPair(o, r); err != nil { return err } } } return replayPairs(reads) } // checkFamilyPair refuses a table read whole beside a path into its family, and a // table one read draws that the other pins, since which token renders first would // then decide the row. // docs/decisions.md#a-bare-reference-draws-each-time-a-reference-path-is-held func checkFamilyPair(a, b pathRead) error { for _, pair := range [][2]pathRead{{a, b}, {b, a}} { x, y := pair[0], pair[1] if len(x.a.tail) == 0 && len(y.a.tail) > 0 { return overlapError(x.at.route, x.a.spelling, y) } if drawn := x.tr.drawnOf(&y.tr.pins); drawn != nil { if s, ok := y.tr.selected(x.tr.headTable); ok && len(x.tr.sels) == 0 { tail := x.a.tail if s.t != x.tr.headTable { tail = append([]string{x.tr.headTable.category}, tail...) } return fmt.Errorf("%s draws %s, which %s selects a row of; write {%s.%s}, or draw them apart with a drawGroup", x.at.route.spelled(x.a.spelling), drawn.category, y.at.route.spelled(y.a.spelling), s.spelling, joinSegments(tail)) } return fmt.Errorf("%s draws %s, which %s selects a row of; select that row in both, or draw them apart with a drawGroup", x.at.route.spelled(x.a.spelling), drawn.category, y.at.route.spelled(y.a.spelling)) } } return nil } // replayPairs replays every two reads that can render together into one draws, the earlier // read first. Pairs find every conflict a full replay would: clash judges a row against one // pinned table, and the read that pinned it holds that pin itself, since a read's pins carry // its rows' ancestors, so the pair of those two reads clashes the same way. func replayPairs(reads []pathRead) error { for i, r := range reads { if r.tr == nil { continue } for _, o := range reads[i+1:] { if o.tr == nil || o.at.group != r.at.group || alternatives(r.at, o.at) { continue } var d pinSet if err := r.tr.replay(&d); err != nil { return conflict(r, err) } if err := o.tr.replay(&d); err != nil { return conflict(o, err) } } } return nil } func conflict(r pathRead, err error) error { return fmt.Errorf("%s: %w; select the same rows in every path into the family, or draw them apart with a drawGroup", r.at.route.spelled(r.a.spelling), err) } // alternatives reports whether two reads never render together: one read renders one row of a // table, so reads under different rows the walks pinned, or under different rows of one whole // draw, never meet. func alternatives(a, b drawAt) bool { return a.pins.differs(&b.pins) || a.wholePins.differs(&b.wholePins) } 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.spelling)) } // 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) } // tableRead is what a reference path reads of a table family: the table its head // names, the rows its selectors pin, the tables it draws — those it walks with no // row pinned, and their unpinned ancestors — each selector's spelling, and whether // it lands on a `tableRow`. type tableRead struct { headTable *table pins pinSet drawn map[*table]bool sels []tableSel landsRow bool } // tableSel is one selector on the way: the table it selects a row of, and the path // as written up to and including it. type tableSel struct { t *table spelling string } // tableReadOf replays a reference path through pathProbe, so two paths pinning one // row by different routes compare equal. checkPath proved // each selector names a row. func tableReadOf(head node, a arm, leaf node) *tableRead { t, isTable := head.(*table) if !isTable { return nil } probe := &pathProbe{drawn: map[*table]bool{}} _, _ = probe.walk(t, a.tail) tr := &tableRead{headTable: t, pins: probe.pins, drawn: probe.drawn} written := a.spelling[:len(a.spelling)-len(joinSegments(a.tail))] cur := t for i, seg := range a.tail { switch d := cur.descendant(seg); { case isSelector(seg): tr.sels = append(tr.sels, tableSel{cur, written + joinSegments(a.tail[:i+1])}) case d != nil: cur = d } } _, tr.landsRow = leaf.(*tableRow) return tr } // selected is the selector in r on t, or on the nearest ancestor of t it selects. func (r *tableRead) selected(t *table) (tableSel, bool) { for ; t != nil; t = t.parentT { for _, s := range r.sels { if s.t == t { return s, true } } } return tableSel{}, false } // drawnOf is a table the read draws that pins holds a row of, if any. func (r *tableRead) drawnOf(pins *pinSet) *table { var found *table pins.each(func(t *table, _ int) { if found == nil && r.drawn[t] { found = t } }) return found } // replay pins the read's rows into d, where they agree with the rows pinned before. func (r *tableRead) replay(d *pinSet) error { var err error r.pins.each(func(t *table, row int) { if err == nil { err = d.pinRow(t, row) } }) return err }