diff --git a/README.md b/README.md index 437c4ad..242bb4f 100644 --- a/README.md +++ b/README.md @@ -968,17 +968,16 @@ format.go a format's tokens checked against their fields, and compiled pins.go the pin set: the table rows one path or one named pick fixes reference.go resolving references across the tree graph.go the node graph: tree walks, containment and render edges, and the cycle and repeat fences -builtins.go the {name()} function registry and its implementations -layout.go date and time layouts: the instants one is proved against, and the two samples -checksum.go the check characters a derivation appends, and the IBAN they sit inside -transform.go the builtins that rewrite an operand's value, and the ASCII folding -calc.go the {calc()} arithmetic: eval and validation +calc.go the {calc()} operand checks: never a number, and a constant zero divisor datatype.go column datatypes: DataType, where datatype and null may sit, a column's datatype valueproof.go the value proof: what a typed column, calc operand or struct field holds, checked at load data.go the load path: fs.FS folders/files -> merged namespace tree, and the bind pipeline every entry point runs cmd/fejkdata/ the fejkdata CLI +internal/builtinfunc/ the {name()} functions: their checks and draws, checksums, date and time layouts, transforms and calc arithmetic +internal/datatype/ what a record column holds, which DataType aliases internal/drawstate/ the seeded randomness and the {seq()} counters a generator draws through internal/grammar/ how the template language is written: format tokens, paths and selectors, reference sigils, names, calc syntax, and whether an argument is a template or a path +internal/proven/ what a proof knows of a value, and the bounds a calc takes from its operands data/ shipped data (JSON, and a TSV per table), embedded at build: locale folders, geo, misc data-import/ the scripts that rebuild each sourced table (see DATA-LICENSES.md) docs/ the decision log, the register research behind the shipped data, and the survey of other libraries diff --git a/calc.go b/calc.go index f921524..b6ab37c 100644 --- a/calc.go +++ b/calc.go @@ -2,87 +2,33 @@ package fejkdata import ( "fmt" - "math" "slices" "strconv" "strings" - "github.com/larvit/fejkdata/internal/drawstate" + "github.com/larvit/fejkdata/internal/builtinfunc" "github.com/larvit/fejkdata/internal/grammar" - "github.com/larvit/fejkdata/internal/proven" ) -// evalCalc evaluates an expression over the operand values expand already read, so it -// draws nothing and reads no template node. -func evalCalc(n grammar.CalcNode, operands []string) float64 { - switch n := n.(type) { - case grammar.CalcNum: - return float64(n) - case grammar.CalcVar: - v, err := strconv.ParseFloat(strings.TrimSpace(operands[n.At]), 64) - if err != nil { - return math.NaN() // a non-numeric operand stays visible, never an error - } - return v - case grammar.CalcNeg: - return -evalCalc(n.X, operands) - case grammar.CalcBin: - return arith(n.Operator, evalCalc(n.L, operands), evalCalc(n.R, operands)) - } - panic(internalError("calc node %#v has no evaluation", n)) -} - -func arith(operator byte, l, r float64) float64 { - switch operator { - case '+': - return l + r - case '-': - return l - r - case '*': - return l * r - default: // '/' - return l / r - } -} - -// checkCalc validates a calc token at compile time: a parseable expression, operands -// that can be numbers, and an optional non-negative integer dp. An operand no field -// holds is left for a name to answer, and checkCalcNames checks it once names link. -func checkCalc(fields map[string]node, args []string) error { - if len(args) < 1 || len(args) > 2 { - return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args)) - } - c, err := grammar.ParseCalc(args[0]) - if err != nil { - return fmt.Errorf("calc(%q): %w", args[0], err) - } - if err := checkOperands(args[0], c, func(name string) []node { +// checkCalcFields refuses a calc operand a field holds that is never a number, and a division +// by a constant zero. An operand no field holds is left for a name to answer, and +// checkCalcNames checks it once names link. +func checkCalcFields(args []string, fields map[string]node) error { + return checkOperands(args[0], builtinfunc.ParsedCalc(args[0]), func(name string) []node { if n, ok := fields[name]; ok { return []node{n} } return nil - }); err != nil { - return err - } - if len(args) == 2 { - dp, err := plainInt(args[1]) - if err != nil { - return fmt.Errorf("calc decimals %w", err) - } - if dp < 0 || dp > maxDecimals { - return fmt.Errorf("calc decimals %d must be in 0..%d", dp, maxDecimals) - } - } - return nil + }) } -// checkCalcNames holds each calc of t reading a name to the checks checkCalc makes of a field. +// checkCalcNames holds each calc of t reading a name to the checks checkCalcFields makes of a field. func checkCalcNames(path string, t *template) error { for _, o := range t.compiled.ops { if o.Fn != "calc" || !slices.ContainsFunc(o.operands, func(a arm) bool { return a.kind == namedRead }) { continue } - if err := checkOperands(o.Args[0], parsedCalc(o.Args[0]), operandNodes(o)); err != nil { + if err := checkOperands(o.Args[0], builtinfunc.ParsedCalc(o.Args[0]), operandNodes(o)); err != nil { return fmt.Errorf("%s: token {%s}: %w", t.site.label(path), o.Body, err) } } @@ -163,7 +109,7 @@ func constantValue(n grammar.CalcNode, operand func(string) []node) (float64, bo if !lok || !rok { return 0, false } - return arith(n.Operator, l, r), true + return builtinfunc.Arith(n.Operator, l, r), true } return 0, false } @@ -199,43 +145,3 @@ func allNeverNumeric(nodes []node) (text string, never bool) { } return text, len(nodes) > 0 } - -// calcPrep parses the expression and decimals once, at compile time. checkCalc proved -// both args valid, so no step here can fail. -func calcPrep(args []string) callFn { - expr := parsedCalc(args[0]).Expr - dp := calcDecimals(args) - return func(_ *drawstate.State, _ string, operands []string) string { - return formatFloat(evalCalc(expr, operands), dp) - } -} - -// parsedCalc parses an expression checkCalc accepted. -func parsedCalc(expr string) grammar.Calc { - c, err := grammar.ParseCalc(expr) - if err != nil { - panic(internalError("calc(%q) passed its check unparsed: %v", expr, err)) - } - return c -} - -// calcDecimals is a calc's decimals count, or proven.ShortestDecimals where it names none. -func calcDecimals(args []string) int { - if len(args) == 2 { - return atoi(args[1]) - } - return proven.ShortestDecimals -} - -// calcOperands lists the operands a calc's args read, fields or names. checkCalc reports -// an expression that does not parse, so one that does not simply names nothing. -func calcOperands(args []string) []string { - if len(args) == 0 { - return nil - } - c, err := grammar.ParseCalc(args[0]) - if err != nil { - return nil - } - return c.Operands -} diff --git a/format.go b/format.go index 176ba2e..b090de6 100644 --- a/format.go +++ b/format.go @@ -4,60 +4,19 @@ import ( "fmt" "strings" - "github.com/larvit/fejkdata/internal/drawstate" + "github.com/larvit/fejkdata/internal/builtinfunc" "github.com/larvit/fejkdata/internal/grammar" - "github.com/larvit/fejkdata/internal/proven" ) -// builtin is a format-string function invoked as {name(args)}. It receives the -// draw state, the output emitted so far in the current expansion (for derivations -// such as a checksum over preceding digits), and the values of the operands it named -// (calc and the transforms name them). All must stay pure over (draw state, emitted, operands) so -// seeded output is reproducible. arity is the exact arg count, or -1 for variadic (then -// checkArgs does all the validation). -type builtin struct { - arity int - // prep parses validated args once, at compile time, into the closure expand calls. - prep func(args []string) callFn - checkArgs func(fields map[string]node, args []string) error - // operands names the fields the call reads, which expand renders for it; nil - // for a builtin that reads none. - operands func(args []string) []string - // noRefOperands says no operand can be a reference, so a name read once there is not refused. - noRefOperands bool - // proveNumber bounds the number a call's text reads as, token its body, and says which - // datatypes that text is not; set it where every render reads as a finite number, which - // makes the call a calc operand, and leave it nil otherwise. - proveNumber func(token string, prints DataType, args []string) proven.Value - // prints is the datatype a call's text is, handed to proveNumber: DataTypeString where it - // reads as a number no column should type, as digits' leading zeros; unset where - // proveNumber is nil. - prints DataType -} - -func plural(n int) string { - if n == 1 { - return "" - } - return "s" -} - -// checkFunc validates a call at compile time: naming a known builtin, with the arg -// count that builtin takes and args its check accepts. fields is passed through for -// the builtins (calc, the transforms) that validate against them. +// checkFunc validates a call at compile time: a known builtin, its args, and for calc the +// operands fields holds. func checkFunc(tok grammar.Token, fields map[string]node) error { - body, name, args := tok.Body, tok.Fn, tok.Args - b, known := builtins[name] - if !known { - return fmt.Errorf("token {%s}: unknown function %q", body, name) + err := builtinfunc.Check(tok.Fn, tok.Args) + if err == nil && tok.Fn == "calc" { + err = checkCalcFields(tok.Args, fields) } - if b.arity >= 0 && len(args) != b.arity { - return fmt.Errorf("token {%s}: %s takes %d argument%s, got %d", body, name, b.arity, plural(b.arity), len(args)) - } - if b.checkArgs != nil { - if err := b.checkArgs(fields, args); err != nil { - return fmt.Errorf("token {%s}: %w", body, err) - } + if err != nil { + return fmt.Errorf("token {%s}: %w", tok.Body, err) } return nil } @@ -137,7 +96,7 @@ func checkReads(t grammar.Token, fields map[string]node, operands bool) ([]unbou return nil, err } a := splitArm(name, nil) - unbound = append(unbound, unboundRead{head: a.head, tail: grammar.JoinSegments(a.tail), body: t.Body, operand: operands, noRef: operands && builtins[t.Fn].noRefOperands, err: err}) + unbound = append(unbound, unboundRead{head: a.head, tail: grammar.JoinSegments(a.tail), body: t.Body, operand: operands, noRef: operands && builtinfunc.NoRefOperands(t.Fn), err: err}) } return unbound, nil } @@ -219,21 +178,11 @@ func hintableRef(name string) bool { // tokenReads lists the names t reads: a read's arms, or the operands a call's builtin reads. func tokenReads(t grammar.Token) []string { if t.Kind == grammar.BuiltinCall { - return builtinOperands(t.Fn, t.Args) + return builtinfunc.Operands(t.Fn, t.Args) } return t.Arms } -// builtinOperands lists the fields a call reads as operands, empty for a builtin that -// reads none or is unknown. -func builtinOperands(name string, args []string) []string { - b, known := builtins[name] - if !known || b.operands == nil { - return nil - } - return b.operands(args) -} - // checkNoRepeatedArm rejects {a|a|b}: an alternation picks its arms evenly, so a // repeated one is a second spelling of weight. The error names the spelling that // does skew a pick. @@ -312,17 +261,12 @@ func checkSegments(a arm) error { return grammar.CheckSegments(append([]string{a.head}, a.tail...)) } -// callFn is a builtin prepared for 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. -type callFn func(s *drawstate.State, emitted string, operands []string) string - // op is one compiled unit of a format string: a literal run, a name read, // or a builtin already prepared with its args. type op struct { grammar.Token arms []arm // grammar.NameRead: the '|' alternatives, split into head and tail once - call callFn + call builtinfunc.Call // operands are the fields the builtin reads, in the order its operands func // fixed; expand reads them before the call. nil for a builtin that reads none. operands []arm @@ -336,14 +280,14 @@ type formatOps struct { func (c *formatOps) function(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) { var operands []arm - for _, operand := range builtinOperands(tok.Fn, tok.Args) { + for _, operand := range builtinfunc.Operands(tok.Fn, tok.Args) { a := splitArm(operand, refs) if isName(a.head) { a.kind = namedRead } operands = append(operands, a) } - c.ops = append(c.ops, op{Token: tok, call: builtins[tok.Fn].prep(tok.Args), operands: operands}) + c.ops = append(c.ops, op{Token: tok, call: builtinfunc.Prep(tok.Fn, tok.Args), operands: operands}) } func (c *formatOps) field(tok grammar.Token, refs map[string]refBinding, isName func(string) bool) { diff --git a/internal/builtinfunc/builtinfunc.go b/internal/builtinfunc/builtinfunc.go new file mode 100644 index 0000000..c513d32 --- /dev/null +++ b/internal/builtinfunc/builtinfunc.go @@ -0,0 +1,122 @@ +// Package builtinfunc holds the {name(args)} functions a format calls: their checks, the +// draws they make, and the numbers their text is proven to read as. +package builtinfunc + +import ( + "fmt" + "slices" + "strings" + + "github.com/larvit/fejkdata/internal/datatype" + "github.com/larvit/fejkdata/internal/drawstate" + "github.com/larvit/fejkdata/internal/proven" +) + +// Call is a builtin prepared for 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 the caller read for it. +type Call func(s *drawstate.State, emitted string, operands []string) string + +// builtin is a format-string function invoked as {name(args)}. It receives the +// draw state, the output emitted so far in the current expansion (for derivations +// such as a checksum over preceding digits), and the values of the operands it named +// (calc and the transforms name them). All must stay pure over (draw state, emitted, operands) so +// seeded output is reproducible. arity is the exact arg count, or -1 for variadic (then +// checkArgs does all the validation). +type builtin struct { + arity int + // prep parses validated args once, at compile time, into the closure a render calls. + prep func(args []string) Call + checkArgs func(args []string) error + // operands names the fields the call reads, which a render reads for it; nil + // for a builtin that reads none. + operands func(args []string) []string + // noRefOperands says no operand can be a reference, so a name read once there is not refused. + noRefOperands bool + // proveNumber bounds the number a call's text reads as, token its body, and says which + // datatypes that text is not; set it where every render reads as a finite number, which + // makes the call a calc operand, and leave it nil otherwise. + proveNumber func(token string, prints datatype.DataType, args []string) proven.Value + // prints is the datatype a call's text is, handed to proveNumber: datatype.String where it + // reads as a number no column should type, as digits' leading zeros; unset where + // proveNumber is nil. + prints datatype.DataType +} + +// rng is the randomness a builtin sample draws from, which *drawstate.State satisfies. +type rng interface { + IntN(n int) int + Float64() float64 +} + +// Check validates a call at compile time: naming a known builtin, with the arg count that +// builtin takes and args its check accepts. +func Check(name string, args []string) error { + b, known := builtins[name] + if !known { + return fmt.Errorf("unknown function %q", name) + } + if b.arity >= 0 && len(args) != b.arity { + return fmt.Errorf("%s takes %d argument%s, got %d", name, b.arity, plural(b.arity), len(args)) + } + if b.checkArgs != nil { + return b.checkArgs(args) + } + return nil +} + +// Prep parses a call's args, which Check accepted, into the Call a render makes. +func Prep(name string, args []string) Call { return builtins[name].prep(args) } + +// Operands lists the fields a call reads as operands, empty for a builtin that reads none +// or is unknown. +func Operands(name string, args []string) []string { + b, known := builtins[name] + if !known || b.operands == nil { + return nil + } + return b.operands(args) +} + +// NoRefOperands reports a builtin no operand of which can be a reference. +func NoRefOperands(name string) bool { return builtins[name].noRefOperands } + +// IsTransform reports a builtin that rewrites the text of its one operand. +func IsTransform(name string) bool { + _, isTransform := transforms[name] + return isTransform +} + +// ProveNumber proves a call whose every render reads as a finite number, token its body; +// false for any other call. A calc's proof reads its operands, which the caller proves. +func ProveNumber(name, token string, args []string) (proven.Value, bool) { + b := builtins[name] + if b.proveNumber == nil { + return proven.Value{}, false + } + return b.proveNumber(token, b.prints, args), true +} + +// NumberCalls lists the calls printing a number, or with text also those whose text is +// one, as an error names them. +func NumberCalls(text bool) string { + var calls []string + for name, b := range builtins { + if b.proveNumber != nil && (text || b.prints != datatype.String) { + calls = append(calls, "{"+name+"()}") + } + } + slices.Sort(calls) + return strings.Join(calls, ", ") + " or {calc()}" +} + +func plural(n int) string { + if n == 1 { + return "" + } + return "s" +} + +func internalError(format string, a ...any) string { + return "fejkdata: internal error: " + fmt.Sprintf(format, a...) +} diff --git a/builtins.go b/internal/builtinfunc/builtins.go similarity index 89% rename from builtins.go rename to internal/builtinfunc/builtins.go index 9b797a1..5146c76 100644 --- a/builtins.go +++ b/internal/builtinfunc/builtins.go @@ -1,4 +1,4 @@ -package fejkdata +package builtinfunc import ( "encoding/base64" @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/larvit/fejkdata/internal/datatype" "github.com/larvit/fejkdata/internal/drawstate" "github.com/larvit/fejkdata/internal/proven" ) @@ -33,32 +34,32 @@ var builtins = withTransforms(map[string]builtin{ "ulid": {arity: 0, prep: sample(ulid)}, "nanoid": {arity: 1, checkArgs: posIntArg, prep: chars(nanoidAlphabet)}, "hex": {arity: 1, checkArgs: posIntArg, prep: chars(hexDigits)}, - "digits": {arity: 1, checkArgs: posIntArg, prep: chars("0123456789"), prints: DataTypeString, proveNumber: func(token string, prints DataType, a []string) proven.Value { + "digits": {arity: 1, checkArgs: posIntArg, prep: chars("0123456789"), prints: datatype.String, proveNumber: func(token string, prints datatype.DataType, a []string) proven.Value { return proven.Printing(token, prints, proven.Bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true)) }}, "upper": {arity: 1, checkArgs: posIntArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")}, "lower": {arity: 1, checkArgs: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")}, - "base64": {arity: 1, checkArgs: posIntArg, prep: func(a []string) callFn { + "base64": {arity: 1, checkArgs: posIntArg, prep: func(a []string) Call { n := atoi(a[0]) return func(s *drawstate.State, _ string, _ []string) string { return base64.StdEncoding.EncodeToString(randBytes(s, n)) } }}, - "int": {arity: 2, checkArgs: intRangeArgs, prep: func(a []string) callFn { + "int": {arity: 2, checkArgs: intRangeArgs, prep: func(a []string) Call { lo, span := atoi(a[0]), atoi(a[1])-atoi(a[0])+1 return func(s *drawstate.State, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) } - }, prints: DataTypeInteger, proveNumber: func(token string, prints DataType, a []string) proven.Value { + }, prints: datatype.Integer, proveNumber: func(token string, prints datatype.DataType, a []string) proven.Value { return proven.Printing(token, prints, proven.Bounded(float64(atoi(a[0])), float64(atoi(a[1])), true)) }}, - "float": {arity: 3, checkArgs: floatArgs, prep: func(a []string) callFn { + "float": {arity: 3, checkArgs: floatArgs, prep: func(a []string) Call { lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2]) return func(s *drawstate.State, _ string, _ []string) string { return formatFloat(lo+s.Float64()*(hi-lo), dp) } - }, prints: DataTypeNumber, proveNumber: func(token string, _ DataType, a []string) proven.Value { + }, prints: datatype.Number, proveNumber: func(token string, _ datatype.DataType, a []string) proven.Value { return proven.PrintedNumber(token, proven.Bounded(atof(a[0]), atof(a[1]), false), atoi(a[2])) }}, - "iban": {arity: 1, checkArgs: ibanArg, prep: func(a []string) callFn { + "iban": {arity: 1, checkArgs: ibanArg, prep: func(a []string) Call { cc := a[0] return func(s *drawstate.State, _ string, _ []string) string { return iban(s, cc) } }}, @@ -68,7 +69,7 @@ var builtins = withTransforms(map[string]builtin{ // seq is the one stateful builtin: a per-generator counter from 1, advancing on // each call. An optional name selects an independent counter; no name uses the // default one. Deterministic by construction, so seeded output stays stable. - "seq": {arity: -1, checkArgs: seqArg, prep: func(a []string) callFn { + "seq": {arity: -1, checkArgs: seqArg, prep: func(a []string) Call { key := "" if len(a) == 1 { key = a[0] @@ -76,7 +77,7 @@ var builtins = withTransforms(map[string]builtin{ return func(s *drawstate.State, _ string, _ []string) string { return strconv.FormatUint(s.Seq(key), 10) } - }, prints: DataTypeInteger, proveNumber: func(token string, prints DataType, _ []string) proven.Value { + }, prints: datatype.Integer, proveNumber: func(token string, prints datatype.DataType, _ []string) proven.Value { return proven.Printing(token, prints, proven.Bounded(1, math.MaxInt64, true)) }}, }) @@ -84,18 +85,18 @@ var builtins = withTransforms(map[string]builtin{ // derive and sample are the two argument-free builtin shapes: a derivation reads // the output emitted so far, a sample reads only the rng. chars is the shape of a // sample of n characters drawn from an alphabet. -func derive(f func(emitted string) string) func([]string) callFn { - return func([]string) callFn { +func derive(f func(emitted string) string) func([]string) Call { + return func([]string) Call { return func(_ *drawstate.State, emitted string, _ []string) string { return f(emitted) } } } -func sample(f func(rng) string) func([]string) callFn { - return func([]string) callFn { +func sample(f func(rng) string) func([]string) Call { + return func([]string) Call { return func(s *drawstate.State, _ string, _ []string) string { return f(s) } } } -func chars(alphabet string) func([]string) callFn { - return func(a []string) callFn { +func chars(alphabet string) func([]string) Call { + return func(a []string) Call { n := atoi(a[0]) return func(s *drawstate.State, _ string, _ []string) string { return randChars(s, n, alphabet) } } @@ -160,7 +161,7 @@ func plainInt(s string) (int, error) { } return n, nil } -func posIntArg(_ map[string]node, a []string) error { +func posIntArg(a []string) error { n, err := plainInt(a[0]) if errors.Is(err, strconv.ErrRange) { return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen) @@ -176,7 +177,7 @@ func posIntArg(_ map[string]node, a []string) error { } return nil } -func intRangeArgs(_ map[string]node, a []string) error { +func intRangeArgs(a []string) error { lo, err := plainInt(a[0]) if err != nil { return fmt.Errorf("int(min,max): min %w", err) @@ -196,7 +197,7 @@ func intRangeArgs(_ map[string]node, a []string) error { } return nil } -func floatArgs(_ map[string]node, a []string) error { +func floatArgs(a []string) error { lo, e1 := strconv.ParseFloat(a[0], 64) hi, e2 := strconv.ParseFloat(a[1], 64) if e1 != nil || e2 != nil { @@ -223,7 +224,7 @@ func floatArgs(_ map[string]node, a []string) error { } return nil } -func seqArg(_ map[string]node, a []string) error { +func seqArg(a []string) error { if len(a) > 1 { return fmt.Errorf("seq takes at most one name, got %d", len(a)) } diff --git a/internal/builtinfunc/calc.go b/internal/builtinfunc/calc.go new file mode 100644 index 0000000..b4ea1b0 --- /dev/null +++ b/internal/builtinfunc/calc.go @@ -0,0 +1,108 @@ +package builtinfunc + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/larvit/fejkdata/internal/drawstate" + "github.com/larvit/fejkdata/internal/grammar" + "github.com/larvit/fejkdata/internal/proven" +) + +// evalCalc evaluates an expression over the operand values a render already read, so it +// draws nothing. +func evalCalc(n grammar.CalcNode, operands []string) float64 { + switch n := n.(type) { + case grammar.CalcNum: + return float64(n) + case grammar.CalcVar: + v, err := strconv.ParseFloat(strings.TrimSpace(operands[n.At]), 64) + if err != nil { + return math.NaN() // a non-numeric operand stays visible, never an error + } + return v + case grammar.CalcNeg: + return -evalCalc(n.X, operands) + case grammar.CalcBin: + return Arith(n.Operator, evalCalc(n.L, operands), evalCalc(n.R, operands)) + } + panic(internalError("calc node %#v has no evaluation", n)) +} + +// Arith applies a calc operator. +func Arith(operator byte, l, r float64) float64 { + switch operator { + case '+': + return l + r + case '-': + return l - r + case '*': + return l * r + default: // '/' + return l / r + } +} + +// checkCalc validates a calc token's own args: a parseable expression and an optional +// non-negative integer dp. What its operands hold is the caller's to check. +func checkCalc(args []string) error { + if len(args) < 1 || len(args) > 2 { + return fmt.Errorf("calc takes an expression and an optional decimals count, got %d args", len(args)) + } + if _, err := grammar.ParseCalc(args[0]); err != nil { + return fmt.Errorf("calc(%q): %w", args[0], err) + } + if len(args) == 2 { + dp, err := plainInt(args[1]) + if err != nil { + return fmt.Errorf("calc decimals %w", err) + } + if dp < 0 || dp > maxDecimals { + return fmt.Errorf("calc decimals %d must be in 0..%d", dp, maxDecimals) + } + } + return nil +} + +// calcPrep parses the expression and decimals once, at compile time. checkCalc proved +// both args valid, so no step here can fail. +func calcPrep(args []string) Call { + expr := ParsedCalc(args[0]).Expr + dp := CalcDecimals(args) + return func(_ *drawstate.State, _ string, operands []string) string { + return formatFloat(evalCalc(expr, operands), dp) + } +} + +// ParsedCalc parses an expression Check accepted. +func ParsedCalc(expr string) grammar.Calc { + c, err := grammar.ParseCalc(expr) + if err != nil { + panic(internalError("calc(%q) passed its check unparsed: %v", expr, err)) + } + return c +} + +// CalcDecimals is the decimals count of a calc Check accepted, or proven.ShortestDecimals +// where it names none. +func CalcDecimals(args []string) int { + if len(args) == 2 { + return atoi(args[1]) + } + return proven.ShortestDecimals +} + +// calcOperands lists the operands a calc's args read, fields or names. checkCalc reports +// an expression that does not parse, so one that does not simply names nothing. +func calcOperands(args []string) []string { + if len(args) == 0 { + return nil + } + c, err := grammar.ParseCalc(args[0]) + if err != nil { + return nil + } + return c.Operands +} diff --git a/checksum.go b/internal/builtinfunc/checksum.go similarity index 97% rename from checksum.go rename to internal/builtinfunc/checksum.go index 8029f75..16dd5f0 100644 --- a/checksum.go +++ b/internal/builtinfunc/checksum.go @@ -1,4 +1,4 @@ -package fejkdata +package builtinfunc import "fmt" @@ -65,7 +65,7 @@ func eanCheck(s string) string { // a left-to-right derivation; it generates the whole value instead. var ibanLen = map[string]int{"BE": 16, "DE": 22, "DK": 18, "ES": 24, "FI": 18, "NO": 15, "SE": 24} -func ibanArg(_ map[string]node, a []string) error { +func ibanArg(a []string) error { if _, ok := ibanLen[a[0]]; !ok { return fmt.Errorf("iban(%q): unsupported country code", a[0]) } diff --git a/layout.go b/internal/builtinfunc/layout.go similarity index 96% rename from layout.go rename to internal/builtinfunc/layout.go index 511c551..78c7cf5 100644 --- a/layout.go +++ b/internal/builtinfunc/layout.go @@ -1,4 +1,4 @@ -package fejkdata +package builtinfunc import ( "fmt" @@ -84,7 +84,7 @@ func holdsQuotedLayout(a []string) bool { } return false } -func dateArgs(_ map[string]node, a []string) error { +func dateArgs(a []string) error { if err := layoutArity("date", 3, a); err != nil { return err } @@ -111,7 +111,7 @@ func dateArgs(_ map[string]node, a []string) error { } return nil } -func timeArg(_ map[string]node, a []string) error { +func timeArg(a []string) error { if err := layoutArity("time", 1, a); err != nil { return err } @@ -127,7 +127,7 @@ func timeArg(_ map[string]node, a []string) error { // datePrep draws a second in [from 00:00:00, to 23:59:59] UTC; the span is counted // in seconds, since a Duration overflows past 292 years. -func datePrep(a []string) callFn { +func datePrep(a []string) Call { from, err := time.Parse(dayLayout, a[0]) if err != nil { panic(internalError("builtin arg %q reached prep unvalidated: %v", a[0], err)) @@ -141,7 +141,7 @@ func datePrep(a []string) callFn { return time.Unix(start+int64(s.IntN(span)), 0).UTC().Format(layout) } } -func timePrep(a []string) callFn { +func timePrep(a []string) Call { layout := layoutOf(a[0]) return func(s *drawstate.State, _ string, _ []string) string { return time.Unix(int64(s.IntN(86400)), 0).UTC().Format(layout) diff --git a/transform.go b/internal/builtinfunc/transform.go similarity index 94% rename from transform.go rename to internal/builtinfunc/transform.go index 2dd0feb..cad92d6 100644 --- a/transform.go +++ b/internal/builtinfunc/transform.go @@ -1,4 +1,4 @@ -package fejkdata +package builtinfunc import ( "fmt" @@ -48,7 +48,7 @@ func unwrapTransform(arg string) (leaf string, chain []func(string) string, err } // transformArg proves a transform's chain; checkReads proves the operand it reads. -func transformArg(_ map[string]node, a []string) error { +func transformArg(a []string) error { _, _, err := unwrapTransform(a[0]) return err } @@ -62,8 +62,8 @@ func transformOperand(a []string) []string { } return []string{leaf} } -func transformPrep(outer func(string) string) func([]string) callFn { - return func(a []string) callFn { +func transformPrep(outer func(string) string) func([]string) Call { + return func(a []string) Call { _, chain, err := unwrapTransform(a[0]) if err != nil { panic(internalError("transform arg %q reached prep unvalidated: %v", a[0], err)) diff --git a/render.go b/render.go index 2b724b0..c78af41 100644 --- a/render.go +++ b/render.go @@ -8,13 +8,6 @@ import ( "github.com/larvit/fejkdata/internal/grammar" ) -// rng is the randomness a builtin sample draws from, which *drawstate.State satisfies. The -// render path takes the concrete type instead, which keeps its draws off the heap. -type rng interface { - IntN(n int) int - Float64() float64 -} - // Fake generates a value for a dot path. Each segment descends one level: folder // names and the category (JSON file) come first, then named fields within it, // e.g. "sv_SE.address" or "sv_SE.address.street". Choices along the way are diff --git a/valueproof.go b/valueproof.go index 58f9a20..fc82ed2 100644 --- a/valueproof.go +++ b/valueproof.go @@ -3,9 +3,8 @@ package fejkdata import ( "fmt" "reflect" - "slices" - "strings" + "github.com/larvit/fejkdata/internal/builtinfunc" "github.com/larvit/fejkdata/internal/grammar" "github.com/larvit/fejkdata/internal/proven" ) @@ -166,7 +165,7 @@ func (p *valueProof) proveTemplate(t *template) proven.Value { } o := t.compiled.ops[0] body, name, args := o.Body, o.Fn, o.Args - switch _, isTransform := transforms[name]; { + switch { case o.Kind == grammar.NameRead: var leaves []node for _, a := range o.arms { @@ -175,38 +174,26 @@ func (p *valueProof) proveTemplate(t *template) proven.Value { return p.proveUnion(leaves) case name == "calc": return p.proveCalc(o) - case builtins[name].proveNumber != nil: - return builtins[name].proveNumber(body, builtins[name].prints, args) - case isTransform: + case builtinfunc.IsTransform(name): return proven.Unproven(fmt.Sprintf("{%s} rewrites text rather than printing a value; write the values it would print", body)) } + if v, numeric := builtinfunc.ProveNumber(name, body, args); numeric { + return v + } return proven.Printing(body, DataTypeString, proven.Value{NotOperand: fmt.Sprintf("{%s} prints text, not a number", body)}) } func (p *valueProof) proveCalc(o op) proven.Value { body, args := o.Body, o.Args nodes := operandNodes(o) - v, doubt := proven.Calc(parsedCalc(args[0]).Expr, func(name string) proven.Value { return p.proveUnion(nodes(name)) }) + v, doubt := proven.Calc(builtinfunc.ParsedCalc(args[0]).Expr, func(name string) proven.Value { return p.proveUnion(nodes(name)) }) if doubt != "" { return proven.Unproven(fmt.Sprintf("{%s}: %s", body, doubt)) } - return proven.PrintedNumber(body, v, calcDecimals(args)) + return proven.PrintedNumber(body, v, builtinfunc.CalcDecimals(args)) } -var typedCalls, operandCalls = numberCalls(false), numberCalls(true) - -// numberCalls lists the builtins printing a number, or with text also those whose text -// is one. -func numberCalls(text bool) string { - var calls []string - for name, b := range builtins { - if b.proveNumber != nil && (text || b.prints != DataTypeString) { - calls = append(calls, "{"+name+"()}") - } - } - slices.Sort(calls) - return strings.Join(calls, ", ") + " or {calc()}" -} +var typedCalls, operandCalls = builtinfunc.NumberCalls(false), builtinfunc.NumberCalls(true) func notOneValue(format, calls string) string { return fmt.Sprintf("%q is not one value; write one literal or one %s, or read one", format, calls)