Skip to content

Commit 770abe7

Browse files
committed
coverage: Build an "expansion tree" and use it to unexpand raw spans
1 parent cb92390 commit 770abe7

File tree

9 files changed

+239
-162
lines changed

9 files changed

+239
-162
lines changed
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use rustc_data_structures::fx::{FxIndexMap, FxIndexSet, IndexEntry};
2+
use rustc_middle::mir::coverage::BasicCoverageBlock;
3+
use rustc_span::{ExpnId, ExpnKind, Span};
4+
5+
#[derive(Clone, Copy, Debug)]
6+
pub(crate) struct SpanWithBcb {
7+
pub(crate) span: Span,
8+
pub(crate) bcb: BasicCoverageBlock,
9+
}
10+
11+
#[derive(Debug)]
12+
pub(crate) struct ExpnTree {
13+
nodes: FxIndexMap<ExpnId, ExpnNode>,
14+
}
15+
16+
impl ExpnTree {
17+
pub(crate) fn get(&self, expn_id: ExpnId) -> Option<&ExpnNode> {
18+
self.nodes.get(&expn_id)
19+
}
20+
21+
/// Yields the tree node for the given expansion ID (if present), followed
22+
/// by the nodes of all of its descendants in depth-first order.
23+
pub(crate) fn iter_node_and_descendants(
24+
&self,
25+
root_expn_id: ExpnId,
26+
) -> impl Iterator<Item = &ExpnNode> {
27+
gen move {
28+
let Some(root_node) = self.get(root_expn_id) else { return };
29+
yield root_node;
30+
31+
// Stack of child-node-ID iterators that drives the depth-first traversal.
32+
let mut iter_stack = vec![root_node.child_expn_ids.iter()];
33+
34+
while let Some(curr_iter) = iter_stack.last_mut() {
35+
// Pull the next ID from the top of the stack.
36+
let Some(&curr_id) = curr_iter.next() else {
37+
iter_stack.pop();
38+
continue;
39+
};
40+
41+
// Yield this node.
42+
let Some(node) = self.get(curr_id) else { continue };
43+
yield node;
44+
45+
// Push the node's children, to be traversed next.
46+
if !node.child_expn_ids.is_empty() {
47+
iter_stack.push(node.child_expn_ids.iter());
48+
}
49+
}
50+
}
51+
}
52+
}
53+
54+
#[derive(Debug)]
55+
pub(crate) struct ExpnNode {
56+
/// Storing the expansion ID in its own node is not strictly necessary,
57+
/// but is helpful for debugging and might be useful later.
58+
#[expect(dead_code)]
59+
pub(crate) expn_id: ExpnId,
60+
61+
// Useful info extracted from `ExpnData`.
62+
pub(crate) expn_kind: ExpnKind,
63+
/// Non-dummy `ExpnData::call_site` span.
64+
pub(crate) call_site: Option<Span>,
65+
/// Expansion ID of `call_site`, if present.
66+
/// This links an expansion node to its parent in the tree.
67+
pub(crate) call_site_expn_id: Option<ExpnId>,
68+
69+
/// Spans (and their associated BCBs) belonging to this expansion.
70+
pub(crate) spans: Vec<SpanWithBcb>,
71+
/// Expansions whose call-site is in this expansion.
72+
pub(crate) child_expn_ids: FxIndexSet<ExpnId>,
73+
}
74+
75+
impl ExpnNode {
76+
fn new(expn_id: ExpnId) -> Self {
77+
let expn_data = expn_id.expn_data();
78+
79+
let call_site = Some(expn_data.call_site).filter(|sp| !sp.is_dummy());
80+
let call_site_expn_id = try { call_site?.ctxt().outer_expn() };
81+
82+
Self {
83+
expn_id,
84+
85+
expn_kind: expn_data.kind.clone(),
86+
call_site,
87+
call_site_expn_id,
88+
89+
spans: vec![],
90+
child_expn_ids: FxIndexSet::default(),
91+
}
92+
}
93+
}
94+
95+
/// Given a collection of span/BCB pairs from potentially-different syntax contexts,
96+
/// arranges them into an "expansion tree" based on their expansion call-sites.
97+
pub(crate) fn build_expn_tree(spans: impl IntoIterator<Item = SpanWithBcb>) -> ExpnTree {
98+
let mut nodes = FxIndexMap::default();
99+
let new_node = |&expn_id: &ExpnId| ExpnNode::new(expn_id);
100+
101+
for span_with_bcb in spans {
102+
// Create a node for this span's enclosing expansion, and add the span to it.
103+
let expn_id = span_with_bcb.span.ctxt().outer_expn();
104+
let node = nodes.entry(expn_id).or_insert_with_key(new_node);
105+
node.spans.push(span_with_bcb);
106+
107+
// Now walk up the expansion call-site chain, creating nodes and registering children.
108+
let mut prev = expn_id;
109+
let mut curr_expn_id = node.call_site_expn_id;
110+
while let Some(expn_id) = curr_expn_id {
111+
let entry = nodes.entry(expn_id);
112+
let node_existed = matches!(entry, IndexEntry::Occupied(_));
113+
114+
let node = entry.or_insert_with_key(new_node);
115+
node.child_expn_ids.insert(prev);
116+
117+
if node_existed {
118+
break;
119+
}
120+
121+
prev = expn_id;
122+
curr_expn_id = node.call_site_expn_id;
123+
}
124+
}
125+
126+
ExpnTree { nodes }
127+
}

