blob: 05a28c48f80633e34fe764022e618e6f72dff177 [file] [log] [blame]
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!(non_query);
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()
}
/// Declaration of a compiler query.
///
/// ```ignore (illustrative)
/// /// Doc comment for `my_query`.
/// // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ doc_comments
/// query my_query(key: DefId) -> Value { anon }
/// // ^^^^^^^^ name
/// // ^^^ key_pat
/// // ^^^^^ key_ty
/// // ^^^^^^^^ return_ty
/// // ^^^^ modifiers
/// ```
struct Query {
doc_comments: Vec<Attribute>,
name: Ident,
/// Parameter name for the key, or an arbitrary irrefutable pattern (e.g. `_`).
key_pat: Pat,
key_ty: Type,
return_ty: ReturnType,
modifiers: QueryModifiers,
}
/// Declaration of a non-query dep kind.
/// ```ignore (illustrative)
/// /// Doc comment for `MyNonQuery`.
/// // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ doc_comments
/// non_query MyNonQuery
/// // ^^^^^^^^^^ name
/// ```
struct NonQuery {
doc_comments: Vec<Attribute>,
name: Ident,
}
enum QueryEntry {
Query(Query),
NonQuery(NonQuery),
}
impl Parse for QueryEntry {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut doc_comments = check_attributes(input.call(Attribute::parse_outer)?)?;
// Try the non-query case first.
if input.parse::<kw::non_query>().is_ok() {
let name: Ident = input.parse()?;
return Ok(QueryEntry::NonQuery(NonQuery { doc_comments, name }));
}
// Parse the query declaration. Like `query type_of(key: DefId) -> Ty<'tcx>`
if input.parse::<kw::query>().is_err() {
return Err(input.error("expected `query` or `non_query`"));
}
let name: Ident = input.parse()?;
// `(key: DefId)`
let parens_content;
parenthesized!(parens_content in input);
let key_pat = Pat::parse_single(&parens_content)?;
parens_content.parse::<Token![:]>()?;
let key_ty = parens_content.parse::<Type>()?;
let _trailing_comma = parens_content.parse::<Option<Token![,]>>()?;
// `-> Value`
let return_ty = input.parse::<ReturnType>()?;
// Parse the query modifiers
let braces_content;
braced!(braces_content in input);
let modifiers = parse_query_modifiers(&braces_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(QueryEntry::Query(Query { doc_comments, modifiers, name, key_pat, key_ty, return_ty }))
}
}
/// 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,
expr_list: Punctuated<Expr, Token![,]>,
}
struct CacheOnDiskIf {
modifier: Ident,
block: Block,
}
/// See `rustc_middle::query::modifiers` for documentation of each query modifier.
struct QueryModifiers {
// tidy-alphabetical-start
anon: Option<Ident>,
arena_cache: Option<Ident>,
cache_on_disk_if: Option<CacheOnDiskIf>,
cycle_delay_bug: Option<Ident>,
cycle_stash: Option<Ident>,
depth_limit: Option<Ident>,
desc: Desc,
eval_always: Option<Ident>,
feedable: Option<Ident>,
no_hash: Option<Ident>,
separate_provide_extern: Option<Ident>,
// tidy-alphabetical-end
}
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_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;
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 { "foo {}", tcx.item_path(key) }`
let attr_content;
braced!(attr_content in input);
let expr_list = attr_content.parse_terminated(Expr::parse, Token![,])?;
try_insert!(desc = Desc { modifier, expr_list });
} else if modifier == "cache_on_disk_if" {
// Parse a cache-on-disk modifier like:
// `cache_on_disk_if { tcx.is_typeck_child(key.to_def_id()) }`
let block = input.parse()?;
try_insert!(cache_on_disk_if = CacheOnDiskIf { modifier, block });
} else if modifier == "arena_cache" {
try_insert!(arena_cache = 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 {
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_delay_bug,
cycle_stash,
no_hash,
anon,
eval_always,
depth_limit,
separate_provide_extern,
feedable,
})
}
// Does `ret_ty` match `Result<_, ErrorGuaranteed>`?
fn returns_error_guaranteed(ret_ty: &ReturnType) -> bool {
use ::syn::*;
if let ReturnType::Type(_, ret_ty) = ret_ty
&& let Type::Path(type_path) = ret_ty.as_ref()
&& let Some(PathSegment { ident, arguments }) = type_path.path.segments.last()
&& ident == "Result"
&& let PathArguments::AngleBracketed(args) = arguments
&& args.args.len() == 2
&& let GenericArgument::Type(ty) = &args.args[1]
&& let Type::Path(type_path) = ty
&& type_path.path.is_ident("ErrorGuaranteed")
{
true
} else {
false
}
}
fn make_modifiers_stream(query: &Query) -> proc_macro2::TokenStream {
let QueryModifiers {
// tidy-alphabetical-start
anon,
arena_cache,
cache_on_disk_if,
cycle_delay_bug,
cycle_stash,
depth_limit,
desc: _,
eval_always,
feedable,
no_hash,
separate_provide_extern,
// tidy-alphabetical-end
} = &query.modifiers;
let anon = anon.is_some();
let arena_cache = arena_cache.is_some();
let cache_on_disk = cache_on_disk_if.is_some();
let cycle_error_handling = if cycle_delay_bug.is_some() {
quote! { DelayBug }
} else if cycle_stash.is_some() {
quote! { Stash }
} else {
quote! { Error }
};
let depth_limit = depth_limit.is_some();
let eval_always = eval_always.is_some();
let feedable = feedable.is_some();
let no_hash = no_hash.is_some();
let returns_error_guaranteed = returns_error_guaranteed(&query.return_ty);
let separate_provide_extern = separate_provide_extern.is_some();
// Giving an input span to the modifier names in the modifier list seems
// to give slightly more helpful errors when one of the callback macros
// fails to parse the modifier list.
let query_name_span = query.name.span();
quote_spanned! {
query_name_span =>
// Search for (QMODLIST) to find all occurrences of this query modifier list.
// tidy-alphabetical-start
anon: #anon,
arena_cache: #arena_cache,
cache_on_disk: #cache_on_disk,
cycle_error_handling: #cycle_error_handling,
depth_limit: #depth_limit,
eval_always: #eval_always,
feedable: #feedable,
no_hash: #no_hash,
returns_error_guaranteed: #returns_error_guaranteed,
separate_provide_extern: #separate_provide_extern,
// tidy-alphabetical-end
}
}
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_pat, key_ty, modifiers, .. } = &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 { block, .. }) = modifiers.cache_on_disk_if.as_ref() {
streams.cache_on_disk_if_fns_stream.extend(quote! {
#[allow(unused_variables)]
#[inline]
pub fn #erased_name<'tcx>(tcx: TyCtxt<'tcx>, #key_pat: #key_ty) -> bool
#block
});
}
let Desc { expr_list, .. } = &modifiers.desc;
let desc = quote! {
#[allow(unused_variables)]
pub fn #erased_name<'tcx>(tcx: TyCtxt<'tcx>, #key_pat: #key_ty) -> String {
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_delay_bug,
cycle_stash,
no_hash,
anon,
eval_always,
depth_limit,
separate_provide_extern,
feedable,
);
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.return_ty;
// 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<QueryEntry>);
let mut query_stream = quote! {};
let mut non_query_stream = quote! {};
let mut helpers = HelperTokenStreams::default();
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 = match query {
QueryEntry::Query(query) => query,
QueryEntry::NonQuery(NonQuery { doc_comments, name }) => {
// Get the exceptional non-query case out of the way first.
non_query_stream.extend(quote! {
#(#doc_comments)*
#name,
});
continue;
}
};
let Query { doc_comments, name, key_ty, return_ty, modifiers, .. } = &query;
// Normalize an absent return type into `-> ()` to make macro-rules parsing easier.
let return_ty = match return_ty {
ReturnType::Default => quote! { -> () },
ReturnType::Type(..) => quote! { #return_ty },
};
let modifiers_stream = make_modifiers_stream(&query);
// Add the query to the group
query_stream.extend(quote! {
#(#doc_comments)*
fn #name(#key_ty) #return_ty
{ #modifiers_stream }
});
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`."
);
}
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) a list of all query
/// signatures (including modifiers), and (b) a list of non-query names. This allows
/// multiple simpler macros to each have access to these lists.
#[macro_export]
macro_rules! rustc_with_all_queries {
(
// The macro to invoke once, on all queries and non-queries.
$macro:ident!
) => {
$macro! {
queries { #query_stream }
non_queries { #non_query_stream }
}
}
}
// 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
})
}