Skip to main content

rustc_macros/
query.rs

1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::{quote, quote_spanned};
4use syn::parse::{Parse, ParseStream, Result};
5use syn::punctuated::Punctuated;
6use syn::spanned::Spanned;
7use syn::{
8    AttrStyle, Attribute, Block, Error, Expr, Ident, Pat, ReturnType, Token, Type, braced,
9    parenthesized, parse_macro_input, token,
10};
11
12mod kw {
13    syn::custom_keyword!(query);
14}
15
16/// Ensures only doc comment attributes are used
17fn check_attributes(attrs: Vec<Attribute>) -> Result<Vec<Attribute>> {
18    let inner = |attr: Attribute| {
19        if !attr.path().is_ident("doc") {
20            Err(Error::new(attr.span(), "attributes not supported on queries"))
21        } else if attr.style != AttrStyle::Outer {
22            Err(Error::new(
23                attr.span(),
24                "attributes must be outer attributes (`///`), not inner attributes",
25            ))
26        } else {
27            Ok(attr)
28        }
29    };
30    attrs.into_iter().map(inner).collect()
31}
32
33/// A compiler query. `query ... { ... }`
34struct Query {
35    doc_comments: Vec<Attribute>,
36    modifiers: QueryModifiers,
37    name: Ident,
38    key: Pat,
39    arg: Type,
40    result: ReturnType,
41}
42
43impl Parse for Query {
44    fn parse(input: ParseStream<'_>) -> Result<Self> {
45        let mut doc_comments = check_attributes(input.call(Attribute::parse_outer)?)?;
46
47        // Parse the query declaration. Like `query type_of(key: DefId) -> Ty<'tcx>`
48        input.parse::<kw::query>()?;
49        let name: Ident = input.parse()?;
50        let arg_content;
51        parenthesized!(arg_content in input);
52        let key = Pat::parse_single(&arg_content)?;
53        arg_content.parse::<Token![:]>()?;
54        let arg = arg_content.parse()?;
55        let _ = arg_content.parse::<Option<Token![,]>>()?;
56        let result = input.parse()?;
57
58        // Parse the query modifiers
59        let content;
60        braced!(content in input);
61        let modifiers = parse_query_modifiers(&content)?;
62
63        // If there are no doc-comments, give at least some idea of what
64        // it does by showing the query description.
65        if doc_comments.is_empty() {
66            doc_comments.push(doc_comment_from_desc(&modifiers.desc.expr_list)?);
67        }
68
69        Ok(Query { doc_comments, modifiers, name, key, arg, result })
70    }
71}
72
73/// A type used to greedily parse another type until the input is empty.
74struct List<T>(Vec<T>);
75
76impl<T: Parse> Parse for List<T> {
77    fn parse(input: ParseStream<'_>) -> Result<Self> {
78        let mut list = Vec::new();
79        while !input.is_empty() {
80            list.push(input.parse()?);
81        }
82        Ok(List(list))
83    }
84}
85
86struct Desc {
87    modifier: Ident,
88    tcx_binding: Option<Ident>,
89    expr_list: Punctuated<Expr, Token![,]>,
90}
91
92struct CacheOnDiskIf {
93    modifier: Ident,
94    tcx_binding: Option<Pat>,
95    block: Block,
96}
97
98struct QueryModifiers {
99    /// The description of the query.
100    desc: Desc,
101
102    /// Use this type for the in-memory cache.
103    arena_cache: Option<Ident>,
104
105    /// Cache the query to disk if the `Block` returns true.
106    cache_on_disk_if: Option<CacheOnDiskIf>,
107
108    /// A cycle error for this query aborting the compilation with a fatal error.
109    cycle_fatal: Option<Ident>,
110
111    /// A cycle error results in a delay_bug call
112    cycle_delay_bug: Option<Ident>,
113
114    /// A cycle error results in a stashed cycle error that can be unstashed and canceled later
115    cycle_stash: Option<Ident>,
116
117    /// Don't hash the result, instead just mark a query red if it runs
118    no_hash: Option<Ident>,
119
120    /// Generate a dep node based on the dependencies of the query
121    anon: Option<Ident>,
122
123    /// Always evaluate the query, ignoring its dependencies
124    eval_always: Option<Ident>,
125
126    /// Whether the query has a call depth limit
127    depth_limit: Option<Ident>,
128
129    /// Use a separate query provider for local and extern crates
130    separate_provide_extern: Option<Ident>,
131
132    /// Generate a `feed` method to set the query's value from another query.
133    feedable: Option<Ident>,
134
135    /// When this query is called via `tcx.ensure_ok()`, it returns
136    /// `Result<(), ErrorGuaranteed>` instead of `()`. If the query needs to
137    /// be executed, and that execution returns an error, the error result is
138    /// returned to the caller.
139    ///
140    /// If execution is skipped, a synthetic `Ok(())` is returned, on the
141    /// assumption that a query with all-green inputs must have succeeded.
142    ///
143    /// Can only be applied to queries with a return value of
144    /// `Result<_, ErrorGuaranteed>`.
145    return_result_from_ensure_ok: Option<Ident>,
146}
147
148fn parse_query_modifiers(input: ParseStream<'_>) -> Result<QueryModifiers> {
149    let mut arena_cache = None;
150    let mut cache_on_disk_if = None;
151    let mut desc = None;
152    let mut cycle_fatal = None;
153    let mut cycle_delay_bug = None;
154    let mut cycle_stash = None;
155    let mut no_hash = None;
156    let mut anon = None;
157    let mut eval_always = None;
158    let mut depth_limit = None;
159    let mut separate_provide_extern = None;
160    let mut feedable = None;
161    let mut return_result_from_ensure_ok = None;
162
163    while !input.is_empty() {
164        let modifier: Ident = input.parse()?;
165
166        macro_rules! try_insert {
167            ($name:ident = $expr:expr) => {
168                if $name.is_some() {
169                    return Err(Error::new(modifier.span(), "duplicate modifier"));
170                }
171                $name = Some($expr);
172            };
173        }
174
175        if modifier == "desc" {
176            // Parse a description modifier like:
177            // `desc { |tcx| "foo {}", tcx.item_path(key) }`
178            let attr_content;
179            braced!(attr_content in input);
180            let tcx_binding = if attr_content.peek(Token![|]) {
181                attr_content.parse::<Token![|]>()?;
182                let tcx = attr_content.parse()?;
183                attr_content.parse::<Token![|]>()?;
184                Some(tcx)
185            } else {
186                None
187            };
188            let expr_list = attr_content.parse_terminated(Expr::parse, Token![,])?;
189            try_insert!(desc = Desc { modifier, tcx_binding, expr_list });
190        } else if modifier == "cache_on_disk_if" {
191            // Parse a cache-on-disk modifier like:
192            //
193            // `cache_on_disk_if { true }`
194            // `cache_on_disk_if { key.is_local() }`
195            // `cache_on_disk_if(tcx) { tcx.is_typeck_child(key.to_def_id()) }`
196            let tcx_binding = if input.peek(token::Paren) {
197                let args;
198                parenthesized!(args in input);
199                let tcx = Pat::parse_single(&args)?;
200                Some(tcx)
201            } else {
202                None
203            };
204            let block = input.parse()?;
205            try_insert!(cache_on_disk_if = CacheOnDiskIf { modifier, tcx_binding, block });
206        } else if modifier == "arena_cache" {
207            try_insert!(arena_cache = modifier);
208        } else if modifier == "cycle_fatal" {
209            try_insert!(cycle_fatal = modifier);
210        } else if modifier == "cycle_delay_bug" {
211            try_insert!(cycle_delay_bug = modifier);
212        } else if modifier == "cycle_stash" {
213            try_insert!(cycle_stash = modifier);
214        } else if modifier == "no_hash" {
215            try_insert!(no_hash = modifier);
216        } else if modifier == "anon" {
217            try_insert!(anon = modifier);
218        } else if modifier == "eval_always" {
219            try_insert!(eval_always = modifier);
220        } else if modifier == "depth_limit" {
221            try_insert!(depth_limit = modifier);
222        } else if modifier == "separate_provide_extern" {
223            try_insert!(separate_provide_extern = modifier);
224        } else if modifier == "feedable" {
225            try_insert!(feedable = modifier);
226        } else if modifier == "return_result_from_ensure_ok" {
227            try_insert!(return_result_from_ensure_ok = modifier);
228        } else {
229            return Err(Error::new(modifier.span(), "unknown query modifier"));
230        }
231    }
232    let Some(desc) = desc else {
233        return Err(input.error("no description provided"));
234    };
235    Ok(QueryModifiers {
236        arena_cache,
237        cache_on_disk_if,
238        desc,
239        cycle_fatal,
240        cycle_delay_bug,
241        cycle_stash,
242        no_hash,
243        anon,
244        eval_always,
245        depth_limit,
246        separate_provide_extern,
247        feedable,
248        return_result_from_ensure_ok,
249    })
250}
251
252fn doc_comment_from_desc(list: &Punctuated<Expr, token::Comma>) -> Result<Attribute> {
253    use ::syn::*;
254    let mut iter = list.iter();
255    let format_str: String = match iter.next() {
256        Some(&Expr::Lit(ExprLit { lit: Lit::Str(ref lit_str), .. })) => {
257            lit_str.value().replace("`{}`", "{}") // We add them later anyways for consistency
258        }
259        _ => return Err(Error::new(list.span(), "Expected a string literal")),
260    };
261    let mut fmt_fragments = format_str.split("{}");
262    let mut doc_string = fmt_fragments.next().unwrap().to_string();
263    iter.map(::quote::ToTokens::to_token_stream).zip(fmt_fragments).for_each(
264        |(tts, next_fmt_fragment)| {
265            use ::core::fmt::Write;
266            write!(
267                &mut doc_string,
268                " `{}` {}",
269                tts.to_string().replace(" . ", "."),
270                next_fmt_fragment,
271            )
272            .unwrap();
273        },
274    );
275    let doc_string = format!("[query description - consider adding a doc-comment!] {doc_string}");
276    Ok(parse_quote! { #[doc = #doc_string] })
277}
278
279/// Contains token streams that are used to accumulate per-query helper
280/// functions, to be used by the final output of `rustc_queries!`.
281///
282/// Helper items typically have the same name as the query they relate to,
283/// and expect to be interpolated into a dedicated module.
284#[derive(Default)]
285struct HelperTokenStreams {
286    description_fns_stream: proc_macro2::TokenStream,
287    cache_on_disk_if_fns_stream: proc_macro2::TokenStream,
288}
289
290fn make_helpers_for_query(query: &Query, streams: &mut HelperTokenStreams) {
291    let Query { name, key, modifiers, arg, .. } = &query;
292
293    // Replace span for `name` to make rust-analyzer ignore it.
294    let mut erased_name = name.clone();
295    erased_name.set_span(Span::call_site());
296
297    // Generate a function to check whether we should cache the query to disk, for some key.
298    if let Some(CacheOnDiskIf { tcx_binding, block, .. }) = modifiers.cache_on_disk_if.as_ref() {
299        let tcx = tcx_binding.as_ref().map(|t| quote! { #t }).unwrap_or_else(|| quote! { _ });
300        // we're taking `key` by reference, but some rustc types usually prefer being passed by value
301        streams.cache_on_disk_if_fns_stream.extend(quote! {
302            #[allow(unused_variables, rustc::pass_by_value)]
303            #[inline]
304            pub fn #erased_name<'tcx>(#tcx: TyCtxt<'tcx>, #key: &crate::queries::#name::Key<'tcx>) -> bool
305            #block
306        });
307    }
308
309    let Desc { tcx_binding, expr_list, .. } = &modifiers.desc;
310    let tcx = tcx_binding.as_ref().map_or_else(|| quote! { _ }, |t| quote! { #t });
311
312    let desc = quote! {
313        #[allow(unused_variables)]
314        pub fn #erased_name<'tcx>(tcx: TyCtxt<'tcx>, key: #arg) -> String {
315            let (#tcx, #key) = (tcx, key);
316            format!(#expr_list)
317        }
318    };
319
320    streams.description_fns_stream.extend(quote! {
321        #desc
322    });
323}
324
325/// Add hints for rust-analyzer
326fn add_to_analyzer_stream(query: &Query, analyzer_stream: &mut proc_macro2::TokenStream) {
327    // Add links to relevant modifiers
328
329    let modifiers = &query.modifiers;
330
331    let mut modifiers_stream = quote! {};
332
333    let name = &modifiers.desc.modifier;
334    modifiers_stream.extend(quote! {
335        crate::query::modifiers::#name;
336    });
337
338    if let Some(CacheOnDiskIf { modifier, .. }) = &modifiers.cache_on_disk_if {
339        modifiers_stream.extend(quote! {
340            crate::query::modifiers::#modifier;
341        });
342    }
343
344    macro_rules! doc_link {
345        ( $( $modifier:ident ),+ $(,)? ) => {
346            $(
347                if let Some(name) = &modifiers.$modifier {
348                    modifiers_stream.extend(quote! {
349                        crate::query::modifiers::#name;
350                    });
351                }
352            )+
353        }
354    }
355
356    doc_link!(
357        arena_cache,
358        cycle_fatal,
359        cycle_delay_bug,
360        cycle_stash,
361        no_hash,
362        anon,
363        eval_always,
364        depth_limit,
365        separate_provide_extern,
366        feedable,
367        return_result_from_ensure_ok,
368    );
369
370    let name = &query.name;
371
372    // Replace span for `name` to make rust-analyzer ignore it.
373    let mut erased_name = name.clone();
374    erased_name.set_span(Span::call_site());
375
376    let result = &query.result;
377
378    // This dead code exists to instruct rust-analyzer about the link between the `rustc_queries`
379    // query names and the corresponding produced provider. The issue is that by nature of this
380    // macro producing a higher order macro that has all its token in the macro declaration we lose
381    // any meaningful spans, resulting in rust-analyzer being unable to make the connection between
382    // the query name and the corresponding providers field. The trick to fix this is to have
383    // `rustc_queries` emit a field access with the given name's span which allows it to successfully
384    // show references / go to definition to the corresponding provider assignment which is usually
385    // the more interesting place.
386    let ra_hint = quote! {
387        let crate::query::Providers { #name: _, .. };
388    };
389
390    analyzer_stream.extend(quote! {
391        #[inline(always)]
392        fn #erased_name<'tcx>() #result {
393            #ra_hint
394            #modifiers_stream
395            loop {}
396        }
397    });
398}
399
400pub(super) fn rustc_queries(input: TokenStream) -> TokenStream {
401    let queries = parse_macro_input!(input as List<Query>);
402
403    let mut query_stream = quote! {};
404    let mut helpers = HelperTokenStreams::default();
405    let mut feedable_queries = quote! {};
406    let mut analyzer_stream = quote! {};
407    let mut errors = quote! {};
408
409    macro_rules! assert {
410        ( $cond:expr, $span:expr, $( $tt:tt )+ ) => {
411            if !$cond {
412                errors.extend(
413                    Error::new($span, format!($($tt)+)).into_compile_error(),
414                );
415            }
416        }
417    }
418
419    for query in queries.0 {
420        let Query { name, arg, modifiers, .. } = &query;
421        let result_full = &query.result;
422        let result = match query.result {
423            ReturnType::Default => quote! { -> () },
424            _ => quote! { #result_full },
425        };
426
427        let mut attributes = Vec::new();
428
429        macro_rules! passthrough {
430            ( $( $modifier:ident ),+ $(,)? ) => {
431                $( if let Some($modifier) = &modifiers.$modifier {
432                    attributes.push(quote! { (#$modifier) });
433                }; )+
434            }
435        }
436
437        passthrough!(
438            arena_cache,
439            cycle_fatal,
440            cycle_delay_bug,
441            cycle_stash,
442            no_hash,
443            anon,
444            eval_always,
445            feedable,
446            depth_limit,
447            separate_provide_extern,
448            return_result_from_ensure_ok,
449        );
450
451        // If there was a `cache_on_disk_if` modifier in the real input, pass
452        // on a synthetic `(cache_on_disk)` modifier that can be inspected by
453        // macro-rules macros.
454        if modifiers.cache_on_disk_if.is_some() {
455            attributes.push(quote! { (cache_on_disk) });
456        }
457
458        // This uses the span of the query definition for the commas,
459        // which can be important if we later encounter any ambiguity
460        // errors with any of the numerous macro_rules! macros that
461        // we use. Using the call-site span would result in a span pointing
462        // at the entire `rustc_queries!` invocation, which wouldn't
463        // be very useful.
464        let span = name.span();
465        let attribute_stream = quote_spanned! {span=> #(#attributes),*};
466        let doc_comments = &query.doc_comments;
467        // Add the query to the group
468        query_stream.extend(quote! {
469            #(#doc_comments)*
470            [#attribute_stream] fn #name(#arg) #result,
471        });
472
473        if let Some(feedable) = &modifiers.feedable {
474            assert!(
475                modifiers.anon.is_none(),
476                feedable.span(),
477                "Query {name} cannot be both `feedable` and `anon`."
478            );
479            assert!(
480                modifiers.eval_always.is_none(),
481                feedable.span(),
482                "Query {name} cannot be both `feedable` and `eval_always`."
483            );
484            feedable_queries.extend(quote! {
485                [#attribute_stream] fn #name(#arg) #result,
486            });
487        }
488
489        add_to_analyzer_stream(&query, &mut analyzer_stream);
490        make_helpers_for_query(&query, &mut helpers);
491    }
492
493    let HelperTokenStreams { description_fns_stream, cache_on_disk_if_fns_stream } = helpers;
494
495    TokenStream::from(quote! {
496        /// Higher-order macro that invokes the specified macro with a prepared
497        /// list of all query signatures (including modifiers).
498        ///
499        /// This allows multiple simpler macros to each have access to the list
500        /// of queries.
501        #[macro_export]
502        macro_rules! rustc_with_all_queries {
503            (
504                // The macro to invoke once, on all queries (plus extras).
505                $macro:ident!
506
507                // Within [], an optional list of extra "query" signatures to
508                // pass to the given macro, in addition to the actual queries.
509                $( [$($extra_fake_queries:tt)*] )?
510            ) => {
511                $macro! {
512                    $( $($extra_fake_queries)* )?
513                    #query_stream
514                }
515            }
516        }
517        macro_rules! rustc_feedable_queries {
518            ( $macro:ident! ) => {
519                $macro!(#feedable_queries);
520            }
521        }
522
523        // Add hints for rust-analyzer
524        mod _analyzer_hints {
525            use super::*;
526            #analyzer_stream
527        }
528
529        /// Functions that format a human-readable description of each query
530        /// and its key, as specified by the `desc` query modifier.
531        ///
532        /// (The leading `_` avoids collisions with actual query names when
533        /// expanded in `rustc_middle::queries`, and makes this macro-generated
534        /// module easier to search for.)
535        pub mod _description_fns {
536            use super::*;
537            #description_fns_stream
538        }
539
540        // FIXME(Zalathar): Instead of declaring these functions directly, can
541        // we put them in a macro and then expand that macro downstream in
542        // `rustc_query_impl`, where the functions are actually used?
543        pub mod _cache_on_disk_if_fns {
544            use super::*;
545            #cache_on_disk_if_fns_stream
546        }
547
548        #errors
549    })
550}