Skip to main content

rapx/verify/slicer/
visitor.rs

1//! Backward path visitor — walks a finite path backward from a checkpoint and
2//! keeps only MIR items that can affect the required property.
3//!
4//! The def-use layer lives in [`super::super::def_use`]; this module focuses on
5//! the path-level control flow decisions: calls, SCC exits, and path-condition
6//! branches.
7
8use rustc_hir::def_id::DefId;
9use rustc_middle::mir::Body;
10use rustc_middle::mir::{BasicBlock, Local, Operand, Rvalue, StatementKind, TerminatorKind};
11use rustc_middle::ty::TyCtxt;
12
13use std::collections::{HashMap, HashSet};
14
15use crate::analysis::dataflow::graph::build_dataflow_graph;
16use crate::analysis::dataflow::types::DataflowGraph;
17
18use super::super::{
19    contract,
20    def_use::{
21        RelevantPlaces, bind_callsite_roots, call_args_uses_at, operand_uses, terminator_use_def,
22    },
23    path_extractor::{Path, PathStep},
24};
25use crate::helpers::mir_scan::{Checkpoint, CheckpointKind, CheckpointLocation};
26
27use crate::analysis::path::{PathNode, PathTree};
28
29use super::{
30    call_visit,
31    types::{ProofGoal, RelevantItem},
32};
33
34/// Entry point for backward path visiting.
35pub(crate) struct BackwardSlicer<'tcx> {
36    tcx: TyCtxt<'tcx>,
37}
38
39impl<'tcx> BackwardSlicer<'tcx> {
40    /// Create a backward visitor over the current compiler type context.
41    pub(crate) fn new(tcx: TyCtxt<'tcx>) -> Self {
42        Self { tcx }
43    }
44
45    /// Visit a path tree in post-order, sharing backward analysis across
46    /// common prefixes. Merges child-relevance sets at branch nodes (the
47    /// union is a sound over-approximation). Returns per-leaf results.
48    ///
49    /// Callee parameter roots are bound at checkpoint nodes.
50    pub(crate) fn visit_path_tree(
51        &self,
52        tree: &PathTree,
53        target_block: usize,
54        checkpoint: &Checkpoint<'tcx>,
55        property: &contract::Property<'tcx>,
56    ) -> Vec<ProofGoal<'tcx>> {
57        self.visit_path_tree_impl(
58            tree,
59            target_block,
60            checkpoint.caller,
61            checkpoint.block,
62            Some(checkpoint),
63            property,
64        )
65    }
66
67    /// Like [`visit_path_tree`] but without callee-root binding (used for
68    /// struct-invariant checks where property places are already in the
69    /// caller's local namespace).
70    pub(crate) fn visit_path_tree_for_checkpoint(
71        &self,
72        tree: &PathTree,
73        target_block: usize,
74        caller: DefId,
75        checkpoint_loc: CheckpointLocation,
76        property: &contract::Property<'tcx>,
77    ) -> Vec<ProofGoal<'tcx>> {
78        self.visit_path_tree_impl(
79            tree,
80            target_block,
81            caller,
82            checkpoint_loc.block,
83            None,
84            property,
85        )
86    }
87
88    /// Internal: post-order recursion returning per-leaf
89    /// `(block_path, backward_items)`.
90    fn visit_path_tree_impl(
91        &self,
92        tree: &PathTree,
93        target_block: usize,
94        caller: DefId,
95        checkpoint_block: BasicBlock,
96        bind_checkpoint: Option<&Checkpoint<'tcx>>,
97        property: &contract::Property<'tcx>,
98    ) -> Vec<ProofGoal<'tcx>> {
99        let Some(root) = tree.root() else {
100            return Vec::new();
101        };
102        let checkpoint_loc = CheckpointLocation {
103            caller,
104            block: checkpoint_block,
105        };
106
107        // Pre-build the MIR body and dataflow graph for every function reachable
108        // through this tree (caller + inlined callees), so inlined blocks resolve
109        // to the correct body/flow.
110        let mut bodies: HashMap<DefId, &'tcx Body<'tcx>> = HashMap::new();
111        let mut flows: HashMap<DefId, DataflowGraph> = HashMap::new();
112        let mut def_ids: HashSet<DefId> = tree.block_fns().iter().map(|(d, _)| *d).collect();
113        def_ids.insert(caller);
114        for d in def_ids {
115            bodies.insert(d, self.tcx.optimized_mir(d));
116            flows.insert(d, build_dataflow_graph(self.tcx, d));
117        }
118
119        let leaf_results = Self::build_leaf_items(
120            self,
121            tree,
122            root,
123            target_block,
124            checkpoint_block,
125            bind_checkpoint,
126            property,
127            caller,
128            &bodies,
129            &flows,
130        );
131
132        let mut results = Vec::new();
133        for (block_path, backward_items, _relevant, _) in leaf_results {
134            let mut items = backward_items;
135            items.reverse();
136            let steps: Vec<PathStep> = block_path
137                .iter()
138                .map(|&b| PathStep::Block(BasicBlock::from(b)))
139                .chain(std::iter::once(PathStep::Checkpoint(checkpoint_loc)))
140                .collect();
141            results.push(ProofGoal {
142                path: Path {
143                    target: checkpoint_loc,
144                    steps,
145                },
146                items,
147                block_fn: tree.block_fns().to_vec(),
148            });
149        }
150        results
151    }
152
153    /// Post-order recursion: returns one `(block_path, backward_items,
154    /// relevant_before_block, parked_caller_relevant)` per checkpoint leaf.
155    /// Each leaf is independent — no merging, no HashMap collision.
156    fn build_leaf_items(
157        visitor: &Self,
158        tree: &PathTree,
159        node: &PathNode,
160        target_block: usize,
161        checkpoint_block: BasicBlock,
162        bind_checkpoint: Option<&Checkpoint<'tcx>>,
163        property: &contract::Property<'tcx>,
164        caller: DefId,
165        bodies: &HashMap<DefId, &'tcx Body<'tcx>>,
166        flows: &HashMap<DefId, DataflowGraph>,
167    ) -> Vec<(
168        Vec<usize>,
169        Vec<RelevantItem<'tcx>>,
170        RelevantPlaces,
171        Vec<(DefId, Vec<usize>, RelevantPlaces)>,
172    )> {
173        let (def_id, local_index) = tree.block_fn_of(node.block).unwrap_or((caller, node.block));
174        let body = &bodies[&def_id];
175        let flow = &flows[&def_id];
176        let block = BasicBlock::from(local_index);
177        let keep_inv = property
178            .kind()
179            .is_some_and(|k| needs_invalidation_tracking(&k));
180        // Only `Owning` needs the owner's construction chain; other invalidations
181        // (Allocated/Init/…) must not be perturbed by the extra needs_drop keeps.
182        let keep_owner = matches!(property.kind(), Some(contract::PropertyKind::Owning));
183        let block_data = &body.basic_blocks[block];
184        let mut results = Vec::new();
185
186        // Build the checkpoint-layer items when this block IS the target.
187        let (checkpoint_items, checkpoint_relevant) = if node.block == target_block {
188            let mut relevant = RelevantPlaces::from_property(property);
189            if let Some(cs) = bind_checkpoint {
190                bind_callsite_roots(visitor.tcx, &mut relevant, cs);
191            }
192            let mut items = Vec::new();
193            // The block's *terminator* is the checkpoint for an unsafe call, but
194            // for a statement-level checkpoint (raw-ptr-deref / static-mut-access)
195            // the terminator executes *after* the checked statement, so it must not
196            // be part of the slice: a `drop(box)` in the same block would otherwise
197            // mark the heap dead before the deref's `Allocated` check reads it.
198            let is_statement_checkpoint = matches!(
199                bind_checkpoint.map(|c| c.kind),
200                Some(CheckpointKind::RawPtrDeref) | Some(CheckpointKind::StaticMutAccess)
201            );
202            if !is_statement_checkpoint {
203                items.push(RelevantItem::Terminator {
204                    def_id: caller,
205                    block: checkpoint_block,
206                    switch_succ: None,
207                });
208            }
209            // Pass 1: normal processing.
210            for (si, stmt) in block_data.statements.iter().enumerate().rev() {
211                visitor.visit_statement(
212                    def_id,
213                    checkpoint_block,
214                    si,
215                    stmt,
216                    flow,
217                    &mut relevant,
218                    &mut items,
219                    keep_inv,
220                    keep_owner,
221                );
222            }
223            // Pass 2: re-visit definitions that became relevant only
224            // during pass 1.
225            Self::re_visit_newly_added(
226                visitor,
227                def_id,
228                checkpoint_block,
229                block_data,
230                flow,
231                &mut relevant,
232                &mut items,
233                keep_inv,
234                keep_owner,
235            );
236            (items, relevant)
237        } else {
238            (Vec::new(), RelevantPlaces::new())
239        };
240
241        // Process children — even when this is the target block,
242        // deeper checkpoint occurrences may hide below.
243        for child in &node.children {
244            let child_results = Self::build_leaf_items(
245                visitor,
246                tree,
247                child,
248                target_block,
249                checkpoint_block,
250                bind_checkpoint,
251                property,
252                caller,
253                bodies,
254                flows,
255            );
256            for (mut child_path, child_items, child_relevant, mut frames) in child_results {
257                let mut relevant = child_relevant;
258                let mut items = child_items;
259                // Entering an inlined callee (its first block in backward order,
260                // i.e. the return block): the caller's destination local becomes
261                // the callee's `_0`, and the rest of the caller's relevance is
262                // parked on a per-level frame stack. A single `Option` cannot
263                // express nested callees (get_ext → NonZero::get), whose bodies
264                // are split around the inner callee.
265                if def_id != caller
266                    && frames.last().map(|f| f.0) != Some(def_id)
267                    && let Some(binding) = tree.inline_binding(node.block - local_index)
268                {
269                    let dest = Local::from_usize(binding.dest_local);
270                    let mut callee_relevant = RelevantPlaces::new();
271                    if relevant.locals.contains(&dest) {
272                        relevant.locals.remove(&dest);
273                        relevant.places.retain(|p| p.local() != Some(dest));
274                        callee_relevant.insert_local(Local::from_usize(0));
275                    }
276                    frames.push((
277                        def_id,
278                        binding.arg_locals.clone(),
279                        std::mem::replace(&mut relevant, callee_relevant),
280                    ));
281                }
282                // Skip the `Call` terminator when this block's call was inlined:
283                // the callee's statements are already sliced via the path, so
284                // treating the call as atomic would double-count it.
285                if !tree.is_inlined_call(node.block) {
286                    // The successor this path takes out of `node` (only used for
287                    // `SwitchInt`, whose branch is resolved here rather than by
288                    // the forward VM). `child.block` is a global index; map back
289                    // to the local MIR block.
290                    let successor = tree
291                        .block_fn_of(child.block)
292                        .map(|(_, li)| BasicBlock::from(li))
293                        .or(Some(BasicBlock::from(child.block)));
294                    visitor.visit_terminator(
295                        def_id,
296                        block,
297                        block_data.terminator(),
298                        flow,
299                        body,
300                        &mut relevant,
301                        &mut items,
302                        keep_inv,
303                        keep_owner,
304                        successor,
305                    );
306                }
307                let block_stmt_count = block_data.statements.len();
308                for (si, stmt) in block_data.statements.iter().enumerate().rev() {
309                    visitor.visit_statement(
310                        def_id,
311                        block,
312                        si,
313                        stmt,
314                        flow,
315                        &mut relevant,
316                        &mut items,
317                        keep_inv,
318                        keep_owner,
319                    );
320                }
321                let dist_to_target = child_path.iter().position(|&b| b == target_block);
322                if block_stmt_count > 0 && dist_to_target.is_some_and(|d| d <= 2) {
323                    Self::re_visit_newly_added(
324                        visitor,
325                        def_id,
326                        block,
327                        block_data,
328                        flow,
329                        &mut relevant,
330                        &mut items,
331                        keep_inv,
332                        keep_owner,
333                    );
334                }
335                // Leaving an inlined callee entry: remap the callee's parameter
336                // locals back to the caller's argument locals so the caller's
337                // argument-producing statements stay relevant.
338                if let Some(binding) = tree.inline_binding(node.block) {
339                    let (_, frame_arg_locals, parked) = frames
340                        .pop()
341                        .unwrap_or((def_id, binding.arg_locals.clone(), RelevantPlaces::new()));
342                    let mut caller_relevant = parked;
343                    for (i, arg_local) in frame_arg_locals.iter().enumerate() {
344                        if relevant.locals.contains(&Local::from_usize(i + 1)) {
345                            caller_relevant.insert_local(Local::from_usize(*arg_local));
346                        }
347                    }
348                    relevant = caller_relevant;
349                }
350                child_path.insert(0, node.block);
351                results.push((child_path, items, relevant, frames));
352            }
353        }
354
355        // Produce a leaf for every checkpoint occurrence so that each
356        // distinct path prefix reaching the target block is covered.
357        // Deeper loop-unrolled occurrences provide superset backward
358        // slices, but earlier occurrences are also needed for branches
359        // that exit the loop (e.g. unwind/cleanup) without hitting the
360        // target block again.
361        if !checkpoint_items.is_empty() {
362            results.push((
363                vec![node.block],
364                checkpoint_items,
365                checkpoint_relevant,
366                Vec::new(),
367            ));
368        }
369
370        results
371    }
372
373    /// After the first backward pass, re-visit statements whose defs
374    /// became relevant because of discoveries made during that pass
375    /// (tracked in `RelevantPlaces::just_added`).
376    fn re_visit_newly_added(
377        visitor: &Self,
378        def_id: DefId,
379        block: BasicBlock,
380        block_data: &'tcx rustc_middle::mir::BasicBlockData<'tcx>,
381        flow: &DataflowGraph,
382        relevant: &mut RelevantPlaces,
383        items: &mut Vec<RelevantItem<'tcx>>,
384        keep_inv: bool,
385        keep_owner: bool,
386    ) {
387        let newly_added = std::mem::take(&mut relevant.just_added);
388        if newly_added.is_empty() {
389            return;
390        }
391        for (si, stmt) in block_data.statements.iter().enumerate().rev() {
392            let defs = match &stmt.kind {
393                rustc_middle::mir::StatementKind::Assign(assign) => {
394                    let mut d = crate::verify::def_use::RelevantPlaces::new();
395                    d.insert_mir_place(&assign.0);
396                    d
397                }
398                _ => continue,
399            };
400            let any_new = defs
401                .places
402                .iter()
403                .any(|dp| newly_added.iter().any(|np| dp.local() == np.local()));
404            if any_new {
405                visitor.visit_statement(
406                    def_id, block, si, stmt, flow, relevant, items, keep_inv, keep_owner,
407                );
408            }
409        }
410    }
411
412    /// Visit one MIR statement against the current relevance frontier.
413    fn visit_statement(
414        &self,
415        def_id: DefId,
416        block: BasicBlock,
417        statement_index: usize,
418        statement: &'tcx rustc_middle::mir::Statement<'tcx>,
419        flow: &DataflowGraph,
420        relevant: &mut RelevantPlaces,
421        items: &mut Vec<RelevantItem<'tcx>>,
422        keep_invalidations: bool,
423        keep_owner: bool,
424    ) {
425        if keep_invalidations
426            && matches!(
427                statement.kind,
428                StatementKind::StorageDead(_) | StatementKind::StorageLive(_)
429            )
430        {
431            items.push(RelevantItem::Statement {
432                def_id,
433                block,
434                statement_index,
435            });
436            return;
437        }
438
439        // Keep the definition of a `needs_drop` local (an owner's construction
440        // chain) so `Owning` can trace its field provenance.
441        if keep_owner {
442            if let StatementKind::Assign(assign) = &statement.kind {
443                let (place, _) = &**assign;
444                let body = self.tcx.optimized_mir(def_id);
445                let ty = body.local_decls[place.local].ty;
446                let typing_env = rustc_middle::ty::TypingEnv::non_body_analysis(self.tcx, def_id);
447                if ty.needs_drop(self.tcx, typing_env) {
448                    let mut defs = RelevantPlaces::new();
449                    defs.insert_mir_place(place);
450                    let uses =
451                        collect_statement_uses(statement, block, statement_index, flow, &defs);
452                    items.push(RelevantItem::Statement {
453                        def_id,
454                        block,
455                        statement_index,
456                    });
457                    relevant.remove_all(&defs);
458                    relevant.extend(uses);
459                    return;
460                }
461            }
462        }
463
464        let mut defs = RelevantPlaces::new();
465        match &statement.kind {
466            StatementKind::Assign(assign) => {
467                let (place, _) = &**assign;
468                defs.insert_mir_place(place);
469            }
470            StatementKind::StorageDead(local) => {
471                defs.insert_local(*local);
472            }
473            _ => {}
474        }
475
476        // A provenance-carrying assignment — a pointer cast (`*const T as
477        // *const U`, a reference→pointer cast) or a reborrow through a
478        // reference/pointer (`&mut (*_1)`) — carries the source's
479        // provenance/align_n even when its destination is not *value*-relevant
480        // to the property.  Keep it — and follow its source — so the forward VM
481        // propagates the provenance instead of relying on the backward
482        // `propagate_single_assign` fill-in.
483        let is_provenance_carrier = match &statement.kind {
484            StatementKind::Assign(assign) => {
485                let (place, rvalue) = &**assign;
486                let dest_is_ptr = matches!(
487                    self.tcx.optimized_mir(def_id).local_decls[place.local].ty.kind(),
488                    rustc_middle::ty::TyKind::RawPtr(..) | rustc_middle::ty::TyKind::Ref(..)
489                );
490                match rvalue {
491                    Rvalue::Cast(..) => dest_is_ptr,
492                    Rvalue::Ref(_, _, src_place) | Rvalue::RawPtr(_, src_place) => src_place
493                        .projection
494                        .iter()
495                        .any(|p| {
496                            matches!(
497                                p.kind(),
498                                rustc_middle::mir::ProjectionElem::Deref
499                            )
500                        }),
501                    #[cfg(rapx_rvalue_use_with_retag)]
502                    Rvalue::Use(operand, _) => {
503                        let is_projected = match operand {
504                            Operand::Copy(p) | Operand::Move(p) => p.projection.iter().any(|e| {
505                                matches!(e.kind(), rustc_middle::mir::ProjectionElem::Deref)
506                            }),
507                            _ => false,
508                        };
509                        dest_is_ptr || is_projected
510                    }
511                    #[cfg(not(rapx_rvalue_use_with_retag))]
512                    Rvalue::Use(operand) => {
513                        let is_projected = match operand {
514                            Operand::Copy(p) | Operand::Move(p) => p.projection.iter().any(|e| {
515                                matches!(e.kind(), rustc_middle::mir::ProjectionElem::Deref)
516                            }),
517                            _ => false,
518                        };
519                        dest_is_ptr || is_projected
520                    }
521                    Rvalue::CopyForDeref(p) => dest_is_ptr || !p.projection.is_empty(),
522                    _ => false,
523                }
524            }
525            _ => false,
526        };
527
528        // A statement that writes an iterator's `ptr` field (`(*self).0 = ...`,
529        // the inlined `post_inc_start`) must be kept even when it is not
530        // *value*-relevant to the property: the forward VM tracks the iterator's
531        // cumulative offset from this write (`track_iter_ptr_update`), which is
532        // what makes the loop-carried `i < n` invariant provable.
533        let is_iter_ptr_write = match &statement.kind {
534            StatementKind::Assign(assign) => {
535                let (place, _) = &**assign;
536                let mut proj = place.projection.iter();
537                if !matches!(
538                    proj.next().map(|p| p.kind()),
539                    Some(rustc_middle::mir::ProjectionElem::Deref)
540                ) {
541                    false
542                } else {
543                    let is_field0 = matches!(
544                        (proj.next().map(|p| p.kind()), proj.next()),
545                        (Some(rustc_middle::mir::ProjectionElem::Field(f, _)), None)
546                            if f.as_usize() == 0
547                    );
548                    if !is_field0 {
549                        false
550                    } else {
551                        let base_ty = self.tcx.optimized_mir(def_id).local_decls[place.local].ty;
552                        match base_ty.kind() {
553                            rustc_middle::ty::TyKind::Ref(_, pointee, _) => {
554                                match pointee.kind() {
555                                    rustc_middle::ty::TyKind::Adt(adt_def, _) => {
556                                        crate::verify::api_classify::is_std_iter_or_itermut(
557                                            adt_def.did(),
558                                        )
559                                    }
560                                    _ => false,
561                                }
562                            }
563                            _ => false,
564                        }
565                    }
566                }
567            }
568            _ => false,
569        };
570
571        // A write into a `[u8; N]`/`[u8]` buffer (e.g. `box [a, b, 0]`'s element
572        // store `((*_8).1).0 = [a, b, 0]`) must be kept even when it is not
573        // *value*-relevant to the property: the forward VM records these stores
574        // as per-byte values, and `ValidCStr`/`ValidString` reason over them.
575        // The backward def-use graph is place-level (it cannot follow the
576        // `Box`/`Vec` → buffer indirection), so this structural keep is what
577        // preserves the byte-level dataflow.
578        let is_byte_write = match &statement.kind {
579            StatementKind::Assign(assign) => {
580                let (place, _) = &**assign;
581                let ty = place.ty(self.tcx.optimized_mir(def_id), self.tcx).ty;
582                crate::helpers::mir_utils::is_u8_array_or_slice(ty)
583            }
584            _ => false,
585        };
586
587        if defs.intersects(relevant) || is_provenance_carrier || is_iter_ptr_write || is_byte_write
588        {
589            let mut uses = collect_statement_uses(statement, block, statement_index, flow, &defs);
590            items.push(RelevantItem::Statement {
591                def_id,
592                block,
593                statement_index,
594            });
595            // Save places already in the relevance set before removing
596            // the current definition.  When the uses of this statement
597            // would re-add a place whose definition was already found earlier
598            // in the walk, skip it to prevent wrong (duplicate) matches.
599            let already_seen: crate::compat::FxHashSet<crate::verify::def_use::PlaceKey> =
600                relevant.places.clone();
601            relevant.remove_all(&defs);
602            uses.places.retain(|p| !already_seen.contains(p));
603            relevant.extend(uses);
604            return;
605        }
606
607        if statement_can_refine(statement) {
608            let uses = collect_flow_uses(flow, block, statement_index, &defs);
609            if uses.intersects(relevant) {
610                items.push(RelevantItem::Statement {
611                    def_id,
612                    block,
613                    statement_index,
614                });
615            }
616        }
617    }
618
619    /// Visit one MIR terminator against the current relevance frontier.
620    fn visit_terminator(
621        &self,
622        def_id: DefId,
623        block: BasicBlock,
624        terminator: &rustc_middle::mir::Terminator<'tcx>,
625        flow: &DataflowGraph,
626        body: &Body<'tcx>,
627        relevant: &mut RelevantPlaces,
628        items: &mut Vec<RelevantItem<'tcx>>,
629        keep_invalidations: bool,
630        keep_owner: bool,
631        successor: Option<BasicBlock>,
632    ) {
633        if keep_invalidations {
634            if matches!(terminator.kind, TerminatorKind::Drop { .. }) {
635                items.push(RelevantItem::Terminator { def_id, block, switch_succ: None });
636                return;
637            }
638            // A manual drop (`std::mem::drop` / `ManuallyDrop::drop`) also frees
639            // the pointee's heap: keep the call and its argument's construction
640            // chain so `Allocated`/`Owning` can see the freed allocation and
641            // detect a later use / second drop.
642            if let TerminatorKind::Call { func, args, .. } = &terminator.kind {
643                let is_drop_call =
644                    crate::helpers::mir_utils::dep_callee_def_id(func).is_some_and(|c| {
645                        crate::verify::api_classify::is_manually_drop_drop(Some(c))
646                            || crate::verify::api_classify::is_std_drop(Some(c))
647                    });
648                if is_drop_call {
649                    items.push(RelevantItem::Terminator { def_id, block, switch_succ: None });
650                    relevant.extend(call_args_uses_at(args, &[0]));
651                    return;
652                }
653            }
654        }
655
656        if let TerminatorKind::Call {
657            func,
658            args,
659            destination,
660            ..
661        } = &terminator.kind
662        {
663            // `Owning` traces an owner's construction chain: keep a call whose
664            // destination is a `needs_drop` value even when that local is not
665            // itself relevant, so its field provenance survives.
666            if keep_owner {
667                let dest_ty = body.local_decls[destination.local].ty;
668                let typing_env = rustc_middle::ty::TypingEnv::non_body_analysis(self.tcx, def_id);
669                if dest_ty.needs_drop(self.tcx, typing_env) {
670                    let use_def = terminator_use_def(terminator);
671                    items.push(RelevantItem::Terminator { def_id, block, switch_succ: None });
672                    relevant.remove_all(&use_def.defs);
673                    relevant.extend(use_def.uses);
674                    return;
675                }
676            }
677            call_visit::visit(
678                self.tcx,
679                def_id,
680                block,
681                func,
682                args,
683                destination,
684                flow,
685                body,
686                relevant,
687                items,
688            );
689            return;
690        }
691
692        let use_def = terminator_use_def(terminator);
693        if terminator_is_path_condition(terminator) {
694            let switch_succ = match terminator.kind {
695                TerminatorKind::SwitchInt { .. } => successor,
696                _ => None,
697            };
698            items.push(RelevantItem::Terminator { def_id, block, switch_succ });
699            relevant.extend(use_def.uses.clone());
700            return;
701        }
702
703        if use_def.defs.intersects(relevant) {
704            items.push(RelevantItem::Terminator { def_id, block, switch_succ: None });
705            relevant.remove_all(&use_def.defs);
706            relevant.extend(use_def.uses);
707            return;
708        }
709
710        if use_def.uses.intersects(relevant) {
711            items.push(RelevantItem::Terminator { def_id, block, switch_succ: None });
712        }
713    }
714}
715
716// ── property helpers ──────────────────────────────────────────────────
717
718/// Whether a property's checker reads allocation liveness (`alloc.facts.dead`), so
719/// the backward slice must keep `StorageDead`/`StorageLive`/`Drop` unconditionally
720/// (the allocation owner may not be reachable from the pointer target, e.g. a
721/// raw pointer into a separately-owned Vec/Box buffer).
722fn needs_invalidation_tracking(kind: &contract::PropertyKind) -> bool {
723    matches!(
724        kind,
725        contract::PropertyKind::Allocated
726            | contract::PropertyKind::Init
727            | contract::PropertyKind::Alive
728            | contract::PropertyKind::ValidString
729            | contract::PropertyKind::ValidCStr
730            | contract::PropertyKind::Owning
731    )
732}
733
734// ── classification helpers ──────────────────────────────────────────────
735
736fn statement_can_refine(statement: &rustc_middle::mir::Statement<'_>) -> bool {
737    matches!(&statement.kind, StatementKind::Assign(assign) if matches!(
738        &**assign,
739        (
740            _,
741            rustc_middle::mir::Rvalue::BinaryOp(_, _)
742            | rustc_middle::mir::Rvalue::UnaryOp(_, _)
743            | rustc_middle::mir::Rvalue::Cast(_, _, _),
744        )
745    ))
746}
747
748fn terminator_is_path_condition(terminator: &rustc_middle::mir::Terminator<'_>) -> bool {
749    matches!(
750        terminator.kind,
751        TerminatorKind::SwitchInt { .. } | TerminatorKind::Assert { .. }
752    )
753}
754
755/// Collect all place-uses for a statement from dataflow edges and operands.
756fn collect_statement_uses<'tcx>(
757    statement: &'tcx rustc_middle::mir::Statement<'tcx>,
758    block: BasicBlock,
759    statement_index: usize,
760    flow: &DataflowGraph,
761    defs: &RelevantPlaces,
762) -> RelevantPlaces {
763    let mut uses = collect_flow_uses(flow, block, statement_index, defs);
764
765    // Also collect uses directly from operands — the dataflow graph
766    // creates synthetic nodes for field projections (e.g. _13.0),
767    // so we need the direct operand uses to reach through.
768    if let StatementKind::Assign(assign) = &statement.kind {
769        let (_, rvalue) = &**assign;
770        for operand in super::super::def_use::rvalue_operands(rvalue) {
771            uses.extend(operand_uses(operand));
772        }
773        // A reborrow (`_p = &(*_q)`, `_p = &raw (*_q)`) carries no operands, so
774        // `rvalue_operands` misses its referent.  Only when the referent traces
775        // back to a projection out of a call's returned tuple (a `split_at`
776        // prefix/suffix slice) do we keep the referent's base local, so the
777        // split — and its `mid` argument — stays in the backward slice and
778        // feeds downstream `len(self)` obligations.  This stays narrow to avoid
779        // inflating relevance for ordinary reborrows, which explodes loop path
780        // enumeration.
781        if let rustc_middle::mir::Rvalue::Ref(_, _, place)
782        | rustc_middle::mir::Rvalue::RawPtr(_, place) = rvalue
783        {
784            uses.insert_local(place.local);
785        }
786    }
787
788    uses
789}
790
791/// Collect the source locals of the dataflow edges entering
792/// `(block, statement_index)` from the def locals of `defs`.
793fn collect_flow_uses(
794    flow: &DataflowGraph,
795    block: BasicBlock,
796    statement_index: usize,
797    defs: &RelevantPlaces,
798) -> RelevantPlaces {
799    let mut uses = RelevantPlaces::new();
800    for &local in &defs.locals {
801        for &edge_idx in &flow.node(local).in_edges {
802            let edge = &flow.edges[edge_idx];
803            if edge.block == block.as_usize() && edge.statement_index == statement_index {
804                uses.insert_local(edge.src);
805            }
806        }
807    }
808    uses
809}