Skip to content

Extremely weird hygiene behavior when invoking a macro from the calling crate in a Derive #47988

Open
@sgrif

Description

@sgrif

I'm not entirely sure whether this is a bug or not, but the current behavior seems super finicky and unintuitive at worst, and I think it's likely a bug. I think this warrants some context on the use case, so I'd like to preface with that. There's a repro script at the bottom if you don't care about the context.

Custom derives have to work around the fact that they don't have access to $crate for the crate they're associated with. Typically the way this is worked around is by doing const UNIQUE_NAME: () = { extern crate your_crate; /*code*/ };. Diesel provides several derives which we want to allow third party crates to use, but also use them within Diesel itself. This means that the extern crate workaround won't work for us. Instead we have this macro in Diesel:

macro_rules! __diesel_use_everything {
  () => { pub use $crate::*; }
}

and then the generated code looks like this:

mod unique_name {
    mod diesel {
        __diesel_use_everything!();
    }
    /*code*/
}

However, this gets super finicky with hygiene. If we try to do that with nightly, using the derives within Diesel itself will complain that __diesel_use_everything! can't be found. The fix for this is to give __diesel_use_everything!() a call_site span. Interestingly, the semicolon after it must have a def_site span, or nothing it imported will be visible. The semicolon being significant is particularly weird to me, because it's basically enforcing that () or [] be used as the delimiters. If I wanted to invoke the macro with {}, it would be impossible for me to make it work I have to ensure the braces have a def_site span.

Anyway it's possible to work around this in the most basic cases by giving __diesel_use_everything!() a call_site span. However, we run into additional trouble when the use of the derive originates inside a macro from Diesel (the actual macro is sql_function! if you want a real use case). It'll still find __diesel_use_everything!() but we get the same problem that we had if the ; has a call_site span. Nothing in this diesel module is visible. use self::diesel::anything will fail.

With all that said, here's a minimum repro script:

foo/lib.rs

#[macro_use]
extern crate bar;

macro_rules! __foo_use_everything {
    () => {
        pub use $crate::*;
    };
}

pub struct Foo;

macro_rules! make_a_struct {
    () => {
        #[derive(Thingy)]
        pub struct Bar;
    };
}

make_a_struct!();

bar/lib.rs

#![feature(proc_macro)]

#[macro_use]
extern crate quote;
extern crate proc_macro2;
extern crate proc_macro;

use proc_macro::TokenStream;
use proc_macro2::Span;

#[proc_macro_derive(Thingy)]
pub fn derive(_: TokenStream) -> TokenStream {
    let call_site = Span::call_site();
    let use_everything = quote_spanned!(call_site=> __foo_use_everything!());
    quote!(
        mod a_unique_name {
            mod foo {
                #use_everything;
            }
            use self::foo::Foo;
        }
    ).into()
}

The workaround here is to call source on the call_site span (in proc_macro2 that looks like Span::call_site().unstable().source().into()), but this requirement seems really weird to me. For that matter, the need to give the macro invocation any particular span at all is really surprising to me. This is a macro_rules! macro, which by its very nature is non-hygienic and global. I think this invocation should work regardless of the span the macro name has. Even putting that aside though, it seems to me that a derive used inside a macro_rules! macro should behave basically the same as one without (e.g. __diesel_use_everything! should certainly resolve with call_site regardless of where it's used)

Metadata

Metadata

Assignees

Labels

A-decl-macros-2-0Area: Declarative macros 2.0 (#39412)A-hygieneArea: Macro hygieneC-bugCategory: This is a bug.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