Skip to main content

rapx/verify/
loop_sensitivity.rs

1//! Loop-sensitivity planning for the staged verifier.
2//!
3//! The planner runs before path extraction in `verify --postfix-repeat auto`.
4//! It keeps the decision about loop depth separate from individual safety-tag
5//! checkers: tags still describe what must hold, while this module decides
6//! whether the checked value depends on loop-carried MIR state.
7//!
8//! # Pipeline role
9//!
10//! ```text
11//! VerifyTargetCollector ──► LoopSensitivityAnalyzer ──► VerifyDriver
12//!        (sinks)                 (repeat plan)             (paths + checks)
13//! ```
14//!
15//! The normal verifier is path-bounded: `PathExtractor` receives a numeric
16//! `allow_repeat` and enumerates only that many extra SCC postfix repetitions.
17//! In auto mode, this module chooses that number from MIR structure instead of
18//! from an already-failed tag-specific retry loop.
19//!
20//! # Current abstraction
21//!
22//! The planner now has two internal hint streams:
23//!
24//! - `DataflowDistanceHint`: a sink depends on loop-carried state, and the
25//!   planner estimates how many loop backedges are needed for that state to
26//!   reach the sink.
27//! - `NumericRangeHint`: a numeric/index obligation depends on induction-style
28//!   state, and the planner estimates the first iteration that can witness a
29//!   range violation.
30//!
31//! `RepeatPlan` is the single product consumed by the driver.  It calibrates
32//! both hint kinds into the `PathEnumerator`'s `allow_repeat` budget and picks
33//! the maximum, so detector-specific details stay inside this module.
34
35use crate::analysis::path::graph::PathGraph;
36use crate::compat::{FxHashMap, FxHashSet};
37use rustc_hir::def_id::DefId;
38use rustc_middle::{
39    mir::{
40        BasicBlock, BinOp, Body, Local, Operand, Place, ProjectionElem, Rvalue, StatementKind,
41        TerminatorKind,
42    },
43    ty::{TyCtxt, TyKind, TypingEnv},
44};
45
46use super::{
47    contract::{ContractExpr, NumericPredicate, Property, PropertyArg, PropertyKind, RelOp},
48    def_use::{RelevantPlaces, bind_callsite_roots},
49    target::FunctionTarget,
50};
51use crate::helpers::mir_scan::Checkpoint;
52
53use crate::limit::{
54    DEFAULT_LOOP_CARRIED_BACKEDGES, DEFAULT_NUMERIC_WITNESS_ITERATION, MAX_AUTO_REPEAT,
55    MIN_DATAFLOW_REPEAT,
56};
57
58/// User-selected policy for SCC postfix repetition.
59///
60/// `Fixed(n)` preserves the explicit CLI behavior: the driver checks all
61/// rounds `0..=n`.  `Auto` delegates the upper bound to this module and checks
62/// `0..=plan.repeat`, because the current SCC path enumerator is not strictly
63/// monotonic across repeat depths when path limits and branch order interact.
64#[derive(Clone, Copy, Debug)]
65pub(crate) enum RepeatStrategy {
66    /// Let `LoopSensitivityAnalyzer` choose a repeat count from MIR structure.
67    Auto,
68    /// Use the concrete repeat count supplied by `--postfix-repeat N`.
69    Fixed(usize),
70}
71
72/// Unified product of the auto loop-sensitivity pass for one function target.
73#[derive(Clone, Debug, Default)]
74pub(crate) struct RepeatPlan {
75    /// Repeat count that should be passed to `VerifyDriver::new_with_repeat`.
76    pub repeat: usize,
77}
78
79impl RepeatPlan {
80    /// Merge both detector streams into the path-enumerator repeat budget.
81    fn from_hints(
82        dataflow_hints: Vec<DataflowDistanceHint>,
83        numeric_hints: Vec<NumericRangeHint>,
84    ) -> Self {
85        let repeat = dataflow_hints
86            .iter()
87            .map(DataflowDistanceHint::calibrated_repeat)
88            .chain(
89                numeric_hints
90                    .iter()
91                    .map(NumericRangeHint::calibrated_repeat),
92            )
93            .max()
94            .unwrap_or(0)
95            .min(MAX_AUTO_REPEAT);
96
97        Self { repeat }
98    }
99}
100
101/// A loop-carried dataflow hint.
102///
103/// `needed_backedges` is measured in loop backedge traversals between the first
104/// loop iteration where a source can be produced and the first iteration where
105/// that source can reach the sink.  It is converted to `allow_repeat` by
106/// [`repeat_for_backedges`].
107#[derive(Clone, Debug)]
108pub(crate) struct DataflowDistanceHint {
109    /// Estimated number of loop backedges required for propagation.
110    pub needed_backedges: usize,
111}
112
113impl DataflowDistanceHint {
114    /// Convert this hint into the path enumerator's repeat budget.
115    fn calibrated_repeat(&self) -> usize {
116        repeat_for_backedges(self.needed_backedges)
117    }
118}
119
120/// A numeric/index range hint.
121///
122/// `witness_iteration` is the first loop-body execution that may violate the
123/// property under the simple numeric model.  For example, `value += 1` followed
124/// by `ValidNum(value < 100)` yields witness iteration 100 when `value` starts
125/// at zero.
126#[derive(Clone, Debug)]
127pub(crate) struct NumericRangeHint {
128    /// First loop-body execution that can witness a violation.
129    pub witness_iteration: usize,
130}
131
132impl NumericRangeHint {
133    /// Convert this hint into the path enumerator's repeat budget.
134    fn calibrated_repeat(&self) -> usize {
135        repeat_for_witness_iteration(self.witness_iteration)
136    }
137}
138
139/// One safety obligation whose arguments have been bound to caller MIR locals.
140///
141/// A sink starts as a `Property` written in the callee's namespace, for example
142/// `NonNull(_ptr)`.  `bind_callsite_roots` rewrites the initial relevance roots
143/// so they point at the caller locals passed into the unsafe checkpoint.  The
144/// loop planner can then ask whether those caller locals depend on loop state.
145struct SafetySink<'target, 'tcx> {
146    /// Unsafe call, raw pointer dereference, or static mut synthetic checkpoint.
147    checkpoint: &'target Checkpoint<'tcx>,
148    /// Safety property checked at this sink.
149    property: &'target Property<'tcx>,
150    /// Caller-side MIR locals and places relevant to `property`.
151    roots: RelevantPlaces,
152}
153
154/// Computes the repeat plan for one verification target.
155///
156/// The analyzer is intentionally target-local.  It does not cache across
157/// functions because MIR bodies, SCCs, and property roots are small enough for
158/// the current auto pass, and keeping the state local prevents stale repeat
159/// choices when the same function is checked under a virtual target.
160pub(crate) struct LoopSensitivityAnalyzer<'tcx> {
161    tcx: TyCtxt<'tcx>,
162}
163
164impl<'tcx> LoopSensitivityAnalyzer<'tcx> {
165    /// Create an analyzer over the current compiler type context.
166    pub(crate) fn new(tcx: TyCtxt<'tcx>) -> Self {
167        Self { tcx }
168    }
169
170    /// Build a loop-sensitivity repeat plan for `target`.
171    ///
172    /// The algorithm is:
173    ///
174    /// 1. Collect all safety sinks and bind their roots to caller locals.
175    /// 2. Build an SCC-aware `PathGraph` for the target function.
176    /// 3. Build a whole-function local dependency index.
177    /// 4. Produce dataflow-distance hints from loop-carried local transfers.
178    /// 5. Produce numeric-range hints from simple induction-sensitive sinks.
179    /// 6. Calibrate both hint streams into one repeat budget.
180    ///
181    /// This is a conservative planner: it may choose a deeper repeat for loops
182    /// that turn out to be safe, but it avoids adding tag-specific verifier
183    /// reruns or new user-visible output.
184    pub(crate) fn analyze(&self, target: &FunctionTarget<'tcx>) -> RepeatPlan {
185        if !self.tcx.is_mir_available(target.def_id) {
186            return RepeatPlan::default();
187        }
188
189        let sinks = self.collect_sinks(target);
190        if sinks.is_empty() {
191            return RepeatPlan::default();
192        }
193
194        let mut graph = PathGraph::new(self.tcx, target.def_id);
195        graph.find_scc();
196        let body = self.tcx.optimized_mir(target.def_id);
197        let dependencies = LocalDependencyIndex::new(self.tcx, target.def_id);
198        let component_summaries: Vec<_> = loop_components(&graph)
199            .into_iter()
200            .map(|component| {
201                let local_summary = LoopLocalSummary::new(body, &component);
202                let numeric_summary =
203                    LoopNumericSummary::new(self.tcx, target.def_id, body, &graph, &component);
204                (component, local_summary, numeric_summary)
205            })
206            .collect();
207
208        let dataflow_hints =
209            self.dataflow_distance_hints(&sinks, &graph, &dependencies, &component_summaries);
210        let numeric_hints =
211            self.numeric_range_hints(&sinks, &graph, &dependencies, &component_summaries);
212
213        RepeatPlan::from_hints(dataflow_hints, numeric_hints)
214    }
215
216    /// Compute loop-carried dataflow hints for every safety sink.
217    ///
218    /// This detector is tag-agnostic: it only asks whether the sink's caller
219    /// locals depend on state locals that are redefined across an SCC.  The
220    /// concrete safety checker remains responsible for deciding whether the
221    /// propagated value is actually invalid once the deeper path is enumerated.
222    fn dataflow_distance_hints<'target>(
223        &self,
224        sinks: &[SafetySink<'target, 'tcx>],
225        graph: &PathGraph<'_>,
226        dependencies: &LocalDependencyIndex,
227        component_summaries: &[(LoopComponent, LoopLocalSummary, LoopNumericSummary)],
228    ) -> Vec<DataflowDistanceHint> {
229        let mut hints = Vec::new();
230
231        for sink in sinks {
232            if sink.property.is_or() || matches!(sink.property.kind(), Some(PropertyKind::Unknown))
233            {
234                continue;
235            }
236            let root_closure = dependencies.closure_from(&sink.roots.locals);
237            if root_closure.is_empty() {
238                continue;
239            }
240
241            for (component, local_summary, _) in component_summaries {
242                if !component_reaches_checkpoint(graph, component, sink.checkpoint.block) {
243                    continue;
244                }
245                if root_closure
246                    .iter()
247                    .any(|local| local_summary.assigned_inside.contains(local))
248                {
249                    let distance_backedges = estimate_dataflow_backedges(
250                        dependencies,
251                        &sink.roots.locals,
252                        local_summary,
253                    )
254                    .unwrap_or(DEFAULT_LOOP_CARRIED_BACKEDGES);
255                    let branch_backedges = estimate_branch_sensitive_backedges(
256                        graph,
257                        component,
258                        dependencies,
259                        &root_closure,
260                        local_summary,
261                    )
262                    .unwrap_or(0);
263                    let needed_backedges = distance_backedges.max(branch_backedges);
264                    hints.push(DataflowDistanceHint { needed_backedges });
265                    break;
266                }
267            }
268        }
269
270        hints
271    }
272
273    /// Compute numeric/index range hints for induction-sensitive sinks.
274    ///
275    /// This is intentionally small but already separates numeric planning from
276    /// generic dataflow.  `ValidNum` tries to recover a concrete violating
277    /// iteration for positive affine increments; `InBound` currently marks the
278    /// first iteration missed by shallow unrolling when the pointer/index root
279    /// depends on an induction variable.
280    fn numeric_range_hints<'target>(
281        &self,
282        sinks: &[SafetySink<'target, 'tcx>],
283        graph: &PathGraph<'_>,
284        dependencies: &LocalDependencyIndex,
285        component_summaries: &[(LoopComponent, LoopLocalSummary, LoopNumericSummary)],
286    ) -> Vec<NumericRangeHint> {
287        let mut hints = Vec::new();
288
289        for sink in sinks {
290            if !matches!(
291                sink.property.kind(),
292                Some(PropertyKind::ValidNum | PropertyKind::InBound)
293            ) {
294                continue;
295            }
296            let root_closure = dependencies.closure_from(&sink.roots.locals);
297            if root_closure.is_empty() {
298                continue;
299            }
300
301            for (component, local_summary, numeric_summary) in component_summaries {
302                if !component_reaches_checkpoint(graph, component, sink.checkpoint.block) {
303                    continue;
304                }
305                if !root_closure
306                    .iter()
307                    .any(|local| local_summary.assigned_inside.contains(local))
308                {
309                    continue;
310                }
311
312                let witness_iteration = match sink.property.kind() {
313                    Some(PropertyKind::ValidNum) => {
314                        estimate_valid_num_witness(sink.property, &root_closure, numeric_summary)
315                    }
316                    Some(PropertyKind::InBound) => {
317                        estimate_inbound_witness(&root_closure, numeric_summary)
318                    }
319                    _ => None,
320                };
321
322                if let Some(witness_iteration) = witness_iteration {
323                    hints.push(NumericRangeHint { witness_iteration });
324                    break;
325                }
326            }
327        }
328
329        hints
330    }
331
332    /// Collect caller-side sinks from all checkpoint kinds in a target.
333    ///
334    /// This mirrors `VerifyDriver::properties_for_callsite` so the planner and
335    /// verifier operate on the same obligations.  Keeping it here avoids
336    /// constructing a `VerifyDriver` only to learn whether a deeper repeat is
337    /// needed before path extraction.
338    fn collect_sinks<'target>(
339        &self,
340        target: &'target FunctionTarget<'tcx>,
341    ) -> Vec<SafetySink<'target, 'tcx>> {
342        let mut sinks = Vec::new();
343
344        for checkpoint in target.all_checkpoints() {
345            let properties = target.properties_for_callsite(checkpoint);
346            if properties.is_empty() {
347                continue;
348            }
349
350            for property in properties.iter() {
351                // Expand `Or` compound contracts into their atoms so the
352                // loop planner reasons about the underlying primitives (e.g.
353                // `ValidPtr = Size(T,0) || Deref` → `Allocated`/`InBound`).
354                let mut atoms = Vec::new();
355                collect_atoms(property, &mut atoms);
356                for atom in atoms {
357                    let mut roots = RelevantPlaces::from_property(atom);
358                    bind_callsite_roots(self.tcx, &mut roots, checkpoint);
359                    if roots.locals.is_empty() {
360                        continue;
361                    }
362                    sinks.push(SafetySink {
363                        checkpoint,
364                        property: atom,
365                        roots,
366                    });
367                }
368            }
369        }
370
371        sinks
372    }
373}
374
375/// Collect the atom leaves of a (possibly compound) property tree.
376fn collect_atoms<'a, 'tcx>(property: &'a Property<'tcx>, out: &mut Vec<&'a Property<'tcx>>) {
377    match property {
378        Property::Atom(_) => out.push(property),
379        Property::And(and) => {
380            for conjunct in &and.conjuncts {
381                collect_atoms(conjunct, out);
382            }
383        }
384        Property::Or(or) => {
385            for disjunct in &or.disjuncts {
386                collect_atoms(disjunct, out);
387            }
388        }
389    }
390}
391
392/// A non-trivial SCC in the MIR control-flow graph.
393///
394/// `PathGraph` records SCC members under the entry/root block.  This wrapper
395/// stores a complete member set of the SCC.
396#[derive(Clone, Debug)]
397struct LoopComponent {
398    blocks: FxHashSet<usize>,
399}
400
401/// Extract loop SCCs from `PathGraph`.
402///
403/// Single-block non-cyclic components are ignored because they do not produce
404/// loop-carried state and therefore do not need extra postfix repetition.
405fn loop_components(graph: &PathGraph<'_>) -> Vec<LoopComponent> {
406    let mut components = Vec::new();
407    for block in &graph.cfg.blocks {
408        let scc = &block.scc;
409        if block.index != scc.enter || scc.nodes.is_empty() {
410            continue;
411        }
412        let mut blocks = scc.nodes.clone();
413        blocks.insert(scc.enter);
414        components.push(LoopComponent { blocks });
415    }
416    components
417}
418
419/// Return whether a loop SCC can reach a given checkpoint block.
420///
421fn graph_reaches_any(
422    graph: &PathGraph<'_>,
423    sources: &[usize],
424    target_pred: impl Fn(usize) -> bool,
425) -> bool {
426    if sources.iter().any(|&s| target_pred(s)) {
427        return true;
428    }
429    let mut stack: Vec<usize> = sources.to_vec();
430    let mut seen = FxHashSet::default();
431    while let Some(block) = stack.pop() {
432        if target_pred(block) {
433            return true;
434        }
435        if !seen.insert(block) || block >= graph.cfg.blocks.len() {
436            continue;
437        }
438        for next in &graph.cfg.block(block).next {
439            stack.push(*next);
440        }
441    }
442    false
443}
444
445/// Returns true if any block in `component` can reach `checkpoint` via CFG edges.
446fn component_reaches_checkpoint(
447    graph: &PathGraph<'_>,
448    component: &LoopComponent,
449    checkpoint: BasicBlock,
450) -> bool {
451    let sources: Vec<usize> = component.blocks.iter().copied().collect();
452    graph_reaches_any(graph, &sources, |b| b == checkpoint.as_usize())
453}
454
455/// Return whether `start` can reach any block in `component`.
456fn block_reaches_component(graph: &PathGraph<'_>, start: usize, component: &LoopComponent) -> bool {
457    graph_reaches_any(graph, &[start], |b| component.blocks.contains(&b))
458}
459
460/// Local assignment and transfer summary for one loop SCC.
461///
462/// The planner uses this for two related questions:
463///
464/// - Did a sink dependency get redefined inside this SCC?
465/// - If so, how many loop-carried state locals are on the dependency path from
466///   the sink back to an earlier source?
467struct LoopLocalSummary {
468    /// Locals directly assigned inside the SCC.
469    assigned_inside: FxHashSet<Local>,
470    /// Locals that have both an incoming value and an SCC update.
471    state_locals: FxHashSet<Local>,
472}
473
474impl LoopLocalSummary {
475    /// Build local assignment sets for `component`.
476    fn new(body: &Body<'_>, component: &LoopComponent) -> Self {
477        let mut assigned_inside = FxHashSet::default();
478        let mut assigned_outside = FxHashSet::default();
479
480        for (block, data) in body.basic_blocks.iter_enumerated() {
481            let assigned = collect_assigned_locals(data);
482            if component.blocks.contains(&block.as_usize()) {
483                assigned_inside.extend(assigned);
484            } else {
485                assigned_outside.extend(assigned);
486            }
487        }
488
489        let mut state_locals = FxHashSet::default();
490        for local in &assigned_inside {
491            if assigned_outside.contains(local) || local_is_argument(*local, body) {
492                state_locals.insert(*local);
493            }
494        }
495
496        Self {
497            assigned_inside,
498            state_locals,
499        }
500    }
501}
502
503/// Small numeric term language for loop guards.
504#[derive(Clone, Copy, Debug)]
505enum NumericTerm {
506    /// MIR local.
507    Local(Local),
508    /// Concrete integer constant.
509    Const(i128),
510}
511
512/// A MIR comparison that feeds a boolean branch.
513#[derive(Clone, Copy, Debug)]
514struct ComparisonFact {
515    /// Comparison operator.
516    op: BinOp,
517    /// Left-hand side.
518    lhs: NumericTerm,
519    /// Right-hand side.
520    rhs: NumericTerm,
521}
522
523/// Numeric induction summary for one loop SCC.
524///
525/// This pass currently recognizes the small affine fragment needed by the
526/// threshold tests and by common counter loops:
527///
528/// - `x = const` before the loop.
529/// - `x = x + c`, `x = x - c`, or the checked-overflow form
530///   `tmp = x + c; x = tmp.0` inside the loop.
531struct LoopNumericSummary {
532    /// Constants assigned before entering this SCC.
533    initial_constants: FxHashMap<Local, i128>,
534    /// Per-iteration affine step for locals recognized as induction variables.
535    steps: FxHashMap<Local, i128>,
536    /// Loop guard upper bounds, for example `i < len`.
537    guard_upper_bounds: FxHashMap<Local, NumericTerm>,
538    /// Lower bounds known on paths that enter this SCC, for example
539    /// `if len < 10 { return }` gives `len >= 10`.
540    entry_lower_bounds: FxHashMap<Local, i128>,
541}
542
543impl LoopNumericSummary {
544    /// Build a numeric summary for `component`.
545    fn new<'tcx>(
546        tcx: TyCtxt<'tcx>,
547        def_id: DefId,
548        body: &Body<'tcx>,
549        graph: &PathGraph<'_>,
550        component: &LoopComponent,
551    ) -> Self {
552        let mut initial_constants = FxHashMap::default();
553        let mut tuple_steps: FxHashMap<Local, (Local, i128)> = FxHashMap::default();
554        let mut steps = FxHashMap::default();
555        let mut copy_sources = FxHashMap::default();
556        let mut comparisons = FxHashMap::default();
557
558        for (block, data) in body.basic_blocks.iter_enumerated() {
559            let in_component = component.blocks.contains(&block.as_usize());
560            for statement in &data.statements {
561                let StatementKind::Assign(assign) = &statement.kind else {
562                    continue;
563                };
564                let (place, rvalue) = &**assign;
565                if place_is_indirect_write(place) {
566                    continue;
567                }
568
569                if let Some(source) = plain_copy_source(rvalue) {
570                    copy_sources.insert(place.local, source);
571                }
572                if let Some(comparison) = comparison_fact(tcx, def_id, rvalue) {
573                    comparisons.insert(place.local, comparison);
574                }
575
576                if !in_component {
577                    if let Some(value) = rvalue_const_i128(tcx, def_id, rvalue) {
578                        initial_constants.insert(place.local, value);
579                    }
580                    continue;
581                }
582
583                if let Some((source, step)) = increment_source_and_step(tcx, def_id, rvalue) {
584                    if source == place.local {
585                        steps.insert(place.local, step);
586                    } else {
587                        tuple_steps.insert(place.local, (source, step));
588                    }
589                }
590            }
591        }
592
593        for block in &component.blocks {
594            let data = &body.basic_blocks[BasicBlock::from(*block)];
595            for statement in &data.statements {
596                let StatementKind::Assign(assign) = &statement.kind else {
597                    continue;
598                };
599                let (place, rvalue) = &**assign;
600                if place_is_indirect_write(place) {
601                    continue;
602                }
603                let Some(source_temp) = rvalue_projection_source(rvalue, 0) else {
604                    continue;
605                };
606                let Some((source, step)) = tuple_steps.get(&source_temp).copied() else {
607                    continue;
608                };
609                if source == place.local {
610                    steps.insert(place.local, step);
611                }
612            }
613        }
614
615        let guard_upper_bounds =
616            collect_loop_guard_upper_bounds(&steps, &copy_sources, &comparisons);
617        let entry_lower_bounds =
618            collect_entry_lower_bounds(graph, component, &copy_sources, &comparisons);
619
620        Self {
621            initial_constants,
622            steps,
623            guard_upper_bounds,
624            entry_lower_bounds,
625        }
626    }
627}
628
629/// Collect locals directly redefined by statements or call destinations.
630///
631/// Assignments through a dereference, such as `*p = value`, mutate pointed-to
632/// memory but do not redefine the pointer local `p`; those writes are ignored
633/// here so the local-dependency heuristic stays focused on local state.
634fn collect_assigned_locals(data: &rustc_middle::mir::BasicBlockData<'_>) -> FxHashSet<Local> {
635    let mut locals = FxHashSet::default();
636    for statement in &data.statements {
637        let StatementKind::Assign(assign) = &statement.kind else {
638            continue;
639        };
640        let (place, _) = &**assign;
641        if !place_is_indirect_write(place) {
642            locals.insert(place.local);
643        }
644    }
645    if let TerminatorKind::Call { destination, .. } = &data.terminator().kind {
646        locals.insert(destination.local);
647    }
648    locals
649}
650
651/// Collect loop-guard upper bounds for induction locals.
652fn collect_loop_guard_upper_bounds(
653    steps: &FxHashMap<Local, i128>,
654    copy_sources: &FxHashMap<Local, Local>,
655    comparisons: &FxHashMap<Local, ComparisonFact>,
656) -> FxHashMap<Local, NumericTerm> {
657    let mut bounds = FxHashMap::default();
658    for comparison in comparisons.values() {
659        let lhs = resolve_numeric_term(comparison.lhs, copy_sources);
660        let rhs = resolve_numeric_term(comparison.rhs, copy_sources);
661        match (comparison.op, lhs, rhs) {
662            (BinOp::Lt | BinOp::Le, NumericTerm::Local(local), bound)
663                if steps.contains_key(&local) =>
664            {
665                bounds.insert(local, bound);
666            }
667            (BinOp::Gt | BinOp::Ge, bound, NumericTerm::Local(local))
668                if steps.contains_key(&local) =>
669            {
670                bounds.insert(local, bound);
671            }
672            _ => {}
673        }
674    }
675    bounds
676}
677
678/// Collect numeric lower bounds that hold on paths entering this SCC.
679fn collect_entry_lower_bounds(
680    graph: &PathGraph<'_>,
681    component: &LoopComponent,
682    copy_sources: &FxHashMap<Local, Local>,
683    comparisons: &FxHashMap<Local, ComparisonFact>,
684) -> FxHashMap<Local, i128> {
685    let mut bounds: FxHashMap<Local, i128> = FxHashMap::default();
686
687    for block in &graph.cfg.blocks {
688        if component.blocks.contains(&block.index) {
689            continue;
690        }
691        let Some(terminator) = graph.cfg.terminator(block.index) else {
692            continue;
693        };
694        let TerminatorKind::SwitchInt { discr, targets } = &terminator.kind else {
695            continue;
696        };
697        let Some(discr_local) = crate::helpers::mir_utils::extract_local(discr) else {
698            continue;
699        };
700        let discr_local = resolve_local_copy(discr_local, copy_sources);
701        let Some(comparison) = comparisons.get(&discr_local).copied() else {
702            continue;
703        };
704
705        for successor in switch_successors(targets) {
706            if !block_reaches_component(graph, successor.block, component) {
707                continue;
708            }
709            let Some((local, lower_bound)) =
710                lower_bound_from_branch(comparison, successor.value, copy_sources)
711            else {
712                continue;
713            };
714            bounds
715                .entry(local)
716                .and_modify(|existing| *existing = (*existing).max(lower_bound))
717                .or_insert(lower_bound);
718        }
719    }
720
721    bounds
722}
723
724/// Switch successor annotated with the boolean value that selects it.
725#[derive(Clone, Copy)]
726struct SwitchSuccessor {
727    block: usize,
728    value: u128,
729}
730
731/// Return all explicit and `otherwise` successors of a bool-like switch.
732fn switch_successors(targets: &rustc_middle::mir::SwitchTargets) -> Vec<SwitchSuccessor> {
733    let explicit: Vec<_> = targets.iter().collect();
734    let mut successors: Vec<_> = explicit
735        .iter()
736        .map(|(value, target)| SwitchSuccessor {
737            block: target.as_usize(),
738            value: *value,
739        })
740        .collect();
741    let otherwise_value = if explicit.iter().any(|(value, _)| *value == 0) {
742        1
743    } else {
744        0
745    };
746    successors.push(SwitchSuccessor {
747        block: targets.otherwise().as_usize(),
748        value: otherwise_value,
749    });
750    successors
751}
752
753/// Estimate extra budget needed when a loop-carried sink is controlled by
754/// internal SCC branches.
755///
756/// This is still a dataflow hint: the sink depends on a local that is assigned
757/// inside the SCC.  The extra budget accounts for a separate limitation of the
758/// path extractor, where the unsafe source may live on a branch combination
759/// that is not reached by the shortest value-dependency chain alone.
760fn estimate_branch_sensitive_backedges(
761    graph: &PathGraph<'_>,
762    component: &LoopComponent,
763    dependencies: &LocalDependencyIndex,
764    root_closure: &FxHashSet<Local>,
765    local_summary: &LoopLocalSummary,
766) -> Option<usize> {
767    if !component_has_internal_branch(graph, component) {
768        return None;
769    }
770
771    let sink_state_reassigned = root_closure
772        .iter()
773        .any(|local| local_summary.state_locals.contains(local));
774    let multi_source_assignment = root_closure.iter().any(|local| {
775        local_summary.assigned_inside.contains(local)
776            && dependencies
777                .sources_by_dest
778                .get(local)
779                .is_some_and(|sources| sources.len() > 1)
780    });
781
782    (sink_state_reassigned || multi_source_assignment).then_some(DEFAULT_LOOP_CARRIED_BACKEDGES)
783}
784
785/// Return true when a component contains a real in-loop branch.
786///
787/// The ordinary loop guard usually has one successor back into the SCC and one
788/// successor to the exit.  That shape alone does not need the expensive branch
789/// budget.  We only count branch points where two or more successors remain
790/// inside the SCC, such as `if`/`match` choices in the loop body.
791fn component_has_internal_branch(graph: &PathGraph<'_>, component: &LoopComponent) -> bool {
792    component.blocks.iter().any(|block| {
793        graph
794            .cfg
795            .block(*block)
796            .next
797            .iter()
798            .filter(|next| component.blocks.contains(next))
799            .take(2)
800            .count()
801            >= 2
802    })
803}
804
805/// Infer a local lower bound from taking one boolean branch of a comparison.
806fn lower_bound_from_branch(
807    comparison: ComparisonFact,
808    branch_value: u128,
809    copy_sources: &FxHashMap<Local, Local>,
810) -> Option<(Local, i128)> {
811    let is_true = branch_value != 0;
812    let lhs = resolve_numeric_term(comparison.lhs, copy_sources);
813    let rhs = resolve_numeric_term(comparison.rhs, copy_sources);
814    match (is_true, comparison.op, lhs, rhs) {
815        (false, BinOp::Lt, NumericTerm::Local(local), NumericTerm::Const(bound)) => {
816            Some((local, bound))
817        }
818        (false, BinOp::Le, NumericTerm::Local(local), NumericTerm::Const(bound)) => {
819            Some((local, bound.checked_add(1)?))
820        }
821        (false, BinOp::Gt, NumericTerm::Const(bound), NumericTerm::Local(local)) => {
822            Some((local, bound))
823        }
824        (false, BinOp::Ge, NumericTerm::Const(bound), NumericTerm::Local(local)) => {
825            Some((local, bound.checked_add(1)?))
826        }
827        (true, BinOp::Ge, NumericTerm::Local(local), NumericTerm::Const(bound)) => {
828            Some((local, bound))
829        }
830        (true, BinOp::Gt, NumericTerm::Local(local), NumericTerm::Const(bound)) => {
831            Some((local, bound.checked_add(1)?))
832        }
833        (true, BinOp::Le, NumericTerm::Const(bound), NumericTerm::Local(local)) => {
834            Some((local, bound))
835        }
836        (true, BinOp::Lt, NumericTerm::Const(bound), NumericTerm::Local(local)) => {
837            Some((local, bound.checked_add(1)?))
838        }
839        _ => None,
840    }
841}
842
843/// Convert loop backedge distance to `PathEnumerator::allow_repeat`.
844///
845/// With the current SCC postfix encoding, `allow_repeat = 1` reaches the first
846/// shallow repeated postfix paths, while the delayed threshold fixtures require
847/// three loop backedges and are first exposed at `allow_repeat = 2`.
848fn repeat_for_backedges(needed_backedges: usize) -> usize {
849    if needed_backedges == 0 {
850        0
851    } else {
852        needed_backedges
853            .saturating_sub(1)
854            .max(MIN_DATAFLOW_REPEAT)
855            .min(MAX_AUTO_REPEAT)
856    }
857}
858
859/// Convert a 1-based loop-body witness iteration to `allow_repeat`.
860///
861/// The existing threshold fixtures document the calibration point:
862/// witness iteration 4 is first visible at `--postfix-repeat 2`.
863fn repeat_for_witness_iteration(witness_iteration: usize) -> usize {
864    witness_iteration.saturating_sub(2).min(MAX_AUTO_REPEAT)
865}
866
867/// Estimate how far loop-carried state must travel before reaching a sink.
868///
869/// This uses the whole-function local dependency graph, but only increments the
870/// distance when the dependency path crosses a state local for the current SCC.
871/// Temporaries introduced by calls/casts/projections therefore do not inflate
872/// the estimate.
873fn estimate_dataflow_backedges(
874    dependencies: &LocalDependencyIndex,
875    roots: &FxHashSet<Local>,
876    local_summary: &LoopLocalSummary,
877) -> Option<usize> {
878    let mut best_state_distance = 0usize;
879    for root in roots {
880        let mut visited = FxHashSet::default();
881        best_state_distance = best_state_distance.max(max_state_distance_from(
882            dependencies,
883            *root,
884            local_summary,
885            0,
886            &mut visited,
887        ));
888    }
889
890    if best_state_distance == 0 {
891        None
892    } else {
893        Some(best_state_distance)
894    }
895}
896
897/// DFS over dependency edges, counting only loop-carried state locals.
898fn max_state_distance_from(
899    dependencies: &LocalDependencyIndex,
900    local: Local,
901    local_summary: &LoopLocalSummary,
902    distance: usize,
903    visited: &mut FxHashSet<Local>,
904) -> usize {
905    if !visited.insert(local) {
906        return distance;
907    }
908
909    let mut best = distance;
910    if let Some(sources) = dependencies.sources_by_dest.get(&local) {
911        for source in sources {
912            let next_distance = distance + usize::from(local_summary.state_locals.contains(source));
913            let mut branch_visited = visited.clone();
914            best = best.max(max_state_distance_from(
915                dependencies,
916                *source,
917                local_summary,
918                next_distance,
919                &mut branch_visited,
920            ));
921        }
922    }
923    best
924}
925
926/// Estimate the first violating iteration for a simple `ValidNum` sink.
927fn estimate_valid_num_witness(
928    property: &Property<'_>,
929    root_closure: &FxHashSet<Local>,
930    numeric_summary: &LoopNumericSummary,
931) -> Option<usize> {
932    let violation_value = valid_num_violation_value(property)?;
933    root_closure
934        .iter()
935        .filter_map(|local| {
936            let init = numeric_summary.initial_constants.get(local).copied()?;
937            let step = numeric_summary.steps.get(local).copied()?;
938            witness_iteration_for_threshold(init, step, violation_value)
939        })
940        .min()
941}
942
943/// Estimate an index-range witness for `InBound`.
944///
945/// The current implementation recognizes that a checked pointer/index depends
946/// on an induction variable.  It returns the first loop-body count that is not
947/// covered by `--postfix-repeat 1`; a later affine summary can replace this
948/// fallback with a symbolic `i in [0, len)` proof and a concrete witness.
949fn estimate_inbound_witness(
950    root_closure: &FxHashSet<Local>,
951    numeric_summary: &LoopNumericSummary,
952) -> Option<usize> {
953    let mut fallback = false;
954    let mut best = None;
955
956    for local in root_closure {
957        let Some(init) = numeric_summary.initial_constants.get(local).copied() else {
958            continue;
959        };
960        let Some(step) = numeric_summary.steps.get(local).copied() else {
961            continue;
962        };
963        if step == 0 {
964            continue;
965        }
966        fallback = true;
967
968        let Some(guard_bound) = numeric_summary.guard_upper_bounds.get(local).copied() else {
969            continue;
970        };
971        let Some(bound_lower) = numeric_term_lower_bound(guard_bound, numeric_summary) else {
972            continue;
973        };
974        let Some(witness) = witness_iteration_for_threshold(init, step, bound_lower) else {
975            continue;
976        };
977        best = Some(best.map_or(witness, |current: usize| current.min(witness)));
978    }
979
980    best.or_else(|| fallback.then_some(DEFAULT_NUMERIC_WITNESS_ITERATION))
981}
982
983/// Extract the lowest value that violates a simple upper-bound `ValidNum`.
984fn valid_num_violation_value(property: &Property<'_>) -> Option<i128> {
985    if !matches!(property.kind(), Some(PropertyKind::ValidNum)) {
986        return None;
987    }
988    let Some(PropertyArg::Predicates(predicates)) = property.args().first() else {
989        return None;
990    };
991    predicates
992        .iter()
993        .filter_map(simple_upper_bound_violation_value)
994        .min()
995}
996
997/// Match `x < C`, `x <= C`, and their constant-on-left equivalents.
998fn simple_upper_bound_violation_value(predicate: &NumericPredicate<'_>) -> Option<i128> {
999    match (&predicate.lhs, predicate.op, &predicate.rhs) {
1000        (lhs, RelOp::Lt, rhs) if expr_is_place(lhs) => expr_const_i128(rhs),
1001        (lhs, RelOp::Le, rhs) if expr_is_place(lhs) => expr_const_i128(rhs)?.checked_add(1),
1002        (lhs, RelOp::Gt, rhs) if expr_is_place(rhs) => expr_const_i128(lhs),
1003        (lhs, RelOp::Ge, rhs) if expr_is_place(rhs) => expr_const_i128(lhs)?.checked_add(1),
1004        _ => None,
1005    }
1006}
1007
1008/// Return true when a contract expression is a plain numeric place.
1009fn expr_is_place(expr: &ContractExpr<'_>) -> bool {
1010    matches!(expr, ContractExpr::Place(_))
1011}
1012
1013/// Extract a contract integer constant small enough for the planner model.
1014fn expr_const_i128(expr: &ContractExpr<'_>) -> Option<i128> {
1015    match expr {
1016        ContractExpr::Const(value) if *value <= i128::MAX as u128 => Some(*value as i128),
1017        _ => None,
1018    }
1019}
1020
1021/// Resolve a known lower bound for a numeric term.
1022fn numeric_term_lower_bound(term: NumericTerm, summary: &LoopNumericSummary) -> Option<i128> {
1023    match term {
1024        NumericTerm::Const(value) => Some(value),
1025        NumericTerm::Local(local) => summary.entry_lower_bounds.get(&local).copied(),
1026    }
1027}
1028
1029/// Compute the first 1-based iteration where `init + step * iteration` reaches
1030/// `violation_value`.
1031fn witness_iteration_for_threshold(init: i128, step: i128, violation_value: i128) -> Option<usize> {
1032    if step <= 0 {
1033        return None;
1034    }
1035    if init >= violation_value {
1036        return Some(0);
1037    }
1038    let delta = violation_value.checked_sub(init)?;
1039    usize::try_from(ceil_div_i128(delta, step)).ok()
1040}
1041
1042/// Integer ceil-div for positive operands.
1043fn ceil_div_i128(lhs: i128, rhs: i128) -> i128 {
1044    debug_assert!(lhs >= 0);
1045    debug_assert!(rhs > 0);
1046    (lhs + rhs - 1) / rhs
1047}
1048
1049/// Return true if `local` is a function argument local.
1050fn local_is_argument(local: Local, body: &Body<'_>) -> bool {
1051    let index = local.as_usize();
1052    index > 0 && index <= body.arg_count
1053}
1054
1055/// Extract a direct constant assignment from an rvalue.
1056fn rvalue_const_i128<'tcx>(
1057    tcx: TyCtxt<'tcx>,
1058    def_id: DefId,
1059    rvalue: &Rvalue<'tcx>,
1060) -> Option<i128> {
1061    match rvalue {
1062        Rvalue::Use(operand, ..) | Rvalue::Cast(_, operand, _) => {
1063            operand_const_i128(tcx, def_id, operand)
1064        }
1065        _ => None,
1066    }
1067}
1068
1069/// Return the source local for a plain local copy/move rvalue.
1070fn plain_copy_source(rvalue: &Rvalue<'_>) -> Option<Local> {
1071    let Rvalue::Use(Operand::Copy(place) | Operand::Move(place), ..) = rvalue else {
1072        return None;
1073    };
1074    place.projection.is_empty().then_some(place.local)
1075}
1076
1077/// Extract a simple comparison from an rvalue.
1078fn comparison_fact<'tcx>(
1079    tcx: TyCtxt<'tcx>,
1080    def_id: DefId,
1081    rvalue: &Rvalue<'tcx>,
1082) -> Option<ComparisonFact> {
1083    let Rvalue::BinaryOp(op, operands) = rvalue else {
1084        return None;
1085    };
1086    if !matches!(
1087        op,
1088        BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge | BinOp::Eq | BinOp::Ne
1089    ) {
1090        return None;
1091    }
1092    Some(ComparisonFact {
1093        op: *op,
1094        lhs: numeric_term_from_operand(tcx, def_id, &operands.0)?,
1095        rhs: numeric_term_from_operand(tcx, def_id, &operands.1)?,
1096    })
1097}
1098
1099/// Convert a MIR operand into the planner's small numeric term language.
1100fn numeric_term_from_operand<'tcx>(
1101    tcx: TyCtxt<'tcx>,
1102    def_id: DefId,
1103    operand: &Operand<'tcx>,
1104) -> Option<NumericTerm> {
1105    crate::helpers::mir_utils::extract_local(operand)
1106        .map(NumericTerm::Local)
1107        .or_else(|| operand_const_i128(tcx, def_id, operand).map(NumericTerm::Const))
1108}
1109
1110/// Follow plain copy chains for a local.
1111fn resolve_local_copy(local: Local, copy_sources: &FxHashMap<Local, Local>) -> Local {
1112    let mut current = local;
1113    let mut seen = FxHashSet::default();
1114    while seen.insert(current) {
1115        let Some(next) = copy_sources.get(&current).copied() else {
1116            break;
1117        };
1118        current = next;
1119    }
1120    current
1121}
1122
1123/// Follow copy chains inside a numeric term.
1124fn resolve_numeric_term(term: NumericTerm, copy_sources: &FxHashMap<Local, Local>) -> NumericTerm {
1125    match term {
1126        NumericTerm::Local(local) => NumericTerm::Local(resolve_local_copy(local, copy_sources)),
1127        NumericTerm::Const(value) => NumericTerm::Const(value),
1128    }
1129}
1130
1131/// Extract `local +/- const` from an arithmetic rvalue.
1132fn increment_source_and_step<'tcx>(
1133    tcx: TyCtxt<'tcx>,
1134    def_id: DefId,
1135    rvalue: &Rvalue<'tcx>,
1136) -> Option<(Local, i128)> {
1137    let Rvalue::BinaryOp(op, operands) = rvalue else {
1138        return None;
1139    };
1140    let lhs_local = crate::helpers::mir_utils::extract_local(&operands.0);
1141    let rhs_local = crate::helpers::mir_utils::extract_local(&operands.1);
1142    let lhs_const = operand_const_i128(tcx, def_id, &operands.0);
1143    let rhs_const = operand_const_i128(tcx, def_id, &operands.1);
1144
1145    match op {
1146        BinOp::Add | BinOp::AddWithOverflow | BinOp::AddUnchecked => match (lhs_local, rhs_local) {
1147            (Some(local), None) => Some((local, rhs_const?)),
1148            (None, Some(local)) => Some((local, lhs_const?)),
1149            _ => None,
1150        },
1151        BinOp::Sub | BinOp::SubWithOverflow | BinOp::SubUnchecked => match (lhs_local, rhs_const) {
1152            (Some(local), Some(value)) => Some((local, -value)),
1153            _ => None,
1154        },
1155        _ => None,
1156    }
1157}
1158
1159/// Return the source temp for `tmp.field_index`.
1160fn rvalue_projection_source(rvalue: &Rvalue<'_>, field_index: usize) -> Option<Local> {
1161    let Rvalue::Use(Operand::Copy(place) | Operand::Move(place), ..) = rvalue else {
1162        return None;
1163    };
1164    (first_field_projection(place) == Some(field_index)).then_some(place.local)
1165}
1166
1167/// Return the local used by an operand when the operand is a plain local place.
1168/// Extract an integer constant from an operand.
1169fn operand_const_i128<'tcx>(
1170    tcx: TyCtxt<'tcx>,
1171    def_id: DefId,
1172    operand: &Operand<'tcx>,
1173) -> Option<i128> {
1174    let Operand::Constant(constant) = operand else {
1175        return None;
1176    };
1177    let typing_env = TypingEnv::post_analysis(tcx, def_id);
1178    match constant.const_.ty().kind() {
1179        TyKind::Bool => constant
1180            .const_
1181            .try_eval_bool(tcx, typing_env)
1182            .map(|value| if value { 1 } else { 0 }),
1183        TyKind::Int(_) | TyKind::Uint(_) => constant
1184            .const_
1185            .try_eval_bits(tcx, typing_env)
1186            .and_then(|bits| {
1187                if bits <= i128::MAX as u128 {
1188                    Some(bits as i128)
1189                } else {
1190                    None
1191                }
1192            }),
1193        _ => None,
1194    }
1195}
1196
1197/// Return the first field projected from a place.
1198fn first_field_projection(place: &Place<'_>) -> Option<usize> {
1199    for projection in place.projection.iter() {
1200        if let ProjectionElem::Field(field, _) = projection {
1201            return Some(field.as_usize());
1202        }
1203    }
1204    None
1205}
1206
1207/// Whole-function local dependency index.
1208///
1209/// The map is intentionally directioned from destination to source:
1210/// `sources_by_dest[x] = {y, z}` means the value of local `x` may have been
1211/// assigned from `y` or `z`.  Starting from a sink root, a reverse closure over
1212/// this map finds all locals that may flow into the checked argument.
1213struct LocalDependencyIndex {
1214    sources_by_dest: FxHashMap<Local, FxHashSet<Local>>,
1215}
1216
1217impl LocalDependencyIndex {
1218    /// Build dependency edges from MIR assignments and call arguments.
1219    ///
1220    /// Calls are approximated by connecting their destination to all argument
1221    /// locals.  That is coarse but useful for wrappers such as
1222    /// `ptr.wrapping_add(offset)`: the resulting pointer local depends on both
1223    /// the base pointer and the offset local.
1224    fn new(tcx: TyCtxt<'_>, def_id: DefId) -> Self {
1225        let body = tcx.optimized_mir(def_id);
1226        let mut sources_by_dest: FxHashMap<Local, FxHashSet<Local>> = FxHashMap::default();
1227
1228        for data in body.basic_blocks.iter() {
1229            for statement in &data.statements {
1230                let StatementKind::Assign(assign) = &statement.kind else {
1231                    continue;
1232                };
1233                let (place, rvalue) = &**assign;
1234                if place_is_indirect_write(place) {
1235                    continue;
1236                }
1237                let mut sources = FxHashSet::default();
1238                collect_rvalue_sources(rvalue, &mut sources);
1239                if !sources.is_empty() {
1240                    sources_by_dest
1241                        .entry(place.local)
1242                        .or_default()
1243                        .extend(sources);
1244                }
1245            }
1246
1247            if let TerminatorKind::Call {
1248                args, destination, ..
1249            } = &data.terminator().kind
1250            {
1251                let mut sources = FxHashSet::default();
1252                for arg in args {
1253                    collect_operand_sources(&arg.node, &mut sources);
1254                }
1255                if !sources.is_empty() {
1256                    sources_by_dest
1257                        .entry(destination.local)
1258                        .or_default()
1259                        .extend(sources);
1260                }
1261            }
1262        }
1263
1264        Self { sources_by_dest }
1265    }
1266
1267    /// Compute all locals that may flow into `roots`.
1268    ///
1269    /// This deliberately ignores statement order; the planner only decides
1270    /// whether extra loop depth may be useful.  The ordinary verifier remains
1271    /// responsible for path-ordered proof or failure once the deeper paths are
1272    /// enumerated.
1273    fn closure_from(&self, roots: &FxHashSet<Local>) -> FxHashSet<Local> {
1274        let mut closure = FxHashSet::default();
1275        let mut stack: Vec<Local> = roots.iter().copied().collect();
1276        while let Some(local) = stack.pop() {
1277            if !closure.insert(local) {
1278                continue;
1279            }
1280            if let Some(sources) = self.sources_by_dest.get(&local) {
1281                for source in sources {
1282                    stack.push(*source);
1283                }
1284            }
1285        }
1286        closure
1287    }
1288}
1289
1290/// Add local operands referenced by an rvalue to `out`.
1291///
1292/// This is a shallow syntactic dependency extractor.  It does not try to
1293/// classify whether the dependency is pointer provenance, numeric dataflow, or
1294/// a guard value; those distinctions belong in future precise summaries.
1295fn collect_rvalue_sources(rvalue: &Rvalue<'_>, out: &mut FxHashSet<Local>) {
1296    match rvalue {
1297        Rvalue::Use(operand, ..) => collect_operand_sources(operand, out),
1298        Rvalue::Repeat(operand, _) => collect_operand_sources(operand, out),
1299        Rvalue::Ref(_, _, place) | Rvalue::RawPtr(_, place) | Rvalue::Discriminant(place) => {
1300            out.insert(place.local);
1301        }
1302        Rvalue::Cast(_, operand, _) | Rvalue::UnaryOp(_, operand) => {
1303            collect_operand_sources(operand, out);
1304        }
1305        Rvalue::BinaryOp(_, operands) => {
1306            collect_operand_sources(&operands.0, out);
1307            collect_operand_sources(&operands.1, out);
1308        }
1309        Rvalue::Aggregate(_, operands) => {
1310            for operand in operands {
1311                collect_operand_sources(operand, out);
1312            }
1313        }
1314        Rvalue::CopyForDeref(place) => {
1315            out.insert(place.local);
1316        }
1317        #[cfg(not(rapx_ge_99))]
1318        Rvalue::ShallowInitBox(operand, _) => collect_operand_sources(operand, out),
1319        Rvalue::ThreadLocalRef(_) => {}
1320        #[cfg(not(rapx_ge_95))]
1321        Rvalue::NullaryOp(..) => {}
1322        _ => {}
1323    }
1324}
1325
1326/// Add the local read by a MIR operand, if any.
1327fn collect_operand_sources(operand: &Operand<'_>, out: &mut FxHashSet<Local>) {
1328    match operand {
1329        Operand::Copy(place) | Operand::Move(place) => {
1330            out.insert(place.local);
1331        }
1332        Operand::Constant(_) => {}
1333        #[cfg(rapx_ge_95)]
1334        Operand::RuntimeChecks(_) => {}
1335    }
1336}
1337
1338/// Return true when an assignment writes through a pointer/reference.
1339///
1340/// Such a statement changes memory reachable from a local rather than the local
1341/// itself, so it is not a local redefinition for this planner.
1342fn place_is_indirect_write(place: &Place<'_>) -> bool {
1343    place
1344        .projection
1345        .iter()
1346        .any(|projection| matches!(projection, ProjectionElem::Deref))
1347}