Skip to content

ndarray: Fix four bugs in the modulo operator (% and %=)#749

Merged
v923z merged 3 commits into
v923z:masterfrom
kwagyeman:kwabena/fix_modulo
Apr 8, 2026
Merged

ndarray: Fix four bugs in the modulo operator (% and %=)#749
v923z merged 3 commits into
v923z:masterfrom
kwagyeman:kwabena/fix_modulo

Conversation

@kwagyeman
Copy link
Copy Markdown
Contributor

@kwagyeman kwagyeman commented Apr 8, 2026

The recently added modulo operator had four bugs, two of which caused
memory corruption or silent wrong results at runtime.

Binary %uint16 % uint8 result dtype

ndarray_binary_modulo allocated the result array as NDARRAY_UINT8
but then looped with uint16_t arithmetic via BINARY_LOOP. Because
BINARY_LOOP writes through a uint16_t pointer but the array's
itemsize is 1 byte, this caused out-of-bounds stores that corrupted
both the result values and adjacent memory. Fixed by allocating the
result as NDARRAY_UINT16 to match the left operand.

Inplace %= — four bugs

  1. Feature guard typo: ulab.h defined NDARRAY_HAS_INPLACE_MODU
    (missing LO), so NDARRAY_HAS_INPLACE_MODULO was never set and
    ndarray_inplace_modulo was compiled out entirely. All %=
    operations silently fell back to binary % + rebind.

  2. Missing variable declarations: Every INLINE_MODULO_FLOAT_LOOP
    call referenced larray and rarray but they were never declared —
    a compile error waiting to happen once the typo above was fixed.

  3. Duplicate dtype branch: The second else if checked
    NDARRAY_UINT8 again instead of NDARRAY_INT8, making
    float %= int8 a silent no-op.

  4. Integer lhs unhandled: Only NDARRAY_FLOAT lhs was dispatched;
    all integer lhs types fell through and returned the array unchanged.
    Added full integer dispatch using INPLACE_LOOP with %=. Integer
    lhs %= float rhs now raises TypeError, matching NumPy's in-place
    casting semantics.

Test plan

  • Updated tests/2d/numpy/modulo.py and modulo.py.exp to reflect
    correct dtypes and in-place semantics, including expected TypeError
    for integer %= float
  • Added a regression section with one targeted case per bug

ndarray_binary_modulo allocated the result array as NDARRAY_UINT8 but
then looped with uint16_t arithmetic via BINARY_LOOP.  Because
BINARY_LOOP writes through a uint16_t pointer but the array's itemsize
is 1 byte, this caused out-of-bounds writes that corrupted both the
result values and adjacent memory.  The result dtype must be
NDARRAY_UINT16 to match the left operand and to give BINARY_LOOP the
correct element size.
Four bugs in the inplace modulo implementation:

1. ulab.h defined NDARRAY_HAS_INPLACE_MODU (typo, missing "LO"), so the
   NDARRAY_HAS_INPLACE_MODULO feature guard was never set and the entire
   ndarray_inplace_modulo function was compiled out.  All %= operations
   silently fell back to binary % + rebind.

2. The function referenced larray and rarray in every INLINE_MODULO_FLOAT_LOOP
   call but never declared them, which would have been a compile error once
   the typo above was fixed.

3. The second dtype branch checked NDARRAY_UINT8 again instead of
   NDARRAY_INT8, making float %= int8 a no-op.

4. Only float lhs was handled; integer lhs returned the array unchanged
   instead of performing in-place integer modulo.

Fix all four: correct the feature-guard typo, add the larray/rarray
declarations, fix the NDARRAY_INT8 branch, and add full integer dispatch
using INPLACE_LOOP with %=.  The float path continues to use fmod via
INLINE_MODULO_FLOAT_LOOP since C does not support %= on floating-point
types.  Integer lhs with float rhs raises TypeError, matching NumPy's
in-place casting semantics.
- Add try/except TypeError around the inplace loop: integer lhs %= float
  rhs now raises TypeError (matching NumPy casting semantics) rather than
  silently rebinding via binary % fallback.
- Update modulo.py.exp: binary uint16 % uint8 now shows correct values
  and dtype=uint16; inplace section now shows stable dtypes (true in-place
  for integer types) and the four expected TypeErrors.
- Add a regression section with targeted cases for each of the four bugs:
  binary uint16 % uint8 dtype, inplace integer types, and float %= int8.
@kwagyeman
Copy link
Copy Markdown
Contributor Author

@v923z - We tried to integrate ulab back in October and ran into some bugs, then got busy. I came back to this and fixed all the issues. Can you check if it's good?

I've done some trivial testing of it and using the new operator works now with this patch.

Also, after this is merged, it would be great if you could release again.

@v923z
Copy link
Copy Markdown
Owner

v923z commented Apr 8, 2026

@kwagyeman Yes, it's good, many thanks for putting in the effort!

@v923z
Copy link
Copy Markdown
Owner

v923z commented Apr 8, 2026

@kwagyeman
Copy link
Copy Markdown
Contributor Author

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants