blob: 1b2c58c7e514cfb5426f6bb2b798f5d1915f514e [file]
use std::assert_matches;
use rustc_abi::VariantIdx;
use rustc_data_structures::fx::FxIndexSet;
use rustc_index::bit_set::{DenseBitSet, MixedBitSet};
use rustc_middle::bug;
use rustc_middle::mir::{
self, BasicBlock, Body, CallReturnPlaces, Local, Location, StatementKind, TerminatorEdges,
};
use rustc_middle::ty::{self, TyCtxt};
use smallvec::SmallVec;
use tracing::instrument;
use crate::drop_flag_effects::{DropFlagState, InactiveVariants};
use crate::move_paths::{
HasMoveData, Init, InitKind, InitLocation, LookupResult, MoveData, MovePathIndex,
};
use crate::{
Analysis, GenKill, MaybeReachable, SwitchTargetIndex, drop_flag_effects,
drop_flag_effects_for_function_entry, drop_flag_effects_for_location, on_all_children_bits,
on_lookup_result_bits,
};
// Used by both `MaybeInitializedPlaces` and `MaybeUninitializedPlaces`.
pub struct MaybePlacesSwitchIntData<'tcx> {
enum_place: mir::Place<'tcx>,
// Variant indices targeted by the SwitchInt. For example, if you have:
// ```
// enum E { A = 1, B = 3, C = 5, D = 7 }
// ```
// and a `SwitchInt(A -> bb1, C -> bb2, _ -> bb3)`, this vec will contain `[0, 2]` because
// those are the variant indices for `A` and `C`.
variants: SmallVec<[VariantIdx; 4]>,
}
impl<'tcx> MaybePlacesSwitchIntData<'tcx> {
fn new(
tcx: TyCtxt<'tcx>,
body: &Body<'tcx>,
block: mir::BasicBlock,
targets: &mir::SwitchTargets,
discr: &mir::Operand<'tcx>,
) -> Option<Self> {
let Some(discr) = discr.place() else { return None };
// 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 gathers the place corresponding
// to the enum (`_1` in the example above) as well as the discriminants.
let block_data = &body[block];
for statement in block_data.statements.iter().rev() {
match statement.kind {
mir::StatementKind::Assign((lhs, mir::Rvalue::Discriminant(enum_place)))
if lhs == discr =>
{
match enum_place.ty(body, tcx).ty.kind() {
ty::Adt(enum_def, _) => {
// For each value in the SwitchInt, find the VariantIdx for the variant
// with that value. This works because `discriminant_vals` and
// `targets.all_values()` are guaranteed to list variants in the same
// AdtDef order. (If that ever changes the `expect` will panic.)
let mut discriminants = enum_def.discriminants(tcx);
let variants = targets
.all_values()
.iter()
.map(|value| {
// On each call to this closure `find` only consumes part of
// the `discriminants` iterator.
discriminants
.find(|(_, discr)| discr.val == value.get())
.expect("SwitchInt vals should match a variant")
.0
})
.collect();
return Some(MaybePlacesSwitchIntData { enum_place, variants });
}
// `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(..) => break,
t => bug!("`discriminant` called on unexpected type {:?}", t),
}
}
mir::StatementKind::Coverage(_) => continue,
_ => break,
}
}
None
}
}
/// `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;
/// #[rustfmt::skip]
/// fn foo(p: bool) { // maybe-init:
/// // {p}
/// let a = S; let mut b = S; let c; let d; // {p, a, b}
///
/// if p {
/// drop(a); // {p, b}
/// b = S; // {p, b}
///
/// } else {
/// drop(b); // {p, a}
/// d = S; // {p, a, d}
///
/// } // {p, a, b, d}
///
/// c = S; // {p, a, b, c, d}
/// }
/// ```
///
/// To determine whether a place is *definitely* initialized at a
/// particular control-flow point, one can take the set-complement
/// of 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>,
move_data: &'a MoveData<'tcx>,
exclude_inactive_in_otherwise: bool,
skip_unreachable_unwind: bool,
}
impl<'a, 'tcx> MaybeInitializedPlaces<'a, 'tcx> {
pub fn new(tcx: TyCtxt<'tcx>, body: &'a Body<'tcx>, move_data: &'a MoveData<'tcx>) -> Self {
MaybeInitializedPlaces {
tcx,
body,
move_data,
exclude_inactive_in_otherwise: false,
skip_unreachable_unwind: false,
}
}
/// Ensures definitely inactive variants are excluded from the set of initialized places for
/// blocks reached through an `otherwise` edge.
pub fn exclude_inactive_in_otherwise(mut self) -> Self {
self.exclude_inactive_in_otherwise = true;
self
}
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: &<Self as Analysis<'tcx>>::Domain,
) -> 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.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;
/// #[rustfmt::skip]
/// fn foo(p: bool) { // maybe-uninit:
/// // {a, b, c, d}
/// let a = S; let mut b = S; let c; let d; // { c, d}
///
/// if p {
/// 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 is *definitely* uninitialized at a
/// particular control-flow point, one can take the set-complement
/// of 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>,
move_data: &'a MoveData<'tcx>,
mark_inactive_variants_as_uninit: bool,
skip_unreachable_unwind: DenseBitSet<mir::BasicBlock>,
}
impl<'a, 'tcx> MaybeUninitializedPlaces<'a, 'tcx> {
pub fn new(tcx: TyCtxt<'tcx>, body: &'a Body<'tcx>, move_data: &'a MoveData<'tcx>) -> Self {
MaybeUninitializedPlaces {
tcx,
body,
move_data,
mark_inactive_variants_as_uninit: false,
skip_unreachable_unwind: DenseBitSet::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: DenseBitSet<mir::BasicBlock>,
) -> Self {
self.skip_unreachable_unwind = unreachable_unwind;
self
}
}
impl<'tcx> HasMoveData<'tcx> for MaybeUninitializedPlaces<'_, 'tcx> {
fn move_data(&self) -> &MoveData<'tcx> {
self.move_data
}
}
/// `EverInitializedPlaces` tracks all initializations of locals that may have
/// occurred 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;
/// #[rustfmt::skip]
/// fn foo(p: bool) { // ever-init:
/// // {p, }
/// let a = S; let mut b = S; let c; let d; // {p, a, b, }
///
/// if p {
/// drop(a); // {p, a, b, }
/// b = S; // {p, a, b, }
///
/// } else {
/// drop(b); // {p, a, b, }
/// d = S; // {p, a, b, d}
///
/// } // {p, a, b, d}
///
/// c = S; // {p, a, b, c, d}
/// }
/// ```
pub struct EverInitializedPlaces<'a, 'tcx> {
body: &'a Body<'tcx>,
move_data: &'a MoveData<'tcx>,
}
impl<'a, 'tcx> EverInitializedPlaces<'a, 'tcx> {
pub fn new(body: &'a Body<'tcx>, move_data: &'a MoveData<'tcx>) -> Self {
EverInitializedPlaces { body, move_data }
}
}
impl<'tcx> HasMoveData<'tcx> for EverInitializedPlaces<'_, 'tcx> {
fn move_data(&self) -> &MoveData<'tcx> {
self.move_data
}
}
impl<'a, 'tcx> MaybeInitializedPlaces<'a, 'tcx> {
fn update_bits(
state: &mut <Self as Analysis<'tcx>>::Domain,
path: MovePathIndex,
dfstate: DropFlagState,
) {
match dfstate {
DropFlagState::Absent => state.kill(path),
DropFlagState::Present => state.gen_(path),
}
}
}
impl<'tcx> MaybeUninitializedPlaces<'_, 'tcx> {
fn update_bits(
state: &mut <Self as Analysis<'tcx>>::Domain,
path: MovePathIndex,
dfstate: DropFlagState,
) {
match dfstate {
DropFlagState::Absent => state.gen_(path),
DropFlagState::Present => state.kill(path),
}
}
}
impl<'tcx> Analysis<'tcx> for MaybeInitializedPlaces<'_, 'tcx> {
/// There can be many more `MovePathIndex` than there are locals in a MIR body.
/// We use a mixed bitset to avoid paying too high a memory footprint.
type Domain = MaybeReachable<MixedBitSet<MovePathIndex>>;
type SwitchIntData = MaybePlacesSwitchIntData<'tcx>;
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(MixedBitSet::new_empty(self.move_data().move_paths.len()));
drop_flag_effects_for_function_entry(self.body, self.move_data, |path, s| {
assert!(s == DropFlagState::Present);
state.gen_(path);
});
}
fn apply_primary_statement_effect(
&self,
state: &mut Self::Domain,
statement: &mir::Statement<'tcx>,
location: Location,
) {
drop_flag_effects_for_location(self.body, self.move_data, location, |path, s| {
Self::update_bits(state, 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::RawPtr(_, place) = rvalue
&& let LookupResult::Exact(mpi) = self.move_data().rev_lookup.find(place.as_ref())
{
on_all_children_bits(self.move_data(), mpi, |child| {
state.gen_(child);
})
}
}
fn get_terminator_edges<'mir>(
&self,
state: &Self::Domain,
terminator: &'mir mir::Terminator<'tcx>,
_location: Location,
) -> TerminatorEdges<'mir, 'tcx> {
// Note: this relies on `get_terminator_edges` being called before
// `apply_primary_terminator_effect` because the result of `is_unwind_dead` is affected by
// the `drop_flag_effects_for_location` in `apply_primary_terminator_effect`.
let mut edges = terminator.edges();
if self.skip_unreachable_unwind
&& let mir::TerminatorKind::Drop { target, unwind, place, replace: _, drop: _ } =
terminator.kind
&& matches!(unwind, mir::UnwindAction::Cleanup(_))
&& self.is_unwind_dead(place, state)
{
edges = TerminatorEdges::Single(target);
}
edges
}
fn apply_primary_terminator_effect(
&self,
state: &mut Self::Domain,
_terminator: &mir::Terminator<'tcx>,
location: Location,
) {
drop_flag_effects_for_location(self.body, self.move_data, location, |path, s| {
Self::update_bits(state, path, s)
});
}
fn apply_call_return_effect(
&self,
state: &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| {
state.gen_(mpi);
},
);
});
}
fn get_switch_int_data(
&self,
block: mir::BasicBlock,
targets: &mir::SwitchTargets,
discr: &mir::Operand<'tcx>,
) -> Option<Self::SwitchIntData> {
if !self.tcx.sess.opts.unstable_opts.precise_enum_drop_elaboration {
return None;
}
MaybePlacesSwitchIntData::new(self.tcx, self.body, block, targets, discr)
}
fn apply_switch_int_edge_effect(
&self,
state: &mut Self::Domain,
data: &mut Self::SwitchIntData,
target_idx: SwitchTargetIndex,
) {
let inactive_variants = match target_idx {
SwitchTargetIndex::Normal(target_idx) => {
InactiveVariants::Active(data.variants[target_idx])
}
SwitchTargetIndex::Otherwise if self.exclude_inactive_in_otherwise => {
InactiveVariants::Inactives(data.variants.clone())
}
_ => return,
};
// 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,
data.enum_place,
&inactive_variants,
|mpi| state.kill(mpi),
);
}
}
/// There can be many more `MovePathIndex` than there are locals in a MIR body.
/// We use a mixed bitset to avoid paying too high a memory footprint.
pub type MaybeUninitializedPlacesDomain = MixedBitSet<MovePathIndex>;
impl<'tcx> Analysis<'tcx> for MaybeUninitializedPlaces<'_, 'tcx> {
type Domain = MaybeUninitializedPlacesDomain;
type SwitchIntData = MaybePlacesSwitchIntData<'tcx>;
const NAME: &'static str = "maybe_uninit";
fn bottom_value(&self, _: &mir::Body<'tcx>) -> Self::Domain {
// bottom = initialized (`initialize_start_block` overwrites this on first entry)
MixedBitSet::new_empty(self.move_data().move_paths.len())
}
// sets state 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.move_data, |path, s| {
assert!(s == DropFlagState::Present);
state.remove(path);
});
}
fn apply_primary_statement_effect(
&self,
state: &mut Self::Domain,
_statement: &mir::Statement<'tcx>,
location: Location,
) {
drop_flag_effects_for_location(self.body, self.move_data, location, |path, s| {
Self::update_bits(state, 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 get_terminator_edges<'mir>(
&self,
_state: &Self::Domain,
terminator: &'mir mir::Terminator<'tcx>,
location: Location,
) -> TerminatorEdges<'mir, 'tcx> {
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 apply_primary_terminator_effect(
&self,
state: &mut Self::Domain,
_terminator: &mir::Terminator<'tcx>,
location: Location,
) {
drop_flag_effects_for_location(self.body, self.move_data, location, |path, s| {
Self::update_bits(state, path, s)
});
}
fn apply_call_return_effect(
&self,
state: &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| {
state.kill(mpi);
},
);
});
}
fn get_switch_int_data(
&self,
block: mir::BasicBlock,
targets: &mir::SwitchTargets,
discr: &mir::Operand<'tcx>,
) -> Option<Self::SwitchIntData> {
if !self.tcx.sess.opts.unstable_opts.precise_enum_drop_elaboration {
return None;
}
if !self.mark_inactive_variants_as_uninit {
return None;
}
MaybePlacesSwitchIntData::new(self.tcx, self.body, block, targets, discr)
}
fn apply_switch_int_edge_effect(
&self,
state: &mut Self::Domain,
data: &mut Self::SwitchIntData,
target_idx: SwitchTargetIndex,
) {
let inactive_variants = match target_idx {
SwitchTargetIndex::Normal(target_idx) => {
InactiveVariants::Active(data.variants[target_idx])
}
SwitchTargetIndex::Otherwise => InactiveVariants::Inactives(data.variants.clone()),
};
// 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,
data.enum_place,
&inactive_variants,
|mpi| state.gen_(mpi),
);
}
}
pub type EverInitializedPlacesDomain = DenseBitSet<Local>;
impl<'tcx> Analysis<'tcx> for EverInitializedPlaces<'_, 'tcx> {
type Domain = EverInitializedPlacesDomain;
const NAME: &'static str = "ever_init";
fn bottom_value(&self, body: &mir::Body<'tcx>) -> Self::Domain {
// bottom = no initialized locals by default
DenseBitSet::new_empty(body.local_decls.len())
}
fn initialize_start_block(&self, body: &mir::Body<'tcx>, state: &mut Self::Domain) {
for arg in body.args_iter() {
state.insert(arg);
}
}
#[instrument(skip(self, state), level = "debug")]
fn apply_primary_statement_effect(
&self,
state: &mut Self::Domain,
stmt: &mir::Statement<'tcx>,
location: Location,
) {
let move_data = self.move_data();
let init_loc_map = &move_data.init_loc_map;
// Record inits of locals. Projections can be ignored.
state.gen_all(init_loc_map[location].iter().copied().filter_map(|ii| {
let init_mpi = move_data.inits[ii].path;
move_data.move_paths[init_mpi].place.as_local()
}));
// Kill on StorageDead, so that an immutable variable can
// be reinitialized on the next iteration of the loop.
if let mir::StatementKind::StorageDead(local) = stmt.kind {
state.kill(local);
}
}
#[instrument(skip(self, state, _terminator), level = "debug")]
fn apply_primary_terminator_effect(
&self,
state: &mut Self::Domain,
_terminator: &mir::Terminator<'tcx>,
location: Location,
) {
let move_data = self.move_data();
let init_loc_map = &move_data.init_loc_map;
// Record inits of locals. Projections can be ignored.
state.gen_all(init_loc_map[location].iter().copied().filter_map(|ii| {
let init = &move_data.inits[ii];
if init.kind != InitKind::NonPanicPathOnly {
move_data.move_paths[init.path].place.as_local()
} else {
None
}
}));
}
fn apply_call_return_effect(
&self,
state: &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;
// Record inits of locals. Projections can be ignored.
let call_loc = self.body.terminator_loc(block);
state.gen_all(init_loc_map[call_loc].iter().copied().filter_map(|ii| {
let init = &move_data.inits[ii];
if init.kind == InitKind::NonPanicPathOnly {
move_data.move_paths[init.path].place.as_local()
} else {
None
}
}));
}
}
impl EverInitializedPlaces<'_, '_> {
/// Whether the init of `local` at `init` can reach `target` via a path that doesn't pass
/// through a `StorageDead(local)`. Mirrors the gen/kill structure of `EverInitializedPlaces`.
pub fn init_reaches_location(
body: &Body<'_>,
local: Local,
init: Init,
target: Location,
) -> bool {
let init_loc = match init.location {
// Arguments are initialized on entry, and `StorageDead` is never emitted for them, so
// they reach every location.
InitLocation::Argument(_) => return true,
InitLocation::Statement(init_loc) => init_loc,
};
// Worklist of locations to walk forward from, seeded with the location(s) following `init`.
let mut queue = vec![];
let basic_blocks = &body.basic_blocks;
let init_block_data = &basic_blocks[init_loc.block];
if init_loc.statement_index < init_block_data.statements.len() {
// This case mirrors `apply_primary_statement_effect`.
queue.push(init_loc.successor_within_block());
} else if init.kind == InitKind::NonPanicPathOnly {
// This case mirrors `apply_call_return_effect`.
let TerminatorEdges::AssignOnReturn { return_, .. } =
init_block_data.terminator().edges()
else {
bug!("`NonPanicPathOnly` should only be seen on terminators with return edges");
};
queue.extend(return_.into_iter().map(BasicBlock::start_location));
} else {
// This case mirrors `apply_primary_terminator_effect`.
queue.extend(init_block_data.terminator().successors().map(BasicBlock::start_location));
}
let mut visited = FxIndexSet::default();
'outer: while let Some(loc) = queue.pop() {
if !visited.insert(loc) {
continue;
}
// Walk from `loc` to the end of its block, looking for `target` or a kill.
let block_data = &basic_blocks[loc.block];
for statement_index in loc.statement_index..=block_data.statements.len() {
if target == (Location { block: loc.block, statement_index }) {
return true;
}
if let Some(stmt) = block_data.statements.get(statement_index)
&& let StatementKind::StorageDead(dead) = stmt.kind
&& dead == local
{
continue 'outer;
}
}
queue.extend(block_data.terminator().successors().map(BasicBlock::start_location));
}
false
}
}