Skip to content

Conversation

@YuniqueUnic
Copy link
Contributor

@YuniqueUnic YuniqueUnic commented Oct 24, 2024

I encountered an “unreachable” issue while utilizing Rust’s match statement. In search of a more effective solution, I considered employing a macro. However, I found that there wasn’t an existing macro to address this issue. Consequently, I decided to create one myself.

Could you please review my code? I’m looking for feedback and suggestions. If you find it beneficial and suitable, I would appreciate it if you could merge it. Thank you.

related issues:

  1. matching on bitflags leads to unreachable patterns #201
  2. Match expression question #375
  3. ...

Implemented a new macro, match_bitflag, to perform matching operations on bitflags, similar to Rust's match expression. This macro addresses the matching issues that arise when using regular match expressions with bitflags.

  • The macro supports complex bitflag combination matching.
  • Added unit tests to verify the correctness of the macro.
  • Provided usage examples and an explanation of the macro's internal implementation.
/// bitflags match patterns similar to Rust's match expression
///
/// This macro allows for matching bitflag combinations in a way that resembles
/// Rust's native match expression, but works correctly with bitflag operations.
///
/// # Requirements
///
/// - The struct used with this macro must implement `PartialEq`.
/// - It's recommended to use this macro with structs created by the `bitflags!` macro.
///
/// # Syntax
///
/// ```ignore
/// bitflags_match!(expression, {
///     pattern1 => result1,
///     pattern2 => result2,
///     ...
///     _ => default_result
/// })
/// ```
///
/// The final `_ => default_result` arm is required, otherwise the macro will fail to compile.
///
/// # Examples
///
/// ```rust
/// use bitflags::{bitflags, bitflags_match};
///
/// bitflags! {
///     #[derive(PartialEq)]
///     struct Flags: u8 {
///         const A = 1 << 0;
///         const B = 1 << 1;
///         const C = 1 << 2;
///     }
/// }
///
/// let flags = Flags::A | Flags::B;
///
/// bitflags_match!(flags, {
///     Flags::A => println!("A"),
///     Flags::B => { println!("B"); }
///     Flags::C => println!("C"),
///     Flags::A | Flags::B => {
///         print!("A");
///         print!(" | ");
///         print!("B");
///     },
///     Flags::A | Flags::C => { println!("A | C") },
///     Flags::B | Flags::C => println!("B | C"),
///     Flags::A | Flags::B | Flags::C => println!("A | B | C"),
///     _ => println!("other")
/// })
/// ```
///
/// # How it works
///
/// The macro expands to a series of if(pattern){ return result; } statements,
/// checking equality between the input expression and each pattern. This allows for
/// correct matching of bitflag combinations, which is not possible with a regular
/// match expression due to the way bitflags are implemented.
///
/// # Note
///
/// The order of patterns matters. The first matching pattern will be executed,
/// so more specific patterns should come before more general ones.
#[macro_export]
macro_rules! bitflags_match {
    ($operation:expr, {
        $($t:tt)*
    }) => {
        // Expand to a closure so we can use `return`
        // This makes it possible to apply attributes to the "match arms"
        (|| {
            $crate::__bitflags_match!($operation, { $($t)* })
        })()
    };
}

/// Expand the `bitflags_match` macro
#[macro_export]
#[doc(hidden)]
macro_rules! __bitflags_match {
    // Eat an optional `,` following a block match arm
    ($operation:expr, { $pattern:expr => { $($body:tt)* } , $($t:tt)+ }) => {
        $crate::__bitflags_match!($operation, { $pattern => { $($body)* } $($t)+ })
    };
    // Expand a block match arm `A => { .. }`
    ($operation:expr, { $pattern:expr => { $($body:tt)* } $($t:tt)+ }) => {
        {
            if $operation == $pattern {
                return {
                    $($body)*
                };
            }

            $crate::__bitflags_match!($operation, { $($t)+ })
        }
    };
    // Expand an expression match arm `A => x,`
    ($operation:expr, { $pattern:expr => $body:expr , $($t:tt)+ }) => {
        {
            if $operation == $pattern {
                return $body;
            }

            $crate::__bitflags_match!($operation, { $($t)+ })
        }
    };
    // Expand the default case
    ($operation:expr, { _ => $default:expr $(,)? }) => {
        $default
    }
}

