Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
11 changed files with 298 additions and 238 deletions
Showing only changes of commit 3bb96ded91 - Show all commits
+4 -5
View File
@@ -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
+10 -104
View File
@@ -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
}
+13 -69
View File
@@ -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) {
+122
View File
@@ -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))
}
+108
View File
@@ -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])
}
+5 -5
View File
@@ -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))
-7
View File
@@ -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
View File
@@ -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)