Exact remainders; refuse numbers held rounded (closes #3)
check / check (push) Successful in 1m14s

x % y is now y times the fractional part of x/y. The old form, x minus
y times the whole part of x/y, passed through a product that go/constant
could hold only rounded even when both operands and the remainder were
exact, and then replied 0.

A number whose numerator or denominator reaches 4096 bits, which
go/constant holds rounded, is now refused as too large wherever it
appears, literals included. A sum of such numbers could lose the answer,
and a rounded exponent near a whole number was computed as an exact
power. This replaces the separate check on a negative base's exponent.

Model: opus-5-5
This commit is contained in:
clawbot
2026-09-29 00:42:19 +00:00
parent aeb040d156
commit 3a8f1cc334
3 changed files with 43 additions and 32 deletions
+4 -1
View File
@@ -176,7 +176,10 @@ container.
Whole numbers below 10<sup>21</sup> are written exactly; other results 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 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>. A result notation from 10<sup>21</sup> up and below 10<sup>-6</sup>. A result
beyond the range of a double is refused as too large. beyond the range of a double is refused as too large, and so is any
number, even a small one such as `1e-1300` or one inside a longer
expression, whose numerator or denominator reaches 4096 bits:
`go/constant` could hold it only rounded.
- **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
+25 -25
View File
@@ -246,8 +246,8 @@ func number(tok string) (constant.Value, error) {
v := constant.MakeFromLiteral(tok, token.FLOAT, 0) v := constant.MakeFromLiteral(tok, token.FLOAT, 0)
// The syntax was checked above, so Unknown here means the exponent // The syntax was checked above, so Unknown here means the exponent
// overflowed. // overflowed. A literal such as 1e1300 is held rounded: see rounded.
if v.Kind() == constant.Unknown { if v.Kind() == constant.Unknown || rounded(v) {
return nil, ErrTooLarge return nil, ErrTooLarge
} }
@@ -282,8 +282,9 @@ func apply(x constant.Value, op string, y constant.Value) (constant.Value, error
return nil, err return nil, err
} }
// go/constant represents an overflow to infinity as Unknown. // go/constant represents an overflow to infinity as Unknown. A
if v.Kind() == constant.Unknown { // rounded number is refused too: see rounded.
if v.Kind() == constant.Unknown || rounded(v) {
return nil, ErrTooLarge return nil, ErrTooLarge
} }
@@ -309,22 +310,28 @@ func modulo(x, y constant.Value) (constant.Value, error) {
return nil, err return nil, err
} }
// The whole part of a rounded quotient, and so the remainder, would // The fractional part of a rounded quotient, and so the remainder,
// be wrong. // would be wrong.
if rounded(q) { if rounded(q) {
return nil, ErrTooLarge return nil, ErrTooLarge
} }
// token.QUO_ASSIGN is go/constant's integer division, which // x % y is y times the fractional part of x/y, which is at least 0
// truncates, so r has the sign of x and is less than y in size. // and less than 1, so the result has the sign of y. It is not
whole := constant.BinaryOp(constant.Num(q), token.QUO_ASSIGN, constant.Denom(q)) // computed as x minus y times the whole part of x/y: that product
r := constant.BinaryOp(x, token.SUB, constant.BinaryOp(y, token.MUL, whole)) // can be too large to hold exactly when the remainder is not.
//
// For x/y = n/d the fractional part is (n mod d)/d, exact because d
// is. token.REM truncates, leaving the sign of n; adding d brings a
// negative one into range.
n, d := constant.Num(q), constant.Denom(q)
if constant.Sign(r) != 0 && constant.Sign(r) != constant.Sign(y) { m := constant.BinaryOp(n, token.REM, d)
r = constant.BinaryOp(r, token.ADD, y) if constant.Sign(m) < 0 {
m = constant.BinaryOp(m, token.ADD, d)
} }
return r, nil return constant.BinaryOp(y, token.MUL, constant.BinaryOp(m, token.QUO, d)), nil
} }
// power computes x^y. A negative x needs a whole y, and its sign is // power computes x^y. A negative x needs a whole y, and its sign is
@@ -339,9 +346,6 @@ func power(x, y constant.Value) (constant.Value, error) {
return nil, ErrDivisionByZero return nil, ErrDivisionByZero
case constant.Sign(x) >= 0: case constant.Sign(x) >= 0:
return nonNegativePower(x, y, n), nil return nonNegativePower(x, y, n), nil
case rounded(y):
// Whether a rounded y is whole, or odd, is unknown.
return nil, ErrTooLarge
case n.Kind() != constant.Int: case n.Kind() != constant.Int:
return nil, ErrNoRealResult return nil, ErrNoRealResult
} }
@@ -381,14 +385,7 @@ func exactPowerFits(x constant.Value, e int64) bool {
return false return false
} }
// Num and Denom are Unknown for a value too large or too small to bits := int64(constant.BitLen(constant.Num(x)) + constant.BitLen(constant.Denom(x)))
// be held as a fraction.
num, den := constant.Num(x), constant.Denom(x)
if num.Kind() != constant.Int {
return false
}
bits := int64(constant.BitLen(num) + constant.BitLen(den))
return bits*max(e, -e) <= maxExactPowerBits return bits*max(e, -e) <= maxExactPowerBits
} }
@@ -416,7 +413,10 @@ func exactPower(x constant.Value, e int64) constant.Value {
// rounded reports whether go/constant holds v rounded. It holds a // rounded reports whether go/constant holds v rounded. It holds a
// number exactly, as a fraction, only while the numerator and the // number exactly, as a fraction, only while the numerator and the
// denominator each stay under 4096 bits; past that, and for a literal of // denominator each stay under 4096 bits; past that, and for a literal of
// that size, it holds a 512-bit float. // that size, it holds a 512-bit float. Such a number is refused as too
// large 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 rounded(v constant.Value) bool { func rounded(v constant.Value) bool {
_, isFloat := constant.Val(v).(*big.Float) _, isFloat := constant.Val(v).(*big.Float)
+14 -6
View File
@@ -121,6 +121,9 @@ func TestEvaluateModulo(t *testing.T) {
"10 / 8 % 1": "0.25", "10 / 8 % 1": "0.25",
"(7 % 3)^2": "1", "(7 % 3)^2": "1",
"7 % (3 ^ 2)": "7", "7 % (3 ^ 2)": "7",
// Both operands and their quotient are held exactly, but y times
// the whole part of x/y is too large to be.
"(5^860*3^630/7) % (5^860/2^998/2^998)": "0.5179219763783696",
}) })
} }
@@ -198,11 +201,16 @@ func TestEvaluateRefuses(t *testing.T) {
"2^5000": calc.ErrTooLarge, "2^5000": calc.ErrTooLarge,
"(-2)^5001": calc.ErrTooLarge, "(-2)^5001": calc.ErrTooLarge,
"0.5^-5000": calc.ErrTooLarge, "0.5^-5000": calc.ErrTooLarge,
// go/constant holds a product of this size rounded, so the // go/constant holds numbers of this size rounded. A sum of them
// remainder, or the sign of -1 to its power, cannot be known. // can lose the answer (this one would be 0), and so can a
"7^1000 * 7^1000 / 7^1000 % 10": calc.ErrTooLarge, // remainder or the sign of -1 to such a power.
"(-1)^(3^1365 * 3^1365 / 3^1365)": calc.ErrTooLarge, "7^1000 * 7^1000 + 5 - 7^1000 * 7^1000": calc.ErrTooLarge,
"(-1)^1e1300": calc.ErrTooLarge, "7^1000 * 7^1000 / 7^1000 % 10": calc.ErrTooLarge,
"(-1)^(3^1365 * 3^1365 / 3^1365)": calc.ErrTooLarge,
"(-1)^1e1300": calc.ErrTooLarge,
"1e-1300": calc.ErrTooLarge,
// Both operands are held exactly, but their quotient is not.
"3^1365 % 7^-1000": calc.ErrTooLarge,
} }
for in, want := range cases { for in, want := range cases {
@@ -236,7 +244,7 @@ func TestEvaluateBoundsWork(t *testing.T) {
// The longest tower that fits. // The longest tower that fits.
{in: strings.Repeat("9^", 127) + "9", err: calc.ErrTooLarge}, {in: strings.Repeat("9^", 127) + "9", err: calc.ErrTooLarge},
// The largest power computed exactly, as often as fits. // The largest power computed exactly, as often as fits.
{in: strings.Repeat("3^1365*", 36) + "0", want: "0"}, {in: "0" + strings.Repeat("*3^1365", 36), want: "0"},
} }
for _, c := range cases { for _, c := range cases {