-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Experimental: Add Derive Proc-Macro Caching #129102
base: master
Are you sure you want to change the base?
Changes from all commits
3db813e
2507253
0733fbd
a4b675c
209fd8a
fe36059
eb99b01
fc3bc29
075853d
7a1b2f5
d781c4b
a81aaed
91c0d1b
8521154
17dcadf
35beb09
382c5d0
b71c42d
a359e3c
f768697
bb3bddf
77944c9
de0fad9
d6c2527
00cff94
abcc4a2
e93beac
cd82e38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,17 @@ | ||
use std::cell::Cell; | ||
use std::ptr::{self, NonNull}; | ||
|
||
use rustc_ast as ast; | ||
use rustc_ast::ptr::P; | ||
use rustc_ast::tokenstream::TokenStream; | ||
use rustc_data_structures::svh::Svh; | ||
use rustc_errors::ErrorGuaranteed; | ||
use rustc_middle::ty::{self, TyCtxt}; | ||
use rustc_parse::parser::{ForceCollect, Parser}; | ||
use rustc_session::Session; | ||
use rustc_session::config::ProcMacroExecutionStrategy; | ||
use rustc_span::Span; | ||
use rustc_span::profiling::SpannedEventArgRecorder; | ||
use rustc_span::{LocalExpnId, Span}; | ||
|
||
use crate::base::{self, *}; | ||
use crate::{errors, proc_macro_server}; | ||
|
@@ -31,9 +37,9 @@ impl<T> pm::bridge::server::MessagePipe<T> for MessagePipe<T> { | |
} | ||
} | ||
|
||
fn exec_strategy(ecx: &ExtCtxt<'_>) -> impl pm::bridge::server::ExecutionStrategy { | ||
pub fn exec_strategy(sess: &Session) -> impl pm::bridge::server::ExecutionStrategy { | ||
pm::bridge::server::MaybeCrossThread::<MessagePipe<_>>::new( | ||
ecx.sess.opts.unstable_opts.proc_macro_execution_strategy | ||
sess.opts.unstable_opts.proc_macro_execution_strategy | ||
== ProcMacroExecutionStrategy::CrossThread, | ||
) | ||
} | ||
|
@@ -55,7 +61,7 @@ impl base::BangProcMacro for BangProcMacro { | |
}); | ||
|
||
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace; | ||
let strategy = exec_strategy(ecx); | ||
let strategy = exec_strategy(ecx.sess); | ||
let server = proc_macro_server::Rustc::new(ecx); | ||
self.client.run(&strategy, server, input, proc_macro_backtrace).map_err(|e| { | ||
ecx.dcx().emit_err(errors::ProcMacroPanicked { | ||
|
@@ -86,7 +92,7 @@ impl base::AttrProcMacro for AttrProcMacro { | |
}); | ||
|
||
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace; | ||
let strategy = exec_strategy(ecx); | ||
let strategy = exec_strategy(ecx.sess); | ||
let server = proc_macro_server::Rustc::new(ecx); | ||
self.client.run(&strategy, server, annotation, annotated, proc_macro_backtrace).map_err( | ||
|e| { | ||
|
@@ -114,6 +120,13 @@ impl MultiItemModifier for DeriveProcMacro { | |
item: Annotatable, | ||
_is_derive_const: bool, | ||
) -> ExpandResult<Vec<Annotatable>, Annotatable> { | ||
let _timer = ecx.sess.prof.generic_activity_with_arg_recorder( | ||
"expand_derive_proc_macro_outer", | ||
|recorder| { | ||
recorder.record_arg_with_span(ecx.sess.source_map(), ecx.expansion_descr(), span); | ||
}, | ||
); | ||
|
||
// We need special handling for statement items | ||
// (e.g. `fn foo() { #[derive(Debug)] struct Bar; }`) | ||
let is_stmt = matches!(item, Annotatable::Stmt(..)); | ||
|
@@ -124,36 +137,40 @@ impl MultiItemModifier for DeriveProcMacro { | |
// altogether. See #73345. | ||
crate::base::ann_pretty_printing_compatibility_hack(&item, &ecx.sess); | ||
let input = item.to_tokens(); | ||
let stream = { | ||
let _timer = | ||
ecx.sess.prof.generic_activity_with_arg_recorder("expand_proc_macro", |recorder| { | ||
recorder.record_arg_with_span( | ||
ecx.sess.source_map(), | ||
ecx.expansion_descr(), | ||
span, | ||
); | ||
}); | ||
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace; | ||
let strategy = exec_strategy(ecx); | ||
let server = proc_macro_server::Rustc::new(ecx); | ||
match self.client.run(&strategy, server, input, proc_macro_backtrace) { | ||
Ok(stream) => stream, | ||
Err(e) => { | ||
ecx.dcx().emit_err({ | ||
errors::ProcMacroDerivePanicked { | ||
span, | ||
message: e.as_str().map(|message| { | ||
errors::ProcMacroDerivePanickedHelp { message: message.into() } | ||
}), | ||
} | ||
}); | ||
return ExpandResult::Ready(vec![]); | ||
} | ||
let res = ty::tls::with(|tcx| { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be necessary, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I gave it a try, but this seems to require adding a lifetime |
||
// FIXME(pr-time): without flattened some (weird) tests fail, but no idea if it's correct/enough | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In theory flattening should always be correct (proc macro APIs will perform flattening lazily when necessary), but in practice there may be issues with badly written proc macros that pretty-print token streams and then parse the result as a text. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mhhm, makes sense. What would you suggest for now? I think caching needs the flattening, so should I try to not flatten when caching is disabled, that is, use the same behavior as before this PR? |
||
let input = tcx.arena.alloc(input.flattened()) as &TokenStream; | ||
let invoc_id = ecx.current_expansion.id; | ||
let invoc_expn_data = invoc_id.expn_data(); | ||
|
||
assert_eq!(invoc_expn_data.call_site, span); | ||
|
||
// FIXME(pr-time): Is this the correct way to check for incremental compilation (as | ||
// well as for `cache_proc_macros`)? | ||
if tcx.sess.opts.incremental.is_some() && tcx.sess.opts.unstable_opts.cache_proc_macros | ||
{ | ||
// FIXME(pr-time): Just using the crate hash to notice when the proc-macro code has | ||
// changed. How to *correctly* depend on exactly the macro definition? | ||
// I.e., depending on the crate hash is just a HACK, and ideally the dependency would be | ||
// more narrow. | ||
let macro_def_id = invoc_expn_data.macro_def_id.unwrap(); | ||
let proc_macro_crate_hash = tcx.crate_hash(macro_def_id.krate); | ||
|
||
let key = (invoc_id, proc_macro_crate_hash, input); | ||
|
||
enter_context((ecx, self.client), move || tcx.derive_macro_expansion(key).cloned()) | ||
} else { | ||
expand_derive_macro(tcx, invoc_id, input, ecx, self.client).cloned() | ||
} | ||
}); | ||
|
||
let Ok(output) = res else { | ||
// error will already have been emitted | ||
return ExpandResult::Ready(vec![]); | ||
}; | ||
|
||
let error_count_before = ecx.dcx().err_count(); | ||
let mut parser = Parser::new(&ecx.sess.psess, stream, Some("proc-macro derive")); | ||
let mut parser = Parser::new(&ecx.sess.psess, output, Some("proc-macro derive")); | ||
let mut items = vec![]; | ||
|
||
loop { | ||
|
@@ -181,3 +198,102 @@ impl MultiItemModifier for DeriveProcMacro { | |
ExpandResult::Ready(items) | ||
} | ||
} | ||
|
||
pub(super) fn provide_derive_macro_expansion<'tcx>( | ||
tcx: TyCtxt<'tcx>, | ||
key: (LocalExpnId, Svh, &'tcx TokenStream), | ||
) -> Result<&'tcx TokenStream, ()> { | ||
let (invoc_id, _macro_crate_hash, input) = key; | ||
|
||
with_context(|(ecx, client)| expand_derive_macro(tcx, invoc_id, input, ecx, *client)) | ||
} | ||
|
||
type CLIENT = pm::bridge::client::Client<pm::TokenStream, pm::TokenStream>; | ||
|
||
fn expand_derive_macro<'tcx>( | ||
tcx: TyCtxt<'tcx>, | ||
invoc_id: LocalExpnId, | ||
input: &'tcx TokenStream, | ||
ecx: &mut ExtCtxt<'_>, | ||
client: CLIENT, | ||
) -> Result<&'tcx TokenStream, ()> { | ||
let invoc_expn_data = invoc_id.expn_data(); | ||
let span = invoc_expn_data.call_site; | ||
let event_arg = invoc_expn_data.kind.descr(); | ||
let _timer = tcx.sess.prof.generic_activity_with_arg_recorder( | ||
"expand_derive_proc_macro_inner", | ||
|recorder| { | ||
recorder.record_arg_with_span(tcx.sess.source_map(), event_arg.clone(), span); | ||
}, | ||
); | ||
|
||
let proc_macro_backtrace = ecx.ecfg.proc_macro_backtrace; | ||
let strategy = crate::proc_macro::exec_strategy(tcx.sess); | ||
let server = crate::proc_macro_server::Rustc::new(ecx); | ||
|
||
match client.run(&strategy, server, input.clone(), proc_macro_backtrace) { | ||
Ok(stream) => Ok(tcx.arena.alloc(stream) as &TokenStream), | ||
Err(e) => { | ||
tcx.dcx().emit_err({ | ||
errors::ProcMacroDerivePanicked { | ||
span, | ||
message: e.as_str().map(|message| errors::ProcMacroDerivePanickedHelp { | ||
message: message.into(), | ||
}), | ||
} | ||
}); | ||
Err(()) | ||
} | ||
} | ||
} | ||
|
||
// based on rust/compiler/rustc_middle/src/ty/context/tls.rs | ||
thread_local! { | ||
/// A thread local variable that stores a pointer to the current `CONTEXT`. | ||
static TLV: Cell<(*mut (), Option<CLIENT>)> = const { Cell::new((ptr::null_mut(), None)) }; | ||
} | ||
|
||
/// Sets `context` as the new current `CONTEXT` for the duration of the function `f`. | ||
#[inline] | ||
pub(crate) fn enter_context<'a, F, R>(context: (&mut ExtCtxt<'a>, CLIENT), f: F) -> R | ||
where | ||
F: FnOnce() -> R, | ||
{ | ||
let (ectx, client) = context; | ||
let erased = (ectx as *mut _ as *mut (), Some(client)); | ||
TLV.with(|tlv| { | ||
let old = tlv.replace(erased); | ||
let _reset = rustc_data_structures::defer(move || tlv.set(old)); | ||
f() | ||
}) | ||
} | ||
|
||
/// Allows access to the current `CONTEXT`. | ||
/// Panics if there is no `CONTEXT` available. | ||
#[inline] | ||
#[track_caller] | ||
fn with_context<F, R>(f: F) -> R | ||
where | ||
F: for<'a, 'b> FnOnce(&'b mut (&mut ExtCtxt<'a>, CLIENT)) -> R, | ||
{ | ||
let (ectx, client_opt) = TLV.get(); | ||
let ectx = NonNull::new(ectx).expect("no CONTEXT stored in tls"); | ||
|
||
// We could get an `CONTEXT` pointer from another thread. | ||
// Ensure that `CONTEXT` is `DynSync`. | ||
// FIXME(pr-time): we should not be able to? | ||
// sync::assert_dyn_sync::<CONTEXT<'_>>(); | ||
|
||
// prevent double entering, as that would allow creating two `&mut ExtCtxt`s | ||
// FIXME(pr-time): probably use a RefCell instead (which checks this properly)? | ||
TLV.with(|tlv| { | ||
let old = tlv.replace((ptr::null_mut(), None)); | ||
let _reset = rustc_data_structures::defer(move || tlv.set(old)); | ||
let ectx = { | ||
let mut casted = ectx.cast::<ExtCtxt<'_>>(); | ||
unsafe { casted.as_mut() } | ||
}; | ||
|
||
f(&mut (ectx, client_opt.unwrap())) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is implementation of unstable hash required for
TokenStream
?Unstable hashing shouldn't be used in incremental compilation, and
TokenStrem
already has a stable hashing implementation because token streams are used in attributes.(Also the use of
(Span)Encoder
here is strange, I don't think it's supposed to be used like this.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried removing the
Hash
-impl again, but it seems the query system requires it somehow:Adding
no_hash
also didn't help (i.e., error stayed the same). I know there is something likearena_cache
, but I'm not really familiar with the query/caching system, so no idea if that would be better here/not require theHash
-impl.Full error message (long!)