Macro to recognize a specific type in struct

I'm trying to write a macro to generate struct and change fields type which are Handle<_> into String.
But for some reason foo!(@field_type $field_ty) is wrongly resolved but when I call directly foo!(@field_type Handle<u8>) it's work fine.

I don't understand why it didn't work and how to fix it.

macro_rules! foo {
    (
        $vis:vis struct $name:ident {
            $($field_name:ident : $field_ty:ty),* $(,)?
        }
    ) => {
        $vis struct $name {
            $(
                $field_name: foo!(@field_type $field_ty)
            ),*
        }
    };
    (
        @field_type Handle<$ty:ty>
    ) => {
        String
    };
    (
        @field_type $t:ty
    ) => {
       $t
    };
}
foo! {
    struct Foo {
        foo: Handle<u8>,
    }
}

struct Goo {
    goo: foo!(@field_type Handle<u8>),
}

Expected

struct Foo {
    foo: String,
}

struct Goo {
    goo: String,
}

Got

struct Foo {
    foo: Handle<u8>, // <- Not changed
}

struct Goo {
    goo: String, // <- This one work fine
}

In the definition of foo, you have this: $field_ty:ty. This is what's breaking it.

Think about it like this: the input to a macro (what is being matched on) is a sequence of simple tokens. These simple tokens support literal matches (i.e. matching two sequences of arbitrary tokens to make sure they're the same).

When you perform a capture like $field_ty:ty, the macro processor goes over to the compiler's parser and asks "hey, can you turn this chunk of random tokens into a Type tree for me?". Which it does. When you then substitute that captured $field_ty into the output of the macro, the macro processor does not turn the captured Type tree back into random tokens, it substitutes the syntax tree node directly.

The problem this creates is that this magic "not really a token anymore" token does not support literal matching. You can re-capture it, but you can't "deconstruct" it into its constituent tokens.

Basically, once you capture Handle<u8> as a ty token, you cannot match against the Handle part ever again.

Unfortunately, there's no easy way to really fix this. When I've needed to do something like this previously, I've had to resort to a recursive macro that matches each field one at a time. That way, you can do the "is this a Handle" check before falling back to ty matching.

Edit: okay, here's a playpen with a working macro. Or just read it here:

struct Handle<T>(std::marker::PhantomData<T>);

macro_rules! foo {
    (
        $vis:vis struct $name:ident {
            $($fields:tt)*
        }
    ) => {
        foo! {
            @parse
            { $vis struct $name },
            { },
            $($fields)*
        }
    };
    
    // `Handle<T>` field.
    (
        @parse
        $bundle:tt,
        { $($fields:tt)* },
        $field_name:ident : Handle<$field_ty:ty>,
        $($tail:tt)*
    ) => {
        foo! {
            @parse
            $bundle,
            {
                $($fields)*
                $field_name: $field_ty,
            },
            $($tail)*
        }
    };
    
    // Other field.
    (
        @parse
        $bundle:tt,
        { $($fields:tt)* },
        $field_name:ident : $field_ty:ty,
        $($tail:tt)*
    ) => {
        foo! {
            @parse
            $bundle,
            {
                $($fields)*
                $field_name: $field_ty,
            },
            $($tail)*
        }
    };
    
    // You can make `,`s optional by adding two more cases for the above where
    // the field is the *last* thing in the input and doesn't have a comma.
    
    // End of input.
    (
        @parse
        { $vis:vis struct $name:ident },
        { $($fields:tt)* },
        $(,)*
    ) => {
        $vis struct $name {
            $($fields)*
        }
    };
}

foo! {
    struct Foo {
        foo: Handle<u8>,
        bar: String,
    }
}

That or write a proc macro. Depending on what you're doing, that might be simpler.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.