diff --git a/AGENTS.md b/AGENTS.md index 6fc879a..5c0a286 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,7 +121,7 @@ Do not weaken them. cmd/simplexcalc/ main(), a single call into internal/cli internal/api/ the HTTP API: its credential, headers and endpoints internal/bot/ startup, address setup, and the reply to a message -internal/calc/ the arithmetic: go/parser and go/constant +internal/calc/ the arithmetic: its own parser, and go/constant internal/cli/ cobra command tree: run and version internal/config/ viper-backed configuration; the abort-on-garbage rule internal/logger/ log/slog, JSON always diff --git a/README.md b/README.md index 7e30288..e8eac27 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,10 @@ SimpleX Chat network: it accepts every contact request and answers arithmetic such as `2 + 2` with the result. Send it `2 + 2` and it replies `4`; send `5 * 5/2` and it replies -`12.5`. It understands decimal numbers, `+ - * /`, unary minus and -parentheses, and computes exactly, so `0.1 + 0.2` is `0.3`. Anything -else gets a short explanation instead of a result. +`12.5`. It understands decimal numbers, `+ - * /`, powers written `2^10` +or `2**10`, remainders written `7 % 3`, signs and parentheses, and +computes exactly, so `0.1 + 0.2` is `0.3`. Anything else gets a short +explanation instead of a result. ## Getting Started @@ -232,16 +233,33 @@ container. - **Replies**: for each text message a contact sends in a direct chat, the bot sends back the result, as a reply quoting the message. Group messages, files and the bot's own messages are ignored. -- **Arithmetic** (`internal/calc`): the text is parsed as a Go - expression with `go/parser`, and only numbers, `+ - * /`, unary signs - and parentheses are evaluated; anything else in the syntax tree is - refused. `go/constant` computes with exact rationals. Numbers are read - as decimal, so `010` is ten. Input over 256 bytes is refused, so a +- **Arithmetic** (`internal/calc`): a small parser of its own reads + numbers, `+ - * / % ^`, signs and parentheses, and refuses anything + else. `go/constant` computes with exact rationals. `^`, also written + `**`, is a power: it binds tighter than `*`, `/`, `%` and a sign on + its left, and groups to the right, so `2^3^2` is `512`, `-2^2` is + `-4`, `(-2)^2` is `4` and `2^-1` is `0.5`. `%` is the remainder and + ranks with `*` and `/`; its result takes the sign of the divisor, as + in Python, so `7 % 3` is `1`, `-7 % 3` is `2` and `7.5 % 2` is `1.5`. + A power with a whole exponent is exact, so `0.1^2` is `0.01` and + `2^-1400 * 2^1400` is `1`, unless `go/constant` could hold the result + only rounded; that power, and one with a fractional exponent, is + computed as a double, so `2^0.5` is `1.4142135623730951`. A negative + number to a fractional power is refused, as having no real result. + Numbers are read as decimal, so `010` is ten. Input over 256 bytes is + refused, exact powers are capped, and every number is held as a + fraction, whole numbers too, under the 4096-bit limit below, so a message cannot make the bot do unbounded work. Whole numbers below 1021 are written exactly; other results in the shortest form that reads back as the same double, in exponent notation from - 1021 up and below 10-6. A result beyond the - range of a double is refused as too large. + 1021 up and below 10-6. Refused as too large or + too small: any number whose numerator or denominator reaches 4096 + bits, wherever it appears, as `go/constant` rounds a fraction that + grows that large (`1e-1300 + 1`); a power computed as a double whose + base or result is outside the normal range of a double, about 2.2e-308 + to 1.8e308 in magnitude, where a double keeps all its digits + (`1e-400^0.5`); and a result other than zero outside that range, as it + is written through a double (`1e400`, `2^-1400`). - **Failure is an exit.** If the chat client exits or the connection to it drops, the bot exits with an error and the container's restart policy starts both again. `SIGTERM` stops the bot, which stops the diff --git a/docs/TODO.md b/docs/TODO.md index bd8496f..3b09904 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -30,6 +30,9 @@ with no deprecation warning. - 2026-09-29 Added the HTTP API in `internal/api`: the server on `PORT`, the bearer credential from `API_TOKEN_FILE`, security headers and limits, and `GET /api/v1/chats` +- 2026-09-28 Powers (`^`, also written `**`) and remainders (`%`) in + `internal/calc`, which now reads expressions with a parser of its own + in place of `go/parser` - 2026-09-28 Moved the command tree and the `run` and `version` commands from `cmd/simplexcalc/` into `internal/cli`; `cmd/simplexcalc/main.go` is now a single call to `cli.Main` diff --git a/internal/bot/bot.go b/internal/bot/bot.go index e0d3738..ce7924e 100644 --- a/internal/bot/bot.go +++ b/internal/bot/bot.go @@ -25,7 +25,7 @@ import ( const DisplayName = "calc" // Welcome is sent to everyone whose contact request the bot accepts. -const Welcome = "Send me arithmetic, such as 2 + 2 or 5 * 5/2, " + +const Welcome = "Send me arithmetic, such as 2 + 2, 5 * 5/2, 2^10 or 7 % 3, " + "and I will reply with the result." // ChatPort is where the chat client serves its API, on localhost inside @@ -275,10 +275,12 @@ func Reply(text string) string { calc.MaxInputLength) case errors.Is(err, calc.ErrDivisionByZero): return "I cannot divide by zero." - case errors.Is(err, calc.ErrTooLarge): - return "The result is too large for me." + case errors.Is(err, calc.ErrOutOfRange): + return "That needs a number too large or too small for me." + case errors.Is(err, calc.ErrNoRealResult): + return "A negative number to a fractional power has no real result." default: - return "I only understand arithmetic: numbers, + - * / and " + - "parentheses, such as 5 * 5/2." + return "I only understand arithmetic: numbers, + - * /, ^ for a power, " + + "% for a remainder, and parentheses, such as 5 * 5/2 or 2^10." } } diff --git a/internal/bot/bot_test.go b/internal/bot/bot_test.go index 3f7971e..3bbb661 100644 --- a/internal/bot/bot_test.go +++ b/internal/bot/bot_test.go @@ -16,6 +16,8 @@ func TestReply(t *testing.T) { for in, want := range map[string]string{ "2 + 2": "4", "5 * 5/2": "12.5", + "2^10": "1024", + "7 % 3": "1", } { if got := bot.Reply(in); got != want { t.Errorf("Reply(%q) = %q, want %q", in, got, want) @@ -23,9 +25,11 @@ func TestReply(t *testing.T) { } for in, want := range map[string]string{ - "hello": "I only understand arithmetic", - "1 / 0": "I cannot divide by zero.", - "1e400": "The result is too large for me.", + "hello": "I only understand arithmetic", + "1 / 0": "I cannot divide by zero.", + "1e400": "That needs a number too large or too small for me.", + "1e-1300": "That needs a number too large or too small for me.", + "(-8)^0.5": "A negative number to a fractional power has no real", strings.Repeat("1+", calc.MaxInputLength) + "1": "That is too long for me", } { if got := bot.Reply(in); !strings.HasPrefix(got, want) { diff --git a/internal/calc/calc.go b/internal/calc/calc.go index 2f2bd65..606a1f7 100644 --- a/internal/calc/calc.go +++ b/internal/calc/calc.go @@ -1,29 +1,45 @@ // Package calc evaluates the arithmetic people send the bot: decimal -// numbers, + - * /, unary minus and parentheses. +// numbers, + - * / % ^, signs and parentheses. // -// The expression is parsed by go/parser and computed by go/constant, -// which does exact rational arithmetic: 5 * 5/2 is exactly 12.5, and -// 0.1 + 0.2 is exactly 0.3, so a result carries no binary floating -// point noise until the moment it is formatted. +// The expression is read by a small parser of its own, because Go's +// grammar has no power operator (^ is XOR there), and computed by +// go/constant, which does exact rational arithmetic: 5 * 5/2 is exactly +// 12.5, and 0.1 + 0.2 is exactly 0.3, so a result carries no binary +// floating point noise until the moment it is formatted. A power is the +// exception: one with a fractional exponent, or whose result go/constant +// cannot hold exactly, is computed in float64. package calc import ( "errors" - "go/ast" "go/constant" - "go/parser" "go/token" "math" + "math/big" "regexp" "strconv" "strings" ) -// MaxInputLength caps an expression, in bytes, so a message cannot make -// the bot do unbounded work. Every operation's cost grows with the size -// of its operands, and the operands can only grow with the input. +// MaxInputLength caps an expression, in bytes. With maxExactExponent, +// which caps a power computed exactly, and bitLimit, which caps every +// number, it keeps a message from making the bot do unbounded work. const MaxInputLength = 256 +// bitLimit caps the numerator and denominator of every number: see +// exact. +const bitLimit = 4096 + +// maxExactExponent is the largest exponent, either way, of a power +// computed exactly. Past it, x^n has a numerator or denominator of more +// than 4096 bits, which go/constant holds only rounded, unless x is 0 or +// 1, and float64 computes those exactly. +const maxExactExponent = 4096 + +// smallestNormal is the smallest positive normal double, about 2.2e-308. +// Below it a double keeps fewer digits, down to one. +const smallestNormal = 0x1p-1022 + // Results of magnitude plainUpper or more are written in exponent form // (1e+21 rather than twenty-two digits), and so are fractions smaller // than plainLower (1e-07 rather than 0.0000001). @@ -32,20 +48,37 @@ const ( plainLower = 1e-6 ) +// The precedence of the binary operators: the higher, the tighter the +// operator binds. +const ( + sumPrecedence = iota + 1 + productPrecedence + powerPrecedence +) + // Errors returned by Evaluate. The bot turns each into a reply. var ( ErrTooLong = errors.New("expression too long") ErrNotArithmetic = errors.New("not an arithmetic expression") ErrDivisionByZero = errors.New("division by zero") - ErrTooLarge = errors.New("result too large") + ErrOutOfRange = errors.New("number too large or too small") + ErrNoRealResult = errors.New("no real result") ) -// decimalLiteral is the only number syntax accepted. Go's own literal -// syntax is wider, and parts of it are traps for someone typing -// arithmetic: 010 is octal 8, and 0x10, 1_000 and 1i are not what a -// calculator user means by a number. -var decimalLiteral = regexp.MustCompile( - `^([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?$`, +// decimal is the only number syntax accepted. Go's own literal syntax is +// wider, and parts of it are traps for someone typing arithmetic: 010 is +// octal 8, and 0x10, 1_000 and 1i are not what a calculator user means +// by a number. Here the x, _ or i matches no token and is refused. +const decimal = `([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?` + +var ( + // nextToken matches the token at the start of the input, after any + // whitespace: an operator, a parenthesis or a number. ** comes + // before * so that it is read as one token. + nextToken = regexp.MustCompile(`^\s*(\*\*|[-+*/%^()]|` + decimal + `)`) + + // decimalLiteral matches a token that is a number. + decimalLiteral = regexp.MustCompile(`^` + decimal + `$`) ) // Evaluate computes an arithmetic expression and returns its result as @@ -57,114 +90,384 @@ func Evaluate(input string) (string, error) { return "", ErrTooLong } - if s == "" { - return "", ErrNotArithmetic - } - - expr, err := parser.ParseExpr(s) - if err != nil { - return "", ErrNotArithmetic - } - - v, err := eval(expr) + tokens, err := tokenize(s) if err != nil { return "", err } + p := parser{tokens: tokens} + + v, err := p.expr(sumPrecedence) + if err != nil { + return "", err + } + + if p.next() != "" { + return "", ErrNotArithmetic + } + return format(v) } -// eval walks the syntax tree, allowing only the node types and -// operators of arithmetic. Anything else — identifiers, calls, strings, -// shifts, comparisons — is refused, not evaluated. -func eval(e ast.Expr) (constant.Value, error) { - switch n := e.(type) { - case *ast.BasicLit: - return literal(n) - case *ast.ParenExpr: - return eval(n.X) - case *ast.UnaryExpr: - if n.Op != token.ADD && n.Op != token.SUB { +// tokenize splits an expression into operators, parentheses and +// numbers, and refuses anything else. ** is returned as ^. +func tokenize(s string) ([]string, error) { + var tokens []string + + for strings.TrimSpace(s) != "" { + m := nextToken.FindStringSubmatch(s) + if m == nil { return nil, ErrNotArithmetic } - x, err := eval(n.X) + tok := m[1] + if tok == "**" { + tok = "^" + } + + tokens = append(tokens, tok) + s = s[len(m[0]):] + } + + return tokens, nil +} + +// parser computes an expression as it reads it, by precedence climbing: +// expr reads operands joined by operators of at least a given +// precedence, and hands the right operand of each to a deeper call that +// takes only the operators that bind tighter, so those are applied +// first. +type parser struct { + tokens []string +} + +// next removes and returns the next token, or "" at the end. +func (p *parser) next() string { + tok := p.peek() + if tok != "" { + p.tokens = p.tokens[1:] + } + + return tok +} + +// peek returns the next token, or "" at the end, and leaves it unread. +func (p *parser) peek() string { + if len(p.tokens) == 0 { + return "" + } + + return p.tokens[0] +} + +// expr reads and computes an expression whose binary operators all have +// at least minPrecedence. Operators of equal precedence group to the +// left, 8/2/2 is (8/2)/2, except ^, which groups to the right: 2^3^2 is +// 2^(3^2). +func (p *parser) expr(minPrecedence int) (constant.Value, error) { + x, err := p.operand() + if err != nil { + return nil, err + } + + for { + op := p.peek() + + prec := precedence(op) + if prec < minPrecedence { + return x, nil + } + + p.next() + + rightPrecedence := prec + 1 + if op == "^" { + rightPrecedence = prec + } + + y, err := p.expr(rightPrecedence) if err != nil { return nil, err } - return constant.UnaryOp(n.Op, x, 0), nil - case *ast.BinaryExpr: - return binary(n) - default: - return nil, ErrNotArithmetic + x, err = apply(x, op, y) + if err != nil { + return nil, err + } } } -func binary(n *ast.BinaryExpr) (constant.Value, error) { - switch n.Op { //nolint:exhaustive // every other operator is refused. - case token.ADD, token.SUB, token.MUL, token.QUO: +// operand reads a number, an expression in parentheses, or a sign and +// its operand. A sign binds more loosely than a power that follows it, +// so -2^2 is -(2^2), and 2^-1 is 2^(-1). +func (p *parser) operand() (constant.Value, error) { + switch tok := p.next(); tok { + case "+", "-": + x, err := p.expr(powerPrecedence) + if err != nil { + return nil, err + } + + if tok == "-" { + x = constant.UnaryOp(token.SUB, x, 0) + } + + return x, nil + case "(": + x, err := p.expr(sumPrecedence) + if err != nil { + return nil, err + } + + if p.next() != ")" { + return nil, ErrNotArithmetic + } + + return x, nil default: + return number(tok) + } +} + +// precedence returns the precedence of a binary operator, and 0 for any +// other token, which ends an expression. +func precedence(op string) int { + switch op { + case "+", "-": + return sumPrecedence + case "*", "/", "%": + return productPrecedence + case "^": + return powerPrecedence + default: + return 0 + } +} + +func number(tok string) (constant.Value, error) { + if !decimalLiteral.MatchString(tok) { return nil, ErrNotArithmetic } - x, err := eval(n.X) + // Read as FLOAT, which makes every literal decimal and a fraction + // (see exact): as INT, a leading zero would make it octal. + v := constant.MakeFromLiteral(tok, token.FLOAT, 0) + + // A literal such as 1e1300 or 1e-1233 is past bitLimit: see exact. + if !exact(v) { + return nil, ErrOutOfRange + } + + // One too small even to be held rounded, such as 1e-999999999, is + // read as 0. + mantissa, _, _ := strings.Cut(strings.ToLower(tok), "e") + if constant.Sign(v) == 0 && strings.ContainsAny(mantissa, "123456789") { + return nil, ErrOutOfRange + } + + return v, nil +} + +// apply computes x op y. +func apply(x constant.Value, op string, y constant.Value) (constant.Value, error) { + var ( + v constant.Value + err error + ) + + switch op { + case "+": + v = constant.BinaryOp(x, token.ADD, y) + case "-": + v = constant.BinaryOp(x, token.SUB, y) + case "*": + v = constant.BinaryOp(x, token.MUL, y) + case "/": + v, err = divide(x, y) + case "%": + v, err = modulo(x, y) + case "^": + v, err = power(x, y) + default: + err = ErrNotArithmetic + } + if err != nil { return nil, err } - y, err := eval(n.Y) - if err != nil { - return nil, err + if !exact(v) { + return nil, ErrOutOfRange } + return v, nil +} + +func divide(x, y constant.Value) (constant.Value, error) { // constant.BinaryOp panics on a zero divisor. - if n.Op == token.QUO && constant.Sign(y) == 0 { + if constant.Sign(y) == 0 { return nil, ErrDivisionByZero } // token.QUO divides exactly, integers included: 25/2 is 12.5. - v := constant.BinaryOp(x, n.Op, y) + return constant.BinaryOp(x, token.QUO, y), nil +} - // go/constant represents an overflow to infinity as Unknown. - if v.Kind() == constant.Unknown { - return nil, ErrTooLarge +// modulo computes x % y, whose result takes the sign of y, as in Python: +// -7 % 3 is 2 and 7 % -3 is -2. It is exact for decimals too: 7.5 % 2 +// is 1.5. +func modulo(x, y constant.Value) (constant.Value, error) { + q, err := divide(x, y) + if err != nil { + return nil, err + } + + // The fractional part of a rounded quotient, and so the remainder, + // would be wrong. + if !exact(q) { + return nil, ErrOutOfRange + } + + // x % y is y times the fractional part of x/y, which is at least 0 + // and less than 1, so the result has the sign of y. It is not + // computed as x minus y times the whole part of x/y: that product + // can be too large to hold exactly when the remainder is not. + // + // For x/y = n/d the fractional part is (n mod d)/d, exact because d + // is. token.REM truncates, leaving the sign of n; adding d brings a + // negative one into range. + n, d := constant.Num(q), constant.Denom(q) + + m := constant.BinaryOp(n, token.REM, d) + if constant.Sign(m) < 0 { + m = constant.BinaryOp(m, token.ADD, d) + } + + return constant.BinaryOp(y, token.MUL, constant.BinaryOp(m, token.QUO, d)), nil +} + +// power computes x^y. A negative x needs a whole y, and its sign is +// applied here: math.Pow would take it from the parity of y's float64 +// value, and every float64 from 2^53 up is even. +func power(x, y constant.Value) (constant.Value, error) { + // n is y if y is a whole number, and Unknown otherwise. + n := constant.ToInt(y) + + switch { + case constant.Sign(x) == 0 && constant.Sign(y) < 0: + return nil, ErrDivisionByZero + case constant.Sign(x) >= 0: + return nonNegativePower(x, y, n) + case n.Kind() != constant.Int: + return nil, ErrNoRealResult + } + + // x is negative and n whole: x^n is (-x)^n, negated if n is odd. + v, err := nonNegativePower(constant.UnaryOp(token.SUB, x, 0), y, n) + if err != nil { + return nil, err + } + + odd := constant.BinaryOp(n, token.AND, constant.MakeInt64(1)) + if constant.Sign(odd) != 0 { + v = constant.UnaryOp(token.SUB, v, 0) } return v, nil } -func literal(n *ast.BasicLit) (constant.Value, error) { - if n.Kind != token.INT && n.Kind != token.FLOAT { - return nil, ErrNotArithmetic +// nonNegativePower computes x^y for x of at least zero, and y not below +// zero if x is zero: exactly if y is a whole number n and go/constant +// holds the result exactly, otherwise in float64. +func nonNegativePower(x, y, n constant.Value) (constant.Value, error) { + e, ok := constant.Int64Val(n) + if ok && -maxExactExponent <= e && e <= maxExactExponent { + v := exactPower(x, e) + if exact(v) { + return v, nil + } } - if !decimalLiteral.MatchString(n.Value) { - return nil, ErrNotArithmetic + // y is above zero here if x is zero. + if constant.Sign(x) == 0 { + return x, nil } - // Read as FLOAT whatever the token says, which makes every literal - // decimal: as INT, a leading zero would make it octal. - v := constant.MakeFromLiteral(n.Value, token.FLOAT, 0) + xf, _ := constant.Float64Val(x) + yf, _ := constant.Float64Val(y) + f := math.Pow(xf, yf) - // The syntax was checked above, so Unknown here means the exponent - // overflowed. - if v.Kind() == constant.Unknown { - return nil, ErrTooLarge + // Neither x nor x^y is zero. If either is not a normal double, it + // has lost digits, or all of them. + if !normal(xf) || !normal(f) { + return nil, ErrOutOfRange } - return v, nil + return constant.MakeFloat64(f), nil +} + +// exactPower computes x^e by repeated squaring. x is not zero if e is +// negative. It starts from 1 as a fraction, a Float to go/constant, so +// that x^0 is a fraction like every other number (see exact). Each +// step's numbers stay small: go/constant holds one whose numerator or +// denominator reaches 4096 bits as a 512-bit float. +func exactPower(x constant.Value, e int64) constant.Value { + one := constant.MakeFloat64(1) + result := one + + for n := max(e, -e); n > 0; n >>= 1 { + if n&1 == 1 { + result = constant.BinaryOp(result, token.MUL, x) + } + + x = constant.BinaryOp(x, token.MUL, x) + } + + if e < 0 { + result = constant.BinaryOp(one, token.QUO, result) + } + + return result +} + +// exact reports whether v is a fraction whose numerator and denominator +// are both below bitLimit bits, as every number here must be, so that +// each step of arithmetic stays small. go/constant never rounds an +// integer, however large, so every number is made a fraction: literals +// are read as FLOAT, and a power starts from the fraction 1. It rounds +// a fraction that grows past the limit, to a 512-bit float and past +// that float's range to Unknown, but not one it reads from a literal, +// such as 1e-1233, so the limit is checked here. +// +// A number that is not exact is refused wherever it appears: a sum can +// lose the answer entirely (7^1000*7^1000 + 5 - 7^1000*7^1000 would be +// 0), and a remainder, or whether an exponent is whole or odd, cannot be +// read from one. +func exact(v constant.Value) bool { + r, ok := constant.Val(v).(*big.Rat) + + return ok && r.Num().BitLen() < bitLimit && r.Denom().BitLen() < bitLimit +} + +// normal reports whether f is a normal double, finite and at least +// smallestNormal in magnitude: a number other than zero keeps all of a +// double's digits only as one. +func normal(f float64) bool { + abs := math.Abs(f) + + return abs >= smallestNormal && abs <= math.MaxFloat64 } // format writes a result for a person to read. A whole number of // ordinary size is written exactly, digit for digit; anything else goes // through float64, whose shortest round-trip form is free of the noise // (0.30000000000000004) that printing a binary fraction to a fixed -// precision produces. +// precision produces. A result that is not zero must therefore be a +// normal double: 2^-1074 would be written 5e-324. func format(v constant.Value) (string, error) { f, _ := constant.Float64Val(v) - if math.IsInf(f, 0) || math.IsNaN(f) { - return "", ErrTooLarge + if constant.Sign(v) != 0 && !normal(f) { + return "", ErrOutOfRange } abs := math.Abs(f) diff --git a/internal/calc/calc_test.go b/internal/calc/calc_test.go index 9b91d97..21e40ee 100644 --- a/internal/calc/calc_test.go +++ b/internal/calc/calc_test.go @@ -4,6 +4,7 @@ import ( "errors" "strings" "testing" + "time" "sneak.berlin/go/simplexcalc/internal/calc" ) @@ -13,7 +14,7 @@ import ( func TestEvaluate(t *testing.T) { t.Parallel() - cases := map[string]string{ + expectResults(t, map[string]string{ // The specification's own examples. "2 + 2": "4", "5 * 5/2": "12.5", @@ -51,7 +52,92 @@ func TestEvaluate(t *testing.T) { "1234567.5": "1234567.5", "-1 / 4": "-0.25", "1e300 * 1e8": "1e+308", - } + }) +} + +// TestEvaluatePowers: ^ and ** are one operator, binding tighter than +// * / % and a sign on its left, and grouping to the right. +func TestEvaluatePowers(t *testing.T) { + t.Parallel() + + expectResults(t, map[string]string{ + "2^3": "8", + "2**3": "8", + "2 ** 3 ^ 2": "512", + "2^3^2": "512", + "(2^3)^2": "64", + "-2^2": "-4", + "(-2)^2": "4", + "(-2)^3": "-8", + "(-2)^-3": "-0.125", + "2^-1": "0.5", + "2**-1": "0.5", + "-2^-2": "-0.25", + "2^-3^2": "0.001953125", + "2 * 3^2": "18", + "3^2 * 2": "18", + "2^3 / 2^2": "2", + "1 + 2^3 - 3^2": "0", + "010^2": "100", + "0.1^2": "0.01", + "2^100 - 2^100 + 1": "1", + "2^64": "18446744073709551616", + "0^0": "1", + "0^3": "0", + "1.5^2": "2.25", + "2^0.5": "1.4142135623730951", + "-2^0.5": "-1.4142135623730951", + "4^0.5": "2", + "0^0.5": "0", + "2^1023": "8.98846567431158e+307", + "2^-1022": "2.2250738585072014e-308", + // Past 2^53 a float64 cannot tell odd from even. + "(-1)^(2^53 + 1)": "-1", + "(-1)^(10^30)": "1", + "(-1)^-9223372036854775808": "1", + // Whole powers beyond the range of a double, held exactly. + "2^-1400 * 2^1365 * 2^35": "1", + "0.3^900 * 10^470": "0.25652473503365386", + "2^1500 / 2^1000": "3.273390607896142e+150", + }) +} + +// TestEvaluateModulo: % sits with * and /, left to right, and its result +// takes the sign of the divisor. +func TestEvaluateModulo(t *testing.T) { + t.Parallel() + + expectResults(t, map[string]string{ + "7 % 3": "1", + "-7 % 3": "2", + "7 % -3": "-2", + "-7 % -3": "-1", + "6 % 3": "0", + "-6 % 3": "0", + "7.5 % 2": "1.5", + "0.3 % 0.1": "0", + "-0.3 % 0.2": "0.1", + "10 % 4 * 3": "6", + "2 * 7 % 4": "2", + "1 + 7 % 3": "2", + "2^10 % 7": "2", + "10^400 % 7": "4", + "1e-30 % 1": "1e-30", + "-1e-30 % 1": "1", + "10 / 8 % 1": "0.25", + "(7 % 3)^2": "1", + "7 % (3 ^ 2)": "7", + // Both operands and their quotient are held exactly, but y times + // the whole part of x/y is too large to be. + "(5^860*3^630/7) % (5^860/2^998/2^998)": "0.5179219763783696", + // A whole number made from x^0, just below the 4096-bit limit. + "(3^0 + 3^0 + 3^0)^2583 % 10": "7", + }) +} + +// expectResults checks that each expression evaluates to its result. +func expectResults(t *testing.T, cases map[string]string) { + t.Helper() for in, want := range cases { t.Run(in, func(t *testing.T) { @@ -70,11 +156,11 @@ func TestEvaluate(t *testing.T) { } // TestEvaluateRefuses covers what must be answered with an error rather -// than a number, and never with a panic. +// than a number. func TestEvaluateRefuses(t *testing.T) { t.Parallel() - cases := map[string]error{ + expectErrors(t, map[string]error{ "": calc.ErrNotArithmetic, " ": calc.ErrNotArithmetic, "hello": calc.ErrNotArithmetic, @@ -89,20 +175,91 @@ func TestEvaluateRefuses(t *testing.T) { "2i * 2i": calc.ErrNotArithmetic, "0x10 + 1": calc.ErrNotArithmetic, "1_000 + 1": calc.ErrNotArithmetic, - "7 % 2": calc.ErrNotArithmetic, - "2 ^ 3": calc.ErrNotArithmetic, "1 << 10": calc.ErrNotArithmetic, "1 == 1": calc.ErrNotArithmetic, "!1": calc.ErrNotArithmetic, "func() int { return 1 }()": calc.ErrNotArithmetic, + "(1 + 2": calc.ErrNotArithmetic, + "1 + 2)": calc.ErrNotArithmetic, + "()": calc.ErrNotArithmetic, + "(2)(3)": calc.ErrNotArithmetic, + "2 ^": calc.ErrNotArithmetic, + "^ 2": calc.ErrNotArithmetic, + "2 ^^ 3": calc.ErrNotArithmetic, + "2 *** 3": calc.ErrNotArithmetic, + "2 * * 3": calc.ErrNotArithmetic, + "% 3": calc.ErrNotArithmetic, + "50%": calc.ErrNotArithmetic, + "2 × 3": calc.ErrNotArithmetic, "1 / 0": calc.ErrDivisionByZero, "1 / (2 - 2)": calc.ErrDivisionByZero, "5 / 0.0": calc.ErrDivisionByZero, - "1e400": calc.ErrTooLarge, - "1e300 * 1e300": calc.ErrTooLarge, - "1e999999999 * 1e999999999": calc.ErrTooLarge, - "1 / 1e-400": calc.ErrTooLarge, - } + "7 % 0": calc.ErrDivisionByZero, + "7.5 % (1 - 1)": calc.ErrDivisionByZero, + "0^-1": calc.ErrDivisionByZero, + "0^-0.5": calc.ErrDivisionByZero, + "(-2)^0.5": calc.ErrNoRealResult, + "(-8)^(1/3)": calc.ErrNoRealResult, + "(-1)^-0.5": calc.ErrNoRealResult, + }) +} + +// TestEvaluateOutOfRange: a number is held exactly, or computed in +// float64 as a normal double, and a result is written as a normal +// double. Anything else is refused. +func TestEvaluateOutOfRange(t *testing.T) { + t.Parallel() + + expectErrors(t, map[string]error{ + // Results that are not normal doubles: 2^-1074 would be written + // 5e-324. + "1e400": calc.ErrOutOfRange, + "1e300 * 1e300": calc.ErrOutOfRange, + "1e999999999 * 1e999999999": calc.ErrOutOfRange, + "1 / 1e-400": calc.ErrOutOfRange, + "2^1024": calc.ErrOutOfRange, + "2^5000": calc.ErrOutOfRange, + "(-2)^5001": calc.ErrOutOfRange, + "0.5^-5000": calc.ErrOutOfRange, + "2^-1074": calc.ErrOutOfRange, + "2^-1400": calc.ErrOutOfRange, + "-1e-310": calc.ErrOutOfRange, + // Powers computed in float64 whose base or result is not a + // normal double, and so has lost digits, or all of them. + "2^-1073.5 * 2^1073": calc.ErrOutOfRange, + "1e400^-0.001": calc.ErrOutOfRange, + "1e-400^0.001": calc.ErrOutOfRange, + "1e-310^0.5": calc.ErrOutOfRange, + "(0.5^1100)^4 / (0.5^1100)^4": calc.ErrOutOfRange, + "(1/3)^1e400": calc.ErrOutOfRange, + // go/constant holds numbers of this size rounded. A sum of them + // can lose the answer (this one would be 0), and so can a + // remainder or the sign of -1 to such a power. + "7^1000 * 7^1000 + 5 - 7^1000 * 7^1000": calc.ErrOutOfRange, + "7^1000 * 7^1000 / 7^1000 % 10": calc.ErrOutOfRange, + "(-1)^(3^1365 * 3^1365 / 3^1365)": calc.ErrOutOfRange, + "(-1)^1e1300": calc.ErrOutOfRange, + "1e-1300": calc.ErrOutOfRange, + "1e-1300 + 1": calc.ErrOutOfRange, + "1e-700 * 1e-700": calc.ErrOutOfRange, + "0.1^800 * 0.1^800": calc.ErrOutOfRange, + // Both operands are held exactly, but their quotient is not. + "3^1365 % 7^-1000": calc.ErrOutOfRange, + // The same limit for a whole number made from x^0, which + // go/constant would hold as an integer and never round, and for + // a literal it reads exactly as a fraction past the limit. + "(2^0 + 2^0)^4095 % 10": calc.ErrOutOfRange, + "1e-1233 * 0": calc.ErrOutOfRange, + // go/constant reads this literal as 0. + "1e-999999999": calc.ErrOutOfRange, + "1 / 1e-999999999": calc.ErrOutOfRange, + }) +} + +// expectErrors checks that each expression is refused with its error, +// and never with a panic. +func expectErrors(t *testing.T, cases map[string]error) { + t.Helper() for in, want := range cases { t.Run(in, func(t *testing.T) { @@ -116,6 +273,62 @@ func TestEvaluateRefuses(t *testing.T) { } } +// TestEvaluateBoundsWork: computed exactly, each of these powers would +// need more time and memory than any machine has. They must be answered +// at once. +func TestEvaluateBoundsWork(t *testing.T) { + t.Parallel() + + cases := []struct { + in string + want string + err error + }{ + {in: "9^9^9^9^9", err: calc.ErrOutOfRange}, + {in: "((9^999)^999)^999", err: calc.ErrOutOfRange}, + {in: "(3^2583)^4096", err: calc.ErrOutOfRange}, + {in: "1.0000001^99999", want: "1.01005006557947"}, + {in: "0.5^99999999999999999999", err: calc.ErrOutOfRange}, + {in: "2^-9223372036854775808", err: calc.ErrOutOfRange}, + {in: "(-1)^99999999999999999999", want: "-1"}, + // The longest tower that fits. + {in: strings.Repeat("9^", 127) + "9", err: calc.ErrOutOfRange}, + // The largest power of 3 computed exactly, as often as fits. + {in: "0" + strings.Repeat("*3^2583", 36), want: "0"}, + // Whole numbers made from x^0, through each operation. Held as + // integers, which go/constant never rounds, they would escape + // the 4096-bit limit: the first needs about 69 billion bits. + {in: "(((2^0+2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "(((0^0+0^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "(((-2^0-2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "((2^0+2^0)^4000*(2^0+2^0)^4000)^4096", err: calc.ErrOutOfRange}, + {in: "((((2^0+2^0)/2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "((((2^0+2^0) % 3)^4096)^4096)^4096", err: calc.ErrOutOfRange}, + {in: "(((2^0+2^0)^4096)^4096)^4096 * 0", err: calc.ErrOutOfRange}, + // A fraction whose numerator and denominator are both just below + // the limit, and a literal whose exponent is too large to read. + {in: "(3^2583/5^1760)^4096", err: calc.ErrOutOfRange}, + {in: "1e99999999999999999999", err: calc.ErrOutOfRange}, + } + + for _, c := range cases { + t.Run(c.in, func(t *testing.T) { + t.Parallel() + + start := time.Now() + got, err := calc.Evaluate(c.in) + + if elapsed := time.Since(start); elapsed > time.Second { + t.Errorf("Evaluate(%q) took %v", c.in, elapsed) + } + + if !errors.Is(err, c.err) || got != c.want { + t.Errorf("Evaluate(%q) = %q, %v; want %q, %v", c.in, got, err, c.want, c.err) + } + }) + } +} + // TestEvaluateCapsInput: the length cap is what bounds the work a // message can cause, so it must hold exactly at the boundary. func TestEvaluateCapsInput(t *testing.T) {