GNU-style CLI flags, embedded data, and a literal format grammar #3
@@ -180,9 +180,9 @@ many times — each an independent draw — joined by `separator` (default `""`)
|
|||||||
{ "format": "{word}", "repeat": 3, "separator": " ", "word": ["foo", "bar", "baz"] }
|
{ "format": "{word}", "repeat": 3, "separator": " ", "word": ["foo", "bar", "baz"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
Renders e.g. `bar foo baz`. A `separator` without a `repeat` is rejected, and so
|
Renders e.g. `bar foo baz`. Rejected at load: a `separator` without a `repeat`,
|
||||||
is a `repeat` that multiplies to more than 1 048 576 renders along any path of
|
a `separator` of `""` (the default), and a `repeat` that multiplies to more than
|
||||||
nested repeats.
|
1 048 576 renders along any path of nested repeats.
|
||||||
|
|
||||||
### Options and fields
|
### Options and fields
|
||||||
|
|
||||||
@@ -194,9 +194,11 @@ choice naming its item.
|
|||||||
### Functions
|
### Functions
|
||||||
|
|
||||||
A `{name(args)}` token calls a builtin. Arguments are checked at `New`: a bad
|
A `{name(args)}` token calls a builtin. Arguments are checked at `New`: a bad
|
||||||
count, range, country or expression fails fast, and a length, count or decimal
|
count, range, country or expression fails fast; an integer is written plain
|
||||||
place beyond a sane maximum is rejected, so a fat-fingered `hex(2000000000)`
|
(`5`, not `+5` or `05`); bounds are finite; a sample that could only ever emit one
|
||||||
never tries to allocate gigabytes. Every builtin draws only from the seed — a
|
value (`int(5,5)`, `float(1,1,2)`) is rejected naming the text to write instead;
|
||||||
|
and a length, count or decimal place beyond a sane maximum is rejected, so a
|
||||||
|
fat-fingered `hex(2000000000)` never tries to allocate gigabytes. Every builtin draws only from the seed — a
|
||||||
time-based id takes its timestamp from the rng, not the clock — so seeded output
|
time-based id takes its timestamp from the rng, not the clock — so seeded output
|
||||||
stays reproducible.
|
stays reproducible.
|
||||||
|
|
||||||
@@ -243,8 +245,9 @@ hyphenated field can't be an operand.
|
|||||||
```
|
```
|
||||||
|
|
||||||
Renders e.g. `19.99 x 3 = 59.97`. An operand that can never be a number (`"abc"`,
|
Renders e.g. `19.99 x 3 = 59.97`. An operand that can never be a number (`"abc"`,
|
||||||
or a choice of such) is rejected at load; one that sometimes is not yields `NaN`,
|
or a choice of such) is rejected at load, as is a division by a constant zero
|
||||||
and a division by zero `Inf` — both print rather than fail.
|
(`1/0`, or a fixed `"0"` field); an operand that sometimes is not a number yields
|
||||||
|
`NaN`, and a division by a sometimes-zero one `Inf` — both print rather than fail.
|
||||||
|
|
||||||
### Transforms
|
### Transforms
|
||||||
|
|
||||||
|
|||||||
+36
-12
@@ -176,7 +176,7 @@ var asciiFolds = map[rune]string{
|
|||||||
func asciiFold(s string) string {
|
func asciiFold(s string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for _, r := range s {
|
for _, r := range s {
|
||||||
if r < unicode.MaxASCII {
|
if r <= unicode.MaxASCII {
|
||||||
b.WriteRune(r)
|
b.WriteRune(r)
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(asciiFolds[r])
|
b.WriteString(asciiFolds[r])
|
||||||
@@ -221,10 +221,25 @@ func randChars(r rng, n int, alphabet string) string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// plainInt parses an integer arg written the one way: no sign, no leading zero.
|
||||||
|
func plainInt(s string) (int, error) {
|
||||||
|
n, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("%q is not an integer", s)
|
||||||
|
}
|
||||||
|
if strconv.Itoa(n) != s {
|
||||||
|
return 0, fmt.Errorf("%q is not a plain integer; write %d", s, n)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
func posIntArg(_ map[string]node, a []string) error {
|
func posIntArg(_ map[string]node, a []string) error {
|
||||||
n, err := strconv.Atoi(a[0])
|
n, err := plainInt(a[0])
|
||||||
if err != nil || n < 1 {
|
if err != nil {
|
||||||
return fmt.Errorf("count %q must be a positive integer", a[0])
|
return fmt.Errorf("count %w", err)
|
||||||
|
}
|
||||||
|
if n < 1 {
|
||||||
|
return fmt.Errorf("count %q must be positive", a[0])
|
||||||
}
|
}
|
||||||
if n > maxLen {
|
if n > maxLen {
|
||||||
return fmt.Errorf("count %d exceeds the maximum %d", n, maxLen)
|
return fmt.Errorf("count %d exceeds the maximum %d", n, maxLen)
|
||||||
@@ -233,14 +248,17 @@ func posIntArg(_ map[string]node, a []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func intRangeArgs(_ map[string]node, a []string) error {
|
func intRangeArgs(_ map[string]node, a []string) error {
|
||||||
lo, e1 := strconv.Atoi(a[0])
|
lo, e1 := plainInt(a[0])
|
||||||
hi, e2 := strconv.Atoi(a[1])
|
hi, e2 := plainInt(a[1])
|
||||||
if e1 != nil || e2 != nil {
|
if e1 != nil || e2 != nil {
|
||||||
return fmt.Errorf("int(min,max) needs integer args, got %q,%q", a[0], a[1])
|
return fmt.Errorf("int(min,max) needs plain integers, got %q,%q", a[0], a[1])
|
||||||
}
|
}
|
||||||
if lo > hi {
|
if lo > hi {
|
||||||
return fmt.Errorf("int(min,max): min %d > max %d", lo, hi)
|
return fmt.Errorf("int(min,max): min %d > max %d", lo, hi)
|
||||||
}
|
}
|
||||||
|
if lo == hi {
|
||||||
|
return fmt.Errorf("int(%d,%d) is the constant %d; write it as text", lo, hi, lo)
|
||||||
|
}
|
||||||
if uint64(hi)-uint64(lo) >= uint64(math.MaxInt64) { // span hi-lo+1 would overflow int -> IntN panic
|
if uint64(hi)-uint64(lo) >= uint64(math.MaxInt64) { // span hi-lo+1 would overflow int -> IntN panic
|
||||||
return fmt.Errorf("int(min,max): range %d..%d is too wide", lo, hi)
|
return fmt.Errorf("int(min,max): range %d..%d is too wide", lo, hi)
|
||||||
}
|
}
|
||||||
@@ -250,19 +268,25 @@ func intRangeArgs(_ map[string]node, a []string) error {
|
|||||||
func floatArgs(_ map[string]node, a []string) error {
|
func floatArgs(_ map[string]node, a []string) error {
|
||||||
lo, e1 := strconv.ParseFloat(a[0], 64)
|
lo, e1 := strconv.ParseFloat(a[0], 64)
|
||||||
hi, e2 := strconv.ParseFloat(a[1], 64)
|
hi, e2 := strconv.ParseFloat(a[1], 64)
|
||||||
dp, e3 := strconv.Atoi(a[2])
|
dp, e3 := plainInt(a[2])
|
||||||
if e1 != nil || e2 != nil || e3 != nil {
|
if e1 != nil || e2 != nil || e3 != nil {
|
||||||
return fmt.Errorf("float(min,max,dp) needs numeric args, got %q,%q,%q", a[0], a[1], a[2])
|
return fmt.Errorf("float(min,max,dp) needs numeric bounds and a plain decimals count, got %q,%q,%q", a[0], a[1], a[2])
|
||||||
|
}
|
||||||
|
if math.IsNaN(lo) || math.IsNaN(hi) || math.IsInf(lo, 0) || math.IsInf(hi, 0) {
|
||||||
|
return fmt.Errorf("float(min,max,dp) needs finite bounds, got %q,%q", a[0], a[1])
|
||||||
}
|
}
|
||||||
if lo > hi {
|
if lo > hi {
|
||||||
return fmt.Errorf("float(min,max,dp): min %v > max %v", lo, hi)
|
return fmt.Errorf("float(min,max,dp): min %v > max %v", lo, hi)
|
||||||
}
|
}
|
||||||
if math.IsInf(hi-lo, 0) { // an overflowing span would render as "+Inf"
|
|
||||||
return fmt.Errorf("float(min,max,dp): range %v..%v is too wide", lo, hi)
|
|
||||||
}
|
|
||||||
if dp < 0 || dp > maxDecimals {
|
if dp < 0 || dp > maxDecimals {
|
||||||
return fmt.Errorf("float(min,max,dp): decimals %d out of range 0..%d", dp, maxDecimals)
|
return fmt.Errorf("float(min,max,dp): decimals %d out of range 0..%d", dp, maxDecimals)
|
||||||
}
|
}
|
||||||
|
if lo == hi {
|
||||||
|
return fmt.Errorf("float(%s,%s,%d) is the constant %q; write it as text", a[0], a[1], dp, strconv.FormatFloat(lo, 'f', dp, 64))
|
||||||
|
}
|
||||||
|
if math.IsInf(hi-lo, 0) { // an overflowing span would render as "+Inf"
|
||||||
|
return fmt.Errorf("float(min,max,dp): range %v..%v is too wide", lo, hi)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,14 +78,78 @@ func checkCalc(fields map[string]node, args []string) error {
|
|||||||
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", args[0], name, text)
|
return fmt.Errorf("calc(%q): operand %q is never a number: it renders %q", args[0], name, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if divisor, zero := constantZeroDivisor(expr, fields); zero {
|
||||||
|
return fmt.Errorf("calc(%q) divides by %s, which is always zero", args[0], divisor)
|
||||||
|
}
|
||||||
if len(args) == 2 {
|
if len(args) == 2 {
|
||||||
if dp, err := strconv.Atoi(args[1]); err != nil || dp < 0 || dp > maxDecimals {
|
if dp, err := plainInt(args[1]); err != nil || dp < 0 || dp > maxDecimals {
|
||||||
return fmt.Errorf("calc decimals %q must be an integer in 0..%d", args[1], maxDecimals)
|
return fmt.Errorf("calc decimals %q must be a plain integer in 0..%d", args[1], maxDecimals)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// constantZeroDivisor finds a division whose right side is a constant zero: number
|
||||||
|
// literals and fixed operands folded, anything that varies left unknown.
|
||||||
|
func constantZeroDivisor(n calcNode, fields map[string]node) (string, bool) {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case calcNeg:
|
||||||
|
return constantZeroDivisor(n.x, fields)
|
||||||
|
case calcBin:
|
||||||
|
if n.op == '/' {
|
||||||
|
if v, known := constantValue(n.r, fields); known && v == 0 {
|
||||||
|
return calcText(n.r), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d, zero := constantZeroDivisor(n.l, fields); zero {
|
||||||
|
return d, true
|
||||||
|
}
|
||||||
|
return constantZeroDivisor(n.r, fields)
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// constantValue evaluates an expression whose every operand is fixed.
|
||||||
|
func constantValue(n calcNode, fields map[string]node) (float64, bool) {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case calcNum:
|
||||||
|
return float64(n), true
|
||||||
|
case calcVar:
|
||||||
|
t, ok := fields[string(n)].(*template)
|
||||||
|
if !ok || !t.fixed || t.repeat > 1 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(strings.TrimSpace(t.lit), 64)
|
||||||
|
return v, err == nil
|
||||||
|
case calcNeg:
|
||||||
|
v, ok := constantValue(n.x, fields)
|
||||||
|
return -v, ok
|
||||||
|
case calcBin:
|
||||||
|
l, lok := constantValue(n.l, fields)
|
||||||
|
r, rok := constantValue(n.r, fields)
|
||||||
|
if !lok || !rok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return calcBin{n.op, calcNum(l), calcNum(r)}.eval(nil), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcText spells an expression node the way an author would read it.
|
||||||
|
func calcText(n calcNode) string {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case calcNum:
|
||||||
|
return strconv.FormatFloat(float64(n), 'f', -1, 64)
|
||||||
|
case calcVar:
|
||||||
|
return string(n)
|
||||||
|
case calcNeg:
|
||||||
|
return "-" + calcText(n.x)
|
||||||
|
case calcBin:
|
||||||
|
return "(" + calcText(n.l) + " " + string(n.op) + " " + calcText(n.r) + ")"
|
||||||
|
}
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
// neverNumeric reports a node no render of which is a number: fixed text that does
|
// neverNumeric reports a node no render of which is a number: fixed text that does
|
||||||
// not parse, or a choice of only such items. text is one such render.
|
// not parse, or a choice of only such items. text is one such render.
|
||||||
func neverNumeric(n node) (text string, never bool) {
|
func neverNumeric(n node) (text string, never bool) {
|
||||||
|
|||||||
@@ -273,6 +273,9 @@ type draws struct {
|
|||||||
// linkRefs prove every step, so the walk cannot fail.
|
// linkRefs prove every step, so the walk cannot fail.
|
||||||
func readField(s *session, t *template, held *draws, a arm) string {
|
func readField(s *session, t *template, held *draws, a arm) string {
|
||||||
if !t.held[a.key] {
|
if !t.held[a.key] {
|
||||||
|
if len(a.tail) > 0 {
|
||||||
|
panic(fmt.Sprintf("fejkdata: %q reads a path into %q, which the expansion does not hold", a.name, a.key))
|
||||||
|
}
|
||||||
return render(s, t.fields[a.key])
|
return render(s, t.fields[a.key])
|
||||||
}
|
}
|
||||||
if v, read := held.value[a.path]; read {
|
if v, read := held.value[a.path]; read {
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ type template struct {
|
|||||||
|
|
||||||
func (*template) isNode() {}
|
func (*template) isNode() {}
|
||||||
|
|
||||||
|
// field is the node a path segment names; a binding is a render edge, not a field.
|
||||||
|
func (t *template) field(seg string) (node, bool) {
|
||||||
|
if isRef(seg) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
n, ok := t.fields[seg]
|
||||||
|
return n, ok
|
||||||
|
}
|
||||||
|
|
||||||
// compile converts parsed JSON into a node tree, validating structure up front.
|
// compile converts parsed JSON into a node tree, validating structure up front.
|
||||||
// Only a choice's items carry a weight, so one here would be inert whatever its type.
|
// Only a choice's items carry a weight, so one here would be inert whatever its type.
|
||||||
func compile(v any) (node, error) {
|
func compile(v any) (node, error) {
|
||||||
@@ -229,6 +238,9 @@ func readOptions(m map[string]any) (templateOptions, error) {
|
|||||||
if repeat == 1 {
|
if repeat == 1 {
|
||||||
return o, fmt.Errorf("separator joins repeated renders, so it has no effect without a repeat above 1")
|
return o, fmt.Errorf("separator joins repeated renders, so it has no effect without a repeat above 1")
|
||||||
}
|
}
|
||||||
|
if o.separator == "" {
|
||||||
|
return o, fmt.Errorf("separator \"\" is the default, so it has no effect; drop it")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, o.weighted = m["weight"]
|
_, o.weighted = m["weight"]
|
||||||
return o, nil
|
return o, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user