diff --git a/README.md b/README.md
index 185c948..9ebf63e 100644
--- a/README.md
+++ b/README.md
@@ -176,7 +176,10 @@ container.
Whole numbers below 1021 are written exactly; other results
in the shortest form that reads back as the same double, in exponent
notation from 1021 up and below 10-6. 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
it drops, the bot exits with an error and the container's restart
policy starts both again. `SIGTERM` stops the bot, which stops the
diff --git a/internal/calc/calc.go b/internal/calc/calc.go
index 773826d..96df3fb 100644
--- a/internal/calc/calc.go
+++ b/internal/calc/calc.go
@@ -246,8 +246,8 @@ func number(tok string) (constant.Value, error) {
v := constant.MakeFromLiteral(tok, token.FLOAT, 0)
// The syntax was checked above, so Unknown here means the exponent
- // overflowed.
- if v.Kind() == constant.Unknown {
+ // overflowed. A literal such as 1e1300 is held rounded: see rounded.
+ if v.Kind() == constant.Unknown || rounded(v) {
return nil, ErrTooLarge
}
@@ -282,8 +282,9 @@ func apply(x constant.Value, op string, y constant.Value) (constant.Value, error
return nil, err
}
- // go/constant represents an overflow to infinity as Unknown.
- if v.Kind() == constant.Unknown {
+ // go/constant represents an overflow to infinity as Unknown. A
+ // rounded number is refused too: see rounded.
+ if v.Kind() == constant.Unknown || rounded(v) {
return nil, ErrTooLarge
}
@@ -309,22 +310,28 @@ func modulo(x, y constant.Value) (constant.Value, error) {
return nil, err
}
- // The whole part of a rounded quotient, and so the remainder, would
- // be wrong.
+ // The fractional part of a rounded quotient, and so the remainder,
+ // would be wrong.
if rounded(q) {
return nil, ErrTooLarge
}
- // token.QUO_ASSIGN is go/constant's integer division, which
- // truncates, so r has the sign of x and is less than y in size.
- whole := constant.BinaryOp(constant.Num(q), token.QUO_ASSIGN, constant.Denom(q))
- r := constant.BinaryOp(x, token.SUB, constant.BinaryOp(y, token.MUL, whole))
+ // x % y is y times the fractional part of x/y, which is at least 0
+ // and less than 1, so the result has the sign of y. It is not
+ // computed as x minus y times the whole part of x/y: that product
+ // 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) {
- r = constant.BinaryOp(r, token.ADD, y)
+ m := constant.BinaryOp(n, token.REM, d)
+ 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
@@ -339,9 +346,6 @@ func power(x, y constant.Value) (constant.Value, error) {
return nil, ErrDivisionByZero
case constant.Sign(x) >= 0:
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:
return nil, ErrNoRealResult
}
@@ -381,14 +385,7 @@ func exactPowerFits(x constant.Value, e int64) bool {
return false
}
- // Num and Denom are Unknown for a value too large or too small to
- // 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))
+ bits := int64(constant.BitLen(constant.Num(x)) + constant.BitLen(constant.Denom(x)))
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
// number exactly, as a fraction, only while the numerator and the
// 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 {
_, isFloat := constant.Val(v).(*big.Float)
diff --git a/internal/calc/calc_test.go b/internal/calc/calc_test.go
index 87ce0fa..ef44558 100644
--- a/internal/calc/calc_test.go
+++ b/internal/calc/calc_test.go
@@ -121,6 +121,9 @@ func TestEvaluateModulo(t *testing.T) {
"10 / 8 % 1": "0.25",
"(7 % 3)^2": "1",
"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)^5001": calc.ErrTooLarge,
"0.5^-5000": calc.ErrTooLarge,
- // go/constant holds a product of this size rounded, so the
- // remainder, or the sign of -1 to its power, cannot be known.
- "7^1000 * 7^1000 / 7^1000 % 10": calc.ErrTooLarge,
- "(-1)^(3^1365 * 3^1365 / 3^1365)": calc.ErrTooLarge,
- "(-1)^1e1300": calc.ErrTooLarge,
+ // 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
+ // remainder or the sign of -1 to such a power.
+ "7^1000 * 7^1000 + 5 - 7^1000 * 7^1000": 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 {
@@ -236,7 +244,7 @@ func TestEvaluateBoundsWork(t *testing.T) {
// The longest tower that fits.
{in: strings.Repeat("9^", 127) + "9", err: calc.ErrTooLarge},
// 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 {