Change:
Reproduction here: There's more info in the readme on that repo. Essentially, I have some code that segfaults while debug-printing an enum. The enum has one variant, followed by 11 empty variants. The repro only creates one of the empty ones, but it appears the debug routine attempts to read the string variant. It occurs in the context of some mutually recursive functions building a fairly simple datastructure by walking another. I have tried hard to minimise the repro but it could be better, but it's a bit fragile so I don't know what else I can remove. With a quick glance at the commit that bisect-rustc found, it looks like it could be related to optimising match arms, so that could be a start. Edit: for some linkage, the commit below merges , which 're-enables SimplifyArmIdentity'. searched nightlies: from nightly-2020-05-08 to nightly-2020-09-23 regressed nightly: nightly-2020-09-09 searched commits: from to regressed commit: <details<summarybisected with <a href=''cargo-bisect-rustc</av0.5.2</summaryHost triple: x8664-apple-darwin Reproduce with: </detailsNote that the function is the one in which the debug segfaults, and it is debugging a larger structure. I have also seen it segfault when cloning just the enum, which is the String+11x one mentioned above. <details<summaryBacktrace from rust-lldb</summary<p</p</details
Can't reproduce with .
I don’t have MacOS, so I can’t test this myself easily. Here’s some tags though: modify labels: O-macos, T-compiler, E-needs-mcve. And someone might wanna add I-unsound. Edit: To clarify, I’m not seeing any segfault on Windows or Linux (both 2020-09-27). Oddly enough, I’m getting on Windows, and on Linux. And I get without on both (also with on or or in , for that matter). Is this program supposed to be deterministic? Perhaps you could test your less minimized version on Linux or Windows if you have access to those (since you said “the segfault started to change a bit”, so maybe the original version also segfaults on e.g. Linux).
Yes, the program is deterministic. I have seen similar wrong variants as well in the original code on macOS, so I'll go out in a limb and say that's the same bug on Linux. I can run a docker image if you like but I think you've already found it. The fragility I was referring to was in terms of "add a debug statement or delete some line, the issue goes away or pops up on a different input instead". But once you have it it produces the same segfault again and again. Given the complexity of the repro I'm not surprised that platform differences including calling conventions etc would affect reproducibility. I did run in Miri and it didn't find anything amiss, and produced correct output, ie the full .
Yeah I should have made it more obvious about macOS. I can still reproduce with:
Okay, I guess if it is supposed to be deterministic, this means I can then confirm the problem for linux and windows. I’m currently trying to reduce the example further, keeping the different behavior between and debug on linux. modify labels: -O-macos
Just got a segfault on Linux with a reduced example.
SimplifyArmIdentity moves an assignment to a local before StorageLive: cc
The label might also be appropriate.
A slight modification to repro and it will segfault in release mode: I can open a PR disabling this optimization so this doesn't hit beta next week.
Marking this as P-critical based on the
We can probably turn bugs like this into ICEs by extending the MIR validator (unless another optimization interferes before this hits codegen)
Because I didn’t feel like aborting my own minimization attempt, here’s the (heavily) reduced version of OPs example that I got: Causing a segfault on linux with nightly.
Your modified example does not consistently segfault. It only does so for nightlies newer than or so. The difference between and being printed does however start from the same commit that OPs bisection identified. (Actually it does not “segfault” at all but gives me an “invalid pointer” error.)
That sounds about right to me since should have been in nightly 2020-09-26. Prior to that PR, the mis-optimization was disabled in which should have been in nightly-2020-09-19 or so. Between ~2020-09-19 and ~2020-09-26 the issue should not appear. Does your repro work on the playground? When I try it with the nightly available, it works correctly.
Opened with an implementation of this. It does indeed catch this miscompilation.
It doesn’t seem to work in the playground. Even the (mostly) untouched original code that OP provided . (I.e. it correctly displays .) To demonstrate some online reproduction: (exit code 139).
I can reproduce with based on one of the MCVEs: stderr stdout
Perhaps change the title of the issue to "... in release mode"?
Is this still open so that the underlying issue is fixed? (The PR to temporarily disable the optimization was merged.)
We should leave this issue open to track fixing the optimization but we can remove some of the tags. Once a nightly goes out with the fix, we should also confirm it's no longer repros on the nightly channel. (I was going to wait for that confirmation before changing any labels)
Both repros work correctly on the latest nightly (2020-10-02), so I'm adjusting the tags accordingly. Thanks for reporting this issue!
Did you mean to remove ?
Yeah

Discussion:
pub mod simplify; mod simplify_branches; mod simplify_comparison_integral; <del> mod simplify_try; </del> mod sroa; mod uninhabited_enum_branching; mod unreachable_prop;

&o1(simplify_branches::SimplifyConstCondition::new("after-const-prop")), &early_otherwise_branch::EarlyOtherwiseBranch, &simplify_comparison_integral::SimplifyComparisonIntegral, <del> &simplify_try::SimplifyArmIdentity, &simplify_try::SimplifyBranchSame, </del> &dead_store_elimination::DeadStoreElimination, &dest_prop::DestinationPropagation, &o1(simplify_branches::SimplifyConstCondition::new("final")),

