Skip to content

← Ground Rules: Values, Types, Control Flow step 5 of 24

Medium Primitives

Integer overflow: debug panics, release wraps

An eight-bit odometer. It starts at start and each delta moves it. It has 256 positions and it wraps in both directions. Report the reading after each delta.

pub fn meter_readings(start: u8, deltas: &[i32]) -> Vec<u8>

(250, [10, -20, 300]) gives [4, 240, 28]:

  • 250 + 10 = 260, which does not fit in a byte, so it wraps to 4;
  • 4 - 20 = -16, which wraps the other way to 240;
  • 240 + 300 = 540, and 540 mod 256 = 28.

The starter does not compile, and after it does you will meet a behaviour that depends on how the program was built.

The same source, two different behaviours

This is the first place in Rust where identical source code does different things under different build profiles, and it catches everybody once.

let mut x: u8 = 250;
x += 10;
  • Debug build (cargo run, cargo test, no -O): this panics with attempt to add with overflow, and your program stops.
  • Release build (cargo run --release, or rustc -O — which is what this site uses): this wraps to 4, silently, and the program carries on.

Both are defined behaviour. Neither is undefined behaviour — Rust is not C here, and there is no nasal-demon clause. The language deliberately says “overflow is a program bug; checking costs cycles; here is a knob.” The knob is overflow-checks, which defaults to on in debug and off in release.

On this site the build line is rustc -O, so debug_assertions is off and arithmetic wraps. No problem in this course can ever assert “this panics on overflow”, because at -O it does not. That is why this problem asks you to predict the wrap rather than to catch a panic.

The trap: constants are checked at compile time

Try to be clever and hard-code the overflow and you get a hard error, at every optimisation level:

let x: u8 = 250 + 10;
// error: this arithmetic operation will overflow

That is arithmetic_overflow, a deny-by-default rustc lint that runs during constant evaluation. The same is true for a constant out-of-bounds index (unconditional_panic) and for a literal division by zero. So the values in this problem have to flow through parameters — which they do — or the compiler would refuse the program before it ever ran.

This is a genuinely useful thing to know: Rust catches the overflows it can see at compile time, and the ones it cannot see depend on the profile.

What you have to write

Two obstacles, in order.

First, current += d does not compile: current is a u8 and d is an i32, and Rust implements AddAssign<u8> for u8 but not AddAssign<i32>. There is no implicit widening or narrowing in Rust, ever. You must convert.

Second, the conversion you want is d as u8, which keeps the low eight bits and throws the rest away. That is exactly right for an odometer:

  • 10 as u8 is 10;
  • -20 as u8 is 236, because -20 in two’s complement is …11101100, and adding 236 to a byte is the same as subtracting 20 from it;
  • 300 as u8 is 44, because 300 - 256 = 44.

Once every delta is a u8, plain += gives you the wrap-around for free. Item 1.10 goes into as properly; item 1.6 shows you the methods that let you say “I want wrapping” out loud instead of relying on the profile.

The thing to take away

If you copy this function into a cargo project and run it in debug, it will panic on the very first test case. That is not a contradiction — it is the same code, built with checks on. Any time a Rust program behaves differently on your machine and in CI, ask about the profile first.