compiler/rustc_mir_transform/src/coverage/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::coverage::graph::CoverageGraph;
88
use crate::coverage::mappings::ExtractedMappings;
99

1010
mod counters;
11+
mod expansion;
1112
mod graph;
1213
mod hir_info;
1314
mod mappings;

compiler/rustc_mir_transform/src/coverage/spans.rs

Lines changed: 80 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
use rustc_data_structures::fx::FxHashSet;
21
use rustc_middle::mir;
32
use rustc_middle::mir::coverage::{Mapping, MappingKind, START_BCB};
43
use rustc_middle::ty::TyCtxt;
54
use rustc_span::source_map::SourceMap;
6-
use rustc_span::{BytePos, DesugaringKind, ExpnKind, MacroKind, Span};
5+
use rustc_span::{BytePos, DesugaringKind, ExpnId, ExpnKind, MacroKind, Span};
76
use tracing::instrument;
87

8+
use crate::coverage::expansion::{self, ExpnTree, SpanWithBcb};
99
use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph};
1010
use crate::coverage::hir_info::ExtractedHirInfo;
11-
use crate::coverage::spans::from_mir::{Hole, RawSpanFromMir, SpanFromMir};
12-
use crate::coverage::unexpand;
11+
use crate::coverage::spans::from_mir::{Hole, RawSpanFromMir};
1312

1413
mod from_mir;
1514

@@ -34,19 +33,51 @@ pub(super) fn extract_refined_covspans<'tcx>(
3433
let &ExtractedHirInfo { body_span, .. } = hir_info;
3534

3635
let raw_spans = from_mir::extract_raw_spans_from_mir(mir_body, graph);
37-
let mut covspans = raw_spans
38-
.into_iter()
39-
.filter_map(|RawSpanFromMir { raw_span, bcb }| try {
40-
let (span, expn_kind) =
41-
unexpand::unexpand_into_body_span_with_expn_kind(raw_span, body_span)?;
42-
// Discard any spans that fill the entire body, because they tend
43-
// to represent compiler-inserted code, e.g. implicitly returning `()`.
44-
if span.source_equal(body_span) {
45-
return None;
46-
};
47-
SpanFromMir { span, expn_kind, bcb }
48-
})
49-
.collect::<Vec<_>>();
36+
// Use the raw spans to build a tree of expansions for this function.
37+
let expn_tree = expansion::build_expn_tree(
38+
raw_spans
39+
.into_iter()
40+
.map(|RawSpanFromMir { raw_span, bcb }| SpanWithBcb { span: raw_span, bcb }),
41+
);
42+
43+
let mut covspans = vec![];
44+
let mut push_covspan = |covspan: Covspan| {
45+
let covspan_span = covspan.span;
46+
// Discard any spans not contained within the function body span.
47+
// Also discard any spans that fill the entire body, because they tend
48+
// to represent compiler-inserted code, e.g. implicitly returning `()`.
49+
if !body_span.contains(covspan_span) || body_span.source_equal(covspan_span) {
50+
return;
51+
}
52+
53+
// Each pushed covspan should have the same context as the body span.
54+
// If it somehow doesn't, discard the covspan, or panic in debug builds.
55+
if !body_span.eq_ctxt(covspan_span) {
56+
debug_assert!(
57+
false,
58+
"span context mismatch: body_span={body_span:?}, covspan.span={covspan_span:?}"
59+
);
60+
return;
61+
}
62+
63+
covspans.push(covspan);
64+
};
65+
66+
if let Some(node) = expn_tree.get(body_span.ctxt().outer_expn()) {
67+
for &SpanWithBcb { span, bcb } in &node.spans {
68+
push_covspan(Covspan { span, bcb });
69+
}
70+
71+
// For each expansion with its call-site in the body span, try to
72+
// distill a corresponding covspan.
73+
for &child_expn_id in &node.child_expn_ids {
74+
if let Some(covspan) =
75+
single_covspan_for_child_expn(tcx, graph, &expn_tree, child_expn_id)
76+
{
77+
push_covspan(covspan);
78+
}
79+
}
80+
}
5081

