1
Fork 0

Auto merge of #131326 - dingxiangfei2009:issue-130836-attempt-2, r=nikomatsakis

Reduce false positives of tail-expr-drop-order from consumed values (attempt #2)

r? `@nikomatsakis`

Tracked by #123739.

Related to #129864 but not replacing, yet.

Related to #130836.

This is an implementation of the approach suggested in the [Zulip stream](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/temporary.20drop.20order.20changes). A new MIR statement `BackwardsIncompatibleDrop` is added to the MIR syntax. The lint now works by inspecting possibly live move paths before at the `BackwardsIncompatibleDrop` location and the actual drop under the current edition, which should be one before Edition 2024 in practice.
This commit is contained in:
bors 2024-11-20 18:51:54 +00:00
commit 3fee0f12e4
58 changed files with 2015 additions and 538 deletions

View file

@ -1772,6 +1772,7 @@ impl<'tcx> Visitor<'tcx> for EnsureCoroutineFieldAssignmentsNeverAlias<'_> {
| StatementKind::Coverage(..)
| StatementKind::Intrinsic(..)
| StatementKind::ConstEvalCounter
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::Nop => {}
}
}

View file

@ -97,6 +97,7 @@ fn filtered_statement_span(statement: &Statement<'_>) -> Option<Span> {
StatementKind::StorageLive(_)
| StatementKind::StorageDead(_)
| StatementKind::ConstEvalCounter
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::Nop => None,
// FIXME(#78546): MIR InstrumentCoverage - Can the source_info.span for `FakeRead`

View file

@ -186,7 +186,8 @@ impl<'a, 'tcx> ConstAnalysis<'a, 'tcx> {
| StatementKind::FakeRead(..)
| StatementKind::PlaceMention(..)
| StatementKind::Coverage(..)
| StatementKind::AscribeUserType(..) => (),
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::AscribeUserType(..) => {}
}
}

View file

@ -99,7 +99,8 @@ fn eliminate<'tcx>(tcx: TyCtxt<'tcx>, body: &mut Body<'tcx>) {
| StatementKind::Intrinsic(_)
| StatementKind::ConstEvalCounter
| StatementKind::PlaceMention(_)
| StatementKind::Nop => (),
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::Nop => {}
StatementKind::FakeRead(_) | StatementKind::AscribeUserType(_, _) => {
bug!("{:?} not found in this MIR phase!", statement.kind)

View file

@ -581,7 +581,7 @@ impl WriteInfo {
| Rvalue::RawPtr(_, _)
| Rvalue::Len(_)
| Rvalue::Discriminant(_)
| Rvalue::CopyForDeref(_) => (),
| Rvalue::CopyForDeref(_) => {}
}
}
// Retags are technically also reads, but reporting them as a write suffices
@ -596,7 +596,8 @@ impl WriteInfo {
| StatementKind::Coverage(_)
| StatementKind::StorageLive(_)
| StatementKind::StorageDead(_)
| StatementKind::PlaceMention(_) => (),
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::PlaceMention(_) => {}
StatementKind::FakeRead(_) | StatementKind::AscribeUserType(_, _) => {
bug!("{:?} not found in this MIR phase", statement)
}

View file

@ -352,6 +352,7 @@ impl<'a, 'tcx> TOFinder<'a, 'tcx> {
| StatementKind::FakeRead(..)
| StatementKind::ConstEvalCounter
| StatementKind::PlaceMention(..)
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::Nop => None,
}
}

View file

