micropython / micropython/micropython
3-arg pow() with negative exponent silently returns 0 instead of computing modular inverse
Nobody has claimed this yet.
- Dominant language
- C
- Stars
- 22.1k
- Forks
- 9k
- Avg merge
- 6d 4h
- Merged PRs (30d)
- 16
Description
Port, board and/or hardware
Unix port, Linux
MicroPython version
MicroPython v1.27.0
Reproduction
print(pow(2, -1, 4))
[!NOTE]
The simplest demonstration would bepow(2, -1, None), where CPython produces the expected result0.5. However, MicroPython issue #19137 causespow(x, y, None)to raiseTypeErrorbefore the negative-exponent code path is even reached, so that example cannot be used here. The reproduction below falls back to a 3-arg form where CPython raisesValueErrorfor non-invertibility, which still exercises the same buggy code path in MicroPython.
Expected behaviour
CPython v3.11.15:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: base is not invertible for the given modulus
Observed behaviour
MicroPython v1.27.0:
0
Additional Information
pow(base, exp, mod) with a negative exponent silently returns 0 instead of computing the modular inverse of base modulo mod (or raising ValueError when the inverse does not exist). This is a CPython compatibility issue.
Root Cause
mpz_pow3_inpl short-circuits any negative exponent by setting dest = 0 and returning, so mp_obj_int_pow3 silently yields 0 for every pow(x, neg_exp, mod) call regardless of whether the modular inverse exists.
// py/mpz.c:1358-1362
void mpz_pow3_inpl(mpz_t *dest, const mpz_t *lhs, const mpz_t *rhs, const mpz_t *mod) {
if (lhs->len == 0 || rhs->neg != 0 || (mod->len == 1 && mod->dig[0] == 1)) {
mpz_set_from_int(dest, 0);
return;
}
...
CPython's long_pow negates the exponent and calls long_invmod to compute the modular inverse, raising ValueError("base is not invertible for the given modulus") when gcd(base, |mod|) != 1.
CPython reference: Objects/longobject.c#L4455-L4474
// If exponent is negative, negate it and replace the base with // its modular inverse; long_invmod raises ValueError when gcd != 1. if (Py_SIZE(b) < 0) { temp = (PyLongObject *)_PyLong_Copy(b); if (temp == NULL) goto Error; Py_DECREF(b); b = temp; temp = NULL; _PyLong_Negate(&b); if (b == NULL) goto Error; temp = long_invmod(a, c); if (temp == NULL) goto Error; Py_DECREF(a); a = temp; temp = NULL; }
Fix
Replace the rhs->neg != 0 short-circuit in mpz_pow3_inpl with a modular-inverse path (extended Euclidean), change the return type to bool, and raise ValueError("base is not invertible for the given modulus") from mp_obj_int_pow3 when the helper signals a non-invertible base.
Code of Conduct
Yes, I agree
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing pow(2, -1, 4) and reading mpz_pow3_inpl in py/mpz.c together with mp_obj_int_pow3 in py/objint_mpz.c. Compare the negative-exponent behavior with the linked CPython long_pow reference. Done means modular inverses are computed when possible and the specified ValueError is raised for non-invertible bases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, python
- Domain
- embedded-iot
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100