Idea: ! and non-exhaustive enums

Continuing the discussion from Blog post: never patterns, exhaustive matching, and uninhabited types:

I could see this pattern being useful to indicate that compilation should fail for a non-exhaustive enum.

// typically requires default match arm
#[non_exhaustive]
enum Foo {
    Bar,
    Baz
}
// ...
match x {
    Foo::Bar => println!("bar"),
    Foo::Baz => println!("baz"),
    ! // emit error when `Foo` extended with new variant.
}

I imagine this creates problems if ! in this context is UB and a stable ABI is desirable. In that case, could ! desugar to _ => panic!("...")?

1 Like

I would say if the new variant is inhabited the compiler should figure out and output error like “attempt to use ! pattern on inhabited discriminant”.

This defeats the whole purpose of #[non_exhaustive]: allows Foo to add new variants without (semver-)breaking downstream crates.

6 Likes

I’m inclined to agree, but let me play the Devil’s advocate. What this is suggesting is a sort of sugar for an unwrap analogue. This makes the actual unwrap something like

match self {
    Some(x) => x,
    !
}

(Except that unwrap has a specific panic message but let’s not get minor details in the way.)

Now, this is certainly not what the ! pattern is for, but I can see a sentiment of wanting to be able to write non-exhaustive matches that panic when they’re not matched (this is default behavior of match in Scala, which is mostly a footgun but occasionally useful). See non-exhaustive let, which has been proposed in some way or another:

let Some(x)! = y; // strawman for purpose of example
// desug
let x = match y {
    Some($x) => $x,
    _ => panic!("match assertion failed"),
}

There are a number of folks that commented on wanting a way to opt-out of #[non_exhaustive]. @glaebhoerl's formulation of the ! pattern, "which lets you assert 'there are no more cases'", would accomplish this end, because the assertion would fail if a new case were added.

I tend to look at these kinds of things from two perspectives. On one hand is the intent of the author, who wants to leave room for extension. On the other is the intent of the consumer, who wants to ensure that their code properly handles extensions. The consumer has one approach--a default match--available to them, and that's a great solution for runtime safety. A "never match" extends their options, allowing them to opt-in to compile-time safety.

@mcy: The panic! is only for binary compatibility, which, as I understand it, isn't supported by the rust compiler outside of ffi. ! would remain a never branch, and would be compiled out of the application.

I think the answer there is to have a lint that warns, so that the stability promises aren't compromised.

For example, maybe clippy could look for _ => unreachable!() in a match over an enum, and warn if there are unhandled variants. That would be good even for non-non_exhaustive enums, and it could be silenced with something like _ => unreachable!("handled before the match") (like the bug! macro in the compiler) if had false positives.

There should be no need for special syntax when it cannot be a hard error.

5 Likes

To confirm, it cannot be a hard error because that would break semver compatibility?

Assuming that's correct, and further that ! strictly conforms to the outline in @nikomatsakis' blog, I agree. @glaebhoerl's idea, however, uses ! to assert there are no unmatched variants. As I understand it, that formulation means using ! with a non-exhaustive enum isn't a special case; ignoring it would be.

The whole point of non-exhaustive, though, is that it's an error to assert that all variants were matched (to allow semver compatability, as you said). So you'd be able to write out that assertion explicitly (not a special case), but it'd be an error just the same as if it were implicitly asserted.

I think my ideal would be that people still have to write the _ arm (perhaps with some special syntax), but that they get a lint warning if there are cases they are not handling.

The idea is that you as the crate author ought to think about whether you can recover from new variants – even throwing a nice panic might suffice, depending on the scenario.

Using a lint is good because it means that if you are a dependency of some other crate, your lint will be “capped” and that dependency can keep building.

Moreover, you can #[deny] the lint locally to get a stronger error if you like (but you have to keep in mind that – if you are a library – that doesn’t matter).

1 Like

So, I have an idea. As you and @RalfJung proposed in the great blog post, we are going to introduce match { ! } as a new way to match impossible arms, why not have match { ? } to mean "if there are more variants, this is a _ => { <ResultType as Default>::default() }, otherwise like !? Also, if the result type didn't implement Default there should be an compile error.

I finally wrote that blog post about whether and when &mut T must be initialized.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.