Extracting only the name of a type with macro_rules

Hey there, I'm trying to create an Enum using the code bellow. I'm matching against a list of types that could be either Foo or Foo, but in the second case I need to extract the name of the type (i.e. Foo, and I need only the Foo part).

I wonder how I can achieve this, I'm not sure I'm even going in the right direction here.

struct Foo;
struct Bar<T>(T);

macro_rules! enum_variants {
    ($name:ident<$spec:ident>) => {
        $name
    };
    ($name:ident) => {
        $name
    }
}

macro_rules! build_enum {
    ($($type:ty),+) => {
        enum Kinds {
            $(
                enum_variants!($type)($type),
            )+
        }
    }
}

build_enum!(Foo, Bar<T>);
// The intended code would be
//enum Kinds {
//    Foo(Foo),
//    Bar(Bar<T>),
//}

The Error:

error: expected one of `(`, `,`, `=`, `{`, or `}`, found `!`
  --> src/lib.rs:17:30
   |
17 |                 enum_variants!($type)($type),
   |                              ^ expected one of `(`, `,`, `=`, `{`, or `}`
...
23 | build_enum!(Foo, Bar<T>);
   | ------------------------ in this macro invocation
   |
   = help: enum variants can be `Variant`, `Variant = <integer>`, `Variant(Type, ..., TypeN)` or `Variant { fields: Types }`
   = note: this error originates in the macro `build_enum` (in Nightly builds, run with -Z macro-backtrace for more info)

error: macro expansion ignores token `!` and any following

A few things:

Macros are not lexical like they are in C. You can't use macro invocations just anywhere; they have to appear in specific syntax positions, and as the compiler states, enum variants aren't one of them.

In addition, macros cannot "destructure" matches (other than the super simple ones that only ever match exactly one token). So the moment you match some tokens as a ty, you cannot ever break it apart or match it literally again. This is why enum_variants cannot work, even if you could use it in enum variant position.

So, two things you need to do: first, you need to match the input without ever using ty. Second, you need to emit your macros input in one go without any intermediate expansions in invalid positions. To whit, here is a macro that does this.

This matches each type one at a time, building up partial output in $vars until it's finished, then expands to the result in one final step.

1 Like

macros are expanded from outside, so build_enum!(Foo, Bar<T>); expands to

enum Kinds {
  enum_variants!(Foo)(Foo),
  enum_variants!(Bar<T>)(Bar<T>),
}

which is invalid rust syntax.

to achieve what you expected, you have to write a macro that incrementally parse the token trees and use recursion (folding left) to build up a list of names, and in emit the code at the final step. see chapter "incremental tt munchers" for explanation

https://danielkeep.github.io/tlborm/book/pat-incremental-tt-munchers.html

the following code should work for your example invocation


EDIT:

the above link uses two accumulator groups, actually you only need one accumulator, but the repetitions gets a little out of control. I'm not sure which one is more readable, but here is the same functionality with single accumulator. this version also add a convenient entry point so you can invoke without inner macro dispatch tags. for possible ways to get rid of hardcoded generic parameter list, see follow up posts below.


please note I hardcoded generic type parameter as enum Kinds<T> {...}. unfortunately declarative macros can't really deal with arbitrary generic parameters. for example

// non generic, this is trivial
build_enum!(Foo, Bar<bool>);
// expands to non generic enum type
enum Kinds {
    //...
}

// generic, expansion can only choose hardcoded generic parameter names:
build_enum!(Foo, Bar<T>, Baz<U>, Qux<T, U>);
// expands to generic enum type with hardcoded parameters
enum Kinds<A, B, C, T, U, V, X, Y, Z> {
    //...
}

// impossible to support invocation with arbitrary generic parameters
build_enum!(
    Foo<ThisIsImpossibleToDeduplication>,
    Bar<ThisIsImpossibleToDeduplication>,
    Baz<ThisIsImpossibleToParse2>,
    Qux<'a, 'b, LetAloneLifetimes>,
);
1 Like

And it should be mentioned that it seems like this is not quite but getting close to being easier to do using procedural macros than macro rules. If you're going to bring going further down this path, it's worth looking into them, in particular using syn to parse arbitrary Rust syntax and quote to easily emit arbitrary syntax.

1 Like

Oh, they can. You just don't want to go there. Don't click that link if you value your sanity.

1 Like

can you really? I know I can parse a single list of generic parameters with efforts, but can you merge multiple of them into one? I mean, you can't compare meta variables for equality, can you? say, for example, is this doable?

// single generic parameter T
build_enum!(Foo<T>, Bar<T>);
// expands to:
enum Kinds<T> {
    Foo(Foo<T>),
    Bar(Bar<T>),
}

// different generic parameters T, U, and V
build_enum!(Foo<T>, Bar<U, V>, Baz<T, T>);
// expands to:
enum Kinds<T, U, V> {
    Foo(Foo<T>),
    Bar(Bar<U, V>),
    Baz(Baz<T, T>),
}

// in short, I know the following can be parsed
build_enum_generic_explicitly!(<'a, 'b: 'a, T, U, V>, Foo<'a, T>, Bar<'b, U, V>, Baz<T, T>)

// but can we do it without the explicit generic list
// (i.e. compute it by merging the others)?
// in other words, can manipulate tokens like so?
token_set!(a) // => {a}
token_set!(a b c) // => {a b c}
token_set!(a a a) // => {a}
token_set!(a b a a b a b c b c) // => {a b c}

Aah. Now, that I don't think you can do. At least, not without something like a helper proc macro. My assumption was that if you were going to use generics, you'd have an explicit list of the parameters.