0

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Eugene Sh.
  • 17,802
  • 8
  • 40
  • 61

2 Answers2

4

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:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Ouch.. First conclusion - no easy way here. +1, will accept once I understand it.. – Eugene Sh. Sep 24 '19 at 18:27
  • Could you replace the first two rules with this single one `($($tt:tt)*) => { create_states!(@ {[] []} $($tt)*);` and make it a little bit simpler? – rodrigo Sep 25 '19 at 14:12
  • 1
    @rodrigo probably (I didn't copy this code into the playground and make the suggested change), but I was following the [advice from TLBORM](http://danielkeep.github.io/tlborm/book/pat-incremental-tt-munchers.html): *It is recommended that, when writing a TT muncher, you make reasonable efforts to keep recursion as limited as possible. This can be done by adding additional rules to account for variation in the input (as opposed to recursion into an intermediate layer)* – Shepmaster Sep 25 '19 at 14:19
1

Shepmaster's answer is more general, but in your specific case, since you "have full flexibility in the macro invocation", you could just replace final with @final and the naive attempt works barring a couple of minor typos:

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

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

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

playground

Jmb
  • 18,893
  • 2
  • 28
  • 55
  • So just to understand why it works.. is it because `@something` will never be matched with `ident`? – Eugene Sh. Sep 25 '19 at 13:30
  • @EugeneSh. Correct. This is also the reason why it is [customary to use `@something` for internal rules when writing macros](https://stackoverflow.com/a/54406657/5397009). – Jmb Sep 25 '19 at 13:46