GNU-style CLI flags, embedded data, and a literal format grammar #3
@@ -247,7 +247,8 @@ 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, as is a division by a constant zero
|
or a choice of such) is rejected at load, as is a division by a constant zero
|
||||||
(`1/0`, or a fixed `"0"` field); an operand that sometimes is not a number yields
|
(`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.
|
`NaN`, and a division by one that is not constant `Inf` — both print rather than
|
||||||
|
fail.
|
||||||
|
|
||||||
### Transforms
|
### Transforms
|
||||||
|
|
||||||
@@ -390,14 +391,16 @@ tokens add cost in proportion to the output.
|
|||||||
- **64-bit targets only.** The gate builds amd64, and the buffer sizing a render
|
- **64-bit targets only.** The gate builds amd64, and the buffer sizing a render
|
||||||
pre-computes (renders × bytes) assumes a 64-bit int; on a 32-bit target it could
|
pre-computes (renders × bytes) assumes a 64-bit int; on a 32-bit target it could
|
||||||
overflow and panic.
|
overflow and panic.
|
||||||
- **A constant zero divisor is a load error; a sometimes-zero one prints `Inf`.**
|
- **A constant zero divisor is a load error; a divisor that is not constant prints
|
||||||
`1/0` and a fixed `"0"` field are decidable, so they join the never-numeric
|
`Inf`.** `1/0` and a fixed `"0"` field are decidable, so they join the
|
||||||
operand as a load error; a divisor that is only sometimes zero is data, and
|
never-numeric operand as a load error; the fold stops where an operand varies,
|
||||||
`Inf` printing visibly is the never-fail rule.
|
so `a/(b*c)` with `b` fixed at `0` and `c` varying loads and prints `Inf` every
|
||||||
- **A default written out, and a constant spelled as a sample, are load errors.**
|
draw — catching it needs zero-absorbing algebra for a shape nobody writes.
|
||||||
`weight: 1`, `repeat: 1`, `separator: ""`, `int(5,5)`, `float(1,1,2)`, `+5`
|
- **In data, a default written out and a constant spelled as a sample are load
|
||||||
and `05` each spell what a shorter form already spells, so each is rejected
|
errors.** `weight: 1`, `repeat: 1`, `separator: ""`, `int(5,5)`, `float(1,1,2)`,
|
||||||
naming that form.
|
`+5` and `05` each spell what a shorter form already spells, so each is rejected
|
||||||
|
naming that form. The CLI's numbers follow the shell instead: `--seed 007` and
|
||||||
|
`--repeat +3` are 7 and 3, as every command line reads them.
|
||||||
- **Samples say what they emit, transforms what they do.** `{upper(2)}` is two
|
- **Samples say what they emit, transforms what they do.** `{upper(2)}` is two
|
||||||
letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on
|
letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on
|
||||||
whether the argument looks like a number.
|
whether the argument looks like a number.
|
||||||
|
|||||||
+25
-12
@@ -2,6 +2,7 @@ package fejkdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -9,12 +10,12 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxLen caps sample output lengths (hex, nanoid, base64) and the renders a repeat
|
// maxLen caps sample output lengths (hex, nanoid, base64, digits, upper, lower)
|
||||||
// multiplies to along any path; maxDecimals caps float/calc decimal places. So a
|
// and maxDecimals float/calc decimal places, so a fat-fingered or overflowing
|
||||||
// fat-fingered or overflowing argument fails at New instead of trying to allocate
|
// argument fails at New instead of trying to allocate gigabytes — or panicking —
|
||||||
// gigabytes — or panicking — at render.
|
// at render.
|
||||||
const (
|
const (
|
||||||
maxLen = MaxRepeat
|
maxLen = 1 << 20
|
||||||
maxDecimals = 1024
|
maxDecimals = 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -224,6 +225,9 @@ func randChars(r rng, n int, alphabet string) string {
|
|||||||
// plainInt parses an integer arg written the one way: no sign, no leading zero.
|
// plainInt parses an integer arg written the one way: no sign, no leading zero.
|
||||||
func plainInt(s string) (int, error) {
|
func plainInt(s string) (int, error) {
|
||||||
n, err := strconv.Atoi(s)
|
n, err := strconv.Atoi(s)
|
||||||
|
if errors.Is(err, strconv.ErrRange) {
|
||||||
|
return 0, fmt.Errorf("%q is past the integer range: %w", s, err)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("%q is not an integer", s)
|
return 0, fmt.Errorf("%q is not an integer", s)
|
||||||
}
|
}
|
||||||
@@ -235,6 +239,9 @@ func plainInt(s string) (int, error) {
|
|||||||
|
|
||||||
func posIntArg(_ map[string]node, a []string) error {
|
func posIntArg(_ map[string]node, a []string) error {
|
||||||
n, err := plainInt(a[0])
|
n, err := plainInt(a[0])
|
||||||
|
if errors.Is(err, strconv.ErrRange) {
|
||||||
|
return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("count %w", err)
|
return fmt.Errorf("count %w", err)
|
||||||
}
|
}
|
||||||
@@ -248,10 +255,13 @@ 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 := plainInt(a[0])
|
lo, err := plainInt(a[0])
|
||||||
hi, e2 := plainInt(a[1])
|
if err != nil {
|
||||||
if e1 != nil || e2 != nil {
|
return fmt.Errorf("int(min,max): min %w", err)
|
||||||
return fmt.Errorf("int(min,max) needs plain integers, got %q,%q", a[0], a[1])
|
}
|
||||||
|
hi, err := plainInt(a[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("int(min,max): max %w", err)
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -268,9 +278,12 @@ 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 := plainInt(a[2])
|
if e1 != nil || e2 != nil {
|
||||||
if e1 != nil || e2 != nil || e3 != nil {
|
return fmt.Errorf("float(min,max,dp) needs numeric bounds, got %q,%q", a[0], a[1])
|
||||||
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])
|
}
|
||||||
|
dp, err := plainInt(a[2])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("float(min,max,dp): decimals %w", err)
|
||||||
}
|
}
|
||||||
if math.IsNaN(lo) || math.IsNaN(hi) || math.IsInf(lo, 0) || math.IsInf(hi, 0) {
|
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])
|
return fmt.Errorf("float(min,max,dp) needs finite bounds, got %q,%q", a[0], a[1])
|
||||||
|
|||||||
@@ -82,8 +82,12 @@ func checkCalc(fields map[string]node, args []string) error {
|
|||||||
return fmt.Errorf("calc(%q) divides by %s, which is always zero", args[0], divisor)
|
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 := plainInt(args[1]); err != nil || dp < 0 || dp > maxDecimals {
|
dp, err := plainInt(args[1])
|
||||||
return fmt.Errorf("calc decimals %q must be a plain integer in 0..%d", args[1], maxDecimals)
|
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
|
return nil
|
||||||
|
|||||||
@@ -215,8 +215,9 @@ func (in invocation) options() []fejkdata.Option {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// write streams the path's renders to w, repeat of them joined by the separator
|
// write streams the path's renders to w, repeat of them joined by the separator
|
||||||
// and ended by a newline. A path that renders once renders every time, so the only
|
// and ended by a newline. A path that renders once renders every time, so a Fake
|
||||||
// failure comes before anything is written.
|
// failure comes before anything is written; a write failure surfaces from Flush,
|
||||||
|
// bufio keeping the first one.
|
||||||
func (in invocation) write(f *fejkdata.Generator, w io.Writer) error {
|
func (in invocation) write(f *fejkdata.Generator, w io.Writer) error {
|
||||||
out := bufio.NewWriter(w)
|
out := bufio.NewWriter(w)
|
||||||
for i := 0; i < in.repeat; i++ {
|
for i := 0; i < in.repeat; i++ {
|
||||||
|
|||||||
+5
-2
@@ -28,9 +28,12 @@ import (
|
|||||||
//go:embed data
|
//go:embed data
|
||||||
var shippedFS embed.FS
|
var shippedFS embed.FS
|
||||||
|
|
||||||
// MaxRepeat caps a repeat, and the renders nested repeats multiply to along any path.
|
// MaxRepeat caps a repeat, and the renders nested repeats multiply to along any
|
||||||
|
// path; the CLI's --repeat shares it.
|
||||||
const MaxRepeat = 1 << 20
|
const MaxRepeat = 1 << 20
|
||||||
|
|
||||||
|
var _ [^uint(0)>>63 - 1]struct{} // 64-bit only, per the README's Decisions
|
||||||
|
|
||||||
// ErrNoData is returned by New when no source is loaded at all.
|
// ErrNoData is returned by New when no source is loaded at all.
|
||||||
var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithDataPath or WithDataFS")
|
var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithDataPath or WithDataFS")
|
||||||
|
|
||||||
@@ -207,7 +210,7 @@ func join(prefix, name string) string {
|
|||||||
return prefix + "." + name
|
return prefix + "." + name
|
||||||
}
|
}
|
||||||
|
|
||||||
// randomBytes seeds an unseeded generator; a failure is an error, never a fixed seed.
|
// randomBytes seeds an unseeded generator.
|
||||||
var randomBytes = crand.Read
|
var randomBytes = crand.Read
|
||||||
|
|
||||||
func newRand(seed uint64, seeded bool) (*session, error) {
|
func newRand(seed uint64, seeded bool) (*session, error) {
|
||||||
|
|||||||
@@ -181,8 +181,8 @@ func checkRepeatReach(root map[string]node) error {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
return walkNodes(root, func(path string, n node) error {
|
return walkNodes(root, func(path string, n node) error {
|
||||||
if t, ok := n.(*template); ok && t.repeat > 1 && of(n) > maxLen {
|
if t, ok := n.(*template); ok && t.repeat > 1 && of(n) > MaxRepeat {
|
||||||
return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, of(n), maxLen)
|
return fmt.Errorf("%s: repeat %d multiplies to %d renders along one path, above the maximum %d", path, t.repeat, of(n), MaxRepeat)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -288,8 +288,8 @@ func repeatOf(m map[string]any) (int, error) {
|
|||||||
if r == 1 {
|
if r == 1 {
|
||||||
return 0, fmt.Errorf("repeat 1 is the default, so it has no effect; drop it")
|
return 0, fmt.Errorf("repeat 1 is the default, so it has no effect; drop it")
|
||||||
}
|
}
|
||||||
if r > maxLen { // caps the renders one repeat asks for; checkRepeatReach bounds what nested ones multiply to
|
if r > MaxRepeat { // caps the renders one repeat asks for; checkRepeatReach bounds what nested ones multiply to
|
||||||
return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, maxLen)
|
return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, MaxRepeat)
|
||||||
}
|
}
|
||||||
return int(r), nil
|
return int(r), nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user