rustc_mir_transform/early_otherwise_branch.rs
1use std::fmt::Debug;
2
3use rustc_middle::mir::*;
4use rustc_middle::ty::{Ty, TyCtxt};
5use tracing::trace;
6
7use super::simplify::simplify_cfg;
8use crate::PassPolicy;
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 policy(&self, ctx: &crate::PassCtx<'_>) -> PassPolicy {
98 PassPolicy::optional(ctx.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 }),
178 bbs[parent].is_cleanup,
179 );
180
181 let eq_bb = patch.new_block(eq_switch);
182
183 // Jump to it on the basis of the inequality comparison
184 let true_case = opt_data.destination;
185 let false_case = eq_bb;
186 patch.patch_terminator(
187 parent,
188 TerminatorKind::if_(Operand::Move(Place::from(comp_temp)), true_case, false_case),
189 );
190
191 patch.apply(body);
192 }
193
194 // Since this optimization adds new basic blocks and invalidates others,
195 // clean up the cfg to make it nicer for other passes
196 if should_cleanup {
197 simplify_cfg(tcx, body);
198 }
199 }
200}
201
202#[derive(Debug)]
203struct OptimizationData<'tcx> {
204 destination: BasicBlock,
205 child_place: Place<'tcx>,
206 child_ty: Ty<'tcx>,
207 child_source: SourceInfo,
208 need_hoist_discriminant: bool,
209}
210
211fn evaluate_candidate<'tcx>(
212 tcx: TyCtxt<'tcx>,
213 body: &Body<'tcx>,
214 parent: BasicBlock,
215) -> Option<OptimizationData<'tcx>> {
216 let bbs = &body.basic_blocks;
217 // NB: If this BB is a cleanup, we may need to figure out what else needs to be handled.
218 if bbs[parent].is_cleanup {
219 return None;
220 }
221 let TerminatorKind::SwitchInt { targets, discr: parent_discr } = &bbs[parent].terminator().kind
222 else {
223 return None;
224 };
225 let parent_ty = parent_discr.ty(body.local_decls(), tcx);
226 let (_, child) = targets.iter().next()?;
227
228 let Terminator {
229 kind: TerminatorKind::SwitchInt { targets: child_targets, discr: child_discr },
230 source_info,
231 } = bbs[child].terminator()
232 else {
233 return None;
234 };
235 let child_ty = child_discr.ty(body.local_decls(), tcx);
236 if child_ty != parent_ty {
237 return None;
238 }
239
240 // For now, we only handle:
241 // ```
242 // bb4: {
243 // _8 = discriminant((_3.1: Enum1));
244 // switchInt(move _8) -> [2: bb7, otherwise: bb1];
245 // }
246 // ```
247 // and
248 // ```
249 // bb2: {
250 // switchInt((_3.1: u64)) -> [1: bb5, otherwise: bb1];
251 // }
252 // ```
253 if bbs[child].statements.len() > 1 {
254 return None;
255 }
256
257 // When thie BB has exactly one statement, this statement should be discriminant.
258 let need_hoist_discriminant = bbs[child].statements.len() == 1;
259 let otherwise_is_empty_unreachable = bbs[targets.otherwise()].is_empty_unreachable();
260 let child_place = if need_hoist_discriminant {
261 // Handle:
262 // ```
263 // bb4: {
264 // _8 = discriminant((_3.1: Enum1));
265 // switchInt(move _8) -> [2: bb7, otherwise: bb1];
266 // }
267 // ```
268 let [
269 Statement {
270 kind: StatementKind::Assign((_, Rvalue::Discriminant(child_place))), ..
271 },
272 ] = bbs[child].statements.as_slice()
273 else {
274 return None;
275 };
276 *child_place
277 } else {
278 // Handle:
279 // ```
280 // bb2: {
281 // switchInt((_3.1: u64)) -> [1: bb5, otherwise: bb1];
282 // }
283 // ```
284 let Operand::Copy(child_place) = child_discr else {
285 return None;
286 };
287 *child_place
288 };
289 let destination = if otherwise_is_empty_unreachable {
290 child_targets.otherwise()
291 } else {
292 targets.otherwise()
293 };
294
295 // Verify that the optimization is legal for each branch
296 for (value, child) in targets.iter() {
297 if !verify_candidate_branch(
298 &bbs[child],
299 value,
300 child_place,
301 destination,
302 need_hoist_discriminant,
303 otherwise_is_empty_unreachable,
304 ) {
305 return None;
306 }
307 }
308 Some(OptimizationData {
309 destination,
310 child_place,
311 child_ty,
312 child_source: *source_info,
313 need_hoist_discriminant,
314 })
315}
316
317fn verify_candidate_branch<'tcx>(
318 branch: &BasicBlockData<'tcx>,
319 value: u128,
320 place: Place<'tcx>,
321 destination: BasicBlock,
322 need_hoist_discriminant: bool,
323 otherwise_is_empty_unreachable: bool,
324) -> bool {
325 // In order for the optimization to be correct, the terminator must be a `SwitchInt`.
326 let TerminatorKind::SwitchInt { discr: switch_op, targets } = &branch.terminator().kind else {
327 return false;
328 };
329 if !otherwise_is_empty_unreachable {
330 // Someone could write code like this:
331 // ```rust
332 // let Q = val;
333 // if discriminant(P) == otherwise {
334 // let ptr = &mut Q as *mut _ as *mut u8;
335 // // It may be difficult for us to effectively determine whether values are valid.
336 // // Invalid values can come from all sorts of corners.
337 // unsafe { *ptr = 10; }
338 // }
339 //
340 // match P {
341 // A => match Q {
342 // A => {
343 // // code
344 // }
345 // _ => {
346 // // don't use Q
347 // }
348 // }
349 // _ => {
350 // // don't use Q
351 // }
352 // };
353 // ```
354 //
355 // Hoisting the `discriminant(Q)` out of the `A` arm causes us to compute the discriminant of an
356 // invalid value, which is UB.
357 // In order to fix this, **we would either need to show that the discriminant computation of
358 // `place` is computed in all branches**.
359 // For <https://github.com/rust-lang/rust/issues/95162>, we adopt a conservative approach and
360 // consider only the `otherwise` branch has no statements and an unreachable terminator.
361 if need_hoist_discriminant {
362 return false;
363 }
364 // For <https://github.com/rust-lang/rust/issues/159591>:
365 // ```
366 // bb0: {
367 // switchInt(copy _1) -> [1: bb1, 2: bb2, otherwise: bb5];
368 // }
369 // bb1: {
370 // switchInt(copy (*_2)) -> [1: bb3, otherwise: bb5];
371 // }
372 // bb2: {
373 // switchInt(copy (*_2)) -> [2: bb4, otherwise: bb5];
374 // }
375 // ```
376 // We cannot hoist the dereference of `_2` to `bb0`,
377 // because execution can reach `bb5` without dereferencing `_2`.
378 if let Some(place) = switch_op.place()
379 && !place.is_stable_offset()
380 {
381 return false;
382 }
383 }
384 if need_hoist_discriminant {
385 // If we need hoist discriminant, the branch must have exactly one statement.
386 let [statement] = branch.statements.as_slice() else {
387 return false;
388 };
389 // The statement must assign the discriminant of `place`.
390 let StatementKind::Assign((discr_place, Rvalue::Discriminant(from_place))) = statement.kind
391 else {
392 return false;
393 };
394 if from_place != place {
395 return false;
396 }
397 // The assignment must invalidate a local that terminate on a `SwitchInt`.
398 if !discr_place.projection.is_empty() || *switch_op != Operand::Move(discr_place) {
399 return false;
400 }
401 } else {
402 // If we don't need hoist discriminant, the branch must not have any statements.
403 if !branch.statements.is_empty() {
404 return false;
405 }
406 // The place on `SwitchInt` must be the same.
407 if *switch_op != Operand::Copy(place) {
408 return false;
409 }
410 }
411 // It must fall through to `destination` if the switch misses.
412 if destination != targets.otherwise() {
413 return false;
414 }
415 // It must have exactly one branch for value `value` and have no more branches.
416 let mut iter = targets.iter();
417 let (Some((target_value, _)), None) = (iter.next(), iter.next()) else {
418 return false;
419 };
420 target_value == value
421}