RFA-343 · Case file with fixtures · Case 315 of 694 · Runtime evidence
wrapping_abs Keeps the Signed Minimum Negative
A signed two's-complement type has one more negative value than positive values. wrapping_abs maps the unrepresentable magnitude back to MIN; checked, saturating, and unsigned absolute value methods preserve different policies.
- 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 exact positive magnitude of the signed minimum is outside the i32 range, so wrapping negation returns the same minimum bit pattern.
- First discriminating check
- Exercise MIN explicitly and choose checked_abs, saturating_abs, or unsigned_abs according to whether failure, clipping, or exact magnitude is required.
I have seen code normalize a signed distance with wrapping_abs, then check that the result is non-negative. The check looked almost defensive, but one valid input breaks it: i32::MIN remains negative after wrapping_abs.
The failing program tests this single value. It is not an obscure CPU accident. It follows directly from the range of a signed fixed-width type.
The signed range is asymmetric
An i32 ranges from -2,147,483,648 to 2,147,483,647. The mathematical absolute value of the minimum is 2,147,483,648, which cannot be represented as an i32.
Every fixed-width API must choose what to do with this one input. There is no signed result that is both exact and non-negative. A generic sentence such as “take the absolute value” has left a policy undecided.
wrapping_abs uses wrapping arithmetic. Negating the minimum returns the same bit pattern, which is interpreted again as i32::MIN. The result is negative because that is what fixed-width wrapping means.
Rust provides several honest answers
The repaired program places four methods together because their differences matter more than their shared name.
checked_abs returns None for the minimum. This fits validation when an exact signed result is required.
saturating_abs returns i32::MAX. This keeps the result signed and non-negative but loses one unit of magnitude.
unsigned_abs returns a u32 and can represent the exact magnitude 2,147,483,648. This is often the best contract for distances, byte deltas, and magnitude calculations that never needed a signed output.
wrapping_abs retains modulo arithmetic and returns the minimum. That can be correct inside algorithms deliberately defined over bit patterns.
A postcondition cannot override representation
Code sometimes calls a wrapping method and then writes debug_assert!(value >= 0). The assertion does not make the invariant true. It documents a wish that conflicts with the chosen operation.
If later code uses the result as an index, converts it with as usize, or compares it with thresholds, the negative minimum may turn into a huge unsigned number or take the wrong branch. The original arithmetic boundary is then far from the symptom.
I select the return type and failure channel at the point where magnitude is formed. This keeps the exceptional input visible.
Widening first requires care too
For i32, converting to i64 before negating gives enough signed range. That is valid when the application wants an i64. But a generic function cannot assume that a conveniently wider primitive always exists, and narrowing later can reintroduce the same decision.
Unsigned magnitude expresses more directly that a magnitude is never negative. I use widening when subsequent signed arithmetic genuinely needs the wider domain, not as a ritual to silence overflow.
Boundary tests should include MIN explicitly
Typical tests use -7, zero, and 7. All absolute-value policies agree there. They disagree exactly at the signed minimum, so omitting it means the policy was never tested.
My numeric table includes MIN, MIN + 1, -1, 0, 1, and MAX. For conversion code I then test the destination limits. Property tests such as “absolute value is non-negative” must either exclude MIN with a documented precondition or choose an API that satisfies the property.
I do not rely on random generation to eventually hit one exact 32-bit value. Boundary examples are cheap and intentional.
Serialization deserves the same check. If a magnitude crosses an API as a signed integer, the receiver cannot represent the exact minimum magnitude either. I validate before encoding and use an unsigned field when the schema truly describes magnitude. This avoids repairing the arithmetic locally and then losing the result at the next boundary.
This matters beyond absolute values
The same asymmetric range appears in negation, subtraction from zero, signed division of MIN / -1, parsing magnitudes, and difference calculations. Mathematical integers and machine integers are different domains.
Rust makes many policies available: checked, saturating, wrapping, overflowing, and unsigned results. Safety here means selecting the semantic result the application can explain, not simply selecting a method that never panics.
The principle I keep in reviews
Before normalizing a signed value, I ask whether exact magnitude, signed representation, non-negative output, or modulo arithmetic is required. No operation can provide all four for the minimum value.
wrapping_abs is not a safer ordinary absolute value. It is a precise wrapping operation. When the business meaning is magnitude, I usually preserve the exact result with unsigned_abs or preserve invalidity with checked_abs.