Guarantee that 0i32 / 0 panics?

Is it guaranteed that 0i32 / 0 panics?

It seems so (Playground). But where is this documented? I searched here, and didn't find anything yet (but maybe I missed it?). Oh, now I found something here:

But that's pretty hidden, and when I look at the implementations for &i32 (instead of i32), I don't find that guarantee. (I would be surprised if it behaves differently, but… technically it's not guaranteed, right?)

There is also no documentation on that on:

Is this underdocumented, or am I just missing the right section?

1 Like

The existence of the checked_div method kinda implies that / should not be used in cases where division by zero can be expected.

1 Like

How, exactly? impl Div for i32 is exactly where "what happens when I divide i32" should be documented.

It doesn't belong in either of those places. Division is not the only use of i32, and Div is not only implemented for i32. Neither of those pieces of documentation should be concerned by specifically the behavior of division on i32.

But i32 also "should not overflow". Yet the panic on overflow is not guaranteed.

Okay, I agree on that.

It's missing in the overview in the reference:

The reference describes how rounding, signs, etc. are handled. It also talks about panics on overflow in debug mode, but it doesn't mention division by zero. I got mislead by the fact that overflows only panic when in debug mode.

When I want to learn about integer division in a language, I'd first look in the reference, and then in the standard library. Maybe it's different for Rust, because there is a trait for the division operator, but I still find it a bit irritating that it isn't mentioned in the language reference.

Last but not least, the cases:

  • &i32 / i32
  • i32 / &i32
  • &i32 / &i32

don't seem to be covered. Also note:

fn main() {
    // let _ = 1 / 0; // doesn't compile

    // but these can be compiled:
    let _ = &1 / 0;
    let _ = 1 / &0;
    let _ = &1 / &0;
}

(Playground)


It also doesn't seem to be documented for the /= operator:

Performs the /= operation. Read more

And behind "Read more":

Performs the /= operation.

let mut x: u32 = 12;
x /= 2;
assert_eq!(x, 6);

Disregarding whether it's formally a missing guarantee for &i32 or /=, I think it can be pretty confusing for people who are new to the language.

Consider:

fn foo(x: usize) -> usize {
    1 / (x - 1)
}

fn main() {
    println!("{}", foo(0)); // only guaranteed to panic in debug mode
    println!("We get here.");
    println!("{}", foo(1)); // panics unconditionally
    println!("We have panicked by now.");
}

(Playground in release mode)

I feel like there should be easier way to figure out what happens during these operations by reading the Rust reference. (But again, maybe I'm just missing the relevant section in the reference? But there didn't seem to be anything in it on that topic.)

1 Like

I disagree that this is a problem in practice. The signature of the operator means that it can only diverge on division by zero. Whether that's a panic or an abort or something else isn't really significant (because you shouldn't be using it to handle the error anyway).

Division by zero is an error independent of Rust, and pretty much every language treats it as such. I don't think that's a surprise to anyone.

Do you mean the missing formal guarantee for &i32 and /= being not a problem in practice? Or do you mean the (arguably not-well-documented) difference in behavior between overflow and division-by-zero being not a problem in practice?

Floats behave differently in regard to division by zero, for example. And some languages always panic on underflow/overflow, e.g. SQL if I remember right, for example.

I still feel like this whole topic deserves a better overview somewhere prominently.


Another curious case (I understand why it happens, but still find it curious):

fn main() {
    println!("{}", (0.0 / 0.0) as usize);
}

(Playground)

For division, overflow seems to behave differently than for other operations like addition, multiplication, subtraction; so for division there is no difference in behavior between overflow and division by zero. I'm not certain what you did or did not claim with your statement but I wanted to clarify. For demonstration: While i32::MAX + 1 or i32::MIN * -1 will silently overflow and wrap in release mode, i32::MIN / -1 still panics.

Sorry if I wasn't clear. I didn't mean the overflow during division. I (simply) meant the overflow on adding/substracting.

Well which one do you mean? I don't get the premise in the first place. Since div-by-0 is an error, it's expected to behave specially. I have no idea what formal guarantees you are seeking; the operation is mathematically meaningless, so any useful language has to handle it somehow.

Non sequitur. We are talking about integers here. Floats have their own behavior in Rust, too.

Again, this doesn't contradict anything I wrote earlier. Rust panics on overflow too (in debug mode, and misbehaves in release mode).

Just what is the problem? I don't get it.

Okay, originally my problem was:

I wasn't sure if 0i32 / 0 is guaranteed to panic.

While writing my post, I found this:

So my question has been answered while I wrote the post.

However, there are three things I would like to note:

  • Formally, it doesn't seem to be guaranteed that 0 / &0, &0 / 0, or &0 / &0 panic.
  • Formally, it doesn't seem to be guaranteed that x /= 0 panics, if x is a primitive integer.
  • I think that the Rust Reference could do a better job in clarifying the behavior. Currently it doesn't explain when/if the above operations (or even 0 / 0) panic (though the corresponding section contains information on overflow behavior).

I see two problems here:

  • Integer division by zero seems to be formally underspecified in regard to panicking behavior when references or the /= operator are involved. Do you agree on this? (I mean that it's underspecified, not that it's a problem.)
  • I think the Rust reference could be improved such that it's more clear to the reader when Rust is guaranteed to panic (and when not) in regard to "ordinary" overflows and integer division by zero.

Why do I think it matters whether I get a guaranteed panic? If I know that the division by zero panics, I don't necessarily need to add my own check to the code in order to avoid erratic behavior that isn't caught.


P.S.: Afterall, depending on the number system used, you could claim that 1usize / 0usize is an overflow because the result is +∞ or ±∞. (I don't want to claim it, I just want to say that it depends on the number system used. See also Riemann Sphere for an example in a different context.)

I wouldn't think of the official documentation as too much of a "formal" thing. I don't think anyone would ever actually question the fact that /= gives the same results as / for primitive types or that the &-versions of the trait implementations don't do anything different. As it stands, I suppose I could even be taking out the (realistically, inadequate) "but formally..." argument to say that /= might as well perform addition instead of division since the documentation or reference doesn't say otherwise. (Assuming I'm not missing anything.)

So I agree there's an opportunity to improve the standard library documentation, and probably also the reference (I suppose it doesn't hurt to write these things down in multiple places), so I guess feel free to suggest changes in a PR, and/or to open an issue? I would probably not phrase the issue as "formally it isn't guaranteed" but rather as "this detail isn't actually documented properly", since the former seems confusing as if you're suggesting that you believe Rust might actually decide to make the arithmetic operators of primitive types behave inconsistently at some point in the future.

4 Likes

A-ha! In that case, yes, the docs could/should be improved. I think the place that warrants an addition is the operators section; additionally, the reference-based impls could say explicitly that they do exactly the same thing as the value-based impls.

I don't think DivAssign needs that extra documentation, because it is generally understood as being a shorthand for lhs = lhs / rhs.

So I understand that as I can assume that integer division by zero in Rust always panics. (I honestly wasn't 100% sure, so thanks for re-assuring me.)

@H2CO3 said it doesn't belong to the top level documentation of the primitive integer type(s), and I did agree. But I'm not sure now. Maybe there could be a section like "Error behavior" or something like that, which covers overflows and/or division by zero. But I'm not sure.

I feel like the reference would be a good place to mention this (in addition). But it might also be a matter of personal preference. I tend to look into references more often.

If I find time, I'll consider writing a (non-controversial) PR for the "Operator expressions" section of the reference.

Side note: I still have an almost-one-year-old PR open, which didn't make any progress for a while (on a slightly more controversial topic though):

Yes, I feel like this is the place to go.

I always thought like that too, but note that implementation may differ and there are types which don't implement it, for example.

I stumbled upon this when using the num crate noticing num::Num has num::traits::NumOps as supertrait, but not num::traits::NumAssignOps.

To me, it feels more clear that 0 / &0 behaves the same as 0 / 0 than x /= 0 being the same (edit: in regard to what happens under the hood) as x = x / 0. But I guess that's pretty subjective. (And I think in either case it's not a real problem, but I was on guard due to previous surprises with Rust.)

They generally shouldn't differ. That's bad enough even without any regards to documentation or formal guarantees. In general, DivAssign should be implemented in terms of Div, or vice versa (sometimes the former option can avoid allocations).

But then that's not a problem. If a trait isn't implemented, then it can't differ from another one, and it doesn't need to be documented either.

I don't think that's right or universally true.

I guess, if you love surprises with Rust, look no further than the order of evaluation for the arguments of +=, right?

fn main() {
    let mut x = 1;
    println!("\ni32 += i32");
    *{ println!("left!"); &mut x } += { println!("right!"); 1};
    println!("\ni32 += &i32");
    *{ println!("left!"); &mut x } += { println!("right!"); &1};
    f(&mut x, 1, "\ni32 += i32");
    f(&mut x, &1, "\ni32 += &i32");
}

fn f<T: std::ops::AddAssign<U>, U>(x: &mut T, y: U, desc: &str) {
    println!("{desc} in a generic function");
    *{ println!("left!"); x } += { println!("right!"); y};
}
i32 += i32
right!
left!

i32 += &i32
left!
right!

i32 += i32 in a generic function
left!
right!

i32 += &i32 in a generic function
left!
right!
1 Like

:astonished:

Playground or it didn't happen!!1! :laughing:

Technically Div only describes the behavior of 0.div(0), not of 0 / 0. The latter expression does not call Div::div for i32.

1 Like

I guess that also explains the weird evaluation order shown by @steffahn?

1 Like