5182
// Only proceed if we found at least one usable span.
5283
if covspans.is_empty() {
@@ -57,17 +88,10 @@ pub(super) fn extract_refined_covspans<'tcx>(
5788
// Otherwise, add a fake span at the start of the body, to avoid an ugly
5889
// gap between the start of the body and the first real span.
5990
// FIXME: Find a more principled way to solve this problem.
60-
covspans.push(SpanFromMir::for_fn_sig(
61-
hir_info.fn_sig_span.unwrap_or_else(|| body_span.shrink_to_lo()),
62-
));
63-
64-
// First, perform the passes that need macro information.
65-
covspans.sort_by(|a, b| graph.cmp_in_dominator_order(a.bcb, b.bcb));
66-
remove_unwanted_expansion_spans(&mut covspans);
67-
shrink_visible_macro_spans(tcx, &mut covspans);
68-
69-
// We no longer need the extra information in `SpanFromMir`, so convert to `Covspan`.
70-
let mut covspans = covspans.into_iter().map(SpanFromMir::into_covspan).collect::<Vec<_>>();
91+
covspans.push(Covspan {
92+
span: hir_info.fn_sig_span.unwrap_or_else(|| body_span.shrink_to_lo()),
93+
bcb: START_BCB,
94+
});
7195

7296
let compare_covspans = |a: &Covspan, b: &Covspan| {
7397
compare_spans(a.span, b.span)
@@ -117,43 +141,37 @@ pub(super) fn extract_refined_covspans<'tcx>(
117141
}));
118142
}
119143

