diff --git a/data.go b/data.go index f08d07a..a4cb217 100644 --- a/data.go +++ b/data.go @@ -249,7 +249,7 @@ func checkRenders(s nodeScope) error { fence := &drawCheck{} refs := false if err := s(func(path string, n node) error { - if t, ok := n.(*template); ok && len(t.refs) > 0 { + if t, ok := n.(*template); ok && len(t.link.refs) > 0 { refs = true } return fence.checkDrawGroup(path, n) diff --git a/datatype.go b/datatype.go index 8dbf0c3..aba4a9b 100644 --- a/datatype.go +++ b/datatype.go @@ -76,7 +76,7 @@ func checkColumns(s nodeScope) error { checked[column] = true items, _ := columnItems(column) for _, it := range items { - if r := it.readsColumn; r != nil { + if r := it.link.readsColumn; r != nil { if err := check(r.a.key[1:], r.a.tail[0], r.column); err != nil { return err } @@ -119,8 +119,8 @@ func columnDatatype(n node) DataType { // itemDatatype is the datatype a column item declares, else that of the column it reads. func itemDatatype(t *template) DataType { - if t.datatype == DataTypeString && t.readsColumn != nil { - return columnDatatype(t.readsColumn.column) + if t.datatype == DataTypeString && t.link.readsColumn != nil { + return columnDatatype(t.link.readsColumn.column) } return t.datatype } diff --git a/doc.go b/doc.go index e079331..a518fdf 100644 --- a/doc.go +++ b/doc.go @@ -24,9 +24,9 @@ package fejkdata // start one each, `FakeStruct` one per record, and `expandAnew` one per // iteration of a `template.repeat`. // - hold — keeping one draw of a name, so every route to it reads that value: -// `template.held`, `hold`, `groupHold`, `readField`. +// `formatOps.held`, `hold`, `groupHold`, `readField`. // - draw group — reference draws held apart inside one render: -// `template.drawGroup` as the data spells it, `template.drawGroupKey` as a +// `template.drawGroup` as the data spells it, `templateLink.drawGroupKey` as a // render reads it, `holdSet`, `renderScope`. // - pin — fixing which row of a table the render uses, which the draw fences replay: // `pinSet`, `pinSet.pin`, `table.drawIn`. diff --git a/drawfence.go b/drawfence.go index b46aff1..eb08c3f 100644 --- a/drawfence.go +++ b/drawfence.go @@ -83,7 +83,7 @@ func (c *drawCheck) checkDraws(path string, n node) error { func checkOwnFamily(t *template) error { own := t.table if own == nil { - own = t.cellOf + own = t.cell.table } if own == nil { return nil @@ -116,7 +116,7 @@ func checkOwnFamily(t *template) error { func renderDraws(t *template) *drawWalk { w := newDrawWalk() for _, e := range renderEdges(t) { - w.edge(t, e, drawAt{group: t.drawGroupKey, route: drawRoute{e.reached(), e.label}}) + w.edge(t, e, drawAt{group: t.link.drawGroupKey, route: drawRoute{e.reached(), e.label}}) } return w } @@ -189,7 +189,7 @@ func repeats(n node) bool { func grouped(n node) bool { t, isTemplate := n.(*template) - return isTemplate && t.drawGroupKey != "" + return isTemplate && t.link.drawGroupKey != "" } // readsTable reports whether a reference of n names a table, whose draw the render's @@ -209,7 +209,7 @@ func refRead(n node, label string) (arm, bool) { if !isTemplate { return arm{}, false } - a := splitArm(label, t.refs) + a := splitArm(label, t.link.refs) return a, isRef(a.key) } @@ -219,7 +219,7 @@ func checkColumnDraws(t *template, columns []string) error { return columnDraws( func columnDraws(t *template, columns []string) *drawWalk { w := newDrawWalk() for _, name := range columns { - w.walk(t.fields[name], drawAt{group: t.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}}) + w.walk(t.fields[name], drawAt{group: t.link.drawGroupKey, route: drawRoute{spelling: fmt.Sprintf("column %q", name)}}) } return w } @@ -311,25 +311,29 @@ func (w *drawWalk) walk(n node, at drawAt) { if repeats(n) { return } - if t, isTemplate := n.(*template); isTemplate && t.drawGroupKey != "" { - at.group = t.drawGroupKey + 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) { - cell, isCell := e.to.(*template) + to, isTemplate := e.to.(*template) + cell := cellSite{} + if isTemplate { + cell = to.cell + } switch { - case !isCell || cell.cellOf == nil: + case cell.table == nil: w.edge(n, e, at) - case cell.cellOf == at.wholeTable: + case cell.table == at.wholeTable: in := at in.wholePins = at.wholePins.clone() - in.wholePins.add(cell.cellOf, cell.cellRow) + in.wholePins.add(cell.table, cell.row) w.edge(n, e, in) - case at.pins.clash(cell.cellOf, cell.cellRow) == nil: + case at.pins.clash(cell.table, cell.row) == nil: in := at - in.pins = at.pins.entered(cell.cellOf, cell.cellRow) + in.pins = at.pins.entered(cell.table, cell.row) w.edge(n, e, in) } } diff --git a/graph.go b/graph.go index 252bcba..5eab557 100644 --- a/graph.go +++ b/graph.go @@ -136,7 +136,7 @@ func renderEdges(n node) []renderEdge { es = append(es, renderEdge{leaf, a.spelling, operand}) } } - for _, o := range n.ops { + for _, o := range n.compiled.ops { for _, a := range o.operands { add(a, o.fn) } diff --git a/hold.go b/hold.go index 02ff5d5..54b553b 100644 --- a/hold.go +++ b/hold.go @@ -37,7 +37,7 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) readV if s.trace != nil { traceRead(s.trace, t, sc, a) } - if !t.held[a.key] { + if !t.compiled.held[a.key] { if len(a.tail) > 0 { panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.spelling, a.key)) } @@ -73,8 +73,8 @@ func traceRead(trace *renderTrace, t *template, sc renderScope, a arm) { var table string var row int switch { - case t.cellOf != nil: - table, row = t.cellOf.path, t.cellRow + case t.cell.table != nil: + table, row = t.cell.table.path, t.cell.row case t.table != nil: table, row = t.table.path, sc.rowOf(t.table) } @@ -89,8 +89,8 @@ func renderLeaf(s *session, n node, sc renderScope) readValue { case *nullItem: return readValue{null: true} case *template: - if leaf.readsColumn != nil { - return readField(s, leaf, nil, sc.in(leaf), leaf.readsColumn.a) + if leaf.link.readsColumn != nil { + return readField(s, leaf, nil, sc.in(leaf), leaf.link.readsColumn.a) } } return readValue{text: render(s, n, sc)} diff --git a/holdfence.go b/holdfence.go index 59be3f0..83a14fb 100644 --- a/holdfence.go +++ b/holdfence.go @@ -12,12 +12,12 @@ import ( // docs/decisions.md#the-expansion-hold-and-the-renders-draws-are-two-fences func heldCheck(path string, n node) error { t, ok := n.(*template) - if !ok || len(t.held) == 0 { + if !ok || len(t.compiled.held) == 0 { return nil } - readers := pathKeyReaders(t.ops, t.pathKeys) + readers := pathKeyReaders(t.compiled.ops, t.compiled.pathKeys) for _, name := range heldNames(t) { - if _, isPath := t.pathKeys[name]; isPath && isRef(name) { + if _, isPath := t.compiled.pathKeys[name]; isPath && isRef(name) { continue // held for the render: drawCheck compares every read of it, by path, across the render and its groups } if err := checkNameHeld(t, name, readers); err != nil { @@ -31,13 +31,13 @@ func heldCheck(path string, n node) error { // in name order: a level read both ways is reported by the operand's fence, and // which overlap is reported does not vary. func heldNames(t *template) []string { - names := make([]string, 0, len(t.held)) - for name := range t.held { + names := make([]string, 0, len(t.compiled.held)) + for name := range t.compiled.held { names = append(names, name) } sort.Slice(names, func(i, j int) bool { - _, pi := t.pathKeys[names[i]] - _, pj := t.pathKeys[names[j]] + _, pi := t.compiled.pathKeys[names[i]] + _, pj := t.compiled.pathKeys[names[j]] if pi != pj { return !pi } @@ -51,12 +51,12 @@ func heldNames(t *template) []string { // produces. func heldNodes(t *template, name string, readers []reader) map[node]bool { held := map[node]bool{} - if _, isPath := t.pathKeys[name]; !isPath { + if _, isPath := t.compiled.pathKeys[name]; !isPath { operandDraw(t.head(name), held) return held } for _, r := range readers { - if a := splitArm(r.spelling, t.refs); a.key == name { + if a := splitArm(r.spelling, t.link.refs); a.key == name { coverPath(t.head(name), a.tail, held) } } @@ -71,10 +71,10 @@ func checkNameHeld(t *template, name string, readers []reader) error { if len(held) == 0 { return nil // a fixed head holds nothing to reach } - reader, isPath := t.pathKeys[name] + reader, isPath := t.compiled.pathKeys[name] seen := map[node]bool{} for _, e := range renderEdges(t) { - if splitArm(e.label, t.refs).key == name || !renders(e.to, held, seen) { + if splitArm(e.label, t.link.refs).key == name || !renders(e.to, held, seen) { continue } if isPath { @@ -87,7 +87,7 @@ func checkNameHeld(t *template, name string, readers []reader) error { // operandReader names the builtin whose operand holds name. func operandReader(t *template, name string) string { - for _, o := range t.ops { + for _, o := range t.compiled.ops { for _, a := range o.operands { if a.key == name { return o.fn diff --git a/holdset.go b/holdset.go index eacf77f..93be3fa 100644 --- a/holdset.go +++ b/holdset.go @@ -42,8 +42,8 @@ func expandAnew(s *session, t *template) string { // in is the scope t renders in: its draw group where it names one, else its caller's. func (sc renderScope) in(t *template) renderScope { - if t.drawGroupKey != "" { - sc.group = t.drawGroupKey + if t.link.drawGroupKey != "" { + sc.group = t.link.drawGroupKey } return sc } diff --git a/inline.go b/inline.go index 3685670..91749e0 100644 --- a/inline.go +++ b/inline.go @@ -168,7 +168,6 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error { if !ok { return nil } - t.keyDrawGroup("") for _, name := range refTokens(t.tokens) { sigil, rest, err := refShape(name) if err != nil { @@ -178,6 +177,6 @@ func linkNodeRefs(scope nodeScope, root map[string]node) error { return fmt.Errorf("%s: reference {%s}: an inline template has no folder; write {/%s}", path, name, rest) } } - return linkTemplateRefs(nil, path, "", t, root) + return linkTemplate(nil, path, "", t, root) }) } diff --git a/node.go b/node.go index f4a689c..352f96b 100644 --- a/node.go +++ b/node.go @@ -41,9 +41,10 @@ func (*nullItem) isNode() {} // template renders a format string, substituting {tokens} from fields. A bare // JSON string is a template with no fields. repeat (default 1) renders that format // that many times and joins the results with separator (default ""), each render -// an independent pick. Every format compiles in `linkTemplateRefs`. +// an independent pick. Every format compiles in `linkTemplate`. type template struct { - // Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`: + // Filled by `compileString`, `compileTemplate`, `table.checkCells` and + // `table.compileRowFormat`: format string tokens []formatToken fields map[string]node @@ -54,24 +55,21 @@ type template struct { fromString bool // written as a JSON string rather than an object isRecord bool // compiled at the top without a repeat, so its fields are record columns table *table // the table whose format this is, whose columns are the fields + cell cellSite - // Filled by `checkCells`: - cellOf *table // the table whose cell this is - cellRow int // the row the cell sits in + // Filled by `linkTemplate`, from the assembled tree: + link templateLink + compiled formatOps +} - // Filled by `template.compileFormat` at link, from `template.tokens` and `template.refs`: - ops []op // what expand walks - grow int // minimum output size, to size the render buffer - // pathKeys maps each key a dotted path in the format starts from to one path token - // reading it, which is the half of an overlap the fences name. nil when the - // format takes no path. - pathKeys map[string]string - // held is every name drawn once per expansion: the path keys above, plus the - // siblings a {calc()} reads. nil when the format holds nothing (see expand). - held map[string]bool - heldLocal bool // some held name is kept by the expansion itself, so expand makes its hold +// cellSite is the table cell a template is compiled from; table is nil for any other template. +type cellSite struct { + table *table + row int +} - // Filled by `linkTemplateRefs` and `keyDrawGroup`, from the assembled tree: +// templateLink is what a template's references resolve to in the assembled tree. +type templateLink struct { refs map[string]refBinding // each reference the format reads -> what it resolves to refHeads map[string]node // each refBinding.key -> the category it names readsColumn *columnRead // set when the format is one reference alone reading a record's column @@ -83,7 +81,7 @@ func (*template) isNode() {} // head is the node an arm's key names: a sibling field, or a reference's category. func (t *template) head(key string) node { if isRef(key) { - return t.refHeads[key] + return t.link.refHeads[key] } return t.fields[key] } @@ -220,15 +218,14 @@ func (t *template) fixedText() (string, bool) { return "", false } -// compileFormat compiles the format into ops, and applies the fences that need the +// compileFormat compiles a format into ops, and applies the fences that need the // compiled reads. -func (t *template) compileFormat() error { - c := compileOps(t.tokens, t.refs) - t.ops, t.grow, t.pathKeys, t.held, t.heldLocal = c.ops, c.grow, c.pathKeys, c.held, c.heldLocal - if err := checkNoOverlap(t.ops, t.pathKeys); err != nil { - return err +func compileFormat(toks []formatToken, refs map[string]refBinding) (formatOps, error) { + c := compileOps(toks, refs) + if err := checkNoOverlap(c.ops, c.pathKeys); err != nil { + return c, err } - return checkNoRepeatedRead(c) + return c, checkNoRepeatedRead(c) } func compileChoice(items []any, pos position) (node, error) { @@ -442,14 +439,6 @@ func drawGroupOf(m map[string]any, repeat int) (string, error) { 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 - } -} - // weightOf reads a node's "weight" (default 1) from its raw JSON form. Only // template objects carry weight; a present one must be finite and positive. func weightOf(raw any) (float64, error) { diff --git a/node_test.go b/node_test.go index f994f27..225f496 100644 --- a/node_test.go +++ b/node_test.go @@ -94,7 +94,7 @@ func TestEveryFormatCompilesAtLink(t *testing.T) { compiled := map[string]bool{} if err := eachNode(n, "t", func(path string, m node) error { if tm, ok := m.(*template); ok { - compiled[path+" "+tm.format] = tm.ops != nil + compiled[path+" "+tm.format] = tm.compiled.ops != nil } return nil }); err != nil { diff --git a/reference.go b/reference.go index 4814a10..0563604 100644 --- a/reference.go +++ b/reference.go @@ -74,52 +74,63 @@ func refSegments(name string, folder []string) ([]string, error) { func linkRefs(root map[string]node) error { return eachTemplate(root, func(folder []string, path string, t *template) error { category := strings.Join(strings.Split(path, ".")[:len(folder)+1], ".") - t.keyDrawGroup(category) - return linkTemplateRefs(folder, path, category, t, root) + return linkTemplate(folder, path, category, t, root) }) } -func linkTemplateRefs(folder []string, path, category string, t *template, root map[string]node) error { - if t.cellOf != nil { - path = fmt.Sprintf("%s, line %d", path, t.cellRow+2) +// linkTemplate resolves t's references in category, "" for an inline template, and +// compiles its format against them. +func linkTemplate(folder []string, path, category string, t *template, root map[string]node) error { + if t.cell.table != nil { + path = fmt.Sprintf("%s, line %d", path, t.cell.row+2) } - if names := refTokens(t.tokens); len(names) > 0 { - if err := t.bindRefs(folder, path, category, names, root); err != nil { - return err - } + link, err := t.resolveLink(folder, path, category, root) + if err != nil { + return err } - if err := t.compileFormat(); err != nil { + compiled, err := compileFormat(t.tokens, link.refs) + if err != nil { return fmt.Errorf("%s: %w", path, err) } - t.readsColumn = columnReadOf(t) + t.link, t.compiled = link, compiled return nil } -// bindRefs fills refs and refHeads, refusing a reference to t's own category: +// resolveLink binds every reference t reads, refusing one to t's own category, and keys +// t's draw group by that category, so a name is local to it: // docs/decisions.md#a-category-never-references-itself-and-a-records-fences-run-at-load -func (t *template) bindRefs(folder []string, path, category string, names []string, root map[string]node) error { - t.refs = make(map[string]refBinding, len(names)) - t.refHeads = make(map[string]node, len(names)) +func (t *template) resolveLink(folder []string, path, category string, root map[string]node) (templateLink, error) { + var link templateLink + if t.drawGroup != "" { + link.drawGroupKey = category + "/" + t.drawGroup + } + names := refTokens(t.tokens) + if len(names) == 0 { + return link, nil + } + link.refs = make(map[string]refBinding, len(names)) + link.refHeads = make(map[string]node, len(names)) for _, name := range names { segments, err := refSegments(name, folder) if err != nil { - return fmt.Errorf("%s: reference {%s}: %w", path, name, err) + return link, fmt.Errorf("%s: reference {%s}: %w", path, name, err) } categorySegs, target, tail, err := resolveCategory(root, segments) if err != nil { - return fmt.Errorf("%s: reference {%s}: %w", path, name, err) + return link, fmt.Errorf("%s: reference {%s}: %w", path, name, err) } key := "/" + strings.Join(categorySegs, ".") if category != "" && key == "/"+category { - return fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", path, name) + return link, fmt.Errorf("%s: reference {%s}: names the category it sits in; read a sibling field as a path, or move the shared value into its own category and reference that", path, name) } if err := checkPath(target, tail, key); err != nil { - return fmt.Errorf("%s: reference {%s}: %w", path, name, err) + return link, fmt.Errorf("%s: reference {%s}: %w", path, name, err) } - t.refHeads[key] = target - t.refs[name] = refBinding{key, tail} + link.refHeads[key] = target + link.refs[name] = refBinding{key, tail} } - return nil + link.readsColumn = columnReadOf(t, link) + return link, nil } // columnRead is a record's column read by a format of that one reference alone, which is the @@ -129,13 +140,13 @@ type columnRead struct { column node } -func columnReadOf(t *template) *columnRead { +func columnReadOf(t *template, link templateLink) *columnRead { name, lone := loneRef(t.tokens) if !lone || t.repeat != 1 { return nil } - a := splitArm(name, t.refs) - target, isTemplate := t.head(a.key).(*template) + a := splitArm(name, link.refs) + target, isTemplate := link.refHeads[a.key].(*template) if !isTemplate || !target.isRecord || len(a.tail) != 1 { return nil } diff --git a/render.go b/render.go index 9bffb12..5fe866a 100644 --- a/render.go +++ b/render.go @@ -75,7 +75,7 @@ func render(s *session, n node, sc renderScope) string { return expand(s, n, sc) } var b strings.Builder - b.Grow(n.repeat * (n.grow + len(n.separator))) + b.Grow(n.repeat * (n.compiled.grow + len(n.separator))) for i := 0; i < n.repeat; i++ { if i > 0 { b.WriteString(n.separator) @@ -104,19 +104,19 @@ func pick(s *session, c *choice) node { // cannot fail. func expand(s *session, t *template, sc renderScope) string { var b strings.Builder - b.Grow(t.grow) + b.Grow(t.compiled.grow) // One draw per held name, for this expansion only: a nested template and each // repeat iteration get their own, since each is its own expansion. A reference // path reads the render's hold in sc instead. var held *hold - if t.heldLocal { + if t.compiled.heldLocal { held = &hold{ - variant: make(map[string]node, len(t.held)), - value: make(map[string]readValue, len(t.held)), + variant: make(map[string]node, len(t.compiled.held)), + value: make(map[string]readValue, len(t.compiled.held)), } } - for i := range t.ops { - o := &t.ops[i] + for i := range t.compiled.ops { + o := &t.compiled.ops[i] switch o.kind { case literalRun: b.WriteString(o.lit) diff --git a/render_test.go b/render_test.go index 4302a69..d0da7aa 100644 --- a/render_test.go +++ b/render_test.go @@ -40,8 +40,8 @@ func TestGrowIsALowerBound(t *testing.T) { } for i := 0; i < 50; i++ { var set holdSet - if got := len(expand(f.rand, tmpl, renderScope{set: &set})); got < tmpl.grow { - t.Errorf("format %q: expand emitted %d bytes, below grow %d", format, got, tmpl.grow) + if got := len(expand(f.rand, tmpl, renderScope{set: &set})); got < tmpl.compiled.grow { + t.Errorf("format %q: expand emitted %d bytes, below grow %d", format, got, tmpl.compiled.grow) } } } diff --git a/shape_test.go b/shape_test.go index 9747da2..bc72377 100644 --- a/shape_test.go +++ b/shape_test.go @@ -101,7 +101,7 @@ func readsFact(n node) string { collect(cell) } case *template: - for _, b := range n.refs { + for _, b := range n.link.refs { set[strings.TrimPrefix(b.key, "/")] = true } for _, field := range n.fields { diff --git a/table.go b/table.go index 65b2ec8..4c9165d 100644 --- a/table.go +++ b/table.go @@ -308,15 +308,14 @@ func (t *table) checkCells() error { if strings.IndexByte(cell, '{') < 0 && strings.IndexByte(cell, '}') < 0 { continue } - n, err := compileString(cell) + toks, err := parseChecked(cell, nil) if err != nil { return fmt.Errorf("line %d, %s: %w", row+2, t.header[col], err) } if t.cellTemplates == nil { t.cellTemplates = map[int]*template{} } - t.cellTemplates[i] = n.(*template) - t.cellTemplates[i].cellOf, t.cellTemplates[i].cellRow = t, row + t.cellTemplates[i] = &template{format: cell, tokens: toks, repeat: 1, fromString: true, cell: cellSite{t, row}} } return nil } diff --git a/value.go b/value.go index 4b405c6..3a764d8 100644 --- a/value.go +++ b/value.go @@ -34,7 +34,7 @@ func (p *valueProof) checkDatatype(path string, n node) error { if !ok || t.datatype == DataTypeString { return nil } - if r := t.readsColumn; r != nil && columnDatatype(r.column) == t.datatype { + if r := t.link.readsColumn; r != nil && columnDatatype(r.column) == t.datatype { return fmt.Errorf(`%s: %s takes datatype %s from the column it reads; drop "datatype"`, path, t.format, t.datatype) } if reason := p.proveColumnItem(t).not[t.datatype]; reason != "" { @@ -45,10 +45,10 @@ func (p *valueProof) checkDatatype(path string, n node) error { // proveColumnItem proves a column item: what it renders, or, when it is a column it reads, that column. func (p *valueProof) proveColumnItem(t *template) proven { - if t.readsColumn == nil { + if t.link.readsColumn == nil { return p.prove(t) } - return p.proveColumn(t.readsColumn.column) + return p.proveColumn(t.link.readsColumn.column) } // proveColumn proves a column over what its items draw, a null item marking it null rather than @@ -149,12 +149,12 @@ func (p *valueProof) proveTemplate(t *template) proven { return unproven(fmt.Sprintf("%q carries a repeat, which composes text rather than one value", t.format)) case fixed: return literalValue(lit) - case len(t.ops) != 1: + case len(t.compiled.ops) != 1: v := unproven(notOneValue(t.format, typedCalls)) v.notOperand = notOneValue(t.format, operandCalls) return v } - o := t.ops[0] + o := t.compiled.ops[0] body, name, args := o.body, o.fn, o.args switch _, isTransform := transforms[name]; { case o.kind == fieldAlternation: