Skip to content

Commit 2022ef7

Browse files
committed
coverage: Use a query to find counters/expressions that must be zero
This query (`coverage_ids_info`) already determines which counter/expression IDs are unused, so it only takes a little extra effort to also determine which counters/expressions must have a value of zero.
1 parent f3f7c20 commit 2022ef7

File tree

3 files changed

+117
-117
lines changed

3 files changed

+117
-117
lines changed
+4-112
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
use rustc_data_structures::captures::Captures;
2-
use rustc_data_structures::fx::FxIndexSet;
3-
use rustc_index::bit_set::BitSet;
42
use rustc_middle::mir::coverage::{
5-
CounterId, CovTerm, CoverageIdsInfo, Expression, ExpressionId, FunctionCoverageInfo, Mapping,
6-
MappingKind, Op, SourceRegion,
3+
CovTerm, CoverageIdsInfo, Expression, FunctionCoverageInfo, Mapping, MappingKind, Op,
4+
SourceRegion,
75
};
86
use rustc_middle::ty::Instance;
97
use tracing::debug;
@@ -55,92 +53,17 @@ impl<'tcx> FunctionCoverageCollector<'tcx> {
5553
Self { function_coverage_info, ids_info, is_used }
5654
}
5755

58-
/// Identify expressions that will always have a value of zero, and note
59-
/// their IDs in [`ZeroExpressions`]. Mappings that refer to a zero expression
60-
/// can instead become mappings to a constant zero value.
61-
///
62-
/// This method mainly exists to preserve the simplifications that were
63-
/// already being performed by the Rust-side expression renumbering, so that
64-
/// the resulting coverage mappings don't get worse.
65-
fn identify_zero_expressions(&self) -> ZeroExpressions {
66-
// The set of expressions that either were optimized out entirely, or
67-
// have zero as both of their operands, and will therefore always have
68-
// a value of zero. Other expressions that refer to these as operands
69-
// can have those operands replaced with `CovTerm::Zero`.
70-
let mut zero_expressions = ZeroExpressions::default();
71-
72-
// Simplify a copy of each expression based on lower-numbered expressions,
73-
// and then update the set of always-zero expressions if necessary.
74-
// (By construction, expressions can only refer to other expressions
75-
// that have lower IDs, so one pass is sufficient.)
76-
for (id, expression) in self.function_coverage_info.expressions.iter_enumerated() {
77-
if !self.is_used || !self.ids_info.expressions_seen.contains(id) {
78-
// If an expression was not seen, it must have been optimized away,
79-
// so any operand that refers to it can be replaced with zero.
80-
zero_expressions.insert(id);
81-
continue;
82-
}
83-
84-
// We don't need to simplify the actual expression data in the
85-
// expressions list; we can just simplify a temporary copy and then
86-
// use that to update the set of always-zero expressions.
87-
let Expression { mut lhs, op, mut rhs } = *expression;
88-
89-
// If an expression has an operand that is also an expression, the
90-
// operand's ID must be strictly lower. This is what lets us find
91-
// all zero expressions in one pass.
92-
let assert_operand_expression_is_lower = |operand_id: ExpressionId| {
93-
assert!(
94-
operand_id < id,
95-
"Operand {operand_id:?} should be less than {id:?} in {expression:?}",
96-
)
97-
};
98-
99-
// If an operand refers to a counter or expression that is always
100-
// zero, then that operand can be replaced with `CovTerm::Zero`.
101-
let maybe_set_operand_to_zero = |operand: &mut CovTerm| {
102-
if let CovTerm::Expression(id) = *operand {
103-
assert_operand_expression_is_lower(id);
104-
}
105-
106-
if is_zero_term(&self.ids_info.counters_seen, &zero_expressions, *operand) {
107-
*operand = CovTerm::Zero;
108-
}
109-
};
110-
maybe_set_operand_to_zero(&mut lhs);
111-
maybe_set_operand_to_zero(&mut rhs);
112-
113-
// Coverage counter values cannot be negative, so if an expression
114-
// involves subtraction from zero, assume that its RHS must also be zero.
115-
// (Do this after simplifications that could set the LHS to zero.)
116-
if lhs == CovTerm::Zero && op == Op::Subtract {
117-
rhs = CovTerm::Zero;
118-
}
119-
120-
// After the above simplifications, if both operands are zero, then
121-
// we know that this expression is always zero too.
122-
if lhs == CovTerm::Zero && rhs == CovTerm::Zero {
123-
zero_expressions.insert(id);
124-
}
125-
}
126-
127-
zero_expressions
128-
}
129-
13056
pub(crate) fn into_finished(self) -> FunctionCoverage<'tcx> {
131-
let zero_expressions = self.identify_zero_expressions();
13257
let FunctionCoverageCollector { function_coverage_info, ids_info, is_used, .. } = self;
13358

