Proc Macros: Span Preservation for Type Bound Errors?

Hello everyone,

I'm trying procedural macros for the first time. I've gotten quite far, and find it a pretty good experience. However, I'm having a problem preserving some span information. It seems straightforward for many cases, but not one that would help usability tremendously: checking the user's type bounds the macro requires.

The code is too large to paste, but here is the outline. I have a macro that the user will place on their functions. It's job is to create an API glue function from a Box-like type this crate uses, so that their function can be called. (This is for an interpreted language.)

Here is the struct I'm using to parse the user's function:

#[derive(Debug)]
pub(crate) struct Function {
    entire_span: proc_macro2::Span,
    signature: syn::Signature,
    // ...
}

impl Parse for Function {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let fn_all: syn::ItemFn = input.parse()?;
        let entire_span = fn_all.span();
        // ... checking and field setup ...
        Ok(Function {
            entire_span,
            signature: fn_all.sig,
            // ...
        })
    }
}

Once generate is called, it creates an extra function callable from elsewhere, which does the type conversion. The type conversion code in its body is generated by this loop:

let mut unpack_vars = Vec::new();
let mut unpack_stmts: Vec<syn::Stmt> = Vec::new();
// ...

for (i, arg) in args_iter.enumerate() {
    let var = syn::Ident::new(&format!("arg{}", i), proc_macro2::Span::call_site());
    match arg {
        syn::FnArg::Typed(pattern) =>  {
            let arg_type: &syn::Type = pattern.ty.as_ref();
            unpack_stmts.push(syn::parse2::<syn::Stmt>(quote! {
                let #var = args[#i].downcast_clone::<#arg_type>().unwrap();
            }).unwrap());
        },
        syn::FnArg::Receiver(_) => unimplemented!(),
    }
    unpack_vars.push(var);
}

This code seems to preserve spans pretty well, given other tests I've written. But there is one quirk I cannot figure out.

See that downcast_clone? As the name implies, that requires a bound of T: Clone. Since it's applied to the type of the user's function input, that means the user must have their input types implement Clone.

In my trybuild test case where a user forgets to do that, this is the error they get:

error[E0277]: the trait bound `NonClonable: std::clone::Clone` is not satisfied
  --> $DIR/non_clonable.rs:10:1
   |
10 | #[function]
   | ^^^^^^^^^^^ the trait `std::clone::Clone` is not implemented for `NonClonable`
   |
   = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

What I'd really like is this:

error[E0277]: the trait bound `NonClonable: std::clone::Clone` is not satisfied
  --> $DIR/non_clonable.rs:11:23
   |
11 |     pub fn test_function(input: NonClonable) -> bool {
   |                                   ^^^^^^^^^^^ the trait `std::clone::Clone` is not implemented for `NonClonable`
   = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

I have done quite a bit of testing, and it seems that the Type the user passed in has its span preserved all the way to the loop, and quote! is good at preserving spans when you paste in tokens directly. I don't know why it's being lost.

Any guidance?

In case I didn't paste enough, you can look at the entirety of the code on GitHub.

I suspect it's pointing at the .downcast_clone(), which will be getting a Span::call_site() span. Using quote_spanned! with the span you'd like to see the error at should work properly.

If you ever want to know exactly what tokens the compiler is pointing at, expand the macro (manually, cargo expand, or via panicking with the resulting token stream instead of returning it) and compile it directly (after cleaning up anything that doesn't work due to the lack of span information). Then set the span of whatever's being pointed to in the expansion to where you want it to point in the macro call.

1 Like

Putting the subexpression into a separate quote_spanned and then quote ing that did the trick! Thanks, @CAD97!

I also didn't know about cargo expand, so thanks for the pointer. It's way easier than cargo rustc --test specific_case -- -Zhir=unpretty! :smile:

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.