RFA-258 · Case file with fixtures · Case 230 of 694 · Runtime evidence
Why 0.is_multiple_of(0) Is True in Rust
is_multiple_of defines a total predicate: zero is a multiple of zero, while nonzero values are not. Keep this classification separate from division and validate a nonzero divisor when the domain requires one.
- 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
- The method defines a total classification predicate where zero is a multiple of zero but nonzero values are not.
- First discriminating check
- Test zero-zero and nonzero-zero independently, then keep a separate nonzero guard before any division or remainder.
Writing value % divisor == 0 panics when the divisor is zero. Rust's is_multiple_of deliberately defines that edge instead.
The failing program expects 0_u32.is_multiple_of(0) to be false. The method returns true. For a nonzero value such as six with divisor zero, it returns false.
The predicate is total over integer inputs
is_multiple_of answers whether the first integer is an integer multiple of the second. Rust defines zero as a multiple of zero and no nonzero integer as a multiple of zero.
This makes the predicate return a boolean for every pair without a division-by-zero panic. It is similar to % for ordinary nonzero divisors, but not implemented as a contractually identical spelling of remainder equality.
The special case is documented and deserves a test whenever zero can arrive.
Classification is not division
Calling a number a multiple does not require producing a quotient in this API. Integer division by zero remains undefined and panics; checked division or remainder reports failure.
I do not use is_multiple_of(divisor) as proof that later value / divisor is safe. For (0, 0), the predicate is true and the division still panics.
If a quotient will be calculated, divisor != 0 is a separate precondition. This separation prevents a boolean classification from being treated as a guard for another operation.
Mathematical conventions and software contracts can differ
People use different definitions around divisibility by zero. Rust's choice gives one predictable total predicate. A specification may instead declare every zero divisor invalid.
At that boundary I validate zero before calling and return a domain error. I do not argue from the standard-library convention to a financial, protocol, or user-interface rule.
The repaired program records Rust's complete relevant behavior: zero/zero true, nonzero/zero false, and an ordinary multiple true.
checked remainder preserves invalidity
checked_rem returns None for a zero divisor. It is useful when the application wants divisibility classification to remain fallible:
let divides = value.checked_rem(divisor).map(|remainder| remainder == 0);
Now None distinguishes invalid divisor from Some(false). This can be stronger than a total boolean at an input boundary.
The extra state is only valuable if callers preserve it rather than immediately replacing None with false.
Rounding to a multiple has another contract
checked_next_multiple_of returns None for a zero divisor and also for representability overflow. It cannot use the same total-predicate convention because it must produce an actual rounded integer.
This shows why related method names should not be generalized from one edge case. Classification, remainder, division, and rounding need different outputs.
Alignment code often wants a nonzero power of two
Memory alignment and ring-buffer calculations commonly test multiples. Their divisor usually has stronger invariants than nonzero: it may also need to be a power of two and within a supported range.
I validate those conditions at construction and carry an alignment type. Accepting zero because a predicate is total can lead to later mask underflow or allocation errors.
For user-entered reporting intervals, the domain rule may simply reject zero with a clear message.
What I test
My truth table contains (0,0), (nonzero,0), (0,nonzero), an exact ordinary multiple, and a non-multiple. If the application wraps the operation, it separately checks the invalid-divisor error.
I also test any downstream division so a future refactor cannot replace a dedicated nonzero guard with is_multiple_of alone.
Zero as a period creates another ambiguity
Scheduling code sometimes asks whether a tick is a multiple of a period. A zero period could mean disabled, run continuously, or invalid configuration. Rust's arithmetic predicate cannot choose among those product meanings.
I handle the disabled state before arithmetic, preferably with an enum rather than a magic zero. This makes Disabled different from Every(NonZeroU32), and every active schedule carries a usable divisor. The predicate then answers only the ordinary divisibility question.
The same model helps with batch sizes and alignment. A raw integer combines absence with magnitude; a domain type keeps them separate.
Boolean APIs are easy to misuse as validation
A function that never panics can appear safer than a checked function returning Option, but totality may come from choosing an edge convention. That convention can discard information the caller needs.
I read total predicates as classification tools. When input validity matters, I keep a validation result beside the classification or expose one function that returns a domain-specific Result<bool, Error>. This is more verbose at the boundary and simpler everywhere after it.
Property tests can compare is_multiple_of with remainder only under the precondition divisor != 0. Stating that precondition prevents the test itself from panicking and records exactly where the two contracts agree.
The core principle is that similarly related arithmetic APIs need not share one failure model. is_multiple_of is a total classification predicate with a documented zero convention. Division and remainder have different domains. I use the boolean when that convention fits, and a checked or validated operation when zero must remain an error.