Enum to number in a match - works, but clunky

enum Pcode { Volume: = 9, Avatar = 47 }

...

    const VOLUME: u8 = Pcode::Volume as u8;   // convert from enum
    const AVATAR: u8 = Pcode::Avatar as u8;
    let result = match pcode {
        VOLUME => { something() }, 
        AVATAR =>  { something_else() },
        _ => { default_something() }
    };

Match expressions don't seem to allow enough compute to do the conversion in the match expression, even though that evaluates to a constant.

I do agree it's clunky, but there's no other way in stable Rust. There's inline_const feature in nightly to solve this issue but it's not stabilized yet.

4 Likes

OK. Rust is so expression-oriented that I thought there would be a way, and I just didn't know it.

I think you would find num_enum::FromPrimitive useful here.

3 Likes

Maybe not a very good idea, but this seems to work fine (this is UB, see below) and the reference says that if all of the discriminants are fieldless, enum variants are just integers:

use std::mem;

enum Pcode { Volume = 9, Avatar = 47 }

fn main() {
	let pcode: u8 = 48;

    let result = match unsafe { mem::transmute::<_, Pcode>(pcode) } {
        Pcode::Volume => { "volume" }, 
        Pcode::Avatar => { "avatar" },
        _ => { "not a valid discriminant" },
    };
    println!("{:?}", result);
}

I wonder if this is actualy safe to do or not.


I found a crate that does a similar thing:

#[macro_use] extern crate enum_primitive;
use enum_primitive::FromPrimitive;

enum_from_primitive! {
enum Pcode { Volume = 9, Avatar = 47 }
}

fn main() {
	let pcode = 9;

    let result = match Pcode::from_i32(pcode) {
        Some(Pcode::Volume) => { "volume" }, 
        Some(Pcode::Avatar) => { "avatar" },
        None => { "not a valid discriminant" },
    };
    println!("{:?}", result);
}

It's not safe, it is Undefined Behavior to create an enum value from an invalid discriminant.

4 Likes

You are right, and it seems like the compiler normally assumes that all enum values have an valid discriminant:

use std::mem;

enum Pcode { Volume = 9, Avatar = 47 }

fn main() {
	let pcode: u8 = 48;

    let result = match unsafe { mem::transmute::<_, Pcode>(pcode) } {
        Pcode::Volume => { "volume" }, 
        Pcode::Avatar => { "avatar" },
        //_ => { "not a valid discriminant" },
    };
    assert_eq!("avatar", result);
}

What I found interesting is that, if I uncomment the line which starts with _, then compiler also checks for that, even in release build:

use std::mem;

enum Pcode { Volume = 9, Avatar = 47 }

fn main() {
	let pcode: u8 = 48;

    let result = match unsafe { mem::transmute::<_, Pcode>(pcode) } {
        Pcode::Volume => { "volume" }, 
        Pcode::Avatar => { "avatar" },
        _ => { "not a valid discriminant" },
    };
    assert_eq!("not a valid discriminant", result);
}

Shouldn't the compiler just assume unsafe { mem::transmute::<_, Pcode>(pcode) } is a valid variant of Pcode since it is UB otherwise?

It does assume the result is a valid variant of Pcode (hence the first example compiling, and the unreachable pattern warning in the second example). But when it isn't, that's UB and anything could happen. Including "nothing bad", but you cannot count on that.

(I am surprised it kept the _ branch around as written instead of turning it into an unreachable!() or worse, but compiler does what compiler wants... especially in the face of UB. It may do something else tomorrow.)

I know, it is just unspecified. I meant that if it is UB, and probably it will stay UB, then I think the compiler should exploit it for optimization. Should I open an issue?

It sounds like a possible optimization but it's only interesting if it somehow shows up in real life programs with some frequency. This issue seems related Arms permitted when matching on uninhabited types · Issue #55123 · rust-lang/rust · GitHub

1 Like

I think it should be good to even warn if _ is used while all of the enum variants handled since it is a sign of relying on a UB. But I don't know if it worths adding it considering compile time effect.

Right. All I want to do is create a numeric value from a valid enum. That's a safe operation, subject only to range checks.

Match requires a literal_pattern. Plus there's a hack to allow a minus sign in front of a numeric literal. I'm surprised that a concrete const isn't allowed there, of any of the types allowed in a match pattern.

It does:

pub enum Foo { A, B }
pub fn demo(foo: Foo) {
    match foo {
        Foo::A => (),
        Foo::B => (),
        _ => panic!("new"),
    }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=d23975cb988f0c1020adc72f4a5ee9fa

warning: unreachable pattern
 --> src/lib.rs:6:9
  |
6 |         _ => panic!("new"),
  |         ^
  |
  = note: `#[warn(unreachable_patterns)]` on by default
1 Like

Right, that's what you're doing with as. I was (too indirectly) suggesting a different overall approach. Instead of matching your pcode: u8 variable against some literal u8s, you could instead create a valid enum (with no payload) from pcode instead, and then match against all the enum variants. Then you also get the benefit of Rust's exhaustive match checking.

Going that direction -- from the numeric value to a payload-less enum variant -- is the unsafe part (because transmuting an invalid discriminant to an enum variant is UB). You could check your values and then transmute yourself, keeping everything in sync whenever your enum changes. Or you could use a crate like num_enum to generate that code automatically (FromPrimitive), which also encapsulates the unsafeness. Or you could write your own macros to do the same.

Nice, I didn't notice it. It is still interesting to me that compiler generates a code for the arm which it already knows unreachable.

Sounds like a good codegen bug to file!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.