Answer fractional powers of exact numbers past a double's range (closes #16)
check / check (push) Successful in 1m21s

A fractional power turned its base into a double first and refused a
base outside the normal range of a double, so (2^1200)^0.5, (2^1024)^0.5,
(2^-1200)^0.5 and 1e400^0.5 were refused although each answer is an
ordinary double. Such a base is now brought into that range by square
roots taken from its exact value in a big.Float, at most three under the
4096-bit limit, with the exponent doubled for each, and only a result
that is not a normal double is refused. The tests give these powers and
the edges of the range their values, and the bounded-work test covers a
base that needs three roots. The README's refusal sentence and example
are updated.

Model: opus-5-5
This commit is contained in:
2026-09-29 07:37:57 +00:00
parent 4b871c2b12
commit c4094fff7d
4 changed files with 55 additions and 14 deletions
+5 -2
View File
@@ -438,8 +438,11 @@ container.
`4.9406564584124654e-324`. Refused as too large or too small: any `4.9406564584124654e-324`. Refused as too large or too small: any
number whose numerator or denominator reaches 4096 bits, wherever it number whose numerator or denominator reaches 4096 bits, wherever it
appears, as `go/constant` rounds a fraction that grows that large 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 (`2^4095`, `1e-1300 + 1`); and a power computed as a double whose
or result is outside the normal range of a double (`1e-400^0.5`). result is outside the normal range of a double (`2^1500.5`). The base
of such a power may be outside that range: square roots taken from its
exact value bring it inside first, so `(2^1200)^0.5` is
`4.149515568880993e+180` and `1e-400^0.5` is `1e-200`.
- **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
+2 -1
View File
@@ -28,7 +28,8 @@ with no deprecation warning.
# Completed Steps # Completed Steps
- 2026-09-29 Exact results past the range of a double, such as `2^1200`, - 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 are written to 17 significant digits instead of being refused, and a
fractional power of such a number, such as `(2^1200)^0.5`, is answered
- 2026-09-29 `POST`, `GET` and `DELETE` on a chat's webhooks, under - 2026-09-29 `POST`, `GET` and `DELETE` on a chat's webhooks, under
`/api/v1/chats/{id}/webhooks`, kept in `$DATA_DIR/webhooks.json` `/api/v1/chats/{id}/webhooks`, kept in `$DATA_DIR/webhooks.json`
- 2026-09-29 `GET` and `POST /api/v1/chats/{id}/messages`: a chat's - 2026-09-29 `GET` and `POST /api/v1/chats/{id}/messages`: a chat's
+21 -5
View File
@@ -52,7 +52,9 @@ const (
// significantDigits significant digits, the most the shortest form of a // significantDigits significant digits, the most the shortest form of a
// double takes. It is rounded to them from a float of floatPrecision // 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 // 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. // digits, so that the float rounds as the exact result would. The square
// roots of a power's base past that range are taken in such a float too:
// see nonNegativePower.
const ( const (
significantDigits = 17 significantDigits = 17
floatPrecision = bitLimit + 64 floatPrecision = bitLimit + 64
@@ -404,11 +406,25 @@ func nonNegativePower(x, y, n constant.Value) (constant.Value, error) {
xf, _ := constant.Float64Val(x) xf, _ := constant.Float64Val(x)
yf, _ := constant.Float64Val(y) yf, _ := constant.Float64Val(y)
f := math.Pow(xf, yf)
// Neither x nor x^y is zero. If either is not a normal double, it // x^y is (√x)^(2y). An x outside the normal range of a double, such
// has lost digits, or all of them. // as 2^1200, would lose digits as a double, or all of them, so square
if !normal(xf) || !normal(f) { // roots taken from its exact value bring it into that range first. As
// x is between 2^-4096 and 2^4096 (see exact), three at most are
// needed.
r, _ := constant.Val(x).(*big.Rat)
root := new(big.Float).SetPrec(floatPrecision).SetRat(r)
for !normal(xf) {
root.Sqrt(root)
xf, _ = root.Float64()
yf *= 2
}
// x^y is not zero. If it is not a normal double, it has lost digits,
// or all of them.
f := math.Pow(xf, yf)
if !normal(f) {
return nil, ErrOutOfRange return nil, ErrOutOfRange
} }
+27 -6
View File
@@ -130,6 +130,21 @@ func TestEvaluatePowers(t *testing.T) {
// A power computed in float64 carries its rounding into the exact // A power computed in float64 carries its rounding into the exact
// arithmetic after it, past the range of a double as within it. // arithmetic after it, past the range of a double as within it.
"2^0.5 * 1e400": "1.4142135623730951e+400", "2^0.5 * 1e400": "1.4142135623730951e+400",
// A fractional power of an exact number outside the range of a
// double, taken from its exact value, up to the edges of that
// range.
"(2^1200)^0.5": "4.149515568880993e+180",
"(2^1024)^0.5": "1.3407807929942597e+154",
"(2^-1200)^0.5": "2.409919865102884e-181",
"1e400^0.5": "1e+200",
"1e-400^0.5": "1e-200",
"1e-310^0.5": "1e-155",
"(2^1200)^-0.5": "2.409919865102884e-181",
"(2^1200)^0.5 / 2^600": "1",
"1e400^-0.001": "0.39810717055349726",
"1e-400^0.001": "0.39810717055349726",
"(2^2047)^0.5": "1.2711610061536464e+308",
"(2^-2044)^0.5": "2.2250738585072014e-308",
}) })
} }
@@ -252,15 +267,17 @@ func TestEvaluateOutOfRange(t *testing.T) {
"2^5000": calc.ErrOutOfRange, "2^5000": calc.ErrOutOfRange,
"(-2)^5001": calc.ErrOutOfRange, "(-2)^5001": calc.ErrOutOfRange,
"0.5^-5000": calc.ErrOutOfRange, "0.5^-5000": calc.ErrOutOfRange,
// Powers computed in float64 whose base or result is not a // Powers computed in float64 whose result is not a normal double,
// normal double, and so has lost digits, or all of them. // and so has lost digits, or all of them, whatever the size of
// the base.
"2^-1073.5 * 2^1073": calc.ErrOutOfRange, "2^-1073.5 * 2^1073": calc.ErrOutOfRange,
"1e400^-0.001": calc.ErrOutOfRange, "2^1500.5": calc.ErrOutOfRange,
"1e-400^0.001": calc.ErrOutOfRange, "(2^1200)^0.9": calc.ErrOutOfRange,
"1e-310^0.5": calc.ErrOutOfRange, "1e-400^0.9": calc.ErrOutOfRange,
"(2^2048)^0.5": calc.ErrOutOfRange,
"(2^-2046)^0.5": calc.ErrOutOfRange,
"(0.5^1100)^4 / (0.5^1100)^4": calc.ErrOutOfRange, "(0.5^1100)^4 / (0.5^1100)^4": calc.ErrOutOfRange,
"(1/3)^1e400": 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 // 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 // can lose the answer (this one would be 0), and so can a
// remainder or the sign of -1 to such a power. // remainder or the sign of -1 to such a power.
@@ -343,6 +360,10 @@ func TestEvaluateBoundsWork(t *testing.T) {
{in: "3^2583", want: "2.5363018640659988e+1232"}, {in: "3^2583", want: "2.5363018640659988e+1232"},
{in: "2^-4094", want: "3.8299909843808741e-1233"}, {in: "2^-4094", want: "3.8299909843808741e-1233"},
{in: "-1/3^2583", want: "-3.9427483540814775e-1233"}, {in: "-1/3^2583", want: "-3.9427483540814775e-1233"},
// Fractional powers of numbers just below the limit, whose bases
// take the most square roots to bring into the range of a double.
{in: "(2^-4094)^0.125", want: "8.869511863657883e-155"},
{in: "(1/3^2583)^0.5", err: calc.ErrOutOfRange},
} }
for _, c := range cases { for _, c := range cases {