Rust macro accepting type with generic parameters

First of all parsing generics with macro_rules! in a fool proof way is extremely difficult (might be impossible), because patterns do not support mixed repetitions (e.g. $( $( $lt:lifetime ) | $( $gen:ident )* )*, which would match either a lifetime ('a) or a generic parameter (T)).

If this is required, you should consider using a proc-macro (you can even put them in expression position by using proc-macro-hack).

Simply putting the code here without an explanation would benefit nobody, so the below goes through all the steps that are required to understand the final declarative macro :)


Parsing an input in the form of Hello<'a, 'b> or Hello is relatively simple:

macro_rules! simple_match {
    (
        // name of the struct/enum
        $name:ident
        // only one or none `<>`
        $(<
            // match one or more lifetimes separated by a comma
            $( $lt:lifetime ),+
        >)?
    ) => {}
}

simple_match!( Hello<'a, 'b, 'static> );

One might also have constrained lifetimes (e.g. Hello<'a, 'b: 'a, 'static>), which can not be parsed with the above.

To parse this too, the following pattern would have to be added to the end of $lt:lifetime:

// optional constraint: 'a: 'b
$( : $clt:lifetime )?
macro_rules! better_match {
    (
        // name of the struct/enum
        $name:ident
        // only one or none `<>`
        $(<
            // match one or more lifetimes separated by a comma
            $(
                $lt:lifetime
                // optional constraint: 'a: 'b
                $( : $clt:lifetime )?
            ),+
        >)?
    ) => {}
}

better_match!( Hello<'a, 'b: 'static> );

The above is limited to only a single constrained lifetime (Hello<'a: 'b + 'c> would fail to parse). In order to support multiple constrained lifetimes one has to change the pattern to:

$(
    : $clt:lifetime
    // allow `'z: 'a + 'b + 'c`
    $(+ $dlt:lifetime )*
)?

and that is everything needed for parsing generic lifetimes. One could also try parsing higher ranked lifetimes, but this is would make the pattern even more complex.

So the final macro for parsing lifetimes looks like this

macro_rules! lifetimes {
    ( $name:ident $(< $( $lt:lifetime $( : $clt:lifetime $(+ $dlt:lifetime )* )? ),+ >)? ) => {}
}

lifetimes!( Hello<'b, 'a: 'b, 'static, 'c: 'a + 'b> );

The above macro does only allow lifetimes, which can be fixed by replacing lifetime with tt in the pattern (both lifetimes and generic params can be parsed as a tt):

macro_rules! generic {
    ( $name:ident $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? ) => {}
}

generic!( Hello<'b, 'a: 'b, 'static, 'c: 'a + 'b> );
generic!( Hello<T: Display, D: Debug + 'static + Display, 'c: 'a + 'b> );

Like I mentioned above, I think it is currently impossible to differentiate between a lifetime and a trait bound. If this is required one could do it partially with ( $(+ $lt:lifetime )* $(+ $param:ident )* ), but this would not work for unsorted bounds like Hello<'a, T, 'b> or T: 'a + Debug + 'c.


The impl_trait-macro would then be written like this:

use std::fmt::{Debug, Display};

trait ExampleTrait {}

struct Alpha;
struct Beta<'b>(&'b usize);
struct Gamma<T>(T);
struct Delta<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a> {
    hello: &'a T,
    what: &'b D,
}

macro_rules! impl_trait {
    ( $name:ident $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? ) => {
        // I split this over multiple lines to make it more readable...
        // this is essentially just a copy of the above match without the
        // type annotations
        impl $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
            ExampleTrait
        for $name
            // the bounds are not required here
            $(< $( $lt ),+ >)?
        {}
    }
}

impl_trait!(Alpha);
impl_trait!(Beta<'b>);
impl_trait!(Gamma<T>);
impl_trait!(Delta<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a>);

Note: Paths are not supported (for ex. impl_trait!(Hello<D: std::fmt::Display>)


The below macro works with multiple structs in a call:

macro_rules! impl_trait_all {
    ( $( $name:ident $(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)? ),+ ) => {
        $(
            // I split this over multiple lines to make it more readable...
            // this is essentially just a copy of the above match without the
            // type annotations
            impl $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
                ExampleTrait
            for $name
                // the bounds are not required here
                $(< $( $lt ),+ >)?
            {}
        )+
    }
}

impl_trait_all!(
    Alpha,
    Beta<'b>,
    Gamma<T>,
    Delta<'b, 'a: 'static + 'b, T: 'a, D: Debug + Display + 'a>
);

Link to playground with all the code


You could use a tt (single token) identifier to accept a lifetime you want in another macro arm (playground link)

macro_rules! impl_FooTrait {
    ($name:ty, $lifetime:tt) => {
        impl<$lifetime> $crate::FooTrait for $name {  }
    };
    ($name:ty) => {
        impl $crate::FooTrait for $name {  }
    };
}

struct Bar(i32);
impl_FooTrait!(Bar);

struct Baz<'a>(&'a i32);
impl_FooTrait!(Baz<'a>, 'a); // Use and declare the lifetime during macro invocation

Here is an example that actually implements something.

Its a bit weird to look at I guess. I am interested to see any alternative answers. There is possibly a nicer way to do this; I'm not well versed in macro land yet.


I have a partial solution, though I can't make it work for lifetime parameters.

#[macro_export]
macro_rules! impl_trait {
    // this evil monstrosity matches <A, B: T, C: S+T>
    ($ty:ident < $( $N:ident $(: $b0:ident $(+$b:ident)* )? ),* >) =>
    {
        impl< $( $N $(: $b0 $(+$b)* )? ),* >
            $crate::path::to::Trait
            for $ty< $( $N ),* >
        {
            // function implementations go here
        }
    };
    // match when no type parameters are present
    ($ty:ident) => {
        impl_trait!($ty<>);
    };
}

Example (play)