<del> //! The general point of the optimizations provided here is to simplify something like: //! //! ```rust //! # fn foo<T, E>(x: Result<T, E>) -> Result<T, E> { //! match x { //!     Ok(x) => Ok(x), //!     Err(x) => Err(x) //! } //! # } //! ``` //! //! into just `x`. use crate::{simplify, MirPass}; use itertools::Itertools as _; use rustc_index::{bit_set::BitSet, vec::IndexVec}; use rustc_middle::mir::visit::{NonUseContext, PlaceContext, Visitor}; use rustc_middle::mir::*; use rustc_middle::ty::{self, List, Ty, TyCtxt}; use rustc_target::abi::VariantIdx; use std::iter::{once, Enumerate, Peekable}; use std::slice::Iter; /// Simplifies arms of form `Variant(x) => Variant(x)` to just a move. /// /// This is done by transforming basic blocks where the statements match: /// /// ```ignore (MIR) /// _LOCAL_TMP = ((_LOCAL_1 as Variant ).FIELD: TY ); /// _TMP_2 = _LOCAL_TMP; /// ((_LOCAL_0 as Variant).FIELD: TY) = move _TMP_2; /// discriminant(_LOCAL_0) = VAR_IDX; /// ``` /// /// into: /// /// ```ignore (MIR) /// _LOCAL_0 = move _LOCAL_1 /// ``` pub struct SimplifyArmIdentity; #[derive(Debug)] struct ArmIdentityInfo<'tcx> { /// Storage location for the variant's field local_temp_0: Local, /// Storage location holding the variant being read from local_1: Local, /// The variant field being read from vf_s0: VarField<'tcx>, /// Index of the statement which loads the variant being read get_variant_field_stmt: usize, /// Tracks each assignment to a temporary of the variant's field field_tmp_assignments: Vec<(Local, Local)>, /// Storage location holding the variant's field that was read from local_tmp_s1: Local, /// Storage location holding the enum that we are writing to local_0: Local, /// The variant field being written to vf_s1: VarField<'tcx>, /// Storage location that the discriminant is being written to set_discr_local: Local, /// The variant being written set_discr_var_idx: VariantIdx, /// Index of the statement that should be overwritten as a move stmt_to_overwrite: usize, /// SourceInfo for the new move source_info: SourceInfo, /// Indices of matching Storage{Live,Dead} statements encountered. /// (StorageLive index,, StorageDead index, Local) storage_stmts: Vec<(usize, usize, Local)>, /// The statements that should be removed (turned into nops) stmts_to_remove: Vec<usize>, /// Indices of debug variables that need to be adjusted to point to // `{local_0}.{dbg_projection}`. dbg_info_to_adjust: Vec<usize>, /// The projection used to rewrite debug info. dbg_projection: &'tcx List<PlaceElem<'tcx>>, } fn get_arm_identity_info<'a, 'tcx>( stmts: &'a [Statement<'tcx>], locals_count: usize, debug_info: &'a [VarDebugInfo<'tcx>], ) -> Option<ArmIdentityInfo<'tcx>> { // This can't possibly match unless there are at least 3 statements in the block // so fail fast on tiny blocks. if stmts.len() < 3 { return None; } let mut tmp_assigns = Vec::new(); let mut nop_stmts = Vec::new(); let mut storage_stmts = Vec::new(); let mut storage_live_stmts = Vec::new(); let mut storage_dead_stmts = Vec::new(); type StmtIter<'a, 'tcx> = Peekable<Enumerate<Iter<'a, Statement<'tcx>>>>; fn is_storage_stmt(stmt: &Statement<'_>) -> bool { matches!(stmt.kind, StatementKind::StorageLive(_) | StatementKind::StorageDead(_)) } /// Eats consecutive Statements which match `test`, performing the specified `action` for each. /// The iterator `stmt_iter` is not advanced if none were matched. fn try_eat<'a, 'tcx>( stmt_iter: &mut StmtIter<'a, 'tcx>, test: impl Fn(&'a Statement<'tcx>) -> bool, mut action: impl FnMut(usize, &'a Statement<'tcx>), ) { while stmt_iter.peek().map_or(false, |(_, stmt)| test(stmt)) { let (idx, stmt) = stmt_iter.next().unwrap(); action(idx, stmt); } } /// Eats consecutive `StorageLive` and `StorageDead` Statements. /// The iterator `stmt_iter` is not advanced if none were found. fn try_eat_storage_stmts( stmt_iter: &mut StmtIter<'_, '_>, storage_live_stmts: &mut Vec<(usize, Local)>, storage_dead_stmts: &mut Vec<(usize, Local)>, ) { try_eat(stmt_iter, is_storage_stmt, |idx, stmt| { if let StatementKind::StorageLive(l) = stmt.kind { storage_live_stmts.push((idx, l)); } else if let StatementKind::StorageDead(l) = stmt.kind { storage_dead_stmts.push((idx, l)); } }) } fn is_tmp_storage_stmt(stmt: &Statement<'_>) -> bool { use rustc_middle::mir::StatementKind::Assign; if let Assign(box (place, Rvalue::Use(Operand::Copy(p) | Operand::Move(p)))) = &stmt.kind { place.as_local().is_some() && p.as_local().is_some() } else { false } } /// Eats consecutive `Assign` Statements. // The iterator `stmt_iter` is not advanced if none were found. fn try_eat_assign_tmp_stmts( stmt_iter: &mut StmtIter<'_, '_>, tmp_assigns: &mut Vec<(Local, Local)>, nop_stmts: &mut Vec<usize>, ) { try_eat(stmt_iter, is_tmp_storage_stmt, |idx, stmt| { use rustc_middle::mir::StatementKind::Assign; if let Assign(box (place, Rvalue::Use(Operand::Copy(p) | Operand::Move(p)))) = &stmt.kind { tmp_assigns.push((place.as_local().unwrap(), p.as_local().unwrap())); nop_stmts.push(idx); } }) } fn find_storage_live_dead_stmts_for_local( local: Local, stmts: &[Statement<'_>], ) -> Option<(usize, usize)> { trace!("looking for {:?}", local); let mut storage_live_stmt = None; let mut storage_dead_stmt = None; for (idx, stmt) in stmts.iter().enumerate() { if stmt.kind == StatementKind::StorageLive(local) { storage_live_stmt = Some(idx); } else if stmt.kind == StatementKind::StorageDead(local) { storage_dead_stmt = Some(idx); } } Some((storage_live_stmt?, storage_dead_stmt.unwrap_or(usize::MAX))) } // Try to match the expected MIR structure with the basic block we're processing. // We want to see something that looks like: // ``` // (StorageLive(_) | StorageDead(_));* // _LOCAL_INTO = ((_LOCAL_FROM as Variant).FIELD: TY); // (StorageLive(_) | StorageDead(_));* // (tmp_n+1 = tmp_n);* // (StorageLive(_) | StorageDead(_));* // (tmp_n+1 = tmp_n);* // ((LOCAL_FROM as Variant).FIELD: TY) = move tmp; // discriminant(LOCAL_FROM) = VariantIdx; // (StorageLive(_) | StorageDead(_));* // ``` let mut stmt_iter = stmts.iter().enumerate().peekable(); try_eat_storage_stmts(&mut stmt_iter, &mut storage_live_stmts, &mut storage_dead_stmts); let (get_variant_field_stmt, stmt) = stmt_iter.next()?; let (local_tmp_s0, local_1, vf_s0, dbg_projection) = match_get_variant_field(stmt)?; try_eat_storage_stmts(&mut stmt_iter, &mut storage_live_stmts, &mut storage_dead_stmts); try_eat_assign_tmp_stmts(&mut stmt_iter, &mut tmp_assigns, &mut nop_stmts); try_eat_storage_stmts(&mut stmt_iter, &mut storage_live_stmts, &mut storage_dead_stmts); try_eat_assign_tmp_stmts(&mut stmt_iter, &mut tmp_assigns, &mut nop_stmts); let (idx, stmt) = stmt_iter.next()?; let (local_tmp_s1, local_0, vf_s1) = match_set_variant_field(stmt)?; nop_stmts.push(idx); let (idx, stmt) = stmt_iter.next()?; let (set_discr_local, set_discr_var_idx) = match_set_discr(stmt)?; let discr_stmt_source_info = stmt.source_info; nop_stmts.push(idx); try_eat_storage_stmts(&mut stmt_iter, &mut storage_live_stmts, &mut storage_dead_stmts); for (live_idx, live_local) in storage_live_stmts { if let Some(i) = storage_dead_stmts.iter().rposition(|(_, l)| *l == live_local) { let (dead_idx, _) = storage_dead_stmts.swap_remove(i); storage_stmts.push((live_idx, dead_idx, live_local)); if live_local == local_tmp_s0 { nop_stmts.push(get_variant_field_stmt); } } } // We sort primitive usize here so we can use unstable sort nop_stmts.sort_unstable(); // Use one of the statements we're going to discard between the point // where the storage location for the variant field becomes live and // is killed. let (live_idx, dead_idx) = find_storage_live_dead_stmts_for_local(local_tmp_s0, stmts)?; let stmt_to_overwrite = nop_stmts.iter().find(|stmt_idx| live_idx < **stmt_idx && **stmt_idx < dead_idx); let mut tmp_assigned_vars = BitSet::new_empty(locals_count); for (l, r) in &tmp_assigns { tmp_assigned_vars.insert(*l); tmp_assigned_vars.insert(*r); } let dbg_info_to_adjust: Vec<_> = debug_info .iter() .enumerate() .filter_map(|(i, var_info)| { if let VarDebugInfoContents::Place(p) = var_info.value { if tmp_assigned_vars.contains(p.local) { return Some(i); } } None }) .collect(); Some(ArmIdentityInfo { local_temp_0: local_tmp_s0, local_1, vf_s0, get_variant_field_stmt, field_tmp_assignments: tmp_assigns, local_tmp_s1, local_0, vf_s1, set_discr_local, set_discr_var_idx, stmt_to_overwrite: *stmt_to_overwrite?, source_info: discr_stmt_source_info, storage_stmts, stmts_to_remove: nop_stmts, dbg_info_to_adjust, dbg_projection, }) } fn optimization_applies<'tcx>( opt_info: &ArmIdentityInfo<'tcx>, local_decls: &IndexVec<Local, LocalDecl<'tcx>>, local_uses: &IndexVec<Local, usize>, var_debug_info: &[VarDebugInfo<'tcx>], ) -> bool { trace!("testing if optimization applies..."); // FIXME(wesleywiser): possibly relax this restriction? if opt_info.local_0 == opt_info.local_1 { trace!("NO: moving into ourselves"); return false; } else if opt_info.vf_s0 != opt_info.vf_s1 { trace!("NO: the field-and-variant information do not match"); return false; } else if local_decls[opt_info.local_0].ty != local_decls[opt_info.local_1].ty { // FIXME(Centril,oli-obk): possibly relax to same layout? trace!("NO: source and target locals have different types"); return false; } else if (opt_info.local_0, opt_info.vf_s0.var_idx) != (opt_info.set_discr_local, opt_info.set_discr_var_idx) { trace!("NO: the discriminants do not match"); return false; } // Verify the assignment chain consists of the form b = a; c = b; d = c; etc... if opt_info.field_tmp_assignments.is_empty() { trace!("NO: no assignments found"); return false; } let mut last_assigned_to = opt_info.field_tmp_assignments[0].1; let source_local = last_assigned_to; for (l, r) in &opt_info.field_tmp_assignments { if *r != last_assigned_to { trace!("NO: found unexpected assignment {:?} = {:?}", l, r); return false; } last_assigned_to = *l; } // Check that the first and last used locals are only used twice // since they are of the form: // // ``` // _first = ((_x as Variant).n: ty); // _n = _first; // ... // ((_y as Variant).n: ty) = _n; // discriminant(_y) = z; // ``` for (l, r) in &opt_info.field_tmp_assignments { if local_uses[*l] != 2 { warn!("NO: FAILED assignment chain local {:?} was used more than twice", l); return false; } else if local_uses[*r] != 2 { warn!("NO: FAILED assignment chain local {:?} was used more than twice", r); return false; } } // Check that debug info only points to full Locals and not projections. for dbg_idx in &opt_info.dbg_info_to_adjust { let dbg_info = &var_debug_info[*dbg_idx]; if let VarDebugInfoContents::Place(p) = dbg_info.value { if !p.projection.is_empty() { trace!("NO: debug info for {:?} had a projection {:?}", dbg_info.name, p); return false; } } } if source_local != opt_info.local_temp_0 { trace!( "NO: start of assignment chain does not match enum variant temp: {:?} != {:?}", source_local, opt_info.local_temp_0 ); return false; } else if last_assigned_to != opt_info.local_tmp_s1 { trace!( "NO: end of assignment chain does not match written enum temp: {:?} != {:?}", last_assigned_to, opt_info.local_tmp_s1 ); return false; } trace!("SUCCESS: optimization applies!"); true } impl<'tcx> MirPass<'tcx> for SimplifyArmIdentity { fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { // FIXME(77359): This optimization can result in unsoundness. if !tcx.sess.opts.unstable_opts.unsound_mir_opts { return; } let source = body.source; trace!("running SimplifyArmIdentity on {:?}", source); let local_uses = LocalUseCounter::get_local_uses(body); for bb in body.basic_blocks.as_mut() { if let Some(opt_info) = get_arm_identity_info(&bb.statements, body.local_decls.len(), &body.var_debug_info) { trace!("got opt_info = {:#?}", opt_info); if !optimization_applies( &opt_info, &body.local_decls, &local_uses, &body.var_debug_info, ) { debug!("optimization skipped for {:?}", source); continue; } // Also remove unused Storage{Live,Dead} statements which correspond // to temps used previously. for (live_idx, dead_idx, local) in &opt_info.storage_stmts { // The temporary that we've read the variant field into is scoped to this block, // so we can remove the assignment. if *local == opt_info.local_temp_0 { bb.statements[opt_info.get_variant_field_stmt].make_nop(); } for (left, right) in &opt_info.field_tmp_assignments { if local == left || local == right { bb.statements[*live_idx].make_nop(); bb.statements[*dead_idx].make_nop(); } } } // Right shape; transform for stmt_idx in opt_info.stmts_to_remove { bb.statements[stmt_idx].make_nop(); } let stmt = &mut bb.statements[opt_info.stmt_to_overwrite]; stmt.source_info = opt_info.source_info; stmt.kind = StatementKind::Assign(Box::new(( opt_info.local_0.into(), Rvalue::Use(Operand::Move(opt_info.local_1.into())), ))); bb.statements.retain(|stmt| stmt.kind != StatementKind::Nop); // Fix the debug info to point to the right local for dbg_index in opt_info.dbg_info_to_adjust { let dbg_info = &mut body.var_debug_info[dbg_index]; assert!( matches!(dbg_info.value, VarDebugInfoContents::Place(_)), "value was not a Place" ); if let VarDebugInfoContents::Place(p) = &mut dbg_info.value { assert!(p.projection.is_empty()); p.local = opt_info.local_0; p.projection = opt_info.dbg_projection; } } trace!("block is now {:?}", bb.statements); } } } } struct LocalUseCounter { local_uses: IndexVec<Local, usize>, } impl LocalUseCounter { fn get_local_uses(body: &Body<'_>) -> IndexVec<Local, usize> { let mut counter = LocalUseCounter { local_uses: IndexVec::from_elem(0, &body.local_decls) }; counter.visit_body(body); counter.local_uses } } impl Visitor<'_> for LocalUseCounter { fn visit_local(&mut self, local: Local, context: PlaceContext, _location: Location) { if context.is_storage_marker() || context == PlaceContext::NonUse(NonUseContext::VarDebugInfo) { return; } self.local_uses[local] += 1; } } /// Match on: /// ```ignore (MIR) /// _LOCAL_INTO = ((_LOCAL_FROM as Variant).FIELD: TY); /// ``` fn match_get_variant_field<'tcx>( stmt: &Statement<'tcx>, ) -> Option<(Local, Local, VarField<'tcx>, &'tcx List<PlaceElem<'tcx>>)> { match &stmt.kind { StatementKind::Assign(box ( place_into, Rvalue::Use(Operand::Copy(pf) | Operand::Move(pf)), )) => { let local_into = place_into.as_local()?; let (local_from, vf) = match_variant_field_place(*pf)?; Some((local_into, local_from, vf, pf.projection)) } _ => None, } } /// Match on: /// ```ignore (MIR) /// ((_LOCAL_FROM as Variant).FIELD: TY) = move _LOCAL_INTO; /// ``` fn match_set_variant_field<'tcx>(stmt: &Statement<'tcx>) -> Option<(Local, Local, VarField<'tcx>)> { match &stmt.kind { StatementKind::Assign(box (place_from, Rvalue::Use(Operand::Move(place_into)))) => { let local_into = place_into.as_local()?; let (local_from, vf) = match_variant_field_place(*place_from)?; Some((local_into, local_from, vf)) } _ => None, } } /// Match on: /// ```ignore (MIR) /// discriminant(_LOCAL_TO_SET) = VAR_IDX; /// ``` fn match_set_discr(stmt: &Statement<'_>) -> Option<(Local, VariantIdx)> { match &stmt.kind { StatementKind::SetDiscriminant { place, variant_index } => { Some((place.as_local()?, *variant_index)) } _ => None, } } #[derive(PartialEq, Debug)] struct VarField<'tcx> { field: Field, field_ty: Ty<'tcx>, var_idx: VariantIdx, } /// Match on `((_LOCAL as Variant).FIELD: TY)`. fn match_variant_field_place(place: Place<'_>) -> Option<(Local, VarField<'_>)> { match place.as_ref() { PlaceRef { local, projection: &[ProjectionElem::Downcast(_, var_idx), ProjectionElem::Field(field, ty)], } => Some((local, VarField { field, field_ty: ty, var_idx })), _ => None, } } /// Simplifies `SwitchInt(_) -> [targets]`, /// where all the `targets` have the same form, /// into `goto -> target_first`. pub struct SimplifyBranchSame; impl<'tcx> MirPass<'tcx> for SimplifyBranchSame { fn run_pass(&self, tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) { // This optimization is disabled by default for now due to // soundness concerns; see issue #89485 and PR #89489. if !tcx.sess.opts.unstable_opts.unsound_mir_opts { return; } trace!("Running SimplifyBranchSame on {:?}", body.source); let finder = SimplifyBranchSameOptimizationFinder { body, tcx }; let opts = finder.find(); let did_remove_blocks = opts.len() > 0; for opt in opts.iter() { trace!("SUCCESS: Applying optimization {:?}", opt); // Replace `SwitchInt(..) -> [bb_first, ..];` with a `goto -> bb_first;`. body.basic_blocks_mut()[opt.bb_to_opt_terminator].terminator_mut().kind = TerminatorKind::Goto { target: opt.bb_to_goto }; } if did_remove_blocks { // We have dead blocks now, so remove those. simplify::remove_dead_blocks(tcx, body); } } } #[derive(Debug)] struct SimplifyBranchSameOptimization { /// All basic blocks are equal so go to this one bb_to_goto: BasicBlock, /// Basic block where the terminator can be simplified to a goto bb_to_opt_terminator: BasicBlock, } struct SwitchTargetAndValue { target: BasicBlock, // None in case of the `otherwise` case value: Option<u128>, } struct SimplifyBranchSameOptimizationFinder<'a, 'tcx> { body: &'a Body<'tcx>, tcx: TyCtxt<'tcx>, } impl<'tcx> SimplifyBranchSameOptimizationFinder<'_, 'tcx> { fn find(&self) -> Vec<SimplifyBranchSameOptimization> { self.body .basic_blocks .iter_enumerated() .filter_map(|(bb_idx, bb)| { let (discr_switched_on, targets_and_values) = match &bb.terminator().kind { TerminatorKind::SwitchInt { targets, discr, .. } => { let targets_and_values: Vec<_> = targets.iter() .map(|(val, target)| SwitchTargetAndValue { target, value: Some(val) }) .chain(once(SwitchTargetAndValue { target: targets.otherwise(), value: None })) .collect(); (discr, targets_and_values) }, _ => return None, }; // find the adt that has its discriminant read // assuming this must be the last statement of the block let adt_matched_on = match &bb.statements.last()?.kind { StatementKind::Assign(box (place, rhs)) if Some(*place) == discr_switched_on.place() => { match rhs { Rvalue::Discriminant(adt_place) if adt_place.ty(self.body, self.tcx).ty.is_enum() => adt_place, _ => { trace!("NO: expected a discriminant read of an enum instead of: {:?}", rhs); return None; } } } other => { trace!("NO: expected an assignment of a discriminant read to a place. Found: {:?}", other); return None }, }; let mut iter_bbs_reachable = targets_and_values .iter() .map(|target_and_value| (target_and_value, &self.body.basic_blocks[target_and_value.target])) .filter(|(_, bb)| { // Reaching `unreachable` is UB so assume it doesn't happen. bb.terminator().kind != TerminatorKind::Unreachable }) .peekable(); let bb_first = iter_bbs_reachable.peek().map_or(&targets_and_values[0], |(idx, _)| *idx); let mut all_successors_equivalent = StatementEquality::TrivialEqual; // All successor basic blocks must be equal or contain statements that are pairwise considered equal. for ((target_and_value_l,bb_l), (target_and_value_r,bb_r)) in iter_bbs_reachable.tuple_windows() { let trivial_checks = bb_l.is_cleanup == bb_r.is_cleanup && bb_l.terminator().kind == bb_r.terminator().kind && bb_l.statements.len() == bb_r.statements.len(); let statement_check = || { bb_l.statements.iter().zip(&bb_r.statements).try_fold(StatementEquality::TrivialEqual, |acc,(l,r)| { let stmt_equality = self.statement_equality(*adt_matched_on, &l, target_and_value_l, &r, target_and_value_r); if matches!(stmt_equality, StatementEquality::NotEqual) { // short circuit None } else { Some(acc.combine(&stmt_equality)) } }) .unwrap_or(StatementEquality::NotEqual) }; if !trivial_checks { all_successors_equivalent = StatementEquality::NotEqual; break; } all_successors_equivalent = all_successors_equivalent.combine(&statement_check()); }; match all_successors_equivalent{ StatementEquality::TrivialEqual => { // statements are trivially equal, so just take first trace!("Statements are trivially equal"); Some(SimplifyBranchSameOptimization { bb_to_goto: bb_first.target, bb_to_opt_terminator: bb_idx, }) } StatementEquality::ConsideredEqual(bb_to_choose) => { trace!("Statements are considered equal"); Some(SimplifyBranchSameOptimization { bb_to_goto: bb_to_choose, bb_to_opt_terminator: bb_idx, }) } StatementEquality::NotEqual => { trace!("NO: not all successors of basic block {:?} were equivalent", bb_idx); None } } }) .collect() } /// Tests if two statements can be considered equal /// /// Statements can be trivially equal if the kinds match. /// But they can also be considered equal in the following case A: /// ```ignore (MIR) /// discriminant(_0) = 0;   // bb1 /// _0 = move _1;           // bb2 /// ``` /// In this case the two statements are equal iff /// - `_0` is an enum where the variant index 0 is fieldless, and /// -  bb1 was targeted by a switch where the discriminant of `_1` was switched on fn statement_equality( &self, adt_matched_on: Place<'tcx>, x: &Statement<'tcx>, x_target_and_value: &SwitchTargetAndValue, y: &Statement<'tcx>, y_target_and_value: &SwitchTargetAndValue, ) -> StatementEquality { let helper = |rhs: &Rvalue<'tcx>, place: &Place<'tcx>, variant_index: VariantIdx, switch_value: u128, side_to_choose| { let place_type = place.ty(self.body, self.tcx).ty; let adt = match *place_type.kind() { ty::Adt(adt, _) if adt.is_enum() => adt, _ => return StatementEquality::NotEqual, }; // We need to make sure that the switch value that targets the bb with // SetDiscriminant is the same as the variant discriminant. let variant_discr = adt.discriminant_for_variant(self.tcx, variant_index).val; if variant_discr != switch_value { trace!( "NO: variant discriminant {} does not equal switch value {}", variant_discr, switch_value ); return StatementEquality::NotEqual; } let variant_is_fieldless = adt.variant(variant_index).fields.is_empty(); if !variant_is_fieldless { trace!("NO: variant {:?} was not fieldless", variant_index); return StatementEquality::NotEqual; } match rhs { Rvalue::Use(operand) if operand.place() == Some(adt_matched_on) => { StatementEquality::ConsideredEqual(side_to_choose) } _ => { trace!( "NO: RHS of assignment was {:?}, but expected it to match the adt being matched on in the switch, which is {:?}", rhs, adt_matched_on ); StatementEquality::NotEqual } } }; match (&x.kind, &y.kind) { // trivial case (x, y) if x == y => StatementEquality::TrivialEqual, // check for case A ( StatementKind::Assign(box (_, rhs)), &StatementKind::SetDiscriminant { ref place, variant_index }, ) if y_target_and_value.value.is_some() => { // choose basic block of x, as that has the assign helper( rhs, place, variant_index, y_target_and_value.value.unwrap(), x_target_and_value.target, ) } ( &StatementKind::SetDiscriminant { ref place, variant_index }, &StatementKind::Assign(box (_, ref rhs)), ) if x_target_and_value.value.is_some() => { // choose basic block of y, as that has the assign helper( rhs, place, variant_index, x_target_and_value.value.unwrap(), y_target_and_value.target, ) } _ => { trace!("NO: statements `{:?}` and `{:?}` not considered equal", x, y); StatementEquality::NotEqual } } } } #[derive(Copy, Clone, Eq, PartialEq)] enum StatementEquality { /// The two statements are trivially equal; same kind TrivialEqual, /// The two statements are considered equal, but may be of different kinds. The BasicBlock field is the basic block to jump to when performing the branch-same optimization. /// For example, `_0 = _1` and `discriminant(_0) = discriminant(0)` are considered equal if 0 is a fieldless variant of an enum. But we don't want to jump to the basic block with the SetDiscriminant, as that is not legal if _1 is not the 0 variant index ConsideredEqual(BasicBlock), /// The two statements are not equal NotEqual, } impl StatementEquality { fn combine(&self, other: &StatementEquality) -> StatementEquality { use StatementEquality::*; match (self, other) { (TrivialEqual, TrivialEqual) => TrivialEqual, (TrivialEqual, ConsideredEqual(b)) | (ConsideredEqual(b), TrivialEqual) => { ConsideredEqual(*b) } (ConsideredEqual(b1), ConsideredEqual(b2)) => { if b1 == b2 { ConsideredEqual(*b1) } else { NotEqual } } (_, NotEqual) | (NotEqual, _) => NotEqual, } } } </del>

