rustc_mir_transform/
lint_tail_expr_drop_order.rs

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