rustc_mir_transform/
lint_tail_expr_drop_order.rs

1use std::cell::RefCell;
2use std::collections::hash_map;
3use std::rc::Rc;
4
5use itertools::Itertools as _;
6use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxIndexMap};
7use rustc_data_structures::unord::{UnordMap, UnordSet};
8use rustc_errors::Subdiagnostic;
9use rustc_hir::CRATE_HIR_ID;
10use rustc_hir::def_id::LocalDefId;
11use rustc_index::bit_set::MixedBitSet;
12use rustc_index::{IndexSlice, IndexVec};
13use rustc_macros::{LintDiagnostic, Subdiagnostic};
14use rustc_middle::bug;
15use rustc_middle::mir::{
16    self, BackwardIncompatibleDropReason, BasicBlock, Body, ClearCrossCrate, Local, Location,
17    MirDumper, Place, StatementKind, TerminatorKind,
18};
19use rustc_middle::ty::significant_drop_order::{
20    extract_component_with_significant_dtor, ty_dtor_span,
21};
22use rustc_middle::ty::{self, TyCtxt};
23use rustc_mir_dataflow::impls::MaybeInitializedPlaces;
24use rustc_mir_dataflow::move_paths::{LookupResult, MoveData, MovePathIndex};
25use rustc_mir_dataflow::{Analysis, MaybeReachable, ResultsCursor};
26use rustc_session::lint::builtin::TAIL_EXPR_DROP_ORDER;
27use rustc_session::lint::{self};
28use rustc_span::{DUMMY_SP, Span, Symbol};
29use tracing::debug;
30
31fn place_has_common_prefix<'tcx>(left: &Place<'tcx>, right: &Place<'tcx>) -> bool {
32    left.local == right.local
33        && left.projection.iter().zip(right.projection).all(|(left, right)| left == right)
34}
35
36/// Cache entry of `drop` at a `BasicBlock`
37#[derive(Debug, Clone, Copy)]
38enum MovePathIndexAtBlock {
39    /// We know nothing yet
40    Unknown,
41    /// We know that the `drop` here has no effect
42    None,
43    /// We know that the `drop` here will invoke a destructor
44    Some(MovePathIndex),
45}
46
47struct DropsReachable<'a, 'mir, 'tcx> {
48    body: &'a Body<'tcx>,
49    place: &'a Place<'tcx>,
50    drop_span: &'a mut Option<Span>,
51    move_data: &'a MoveData<'tcx>,
52    maybe_init: &'a mut ResultsCursor<'mir, 'tcx, MaybeInitializedPlaces<'mir, 'tcx>>,
53    block_drop_value_info: &'a mut IndexSlice<BasicBlock, MovePathIndexAtBlock>,
54    collected_drops: &'a mut MixedBitSet<MovePathIndex>,
55    visited: FxHashMap<BasicBlock, Rc<RefCell<MixedBitSet<MovePathIndex>>>>,
56}
57
58impl<'a, 'mir, 'tcx> DropsReachable<'a, 'mir, 'tcx> {
59    fn visit(&mut self, block: BasicBlock) {
60        let move_set_size = self.move_data.move_paths.len();
61        let make_new_path_set = || Rc::new(RefCell::new(MixedBitSet::new_empty(move_set_size)));
62
63        let data = &self.body.basic_blocks[block];
64        let Some(terminator) = &data.terminator else { return };
65        // Given that we observe these dropped locals here at `block` so far, we will try to update
66        // the successor blocks. An occupied entry at `block` in `self.visited` signals that we
67        // have visited `block` before.
68        let dropped_local_here =
69            Rc::clone(self.visited.entry(block).or_insert_with(make_new_path_set));
70        // We could have invoked reverse lookup for a `MovePathIndex` every time, but unfortunately
71        // it is expensive. Let's cache them in `self.block_drop_value_info`.
72        match self.block_drop_value_info[block] {
73            MovePathIndexAtBlock::Some(dropped) => {
74                dropped_local_here.borrow_mut().insert(dropped);
75            }
76            MovePathIndexAtBlock::Unknown => {
77                if let TerminatorKind::Drop { place, .. } = &terminator.kind
78                    && let LookupResult::Exact(idx) | LookupResult::Parent(Some(idx)) =
79                        self.move_data.rev_lookup.find(place.as_ref())
80                {
81                    // Since we are working with MIRs at a very early stage, observing a `drop`
82                    // terminator is not indicative enough that the drop will definitely happen.
83                    // That is decided in the drop elaboration pass instead. Therefore, we need to
84                    // consult with the maybe-initialization information.
85                    self.maybe_init.seek_before_primary_effect(Location {
86                        block,
87                        statement_index: data.statements.len(),
88                    });
89
90                    // Check if the drop of `place` under inspection is really in effect. This is
91                    // true only when `place` may have been initialized along a control flow path
92                    // from a BID to the drop program point today. In other words, this is where
93                    // the drop of `place` will happen in the future instead.
94                    if let MaybeReachable::Reachable(maybe_init) = self.maybe_init.get()
95                        && maybe_init.contains(idx)
96                    {
97                        // We also cache the drop information, so that we do not need to check on
98                        // data-flow cursor again.
99                        self.block_drop_value_info[block] = MovePathIndexAtBlock::Some(idx);
100                        dropped_local_here.borrow_mut().insert(idx);
101                    } else {
102                        self.block_drop_value_info[block] = MovePathIndexAtBlock::None;
103                    }
104                }
105            }
106            MovePathIndexAtBlock::None => {}
107        }
108
109        for succ in terminator.successors() {
110            let target = &self.body.basic_blocks[succ];
111            if target.is_cleanup {
112                continue;
113            }
114
115            // As long as we are passing through a new block, or new dropped places to propagate,
116            // we will proceed with `succ`
117            let dropped_local_there = match self.visited.entry(succ) {
118                hash_map::Entry::Occupied(occupied_entry) => {
119                    if succ == block
120                        || !occupied_entry.get().borrow_mut().union(&*dropped_local_here.borrow())
121                    {
122                        // `succ` has been visited but no new drops observed so far,
123                        // so we can bail on `succ` until new drop information arrives
124                        continue;
125                    }
126                    Rc::clone(occupied_entry.get())
127                }
128                hash_map::Entry::Vacant(vacant_entry) => Rc::clone(
129                    vacant_entry.insert(Rc::new(RefCell::new(dropped_local_here.borrow().clone()))),
130                ),
131            };
132            if let Some(terminator) = &target.terminator
133                && let TerminatorKind::Drop {
134                    place: dropped_place,
135                    target: _,
136                    unwind: _,
137                    replace: _,
138                    drop: _,
139                    async_fut: _,
140                } = &terminator.kind
141                && place_has_common_prefix(dropped_place, self.place)
142            {
143                // We have now reached the current drop of the `place`.
144                // Let's check the observed dropped places in.
145                self.collected_drops.union(&*dropped_local_there.borrow());
146                if self.drop_span.is_none() {
147                    // FIXME(@dingxiangfei2009): it turns out that `self.body.source_scopes` are
148                    // still a bit wonky. There is a high chance that this span still points to a
149                    // block rather than a statement semicolon.
150                    *self.drop_span = Some(terminator.source_info.span);
151                }
152                // Now we have discovered a simple control flow path from a future drop point
153                // to the current drop point.
154                // We will not continue from there.
155            } else {
156                self.visit(succ)
157            }
158        }
159    }
160}
161
162/// Check if a moved place at `idx` is a part of a BID.
163/// The use of this check is that we will consider drops on these
164/// as a drop of the overall BID and, thus, we can exclude it from the diagnosis.
165fn place_descendent_of_bids<'tcx>(
166    mut idx: MovePathIndex,
167    move_data: &MoveData<'tcx>,
168    bids: &UnordSet<&Place<'tcx>>,
169) -> bool {
170    loop {
171        let path = &move_data.move_paths[idx];
172        if bids.contains(&path.place) {
173            return true;
174        }
175        if let Some(parent) = path.parent {
176            idx = parent;
177        } else {
178            return false;
179        }
180    }
181}
182
183/// The core of the lint `tail-expr-drop-order`
184pub(crate) fn run_lint<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalDefId, body: &Body<'tcx>) {
185    if matches!(tcx.def_kind(def_id), rustc_hir::def::DefKind::SyntheticCoroutineBody) {
186        // A synthetic coroutine has no HIR body and it is enough to just analyse the original body
187        return;
188    }
189    if body.span.edition().at_least_rust_2024()
190        || tcx.lints_that_dont_need_to_run(()).contains(&lint::LintId::of(TAIL_EXPR_DROP_ORDER))
191    {
192        return;
193    }
194
195    // FIXME(typing_env): This should be able to reveal the opaques local to the
196    // body using the typeck results.
197    let typing_env = ty::TypingEnv::non_body_analysis(tcx, def_id);
198
199    // ## About BIDs in blocks ##
200    // Track the set of blocks that contain a backwards-incompatible drop (BID)
201    // and, for each block, the vector of locations.
202    //
203    // We group them per-block because they tend to scheduled in the same drop ladder block.
204    let mut bid_per_block = FxIndexMap::default();
205    let mut bid_places = UnordSet::new();
206
207    let mut ty_dropped_components = UnordMap::default();
208    for (block, data) in body.basic_blocks.iter_enumerated() {
209        for (statement_index, stmt) in data.statements.iter().enumerate() {
210            if let StatementKind::BackwardIncompatibleDropHint {
211                place,
212                reason: BackwardIncompatibleDropReason::Edition2024,
213            } = &stmt.kind
214            {
215                let ty = place.ty(body, tcx).ty;
216                if ty_dropped_components
217                    .entry(ty)
218                    .or_insert_with(|| extract_component_with_significant_dtor(tcx, typing_env, ty))
219                    .is_empty()
220                {
221                    continue;
222                }
223                bid_per_block
224                    .entry(block)
225                    .or_insert(vec![])
226                    .push((Location { block, statement_index }, &**place));
227                bid_places.insert(&**place);
228            }
229        }
230    }
231    if bid_per_block.is_empty() {
232        return;
233    }
234
235    if let Some(dumper) = MirDumper::new(tcx, "lint_tail_expr_drop_order", body) {
236        dumper.dump_mir(body);
237    }
238
239    let locals_with_user_names = collect_user_names(body);
240    let is_closure_like = tcx.is_closure_like(def_id.to_def_id());
241
242    // Compute the "maybe initialized" information for this body.
243    // When we encounter a DROP of some place P we only care
244    // about the drop if `P` may be initialized.
245    let move_data = MoveData::gather_moves(body, tcx, |_| true);
246    let mut maybe_init = MaybeInitializedPlaces::new(tcx, body, &move_data)
247        .iterate_to_fixpoint(tcx, body, None)
248        .into_results_cursor(body);
249    let mut block_drop_value_info =
250        IndexVec::from_elem_n(MovePathIndexAtBlock::Unknown, body.basic_blocks.len());
251    for (&block, candidates) in &bid_per_block {
252        // We will collect drops on locals on paths between BID points to their actual drop locations
253        // into `all_locals_dropped`.
254        let mut all_locals_dropped = MixedBitSet::new_empty(move_data.move_paths.len());
255        let mut drop_span = None;
256        for &(_, place) in candidates.iter() {
257            let mut collected_drops = MixedBitSet::new_empty(move_data.move_paths.len());
258            // ## On detecting change in relative drop order ##
259            // Iterate through each BID-containing block `block`.
260            // If the place `P` targeted by the BID is "maybe initialized",
261            // then search forward to find the actual `DROP(P)` point.
262            // Everything dropped between the BID and the actual drop point
263            // is something whose relative drop order will change.
264            DropsReachable {
265                body,
266                place,
267                drop_span: &mut drop_span,
268                move_data: &move_data,
269                maybe_init: &mut maybe_init,
270                block_drop_value_info: &mut block_drop_value_info,
271                collected_drops: &mut collected_drops,
272                visited: Default::default(),
273            }
274            .visit(block);
275            // Compute the set `all_locals_dropped` of local variables that are dropped
276            // after the BID point but before the current drop point.
277            //
278            // These are the variables whose drop impls will be reordered with respect
279            // to `place`.
280            all_locals_dropped.union(&collected_drops);
281        }
282
283        // We shall now exclude some local bindings for the following cases.
284        {
285            let mut to_exclude = MixedBitSet::new_empty(all_locals_dropped.domain_size());
286            // We will now do subtraction from the candidate dropped locals, because of the
287            // following reasons.
288            for path_idx in all_locals_dropped.iter() {
289                let move_path = &move_data.move_paths[path_idx];
290                let dropped_local = move_path.place.local;
291                // a) A return value _0 will eventually be used
292                // Example:
293                // fn f() -> Droppy {
294                //     let _x = Droppy;
295                //     Droppy
296                // }
297                // _0 holds the literal `Droppy` and rightfully `_x` has to be dropped first
298                if dropped_local == Local::ZERO {
299                    debug!(?dropped_local, "skip return value");
300                    to_exclude.insert(path_idx);
301                    continue;
302                }
303                // b) If we are analysing a closure, the captures are still dropped last.
304                // This is part of the closure capture lifetime contract.
305                // They are similar to the return value _0 with respect to lifetime rules.
306                if is_closure_like && matches!(dropped_local, ty::CAPTURE_STRUCT_LOCAL) {
307                    debug!(?dropped_local, "skip closure captures");
308                    to_exclude.insert(path_idx);
309                    continue;
310                }
311                // c) Sometimes we collect places that are projections into the BID locals,
312                // so they are considered dropped now.
313                // Example:
314                // struct NotVeryDroppy(Droppy);
315                // impl Drop for Droppy {..}
316                // fn f() -> NotVeryDroppy {
317                //    let x = NotVeryDroppy(droppy());
318                //    {
319                //        let y: Droppy = x.0;
320                //        NotVeryDroppy(y)
321                //    }
322                // }
323                // `y` takes `x.0`, which invalidates `x` as a complete `NotVeryDroppy`
324                // so there is no point in linting against `x` any more.
325                if place_descendent_of_bids(path_idx, &move_data, &bid_places) {
326                    debug!(?dropped_local, "skip descendent of bids");
327                    to_exclude.insert(path_idx);
328                    continue;
329                }
330                let observer_ty = move_path.place.ty(body, tcx).ty;
331                // d) The collected local has no custom destructor that passes our ecosystem filter.
332                if ty_dropped_components
333                    .entry(observer_ty)
334                    .or_insert_with(|| {
335                        extract_component_with_significant_dtor(tcx, typing_env, observer_ty)
336                    })
337                    .is_empty()
338                {
339                    debug!(?dropped_local, "skip non-droppy types");
340                    to_exclude.insert(path_idx);
341                    continue;
342                }
343            }
344            // Suppose that all BIDs point into the same local,
345            // we can remove the this local from the observed drops,
346            // so that we can focus our diagnosis more on the others.
347            if let Ok(local) = candidates.iter().map(|&(_, place)| place.local).all_equal_value() {
348                for path_idx in all_locals_dropped.iter() {
349                    if move_data.move_paths[path_idx].place.local == local {
350                        to_exclude.insert(path_idx);
351                    }
352                }
353            }
354            all_locals_dropped.subtract(&to_exclude);
355        }
356        if all_locals_dropped.is_empty() {
357            // No drop effect is observable, so let us move on.
358            continue;
359        }
360
361        // ## The final work to assemble the diagnosis ##
362        // First collect or generate fresh names for local variable bindings and temporary values.
363        let local_names = assign_observables_names(
364            all_locals_dropped
365                .iter()
366                .map(|path_idx| move_data.move_paths[path_idx].place.local)
367                .chain(candidates.iter().map(|(_, place)| place.local)),
368            &locals_with_user_names,
369        );
370
371        let mut lint_root = None;
372        let mut local_labels = vec![];
373        // We now collect the types with custom destructors.
374        for &(_, place) in candidates {
375            let linted_local_decl = &body.local_decls[place.local];
376            let Some(&(ref name, is_generated_name)) = local_names.get(&place.local) else {
377                bug!("a name should have been assigned")
378            };
379            let name = name.as_str();
380
381            if lint_root.is_none()
382                && let ClearCrossCrate::Set(data) =
383                    &body.source_scopes[linted_local_decl.source_info.scope].local_data
384            {
385                lint_root = Some(data.lint_root);
386            }
387
388            // Collect spans of the custom destructors.
389            let mut seen_dyn = false;
390            let destructors = ty_dropped_components
391                .get(&linted_local_decl.ty)
392                .unwrap()
393                .iter()
394                .filter_map(|&ty| {
395                    if let Some(span) = ty_dtor_span(tcx, ty) {
396                        Some(DestructorLabel { span, name, dtor_kind: "concrete" })
397                    } else if matches!(ty.kind(), ty::Dynamic(..)) {
398                        if seen_dyn {
399                            None
400                        } else {
401                            seen_dyn = true;
402                            Some(DestructorLabel { span: DUMMY_SP, name, dtor_kind: "dyn" })
403                        }
404                    } else {
405                        None
406                    }
407                })
408                .collect();
409            local_labels.push(LocalLabel {
410                span: linted_local_decl.source_info.span,
411                destructors,
412                name,
413                is_generated_name,
414                is_dropped_first_edition_2024: true,
415            });
416        }
417
418        // Similarly, custom destructors of the observed drops.
419        for path_idx in all_locals_dropped.iter() {
420            let place = &move_data.move_paths[path_idx].place;
421            // We are not using the type of the local because the drop may be partial.
422            let observer_ty = place.ty(body, tcx).ty;
423
424            let observer_local_decl = &body.local_decls[place.local];
425            let Some(&(ref name, is_generated_name)) = local_names.get(&place.local) else {
426                bug!("a name should have been assigned")
427            };
428            let name = name.as_str();
429
430            let mut seen_dyn = false;
431            let destructors = extract_component_with_significant_dtor(tcx, typing_env, observer_ty)
432                .into_iter()
433                .filter_map(|ty| {
434                    if let Some(span) = ty_dtor_span(tcx, ty) {
435                        Some(DestructorLabel { span, name, dtor_kind: "concrete" })
436                    } else if matches!(ty.kind(), ty::Dynamic(..)) {
437                        if seen_dyn {
438                            None
439                        } else {
440                            seen_dyn = true;
441                            Some(DestructorLabel { span: DUMMY_SP, name, dtor_kind: "dyn" })
442                        }
443                    } else {
444                        None
445                    }
446                })
447                .collect();
448            local_labels.push(LocalLabel {
449                span: observer_local_decl.source_info.span,
450                destructors,
451                name,
452                is_generated_name,
453                is_dropped_first_edition_2024: false,
454            });
455        }
456
457        let span = local_labels[0].span;
458        tcx.emit_node_span_lint(
459            lint::builtin::TAIL_EXPR_DROP_ORDER,
460            lint_root.unwrap_or(CRATE_HIR_ID),
461            span,
462            TailExprDropOrderLint { local_labels, drop_span, _epilogue: () },
463        );
464    }
465}
466
467/// Extract binding names if available for diagnosis
468fn collect_user_names(body: &Body<'_>) -> FxIndexMap<Local, Symbol> {
469    let mut names = FxIndexMap::default();
470    for var_debug_info in &body.var_debug_info {
471        if let mir::VarDebugInfoContents::Place(place) = &var_debug_info.value
472            && let Some(local) = place.local_or_deref_local()
473        {
474            names.entry(local).or_insert(var_debug_info.name);
475        }
476    }
477    names
478}
479
480/// Assign names for anonymous or temporary values for diagnosis
481fn assign_observables_names(
482    locals: impl IntoIterator<Item = Local>,
483    user_names: &FxIndexMap<Local, Symbol>,
484) -> FxIndexMap<Local, (String, bool)> {
485    let mut names = FxIndexMap::default();
486    let mut assigned_names = FxHashSet::default();
487    let mut idx = 0u64;
488    let mut fresh_name = || {
489        idx += 1;
490        (format!("#{idx}"), true)
491    };
492    for local in locals {
493        let name = if let Some(name) = user_names.get(&local) {
494            let name = name.as_str();
495            if assigned_names.contains(name) { fresh_name() } else { (name.to_owned(), false) }
496        } else {
497            fresh_name()
498        };
499        assigned_names.insert(name.0.clone());
500        names.insert(local, name);
501    }
502    names
503}
504
505#[derive(LintDiagnostic)]
506#[diag(mir_transform_tail_expr_drop_order)]
507struct TailExprDropOrderLint<'a> {
508    #[subdiagnostic]
509    local_labels: Vec<LocalLabel<'a>>,
510    #[label(mir_transform_drop_location)]
511    drop_span: Option<Span>,
512    #[note(mir_transform_note_epilogue)]
513    _epilogue: (),
514}
515
516struct LocalLabel<'a> {
517    span: Span,
518    name: &'a str,
519    is_generated_name: bool,
520    is_dropped_first_edition_2024: bool,
521    destructors: Vec<DestructorLabel<'a>>,
522}
523
524/// A custom `Subdiagnostic` implementation so that the notes are delivered in a specific order
525impl Subdiagnostic for LocalLabel<'_> {
526    fn add_to_diag<G: rustc_errors::EmissionGuarantee>(self, diag: &mut rustc_errors::Diag<'_, G>) {
527        // Because parent uses this field , we need to remove it delay before adding it.
528        diag.remove_arg("name");
529        diag.arg("name", self.name);
530        diag.remove_arg("is_generated_name");
531        diag.arg("is_generated_name", self.is_generated_name);
532        diag.remove_arg("is_dropped_first_edition_2024");
533        diag.arg("is_dropped_first_edition_2024", self.is_dropped_first_edition_2024);
534        let msg = diag.eagerly_translate(crate::fluent_generated::mir_transform_tail_expr_local);
535        diag.span_label(self.span, msg);
536        for dtor in self.destructors {
537            dtor.add_to_diag(diag);
538        }
539        let msg =
540            diag.eagerly_translate(crate::fluent_generated::mir_transform_label_local_epilogue);
541        diag.span_label(self.span, msg);
542    }
543}
544
545#[derive(Subdiagnostic)]
546#[note(mir_transform_tail_expr_dtor)]
547struct DestructorLabel<'a> {
548    #[primary_span]
549    span: Span,
550    dtor_kind: &'static str,
551    name: &'a str,
552}