diff --git a/README.md b/README.md index 08269f1..66dbb2d 100644 --- a/README.md +++ b/README.md @@ -413,7 +413,7 @@ datatype cannot hold is a load error naming it: ```text order.id: datatype integer: {digits(3)} prints text, not an integer -order.id: datatype integer: "1{digits(2)}" is not one value; write one literal or one {int()}, {float()}, {seq()} or {calc()}, or read one +order.id: datatype integer: "1{digits(2)}" is not one value; write one literal or one {float()}, {int()}, {seq()} or {calc()}, or read one ``` A column of one reference alone to another record's column — `"score": "{/src.score}"` diff --git a/builtins.go b/builtins.go index c6d6c0d..d960701 100644 --- a/builtins.go +++ b/builtins.go @@ -22,7 +22,7 @@ const ( // emitted so far in the current expansion (place them after their payload); // samples read only the rng. A time-based id (uuid v7, ulid) draws its timestamp // from the rng, not the wall clock, so seeded output stays reproducible. -var builtins = map[string]builtin{ +var builtins = withTransforms(map[string]builtin{ "luhn": {arity: 0, prep: derive(func(e string) string { return string(rune('0' + luhnCheck(e))) })}, "mod11": {arity: 0, prep: derive(mod11Check)}, "ean": {arity: 0, prep: derive(eanCheck)}, @@ -30,8 +30,8 @@ var builtins = map[string]builtin{ "ulid": {arity: 0, prep: sample(ulid)}, "nanoid": {arity: 1, check: posIntArg, prep: chars(nanoidAlphabet)}, "hex": {arity: 1, check: posIntArg, prep: chars(hexDigits)}, - "digits": {arity: 1, check: posIntArg, prep: chars("0123456789"), number: func(token string, a []string) proven { - return printing(token, DataTypeString, bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true)) + "digits": {arity: 1, check: posIntArg, prep: chars("0123456789"), prints: DataTypeString, number: func(token string, prints DataType, a []string) proven { + return printing(token, prints, bounded(0, math.Pow(10, float64(atoi(a[0])))-1, true)) }}, "upper": {arity: 1, check: posIntArg, prep: chars("ABCDEFGHIJKLMNOPQRSTUVWXYZ")}, "lower": {arity: 1, check: posIntArg, prep: chars("abcdefghijklmnopqrstuvwxyz")}, @@ -44,27 +44,24 @@ var builtins = map[string]builtin{ "int": {arity: 2, check: intRangeArgs, prep: func(a []string) callFn { lo, span := atoi(a[0]), atoi(a[1])-atoi(a[0])+1 return func(s *session, _ string, _ []string) string { return strconv.Itoa(lo + s.IntN(span)) } - }, number: func(token string, a []string) proven { - return printing(token, DataTypeInteger, bounded(float64(atoi(a[0])), float64(atoi(a[1])), true)) + }, prints: DataTypeInteger, number: func(token string, prints DataType, a []string) proven { + return printing(token, prints, bounded(float64(atoi(a[0])), float64(atoi(a[1])), true)) }}, "float": {arity: 3, check: floatArgs, prep: func(a []string) callFn { lo, hi, dp := atof(a[0]), atof(a[1]), atoi(a[2]) return func(s *session, _ string, _ []string) string { return formatFloat(lo+s.Float64()*(hi-lo), dp) } - }, number: func(token string, a []string) proven { + }, prints: DataTypeNumber, number: func(token string, _ DataType, a []string) proven { return printedNumber(token, bounded(atof(a[0]), atof(a[1]), false), atoi(a[2])) }}, "iban": {arity: 1, check: ibanArg, prep: func(a []string) callFn { cc := a[0] return func(s *session, _ string, _ []string) string { return iban(s, cc) } }}, - "date": {arity: -1, check: dateArgs, prep: datePrep}, - "time": {arity: -1, check: timeArg, prep: timePrep}, - "calc": {arity: -1, check: checkCalc, prep: calcPrep, operands: calcOperands}, - "lowercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToLower), operands: transformOperand}, - "uppercase": {arity: 1, check: transformArg, prep: transformPrep(strings.ToUpper), operands: transformOperand}, - "ascii": {arity: 1, check: transformArg, prep: transformPrep(asciiFold), operands: transformOperand}, + "date": {arity: -1, check: dateArgs, prep: datePrep}, + "time": {arity: -1, check: timeArg, prep: timePrep}, + "calc": {arity: -1, check: checkCalc, prep: calcPrep, operands: calcOperands}, // seq is the one stateful builtin: a per-session 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. @@ -76,10 +73,10 @@ var builtins = map[string]builtin{ return func(s *session, _ string, _ []string) string { return strconv.FormatUint(s.next(key), 10) } - }, number: func(token string, _ []string) proven { - return printing(token, DataTypeInteger, bounded(1, math.MaxInt64, true)) + }, prints: DataTypeInteger, number: func(token string, prints DataType, _ []string) proven { + return printing(token, prints, bounded(1, math.MaxInt64, true)) }}, -} +}) // 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 diff --git a/template.go b/template.go index b5f8a45..d23dd0b 100644 --- a/template.go +++ b/template.go @@ -119,8 +119,9 @@ type builtin struct { // for a builtin that reads none. operands func(args []string) []string // number proves what a call prints, token its body: the bounds of its number and the - // datatype of its text; nil for a builtin whose text is no number. - number func(token string, args []string) proven + // datatype of its text, prints or one inside it; nil for a builtin whose text is no number. + prints DataType + number func(token string, prints DataType, args []string) proven } // funcCall splits a "{token}" body shaped name(args) into its parts; ok is false diff --git a/transform.go b/transform.go index 2b9fecd..0515889 100644 --- a/transform.go +++ b/transform.go @@ -14,6 +14,13 @@ var transforms = map[string]func(string) string{ "uppercase": strings.ToUpper, } +func withTransforms(builtins map[string]builtin) map[string]builtin { + for name, fn := range transforms { + builtins[name] = builtin{arity: 1, check: transformArg, prep: transformPrep(fn), operands: transformOperand} + } + return builtins +} + // unwrapTransform peels nested transform calls off an operand arg, returning the // field it finally names and the transforms to apply, innermost last. func unwrapTransform(arg string) (leaf string, chain []func(string) string, err error) { diff --git a/value.go b/value.go index 9b310c6..d217fd0 100644 --- a/value.go +++ b/value.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "regexp" + "slices" "strconv" "strings" ) @@ -148,8 +149,8 @@ func (p *valueProof) template(t *template) proven { case t.fixed: return literalValue(t.lit) case len(t.ops) != 1: - v := unproven(notOneValue(t.format, "{int()}, {float()}, {seq()} or {calc()}")) - v.notOperand = notOneValue(t.format, "{int()}, {float()}, {seq()}, {digits()} or {calc()}") + v := unproven(notOneValue(t.format, numberCalls(false))) + v.notOperand = notOneValue(t.format, numberCalls(true)) return v } o := t.ops[0] @@ -164,7 +165,7 @@ func (p *valueProof) template(t *template) proven { case name == "calc": return p.calc(t, body, args) case builtins[name].number != nil: - return builtins[name].number(body, args) + return builtins[name].number(body, builtins[name].prints, args) case isTransform: return unproven(fmt.Sprintf("{%s} rewrites text rather than printing a value; write the values it would print", body)) } @@ -287,6 +288,19 @@ func printing(token string, prints DataType, v proven) proven { return v } +// 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.number != nil && (text || b.prints != DataTypeString) { + calls = append(calls, "{"+name+"()}") + } + } + slices.Sort(calls) + return strings.Join(calls, ", ") + " or {calc()}" +} + 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) }