diff options
Diffstat (limited to 'compiler/rustc_mir_build/src/builder/coverageinfo.rs')
| -rw-r--r-- | compiler/rustc_mir_build/src/builder/coverageinfo.rs | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/compiler/rustc_mir_build/src/builder/coverageinfo.rs b/compiler/rustc_mir_build/src/builder/coverageinfo.rs new file mode 100644 index 00000000000..a80bd4f3c80 --- /dev/null +++ b/compiler/rustc_mir_build/src/builder/coverageinfo.rs @@ -0,0 +1,310 @@ +use std::assert_matches::assert_matches; +use std::collections::hash_map::Entry; + +use rustc_data_structures::fx::FxHashMap; +use rustc_middle::mir::coverage::{BlockMarkerId, BranchSpan, CoverageInfoHi, CoverageKind}; +use rustc_middle::mir::{self, BasicBlock, SourceInfo, UnOp}; +use rustc_middle::thir::{ExprId, ExprKind, Pat, Thir}; +use rustc_middle::ty::TyCtxt; +use rustc_span::def_id::LocalDefId; + +use crate::builder::coverageinfo::mcdc::MCDCInfoBuilder; +use crate::builder::{Builder, CFG}; + +mod mcdc; + +/// Collects coverage-related information during MIR building, to eventually be +/// turned into a function's [`CoverageInfoHi`] when MIR building is complete. +pub(crate) struct CoverageInfoBuilder { + /// Maps condition expressions to their enclosing `!`, for better instrumentation. + nots: FxHashMap<ExprId, NotInfo>, + + markers: BlockMarkerGen, + + /// Present if branch coverage is enabled. + branch_info: Option<BranchInfo>, + /// Present if MC/DC coverage is enabled. + mcdc_info: Option<MCDCInfoBuilder>, +} + +#[derive(Default)] +struct BranchInfo { + branch_spans: Vec<BranchSpan>, +} + +#[derive(Clone, Copy)] +struct NotInfo { + /// When visiting the associated expression as a branch condition, treat this + /// enclosing `!` as the branch condition instead. + enclosing_not: ExprId, + /// True if the associated expression is nested within an odd number of `!` + /// expressions relative to `enclosing_not` (inclusive of `enclosing_not`). + is_flipped: bool, +} + +#[derive(Default)] +struct BlockMarkerGen { + num_block_markers: usize, +} + +impl BlockMarkerGen { + fn next_block_marker_id(&mut self) -> BlockMarkerId { + let id = BlockMarkerId::from_usize(self.num_block_markers); + self.num_block_markers += 1; + id + } + + fn inject_block_marker( + &mut self, + cfg: &mut CFG<'_>, + source_info: SourceInfo, + block: BasicBlock, + ) -> BlockMarkerId { + let id = self.next_block_marker_id(); + let marker_statement = mir::Statement { + source_info, + kind: mir::StatementKind::Coverage(CoverageKind::BlockMarker { id }), + }; + cfg.push(block, marker_statement); + + id + } +} + +impl CoverageInfoBuilder { + /// Creates a new coverage info builder, but only if coverage instrumentation + /// is enabled and `def_id` represents a function that is eligible for coverage. + pub(crate) fn new_if_enabled(tcx: TyCtxt<'_>, def_id: LocalDefId) -> Option<Self> { + if !tcx.sess.instrument_coverage() || !tcx.is_eligible_for_coverage(def_id) { + return None; + } + + Some(Self { + nots: FxHashMap::default(), + markers: BlockMarkerGen::default(), + branch_info: tcx.sess.instrument_coverage_branch().then(BranchInfo::default), + mcdc_info: tcx.sess.instrument_coverage_mcdc().then(MCDCInfoBuilder::new), + }) + } + + /// Unary `!` expressions inside an `if` condition are lowered by lowering + /// their argument instead, and then reversing the then/else arms of that `if`. + /// + /// That's awkward for branch coverage instrumentation, so to work around that + /// we pre-emptively visit any affected `!` expressions, and record extra + /// information that [`Builder::visit_coverage_branch_condition`] can use to + /// synthesize branch instrumentation for the enclosing `!`. + pub(crate) fn visit_unary_not(&mut self, thir: &Thir<'_>, unary_not: ExprId) { + assert_matches!(thir[unary_not].kind, ExprKind::Unary { op: UnOp::Not, .. }); + + // The information collected by this visitor is only needed when branch + // coverage or higher is enabled. + if self.branch_info.is_none() { + return; + } + + self.visit_with_not_info( + thir, + unary_not, + // Set `is_flipped: false` for the `!` itself, so that its enclosed + // expression will have `is_flipped: true`. + NotInfo { enclosing_not: unary_not, is_flipped: false }, + ); + } + + fn visit_with_not_info(&mut self, thir: &Thir<'_>, expr_id: ExprId, not_info: NotInfo) { + match self.nots.entry(expr_id) { + // This expression has already been marked by an enclosing `!`. + Entry::Occupied(_) => return, + Entry::Vacant(entry) => entry.insert(not_info), + }; + + match thir[expr_id].kind { + ExprKind::Unary { op: UnOp::Not, arg } => { + // Invert the `is_flipped` flag for the contents of this `!`. + let not_info = NotInfo { is_flipped: !not_info.is_flipped, ..not_info }; + self.visit_with_not_info(thir, arg, not_info); + } + ExprKind::Scope { value, .. } => self.visit_with_not_info(thir, value, not_info), + ExprKind::Use { source } => self.visit_with_not_info(thir, source, not_info), + // All other expressions (including `&&` and `||`) don't need any + // special handling of their contents, so stop visiting. + _ => {} + } + } + + fn register_two_way_branch<'tcx>( + &mut self, + tcx: TyCtxt<'tcx>, + cfg: &mut CFG<'tcx>, + source_info: SourceInfo, + true_block: BasicBlock, + false_block: BasicBlock, + ) { + // Separate path for handling branches when MC/DC is enabled. + if let Some(mcdc_info) = self.mcdc_info.as_mut() { + let inject_block_marker = + |source_info, block| self.markers.inject_block_marker(cfg, source_info, block); + mcdc_info.visit_evaluated_condition( + tcx, + source_info, + true_block, + false_block, + inject_block_marker, + ); + return; + } + + // Bail out if branch coverage is not enabled. + let Some(branch_info) = self.branch_info.as_mut() else { return }; + + let true_marker = self.markers.inject_block_marker(cfg, source_info, true_block); + let false_marker = self.markers.inject_block_marker(cfg, source_info, false_block); + + branch_info.branch_spans.push(BranchSpan { + span: source_info.span, + true_marker, + false_marker, + }); + } + + pub(crate) fn into_done(self) -> Box<CoverageInfoHi> { + let Self { nots: _, markers: BlockMarkerGen { num_block_markers }, branch_info, mcdc_info } = + self; + + let branch_spans = + branch_info.map(|branch_info| branch_info.branch_spans).unwrap_or_default(); + + let (mcdc_spans, mcdc_degraded_branch_spans) = + mcdc_info.map(MCDCInfoBuilder::into_done).unwrap_or_default(); + + // For simplicity, always return an info struct (without Option), even + // if there's nothing interesting in it. + Box::new(CoverageInfoHi { + num_block_markers, + branch_spans, + mcdc_degraded_branch_spans, + mcdc_spans, + }) + } +} + +impl<'tcx> Builder<'_, 'tcx> { + /// If condition coverage is enabled, inject extra blocks and marker statements + /// that will let us track the value of the condition in `place`. + pub(crate) fn visit_coverage_standalone_condition( + &mut self, + mut expr_id: ExprId, // Expression giving the span of the condition + place: mir::Place<'tcx>, // Already holds the boolean condition value + block: &mut BasicBlock, + ) { + // Bail out if condition coverage is not enabled for this function. + let Some(coverage_info) = self.coverage_info.as_mut() else { return }; + if !self.tcx.sess.instrument_coverage_condition() { + return; + }; + + // Remove any wrappers, so that we can inspect the real underlying expression. + while let ExprKind::Use { source: inner } | ExprKind::Scope { value: inner, .. } = + self.thir[expr_id].kind + { + expr_id = inner; + } + // If the expression is a lazy logical op, it will naturally get branch + // coverage as part of its normal lowering, so we can disregard it here. + if let ExprKind::LogicalOp { .. } = self.thir[expr_id].kind { + return; + } + + let source_info = SourceInfo { span: self.thir[expr_id].span, scope: self.source_scope }; + + // Using the boolean value that has already been stored in `place`, set up + // control flow in the shape of a diamond, so that we can place separate + // marker statements in the true and false blocks. The coverage MIR pass + // will use those markers to inject coverage counters as appropriate. + // + // block + // / \ + // true_block false_block + // (marker) (marker) + // \ / + // join_block + + let true_block = self.cfg.start_new_block(); + let false_block = self.cfg.start_new_block(); + self.cfg.terminate( + *block, + source_info, + mir::TerminatorKind::if_(mir::Operand::Copy(place), true_block, false_block), + ); + + // Separate path for handling branches when MC/DC is enabled. + coverage_info.register_two_way_branch( + self.tcx, + &mut self.cfg, + source_info, + true_block, + false_block, + ); + + let join_block = self.cfg.start_new_block(); + self.cfg.goto(true_block, source_info, join_block); + self.cfg.goto(false_block, source_info, join_block); + // Any subsequent codegen in the caller should use the new join block. + *block = join_block; + } + + /// If branch coverage is enabled, inject marker statements into `then_block` + /// and `else_block`, and record their IDs in the table of branch spans. + pub(crate) fn visit_coverage_branch_condition( + &mut self, + mut expr_id: ExprId, + mut then_block: BasicBlock, + mut else_block: BasicBlock, + ) { + // Bail out if coverage is not enabled for this function. + let Some(coverage_info) = self.coverage_info.as_mut() else { return }; + + // If this condition expression is nested within one or more `!` expressions, + // replace it with the enclosing `!` collected by `visit_unary_not`. + if let Some(&NotInfo { enclosing_not, is_flipped }) = coverage_info.nots.get(&expr_id) { + expr_id = enclosing_not; + if is_flipped { + std::mem::swap(&mut then_block, &mut else_block); + } + } + + let source_info = SourceInfo { span: self.thir[expr_id].span, scope: self.source_scope }; + + coverage_info.register_two_way_branch( + self.tcx, + &mut self.cfg, + source_info, + then_block, + else_block, + ); + } + + /// If branch coverage is enabled, inject marker statements into `true_block` + /// and `false_block`, and record their IDs in the table of branches. + /// + /// Used to instrument let-else and if-let (including let-chains) for branch coverage. + pub(crate) fn visit_coverage_conditional_let( + &mut self, + pattern: &Pat<'tcx>, // Pattern that has been matched when the true path is taken + true_block: BasicBlock, + false_block: BasicBlock, + ) { + // Bail out if coverage is not enabled for this function. + let Some(coverage_info) = self.coverage_info.as_mut() else { return }; + + let source_info = SourceInfo { span: pattern.span, scope: self.source_scope }; + coverage_info.register_two_way_branch( + self.tcx, + &mut self.cfg, + source_info, + true_block, + false_block, + ); + } +} |
