rustmacrosrust-proc-macros

Eager proc macro expansion


Given a TokenStream, can I produce a new TokenStream with all the attributes pre-expanded?

I suspect this would dramatically increase compile time, or require multiple compile steps.

I've tried to take a stab at writing a proc macro that can execute a closure on a function exit. The exact implementation looks like the following

#[proc_macro_attribute]
pub fn inspect(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut callback = parse_macro_input!(attr as ExprClosure);
    let function = parse_macro_input!(item as ItemFn);
    let sig = &function.sig;
    let body = &function.block;

    if let Some(compile_error) = validate_inspect(&callback, &function) {
        return compile_error;
    }

    let out = if sig.asyncness.is_some() {
        quote! { let out = async move { #body }.await; }
    } else {
        quote! { let out = (move || { #body })(); }
    };

    quote! {
        #sig {
            #out
            (#callback)(&out);
            out
        }
    }
    .into()
}

It may be used as follows:

#[inspect(|out| println!("increment returns {i}"))]
async fn increment(value: i32) -> i32 {
    value + 1
}

The problem is that this does not play well with async_trait as the proc_macro for async_trait removes the asyncness of the function.

#[async_trait]
trait Foo {
    async fn foo() -> i32;
}

struct Bar;

/// ! Compile error
#[async_trait]
impl Foo for Bar {
    #[inspect(|out| println!("foo returns {}", i))]
    async fn foo() -> i32 {
        42
    }
}

The following example does not compile because proc-macros are expanded outside in.

Without my proc macro,

#[async_trait]
impl Foo for Bar {
    async fn foo() -> i32 {
        42
    }
}

async_trait expands to roughly something like this1:

impl Foo for Bar {
    fn foo<'async_trait>() -> Pin<
        Box<dyn Future<Output = i32> + Send + 'async_trait>,
    > {
        Box::pin(async move {
            42
        })
    }
}

As you can see the function is converted to a synchronous function returning a dynamic future since async traits are not yet stabilized.

I would like to have a proc macro called invert_expansion_trait which expands the attributes of items inside a trait implementation first, before running it's own subsequent attributes. Would this be possible to implement today?

I tried creating a eager proc macro expansion that expands the attributes of an ItemImpl but couldn't get it to work because I lack knowledge / resources to understand the expansion process.


1: some code removed from expansion for ease of viewing


Solution

  • Per @chayim-friedman, I went with option 1, to eagerly inspect and expand the proc macros manually. It looks like this:

    /// Applies internal proc macro expansions eagerly onto trait implementations
    ///
    /// Proc macros work outside in, therefore, proc macros which affect the function signature will
    /// cause `inspect` and `trap` to fail. In order to avoid this, apply a `hook_trait` at the beginning
    /// of an impl block. The macro will manually expand the internal proc macros first, before external
    /// proc macros may affect the signature or implementation.
    #[proc_macro_attribute]
    pub fn hook_trait(_attr: TokenStream, item: TokenStream) -> TokenStream {
        let mut item_impl = parse_macro_input!(item as ItemImpl);
    
        for impl_item_fn in item_impl.items.iter_mut().filter_map(|item| match item {
            ImplItem::Fn(impl_item_fn) => Some(impl_item_fn),
            _ => None,
        }) {
            // Extract the proc macro attributes
            let mut attrs = vec![];
            impl_item_fn.attrs = impl_item_fn
                .attrs
                .drain(..)
                .filter_map(|attr| {
                    if attr.path().is_ident(stringify!(inspect)) {
                        attrs.push((inspect as ProcMacroFn, attr));
                        None
                    } else if attr.path().is_ident(stringify!(trap)) {
                        attrs.push((trap as ProcMacroFn, attr));
                        None
                    } else {
                        Some(attr)
                    }
                })
                .collect();
    
            // Eagerly apply the proc macro to each implementation
            for (proc_macro, attr) in attrs {
                let Meta::List(meta_list) = attr.meta else {
                    return Error::new(attr.span(), "Attributes requires a metadata parameters")
                        .to_compile_error()
                        .into();
                };
    
                let tokens = proc_macro(
                    meta_list.tokens.to_token_stream().into(),
                    impl_item_fn.to_token_stream().into(),
                );
                match parse(tokens.clone()) {
                    Ok(new_impl_item_fn) => *impl_item_fn = new_impl_item_fn,
                    Err(_) => return tokens,
                }
            }
        }
    
        item_impl.to_token_stream().into()
    }