Search code examples
rustrust-macrosrust-decl-macros

Macro to build enum with different "kinds" of elements


I am trying to come up with a macro which I would call like

create_states!(S0, S1, final S2, final S3);

It will create an enum to represent state machine states and some of them will be final (accepting) states - S2, S3. The resulting enum and its impl should look like:

enum State {
    S0,
    S1, 
    S2,
    S3,
}

impl State {
    fn is_final(&self) -> bool {
        match self {
            Self::S2 => true,
            Self::S3 => true,
            _ => false,
        }
    }
}

My naive attempt:

macro_rules! create_states {
    ($($r:ident),+, $(final $f:ident),*) => {
        #[derive(Copy, Clone)]
        enum State {
            $($s),*
            $($f),*
        }

        impl State {
            fn is_final(&self) -> bool {
                match self {
                    $(Self::$f => true,)*
                    _ => false,
                }
            }
        }
    }
}

is ending up with the following error:

error: local ambiguity: multiple parsing options: built-in NTs ident ('r') or 1 other option.
  --> src/lib.rs:20:24
   |
20 | create_states!(S0, S1, final S2, final S3);
   |                        ^^^^^

Trying to remove the comma between the patterns in the second line:

($($r:ident),+ $(final $f:ident),*) => { ...

is producing another one:

error: no rules expected the token `S2`
  --> src/lib.rs:20:30
   |
1  | macro_rules! create_states {
   | -------------------------- when calling this macro
...
20 | create_states!(S0, S1, final S2, final S3);
   |                              ^^ no rules expected this token in macro call

I think I understand what causing these errors - it thinks that final is another identifier matching r. But what would be the right way to write such a macro (if possible at all without overcomplicating)?

I have full flexibility with the macro invocation as this is my personal learning exercise. The main objective is to learn the right way to do things. It would be nice to have this macro to accept the final at any position if possible.


Solution

  • This can be accomplished with a TT muncher, push-down accumulation, and handling the trailing separators.

    macro_rules! create_states {
        // User entry points.
        (final $name:ident $($tt:tt)*) => {
            create_states!(@ {[] [$name]} $($tt)*);
        };
        ($name:ident $($tt:tt)*) => {
            create_states!(@ {[$name] []} $($tt)*);
        };
    
        // Internal rules to categorize each value
        (@ {[$($n:ident)*] [$($t:ident)*]} $(,)? final $name:ident $($tt:tt)*) => {
            create_states!(@ {[$($n)*] [$($t)* $name]} $($tt)*);
        };
        (@ {[$($n:ident)*] [$($t:ident)*]} $(,)? $name:ident $($tt:tt)*) => {
            create_states!(@ {[$($n)* $name] [$($t)*]} $($tt)*);
        };
    
        // Final internal rule that generates the enum from the categorized input
        (@ {[$($n:ident)*] [$($t:ident)*]} $(,)?) => {
            #[derive(Copy, Clone)]
            enum State {
                $($n,)*
                $($t,)*
            }
    
            impl State {
                fn is_final(&self) -> bool {
                    match self {
                        $(Self::$t => true,)*
                        _ => false,
                    }
                }
            }
        };
    }
    

    See also: