Write exact results past the range of a double (closes #16) #18

Merged
clawbot merged 3 commits from issue-16-exact-results-past-float64 into next 2026-09-29 09:56:56 +02:00
4 changed files with 55 additions and 14 deletions
Showing only changes of commit c4094fff7d - Show all commits
+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 {