rustc_mir_build/builder/coverageinfo/
mcdc.rs

1use std::collections::VecDeque;
2
3use rustc_middle::bug;
4use rustc_middle::mir::coverage::{
5    BlockMarkerId, ConditionId, ConditionInfo, MCDCBranchSpan, MCDCDecisionSpan,
6};
7use rustc_middle::mir::{BasicBlock, SourceInfo};
8use rustc_middle::thir::LogicalOp;
9use rustc_middle::ty::TyCtxt;
10use rustc_span::Span;
11
12use crate::builder::Builder;
13use crate::errors::MCDCExceedsConditionLimit;
14
15/// LLVM uses `i16` to represent condition id. Hence `i16::MAX` is the hard limit for number of
16/// conditions in a decision.
17const MAX_CONDITIONS_IN_DECISION: usize = i16::MAX as usize;
18
19#[derive(Default)]
20struct MCDCDecisionCtx {
21    /// To construct condition evaluation tree.
22    decision_stack: VecDeque<ConditionInfo>,
23    processing_decision: Option<MCDCDecisionSpan>,
24    conditions: Vec<MCDCBranchSpan>,
25}
26
27struct MCDCState {
28    decision_ctx_stack: Vec<MCDCDecisionCtx>,
29}
30
31impl MCDCState {
32    fn new() -> Self {
33        Self { decision_ctx_stack: vec![MCDCDecisionCtx::default()] }
34    }
35
36    /// Decision depth is given as a u16 to reduce the size of the `CoverageKind`,
37    /// as it is very unlikely that the depth ever reaches 2^16.
38    #[inline]
39    fn decision_depth(&self) -> u16 {
40        match u16::try_from(self.decision_ctx_stack.len())
41            .expect(
42                "decision depth did not fit in u16, this is likely to be an instrumentation error",
43            )
44            .checked_sub(1)
45        {
46            Some(d) => d,
47            None => bug!("Unexpected empty decision stack"),
48        }
49    }
50
51    // At first we assign ConditionIds for each sub expression.
52    // If the sub expression is composite, re-assign its ConditionId to its LHS and generate a new ConditionId for its RHS.
53    //
54    // Example: "x = (A && B) || (C && D) || (D && F)"
55    //
56    //      Visit Depth1:
57    //              (A && B) || (C && D) || (D && F)
58    //              ^-------LHS--------^    ^-RHS--^
59    //                      ID=1              ID=2
60    //
61    //      Visit LHS-Depth2:
62    //              (A && B) || (C && D)
63    //              ^-LHS--^    ^-RHS--^
64    //                ID=1        ID=3
65    //
66    //      Visit LHS-Depth3:
67    //               (A && B)
68    //               LHS   RHS
69    //               ID=1  ID=4
70    //
71    //      Visit RHS-Depth3:
72    //                         (C && D)
73    //                         LHS   RHS
74    //                         ID=3  ID=5
75    //
76    //      Visit RHS-Depth2:              (D && F)
77    //                                     LHS   RHS
78    //                                     ID=2  ID=6
79    //
80    //      Visit Depth1:
81    //              (A && B)  || (C && D)  || (D && F)
82    //              ID=1  ID=4   ID=3  ID=5   ID=2  ID=6
83    //
84    // A node ID of '0' always means MC/DC isn't being tracked.
85    //
86    // If a "next" node ID is '0', it means it's the end of the test vector.
87    //
88    // As the compiler tracks expression in pre-order, we can ensure that condition info of parents are always properly assigned when their children are visited.
89    // - If the op is AND, the "false_next" of LHS and RHS should be the parent's "false_next". While "true_next" of the LHS is the RHS, the "true next" of RHS is the parent's "true_next".
90    // - If the op is OR, the "true_next" of LHS and RHS should be the parent's "true_next". While "false_next" of the LHS is the RHS, the "false next" of RHS is the parent's "false_next".
91    fn record_conditions(&mut self, op: LogicalOp, span: Span) {
92        let decision_depth = self.decision_depth();
93        let Some(decision_ctx) = self.decision_ctx_stack.last_mut() else {
94            bug!("Unexpected empty decision_ctx_stack")
95        };
96        let decision = match decision_ctx.processing_decision.as_mut() {
97            Some(decision) => {
98                decision.span = decision.span.to(span);
99                decision
100            }
101            None => decision_ctx.processing_decision.insert(MCDCDecisionSpan {
102                span,
103                num_conditions: 0,
104                end_markers: vec![],
105                decision_depth,
106            }),
107        };
108
109        let parent_condition = decision_ctx.decision_stack.pop_back().unwrap_or_else(|| {
110            assert_eq!(
111                decision.num_conditions, 0,
112                "decision stack must be empty only for empty decision"
113            );
114            decision.num_conditions += 1;
115            ConditionInfo {
116                condition_id: ConditionId::START,
117                true_next_id: None,
118                false_next_id: None,
119            }
120        });
121        let lhs_id = parent_condition.condition_id;
122
123        let rhs_condition_id = ConditionId::from(decision.num_conditions);
124        decision.num_conditions += 1;
125        let (lhs, rhs) = match op {
126            LogicalOp::And => {
127                let lhs = ConditionInfo {
128                    condition_id: lhs_id,
129                    true_next_id: Some(rhs_condition_id),
130                    false_next_id: parent_condition.false_next_id,
131                };
132                let rhs = ConditionInfo {
133                    condition_id: rhs_condition_id,
134                    true_next_id: parent_condition.true_next_id,
135                    false_next_id: parent_condition.false_next_id,
136                };
137                (lhs, rhs)
138            }
139            LogicalOp::Or => {
140                let lhs = ConditionInfo {
141                    condition_id: lhs_id,
142                    true_next_id: parent_condition.true_next_id,
143                    false_next_id: Some(rhs_condition_id),
144                };
145                let rhs = ConditionInfo {
146                    condition_id: rhs_condition_id,
147                    true_next_id: parent_condition.true_next_id,
148                    false_next_id: parent_condition.false_next_id,
149                };
150                (lhs, rhs)
151            }
152        };
153        // We visit expressions tree in pre-order, so place the left-hand side on the top.
154        decision_ctx.decision_stack.push_back(rhs);
155        decision_ctx.decision_stack.push_back(lhs);
156    }
157
158    fn try_finish_decision(
159        &mut self,
160        span: Span,
161        true_marker: BlockMarkerId,
162        false_marker: BlockMarkerId,
163        degraded_branches: &mut Vec<MCDCBranchSpan>,
164    ) -> Option<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)> {
165        let Some(decision_ctx) = self.decision_ctx_stack.last_mut() else {
166            bug!("Unexpected empty decision_ctx_stack")
167        };
168        let Some(condition_info) = decision_ctx.decision_stack.pop_back() else {
169            let branch = MCDCBranchSpan {
170                span,
171                condition_info: ConditionInfo {
172                    condition_id: ConditionId::START,
173                    true_next_id: None,
174                    false_next_id: None,
175                },
176                true_marker,
177                false_marker,
178            };
179            degraded_branches.push(branch);
180            return None;
181        };
182        let Some(decision) = decision_ctx.processing_decision.as_mut() else {
183            bug!("Processing decision should have been created before any conditions are taken");
184        };
185        if condition_info.true_next_id.is_none() {
186            decision.end_markers.push(true_marker);
187        }
188        if condition_info.false_next_id.is_none() {
189            decision.end_markers.push(false_marker);
190        }
191        decision_ctx.conditions.push(MCDCBranchSpan {
192            span,
193            condition_info,
194            true_marker,
195            false_marker,
196        });
197
198        if decision_ctx.decision_stack.is_empty() {
199            let conditions = std::mem::take(&mut decision_ctx.conditions);
200            decision_ctx.processing_decision.take().map(|decision| (decision, conditions))
201        } else {
202            None
203        }
204    }
205}
206
207pub(crate) struct MCDCInfoBuilder {
208    degraded_spans: Vec<MCDCBranchSpan>,
209    mcdc_spans: Vec<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)>,
210    state: MCDCState,
211}
212
213impl MCDCInfoBuilder {
214    pub(crate) fn new() -> Self {
215        Self { degraded_spans: vec![], mcdc_spans: vec![], state: MCDCState::new() }
216    }
217
218    pub(crate) fn visit_evaluated_condition(
219        &mut self,
220        tcx: TyCtxt<'_>,
221        source_info: SourceInfo,
222        true_block: BasicBlock,
223        false_block: BasicBlock,
224        mut inject_block_marker: impl FnMut(SourceInfo, BasicBlock) -> BlockMarkerId,
225    ) {
226        let true_marker = inject_block_marker(source_info, true_block);
227        let false_marker = inject_block_marker(source_info, false_block);
228
229        // take_condition() returns Some for decision_result when the decision stack
230        // is empty, i.e. when all the conditions of the decision were instrumented,
231        // and the decision is "complete".
232        if let Some((decision, conditions)) = self.state.try_finish_decision(
233            source_info.span,
234            true_marker,
235            false_marker,
236            &mut self.degraded_spans,
237        ) {
238            let num_conditions = conditions.len();
239            assert_eq!(
240                num_conditions, decision.num_conditions,
241                "final number of conditions is not correct"
242            );
243            match num_conditions {
244                0 => {
245                    unreachable!("Decision with no condition is not expected");
246                }
247                1..=MAX_CONDITIONS_IN_DECISION => {
248                    self.mcdc_spans.push((decision, conditions));
249                }
250                _ => {
251                    self.degraded_spans.extend(conditions);
252
253                    tcx.dcx().emit_warn(MCDCExceedsConditionLimit {
254                        span: decision.span,
255                        num_conditions,
256                        max_conditions: MAX_CONDITIONS_IN_DECISION,
257                    });
258                }
259            }
260        }
261    }
262
263    pub(crate) fn into_done(
264        self,
265    ) -> (Vec<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)>, Vec<MCDCBranchSpan>) {
266        (self.mcdc_spans, self.degraded_spans)
267    }
268}
269
270impl Builder<'_, '_> {
271    pub(crate) fn visit_coverage_branch_operation(&mut self, logical_op: LogicalOp, span: Span) {
272        if let Some(coverage_info) = self.coverage_info.as_mut()
273            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
274        {
275            mcdc_info.state.record_conditions(logical_op, span);
276        }
277    }
278
279    pub(crate) fn mcdc_increment_depth_if_enabled(&mut self) {
280        if let Some(coverage_info) = self.coverage_info.as_mut()
281            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
282        {
283            mcdc_info.state.decision_ctx_stack.push(MCDCDecisionCtx::default());
284        };
285    }
286
287    pub(crate) fn mcdc_decrement_depth_if_enabled(&mut self) {
288        if let Some(coverage_info) = self.coverage_info.as_mut()
289            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
290            && mcdc_info.state.decision_ctx_stack.pop().is_none()
291        {
292            bug!("Unexpected empty decision stack");
293        };
294    }
295}