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 {