<del> - // MIR for `encode` before SimplifyBranchSame + // MIR for `encode` after SimplifyBranchSame fn encode(_1: Type) -> Type { debug v => _1;                       // in scope 0 at $DIR/76803_regression.rs:+0:15: +0:16 let mut _0: Type;                    // return place in scope 0 at $DIR/76803_regression.rs:+0:27: +0:31 let mut _2: isize;                   // in scope 0 at $DIR/76803_regression.rs:+2:9: +2:16 bb0: { _2 = discriminant(_1);           // scope 0 at $DIR/76803_regression.rs:+1:11: +1:12 switchInt(move _2) -> [0: bb2, otherwise: bb1]; // scope 0 at $DIR/76803_regression.rs:+1:5: +1:12 } bb1: { _0 = move _1;                    // scope 0 at $DIR/76803_regression.rs:+3:14: +3:15 goto -> bb3;                     // scope 0 at $DIR/76803_regression.rs:+3:14: +3:15 } bb2: { Deinit(_0);                      // scope 0 at $DIR/76803_regression.rs:+2:20: +2:27 discriminant(_0) = 1;            // scope 0 at $DIR/76803_regression.rs:+2:20: +2:27 goto -> bb3;                     // scope 0 at $DIR/76803_regression.rs:+2:20: +2:27 } bb3: { return;                          // scope 0 at $DIR/76803_regression.rs:+5:2: +5:2 } } </del>

