Skip to content

Commit 9f8d05f

Browse files
committed
macro_rules: Preserve all metavariable spans in a global side table
1 parent 23a3d77 commit 9f8d05f

29 files changed

+232
-125
lines changed

compiler/rustc_expand/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#![feature(if_let_guard)]
77
#![feature(let_chains)]
88
#![feature(macro_metavar_expr)]
9+
#![feature(map_try_insert)]
910
#![feature(proc_macro_diagnostic)]
1011
#![feature(proc_macro_internals)]
1112
#![feature(proc_macro_span)]

compiler/rustc_expand/src/mbe/transcribe.rs

+45-10
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use rustc_errors::DiagnosticBuilder;
1313
use rustc_errors::{pluralize, PResult};
1414
use rustc_span::hygiene::{LocalExpnId, Transparency};
1515
use rustc_span::symbol::{sym, Ident, MacroRulesNormalizedIdent};
16-
use rustc_span::{Span, SyntaxContext};
16+
use rustc_span::{with_metavar_spans, Span, SyntaxContext};
1717

1818
use smallvec::{smallvec, SmallVec};
1919
use std::mem;
@@ -254,7 +254,8 @@ pub(super) fn transcribe<'a>(
254254
MatchedTokenTree(tt) => {
255255
// `tt`s are emitted into the output stream directly as "raw tokens",
256256
// without wrapping them into groups.
257-
result.push(maybe_use_metavar_location(cx, &stack, sp, tt));
257+
let tt = maybe_use_metavar_location(cx, &stack, sp, tt, &mut marker);
258+
result.push(tt);
258259
}
259260
MatchedNonterminal(nt) => {
260261
// Other variables are emitted into the output stream as groups with
@@ -319,6 +320,17 @@ pub(super) fn transcribe<'a>(
319320
}
320321
}
321322

323+
/// Store the metavariable span for this original span into a side table.
324+
/// FIXME: Try to put the metavariable span into `SpanData` instead of a side table (#118517).
325+
/// An optimal encoding for inlined spans will need to be selected to minimize regressions.
326+
/// The side table approach is relatively good, but not perfect due to collisions.
327+
/// In particular, collisions happen when token is passed as an argument through several macro
328+
/// calls, like in recursive macros.
329+
/// The old heuristic below is used to improve spans in case of collisions, but diagnostics are
330+
/// still degraded sometimes in those cases.
331+
///
332+
/// The old heuristic:
333+
///
322334
/// Usually metavariables `$var` produce interpolated tokens, which have an additional place for
323335
/// keeping both the original span and the metavariable span. For `tt` metavariables that's not the
324336
/// case however, and there's no place for keeping a second span. So we try to give the single
@@ -338,15 +350,12 @@ pub(super) fn transcribe<'a>(
338350
/// These are typically used for passing larger amounts of code, and tokens in that code usually
339351
/// combine with each other and not with tokens outside of the sequence.
340352
/// - The metavariable span comes from a different crate, then we prefer the more local span.
341-
///
342-
/// FIXME: Find a way to keep both original and metavariable spans for all tokens without
343-
/// regressing compilation time too much. Several experiments for adding such spans were made in
344-
/// the past (PR #95580, #118517, #118671) and all showed some regressions.
345353
fn maybe_use_metavar_location(
346354
cx: &ExtCtxt<'_>,
347355
stack: &[Frame<'_>],
348-
metavar_span: Span,
356+
mut metavar_span: Span,
349357
orig_tt: &TokenTree,
358+
marker: &mut Marker,
350359
) -> TokenTree {
351360
let undelimited_seq = matches!(
352361
stack.last(),
@@ -357,18 +366,44 @@ fn maybe_use_metavar_location(
357366
..
358367
})
359368
);
360-
if undelimited_seq || cx.source_map().is_imported(metavar_span) {
369+
if undelimited_seq {
370+
// Do not record metavar spans for tokens from undelimited sequences, for perf reasons.
371+
return orig_tt.clone();
372+
}
373+
374+
let insert = |mspans: &mut FxHashMap<_, _>, s, ms| match mspans.try_insert(s, ms) {
375+
Ok(_) => true,
376+
Err(err) => *err.entry.get() == ms, // Tried to insert the same span, still success
377+
};
378+
marker.visit_span(&mut metavar_span);
379+
let no_collision = match orig_tt {
380+
TokenTree::Token(token, ..) => {
381+
with_metavar_spans(|mspans| insert(mspans, token.span, metavar_span))
382+
}
383+
TokenTree::Delimited(dspan, ..) => with_metavar_spans(|mspans| {
384+
insert(mspans, dspan.open, metavar_span)
385+
&& insert(mspans, dspan.close, metavar_span)
386+
&& insert(mspans, dspan.entire(), metavar_span)
387+
}),
388+
};
389+
if no_collision || cx.source_map().is_imported(metavar_span) {
361390
return orig_tt.clone();
362391
}
363392

393+
// Setting metavar spans for the heuristic spans gives better opportunities for combining them
394+
// with neighboring spans even despite their different syntactic contexts.
364395
match orig_tt {
365396
TokenTree::Token(Token { kind, span }, spacing) => {
366397
let span = metavar_span.with_ctxt(span.ctxt());
398+
with_metavar_spans(|mspans| insert(mspans, span, metavar_span));
367399
TokenTree::Token(Token { kind: kind.clone(), span }, *spacing)
368400
}
369401
TokenTree::Delimited(dspan, dspacing, delimiter, tts) => {
370-
let open = metavar_span.shrink_to_lo().with_ctxt(dspan.open.ctxt());
371-
let close = metavar_span.shrink_to_hi().with_ctxt(dspan.close.ctxt());
402+
let open = metavar_span.with_ctxt(dspan.open.ctxt());
403+
let close = metavar_span.with_ctxt(dspan.close.ctxt());
404+
with_metavar_spans(|mspans| {
405+
insert(mspans, open, metavar_span) && insert(mspans, close, metavar_span)
406+
});
372407
let dspan = DelimSpan::from_pair(open, close);
373408
TokenTree::Delimited(dspan, *dspacing, *delimiter, tts.clone())
374409
}

compiler/rustc_span/src/lib.rs

+59-14
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ pub mod fatal_error;
7272

7373
pub mod profiling;
7474

75+
use rustc_data_structures::fx::FxHashMap;
7576
use rustc_data_structures::stable_hasher::{Hash128, Hash64, HashStable, StableHasher};
7677
use rustc_data_structures::sync::{FreezeLock, FreezeWriteGuard, Lock, Lrc};
7778

@@ -98,6 +99,9 @@ mod tests;
9899
pub struct SessionGlobals {
99100
symbol_interner: symbol::Interner,
100101
span_interner: Lock<span_encoding::SpanInterner>,
102+
/// Maps a macro argument token into use of the corresponding metavariable in the macro body.
103+
/// Collisions are possible and processed in `maybe_use_metavar_location` on best effort basis.
104+
metavar_spans: Lock<FxHashMap<Span, Span>>,
101105
hygiene_data: Lock<hygiene::HygieneData>,
102106

103107
/// A reference to the source map in the `Session`. It's an `Option`
@@ -115,6 +119,7 @@ impl SessionGlobals {
115119
SessionGlobals {
116120
symbol_interner: symbol::Interner::fresh(),
117121
span_interner: Lock::new(span_encoding::SpanInterner::default()),
122+
metavar_spans: Default::default(),
118123
hygiene_data: Lock::new(hygiene::HygieneData::new(edition)),
119124
source_map: Lock::new(None),
120125
}
@@ -168,6 +173,11 @@ pub fn create_default_session_globals_then<R>(f: impl FnOnce() -> R) -> R {
168173
// deserialization.
169174
scoped_tls::scoped_thread_local!(static SESSION_GLOBALS: SessionGlobals);
170175

176+
#[inline]
177+
pub fn with_metavar_spans<R>(f: impl FnOnce(&mut FxHashMap<Span, Span>) -> R) -> R {
178+
with_session_globals(|session_globals| f(&mut session_globals.metavar_spans.lock()))
179+
}
180+
171181
// FIXME: We should use this enum or something like it to get rid of the
172182
// use of magic `/rust/1.x/...` paths across the board.
173183
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Decodable)]
@@ -824,29 +834,64 @@ impl Span {
824834
)
825835
}
826836

837+
/// Check if you can select metavar spans for the given spans to get matching contexts.
838+
fn try_metavars(a: SpanData, b: SpanData, a_orig: Span, b_orig: Span) -> (SpanData, SpanData) {
839+
let get = |mspans: &FxHashMap<_, _>, s| mspans.get(&s).copied();
840+
match with_metavar_spans(|mspans| (get(mspans, a_orig), get(mspans, b_orig))) {
841+
(None, None) => {}
842+
(Some(meta_a), None) => {
843+
let meta_a = meta_a.data();
844+
if meta_a.ctxt == b.ctxt {
845+
return (meta_a, b);
846+
}
847+
}
848+
(None, Some(meta_b)) => {
849+
let meta_b = meta_b.data();
850+
if a.ctxt == meta_b.ctxt {
851+
return (a, meta_b);
852+
}
853+
}
854+
(Some(meta_a), Some(meta_b)) => {
855+
let meta_b = meta_b.data();
856+
if a.ctxt == meta_b.ctxt {
857+
return (a, meta_b);
858+
}
859+
let meta_a = meta_a.data();
860+
if meta_a.ctxt == b.ctxt {
861+
return (meta_a, b);
862+
} else if meta_a.ctxt == meta_b.ctxt {
863+
return (meta_a, meta_b);
864+
}
865+
}
866+
}
867+
868+
(a, b)
869+
}
870+
827871
/// Prepare two spans to a combine operation like `to` or `between`.
828-
/// FIXME: consider using declarative macro metavariable spans for the given spans if they are
829-
/// better suitable for combining (#119412).
830872
fn prepare_to_combine(
831873
a_orig: Span,
832874
b_orig: Span,
833875
) -> Result<(SpanData, SpanData, Option<LocalDefId>), Span> {
834876
let (a, b) = (a_orig.data(), b_orig.data());
877+
if a.ctxt == b.ctxt {
878+
return Ok((a, b, if a.parent == b.parent { a.parent } else { None }));
879+
}
835880

836-
if a.ctxt != b.ctxt {
837-
// Context mismatches usually happen when procedural macros combine spans copied from
838-
// the macro input with spans produced by the macro (`Span::*_site`).
839-
// In that case we consider the combined span to be produced by the macro and return
840-
// the original macro-produced span as the result.
841-
// Otherwise we just fall back to returning the first span.
842-
// Combining locations typically doesn't make sense in case of context mismatches.
843-
// `is_root` here is a fast path optimization.
844-
let a_is_callsite = a.ctxt.is_root() || a.ctxt == b.span().source_callsite().ctxt();
845-
return Err(if a_is_callsite { b_orig } else { a_orig });
881+
let (a, b) = Span::try_metavars(a, b, a_orig, b_orig);
882+
if a.ctxt == b.ctxt {
883+
return Ok((a, b, if a.parent == b.parent { a.parent } else { None }));
846884
}
847885

848-
let parent = if a.parent == b.parent { a.parent } else { None };
849-
Ok((a, b, parent))
886+
// Context mismatches usually happen when procedural macros combine spans copied from
887+
// the macro input with spans produced by the macro (`Span::*_site`).
888+
// In that case we consider the combined span to be produced by the macro and return
889+
// the original macro-produced span as the result.
890+
// Otherwise we just fall back to returning the first span.
891+
// Combining locations typically doesn't make sense in case of context mismatches.
892+
// `is_root` here is a fast path optimization.
893+
let a_is_callsite = a.ctxt.is_root() || a.ctxt == b.span().source_callsite().ctxt();
894+
Err(if a_is_callsite { b_orig } else { a_orig })
850895
}
851896

852897
/// This span, but in a larger context, may switch to the metavariable span if suitable.

tests/coverage/no_spans.cov-map

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
Function name: no_spans::affected_function
2+
Raw bytes (9): 0x[01, 01, 00, 01, 01, 1a, 1c, 00, 1d]
3+
Number of files: 1
4+
- file 0 => global file 1
5+
Number of expressions: 0
6+
Number of file 0 mappings: 1
7+
- Code(Counter(0)) at (prev + 26, 28) to (start + 0, 29)
8+
19
Function name: no_spans::affected_function::{closure#0}
210
Raw bytes (9): 0x[01, 01, 00, 01, 01, 1b, 0c, 00, 0e]
311
Number of files: 1

tests/coverage/no_spans_if_not.cov-map

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
Function name: no_spans_if_not::affected_function
2+
Raw bytes (21): 0x[01, 01, 01, 01, 00, 03, 01, 16, 1c, 01, 12, 02, 02, 0d, 00, 0f, 00, 02, 0d, 00, 0f]
3+
Number of files: 1
4+
- file 0 => global file 1
5+
Number of expressions: 1
6+
- expression 0 operands: lhs = Counter(0), rhs = Zero
7+
Number of file 0 mappings: 3
8+
- Code(Counter(0)) at (prev + 22, 28) to (start + 1, 18)
9+
- Code(Expression(0, Sub)) at (prev + 2, 13) to (start + 0, 15)
10+
= (c0 - Zero)
11+
- Code(Zero) at (prev + 2, 13) to (start + 0, 15)
12+
113
Function name: no_spans_if_not::main
214
Raw bytes (9): 0x[01, 01, 00, 01, 01, 0b, 01, 02, 02]
315
Number of files: 1

tests/ui/anon-params/anon-params-edition-hygiene.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//@ check-pass
12
//@ edition:2018
23
//@ aux-build:anon-params-edition-hygiene.rs
34

@@ -8,7 +9,6 @@
89
extern crate anon_params_edition_hygiene;
910

1011
generate_trait_2015_ident!(u8);
11-
// FIXME: Edition hygiene doesn't work correctly with `tt`s in this case.
12-
generate_trait_2015_tt!(u8); //~ ERROR expected one of `:`, `@`, or `|`, found `)`
12+
generate_trait_2015_tt!(u8);
1313

1414
fn main() {}

tests/ui/anon-params/anon-params-edition-hygiene.stderr

-23
This file was deleted.

tests/ui/editions/edition-keywords-2018-2018-parsing.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ macro_rules! local_passes_ident {
1616
($i: ident) => ($i) //~ ERROR macro expansion ends with an incomplete expression
1717
}
1818
macro_rules! local_passes_tt {
19-
($i: tt) => ($i) //~ ERROR macro expansion ends with an incomplete expression
19+
($i: tt) => ($i)
2020
}
2121

2222
pub fn check_async() {
@@ -34,7 +34,7 @@ pub fn check_async() {
3434
if passes_tt!(r#async) == 1 {} // OK
3535
if local_passes_ident!(async) == 1 {} // Error reported above in the macro
3636
if local_passes_ident!(r#async) == 1 {} // OK
37-
if local_passes_tt!(async) == 1 {} // Error reported above in the macro
37+
if local_passes_tt!(async) == 1 {} //~ ERROR macro expansion ends with an incomplete expression
3838
if local_passes_tt!(r#async) == 1 {} // OK
3939
module::async(); //~ ERROR expected identifier, found keyword `async`
4040
module::r#async(); // OK

tests/ui/editions/edition-keywords-2018-2018-parsing.stderr

+3-3
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ LL | ($i: ident) => ($i)
6868
| ^ expected one of `move`, `|`, or `||`
6969

7070
error: macro expansion ends with an incomplete expression: expected one of `move`, `|`, or `||`
71-
--> $DIR/edition-keywords-2018-2018-parsing.rs:19:20
71+
--> $DIR/edition-keywords-2018-2018-parsing.rs:37:30
7272
|
73-
LL | ($i: tt) => ($i)
74-
| ^ expected one of `move`, `|`, or `||`
73+
LL | if local_passes_tt!(async) == 1 {}
74+
| ^ expected one of `move`, `|`, or `||`
7575

7676
error[E0308]: mismatched types
7777
--> $DIR/edition-keywords-2018-2018-parsing.rs:42:33

tests/ui/fmt/format-args-capture-first-literal-is-macro.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ extern crate format_string_proc_macro;
66
macro_rules! identity_mbe {
77
($tt:tt) => {
88
$tt
9-
//~^ ERROR there is no argument named `a`
109
};
1110
}
1211

@@ -16,6 +15,7 @@ fn main() {
1615
format!(identity_pm!("{a}"));
1716
//~^ ERROR there is no argument named `a`
1817
format!(identity_mbe!("{a}"));
18+
//~^ ERROR there is no argument named `a`
1919
format!(concat!("{a}"));
2020
//~^ ERROR there is no argument named `a`
2121
}

tests/ui/fmt/format-args-capture-first-literal-is-macro.stderr

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
error: there is no argument named `a`
2-
--> $DIR/format-args-capture-first-literal-is-macro.rs:16:26
2+
--> $DIR/format-args-capture-first-literal-is-macro.rs:15:26
33
|
44
LL | format!(identity_pm!("{a}"));
55
| ^^^^^
@@ -8,10 +8,10 @@ LL | format!(identity_pm!("{a}"));
88
= note: to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro
99

1010
error: there is no argument named `a`
11-
--> $DIR/format-args-capture-first-literal-is-macro.rs:8:9
11+
--> $DIR/format-args-capture-first-literal-is-macro.rs:17:27
1212
|
13-
LL | $tt
14-
| ^^^
13+
LL | format!(identity_mbe!("{a}"));
14+
| ^^^^^
1515
|
1616
= note: did you intend to capture a variable `a` from the surrounding scope?
1717
= note: to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro

tests/ui/lint/wide_pointer_comparisons.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,12 @@ fn main() {
110110
{
111111
macro_rules! cmp {
112112
($a:tt, $b:tt) => { $a == $b }
113-
//~^ WARN ambiguous wide pointer comparison
114113
}
115114

115+
// FIXME: This lint uses some custom span combination logic.
116+
// Rewrite it to adapt to the new metavariable span rules.
116117
cmp!(a, b);
118+
//~^ WARN ambiguous wide pointer comparison
117119
}
118120

119121
{

0 commit comments

Comments
 (0)