Brings in the exact results past the range of a double. docs/TODO.md conflicted: both Completed Steps lines are kept, the newer one first. Model: opus-5-5
This commit is contained in:
@@ -478,16 +478,23 @@ 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
|
||||
10<sup>21</sup> are written exactly; other results in the shortest
|
||||
form that reads back as the same double, in exponent notation from
|
||||
10<sup>21</sup> up and below 10<sup>-6</sup>. 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`).
|
||||
10<sup>21</sup> 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
|
||||
10<sup>21</sup> up and below 10<sup>-6</sup>. 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
|
||||
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
|
||||
it drops, the bot exits with an error and the container's restart
|
||||
policy starts both again. `SIGTERM` stops the bot, which stops the
|
||||
|
||||
@@ -27,6 +27,9 @@ 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, and a
|
||||
fractional power of such a number, such as `(2^1200)^0.5`, is answered
|
||||
- 2026-09-29 Each new incoming message is posted to its chat's webhooks
|
||||
- 2026-09-29 `POST`, `GET` and `DELETE` on a chat's webhooks, under
|
||||
`/api/v1/chats/{id}/webhooks`, kept in `$DATA_DIR/webhooks.json`
|
||||
|
||||
@@ -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",
|
||||
|
||||
+47
-17
@@ -48,6 +48,18 @@ 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. The square
|
||||
// roots of a power's base past that range are taken in such a float too:
|
||||
// see nonNegativePower.
|
||||
const (
|
||||
significantDigits = 17
|
||||
floatPrecision = bitLimit + 64
|
||||
)
|
||||
|
||||
// The precedence of the binary operators: the higher, the tighter the
|
||||
// operator binds.
|
||||
const (
|
||||
@@ -82,8 +94,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 +117,7 @@ func Evaluate(input string) (string, error) {
|
||||
return "", ErrNotArithmetic
|
||||
}
|
||||
|
||||
return format(v)
|
||||
return format(v), nil
|
||||
}
|
||||
|
||||
// tokenize splits an expression into operators, parentheses and
|
||||
@@ -395,11 +406,25 @@ func nonNegativePower(x, y, n constant.Value) (constant.Value, error) {
|
||||
|
||||
xf, _ := constant.Float64Val(x)
|
||||
yf, _ := constant.Float64Val(y)
|
||||
f := math.Pow(xf, yf)
|
||||
|
||||
// Neither x nor x^y is zero. If either is not a normal double, it
|
||||
// has lost digits, or all of them.
|
||||
if !normal(xf) || !normal(f) {
|
||||
// x^y is (√x)^(2y). An x outside the normal range of a double, such
|
||||
// as 2^1200, would lose digits as a double, or all of them, so square
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -459,26 +484,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)
|
||||
}
|
||||
|
||||
+71
-16
@@ -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,44 @@ 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",
|
||||
// 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",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,31 +251,31 @@ 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.
|
||||
// Powers computed in float64 whose result is not a normal double,
|
||||
// and so has lost digits, or all of them, whatever the size of
|
||||
// the base.
|
||||
"2^-1073.5 * 2^1073": calc.ErrOutOfRange,
|
||||
"1e400^-0.001": calc.ErrOutOfRange,
|
||||
"1e-400^0.001": calc.ErrOutOfRange,
|
||||
"1e-310^0.5": calc.ErrOutOfRange,
|
||||
"2^1500.5": calc.ErrOutOfRange,
|
||||
"(2^1200)^0.9": 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,
|
||||
"(1/3)^1e400": calc.ErrOutOfRange,
|
||||
// go/constant holds numbers of this size rounded. A sum of them
|
||||
@@ -309,6 +355,15 @@ 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"},
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user