Search code examples
rustrust-macros

Macro error while trying to implement a trait inside a macro expansion


fn a() {} seems to satisfy a parsing rule that expected a fn, then some other stuff. items should be able to be function definitions, right? So they should work, right?

macro_rules! multi_impl {
    (for $base:ty :
        $($t:ty {
            $($i:item);*
        }),+) =>
    {
        $(
            impl $t for $base
            {
                $( $i )*
            }
        )+
    }
}

trait A {
    fn a();
}

trait B {
    fn b();
}

struct S;

multi_impl! {
    for S:
    A {
        fn a() {}
    }, B {
        fn b() {}
    }
}

fn main() {
    S::a();
    S::b();
}

playground

The error in question:

error: expected one of `const`, `default`, `extern`, `fn`, `pub`, `type`, `unsafe`, or `}`, found `fn a() { }`
  --> <anon>:11:20
   |
11 |                 $( $i )*
   |                    ^^

Making it $( fn $i)* only changes the error to complain about expecting an identifier after the fn, which makes sense, but the initial error does not (at least to me).

Is there a difference to the parser about code that is in the source vs code that's placed into the source by a macro?


Solution

  • The problem isn't that a fn isn't an item, it's that the body of an impl does not contain items. It contains "impl items". What it's complaining about is you trying to put a square-shaped block into a round hole, not that the block is the wrong colour.

    Yes, those are two different things. No, you can't capture "impl items" in a macro. No, you can't turn an item into an impl item. Because macros capture AST nodes, not the tokens. Well, methods can have a self argument and regular function's don't. I don't know, presumably it seemed like a good idea at the time.

    Putting the hypothetical back-and-forth aside, the solution in this case is to not bother trying to match anything in particular and just match anything.

    macro_rules! multi_impl
    {
        (for $base:ty :
            $($t:ty {
                $($body:tt)*
            }),+) =>
        {
            $(
                impl $t for $base
                {
                    $($body)*
                }
            )+
        }
    }