Matching on int with constants - unexpected behaviour

I am playing around with a Windows message loop and ran across something that seems a bit off and was wondering if there is a better way of doing things?

fn winproc (...., message: u32,...) {
  match message {
    WM_RBUTTONUP => { }
    WM_MOUSEMOVE => {}
    WM_MOUSELEAVE => {}
  
  }

Now the problem is that I messed up the use statements at the top of the file and there is no constant WM_MOUSELEAVE - but this still compiles as what Rust seems to be doing is creating a variable called
WM_MOUSELEAVE as a catch all in the final statement.

The only indication I can see that the compiler is giving me that there is something wrong is an unused variable WM_MOUSELEAVE - or when I add statements after the bad one, it warns about unreachable branches.
It took me a while to notice this as I have a lot of similar benign warnings in my in-progress code.

Is there a way I should structure the match code in this case to prevent these sort of accidents?

3 Likes

I can think of a few ways to deal with this problem; you can:

  • Use if/else instead of match, which doesn't pattern match and will therefore not interpret an unknown identifier as a variable binding.
  • Define a #[repr(u32)] enum WinMsg {...} of all the valid messages, and use WinMsg::RButtonUp et al. as your match arms. This will require a conversion from the u32 that's passed to winproc(), which is nontrivial; there are crates to help with this, but I'm not personally familiar with them.
  • Annotate your match statement with #[deny(unused_variables)] to turn the warning you're currently getting into a hard error.
4 Likes

I really wish this footgun would finally be fixed. Just add a lint, deny-by-default in the 2024 edition, that forbids the use of ALL_CAPS (edit: and CamelCase) catch-all patterns in matches. This would also fix the common problem of matching enum variants and forgetting to use the full prefixed name (or import the variants).

8 Likes

I'll normally structure it like this:

mod consts { 
  const WM_RBUTTONUP: u32 = ...;
  ...
}

fn winproc (...., message: u32,...) {
  match message {
    consts::WM_RBUTTONUP => { }
    consts::WM_MOUSEMOVE => {}
    consts::WM_MOUSELEAVE => {}
  }

By using a path, it becomes unambiguous that you are referring to the constant and not trying to create a new variable binding.

10 Likes

This is the best way in my opinion. Also often gives opportunity to make it look nicer, like winmsg::MOUSEMOVE which I prefer once I have to import the constants a lot.

I tried this :laughing:, but doesn't work:

const X: i32 = 5;

mod dummy {
    pub use super::{self as this_module};
}
use dummy::this_module;

fn main() {
    match 3 {
        this_module::X => println!("Match!"),
        _ => (),
    }
}

(Playground)


Ugly, but works:

const X: i32 = 5;

fn main() {
    match 3 {
        v if v == X => println!("Match!"),
        //v if v == Y => println!("Match!"),
        _ => (),
    }
}

(Playground)

P.S.: It might be possible to write a macro match_value! to do this automatically.

Imho a more common error (particularly among new rustaceans) is to use an existing lower-case variable in one of the match arms. They expect that it will compare values, but instead it creates a new binding.

2 Likes

Yes, really the ideal here would be introduce let x patterns for explicitly binding a name and gradually lint in favor of that over bare names. But I think that ship has sailed, unfortunately.

Agreed, but it's more difficult to lint without causing (many?) false positives. Unless we add some sort of a "beginner mode" set of lints to the compiler. Myself, I wouldn't be against a deny-by-default (or at least warn-by-default) lint that disallows shadowing variables in match patterns though, but others might disagree.

also CamelCase, exactly for the enum-related case.

2 Likes

There's no need for an extra module or clunky macros. Just use self.

const X: i32 = 5;

fn main() {
    match 3 {
        self::X => println!("Match!"),
        _ => (),
    }
}
4 Likes

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.