RFA-273 · Case file with fixtures · Case 245 of 694 · Runtime evidence
Why div_euclid and Slash Disagree for Negative Integers
Integer slash truncates toward zero, while div_euclid chooses a quotient paired with a nonnegative Euclidean remainder. Select the rule from the domain, especially for negative coordinates and cyclic indexes.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Signed slash truncates the quotient toward zero, while Euclidean division chooses the quotient paired with a least nonnegative remainder.
- First discriminating check
- Test one inexact negative dividend and assert both the reconstruction identity and the required sign range of the remainder.
Positive integer division made two APIs look interchangeable in my tests. The difference appeared only when a coordinate became negative: -7 / 4 gave -1, while (-7).div_euclid(4) gave -2.
The failing program asserts those quotients are equal. Rust rejects the assumption because the two operations use different rounding rules.
Slash truncates toward zero
For signed integers, the / operator computes an integer quotient and rounds the exact result toward zero. The exact value of -7 / 4 is -1.75, so truncation gives -1.
The paired % remainder is then -3, keeping the identity:
-7 = (-1 * 4) + -3
This arithmetic is internally consistent. It simply permits a negative remainder when the dividend is negative.
Euclidean division keeps the remainder nonnegative
i32::div_euclid chooses its quotient together with rem_euclid. For divisor four, the remainder must satisfy 0 <= r < 4.
The pair becomes:
quotient = -2
remainder = 1
-7 = (-2 * 4) + 1
Neither answer is a more accurate approximation in isolation. Each belongs to a different quotient-and-remainder convention.
The domain chooses the convention
Euclidean remainder is often convenient for cyclic values. Mapping a signed offset into four slots with rem_euclid(4) produces a value from zero through three. Plain % 4 can produce a negative index that requires another adjustment.
Grid partitioning is another example. If cells cover half-open coordinate ranges of width four, coordinate -1 normally belongs to the cell beginning at -4. Euclidean division yields cell index -1, while truncation toward zero yields zero and incorrectly groups both sides of the origin.
Truncating division can still be right for quantities where discarding the fractional part toward zero is the written rule. I do not replace every slash with div_euclid. I name the spatial, cyclic, accounting, or protocol convention first.
Floor division is related but not identical in every sign combination
With a positive divisor, Euclidean division behaves like floor division. With negative divisors, it is better to rely on the documented remainder condition than to remember a slogan.
I test all sign quadrants when negative divisors are allowed:
positive / positive
positive / negative
negative / positive
negative / negative
If the domain requires positive widths or moduli, I validate that invariant and reduce the matrix. Allowing meaningless negative divisors and hoping arithmetic will clarify them later makes code harder to review.
Zero and minimum overflow still fail
Euclidean methods do not make integer division total. A zero divisor panics. Dividing i32::MIN by -1 also cannot fit in i32 and panics, independently from the ordinary overflow-check setting.
For untrusted divisors or extreme values, checked variants or prior validation are needed. Changing the rounding convention does not remove the numeric domain boundaries.
I keep these concerns separate in tests: rounding examples use small safe values, while failure cases exercise zero and minimum overflow explicitly.
Store both values when both carry meaning
Code sometimes computes a quotient with one convention and a remainder with another. The reconstruction identity then fails or requires a compensating branch.
The repaired program calculates both Euclidean values and asserts:
dividend = quotient * divisor + remainder
0 <= remainder < abs(divisor)
These property-style assertions are stronger than checking one memorized output. They cover many inputs and document the convention for later maintainers.
For a data structure, I often wrap the pair in a helper named after the domain, such as tile_and_offset. Callers then receive values that were computed under one rule together.
Cross-language ports need explicit tests
Languages differ in signed division and modulo behavior, and libraries sometimes offer several operations. A formula copied from another implementation may compile and still move negative values into different buckets.
I include negative golden cases beside the port, even when production data is “normally positive.” Historical timestamps, relative positions, hashes converted to signed types, and error values have a habit of reaching the negative path later.
The test -7 and 4 is useful because the division is not exact and the conventions visibly disagree. Testing only -8 / 4 proves almost nothing about rounding.
What I verify
I test exact and inexact division, values on both sides of zero, all allowed divisor signs, zero policy, and minimum overflow policy. For Euclidean results I assert the reconstruction identity and remainder bounds.
For cyclic indexing I additionally assert that every produced remainder converts safely to the collection index type. For spatial partitioning I verify that reconstructing the coordinate from cell origin and offset returns the original point.
The core principle is that integer division includes a rounding contract. Slash truncates signed results toward zero. div_euclid chooses the quotient that pairs with a least nonnegative remainder. Negative examples expose the difference, and the application domain—not habit—must decide which contract is correct.