From 8f259fb560d039e2e5f213e08cf1644d850269b1 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Tue, 30 Dec 2025 16:25:49 +0900 Subject: [PATCH] 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 --- mrbgems/mruby-bigint/core/bigint.c | 34 +++++++++++++++++++++ mrbgems/mruby-numeric-ext/src/numeric_ext.c | 18 ++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/mrbgems/mruby-bigint/core/bigint.c b/mrbgems/mruby-bigint/core/bigint.c index e5f912ffa..9506323de 100644 --- a/mrbgems/mruby-bigint/core/bigint.c +++ b/mrbgems/mruby-bigint/core/bigint.c @@ -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)); diff --git a/mrbgems/mruby-numeric-ext/src/numeric_ext.c b/mrbgems/mruby-numeric-ext/src/numeric_ext.c index e607eb89a..59d122579 100644 --- a/mrbgems/mruby-numeric-ext/src/numeric_ext.c +++ b/mrbgems/mruby-numeric-ext/src/numeric_ext.c @@ -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); }