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}