use rustc_index::bit_set::{BitSet, ChunkedBitSet}; use rustc_index::Idx; use rustc_middle::mir::{self, Body, CallReturnPlaces, Location, TerminatorEdges}; use rustc_middle::ty::{self, TyCtxt}; use crate::drop_flag_effects_for_function_entry; use crate::drop_flag_effects_for_location; use crate::elaborate_drops::DropFlagState; use crate::framework::SwitchIntEdgeEffects; use crate::move_paths::{HasMoveData, InitIndex, InitKind, LookupResult, MoveData, MovePathIndex}; use crate::on_lookup_result_bits; use crate::MoveDataParamEnv; use crate::{drop_flag_effects, on_all_children_bits}; use crate::{lattice, AnalysisDomain, GenKill, GenKillAnalysis, MaybeReachable}; /// `MaybeInitializedPlaces` tracks all places that might be /// initialized upon reaching a particular point in the control flow /// for a function. /// /// For example, in code like the following, we have corresponding /// dataflow information shown in the right-hand comments. /// /// ```rust /// struct S; /// fn foo(pred: bool) { // maybe-init: /// // {} /// let a = S; let mut b = S; let c; let d; // {a, b} /// /// if pred { /// drop(a); // { b} /// b = S; // { b} /// /// } else { /// drop(b); // {a} /// d = S; // {a, d} /// /// } // {a, b, d} /// /// c = S; // {a, b, c, d} /// } /// ``` /// /// To determine whether a place *must* be initialized at a /// particular control-flow point, one can take the set-difference /// between this data and the data from `MaybeUninitializedPlaces` at the /// corresponding control-flow point. /// /// Similarly, at a given `drop` statement, the set-intersection /// between this data and `MaybeUninitializedPlaces` yields the set of /// places that would require a dynamic drop-flag at that statement. pub struct MaybeInitializedPlaces<'a, 'tcx> { tcx: TyCtxt<'tcx>, body: &'a Body<'tcx>, mdpe: &'a MoveDataParamEnv<'tcx>, skip_unreachable_unwind: bool, } impl<'a, 'tcx> MaybeInitializedPlaces<'a, 'tcx> { pub fn new(tcx: TyCtxt<'tcx>, body: &'a Body<'tcx>, mdpe: &'a MoveDataParamEnv<'tcx>) -> Self { MaybeInitializedPlaces { tcx, body, mdpe, skip_unreachable_unwind: false } } pub fn skipping_unreachable_unwind(mut self) -> Self { self.skip_unreachable_unwind = true; self } pub fn is_unwind_dead( &self, place: mir::Place<'tcx>, state: &MaybeReachable>, ) -> bool { if let LookupResult::Exact(path) = self.move_data().rev_lookup.find(place.as_ref()) { let mut maybe_live = false; on_all_children_bits(self.move_data(), path, |child| { maybe_live |= state.contains(child); }); !maybe_live } else { false } } } impl<'a, 'tcx> HasMoveData<'tcx> for MaybeInitializedPlaces<'a, 'tcx> { fn move_data(&self) -> &MoveData<'tcx> { &self.mdpe.move_data } } /// `MaybeUninitializedPlaces` tracks all places that might be /// uninitialized upon reaching a particular point in the control flow /// for a function. /// /// For example, in code like the following, we have corresponding /// dataflow information shown in the right-hand comments. /// /// ```rust /// struct S; /// fn foo(pred: bool) { // maybe-uninit: /// // {a, b, c, d} /// let a = S; let mut b = S; let c; let d; // { c, d} /// /// if pred { /// drop(a); // {a, c, d} /// b = S; // {a, c, d} /// /// } else { /// drop(b); // { b, c, d} /// d = S; // { b, c } /// /// } // {a, b, c, d} /// /// c = S; // {a, b, d} /// } /// ``` /// /// To determine whether a place *must* be uninitialized at a /// particular control-flow point, one can take the set-difference /// between this data and the data from `MaybeInitializedPlaces` at the /// corresponding control-flow point. /// /// Similarly, at a given `drop` statement, the set-intersection /// between this data and `MaybeInitializedPlaces` yields the set of /// places that would require a dynamic drop-flag at that statement. pub struct MaybeUninitializedPlaces<'a, 'tcx> { tcx: TyCtxt<'tcx>, body: &'a Body<'tcx>, mdpe: &'a MoveDataParamEnv<'tcx>, mark_inactive_variants_as_uninit: bool, skip_unreachable_unwind: BitSet, } impl<'a, 'tcx> MaybeUninitializedPlaces<'a, 'tcx> { pub fn new(tcx: TyCtxt<'tcx>, body: &'a Body<'tcx>, mdpe: &'a MoveDataParamEnv<'tcx>) -> Self { MaybeUninitializedPlaces { tcx, body, mdpe, mark_inactive_variants_as_uninit: false, skip_unreachable_unwind: BitSet::new_empty(body.basic_blocks.len()), } } /// Causes inactive enum variants to be marked as "maybe uninitialized" after a switch on an /// enum discriminant. /// /// This is correct in a vacuum but is not the default because it causes problems in the borrow /// checker, where this information gets propagated along `FakeEdge`s. pub fn mark_inactive_variants_as_uninit(mut self) -> Self { self.mark_inactive_variants_as_uninit = true; self } pub fn skipping_unreachable_unwind( mut self, unreachable_unwind: BitSet, ) -> Self { self.skip_unreachable_unwind = unreachable_unwind; self } } impl<'a, 'tcx> HasMoveData<'tcx> for MaybeUninitializedPlaces<'a, 'tcx> { fn move_data(&self) -> &MoveData<'tcx> { &self.mdpe.move_data } } /// `DefinitelyInitializedPlaces` tracks all places that are definitely /// initialized upon reaching a particular point in the control flow /// for a function. /// /// For example, in code like the following, we have corresponding /// dataflow information shown in the right-hand comments. /// /// ```rust /// struct S; /// fn foo(pred: bool) { // definite-init: /// // { } /// let a = S; let mut b = S; let c; let d; // {a, b } /// /// if pred { /// drop(a); // { b, } /// b = S; // { b, } /// /// } else { /// drop(b); // {a, } /// d = S; // {a, d} /// /// } // { } /// /// c = S; // { c } /// } /// ``` /// /// To determine whether a place *may* be uninitialized at a /// particular control-flow point, one can take the set-complement /// of this data. /// /// Similarly, at a given `drop` statement, the set-difference between /// this data and `MaybeInitializedPlaces` yields the set of places /// that would require a dynamic drop-flag at that statement. pub struct DefinitelyInitializedPlaces<'a, 'tcx> { body: &'a Body<'tcx>, mdpe: &'a MoveDataParamEnv<'tcx>, } impl<'a, 'tcx> DefinitelyInitializedPlaces<'a, 'tcx> { pub fn new(body: &'a Body<'tcx>, mdpe: &'a MoveDataParamEnv<'tcx>) -> Self { DefinitelyInitializedPlaces { body, mdpe } } } impl<'a, 'tcx> HasMoveData<'tcx> for DefinitelyInitializedPlaces<'a, 'tcx> { fn move_data(&self) -> &MoveData<'tcx> { &self.mdpe.move_data } } /// `EverInitializedPlaces` tracks all places that might have ever been /// initialized upon reaching a particular point in the control flow /// for a function, without an intervening `StorageDead`. /// /// This dataflow is used to determine if an immutable local variable may /// be assigned to. /// /// For example, in code like the following, we have corresponding /// dataflow information shown in the right-hand comments. /// /// ```rust /// struct S; /// fn foo(pred: bool) { // ever-init: /// // { } /// let a = S; let mut b = S; let c; let d; // {a, b } /// /// if pred { /// drop(a); // {a, b, } /// b = S; // {a, b, } /// /// } else { /// drop(b); // {a, b, } /// d = S; // {a, b, d } /// /// } // {a, b, d } /// /// c = S; // {a, b, c, d } /// } /// ``` pub struct EverInitializedPlaces<'a, 'tcx> { body: &'a Body<'tcx>, mdpe: &'a MoveDataParamEnv<'tcx>, } impl<'a, 'tcx> EverInitializedPlaces<'a, 'tcx> { pub fn new(body: &'a Body<'tcx>, mdpe: &'a MoveDataParamEnv<'tcx>) -> Self { EverInitializedPlaces { body, mdpe } } } impl<'a, 'tcx> HasMoveData<'tcx> for EverInitializedPlaces<'a, 'tcx> { fn move_data(&self) -> &MoveData<'tcx> { &self.mdpe.move_data } } impl<'a, 'tcx> MaybeInitializedPlaces<'a, 'tcx> { fn update_bits( trans: &mut impl GenKill, path: MovePathIndex, state: DropFlagState, ) { match state { DropFlagState::Absent => trans.kill(path), DropFlagState::Present => trans.gen(path), } } } impl<'a, 'tcx> MaybeUninitializedPlaces<'a, 'tcx> { fn update_bits( trans: &mut impl GenKill, path: MovePathIndex, state: DropFlagState, ) { match state { DropFlagState::Absent => trans.gen(path), DropFlagState::Present => trans.kill(path), } } } impl<'a, 'tcx> DefinitelyInitializedPlaces<'a, 'tcx> { fn update_bits( trans: &mut impl GenKill, path: MovePathIndex, state: DropFlagState, ) { match state { DropFlagState::Absent => trans.kill(path), DropFlagState::Present => trans.gen(path), } } } impl<'tcx> AnalysisDomain<'tcx> for MaybeInitializedPlaces<'_, 'tcx> { type Domain = MaybeReachable>; const NAME: &'static str = "maybe_init"; fn bottom_value(&self, _: &mir::Body<'tcx>) -> Self::Domain { // bottom = uninitialized MaybeReachable::Unreachable } fn initialize_start_block(&self, _: &mir::Body<'tcx>, state: &mut Self::Domain) { *state = MaybeReachable::Reachable(ChunkedBitSet::new_empty(self.move_data().move_paths.len())); drop_flag_effects_for_function_entry(self.body, self.mdpe, |path, s| { assert!(s == DropFlagState::Present); state.gen(path); }); } } impl<'tcx> GenKillAnalysis<'tcx> for MaybeInitializedPlaces<'_, 'tcx> { type Idx = MovePathIndex; fn domain_size(&self, _: &Body<'tcx>) -> usize { self.move_data().move_paths.len() } fn statement_effect( &mut self, trans: &mut impl GenKill, statement: &mir::Statement<'tcx>, location: Location, ) { drop_flag_effects_for_location(self.body, self.mdpe, location, |path, s| { Self::update_bits(trans, path, s) }); // Mark all places as "maybe init" if they are mutably borrowed. See #90752. if self.tcx.sess.opts.unstable_opts.precise_enum_drop_elaboration && let Some((_, rvalue)) = statement.kind.as_assign() && let mir::Rvalue::Ref(_, mir::BorrowKind::Mut { .. }, place) // FIXME: Does `&raw const foo` allow mutation? See #90413. | mir::Rvalue::AddressOf(_, place) = rvalue && let LookupResult::Exact(mpi) = self.move_data().rev_lookup.find(place.as_ref()) { on_all_children_bits(self.move_data(), mpi, |child| { trans.gen(child); }) } } fn terminator_effect<'mir>( &mut self, state: &mut Self::Domain, terminator: &'mir mir::Terminator<'tcx>, location: Location, ) -> TerminatorEdges<'mir, 'tcx> { let mut edges = terminator.edges(); if self.skip_unreachable_unwind && let mir::TerminatorKind::Drop { target, unwind, place, replace: _ } = terminator.kind && matches!(unwind, mir::UnwindAction::Cleanup(_)) && self.is_unwind_dead(place, state) { edges = TerminatorEdges::Single(target); } drop_flag_effects_for_location(self.body, self.mdpe, location, |path, s| { Self::update_bits(state, path, s) }); edges } fn call_return_effect( &mut self, trans: &mut Self::Domain, _block: mir::BasicBlock, return_places: CallReturnPlaces<'_, 'tcx>, ) { return_places.for_each(|place| { // when a call returns successfully, that means we need to set // the bits for that dest_place to 1 (initialized). on_lookup_result_bits( self.move_data(), self.move_data().rev_lookup.find(place.as_ref()), |mpi| { trans.gen(mpi); }, ); }); } fn switch_int_edge_effects>( &mut self, block: mir::BasicBlock, discr: &mir::Operand<'tcx>, edge_effects: &mut impl SwitchIntEdgeEffects, ) { if !self.tcx.sess.opts.unstable_opts.precise_enum_drop_elaboration { return; } let enum_ = discr.place().and_then(|discr| { switch_on_enum_discriminant(self.tcx, self.body, &self.body[block], discr) }); let Some((enum_place, enum_def)) = enum_ else { return; }; let mut discriminants = enum_def.discriminants(self.tcx); edge_effects.apply(|trans, edge| { let Some(value) = edge.value else { return; }; // MIR building adds discriminants to the `values` array in the same order as they // are yielded by `AdtDef::discriminants`. We rely on this to match each // discriminant in `values` to its corresponding variant in linear time. let (variant, _) = discriminants .find(|&(_, discr)| discr.val == value) .expect("Order of `AdtDef::discriminants` differed from `SwitchInt::values`"); // Kill all move paths that correspond to variants we know to be inactive along this // particular outgoing edge of a `SwitchInt`. drop_flag_effects::on_all_inactive_variants( self.move_data(), enum_place, variant, |mpi| trans.kill(mpi), ); }); } } impl<'tcx> AnalysisDomain<'tcx> for MaybeUninitializedPlaces<'_, 'tcx> { type Domain = ChunkedBitSet; const NAME: &'static str = "maybe_uninit"; fn bottom_value(&self, _: &mir::Body<'tcx>) -> Self::Domain { // bottom = initialized (start_block_effect counters this at outset) ChunkedBitSet::new_empty(self.move_data().move_paths.len()) } // sets on_entry bits for Arg places fn initialize_start_block(&self, _: &mir::Body<'tcx>, state: &mut Self::Domain) { // set all bits to 1 (uninit) before gathering counter-evidence state.insert_all(); drop_flag_effects_for_function_entry(self.body, self.mdpe, |path, s| { assert!(s == DropFlagState::Present); state.remove(path); }); } } impl<'tcx> GenKillAnalysis<'tcx> for MaybeUninitializedPlaces<'_, 'tcx> { type Idx = MovePathIndex; fn domain_size(&self, _: &Body<'tcx>) -> usize { self.move_data().move_paths.len() } fn statement_effect( &mut self, trans: &mut impl GenKill, _statement: &mir::Statement<'tcx>, location: Location, ) { drop_flag_effects_for_location(self.body, self.mdpe, location, |path, s| { Self::update_bits(trans, path, s) }); // Unlike in `MaybeInitializedPlaces` above, we don't need to change the state when a // mutable borrow occurs. Places cannot become uninitialized through a mutable reference. } fn terminator_effect<'mir>( &mut self, trans: &mut Self::Domain, terminator: &'mir mir::Terminator<'tcx>, location: Location, ) -> TerminatorEdges<'mir, 'tcx> { drop_flag_effects_for_location(self.body, self.mdpe, location, |path, s| { Self::update_bits(trans, path, s) }); if self.skip_unreachable_unwind.contains(location.block) { let mir::TerminatorKind::Drop { target, unwind, .. } = terminator.kind else { bug!() }; assert!(matches!(unwind, mir::UnwindAction::Cleanup(_))); TerminatorEdges::Single(target) } else { terminator.edges() } } fn call_return_effect( &mut self, trans: &mut Self::Domain, _block: mir::BasicBlock, return_places: CallReturnPlaces<'_, 'tcx>, ) { return_places.for_each(|place| { // when a call returns successfully, that means we need to set // the bits for that dest_place to 0 (initialized). on_lookup_result_bits( self.move_data(), self.move_data().rev_lookup.find(place.as_ref()), |mpi| { trans.kill(mpi); }, ); }); } fn switch_int_edge_effects>( &mut self, block: mir::BasicBlock, discr: &mir::Operand<'tcx>, edge_effects: &mut impl SwitchIntEdgeEffects, ) { if !self.tcx.sess.opts.unstable_opts.precise_enum_drop_elaboration { return; } if !self.mark_inactive_variants_as_uninit { return; } let enum_ = discr.place().and_then(|discr| { switch_on_enum_discriminant(self.tcx, self.body, &self.body[block], discr) }); let Some((enum_place, enum_def)) = enum_ else { return; }; let mut discriminants = enum_def.discriminants(self.tcx); edge_effects.apply(|trans, edge| { let Some(value) = edge.value else { return; }; // MIR building adds discriminants to the `values` array in the same order as they // are yielded by `AdtDef::discriminants`. We rely on this to match each // discriminant in `values` to its corresponding variant in linear time. let (variant, _) = discriminants .find(|&(_, discr)| discr.val == value) .expect("Order of `AdtDef::discriminants` differed from `SwitchInt::values`"); // Mark all move paths that correspond to variants other than this one as maybe // uninitialized (in reality, they are *definitely* uninitialized). drop_flag_effects::on_all_inactive_variants( self.move_data(), enum_place, variant, |mpi| trans.gen(mpi), ); }); } } impl<'a, 'tcx> AnalysisDomain<'tcx> for DefinitelyInitializedPlaces<'a, 'tcx> { /// Use set intersection as the join operator. type Domain = lattice::Dual>; const NAME: &'static str = "definite_init"; fn bottom_value(&self, _: &mir::Body<'tcx>) -> Self::Domain { // bottom = initialized (start_block_effect counters this at outset) lattice::Dual(BitSet::new_filled(self.move_data().move_paths.len())) } // sets on_entry bits for Arg places fn initialize_start_block(&self, _: &mir::Body<'tcx>, state: &mut Self::Domain) { state.0.clear(); drop_flag_effects_for_function_entry(self.body, self.mdpe, |path, s| { assert!(s == DropFlagState::Present); state.0.insert(path); }); } } impl<'tcx> GenKillAnalysis<'tcx> for DefinitelyInitializedPlaces<'_, 'tcx> { type Idx = MovePathIndex; fn domain_size(&self, _: &Body<'tcx>) -> usize { self.move_data().move_paths.len() } fn statement_effect( &mut self, trans: &mut impl GenKill, _statement: &mir::Statement<'tcx>, location: Location, ) { drop_flag_effects_for_location(self.body, self.mdpe, location, |path, s| { Self::update_bits(trans, path, s) }) } fn terminator_effect<'mir>( &mut self, trans: &mut Self::Domain, terminator: &'mir mir::Terminator<'tcx>, location: Location, ) -> TerminatorEdges<'mir, 'tcx> { drop_flag_effects_for_location(self.body, self.mdpe, location, |path, s| { Self::update_bits(trans, path, s) }); terminator.edges() } fn call_return_effect( &mut self, trans: &mut Self::Domain, _block: mir::BasicBlock, return_places: CallReturnPlaces<'_, 'tcx>, ) { return_places.for_each(|place| { // when a call returns successfully, that means we need to set // the bits for that dest_place to 1 (initialized). on_lookup_result_bits( self.move_data(), self.move_data().rev_lookup.find(place.as_ref()), |mpi| { trans.gen(mpi); }, ); }); } } impl<'tcx> AnalysisDomain<'tcx> for EverInitializedPlaces<'_, 'tcx> { type Domain = ChunkedBitSet; const NAME: &'static str = "ever_init"; fn bottom_value(&self, _: &mir::Body<'tcx>) -> Self::Domain { // bottom = no initialized variables by default ChunkedBitSet::new_empty(self.move_data().inits.len()) } fn initialize_start_block(&self, body: &mir::Body<'tcx>, state: &mut Self::Domain) { for arg_init in 0..body.arg_count { state.insert(InitIndex::new(arg_init)); } } } impl<'tcx> GenKillAnalysis<'tcx> for EverInitializedPlaces<'_, 'tcx> { type Idx = InitIndex; fn domain_size(&self, _: &Body<'tcx>) -> usize { self.move_data().inits.len() } #[instrument(skip(self, trans), level = "debug")] fn statement_effect( &mut self, trans: &mut impl GenKill, stmt: &mir::Statement<'tcx>, location: Location, ) { let move_data = self.move_data(); let init_path_map = &move_data.init_path_map; let init_loc_map = &move_data.init_loc_map; let rev_lookup = &move_data.rev_lookup; debug!("initializes move_indexes {:?}", &init_loc_map[location]); trans.gen_all(init_loc_map[location].iter().copied()); if let mir::StatementKind::StorageDead(local) = stmt.kind { // End inits for StorageDead, so that an immutable variable can // be reinitialized on the next iteration of the loop. if let Some(move_path_index) = rev_lookup.find_local(local) { debug!( "clears the ever initialized status of {:?}", init_path_map[move_path_index] ); trans.kill_all(init_path_map[move_path_index].iter().copied()); } } } #[instrument(skip(self, trans, terminator), level = "debug")] fn terminator_effect<'mir>( &mut self, trans: &mut Self::Domain, terminator: &'mir mir::Terminator<'tcx>, location: Location, ) -> TerminatorEdges<'mir, 'tcx> { let (body, move_data) = (self.body, self.move_data()); let term = body[location.block].terminator(); let init_loc_map = &move_data.init_loc_map; debug!(?term); debug!("initializes move_indexes {:?}", init_loc_map[location]); trans.gen_all( init_loc_map[location] .iter() .filter(|init_index| { move_data.inits[**init_index].kind != InitKind::NonPanicPathOnly }) .copied(), ); terminator.edges() } fn call_return_effect( &mut self, trans: &mut Self::Domain, block: mir::BasicBlock, _return_places: CallReturnPlaces<'_, 'tcx>, ) { let move_data = self.move_data(); let init_loc_map = &move_data.init_loc_map; let call_loc = self.body.terminator_loc(block); for init_index in &init_loc_map[call_loc] { trans.gen(*init_index); } } } /// Inspect a `SwitchInt`-terminated basic block to see if the condition of that `SwitchInt` is /// an enum discriminant. /// /// We expect such blocks to have a call to `discriminant` as their last statement like so: /// /// ```text /// ... /// _42 = discriminant(_1) /// SwitchInt(_42, ..) /// ``` /// /// If the basic block matches this pattern, this function returns the place corresponding to the /// enum (`_1` in the example above) as well as the `AdtDef` of that enum. fn switch_on_enum_discriminant<'mir, 'tcx>( tcx: TyCtxt<'tcx>, body: &'mir mir::Body<'tcx>, block: &'mir mir::BasicBlockData<'tcx>, switch_on: mir::Place<'tcx>, ) -> Option<(mir::Place<'tcx>, ty::AdtDef<'tcx>)> { for statement in block.statements.iter().rev() { match &statement.kind { mir::StatementKind::Assign(box (lhs, mir::Rvalue::Discriminant(discriminated))) if *lhs == switch_on => { match discriminated.ty(body, tcx).ty.kind() { ty::Adt(def, _) => return Some((*discriminated, *def)), // `Rvalue::Discriminant` is also used to get the active yield point for a // coroutine, but we do not need edge-specific effects in that case. This may // change in the future. ty::Coroutine(..) => return None, t => bug!("`discriminant` called on unexpected type {:?}", t), } } mir::StatementKind::Coverage(_) => continue, _ => return None, } } None }