From c4e4041cd91452d08ee708a6b60938e92c10d018 Mon Sep 17 00:00:00 2001
From: clawbot <35+clawbot@noreply.example.org>
Date: Tue, 29 Sep 2026 06:18:19 +0000
Subject: [PATCH] Write exact results past the range of a double (closes #16)
An exact result outside the normal range of a double, such as 2^1200,
10^400 or 2^-1200, was refused because format wrote every result
through float64. Such a result is now written from its exact value,
rounded to 17 significant digits with trailing zeros dropped, through a
big.Float precise enough to round as the exact value would. Results
inside the range are written as before, and a power computed in float64
must still be a normal double. The tests check the written text of these
results, the refusals just past the 4096-bit limit, and the time taken
to write the largest and smallest numbers under it.
Model: opus-5-5
---
README.md | 24 +++++++++-------
docs/TODO.md | 2 ++
internal/bot/bot_test.go | 3 +-
internal/calc/calc.go | 40 ++++++++++++++++++---------
internal/calc/calc_test.go | 56 ++++++++++++++++++++++++++++++--------
5 files changed, 90 insertions(+), 35 deletions(-)
diff --git a/README.md b/README.md
index c7f8d06..1506d7e 100644
--- a/README.md
+++ b/README.md
@@ -335,16 +335,20 @@ container.
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
- 1021 are written exactly; other results in the shortest
- form that reads back as the same double, in exponent notation from
- 1021 up and below 10-6. Refused as too large or
- 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`).
+ 1021 are written exactly. Other results inside the normal
+ range of a double, about 2.2e-308 to 1.8e308 in magnitude, where a
+ double keeps all its digits, are written in the shortest form that
+ reads back as the same double, in exponent notation from
+ 1021 up and below 10-6. Results outside that
+ range are written from their exact value in exponent notation, rounded
+ to 17 significant digits, the most the shortest form of a double
+ takes, with trailing zeros dropped: `2^1200` is
+ `1.7218479456385751e+361`, `10^400` is `1e+400` and `2^-1074` is
+ `4.9406564584124654e-324`. Refused as too large or too small: any
+ number whose numerator or denominator reaches 4096 bits, wherever it
+ appears, as `go/constant` rounds a fraction that grows that large
+ (`2^4095`, `1e-1300 + 1`); and a power computed as a double whose base
+ or result is outside the normal range of a double (`1e-400^0.5`).
- **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
diff --git a/docs/TODO.md b/docs/TODO.md
index 7fa040e..d7f18d3 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -27,6 +27,8 @@ with no deprecation warning.
# Completed Steps
+- 2026-09-29 Exact results past the range of a double, such as `2^1200`,
+ are written to 17 significant digits instead of being refused
- 2026-09-29 `GET` and `POST /api/v1/chats/{id}/messages`: a chat's
latest messages, and sending one
- 2026-09-29 Added the HTTP API in `internal/api`: the server on `PORT`,
diff --git a/internal/bot/bot_test.go b/internal/bot/bot_test.go
index 3bbb661..e76914e 100644
--- a/internal/bot/bot_test.go
+++ b/internal/bot/bot_test.go
@@ -18,6 +18,7 @@ func TestReply(t *testing.T) {
"5 * 5/2": "12.5",
"2^10": "1024",
"7 % 3": "1",
+ "2^1200": "1.7218479456385751e+361",
} {
if got := bot.Reply(in); got != want {
t.Errorf("Reply(%q) = %q, want %q", in, got, want)
@@ -27,7 +28,7 @@ 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": "That needs a number too large or too small for me.",
+ "1e1300": "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",
diff --git a/internal/calc/calc.go b/internal/calc/calc.go
index 606a1f7..b193687 100644
--- a/internal/calc/calc.go
+++ b/internal/calc/calc.go
@@ -48,6 +48,16 @@ const (
plainLower = 1e-6
)
+// A result past the normal range of a double is written to
+// significantDigits significant digits, the most the shortest form of a
+// double takes. It is rounded to them from a float of floatPrecision
+// bits, the bits a numerator or denominator can hold and 64 more for the
+// digits, so that the float rounds as the exact result would.
+const (
+ significantDigits = 17
+ floatPrecision = bitLimit + 64
+)
+
// The precedence of the binary operators: the higher, the tighter the
// operator binds.
const (
@@ -82,8 +92,7 @@ var (
)
// Evaluate computes an arithmetic expression and returns its result as
-// text: whole numbers without a decimal point, fractions in the
-// shortest form that reads back as the same float64.
+// text, written as format describes.
func Evaluate(input string) (string, error) {
s := strings.TrimSpace(input)
if len(s) > MaxInputLength {
@@ -106,7 +115,7 @@ func Evaluate(input string) (string, error) {
return "", ErrNotArithmetic
}
- return format(v)
+ return format(v), nil
}
// tokenize splits an expression into operators, parentheses and
@@ -459,26 +468,31 @@ func normal(f float64) bool {
}
// 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
-// (0.30000000000000004) that printing a binary fraction to a fixed
-// 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) {
+// ordinary size is written exactly, digit for digit. Any other result in
+// the normal range of a double goes through float64, whose shortest
+// round-trip form is free of the noise (0.30000000000000004) that
+// printing a binary fraction to a fixed precision produces. Past that
+// range a double keeps fewer digits, or none (2^-1074 would be written
+// 5e-324, and 2^1024 is infinite), so such a result is written from its
+// exact value, to significantDigits.
+func format(v constant.Value) string {
f, _ := constant.Float64Val(v)
if constant.Sign(v) != 0 && !normal(f) {
- return "", ErrOutOfRange
+ // Every number here is exact: see exact.
+ r, _ := constant.Val(v).(*big.Rat)
+
+ return new(big.Float).SetPrec(floatPrecision).SetRat(r).Text('g', significantDigits)
}
abs := math.Abs(f)
if i := constant.ToInt(v); i.Kind() == constant.Int && abs < plainUpper {
- return i.ExactString(), nil
+ return i.ExactString()
}
if abs >= plainUpper || abs < plainLower {
- return strconv.FormatFloat(f, 'g', -1, 64), nil
+ return strconv.FormatFloat(f, 'g', -1, 64)
}
- return strconv.FormatFloat(f, 'f', -1, 64), nil
+ return strconv.FormatFloat(f, 'f', -1, 64)
}
diff --git a/internal/calc/calc_test.go b/internal/calc/calc_test.go
index 21e40ee..f5b1b3f 100644
--- a/internal/calc/calc_test.go
+++ b/internal/calc/calc_test.go
@@ -52,6 +52,13 @@ func TestEvaluate(t *testing.T) {
"1234567.5": "1234567.5",
"-1 / 4": "-0.25",
"1e300 * 1e8": "1e+308",
+ // Past the normal range of a double, written from the exact value
+ // to 17 significant digits, trailing zeros dropped.
+ "1e400": "1e+400",
+ "1e300 * 1e300": "1e+600",
+ "1 / 2e-400": "5e+399",
+ "-1e-310": "-1e-310",
+ "1 / 3e400": "3.3333333333333333e-401",
})
}
@@ -82,6 +89,7 @@ func TestEvaluatePowers(t *testing.T) {
"0.1^2": "0.01",
"2^100 - 2^100 + 1": "1",
"2^64": "18446744073709551616",
+ "2^100": "1.2676506002282294e+30",
"0^0": "1",
"0^3": "0",
"1.5^2": "2.25",
@@ -99,6 +107,29 @@ func TestEvaluatePowers(t *testing.T) {
"2^-1400 * 2^1365 * 2^35": "1",
"0.3^900 * 10^470": "0.25652473503365386",
"2^1500 / 2^1000": "3.273390607896142e+150",
+ "(2^1200)/(2^1199)": "2",
+ "2^1200 % 7": "1",
+ // Results past the range of a double, written to 17 significant
+ // digits, up to the largest power of 2 under the 4096-bit limit.
+ "2^1200": "1.7218479456385751e+361",
+ "2**1200": "1.7218479456385751e+361",
+ "2^1024": "1.7976931348623159e+308",
+ "10^400": "1e+400",
+ "2^-1074": "4.9406564584124654e-324",
+ "2^-1200": "5.8077137562175032e-362",
+ "2^-1400": "3.6141491434385841e-422",
+ "0.5^1100": "7.3621518290228627e-332",
+ "1.5^2000": "1.5223626185737825e+352",
+ "(1/3)^-2000": "1.7478712517226516e+954",
+ "2^1200 - 2^1199": "8.6092397281928753e+360",
+ "-2^1201": "-3.4436958912771501e+361",
+ "(-2)^1201": "-3.4436958912771501e+361",
+ "2^2000 * 2^2000": "1.3182040934309431e+1204",
+ "10^1232": "1e+1232",
+ "2^4094": "2.6109722035328813e+1232",
+ // A power computed in float64 carries its rounding into the exact
+ // arithmetic after it, past the range of a double as within it.
+ "2^0.5 * 1e400": "1.4142135623730951e+400",
})
}
@@ -205,25 +236,22 @@ func TestEvaluateRefuses(t *testing.T) {
}
// 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.
+// float64 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,
+ // Just past the 4096-bit limit, which 2^4094 and 10^1232 are
+ // under, and far past it.
+ "2^4095": calc.ErrOutOfRange,
+ "-2^4095": calc.ErrOutOfRange,
+ "2^-4095": calc.ErrOutOfRange,
+ "10^1233": calc.ErrOutOfRange,
+ "2^4094 * 2": 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,
@@ -232,6 +260,7 @@ func TestEvaluateOutOfRange(t *testing.T) {
"1e-310^0.5": calc.ErrOutOfRange,
"(0.5^1100)^4 / (0.5^1100)^4": calc.ErrOutOfRange,
"(1/3)^1e400": calc.ErrOutOfRange,
+ "(2^1200)^0.5": 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.
@@ -309,6 +338,11 @@ func TestEvaluateBoundsWork(t *testing.T) {
// 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},
+ // Results just below the limit, written from their exact value.
+ {in: "2^4094", want: "2.6109722035328813e+1232"},
+ {in: "3^2583", want: "2.5363018640659988e+1232"},
+ {in: "2^-4094", want: "3.8299909843808741e-1233"},
+ {in: "-1/3^2583", want: "-3.9427483540814775e-1233"},
}
for _, c := range cases {