411 lines
16 KiB
Rust
411 lines
16 KiB
Rust
use std::collections::BTreeSet;
|
|
|
|
use rustc_data_structures::fx::FxIndexMap;
|
|
use rustc_data_structures::graph::DirectedGraph;
|
|
use rustc_index::IndexVec;
|
|
use rustc_index::bit_set::BitSet;
|
|
use rustc_middle::mir::coverage::{
|
|
BlockMarkerId, BranchSpan, ConditionId, ConditionInfo, CoverageInfoHi, CoverageKind,
|
|
};
|
|
use rustc_middle::mir::{self, BasicBlock, StatementKind};
|
|
use rustc_middle::ty::TyCtxt;
|
|
use rustc_span::Span;
|
|
|
|
use crate::coverage::ExtractedHirInfo;
|
|
use crate::coverage::graph::{BasicCoverageBlock, CoverageGraph, START_BCB};
|
|
use crate::coverage::spans::extract_refined_covspans;
|
|
use crate::coverage::unexpand::unexpand_into_body_span;
|
|
use crate::errors::MCDCExceedsTestVectorLimit;
|
|
|
|
/// Associates an ordinary executable code span with its corresponding BCB.
|
|
#[derive(Debug)]
|
|
pub(super) struct CodeMapping {
|
|
pub(super) span: Span,
|
|
pub(super) bcb: BasicCoverageBlock,
|
|
}
|
|
|
|
/// This is separate from [`MCDCBranch`] to help prepare for larger changes
|
|
/// that will be needed for improved branch coverage in the future.
|
|
/// (See <https://github.com/rust-lang/rust/pull/124217>.)
|
|
#[derive(Debug)]
|
|
pub(super) struct BranchPair {
|
|
pub(super) span: Span,
|
|
pub(super) true_bcb: BasicCoverageBlock,
|
|
pub(super) false_bcb: BasicCoverageBlock,
|
|
}
|
|
|
|
/// Associates an MC/DC branch span with condition info besides fields for normal branch.
|
|
#[derive(Debug)]
|
|
pub(super) struct MCDCBranch {
|
|
pub(super) span: Span,
|
|
pub(super) true_bcb: BasicCoverageBlock,
|
|
pub(super) false_bcb: BasicCoverageBlock,
|
|
pub(super) condition_info: ConditionInfo,
|
|
// Offset added to test vector idx if this branch is evaluated to true.
|
|
pub(super) true_index: usize,
|
|
// Offset added to test vector idx if this branch is evaluated to false.
|
|
pub(super) false_index: usize,
|
|
}
|
|
|
|
/// Associates an MC/DC decision with its join BCBs.
|
|
#[derive(Debug)]
|
|
pub(super) struct MCDCDecision {
|
|
pub(super) span: Span,
|
|
pub(super) end_bcbs: BTreeSet<BasicCoverageBlock>,
|
|
pub(super) bitmap_idx: usize,
|
|
pub(super) num_test_vectors: usize,
|
|
pub(super) decision_depth: u16,
|
|
}
|
|
|
|
// LLVM uses `i32` to index the bitmap. Thus `i32::MAX` is the hard limit for number of all test vectors
|
|
// in a function.
|
|
const MCDC_MAX_BITMAP_SIZE: usize = i32::MAX as usize;
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct ExtractedMappings {
|
|
/// Store our own copy of [`CoverageGraph::num_nodes`], so that we don't
|
|
/// need access to the whole graph when allocating per-BCB data. This is
|
|
/// only public so that other code can still use exhaustive destructuring.
|
|
pub(super) num_bcbs: usize,
|
|
pub(super) code_mappings: Vec<CodeMapping>,
|
|
pub(super) branch_pairs: Vec<BranchPair>,
|
|
pub(super) mcdc_bitmap_bits: usize,
|
|
pub(super) mcdc_degraded_branches: Vec<MCDCBranch>,
|
|
pub(super) mcdc_mappings: Vec<(MCDCDecision, Vec<MCDCBranch>)>,
|
|
}
|
|
|
|
/// Extracts coverage-relevant spans from MIR, and associates them with
|
|
/// their corresponding BCBs.
|
|
pub(super) fn extract_all_mapping_info_from_mir<'tcx>(
|
|
tcx: TyCtxt<'tcx>,
|
|
mir_body: &mir::Body<'tcx>,
|
|
hir_info: &ExtractedHirInfo,
|
|
graph: &CoverageGraph,
|
|
) -> ExtractedMappings {
|
|
let mut code_mappings = vec![];
|
|
let mut branch_pairs = vec![];
|
|
let mut mcdc_bitmap_bits = 0;
|
|
let mut mcdc_degraded_branches = vec![];
|
|
let mut mcdc_mappings = vec![];
|
|
|
|
if hir_info.is_async_fn || tcx.sess.coverage_no_mir_spans() {
|
|
// An async function desugars into a function that returns a future,
|
|
// with the user code wrapped in a closure. Any spans in the desugared
|
|
// outer function will be unhelpful, so just keep the signature span
|
|
// and ignore all of the spans in the MIR body.
|
|
//
|
|
// When debugging flag `-Zcoverage-options=no-mir-spans` is set, we need
|
|
// to give the same treatment to _all_ functions, because `llvm-cov`
|
|
// seems to ignore functions that don't have any ordinary code spans.
|
|
if let Some(span) = hir_info.fn_sig_span_extended {
|
|
code_mappings.push(CodeMapping { span, bcb: START_BCB });
|
|
}
|
|
} else {
|
|
// Extract coverage spans from MIR statements/terminators as normal.
|
|
extract_refined_covspans(mir_body, hir_info, graph, &mut code_mappings);
|
|
}
|
|
|
|
branch_pairs.extend(extract_branch_pairs(mir_body, hir_info, graph));
|
|
|
|
extract_mcdc_mappings(
|
|
mir_body,
|
|
tcx,
|
|
hir_info.body_span,
|
|
graph,
|
|
&mut mcdc_bitmap_bits,
|
|
&mut mcdc_degraded_branches,
|
|
&mut mcdc_mappings,
|
|
);
|
|
|
|
ExtractedMappings {
|
|
num_bcbs: graph.num_nodes(),
|
|
code_mappings,
|
|
branch_pairs,
|
|
mcdc_bitmap_bits,
|
|
mcdc_degraded_branches,
|
|
mcdc_mappings,
|
|
}
|
|
}
|
|
|
|
impl ExtractedMappings {
|
|
pub(super) fn all_bcbs_with_counter_mappings(&self) -> BitSet<BasicCoverageBlock> {
|
|
// Fully destructure self to make sure we don't miss any fields that have mappings.
|
|
let Self {
|
|
num_bcbs,
|
|
code_mappings,
|
|
branch_pairs,
|
|
mcdc_bitmap_bits: _,
|
|
mcdc_degraded_branches,
|
|
mcdc_mappings,
|
|
} = self;
|
|
|
|
// Identify which BCBs have one or more mappings.
|
|
let mut bcbs_with_counter_mappings = BitSet::new_empty(*num_bcbs);
|
|
let mut insert = |bcb| {
|
|
bcbs_with_counter_mappings.insert(bcb);
|
|
};
|
|
|
|
for &CodeMapping { span: _, bcb } in code_mappings {
|
|
insert(bcb);
|
|
}
|
|
for &BranchPair { true_bcb, false_bcb, .. } in branch_pairs {
|
|
insert(true_bcb);
|
|
insert(false_bcb);
|
|
}
|
|
for &MCDCBranch { true_bcb, false_bcb, .. } in mcdc_degraded_branches
|
|
.iter()
|
|
.chain(mcdc_mappings.iter().map(|(_, branches)| branches.into_iter()).flatten())
|
|
{
|
|
insert(true_bcb);
|
|
insert(false_bcb);
|
|
}
|
|
|
|
// MC/DC decisions refer to BCBs, but don't require those BCBs to have counters.
|
|
if bcbs_with_counter_mappings.is_empty() {
|
|
debug_assert!(
|
|
mcdc_mappings.is_empty(),
|
|
"A function with no counter mappings shouldn't have any decisions: {mcdc_mappings:?}",
|
|
);
|
|
}
|
|
|
|
bcbs_with_counter_mappings
|
|
}
|
|
|
|
/// Returns the set of BCBs that have one or more `Code` mappings.
|
|
pub(super) fn bcbs_with_ordinary_code_mappings(&self) -> BitSet<BasicCoverageBlock> {
|
|
let mut bcbs = BitSet::new_empty(self.num_bcbs);
|
|
for &CodeMapping { span: _, bcb } in &self.code_mappings {
|
|
bcbs.insert(bcb);
|
|
}
|
|
bcbs
|
|
}
|
|
}
|
|
|
|
fn resolve_block_markers(
|
|
coverage_info_hi: &CoverageInfoHi,
|
|
mir_body: &mir::Body<'_>,
|
|
) -> IndexVec<BlockMarkerId, Option<BasicBlock>> {
|
|
let mut block_markers = IndexVec::<BlockMarkerId, Option<BasicBlock>>::from_elem_n(
|
|
None,
|
|
coverage_info_hi.num_block_markers,
|
|
);
|
|
|
|
// Fill out the mapping from block marker IDs to their enclosing blocks.
|
|
for (bb, data) in mir_body.basic_blocks.iter_enumerated() {
|
|
for statement in &data.statements {
|
|
if let StatementKind::Coverage(CoverageKind::BlockMarker { id }) = statement.kind {
|
|
block_markers[id] = Some(bb);
|
|
}
|
|
}
|
|
}
|
|
|
|
block_markers
|
|
}
|
|
|
|
// FIXME: There is currently a lot of redundancy between
|
|
// `extract_branch_pairs` and `extract_mcdc_mappings`. This is needed so
|
|
// that they can each be modified without interfering with the other, but in
|
|
// the long term we should try to bring them together again when branch coverage
|
|
// and MC/DC coverage support are more mature.
|
|
|
|
pub(super) fn extract_branch_pairs(
|
|
mir_body: &mir::Body<'_>,
|
|
hir_info: &ExtractedHirInfo,
|
|
graph: &CoverageGraph,
|
|
) -> Vec<BranchPair> {
|
|
let Some(coverage_info_hi) = mir_body.coverage_info_hi.as_deref() else { return vec![] };
|
|
|
|
let block_markers = resolve_block_markers(coverage_info_hi, mir_body);
|
|
|
|
coverage_info_hi
|
|
.branch_spans
|
|
.iter()
|
|
.filter_map(|&BranchSpan { span: raw_span, true_marker, false_marker }| {
|
|
// For now, ignore any branch span that was introduced by
|
|
// expansion. This makes things like assert macros less noisy.
|
|
if !raw_span.ctxt().outer_expn_data().is_root() {
|
|
return None;
|
|
}
|
|
let span = unexpand_into_body_span(raw_span, hir_info.body_span)?;
|
|
|
|
let bcb_from_marker = |marker: BlockMarkerId| graph.bcb_from_bb(block_markers[marker]?);
|
|
|
|
let true_bcb = bcb_from_marker(true_marker)?;
|
|
let false_bcb = bcb_from_marker(false_marker)?;
|
|
|
|
Some(BranchPair { span, true_bcb, false_bcb })
|
|
})
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
pub(super) fn extract_mcdc_mappings(
|
|
mir_body: &mir::Body<'_>,
|
|
tcx: TyCtxt<'_>,
|
|
body_span: Span,
|
|
graph: &CoverageGraph,
|
|
mcdc_bitmap_bits: &mut usize,
|
|
mcdc_degraded_branches: &mut impl Extend<MCDCBranch>,
|
|
mcdc_mappings: &mut impl Extend<(MCDCDecision, Vec<MCDCBranch>)>,
|
|
) {
|
|
let Some(coverage_info_hi) = mir_body.coverage_info_hi.as_deref() else { return };
|
|
|
|
let block_markers = resolve_block_markers(coverage_info_hi, mir_body);
|
|
|
|
let bcb_from_marker = |marker: BlockMarkerId| graph.bcb_from_bb(block_markers[marker]?);
|
|
|
|
let check_branch_bcb =
|
|
|raw_span: Span, true_marker: BlockMarkerId, false_marker: BlockMarkerId| {
|
|
// For now, ignore any branch span that was introduced by
|
|
// expansion. This makes things like assert macros less noisy.
|
|
if !raw_span.ctxt().outer_expn_data().is_root() {
|
|
return None;
|
|
}
|
|
let span = unexpand_into_body_span(raw_span, body_span)?;
|
|
|
|
let true_bcb = bcb_from_marker(true_marker)?;
|
|
let false_bcb = bcb_from_marker(false_marker)?;
|
|
Some((span, true_bcb, false_bcb))
|
|
};
|
|
|
|
let to_mcdc_branch = |&mir::coverage::MCDCBranchSpan {
|
|
span: raw_span,
|
|
condition_info,
|
|
true_marker,
|
|
false_marker,
|
|
}| {
|
|
let (span, true_bcb, false_bcb) = check_branch_bcb(raw_span, true_marker, false_marker)?;
|
|
Some(MCDCBranch {
|
|
span,
|
|
true_bcb,
|
|
false_bcb,
|
|
condition_info,
|
|
true_index: usize::MAX,
|
|
false_index: usize::MAX,
|
|
})
|
|
};
|
|
|
|
let mut get_bitmap_idx = |num_test_vectors: usize| -> Option<usize> {
|
|
let bitmap_idx = *mcdc_bitmap_bits;
|
|
let next_bitmap_bits = bitmap_idx.saturating_add(num_test_vectors);
|
|
(next_bitmap_bits <= MCDC_MAX_BITMAP_SIZE).then(|| {
|
|
*mcdc_bitmap_bits = next_bitmap_bits;
|
|
bitmap_idx
|
|
})
|
|
};
|
|
mcdc_degraded_branches
|
|
.extend(coverage_info_hi.mcdc_degraded_branch_spans.iter().filter_map(to_mcdc_branch));
|
|
|
|
mcdc_mappings.extend(coverage_info_hi.mcdc_spans.iter().filter_map(|(decision, branches)| {
|
|
if branches.len() == 0 {
|
|
return None;
|
|
}
|
|
let decision_span = unexpand_into_body_span(decision.span, body_span)?;
|
|
|
|
let end_bcbs = decision
|
|
.end_markers
|
|
.iter()
|
|
.map(|&marker| bcb_from_marker(marker))
|
|
.collect::<Option<_>>()?;
|
|
let mut branch_mappings: Vec<_> = branches.into_iter().filter_map(to_mcdc_branch).collect();
|
|
if branch_mappings.len() != branches.len() {
|
|
mcdc_degraded_branches.extend(branch_mappings);
|
|
return None;
|
|
}
|
|
let num_test_vectors = calc_test_vectors_index(&mut branch_mappings);
|
|
let Some(bitmap_idx) = get_bitmap_idx(num_test_vectors) else {
|
|
tcx.dcx().emit_warn(MCDCExceedsTestVectorLimit {
|
|
span: decision_span,
|
|
max_num_test_vectors: MCDC_MAX_BITMAP_SIZE,
|
|
});
|
|
mcdc_degraded_branches.extend(branch_mappings);
|
|
return None;
|
|
};
|
|
// LLVM requires span of the decision contains all spans of its conditions.
|
|
// Usually the decision span meets the requirement well but in cases like macros it may not.
|
|
let span = branch_mappings
|
|
.iter()
|
|
.map(|branch| branch.span)
|
|
.reduce(|lhs, rhs| lhs.to(rhs))
|
|
.map(
|
|
|joint_span| {
|
|
if decision_span.contains(joint_span) { decision_span } else { joint_span }
|
|
},
|
|
)
|
|
.expect("branch mappings are ensured to be non-empty as checked above");
|
|
Some((
|
|
MCDCDecision {
|
|
span,
|
|
end_bcbs,
|
|
bitmap_idx,
|
|
num_test_vectors,
|
|
decision_depth: decision.decision_depth,
|
|
},
|
|
branch_mappings,
|
|
))
|
|
}));
|
|
}
|
|
|
|
// LLVM checks the executed test vector by accumulating indices of tested branches.
|
|
// We calculate number of all possible test vectors of the decision and assign indices
|
|
// to branches here.
|
|
// See [the rfc](https://discourse.llvm.org/t/rfc-coverage-new-algorithm-and-file-format-for-mc-dc/76798/)
|
|
// for more details about the algorithm.
|
|
// This function is mostly like [`TVIdxBuilder::TvIdxBuilder`](https://github.com/llvm/llvm-project/blob/d594d9f7f4dc6eb748b3261917db689fdc348b96/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp#L226)
|
|
fn calc_test_vectors_index(conditions: &mut Vec<MCDCBranch>) -> usize {
|
|
let mut indegree_stats = IndexVec::<ConditionId, usize>::from_elem_n(0, conditions.len());
|
|
// `num_paths` is `width` described at the llvm rfc, which indicates how many paths reaching the condition node.
|
|
let mut num_paths_stats = IndexVec::<ConditionId, usize>::from_elem_n(0, conditions.len());
|
|
let mut next_conditions = conditions
|
|
.iter_mut()
|
|
.map(|branch| {
|
|
let ConditionInfo { condition_id, true_next_id, false_next_id } = branch.condition_info;
|
|
[true_next_id, false_next_id]
|
|
.into_iter()
|
|
.flatten()
|
|
.for_each(|next_id| indegree_stats[next_id] += 1);
|
|
(condition_id, branch)
|
|
})
|
|
.collect::<FxIndexMap<_, _>>();
|
|
|
|
let mut queue =
|
|
std::collections::VecDeque::from_iter(next_conditions.swap_remove(&ConditionId::START));
|
|
num_paths_stats[ConditionId::START] = 1;
|
|
let mut decision_end_nodes = Vec::new();
|
|
while let Some(branch) = queue.pop_front() {
|
|
let ConditionInfo { condition_id, true_next_id, false_next_id } = branch.condition_info;
|
|
let (false_index, true_index) = (&mut branch.false_index, &mut branch.true_index);
|
|
let this_paths_count = num_paths_stats[condition_id];
|
|
// Note. First check the false next to ensure conditions are touched in same order with llvm-cov.
|
|
for (next, index) in [(false_next_id, false_index), (true_next_id, true_index)] {
|
|
if let Some(next_id) = next {
|
|
let next_paths_count = &mut num_paths_stats[next_id];
|
|
*index = *next_paths_count;
|
|
*next_paths_count = next_paths_count.saturating_add(this_paths_count);
|
|
let next_indegree = &mut indegree_stats[next_id];
|
|
*next_indegree -= 1;
|
|
if *next_indegree == 0 {
|
|
queue.push_back(next_conditions.swap_remove(&next_id).expect(
|
|
"conditions with non-zero indegree before must be in next_conditions",
|
|
));
|
|
}
|
|
} else {
|
|
decision_end_nodes.push((this_paths_count, condition_id, index));
|
|
}
|
|
}
|
|
}
|
|
assert!(next_conditions.is_empty(), "the decision tree has untouched nodes");
|
|
let mut cur_idx = 0;
|
|
// LLVM hopes the end nodes are sorted in descending order by `num_paths` so that it can
|
|
// optimize bitmap size for decisions in tree form such as `a && b && c && d && ...`.
|
|
decision_end_nodes.sort_by_key(|(num_paths, _, _)| usize::MAX - *num_paths);
|
|
for (num_paths, condition_id, index) in decision_end_nodes {
|
|
assert_eq!(
|
|
num_paths, num_paths_stats[condition_id],
|
|
"end nodes should not be updated since they were visited"
|
|
);
|
|
assert_eq!(*index, usize::MAX, "end nodes should not be assigned index before");
|
|
*index = cur_idx;
|
|
cur_idx += num_paths;
|
|
}
|
|
cur_idx
|
|
}
|