Skip to main content

rustc_mir_transform/
early_otherwise_branch.rs

1use std::fmt::Debug;
2
3use rustc_data_structures::thin_vec::ThinVec;
4use rustc_middle::mir::*;
5use rustc_middle::ty::{Ty, TyCtxt};
6use tracing::trace;
7
8use super::simplify::simplify_cfg;
9use crate::PassPolicy;
10use crate::patch::MirPatch;
11
12/// This pass optimizes something like
13/// ```ignore (syntax-highlighting-only)
14/// let x: Option<()>;
15/// let y: Option<()>;
16/// match (x,y) {
17///     (Some(_), Some(_)) => {0},
18///     (None, None) => {2},
19///     _ => {1}
20/// }
21/// ```
22/// into something like
23/// ```ignore (syntax-highlighting-only)
24/// let x: Option<()>;
25/// let y: Option<()>;
26/// let discriminant_x = std::mem::discriminant(x);
27/// let discriminant_y = std::mem::discriminant(y);
28/// if discriminant_x == discriminant_y {
29///     match x {
30///         Some(_) => 0,
31///         None => 2,
32///     }
33/// } else {
34///     1
35/// }
36/// ```
37///
38/// Specifically, it looks for instances of control flow like this:
39/// ```text
40///
41///     =================
42///     |      BB1      |
43///     |---------------|                  ============================
44///     |     ...       |         /------> |            BBC           |
45///     |---------------|         |        |--------------------------|
46///     |  switchInt(Q) |         |        |   _cl = discriminant(P)  |
47///     |       c       | --------/        |--------------------------|
48///     |       d       | -------\         |       switchInt(_cl)     |
49///     |      ...      |        |         |            c             | ---> BBC.2
50///     |    otherwise  | --\    |    /--- |         otherwise        |
51///     =================   |    |    |    ============================
52///                         |    |    |
53///     =================   |    |    |
54///     |      BBU      | <-|    |    |    ============================
55///     |---------------|        \-------> |            BBD           |
56///     |---------------|             |    |--------------------------|
57///     |  unreachable  |             |    |   _dl = discriminant(P)  |
58///     =================             |    |--------------------------|
59///                                   |    |       switchInt(_dl)     |
60///     =================             |    |            d             | ---> BBD.2
61///     |      BB9      | <--------------- |         otherwise        |
62///     |---------------|                  ============================
63///     |      ...      |
64///     =================
65/// ```
66/// Where the `otherwise` branch on `BB1` is permitted to either go to `BBU`. In the
67/// code:
68///  - `BB1` is `parent` and `BBC, BBD` are children
69///  - `P` is `child_place`
70///  - `child_ty` is the type of `_cl`.
71///  - `Q` is `parent_op`.
72///  - `parent_ty` is the type of `Q`.
73///  - `BB9` is `destination`
74/// All this is then transformed into:
75/// ```text
76///
77///     =======================
78///     |          BB1        |
79///     |---------------------|                  ============================
80///     |          ...        |         /------> |           BBEq           |
81///     | _s = discriminant(P)|         |        |--------------------------|
82///     | _t = Ne(Q, _s)      |         |        |--------------------------|
83///     |---------------------|         |        |       switchInt(Q)       |
84///     |     switchInt(_t)   |         |        |            c             | ---> BBC.2
85///     |        false        | --------/        |            d             | ---> BBD.2
86///     |       otherwise     |       /--------- |         otherwise        |
87///     =======================       |          ============================
88///                                   |
89///     =================             |
90///     |      BB9      | <-----------/
91///     |---------------|
92///     |      ...      |
93///     =================
94/// ```
95pub(super) struct EarlyOtherwiseBranch;
96
97impl<'tcx> crate::MirPass<'tcx> for EarlyOtherwiseBranch {
98    fn policy(&self, sess: &rustc_session::Session) -> PassPolicy {
99        PassPolicy::optimization(sess.mir_opt_level() >= 2)
100    }
101
102    fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
103        trace!("running EarlyOtherwiseBranch on {:?}", body.source);
104
105        let mut should_cleanup = false;
106
107        // Also consider newly generated bbs in the same pass
108        for parent in body.basic_blocks.indices() {
109            let bbs = &*body.basic_blocks;
110            let Some(opt_data) = evaluate_candidate(tcx, body, parent) else { continue };
111
112            trace!("SUCCESS: found optimization possibility to apply: {opt_data:?}");
113
114            should_cleanup = true;
115
116            let TerminatorKind::SwitchInt { discr: parent_op, targets: parent_targets } =
117                &bbs[parent].terminator().kind
118            else {
119                unreachable!()
120            };
121            // Always correct since we can only switch on `Copy` types
122            let parent_op = parent_op.to_copy();
123            let parent_ty = parent_op.ty(body.local_decls(), tcx);
124            let statements_before = bbs[parent].statements.len();
125            let parent_end = Location { block: parent, statement_index: statements_before };
126
127            let mut patch = MirPatch::new(body);
128
129            let second_operand = if opt_data.need_hoist_discriminant {
130                // create temp to store second discriminant in, `_s` in example above
131                let second_discriminant_temp =
132                    patch.new_temp(opt_data.child_ty, opt_data.child_source.span);
133
134                // create assignment of discriminant
135                patch.add_assign(
136                    parent_end,
137                    Place::from(second_discriminant_temp),
138                    Rvalue::Discriminant(opt_data.child_place),
139                );
140                Operand::Move(Place::from(second_discriminant_temp))
141            } else {
142                Operand::Copy(opt_data.child_place)
143            };
144
145            // create temp to store inequality comparison between the two discriminants, `_t` in
146            // example above
147            let nequal = BinOp::Ne;
148            let comp_res_type = nequal.ty(tcx, parent_ty, opt_data.child_ty);
149            let comp_temp = patch.new_temp(comp_res_type, opt_data.child_source.span);
150
151            // create inequality comparison
152            let comp_rvalue =
153                Rvalue::BinaryOp(nequal, Box::new((parent_op.clone(), second_operand)));
154            patch.add_statement(
155                parent_end,
156                StatementKind::Assign(Box::new((Place::from(comp_temp), comp_rvalue))),
157            );
158
159            let eq_new_targets = parent_targets.iter().map(|(value, child)| {
160                let TerminatorKind::SwitchInt { targets, .. } = &bbs[child].terminator().kind
161                else {
162                    unreachable!()
163                };
164                (value, targets.target_for_value(value))
165            });
166            // The otherwise either is the same target branch or an unreachable.
167            let eq_targets = SwitchTargets::new(eq_new_targets, parent_targets.otherwise());
168
169            // Create `bbEq` in example above
170            let eq_switch = BasicBlockData::new(
171                Some(Terminator {
172                    source_info: bbs[parent].terminator().source_info,
173                    kind: TerminatorKind::SwitchInt {
174                        // switch on the first discriminant, so we can mark the second one as dead
175                        discr: parent_op,
176                        targets: eq_targets,
177                    },
178                    attributes: ThinVec::new(),
179                }),
180                bbs[parent].is_cleanup,
181            );
182
183            let eq_bb = patch.new_block(eq_switch);
184
185            // Jump to it on the basis of the inequality comparison
186            let true_case = opt_data.destination;
187            let false_case = eq_bb;
188            patch.patch_terminator(
189                parent,
190                TerminatorKind::if_(Operand::Move(Place::from(comp_temp)), true_case, false_case),
191            );
192
193            patch.apply(body);
194        }
195
196        // Since this optimization adds new basic blocks and invalidates others,
197        // clean up the cfg to make it nicer for other passes
198        if should_cleanup {
199            simplify_cfg(tcx, body);
200        }
201    }
202}
203
204#[derive(Debug)]
205struct OptimizationData<'tcx> {
206    destination: BasicBlock,
207    child_place: Place<'tcx>,
208    child_ty: Ty<'tcx>,
209    child_source: SourceInfo,
210    need_hoist_discriminant: bool,
211}
212
213fn evaluate_candidate<'tcx>(
214    tcx: TyCtxt<'tcx>,
215    body: &Body<'tcx>,
216    parent: BasicBlock,
217) -> Option<OptimizationData<'tcx>> {
218    let bbs = &body.basic_blocks;
219    // NB: If this BB is a cleanup, we may need to figure out what else needs to be handled.
220    if bbs[parent].is_cleanup {
221        return None;
222    }
223    let TerminatorKind::SwitchInt { targets, discr: parent_discr } = &bbs[parent].terminator().kind
224    else {
225        return None;
226    };
227    let parent_ty = parent_discr.ty(body.local_decls(), tcx);
228    let (_, child) = targets.iter().next()?;
229
230    let Terminator {
231        kind: TerminatorKind::SwitchInt { targets: child_targets, discr: child_discr },
232        source_info,
233        attributes: _,
234    } = bbs[child].terminator()
235    else {
236        return None;
237    };
238    let child_ty = child_discr.ty(body.local_decls(), tcx);
239    if child_ty != parent_ty {
240        return None;
241    }
242
243    // For now, we only handle:
244    // ```
245    // bb4: {
246    //     _8 = discriminant((_3.1: Enum1));
247    //    switchInt(move _8) -> [2: bb7, otherwise: bb1];
248    // }
249    // ```
250    // and
251    // ```
252    // bb2: {
253    //     switchInt((_3.1: u64)) -> [1: bb5, otherwise: bb1];
254    // }
255    // ```
256    if bbs[child].statements.len() > 1 {
257        return None;
258    }
259
260    // When thie BB has exactly one statement, this statement should be discriminant.
261    let need_hoist_discriminant = bbs[child].statements.len() == 1;
262    let otherwise_is_empty_unreachable = bbs[targets.otherwise()].is_empty_unreachable();
263    let child_place = if need_hoist_discriminant {
264        // Handle:
265        // ```
266        // bb4: {
267        //     _8 = discriminant((_3.1: Enum1));
268        //    switchInt(move _8) -> [2: bb7, otherwise: bb1];
269        // }
270        // ```
271        let [
272            Statement {
273                kind: StatementKind::Assign((_, Rvalue::Discriminant(child_place))), ..
274            },
275        ] = bbs[child].statements.as_slice()
276        else {
277            return None;
278        };
279        *child_place
280    } else {
281        // Handle:
282        // ```
283        // bb2: {
284        //     switchInt((_3.1: u64)) -> [1: bb5, otherwise: bb1];
285        // }
286        // ```
287        let Operand::Copy(child_place) = child_discr else {
288            return None;
289        };
290        *child_place
291    };
292    let destination = if otherwise_is_empty_unreachable {
293        child_targets.otherwise()
294    } else {
295        targets.otherwise()
296    };
297
298    // Verify that the optimization is legal for each branch
299    for (value, child) in targets.iter() {
300        if !verify_candidate_branch(
301            &bbs[child],
302            value,
303            child_place,
304            destination,
305            need_hoist_discriminant,
306            otherwise_is_empty_unreachable,
307        ) {
308            return None;
309        }
310    }
311    Some(OptimizationData {
312        destination,
313        child_place,
314        child_ty,
315        child_source: *source_info,
316        need_hoist_discriminant,
317    })
318}
319
320fn verify_candidate_branch<'tcx>(
321    branch: &BasicBlockData<'tcx>,
322    value: u128,
323    place: Place<'tcx>,
324    destination: BasicBlock,
325    need_hoist_discriminant: bool,
326    otherwise_is_empty_unreachable: bool,
327) -> bool {
328    // In order for the optimization to be correct, the terminator must be a `SwitchInt`.
329    let TerminatorKind::SwitchInt { discr: switch_op, targets } = &branch.terminator().kind else {
330        return false;
331    };
332    if !otherwise_is_empty_unreachable {
333        // Someone could write code like this:
334        // ```rust
335        // let Q = val;
336        // if discriminant(P) == otherwise {
337        //     let ptr = &mut Q as *mut _ as *mut u8;
338        //     // It may be difficult for us to effectively determine whether values are valid.
339        //     // Invalid values can come from all sorts of corners.
340        //     unsafe { *ptr = 10; }
341        // }
342        //
343        // match P {
344        //    A => match Q {
345        //        A => {
346        //            // code
347        //        }
348        //        _ => {
349        //            // don't use Q
350        //        }
351        //    }
352        //    _ => {
353        //        // don't use Q
354        //    }
355        // };
356        // ```
357        //
358        // Hoisting the `discriminant(Q)` out of the `A` arm causes us to compute the discriminant of an
359        // invalid value, which is UB.
360        // In order to fix this, **we would either need to show that the discriminant computation of
361        // `place` is computed in all branches**.
362        // For <https://github.com/rust-lang/rust/issues/95162>, we adopt a conservative approach and
363        // consider only the `otherwise` branch has no statements and an unreachable terminator.
364        if need_hoist_discriminant {
365            return false;
366        }
367        // For <https://github.com/rust-lang/rust/issues/159591>:
368        // ```
369        // bb0: {
370        //     switchInt(copy _1) -> [1: bb1, 2: bb2, otherwise: bb5];
371        // }
372        // bb1: {
373        //     switchInt(copy (*_2)) -> [1: bb3, otherwise: bb5];
374        // }
375        // bb2: {
376        //     switchInt(copy (*_2)) -> [2: bb4, otherwise: bb5];
377        // }
378        // ```
379        // We cannot hoist the dereference of `_2` to `bb0`,
380        // because execution can reach `bb5` without dereferencing `_2`.
381        if let Some(place) = switch_op.place()
382            && !place.is_stable_offset()
383        {
384            return false;
385        }
386    }
387    if need_hoist_discriminant {
388        // If we need hoist discriminant, the branch must have exactly one statement.
389        let [statement] = branch.statements.as_slice() else {
390            return false;
391        };
392        // The statement must assign the discriminant of `place`.
393        let StatementKind::Assign((discr_place, Rvalue::Discriminant(from_place))) = statement.kind
394        else {
395            return false;
396        };
397        if from_place != place {
398            return false;
399        }
400        // The assignment must invalidate a local that terminate on a `SwitchInt`.
401        if !discr_place.projection.is_empty() || *switch_op != Operand::Move(discr_place) {
402            return false;
403        }
404    } else {
405        // If we don't need hoist discriminant, the branch must not have any statements.
406        if !branch.statements.is_empty() {
407            return false;
408        }
409        // The place on `SwitchInt` must be the same.
410        if *switch_op != Operand::Copy(place) {
411            return false;
412        }
413    }
414    // It must fall through to `destination` if the switch misses.
415    if destination != targets.otherwise() {
416        return false;
417    }
418    // It must have exactly one branch for value `value` and have no more branches.
419    let mut iter = targets.iter();
420    let (Some((target_value, _)), None) = (iter.next(), iter.next()) else {
421        return false;
422    };
423    target_value == value
424}