@ -50,6 +50,7 @@ mod deduce_param_attrs;
mod errors;
mod ffi_unwind_calls;
mod lint;
mod lint_tail_expr_drop_order;
mod shim;
mod ssa;
@ -490,6 +491,7 @@ fn mir_drops_elaborated_and_const_checked(tcx: TyCtxt<'_>, def: LocalDefId) -> &
}
let (body, _) = tcx.mir_promoted(def);
lint_tail_expr_drop_order::run_lint(tcx, def, &body.borrow());
let mut body = body.steal();
if let Some(error_reported) = tainted_by_errors {

View file

@ -0,0 +1,701 @@
use std::cell::RefCell;
use std::collections::hash_map;
use std::rc::Rc;
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_data_structures::unord::{UnordMap, UnordSet};
use rustc_errors::Subdiagnostic;
use rustc_hir::CRATE_HIR_ID;
use rustc_hir::def_id::{DefId, LocalDefId};
use rustc_index::bit_set::ChunkedBitSet;
use rustc_index::{IndexSlice, IndexVec};
use rustc_macros::{LintDiagnostic, Subdiagnostic};
use rustc_middle::bug;
use rustc_middle::mir::{
self, BasicBlock, Body, ClearCrossCrate, Local, Location, Place, StatementKind, TerminatorKind,
dump_mir,
};
use rustc_middle::ty::{self, Ty, TyCtxt};
use rustc_mir_dataflow::impls::MaybeInitializedPlaces;
use rustc_mir_dataflow::move_paths::{LookupResult, MoveData, MovePathIndex};
use rustc_mir_dataflow::{Analysis, MaybeReachable, ResultsCursor};
use rustc_session::lint::builtin::TAIL_EXPR_DROP_ORDER;
use rustc_session::lint::{self};
use rustc_span::{DUMMY_SP, Span, Symbol};
use rustc_type_ir::data_structures::IndexMap;
use smallvec::{SmallVec, smallvec};
use tracing::{debug, instrument};
fn place_has_common_prefix<'tcx>(left: &Place<'tcx>, right: &Place<'tcx>) -> bool {
left.local == right.local
&& left.projection.iter().zip(right.projection).all(|(left, right)| left == right)
}
/// Cache entry of `drop` at a `BasicBlock`
#[derive(Debug, Clone, Copy)]
enum MovePathIndexAtBlock {
/// We know nothing yet
Unknown,
/// We know that the `drop` here has no effect
None,
/// We know that the `drop` here will invoke a destructor
Some(MovePathIndex),
}
struct DropsReachable<'a, 'mir, 'tcx> {
body: &'a Body<'tcx>,
place: &'a Place<'tcx>,
drop_span: &'a mut Option<Span>,
move_data: &'a MoveData<'tcx>,
maybe_init: &'a mut ResultsCursor<'mir, 'tcx, MaybeInitializedPlaces<'mir, 'tcx>>,
block_drop_value_info: &'a mut IndexSlice<BasicBlock, MovePathIndexAtBlock>,
collected_drops: &'a mut ChunkedBitSet<MovePathIndex>,
visited: FxHashMap<BasicBlock, Rc<RefCell<ChunkedBitSet<MovePathIndex>>>>,
}
impl<'a, 'mir, 'tcx> DropsReachable<'a, 'mir, 'tcx> {
fn visit(&mut self, block: BasicBlock) {
let move_set_size = self.move_data.move_paths.len();
let make_new_path_set = || Rc::new(RefCell::new(ChunkedBitSet::new_empty(move_set_size)));
let data = &self.body.basic_blocks[block];
let Some(terminator) = &data.terminator else { return };
// Given that we observe these dropped locals here at `block` so far,
// we will try to update the successor blocks.
// An occupied entry at `block` in `self.visited` signals that we have visited `block` before.
let dropped_local_here =
Rc::clone(self.visited.entry(block).or_insert_with(make_new_path_set));
// We could have invoked reverse lookup for a `MovePathIndex` every time, but unfortunately it is expensive.
// Let's cache them in `self.block_drop_value_info`.
match self.block_drop_value_info[block] {
MovePathIndexAtBlock::Some(dropped) => {
dropped_local_here.borrow_mut().insert(dropped);
}
MovePathIndexAtBlock::Unknown => {
if let TerminatorKind::Drop { place, .. } = &terminator.kind
&& let LookupResult::Exact(idx) | LookupResult::Parent(Some(idx)) =
self.move_data.rev_lookup.find(place.as_ref())
{
// Since we are working with MIRs at a very early stage,
// observing a `drop` terminator is not indicative enough that
// the drop will definitely happen.
// That is decided in the drop elaboration pass instead.
// Therefore, we need to consult with the maybe-initialization information.
self.maybe_init.seek_before_primary_effect(Location {
block,
statement_index: data.statements.len(),
});
// Check if the drop of `place` under inspection is really in effect.
// This is true only when `place` may have been initialized along a control flow path from a BID to the drop program point today.
// In other words, this is where the drop of `place` will happen in the future instead.
if let MaybeReachable::Reachable(maybe_init) = self.maybe_init.get()
&& maybe_init.contains(idx)
{
// We also cache the drop information, so that we do not need to check on data-flow cursor again
self.block_drop_value_info[block] = MovePathIndexAtBlock::Some(idx);
dropped_local_here.borrow_mut().insert(idx);
} else {
self.block_drop_value_info[block] = MovePathIndexAtBlock::None;
}
}
}
MovePathIndexAtBlock::None => {}
}
for succ in terminator.successors() {
let target = &self.body.basic_blocks[succ];
if target.is_cleanup {
continue;
}
// As long as we are passing through a new block, or new dropped places to propagate,
// we will proceed with `succ`
let dropped_local_there = match self.visited.entry(succ) {
hash_map::Entry::Occupied(occupied_entry) => {
if succ == block
|| !occupied_entry.get().borrow_mut().union(&*dropped_local_here.borrow())
{
// `succ` has been visited but no new drops observed so far,
// so we can bail on `succ` until new drop information arrives
continue;
}
Rc::clone(occupied_entry.get())
}
hash_map::Entry::Vacant(vacant_entry) => Rc::clone(
vacant_entry.insert(Rc::new(RefCell::new(dropped_local_here.borrow().clone()))),
),
};
if let Some(terminator) = &target.terminator
&& let TerminatorKind::Drop {
place: dropped_place,
target: _,
unwind: _,
replace: _,
} = &terminator.kind
&& place_has_common_prefix(dropped_place, self.place)
{
// We have now reached the current drop of the `place`.
// Let's check the observed dropped places in.
self.collected_drops.union(&*dropped_local_there.borrow());
if self.drop_span.is_none() {
// FIXME(@dingxiangfei2009): it turns out that `self.body.source_scopes` are still a bit wonky.
// There is a high chance that this span still points to a block rather than a statement semicolon.
*self.drop_span = Some(terminator.source_info.span);
}
// Now we have discovered a simple control flow path from a future drop point
// to the current drop point.
// We will not continue from there.
} else {
self.visit(succ)
}
}
}
}
/// An additional filter to exclude well-known types from the ecosystem
/// because their drops are trivial.
/// This returns additional types to check if the drops are delegated to those.
/// A typical example is `hashbrown::HashMap<K, V>`, whose drop is delegated to `K` and `V`.
fn true_significant_drop_ty<'tcx>(
tcx: TyCtxt<'tcx>,
ty: Ty<'tcx>,
) -> Option<SmallVec<[Ty<'tcx>; 2]>> {
if let ty::Adt(def, args) = ty.kind() {
let mut did = def.did();
let mut name_rev = vec![];
loop {
let key = tcx.def_key(did);
match key.disambiguated_data.data {
rustc_hir::definitions::DefPathData::CrateRoot => {
name_rev.push(tcx.crate_name(did.krate))
}
rustc_hir::definitions::DefPathData::TypeNs(symbol) => name_rev.push(symbol),
_ => return None,
}
if let Some(parent) = key.parent {
did = DefId { krate: did.krate, index: parent };
} else {
break;
}
}
let name_str: Vec<_> = name_rev.iter().rev().map(|x| x.as_str()).collect();
debug!(?name_str);
match name_str[..] {
// These are the types from Rust core ecosystem
["sym" | "proc_macro2", ..]
| ["core" | "std", "task", "LocalWaker" | "Waker"]
| ["core" | "std", "task", "wake", "LocalWaker" | "Waker"] => Some(smallvec![]),
// These are important types from Rust ecosystem
["tracing", "instrument", "Instrumented"] | ["bytes", "Bytes"] => Some(smallvec![]),
["hashbrown", "raw", "RawTable" | "RawIntoIter"] => {
if let [ty, ..] = &***args
&& let Some(ty) = ty.as_type()
{
Some(smallvec![ty])
} else {
None
}
}
["hashbrown", "raw", "RawDrain"] => {
if let [_, ty, ..] = &***args
&& let Some(ty) = ty.as_type()
{
Some(smallvec![ty])
} else {
None
}
}
_ => None,
}
} else {
None
}
}
/// Returns the list of types with a "potentially sigificant" that may be dropped
/// by dropping a value of type `ty`.
#[instrument(level = "debug", skip(tcx, param_env))]
fn extract_component_raw<'tcx>(
tcx: TyCtxt<'tcx>,
param_env: ty::ParamEnv<'tcx>,
ty: Ty<'tcx>,
ty_seen: &mut UnordSet<Ty<'tcx>>,
) -> SmallVec<[Ty<'tcx>; 4]> {
// Droppiness does not depend on regions, so let us erase them.
let ty = tcx
.try_normalize_erasing_regions(
ty::TypingEnv { param_env, typing_mode: ty::TypingMode::PostAnalysis },
ty,
)
.unwrap_or(ty);
let tys = tcx.list_significant_drop_tys(param_env.and(ty));
debug!(?ty, "components");
let mut out_tys = smallvec![];
for ty in tys {
if let Some(tys) = true_significant_drop_ty(tcx, ty) {
// Some types can be further opened up because the drop is simply delegated
for ty in tys {
if ty_seen.insert(ty) {
out_tys.extend(extract_component_raw(tcx, param_env, ty, ty_seen));
}
}
} else {
if ty_seen.insert(ty) {
out_tys.push(ty);
}
}
}
out_tys
}
#[instrument(level = "debug", skip(tcx, param_env))]
fn extract_component_with_significant_dtor<'tcx>(
tcx: TyCtxt<'tcx>,
param_env: ty::ParamEnv<'tcx>,
ty: Ty<'tcx>,
) -> SmallVec<[Ty<'tcx>; 4]> {
let mut tys = extract_component_raw(tcx, param_env, ty, &mut Default::default());
let mut deduplicate = FxHashSet::default();
tys.retain(|oty| deduplicate.insert(*oty));
tys.into_iter().collect()
}
/// Extract the span of the custom destructor of a type
/// especially the span of the `impl Drop` header or its entire block
/// when we are working with current local crate.
#[instrument(level = "debug", skip(tcx))]
fn ty_dtor_span<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option<Span> {
match ty.kind() {
ty::Bool
| ty::Char
| ty::Int(_)
| ty::Uint(_)
| ty::Float(_)
| ty::Error(_)
| ty::Str
| ty::Never
| ty::RawPtr(_, _)
| ty::Ref(_, _, _)
| ty::FnPtr(_, _)
| ty::Tuple(_)
| ty::Dynamic(_, _, _)
| ty::Alias(_, _)
| ty::Bound(_, _)
| ty::Pat(_, _)
| ty::Placeholder(_)
| ty::Infer(_)
| ty::Slice(_)
| ty::Array(_, _) => None,
ty::Adt(adt_def, _) => {
let did = adt_def.did();
let try_local_did_span = |did: DefId| {
if let Some(local) = did.as_local() {
tcx.source_span(local)
} else {
tcx.def_span(did)
}
};
let dtor = if let Some(dtor) = tcx.adt_destructor(did) {
dtor.did
} else if let Some(dtor) = tcx.adt_async_destructor(did) {
dtor.future
} else {
return Some(try_local_did_span(did));
};
let def_key = tcx.def_key(dtor);
let Some(parent_index) = def_key.parent else { return Some(try_local_did_span(dtor)) };
let parent_did = DefId { index: parent_index, krate: dtor.krate };
Some(try_local_did_span(parent_did))
}
ty::Coroutine(did, _)
| ty::CoroutineWitness(did, _)
| ty::CoroutineClosure(did, _)
| ty::Closure(did, _)
| ty::FnDef(did, _)
| ty::Foreign(did) => Some(tcx.def_span(did)),
ty::Param(_) => None,
}
}
/// Check if a moved place at `idx` is a part of a BID.
/// The use of this check is that we will consider drops on these
/// as a drop of the overall BID and, thus, we can exclude it from the diagnosis.
fn place_descendent_of_bids<'tcx>(
mut idx: MovePathIndex,
move_data: &MoveData<'tcx>,
bids: &UnordSet<&Place<'tcx>>,
) -> bool {
loop {
let path = &move_data.move_paths[idx];
if bids.contains(&path.place) {
return true;
}
if let Some(parent) = path.parent {
idx = parent;
} else {
return false;
}
}
}
/// The core of the lint `tail-expr-drop-order`
pub(crate) fn run_lint<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId, body: &Body<'tcx>) {
if matches!(tcx.def_kind(def_id), rustc_hir::def::DefKind::SyntheticCoroutineBody) {
// A synthetic coroutine has no HIR body and it is enough to just analyse the original body
return;
}
if body.span.edition().at_least_rust_2024()
|| tcx.lints_that_dont_need_to_run(()).contains(&lint::LintId::of(TAIL_EXPR_DROP_ORDER))
{
return;
}
// ## About BIDs in blocks ##
// Track the set of blocks that contain a backwards-incompatible drop (BID)
// and, for each block, the vector of locations.
//
// We group them per-block because they tend to scheduled in the same drop ladder block.
let mut bid_per_block = IndexMap::default();
let mut bid_places = UnordSet::new();
let param_env = tcx.param_env(def_id).with_reveal_all_normalized(tcx);
let mut ty_dropped_components = UnordMap::default();
for (block, data) in body.basic_blocks.iter_enumerated() {
for (statement_index, stmt) in data.statements.iter().enumerate() {
if let StatementKind::BackwardIncompatibleDropHint { place, reason: _ } = &stmt.kind {
let ty = place.ty(body, tcx).ty;
if ty_dropped_components
.entry(ty)
.or_insert_with(|| extract_component_with_significant_dtor(tcx, param_env, ty))
.is_empty()
{
continue;
}
bid_per_block
.entry(block)
.or_insert(vec![])
.push((Location { block, statement_index }, &**place));
bid_places.insert(&**place);
}
}
}
if bid_per_block.is_empty() {
return;
}
dump_mir(tcx, false, "lint_tail_expr_drop_order", &0 as _, body, |_, _| Ok(()));
let locals_with_user_names = collect_user_names(body);
let is_closure_like = tcx.is_closure_like(def_id.to_def_id());
// Compute the "maybe initialized" information for this body.
// When we encounter a DROP of some place P we only care
// about the drop if `P` may be initialized.
let move_data = MoveData::gather_moves(body, tcx, |_| true);
let maybe_init = MaybeInitializedPlaces::new(tcx, body, &move_data);
let mut maybe_init = maybe_init.iterate_to_fixpoint(tcx, body, None).into_results_cursor(body);
let mut block_drop_value_info =
IndexVec::from_elem_n(MovePathIndexAtBlock::Unknown, body.basic_blocks.len());
for (&block, candidates) in &bid_per_block {
// We will collect drops on locals on paths between BID points to their actual drop locations
// into `all_locals_dropped`.
let mut all_locals_dropped = ChunkedBitSet::new_empty(move_data.move_paths.len());
let mut drop_span = None;
for &(_, place) in candidates.iter() {
let mut collected_drops = ChunkedBitSet::new_empty(move_data.move_paths.len());
// ## On detecting change in relative drop order ##
// Iterate through each BID-containing block `block`.
// If the place `P` targeted by the BID is "maybe initialized",
// then search forward to find the actual `DROP(P)` point.
// Everything dropped between the BID and the actual drop point
// is something whose relative drop order will change.
DropsReachable {
body,
place,
drop_span: &mut drop_span,
move_data: &move_data,
maybe_init: &mut maybe_init,
block_drop_value_info: &mut block_drop_value_info,
collected_drops: &mut collected_drops,
visited: Default::default(),
}
.visit(block);
// Compute the set `all_locals_dropped` of local variables that are dropped
// after the BID point but before the current drop point.
//
// These are the variables whose drop impls will be reordered with respect
// to `place`.
all_locals_dropped.union(&collected_drops);
}
// We shall now exclude some local bindings for the following cases.
{
let mut to_exclude = ChunkedBitSet::new_empty(all_locals_dropped.domain_size());
// We will now do subtraction from the candidate dropped locals, because of the following reasons.
for path_idx in all_locals_dropped.iter() {
let move_path = &move_data.move_paths[path_idx];
let dropped_local = move_path.place.local;
// a) A return value _0 will eventually be used
// Example:
// fn f() -> Droppy {
// let _x = Droppy;
// Droppy
// }
// _0 holds the literal `Droppy` and rightfully `_x` has to be dropped first
if dropped_local == Local::ZERO {
debug!(?dropped_local, "skip return value");
to_exclude.insert(path_idx);
continue;
}
// b) If we are analysing a closure, the captures are still dropped last.
// This is part of the closure capture lifetime contract.
// They are similar to the return value _0 with respect to lifetime rules.
if is_closure_like && matches!(dropped_local, ty::CAPTURE_STRUCT_LOCAL) {
debug!(?dropped_local, "skip closure captures");
to_exclude.insert(path_idx);
continue;
}
// c) Sometimes we collect places that are projections into the BID locals,
// so they are considered dropped now.
// Example:
// struct NotVeryDroppy(Droppy);
// impl Drop for Droppy {..}
// fn f() -> NotVeryDroppy {
// let x = NotVeryDroppy(droppy());
// {
// let y: Droppy = x.0;
// NotVeryDroppy(y)
// }
// }
// `y` takes `x.0`, which invalidates `x` as a complete `NotVeryDroppy`
// so there is no point in linting against `x` any more.
if place_descendent_of_bids(path_idx, &move_data, &bid_places) {
debug!(?dropped_local, "skip descendent of bids");
to_exclude.insert(path_idx);
continue;
}
let observer_ty = move_path.place.ty(body, tcx).ty;
// d) The collected local has no custom destructor that passes our ecosystem filter.
if ty_dropped_components
.entry(observer_ty)
.or_insert_with(|| {
extract_component_with_significant_dtor(tcx, param_env, observer_ty)
})
.is_empty()
{
debug!(?dropped_local, "skip non-droppy types");
to_exclude.insert(path_idx);
continue;
}
}
// Suppose that all BIDs point into the same local,
// we can remove the this local from the observed drops,
// so that we can focus our diagnosis more on the others.
if candidates.iter().all(|&(_, place)| candidates[0].1.local == place.local) {
for path_idx in all_locals_dropped.iter() {
if move_data.move_paths[path_idx].place.local == candidates[0].1.local {
to_exclude.insert(path_idx);
}
}
}
all_locals_dropped.subtract(&to_exclude);
}
if all_locals_dropped.is_empty() {
// No drop effect is observable, so let us move on.
continue;
}
// ## The final work to assemble the diagnosis ##
// First collect or generate fresh names for local variable bindings and temporary values.
let local_names = assign_observables_names(
all_locals_dropped
.iter()
.map(|path_idx| move_data.move_paths[path_idx].place.local)
.chain(candidates.iter().map(|(_, place)| place.local)),
&locals_with_user_names,
);
let mut lint_root = None;
let mut local_labels = vec![];
// We now collect the types with custom destructors.
for &(_, place) in candidates {
let linted_local_decl = &body.local_decls[place.local];
let Some(&(ref name, is_generated_name)) = local_names.get(&place.local) else {
bug!("a name should have been assigned")
};
let name = name.as_str();
if lint_root.is_none()
&& let ClearCrossCrate::Set(data) =
&body.source_scopes[linted_local_decl.source_info.scope].local_data
{
lint_root = Some(data.lint_root);
}
// Collect spans of the custom destructors.
let mut seen_dyn = false;
let destructors = ty_dropped_components
.get(&linted_local_decl.ty)
.unwrap()
.iter()
.filter_map(|&ty| {
if let Some(span) = ty_dtor_span(tcx, ty) {
Some(DestructorLabel { span, name, dtor_kind: "concrete" })
} else if matches!(ty.kind(), ty::Dynamic(..)) {
if seen_dyn {
None
} else {
seen_dyn = true;
Some(DestructorLabel { span: DUMMY_SP, name, dtor_kind: "dyn" })
}
} else {
None
}
})
.collect();
local_labels.push(LocalLabel {
span: linted_local_decl.source_info.span,
destructors,
name,
is_generated_name,
is_dropped_first_edition_2024: true,
});
}
// Similarly, custom destructors of the observed drops.
for path_idx in all_locals_dropped.iter() {
let place = &move_data.move_paths[path_idx].place;
// We are not using the type of the local because the drop may be partial.
let observer_ty = place.ty(body, tcx).ty;
let observer_local_decl = &body.local_decls[place.local];
let Some(&(ref name, is_generated_name)) = local_names.get(&place.local) else {
bug!("a name should have been assigned")
};
let name = name.as_str();
let mut seen_dyn = false;
let destructors = extract_component_with_significant_dtor(tcx, param_env, observer_ty)
.into_iter()
.filter_map(|ty| {
if let Some(span) = ty_dtor_span(tcx, ty) {
Some(DestructorLabel { span, name, dtor_kind: "concrete" })
} else if matches!(ty.kind(), ty::Dynamic(..)) {
if seen_dyn {
None
} else {
seen_dyn = true;
Some(DestructorLabel { span: DUMMY_SP, name, dtor_kind: "dyn" })
}
} else {
None
}
})
.collect();
local_labels.push(LocalLabel {
span: observer_local_decl.source_info.span,
destructors,
name,
is_generated_name,
is_dropped_first_edition_2024: false,
});
}
let span = local_labels[0].span;
tcx.emit_node_span_lint(
lint::builtin::TAIL_EXPR_DROP_ORDER,
lint_root.unwrap_or(CRATE_HIR_ID),
span,
TailExprDropOrderLint { local_labels, drop_span, _epilogue: () },
);
}
}
/// Extract binding names if available for diagnosis
fn collect_user_names(body: &Body<'_>) -> IndexMap<Local, Symbol> {
let mut names = IndexMap::default();
for var_debug_info in &body.var_debug_info {
if let mir::VarDebugInfoContents::Place(place) = &var_debug_info.value
&& let Some(local) = place.local_or_deref_local()
{
names.entry(local).or_insert(var_debug_info.name);
}
}
names
}
/// Assign names for anonymous or temporary values for diagnosis
fn assign_observables_names(
locals: impl IntoIterator<Item = Local>,
user_names: &IndexMap<Local, Symbol>,
) -> IndexMap<Local, (String, bool)> {
let mut names = IndexMap::default();
let mut assigned_names = FxHashSet::default();
let mut idx = 0u64;
let mut fresh_name = || {
idx += 1;
(format!("#{idx}"), true)
};
for local in locals {
let name = if let Some(name) = user_names.get(&local) {
let name = name.as_str();
if assigned_names.contains(name) { fresh_name() } else { (name.to_owned(), false) }
} else {
fresh_name()
};
assigned_names.insert(name.0.clone());
names.insert(local, name);
}
names
}
#[derive(LintDiagnostic)]
#[diag(mir_transform_tail_expr_drop_order)]
struct TailExprDropOrderLint<'a> {
#[subdiagnostic]
local_labels: Vec<LocalLabel<'a>>,
#[label(mir_transform_drop_location)]
drop_span: Option<Span>,
#[note(mir_transform_note_epilogue)]
_epilogue: (),
}
struct LocalLabel<'a> {
span: Span,
name: &'a str,
is_generated_name: bool,
is_dropped_first_edition_2024: bool,
destructors: Vec<DestructorLabel<'a>>,
}
/// A custom `Subdiagnostic` implementation so that the notes are delivered in a specific order
impl Subdiagnostic for LocalLabel<'_> {
fn add_to_diag_with<
G: rustc_errors::EmissionGuarantee,
F: rustc_errors::SubdiagMessageOp<G>,
>(
self,
diag: &mut rustc_errors::Diag<'_, G>,
f: &F,
) {
diag.arg("name", self.name);
diag.arg("is_generated_name", self.is_generated_name);
diag.arg("is_dropped_first_edition_2024", self.is_dropped_first_edition_2024);
let msg = f(diag, crate::fluent_generated::mir_transform_tail_expr_local.into());
diag.span_label(self.span, msg);
for dtor in self.destructors {
dtor.add_to_diag_with(diag, f);
}
let msg = f(diag, crate::fluent_generated::mir_transform_label_local_epilogue.into());
diag.span_label(self.span, msg);
}
}
#[derive(Subdiagnostic)]
#[note(mir_transform_tail_expr_dtor)]
struct DestructorLabel<'a> {
#[primary_span]
span: Span,
dtor_kind: &'static str,
name: &'a str,
}

