Search code examples
rustrust-macros

How to distinguish different kinds of items in macro_rules macros?


I want to write a macro_rules based macro that will be used to wrap a series of type aliases and struct definitions. I can match on "items" with $e:item, but this will match both aliases and structs. I would like to treat the two separately (I need to add some #[derive(...)] just on the structs). Do I have to imitate their syntax directly by matching on something like type $name:ident = $type:ty; or is there a better way? This route seems annoying for structs because of regular vs tuple like. If I also wanted to distinguish functions that would be really painful because they have a lot of syntactical variation.


Solution

  • I believe for that problem somewhat simple cases can be solved with macro_rules!, but that probably would be limited (you can't lookahead) and super error-prone. I only cover an example for types, I hope that would be convincing enough to avoid macro_rules!. Consider this simple macro:

    macro_rules! traverse_types {
        ($(type $tp:ident = $alias:ident;)*) => {
            $(type $tp = $alias;)*
        }
    }
    
    traverse_types!(
        type T = i32;
        type Y = Result<i32, u64>;
    );
    

    That works fine for the trivial aliases. At some point you probably also would like to handle generic type aliases (i.e. in the form type R<Y> = ...). Ok, your might still rewrite the macro to the recursive form (and that already a non-trivial task) to handle all of cases. Then you figure out that generics can be complex (type-bounds, lifetime parameters, where-clause, default types, etc):

    type W<A: Send + Sync> = Option<A>;
    type X<A: Iterator<Item = usize>> where A: 'static = Option<A>;
    type Y<'a, X, Y: for<'t> Trait<'t>> = Result<&'a X, Y>;
    type Z<A, B = u64> = Result<A, B>;
    

    Probably all of these cases still can be handled with a barely readable macro_rules!. Nevertheless it would be really hard to understand (even to the person who wrote it) what's going on. Besides, it is hard to support new syntax (e.g. impl-trait alias type T = impl K), you may even need to have a complete rewrite of the macro. And I only cover the type aliases part, there's more to handle for the structs.

    My point is: one better avoid macro_rules! for that (and similar) problem(-s), procedural macros is a way much a better tool for that. It easier to read (and thus extend) and handles new syntax for free (if syn and quote crates are maintained). For the type alias this can be done as simple as:

    extern crate proc_macro;
    
    use proc_macro::TokenStream;
    use syn::parse::{Parse, ParseStream};
    
    struct TypeAliases {
        aliases: Vec<syn::ItemType>,
    }
    
    impl Parse for TypeAliases {
        fn parse(input: ParseStream) -> syn::Result<Self> {
            let mut aliases = vec![];
            while !input.is_empty() {
                aliases.push(input.parse()?);
            }
            Ok(Self { aliases })
        }
    }
    
    #[proc_macro]
    pub fn traverse_types(token: TokenStream) -> TokenStream {
        let input = syn::parse_macro_input!(token as TypeAliases);
    
        // do smth with input here
    
        // You may remove this binding by implementing a `Deref` or `quote::ToTokens` for `TypeAliases`
        let aliases = input.aliases;
        let gen = quote::quote! {
            #(#aliases)*
        };
    
        TokenStream::from(gen)
    }
    

    For the struct parsing code is the same using ItemStruct type and also you need a lookahead to determine wether it's an type-alias or a struct, there's very similar example at syn for that.