Powers and remainders in the calculator (closes #3) #11

Merged
clawbot merged 4 commits from issue-3-power-modulo into next 2026-09-29 04:28:16 +02:00
7 changed files with 522 additions and 96 deletions
Showing only changes of commit aeb040d156 - Show all commits
+1 -1
View File
@@ -120,7 +120,7 @@ Do not weaken them.
```
cmd/simplexcalc/ main(), a single call into internal/cli
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
+23 -13
View File
@@ -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
@@ -157,16 +158,25 @@ 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
message cannot make the bot do unbounded work. Whole numbers below
10<sup>21</sup> are written exactly; other results in the shortest
form that reads back as the same double, in exponent notation from
10<sup>21</sup> up and below 10<sup>-6</sup>. A result beyond the
range of a double is refused as too large.
- **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`, unless
its numerator and denominator together could pass 4096 bits; 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 and exact
powers are capped, so a message cannot make the bot do unbounded work.
Whole numbers below 10<sup>21</sup> are written exactly; other results
in the shortest form that reads back as the same double, in exponent
notation from 10<sup>21</sup> up and below 10<sup>-6</sup>. A result
beyond the range of a double is refused as too large.
- **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
+3
View File
@@ -27,6 +27,9 @@ with no deprecation warning.
# Completed Steps
- 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`
+5 -3
View File
@@ -22,7 +22,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."
const (
@@ -223,8 +223,10 @@ func Reply(text string) string {
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.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."
}
}
+6 -3
View File
@@ -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,10 @@ 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": "The result is too large 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) {
+339 -72
View File
@@ -1,29 +1,37 @@
// 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 too large to compute
// 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, and maxExactPowerBits
// caps a power, so that a message cannot make the bot do unbounded work.
const MaxInputLength = 256
// maxExactPowerBits caps a power computed exactly: its numerator and
// denominator together have at most this many bits, estimated before
// multiplying as the exponent times the bits in the base's numerator and
// denominator. A larger power is computed in float64, whose cost does not
// grow with it.
const maxExactPowerBits = 4096
// 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 +40,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")
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,75 +82,206 @@ 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: as INT, a
// leading zero would make it octal.
v := constant.MakeFromLiteral(tok, token.FLOAT, 0)
// The syntax was checked above, so Unknown here means the exponent
// overflowed.
if v.Kind() == constant.Unknown {
return nil, ErrTooLarge
}
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
}
// constant.BinaryOp panics on a zero divisor.
if n.Op == token.QUO && 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)
// go/constant represents an overflow to infinity as Unknown.
if v.Kind() == constant.Unknown {
return nil, ErrTooLarge
@@ -134,28 +290,139 @@ func binary(n *ast.BinaryExpr) (constant.Value, error) {
return v, nil
}
func literal(n *ast.BasicLit) (constant.Value, error) {
if n.Kind != token.INT && n.Kind != token.FLOAT {
return nil, ErrNotArithmetic
func divide(x, y constant.Value) (constant.Value, error) {
// constant.BinaryOp panics on a zero divisor.
if constant.Sign(y) == 0 {
return nil, ErrDivisionByZero
}
if !decimalLiteral.MatchString(n.Value) {
return nil, ErrNotArithmetic
// token.QUO divides exactly, integers included: 25/2 is 12.5.
return constant.BinaryOp(x, token.QUO, y), nil
}
// 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
}
// 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)
// The syntax was checked above, so Unknown here means the exponent
// overflowed.
if v.Kind() == constant.Unknown {
// The whole part of a rounded quotient, and so the remainder, would
// be wrong.
if rounded(q) {
return nil, ErrTooLarge
}
// token.QUO_ASSIGN is go/constant's integer division, which
// truncates, so r has the sign of x and is less than y in size.
whole := constant.BinaryOp(constant.Num(q), token.QUO_ASSIGN, constant.Denom(q))
r := constant.BinaryOp(x, token.SUB, constant.BinaryOp(y, token.MUL, whole))
if constant.Sign(r) != 0 && constant.Sign(r) != constant.Sign(y) {
r = constant.BinaryOp(r, token.ADD, y)
}
return r, 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), nil
case rounded(y):
// Whether a rounded y is whole, or odd, is unknown.
return nil, ErrTooLarge
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 := nonNegativePower(constant.UnaryOp(token.SUB, x, 0), y, n)
odd := constant.BinaryOp(n, token.AND, constant.MakeInt64(1))
if constant.Sign(odd) != 0 {
v = constant.UnaryOp(token.SUB, v, 0)
}
return v, nil
}
// 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 the result
// fits in maxExactPowerBits, otherwise in float64.
func nonNegativePower(x, y, n constant.Value) constant.Value {
e, ok := constant.Int64Val(n)
if ok && exactPowerFits(x, e) {
return exactPower(x, e)
}
xf, _ := constant.Float64Val(x)
yf, _ := constant.Float64Val(y)
// An infinite result becomes Unknown, which apply refuses as too
// large.
return constant.MakeFloat64(math.Pow(xf, yf))
}
// exactPowerFits reports whether x^e fits in maxExactPowerBits.
func exactPowerFits(x constant.Value, e int64) bool {
// Checked first so that the product below cannot overflow.
if e < -maxExactPowerBits || e > maxExactPowerBits {
return false
}
// Num and Denom are Unknown for a value too large or too small to
// be held as a fraction.
num, den := constant.Num(x), constant.Denom(x)
if num.Kind() != constant.Int {
return false
}
bits := int64(constant.BitLen(num) + constant.BitLen(den))
return bits*max(e, -e) <= maxExactPowerBits
}
// exactPower computes x^e by repeated squaring. x is not zero if e is
// negative.
func exactPower(x constant.Value, e int64) constant.Value {
result := constant.MakeInt64(1)
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(constant.MakeInt64(1), token.QUO, result)
}
return result
}
// rounded reports whether go/constant holds v rounded. It holds a
// number exactly, as a fraction, only while the numerator and the
// denominator each stay under 4096 bits; past that, and for a literal of
// that size, it holds a 512-bit float.
func rounded(v constant.Value) bool {
_, isFloat := constant.Val(v).(*big.Float)
return isFloat
}
// 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
+145 -4
View File
@@ -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,81 @@ 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",
// Past 2^53 a float64 cannot tell odd from even.
"(-1)^(2^53 + 1)": "-1",
"(-1)^(10^30)": "1",
})
}
// 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",
})
}
// 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) {
@@ -89,19 +164,45 @@ 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,
"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,
"1e400": calc.ErrTooLarge,
"1e300 * 1e300": calc.ErrTooLarge,
"1e999999999 * 1e999999999": calc.ErrTooLarge,
"1 / 1e-400": calc.ErrTooLarge,
"2^1024": calc.ErrTooLarge,
"2^5000": calc.ErrTooLarge,
"(-2)^5001": calc.ErrTooLarge,
"0.5^-5000": calc.ErrTooLarge,
// go/constant holds a product of this size rounded, so the
// remainder, or the sign of -1 to its power, cannot be known.
"7^1000 * 7^1000 / 7^1000 % 10": calc.ErrTooLarge,
"(-1)^(3^1365 * 3^1365 / 3^1365)": calc.ErrTooLarge,
"(-1)^1e1300": calc.ErrTooLarge,
}
for in, want := range cases {
@@ -116,6 +217,46 @@ 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.ErrTooLarge},
{in: "((9^999)^999)^999", err: calc.ErrTooLarge},
{in: "1.0000001^99999", want: "1.01005006557947"},
{in: "0.5^99999999999999999999", want: "0"},
{in: "(-1)^99999999999999999999", want: "-1"},
// The longest tower that fits.
{in: strings.Repeat("9^", 127) + "9", err: calc.ErrTooLarge},
// The largest power computed exactly, as often as fits.
{in: strings.Repeat("3^1365*", 36) + "0", want: "0"},
}
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) {