Skip to content

Commit dd5c7eb

Browse files
authored
Rollup merge of #149587 - tree-sort, r=davidtwco
coverage: Sort the expansion tree to help choose a single BCB for child expansions This PR is another incremental step on the road towards proper coverage instrumentation of expansion regions. When creating coverage mappings for each relevant span in a function body, the current implementation also needs to do a separate tree traversal for each child expansion (e.g. macro calls like `println!("foo")`), in order to associate a single control-flow point (BCB) with that macro call's span. This PR changes things so that we now keep track of the “minimum” and ”maximum” BCB associated with each node in the expansion tree, which makes it much easier for the parent node to get a single BCB (min or max) for each of its child expansions. In order to make this (relatively) easy, we first need to sort the tree nodes into depth-first order. Once that's taken care of, we can iterate over all the nodes in reverse order, knowing that each node's children will have been visited before visiting the node itself.
2 parents 058a763 + 986db13 commit dd5c7eb

8 files changed

Lines changed: 304 additions & 52 deletions

File tree

compiler/rustc_mir_transform/src/coverage/expansion.rs

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use itertools::Itertools;
12
use rustc_data_structures::fx::{FxIndexMap, FxIndexSet, IndexEntry};
23
use rustc_middle::mir;
34
use rustc_middle::mir::coverage::{BasicCoverageBlock, BranchSpan};
@@ -6,6 +7,7 @@ use rustc_span::{ExpnId, ExpnKind, Span};
67
use crate::coverage::from_mir;
78
use crate::coverage::graph::CoverageGraph;
89
use crate::coverage::hir_info::ExtractedHirInfo;
10+
use crate::coverage::mappings::MappingsError;
911

