Ensure exhaustiveness of list of enum variants

Suppose I have a value-like enum,

enum FileStatus {
    Intact, Touched, Modified, Corrupt, Untagged, IllFormedXattrs
}

and a test that iterates over all the values,

#[test] fn test_file_status_traits() {
    use FileStatus::*;
    for val in [Intact, Touched, Modified, Corrupt, Untagged, IllFormedXattrs] {
        // ... test stuff about val ...
    }
}

Without using proc macros, is there any way to enforce at compile time that the array in test_file_status_traits is exhaustive? I'm aware that this would be trivial with strum::EnumIter, but the overall program currently has no transitive dependencies on syn and I'm trying to avoid adding any.

You don't have to use syn if you use a macro_rules macro around your enum definition. This way you could implement an iterator on it yourself.

try this macro, it expands to a match expression so the compiler checks it's exhaustive

macro_rules! ensure_exhaustive {
    ($E:path, $($variant:ident),*) => {
        {
            use $E as E;
            let _ = |dummy: E| {
                match dummy {
                    $(E::$variant => ()),*
                }
            };
            [$(E::$variant),*]
        }
    }
}

you can use it like this:

enum Bool {
	False,
	True,
}

let all_variants = ensure_exhaustive!(Bool, False, True);
// => let all_variants = [Bool::False, Bool::True];

let all_variants = ensure_exhaustive!(Bool, False);
// => error: `Bool::True` not covered
16 Likes

That's pretty clever, because it doesn't require the enum definition to be modified (so it also works with 3rd-party types).

2 Likes

That is pretty much exactly what I needed. Like @H2CO3 I appreciate not needing to modify the enum definition -- in my case, it is an enum I defined, but the exhaustive list is only needed in tests, and users of my crate should not need to iterate over the values.

Tiny suggested refinements:

macro_rules! exhaustive_list {
    ($E:path; $($variant:ident),* $(,)?) => {
//          |                     ^^^^^ allow a trailing comma
//          \ syntactically distinguish the type argument
        {
            use $E as E;
            let _ = |dummy: E| {
                match dummy {
                    $(E::$variant => ()),*
                }
            };
            [$(E::$variant),*]
        }
    }
}

[Editorial aside: Practically every time I use macros-by-example, it bugs me that you can't write an MBE whose invocation syntax looks like anything but a basic function call (possibly with square or curly delimiters). It should be possible to do things like declare! Name { ... } (like macro_rules itself!) or, in this case, construct!::<Type>[...].]

1 Like

Given how weird macros can (and do!) get, I think requiring explicit parentheses is an excellent idea for readability (by humans and machines alike).

1 Like

I see where you're coming from there, but I don't agree: I think this limitation gets in the way of defining macros whose invocations are easy to read, and because of that, I also think it might be pushing people to use proc macros for things that could have been macro_rules macros (except that the invocation syntax would have to be suboptimal).

... Probably we should leave it there though, I don't have the mental bandwidth anytime soon to work up a proper analysis of what more flexible MBEs would look like, and without that we're both speculating in a vacuum.

I don't define "easy to read" as an æsthetic quality. I meant unambiguous to parse. Given that macros can support matching almost arbitrary syntax, avoiding ambiguity must basically be the top #1 priority in their design, otherwise they would basically be a huge footgun.

You might not like a pair of parentheses or an extra level of indentation, but they are the difference between practical and completely bewildering.

If you are interested in using MBEs with attribute macro syntax, check out macro_rules_attribute - Rust