Powers and remainders in the calculator (closes #3)
check / check (push) Successful in 57s

internal/calc gets its own tokenizer and precedence-climbing parser in
place of go/parser, which cannot express a power operator; go/constant
still computes. ^ (also **) binds tighter than * / % and a sign on its
left, and groups to the right. % takes the sign of the divisor and is
exact on decimals. A whole exponent is computed exactly while the
result's numerator and denominator stay within 4096 bits; otherwise,
and for a fractional exponent, in float64. A remainder, or the sign of
a negative base's power, that depends on a value go/constant holds
rounded is refused as too large. The welcome text and the replies
mention the new operators; a negative base with a fractional exponent
gets its own reply.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-29 00:08:26 +00:00
parent 09a5caa8ab
commit aeb040d156
7 changed files with 522 additions and 96 deletions
+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) {