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 67 additions and 35 deletions
Showing only changes of commit 3f5e453a18 - Show all commits
+13 -12
View File
@@ -172,18 +172,19 @@ container.
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.
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
do unbounded work. 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 notation from 10<sup>21</sup> up and below
10<sup>-6</sup>. Refused as too large or too small: any number whose
numerator or denominator reaches 4096 bits, wherever it appears, as
`go/constant` could hold it only rounded (`1e-1300 + 1`); a power
computed as a double whose base or result is outside the normal range
of a double, about 2.2e-308 to 1.8e308 in magnitude, where a double
keeps all its digits (`1e-400^0.5`); and a result other than zero
outside that range, as it is written through a double (`1e400`,
`2^-1400`).
refused, exact powers are capped, and every number is held as a
fraction, whole numbers too, under the 4096-bit limit below, so a
message cannot make the bot do unbounded work. 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 notation from
10<sup>21</sup> up and below 10<sup>-6</sup>. Refused as too large or
too small: any number whose numerator or denominator reaches 4096
bits, wherever it appears, as `go/constant` rounds a fraction that
grows that large (`1e-1300 + 1`); a power computed as a double whose
base or result is outside the normal range of a double, about 2.2e-308
to 1.8e308 in magnitude, where a double keeps all its digits
(`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
it drops, the bot exits with an error and the container's restart
policy starts both again. `SIGTERM` stops the bot, which stops the
+33 -23
View File
@@ -21,11 +21,15 @@ import (
"strings"
)
// MaxInputLength caps an expression, in bytes, and maxExactExponent caps
// a power computed exactly, so that a message cannot make the bot do
// unbounded work.
// MaxInputLength caps an expression, in bytes. With maxExactExponent,
// which caps a power computed exactly, and bitLimit, which caps every
// number, it keeps a message from making the bot do unbounded work.
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
// 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
@@ -245,11 +249,11 @@ func number(tok string) (constant.Value, error) {
return nil, ErrNotArithmetic
}
// Read as FLOAT, which makes every literal decimal: as INT, a
// leading zero would make it octal.
// Read as FLOAT, which makes every literal decimal and a fraction
// (see exact): as INT, a leading zero would make it octal.
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) {
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
// negative. Each step's numbers stay small: go/constant holds one whose
// numerator or denominator reaches 4096 bits as a 512-bit float.
// negative. It starts from 1 as a fraction, a Float to go/constant, so
// 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 {
result := constant.MakeInt64(1)
one := constant.MakeFloat64(1)
result := one
for n := max(e, -e); n > 0; n >>= 1 {
if n&1 == 1 {
@@ -417,26 +424,29 @@ func exactPower(x constant.Value, e int64) constant.Value {
}
if e < 0 {
result = constant.BinaryOp(constant.MakeInt64(1), token.QUO, result)
result = constant.BinaryOp(one, token.QUO, result)
}
return result
}
// exact reports whether go/constant holds v exactly. It holds a number
// as a fraction until its numerator or denominator reaches 4096 bits,
// then as a 512-bit float, and past that float's range as Unknown. A
// number not held exactly 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.
// exact reports whether v is a fraction whose numerator and denominator
// are both below bitLimit bits, as every number here must be, so that
// each step of arithmetic stays small. go/constant never rounds an
// integer, however large, so every number is made a fraction: literals
// are read as FLOAT, and a power starts from the fraction 1. It rounds
// a fraction that grows past the limit, to a 512-bit float and past
// 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 {
switch constant.Val(v).(type) {
case int64, *big.Int, *big.Rat:
return true
default:
return false
}
r, ok := constant.Val(v).(*big.Rat)
return ok && r.Num().BitLen() < bitLimit && r.Denom().BitLen() < bitLimit
}
// 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
// the whole part of x/y is too large to be.
"(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,
// Both operands are held exactly, but their quotient is not.
"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.
"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},
// The largest power of 3 computed exactly, as often as fits.
{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 {