1012
#[derive(Clone, Copy, Debug)]
1113
pub(crate) struct SpanWithBcb {
@@ -22,38 +24,6 @@ impl ExpnTree {
2224
pub(crate) fn get(&self, expn_id: ExpnId) -> Option<&ExpnNode> {
2325
self.nodes.get(&expn_id)
2426
}
25-
26-
/// Yields the tree node for the given expansion ID (if present), followed
27-
/// by the nodes of all of its descendants in depth-first order.
28-
pub(crate) fn iter_node_and_descendants(
29-
&self,
30-
root_expn_id: ExpnId,
31-
) -> impl Iterator<Item = &ExpnNode> {
32-
gen move {
33-
let Some(root_node) = self.get(root_expn_id) else { return };
34-
yield root_node;
35-
36-
// Stack of child-node-ID iterators that drives the depth-first traversal.
37-
let mut iter_stack = vec![root_node.child_expn_ids.iter()];
38-
39-
while let Some(curr_iter) = iter_stack.last_mut() {
40-
// Pull the next ID from the top of the stack.
41-
let Some(&curr_id) = curr_iter.next() else {
42-
iter_stack.pop();
43-
continue;
44-
};
45-
46-
// Yield this node.
47-
let Some(node) = self.get(curr_id) else { continue };
48-
yield node;
49-
50-
// Push the node's children, to be traversed next.
51-
if !node.child_expn_ids.is_empty() {
52-
iter_stack.push(node.child_expn_ids.iter());
53-
}
54-
}
55-
}
56-
}
5727
}
5828

5929
#[derive(Debug)]
@@ -62,6 +32,8 @@ pub(crate) struct ExpnNode {
6232
/// but is helpful for debugging and might be useful later.
6333
#[expect(dead_code)]
6434
pub(crate) expn_id: ExpnId,
35+
/// Index of this node in a depth-first traversal from the root.
36+
pub(crate) dfs_rank: usize,
6537

6638
// Useful info extracted from `ExpnData`.
6739
pub(crate) expn_kind: ExpnKind,
@@ -82,6 +54,10 @@ pub(crate) struct ExpnNode {
8254
pub(crate) spans: Vec<SpanWithBcb>,
8355
/// Expansions whose call-site is in this expansion.
8456
pub(crate) child_expn_ids: FxIndexSet<ExpnId>,
57+
/// The "minimum" and "maximum" BCBs (in dominator order) of ordinary spans
58+
/// belonging to this tree node and all of its descendants. Used when
59+
/// creating a single code mapping representing an entire child expansion.
60+
pub(crate) minmax_bcbs: Option<MinMaxBcbs>,
8561

8662
/// Branch spans (recorded during MIR building) belonging to this expansion.
8763
pub(crate) branch_spans: Vec<BranchSpan>,
@@ -100,6 +76,7 @@ impl ExpnNode {
10076

10177
Self {
10278
expn_id,
79+
dfs_rank: usize::MAX,
10380

10481
expn_kind: expn_data.kind,
10582
call_site,
@@ -110,6 +87,7 @@ impl ExpnNode {
11087

11188
spans: vec![],
11289
child_expn_ids: FxIndexSet::default(),
90+
minmax_bcbs: None,
11391

11492
branch_spans: vec![],
11593

@@ -124,7 +102,7 @@ pub(crate) fn build_expn_tree(
124102
mir_body: &mir::Body<'_>,
125103
hir_info: &ExtractedHirInfo,
126104
graph: &CoverageGraph,
127-
) -> ExpnTree {
105+
) -> Result<ExpnTree, MappingsError> {
128106
let raw_spans = from_mir::extract_raw_spans_from_mir(mir_body, graph);
129107

130108
let mut nodes = FxIndexMap::default();
@@ -157,6 +135,20 @@ pub(crate) fn build_expn_tree(
157135
}
158136
}
159137

138+
// Sort the tree nodes into depth-first order.
139+
sort_nodes_depth_first(&mut nodes)?;
140+
141+
// For each node, determine its "minimum" and "maximum" BCBs, based on its
142+
// own spans and its immediate children. This relies on the nodes having
143+
// been sorted, so that each node's children are processed before the node
144+
// itself.
145+
for i in (0..nodes.len()).rev() {
146+
// Computing a node's min/max BCBs requires a shared ref to other nodes.
147+
let minmax_bcbs = minmax_bcbs_for_expn_tree_node(graph, &nodes, &nodes[i]);
148+
// Now we can mutate the current node to set its min/max BCBs.
149+
nodes[i].minmax_bcbs = minmax_bcbs;
150+
}
151+
160152
// If we have a span for the function signature, associate it with the
161153
// corresponding expansion tree node.
162154
if let Some(fn_sig_span) = hir_info.fn_sig_span
@@ -189,5 +181,60 @@ pub(crate) fn build_expn_tree(
189181
}
190182
}
191183

192-
ExpnTree { nodes }
184+
Ok(ExpnTree { nodes })
185+
}
186+
187+
/// Sorts the tree nodes in the map into depth-first order.
188+
///
189+
/// This allows subsequent operations to iterate over all nodes, while assuming
190+
/// that every node occurs before all of its descendants.
191+
fn sort_nodes_depth_first(nodes: &mut FxIndexMap<ExpnId, ExpnNode>) -> Result<(), MappingsError> {
192+
let mut dfs_stack = vec![ExpnId::root()];
193+
let mut next_dfs_rank = 0usize;
194+
while let Some(expn_id) = dfs_stack.pop() {
195+
if let Some(node) = nodes.get_mut(&expn_id) {
196+
node.dfs_rank = next_dfs_rank;
197+
next_dfs_rank += 1;
198+
dfs_stack.extend(node.child_expn_ids.iter().rev().copied());
199+
}
200+
}
201+
nodes.sort_by_key(|_expn_id, node| node.dfs_rank);
202+
203+
// Verify that the depth-first search visited each node exactly once.
204+
for (i, &ExpnNode { dfs_rank, .. }) in nodes.values().enumerate() {
205+
if dfs_rank != i {
206+
tracing::debug!(dfs_rank, i, "expansion tree node's rank does not match its index");
207+
return Err(MappingsError::TreeSortFailure);
208+
}
209+
}
210+
211+
Ok(())
212+
}
213+
214+
#[derive(Clone, Copy, Debug)]
215+
pub(crate) struct MinMaxBcbs {
216+
pub(crate) min: BasicCoverageBlock,
217+
pub(crate) max: BasicCoverageBlock,
218+
}
219+
220+
/// For a single node in the expansion tree, compute its "minimum" and "maximum"
221+
/// BCBs (in dominator order), from among the BCBs of its immediate spans,
222+
/// and the min/max of its immediate children.
223+
fn minmax_bcbs_for_expn_tree_node(
224+
graph: &CoverageGraph,
225+
nodes: &FxIndexMap<ExpnId, ExpnNode>,
226+
node: &ExpnNode,
227+
) -> Option<MinMaxBcbs> {
228+
let immediate_span_bcbs = node.spans.iter().map(|sp: &SpanWithBcb| sp.bcb);
229+
let child_minmax_bcbs = node
230+
.child_expn_ids
231+
.iter()
232+
.flat_map(|id| nodes.get(id))
233+
.flat_map(|child| child.minmax_bcbs)
234+
.flat_map(|MinMaxBcbs { min, max }| [min, max]);
235+
236+
let (min, max) = Iterator::chain(immediate_span_bcbs, child_minmax_bcbs)
237+
.minmax_by(|&a, &b| graph.cmp_in_dominator_order(a, b))
238+
.into_option()?;
239+
Some(MinMaxBcbs { min, max })
193240
}

compiler/rustc_mir_transform/src/coverage/mappings.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ use crate::coverage::graph::CoverageGraph;
1111
use crate::coverage::hir_info::ExtractedHirInfo;
1212
use crate::coverage::spans::extract_refined_covspans;
1313

14+
/// Indicates why mapping extraction failed, for debug-logging purposes.
15+
#[derive(Debug)]
16+
pub(crate) enum MappingsError {
17+
NoMappings,
18+
TreeSortFailure,
19+
}
20+
1421
#[derive(Default)]
1522
pub(crate) struct ExtractedMappings {
1623
pub(crate) mappings: Vec<Mapping>,
@@ -23,8 +30,8 @@ pub(crate) fn extract_mappings_from_mir<'tcx>(
2330
mir_body: &mir::Body<'tcx>,
2431
hir_info: &ExtractedHirInfo,
2532
graph: &CoverageGraph,
26-
) -> ExtractedMappings {
27-
let expn_tree = expansion::build_expn_tree(mir_body, hir_info, graph);
33+
) -> Result<ExtractedMappings, MappingsError> {
34+
let expn_tree = expansion::build_expn_tree(mir_body, hir_info, graph)?;
2835

2936
let mut mappings = vec![];
3037

@@ -33,7 +40,11 @@ pub(crate) fn extract_mappings_from_mir<'tcx>(
3340

3441
extract_branch_mappings(mir_body, hir_info, graph, &expn_tree, &mut mappings);
3542

36-
ExtractedMappings { mappings }
43+
if mappings.is_empty() {
44+
tracing::debug!("no mappings were extracted");
45+
return Err(MappingsError::NoMappings);
46+
}
47+
Ok(ExtractedMappings { mappings })
3748
}
3849

3950
fn resolve_block_markers(

compiler/rustc_mir_transform/src/coverage/mod.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ fn instrument_function_for_coverage<'tcx>(tcx: TyCtxt<'tcx>, mir_body: &mut mir:
7373
////////////////////////////////////////////////////
7474
// Extract coverage spans and other mapping info from MIR.
7575
let ExtractedMappings { mappings } =
76-
mappings::extract_mappings_from_mir(tcx, mir_body, &hir_info, &graph);
77-
if mappings.is_empty() {
78-
// No spans could be converted into valid mappings, so skip this function.
79-
debug!("no spans could be converted into valid mappings; skipping");
80-
return;
81-
}
76+
match mappings::extract_mappings_from_mir(tcx, mir_body, &hir_info, &graph) {
77+
Ok(m) => m,
78+
Err(error) => {
79+
tracing::debug!(?error, "mapping extraction failed; skipping this function");
80+
return;
81+
}
82+
};
8283

8384
// Use the coverage graph to prepare intermediate data that will eventually
8485
// be used to assign physical counters and counter expressions to points in

compiler/rustc_mir_transform/src/coverage/spans.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ pub(super) fn extract_refined_covspans<'tcx>(
3939
// For each expansion with its call-site in the body span, try to
4040
// distill a corresponding covspan.
4141
for &child_expn_id in &node.child_expn_ids {
42-
if let Some(covspan) = single_covspan_for_child_expn(tcx, graph, &expn_tree, child_expn_id)
43-
{
42+
if let Some(covspan) = single_covspan_for_child_expn(tcx, &expn_tree, child_expn_id) {
4443
covspans.push(covspan);
4544
}
4645
}
@@ -127,24 +126,21 @@ pub(super) fn extract_refined_covspans<'tcx>(
127126
/// For a single child expansion, try to distill it into a single span+BCB mapping.
128127
fn single_covspan_for_child_expn(
129128
tcx: TyCtxt<'_>,
130-
graph: &CoverageGraph,
131129
expn_tree: &ExpnTree,
132130
expn_id: ExpnId,
133131
) -> Option<Covspan> {
134132
let node = expn_tree.get(expn_id)?;
135-
136-
let bcbs =
137-
expn_tree.iter_node_and_descendants(expn_id).flat_map(|n| n.spans.iter().map(|s| s.bcb));
133+
let minmax_bcbs = node.minmax_bcbs?;
138134

139135
let bcb = match node.expn_kind {
140136
// For bang-macros (e.g. `assert!`, `trace!`) and for `await`, taking
141137
// the "first" BCB in dominator order seems to give good results.
142138
ExpnKind::Macro(MacroKind::Bang, _) | ExpnKind::Desugaring(DesugaringKind::Await) => {
143-
bcbs.min_by(|&a, &b| graph.cmp_in_dominator_order(a, b))?
139+
minmax_bcbs.min
144140
}
145141
// For other kinds of expansion, taking the "last" (most-dominated) BCB
146142
// seems to give good results.
147-
_ => bcbs.max_by(|&a, &b| graph.cmp_in_dominator_order(a, b))?,
143+
_ => minmax_bcbs.max,
148144
};
149145

150146
// For bang-macro expansions, limit the call-site span to just the macro

compiler/rustc_mir_transform/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
#![feature(const_type_name)]
66
#![feature(cow_is_borrowed)]
77
#![feature(file_buffered)]
8-
#![feature(gen_blocks)]
98
#![feature(if_let_guard)]
109
#![feature(impl_trait_in_assoc_type)]
1110
#![feature(try_blocks)]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Function name: pass_through::uses_inner_macro
2+
Raw bytes (51): 0x[01, 01, 01, 01, 05, 09, 01, 24, 01, 00, 16, 01, 01, 08, 00, 1d, 05, 01, 09, 00, 0c, 05, 00, 0d, 00, 21, 05, 01, 09, 00, 15, 05, 01, 09, 00, 0c, 05, 00, 0d, 00, 20, 02, 01, 05, 00, 06, 01, 01, 01, 00, 02]
3+
Number of files: 1
4+
- file 0 => $DIR/pass-through.rs
5+
Number of expressions: 1
6+
- expression 0 operands: lhs = Counter(0), rhs = Counter(1)
7+
Number of file 0 mappings: 9
8+
- Code(Counter(0)) at (prev + 36, 1) to (start + 0, 22)
9+
- Code(Counter(0)) at (prev + 1, 8) to (start + 0, 29)
10+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 12)
11+
- Code(Counter(1)) at (prev + 0, 13) to (start + 0, 33)
12+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 21)
13+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 12)
14+
- Code(Counter(1)) at (prev + 0, 13) to (start + 0, 32)
15+
- Code(Expression(0, Sub)) at (prev + 1, 5) to (start + 0, 6)
16+
= (c0 - c1)
17+
- Code(Counter(0)) at (prev + 1, 1) to (start + 0, 2)
18+
Highest counter ID seen: c1
19+
20+
Function name: pass_through::uses_middle_macro
21+
Raw bytes (51): 0x[01, 01, 01, 01, 05, 09, 01, 2c, 01, 00, 17, 01, 01, 08, 00, 1d, 05, 01, 09, 00, 0c, 05, 00, 0d, 00, 22, 05, 01, 09, 00, 16, 05, 01, 09, 00, 0c, 05, 00, 0d, 00, 21, 02, 01, 05, 00, 06, 01, 01, 01, 00, 02]
22+
Number of files: 1
23+
- file 0 => $DIR/pass-through.rs
24+
Number of expressions: 1
25+
- expression 0 operands: lhs = Counter(0), rhs = Counter(1)
26+
Number of file 0 mappings: 9
27+
- Code(Counter(0)) at (prev + 44, 1) to (start + 0, 23)
28+
- Code(Counter(0)) at (prev + 1, 8) to (start + 0, 29)
29+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 12)
30+
- Code(Counter(1)) at (prev + 0, 13) to (start + 0, 34)
31+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 22)
32+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 12)
33+
- Code(Counter(1)) at (prev + 0, 13) to (start + 0, 33)
34+
- Code(Expression(0, Sub)) at (prev + 1, 5) to (start + 0, 6)
35+
= (c0 - c1)
36+
- Code(Counter(0)) at (prev + 1, 1) to (start + 0, 2)
37+
Highest counter ID seen: c1
38+
39+
Function name: pass_through::uses_outer_macro
40+
Raw bytes (51): 0x[01, 01, 01, 01, 05, 09, 01, 34, 01, 00, 16, 01, 01, 08, 00, 1d, 05, 01, 09, 00, 0c, 05, 00, 0d, 00, 21, 05, 01, 09, 00, 15, 05, 01, 09, 00, 0c, 05, 00, 0d, 00, 20, 02, 01, 05, 00, 06, 01, 01, 01, 00, 02]
41+
Number of files: 1
42+
- file 0 => $DIR/pass-through.rs
43+
Number of expressions: 1
44+
- expression 0 operands: lhs = Counter(0), rhs = Counter(1)
45+
Number of file 0 mappings: 9
46+
- Code(Counter(0)) at (prev + 52, 1) to (start + 0, 22)
47+
- Code(Counter(0)) at (prev + 1, 8) to (start + 0, 29)
48+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 12)
49+
- Code(Counter(1)) at (prev + 0, 13) to (start + 0, 33)
50+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 21)
51+
- Code(Counter(1)) at (prev + 1, 9) to (start + 0, 12)
52+
- Code(Counter(1)) at (prev + 0, 13) to (start + 0, 32)
53+
- Code(Expression(0, Sub)) at (prev + 1, 5) to (start + 0, 6)
54+
= (c0 - c1)
55+
- Code(Counter(0)) at (prev + 1, 1) to (start + 0, 2)
56+
Highest counter ID seen: c1
57+

0 commit comments

Comments
 (0)