Skip to content

Macros 2.0: Span::def_site() vs Span::call_site() #45934

Closed
@alexcrichton

Description

@alexcrichton

Up until recently I've considered these two function calls in proc_macro, Span::default() and Span::call_site() relatively different. I'm not realizing, however, that they're actually quite significantly different depending on what you're doing in a procedural macro!

In working with the gnome-class macro we've ended up getting a good deal more experience with the procedural macro system. This macro is using dependencies like quote, syn, and proc-macro2 to parse and generate code. The code itself contains a mixture of modules and macro_rules-like macro expansions.

When we tried to enable the unstable feature in proc-macro2, which switches it to use the "real" proc_macro APIs and preserve span information, it turned out everything broke! When digging into this I found that everything we were experiencing was related to the distinction between the default and call_site functions.

So as a bit of background, the gnome_class! macro uses a few methods to manufacture a TokenStream. Primarily it uses the quote! macro from the quote crate, which primarily at this time uses parse for most tokens to generate a TokenTree. Namely part of the quote! macro will call stringify! on each token to get reparsed at runtime and turned into a list of tokens. For delimiters and such the quote crate currently creates a default span.

Additionally both @federicomenaquintero and I were novices at the procedural macro/hygiene/span systems, so we didn't have a lot of info coming in! Now though we think we're a bit more up to speed :). The rest of this issue will be focused on "weird errors" that we had to work backwards to figure out how to solve. This all, to me at least, seems like a blocker for stabilization in the sense that I wouldn't wish this experience on others.

I'm not really sure what the conclusions from this would be though. The behavior below may be bugs in the compiler or bugs in the libraries we're using, I'm not sure! I'll write up some thoughts at the end though. In general though I wanted to just detail all that we went through in discovering this and showing how the current behavior that we have ends up being quite confusing.

Examples of odd errors

In any case, I've created an example project showcasing a number of the "surprises" (some bugs?) that we ran into. I'll try to recatalog them here:

Using parse breaks super

The first bug that was found was related to generating a program that looked like:

mod foo {
    use super::*;
}

It turns out that if you use parse to generate the token super it doesn't work! If you set the span of super to default, however, it does indeed work.

I was pretty surprised by this (and the odd error messages). I'm not really sure why the parse span was not allowing it to resolve, but I imagine it was related to hygiene? I submitted dtolnay/quote#51 which I think might fix this but I wasn't sure if that was the right fix...

Is that the right fix for quote? Should it be using default wherever it can? I originally though that but then ran into...

Using Span::default means you can't import from yourself

This second bug was found relating to the program that looks like:

struct A;
mod foo {
    use super::A;
}

Here we have a failing procedural macro despite the usage of Span::default on all tokens. This means that by default all modules generated via quote!, if we were to switch spans to Span::default, would not be able to import from one another it looks like? But maybe this is only related to super? I'm not quite sure..

It also turns out that this does indeed work if we use Span::call_site by default everywhere. I'm not really sure why, but it apparently works!

Using Span::default means you can't import generated structs

Next up we had a bug related to:

pub struct A;

It turns out that if these tokens are using Span::default this can't actually be used! In this test case you get an error about an unresolved import.

Like with before though if we use call_site as a span everywhere this case does indeed work.

Is this expected? This means, I think, that all tokens with a Default span can't be improted outside of the procedural macro.

Using Span::default means you can't use external crates

Next we took a look at a program like:

use std::mem;

When generating these tokens with Span::default it turns out that this becomes an unresolved import! That is, the usage of Span::default seems like it's putting it in an entirely new namespace without access to even std at the root. Coming from the perspective of not knowing a lot about spans/hygiene I found this a little confusing :)

As with the previous and remaining cases using call_site as a span does indeed get this working.

Naturally the error message was pretty confusing here, but I guess this is expected? Hygiene I think necessitates this? Unsure...

Using Span::default means you can't import from yourself

Next up we have a program like

use foo::*;
mod foo {
}

Here if we use Span::default everywhere this program will not compile with the import becoming unresolved. For us this seemed to imply that if we generated new modules in a macro we basically can't use imports!

As per usual respanning with call_site everywhere fixes this but I'd imagine has different hygiene implications. I'm not sure if this behavior was intended, although it seemed like it may be a bug in rustc?

Using Span::default precludes working with "non hygienic macros"

This is a particularly interesting use case. The gnome_class! procedural macro internally delegates to the glib_wrapper! macro_rules macro in the expanded tokens. The glib_wrapper! macro, however, in its current state does not work in an empty module but rather requires imports like std::ptr in the environment. With Span::default, however, the generated tokens in glib_wrapper! couldn't see the tokens we generated with gnome_class!.

For example if in one crate we have a macro like

#[macro_export]
macro_rules! a {
    ($a:ident) => (
        fn _bar() {
            mem::drop(3);
        }
    )
}

(note that this requires std::mem to be imported to work)

and then we're generating a token stream that looks like:

mod foo {
    extern crate std;
    use self::std::mem;
    a! {}

Note that the extern crate is necessary due to one of the above situations (we can't import from the top-level extern crate). Here though if we generated tokens with Span::default as with many other cases this doesn't work! As usual if we respan with call_site spans then this does indeed work.

Is this a bug? Or maybe more hygiene?

Conclusions

Overall for our use case we found that 100% of the time we should be using Span::call_site instead of Span::default to get things working. Whether or not that's what we wanted hygienically we're not sure! I couldn't really understand the hygiene implications here because tokens using Span::default couldn't import from other modules defined next to it with the default span as well.

Should quote and syn move to using Span::call_site by default instead of Span::default? Or maybe Span::default should be renamed to sound "less default" if it appears to not work most of the time? Or maybe Span::default has bugs that need fixing?

I'm quite curious to hear what others think! Especially those that are particularly familiar with hygiene/macros, I'd love to hear ideas about whether this is expected behavior (and if so if we could maybe improve the error messages) or if we should perhaps be structuring the macro expansion differently. Similarly what would recommendations be for spanning tokens returned by quote! in an external crate? Or syn? (for example if I maufacture an Ident, is there a "good default" for that?)

In any case, curious to hear others' thoughts!


cc @jseyfried
cc @nrc
cc @nikomatsakis
cc @federicomenaquintero
cc @dtolnay
cc @mystor

Metadata

Metadata

Assignees

Labels

A-decl-macros-2-0Area: Declarative macros 2.0 (#39412)A-diagnosticsArea: Messages for errors, warnings, and lintsC-enhancementCategory: An issue proposing an enhancement or a PR with one.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions