Skip to content

Commit 2bf78d1

Browse files
committed
Auto merge of rust-lang#119673 - petrochenkov:dialoc5, r=compiler-errors,cjgillot
macro_rules: Preserve all metavariable spans in a global side table This PR preserves spans of `tt` metavariables used to pass tokens to declarative macros. Such metavariable spans can then be used in span combination operations like `Span::to` to improve all kinds of diagnostics. Spans of non-`tt` metavariables are currently kept in nonterminal tokens, but the long term plan is remove all nonterminal tokens from rustc parser and rely on the proc macro model with invisible delimiters (rust-lang#114647, rust-lang#67062). In particular, `NtIdent` nonterminal (corresponding to `ident` metavariables) becomes easy to remove when this PR lands (rust-lang#119412 does it). The metavariable spans are kept in a global side table keyed by `Span`s of original tokens. The alternative to the side table is keeping them in `SpanData` instead, but the performance regressions would be large because any spans from tokens passed to declarative macros would stop being inline and would work through span interner instead, and the penalty would be paid even if we never use the metavar span for the given original span. (But also see the comment on `fn maybe_use_metavar_location` describing the map collision issues with the side table approach.) There are also other alternatives - keeping the metavar span in `Token` or `TokenTree`, but associating it with `Span` itsel is the most natural choice because metavar spans are used in span combining operations, and those operations are not necessarily tied to tokens.
2 parents 8a49772 + 12d7bac commit 2bf78d1

31 files changed

+237
-130
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.coverage

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
LL| |}
2424
LL| |
2525
LL| |macro_that_defines_a_function! {
26-
LL| | fn affected_function() {
26+
LL| 1| fn affected_function() {
2727
LL| 1| || ()
2828
LL| | }
2929
LL| |}

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/coverage/no_spans_if_not.coverage

+4-4
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
LL| |}
2020
LL| |
2121
LL| |macro_that_defines_a_function! {
22-
LL| | fn affected_function() {
23-
LL| | if !false {
24-
LL| | ()
22+
LL| 1| fn affected_function() {
23+
LL| 1| if !false {
24+
LL| 1| ()
2525
LL| | } else {
26-
LL| | ()
26+
LL| 0| ()
2727
LL| | }
2828
LL| | }
2929
LL| |}

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

0 commit comments

Comments
 (0)