120-
/// Macros that expand into branches (e.g. `assert!`, `trace!`) tend to generate
121-
/// multiple condition/consequent blocks that have the span of the whole macro
122-
/// invocation, which is unhelpful. Keeping only the first such span seems to
123-
/// give better mappings, so remove the others.
124-
///
125-
/// Similarly, `await` expands to a branch on the discriminant of `Poll`, which
126-
/// leads to incorrect coverage if the `Future` is immediately ready (#98712).
127-
///
128-
/// (The input spans should be sorted in BCB dominator order, so that the
129-
/// retained "first" span is likely to dominate the others.)
130-
fn remove_unwanted_expansion_spans(covspans: &mut Vec<SpanFromMir>) {
131-
let mut deduplicated_spans = FxHashSet::default();
132-
133-
covspans.retain(|covspan| {
134-
match covspan.expn_kind {
135-
// Retain only the first await-related or macro-expanded covspan with this span.
136-
Some(ExpnKind::Desugaring(DesugaringKind::Await)) => {
137-
deduplicated_spans.insert(covspan.span)
138-
}
139-
Some(ExpnKind::Macro(MacroKind::Bang, _)) => deduplicated_spans.insert(covspan.span),
140-
// Ignore (retain) other spans.
141-
_ => true,
144+
/// For a single child expansion, try to distill it into a single span+BCB mapping.
145+
fn single_covspan_for_child_expn(
146+
tcx: TyCtxt<'_>,
147+
graph: &CoverageGraph,
148+
expn_tree: &ExpnTree,
149+
expn_id: ExpnId,
150+
) -> Option<Covspan> {
151+
let node = expn_tree.get(expn_id)?;
152+
153+
let bcbs =
154+
expn_tree.iter_node_and_descendants(expn_id).flat_map(|n| n.spans.iter().map(|s| s.bcb));
155+
156+
let bcb = match node.expn_kind {
157+
// For bang-macros (e.g. `assert!`, `trace!`) and for `await`, taking
158+
// the "first" BCB in dominator order seems to give good results.
159+
ExpnKind::Macro(MacroKind::Bang, _) | ExpnKind::Desugaring(DesugaringKind::Await) => {
160+
bcbs.min_by(|&a, &b| graph.cmp_in_dominator_order(a, b))?
142161
}
143-
});
144-
}
145-
146-
/// When a span corresponds to a macro invocation that is visible from the
147-
/// function body, truncate it to just the macro name plus `!`.
148-
/// This seems to give better results for code that uses macros.
149-
fn shrink_visible_macro_spans(tcx: TyCtxt<'_>, covspans: &mut Vec<SpanFromMir>) {
150-
let source_map = tcx.sess.source_map();
162+
// For other kinds of expansion, taking the "last" (most-dominated) BCB
163+
// seems to give good results.
164+
_ => bcbs.max_by(|&a, &b| graph.cmp_in_dominator_order(a, b))?,
165+
};
151166

152-
for covspan in covspans {
153-
if matches!(covspan.expn_kind, Some(ExpnKind::Macro(MacroKind::Bang, _))) {
154-
covspan.span = source_map.span_through_char(covspan.span, '!');
155-
}
167+
// For bang-macro expansions, limit the call-site span to just the macro
168+
// name plus `!`, excluding the macro arguments.
169+
let mut span = node.call_site?;
170+
if matches!(node.expn_kind, ExpnKind::Macro(MacroKind::Bang, _)) {
171+
span = tcx.sess.source_map().span_through_char(span, '!');
156172
}
173+
174+
Some(Covspan { span, bcb })
157175
}
158176

159177
/// Discard all covspans that overlap a hole.

compiler/rustc_mir_transform/src/coverage/spans/from_mir.rs

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ use rustc_middle::mir::coverage::CoverageKind;
55
use rustc_middle::mir::{
66
self, FakeReadCause, Statement, StatementKind, Terminator, TerminatorKind,
77
};
8-
use rustc_span::{ExpnKind, Span};
8+
use rustc_span::Span;
99

10-
use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph, START_BCB};
11-
use crate::coverage::spans::Covspan;
10+
use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph};
1211

1312
#[derive(Debug)]
1413
pub(crate) struct RawSpanFromMir {
@@ -160,32 +159,3 @@ impl Hole {
160159
true
161160
}
162161
}
163-
164-
#[derive(Debug)]
165-
pub(crate) struct SpanFromMir {
166-
/// A span that has been extracted from MIR and then "un-expanded" back to
167-
/// within the current function's `body_span`. After various intermediate
168-
/// processing steps, this span is emitted as part of the final coverage
169-
/// mappings.
170-
///
171-
/// With the exception of `fn_sig_span`, this should always be contained
172-
/// within `body_span`.
173-
pub(crate) span: Span,
174-
pub(crate) expn_kind: Option<ExpnKind>,
175-
pub(crate) bcb: BasicCoverageBlock,
176-
}
177-
178-
impl SpanFromMir {
179-
pub(crate) fn for_fn_sig(fn_sig_span: Span) -> Self {
180-
Self::new(fn_sig_span, None, START_BCB)
181-
}
182-
183-
pub(crate) fn new(span: Span, expn_kind: Option<ExpnKind>, bcb: BasicCoverageBlock) -> Self {
184-
Self { span, expn_kind, bcb }
185-
}
186-
187-
pub(crate) fn into_covspan(self) -> Covspan {
188-
let Self { span, expn_kind: _, bcb } = self;
189-
Covspan { span, bcb }
190-
}
191-
}

0 commit comments

Comments
 (0)