Answer stability round 1, prose round 1 and product-owner round 1: startOf's doc names the head, the old senses leave names, comments, messages and tests, and the vocabulary defines scope, link and head

This commit is contained in:
2026-10-07 00:56:45 +02:00
committed by Lillem4n
parent 98bb73f096
commit 63223c6d63
25 changed files with 81 additions and 80 deletions
+2 -2
View File
@@ -961,14 +961,14 @@ table.go tables: their options, format and cells, and the link to the par
name.go names: {x as n} bindings, the scopes they live in, and the reads they answer name.go names: {x as n} bindings, the scopes they live in, and the reads they answer
namefence.go the load fences over names namefence.go the load fences over names
pick.go a render's named picks: the frames of the name scopes rendering, the memo a pick keeps, and the reads kept under it pick.go a render's named picks: the frames of the name scopes rendering, the memo a pick keeps, and the reads kept under it
env.go the env a render reads names and rows through env.go renderEnv: what a render reads names and rows through
path.go the dotted-path walks, and proving a path resolves path.go the dotted-path walks, and proving a path resolves
render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make render.go Fake, the recursive renderer (choices, format strings, expansions) and the reads its tokens make
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
struct.go structs: FakeStruct, fake tags as one record's columns, and a field's Go type as its column's datatype struct.go structs: FakeStruct, fake tags as one record's columns, and a field's Go type as its column's datatype
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and resolve inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and resolve
format.go a format's tokens checked against their fields, and compiled format.go a format's tokens checked against their fields, and compiled
reference.go resolving references across the tree reference.go one reference's resolution: its sigil, its category, its tail
resolve.go resolving templates against the assembled tree in steps, each over every template before the next resolve.go resolving templates against the assembled tree in steps, each over every template before the next
graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences
calc.go the {calc()} operand checks: never a number, and a constant zero divisor calc.go the {calc()} operand checks: never a number, and a constant zero divisor
+6 -6
View File
@@ -145,7 +145,7 @@ func TestBuiltinArgLimits(t *testing.T) {
`{"format":"{x}","x":"1","repeat":2000000000}`, // repeat -> multi-GB join `{"format":"{x}","x":"1","repeat":2000000000}`, // repeat -> multi-GB join
} }
for _, tmpl := range bad { for _, tmpl := range bad {
if _, err := linked(t, tmpl); err == nil { if _, err := resolved(t, tmpl); err == nil {
t.Errorf("compile(%s) = nil error, want a limit rejection", tmpl) t.Errorf("compile(%s) = nil error, want a limit rejection", tmpl)
} }
} }
@@ -204,12 +204,12 @@ func mustPanic(t *testing.T, name string, call func()) {
func TestClassBuiltinArgs(t *testing.T) { func TestClassBuiltinArgs(t *testing.T) {
for _, bad := range []string{`"{digits(0)}"`, `"{digits(2000000000)}"`, `"{upper(-1)}"`, `"{lower(x)}"`, `"{digits()}"`, `"{upper(1,2)}"`} { for _, bad := range []string{`"{digits(0)}"`, `"{digits(2000000000)}"`, `"{upper(-1)}"`, `"{lower(x)}"`, `"{digits()}"`, `"{upper(1,2)}"`} {
if _, err := linked(t, bad); err == nil { if _, err := resolved(t, bad); err == nil {
t.Errorf("compile(%s) = nil error, want the arg rejected", bad) t.Errorf("compile(%s) = nil error, want the arg rejected", bad)
} }
} }
for _, ok := range []string{`"{digits(1048576)}"`, `"{upper(1)}"`, `"{lower(26)}"`} { for _, ok := range []string{`"{digits(1048576)}"`, `"{upper(1)}"`, `"{lower(26)}"`} {
if _, err := linked(t, ok); err != nil { if _, err := resolved(t, ok); err != nil {
t.Errorf("compile(%s) = %v", ok, err) t.Errorf("compile(%s) = %v", ok, err)
} }
} }
@@ -291,7 +291,7 @@ func TestBuiltinDateArgs(t *testing.T) {
`"{time('x')}"`: "text", `"{time('x')}"`: "text",
`"{date(1990-01-01,1990-12-31,'15:04')}"`: "time('15:04')", `"{date(1990-01-01,1990-12-31,'15:04')}"`: "time('15:04')",
} { } {
_, err := linked(t, tmpl) _, err := resolved(t, tmpl)
if err == nil || !strings.Contains(err.Error(), want) { if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error mentioning %q", tmpl, err, want) t.Errorf("compile(%s) = %v, want an error mentioning %q", tmpl, err, want)
} }
@@ -405,7 +405,7 @@ func TestBuiltinCompileErrors(t *testing.T) {
`"{iban(US)}"`, // unsupported country `"{iban(US)}"`, // unsupported country
`"{seq(a,b)}"`, // seq takes at most one name `"{seq(a,b)}"`, // seq takes at most one name
} { } {
if _, err := linked(t, bad); err == nil { if _, err := resolved(t, bad); err == nil {
t.Errorf("compile(%s) = nil error, want error", bad) t.Errorf("compile(%s) = nil error, want error", bad)
} }
} }
@@ -419,7 +419,7 @@ func TestArgErrorsNameTheSpelling(t *testing.T) {
`"{hex(99999999999999999999)}"`: "exceeds the maximum", `"{hex(99999999999999999999)}"`: "exceeds the maximum",
`"{int(007,9)}"`: "write 7", `"{int(007,9)}"`: "write 7",
} { } {
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error saying %q", src, err, want) t.Errorf("compile(%s) = %v, want an error saying %q", src, err, want)
} }
} }
+4 -4
View File
@@ -204,7 +204,7 @@ func TestCalcOverANeverNumericOperandIsRejected(t *testing.T) {
`{"format":"{calc(x * 2)}","x":["a","b"]}`: `"x"`, `{"format":"{calc(x * 2)}","x":["a","b"]}`: `"x"`,
`{"format":"{calc(x + y)}","x":"1","y":"{{2}}"}`: `"{2}"`, `{"format":"{calc(x + y)}","x":"1","y":"{{2}}"}`: `"{2}"`,
} { } {
_, err := linked(t, src) _, err := resolved(t, src)
if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "never a number") { if err == nil || !strings.Contains(err.Error(), want) || !strings.Contains(err.Error(), "never a number") {
t.Errorf("compile(%s) = %v, want the operand rejected naming %s", src, err, want) t.Errorf("compile(%s) = %v, want the operand rejected naming %s", src, err, want)
} }
@@ -215,7 +215,7 @@ func TestCalcOverANeverNumericOperandIsRejected(t *testing.T) {
`{"format":"{calc(x * 2)}","x":["1","abc"]}`, `{"format":"{calc(x * 2)}","x":["1","abc"]}`,
`{"format":"{calc(x * 2)}","x":"{digits(2)}"}`, `{"format":"{calc(x * 2)}","x":"{digits(2)}"}`,
} { } {
if _, err := linked(t, ok); err != nil { if _, err := resolved(t, ok); err != nil {
t.Errorf("compile(%s) = %v, want it accepted", ok, err) t.Errorf("compile(%s) = %v, want it accepted", ok, err)
} }
} }
@@ -230,7 +230,7 @@ func TestCalcConstantZeroDivisorIsRejected(t *testing.T) {
`{"format":"{calc((a/0)+b)}","a":"1","b":"2"}`: "divides by 0", `{"format":"{calc((a/0)+b)}","a":"1","b":"2"}`: "divides by 0",
`{"format":"{calc(a/-0)}","a":"1"}`: "divides by -0", `{"format":"{calc(a/-0)}","a":"1"}`: "divides by -0",
} { } {
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want the constant zero divisor rejected naming %q", src, err, want) t.Errorf("compile(%s) = %v, want the constant zero divisor rejected naming %q", src, err, want)
} }
} }
@@ -259,7 +259,7 @@ func TestCalcCompileErrors(t *testing.T) {
`"{calc(1, x)}"`, // decimals arg not an integer `"{calc(1, x)}"`, // decimals arg not an integer
`"{calc(1, -1)}"`, // decimals negative `"{calc(1, -1)}"`, // decimals negative
} { } {
if _, err := linked(t, bad); err == nil { if _, err := resolved(t, bad); err == nil {
t.Errorf("compile(%s) = nil error, want error", bad) t.Errorf("compile(%s) = nil error, want error", bad)
} }
} }
+4 -5
View File
@@ -67,7 +67,7 @@ func mergeChildren(dst, src map[string]node) {
} }
// nodeSet is the set of nodes one validation pass covers: the categories one load // nodeSet is the set of nodes one validation pass covers: the categories one load
// binds, or a single inline node. // reads, or a single inline node.
type nodeSet func(fn func(label string, n node) error) error type nodeSet func(fn func(label string, n node) error) error
// categorySite is a loaded category and where it sits: the folder holding it, that // categorySite is a loaded category and where it sits: the folder holding it, that
@@ -157,10 +157,9 @@ func (p pipeline) run() error {
return checkNodeFences(p.nodes) return checkNodeFences(p.nodes)
} }
// checkNodeFences runs the per-node fences every pipeline runs over its nodes, each // checkNodeFences runs the per-node fences over s, each over every node before the next,
// over all of them before the next, so which of several broken nodes is reported // so which of several broken nodes is reported does not depend on the walk. Its walks
// does not depend on the walk. Its walks recurse unguarded, so run refuses their cycles // recurse unguarded, so pipeline.run refuses cycles first.
// first.
func checkNodeFences(s nodeSet) error { func checkNodeFences(s nodeSet) error {
mem := renderCounts{} mem := renderCounts{}
if err := s(func(label string, n node) error { return repeatCheck(label, n, mem) }); err != nil { if err := s(func(label string, n node) error { return repeatCheck(label, n, mem) }); err != nil {
+2 -2
View File
@@ -18,7 +18,7 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) {
`{"format":"","age":[null,{"format":"{int(18,99)}","datatype":"integer","weight":9}]}`, `{"format":"","age":[null,{"format":"{int(18,99)}","datatype":"integer","weight":9}]}`,
`{"format":"","pick":[[null,"a"],"b"]}`, `{"format":"","pick":[[null,"a"],"b"]}`,
} { } {
if _, err := linked(t, src); err != nil { if _, err := resolved(t, src); err != nil {
t.Errorf("compile(%s) = %v, want a column to take a datatype and null", src, err) t.Errorf("compile(%s) = %v, want a column to take a datatype and null", src, err)
} }
} }
@@ -32,7 +32,7 @@ func TestDatatypeAndNullSitOnlyInAColumn(t *testing.T) {
`null`: `so write ""`, `null`: `so write ""`,
`{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`, `{"format":"{p}","p":{"format":"{x}","x":[null,"a"]}}`: `so write ""`,
} { } {
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error containing %q", src, err, want) t.Errorf("compile(%s) = %v, want an error containing %q", src, err, want)
} }
} }
+9 -6
View File
@@ -24,18 +24,21 @@ package fejkdata
// start one each, and `FakeStruct` one per record. // start one each, and `FakeStruct` one per record.
// - env — what a render reads names and rows through: `renderEnv`. // - env — what a render reads names and rows through: `renderEnv`.
// - named pick — the draw a token such as {/person as p} binds to a name, drawn // - named pick — the draw a token such as {/person as p} binds to a name, drawn
// on its first read and kept while its scope renders: a category, or one // on its first read and kept while its scope renders: `nameBinding`,
// repeat iteration: `nameBinding`, `nameScope`, `namedPick`, `pickFrame`. // `namedPick`, `pickFrame`.
// A scope is only ever a name's. // - scope — where a name is bound: a category, or one repeat iteration: `nameScope`.
// - memo — what a named pick keeps its draws in, the variant drawn at each level // - memo — what a named pick keeps its draws in, the variant drawn at each level
// and the value each path read: `drawMemo`. // and the value each path read: `drawMemo`.
// - pin — fixing which row of a table one path, or one named pick, uses: // - pin — fixing which row of a table one path, or one named pick, uses:
// `pinSet`, `namedPick.pins`, `table.drawStep`. // `pinSet`, `namedPick.pins`, `table.drawStep`.
// - family — a table and every table reaching it through a chain of parent // - family — a table and every table reaching it through a chain of parent
// links (`table.linkParent`). Only a table links. // links.
// - resolve — tying each template's references and names to the merged tree: // - link — a table's tie to its parent table, each row to its parent row:
// `table.linkParent`.
// - resolve — tying each template's references and names to the assembled tree:
// `resolveTemplates`. // `resolveTemplates`.
// - head — a read's first segment, a string: `arm.head`. The node it names is // - head — the string a read starts from: a field's name, or a reference's
// category path with its sigil, /sv_SE.address: `arm.head`. The node it names is
// the read's start: `template.startOf`, `nameTarget.start`. // the read's start: `template.startOf`, `nameTarget.start`.
// - fence — a load-time check, run over each category, template or struct as it // - fence — a load-time check, run over each category, template or struct as it
// loads or compiles, so rendering a compiled tree cannot fail. // loads or compiles, so rendering a compiled tree cannot fail.
+1 -1
View File
@@ -795,7 +795,7 @@ the direct chain is the one a reader can predict from the tables' parents.
2026-09-30, larv-review on the comprehension round the maintainer approved on 2026-09-29; 2026-09-30, larv-review on the comprehension round the maintainer approved on 2026-09-29;
replaces "The path walks are separate loops". replaces "The path walks are separate loops".
Goals 3 and 13: `drawSteps` draws every path from its steps, compiled at resolve by Goals 3 and 13: `drawSteps` draws every path from its steps, compiled when templates resolve, by
`pathCheck` or, for a caller's path, by `probePath` into a stack buffer. Both take each `pathCheck` or, for a caller's path, by `probePath` into a stack buffer. Both take each
step through `takeStep`, and differ only at a choice: the check walks every variant, the step through `takeStep`, and differ only at a choice: the check walks every variant, the
probe the first. On 2026-09-29 one loop over all three walks, switching on a mode field, probe the first. On 2026-09-29 one loop over all three walks, switching on a mode field,
+1 -1
View File
@@ -43,7 +43,7 @@ func (env renderEnv) at(n node, pins *pinSet) renderEnv {
// rowOf is the row of t its columns render from. // rowOf is the row of t its columns render from.
func (env renderEnv) rowOf(t *table) int { func (env renderEnv) rowOf(t *table) int {
if env.row.t != t { if env.row.t != t {
panic(invariant.Broken("a column of %s renders in a scope holding no row of it", t.rows.Segment())) panic(invariant.Broken("a column of %s renders in an env holding no row of it", t.rows.Segment()))
} }
return env.row.index return env.row.index
} }
+4 -4
View File
@@ -111,8 +111,8 @@ func parse(t *testing.T, s string) any {
return v return v
} }
// linked compiles a JSON fragment and links it against no data. // resolved compiles a JSON fragment and resolves it against no data.
func linked(t *testing.T, s string) (node, error) { func resolved(t *testing.T, s string) (node, error) {
t.Helper() t.Helper()
n, err := compile(parse(t, s)) n, err := compile(parse(t, s))
if err != nil { if err != nil {
@@ -121,10 +121,10 @@ func linked(t *testing.T, s string) (node, error) {
return n, resolveInlineTemplates(inlineNodes(n, "template"), nil) return n, resolveInlineTemplates(inlineNodes(n, "template"), nil)
} }
// compiled parses, compiles and links a JSON fragment into a node. // compiled parses, compiles and resolves a JSON fragment into a node.
func compiled(t *testing.T, s string) node { func compiled(t *testing.T, s string) node {
t.Helper() t.Helper()
n, err := linked(t, s) n, err := resolved(t, s)
if err != nil { if err != nil {
t.Fatalf("compile %q: %v", s, err) t.Fatalf("compile %q: %v", s, err)
} }
+9 -10
View File
@@ -120,8 +120,7 @@ func checkBind(t grammar.Token, fields map[string]node) error {
return nil return nil
} }
// checkBound proves what a binding names is a reference or a path into a field of the binding // checkBound proves what a binding binds is a reference or a path into a field of its binder.
// template.
func checkBound(ref string, fields map[string]node) error { func checkBound(ref string, fields map[string]node) error {
if grammar.IsRef(ref) { if grammar.IsRef(ref) {
_, _, err := grammar.RefShape(ref) _, _, err := grammar.RefShape(ref)
@@ -190,7 +189,7 @@ func checkNoRepeatedArm(body string, names []string) error {
seen := make(map[string]bool, len(names)) seen := make(map[string]bool, len(names))
for _, name := range names { for _, name := range names {
if seen[name] { if seen[name] {
return fmt.Errorf("token {%s}: arm %q is repeated; an alternation picks its arms evenly, so skew the odds with a choice's weights instead", body, name) return fmt.Errorf("token {%s}: arm %q is repeated; an alternation draws its arms evenly, so skew the odds with a choice's weights instead", body, name)
} }
seen[name] = true seen[name] = true
} }
@@ -223,17 +222,17 @@ const (
// resolveRefs resolved it to; before that, a reference is whole. // resolveRefs resolved it to; before that, a reference is whole.
func splitArm(name string, refs map[string]resolvedRef) arm { func splitArm(name string, refs map[string]resolvedRef) arm {
if grammar.IsRef(name) { if grammar.IsRef(name) {
b, linked := refs[name] r, resolved := refs[name]
if !linked || len(b.tail) == 0 { if !resolved || len(r.tail) == 0 {
head := name head := name
if linked { if resolved {
head = b.head head = r.head
} }
return arm{spelling: name, head: head, levels: []pickKey{pickKey(head)}} return arm{spelling: name, head: head, levels: []pickKey{pickKey(head)}}
} }
sigil, rest, _ := grammar.RefShape(name) // resolveRefs proved it, and took b.tail as a suffix of its segments sigil, rest, _ := grammar.RefShape(name) // resolveRefs proved it, and took r.tail as a suffix of its segments
written, _ := grammar.SplitPath(rest) written, _ := grammar.SplitPath(rest)
return pathArm(name, b.head, sigil+grammar.JoinSegments(written[:len(written)-len(b.tail)]), b.tail) return pathArm(name, r.head, sigil+grammar.JoinSegments(written[:len(written)-len(r.tail)]), r.tail)
} }
segs, err := grammar.SplitPath(name) segs, err := grammar.SplitPath(name)
if err != nil || len(segs) == 1 { if err != nil || len(segs) == 1 {
@@ -330,7 +329,7 @@ func (t *template) compileArm(name string, targets map[*nameBinding]nameTarget)
if !t.isName(a.head) { if !t.isName(a.head) {
start := t.startOf(a.head) start := t.startOf(a.head)
if start == nil { if start == nil {
panic(invariant.Broken("{%s} reads a head nothing bound", a.spelling)) panic(invariant.Broken("{%s} reads a head nothing resolved", a.spelling))
} }
w := compilePath(start, a.tail) w := compilePath(start, a.tail)
a.steps, a.leaves = w.steps, w.leaves a.steps, a.leaves = w.steps, w.leaves
+3 -3
View File
@@ -20,7 +20,7 @@ func TestStringIsAFormat(t *testing.T) {
if got := mustRender(t, f, `"{digits(3)}"`); !regexp.MustCompile(`^[0-9]{3}$`).MatchString(got) { if got := mustRender(t, f, `"{digits(3)}"`); !regexp.MustCompile(`^[0-9]{3}$`).MatchString(got) {
t.Errorf(`render("{digits(3)}") = %q, want three digits`, got) t.Errorf(`render("{digits(3)}") = %q, want three digits`, got)
} }
if _, err := linked(t, `"{x}"`); err == nil || !strings.Contains(err.Error(), `no field "x"`) { if _, err := resolved(t, `"{x}"`); err == nil || !strings.Contains(err.Error(), `no field "x"`) {
t.Errorf(`compile("{x}") = %v, want a no-field error`, err) t.Errorf(`compile("{x}") = %v, want a no-field error`, err)
} }
} }
@@ -50,7 +50,7 @@ func TestBraceEscapes(t *testing.T) {
`{"format":"{a{b}","a":"Q"}`: "'{'", `{"format":"{a{b}","a":"Q"}`: "'{'",
`"{x"`: "unterminated", `"{x"`: "unterminated",
} { } {
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want) t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want)
} }
} }
@@ -125,7 +125,7 @@ func TestFormatCompileErrors(t *testing.T) {
`"{int(1)}"`, // wrong arity `"{int(1)}"`, // wrong arity
`"{float(1,2)}"`, // wrong arity `"{float(1,2)}"`, // wrong arity
} { } {
if _, err := linked(t, bad); err == nil { if _, err := resolved(t, bad); err == nil {
t.Errorf("compile(%s) = nil error, want error", bad) t.Errorf("compile(%s) = nil error, want error", bad)
} }
} }
+4 -4
View File
@@ -183,18 +183,18 @@ func checkNoCycles(s nodeSet) error {
grey = 1 grey = 1
black = 2 black = 2
) )
inScope := map[node]bool{} inSet := map[node]bool{}
_ = s(func(_ string, n node) error { _ = s(func(_ string, n node) error {
inScope[n] = true inSet[n] = true
if t, isTable := n.(*table); isTable { if t, isTable := n.(*table); isTable {
inScope[t.rowNode] = true // a selector lands on it, and contained lists it nowhere inSet[t.rowNode] = true // a selector lands on it, and contained lists it nowhere
} }
return nil return nil
}) })
color := map[node]int{} color := map[node]int{}
var visit func(n node, label string) error var visit func(n node, label string) error
visit = func(n node, label string) error { visit = func(n node, label string) error {
if !inScope[n] { if !inSet[n] {
return nil // loaded before these nodes and proven then, so it never reaches back into it return nil // loaded before these nodes and proven then, so it never reaches back into it
} }
switch color[n] { switch color[n] {
+11 -11
View File
@@ -92,7 +92,7 @@ func bindNames(root node) error {
} }
} }
} }
read, err := resolveReads(root, scopes) read, err := answerReads(root, scopes)
if err != nil { if err != nil {
return err return err
} }
@@ -165,16 +165,16 @@ func eachContained(n node, where string, fn func(c node, where string) error) er
return nil return nil
} }
// resolveReads answers each unbound read under root with a name its template sees, and returns // answerReads answers each unbound read under root with a name its template sees, and returns
// the set of bindings some read answers to. It resolves a template's fields before its format, the // the set of bindings some read answers to. It answers a template's fields before its format, the
// order compile reports in, and refuses a field spelling a name. A refusal searches scopes, every // order compile reports in, and refuses a field spelling a name. A refusal searches scopes, every
// scope of the category, for a name bound where the read cannot see it. // scope of the category, for a name bound where the read cannot see it.
func resolveReads(root node, scopes []*nameScope) (map[*nameBinding]bool, error) { func answerReads(root node, scopes []*nameScope) (map[*nameBinding]bool, error) {
read := map[*nameBinding]bool{} read := map[*nameBinding]bool{}
var resolve func(n node, where string) error var answer func(n node, where string) error
resolve = func(n node, where string) error { answer = func(n node, where string) error {
t, isTemplate := n.(*template) t, isTemplate := n.(*template)
if err := eachContained(n, where, resolve); err != nil { if err := eachContained(n, where, answer); err != nil {
return err return err
} }
if !isTemplate { if !isTemplate {
@@ -192,18 +192,18 @@ func resolveReads(root node, scopes []*nameScope) (map[*nameBinding]bool, error)
for _, u := range t.unbound { for _, u := range t.unbound {
b := t.nameScope.lookup(u.head) b := t.nameScope.lookup(u.head)
if b == nil { if b == nil {
return unresolved(where, u, t.nameScope, scopes) return unanswered(where, u, t.nameScope, scopes)
} }
read[b] = true read[b] = true
} }
return nil return nil
} }
return read, resolve(root, "") return read, answer(root, "")
} }
// unresolved is the refusal of u, a read no field or name answers: naming the repeat binding the // unanswered is the refusal of u, a read no field or name answers: naming the repeat binding the
// name where the read cannot see it, else the names the read sees. // name where the read cannot see it, else the names the read sees.
func unresolved(where string, u unboundRead, seen *nameScope, scopes []*nameScope) error { func unanswered(where string, u unboundRead, seen *nameScope, scopes []*nameScope) error {
for _, sc := range scopes { for _, sc := range scopes {
if b, ok := sc.bindings[u.head]; ok { if b, ok := sc.bindings[u.head]; ok {
at := strings.TrimSuffix(b.where, ": ") at := strings.TrimSuffix(b.where, ": ")
+1 -1
View File
@@ -61,7 +61,7 @@ func TestANameKeepsWhatItsPathsAddress(t *testing.T) {
differ = differ || got[0][0] != got[0][2] differ = differ || got[0][0] != got[0][2]
} }
if !differ { if !differ {
t.Fatal("{w}-{w} under a name drew w once in 100 renders, want each {w} a pick of its own") t.Fatal("{w}-{w} under a name drew w once in 100 renders, want each {w} a draw of its own")
} }
} }
+2 -2
View File
@@ -47,7 +47,7 @@ func (*nullItem) isNode() {}
// template renders a format string, substituting {tokens} from fields. A bare // 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 // 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 // that many times and joins the results with separator (default ""), each render
// an independent pick. Every format compiles in `resolveTemplates`. // an independent draw. Every format compiles in `resolveTemplates`.
type template struct { type template struct {
// Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`: // Filled by `compileString`, `compileTemplate` and `table.compileRowFormat`:
format string format string
@@ -80,7 +80,7 @@ type templateRefs struct {
func (*template) isNode() {} func (*template) isNode() {}
// startOf is the node an arm's startOf names: a sibling field, or a reference's category. // startOf is the node an arm's head names: a sibling field, or a reference's category.
func (t *template) startOf(name string) node { func (t *template) startOf(name string) node {
if grammar.IsRef(name) { if grammar.IsRef(name) {
return t.refs.categories[name] return t.refs.categories[name]
+2 -2
View File
@@ -7,7 +7,7 @@ import (
) )
func TestWeightZeroIsRejected(t *testing.T) { func TestWeightZeroIsRejected(t *testing.T) {
_, err := linked(t, `[{"format":"X","weight":0},"Y"]`) _, err := resolved(t, `[{"format":"X","weight":0},"Y"]`)
if err == nil || !strings.Contains(err.Error(), "weight 0") || !strings.Contains(err.Error(), "remove") { if err == nil || !strings.Contains(err.Error(), "weight 0") || !strings.Contains(err.Error(), "remove") {
t.Fatalf("compile(weight 0) = %v, want it rejected naming the fix", err) t.Fatalf("compile(weight 0) = %v, want it rejected naming the fix", err)
} }
@@ -75,7 +75,7 @@ func TestNodeCompileErrors(t *testing.T) {
`{"format":"x","repeat":2,"separator":5}`, // non-string separator `{"format":"x","repeat":2,"separator":5}`, // non-string separator
`{"format":"{x}","x":"v","repeat":2,"separator":""}`, // separator "" is the default `{"format":"{x}","x":"v","repeat":2,"separator":""}`, // separator "" is the default
} { } {
if _, err := linked(t, bad); err == nil { if _, err := resolved(t, bad); err == nil {
t.Errorf("compile(%s) = nil error, want error", bad) t.Errorf("compile(%s) = nil error, want error", bad)
} }
} }
+4 -4
View File
@@ -7,7 +7,7 @@ import (
func TestOneItemChoiceIsRejected(t *testing.T) { func TestOneItemChoiceIsRejected(t *testing.T) {
for _, src := range []string{`["x"]`, `[{"format":"{d}","d":"x"}]`, `{"format":"{w}","w":["only"]}`} { for _, src := range []string{`["x"]`, `[{"format":"{d}","d":"x"}]`, `{"format":"{w}","w":["only"]}`} {
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), "one-item choice") { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), "one-item choice") {
t.Errorf("compile(%s) = %v, want the one-item choice rejected", src, err) t.Errorf("compile(%s) = %v, want the one-item choice rejected", src, err)
} }
} }
@@ -20,11 +20,11 @@ func TestRepeatedChoiceItemIsRejected(t *testing.T) {
`{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`, `{"format":"{w}","w":["", "", "x"]}`: `{ "format": "", "weight": 2 }`,
`{"format":"","w":[null,null,"a"]}`: "a null takes no weight", `{"format":"","w":[null,null,"a"]}`: "a null takes no weight",
} { } {
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want) t.Errorf("compile(%s) = %v, want an error naming %s", src, err, want)
} }
} }
if _, err := linked(t, `[{"format":"a","weight":2}, "b"]`); err != nil { if _, err := resolved(t, `[{"format":"a","weight":2}, "b"]`); err != nil {
t.Errorf("compile(weighted a, b) = %v", err) t.Errorf("compile(weighted a, b) = %v", err)
} }
} }
@@ -39,7 +39,7 @@ func TestInertObjectIsRejected(t *testing.T) {
`{"format":"{x}","x":"v","repeat":2,"separator":""}`: "default", `{"format":"{x}","x":"v","repeat":2,"separator":""}`: "default",
`{"format":"","n":{"format":"1","datatype":"string"}}`: `datatype "string" is the default`, `{"format":"","n":{"format":"1","datatype":"string"}}`: `datatype "string" is the default`,
} { } {
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want) t.Errorf("compile(%s) = %v, want an error mentioning %s", src, err, want)
} }
} }
+1 -1
View File
@@ -142,7 +142,7 @@ func TestPathKeyIsUnambiguous(t *testing.T) {
} }
// TestMissingFieldNamesItself keeps the precise diagnosis for the ordinary typo: a // TestMissingFieldNamesItself keeps the precise diagnosis for the ordinary typo: a
// single-variant choice always picks the same item, so it needs no every-variant // single-variant choice always draws the same item, so it needs no every-variant
// guard and the error can name the field that is missing. // guard and the error can name the field that is missing.
func TestMissingFieldNamesItself(t *testing.T) { func TestMissingFieldNamesItself(t *testing.T) {
f := newGenerator(t, "data", WithSeed(1)) f := newGenerator(t, "data", WithSeed(1))
+2 -2
View File
@@ -55,7 +55,7 @@ func TestNoRenderAllocRegression(t *testing.T) {
} }
} }
// The repeat shape prices what keeps a render's scope off the heap: a copy dropped costs an alloc an iteration. // The repeat shape prices what keeps a render's env off the heap: a copy dropped costs an alloc an iteration.
func TestNoReferenceAllocRegression(t *testing.T) { func TestNoReferenceAllocRegression(t *testing.T) {
word := `{"format":"{w}","w":["alpha","beta","gamma","delta"]}` word := `{"format":"{w}","w":["alpha","beta","gamma","delta"]}`
for _, s := range []struct { for _, s := range []struct {
@@ -74,7 +74,7 @@ func TestNoReferenceAllocRegression(t *testing.T) {
t.Fatalf("New(%s): %v", s.name, err) t.Fatalf("New(%s): %v", s.name, err)
} }
if allocs := testing.AllocsPerRun(10000, func() { f.Fake("x") }); allocs > s.base*s.slack { if allocs := testing.AllocsPerRun(10000, func() { f.Fake("x") }); allocs > s.base*s.slack {
t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f × %.2f); a render's scope reaching the heap is the usual cause", s.name, allocs, s.base*s.slack, s.base, s.slack) t.Errorf("%s: %.1f allocs/op regressed past %.1f (baseline %.1f × %.2f); a render's env reaching the heap is the usual cause", s.name, allocs, s.base*s.slack, s.base, s.slack)
} }
} }
} }
+3 -3
View File
@@ -26,7 +26,7 @@ func varies(t *testing.T, f *Generator, src string, differ func(got string) bool
return return
} }
} }
t.Fatalf("400 renders of %s never drew apart, want each {…} a pick of its own", src) t.Fatalf("400 renders of %s never drew apart, want each {…} a draw of its own", src)
} }
func TestEveryTokenDrawsAfresh(t *testing.T) { func TestEveryTokenDrawsAfresh(t *testing.T) {
@@ -57,7 +57,7 @@ func TestEveryReferenceDrawsAfresh(t *testing.T) {
differ = p[0] != p[1] differ = p[0] != p[1]
} }
if !differ { if !differ {
t.Errorf("400 renders of %s never drew apart, want each reference a pick of its own", src) t.Errorf("400 renders of %s never drew apart, want each reference a draw of its own", src)
} }
} }
} }
@@ -124,7 +124,7 @@ func TestAFieldBindingIsRefusedWhereNoFieldIs(t *testing.T) {
`{"format":"{place as p}{calc(p * 2)}","place":["1","2"]}`: `write place where it is read`, `{"format":"{place as p}{calc(p * 2)}","place":["1","2"]}`: `write place where it is read`,
`{"format":"{place as place}{place}{place}","place":["x","y"]}`: `name "place" is a field of the root template too`, `{"format":"{place as place}{place}{place}","place":["x","y"]}`: `name "place" is a field of the root template too`,
} { } {
if _, err := linked(t, src); err == nil || !strings.Contains(err.Error(), want) { if _, err := resolved(t, src); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s = %v, want it refused naming %s", src, err, want) t.Errorf("%s = %v, want it refused naming %s", src, err, want)
} }
} }
+1 -1
View File
@@ -59,7 +59,7 @@ func TestReferenceCombinesLoadedPaths(t *testing.T) {
} }
// TestReferenceChain follows a reference to a node that is itself a reference, so // TestReferenceChain follows a reference to a node that is itself a reference, so
// linking order cannot matter. // resolve order cannot matter.
func TestReferenceChain(t *testing.T) { func TestReferenceChain(t *testing.T) {
dir := writeData(t, map[string]string{ dir := writeData(t, map[string]string{
"a": `"{/b}"`, "a": `"{/b}"`,
+1 -1
View File
@@ -89,7 +89,7 @@ func render(s *drawstate.State, n node, env renderEnv) string {
} }
// renderRepeat renders each iteration of t inside the name scopes rendering t, where a name bound // renderRepeat renders each iteration of t inside the name scopes rendering t, where a name bound
// outside t keeps its pick, and a name t binds picks again. // outside t keeps its pick, and a name t binds takes a fresh pick.
func renderRepeat(s *drawstate.State, t *template, env renderEnv) string { func renderRepeat(s *drawstate.State, t *template, env renderEnv) string {
var b strings.Builder var b strings.Builder
b.Grow(t.repeat * (t.compiled.grow + len(t.separator))) b.Grow(t.repeat * (t.compiled.grow + len(t.separator)))
+1 -1
View File
@@ -30,7 +30,7 @@ func newShippedWhole(t testing.TB, opts ...Option) *Generator {
return f return f
} }
// loadedCategories is the dot path of every category f has parsed and linked. // loadedCategories is the dot path of every category f has parsed and resolved.
func loadedCategories(f *Generator) []string { func loadedCategories(f *Generator) []string {
var out []string var out []string
for _, s := range categorySites(&f.root) { for _, s := range categorySites(&f.root) {
+2 -2
View File
@@ -1105,8 +1105,8 @@ func TestAStepSitsAtTheSegmentItConsumes(t *testing.T) {
"region[12].name", "region[12].name",
} { } {
a := splitArm(path, nil) a := splitArm(path, nil)
head := f.root.children[a.head].(*table) start := f.root.children[a.head].(*table)
a.steps = compilePath(head, a.tail).steps a.steps = compilePath(start, a.tail).steps
for _, st := range a.steps { for _, st := range a.steps {
seg := "" seg := ""
switch st.kind { switch st.kind {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/larvit/fejkdata/internal/proven" "github.com/larvit/fejkdata/internal/proven"
) )
// valueProof proves what typed columns and their calc operands hold, each node once per nodeSet. // valueProof proves what typed columns and their calc operands hold, each node once per proof.
type valueProof struct { type valueProof struct {
memo map[node]proven.Facts memo map[node]proven.Facts
columnMemo map[node]proven.Facts columnMemo map[node]proven.Facts