Implemented a new macro, `match_bitflag`, to perform matching operations on bitflags, similar to Rust's match expression. This macro addresses the matching issues that arise when using regular match expressions with bitflags.

- The macro supports complex bitflag combination matching.
- Added unit tests to verify the correctness of the macro.
- Provided usage examples and an explanation of the macro's internal implementation.
@KodrAus
Copy link
Member

KodrAus commented Jan 1, 2025

Hi @YuniqueUnic 👋

It's a bit of a papercut that A | B becomes an or-pattern inside a match expression instead of an equality check on a bitor'd value for flags. I think enough of one that having a macro for matching is worth it.

Would you be happy to rename this to bitflags_match! and follow the same syntax as match arms do? Match arms don't require a trailing , if the body of the match is a block like:

match x {
    y => {} // <-- this is a block, so no trailing comma needed here
    z => (), // <-- this is not a block, so trailing comma needed here
}

YuniqueUnic and others added 2 commits January 9, 2025 21:44
- 将 `match_bitflag!` 宏重命名为 `bitflags_match!`,以更好地反映其功能
- 更新了 `bitflags_match!` 宏的文档说明,提高了清晰度
- 修改了相关测试模块的名称,以适应新的宏命名
@YuniqueUnic
Copy link
Contributor Author

Hello, @KodrAus

Thanks for the feedback! I've made the changes:

  • Renamed the macro to bitflags_match! as suggested.
  • Improved the documentation and examples to reflect the changes.
  • Adjusted the test module names to match the new macro name.

I tried to make the syntax follow the match arm rules (blocks do not require a trailing comma, non-blocks do), but it failed to compile, so for now, each pattern in the match still requires a trailing comma... I'm unable to solve this problem...

Please let me know if there are any other adjustments needed!

@KodrAus
Copy link
Member

KodrAus commented Jan 10, 2025

Thanks @YuniqueUnic! I think we'll need to make trailing commas work correctly before we can ship this, but it may require some macro-hackery to make it work. I'd be happy to try take a look at it and see if I can come up with a solution.

@KodrAus
Copy link
Member

KodrAus commented Jan 11, 2025

I've had a look at this and come up with a macro that I think matches most of the standard match syntax. You can see the diff here: KodrAus@925d014, or the full macro here: https://github.com/KodrAus/bitflags/blob/925d014c56ec14f6873fd02fd37eab74f289b039/src/lib.rs#L657-L703

What I've written is a style of macro called a tt-muncher. It lets us incrementally pull tokens and expand them as we go. The main benefit it gives us is a way to apply multiple rules to a collection of inputs. So we can have a rule for match arms where the body is surrounded by { }, and another for match arms where the body isn't.

You should be able to pull this commit or my https://github.com/KodrAus/bitflags/tree/feat/bitflags_match branch into this PR.

…flags_match` macro

- Thanks to the help from https://github.com/KodrAus, the syntax of `bitflags_match` is aligned with Rust's `match` syntax.
- Support block statements as the body of match arms, which improves code expressiveness and readability.
- Added handling for trailing commas, making the use of the macro more flexible.
- Optimized the internal implementation of the macro so that it expands to a series of `if(pattern){ return result; }` statements.
- Updated the documentation to clarify the usage and precautions of the macro.
@YuniqueUnic
Copy link
Contributor Author

@KodrAus

Thank you for your support!!!

I forked your code, modified the doc of bitflags_match and I think I can learn the macro from you.
Now, I pulled the request again.

Thanks!!

  • Thanks to the help from @KodrAus, the syntax of bitflags_match is aligned with Rust's match syntax.
  • Optimized the internal implementation of the macro so that it expands to a series of if(pattern){ return result; } statements.
  • Updated the documentation to clarify the usage and precautions of the macro.
  • Updated the test to verify that the macro is functioning correctly.

@YuniqueUnic YuniqueUnic changed the title feat(core): Add match_bitflag macro for bitflag matching feat(core): Add bitflags_match macro for bitflag matching Jan 14, 2025
Copy link
Member

@KodrAus KodrAus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @YuniqueUnic! This looks good to me.

@KodrAus KodrAus merged commit 8d829b6 into bitflags:main Jan 15, 2025
10 checks passed
This was referenced Jan 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants