20
fn main() {
    let num: u8 = 255;
    let num2: u8 = num + 1;
    println!("{}, {}", num, num2);
}

When $ cargo build --release, this code doesn't make compile error. And $ cargo run, make runtime error.

thread 'main' panicked at 'attempt to add with overflow', src/main.rs:3:20 note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

This is okay. But what I don't understand is the situation below. When I delete println line, it makes compile error.

fn main() {
    let num: u8 = 255;
    let num2: u8 = num + 1;
}
$ cargo build --release

error: this arithmetic operation will overflow
 --> src/main.rs:3:20
  |
3 |     let num2: u8 = num + 1;
  |                    ^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

Why does integer overflow sometimes cause compilation errors or runtime error?

Junhee
  • 203
  • 1
  • 5
  • That's very strange. It seems to be a bug of Rust compiler. None of the test cases of `arithmetic_overflow` is tested with `println!`. If you remove `num` in `println!`, it will also fail to compile. – cup11 Apr 05 '23 at 12:50
  • 2
    It looks like the compiler inlines `num` if it's only used once, and then applies constant folding at compile time. If `num` is used more than once, it's not inlined, so the error will happen at runtime (and only in debug mode, since range checking is disabled in release mode by default). I'm only inferring a possible explanation from the compiler behaviour. I don't know whether this is what's actually going on. – Sven Marnach Apr 05 '23 at 12:59
  • 3
    I'd suggest you to report a bug, `println!()` shouldn't supress the lint. – Chayim Friedman Apr 05 '23 at 14:24
  • Here's another case where compile-time checks are thwarted by a simple reference: [Why isn't this out-of-bounds error detected at compile-time?](/q/72999577/2189130) – kmdreko Apr 11 '23 at 15:30

2 Answers2

9

It's going to be a compile time error when the compiler can trivially prove it's going to overflow at runtime, that happens when you remove the println because then num can be inlined easily (it's only used in that one spot anyways), but println is very hard to optimize around because it takes references to it's arguments and it's not easily provable that different addresses don't make a difference for it (consider also there is a fmt::Pointer) all of this leads to the fact that it's not as trivial to prove for the first case where num can't be inlined that easily.

For reference here are the mir representations of the first and second variable in each variant where you can see that in one the variable is replaced with u8::MAX already:

  • without println:
    [...]
        bb0: {
            _1 = const u8::MAX;              // scope 0 at plain_overflow.rs:2:19: 2:22
            _3 = const u8::MAX;              // scope 1 at plain_overflow.rs:3:20: 3:23
            _4 = CheckedAdd(_3, const 1_u8); // scope 1 at plain_overflow.rs:3:20: 3:27
            assert(!move (_4.1: bool), "attempt to compute `{} + {}`, which would overflow", move _3, const 1_u8) -> bb1; // scope 1 at main.rs:3:20: 3:27
        }
    [...]
    
  • with println:
    [...]
        bb0: {
            _1 = const u8::MAX;              // scope 0 at with_print.rs:2:19: 2:22
            _3 = _1;                         // scope 1 at with_print.rs:3:20: 3:23
            _4 = CheckedAdd(_3, const 1_u8); // scope 1 at with_print.rs:3:20: 3:27
            assert(!move (_4.1: bool), "attempt to compute `{} + {}`, which would overflow", move _3, const 1_u8) -> bb1; // scope 1 at with_print.rs:3:20: 3:27
        }
    [...]
    

where in both cases _1, _3 and _4 correspond to num, the value of num in the line where num2 is assigned and the result of a checked additon of num and 1 respectively.

After some more experimentation, not println is the culprit but merely taking a reference to num

cafce25
  • 15,907
  • 4
  • 25
  • 31
  • 1
    Inlining is about functions. You can compile-time calculate `num2` without suppressing the definition of `num`. Of course, the current heuristics may not, but from a theoretical point of view there's really nothing preventing the calculation. This would be _very_ different if `println!` was _between_ the definitions, but the current case -- println or not -- is trivial. – Matthieu M. Apr 05 '23 at 14:59
  • @MatthieuM. It may well be that the compiler looks at how the variable is used before deciding whether to do data-flow analysis on it in a second pass. – kaya3 Apr 05 '23 at 15:11
  • @kaya3: Possibly, but at that point all we have are hypotheses, nothing concrete. – Matthieu M. Apr 05 '23 at 15:14
  • @MattieuM. You're right inlining is probably not the right term, but I can't come up with a better one. Anyways I've added the relevant part of the mir of both cases where what I mean is illustrated, does that make it clearer what's happening? – cafce25 Apr 05 '23 at 16:19
  • @cafce25 I think in this case the "proper term" is "constant folding" or maybe "constant propagation" since the 255 and 1 appear in different expressions. – Jmb Apr 06 '23 at 06:27
2

Compiler warnings are produced by linting and it might be hard to statically check the arithmetic overflow in all cases. In this case, println!() is somehow making it difficult for the compiler to detect the overflow. I think it might be worth reporting this as a bug?

Also, the reason why the code produced a runtime error is that you didn't run it in release mode. Try running the same code as cargo run --release and then you'll get output as

255, 0

This is because, in release mode, Rust does not include checks for arithmetic overflow that cause panics. Instead, if overflow occurs, Rust performs two’s complement wrapping.

anand
  • 271
  • 1
  • 6
  • 3
    In Release mode, rustc doesn't include overflow checks _by default_, but you can still enable them if you so wish. – Matthieu M. Apr 05 '23 at 14:57
  • 1
    *Instead, if overflow occurs, Rust performs two’s complement wrapping.* - is that guaranteed with overflow checking disabled? I thought you still had to use `num.wrapping_add(1)` if you wanted well-defined unsigned binary wrapping. But IIRC Rust doesn't have undefined behaviour, so I guess it has to do *something* in release mode if when you don't enable checking. (https://doc.rust-lang.org/std/primitive.u8.html#method.wrapping_add) – Peter Cordes Apr 05 '23 at 23:13