blob: 77aec902faad5795e9c48457ecdd2802fd65b4e2 [file] [log] [blame]
use itertools::Itertools;
use rustc_data_structures::fx::{FxIndexMap, FxIndexSet, IndexEntry};
use rustc_middle::mir;
use rustc_middle::mir::coverage::{BasicCoverageBlock, BranchSpan};
use rustc_span::{ExpnId, ExpnKind, Span};
use crate::coverage::from_mir;
use crate::coverage::graph::CoverageGraph;
use crate::coverage::hir_info::ExtractedHirInfo;
use crate::coverage::mappings::MappingsError;
#[derive(Clone, Copy, Debug)]
pub(crate) struct SpanWithBcb {
pub(crate) span: Span,
pub(crate) bcb: BasicCoverageBlock,
}
#[derive(Debug)]
pub(crate) struct ExpnTree {
nodes: FxIndexMap<ExpnId, ExpnNode>,
}
impl ExpnTree {
pub(crate) fn get(&self, expn_id: ExpnId) -> Option<&ExpnNode> {
self.nodes.get(&expn_id)
}
}
#[derive(Debug)]
pub(crate) struct ExpnNode {
/// Storing the expansion ID in its own node is not strictly necessary,
/// but is helpful for debugging and might be useful later.
#[expect(dead_code)]
pub(crate) expn_id: ExpnId,
/// Index of this node in a depth-first traversal from the root.
pub(crate) dfs_rank: usize,
// Useful info extracted from `ExpnData`.
pub(crate) expn_kind: ExpnKind,
/// Non-dummy `ExpnData::call_site` span.
pub(crate) call_site: Option<Span>,
/// Expansion ID of `call_site`, if present.
/// This links an expansion node to its parent in the tree.
pub(crate) call_site_expn_id: Option<ExpnId>,
/// Holds the function signature span, if it belongs to this expansion.
/// Used by special-case code in span refinement.
pub(crate) fn_sig_span: Option<Span>,
/// Holds the function body span, if it belongs to this expansion.
/// Used by special-case code in span refinement.
pub(crate) body_span: Option<Span>,
/// Spans (and their associated BCBs) belonging to this expansion.
pub(crate) spans: Vec<SpanWithBcb>,
/// Expansions whose call-site is in this expansion.
pub(crate) child_expn_ids: FxIndexSet<ExpnId>,
/// The "minimum" and "maximum" BCBs (in dominator order) of ordinary spans
/// belonging to this tree node and all of its descendants. Used when
/// creating a single code mapping representing an entire child expansion.
pub(crate) minmax_bcbs: Option<MinMaxBcbs>,
/// Branch spans (recorded during MIR building) belonging to this expansion.
pub(crate) branch_spans: Vec<BranchSpan>,
/// Hole spans belonging to this expansion, to be carved out from the
/// code spans during span refinement.
pub(crate) hole_spans: Vec<Span>,
}
impl ExpnNode {
fn new(expn_id: ExpnId) -> Self {
let expn_data = expn_id.expn_data();
let call_site = Some(expn_data.call_site).filter(|sp| !sp.is_dummy());
let call_site_expn_id = try { call_site?.ctxt().outer_expn() };
Self {
expn_id,
dfs_rank: usize::MAX,
expn_kind: expn_data.kind,
call_site,
call_site_expn_id,
fn_sig_span: None,
body_span: None,
spans: vec![],
child_expn_ids: FxIndexSet::default(),
minmax_bcbs: None,
branch_spans: vec![],
hole_spans: vec![],
}
}
}
/// Extracts raw span/BCB pairs from potentially-different syntax contexts, and
/// arranges them into an "expansion tree" based on their expansion call-sites.
pub(crate) fn build_expn_tree(
mir_body: &mir::Body<'_>,
hir_info: &ExtractedHirInfo,
graph: &CoverageGraph,
) -> Result<ExpnTree, MappingsError> {
let raw_spans = from_mir::extract_raw_spans_from_mir(mir_body, graph);
let mut nodes = FxIndexMap::default();
let new_node = |&expn_id: &ExpnId| ExpnNode::new(expn_id);
for from_mir::RawSpanFromMir { raw_span, bcb } in raw_spans {
let span_with_bcb = SpanWithBcb { span: raw_span, bcb };
// Create a node for this span's enclosing expansion, and add the span to it.
let expn_id = span_with_bcb.span.ctxt().outer_expn();
let node = nodes.entry(expn_id).or_insert_with_key(new_node);
node.spans.push(span_with_bcb);
// Now walk up the expansion call-site chain, creating nodes and registering children.
let mut prev = expn_id;
let mut curr_expn_id = node.call_site_expn_id;
while let Some(expn_id) = curr_expn_id {
let entry = nodes.entry(expn_id);
let node_existed = matches!(entry, IndexEntry::Occupied(_));
let node = entry.or_insert_with_key(new_node);
node.child_expn_ids.insert(prev);
if node_existed {
break;
}
prev = expn_id;
curr_expn_id = node.call_site_expn_id;
}
}
// Sort the tree nodes into depth-first order.
sort_nodes_depth_first(&mut nodes)?;
// For each node, determine its "minimum" and "maximum" BCBs, based on its
// own spans and its immediate children. This relies on the nodes having
// been sorted, so that each node's children are processed before the node
// itself.
for i in (0..nodes.len()).rev() {
// Computing a node's min/max BCBs requires a shared ref to other nodes.
let minmax_bcbs = minmax_bcbs_for_expn_tree_node(graph, &nodes, &nodes[i]);
// Now we can mutate the current node to set its min/max BCBs.
nodes[i].minmax_bcbs = minmax_bcbs;
}
// If we have a span for the function signature, associate it with the
// corresponding expansion tree node.
if let Some(fn_sig_span) = hir_info.fn_sig_span
&& let Some(node) = nodes.get_mut(&fn_sig_span.ctxt().outer_expn())
{
node.fn_sig_span = Some(fn_sig_span);
}
// Also associate the body span with its expansion tree node.
let body_span = hir_info.body_span;
if let Some(node) = nodes.get_mut(&body_span.ctxt().outer_expn()) {
node.body_span = Some(body_span);
}
// Associate each hole span (extracted from HIR) with its corresponding
// expansion tree node.
for &hole_span in &hir_info.hole_spans {
let expn_id = hole_span.ctxt().outer_expn();
let Some(node) = nodes.get_mut(&expn_id) else { continue };
node.hole_spans.push(hole_span);
}
// Associate each branch span (recorded during MIR building) with its
// corresponding expansion tree node.
if let Some(coverage_info_hi) = mir_body.coverage_info_hi.as_deref() {
for branch_span in &coverage_info_hi.branch_spans {
if let Some(node) = nodes.get_mut(&branch_span.span.ctxt().outer_expn()) {
node.branch_spans.push(BranchSpan::clone(branch_span));
}
}
}
Ok(ExpnTree { nodes })
}
/// Sorts the tree nodes in the map into depth-first order.
///
/// This allows subsequent operations to iterate over all nodes, while assuming
/// that every node occurs before all of its descendants.
fn sort_nodes_depth_first(nodes: &mut FxIndexMap<ExpnId, ExpnNode>) -> Result<(), MappingsError> {
let mut dfs_stack = vec![ExpnId::root()];
let mut next_dfs_rank = 0usize;
while let Some(expn_id) = dfs_stack.pop() {
if let Some(node) = nodes.get_mut(&expn_id) {
node.dfs_rank = next_dfs_rank;
next_dfs_rank += 1;
dfs_stack.extend(node.child_expn_ids.iter().rev().copied());
}
}
nodes.sort_by_key(|_expn_id, node| node.dfs_rank);
// Verify that the depth-first search visited each node exactly once.
for (i, &ExpnNode { dfs_rank, .. }) in nodes.values().enumerate() {
if dfs_rank != i {
tracing::debug!(dfs_rank, i, "expansion tree node's rank does not match its index");
return Err(MappingsError::TreeSortFailure);
}
}
Ok(())
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct MinMaxBcbs {
pub(crate) min: BasicCoverageBlock,
pub(crate) max: BasicCoverageBlock,
}
/// For a single node in the expansion tree, compute its "minimum" and "maximum"
/// BCBs (in dominator order), from among the BCBs of its immediate spans,
/// and the min/max of its immediate children.
fn minmax_bcbs_for_expn_tree_node(
graph: &CoverageGraph,
nodes: &FxIndexMap<ExpnId, ExpnNode>,
node: &ExpnNode,
) -> Option<MinMaxBcbs> {
let immediate_span_bcbs = node.spans.iter().map(|sp: &SpanWithBcb| sp.bcb);
let child_minmax_bcbs = node
.child_expn_ids
.iter()
.flat_map(|id| nodes.get(id))
.flat_map(|child| child.minmax_bcbs)
.flat_map(|MinMaxBcbs { min, max }| [min, max]);
let (min, max) = Iterator::chain(immediate_span_bcbs, child_minmax_bcbs)
.minmax_by(|&a, &b| graph.cmp_in_dominator_order(a, b))
.into_option()?;
Some(MinMaxBcbs { min, max })
}