It occasionally comes up that we want to take a number n (let's say it's a u64) and calculate n % m, where m can be any value from 1 to 2^64. Since 2^64 can't be represented in a u64, it would be convenient to store it as 0 and have that act like 2^64 when using the wrapping operations (0 already acts like 2^64 in wrapping_add, wrapping_sub, wrapping_mul, and others). Thus n.wrapping_rem(0) should be equal to n.
This is an example of what I mean:
fn main() {
let small_number: u64 = 34;
let big_number = u64::MAX.wrapping_add(1); // this is supposed to represent 2^64
println!("{}", small_number.wrapping_rem(big_number)); // expected: 34
// panic: attempt to calculate the remainder with a divisor of zero
}
I don't think this should be considered a particularly strange result. Wikipedia notes: "Some systems leave a modulo 0 undefined, though others define it as a."
Also, wrapping_rem is completely useless on unsigned types because it's exactly the same as the normal modulo. It would be nice if it could actually be made useful somehow, although I realize that changing its behaviour at this point is probably intractable. Maybe we'd need a new method...
I believe the story of wrapping_ā¦ methods ā and correspondingly the Wrapping<ā¦>-wrapped number types ā should be that they always behave the same as the wrapping behavior (e.g. the default choice of the release profile) of the corresponding operator.
Iād be wary to break this correspondence (but Iām also not 100% certain if it isnāt broken anywhere yet).
Indeed, such a change could be dangerous. Doing a rem with s.len() is a common/reasonable approach to ensure an index ends up in-bounds, so unsafe code can very reasonably depend on this property for soundness. (The question of whether or not usage of wrapping_rem ā on a usize ā is all that reasonable for this, is perhaps a different one.)
Iād be wary of changing rem to be inconsistent with div, and then Iād be wary of changing wrapping_div(0) to return 0. I wouldnāt say itās completely unreasonable, but it does give me pause.
@steffahn I would argue that doing wrapping_anything alongside indexing is somewhat nonsensical because wrapping operations, by their very nature, mean that you're comfortable with your values jumping around near usize::MAX. But it would be interesting to see whether anyone is actually relying on the value of wrapping_rem(0) in unsafe code.
@jrose Yeah, I agree that wrapping_div(0) should return zero as well although I think division by zero is a bit more controversial than modulo zero.
Fair point. I was still thinking of the idea of explaining that all the āwrappingā operators act as-if 0 is the size of the type, in which case weād have x / 2^something, which could only be 0. But that isnāt strictly necessary for deciding that wrapping_rem should return 0 and also be consistent with wrapping_div in the quotient&remainder sense.
IMO that is just a hack for those languages requiring functions to be total, but at the same time not wanting to use the equivalent of Option<i64> as return value for divisions for convenience reasons.
I'm pretty sure this won't happen. If you want to do this, make your own method for it.
The biggest reason is that a very similar conversation happened as part of stabilizing ilog2: What should 0.ilog2() do in release mode?
There was a long conversation about potentially having that be -1 as _, which is a very natural result for it to give under the simple "well u32::ilog2 is just 31 - leading_zeroes" implementation.
But in the end, it was decided that wrapping results should only exist when there's a defined infinite-precision result. Wrapping an infinite-precision 257 to a finite 1_u8 is fine. But there's no coherent way to "wrap" -ā into an integer type, thus 0.ilog2() and -2 / 0 both always panic.
And for % specifically, I think returning the LHS is particularly wrong because it's inconsistent with what would happen for a hypothetical mixed-type % that I wish we could provide. If you do uNN % uMM, the obvious return type is uMM, but then you can't just return the LHS if the RHS is zero.
I'm also unwilling to weaken postconditions like this. For example, there's a discussion about whether to just define slice.as_chunks::<0>() as ([], slice). But that's horrible to actually use, because you want to give a postcondition of tail.len() < N as the obvious description of what that method's doing, but that implementation for zero violates it, in exactly the same kind of way that a % 0 => a violates the expected postcondition of %.
I donāt have a strong opinion on the ilog2 decision either way, but I this reasoning seems wrong to me. The only reason that arithmetic ops were chosen to wrap in release mode was performance. Thereās nothing āmore correctā about wrapping in release compared to any other behaviorāitās a logic error no matter what, if you actually want wrapping semantics you should use the dedicated methods or wrapper type. So I donāt see the value in consistency.
I think I agree with this. However, even if 0 were allowed, there would still be a postcondition that is not much looser: tail.len() <= N.wrapping_sub(1).
While I agree that if you want wrapping you should use the dedicated APIs for that, it's not true that all the other behaviours are equal.
Most importantly, x + 1 and x - 2 + 3 are equivalent in wrapping -- but not in certain other things, like saturating -- so wrapping is a particularly good choice in that it can often give the correct final answer even if the intermediate operations are outside the supported rage.
That's also a reason for division to differ from other operations. x * 2 - 2 and (x - 1) * 2 work the same in wrapping, but (x + 2) / 2 and x / 2 + 1 don't.
The most important property of modulo is that a = (a / b) * b + a % b. If a / 0 is defined as giving any finite value, than a % 0 should always be a. Regarding ilog2(0), I'd honestly say that it's Ā±ā. With Real integers, it approaches -ā, however in a 2-adic sense, 2^N gets closer to 0 as N approaches ā (if N is larger than the number of stored bits, then the number is indistinguishable from / congruent to 0), so you can argue that the logarithm should also be +ā.
Really it's this interpretation of 0 as "a sufficiently large 2^N" that makes a % 0 = a seem more reasonable. The same argument would then imply that a / 0 = 0, since 0 is effectively larger in magnitude than any representable nonzero number (but is still neither positive nor negative) and so the quotient would always be smaller in magnitude than 1.
This is definitely not the Real integer system that most people were taught, but it is still completely mathematically valid and satisfies the rules that integer division should preserve.
I'll also mention that RISC-V defines a / 0 = -1, for both signed and unsigned a, since it's the closest unsigned value to ā, and it tends to be the natural output when trying to get a division circuit to compute a / 0.
Note that I don't consider "what the hardware does" to be at all a justification for what Rust should do.
IMHO the behaviour of << is just a mistake, for example. The thing that rust does should be the "right" thing (which I would typically define as "the thing that gets the same result as if it had been computed in infinite precision, and importantly not something where doing it in a wider type gives something very different") and if there's some slightly-strange behaviour that's more efficient or situationally-useful that's what specific other methods are for.
I don't even like leading_zeros, because (my_u32 as u64).leading_zeros() as u32 isn't the same as my_u32.leading_zeros(). Thankfully we have ilog2 now, which avoids that problem.
The current behaviour is similar to other arithmetic in that shifting by an out-of-range value is considered an overflow, and panics in debug mode. I expect this helps find bugs in the common case where the shift amount should be in 0..BITS. If it was an unbounded shift, would you have negative amounts shift in the other direction, or alternatively not implement the operator for signed types at all?
IMO, the biggest issue is just not having the convenience of unbounded_{shl,shr} sooner (nor yet, on stable).
Many other operations are fundamentally tied to the bit-width of the type: rotate_{left,right},reverse_bits, {leading,trailing,count}_{zeros,ones} (consider 0 or negative inputs). But I can agree that for the cases where ilog2 is semantically appropriate, it's a big improvement over writing it in terms of leading_zeros.