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