about summary refs log tree commit diff
path: root/compiler/rustc_mir_build/src/builder/coverageinfo/mcdc.rs
diff options
context:
space:
mode:
Diffstat (limited to 'compiler/rustc_mir_build/src/builder/coverageinfo/mcdc.rs')
-rw-r--r--compiler/rustc_mir_build/src/builder/coverageinfo/mcdc.rs295
1 files changed, 295 insertions, 0 deletions
diff --git a/compiler/rustc_mir_build/src/builder/coverageinfo/mcdc.rs b/compiler/rustc_mir_build/src/builder/coverageinfo/mcdc.rs
new file mode 100644
index 00000000000..6b4871dc1fc
--- /dev/null
+++ b/compiler/rustc_mir_build/src/builder/coverageinfo/mcdc.rs
@@ -0,0 +1,295 @@
+use std::collections::VecDeque;
+
+use rustc_middle::bug;
+use rustc_middle::mir::coverage::{
+    BlockMarkerId, ConditionId, ConditionInfo, MCDCBranchSpan, MCDCDecisionSpan,
+};
+use rustc_middle::mir::{BasicBlock, SourceInfo};
+use rustc_middle::thir::LogicalOp;
+use rustc_middle::ty::TyCtxt;
+use rustc_span::Span;
+
+use crate::builder::Builder;
+use crate::errors::MCDCExceedsConditionLimit;
+
+/// LLVM uses `i16` to represent condition id. Hence `i16::MAX` is the hard limit for number of
+/// conditions in a decision.
+const MAX_CONDITIONS_IN_DECISION: usize = i16::MAX as usize;
+
+#[derive(Default)]
+struct MCDCDecisionCtx {
+    /// To construct condition evaluation tree.
+    decision_stack: VecDeque<ConditionInfo>,
+    processing_decision: Option<MCDCDecisionSpan>,
+    conditions: Vec<MCDCBranchSpan>,
+}
+
+struct MCDCState {
+    decision_ctx_stack: Vec<MCDCDecisionCtx>,
+}
+
+impl MCDCState {
+    fn new() -> Self {
+        Self { decision_ctx_stack: vec![MCDCDecisionCtx::default()] }
+    }
+
+    /// Decision depth is given as a u16 to reduce the size of the `CoverageKind`,
+    /// as it is very unlikely that the depth ever reaches 2^16.
+    #[inline]
+    fn decision_depth(&self) -> u16 {
+        match u16::try_from(self.decision_ctx_stack.len())
+            .expect(
+                "decision depth did not fit in u16, this is likely to be an instrumentation error",
+            )
+            .checked_sub(1)
+        {
+            Some(d) => d,
+            None => bug!("Unexpected empty decision stack"),
+        }
+    }
+
+    // At first we assign ConditionIds for each sub expression.
+    // If the sub expression is composite, re-assign its ConditionId to its LHS and generate a new ConditionId for its RHS.
+    //
+    // Example: "x = (A && B) || (C && D) || (D && F)"
+    //
+    //      Visit Depth1:
+    //              (A && B) || (C && D) || (D && F)
+    //              ^-------LHS--------^    ^-RHS--^
+    //                      ID=1              ID=2
+    //
+    //      Visit LHS-Depth2:
+    //              (A && B) || (C && D)
+    //              ^-LHS--^    ^-RHS--^
+    //                ID=1        ID=3
+    //
+    //      Visit LHS-Depth3:
+    //               (A && B)
+    //               LHS   RHS
+    //               ID=1  ID=4
+    //
+    //      Visit RHS-Depth3:
+    //                         (C && D)
+    //                         LHS   RHS
+    //                         ID=3  ID=5
+    //
+    //      Visit RHS-Depth2:              (D && F)
+    //                                     LHS   RHS
+    //                                     ID=2  ID=6
+    //
+    //      Visit Depth1:
+    //              (A && B)  || (C && D)  || (D && F)
+    //              ID=1  ID=4   ID=3  ID=5   ID=2  ID=6
+    //
+    // A node ID of '0' always means MC/DC isn't being tracked.
+    //
+    // If a "next" node ID is '0', it means it's the end of the test vector.
+    //
+    // 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.
+    // - 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".
+    // - 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".
+    fn record_conditions(&mut self, op: LogicalOp, span: Span) {
+        let decision_depth = self.decision_depth();
+        let Some(decision_ctx) = self.decision_ctx_stack.last_mut() else {
+            bug!("Unexpected empty decision_ctx_stack")
+        };
+        let decision = match decision_ctx.processing_decision.as_mut() {
+            Some(decision) => {
+                decision.span = decision.span.to(span);
+                decision
+            }
+            None => decision_ctx.processing_decision.insert(MCDCDecisionSpan {
+                span,
+                num_conditions: 0,
+                end_markers: vec![],
+                decision_depth,
+            }),
+        };
+
+        let parent_condition = decision_ctx.decision_stack.pop_back().unwrap_or_else(|| {
+            assert_eq!(
+                decision.num_conditions, 0,
+                "decision stack must be empty only for empty decision"
+            );
+            decision.num_conditions += 1;
+            ConditionInfo {
+                condition_id: ConditionId::START,
+                true_next_id: None,
+                false_next_id: None,
+            }
+        });
+        let lhs_id = parent_condition.condition_id;
+
+        let rhs_condition_id = ConditionId::from(decision.num_conditions);
+        decision.num_conditions += 1;
+        let (lhs, rhs) = match op {
+            LogicalOp::And => {
+                let lhs = ConditionInfo {
+                    condition_id: lhs_id,
+                    true_next_id: Some(rhs_condition_id),
+                    false_next_id: parent_condition.false_next_id,
+                };
+                let rhs = ConditionInfo {
+                    condition_id: rhs_condition_id,
+                    true_next_id: parent_condition.true_next_id,
+                    false_next_id: parent_condition.false_next_id,
+                };
+                (lhs, rhs)
+            }
+            LogicalOp::Or => {
+                let lhs = ConditionInfo {
+                    condition_id: lhs_id,
+                    true_next_id: parent_condition.true_next_id,
+                    false_next_id: Some(rhs_condition_id),
+                };
+                let rhs = ConditionInfo {
+                    condition_id: rhs_condition_id,
+                    true_next_id: parent_condition.true_next_id,
+                    false_next_id: parent_condition.false_next_id,
+                };
+                (lhs, rhs)
+            }
+        };
+        // We visit expressions tree in pre-order, so place the left-hand side on the top.
+        decision_ctx.decision_stack.push_back(rhs);
+        decision_ctx.decision_stack.push_back(lhs);
+    }
+
+    fn try_finish_decision(
+        &mut self,
+        span: Span,
+        true_marker: BlockMarkerId,
+        false_marker: BlockMarkerId,
+        degraded_branches: &mut Vec<MCDCBranchSpan>,
+    ) -> Option<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)> {
+        let Some(decision_ctx) = self.decision_ctx_stack.last_mut() else {
+            bug!("Unexpected empty decision_ctx_stack")
+        };
+        let Some(condition_info) = decision_ctx.decision_stack.pop_back() else {
+            let branch = MCDCBranchSpan {
+                span,
+                condition_info: ConditionInfo {
+                    condition_id: ConditionId::START,
+                    true_next_id: None,
+                    false_next_id: None,
+                },
+                true_marker,
+                false_marker,
+            };
+            degraded_branches.push(branch);
+            return None;
+        };
+        let Some(decision) = decision_ctx.processing_decision.as_mut() else {
+            bug!("Processing decision should have been created before any conditions are taken");
+        };
+        if condition_info.true_next_id.is_none() {
+            decision.end_markers.push(true_marker);
+        }
+        if condition_info.false_next_id.is_none() {
+            decision.end_markers.push(false_marker);
+        }
+        decision_ctx.conditions.push(MCDCBranchSpan {
+            span,
+            condition_info,
+            true_marker,
+            false_marker,
+        });
+
+        if decision_ctx.decision_stack.is_empty() {
+            let conditions = std::mem::take(&mut decision_ctx.conditions);
+            decision_ctx.processing_decision.take().map(|decision| (decision, conditions))
+        } else {
+            None
+        }
+    }
+}
+
+pub(crate) struct MCDCInfoBuilder {
+    degraded_spans: Vec<MCDCBranchSpan>,
+    mcdc_spans: Vec<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)>,
+    state: MCDCState,
+}
+
+impl MCDCInfoBuilder {
+    pub(crate) fn new() -> Self {
+        Self { degraded_spans: vec![], mcdc_spans: vec![], state: MCDCState::new() }
+    }
+
+    pub(crate) fn visit_evaluated_condition(
+        &mut self,
+        tcx: TyCtxt<'_>,
+        source_info: SourceInfo,
+        true_block: BasicBlock,
+        false_block: BasicBlock,
+        mut inject_block_marker: impl FnMut(SourceInfo, BasicBlock) -> BlockMarkerId,
+    ) {
+        let true_marker = inject_block_marker(source_info, true_block);
+        let false_marker = inject_block_marker(source_info, false_block);
+
+        // take_condition() returns Some for decision_result when the decision stack
+        // is empty, i.e. when all the conditions of the decision were instrumented,
+        // and the decision is "complete".
+        if let Some((decision, conditions)) = self.state.try_finish_decision(
+            source_info.span,
+            true_marker,
+            false_marker,
+            &mut self.degraded_spans,
+        ) {
+            let num_conditions = conditions.len();
+            assert_eq!(
+                num_conditions, decision.num_conditions,
+                "final number of conditions is not correct"
+            );
+            match num_conditions {
+                0 => {
+                    unreachable!("Decision with no condition is not expected");
+                }
+                1..=MAX_CONDITIONS_IN_DECISION => {
+                    self.mcdc_spans.push((decision, conditions));
+                }
+                _ => {
+                    self.degraded_spans.extend(conditions);
+
+                    tcx.dcx().emit_warn(MCDCExceedsConditionLimit {
+                        span: decision.span,
+                        num_conditions,
+                        max_conditions: MAX_CONDITIONS_IN_DECISION,
+                    });
+                }
+            }
+        }
+    }
+
+    pub(crate) fn into_done(
+        self,
+    ) -> (Vec<(MCDCDecisionSpan, Vec<MCDCBranchSpan>)>, Vec<MCDCBranchSpan>) {
+        (self.mcdc_spans, self.degraded_spans)
+    }
+}
+
+impl Builder<'_, '_> {
+    pub(crate) fn visit_coverage_branch_operation(&mut self, logical_op: LogicalOp, span: Span) {
+        if let Some(coverage_info) = self.coverage_info.as_mut()
+            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
+        {
+            mcdc_info.state.record_conditions(logical_op, span);
+        }
+    }
+
+    pub(crate) fn mcdc_increment_depth_if_enabled(&mut self) {
+        if let Some(coverage_info) = self.coverage_info.as_mut()
+            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
+        {
+            mcdc_info.state.decision_ctx_stack.push(MCDCDecisionCtx::default());
+        };
+    }
+
+    pub(crate) fn mcdc_decrement_depth_if_enabled(&mut self) {
+        if let Some(coverage_info) = self.coverage_info.as_mut()
+            && let Some(mcdc_info) = coverage_info.mcdc_info.as_mut()
+            && mcdc_info.state.decision_ctx_stack.pop().is_none()
+        {
+            bug!("Unexpected empty decision stack");
+        };
+    }
+}