Why does `n % 2` have to match more than 0 and 1?

Friends

What is going on with this. a % 2 can only be 0 or 1.

fn main() {
    let a:usize = 26;
    match a % 2 {
	0 => println!("even"),
	1 => println!("odd"),
    }
}

Clippy says:

error[E0004]: non-exhaustive patterns: `2_usize..` not covered
 --> src/main.rs:3:11
  |
3 |     match a % 2 {
  |           ^^^^^ pattern `2_usize..` not covered
  |
  = note: the matched value is of type `usize`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown

I am concerned I am missing something.

Adding a match arm: _ => unreachable!("Not odd or even"), fixes the clippy error, but there is no actual error. Or am I missing something?

Is this a limitation in clippy and the compiler?

1 Like

That's not clippy, that's rustc's error. match exhaustiveness is based on types, and the knowledge that % 2 returns 0 or 1 is not part of the type system. So you need the unreachable branch. The optimizer will be able to tell this branch is unreachable and remove it.

18 Likes

Checking of exhaustiveness of matches is done entirely based on the type of what is being matched. The type is usize, so you must provide patterns that together cover all of usize.

It’s necessary for the compiler to be very precise about what it allows here, because if it tried to use arbitrary information to decide that additional arms weren't necessary, then some change to how it does the analysis might cause your code to stop compiling in future Rust versions. To avoid that kind of fragility, rules that decide whether code is valid have to be carefully scoped. Note that the places where Rust has provided powerful tools to “do the right thing” given just a little input — type inference and traits — are places where Rust moves slowly. It’s difficult to work on those things without breaking existing programs.

There are ideas for how Rust could gain “pattern types”, subsets of existing types, and if that happened, then there could be a new remainder function (probably not changing the existing operator) whose return type was "usize, but only 0..2". That would be a precisely defined way to avoid needing a third match arm here.

15 Likes

Why wouldn't it? If the compiler supported this for n % 2 should it support it for more complex operations too? And where should it stop given this is an undecidable problem?

3 Likes

I think llvm will optimize away the unreachable branch; it "knows" that unsigned modular reduction only takes those values. It's one of many places where the Rust language forces you to write something that the optimizer can deduce is unnecessary.

A pertinent point is that optimizations are almost never guaranteed.

If you want to force it to understand only two states, would this work?

match a % 2 == 0 {
    true => println!("even"),
    false => println!("odd"),
}
5 Likes

You can also do that like:

match a % 2 {
    0 => println!("even"),
    _ => println!("odd"),
}

... which you can expand to more possibilities, like %3 into 0, 1, _.

7 Likes

(Yet) Another way to think of this is:

fn frobnicate(n: usize) -> usize {
    // I'll only ever return 0 or 1... Promise
}

match frobnicate(n) {
    0 => {},
    1 => {},
    // Why do I need this extra arm?
    _ => {},
}

Sure it might (or might not) seem obvious that you only have 0 or 1 as possibilities, but without more information it's impossible to know.

This is basically the compiler's view of the situation. Our argument is essentially reduced to "trust me bro" and the compiler doesn't trust us :smiley:

3 Likes

Note that you can always add

_ => unreachable!(),

to the match to make it explicit.

Personally, I like the trade-off that Rust has made where I have to type that if it's not clear from the types involved, because I can use my own types to tweak it. I much prefer it to other languages that either silently do nothing (_ => {}) or silently throw if you didn't handle something, even if sometimes it means writing unreachable!().

2 Likes