From bcbf044290a31fab6093649fc6f4d5522271a769 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 2 Sep 2026 18:29:45 +0200 Subject: [PATCH] One path walk under checkPath, descend, readField, pathLeaves and coverPath; split the hold and the render graph into their own files --- README.md | 23 +-- graph.go | 222 +++++++++++++++++++++++++++++ hold.go | 345 +++++++++++++++++++++++++++++++++++++++++++++ node.go | 41 +----- path.go | 109 +++++++++++++++ reference.go | 388 --------------------------------------------------- render.go | 129 ++--------------- template.go | 127 ----------------- 8 files changed, 705 insertions(+), 679 deletions(-) create mode 100644 graph.go create mode 100644 hold.go create mode 100644 path.go diff --git a/README.md b/README.md index 2a939bf..773d875 100644 --- a/README.md +++ b/README.md @@ -402,16 +402,19 @@ GO_VERSION=1.22.12 docker compose run --rm test # the same tests, without the im ## Layout ``` -fejkdata.go Generator, New, options, the embedded data set, List -node.go the node model and JSON -> node compilation -render.go Fake and the recursive renderer (choices, format strings, paths, held draws) -template.go the {token} grammar: scanning, arms, operands, validation -reference.go {/path} binding across the tree, the render graph, and the walks over it -builtins.go the {name()} function registry and its implementations -calc.go the {calc()} arithmetic evaluator: parser, eval, validation -data.go data loading: fs.FS folders/files -> namespace tree, multi-source merge -cmd/fejkdata/ the fejkdata CLI -data/ shipped data (JSON), embedded at build: locale folders + a misc folder +fejkdata.go Generator, New, options, the embedded data set, List +node.go the node model and JSON -> node compilation +path.go the dotted-path walk, and proving a path resolves +render.go Fake and the recursive renderer (choices, format strings, expansions) +template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format +hold.go the hold: one draw per expansion for paths and operands, and its fences +reference.go reference sigils, and binding references across the tree +graph.go the render graph: edges, cycles, the repeat bound, tree walks +builtins.go the {name()} function registry and its implementations +calc.go the {calc()} arithmetic evaluator: parser, eval, validation +data.go data loading: fs.FS folders/files -> namespace tree, multi-source merge +cmd/fejkdata/ the fejkdata CLI +data/ shipped data (JSON), embedded at build: locale folders + a misc folder ``` ## License diff --git a/graph.go b/graph.go new file mode 100644 index 0000000..4e2d09a --- /dev/null +++ b/graph.go @@ -0,0 +1,222 @@ +package fejkdata + +import ( + "fmt" + "sort" + "strings" +) + +// walkNodes calls fn once per contained node, passing the dot path that reaches it, +// visiting keys in sorted order so which of several broken nodes gets reported does +// not depend on map iteration. +func walkNodes(root map[string]node, fn func(path string, n node) error) error { + seen := map[node]bool{} + var visit func(string, node) error + visit = func(path string, n node) error { + if n == nil || seen[n] { + return nil + } + seen[n] = true + if err := fn(path, n); err != nil { + return err + } + for _, c := range contained(n) { + if err := visit(join(path, c.name), c.node); err != nil { + return err + } + } + return nil + } + for _, name := range sortedNames(root) { + if err := visit(name, root[name]); err != nil { + return err + } + } + return nil +} + +// namedNode is a contained child and the segment reaching it; a choice's items carry +// no segment, matching how a dot path steps over a choice. +type namedNode struct { + name string + node node +} + +func contained(n node) []namedNode { + switch n := n.(type) { + case *group: + return named(n.children) + case *choice: + out := make([]namedNode, len(n.items)) + for i, it := range n.items { + out[i] = namedNode{node: it} + } + return out + case *template: + return named(n.fields) + default: + return nil + } +} + +// 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. Only +// a template's fields hold bindings — loadDir skips a dot-prefixed entry, so a +// group's children never carry the prefix — so this one skip serves both. +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 +} + +func sortedNames(m map[string]node) []string { + names := make([]string, 0, len(m)) + for name := range m { + names = append(names, name) + } + sort.Strings(names) + return names +} + +// renderEdge is a child a node renders into, labelled by what reaches it (a field +// name, reference, or choice index) for a readable cycle report. operand names +// the builtin when the label is its operand rather than a token, so an error can +// name it the way the author wrote it. +type renderEdge struct { + to node + label string + operand string +} + +// reached names an edge as the author spelled it, the vocabulary boundReaders uses +// for the sibling fence. +func (e renderEdge) reached() string { + if e.operand != "" { + return fmt.Sprintf("%s operand %q", e.operand, e.label) + } + return "{" + e.label + "}" +} + +// renderEdges lists the children rendering n recurses into, mirroring expand: a +// choice's items, and a template's field/reference tokens plus its operands. A +// group renders nothing, so it has no edges. +func renderEdges(n node) []renderEdge { + switch n := n.(type) { + case *choice: + es := make([]renderEdge, len(n.items)) + for i, it := range n.items { + es[i] = renderEdge{to: it, label: fmt.Sprintf("[%d]", i)} + } + return es + case *template: + var es []renderEdge + add := func(name, operand string) { + a := splitArm(name, n.refs) + c, ok := n.fields[a.key] + if !ok { + return + } + for _, leaf := range pathLeaves(c, a.tail) { + es = append(es, renderEdge{leaf, name, operand}) + } + } + _ = eachToken(n.format, func(t ftoken) error { + if t.kind != 'b' { + return nil + } + if fn, _, isFunc := funcCall(t.body); isFunc { + for _, operand := range tokenOperands(t.body) { + add(operand, fn) + } + return nil + } + for _, name := range strings.Split(t.body, "|") { + add(name, "") + } + return nil + }) + return es + default: + return nil + } +} + +// pathLeaves lists what a token's dotted tail renders: a choice on the way +// contributes every variant, since any of them may be the one drawn. checkPath has +// already proved the tail resolves in every variant. +func pathLeaves(n node, tail []string) []node { + var out []node + _ = walkPath(n, tail, pathWalk{ + choice: func(c *choice, _ []string) ([]node, error) { return c.items, nil }, + leaf: func(n node) error { out = append(out, n); return nil }, + }) + return out +} + +// checkRepeatReach bounds the renders a repeat multiplies to along any root-to-leaf +// path, so nested repeats cannot build what one repeat may not. It runs after +// checkNoCycles, whose guarantee is what lets the walk terminate. +func checkRepeatReach(root map[string]node) error { + reach := map[node]int{} + var of func(n node) int + of = func(n node) int { + if r, done := reach[n]; done { + return r + } + r := 1 + for _, e := range renderEdges(n) { + if c := of(e.to); c > r { + r = c + } + } + if t, ok := n.(*template); ok { + r *= t.repeat + } + reach[n] = r + return r + } + return walkNodes(root, func(path string, n node) error { + if t, ok := n.(*template); ok && t.repeat > 1 && of(n) > maxLen { + return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, of(n), maxLen) + } + return nil + }) +} + +// checkNoCycles rejects a reference cycle: a node whose rendering can reach itself +// — directly, mutually, or through a chain — never terminates, so it must fail at +// New rather than stack-overflow at render. It is a depth-first walk of the render +// graph (renderEdges); grey marks nodes on the current path so a back-edge to one +// is the cycle, while black lets a shared node (a DAG, not a cycle) be skipped. +// Every node is a root: a field its parent's format never renders is still reachable +// by dot path, so a cycle in one would otherwise reach render and be fatal there. +func checkNoCycles(root map[string]node) error { + const ( + grey = 1 + black = 2 + ) + color := map[node]int{} + var visit func(n node, path string) error + visit = func(n node, path string) error { + switch color[n] { + case grey: + return fmt.Errorf("reference cycle: %s", path) + case black: + return nil + } + color[n] = grey + for _, e := range renderEdges(n) { + if err := visit(e.to, path+" -> "+e.label); err != nil { + return err + } + } + color[n] = black + return nil + } + return walkNodes(root, func(path string, n node) error { return visit(n, path) }) +} diff --git a/hold.go b/hold.go new file mode 100644 index 0000000..965dce2 --- /dev/null +++ b/hold.go @@ -0,0 +1,345 @@ +package fejkdata + +import ( + "fmt" + "sort" + "strings" +) + +// checkBoundLevelsHeld rejects every route to a held 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. +// +// It runs after checkNoCycles, whose guarantee is what lets the walk terminate. +func checkBoundLevelsHeld(root map[string]node) error { + return walkNodes(root, func(path string, n node) error { + t, ok := n.(*template) + if !ok || len(t.held) == 0 { + return nil + } + heads := make([]string, 0, len(t.held)) + for head := range t.held { + heads = append(heads, head) + } + // 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. + 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] + }) + readers := boundReaders(t.format, t.bound, t.refs) + for _, head := range heads { + // What one draw answers for depends on how the draw is read: a path pins + // the levels it passes through and the leaf it lands on, an operand + // exactly the value its render produces. + held := map[node]bool{} + reader, isPath := t.bound[head] + if isPath { + for _, r := range readers { + if a := splitArm(r.name, t.refs); a.key == head { + coverPath(t.fields[head], a.tail, held) + } + } + } else { + operandDraw(t.fields[head], held) + } + if len(held) == 0 { + continue // an early out: a fixed head holds nothing to reach + } + // 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 here. + seen := map[node]bool{} + for _, e := range renderEdges(t) { + if splitArm(e.label, t.refs).key == head { + continue // a token or operand reading this draw, the routes allowed + } + if renders(e.to, held, seen) { + if isPath { + return fmt.Errorf("%s: %s renders %q, which {%s} reads a path into; name the fields you want instead", path, e.reached(), head, reader) + } + return fmt.Errorf("%s: %s renders %q, which a {%s()} also reads; reach it one way so it is drawn once", path, 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 +} + +// arm is one alternative of a {a|b} token or one operand, split into the key +// naming the node in a template's fields (a sibling field, or the head a +// reference is bound under) and the tail of a dotted path into it. A non-empty +// tail is what makes the arm a bound draw: its head is drawn once per expansion +// (see compileOps). +type arm struct { + name string // as written, and the key a bound draw's value is held under + key string + tail []string + steps []string // key per level passed through; the head and leaf hold their own +} + +// splitArm splits one name into key and tail. refs maps a reference to what +// linkRefs bound it to; before linking, a reference is whole. +func splitArm(name string, refs map[string]refBinding) arm { + if isRef(name) { + b, bound := refs[name] + if !bound || len(b.tail) == 0 { + key := name + if bound { + key = b.key + } + return arm{name: name, key: key} + } + return pathArm(name, b.key, b.tail) + } + head, tail, dotted := strings.Cut(name, ".") + if !dotted { + return arm{name: name, key: name} + } + return pathArm(name, head, strings.Split(tail, ".")) +} + +func pathArm(name, key string, segs []string) arm { + var steps []string + for i := 0; i < len(segs)-1; i++ { // every level except the leaf's own + steps = append(steps, key+"."+strings.Join(segs[:i+1], ".")) + } + return arm{name: name, key: key, tail: segs, steps: steps} +} + +// checkNoOverlap rejects a format that both renders a level and reads a path into +// it — {p} beside {p.first}, or {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. Names 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].name < names[j].name }) + for i, level := range names { + for _, path := range names[i+1:] { + if strings.HasPrefix(path.name, level.name+".") { + 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, and how to name that spelling. +type reader struct{ name, label string } + +// boundReaders lists every way a format reaches a bound 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 { + names = append(names, reader{a.name, fmt.Sprintf("%s operand %q", fn, operand)}) + } + } + return nil + } + for _, a := range splitArms(t.body, refs) { + if _, isBound := bound[a.key]; isBound { + names = append(names, reader{a.name, "token {" + a.name + "}"}) + } + } + return nil + }) + return names +} + +// splitArms splits a token body's '|' alternatives. +func splitArms(body string, refs map[string]refBinding) []arm { + parts := strings.Split(body, "|") + arms := make([]arm, len(parts)) + for i, p := range parts { + arms[i] = splitArm(p, refs) + } + return arms +} + +// 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 + }) +} + +// draws is what an expansion has already drawn for its held names: the variant each +// was drawn as, so every path under it reads one row, and the value each read, so +// the same name read twice reads one value. +type draws struct { + variant map[string]node + value map[string]string +} + +// 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 +// 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 +// drawn afresh, so {word} {word} still draws twice. checkTokens, checkPath and +// linkRefs prove every step, so the walk cannot fail. +func readField(s *session, t *template, held *draws, a arm) string { + if !t.held[a.key] { + return render(s, t.fields[a.key]) + } + if v, read := held.value[a.name]; read { + return v + } + var v string + _ = walkPath(t.fields[a.key], a.tail, pathWalk{ + // Hold the draw at every level passed through, so two paths sharing a + // prefix share it. + choice: func(c *choice, rest []string) ([]node, error) { + key := a.key + if consumed := len(a.tail) - len(rest); consumed > 0 { + key = a.steps[consumed-1] + } + n, drew := held.variant[key] + if !drew { + n = drawn(s, c) + held.variant[key] = n + } + return []node{n}, nil + }, + leaf: func(n node) error { v = render(s, n); return nil }, + }) + held.value[a.name] = v + return v +} + +// drawn resolves a choice to one variant, so a bound head is a concrete node the +// rest of the expansion shares. Nested choices unwrap too: a draw is one value, not +// another set to pick from. +func drawn(s *session, n node) node { + for c, ok := n.(*choice); ok; c, ok = n.(*choice) { + n = pick(s, c) + } + return n +} diff --git a/node.go b/node.go index 84da64c..6cea876 100644 --- a/node.go +++ b/node.go @@ -149,8 +149,8 @@ func compileChoice(items []any) (node, error) { } c.cum = cum } - // Safe to precompute: a choice's items come from one file, so no group can - // appear inside one, and neither mergeChildren nor linkRefs can reach in. + // 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 } @@ -229,43 +229,6 @@ func compileTemplate(m map[string]any) (node, error) { return t, nil } -// checkPath reports whether a token's dotted tail can address a node whichever way -// the draw goes, by the reachability rule descend applies — a choice must carry -// the whole remaining path in the set every variant shares — plus the -// rules a held draw adds, which descend has no need of: a level a path reads may -// not carry a repeat, and each variant answers for that itself. So a path that -// validates here resolves on every render, and a typo is a New-time error. -func checkPath(n node, tail []string, level string) error { - if len(tail) == 0 { - return nil - } - switch n := n.(type) { - case *template: - if n.repeat > 1 { - return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", level) - } - child, ok := n.fields[tail[0]] - if !ok { - return fmt.Errorf("no field %q", tail[0]) - } - return checkPath(child, tail[1:], level+"."+tail[0]) - case *choice: - if want := strings.Join(tail, "."); !n.shared[want] { - return unreachableInChoice(n, want) - } - // Reachability is settled; each variant still answers for itself, so a - // rule about the level (its repeat) holds behind a choice as in front. - for _, item := range n.items { - if err := checkPath(item, tail, level); err != nil { - return err - } - } - return nil - default: - return fmt.Errorf("cannot descend into %T at %q", n, tail[0]) - } -} - // repeatOf reads a template's "repeat" (default 1): how many times its format // is rendered and concatenated. A present one must be an integer above 1. func repeatOf(m map[string]any) (int, error) { diff --git a/path.go b/path.go new file mode 100644 index 0000000..40e7461 --- /dev/null +++ b/path.go @@ -0,0 +1,109 @@ +package fejkdata + +import ( + "fmt" + "sort" + "strings" +) + +// pathWalk is what one walk of a dotted path does at each kind of level: choice +// returns the variants to continue into (none stops the walk); level runs at each +// template a segment descends into; leaf runs where the tail ends. A nil action +// is skipped. +type pathWalk struct { + choice func(c *choice, rest []string) ([]node, error) + level func(t *template, rest []string) error + leaf func(n node) error +} + +// walkPath descends tail from n: a group or template by its next segment, a +// choice by w.choice, which consumes no segment. A missing segment is an error, +// so no walk reaches past what the data holds. +func walkPath(n node, tail []string, w pathWalk) error { + if len(tail) == 0 { + if w.leaf != nil { + return w.leaf(n) + } + return nil + } + switch n := n.(type) { + case *group: + child, ok := n.children[tail[0]] + if !ok { + return fmt.Errorf("no entry %q", tail[0]) + } + return walkPath(child, tail[1:], w) + case *template: + if w.level != nil { + if err := w.level(n, tail); err != nil { + return err + } + } + child, ok := n.fields[tail[0]] + if !ok { + return fmt.Errorf("no field %q", tail[0]) + } + return walkPath(child, tail[1:], w) + case *choice: + if w.choice == nil { + return nil + } + next, err := w.choice(n, tail) + if err != nil { + return err + } + for _, item := range next { + if err := walkPath(item, tail, w); err != nil { + return err + } + } + return nil + } + return fmt.Errorf("cannot descend into %T at %q", n, tail[0]) +} + +// carriedByAll is the choice rule a path that must resolve on every call obeys: +// the rest of the tail must be one every variant carries. +func carriedByAll(c *choice, rest []string) error { + if want := strings.Join(rest, "."); !c.shared[want] { + return unreachableInChoice(c, want) + } + return nil +} + +// unreachableInChoice reports that a path cannot step through this choice, listing +// what every variant does carry. It reads the precomputed set, so a failing path +// costs no more than a rendering one. +func unreachableInChoice(c *choice, want string) error { + if len(c.shared) == 0 { + return fmt.Errorf("no variant of this %d-way choice carries %q", len(c.items), want) + } + offered := make([]string, 0, len(c.shared)) + for p := range c.shared { + offered = append(offered, p) + } + sort.Strings(offered) + return fmt.Errorf("not every variant of this %d-way choice carries %q; all carry %v", len(c.items), want, offered) +} + +// checkPath proves a dotted tail resolves whichever way the draws go — a choice +// must carry the rest of the path in the set every variant shares — and that no +// level a path reads carries a repeat, which one draw of it could not apply. So a +// path that validates here resolves on every render, and a typo is a New-time +// error. +func checkPath(n node, tail []string, level string) error { + return walkPath(n, tail, pathWalk{ + choice: func(c *choice, rest []string) ([]node, error) { + if err := carriedByAll(c, rest); err != nil { + return nil, err + } + return c.items, nil + }, + level: func(t *template, rest []string) error { + if t.repeat > 1 { + return fmt.Errorf("the level %q carries a repeat, which a path reading one draw of it cannot apply", join(level, strings.Join(tail[:len(tail)-len(rest)], "."))) + } + return nil + }, + }) +} diff --git a/reference.go b/reference.go index 0d59f23..6d685e4 100644 --- a/reference.go +++ b/reference.go @@ -2,7 +2,6 @@ package fejkdata import ( "fmt" - "sort" "strings" ) @@ -137,248 +136,6 @@ func eachTemplate(root map[string]node, fn func(folder []string, path string, t return inFolder(nil, root) } -// checkBoundLevelsHeld rejects every route to a held 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. -// -// It runs after checkNoCycles, whose guarantee is what lets the walk terminate. -func checkBoundLevelsHeld(root map[string]node) error { - return walkNodes(root, func(path string, n node) error { - t, ok := n.(*template) - if !ok || len(t.held) == 0 { - return nil - } - heads := make([]string, 0, len(t.held)) - for head := range t.held { - heads = append(heads, head) - } - // 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. - 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] - }) - readers := boundReaders(t.format, t.bound, t.refs) - for _, head := range heads { - // What one draw answers for depends on how the draw is read: a path pins - // the levels it passes through and the leaf it lands on, an operand - // exactly the value its render produces. - held := map[node]bool{} - reader, isPath := t.bound[head] - if isPath { - for _, r := range readers { - if a := splitArm(r.name, t.refs); a.key == head { - coverPath(t.fields[head], a.tail, held) - } - } - } else { - operandDraw(t.fields[head], held) - } - if len(held) == 0 { - continue // an early out: a fixed head holds nothing to reach - } - // 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 here. - seen := map[node]bool{} - for _, e := range renderEdges(t) { - if splitArm(e.label, t.refs).key == head { - continue // a token or operand reading this draw, the routes allowed - } - if renders(e.to, held, seen) { - if isPath { - return fmt.Errorf("%s: %s renders %q, which {%s} reads a path into; name the fields you want instead", path, e.reached(), head, reader) - } - return fmt.Errorf("%s: %s renders %q, which a {%s()} also reads; reach it one way so it is drawn once", path, 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) { - if _, isChoice := n.(*choice); isChoice || len(tail) == 0 { - cover(n, into, false) - return - } - t, ok := n.(*template) - if !ok { - return - } - if child, ok := t.fields[tail[0]]; ok { - coverPath(child, tail[1:], into) - } -} - -// 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 -} - -// walkNodes calls fn once per contained node, passing the dot path that reaches it, -// visiting keys in sorted order so which of several broken nodes gets reported does -// not depend on map iteration. -func walkNodes(root map[string]node, fn func(path string, n node) error) error { - seen := map[node]bool{} - var visit func(string, node) error - visit = func(path string, n node) error { - if n == nil || seen[n] { - return nil - } - seen[n] = true - if err := fn(path, n); err != nil { - return err - } - for _, c := range contained(n) { - if err := visit(join(path, c.name), c.node); err != nil { - return err - } - } - return nil - } - for _, name := range sortedNames(root) { - if err := visit(name, root[name]); err != nil { - return err - } - } - return nil -} - -// namedNode is a contained child and the segment reaching it; a choice's items carry -// no segment, matching how a dot path steps over a choice. -type namedNode struct { - name string - node node -} - -func contained(n node) []namedNode { - switch n := n.(type) { - case *group: - return named(n.children) - case *choice: - out := make([]namedNode, len(n.items)) - for i, it := range n.items { - out[i] = namedNode{node: it} - } - return out - case *template: - return named(n.fields) - default: - return nil - } -} - -// 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. Only -// a template's fields hold bindings — loadDir skips a dot-prefixed entry, so a -// group's children never carry the prefix — so this one skip serves both. -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 -} - -func sortedNames(m map[string]node) []string { - names := make([]string, 0, len(m)) - for name := range m { - names = append(names, name) - } - sort.Strings(names) - return names -} - // resolveRef walks a reference path through the folders to the category it names, // returning that head, the node, and the tail left to read into it. func resolveRef(root map[string]node, segments []string) (head []string, target node, tail []string, err error) { @@ -413,148 +170,3 @@ func refTokens(format string) []string { } return refs } - -// renderEdge is a child a node renders into, labelled by what reaches it (a field -// name, reference, or choice index) for a readable cycle report. operand names -// the builtin when the label is its operand rather than a token, so an error can -// name it the way the author wrote it. -type renderEdge struct { - to node - label string - operand string -} - -// reached names an edge as the author spelled it, the vocabulary boundReaders uses -// for the sibling fence. -func (e renderEdge) reached() string { - if e.operand != "" { - return fmt.Sprintf("%s operand %q", e.operand, e.label) - } - return "{" + e.label + "}" -} - -// renderEdges lists the children rendering n recurses into, mirroring expand: a -// choice's items, and a template's field/reference tokens plus its operands. A -// group renders nothing, so it has no edges. -func renderEdges(n node) []renderEdge { - switch n := n.(type) { - case *choice: - es := make([]renderEdge, len(n.items)) - for i, it := range n.items { - es[i] = renderEdge{to: it, label: fmt.Sprintf("[%d]", i)} - } - return es - case *template: - var es []renderEdge - add := func(name, operand string) { - a := splitArm(name, n.refs) - c, ok := n.fields[a.key] - if !ok { - return - } - for _, leaf := range pathLeaves(c, a.tail) { - es = append(es, renderEdge{leaf, name, operand}) - } - } - _ = eachToken(n.format, func(t ftoken) error { - if t.kind != 'b' { - return nil - } - if fn, _, isFunc := funcCall(t.body); isFunc { - for _, operand := range tokenOperands(t.body) { - add(operand, fn) - } - return nil - } - for _, name := range strings.Split(t.body, "|") { - add(name, "") - } - return nil - }) - return es - default: - return nil - } -} - -// pathLeaves lists what a token's dotted tail renders. A path draws the levels it -// passes through but renders only what it lands on, so the leaf is the edge — a -// bare token, whose tail is empty, lands on the field itself. A choice on the way -// contributes every variant, since any of them may be the one drawn. checkPath has -// already proved the tail resolves in every variant, so the walk drops nothing. -func pathLeaves(n node, tail []string) []node { - if len(tail) == 0 { - return []node{n} - } - if c, ok := n.(*choice); ok { - var out []node - for _, it := range c.items { - out = append(out, pathLeaves(it, tail)...) - } - return out - } - return pathLeaves(child(n, tail[0]), tail[1:]) -} - -// checkRepeatReach bounds the renders a repeat multiplies to along any root-to-leaf -// path, so nested repeats cannot build what one repeat may not. It runs after -// checkNoCycles, whose guarantee is what lets the walk terminate. -func checkRepeatReach(root map[string]node) error { - reach := map[node]int{} - var of func(n node) int - of = func(n node) int { - if r, done := reach[n]; done { - return r - } - r := 1 - for _, e := range renderEdges(n) { - if c := of(e.to); c > r { - r = c - } - } - if t, ok := n.(*template); ok { - r *= t.repeat - } - reach[n] = r - return r - } - return walkNodes(root, func(path string, n node) error { - if t, ok := n.(*template); ok && t.repeat > 1 && of(n) > maxLen { - return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, of(n), maxLen) - } - return nil - }) -} - -// checkNoCycles rejects a reference cycle: a node whose rendering can reach itself -// — directly, mutually, or through a chain — never terminates, so it must fail at -// New rather than stack-overflow at render. It is a depth-first walk of the render -// graph (renderEdges); grey marks nodes on the current path so a back-edge to one -// is the cycle, while black lets a shared node (a DAG, not a cycle) be skipped. -// Every node is a root: a field its parent's format never renders is still reachable -// by dot path, so a cycle in one would otherwise reach render and be fatal there. -func checkNoCycles(root map[string]node) error { - const ( - grey = 1 - black = 2 - ) - color := map[node]int{} - var visit func(n node, path string) error - visit = func(n node, path string) error { - switch color[n] { - case grey: - return fmt.Errorf("reference cycle: %s", path) - case black: - return nil - } - color[n] = grey - for _, e := range renderEdges(n) { - if err := visit(e.to, path+" -> "+e.label); err != nil { - return err - } - } - color[n] = black - return nil - } - return walkNodes(root, func(path string, n node) error { return visit(n, path) }) -} diff --git a/render.go b/render.go index 937874e..2d998fb 100644 --- a/render.go +++ b/render.go @@ -33,48 +33,20 @@ func (f *Generator) Fake(path string) (string, error) { // descend walks named fields to the node a path names. It is the one render-side // step that can fail, because the path comes from the caller and may name a field // that does not exist. A choice consumes no segment, so the rest of the path must -// be one every variant carries (the set compile stored) before a variant is picked -// — a path that resolves at all resolves on every call. -func descend(s *session, n node, segments []string) (node, error) { - if len(segments) == 0 { - return n, nil - } - switch n := n.(type) { - case *group: - child, ok := n.children[segments[0]] - if !ok { - return nil, fmt.Errorf("no entry %q", segments[0]) - } - return descend(s, child, segments[1:]) - case *template: - child, ok := n.fields[segments[0]] - if !ok { - return nil, fmt.Errorf("no field %q", segments[0]) - } - return descend(s, child, segments[1:]) - case *choice: - if want := strings.Join(segments, "."); !n.shared[want] { - return nil, unreachableInChoice(n, want) - } - return descend(s, pick(s, n), segments) - default: - return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[0]) - } -} - -// unreachableInChoice reports that a path cannot step through this choice, listing -// what every variant does carry. It reads the precomputed set, so a failing path -// costs no more than a rendering one. -func unreachableInChoice(c *choice, want string) error { - if len(c.shared) == 0 { - return fmt.Errorf("no variant of this %d-way choice carries %q", len(c.items), want) - } - offered := make([]string, 0, len(c.shared)) - for p := range c.shared { - offered = append(offered, p) - } - sort.Strings(offered) - return fmt.Errorf("not every variant of this %d-way choice carries %q; all carry %v", len(c.items), want, offered) +// be one every variant carries before a variant is picked — a path that resolves +// at all resolves on every call. +func descend(s *session, root node, segments []string) (node, error) { + var found node + err := walkPath(root, segments, pathWalk{ + choice: func(c *choice, rest []string) ([]node, error) { + if err := carriedByAll(c, rest); err != nil { + return nil, err + } + return []node{pick(s, c)}, nil + }, + leaf: func(n node) error { found = n; return nil }, + }) + return found, err } // render evaluates a compiled node to a string. compile validates every node up @@ -152,76 +124,3 @@ func expand(s *session, t *template) string { } return b.String() } - -// draws is what an expansion has already drawn for its held names: the variant each -// was drawn as, so every path under it reads one row, and the value each read, so -// the same name read twice reads one value. -type draws struct { - variant map[string]node - value map[string]string -} - -// readField renders one arm of a token. An arm's key is a sibling field or a -// {/path} reference, which linkRefs bound into fields too. A name the expansion -// holds — a level some token addresses by dotted path, or a sibling a {calc()} -// 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 drawn afresh, so {word} {word} still draws twice. -// checkTokens, checkPath and linkRefs prove every step, so this cannot fail. -func readField(s *session, t *template, held *draws, a arm) string { - if !t.held[a.key] { - return render(s, t.fields[a.key]) - } - if v, read := held.value[a.name]; read { - return v - } - n, drew := held.variant[a.key] - if !drew { - n = drawn(s, t.fields[a.key]) - held.variant[a.key] = n - } - // Hold the draw at every level passed through, so two paths sharing a prefix - // share it. - for i, seg := range a.tail { - if i < len(a.steps) { - step, drew := held.variant[a.steps[i]] - if !drew { - step = drawn(s, child(n, seg)) - held.variant[a.steps[i]] = step - } - n = step - continue - } - n = child(n, seg) - } - v := render(s, n) - held.value[a.name] = v - return v -} - -// child is the node one path segment names below an already-drawn node. It holds -// while checkPath and the set a choice shares (see sharedPaths) agree with this -// walk: both prove the segment exists and that drawn leaves a template here. Each -// way that can break panics naming the segment, so a slip in that agreement -// reports where it happened rather than surfacing a nil node a level later. -func child(n node, seg string) node { - t, ok := n.(*template) - if !ok { - panic(fmt.Sprintf("fejkdata: %q under %T, which carries no fields", seg, n)) - } - c, ok := t.fields[seg] - if !ok { - panic(fmt.Sprintf("fejkdata: no field %q under a drawn level", seg)) - } - return c -} - -// drawn resolves a choice to one variant, so a bound head is a concrete node the -// rest of the expansion shares. Nested choices unwrap too: a draw is one value, not -// another set to pick from. -func drawn(s *session, n node) node { - for c, ok := n.(*choice); ok; c, ok = n.(*choice) { - n = pick(s, c) - } - return n -} diff --git a/template.go b/template.go index bb337cd..03c057e 100644 --- a/template.go +++ b/template.go @@ -2,7 +2,6 @@ package fejkdata import ( "fmt" - "sort" "strings" ) @@ -229,98 +228,6 @@ func fieldTokens(format string) []string { return names } -// arm is one alternative of a {a|b} token or one operand, split into the key -// naming the node in a template's fields (a sibling field, or the head a -// reference is bound under) and the tail of a dotted path into it. A non-empty -// tail is what makes the arm a bound draw: its head is drawn once per expansion -// (see compileOps). -type arm struct { - name string // as written, and the key a bound draw's value is held under - key string - tail []string - steps []string // key per level passed through; the head and leaf hold their own -} - -// splitArm splits one name into key and tail. refs maps a reference to what -// linkRefs bound it to; before linking, a reference is whole. -func splitArm(name string, refs map[string]refBinding) arm { - if isRef(name) { - b, bound := refs[name] - if !bound || len(b.tail) == 0 { - key := name - if bound { - key = b.key - } - return arm{name: name, key: key} - } - return pathArm(name, b.key, b.tail) - } - head, tail, dotted := strings.Cut(name, ".") - if !dotted { - return arm{name: name, key: name} - } - return pathArm(name, head, strings.Split(tail, ".")) -} - -func pathArm(name, key string, segs []string) arm { - var steps []string - for i := 0; i < len(segs)-1; i++ { // every level except the leaf's own - steps = append(steps, key+"."+strings.Join(segs[:i+1], ".")) - } - return arm{name: name, key: key, tail: segs, steps: steps} -} - -// checkNoOverlap rejects a format that both renders a level and reads a path into -// it — {p} beside {p.first}, or {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. Names 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].name < names[j].name }) - for i, level := range names { - for _, path := range names[i+1:] { - if strings.HasPrefix(path.name, level.name+".") { - 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, and how to name that spelling. -type reader struct{ name, label string } - -// boundReaders lists every way a format reaches a bound 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 { - names = append(names, reader{a.name, fmt.Sprintf("%s operand %q", fn, operand)}) - } - } - return nil - } - for _, a := range splitArms(t.body, refs) { - if _, isBound := bound[a.key]; isBound { - names = append(names, reader{a.name, "token {" + a.name + "}"}) - } - } - return nil - }) - return names -} - // checkSegments rejects an unfinished path: "{a.}", "{.b}" and "{a..b}" each have // a segment naming nothing. A field really named "" would otherwise make them // resolve, so a typo would read as a path that worked. @@ -339,16 +246,6 @@ func checkSegments(a arm) error { return nil } -// splitArms splits a token body's '|' alternatives. -func splitArms(body string, refs map[string]refBinding) []arm { - parts := strings.Split(body, "|") - arms := make([]arm, len(parts)) - for i, p := range parts { - arms[i] = splitArm(p, refs) - } - return arms -} - // callFn is a builtin bound to one call site: its args already parsed. It reads the // output emitted so far in the current expansion (a derivation's payload) and the // values of the operands it named, which expand read for it. @@ -450,27 +347,3 @@ func compileOps(format string, refs map[string]refBinding) formatOps { flush() return c } - -// 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 - }) -}