Match in match raises non-exhaustive patterns error

enum E {
    A,
    B,
    C,
    D,
    E,
}

fn process_e(e: E) {
    match e {
        E::A | E::B => {
            match e {
                E::A => todo!(),
                E::B => todo!(),
            }
        }
        _ => todo!(),
    }
}

raises:

error[E0004]: non-exhaustive patterns: `E::C`, `E::D` and `E::E` not covered
  --> src/main.rs:12:19
   |
12 |             match e {
   |                   ^ patterns `E::C`, `E::D` and `E::E` not covered
   |
note: `E` defined here
  --> src/main.rs:4:5
   |
1  | enum E {
   |      -
...
4  |     C,
   |     ^ not covered
5  |     D,
   |     ^ not covered
6  |     E,
   |     ^ not covered
   = note: the matched value is of type `E`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern, a match arm with multiple or-patterns as shown, or multiple match arms
   |
14 ~                 E::B => todo!(),
15 ~                 E::C | E::D | E::E => todo!(),
   |

playground

Is it possible to solve this problem at the language or type system level, rather than adding an unreachable!()?

This is indeed possible to solve at a type system level; it's called flow-sensitive typing. TypeScript and Kotlin both provide forms of flow-sensitive typing.

Technically, flow-sensitive typing isn't necessary; if you rewrite the example slightly to

    match e {
        e2 @ (E::A | E::B) => {
            match e2 {
                E::A => todo!(),
                E::B => todo!(),
            }
        }
        _ => todo!(),
    }

then refinement typing is sufficient to determine the inner match is exhaustive.

There currently exists a one-person experiment to introduce pattern refined types into Rustc as the implementation of the internal #[rustc_scalar_valid_start] attributes underlying the NonZeroUnn types, but any introduction of surface-level syntax for such is a ways off yet.

If someone wanted to introduce some small amount of flow-sensitive typing to Rust, then after first getting sign-off from the types team the likely first step would probably be attaching the requisite information to replace the => todo!() addition with => unreachable!() if-and-only-if flow-sensitive typing indicates that the arm is unreachable semantically (but not yet statically).

For the specific case of Result-like enums, it's already the case that #![feature(never_type)] makes it so that Result<T, !> actually essentially deletes the Err variant, allowing you to treat Ok(_) as an irrefutable pattern (e.g. let Ok(val) = res;).

3 Likes

Perhaps this could be done by having inheritance-like relationships between enums?

pub enum AB {
   A, B
}

pub enum ABCD {
   #[flatten] enum AB, // makes ABCD::A and ABCD::B
   C, D
}

match e {
    val @ enum AB => match val { AB::A | AB::B => {} }
    ABCD::C | ABCD::D => {}
}

When working with images I often need different subsets of color spaces (RGB/Gray/YUV/CMYK with and without alpha, float, planar layout, premultiplied, etc.) so being able to easily assemble color enums from subsets would be nice.

My case is a c-like repr(u8) enum used as tag in a binary format. Does it work the same way?

It could if the discriminant values don't conflict. For enums from the same crate the compiler should be able to compute non-overlapping discriminants when they're not explicitly specified.