Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
+9
-22
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user