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, StatementKind, TerminatorKind};
11use rustc_middle::ty::TyCtxt;
12
13use crate::analysis::dataflow::graph::build_dataflow_graph;
14use crate::analysis::dataflow::types::DataflowGraph;
15
16use super::super::{
17    contract,
18    def_use::{RelevantPlaces, bind_callsite_roots, operand_uses, terminator_use_def},
19    path_extractor::{Path, PathStep},
20};
21use crate::helpers::mir_scan::{Checkpoint, CheckpointLocation};
22
23use crate::analysis::path::{PathNode, PathTree};
24
25use super::{
26    call_visit,
27    types::{RelevantItem, ProofGoal},
28};
29
30/// Entry point for backward path visiting.
31pub struct BackwardSlicer<'tcx> {
32    tcx: TyCtxt<'tcx>,
33}
34
35impl<'tcx> BackwardSlicer<'tcx> {
36    /// Create a backward visitor over the current compiler type context.
37    pub fn new(tcx: TyCtxt<'tcx>) -> Self {
38        Self { tcx }
39    }
40
41    /// Return the compiler type context owned by this visitor.
42    pub fn tcx(&self) -> TyCtxt<'tcx> {
43        self.tcx
44    }
45
46    /// Visit a path tree in post-order, sharing backward analysis across
47    /// common prefixes. Merges child-relevance sets at branch nodes (the
48    /// union is a sound over-approximation). Returns per-leaf results.
49    ///
50    /// Callee parameter roots are bound at checkpoint nodes.
51    pub fn visit_path_tree(
52        &self,
53        tree: &PathTree,
54        target_block: usize,
55        checkpoint: &Checkpoint<'tcx>,
56        property: &contract::Property<'tcx>,
57    ) -> Vec<ProofGoal<'tcx>> {
58        self.visit_path_tree_impl(
59            tree,
60            target_block,
61            checkpoint.caller,
62            checkpoint.block,
63            Some(checkpoint),
64            property,
65        )
66    }
67
68    /// Like [`visit_path_tree`] but without callee-root binding (used for
69    /// struct-invariant checks where property places are already in the
70    /// caller's local namespace).
71    pub fn visit_path_tree_for_checkpoint(
72        &self,
73        tree: &PathTree,
74        target_block: usize,
75        caller: DefId,
76        checkpoint_loc: CheckpointLocation,
77        property: &contract::Property<'tcx>,
78    ) -> Vec<ProofGoal<'tcx>> {
79        self.visit_path_tree_impl(
80            tree,
81            target_block,
82            caller,
83            checkpoint_loc.block,
84            None,
85            property,
86        )
87    }
88
89    /// Internal: post-order recursion returning per-leaf
90    /// `(block_path, backward_items)`.
91    fn visit_path_tree_impl(
92        &self,
93        tree: &PathTree,
94        target_block: usize,
95        caller: DefId,
96        checkpoint_block: BasicBlock,
97        bind_checkpoint: Option<&Checkpoint<'tcx>>,
98        property: &contract::Property<'tcx>,
99    ) -> Vec<ProofGoal<'tcx>> {
100        let Some(root) = tree.root() else {
101            return Vec::new();
102        };
103        let checkpoint_loc = CheckpointLocation {
104            caller,
105            block: checkpoint_block,
106        };
107        let body = self.tcx.optimized_mir(caller);
108        let flow = build_dataflow_graph(self.tcx, caller);
109
110        let leaf_results = Self::build_leaf_items(
111            self,
112            root,
113            target_block,
114            checkpoint_block,
115            bind_checkpoint,
116            property,
117            &body,
118            &flow,
119        );
120
121        let mut results = Vec::new();
122        for (block_path, backward_items, _relevant) in leaf_results {
123            let mut items = backward_items;
124            items.reverse();
125            let steps: Vec<PathStep> = block_path
126                .iter()
127                .map(|&b| PathStep::Block(BasicBlock::from(b)))
128                .chain(std::iter::once(PathStep::Checkpoint(checkpoint_loc)))
129                .collect();
130            results.push(ProofGoal {
131                path: Path {
132                    target: checkpoint_loc,
133                    steps,
134                },
135                items,
136            });
137        }
138        results
139    }
140
141    /// Post-order recursion: returns one `(block_path, backward_items,
142    /// relevant_before_block)` per checkpoint leaf. Each leaf is independent
143    /// — no merging, no HashMap collision.
144    fn build_leaf_items(
145        visitor: &Self,
146        node: &PathNode,
147        target_block: usize,
148        checkpoint_block: BasicBlock,
149        bind_checkpoint: Option<&Checkpoint<'tcx>>,
150        property: &contract::Property<'tcx>,
151        body: &'tcx rustc_middle::mir::Body<'tcx>,
152        flow: &DataflowGraph,
153    ) -> Vec<(Vec<usize>, Vec<RelevantItem<'tcx>>, RelevantPlaces)> {
154        let block = BasicBlock::from(node.block);
155        let keep_inv = property.kind().is_some_and(|k| needs_invalidation_tracking(&k));
156        let block_data = &body.basic_blocks[block];
157        let mut results = Vec::new();
158
159        // Build the checkpoint-layer items when this block IS the target.
160        let (checkpoint_items, checkpoint_relevant) = if node.block == target_block {
161            let mut relevant = RelevantPlaces::from_property(property);
162            if let Some(cs) = bind_checkpoint {
163                bind_callsite_roots(visitor.tcx, &mut relevant, cs);
164            }
165            let mut items = Vec::new();
166            items.push(RelevantItem::Terminator {
167                block: checkpoint_block,
168            });
169            // Pass 1: normal processing.
170            for (si, stmt) in block_data.statements.iter().enumerate().rev() {
171                visitor.visit_statement(
172                    checkpoint_block,
173                    si,
174                    stmt,
175                    flow,
176                    &mut relevant,
177                    &mut items,
178                    keep_inv,
179                );
180            }
181            // Pass 2: re-visit definitions that became relevant only
182            // during pass 1.
183            Self::re_visit_newly_added(visitor, checkpoint_block, block_data, flow, &mut relevant, &mut items, keep_inv);
184            (items, relevant)
185        } else {
186            (Vec::new(), RelevantPlaces::new())
187        };
188
189        // Process children — even when this is the target block,
190        // deeper checkpoint occurrences may hide below.
191        for child in &node.children {
192            let child_results = Self::build_leaf_items(
193                visitor,
194                child,
195                target_block,
196                checkpoint_block,
197                bind_checkpoint,
198                property,
199                body,
200                flow,
201            );
202            for (mut child_path, child_items, child_relevant) in child_results {
203                let mut relevant = child_relevant;
204                let mut items = child_items;
205                // function entry even for child (deeper SCC) paths,
206                // otherwise allocation/initialization facts are missing.
207                visitor.visit_terminator(
208                    block,
209                    block_data.terminator(),
210                    flow,
211                    body,
212                    &mut relevant,
213                    &mut items,
214                    keep_inv,
215                );
216                let block_stmt_count = block_data.statements.len();
217                for (si, stmt) in block_data.statements.iter().enumerate().rev() {
218                    visitor.visit_statement(
219                        block,
220                        si,
221                        stmt,
222                        flow,
223                        &mut relevant,
224                        &mut items,
225                        keep_inv,
226                    );
227                }
228                // For ancestors of the checkpoint block, do a second
229                // pass limited to statements whose defs became relevant
230                // only during pass 1.  This catches the case where a
231                // copy adds a place to relevance, enabling an earlier
232                // definition to match.  Limited to 3 levels above the
233                // checkpoint to avoid spurious matches in deep trees.
234                let dist_to_target = child_path.iter().position(|&b| b == target_block);
235                if block_stmt_count > 0 && dist_to_target.map_or(false, |d| d <= 2) {
236                    Self::re_visit_newly_added(visitor, block, block_data, flow, &mut relevant, &mut items, keep_inv);
237                }
238                child_path.insert(0, node.block);
239                results.push((child_path, items, relevant));
240            }
241        }
242
243        // Produce a leaf for every checkpoint occurrence so that each
244        // distinct path prefix reaching the target block is covered.
245        // Deeper loop-unrolled occurrences provide superset backward
246        // slices, but earlier occurrences are also needed for branches
247        // that exit the loop (e.g. unwind/cleanup) without hitting the
248        // target block again.
249        if !checkpoint_items.is_empty() {
250            results.push((vec![node.block], checkpoint_items, checkpoint_relevant));
251        }
252
253        results
254    }
255
256    /// After the first backward pass, re-visit statements whose defs
257    /// became relevant because of discoveries made during that pass
258    /// (tracked in `RelevantPlaces::just_added`).
259    fn re_visit_newly_added(
260        visitor: &Self,
261        block: BasicBlock,
262        block_data: &'tcx rustc_middle::mir::BasicBlockData<'tcx>,
263        flow: &DataflowGraph,
264        relevant: &mut RelevantPlaces,
265        items: &mut Vec<RelevantItem<'tcx>>,
266        keep_inv: bool,
267    ) {
268        let newly_added = std::mem::take(&mut relevant.just_added);
269        if newly_added.is_empty() {
270            return;
271        }
272        for (si, stmt) in block_data.statements.iter().enumerate().rev() {
273            let defs = match &stmt.kind {
274                rustc_middle::mir::StatementKind::Assign(assign) => {
275                    let mut d = crate::verify::def_use::RelevantPlaces::new();
276                    d.insert_mir_place(&assign.0);
277                    d
278                }
279                _ => continue,
280            };
281            let any_new = defs.places.iter().any(|dp| {
282                newly_added.iter().any(|np| dp.local() == np.local())
283            });
284            if any_new {
285                visitor.visit_statement(
286                    block,
287                    si,
288                    stmt,
289                    flow,
290                    relevant,
291                    items,
292                    keep_inv,
293                );
294            }
295        }
296    }
297
298    /// Visit one MIR statement against the current relevance frontier.
299    fn visit_statement(
300        &self,
301        block: BasicBlock,
302        statement_index: usize,
303        statement: &'tcx rustc_middle::mir::Statement<'tcx>,
304        flow: &DataflowGraph,
305        relevant: &mut RelevantPlaces,
306        items: &mut Vec<RelevantItem<'tcx>>,
307        keep_invalidations: bool,
308    ) {
309        if keep_invalidations && matches!(statement.kind, StatementKind::StorageDead(_) | StatementKind::StorageLive(_))
310        {
311            items.push(RelevantItem::Statement {
312                block,
313                statement_index,
314            });
315            return;
316        }
317
318        let mut defs = RelevantPlaces::new();
319        match &statement.kind {
320            StatementKind::Assign(assign) => {
321                let (place, _) = &**assign;
322                defs.insert_mir_place(place);
323            }
324            StatementKind::StorageDead(local) => {
325                defs.insert_local(*local);
326            }
327            _ => {}
328        }
329
330        if defs.intersects(relevant) {
331            let mut uses = collect_statement_uses(statement, block, statement_index, flow);
332            items.push(RelevantItem::Statement {
333                block,
334                statement_index,
335            });
336            // Save places already in the relevance set before removing
337            // the current definition.  When the uses of this statement
338            // (e.g. an aggregate struct literal) would re-add a field
339            // whose definition was already found earlier in the walk,
340            // skip it to prevent wrong (duplicate) matches.
341            let mut already_seen: crate::compat::FxHashSet<crate::verify::def_use::PlaceKey> =
342                relevant.places.clone();
343            // For aggregate (struct literal) statements, also block uses
344            // that were already saturated by a descendant block.  This
345            // prevents fields like `_4` from being re-added when they
346            // were already resolved outside this block (e.g. via a copy
347            // `_4 = _8`).  Without this guard, the wrong definition
348            // (e.g. `_4 = null_mut()` from struct field init) may match.
349            let is_aggregate =
350                if let rustc_middle::mir::StatementKind::Assign(assign) = &statement.kind {
351                    matches!(assign.1, rustc_middle::mir::Rvalue::Aggregate(..))
352                } else {
353                    false
354                };
355            if is_aggregate {
356                already_seen.extend(relevant.saturated.iter().cloned());
357            }
358            relevant.remove_all(&defs);
359            uses.places.retain(|p| !already_seen.contains(p));
360            relevant.extend(uses);
361            return;
362        }
363
364        if statement_invalidates_relevant(statement, relevant) {
365            items.push(RelevantItem::Statement {
366                block,
367                statement_index,
368            });
369        } else if statement_can_refine(statement) {
370            let mut uses = RelevantPlaces::new();
371            for &local in &defs.locals {
372                for &edge_idx in &flow.node(local).in_edges {
373                    let edge = &flow.edges[edge_idx];
374                    if edge.block == block.as_usize() && edge.statement_index == statement_index {
375                        uses.insert_local(edge.src);
376                    }
377                }
378            }
379            if uses.intersects(relevant) {
380                items.push(RelevantItem::Statement {
381                    block,
382                    statement_index,
383                });
384            }
385        }
386    }
387
388    /// Visit one MIR terminator against the current relevance frontier.
389    fn visit_terminator(
390        &self,
391        block: BasicBlock,
392        terminator: &rustc_middle::mir::Terminator<'tcx>,
393        flow: &DataflowGraph,
394        body: &Body<'tcx>,
395        relevant: &mut RelevantPlaces,
396        items: &mut Vec<RelevantItem<'tcx>>,
397        keep_invalidations: bool,
398    ) {
399        if keep_invalidations && matches!(terminator.kind, TerminatorKind::Drop { .. }) {
400            items.push(RelevantItem::Terminator { block });
401            return;
402        }
403
404        if let TerminatorKind::Call {
405            func,
406            args,
407            destination,
408            ..
409        } = &terminator.kind
410        {
411            call_visit::visit(
412                self.tcx,
413                block,
414                func,
415                args,
416                destination,
417                flow,
418                body,
419                relevant,
420                items,
421            );
422            return;
423        }
424
425        let use_def = terminator_use_def(terminator);
426        if terminator_is_path_condition(terminator) {
427            items.push(RelevantItem::Terminator { block });
428            relevant.extend(use_def.uses.clone());
429            return;
430        }
431
432        if use_def.defs.intersects(relevant) {
433            if terminator_may_havoc(terminator) {
434                items.push(RelevantItem::Forget);
435            }
436            items.push(RelevantItem::Terminator { block });
437            relevant.remove_all(&use_def.defs);
438            relevant.extend(use_def.uses);
439            return;
440        }
441
442        if use_def.uses.intersects(relevant) {
443            if terminator_may_havoc(terminator) {
444                items.push(RelevantItem::Forget);
445            }
446            items.push(RelevantItem::Terminator { block });
447        }
448    }
449}
450
451// ── property helpers ──────────────────────────────────────────────────
452
453fn needs_invalidation_tracking(kind: &contract::PropertyKind) -> bool {
454    matches!(kind, contract::PropertyKind::Allocated)
455}
456
457// ── classification helpers ──────────────────────────────────────────────
458
459fn statement_can_refine(statement: &rustc_middle::mir::Statement<'_>) -> bool {
460    matches!(&statement.kind, StatementKind::Assign(assign) if matches!(
461        &**assign,
462        (
463            _,
464            rustc_middle::mir::Rvalue::BinaryOp(_, _)
465            | rustc_middle::mir::Rvalue::UnaryOp(_, _)
466            | rustc_middle::mir::Rvalue::Cast(_, _, _),
467        )
468    ))
469}
470
471fn statement_invalidates_relevant(
472    statement: &rustc_middle::mir::Statement<'_>,
473    relevant: &RelevantPlaces,
474) -> bool {
475    match &statement.kind {
476        StatementKind::StorageDead(local) => relevant.locals.contains(local),
477        _ => false,
478    }
479}
480
481fn terminator_is_path_condition(terminator: &rustc_middle::mir::Terminator<'_>) -> bool {
482    matches!(
483        terminator.kind,
484        TerminatorKind::SwitchInt { .. } | TerminatorKind::Assert { .. }
485    )
486}
487
488fn terminator_may_havoc(terminator: &rustc_middle::mir::Terminator<'_>) -> bool {
489    matches!(terminator.kind, TerminatorKind::Call { .. })
490}
491
492/// Collect all place-uses for a statement from dataflow edges and operands.
493fn collect_statement_uses<'tcx>(
494    statement: &'tcx rustc_middle::mir::Statement<'tcx>,
495    block: BasicBlock,
496    statement_index: usize,
497    flow: &DataflowGraph,
498) -> RelevantPlaces {
499    let mut uses = RelevantPlaces::new();
500
501    // Collect def locals (we know there are defs — caller already checked)
502    let def_locals = match &statement.kind {
503        StatementKind::Assign(assign) => {
504            let (place, _) = &**assign;
505            vec![place.local]
506        }
507        StatementKind::StorageDead(local) => vec![*local],
508        _ => Vec::new(),
509    };
510
511    for &local in &def_locals {
512        for &edge_idx in &flow.node(local).in_edges {
513            let edge = &flow.edges[edge_idx];
514            if edge.block == block.as_usize() && edge.statement_index == statement_index {
515                uses.insert_local(edge.src);
516            }
517        }
518    }
519
520    // Also collect uses directly from operands — the dataflow graph
521    // creates synthetic nodes for field projections (e.g. _13.0),
522    // so we need the direct operand uses to reach through.
523    if let StatementKind::Assign(assign) = &statement.kind {
524        let (_, rvalue) = &**assign;
525        for operand in super::super::def_use::rvalue_operands(rvalue) {
526            uses.extend(operand_uses(operand));
527        }
528        // A reborrow (`_p = &(*_q)`, `_p = &raw (*_q)`) carries no operands, so
529        // `rvalue_operands` misses its referent.  Only when the referent traces
530        // back to a projection out of a call's returned tuple (a `split_at`
531        // prefix/suffix slice) do we keep the referent's base local, so the
532        // split — and its `mid` argument — stays in the backward slice and
533        // feeds downstream `len(self)` obligations.  This stays narrow to avoid
534        // inflating relevance for ordinary reborrows, which explodes loop path
535        // enumeration.
536        if let rustc_middle::mir::Rvalue::Ref(_, _, place)
537        | rustc_middle::mir::Rvalue::RawPtr(_, place) = rvalue
538        {
539            uses.insert_local(place.local);
540        }
541    }
542
543    uses
544}