next: exponentiation, modulo, chat API and webhooks #10

Open
clawbot wants to merge 8 commits from next into main
5 changed files with 140 additions and 44 deletions
Showing only changes of commit 0b9121a806 - Show all commits
+17 -10
View File
@@ -426,16 +426,23 @@ container.
refused, exact powers are capped, and every number is held as a refused, exact powers are capped, and every number is held as a
fraction, whole numbers too, under the 4096-bit limit below, so a fraction, whole numbers too, under the 4096-bit limit below, so a
message cannot make the bot do unbounded work. Whole numbers below message cannot make the bot do unbounded work. Whole numbers below
10<sup>21</sup> are written exactly; other results in the shortest 10<sup>21</sup> are written exactly. Other results inside the normal
form that reads back as the same double, in exponent notation from range of a double, about 2.2e-308 to 1.8e308 in magnitude, where a
10<sup>21</sup> up and below 10<sup>-6</sup>. Refused as too large or double keeps all its digits, are written in the shortest form that
too small: any number whose numerator or denominator reaches 4096 reads back as the same double, in exponent notation from
bits, wherever it appears, as `go/constant` rounds a fraction that 10<sup>21</sup> up and below 10<sup>-6</sup>. Results outside that
grows that large (`1e-1300 + 1`); a power computed as a double whose range are written from their exact value in exponent notation, rounded
base or result is outside the normal range of a double, about 2.2e-308 to 17 significant digits, the most the shortest form of a double
to 1.8e308 in magnitude, where a double keeps all its digits takes, with trailing zeros dropped: `2^1200` is
(`1e-400^0.5`); and a result other than zero outside that range, as it `1.7218479456385751e+361`, `10^400` is `1e+400` and `2^-1074` is
is written through a double (`1e400`, `2^-1400`). `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 - **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
+3
View File
@@ -27,6 +27,9 @@ with no deprecation warning.
# Completed Steps # 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 `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
+2 -1
View File
@@ -18,6 +18,7 @@ func TestReply(t *testing.T) {
"5 * 5/2": "12.5", "5 * 5/2": "12.5",
"2^10": "1024", "2^10": "1024",
"7 % 3": "1", "7 % 3": "1",
"2^1200": "1.7218479456385751e+361",
} { } {
if got := bot.Reply(in); got != want { if got := bot.Reply(in); got != want {
t.Errorf("Reply(%q) = %q, want %q", 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{ for in, want := range map[string]string{
"hello": "I only understand arithmetic", "hello": "I only understand arithmetic",
"1 / 0": "I cannot divide by zero.", "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.", "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", "(-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", strings.Repeat("1+", calc.MaxInputLength) + "1": "That is too long for me",
+47 -17
View File
@@ -48,6 +48,18 @@ const (
plainLower = 1e-6 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 // The precedence of the binary operators: the higher, the tighter the
// operator binds. // operator binds.
const ( const (
@@ -82,8 +94,7 @@ var (
) )
// Evaluate computes an arithmetic expression and returns its result as // Evaluate computes an arithmetic expression and returns its result as
// text: whole numbers without a decimal point, fractions in the // text, written as format describes.
// shortest form that reads back as the same float64.
func Evaluate(input string) (string, error) { func Evaluate(input string) (string, error) {
s := strings.TrimSpace(input) s := strings.TrimSpace(input)
if len(s) > MaxInputLength { if len(s) > MaxInputLength {
@@ -106,7 +117,7 @@ func Evaluate(input string) (string, error) {
return "", ErrNotArithmetic return "", ErrNotArithmetic
} }
return format(v) return format(v), nil
} }
// tokenize splits an expression into operators, parentheses and // 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) 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
} }
@@ -459,26 +484,31 @@ func normal(f float64) bool {
} }
// format writes a result for a person to read. A whole number of // format writes a result for a person to read. A whole number of
// ordinary size is written exactly, digit for digit; anything else goes // ordinary size is written exactly, digit for digit. Any other result in
// through float64, whose shortest round-trip form is free of the noise // the normal range of a double goes through float64, whose shortest
// (0.30000000000000004) that printing a binary fraction to a fixed // round-trip form is free of the noise (0.30000000000000004) that
// precision produces. A result that is not zero must therefore be a // printing a binary fraction to a fixed precision produces. Past that
// normal double: 2^-1074 would be written 5e-324. // range a double keeps fewer digits, or none (2^-1074 would be written
func format(v constant.Value) (string, error) { // 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) f, _ := constant.Float64Val(v)
if constant.Sign(v) != 0 && !normal(f) { 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) abs := math.Abs(f)
if i := constant.ToInt(v); i.Kind() == constant.Int && abs < plainUpper { if i := constant.ToInt(v); i.Kind() == constant.Int && abs < plainUpper {
return i.ExactString(), nil return i.ExactString()
} }
if abs >= plainUpper || abs < plainLower { 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
View File
@@ -52,6 +52,13 @@ func TestEvaluate(t *testing.T) {
"1234567.5": "1234567.5", "1234567.5": "1234567.5",
"-1 / 4": "-0.25", "-1 / 4": "-0.25",
"1e300 * 1e8": "1e+308", "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", "0.1^2": "0.01",
"2^100 - 2^100 + 1": "1", "2^100 - 2^100 + 1": "1",
"2^64": "18446744073709551616", "2^64": "18446744073709551616",
"2^100": "1.2676506002282294e+30",
"0^0": "1", "0^0": "1",
"0^3": "0", "0^3": "0",
"1.5^2": "2.25", "1.5^2": "2.25",
@@ -99,6 +107,44 @@ func TestEvaluatePowers(t *testing.T) {
"2^-1400 * 2^1365 * 2^35": "1", "2^-1400 * 2^1365 * 2^35": "1",
"0.3^900 * 10^470": "0.25652473503365386", "0.3^900 * 10^470": "0.25652473503365386",
"2^1500 / 2^1000": "3.273390607896142e+150", "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 // TestEvaluateOutOfRange: a number is held exactly, or computed in
// float64 as a normal double, and a result is written as a normal // float64 as a normal double. Anything else is refused.
// double. Anything else is refused.
func TestEvaluateOutOfRange(t *testing.T) { func TestEvaluateOutOfRange(t *testing.T) {
t.Parallel() t.Parallel()
expectErrors(t, map[string]error{ expectErrors(t, map[string]error{
// Results that are not normal doubles: 2^-1074 would be written // Just past the 4096-bit limit, which 2^4094 and 10^1232 are
// 5e-324. // under, and far past it.
"1e400": calc.ErrOutOfRange, "2^4095": calc.ErrOutOfRange,
"1e300 * 1e300": calc.ErrOutOfRange, "-2^4095": calc.ErrOutOfRange,
"2^-4095": calc.ErrOutOfRange,
"10^1233": calc.ErrOutOfRange,
"2^4094 * 2": calc.ErrOutOfRange,
"1e999999999 * 1e999999999": calc.ErrOutOfRange, "1e999999999 * 1e999999999": calc.ErrOutOfRange,
"1 / 1e-400": calc.ErrOutOfRange,
"2^1024": calc.ErrOutOfRange,
"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,
"2^-1074": calc.ErrOutOfRange, // Powers computed in float64 whose result is not a normal double,
"2^-1400": calc.ErrOutOfRange, // and so has lost digits, or all of them, whatever the size of
"-1e-310": calc.ErrOutOfRange, // the base.
// 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, "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,
// go/constant holds numbers of this size rounded. A sum of them // 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. // the limit, and a literal whose exponent is too large to read.
{in: "(3^2583/5^1760)^4096", err: calc.ErrOutOfRange}, {in: "(3^2583/5^1760)^4096", err: calc.ErrOutOfRange},
{in: "1e99999999999999999999", 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 { for _, c := range cases {