Skip to content

Commit d8eed98

Browse files
committed
coverage: Record branch information during MIR building
1 parent 692549b commit d8eed98

File tree

2 files changed

+132
-4
lines changed

2 files changed

+132
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
1-
use rustc_middle::mir;
2-
use rustc_middle::mir::coverage::BranchSpan;
1+
use std::assert_matches::assert_matches;
2+
use std::collections::hash_map::Entry;
3+
4+
use rustc_data_structures::fx::FxHashMap;
5+
use rustc_middle::mir::coverage::{BlockMarkerId, BranchSpan, CoverageKind};
6+
use rustc_middle::mir::{self, BasicBlock, UnOp};
7+
use rustc_middle::thir::{ExprId, ExprKind, Thir};
38
use rustc_middle::ty::TyCtxt;
49
use rustc_span::def_id::LocalDefId;
510

11+
use crate::build::Builder;
12+
613
pub(crate) struct HirBranchInfoBuilder {
14+
/// Maps condition expressions to their enclosing `!`, for better instrumentation.
15+
inversions: FxHashMap<ExprId, Inversion>,
16+
717
num_block_markers: usize,
818
branch_spans: Vec<BranchSpan>,
919
}
1020

21+
#[derive(Clone, Copy)]
22+
struct Inversion {
23+
/// When visiting the associated expression as a branch condition, treat this
24+
/// enclosing `!` as the branch condition instead.
25+
enclosing_not: ExprId,
26+
/// True if the associated expression is nested within an odd number of `!`
27+
/// expressions relative to `enclosing_not` (inclusive of `enclosing_not`).
28+
is_inverted: bool,
29+
}
30+
1131
impl HirBranchInfoBuilder {
1232
/// Creates a new branch info builder, but only if branch coverage instrumentation
1333
/// is enabled and `def_id` represents a function that is eligible for coverage.
1434
pub(crate) fn new_if_enabled(tcx: TyCtxt<'_>, def_id: LocalDefId) -> Option<Self> {
1535
if tcx.sess.instrument_coverage_branch() && tcx.is_eligible_for_coverage(def_id) {
1636
Some(Self {
17-
// (placeholder)
37+
inversions: FxHashMap::default(),
1838
num_block_markers: 0,
1939
branch_spans: vec![],
2040
})
@@ -23,8 +43,54 @@ impl HirBranchInfoBuilder {
2343
}
2444
}
2545

46+
/// Unary `!` expressions inside an `if` condition are lowered by lowering
47+
/// their argument instead, and then reversing the then/else arms of that `if`.
48+
///
49+
/// That's awkward for branch coverage instrumentation, so to work around that
50+
/// we pre-emptively visit any affected `!` expressions, and record extra
51+
/// information that [`Builder::visit_coverage_branch_condition`] can use to
52+
/// synthesize branch instrumentation for the enclosing `!`.
53+
pub(crate) fn visit_unary_not(&mut self, thir: &Thir<'_>, unary_not: ExprId) {
54+
assert_matches!(thir[unary_not].kind, ExprKind::Unary { op: UnOp::Not, .. });
55+
56+
self.visit_inverted(
57+
thir,
58+
unary_not,
59+
// Set `is_inverted: false` for the `!` itself, so that its enclosed
60+
// expression will have `is_inverted: true`.
61+
Inversion { enclosing_not: unary_not, is_inverted: false },
62+
);
63+
}
64+
65+
fn visit_inverted(&mut self, thir: &Thir<'_>, expr_id: ExprId, inversion: Inversion) {
66+
match self.inversions.entry(expr_id) {
67+
// This expression has already been marked by an enclosing `!`.
68+
Entry::Occupied(_) => return,
69+
Entry::Vacant(entry) => entry.insert(inversion),
70+
};
71+
72+
match thir[expr_id].kind {
73+
ExprKind::Unary { op: UnOp::Not, arg } => {
74+
// Flip the `is_inverted` flag for the contents of this `!`.
75+
let inversion = Inversion { is_inverted: !inversion.is_inverted, ..inversion };
76+
self.visit_inverted(thir, arg, inversion);
77+
}
78+
ExprKind::Scope { value, .. } => self.visit_inverted(thir, value, inversion),
79+
ExprKind::Use { source } => self.visit_inverted(thir, source, inversion),
80+
// All other expressions (including `&&` and `||`) don't need any
81+
// special handling of their contents, so stop visiting.
82+
_ => {}
83+
}
84+
}
85+
86+
fn next_block_marker_id(&mut self) -> BlockMarkerId {
87+
let id = BlockMarkerId::from_usize(self.num_block_markers);
88+
self.num_block_markers += 1;
89+
id
90+
}
91+
2692
pub(crate) fn into_done(self) -> Option<Box<mir::coverage::HirBranchInfo>> {
27-
let Self { num_block_markers, branch_spans } = self;
93+
let Self { inversions: _, num_block_markers, branch_spans } = self;
2894

2995
if num_block_markers == 0 {
3096
assert!(branch_spans.is_empty());
@@ -34,3 +100,54 @@ impl HirBranchInfoBuilder {
34100
Some(Box::new(mir::coverage::HirBranchInfo { num_block_markers, branch_spans }))
35101
}
36102
}
103+
104+
impl Builder<'_, '_> {
105+
/// If branch coverage is enabled, inject marker statements into `then_block`
106+
/// and `else_block`, and record their IDs in the table of branch spans.
107+
pub(crate) fn visit_coverage_branch_condition(
108+
&mut self,
109+
expr_id: ExprId,
110+
then_block: BasicBlock,
111+
else_block: BasicBlock,
112+
) {
113+
// Bail out if branch coverage is not enabled for this function.
114+
let Some(branch_info) = self.coverage_branch_info.as_ref() else { return };
115+
116+
// If this condition expression is nested within one or more `!` expressions,
117+
// replace it with the enclosing `!` collected by `visit_unary_not`.
118+
let (expr_id, is_inverted) = match branch_info.inversions.get(&expr_id) {
119+
Some(&Inversion { enclosing_not, is_inverted }) => (enclosing_not, is_inverted),
120+
None => (expr_id, false),
121+
};
122+
let source_info = self.source_info(self.thir[expr_id].span);
123+
124+
// Now that we have `source_info`, we can upgrade to a &mut reference.
125+
let branch_info = self.coverage_branch_info.as_mut().expect("upgrading & to &mut");
126+
127+
let mut inject_branch_marker = |block: BasicBlock| {
128+
let id = branch_info.next_block_marker_id();
129+
130+
let marker_statement = mir::Statement {
131+
source_info,
132+
kind: mir::StatementKind::Coverage(Box::new(mir::Coverage {
133+
kind: CoverageKind::BlockMarker { id },
134+
})),
135+
};
136+
self.cfg.push(block, marker_statement);
137+
138+
id
139+
};
140+
141+
let mut true_marker = inject_branch_marker(then_block);
142+
let mut false_marker = inject_branch_marker(else_block);
143+
if is_inverted {
144+
std::mem::swap(&mut true_marker, &mut false_marker);
145+
}
146+
147+
branch_info.branch_spans.push(BranchSpan {
148+
span: source_info.span,
149+
true_marker,
150+
false_marker,
151+
});
152+
}
153+
}

compiler/rustc_mir_build/src/build/matches/mod.rs

+11
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
106106
success_block.unit()
107107
}
108108
ExprKind::Unary { op: UnOp::Not, arg } => {
109+
// Improve branch coverage instrumentation by noting conditions
110+
// nested within one or more `!` expressions.
111+
// (Skipped if branch coverage is not enabled.)
112+
if let Some(branch_info) = this.coverage_branch_info.as_mut() {
113+
branch_info.visit_unary_not(this.thir, expr_id);
114+
}
115+
109116
let local_scope = this.local_scope();
110117
let (success_block, failure_block) =
111118
this.in_if_then_scope(local_scope, expr_span, |this| {
@@ -150,6 +157,10 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
150157
let else_block = this.cfg.start_new_block();
151158
let term = TerminatorKind::if_(operand, then_block, else_block);
152159

160+
// Record branch coverage info for this condition.
161+
// (Does nothing if branch coverage is not enabled.)
162+
this.visit_coverage_branch_condition(expr_id, then_block, else_block);
163+
153164
let source_info = this.source_info(expr_span);
154165
this.cfg.terminate(block, source_info, term);
155166
this.break_for_else(else_block, source_info);

0 commit comments

Comments
 (0)