Files
simplexcalc/internal/calc/calc_test.go
T
clawbot 3f5e453a18
check / check (push) Successful in 1m11s
Every number a fraction under the 4096-bit limit (closes #3)
go/constant never rounds an integer, and x^0 returned one, so whole
numbers built from it escaped the 4096-bit limit:
(((2^0+2^0)^4096)^4096)^4096 asked for about 69 billion bits and
stalled the bot. A power now starts from the fraction 1, and a number
is exact only as a fraction whose numerator and denominator are below
4096 bits, which also refuses a literal that go/constant reads exactly
past the limit, such as 1e-1233. The bounded-work test holds the tower
and the other short inputs tried against the change.

Model: opus-5-5
2026-09-29 02:09:33 +00:00

364 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package calc_test
import (
"errors"
"strings"
"testing"
"time"
"sneak.berlin/go/simplexcalc/internal/calc"
)
// TestEvaluate covers the two examples the bot was specified with, and
// the arithmetic around them.
func TestEvaluate(t *testing.T) {
t.Parallel()
expectResults(t, map[string]string{
// The specification's own examples.
"2 + 2": "4",
"5 * 5/2": "12.5",
"2+2": "4",
" 7 - 10 \n": "-3",
"-3 * 2": "-6",
"+4": "4",
"-(-4)": "4",
"(1 + 2) * 3": "9",
"1 + 2 * 3": "7",
"((2))": "2",
"8 / 2 / 2": "2",
"10 - 2 - 3": "5",
"7 / 2": "3.5",
"25/2": "12.5",
"1 / 3": "0.3333333333333333",
"2 / 3": "0.6666666666666666",
"0.1 + 0.2": "0.3",
"1.5 * 2": "3",
"2.50 * 2": "5",
".5 + .5": "1",
"3. * 2": "6",
"1e3 + 1": "1001",
"2.5e-1": "0.25",
"010 + 1": "11",
"-0": "0",
"0 / 5": "0",
// Exact: a float64 would print 99999999980000000000.
"9999999999 * 9999999999": "99999999980000000001",
"1e21": "1e+21",
"1e20": "100000000000000000000",
"1 / 1e7": "1e-07",
"1 / 1e6": "0.000001",
"1234567.5": "1234567.5",
"-1 / 4": "-0.25",
"1e300 * 1e8": "1e+308",
})
}
// TestEvaluatePowers: ^ and ** are one operator, binding tighter than
// * / % and a sign on its left, and grouping to the right.
func TestEvaluatePowers(t *testing.T) {
t.Parallel()
expectResults(t, map[string]string{
"2^3": "8",
"2**3": "8",
"2 ** 3 ^ 2": "512",
"2^3^2": "512",
"(2^3)^2": "64",
"-2^2": "-4",
"(-2)^2": "4",
"(-2)^3": "-8",
"(-2)^-3": "-0.125",
"2^-1": "0.5",
"2**-1": "0.5",
"-2^-2": "-0.25",
"2^-3^2": "0.001953125",
"2 * 3^2": "18",
"3^2 * 2": "18",
"2^3 / 2^2": "2",
"1 + 2^3 - 3^2": "0",
"010^2": "100",
"0.1^2": "0.01",
"2^100 - 2^100 + 1": "1",
"2^64": "18446744073709551616",
"0^0": "1",
"0^3": "0",
"1.5^2": "2.25",
"2^0.5": "1.4142135623730951",
"-2^0.5": "-1.4142135623730951",
"4^0.5": "2",
"0^0.5": "0",
"2^1023": "8.98846567431158e+307",
"2^-1022": "2.2250738585072014e-308",
// Past 2^53 a float64 cannot tell odd from even.
"(-1)^(2^53 + 1)": "-1",
"(-1)^(10^30)": "1",
"(-1)^-9223372036854775808": "1",
// Whole powers beyond the range of a double, held exactly.
"2^-1400 * 2^1365 * 2^35": "1",
"0.3^900 * 10^470": "0.25652473503365386",
"2^1500 / 2^1000": "3.273390607896142e+150",
})
}
// TestEvaluateModulo: % sits with * and /, left to right, and its result
// takes the sign of the divisor.
func TestEvaluateModulo(t *testing.T) {
t.Parallel()
expectResults(t, map[string]string{
"7 % 3": "1",
"-7 % 3": "2",
"7 % -3": "-2",
"-7 % -3": "-1",
"6 % 3": "0",
"-6 % 3": "0",
"7.5 % 2": "1.5",
"0.3 % 0.1": "0",
"-0.3 % 0.2": "0.1",
"10 % 4 * 3": "6",
"2 * 7 % 4": "2",
"1 + 7 % 3": "2",
"2^10 % 7": "2",
"10^400 % 7": "4",
"1e-30 % 1": "1e-30",
"-1e-30 % 1": "1",
"10 / 8 % 1": "0.25",
"(7 % 3)^2": "1",
"7 % (3 ^ 2)": "7",
// Both operands and their quotient are held exactly, but y times
// the whole part of x/y is too large to be.
"(5^860*3^630/7) % (5^860/2^998/2^998)": "0.5179219763783696",
// A whole number made from x^0, just below the 4096-bit limit.
"(3^0 + 3^0 + 3^0)^2583 % 10": "7",
})
}
// expectResults checks that each expression evaluates to its result.
func expectResults(t *testing.T, cases map[string]string) {
t.Helper()
for in, want := range cases {
t.Run(in, func(t *testing.T) {
t.Parallel()
got, err := calc.Evaluate(in)
if err != nil {
t.Fatalf("Evaluate(%q) failed: %v", in, err)
}
if got != want {
t.Errorf("Evaluate(%q) = %q, want %q", in, got, want)
}
})
}
}
// TestEvaluateRefuses covers what must be answered with an error rather
// than a number.
func TestEvaluateRefuses(t *testing.T) {
t.Parallel()
expectErrors(t, map[string]error{
"": calc.ErrNotArithmetic,
" ": calc.ErrNotArithmetic,
"hello": calc.ErrNotArithmetic,
"/help": calc.ErrNotArithmetic,
"2 +": calc.ErrNotArithmetic,
"2 2": calc.ErrNotArithmetic,
"2 + 2 =": calc.ErrNotArithmetic,
"x + 1": calc.ErrNotArithmetic,
"len(\"abc\")": calc.ErrNotArithmetic,
"\"a\" + \"b\"": calc.ErrNotArithmetic,
"'a' + 1": calc.ErrNotArithmetic,
"2i * 2i": calc.ErrNotArithmetic,
"0x10 + 1": calc.ErrNotArithmetic,
"1_000 + 1": 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,
})
}
// TestEvaluateOutOfRange: a number is held exactly, or computed in
// float64 as a normal double, and a result is written as a normal
// double. Anything else is refused.
func TestEvaluateOutOfRange(t *testing.T) {
t.Parallel()
expectErrors(t, map[string]error{
// Results that are not normal doubles: 2^-1074 would be written
// 5e-324.
"1e400": calc.ErrOutOfRange,
"1e300 * 1e300": calc.ErrOutOfRange,
"1e999999999 * 1e999999999": calc.ErrOutOfRange,
"1 / 1e-400": calc.ErrOutOfRange,
"2^1024": calc.ErrOutOfRange,
"2^5000": calc.ErrOutOfRange,
"(-2)^5001": calc.ErrOutOfRange,
"0.5^-5000": calc.ErrOutOfRange,
"2^-1074": calc.ErrOutOfRange,
"2^-1400": calc.ErrOutOfRange,
"-1e-310": calc.ErrOutOfRange,
// Powers computed in float64 whose base or result is not a
// normal double, and so has lost digits, or all of them.
"2^-1073.5 * 2^1073": calc.ErrOutOfRange,
"1e400^-0.001": calc.ErrOutOfRange,
"1e-400^0.001": calc.ErrOutOfRange,
"1e-310^0.5": calc.ErrOutOfRange,
"(0.5^1100)^4 / (0.5^1100)^4": calc.ErrOutOfRange,
"(1/3)^1e400": calc.ErrOutOfRange,
// go/constant holds numbers of this size rounded. A sum of them
// can lose the answer (this one would be 0), and so can a
// remainder or the sign of -1 to such a power.
"7^1000 * 7^1000 + 5 - 7^1000 * 7^1000": calc.ErrOutOfRange,
"7^1000 * 7^1000 / 7^1000 % 10": calc.ErrOutOfRange,
"(-1)^(3^1365 * 3^1365 / 3^1365)": calc.ErrOutOfRange,
"(-1)^1e1300": calc.ErrOutOfRange,
"1e-1300": calc.ErrOutOfRange,
"1e-1300 + 1": calc.ErrOutOfRange,
"1e-700 * 1e-700": calc.ErrOutOfRange,
"0.1^800 * 0.1^800": calc.ErrOutOfRange,
// Both operands are held exactly, but their quotient is not.
"3^1365 % 7^-1000": calc.ErrOutOfRange,
// The same limit for a whole number made from x^0, which
// go/constant would hold as an integer and never round, and for
// a literal it reads exactly as a fraction past the limit.
"(2^0 + 2^0)^4095 % 10": calc.ErrOutOfRange,
"1e-1233 * 0": calc.ErrOutOfRange,
// go/constant reads this literal as 0.
"1e-999999999": calc.ErrOutOfRange,
"1 / 1e-999999999": calc.ErrOutOfRange,
})
}
// expectErrors checks that each expression is refused with its error,
// and never with a panic.
func expectErrors(t *testing.T, cases map[string]error) {
t.Helper()
for in, want := range cases {
t.Run(in, func(t *testing.T) {
t.Parallel()
got, err := calc.Evaluate(in)
if !errors.Is(err, want) {
t.Errorf("Evaluate(%q) = %q, %v; want error %v", in, got, err, want)
}
})
}
}
// TestEvaluateBoundsWork: computed exactly, each of these powers would
// need more time and memory than any machine has. They must be answered
// at once.
func TestEvaluateBoundsWork(t *testing.T) {
t.Parallel()
cases := []struct {
in string
want string
err error
}{
{in: "9^9^9^9^9", err: calc.ErrOutOfRange},
{in: "((9^999)^999)^999", err: calc.ErrOutOfRange},
{in: "(3^2583)^4096", err: calc.ErrOutOfRange},
{in: "1.0000001^99999", want: "1.01005006557947"},
{in: "0.5^99999999999999999999", err: calc.ErrOutOfRange},
{in: "2^-9223372036854775808", err: calc.ErrOutOfRange},
{in: "(-1)^99999999999999999999", want: "-1"},
// The longest tower that fits.
{in: strings.Repeat("9^", 127) + "9", err: calc.ErrOutOfRange},
// The largest power of 3 computed exactly, as often as fits.
{in: "0" + strings.Repeat("*3^2583", 36), want: "0"},
// Whole numbers made from x^0, through each operation. Held as
// integers, which go/constant never rounds, they would escape
// the 4096-bit limit: the first needs about 69 billion bits.
{in: "(((2^0+2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "(((0^0+0^0)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "(((-2^0-2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "((2^0+2^0)^4000*(2^0+2^0)^4000)^4096", err: calc.ErrOutOfRange},
{in: "((((2^0+2^0)/2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "((((2^0+2^0) % 3)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "(((2^0+2^0)^4096)^4096)^4096 * 0", err: calc.ErrOutOfRange},
// A fraction whose numerator and denominator are both just below
// the limit, and a literal whose exponent is too large to read.
{in: "(3^2583/5^1760)^4096", err: calc.ErrOutOfRange},
{in: "1e99999999999999999999", err: calc.ErrOutOfRange},
}
for _, c := range cases {
t.Run(c.in, func(t *testing.T) {
t.Parallel()
start := time.Now()
got, err := calc.Evaluate(c.in)
if elapsed := time.Since(start); elapsed > time.Second {
t.Errorf("Evaluate(%q) took %v", c.in, elapsed)
}
if !errors.Is(err, c.err) || got != c.want {
t.Errorf("Evaluate(%q) = %q, %v; want %q, %v", c.in, got, err, c.want, c.err)
}
})
}
}
// TestEvaluateCapsInput: the length cap is what bounds the work a
// message can cause, so it must hold exactly at the boundary.
func TestEvaluateCapsInput(t *testing.T) {
t.Parallel()
// "1+1+...+1" with the last term padded to land exactly on the cap.
longest := strings.Repeat("1+", calc.MaxInputLength/2-1) + "10"
if len(longest) != calc.MaxInputLength {
t.Fatalf("test setup: expression is %d bytes, want %d",
len(longest), calc.MaxInputLength)
}
got, err := calc.Evaluate(longest)
if err != nil {
t.Fatalf("an expression of exactly MaxInputLength bytes was refused: %v", err)
}
if want := "137"; got != want {
t.Errorf("Evaluate(longest) = %q, want %q", got, want)
}
_, err = calc.Evaluate(longest + "0")
if !errors.Is(err, calc.ErrTooLong) {
t.Errorf("an expression over MaxInputLength gave %v, want ErrTooLong", err)
}
// Surrounding whitespace is not part of the expression.
_, err = calc.Evaluate(" " + longest + "\n")
if err != nil {
t.Errorf("whitespace around a maximal expression counted against the cap: %v", err)
}
}