Enum+match: Get compile warning when i expect an error

I stumbled about an behavior of the rust compiler, which i find strange.
I would expect, that i get a compile error for the following code, but i get only warnings:

[derive(Debug)]
enum Test1 {
    A,
    B
}

#[derive(Debug)]
enum Test2 {
    A,
    B
}

fn convert_a(t1: Test1) -> Test2 {
    match t1 {
        A => Test2::A,
        B => Test2::B,
    }
}

fn convert_b(t1: Test1) -> Test2 {
    match t1 {
        ARRRRGGG => Test2::A,
        B => Test2::B,
    }
}


fn main() {
    { // Part 1
        let val = Test1::B;
        println!("val: {:?}", val);
        let val_conv = convert_a(val);
        println!("val_conv: {:?}", val_conv);
    }
    
    { // Part 2
        let val = Test1::B;
        println!("val: {:?}", val);
        let val_conv = convert_b(val);
        println!("val_conv: {:?}", val_conv);
    }
}

The combination of these to warning are interesting:

warning[E0170]: pattern binding `A` is named the same as one of the variants of the type `Test1`
  --> src/main.rs:15:9
   |
15 |         A => Test2::A,
   |         ^ help: to match on the variant, qualify the path: `Test1::A`
   |
   = note: `#[warn(bindings_with_variant_name)]` on by default
 warning: unreachable pattern
  --> src/main.rs:16:9
   |
15 |         A => Test2::A,
   |         - matches any value
16 |         B => Test2::B,
   |         ^ unreachable pattern
   |
   = note: `#[warn(unreachable_patterns)]` on by default

I get the following output from the program:

val: B
val_conv: A
val: B
val_conv: A

I was expecting, that the compiler infers the type of t1 in function convert_a() and than use Test1::A. And from the warning i know, that the compiler is able to do this.
In case of convert_b i would expect an error message from the compiler, that ARRRRGGG is not defined.

The real problem comes in combination with the second warning: This leads to an behavior of the program which i don't expect. Does anyone know, why the compiler is doing this? Is this intended behavior

If, I fix all warning, than everything is fine. But currently, I'm implementing a trait with several methods and test each implemented method, after i implemented it. Since i use the warnings as a kind of remainder, that i have to do something at this point, i did not give the parameter a name which start with an underscore. Therefore I get a lot of warnings currently and didn't saw these warnings. Therefore i spend some time to debug the program, but could not find the reason.

In combination with "#![allow(warnings)]" this is really dangerous.

I'm an C/C++ programmer and I'm currently trying rust since it can also be used for low-level stuff and has similar performance, but with the promise that i can't shoot my self in the foot anymore. In this case, I think the promise is not true.

PS:

# rustc --version
rustc 1.50.0-nightly (1c389ffef 2020-11-24)

Edit: See the post from jdahlstrom for good explanation.

Well, #[allow(warnings)] is dangerous. You shouldn't use it in general.

The compiler produces no error but some warnings mean the source code is weird, really weird, but at least it is syntactically valid and compiles to program without touching UB(assuming no unsafe code).

3 Likes

Variant names are not automatically in scope in a match statement (or anywhere for that matter), so you need to either import them or use full names:

match x { Test1::A => … }

or

use Test1::*; match x { A => … }

So, why does your code compile anyway? Because a pattern that consists of a single identifier, and does not refer to an enum variant currently in scope, is a wildcard pattern that matches any value and binds the value to the given identifier! This may be perplexing at first, but it is just a "degenerate" case of destructuring, the way you can bind parts of values in more complex patterns, like match opt { Some(val) => … }.

5 Likes

To elaborate on that:

match foo {
    A => bar
}

is more explicitly written as

match foo {
    _ @ A => bar
}

which is in fact the form I increasinly prefer to write, exactly because it's not always immediately obvious (even when reading some of my own old code!) what a bare identifier in that position is doing.

6 Likes

To elaborate even more, note that in Rust, let bindings allow a pattern on the left-hand side, not just a simple identifier, as long as that pattern is irrefutable, that is, can match any value the right-hand side may have. With that in mind, you can see that the most basic form of let that we're all familiar with, let x = y; is simply what you get when you put a wildcard pattern on the left-hand side! I find this quite elegant.

So what happens if you do this:

enum X { A, B }
use X::A;
let A = B;

I actually had to check to make sure, but it is indeed consistent with match: the compiler refuses to compile that because A is a refutable pattern referring to the enum variant A!

3 Likes

I once thought it'd be useful convenient to shorten all those explicit variant names by doing something like this before a match:

use Test1::*

Then I found out what happens if you ever remove the use statement. The variants turn into wildcard patterns and only the first branch will ever be taken, and no compiler errors. EEEK!!

1 Like

#![deny(bindings_with_variant_name)] seems like a sensible thing to do for this reason, tbh.

6 Likes