next: exponentiation, modulo, chat API and webhooks #10
@@ -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
|
||||||
|
|||||||
@@ -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,33 @@ 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
|
||||||
|
`-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
|
message cannot make the bot do unbounded work. Whole numbers below
|
||||||
10<sup>21</sup> are written exactly; other results in the shortest
|
10<sup>21</sup> are written exactly; other results in the shortest
|
||||||
form that reads back as the same double, in exponent notation from
|
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
|
10<sup>21</sup> up and below 10<sup>-6</sup>. Refused as too large or
|
||||||
range of a double is refused as too large.
|
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
|
- **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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
+7
-5
@@ -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 (
|
||||||
@@ -221,10 +221,12 @@ func Reply(text string) string {
|
|||||||
calc.MaxInputLength)
|
calc.MaxInputLength)
|
||||||
case errors.Is(err, calc.ErrDivisionByZero):
|
case errors.Is(err, calc.ErrDivisionByZero):
|
||||||
return "I cannot divide by zero."
|
return "I cannot divide by zero."
|
||||||
case errors.Is(err, calc.ErrTooLarge):
|
case errors.Is(err, calc.ErrOutOfRange):
|
||||||
return "The result is too large for me."
|
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:
|
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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -25,7 +27,9 @@ 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": "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",
|
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) {
|
||||||
|
|||||||
+380
-77
@@ -1,29 +1,45 @@
|
|||||||
// 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 whose result go/constant
|
||||||
|
// cannot hold 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. With maxExactExponent,
|
||||||
// the bot do unbounded work. Every operation's cost grows with the size
|
// which caps a power computed exactly, and bitLimit, which caps every
|
||||||
// of its operands, and the operands can only grow with the input.
|
// number, it keeps a message from making the bot do unbounded work.
|
||||||
const MaxInputLength = 256
|
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
|
// 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 +48,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")
|
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
|
// 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,114 +90,384 @@ 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return constant.UnaryOp(n.Op, x, 0), nil
|
for {
|
||||||
case *ast.BinaryExpr:
|
op := p.peek()
|
||||||
return binary(n)
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
x, err = apply(x, op, y)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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:
|
default:
|
||||||
return nil, ErrNotArithmetic
|
return number(tok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func binary(n *ast.BinaryExpr) (constant.Value, error) {
|
// precedence returns the precedence of a binary operator, and 0 for any
|
||||||
switch n.Op { //nolint:exhaustive // every other operator is refused.
|
// other token, which ends an expression.
|
||||||
case token.ADD, token.SUB, token.MUL, token.QUO:
|
func precedence(op string) int {
|
||||||
|
switch op {
|
||||||
|
case "+", "-":
|
||||||
|
return sumPrecedence
|
||||||
|
case "*", "/", "%":
|
||||||
|
return productPrecedence
|
||||||
|
case "^":
|
||||||
|
return powerPrecedence
|
||||||
default:
|
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 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
y, err := eval(n.Y)
|
if !exact(v) {
|
||||||
if err != nil {
|
return nil, ErrOutOfRange
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func divide(x, y constant.Value) (constant.Value, error) {
|
||||||
// constant.BinaryOp panics on a zero divisor.
|
// constant.BinaryOp panics on a zero divisor.
|
||||||
if n.Op == token.QUO && constant.Sign(y) == 0 {
|
if constant.Sign(y) == 0 {
|
||||||
return nil, ErrDivisionByZero
|
return nil, ErrDivisionByZero
|
||||||
}
|
}
|
||||||
|
|
||||||
// token.QUO divides exactly, integers included: 25/2 is 12.5.
|
// 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.
|
// modulo computes x % y, whose result takes the sign of y, as in Python:
|
||||||
if v.Kind() == constant.Unknown {
|
// -7 % 3 is 2 and 7 % -3 is -2. It is exact for decimals too: 7.5 % 2
|
||||||
return nil, ErrTooLarge
|
// 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
|
return v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func literal(n *ast.BasicLit) (constant.Value, error) {
|
// nonNegativePower computes x^y for x of at least zero, and y not below
|
||||||
if n.Kind != token.INT && n.Kind != token.FLOAT {
|
// zero if x is zero: exactly if y is a whole number n and go/constant
|
||||||
return nil, ErrNotArithmetic
|
// holds the result exactly, otherwise in float64.
|
||||||
}
|
func nonNegativePower(x, y, n constant.Value) (constant.Value, error) {
|
||||||
|
e, ok := constant.Int64Val(n)
|
||||||
if !decimalLiteral.MatchString(n.Value) {
|
if ok && -maxExactExponent <= e && e <= maxExactExponent {
|
||||||
return nil, ErrNotArithmetic
|
v := exactPower(x, e)
|
||||||
}
|
if exact(v) {
|
||||||
|
|
||||||
// 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 {
|
|
||||||
return nil, ErrTooLarge
|
|
||||||
}
|
|
||||||
|
|
||||||
return v, nil
|
return v, nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// y is above zero here if x is zero.
|
||||||
|
if constant.Sign(x) == 0 {
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
xf, _ := constant.Float64Val(x)
|
||||||
|
yf, _ := constant.Float64Val(y)
|
||||||
|
f := math.Pow(xf, yf)
|
||||||
|
|
||||||
|
// 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 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
|
// 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
|
||||||
// (0.30000000000000004) that printing a binary fraction to a fixed
|
// (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) {
|
func format(v constant.Value) (string, error) {
|
||||||
f, _ := constant.Float64Val(v)
|
f, _ := constant.Float64Val(v)
|
||||||
if math.IsInf(f, 0) || math.IsNaN(f) {
|
if constant.Sign(v) != 0 && !normal(f) {
|
||||||
return "", ErrTooLarge
|
return "", ErrOutOfRange
|
||||||
}
|
}
|
||||||
|
|
||||||
abs := math.Abs(f)
|
abs := math.Abs(f)
|
||||||
|
|||||||
+222
-9
@@ -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,8 +52,93 @@ 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",
|
||||||
|
"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 {
|
for in, want := range cases {
|
||||||
t.Run(in, func(t *testing.T) {
|
t.Run(in, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -70,11 +156,11 @@ func TestEvaluate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestEvaluateRefuses covers what must be answered with an error rather
|
// 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) {
|
func TestEvaluateRefuses(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
cases := map[string]error{
|
expectErrors(t, map[string]error{
|
||||||
"": calc.ErrNotArithmetic,
|
"": calc.ErrNotArithmetic,
|
||||||
" ": calc.ErrNotArithmetic,
|
" ": calc.ErrNotArithmetic,
|
||||||
"hello": calc.ErrNotArithmetic,
|
"hello": calc.ErrNotArithmetic,
|
||||||
@@ -89,21 +175,92 @@ 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,
|
||||||
"1e400": calc.ErrTooLarge,
|
"7 % 0": calc.ErrDivisionByZero,
|
||||||
"1e300 * 1e300": calc.ErrTooLarge,
|
"7.5 % (1 - 1)": calc.ErrDivisionByZero,
|
||||||
"1e999999999 * 1e999999999": calc.ErrTooLarge,
|
"0^-1": calc.ErrDivisionByZero,
|
||||||
"1 / 1e-400": calc.ErrTooLarge,
|
"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 {
|
for in, want := range cases {
|
||||||
t.Run(in, func(t *testing.T) {
|
t.Run(in, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -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
|
// 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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user