<del> // compile-flags: -Z mir-opt-level=1 // EMIT_MIR 76803_regression.encode.SimplifyBranchSame.diff #[derive(Debug, Eq, PartialEq)] pub enum Type { A, B, } pub fn encode(v: Type) -> Type { match v { Type::A => Type::B, _ => v, } } fn main() { assert_eq!(Type::B, encode(Type::A)); } </del>

<del> - // MIR for `main` before SimplifyArmIdentity + // MIR for `main` after SimplifyArmIdentity fn main() -> () { let mut _0: ();                      // return place in scope 0 at $DIR/issue_73223.rs:+0:11: +0:11 let _1: i32;                         // in scope 0 at $DIR/issue_73223.rs:+1:9: +1:14 let mut _2: std::option::Option<i32>; // in scope 0 at $DIR/issue_73223.rs:+1:23: +1:30 let mut _3: isize;                   // in scope 0 at $DIR/issue_73223.rs:+2:9: +2:16 let _4: i32;                         // in scope 0 at $DIR/issue_73223.rs:+2:14: +2:15 let mut _6: i32;                     // in scope 0 at $DIR/issue_73223.rs:+6:22: +6:27 let mut _7: &i32;                    // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _8: &i32;                    // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _11: bool;                   // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _12: bool;                   // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _13: i32;                    // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _14: i32;                    // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let _16: !;                          // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _17: core::panicking::AssertKind; // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _18: &i32;                   // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let _19: &i32;                       // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _20: &i32;                   // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let _21: &i32;                       // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _22: std::option::Option<std::fmt::Arguments<'_>>; // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _24: &i32;                   // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _25: &i32;                   // in scope 0 at $SRC_DIR/core/src/macros/mod.rs:LL:COL scope 1 { debug split => _1;               // in scope 1 at $DIR/issue_73223.rs:+1:9: +1:14 let _5: std::option::Option<i32>; // in scope 1 at $DIR/issue_73223.rs:+6:9: +6:14 scope 3 { debug _prev => _5;           // in scope 3 at $DIR/issue_73223.rs:+6:9: +6:14 let _9: &i32;                // in scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let _10: &i32;               // in scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let mut _23: &i32;           // in scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL scope 4 { debug left_val => _9;    // in scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL debug right_val => _10;  // in scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL let _15: core::panicking::AssertKind; // in scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL scope 5 { debug kind => _15;   // in scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL } } } } scope 2 { debug v => _4;                   // in scope 2 at $DIR/issue_73223.rs:+2:14: +2:15 } bb0: { StorageLive(_1);                 // scope 0 at $DIR/issue_73223.rs:+1:9: +1:14 StorageLive(_2);                 // scope 0 at $DIR/issue_73223.rs:+1:23: +1:30 Deinit(_2);                      // scope 0 at $DIR/issue_73223.rs:+1:23: +1:30 ((_2 as Some).0: i32) = const 1_i32; // scope 0 at $DIR/issue_73223.rs:+1:23: +1:30 discriminant(_2) = 1;            // scope 0 at $DIR/issue_73223.rs:+1:23: +1:30 _3 = const 1_isize;              // scope 0 at $DIR/issue_73223.rs:+1:23: +1:30 goto -> bb3;                     // scope 0 at $DIR/issue_73223.rs:+1:17: +1:30 } bb1: { StorageDead(_2);                 // scope 0 at $DIR/issue_73223.rs:+4:6: +4:7 StorageDead(_1);                 // scope 0 at $DIR/issue_73223.rs:+8:1: +8:2 return;                          // scope 0 at $DIR/issue_73223.rs:+8:2: +8:2 } bb2: { unreachable;                     // scope 0 at $DIR/issue_73223.rs:+1:23: +1:30 } bb3: { StorageLive(_4);                 // scope 0 at $DIR/issue_73223.rs:+2:14: +2:15 _4 = ((_2 as Some).0: i32);      // scope 0 at $DIR/issue_73223.rs:+2:14: +2:15 _1 = _4;                         // scope 2 at $DIR/issue_73223.rs:+2:20: +2:21 StorageDead(_4);                 // scope 0 at $DIR/issue_73223.rs:+2:20: +2:21 StorageDead(_2);                 // scope 0 at $DIR/issue_73223.rs:+4:6: +4:7 StorageLive(_5);                 // scope 1 at $DIR/issue_73223.rs:+6:9: +6:14 StorageLive(_6);                 // scope 1 at $DIR/issue_73223.rs:+6:22: +6:27 _6 = _1;                         // scope 1 at $DIR/issue_73223.rs:+6:22: +6:27 Deinit(_5);                      // scope 1 at $DIR/issue_73223.rs:+6:17: +6:28 ((_5 as Some).0: i32) = move _6; // scope 1 at $DIR/issue_73223.rs:+6:17: +6:28 discriminant(_5) = 1;            // scope 1 at $DIR/issue_73223.rs:+6:17: +6:28 StorageDead(_6);                 // scope 1 at $DIR/issue_73223.rs:+6:27: +6:28 StorageLive(_24);                // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_25);                // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_7);                 // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _7 = &_1;                        // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_8);                 // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _23 = const _;                   // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL // mir::Constant // + span: $SRC_DIR/core/src/macros/mod.rs:LL:COL // + literal: Const { ty: &i32, val: Unevaluated(main, [], Some(promoted[0])) } _8 = _23;                        // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL Deinit(_24);                     // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL Deinit(_25);                     // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _24 = move _7;                   // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _25 = move _8;                   // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_8);                 // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_7);                 // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_9);                 // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _9 = _24;                        // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_10);                // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _10 = _25;                       // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_11);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_12);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_13);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _13 = (*_9);                     // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_14);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _14 = const 1_i32;               // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _12 = Eq(move _13, const 1_i32); // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_14);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_13);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _11 = Not(move _12);             // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_12);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL switchInt(move _11) -> [0: bb5, otherwise: bb4]; // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL } bb4: { StorageLive(_15);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL Deinit(_15);                     // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL discriminant(_15) = 0;           // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_16);                // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_17);                // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _17 = const core::panicking::AssertKind::Eq; // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL // mir::Constant // + span: $SRC_DIR/core/src/macros/mod.rs:LL:COL // + literal: Const { ty: core::panicking::AssertKind, val: Value(Scalar(0x00)) } StorageLive(_18);                // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_19);                // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _19 = _9;                        // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _18 = _19;                       // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_20);                // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_21);                // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _21 = _10;                       // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _20 = _21;                       // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageLive(_22);                // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL Deinit(_22);                     // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL discriminant(_22) = 0;           // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL _16 = core::panicking::assert_failed::<i32, i32>(const core::panicking::AssertKind::Eq, move _18, move _20, move _22); // scope 5 at $SRC_DIR/core/src/macros/mod.rs:LL:COL // mir::Constant // + span: $SRC_DIR/core/src/macros/mod.rs:LL:COL // + literal: Const { ty: for<'a, 'b, 'c> fn(core::panicking::AssertKind, &'a i32, &'b i32, Option<Arguments<'c>>) -> ! {core::panicking::assert_failed::<i32, i32>}, val: Value(<ZST>) } // mir::Constant // + span: $SRC_DIR/core/src/macros/mod.rs:LL:COL // + literal: Const { ty: core::panicking::AssertKind, val: Value(Scalar(0x00)) } } bb5: { StorageDead(_11);                // scope 4 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_10);                // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_9);                 // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_24);                // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_25);                // scope 3 at $SRC_DIR/core/src/macros/mod.rs:LL:COL StorageDead(_5);                 // scope 1 at $DIR/issue_73223.rs:+8:1: +8:2 StorageDead(_1);                 // scope 0 at $DIR/issue_73223.rs:+8:1: +8:2 return;                          // scope 0 at $DIR/issue_73223.rs:+8:2: +8:2 } } </del>

<del> fn main() { let split = match Some(1) { Some(v) => v, None => return, }; let _prev = Some(split); assert_eq!(split, 1); } // EMIT_MIR issue_73223.main.SimplifyArmIdentity.diff </del>