There’s a bit of an uncomfortable situation going on with the expansion of derives (compiler plugin or proc_macro). #[cfg]
attributes on sub-items are expanded:
#[derive(MyTrait)]
struct MyStruct {
unconditional_field: Foo,
#[cfg(feature = "some_feature")]
cfg_specific_field: Bar,
}
If some_feature
is not set, the derive for MyTrait
will only see struct MyStruct { unconditional_field: Foo }
.
However, we don’t currently expand any other attribute or macro invocations inside item definitions before expanding their derives, which makes #[cfg]
special when it really shouldn’t be. We could just fix that so #[cfg]
isn’t special at all, and just macro-expand derive input, but there’s a problem.
Many derive crates seem to be either depending on this behavior, or have simply accepted it in a forward-incompatible manner, as they expect macro invocations in their input: https://github.com/rust-lang/rust/pull/48465#issuecomment-377732109
However, as I stated in that comment, it seems that they’re not so much relying on this behavior, but have just come to expect it. In that Crater run I saw multiple breakages where a derive was expecting stringify!()
in its input. I haven’t looked closer but I expect they’re manually stringifying the input when they really shouldn’t have to.
However because of this reliance/acceptance, this change would be breaking unless it was explicitly opt-in.
We’ve worked out three-ish possible solutions:
- Allow
#[proc_macro_derives]
to opt-in to expanded input by declaring it up-front:
#[proc_macro_derive(MyTrait, expand_input = true)]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
// the derive can assume no macro invocations in its input
}
This is the most well scoped solution, affecting only derives that ask for it. However, I think proc-macros and proc-macro-attributes might appreciate this behavior too. We could make them opt-in the same way as well, I guess, but that’s strictly less flexible than:
- Expose a function in
libproc_macro
to macro-expand aTokenStream
:
pub fn expand_macros(tokens: TokenStream) -> TokenStream { ... }
Then any proc-macro may simply ask for it programmatically, even limit expansion to a subset of its input or macro-expand some intermediate tokens:
#[proc_macro]
pub fn macro_expand_input(input: TokenStream) -> TokenStream {
proc_macro::expand_macros(input)
}
I’m not entirely sure what the problems are with this approach, it seems like it could get a little complex WRT resolution and hygiene, like if something like this was passed to the proc-macro:
mod foo {
macro_rules! bar {
() => ()
}
bar();
}
but the proc-macro passes just bar()
to expand_macros()
. I suppose we’d mostly just say “well, would that macro invocation be legal anywhere else in the module in which this proc-macro was invoked?”
- Add this behavior to the Rust 2018 epoch.
Unless I’m drastically misunderstanding their purpose, epochs were created exactly for this kind of breaking change. However, this does leave the utility for proc-macros and proc-macro-attributes unaddressed.