View file

@ -92,6 +92,7 @@ impl RemoveNoopLandingPads {
| StatementKind::AscribeUserType(..)
| StatementKind::Coverage(..)
| StatementKind::ConstEvalCounter
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::Nop => {
// These are all noops in a landing pad
}

View file

@ -125,6 +125,7 @@ impl<'tcx> MutVisitor<'tcx> for Replacer<'_, 'tcx> {
StatementKind::Coverage(_)
| StatementKind::Intrinsic(_)
| StatementKind::Nop
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::ConstEvalCounter => None,
};
if let Some(place_for_ty) = place_for_ty

View file

@ -523,7 +523,8 @@ impl<'tcx> Visitor<'tcx> for UsedLocals {
}
StatementKind::SetDiscriminant { ref place, variant_index: _ }
| StatementKind::Deinit(ref place) => {
| StatementKind::Deinit(ref place)
| StatementKind::BackwardIncompatibleDropHint { ref place, reason: _ } => {
self.visit_lhs(place, location);
}
}
@ -560,6 +561,7 @@ fn remove_unused_definitions_helper(used_locals: &mut UsedLocals, body: &mut Bod
StatementKind::Assign(box (place, _)) => used_locals.is_used(place.local),
StatementKind::SetDiscriminant { ref place, .. }
| StatementKind::BackwardIncompatibleDropHint { ref place, reason: _ }
| StatementKind::Deinit(ref place) => used_locals.is_used(place.local),
StatementKind::Nop => false,
_ => true,
@ -587,6 +589,20 @@ impl<'tcx> MutVisitor<'tcx> for LocalUpdater<'tcx> {
self.tcx
}
fn visit_statement(&mut self, statement: &mut Statement<'tcx>, location: Location) {
if let StatementKind::BackwardIncompatibleDropHint { place, reason: _ } =
&mut statement.kind
{
self.visit_local(
&mut place.local,
PlaceContext::MutatingUse(MutatingUseContext::Store),
location,
);
} else {
self.super_statement(statement, location);
}
}
fn visit_local(&mut self, l: &mut Local, _: PlaceContext, _: Location) {
*l = self.map[*l].unwrap();
}

View file

@ -343,6 +343,7 @@ impl<'a, 'tcx> Visitor<'tcx> for CfgChecker<'a, 'tcx> {
| StatementKind::Intrinsic(_)
| StatementKind::ConstEvalCounter
| StatementKind::PlaceMention(..)
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::Nop => {}
}
@ -1493,6 +1494,7 @@ impl<'a, 'tcx> Visitor<'tcx> for TypeChecker<'a, 'tcx> {
| StatementKind::Coverage(_)
| StatementKind::ConstEvalCounter
| StatementKind::PlaceMention(..)
| StatementKind::BackwardIncompatibleDropHint { .. }
| StatementKind::Nop => {}
}