Load each spelling that only restated a shorter one, as that shorter form
This commit is contained in:
@@ -28,8 +28,6 @@ type builtin struct {
|
||||
// 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.
|
||||
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.
|
||||
@@ -79,9 +77,6 @@ func Operands(name string, args []string) []string {
|
||||
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]
|
||||
|
||||
@@ -2,11 +2,9 @@ package builtinfunc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larvit/fejkdata/internal/grammar"
|
||||
"github.com/larvit/fejkdata/internal/invariant"
|
||||
)
|
||||
|
||||
@@ -23,17 +21,6 @@ func TestRegistryShapes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoRefOperandsMatchesTheOperandGrammar(t *testing.T) {
|
||||
for name, b := range builtins {
|
||||
if b.operands == nil {
|
||||
continue
|
||||
}
|
||||
if takesRef := slices.ContainsFunc(b.operands([]string{"/x"}), grammar.IsRef); b.noRefOperands == takesRef {
|
||||
t.Errorf("%s: noRefOperands = %v, yet a reference /x read as an operand: %v", name, b.noRefOperands, takesRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckNamesWhatACallGetsWrong(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
|
||||
@@ -66,7 +66,7 @@ var builtins = withTransforms(map[string]builtin{
|
||||
}},
|
||||
"date": {arity: -1, checkArgs: dateArgs, prep: datePrep},
|
||||
"time": {arity: -1, checkArgs: timeArg, prep: timePrep},
|
||||
CalcName: {arity: -1, checkArgs: checkCalc, prep: calcPrep, operands: calcOperands, noRefOperands: true},
|
||||
CalcName: {arity: -1, checkArgs: checkCalc, prep: calcPrep, operands: calcOperands},
|
||||
// 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.
|
||||
@@ -148,8 +148,7 @@ func randChars(r rng, n int, alphabet string) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// plainInt parses an integer arg written the one way: no sign, no leading zero.
|
||||
func plainInt(s string) (int, error) {
|
||||
func intArg(s string) (int, error) {
|
||||
n, err := strconv.Atoi(s)
|
||||
if errors.Is(err, strconv.ErrRange) {
|
||||
return 0, fmt.Errorf("%q is past the integer range: %w", s, err)
|
||||
@@ -157,13 +156,10 @@ func plainInt(s string) (int, error) {
|
||||
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(a []string) error {
|
||||
n, err := plainInt(a[0])
|
||||
n, err := intArg(a[0])
|
||||
if errors.Is(err, strconv.ErrRange) {
|
||||
return fmt.Errorf("count %q exceeds the maximum %d", a[0], maxLen)
|
||||
}
|
||||
@@ -179,11 +175,11 @@ func posIntArg(a []string) error {
|
||||
return nil
|
||||
}
|
||||
func intRangeArgs(a []string) error {
|
||||
lo, err := plainInt(a[0])
|
||||
lo, err := intArg(a[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("int(min,max): min %w", err)
|
||||
}
|
||||
hi, err := plainInt(a[1])
|
||||
hi, err := intArg(a[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("int(min,max): max %w", err)
|
||||
}
|
||||
@@ -204,7 +200,7 @@ func floatArgs(a []string) error {
|
||||
if e1 != nil || e2 != nil {
|
||||
return fmt.Errorf("float(min,max,dp) needs numeric bounds, got %q,%q", a[0], a[1])
|
||||
}
|
||||
dp, err := plainInt(a[2])
|
||||
dp, err := intArg(a[2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("float(min,max,dp): decimals %w", err)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func checkCalc(args []string) error {
|
||||
return fmt.Errorf("calc(%q): %w", args[0], err)
|
||||
}
|
||||
if len(args) == 2 {
|
||||
dp, err := plainInt(args[1])
|
||||
dp, err := intArg(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("calc decimals %w", err)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@ import (
|
||||
|
||||
func IsTemplate(arg string) (bool, error) {
|
||||
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
||||
if path, lone := loneReference(arg); lone {
|
||||
return false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
if i := strings.IndexAny(arg, `}"`); i >= 0 {
|
||||
@@ -19,60 +16,13 @@ func IsTemplate(arg string) (bool, error) {
|
||||
if strings.HasPrefix(strings.TrimSpace(arg), "[") {
|
||||
return false, fmt.Errorf(`%q starts with "[" and is not valid JSON, so it names no template; a path starts with a name`, arg)
|
||||
}
|
||||
if _, err := SplitPath(arg); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
|
||||
return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg))
|
||||
}
|
||||
return false, nil
|
||||
_, err := SplitPath(CallerPath(arg))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// CallerPath is a path as a caller passed it, without the slashes it may start with.
|
||||
func CallerPath(path string) string { return strings.TrimLeft(path, "/") }
|
||||
|
||||
func isJSONStart(arg string) bool {
|
||||
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
||||
}
|
||||
|
||||
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
|
||||
func pathAdvice(path, refusal string) error {
|
||||
if err := CheckPathIdentifiers(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%s; write %s", refusal, path)
|
||||
}
|
||||
|
||||
// loneReference is the path a template spells when the value it holds — a format string, a
|
||||
// JSON string, or an object holding only a format — is one reference token and nothing else.
|
||||
func loneReference(arg string) (string, bool) {
|
||||
var raw any
|
||||
if json.Unmarshal([]byte(arg), &raw) != nil {
|
||||
raw = arg
|
||||
}
|
||||
if m, isObject := raw.(map[string]any); isObject && len(m) == 1 {
|
||||
raw = m["format"]
|
||||
}
|
||||
format, isString := raw.(string)
|
||||
if !isString {
|
||||
return "", false
|
||||
}
|
||||
toks, err := ParseFormat(format)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
body, lone := loneRef(toks)
|
||||
if !lone {
|
||||
return "", false
|
||||
}
|
||||
if strings.HasPrefix(body, "/") {
|
||||
body = "/" + strings.TrimLeft(body, "/")
|
||||
}
|
||||
_, path, err := RefShape(body)
|
||||
return path, err == nil
|
||||
}
|
||||
|
||||
// loneRef is the reference a format of one reference token and nothing else reads.
|
||||
func loneRef(toks []Token) (string, bool) {
|
||||
if len(toks) != 1 || toks[0].Kind != PathRead || len(toks[0].Arms) != 1 || !IsRef(toks[0].Arms[0]) {
|
||||
return "", false
|
||||
}
|
||||
return toks[0].Arms[0], true
|
||||
}
|
||||
|
||||
@@ -192,19 +192,5 @@ func Literal(text string) Facts {
|
||||
if text != "true" && text != "false" {
|
||||
v.Not[datatype.Boolean] = fmt.Sprintf("%q is not a boolean", text)
|
||||
}
|
||||
return signedZero(text, v)
|
||||
}
|
||||
|
||||
// signedZero refuses a zero written with a sign as a typed value, naming it unsigned.
|
||||
func signedZero(text string, v Facts) Facts {
|
||||
mantissa, _, _ := strings.Cut(strings.ToLower(text), "e")
|
||||
if !strings.HasPrefix(text, "-") || v.NotOperand != "" || strings.Trim(mantissa, "-0.") != "" {
|
||||
return v
|
||||
}
|
||||
for _, d := range []datatype.DataType{datatype.Integer, datatype.Number} {
|
||||
if v.Not[d] == "" {
|
||||
v.Not[d] = fmt.Sprintf("%q is zero written with a sign; write %q", text, text[1:])
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (t *Table[O]) parseRows(data string) error {
|
||||
t.col[name] = i
|
||||
}
|
||||
if header == data {
|
||||
return fmt.Errorf("has no rows below its header; a table is at least two rows")
|
||||
return fmt.Errorf("has no rows below its header; a table is at least one row")
|
||||
}
|
||||
m := len(t.header)
|
||||
t.cells = make([]string, 0, m*(strings.Count(rest, "\n")+1))
|
||||
@@ -118,9 +118,6 @@ func (t *Table[O]) parseRows(data string) error {
|
||||
}
|
||||
rest = more
|
||||
}
|
||||
if t.Len() < 2 {
|
||||
return fmt.Errorf("has one row, which is a template; write it as one")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -141,13 +138,10 @@ func (t *Table[O]) appendRow(row string, line int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// bindOptions resolves each option to its column, refuses a name option naming the key column, and
|
||||
// proves what the options claim of the cells: a key is unique, a weight a positive number.
|
||||
// bindOptions resolves each option to its column, and proves what the options claim of the cells: a
|
||||
// key is unique, a weight a positive number.
|
||||
func (t *Table[O]) bindOptions() error {
|
||||
o := t.options
|
||||
if o.Key != "" && o.Key == o.Name {
|
||||
return fmt.Errorf("name names the key column %q, which a selector already reads; drop it", o.Name)
|
||||
}
|
||||
for _, opt := range []struct {
|
||||
name, value string
|
||||
into *int
|
||||
|
||||
Reference in New Issue
Block a user