RFA-322 · Case file with fixtures · Case 294 of 694 · Runtime evidence
Signed Integer Midpoint Rounds Toward Zero
Signed integer midpoint behaves like division of the mathematical sum by two with truncation toward zero. That differs from floor division for negative odd sums, so interval conventions must state their rounding rule.
- 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
- Signed midpoint behaves like sufficiently wide addition followed by signed division by two, which rounds a half-integer toward zero.
- First discriminating check
- Test a negative odd sum, extrema, adjacent endpoints, and both operand orders against the interval algorithm's required tie direction.
I replaced a hand-written midpoint formula with i32::midpoint and first assumed it selected the lower integer. For an interval from -7 to 0, I expected -4. Rust returns -3.
The failing program captures exactly this mismatch.
Midpoint uses truncation toward zero
The i32::midpoint documentation describes the result as if both operands were added in a sufficiently large signed type and divided by two. When the exact midpoint lies between two integers, the result rounds toward zero.
The mathematical sum of -7 and 0 is -7. Dividing by two gives -3.5. Rounding toward zero gives -3; rounding toward negative infinity would give -4.
Both are defensible interval rules. They are not interchangeable.
Overflow safety does not define tie direction
One reason to use midpoint is that it avoids overflow. The naive (low + high) / 2 can overflow before division even when the final midpoint is representable. checked_add makes that failure visible but does not by itself calculate the midpoint.
The standard method solves the representability problem. It still needs a rounding contract for half-integers, and Rust chose toward zero for signed integers.
The repaired program checks the documented negative result, verifies operand symmetry, and covers i32::MIN.midpoint(i32::MAX) without an overflowing addition.
Binary search must match its interval invariant
For a normal array index, endpoints are non-negative, so toward-zero and floor rounding agree. This issue appears when signed coordinates, time offsets, prices, or mathematical domains cross zero.
A binary search is correct because each update makes a stated interval smaller. If an algorithm requires the lower of two central integers, midpoint may pick the upper one for a negative odd sum. If it requires progress toward a particular bound, that difference can cause a skipped candidate or a repeated endpoint.
I write the invariant beside the calculation: closed or half-open interval, which endpoint remains possible, and which integer is chosen when the exact midpoint is fractional.
Euclidean division is another explicit rule
div_euclid provides Euclidean quotient semantics. That method is useful when I need remainders in a non-negative range, but blindly replacing midpoint with (a + b).div_euclid(2) brings the original overflow back.
If I truly need a floor-biased or Euclidean midpoint over the complete integer range, I implement it with a proven overflow-safe transformation and test extrema. I do not compose familiar operations and assume the proof survived.
Sometimes the cleanest model is to shift the coordinate system into an unsigned offset whose range is known to fit. Sometimes it is to use a wider integer type at a validated boundary. The right repair depends on the interval domain.
Negative division surprises travel into bucketing
The same rounding distinction appears outside search. Time windows before an epoch, grid cells with negative coordinates, centred pagination, and signed histogram buckets all need a rule at boundaries.
Truncation toward zero makes the intervals around zero asymmetric if I expected mathematical floor. For example, -1 / 2 and 1 / 2 both become zero with signed truncating division. A floor-based spatial grid would normally place -1 in the negative bucket.
I name helper functions after the policy—midpoint_toward_zero, floor_bucket, or lower_middle—rather than hiding it behind “average.”
Tests should cross zero deliberately
Positive examples are not enough for a signed API. My table includes:
- an even positive sum;
- an odd positive sum;
- an even negative sum;
- an odd negative sum;
- endpoints on opposite sides of zero;
- minimum and maximum representable values;
- both operand orders;
- adjacent values where progress is delicate.
Property tests can assert that the result lies between operands and that swapping operands changes nothing. A separate property must encode the desired tie direction; “between” alone permits both -4 and -3 in the example.
What this case does and does not claim
RFA-322 proves the stable Rust 1.98.1 i32 contract. Other signed primitive midpoint methods follow their own linked documentation, and unsigned types do not have a negative rounding question.
It does not say that midpoint is wrong for binary search. For ordinary slice indices it is an excellent overflow-safe choice. It says that a method with a strong safety property can still have a rounding rule that differs from an algorithm's policy.
The core principle is that discrete arithmetic needs a tie-breaking rule. “Halfway” is exact in real numbers but sometimes two integers in a finite domain. Rust chooses toward zero for signed midpoint; reliable code makes sure that choice matches its interval invariant.