134-
FunctionCoverage { function_coverage_info, ids_info, is_used, zero_expressions }
59+
FunctionCoverage { function_coverage_info, ids_info, is_used }
13560
}
13661
}
13762

13863
pub(crate) struct FunctionCoverage<'tcx> {
13964
pub(crate) function_coverage_info: &'tcx FunctionCoverageInfo,
14065
ids_info: &'tcx CoverageIdsInfo,
14166
is_used: bool,
142-
143-
zero_expressions: ZeroExpressions,
14467
}
14568

14669
impl<'tcx> FunctionCoverage<'tcx> {
@@ -195,37 +118,6 @@ impl<'tcx> FunctionCoverage<'tcx> {
195118
}
196119

197120
fn is_zero_term(&self, term: CovTerm) -> bool {
198-
!self.is_used || is_zero_term(&self.ids_info.counters_seen, &self.zero_expressions, term)
199-
}
200-
}
201-
202-
/// Set of expression IDs that are known to always evaluate to zero.
203-
/// Any mapping or expression operand that refers to these expressions can have
204-
/// that reference replaced with a constant zero value.
205-
#[derive(Default)]
206-
struct ZeroExpressions(FxIndexSet<ExpressionId>);
207-
208-
impl ZeroExpressions {
209-
fn insert(&mut self, id: ExpressionId) {
210-
self.0.insert(id);
211-
}
212-
213-
fn contains(&self, id: ExpressionId) -> bool {
214-
self.0.contains(&id)
215-
}
216-
}
217-
218-
/// Returns `true` if the given term is known to have a value of zero, taking
219-
/// into account knowledge of which counters are unused and which expressions
220-
/// are always zero.
221-
fn is_zero_term(
222-
counters_seen: &BitSet<CounterId>,
223-
zero_expressions: &ZeroExpressions,
224-
term: CovTerm,
225-
) -> bool {
226-
match term {
227-
CovTerm::Zero => true,
228-
CovTerm::Counter(id) => !counters_seen.contains(id),
229-
CovTerm::Expression(id) => zero_expressions.contains(id),
121+
!self.is_used || self.ids_info.is_zero_term(term)
230122
}
231123
}

Diff for: compiler/rustc_middle/src/mir/coverage.rs

+12-1
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ pub struct MCDCDecisionSpan {
320320
#[derive(Clone, TyEncodable, TyDecodable, Debug, HashStable)]
321321
pub struct CoverageIdsInfo {
322322
pub counters_seen: BitSet<CounterId>,
323-
pub expressions_seen: BitSet<ExpressionId>,
323+
pub zero_expressions: BitSet<ExpressionId>,
324324
}
325325

326326
impl CoverageIdsInfo {
@@ -337,4 +337,15 @@ impl CoverageIdsInfo {
337337
// used. Fixing this would require adding a renumbering step somewhere.
338338
self.counters_seen.last_set_in(..).map_or(0, |max| max.as_u32() + 1)
339339
}
340+
341+
/// Returns `true` if the given term is known to have a value of zero, taking
342+
/// into account knowledge of which counters are unused and which expressions
343+
/// are always zero.
344+
pub fn is_zero_term(&self, term: CovTerm) -> bool {
345+
match term {
346+
CovTerm::Zero => true,
347+
CovTerm::Counter(id) => !self.counters_seen.contains(id),
348+
CovTerm::Expression(id) => self.zero_expressions.contains(id),
349+
}
350+
}
340351
}

Diff for: compiler/rustc_mir_transform/src/coverage/query.rs

+101-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use rustc_data_structures::captures::Captures;
22
use rustc_index::bit_set::BitSet;
33
use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags;
4-
use rustc_middle::mir::coverage::{CovTerm, CoverageIdsInfo, CoverageKind, MappingKind};
4+
use rustc_middle::mir::coverage::{
5+
CounterId, CovTerm, CoverageIdsInfo, CoverageKind, Expression, ExpressionId,
6+
FunctionCoverageInfo, MappingKind, Op,
7+
};
58
use rustc_middle::mir::{Body, Statement, StatementKind};
69
use rustc_middle::query::TyCtxtAt;
710
use rustc_middle::ty::{self, TyCtxt};
@@ -87,10 +90,10 @@ fn coverage_ids_info<'tcx>(
8790
) -> CoverageIdsInfo {
8891
let mir_body = tcx.instance_mir(instance_def);
8992

90-
let Some(fn_cov_info) = mir_body.function_coverage_info.as_ref() else {
93+
let Some(fn_cov_info) = mir_body.function_coverage_info.as_deref() else {
9194
return CoverageIdsInfo {
9295
counters_seen: BitSet::new_empty(0),
93-
expressions_seen: BitSet::new_empty(0),
96+
zero_expressions: BitSet::new_empty(0),
9497
};
9598
};
9699

@@ -123,7 +126,10 @@ fn coverage_ids_info<'tcx>(
123126
}
124127
}
125128

