Powers and remainders in the calculator (closes #3) #11

Merged
clawbot merged 4 commits from issue-3-power-modulo into next 2026-09-29 04:28:16 +02:00
3 changed files with 43 additions and 32 deletions
Showing only changes of commit 3a8f1cc334 - Show all commits
+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 {