Skip to main content

rustc_mir_transform/
simplify_comparison_integral.rs

1use std::iter;
2
3use rustc_middle::bug;
4use rustc_middle::mir::interpret::Scalar;
5use rustc_middle::mir::{
6    BasicBlock, BinOp, Body, Operand, Place, Rvalue, StatementKind, SwitchTargets, TerminatorKind,
7};
8use rustc_middle::ty::{Ty, TyCtxt};
9use tracing::trace;
10
11use crate::PassPolicy;
12use crate::ssa::SsaLocals;
13
14/// Pass to convert `if` conditions on integrals into switches on the integral.
15/// For an example, it turns something like
16///
17/// ```ignore (MIR)
18/// _3 = Eq(move _4, const 43i32);
19/// switchInt(_3) -> [false: bb2, otherwise: bb3];
20/// ```
21///
22/// into:
23///
24/// ```ignore (MIR)
25/// switchInt(_4) -> [43i32: bb3, otherwise: bb2];
26/// ```
27pub(super) struct SimplifyComparisonIntegral;
28
29impl<'tcx> crate::MirPass<'tcx> for SimplifyComparisonIntegral {
30    fn policy(&self, sess: &rustc_session::Session) -> PassPolicy {
31        PassPolicy::optimization(sess.mir_opt_level() > 1)
32    }
33
34    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
35        trace!("Running SimplifyComparisonIntegral on {:?}", body.source);
36
37        let typing_env = body.typing_env(tcx);
38        let ssa = SsaLocals::new(tcx, body, typing_env);
39        let helper = OptimizationFinder { body };
40        let opts = helper.find_optimizations(&ssa);
41        for opt in opts {
42            trace!("SUCCESS: Applying {:?}", opt);
43            // replace terminator with a switchInt that switches on the integer directly
44            let bbs = &mut body.basic_blocks_mut();
45            let bb = &mut bbs[opt.bb_idx];
46            let new_value = match opt.branch_value_scalar {
47                Scalar::Int(int) => {
48                    let layout = tcx
49                        .layout_of(typing_env.as_query_input(opt.branch_value_ty))
50                        .expect("if we have an evaluated constant we must know the layout");
51                    int.to_bits(layout.size)
52                }
53                Scalar::Ptr(..) => continue,
54            };
55            const FALSE: u128 = 0;
56
57            let mut new_targets = opt.targets;
58            let first_value = new_targets.iter().next().unwrap().0;
59            let first_is_false_target = first_value == FALSE;
60            match opt.op {
61                BinOp::Eq => {
62                    // if the assignment was Eq we want the true case to be first
63                    if first_is_false_target {
64                        new_targets.all_targets_mut().swap(0, 1);
65                    }
66                }
67                BinOp::Ne => {
68                    // if the assignment was Ne we want the false case to be first
69                    if !first_is_false_target {
70                        new_targets.all_targets_mut().swap(0, 1);
71                    }
72                }
73                _ => unreachable!(),
74            }
75
76            // if the integer being compared to a const integral is being moved into the
77            // comparison, e.g `_2 = Eq(move _3, const 'x');`
78            // we want to avoid making a double move later on in the switchInt on _3.
79            // So to avoid `switchInt(move _3) -> ['x': bb2, otherwise: bb1];`,
80            // we convert the move in the comparison statement to a copy.
81
82            // unwrap is safe as we know this statement is an assign
83            let (_, rhs) = bb.statements[opt.bin_op_stmt_idx].kind.as_assign_mut().unwrap();
84
85            use Operand::*;
86            match rhs {
87                Rvalue::BinaryOp(_, (left @ Move(_), Constant(_))) => {
88                    *left = Copy(opt.to_switch_on);
89                }
90                Rvalue::BinaryOp(_, (Constant(_), right @ Move(_))) => {
91                    *right = Copy(opt.to_switch_on);
92                }
93                _ => (),
94            }
95
96            let [bb_cond, bb_otherwise] = match new_targets.all_targets() {
97                [a, b] => [*a, *b],
98                e => bug!("expected 2 switch targets, got: {:?}", e),
99            };
100
101            let targets = SwitchTargets::new(iter::once((new_value, bb_cond)), bb_otherwise);
102
103            let terminator = bb.terminator_mut();
104            terminator.kind =
105                TerminatorKind::SwitchInt { discr: Operand::Copy(opt.to_switch_on), targets };
106        }
107    }
108}
109
110struct OptimizationFinder<'a, 'tcx> {
111    body: &'a Body<'tcx>,
112}
113
114impl<'tcx> OptimizationFinder<'_, 'tcx> {
115    fn find_optimizations(&self, ssa: &SsaLocals) -> Vec<OptimizationInfo<'tcx>> {
116        self.body
117            .basic_blocks
118            .iter_enumerated()
119            .filter_map(|(bb_idx, bb)| {
120                // find switch
121                let (discr, targets) = bb.terminator().kind.as_switch()?;
122                let place_switched_on = discr.place()?;
123                // Make sure that the place is not modified.
124                if !ssa.is_ssa(place_switched_on.local) || !place_switched_on.is_stable_offset() {
125                    return None;
126                }
127
128                // find the statement that assigns the place being switched on
129                bb.statements.iter().enumerate().rev().find_map(|(stmt_idx, stmt)| {
130                    match &stmt.kind {
131                        rustc_middle::mir::StatementKind::Assign((lhs, rhs))
132                            if *lhs == place_switched_on =>
133                        {
134                            match rhs {
135                                Rvalue::BinaryOp(op @ (BinOp::Eq | BinOp::Ne), (left, right)) => {
136                                    let (branch_value_scalar, branch_value_ty, to_switch_on) =
137                                        find_branch_value_info(left, right, ssa)?;
138
139                                    // The transformation adds a use of `to_switch_on` at the
140                                    // terminator. Both storage markers make the local uninitialized,
141                                    // so either invalidates the value used by the comparison.
142                                    if bb.statements[stmt_idx + 1..].iter().any(|stmt| {
143                                        matches!(
144                                            stmt.kind,
145                                            StatementKind::StorageLive(local)
146                                                | StatementKind::StorageDead(local)
147                                                if local == to_switch_on.local
148                                        )
149                                    }) {
150                                        return None;
151                                    }
152
153                                    Some(OptimizationInfo {
154                                        bin_op_stmt_idx: stmt_idx,
155                                        bb_idx,
156                                        to_switch_on,
157                                        branch_value_scalar,
158                                        branch_value_ty,
159                                        op: *op,
160                                        targets: targets.clone(),
161                                    })
162                                }
163                                _ => None,
164                            }
165                        }
166                        _ => None,
167                    }
168                })
169            })
170            .collect()
171    }
172}
173
174fn find_branch_value_info<'tcx>(
175    left: &Operand<'tcx>,
176    right: &Operand<'tcx>,
177    ssa: &SsaLocals,
178) -> Option<(Scalar, Ty<'tcx>, Place<'tcx>)> {
179    // check that either left or right is a constant.
180    // if any are, we can use the other to switch on, and the constant as a value in a switch
181    use Operand::*;
182    match (left, right) {
183        (Constant(branch_value), Copy(to_switch_on) | Move(to_switch_on))
184        | (Copy(to_switch_on) | Move(to_switch_on), Constant(branch_value)) => {
185            // Make sure that the place is not modified.
186            if !ssa.is_ssa(to_switch_on.local) || !to_switch_on.is_stable_offset() {
187                return None;
188            }
189            let branch_value_ty = branch_value.const_.ty();
190            // we only want to apply this optimization if we are matching on integrals (and chars),
191            // as it is not possible to switch on floats
192            if !branch_value_ty.is_integral() && !branch_value_ty.is_char() {
193                return None;
194            };
195            let branch_value_scalar = branch_value.const_.try_to_scalar()?;
196            Some((branch_value_scalar, branch_value_ty, *to_switch_on))
197        }
198        _ => None,
199    }
200}
201
202#[derive(Debug)]
203struct OptimizationInfo<'tcx> {
204    /// Basic block to apply the optimization
205    bb_idx: BasicBlock,
206    /// Statement index of Eq/Ne assignment
207    bin_op_stmt_idx: usize,
208    /// Place that needs to be switched on. This place is of type integral
209    to_switch_on: Place<'tcx>,
210    /// Constant to use in switch target value
211    branch_value_scalar: Scalar,
212    /// Type of the constant value
213    branch_value_ty: Ty<'tcx>,
214    /// Either Eq or Ne
215    op: BinOp,
216    /// Current targets used in the switch
217    targets: SwitchTargets,
218}