126-
CoverageIdsInfo { counters_seen, expressions_seen }
129+
let zero_expressions =
130+
identify_zero_expressions(fn_cov_info, &counters_seen, &expressions_seen);
131+
132+
CoverageIdsInfo { counters_seen, zero_expressions }
127133
}
128134

129135
fn all_coverage_in_mir_body<'a, 'tcx>(
@@ -141,3 +147,94 @@ fn is_inlined(body: &Body<'_>, statement: &Statement<'_>) -> bool {
141147
let scope_data = &body.source_scopes[statement.source_info.scope];
142148
scope_data.inlined.is_some() || scope_data.inlined_parent_scope.is_some()
143149
}
150+
151+
/// Identify expressions that will always have a value of zero, and note
152+
/// their IDs in a `BitSet`. Mappings that refer to a zero expression
153+
/// can instead become mappings to a constant zero value.
154+
///
155+
/// This function mainly exists to preserve the simplifications that were
156+
/// already being performed by the Rust-side expression renumbering, so that
157+
/// the resulting coverage mappings don't get worse.
158+
fn identify_zero_expressions(
159+
fn_cov_info: &FunctionCoverageInfo,
160+
counters_seen: &BitSet<CounterId>,
161+
expressions_seen: &BitSet<ExpressionId>,
162+
) -> BitSet<ExpressionId> {
163+
// The set of expressions that either were optimized out entirely, or
164+
// have zero as both of their operands, and will therefore always have
165+
// a value of zero. Other expressions that refer to these as operands
166+
// can have those operands replaced with `CovTerm::Zero`.
167+
let mut zero_expressions = BitSet::new_empty(fn_cov_info.expressions.len());
168+
169+
// Simplify a copy of each expression based on lower-numbered expressions,
170+
// and then update the set of always-zero expressions if necessary.
171+
// (By construction, expressions can only refer to other expressions
172+
// that have lower IDs, so one pass is sufficient.)
173+
for (id, expression) in fn_cov_info.expressions.iter_enumerated() {
174+
if !expressions_seen.contains(id) {
175+
// If an expression was not seen, it must have been optimized away,
176+
// so any operand that refers to it can be replaced with zero.
177+
zero_expressions.insert(id);
178+
continue;
179+
}
180+
181+
// We don't need to simplify the actual expression data in the
182+
// expressions list; we can just simplify a temporary copy and then
183+
// use that to update the set of always-zero expressions.
184+
let Expression { mut lhs, op, mut rhs } = *expression;
185+
186+
// If an expression has an operand that is also an expression, the
187+
// operand's ID must be strictly lower. This is what lets us find
188+
// all zero expressions in one pass.
189+
let assert_operand_expression_is_lower = |operand_id: ExpressionId| {
190+
assert!(
191+
operand_id < id,
192+
"Operand {operand_id:?} should be less than {id:?} in {expression:?}",
193+
)
194+
};
195+
196+
// If an operand refers to a counter or expression that is always
197+
// zero, then that operand can be replaced with `CovTerm::Zero`.
198+
let maybe_set_operand_to_zero = |operand: &mut CovTerm| {
199+
if let CovTerm::Expression(id) = *operand {
200+
assert_operand_expression_is_lower(id);
201+
}
202+
203+
if is_zero_term(&counters_seen, &zero_expressions, *operand) {
204+
*operand = CovTerm::Zero;
205+
}
206+
};
207+
maybe_set_operand_to_zero(&mut lhs);
208+
maybe_set_operand_to_zero(&mut rhs);
209+
210+
// Coverage counter values cannot be negative, so if an expression
211+
// involves subtraction from zero, assume that its RHS must also be zero.
212+
// (Do this after simplifications that could set the LHS to zero.)
213+
if lhs == CovTerm::Zero && op == Op::Subtract {
214+
rhs = CovTerm::Zero;
215+
}
216+
217+
// After the above simplifications, if both operands are zero, then
218+
// we know that this expression is always zero too.
219+
if lhs == CovTerm::Zero && rhs == CovTerm::Zero {
220+
zero_expressions.insert(id);
221+
}
222+
}
223+
224+
zero_expressions
225+
}
226+
227+
/// Returns `true` if the given term is known to have a value of zero, taking
228+
/// into account knowledge of which counters are unused and which expressions
229+
/// are always zero.
230+
fn is_zero_term(
231+
counters_seen: &BitSet<CounterId>,
232+
zero_expressions: &BitSet<ExpressionId>,
233+
term: CovTerm,
234+
) -> bool {
235+
match term {
236+
CovTerm::Zero => true,
237+
CovTerm::Counter(id) => !counters_seen.contains(id),
238+
CovTerm::Expression(id) => zero_expressions.contains(id),
239+
}
240+
}

0 commit comments

Comments
 (0)