diff --git a/datatype.go b/datatype.go index 36ba1ad..fae8097 100644 --- a/datatype.go +++ b/datatype.go @@ -92,7 +92,7 @@ func checkColumns(s nodeScope) error { if !ok || !t.record { return nil } - for _, name := range recordColumns(t) { + for _, name := range sortedNames(t.fields) { if err := check(path, name, t.fields[name]); err != nil { return err } diff --git a/drawfence.go b/drawfence.go index 1e30b7c..fb1e3f5 100644 --- a/drawfence.go +++ b/drawfence.go @@ -88,7 +88,7 @@ func (c *drawCheck) checkRecordDraws(path string, n node) error { return nil } // A record-only template's format renders nothing, so weigh the columns, not the format. - columns := recordColumns(t) + columns := sortedNames(t.fields) reads := false for _, name := range columns { reads = reads || c.readsPath(t.fields[name]) @@ -148,7 +148,7 @@ func readsTable(n node, a arm) bool { if !isTemplate { return false } - _, isTable := t.fields[a.key].(*table) + _, isTable := t.head(a.key).(*table) return isTable } @@ -248,7 +248,7 @@ func (w *drawWalk) walk(n node, at drawAt) { // 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) + tr := tableReadOf(from.(*template).head(a.key), a, e.to) if k := (readKey{at.group, a.path, at.alt.key()}); !w.read[k] { w.read[k] = true w.reads = append(w.reads, pathRead{at, a, tr}) diff --git a/family.go b/family.go index 2bfd352..87ddc28 100644 --- a/family.go +++ b/family.go @@ -212,7 +212,7 @@ func checkOwnFamily(t *template) error { 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() { + if head, isTable := n.(*template).head(a.key).(*table); isTable && head.family() == own.family() { return e, head, true } } diff --git a/fejkdata.go b/fejkdata.go index ac2fbfa..f476679 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -146,9 +146,6 @@ func paths(n node) []string { case *template: out := []string{""} for _, name := range sortedNames(n.fields) { - if isRef(name) { // a binding, not a path segment - continue - } for _, p := range paths(n.fields[name]) { out = append(out, join(name, p)) } diff --git a/graph.go b/graph.go index c17f881..d68982e 100644 --- a/graph.go +++ b/graph.go @@ -78,14 +78,9 @@ func contained(n node) []namedNode { } } -// named skips a bound {/path} key: it is a render edge, not containment, so using -// it as a path segment would report a node under a path that does not reach it. func named(m map[string]node) []namedNode { out := make([]namedNode, 0, len(m)) for _, name := range sortedNames(m) { - if isRef(name) { - continue - } out = append(out, namedNode{name: name, node: m[name]}) } return out @@ -134,8 +129,8 @@ func renderEdges(n node) []renderEdge { var es []renderEdge add := func(name, operand string) { a := splitArm(name, n.refs) - c, ok := n.fields[a.key] - if !ok { + c := n.head(a.key) + if c == nil { return } for _, leaf := range pathLeaves(c, a.tail) { diff --git a/hold.go b/hold.go index 9ad3629..62b3ab1 100644 --- a/hold.go +++ b/hold.go @@ -21,7 +21,7 @@ type draw struct { } // readField renders one arm of a token. An arm's key is a sibling field or a -// reference linkRefs bound into fields. A name the expansion holds — a level some +// reference linkRefs bound into refHeads. A name the expansion holds — a level some // token addresses by dotted path, or a field an operand reads — is drawn once and // kept, so {place.postal-code} and {place.locality} read one row, either read twice // gives one value, and a shown operand is the operand computed. Every other name is @@ -32,13 +32,13 @@ func readField(s *session, t *template, held *hold, sc renderScope, a arm) draw if len(a.tail) > 0 { panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.name, a.key)) } - return draw{text: render(s, t.fields[a.key], sc)} + return draw{text: render(s, t.head(a.key), sc)} } d := readHold(held, sc, a) if r, done := d.value[a.path]; done { return r } - leaf, err := walkPath(t.fields[a.key], a.tail, pathWalk{pins: &d.pins, draws: &pathDraws{s: s, held: d, a: &a}}) + leaf, err := walkPath(t.head(a.key), a.tail, pathWalk{pins: &d.pins, draws: &pathDraws{s: s, held: d, a: &a}}) if err != nil { panic(fmt.Sprintf("fejkdata: %q: %v; a fence should have refused this at New", a.name, err)) } diff --git a/holdfence.go b/holdfence.go index cab1feb..5eae660 100644 --- a/holdfence.go +++ b/holdfence.go @@ -52,12 +52,12 @@ func heldHeads(t *template) []string { func heldNodes(t *template, head string, readers []reader) map[node]bool { held := map[node]bool{} if _, isPath := t.bound[head]; !isPath { - operandDraw(t.fields[head], held) + operandDraw(t.head(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) + coverPath(t.head(head), a.tail, held) } } return held diff --git a/node.go b/node.go index 4179d69..353acae 100644 --- a/node.go +++ b/node.go @@ -49,7 +49,7 @@ func (*null) isNode() {} type template struct { // Filled by `compileString`, `compileTemplate` and `table.compileWhole`: format string - fields map[string]node // `linkTemplateRefs` adds each bound reference here + fields map[string]node repeat int separator string datatype DataType @@ -78,19 +78,19 @@ type template struct { // Filled by `linkTemplateRefs` and `keyDrawGroup`, from the assembled tree: refs map[string]refBinding // each reference the format reads -> what it is bound 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 drawGroupKey string // its draw group keyed by its category: what a render reads its reference paths under } func (*template) isNode() {} -// field is the node a path segment names; a binding is a render edge, not a field. -func (t *template) field(seg string) (node, bool) { - if isRef(seg) { - return nil, false +// 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] } - n, ok := t.fields[seg] - return n, ok + return t.fields[key] } // compile converts parsed JSON — a category or an inline template — into a node tree, @@ -277,8 +277,6 @@ func compileChoice(items []any, pos position) (node, error) { } c.cum = cum } - // Computed before linkRefs binds references into the items: a binding is keyed - // by a reference sigil, which paths skips, so the set is the same after. c.shared = sharedPaths(c.items) return c, nil } diff --git a/path.go b/path.go index 31b132b..e80a60a 100644 --- a/path.go +++ b/path.go @@ -177,7 +177,7 @@ func walkPath(n node, tail []string, w pathWalk) (node, error) { return nil, err } } - child, ok := n.field(tail[0]) + child, ok := n.fields[tail[0]] if !ok { return nil, fmt.Errorf("no field %q", tail[0]) } diff --git a/record.go b/record.go index 7e4d868..2911f44 100644 --- a/record.go +++ b/record.go @@ -245,7 +245,7 @@ func recordOf(n node) (*template, []Column, error) { if !ok { return nil, nil, errors.New("names a choice, not a template; a record is a template whose fields are its columns") } - names := recordColumns(t) + names := sortedNames(t.fields) if len(names) == 0 { return nil, nil, ErrNoColumns } @@ -274,16 +274,3 @@ func renderRecord(s *session, t *template, columns []Column, sc renderScope) *Re } return r } - -// recordColumns is the sorted non-reference field names — the columns a record -// projects. A {/path} binding is carried in fields under its root path, so only a -// name that is not a reference is a column. -func recordColumns(t *template) []string { - var names []string - for _, name := range sortedNames(t.fields) { - if !isRef(name) { - names = append(names, name) - } - } - return names -} diff --git a/reference.go b/reference.go index 92e8559..f1e34c7 100644 --- a/reference.go +++ b/reference.go @@ -68,7 +68,7 @@ func refSegments(name string, folder []string) ([]string, error) { } // linkRefs resolves every reference in the assembled tree. The head of the path — -// up to the category it names — is bound into the referring template's fields +// up to the category it names — is bound into the referring template's refHeads // under its root path, and the rest reads into it the way a sibling path does, so // a reference is held like a sibling and two spellings of one target are one // draw. It runs once, after all data is merged, so a reference sees the final @@ -91,10 +91,8 @@ func linkTemplateRefs(folder []string, path, category string, t *template, root if len(names) == 0 { return nil } - if t.fields == nil { - t.fields = map[string]node{} - } t.refs = make(map[string]refBinding, len(names)) + t.refHeads = make(map[string]node, len(names)) for _, name := range names { segments, err := refSegments(name, folder) if err != nil { @@ -111,7 +109,7 @@ func linkTemplateRefs(folder []string, path, category string, t *template, root if err := checkPath(target, tail, key); err != nil { return fmt.Errorf("%s: reference {%s}: %w", path, name, err) } - t.fields[key] = target + t.refHeads[key] = target t.refs[name] = refBinding{key, tail} } if err := t.compileFormat(); err != nil { @@ -134,7 +132,7 @@ func columnReadOf(t *template) *columnRead { return nil } a := splitArm(name, t.refs) - target, isTemplate := t.fields[a.key].(*template) + target, isTemplate := t.head(a.key).(*template) if !isTemplate || !target.record || len(a.tail) != 1 { return nil } diff --git a/todo.md b/todo.md index b1e9c44..5a5dbcb 100644 --- a/todo.md +++ b/todo.md @@ -19,8 +19,6 @@ Self-sufficiency 5.6), every seat capped by Locality or Self-sufficiency. This round, in order: -- Keep reference bindings out of `template.fields`, so no walk over a template's fields - needs the `isRef` filter `named`, `paths`, `recordColumns` and `template.field` carry. - Compile a template's format once: `compileFormat` runs again from `linkTemplateRefs`, so `ops`, `bound` and `held` are provisional until linking and its fences run twice. - Parse a format once and have the fences read `t.ops`: `eachToken` rescans the format diff --git a/value.go b/value.go index 148e1b0..e952969 100644 --- a/value.go +++ b/value.go @@ -158,7 +158,7 @@ func (p *valueProof) template(t *template) proven { case !isFunc: var leaves []node for _, a := range splitArms(body, t.refs) { - leaves = append(leaves, pathLeaves(t.fields[a.key], a.tail)...) + leaves = append(leaves, pathLeaves(t.head(a.key), a.tail)...) } return p.unite(leaves) case name == "calc":