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 cmd/simplexcalc/ main(), a single call into internal/cli
internal/bot/ startup, address setup, and the reply to a message 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/cli/ cobra command tree: run and version
internal/config/ viper-backed configuration; the abort-on-garbage rule internal/config/ viper-backed configuration; the abort-on-garbage rule
internal/logger/ log/slog, JSON always 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. arithmetic such as `2 + 2` with the result.
Send it `2 + 2` and it replies `4`; send `5 * 5/2` and it replies Send it `2 + 2` and it replies `4`; send `5 * 5/2` and it replies
`12.5`. It understands decimal numbers, `+ - * /`, unary minus and `12.5`. It understands decimal numbers, `+ - * /`, powers written `2^10`
parentheses, and computes exactly, so `0.1 + 0.2` is `0.3`. Anything or `2**10`, remainders written `7 % 3`, signs and parentheses, and
else gets a short explanation instead of a result. computes exactly, so `0.1 + 0.2` is `0.3`. Anything else gets a short
explanation instead of a result.
## Getting Started ## Getting Started
@@ -157,16 +158,25 @@ container.
- **Replies**: for each text message a contact sends in a direct chat, - **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 the bot sends back the result, as a reply quoting the message. Group
messages, files and the bot's own messages are ignored. messages, files and the bot's own messages are ignored.
- **Arithmetic** (`internal/calc`): the text is parsed as a Go - **Arithmetic** (`internal/calc`): a small parser of its own reads
expression with `go/parser`, and only numbers, `+ - * /`, unary signs numbers, `+ - * / % ^`, signs and parentheses, and refuses anything
and parentheses are evaluated; anything else in the syntax tree is else. `go/constant` computes with exact rationals. `^`, also written
refused. `go/constant` computes with exact rationals. Numbers are read `**`, is a power: it binds tighter than `*`, `/`, `%` and a sign on
as decimal, so `010` is ten. Input over 256 bytes is refused, so a its left, and groups to the right, so `2^3^2` is `512`, `-2^2` is
message cannot make the bot do unbounded work. Whole numbers below `-4`, `(-2)^2` is `4` and `2^-1` is `0.5`. `%` is the remainder and
10<sup>21</sup> are written exactly; other results in the shortest ranks with `*` and `/`; its result takes the sign of the divisor, as
form that reads back as the same double, in exponent notation from in Python, so `7 % 3` is `1`, `-7 % 3` is `2` and `7.5 % 2` is `1.5`.
10<sup>21</sup> up and below 10<sup>-6</sup>. A result beyond the A power with a whole exponent is exact, so `0.1^2` is `0.01`, unless
range of a double is refused as too large. 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 - **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 it drops, the bot exits with an error and the container's restart
policy starts both again. `SIGTERM` stops the bot, which stops the policy starts both again. `SIGTERM` stops the bot, which stops the
+3
View File
@@ -27,6 +27,9 @@ with no deprecation warning.
# Completed Steps # 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 - 2026-09-28 Moved the command tree and the `run` and `version` commands
from `cmd/simplexcalc/` into `internal/cli`; `cmd/simplexcalc/main.go` from `cmd/simplexcalc/` into `internal/cli`; `cmd/simplexcalc/main.go`
is now a single call to `cli.Main` is now a single call to `cli.Main`
+5 -3
View File
@@ -22,7 +22,7 @@ import (
const DisplayName = "calc" const DisplayName = "calc"
// Welcome is sent to everyone whose contact request the bot accepts. // 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." "and I will reply with the result."
const ( const (
@@ -223,8 +223,10 @@ func Reply(text string) string {
return "I cannot divide by zero." return "I cannot divide by zero."
case errors.Is(err, calc.ErrTooLarge): case errors.Is(err, calc.ErrTooLarge):
return "The result is too large for me." 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: default:
return "I only understand arithmetic: numbers, + - * / and " + return "I only understand arithmetic: numbers, + - * /, ^ for a power, " +
"parentheses, such as 5 * 5/2." "% 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{ for in, want := range map[string]string{
"2 + 2": "4", "2 + 2": "4",
"5 * 5/2": "12.5", "5 * 5/2": "12.5",
"2^10": "1024",
"7 % 3": "1",
} { } {
if got := bot.Reply(in); got != want { if got := bot.Reply(in); got != want {
t.Errorf("Reply(%q) = %q, want %q", 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{ for in, want := range map[string]string{
"hello": "I only understand arithmetic", "hello": "I only understand arithmetic",
"1 / 0": "I cannot divide by zero.", "1 / 0": "I cannot divide by zero.",
"1e400": "The result is too large for me.", "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", strings.Repeat("1+", calc.MaxInputLength) + "1": "That is too long for me",
} { } {
if got := bot.Reply(in); !strings.HasPrefix(got, want) { 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 // 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, // The expression is read by a small parser of its own, because Go's
// which does exact rational arithmetic: 5 * 5/2 is exactly 12.5, and // grammar has no power operator (^ is XOR there), and computed by
// 0.1 + 0.2 is exactly 0.3, so a result carries no binary floating // go/constant, which does exact rational arithmetic: 5 * 5/2 is exactly
// point noise until the moment it is formatted. // 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 package calc
import ( import (
"errors" "errors"
"go/ast"
"go/constant" "go/constant"
"go/parser"
"go/token" "go/token"
"math" "math"
"math/big"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
) )
// MaxInputLength caps an expression, in bytes, so a message cannot make // MaxInputLength caps an expression, in bytes, and maxExactPowerBits
// the bot do unbounded work. Every operation's cost grows with the size // caps a power, so that a message cannot make the bot do unbounded work.
// of its operands, and the operands can only grow with the input.
const MaxInputLength = 256 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 // Results of magnitude plainUpper or more are written in exponent form
// (1e+21 rather than twenty-two digits), and so are fractions smaller // (1e+21 rather than twenty-two digits), and so are fractions smaller
// than plainLower (1e-07 rather than 0.0000001). // than plainLower (1e-07 rather than 0.0000001).
@@ -32,20 +40,37 @@ const (
plainLower = 1e-6 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. // Errors returned by Evaluate. The bot turns each into a reply.
var ( var (
ErrTooLong = errors.New("expression too long") ErrTooLong = errors.New("expression too long")
ErrNotArithmetic = errors.New("not an arithmetic expression") ErrNotArithmetic = errors.New("not an arithmetic expression")
ErrDivisionByZero = errors.New("division by zero") ErrDivisionByZero = errors.New("division by zero")
ErrTooLarge = errors.New("result too large") ErrTooLarge = errors.New("result too large")
ErrNoRealResult = errors.New("no real result")
) )
// decimalLiteral is the only number syntax accepted. Go's own literal // decimal is the only number syntax accepted. Go's own literal syntax is
// syntax is wider, and parts of it are traps for someone typing // wider, and parts of it are traps for someone typing arithmetic: 010 is
// arithmetic: 010 is octal 8, and 0x10, 1_000 and 1i are not what a // octal 8, and 0x10, 1_000 and 1i are not what a calculator user means
// calculator user means by a number. // by a number. Here the x, _ or i matches no token and is refused.
var decimalLiteral = regexp.MustCompile( const decimal = `([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?`
`^([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 // Evaluate computes an arithmetic expression and returns its result as
@@ -57,75 +82,206 @@ func Evaluate(input string) (string, error) {
return "", ErrTooLong return "", ErrTooLong
} }
if s == "" { tokens, err := tokenize(s)
return "", ErrNotArithmetic
}
expr, err := parser.ParseExpr(s)
if err != nil {
return "", ErrNotArithmetic
}
v, err := eval(expr)
if err != nil { if err != nil {
return "", err return "", err
} }
p := parser{tokens: tokens}
v, err := p.expr(sumPrecedence)
if err != nil {
return "", err
}
if p.next() != "" {
return "", ErrNotArithmetic
}
return format(v) return format(v)
} }
// eval walks the syntax tree, allowing only the node types and // tokenize splits an expression into operators, parentheses and
// operators of arithmetic. Anything else — identifiers, calls, strings, // numbers, and refuses anything else. ** is returned as ^.
// shifts, comparisons — is refused, not evaluated. func tokenize(s string) ([]string, error) {
func eval(e ast.Expr) (constant.Value, error) { var tokens []string
switch n := e.(type) {
case *ast.BasicLit: for strings.TrimSpace(s) != "" {
return literal(n) m := nextToken.FindStringSubmatch(s)
case *ast.ParenExpr: if m == nil {
return eval(n.X)
case *ast.UnaryExpr:
if n.Op != token.ADD && n.Op != token.SUB {
return nil, ErrNotArithmetic 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 { if err != nil {
return nil, err return nil, err
} }
return constant.UnaryOp(n.Op, x, 0), nil x, err = apply(x, op, y)
case *ast.BinaryExpr: if err != nil {
return binary(n) return nil, err
default: }
return nil, ErrNotArithmetic
} }
} }
func binary(n *ast.BinaryExpr) (constant.Value, error) { // operand reads a number, an expression in parentheses, or a sign and
switch n.Op { //nolint:exhaustive // every other operator is refused. // its operand. A sign binds more loosely than a power that follows it,
case token.ADD, token.SUB, token.MUL, token.QUO: // 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: 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 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 { if err != nil {
return nil, err 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. // go/constant represents an overflow to infinity as Unknown.
if v.Kind() == constant.Unknown { if v.Kind() == constant.Unknown {
return nil, ErrTooLarge return nil, ErrTooLarge
@@ -134,28 +290,139 @@ func binary(n *ast.BinaryExpr) (constant.Value, error) {
return v, nil return v, nil
} }
func literal(n *ast.BasicLit) (constant.Value, error) { func divide(x, y constant.Value) (constant.Value, error) {
if n.Kind != token.INT && n.Kind != token.FLOAT { // constant.BinaryOp panics on a zero divisor.
return nil, ErrNotArithmetic if constant.Sign(y) == 0 {
return nil, ErrDivisionByZero
} }
if !decimalLiteral.MatchString(n.Value) { // token.QUO divides exactly, integers included: 25/2 is 12.5.
return nil, ErrNotArithmetic 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 // The whole part of a rounded quotient, and so the remainder, would
// decimal: as INT, a leading zero would make it octal. // be wrong.
v := constant.MakeFromLiteral(n.Value, token.FLOAT, 0) if rounded(q) {
// The syntax was checked above, so Unknown here means the exponent
// overflowed.
if v.Kind() == constant.Unknown {
return nil, ErrTooLarge 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 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 // format writes a result for a person to read. A whole number of
// ordinary size is written exactly, digit for digit; anything else goes // ordinary size is written exactly, digit for digit; anything else goes
// through float64, whose shortest round-trip form is free of the noise // through float64, whose shortest round-trip form is free of the noise
+145 -4
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"strings" "strings"
"testing" "testing"
"time"
"sneak.berlin/go/simplexcalc/internal/calc" "sneak.berlin/go/simplexcalc/internal/calc"
) )
@@ -13,7 +14,7 @@ import (
func TestEvaluate(t *testing.T) { func TestEvaluate(t *testing.T) {
t.Parallel() t.Parallel()
cases := map[string]string{ expectResults(t, map[string]string{
// The specification's own examples. // The specification's own examples.
"2 + 2": "4", "2 + 2": "4",
"5 * 5/2": "12.5", "5 * 5/2": "12.5",
@@ -51,7 +52,81 @@ func TestEvaluate(t *testing.T) {
"1234567.5": "1234567.5", "1234567.5": "1234567.5",
"-1 / 4": "-0.25", "-1 / 4": "-0.25",
"1e300 * 1e8": "1e+308", "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 { for in, want := range cases {
t.Run(in, func(t *testing.T) { t.Run(in, func(t *testing.T) {
@@ -89,19 +164,45 @@ func TestEvaluateRefuses(t *testing.T) {
"2i * 2i": calc.ErrNotArithmetic, "2i * 2i": calc.ErrNotArithmetic,
"0x10 + 1": calc.ErrNotArithmetic, "0x10 + 1": calc.ErrNotArithmetic,
"1_000 + 1": calc.ErrNotArithmetic, "1_000 + 1": calc.ErrNotArithmetic,
"7 % 2": calc.ErrNotArithmetic,
"2 ^ 3": calc.ErrNotArithmetic,
"1 << 10": calc.ErrNotArithmetic, "1 << 10": calc.ErrNotArithmetic,
"1 == 1": calc.ErrNotArithmetic, "1 == 1": calc.ErrNotArithmetic,
"!1": calc.ErrNotArithmetic, "!1": calc.ErrNotArithmetic,
"func() int { return 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 / 0": calc.ErrDivisionByZero,
"1 / (2 - 2)": calc.ErrDivisionByZero, "1 / (2 - 2)": calc.ErrDivisionByZero,
"5 / 0.0": 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, "1e400": calc.ErrTooLarge,
"1e300 * 1e300": calc.ErrTooLarge, "1e300 * 1e300": calc.ErrTooLarge,
"1e999999999 * 1e999999999": calc.ErrTooLarge, "1e999999999 * 1e999999999": calc.ErrTooLarge,
"1 / 1e-400": 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 { 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 // TestEvaluateCapsInput: the length cap is what bounds the work a
// message can cause, so it must hold exactly at the boundary. // message can cause, so it must hold exactly at the boundary.
func TestEvaluateCapsInput(t *testing.T) { func TestEvaluateCapsInput(t *testing.T) {