Write every builtin one way
A builtin could be spelled as prep (args parsed once, at compile) or call (args parsed per render), reaching the same closure by two routes. Nothing enforced the "exactly one of" invariant: supplying both silently dropped call, supplying neither compiled and then panicked inside a render, and a test existed only to police it. prep does everything call did, so it is now the one spelling. The zero-arg shapes the README already names — a derivation reads the output emitted so far, a generator reads only the rng — lift through derive and generate, so the registry reads as that taxonomy. iban stops looking its country up per render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+24
-8
@@ -24,13 +24,11 @@ const (
|
|||||||
// clock. Add a builtin only for what data can't express: a random v4 UUID and a
|
// clock. Add a builtin only for what data can't express: a random v4 UUID and a
|
||||||
// 24-hex ObjectID both ship as data, so the uuid builtin is v7.
|
// 24-hex ObjectID both ship as data, so the uuid builtin is v7.
|
||||||
var builtins = map[string]builtin{
|
var builtins = map[string]builtin{
|
||||||
"luhn": {arity: 0, call: func(_ *session, e string, _ map[string]node, _ []string) string {
|
"luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })},
|
||||||
return string(rune('0' + luhnCheck(e)))
|
"mod11": {arity: 0, prep: derive(mod11Check)},
|
||||||
}},
|
"ean": {arity: 0, prep: derive(eanCheck)},
|
||||||
"mod11": {arity: 0, call: func(_ *session, e string, _ map[string]node, _ []string) string { return mod11Check(e) }},
|
"uuid": {arity: 0, prep: generate(uuidV7)},
|
||||||
"ean": {arity: 0, call: func(_ *session, e string, _ map[string]node, _ []string) string { return eanCheck(e) }},
|
"ulid": {arity: 0, prep: generate(ulid)},
|
||||||
"uuid": {arity: 0, call: func(s *session, _ string, _ map[string]node, _ []string) string { return uuidV7(s) }},
|
|
||||||
"ulid": {arity: 0, call: func(s *session, _ string, _ map[string]node, _ []string) string { return ulid(s) }},
|
|
||||||
"nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
"nanoid": {arity: 1, check: posIntArg, prep: func(a []string) callFn {
|
||||||
n := atoi(a[0])
|
n := atoi(a[0])
|
||||||
return func(s *session, _ string, _ map[string]node) string { return nanoid(s, n) }
|
return func(s *session, _ string, _ map[string]node) string { return nanoid(s, n) }
|
||||||
@@ -57,7 +55,10 @@ var builtins = map[string]builtin{
|
|||||||
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64)
|
return strconv.FormatFloat(lo+s.Float64()*(hi-lo), 'f', dp, 64)
|
||||||
}
|
}
|
||||||
}},
|
}},
|
||||||
"iban": {arity: 1, check: ibanArg, call: func(s *session, _ string, _ map[string]node, a []string) string { return iban(s, a[0]) }},
|
"iban": {arity: 1, check: ibanArg, prep: func(a []string) callFn {
|
||||||
|
cc := a[0]
|
||||||
|
return func(s *session, _ string, _ map[string]node) string { return iban(s, cc) }
|
||||||
|
}},
|
||||||
// calc is the one builtin that reads the sibling fields (to render its operands);
|
// calc is the one builtin that reads the sibling fields (to render its operands);
|
||||||
// every other ignores them. 1 or 2 args: the expression and an optional decimals.
|
// every other ignores them. 1 or 2 args: the expression and an optional decimals.
|
||||||
"calc": {arity: -1, check: checkCalc, prep: calcPrep},
|
"calc": {arity: -1, check: checkCalc, prep: calcPrep},
|
||||||
@@ -75,6 +76,21 @@ var builtins = map[string]builtin{
|
|||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// derive and generate are the two argument-free builtin shapes the README names: a
|
||||||
|
// derivation reads the output emitted so far, a generator reads only the rng. Each
|
||||||
|
// lifts that one function into the prep every registry entry supplies.
|
||||||
|
func derive(f func(emitted string) string) func([]string) callFn {
|
||||||
|
return func([]string) callFn {
|
||||||
|
return func(_ *session, emitted string, _ map[string]node) string { return f(emitted) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(f func(rng) string) func([]string) callFn {
|
||||||
|
return func([]string) callFn {
|
||||||
|
return func(s *session, _ string, _ map[string]node) string { return f(s) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hexDigits = "0123456789abcdef"
|
const hexDigits = "0123456789abcdef"
|
||||||
|
|
||||||
// atoi parses an arg already validated by a builtin's check, so it cannot fail.
|
// atoi parses an arg already validated by a builtin's check, so it cannot fail.
|
||||||
|
|||||||
+5
-9
@@ -181,18 +181,14 @@ func ibanValid(s string) bool {
|
|||||||
return rem == 1
|
return rem == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRegistryShapes pins the builtin contract bind relies on: exactly one of prep
|
// TestRegistryShapes pins the builtin contract compileOps relies on: every entry
|
||||||
// or call, and args parsed at compile only behind a check. A registry entry with
|
// supplies prep, and args parsed at compile only behind a check.
|
||||||
// neither would compile and then panic inside a render.
|
|
||||||
func TestRegistryShapes(t *testing.T) {
|
func TestRegistryShapes(t *testing.T) {
|
||||||
for name, b := range builtins {
|
for name, b := range builtins {
|
||||||
switch {
|
if b.prep == nil {
|
||||||
case b.prep == nil && b.call == nil:
|
t.Errorf("builtin %q has no prep: compileOps would call a nil func", name)
|
||||||
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 {
|
if b.arity != 0 && b.check == nil {
|
||||||
t.Errorf("builtin %q parses args in prep with no check", name)
|
t.Errorf("builtin %q parses args in prep with no check", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-16
@@ -67,15 +67,12 @@ func eachToken(format string, fn func(ftoken) error) error {
|
|||||||
// seq is the one exception: it advances per-session counter state, which is itself
|
// 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
|
// 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
|
// (then check does all the validation). The optional check validates args at
|
||||||
// compile time (their values, beyond the count). A builtin supplies exactly one of
|
// compile time (their values, beyond the count). The registry lives in builtins.go.
|
||||||
// prep (args parsed once, at compile) or call (args parsed per render). The registry
|
|
||||||
// lives in builtins.go.
|
|
||||||
type builtin struct {
|
type builtin struct {
|
||||||
arity int
|
arity int
|
||||||
// prep parses validated args once, at compile time, into the closure expand calls.
|
// prep parses validated args once, at compile time, into the closure expand calls.
|
||||||
prep func(args []string) callFn
|
prep func(args []string) callFn
|
||||||
check func(fields map[string]node, args []string) error
|
check func(fields map[string]node, args []string) error
|
||||||
call func(s *session, emitted string, fields map[string]node, args []string) string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// funcCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
// funcCall splits a "{token}" body shaped name(args) into its parts; ok is false
|
||||||
@@ -327,7 +324,7 @@ func compileOps(format string) ([]op, int, map[string]string) {
|
|||||||
case 'b':
|
case 'b':
|
||||||
flush()
|
flush()
|
||||||
if name, args, ok := funcCall(t.body); ok {
|
if name, args, ok := funcCall(t.body); ok {
|
||||||
ops = append(ops, op{kind: 'b', call: builtins[name].bind(args)})
|
ops = append(ops, op{kind: 'b', call: builtins[name].prep(args)})
|
||||||
} else {
|
} else {
|
||||||
arms := splitArms(t.body)
|
arms := splitArms(t.body)
|
||||||
for _, a := range arms {
|
for _, a := range arms {
|
||||||
@@ -348,14 +345,3 @@ func compileOps(format string) ([]op, int, map[string]string) {
|
|||||||
flush()
|
flush()
|
||||||
return ops, grow, bound
|
return ops, grow, bound
|
||||||
}
|
}
|
||||||
|
|
||||||
// bind returns the closure for one call site, parsing args once via prep if present.
|
|
||||||
func (b builtin) bind(args []string) callFn {
|
|
||||||
if b.prep != nil {
|
|
||||||
return b.prep(args)
|
|
||||||
}
|
|
||||||
call := b.call
|
|
||||||
return func(s *session, emitted string, fields map[string]node) string {
|
|
||||||
return call(s, emitted, fields, args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user