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