Macro that emits trait impl?

I'm trying to write a macro that will help to conditionally compile either a derive or a manual trait impl based on a cfg. It should output code like:

#[cfg_attr(any(feature = "derive", test), derive(Foo))]
pub struct Bar<T>(T);

#[cfg(not(any(feature = "derive", test)))]
unsafe impl<T> Foo for Bar<T> {}

Here's my attempt:

macro_rules! derive_trait {
        pub struct $name:ident $(<$($tyvar:ident),*>)? { $($body:tt)? }
    ) => {
        #[cfg_attr(any(feature = "derive", test), derive($($trait),*))]
        pub struct $name $(<$($tyvar),*>)? { $($body)? }

            #[cfg(not(any(feature = "derive", test)))]
            unsafe impl $(<$($tyvar),*>)? $trait for $name $(<$($tyvar),*>)? {}

However, when I invoke it like so:

derive_trait! {
    pub struct Bar { }

...I get the following error:

error: meta-variable `tyvar` repeats 0 times, but `r#trait` repeats 1 time
    --> src/
1147 |           $(
     |  __________^
1148 | |             #[cfg(not(any(feature = "derive", test)))]
1149 | |             unsafe impl $(<$($tyvar),*>)? $trait for $name $(<$($tyvar),*>)? {}
1150 | |         )*
     | |_________^

Any advice on how to fix this?

You are intermixing the syntactic levels of the trait names and the generic type variables.

In the matcher, $trait and $tyvar are independent. They are both nested in 1 and 2 layers of repetition, respectively, inside the root, but not inside each other.

In the substitution part, however, you are trying to put $tyvar inside the repetition for $trait – which causes $tyvar be inside one too many layers of repetition.

Is there any way to say "for each $trait, repeat each $tyvar"?

I don't know a way using declarative macros. This is definitely possible with a procedural macro, though.

1 Like

you can't mix different level of expansion of repeated captures, but you can use recursion to expand in steps:

macro_rules! derive_trait {
		$vis:vis struct $name:ident $(<$($tyvar:ident),*>)? { $($body:tt)* }
	} => {
		#[cfg_attr(any(feature = "derive", test), derive($($trait),*))]
		$vis struct $name $(<$($tyvar),*>)? { $($body)* }
		derive_trait!(@recur: $name, <$($($tyvar),*)?>, $($trait),+);
	(@recur: $name:ident, <$($tyvar:ident),*>) => { };
	(@recur: $name:ident, <$($tyvar:ident),*> , $trait:ident $($tt:tt)*) => {
		#[cfg(not(any(feature = "derive", test)))]
		unsafe impl <$($tyvar),*> $trait for $name <$($tyvar),*> {}
		derive_trait!(@recur: $name, <$($tyvar),*> $($tt)*);

note the macro entry point is slight different from your original example code:

  • use vis capture to optionally capture the pub keyword
  • use + repeat instead of * repeat for $trait name captures
  • use * repeat instead of ? optional for struct $body definition
  • omit the optional ? $tyvar captures for the recursion entries: an empty <> pair is equivilent to no angled brackets

also, technically the $trait capture should use $trait:path instead of $trait:ident, but that brings a little bit complication for the recursion patttern, which doesn't help illustrate the technique here (a path catpure can't be directly followed by tt capture, to avoid parsing ambiguouty, it can be done, that unnecessarily make the example more complicated).

1 Like

Awesome, thanks!