Pin the builtin registry shape and grow as a lower bound, and correct stale comments
This commit is contained in:
committed by
lilleman-tw
parent
ab9846c2ff
commit
37759f773f
@@ -33,7 +33,6 @@ func BenchmarkCreditcard(b *testing.B) { benchPath(b, "data/misc", "creditcard")
|
||||
func BenchmarkSSN(b *testing.B) { benchPath(b, "data/sv_SE", "ssn") }
|
||||
func BenchmarkUUIDv7(b *testing.B) { benchPath(b, "data/misc", "uuid") }
|
||||
|
||||
// tmpData writes one category JSON into a fresh dir and returns the dir.
|
||||
func tmpData(b *testing.B, name, body string) string {
|
||||
b.Helper()
|
||||
dir := b.TempDir()
|
||||
@@ -43,19 +42,16 @@ func tmpData(b *testing.B, name, body string) string {
|
||||
return dir
|
||||
}
|
||||
|
||||
// BenchmarkCalc exercises the per-render expression re-parse.
|
||||
func BenchmarkCalc(b *testing.B) {
|
||||
dir := tmpData(b, "inv", `{"format":"{net} x {qty} = {calc(net * qty, 2)}","net":["19.99"],"qty":["3"]}`)
|
||||
benchPath(b, dir, "inv")
|
||||
}
|
||||
|
||||
// BenchmarkLongLiteral exercises the per-rune literal write path.
|
||||
func BenchmarkLongLiteral(b *testing.B) {
|
||||
dir := tmpData(b, "sql", `{"format":"INSERT INTO customers (id, name, city) V#ALUES (#1#2#3, '{word}', '{word}');","word":["alpha","beta","gamma","delta"]}`)
|
||||
benchPath(b, dir, "sql")
|
||||
}
|
||||
|
||||
// BenchmarkRepeat exercises repeat + separator.
|
||||
func BenchmarkRepeat(b *testing.B) {
|
||||
dir := tmpData(b, "many", `{"format":"{word}","repeat":20,"separator":", ","word":["alpha","beta","gamma","delta"]}`)
|
||||
benchPath(b, dir, "many")
|
||||
|
||||
@@ -181,3 +181,20 @@ func ibanValid(s string) bool {
|
||||
}
|
||||
return rem == 1
|
||||
}
|
||||
|
||||
// TestRegistryShapes pins the builtin contract bind relies on: exactly one of prep
|
||||
// or call, and args parsed at compile only behind a check. A registry entry with
|
||||
// neither would compile and then panic inside a render.
|
||||
func TestRegistryShapes(t *testing.T) {
|
||||
for name, b := range builtins {
|
||||
switch {
|
||||
case b.prep == nil && b.call == nil:
|
||||
t.Errorf("builtin %q has neither prep nor call: bind would return a nil-func closure", name)
|
||||
case b.prep != nil && b.call != nil:
|
||||
t.Errorf("builtin %q has both prep and call; call is dead", name)
|
||||
}
|
||||
if b.prep != nil && b.arity != 0 && b.check == nil {
|
||||
t.Errorf("builtin %q parses args in prep with no check", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
// calc is the {calc(expr[, dp])} token: an arithmetic expression over number
|
||||
// literals and sibling-field names (rendered, then parsed as numbers), with
|
||||
// + - * /, unary minus and parentheses. It is a registry builtin like any other
|
||||
// {name(args)} function — the one whose check and call read the sibling fields (to
|
||||
// {name(args)} function — the one whose check and prep read the sibling fields (to
|
||||
// render its operands). The value prints in minimal decimal form, or rounded to dp
|
||||
// when given. A field that doesn't render to a number becomes NaN, which
|
||||
// propagates and prints as "NaN" — visible, never a render error. The expression
|
||||
// is re-parsed each render, mirroring how expand re-scans the format; checkCalc
|
||||
// proves it parses (and names real fields) at New, so render never fails.
|
||||
// propagates and prints as "NaN" — visible, never a render error. The expression is
|
||||
// parsed once at New into an AST every render shares and none mutates, so render
|
||||
// cannot fail and must not carry per-render state.
|
||||
|
||||
// calcNode is a parsed expression node.
|
||||
type calcNode interface {
|
||||
|
||||
@@ -138,7 +138,7 @@ func compileTemplate(m map[string]any) (node, error) {
|
||||
if err := checkTokens(format, t.fields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.ops, t.grow = compileOps(format, t.fields)
|
||||
t.ops, t.grow = compileOps(format)
|
||||
return t, nil
|
||||
}
|
||||
|
||||
|
||||
+12
-13
@@ -20,7 +20,7 @@ type ftoken struct {
|
||||
}
|
||||
|
||||
// eachToken scans a format string once and calls fn for each unit, the single
|
||||
// source of truth for how '#' escapes and {…} braces are read — expand,
|
||||
// source of truth for how '#' escapes and {…} braces are read — compileOps,
|
||||
// checkTokens, fieldTokens and refTokens all drive off it so the grammar can't
|
||||
// drift between the validator and the renderer. '#' escapes the next char to a
|
||||
// literal ("#0" -> '0', "##" -> '#'); a '{' must reach a '}' (else an error); an
|
||||
@@ -66,7 +66,9 @@ func eachToken(format string, fn func(ftoken) error) error {
|
||||
// seq is the one exception: it advances per-session counter state, which is itself
|
||||
// deterministic (1, 2, 3 …). arity is the exact arg count, or -1 for variadic
|
||||
// (then check does all the validation). The optional check validates args at
|
||||
// compile time (their values, beyond the count). The registry lives in builtins.go.
|
||||
// compile time (their values, beyond the count). A builtin supplies exactly one of
|
||||
// prep (args parsed once, at compile) or call (args parsed per render). The registry
|
||||
// lives in builtins.go.
|
||||
type builtin struct {
|
||||
arity int
|
||||
// prep parses validated args once, at compile time, into the closure expand calls.
|
||||
@@ -162,12 +164,12 @@ func fieldTokens(format string) []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// op is one compiled unit of a format string: a literal run, a class char, a field
|
||||
// alternation, or a builtin already bound to its args. compile builds these so
|
||||
// render never re-scans the format.
|
||||
// callFn is a builtin bound to one call site: its args already parsed.
|
||||
type callFn func(s *session, emitted string, fields map[string]node) string
|
||||
|
||||
// op is one compiled unit of a format string: a literal run, a class char, a field
|
||||
// alternation, or a builtin already bound to its args. compile builds these so
|
||||
// render never re-scans the format.
|
||||
type op struct {
|
||||
kind byte // 'l' literal run, 'c' class char, 'f' field alternation, 'b' builtin
|
||||
lit string // kind 'l'
|
||||
@@ -176,14 +178,16 @@ type op struct {
|
||||
call callFn
|
||||
}
|
||||
|
||||
// compileOps turns a validated format string into ops, and returns the smallest
|
||||
// output it can produce (literals plus one byte per class char) to size the buffer.
|
||||
func compileOps(format string, fields map[string]node) ([]op, int) {
|
||||
// compileOps turns a format string into ops, and returns the smallest output it can
|
||||
// produce (literals plus one byte per class char) to size the render buffer. Call
|
||||
// checkTokens first: it is what proves the scan and every token are valid.
|
||||
func compileOps(format string) ([]op, int) {
|
||||
var ops []op
|
||||
var lit strings.Builder
|
||||
grow := 0
|
||||
flush := func() {
|
||||
if lit.Len() > 0 {
|
||||
grow += lit.Len()
|
||||
ops = append(ops, op{kind: 'l', lit: lit.String()})
|
||||
lit.Reset()
|
||||
}
|
||||
@@ -207,11 +211,6 @@ func compileOps(format string, fields map[string]node) ([]op, int) {
|
||||
return nil
|
||||
})
|
||||
flush()
|
||||
for _, o := range ops {
|
||||
if o.kind == 'l' {
|
||||
grow += len(o.lit)
|
||||
}
|
||||
}
|
||||
return ops, grow
|
||||
}
|
||||
|
||||
|
||||
@@ -226,3 +226,40 @@ func TestFakePathNavigation(t *testing.T) {
|
||||
t.Error("Fake(missing) = nil error, want unknown-category error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGrowIsALowerBound pins the property that makes the pre-sized render buffer
|
||||
// safe: grow must never exceed what expand emits. render multiplies it by repeat,
|
||||
// so an over-estimate would amplify up to a million-fold.
|
||||
func TestGrowIsALowerBound(t *testing.T) {
|
||||
f := engine(3)
|
||||
for _, format := range []string{
|
||||
"",
|
||||
"plain literal",
|
||||
"00-11-AA-aa",
|
||||
"#0#1#A#a## literal",
|
||||
"Ö dag åäö 日本語",
|
||||
"{x}{x}{x}",
|
||||
"{hex(8)}-{int(10,99)}-{nanoid(5)}",
|
||||
"9{d}{luhn()}",
|
||||
"{a|b} and {a|b}",
|
||||
} {
|
||||
src := `{"format":` + quote(format) + `,"x":["1"],"a":["A"],"b":["B"],"d":["012345678901234"]}`
|
||||
tmpl, ok := compiled(t, src).(*template)
|
||||
if !ok {
|
||||
t.Fatalf("format %q did not compile to a template", format)
|
||||
}
|
||||
for i := 0; i < 50; i++ {
|
||||
if got := len(expand(f.rand, tmpl)); got < tmpl.grow {
|
||||
t.Errorf("format %q: expand emitted %d bytes, below grow %d", format, got, tmpl.grow)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func quote(s string) string {
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user