Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-378 · Case file with fixtures · Case 350 of 694 · Runtime evidence

overflowing_rem Reports Overflow for i32::MIN % -1

The remainder zero fits in i32, but it belongs to the exceptional MIN/-1 division pair whose quotient cannot be represented. overflowing_rem returns (0, true); checked_rem returns None.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The remainder belongs to the exceptional signed division pair whose quotient is not representable, and the overflowing API reports that arithmetic overflow even though zero itself fits.
First discriminating check
Exercise the MIN and minus-one pair explicitly and choose whether checked rejection or the overflowing result-and-flag contract matches the caller.

I expected the overflow flag of a remainder operation to describe whether the remainder itself fits. For i32::MIN % -1, the mathematical remainder is zero, which is clearly representable. Still, overflowing_rem returns (0, true).

The failing fixture expects (0, false) and shows the actual pair. This is a narrow boundary, but it matters in generic arithmetic code and parser normalization.

Remainder belongs to a division operation

Quotient and remainder are linked by the division identity. For signed fixed-width integers, i32::MIN / -1 has a positive mathematical quotient one larger than i32::MAX. No i32 can represent it.

The remainder component is zero, but Rust treats the MIN, -1 operands as the signed division overflow pair. overflowing_rem documents that it returns zero with the overflow boolean set when overflow would occur.

The flag therefore describes the arithmetic operation and operand pair, not a range check performed only on the first tuple field.

The tuple must be consumed together

It is tempting to write let (value, _) = x.overflowing_rem(y) because the first component looks usable. For the exceptional pair, that silently accepts zero while discarding the signal that ordinary signed remainder is not a valid operation under the chosen policy.

I treat an overflowing result as a tagged outcome. Both components define its meaning. If overflow is acceptable and zero is the specified wrapped remainder, I record that choice. If overflow is invalid, I branch on the boolean or use a checked API.

This is the same discipline I apply to partial parsers and I/O results: the payload cannot be interpreted independently from the status that qualifies it.

checked_rem makes rejection explicit

The repaired fixture confirms that checked_rem returns None for i32::MIN and -1. It also compares the ordinary 5 % 2 case, which returns (1, false) from the overflowing method.

I prefer checked_rem when operands come from untrusted data and the caller already has a path for arithmetic-domain errors. None handles both the special overflow pair and, according to the method contract, invalid remainder operations such as a zero divisor.

If those errors need different messages, I validate the divisor first and then handle the MIN/-1 case separately.

A zero divisor is still a panic for overflowing_rem

The word “overflowing” does not mean “never panics.” The method documentation says a zero right-hand operand panics. Its boolean reports arithmetic overflow for the signed minimum pair; it does not encode division-by-zero.

This is a general Rust API lesson. Return type shape alone does not enumerate every failure mode. I read the Panics section and test external-input boundaries before assuming a tuple or Option makes an operation total.

For a public helper I often return a domain error distinguishing ZeroDivisor from SignedOverflow. That gives callers a stable policy instead of leaking a combination of panic and boolean semantics.

Euclidean remainder has the same exceptional pair

Changing from truncated remainder to Euclidean remainder changes the sign normalization for many operands, but it does not make the unrepresentable quotient of MIN / -1 disappear. The corresponding overflowing Euclidean method also documents (0, true) for this pair.

I choose %-style or Euclidean operations based on the domain: language arithmetic, modular indexing, coordinates, or number theory. I do not switch variants only to avoid one boundary case.

Wider arithmetic can represent the quotient

For i32 inputs, converting both operands to i64 before division makes the positive quotient representable. The remainder is then zero without overflow in the wider type.

This is appropriate only if the surrounding computation accepts a wider result. Casting the remainder back to i32 is harmless for zero, but other derived values may need checked conversion. A wider intermediate is a semantic choice, not proof that the original i32 operation succeeded.

Tests need the paired boundary

Arithmetic property tests often generate ordinary positive numbers and miss the single asymmetric signed minimum. I always include MIN, MAX, -1, 0, and 1, with special handling so zero divisors do not abort the suite unexpectedly.

For an overflowing operation I assert the full tuple. For checked operations I assert None. For a domain wrapper I assert the exact error variant. This prevents a refactor from preserving the numeric zero while losing the overflow decision.

The core principle is that representability of a remainder is not the complete validity rule for signed remainder. i32::MIN % -1 belongs to an overflowing division pair even though its mathematical remainder fits. Rust's (0, true) keeps both facts, and correct code must keep them together too.