| use proc_macro::TokenStream; | |
| use proc_macro2::Span; | |
| use quote::{quote, quote_spanned}; | |
| use syn::parse::{Parse, ParseStream, Result}; | |
| use syn::punctuated::Punctuated; | |
| use syn::spanned::Spanned; | |
| use syn::{ | |
| AttrStyle, Attribute, Block, Error, Expr, Ident, Pat, ReturnType, Token, Type, braced, | |
| parenthesized, parse_macro_input, token, | |
| }; | |
| mod kw { | |
| syn::custom_keyword!(query); | |
| } | |
| /// Ensures only doc comment attributes are used | |
| fn check_attributes(attrs: Vec<Attribute>) -> Result<Vec<Attribute>> { | |
| let inner = |attr: Attribute| { | |
| if !attr.path().is_ident("doc") { | |
| Err(Error::new(attr.span(), "attributes not supported on queries")) | |
| } else if attr.style != AttrStyle::Outer { | |
| Err(Error::new( | |
| attr.span(), | |
| "attributes must be outer attributes (`///`), not inner attributes", | |
| )) | |
| } else { | |
| Ok(attr) | |
| } | |
| }; | |
| attrs.into_iter().map(inner).collect() | |
| } | |
| /// A compiler query. `query ... { ... }` | |
| struct Query { | |
| doc_comments: Vec<Attribute>, | |
| modifiers: QueryModifiers, | |
| name: Ident, | |
| key: Pat, | |
| arg: Type, | |
| result: ReturnType, | |
| } | |
| impl Parse for Query { | |
| fn parse(input: ParseStream<'_>) -> Result<Self> { | |
| let mut doc_comments = check_attributes(input.call(Attribute::parse_outer)?)?; | |
| // Parse the query declaration. Like `query type_of(key: DefId) -> Ty<'tcx>` | |
| input.parse::<kw::query>()?; | |
| let name: Ident = input.parse()?; | |
| let arg_content; | |
| parenthesized!(arg_content in input); | |
| let key = Pat::parse_single(&arg_content)?; | |
| arg_content.parse::<Token![:]>()?; | |
| let arg = arg_content.parse()?; | |
| let _ = arg_content.parse::<Option<Token![,]>>()?; | |
| let result = input.parse()?; | |
| // Parse the query modifiers | |
| let content; | |
| braced!(content in input); | |
| let modifiers = parse_query_modifiers(&content)?; | |
| // If there are no doc-comments, give at least some idea of what | |
| // it does by showing the query description. | |
| if doc_comments.is_empty() { | |
| doc_comments.push(doc_comment_from_desc(&modifiers.desc.expr_list)?); | |
| } | |
| Ok(Query { doc_comments, modifiers, name, key, arg, result }) | |
| } | |
| } | |
| /// A type used to greedily parse another type until the input is empty. | |
| struct List<T>(Vec<T>); | |
| impl<T: Parse> Parse for List<T> { | |
| fn parse(input: ParseStream<'_>) -> Result<Self> { | |
| let mut list = Vec::new(); | |
| while !input.is_empty() { | |
| list.push(input.parse()?); | |
| } | |
| Ok(List(list)) | |
| } | |
| } | |
| struct Desc { | |
| modifier: Ident, | |
| tcx_binding: Option<Ident>, | |
| expr_list: Punctuated<Expr, Token![,]>, | |
| } | |
| struct CacheOnDiskIf { | |
| modifier: Ident, | |
| tcx_binding: Option<Pat>, | |
| block: Block, | |
| } | |
| struct QueryModifiers { | |
| /// The description of the query. | |
| desc: Desc, | |
| /// Use this type for the in-memory cache. | |
| arena_cache: Option<Ident>, | |
| /// Cache the query to disk if the `Block` returns true. | |
| cache_on_disk_if: Option<CacheOnDiskIf>, | |
| /// A cycle error for this query aborting the compilation with a fatal error. | |
| cycle_fatal: Option<Ident>, | |
| /// A cycle error results in a delay_bug call | |
| cycle_delay_bug: Option<Ident>, | |
| /// A cycle error results in a stashed cycle error that can be unstashed and canceled later | |
| cycle_stash: Option<Ident>, | |
| /// Don't hash the result, instead just mark a query red if it runs | |
| no_hash: Option<Ident>, | |
| /// Generate a dep node based on the dependencies of the query | |
| anon: Option<Ident>, | |
| /// Always evaluate the query, ignoring its dependencies | |
| eval_always: Option<Ident>, | |
| /// Whether the query has a call depth limit | |
| depth_limit: Option<Ident>, | |
| /// Use a separate query provider for local and extern crates | |
| separate_provide_extern: Option<Ident>, | |
| /// Generate a `feed` method to set the query's value from another query. | |
| feedable: Option<Ident>, | |
| /// When this query is called via `tcx.ensure_ok()`, it returns | |
| /// `Result<(), ErrorGuaranteed>` instead of `()`. If the query needs to | |
| /// be executed, and that execution returns an error, the error result is | |
| /// returned to the caller. | |
| /// | |
| /// If execution is skipped, a synthetic `Ok(())` is returned, on the | |
| /// assumption that a query with all-green inputs must have succeeded. | |
| /// | |
| /// Can only be applied to queries with a return value of | |
| /// `Result<_, ErrorGuaranteed>`. | |
| return_result_from_ensure_ok: Option<Ident>, | |
| } | |
| fn parse_query_modifiers(input: ParseStream<'_>) -> Result<QueryModifiers> { | |
| let mut arena_cache = None; | |
| let mut cache_on_disk_if = None; | |
| let mut desc = None; | |
| let mut cycle_fatal = None; | |
| let mut cycle_delay_bug = None; | |
| let mut cycle_stash = None; | |
| let mut no_hash = None; | |
| let mut anon = None; | |
| let mut eval_always = None; | |
| let mut depth_limit = None; | |
| let mut separate_provide_extern = None; | |
| let mut feedable = None; | |
| let mut return_result_from_ensure_ok = None; | |
| while !input.is_empty() { | |
| let modifier: Ident = input.parse()?; | |
| macro_rules! try_insert { | |
| ($name:ident = $expr:expr) => { | |
| if $name.is_some() { | |
| return Err(Error::new(modifier.span(), "duplicate modifier")); | |
| } | |
| $name = Some($expr); | |
| }; | |
| } | |
| if modifier == "desc" { | |
| // Parse a description modifier like: | |
| // `desc { |tcx| "foo {}", tcx.item_path(key) }` | |
| let attr_content; | |
| braced!(attr_content in input); | |
| let tcx_binding = if attr_content.peek(Token![|]) { | |
| attr_content.parse::<Token![|]>()?; | |
| let tcx = attr_content.parse()?; | |
| attr_content.parse::<Token![|]>()?; | |
| Some(tcx) | |
| } else { | |
| None | |
| }; | |
| let expr_list = attr_content.parse_terminated(Expr::parse, Token![,])?; | |
| try_insert!(desc = Desc { modifier, tcx_binding, expr_list }); | |
| } else if modifier == "cache_on_disk_if" { | |
| // Parse a cache-on-disk modifier like: | |
| // | |
| // `cache_on_disk_if { true }` | |
| // `cache_on_disk_if { key.is_local() }` | |
| // `cache_on_disk_if(tcx) { tcx.is_typeck_child(key.to_def_id()) }` | |
| let tcx_binding = if input.peek(token::Paren) { | |
| let args; | |
| parenthesized!(args in input); | |
| let tcx = Pat::parse_single(&args)?; | |
| Some(tcx) | |
| } else { | |
| None | |
| }; | |
| let block = input.parse()?; | |
| try_insert!(cache_on_disk_if = CacheOnDiskIf { modifier, tcx_binding, block }); | |
| } else if modifier == "arena_cache" { | |
| try_insert!(arena_cache = modifier); | |
| } else if modifier == "cycle_fatal" { | |
| try_insert!(cycle_fatal = modifier); | |
| } else if modifier == "cycle_delay_bug" { | |
| try_insert!(cycle_delay_bug = modifier); | |
| } else if modifier == "cycle_stash" { | |
| try_insert!(cycle_stash = modifier); | |
| } else if modifier == "no_hash" { | |
| try_insert!(no_hash = modifier); | |
| } else if modifier == "anon" { | |
| try_insert!(anon = modifier); | |
| } else if modifier == "eval_always" { | |
| try_insert!(eval_always = modifier); | |
| } else if modifier == "depth_limit" { | |
| try_insert!(depth_limit = modifier); | |
| } else if modifier == "separate_provide_extern" { | |
| try_insert!(separate_provide_extern = modifier); | |
| } else if modifier == "feedable" { | |
| try_insert!(feedable = modifier); | |
| } else if modifier == "return_result_from_ensure_ok" { | |
| try_insert!(return_result_from_ensure_ok = modifier); | |
| } else { | |
| return Err(Error::new(modifier.span(), "unknown query modifier")); | |
| } | |
| } | |
| let Some(desc) = desc else { | |
| return Err(input.error("no description provided")); | |
| }; | |
| Ok(QueryModifiers { | |
| arena_cache, | |
| cache_on_disk_if, | |
| desc, | |
| cycle_fatal, | |
| cycle_delay_bug, | |
| cycle_stash, | |
| no_hash, | |
| anon, | |
| eval_always, | |
| depth_limit, | |
| separate_provide_extern, | |
| feedable, | |
| return_result_from_ensure_ok, | |
| }) | |
| } | |
| fn doc_comment_from_desc(list: &Punctuated<Expr, token::Comma>) -> Result<Attribute> { | |
| use ::syn::*; | |
| let mut iter = list.iter(); | |
| let format_str: String = match iter.next() { | |
| Some(&Expr::Lit(ExprLit { lit: Lit::Str(ref lit_str), .. })) => { | |
| lit_str.value().replace("`{}`", "{}") // We add them later anyways for consistency | |
| } | |
| _ => return Err(Error::new(list.span(), "Expected a string literal")), | |
| }; | |
| let mut fmt_fragments = format_str.split("{}"); | |
| let mut doc_string = fmt_fragments.next().unwrap().to_string(); | |
| iter.map(::quote::ToTokens::to_token_stream).zip(fmt_fragments).for_each( | |
| |(tts, next_fmt_fragment)| { | |
| use ::core::fmt::Write; | |
| write!( | |
| &mut doc_string, | |
| " `{}` {}", | |
| tts.to_string().replace(" . ", "."), | |
| next_fmt_fragment, | |
| ) | |
| .unwrap(); | |
| }, | |
| ); | |
| let doc_string = format!("[query description - consider adding a doc-comment!] {doc_string}"); | |
| Ok(parse_quote! { #[doc = #doc_string] }) | |
| } | |
| /// Contains token streams that are used to accumulate per-query helper | |
| /// functions, to be used by the final output of `rustc_queries!`. | |
| /// | |
| /// Helper items typically have the same name as the query they relate to, | |
| /// and expect to be interpolated into a dedicated module. | |
| #[derive(Default)] | |
| struct HelperTokenStreams { | |
| description_fns_stream: proc_macro2::TokenStream, | |
| cache_on_disk_if_fns_stream: proc_macro2::TokenStream, | |
| } | |
| fn make_helpers_for_query(query: &Query, streams: &mut HelperTokenStreams) { | |
| let Query { name, key, modifiers, arg, .. } = &query; | |
| // Replace span for `name` to make rust-analyzer ignore it. | |
| let mut erased_name = name.clone(); | |
| erased_name.set_span(Span::call_site()); | |
| // Generate a function to check whether we should cache the query to disk, for some key. | |
| if let Some(CacheOnDiskIf { tcx_binding, block, .. }) = modifiers.cache_on_disk_if.as_ref() { | |
| let tcx = tcx_binding.as_ref().map(|t| quote! { #t }).unwrap_or_else(|| quote! { _ }); | |
| // we're taking `key` by reference, but some rustc types usually prefer being passed by value | |
| streams.cache_on_disk_if_fns_stream.extend(quote! { | |
| #[allow(unused_variables, rustc::pass_by_value)] | |
| #[inline] | |
| pub fn #erased_name<'tcx>(#tcx: TyCtxt<'tcx>, #key: &crate::queries::#name::Key<'tcx>) -> bool | |
| #block | |
| }); | |
| } | |
| let Desc { tcx_binding, expr_list, .. } = &modifiers.desc; | |
| let tcx = tcx_binding.as_ref().map_or_else(|| quote! { _ }, |t| quote! { #t }); | |
| let desc = quote! { | |
| #[allow(unused_variables)] | |
| pub fn #erased_name<'tcx>(tcx: TyCtxt<'tcx>, key: #arg) -> String { | |
| let (#tcx, #key) = (tcx, key); | |
| format!(#expr_list) | |
| } | |
| }; | |
| streams.description_fns_stream.extend(quote! { | |
| #desc | |
| }); | |
| } | |
| /// Add hints for rust-analyzer | |
| fn add_to_analyzer_stream(query: &Query, analyzer_stream: &mut proc_macro2::TokenStream) { | |
| // Add links to relevant modifiers | |
| let modifiers = &query.modifiers; | |
| let mut modifiers_stream = quote! {}; | |
| let name = &modifiers.desc.modifier; | |
| modifiers_stream.extend(quote! { | |
| crate::query::modifiers::#name; | |
| }); | |
| if let Some(CacheOnDiskIf { modifier, .. }) = &modifiers.cache_on_disk_if { | |
| modifiers_stream.extend(quote! { | |
| crate::query::modifiers::#modifier; | |
| }); | |
| } | |
| macro_rules! doc_link { | |
| ( $( $modifier:ident ),+ $(,)? ) => { | |
| $( | |
| if let Some(name) = &modifiers.$modifier { | |
| modifiers_stream.extend(quote! { | |
| crate::query::modifiers::#name; | |
| }); | |
| } | |
| )+ | |
| } | |
| } | |
| doc_link!( | |
| arena_cache, | |
| cycle_fatal, | |
| cycle_delay_bug, | |
| cycle_stash, | |
| no_hash, | |
| anon, | |
| eval_always, | |
| depth_limit, | |
| separate_provide_extern, | |
| feedable, | |
| return_result_from_ensure_ok, | |
| ); | |
| let name = &query.name; | |
| // Replace span for `name` to make rust-analyzer ignore it. | |
| let mut erased_name = name.clone(); | |
| erased_name.set_span(Span::call_site()); | |
| let result = &query.result; | |
| // This dead code exists to instruct rust-analyzer about the link between the `rustc_queries` | |
| // query names and the corresponding produced provider. The issue is that by nature of this | |
| // macro producing a higher order macro that has all its token in the macro declaration we lose | |
| // any meaningful spans, resulting in rust-analyzer being unable to make the connection between | |
| // the query name and the corresponding providers field. The trick to fix this is to have | |
| // `rustc_queries` emit a field access with the given name's span which allows it to successfully | |
| // show references / go to definition to the corresponding provider assignment which is usually | |
| // the more interesting place. | |
| let ra_hint = quote! { | |
| let crate::query::Providers { #name: _, .. }; | |
| }; | |
| analyzer_stream.extend(quote! { | |
| #[inline(always)] | |
| fn #erased_name<'tcx>() #result { | |
| #ra_hint | |
| #modifiers_stream | |
| loop {} | |
| } | |
| }); | |
| } | |
| pub(super) fn rustc_queries(input: TokenStream) -> TokenStream { | |
| let queries = parse_macro_input!(input as List<Query>); | |
| let mut query_stream = quote! {}; | |
| let mut helpers = HelperTokenStreams::default(); | |
| let mut feedable_queries = quote! {}; | |
| let mut analyzer_stream = quote! {}; | |
| let mut errors = quote! {}; | |
| macro_rules! assert { | |
| ( $cond:expr, $span:expr, $( $tt:tt )+ ) => { | |
| if !$cond { | |
| errors.extend( | |
| Error::new($span, format!($($tt)+)).into_compile_error(), | |
| ); | |
| } | |
| } | |
| } | |
| for query in queries.0 { | |
| let Query { name, arg, modifiers, .. } = &query; | |
| let result_full = &query.result; | |
| let result = match query.result { | |
| ReturnType::Default => quote! { -> () }, | |
| _ => quote! { #result_full }, | |
| }; | |
| let mut attributes = Vec::new(); | |
| macro_rules! passthrough { | |
| ( $( $modifier:ident ),+ $(,)? ) => { | |
| $( if let Some($modifier) = &modifiers.$modifier { | |
| attributes.push(quote! { (#$modifier) }); | |
| }; )+ | |
| } | |
| } | |
| passthrough!( | |
| arena_cache, | |
| cycle_fatal, | |
| cycle_delay_bug, | |
| cycle_stash, | |
| no_hash, | |
| anon, | |
| eval_always, | |
| feedable, | |
| depth_limit, | |
| separate_provide_extern, | |
| return_result_from_ensure_ok, | |
| ); | |
| // If there was a `cache_on_disk_if` modifier in the real input, pass | |
| // on a synthetic `(cache_on_disk)` modifier that can be inspected by | |
| // macro-rules macros. | |
| if modifiers.cache_on_disk_if.is_some() { | |
| attributes.push(quote! { (cache_on_disk) }); | |
| } | |
| // This uses the span of the query definition for the commas, | |
| // which can be important if we later encounter any ambiguity | |
| // errors with any of the numerous macro_rules! macros that | |
| // we use. Using the call-site span would result in a span pointing | |
| // at the entire `rustc_queries!` invocation, which wouldn't | |
| // be very useful. | |
| let span = name.span(); | |
| let attribute_stream = quote_spanned! {span=> #(#attributes),*}; | |
| let doc_comments = &query.doc_comments; | |
| // Add the query to the group | |
| query_stream.extend(quote! { | |
| #(#doc_comments)* | |
| [#attribute_stream] fn #name(#arg) #result, | |
| }); | |
| if let Some(feedable) = &modifiers.feedable { | |
| assert!( | |
| modifiers.anon.is_none(), | |
| feedable.span(), | |
| "Query {name} cannot be both `feedable` and `anon`." | |
| ); | |
| assert!( | |
| modifiers.eval_always.is_none(), | |
| feedable.span(), | |
| "Query {name} cannot be both `feedable` and `eval_always`." | |
| ); | |
| feedable_queries.extend(quote! { | |
| [#attribute_stream] fn #name(#arg) #result, | |
| }); | |
| } | |
| add_to_analyzer_stream(&query, &mut analyzer_stream); | |
| make_helpers_for_query(&query, &mut helpers); | |
| } | |
| let HelperTokenStreams { description_fns_stream, cache_on_disk_if_fns_stream } = helpers; | |
| TokenStream::from(quote! { | |
| /// Higher-order macro that invokes the specified macro with a prepared | |
| /// list of all query signatures (including modifiers). | |
| /// | |
| /// This allows multiple simpler macros to each have access to the list | |
| /// of queries. | |
| #[macro_export] | |
| macro_rules! rustc_with_all_queries { | |
| ( | |
| // The macro to invoke once, on all queries (plus extras). | |
| $macro:ident! | |
| // Within [], an optional list of extra "query" signatures to | |
| // pass to the given macro, in addition to the actual queries. | |
| $( [$($extra_fake_queries:tt)*] )? | |
| ) => { | |
| $macro! { | |
| $( $($extra_fake_queries)* )? | |
| #query_stream | |
| } | |
| } | |
| } | |
| macro_rules! rustc_feedable_queries { | |
| ( $macro:ident! ) => { | |
| $macro!(#feedable_queries); | |
| } | |
| } | |
| // Add hints for rust-analyzer | |
| mod _analyzer_hints { | |
| use super::*; | |
| #analyzer_stream | |
| } | |
| /// Functions that format a human-readable description of each query | |
| /// and its key, as specified by the `desc` query modifier. | |
| /// | |
| /// (The leading `_` avoids collisions with actual query names when | |
| /// expanded in `rustc_middle::queries`, and makes this macro-generated | |
| /// module easier to search for.) | |
| pub mod _description_fns { | |
| use super::*; | |
| #description_fns_stream | |
| } | |
| // FIXME(Zalathar): Instead of declaring these functions directly, can | |
| // we put them in a macro and then expand that macro downstream in | |
| // `rustc_query_impl`, where the functions are actually used? | |
| pub mod _cache_on_disk_if_fns { | |
| use super::*; | |
| #cache_on_disk_if_fns_stream | |
| } | |
| #errors | |
| }) | |
| } |