mruby-bigint, mruby-numeric-ext: fix Integer#pow with negative modulus

Support negative modulus in Integer#pow(exp, mod) with proper Ruby
semantics. Previously, negative modulus caused an infinite loop in
Barrett reduction. Now:

- Use absolute value of modulus for computation
- Apply signed modulo adjustment (result + m for non-zero result
  when m is negative)
- Add early return for zero base with positive exponent (0^n = 0)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2025-12-30 16:25:49 +09:00
parent 54fbf6c3ec
commit 8f259fb560
2 changed files with 51 additions and 1 deletions
+34
View File
@@ -3442,12 +3442,17 @@ mrb_value
mrb_bint_powm(mrb_state *mrb, mrb_value x, mrb_value exp, mrb_value mod)
{
mpz_t a, b, c, z;
mrb_bool neg_mod = FALSE;
MPZ_CTX_INIT(mrb, ctx, pool);
bint_as_mpz(RBIGINT(x), &a);
if (mrb_integer_p(mod)) {
mrb_int m = mrb_integer(mod);
if (m == 0) mrb_int_zerodiv(mrb);
if (m < 0) {
neg_mod = TRUE;
m = -m;
}
mpz_init_set_int(ctx, &c, m);
}
else {
@@ -3456,7 +3461,29 @@ mrb_bint_powm(mrb_state *mrb, mrb_value x, mrb_value exp, mrb_value mod)
if (zero_p(&c) || uzero_p(&c)) {
mrb_int_zerodiv(mrb);
}
if (c.sn < 0) {
neg_mod = TRUE;
c.sn = 1; /* use absolute value */
}
}
/* Check for zero base case: 0^n = 0 for n > 0 */
if (zero_p(&a) || uzero_p(&a)) {
mrb_bool exp_positive;
if (mrb_bigint_p(exp)) {
bint_as_mpz(RBIGINT(exp), &b);
exp_positive = (b.sn > 0) && !uzero_p(&b);
}
else {
exp_positive = mrb_integer(exp) > 0;
}
if (exp_positive) {
/* 0^n mod m = 0 for n > 0 */
if (mrb_integer_p(mod)) mpz_clear(ctx, &c);
return mrb_fixnum_value(0);
}
}
mpz_init(ctx, &z);
if (mrb_bigint_p(exp)) {
bint_as_mpz(RBIGINT(exp), &b);
@@ -3468,6 +3495,13 @@ mrb_bint_powm(mrb_state *mrb, mrb_value x, mrb_value exp, mrb_value mod)
if (e < 0) goto raise;
mpz_powm_i(ctx, &z, &a, e, &c);
}
/* Apply signed modulo adjustment for negative modulus */
/* Ruby: result + m for non-zero result when m is negative */
if (neg_mod && !zero_p(&z) && !uzero_p(&z)) {
mpz_sub(ctx, &z, &z, &c); /* z = z - |m| = z + m (since m is negative) */
}
if (mrb_integer_p(mod)) mpz_clear(ctx, &c);
return bint_norm(mrb, bint_new(ctx, &z));
+17 -1
View File
@@ -180,6 +180,7 @@ int_powm(mrb_state *mrb, mrb_value x)
{
mrb_value m, e;
mrb_int exp, mod, result = 1;
mrb_bool neg_mod = FALSE;
if (mrb_get_argc(mrb) == 1) {
return mrb_int_pow(mrb, x, mrb_get_arg1(mrb));
@@ -204,10 +205,19 @@ int_powm(mrb_state *mrb, mrb_value x)
if (exp < 0) mrb_raise(mrb, E_ARGUMENT_ERROR, "int.pow(n,m): n must be positive");
if (!mrb_integer_p(m)) mrb_raise(mrb, E_TYPE_ERROR, "int.pow(n,m): m must be integer");
mod = mrb_integer(m);
if (mod < 0) mrb_raise(mrb, E_ARGUMENT_ERROR, "int.pow(n,m): m must be positive when 2nd argument specified");
if (mod == 0) mrb_int_zerodiv(mrb);
if (mod < 0) {
neg_mod = TRUE;
mod = -mod;
}
if (mod == 1) return mrb_fixnum_value(0);
/* Early return for zero base with positive exponent */
mrb_int base = mrb_integer(x);
if (base == 0 && exp > 0) {
return mrb_fixnum_value(0);
}
for (;;) {
mrb_int tmp;
if (exp & 1) {
@@ -237,6 +247,12 @@ int_powm(mrb_state *mrb, mrb_value x)
}
base = tmp % mod;
}
/* Apply signed modulo adjustment for negative modulus */
/* Ruby: result + m for non-zero result when m is negative */
if (neg_mod && result != 0) {
result = result - mod; /* result - |m| = result + m (since m is negative) */
}
return mrb_int_value(mrb, result);
}