Every number a fraction under the 4096-bit limit (closes #3)
check / check (push) Successful in 1m11s

go/constant never rounds an integer, and x^0 returned one, so whole
numbers built from it escaped the 4096-bit limit:
(((2^0+2^0)^4096)^4096)^4096 asked for about 69 billion bits and
stalled the bot. A power now starts from the fraction 1, and a number
is exact only as a fraction whose numerator and denominator are below
4096 bits, which also refuses a literal that go/constant reads exactly
past the limit, such as 1e-1233. The bounded-work test holds the tower
and the other short inputs tried against the change.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-29 02:09:33 +00:00
parent a1125e59dd
commit 3f5e453a18
3 changed files with 67 additions and 35 deletions
+13 -12
View File
@@ -172,18 +172,19 @@ container.
computed as a double, so `2^0.5` is `1.4142135623730951`. A negative computed as a double, so `2^0.5` is `1.4142135623730951`. A negative
number to a fractional power is refused, as having no real result. number to a fractional power is refused, as having no real result.
Numbers are read as decimal, so `010` is ten. Input over 256 bytes is Numbers are read as decimal, so `010` is ten. Input over 256 bytes is
refused and exact powers are capped, so a message cannot make the bot refused, exact powers are capped, and every number is held as a
do unbounded work. Whole numbers below 10<sup>21</sup> are written fraction, whole numbers too, under the 4096-bit limit below, so a
exactly; other results in the shortest form that reads back as the message cannot make the bot do unbounded work. Whole numbers below
same double, in exponent notation from 10<sup>21</sup> up and below 10<sup>21</sup> are written exactly; other results in the shortest
10<sup>-6</sup>. Refused as too large or too small: any number whose form that reads back as the same double, in exponent notation from
numerator or denominator reaches 4096 bits, wherever it appears, as 10<sup>21</sup> up and below 10<sup>-6</sup>. Refused as too large or
`go/constant` could hold it only rounded (`1e-1300 + 1`); a power too small: any number whose numerator or denominator reaches 4096
computed as a double whose base or result is outside the normal range bits, wherever it appears, as `go/constant` rounds a fraction that
of a double, about 2.2e-308 to 1.8e308 in magnitude, where a double grows that large (`1e-1300 + 1`); a power computed as a double whose
keeps all its digits (`1e-400^0.5`); and a result other than zero base or result is outside the normal range of a double, about 2.2e-308
outside that range, as it is written through a double (`1e400`, to 1.8e308 in magnitude, where a double keeps all its digits
`2^-1400`). (`1e-400^0.5`); and a result other than zero outside that range, as it
is written through a double (`1e400`, `2^-1400`).
- **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
+33 -23
View File
@@ -21,11 +21,15 @@ import (
"strings" "strings"
) )
// MaxInputLength caps an expression, in bytes, and maxExactExponent caps // MaxInputLength caps an expression, in bytes. With maxExactExponent,
// a power computed exactly, so that a message cannot make the bot do // which caps a power computed exactly, and bitLimit, which caps every
// unbounded work. // number, it keeps a message from making the bot do unbounded work.
const MaxInputLength = 256 const MaxInputLength = 256
// bitLimit caps the numerator and denominator of every number: see
// exact.
const bitLimit = 4096
// maxExactExponent is the largest exponent, either way, of a power // maxExactExponent is the largest exponent, either way, of a power
// computed exactly. Past it, x^n has a numerator or denominator of more // computed exactly. Past it, x^n has a numerator or denominator of more
// than 4096 bits, which go/constant holds only rounded, unless x is 0 or // than 4096 bits, which go/constant holds only rounded, unless x is 0 or
@@ -245,11 +249,11 @@ func number(tok string) (constant.Value, error) {
return nil, ErrNotArithmetic return nil, ErrNotArithmetic
} }
// Read as FLOAT, which makes every literal decimal: as INT, a // Read as FLOAT, which makes every literal decimal and a fraction
// leading zero would make it octal. // (see exact): as INT, a leading zero would make it octal.
v := constant.MakeFromLiteral(tok, token.FLOAT, 0) v := constant.MakeFromLiteral(tok, token.FLOAT, 0)
// A literal such as 1e1300 or 1e-1300 is held rounded: see exact. // A literal such as 1e1300 or 1e-1233 is past bitLimit: see exact.
if !exact(v) { if !exact(v) {
return nil, ErrOutOfRange return nil, ErrOutOfRange
} }
@@ -403,10 +407,13 @@ func nonNegativePower(x, y, n constant.Value) (constant.Value, error) {
} }
// exactPower computes x^e by repeated squaring. x is not zero if e is // exactPower computes x^e by repeated squaring. x is not zero if e is
// negative. Each step's numbers stay small: go/constant holds one whose // negative. It starts from 1 as a fraction, a Float to go/constant, so
// numerator or denominator reaches 4096 bits as a 512-bit float. // that x^0 is a fraction like every other number (see exact). Each
// step's numbers stay small: go/constant holds one whose numerator or
// denominator reaches 4096 bits as a 512-bit float.
func exactPower(x constant.Value, e int64) constant.Value { func exactPower(x constant.Value, e int64) constant.Value {
result := constant.MakeInt64(1) one := constant.MakeFloat64(1)
result := one
for n := max(e, -e); n > 0; n >>= 1 { for n := max(e, -e); n > 0; n >>= 1 {
if n&1 == 1 { if n&1 == 1 {
@@ -417,26 +424,29 @@ func exactPower(x constant.Value, e int64) constant.Value {
} }
if e < 0 { if e < 0 {
result = constant.BinaryOp(constant.MakeInt64(1), token.QUO, result) result = constant.BinaryOp(one, token.QUO, result)
} }
return result return result
} }
// exact reports whether go/constant holds v exactly. It holds a number // exact reports whether v is a fraction whose numerator and denominator
// as a fraction until its numerator or denominator reaches 4096 bits, // are both below bitLimit bits, as every number here must be, so that
// then as a 512-bit float, and past that float's range as Unknown. A // each step of arithmetic stays small. go/constant never rounds an
// number not held exactly is refused wherever it appears: a sum can lose // integer, however large, so every number is made a fraction: literals
// the answer entirely (7^1000*7^1000 + 5 - 7^1000*7^1000 would be 0), and // are read as FLOAT, and a power starts from the fraction 1. It rounds
// a remainder, or whether an exponent is whole or odd, cannot be read // a fraction that grows past the limit, to a 512-bit float and past
// from one. // that float's range to Unknown, but not one it reads from a literal,
// such as 1e-1233, so the limit is checked here.
//
// A number that is not exact is refused wherever it appears: a sum can
// lose the answer entirely (7^1000*7^1000 + 5 - 7^1000*7^1000 would be
// 0), and a remainder, or whether an exponent is whole or odd, cannot be
// read from one.
func exact(v constant.Value) bool { func exact(v constant.Value) bool {
switch constant.Val(v).(type) { r, ok := constant.Val(v).(*big.Rat)
case int64, *big.Int, *big.Rat:
return true return ok && r.Num().BitLen() < bitLimit && r.Denom().BitLen() < bitLimit
default:
return false
}
} }
// normal reports whether f is a normal double, finite and at least // normal reports whether f is a normal double, finite and at least
+21
View File
@@ -130,6 +130,8 @@ func TestEvaluateModulo(t *testing.T) {
// Both operands and their quotient are held exactly, but y times // Both operands and their quotient are held exactly, but y times
// the whole part of x/y is too large to be. // the whole part of x/y is too large to be.
"(5^860*3^630/7) % (5^860/2^998/2^998)": "0.5179219763783696", "(5^860*3^630/7) % (5^860/2^998/2^998)": "0.5179219763783696",
// A whole number made from x^0, just below the 4096-bit limit.
"(3^0 + 3^0 + 3^0)^2583 % 10": "7",
}) })
} }
@@ -243,6 +245,11 @@ func TestEvaluateOutOfRange(t *testing.T) {
"0.1^800 * 0.1^800": calc.ErrOutOfRange, "0.1^800 * 0.1^800": calc.ErrOutOfRange,
// Both operands are held exactly, but their quotient is not. // Both operands are held exactly, but their quotient is not.
"3^1365 % 7^-1000": calc.ErrOutOfRange, "3^1365 % 7^-1000": calc.ErrOutOfRange,
// The same limit for a whole number made from x^0, which
// go/constant would hold as an integer and never round, and for
// a literal it reads exactly as a fraction past the limit.
"(2^0 + 2^0)^4095 % 10": calc.ErrOutOfRange,
"1e-1233 * 0": calc.ErrOutOfRange,
// go/constant reads this literal as 0. // go/constant reads this literal as 0.
"1e-999999999": calc.ErrOutOfRange, "1e-999999999": calc.ErrOutOfRange,
"1 / 1e-999999999": calc.ErrOutOfRange, "1 / 1e-999999999": calc.ErrOutOfRange,
@@ -288,6 +295,20 @@ func TestEvaluateBoundsWork(t *testing.T) {
{in: strings.Repeat("9^", 127) + "9", err: calc.ErrOutOfRange}, {in: strings.Repeat("9^", 127) + "9", err: calc.ErrOutOfRange},
// The largest power of 3 computed exactly, as often as fits. // The largest power of 3 computed exactly, as often as fits.
{in: "0" + strings.Repeat("*3^2583", 36), want: "0"}, {in: "0" + strings.Repeat("*3^2583", 36), want: "0"},
// Whole numbers made from x^0, through each operation. Held as
// integers, which go/constant never rounds, they would escape
// the 4096-bit limit: the first needs about 69 billion bits.
{in: "(((2^0+2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "(((0^0+0^0)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "(((-2^0-2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "((2^0+2^0)^4000*(2^0+2^0)^4000)^4096", err: calc.ErrOutOfRange},
{in: "((((2^0+2^0)/2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "((((2^0+2^0) % 3)^4096)^4096)^4096", err: calc.ErrOutOfRange},
{in: "(((2^0+2^0)^4096)^4096)^4096 * 0", err: calc.ErrOutOfRange},
// A fraction whose numerator and denominator are both just below
// 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},
} }
for _, c := range cases { for _, c := range cases {