Skip to main content

rapx/analysis/path/
graph.rs

1use super::PathTree;
2use crate::compat::{FxHashMap, FxHashSet};
3use crate::graphs::{
4    cfg::{CfgBlock, ControlFlowGraph},
5    scc::{Scc, SccInfo},
6};
7use rustc_middle::{
8    mir::{
9        AggregateKind, BasicBlock, BinOp, Local, Operand, ProjectionElem, Rvalue, StatementKind,
10        SwitchTargets, Terminator, TerminatorKind, UnOp, UnwindAction,
11    },
12    ty::{TyCtxt, TyKind, TypingEnv},
13};
14use rustc_span::def_id::DefId;
15use std::collections::hash_map::DefaultHasher;
16use std::hash::{Hash, Hasher};
17
18use crate::limit::{
19    LOCAL_INLINE_BLOCK_LIMIT, SCC_MAX_DEPTH, SCC_MAX_PATH_LEN, SCC_MAX_SEEN_PATHS,
20    SCC_PATH_CACHE_LIMIT, WHOLE_CFG_PATH_DEPTH_LIMIT,
21};
22
23/// Check whether the current entry→entry sub-path introduces a new block
24/// *sequence* (not just new blocks).  Different branch choices inside the SCC
25/// produce different sequences even when all block IDs have already been seen,
26/// e.g. `if i % 2 == 0 { A } else { B }` alternates between two paths through
27/// the same set of blocks on successive loop iterations.
28fn check_postfix_segment(
29    path: &[usize],
30    enter: usize,
31    segment_counts: &mut FxHashMap<Vec<usize>, usize>,
32    max_repeats: usize,
33) -> bool {
34    let segment = extract_segment(path, enter);
35    let count = segment_counts.entry(segment).or_insert(0);
36    *count += 1;
37    *count == 1 || *count - 1 <= max_repeats
38}
39
40fn extract_segment(path: &[usize], enter: usize) -> Vec<usize> {
41    let prev_pos = path[..path.len() - 1]
42        .iter()
43        .rposition(|&node| node == enter)
44        .unwrap_or(0);
45    path[prev_pos + 1..path.len() - 1].to_vec()
46}
47
48#[derive(Clone, Debug)]
49/// A single enumerated acyclic path through an SCC region.
50///
51/// `blocks` is the ordered sequence of MIR block indices from the SCC entry
52/// to the last block before exiting. The last block may have multiple CFG
53/// successors outside the SCC (e.g. a `SwitchInt` branching to different
54/// out-of-SCC targets), which are stored in `exit_successors`.
55///
56/// For example, in a loop with `switch x { A => loop_body, B => done1, C => done2 }`,
57/// the corresponding `SccPath` would have `exit_successors = [done1, done2]`
58/// — the DFS forks recursively into each of these when constructing whole-CFG paths.
59pub struct SccPath {
60    pub blocks: Vec<usize>,
61    pub exit_successors: Vec<usize>,
62}
63
64/// Per-block info collected during construction for path reachability
65/// analysis.  Each block's assignments, constants, and copy chains are
66/// stored together so they can be read with a single index lookup.
67#[derive(Clone, Debug, Default)]
68pub struct BlockConstantInfo {
69    pub assigned_locals: FxHashSet<usize>,
70    pub constants: FxHashMap<usize, usize>,
71    pub constraint_copies: FxHashMap<usize, usize>,
72    /// Maps a local assigned by `AddWithOverflow(src, const)` to `(src, const)`.
73    pub increments: FxHashMap<usize, (usize, usize)>,
74    /// Maps a local assigned by `Rem(src, const)` to `(src, divisor)`.
75    pub remainders: FxHashMap<usize, (usize, usize)>,
76    /// Maps a boolean local (e.g., a guard result) to the binary comparison
77    /// that produced it: `(op, lhs_local, rhs_kind)`.
78    pub comparison_sources: FxHashMap<usize, ComparisonSource>,
79    /// Set of locals that are provably non-null (e.g. assigned from
80    /// `Box::into_raw`).  Used for path reachability pruning.
81    pub known_nonnull_locals: FxHashSet<usize>,
82    pub negation_sources: FxHashMap<usize, usize>,
83    pub and_sources: FxHashMap<usize, (usize, usize)>,
84}
85/// Records the origin of a boolean temporary produced by a binary
86/// comparison during guard-clause evaluation.
87#[derive(Clone, Debug)]
88pub struct ComparisonSource {
89    pub op: rustc_middle::mir::BinOp,
90    pub lhs_local: usize,
91    pub rhs_local: usize,
92    pub rhs_is_constant: bool,
93}
94
95/// Encode a `(local, field_index)` pair into a single `usize`.
96const AGGREGATE_FIELD_MULT: usize = 256;
97const AGGREGATE_FIELD_SENTINEL: usize = 1 << (usize::BITS as usize - 1);
98
99fn encode_aggregate_field(local: usize, field: usize) -> usize {
100    debug_assert!(field < AGGREGATE_FIELD_MULT);
101    AGGREGATE_FIELD_SENTINEL | (local * AGGREGATE_FIELD_MULT + field)
102}
103
104fn decode_aggregate_field(encoded: usize) -> Option<(usize, usize)> {
105    if encoded & AGGREGATE_FIELD_SENTINEL == 0 {
106        return None;
107    }
108    let raw = encoded & !AGGREGATE_FIELD_SENTINEL;
109    Some((raw / AGGREGATE_FIELD_MULT, raw % AGGREGATE_FIELD_MULT))
110}
111
112fn first_field_projection(place: &rustc_middle::mir::Place<'_>) -> Option<usize> {
113    for proj in place.projection.iter() {
114        if let rustc_middle::mir::ProjectionElem::Field(field, _) = proj {
115            return Some(field.as_usize());
116        }
117    }
118    None
119}
120
121/// Enum discriminant metadata used by [`check_switch_transition`].
122///
123/// `source_of` maps a discriminant local to the ADT local it was read from
124/// (via `Rvalue::Discriminant`).  `variant_count_of` tracks the number of
125/// variants for each ADT local, used to determine whether a `SwitchInt`
126/// otherwise-target is uniquely determined.
127#[derive(Clone, Debug, Default)]
128pub struct DiscriminantInfo {
129    pub source_of: FxHashMap<usize, usize>,
130    pub variant_count_of: FxHashMap<usize, usize>,
131}
132
133/// CFG augmented with per-block constant info and discriminant metadata
134/// for path reachability analysis.
135///
136/// `PathGraph` wraps a `ControlFlowGraph` and adds block-indexed data
137/// that track assignments, constants, and copy chains.  These are used by
138/// [`check_transition`](PathGraph::check_transition) to update a set of
139/// discriminant constraints while traversing the CFG, enabling early
140/// pruning of infeasible `SwitchInt` branches during path enumeration.
141#[derive(Clone)]
142pub struct PathGraph<'tcx> {
143    pub cfg: ControlFlowGraph<'tcx>,
144    pub block_info: Vec<BlockConstantInfo>,
145    pub disc_info: DiscriminantInfo,
146    /// Global store: maps encoded `(local, field_idx)` to source local from Aggregates.
147    pub aggregate_field_sources: FxHashMap<usize, usize>,
148    /// Global store: maps `dest` to `src` for pointer-cast chains.
149    pub cast_chains: FxHashMap<usize, usize>,
150    /// Global store: maps `_dest` to encoded `(base, field_idx)` from field-projection copies.
151    pub field_projection_source: FxHashMap<usize, usize>,
152    /// Per-inlined-callee argument binding: maps the callee's entry block
153    /// (global index) to the caller's argument locals and destination local.
154    pub inline_bindings: FxHashMap<usize, InlineBinding>,
155    /// Per-inlined-callee parent: entry block (global index) → the `DefId` of
156    /// the caller whose `Call` was inlined at that entry.
157    pub inline_parents: FxHashMap<usize, DefId>,
158    /// Caller blocks whose `Call` terminator was inlined (their successor edge
159    /// now leads into a callee). The slicer skips these calls.
160    pub inlined_call_blocks: FxHashSet<usize>,
161    /// Per-function local-index namespace: `def_id` → base offset. The root
162    /// caller uses base `0`; every inlined callee gets a fresh base so its MIR
163    /// locals do not clash with the caller's in the flat constraint maps
164    /// (`block_info`, `disc_info`, `cast_chains`, …). A constraint key `k` in
165    /// the namespace of `def_id` denotes MIR local `k - base`.
166    local_bases: FxHashMap<DefId, usize>,
167    /// Next unused local-namespace base (monotonic counter).
168    next_local_base: usize,
169}
170
171/// Argument/return binding recorded when a callee CFG is inlined.
172#[derive(Clone, Debug)]
173pub struct InlineBinding {
174    /// Caller argument locals (callee param `_i+1` ← `arg_locals[i]`).
175    pub arg_locals: Vec<usize>,
176    /// Caller destination local (receives the callee's `_0`).
177    pub dest_local: usize,
178}
179
180/// Scan a function's MIR and collect its per-block constant/discriminant
181/// metadata and global source maps, remapping every MIR local `l` to
182/// `local_base + l`. The root caller uses base `0`; inlined callees use a
183/// fresh base so their locals never collide with the caller's in the flat
184/// constraint maps (see [`PathGraph::local_bases`]).
185#[allow(clippy::type_complexity)]
186fn build_function_info(
187    tcx: TyCtxt<'_>,
188    def_id: DefId,
189    local_base: usize,
190) -> (
191    Vec<BlockConstantInfo>,
192    DiscriminantInfo,
193    FxHashMap<usize, usize>,
194    FxHashMap<usize, usize>,
195    FxHashMap<usize, usize>,
196) {
197    let body = tcx.optimized_mir(def_id);
198    let basicblocks = &body.basic_blocks;
199    let mut block_info = Vec::new();
200    let mut disc_info = DiscriminantInfo::default();
201    let mut aggregate_field_sources: FxHashMap<usize, usize> = FxHashMap::default();
202    let mut field_projection_source: FxHashMap<usize, usize> = FxHashMap::default();
203    let mut cast_chains: FxHashMap<usize, usize> = FxHashMap::default();
204
205    for i in 0..basicblocks.len() {
206        let bb = &basicblocks[BasicBlock::from(i)];
207        let mut info = BlockConstantInfo::default();
208
209        for stmt in &bb.statements {
210            if let StatementKind::Assign(assign) = &stmt.kind {
211                let (place, rvalue) = &**assign;
212                let dest = local_base + place.local.as_usize();
213                // Writing through (*ptr).field doesn't reassign ptr itself,
214                // so ptr's constraint should not be cleared.
215                let is_deref = place
216                    .projection
217                    .iter()
218                    .any(|p| matches!(p, ProjectionElem::Deref));
219                if !is_deref {
220                    info.assigned_locals.insert(dest);
221                }
222                match rvalue {
223                    Rvalue::Use(Operand::Constant(c), ..) => {
224                        let typing_env = TypingEnv::post_analysis(tcx, def_id);
225                        let val = match c.const_.ty().kind() {
226                            TyKind::Bool => c
227                                .const_
228                                .try_eval_bool(tcx, typing_env)
229                                .map(|b| if b { 1 } else { 0 }),
230                            TyKind::Int(_) | TyKind::Uint(_) => {
231                                c.const_.try_eval_bits(tcx, typing_env).map(|v| v as usize)
232                            }
233                            _ => None,
234                        };
235                        if let Some(val) = val {
236                            info.constants.insert(dest, val);
237                        }
238                    }
239                    Rvalue::Use(Operand::Copy(src) | Operand::Move(src), ..) => {
240                        let src_local = local_base + src.local.as_usize();
241                        if let Some(field_proj) = first_field_projection(src) {
242                            let encoded = encode_aggregate_field(src_local, field_proj);
243                            field_projection_source.insert(dest, encoded);
244                        }
245                        info.constraint_copies.insert(dest, src_local);
246                    }
247                    Rvalue::Discriminant(rv_place) => {
248                        let src_local = local_base + rv_place.local.as_usize();
249                        disc_info.source_of.insert(dest, src_local);
250                        if !disc_info.variant_count_of.contains_key(&src_local) {
251                            let src_ty = body.local_decls[rv_place.local].ty;
252                            if let TyKind::Adt(adt_def, _) = src_ty.kind() {
253                                let num = adt_def.variants().len();
254                                if num > 0 {
255                                    disc_info.variant_count_of.insert(src_local, num);
256                                }
257                            }
258                        }
259                    }
260                    Rvalue::Aggregate(kind, operands) => {
261                        if let AggregateKind::Adt(_, _, _, _, _) = kind.as_ref() {
262                            let agg_local = local_base + place.local.as_usize();
263                            for (field_idx, operand) in operands.iter().enumerate() {
264                                if let Operand::Copy(src) | Operand::Move(src) = operand {
265                                    let key = encode_aggregate_field(agg_local, field_idx);
266                                    let src_local = local_base + src.local.as_usize();
267                                    aggregate_field_sources.insert(key, src_local);
268                                }
269                            }
270                        }
271                        let discr = match kind.as_ref() {
272                            AggregateKind::Adt(_, variant_idx, _, _, _) => Some(variant_idx.as_usize()),
273                            _ => None,
274                        };
275                        if let Some(discr) = discr {
276                            info.constants.insert(dest, discr);
277                            if !disc_info.variant_count_of.contains_key(&dest) {
278                                let dest_ty = body.local_decls[place.local].ty;
279                                if let TyKind::Adt(adt_def, _) = dest_ty.kind() {
280                                    let num = adt_def.variants().len();
281                                    if num > 0 {
282                                        disc_info.variant_count_of.insert(dest, num);
283                                    }
284                                }
285                            }
286                        }
287                    }
288                    Rvalue::BinaryOp(op, operands) if matches!(op, BinOp::AddWithOverflow) => {
289                        let (lhs, rhs): (&Operand<'_>, &Operand<'_>) =
290                            (&operands.0, &operands.1);
291                        if let Some(lhs_local) = match lhs {
292                            Operand::Copy(l) | Operand::Move(l) if l.projection.is_empty() => {
293                                Some(local_base + l.local.as_usize())
294                            }
295                            _ => None,
296                        } {
297                            let incr = match rhs {
298                                Operand::Constant(c) => {
299                                    let typing_env = TypingEnv::post_analysis(tcx, def_id);
300                                    c.const_.try_eval_bits(tcx, typing_env).map(|v| v as usize)
301                                }
302                                _ => None,
303                            };
304                            if let Some(incr) = incr {
305                                info.increments.insert(dest, (lhs_local, incr));
306                            }
307                        }
308                    }
309                    Rvalue::BinaryOp(op, operands)
310                        if matches!(
311                            op,
312                            BinOp::Lt
313                                | BinOp::Le
314                                | BinOp::Gt
315                                | BinOp::Ge
316                                | BinOp::Eq
317                                | BinOp::Ne
318                                | BinOp::BitAnd
319                        ) =>
320                    {
321                        let (lhs, rhs): (&Operand<'_>, &Operand<'_>) =
322                            (&operands.0, &operands.1);
323                        let lhs_local = match lhs {
324                            Operand::Copy(l) | Operand::Move(l) if l.projection.is_empty() => {
325                                Some(local_base + l.local.as_usize())
326                            }
327                            _ => None,
328                        };
329                        if let Some(lhs_local) = lhs_local {
330                            let rhs_eval = match rhs {
331                                Operand::Constant(c) => {
332                                    let typing_env = TypingEnv::post_analysis(tcx, def_id);
333                                    c.const_
334                                        .try_eval_bits(tcx, typing_env)
335                                        .map(|v| (v as usize, true))
336                                }
337                                Operand::Copy(r) | Operand::Move(r)
338                                    if r.projection.is_empty() =>
339                                {
340                                    Some((local_base + r.local.as_usize(), false))
341                                }
342                                _ => None,
343                            };
344                            let Some((rhs_local, rhs_is_constant)) = rhs_eval else {
345                                continue;
346                            };
347                            info.comparison_sources.insert(
348                                dest,
349                                ComparisonSource {
350                                    op: *op,
351                                    lhs_local,
352                                    rhs_local,
353                                    rhs_is_constant,
354                                },
355                            );
356                            if matches!(op, BinOp::BitAnd)
357                                && matches!(body.local_decls[place.local].ty.kind(), TyKind::Bool)
358                            {
359                                info.and_sources.insert(dest, (lhs_local, rhs_local));
360                            }
361                        }
362                    }
363                    Rvalue::BinaryOp(op, operands) if matches!(op, BinOp::Rem) => {
364                        let (lhs, rhs): (&Operand<'_>, &Operand<'_>) =
365                            (&operands.0, &operands.1);
366                        if let Some(lhs_local) = match lhs {
367                            Operand::Copy(l) | Operand::Move(l) if l.projection.is_empty() => {
368                                Some(local_base + l.local.as_usize())
369                            }
370                            _ => None,
371                        } {
372                            let divisor = match rhs {
373                                Operand::Constant(c) => {
374                                    let typing_env = TypingEnv::post_analysis(tcx, def_id);
375                                    c.const_.try_eval_bits(tcx, typing_env).map(|v| v as usize)
376                                }
377                                _ => None,
378                            };
379                            if let Some(divisor) = divisor {
380                                if divisor != 0 {
381                                    info.remainders.insert(dest, (lhs_local, divisor));
382                                }
383                            }
384                        }
385                    }
386                    Rvalue::UnaryOp(unop, operand) => {
387                        if matches!(unop, UnOp::Not)
388                            && let Operand::Copy(src) | Operand::Move(src) = operand
389                        {
390                            info.negation_sources
391                                .insert(dest, local_base + src.local.as_usize());
392                        }
393                    }
394                    Rvalue::Cast(_, operand, _) => {
395                        if let Operand::Copy(src) | Operand::Move(src) = operand
396                            && matches!(
397                                body.local_decls[place.local].ty.kind(),
398                                TyKind::RawPtr(..) | TyKind::Int(..) | TyKind::Uint(..)
399                            )
400                        {
401                            cast_chains.insert(dest, local_base + src.local.as_usize());
402                        }
403                    }
404                    _ => {} // close match rvalue
405                }
406            }
407        }
408
409        if let Some(terminator) = &bb.terminator {
410            if let TerminatorKind::Call {
411                destination,
412                ref func,
413                ..
414            } = terminator.kind
415            {
416                let name = crate::helpers::mir_utils::call_name(tcx, func);
417                if name.contains("::into_raw")
418                    || (name.contains("::new") && name.contains("Box"))
419                    || name.contains("::as_mut_ptr")
420                    || name.contains("::as_ptr")
421                {
422                    info.known_nonnull_locals
423                        .insert(local_base + destination.local.as_usize());
424                }
425                if name.contains("null_mut") || (name.contains("null") && name.contains("ptr::")) {
426                    info.constants
427                        .insert(local_base + destination.local.as_usize(), 0);
428                }
429            }
430        }
431
432        block_info.push(info);
433    }
434
435    (
436        block_info,
437        disc_info,
438        aggregate_field_sources,
439        field_projection_source,
440        cast_chains,
441    )
442}
443
444impl<'tcx> PathGraph<'tcx> {
445    pub fn new(tcx: TyCtxt<'tcx>, def_id: DefId) -> PathGraph<'tcx> {
446        let body = tcx.optimized_mir(def_id);
447        let basicblocks = &body.basic_blocks;
448        let mut cfg_blocks = Vec::<CfgBlock>::new();
449
450        for i in 0..basicblocks.len() {
451            let bb = &basicblocks[BasicBlock::from(i)];
452            let mut cfg_block = CfgBlock::new(def_id, i, bb.is_cleanup);
453
454            let Some(terminator) = &bb.terminator else {
455                cfg_blocks.push(cfg_block);
456                continue;
457            };
458
459            match terminator.kind.clone() {
460                TerminatorKind::Goto { ref target } => {
461                    cfg_block.add_next(target.as_usize());
462                }
463                TerminatorKind::SwitchInt {
464                    discr: _,
465                    ref targets,
466                } => {
467                    for (_, ref target) in targets.iter() {
468                        cfg_block.add_next(target.as_usize());
469                    }
470                    cfg_block.add_next(targets.otherwise().as_usize());
471                }
472                TerminatorKind::Drop {
473                    place: _,
474                    target,
475                    unwind,
476                    replace: _,
477                    drop: _,
478                    #[cfg(not(rapx_ge_99))]
479                        async_fut: _,
480                } => {
481                    cfg_block.add_next(target.as_usize());
482                    if let UnwindAction::Cleanup(target) = unwind {
483                        cfg_block.add_next(target.as_usize());
484                    }
485                }
486                TerminatorKind::Call {
487                    ref target,
488                    ref unwind,
489                    ..
490                } => {
491                    if let Some(tt) = target {
492                        cfg_block.add_next(tt.as_usize());
493                    }
494                    if let UnwindAction::Cleanup(tt) = unwind {
495                        cfg_block.add_next(tt.as_usize());
496                    }
497                }
498                TerminatorKind::Assert {
499                    cond: _,
500                    expected: _,
501                    msg: _,
502                    ref target,
503                    ref unwind,
504                } => {
505                    cfg_block.add_next(target.as_usize());
506                    if let UnwindAction::Cleanup(target) = unwind {
507                        cfg_block.add_next(target.as_usize());
508                    }
509                }
510                TerminatorKind::Yield {
511                    value: _,
512                    ref resume,
513                    resume_arg: _,
514                    ref drop,
515                } => {
516                    cfg_block.add_next(resume.as_usize());
517                    if let Some(target) = drop {
518                        cfg_block.add_next(target.as_usize());
519                    }
520                }
521                TerminatorKind::FalseEdge {
522                    ref real_target,
523                    imaginary_target: _,
524                } => {
525                    cfg_block.add_next(real_target.as_usize());
526                }
527                TerminatorKind::FalseUnwind {
528                    ref real_target,
529                    unwind: _,
530                } => {
531                    cfg_block.add_next(real_target.as_usize());
532                }
533                TerminatorKind::InlineAsm {
534                    template: _,
535                    operands: _,
536                    options: _,
537                    line_spans: _,
538                    ref unwind,
539                    targets,
540                    asm_macro: _,
541                } => {
542                    for target in targets {
543                        cfg_block.add_next(target.as_usize());
544                    }
545                    if let UnwindAction::Cleanup(target) = unwind {
546                        cfg_block.add_next(target.as_usize());
547                    }
548                }
549                _ => {}
550            }
551
552            cfg_blocks.push(cfg_block);
553        }
554
555        let cfg = ControlFlowGraph::new(def_id, tcx, cfg_blocks);
556        let (block_info, disc_info, aggregate_field_sources, field_projection_source, cast_chains) =
557            build_function_info(tcx, def_id, 0);
558
559        let mut local_bases = FxHashMap::default();
560        local_bases.insert(def_id, 0);
561
562        PathGraph {
563            cfg,
564            block_info,
565            disc_info,
566            aggregate_field_sources,
567            field_projection_source,
568            cast_chains,
569            inline_bindings: FxHashMap::default(),
570            inline_parents: FxHashMap::default(),
571            inlined_call_blocks: FxHashSet::default(),
572            local_bases,
573            next_local_base: body.local_decls.len(),
574        }
575    }
576
577    /// Inline the CFG of inlinable callees into this graph so that path
578    /// enumeration covers the callee's branches.
579    ///
580    /// Inlining is recursive *within the local crate*: a local callee's own
581    /// calls are inlined too. Cross-crate callees are inlined only at the level
582    /// where a local function calls them — their bodies are not recursed into,
583    /// so the standard library's internal branchy precondition machinery
584    /// (`is_aligned_to`, `ub_checks`, …) is not pulled in, which would blow up
585    /// path enumeration with branches that have no constraint tracking.
586    ///
587    /// Each callee is otherwise subject to the same shape constraints as
588    /// before: cross-crate callees with MIR are always inlined, and a local
589    /// callee is inlined only when it is small (at most
590    /// [`LOCAL_INLINE_BLOCK_LIMIT`](crate::limit::LOCAL_INLINE_BLOCK_LIMIT) basic
591    /// blocks), so branch-free accessors
592    /// (`get`, `count_ones`, …) also land in the CFG and their field
593    /// provenance is reconstructed element-by-element. Intrinsics
594    /// have no MIR; callees with a builtin model are kept opaque because their
595    /// summary is more precise than their body.
596    ///
597    /// Recursion is bounded by an `expanded` set so that (mutually) recursive
598    /// local functions do not grow the CFG without bound — a back-edge to an
599    /// already-inlined callee is left as an ordinary call edge for the VM's
600    /// fallback handling.
601    ///
602    /// Opt-in — callers that assume a single-function block space (e.g. alias
603    /// analysis) must not call this.
604    pub fn inline_callees(&mut self) {
605        let tcx = self.tcx();
606        let caller_def_id = self.def_id();
607
608        let mut expanded: FxHashSet<DefId> = FxHashSet::default();
609        expanded.insert(caller_def_id);
610
611        loop {
612            let mut pending: Vec<(usize, DefId)> = Vec::new();
613            for i in 0..self.cfg.blocks.len() {
614                // Only recurse into *local* functions: cross-crate bodies are
615                // inlined but never scanned for further calls.
616                if self.cfg.block(i).def_id.as_local().is_none() {
617                    continue;
618                }
619                let Some(term) = self.terminator(i) else {
620                    continue;
621                };
622                let TerminatorKind::Call { func, target, .. } = &term.kind else {
623                    continue;
624                };
625                // A diverging call (`panic!` / `unreachable!`) has no normal
626                // successor to reconnect through the inlined body, so it is
627                // never inlined.
628                if target.is_none() {
629                    continue;
630                }
631                // Cross-crate callees keep their raw trait-method def_id (trait
632                // methods have no MIR and are left to builtin models). Local
633                // callees are resolved to the concrete impl so a user trait
634                // method (e.g. `<MyIter as Iterator>::next`) inlines against its
635                // actual MIR.
636                let Some(base_callee) = crate::helpers::mir_utils::dep_callee_def_id(func) else {
637                    continue;
638                };
639                let cross_crate = base_callee.as_local().is_none();
640                let callee = if cross_crate {
641                    base_callee
642                } else {
643                    crate::helpers::mir_utils::dep_callee_resolved_def_id(
644                        tcx,
645                        self.cfg.block(i).def_id,
646                        func,
647                    )
648                    .unwrap_or(base_callee)
649                };
650                let has_fn_sim =
651                    crate::verify::call_summary::builtin_models::is_modeled(Some(callee));
652                // A couple of helpers stay opaque because the VM's
653                // interprocedural execution is more precise than their inlined
654                // body:
655                // - `as_ptr`/`as_mut_ptr` (local re-implementations) lose the
656                //   provenance that `eff_alias_ptr`/`handle_callee_entry`
657                //   otherwise reconstruct from the argument.
658                let is_container_as_ptr =
659                    crate::verify::api_classify::is_container_as_ptr(tcx, callee);
660                if !tcx.is_mir_available(callee)
661                    || expanded.contains(&callee)
662                    || has_fn_sim
663                    || is_container_as_ptr
664                {
665                    continue;
666                }
667                // A local callee is inlined only when it is small (a transitive
668                // shape bound that keeps path enumeration tractable; see
669                // `LOCAL_INLINE_BLOCK_LIMIT`). Branch-free accessors are no
670                // longer excluded: their bodies land in the CFG and the backward
671                // slicer/VM reconstruct field provenance through the inlined
672                // call chain.
673                let small_local = !cross_crate && {
674                    let body = tcx.optimized_mir(callee);
675                    body.basic_blocks.len() <= LOCAL_INLINE_BLOCK_LIMIT
676                };
677                if cross_crate || small_local {
678                    pending.push((i, callee));
679                }
680            }
681
682            if pending.is_empty() {
683                break;
684            }
685
686            // Mark every pending callee as expanded *before* inlining so a
687            // callee that (transitively) calls itself or another pending callee
688            // is not inlined again on a later iteration.
689            for (_, callee) in &pending {
690                expanded.insert(*callee);
691            }
692
693            for (caller_idx, callee) in pending {
694                self.inline_one(caller_idx, callee);
695            }
696        }
697    }
698
699    /// Inline a single callee into `caller_idx`, reconnecting the caller's
700    /// normal successor edge through the callee body.
701    fn inline_one(&mut self, caller_idx: usize, callee: DefId) {
702        let tcx = self.tcx();
703        // The caller block may itself be an inlined callee (nested inlining), so
704        // its MIR successor indices must be shifted into the global block space.
705        let caller_base = caller_idx - self.cfg.block(caller_idx).local_index;
706        let (caller_target, caller_unwind, arg_locals, dest_local) = match self.terminator(caller_idx)
707        {
708            Some(term) => match &term.kind {
709                TerminatorKind::Call {
710                    target,
711                    unwind,
712                    args,
713                    destination,
714                    ..
715                } => {
716                    let target = target.map(|t| caller_base + t.as_usize());
717                    let unwind = match unwind {
718                        UnwindAction::Cleanup(t) => Some(caller_base + t.as_usize()),
719                        _ => None,
720                    };
721                    let arg_locals: Vec<usize> = args
722                        .iter()
723                        .filter_map(|a| a.node.place().map(|p| p.local.as_usize()))
724                        .collect();
725                    (target, unwind, arg_locals, destination.local.as_usize())
726                }
727                _ => (None, None, Vec::new(), 0),
728            },
729            None => (None, None, Vec::new(), 0),
730        };
731        let Some(target) = caller_target else {
732            return;
733        };
734
735        let body = tcx.optimized_mir(callee);
736        let base = self.cfg.blocks.len();
737        let block_count = body.basic_blocks.len();
738
739        // Build the callee's constant/discriminant metadata under its own local
740        // namespace so its constraint tracking doesn't clash with the caller's.
741        let local_base = self.assign_local_base(callee);
742        let (callee_info, callee_disc, callee_agg, callee_fproj, callee_cast) =
743            build_function_info(tcx, callee, local_base);
744
745        for i in 0..block_count {
746            let bb = &body.basic_blocks[BasicBlock::from(i)];
747            let mut cb = CfgBlock {
748                index: base + i,
749                def_id: callee,
750                local_index: i,
751                is_cleanup: bb.is_cleanup,
752                next: FxHashSet::default(),
753                scc: SccInfo::new(base + i),
754            };
755            if let Some(term) = &bb.terminator {
756                match &term.kind {
757                    TerminatorKind::Goto { target: t } => {
758                        cb.add_next(base + t.as_usize());
759                    }
760                    TerminatorKind::SwitchInt { targets, .. } => {
761                        for (_, t) in targets.iter() {
762                            cb.add_next(base + t.as_usize());
763                        }
764                        cb.add_next(base + targets.otherwise().as_usize());
765                    }
766                    TerminatorKind::Return => {
767                        cb.add_next(target);
768                    }
769                    TerminatorKind::UnwindResume => {
770                        // A callee that unwinds re-enters the caller at its
771                        // unwind/cleanup block (the caller's `Cleanup` target of
772                        // the inlined call), never at the normal return target.
773                        if let Some(u) = caller_unwind {
774                            cb.add_next(u);
775                        }
776                    }
777                    TerminatorKind::Call {
778                        target: t,
779                        unwind,
780                        ..
781                    } => {
782                        if let Some(t) = t {
783                            cb.add_next(base + t.as_usize());
784                        }
785                        if let UnwindAction::Cleanup(tt) = unwind {
786                            cb.add_next(base + tt.as_usize());
787                        }
788                    }
789                    TerminatorKind::Drop {
790                        target: t, unwind, ..
791                    } => {
792                        cb.add_next(base + t.as_usize());
793                        if let UnwindAction::Cleanup(tt) = unwind {
794                            cb.add_next(base + tt.as_usize());
795                        }
796                    }
797                    TerminatorKind::Assert {
798                        target: t,
799                        unwind,
800                        ..
801                    } => {
802                        cb.add_next(base + t.as_usize());
803                        if let UnwindAction::Cleanup(tt) = unwind {
804                            cb.add_next(base + tt.as_usize());
805                        }
806                    }
807                    TerminatorKind::FalseEdge { real_target, .. }
808                    | TerminatorKind::FalseUnwind { real_target, .. } => {
809                        cb.add_next(base + real_target.as_usize());
810                    }
811                    _ => {}
812                }
813            }
814            self.cfg.blocks.push(cb);
815        }
816
817        // Append the callee's per-block metadata and merge its global source
818        // maps (all keyed in the callee's own local namespace).
819        self.block_info.extend(callee_info);
820        self.disc_info.source_of.extend(callee_disc.source_of);
821        self.disc_info.variant_count_of.extend(callee_disc.variant_count_of);
822        self.aggregate_field_sources.extend(callee_agg);
823        self.field_projection_source.extend(callee_fproj);
824        self.cast_chains.extend(callee_cast);
825
826        // Record the argument binding for the callee entry.
827        self.inline_bindings.insert(
828            base,
829            InlineBinding {
830                arg_locals,
831                dest_local,
832            },
833        );
834        self.inline_parents
835            .insert(base, self.cfg.block(caller_idx).def_id);
836        self.inlined_call_blocks.insert(caller_idx);
837
838        // Reconnect the caller's normal successor edge through the callee entry.
839        let caller_block = self.cfg.block_mut(caller_idx);
840        caller_block.next.remove(&target);
841        caller_block.next.insert(base);
842    }
843
844    pub fn find_scc(&mut self) {
845        self.cfg.find_scc();
846        self.populate_all_child_sccs();
847    }
848
849    pub fn def_id(&self) -> DefId {
850        self.cfg.def_id
851    }
852
853    pub fn tcx(&self) -> TyCtxt<'tcx> {
854        self.cfg.tcx
855    }
856
857    pub fn cfg_block(&self, index: usize) -> &CfgBlock {
858        self.cfg.block(index)
859    }
860
861    pub fn cfg_block_mut(&mut self, index: usize) -> &mut CfgBlock {
862        self.cfg.block_mut(index)
863    }
864
865    /// Retrieve the MIR terminator for the block at `index` on demand.
866    pub fn terminator(&self, index: usize) -> Option<&Terminator<'tcx>> {
867        self.cfg.terminator(index)
868    }
869
870    pub fn is_cleanup_block(&self, index: usize) -> bool {
871        self.cfg
872            .blocks
873            .get(index)
874            .map(|b| b.is_cleanup)
875            .unwrap_or(false)
876    }
877
878    /// The local-index base of `def_id`'s MIR locals in the flat constraint
879    /// maps. The root caller is base `0`; inlined callees get a fresh base.
880    fn local_base_of(&self, def_id: DefId) -> usize {
881        self.local_bases.get(&def_id).copied().unwrap_or(0)
882    }
883
884    /// Remap `def_id`'s MIR local `local` into the global constraint namespace.
885    fn remap_local(&self, def_id: DefId, local: usize) -> usize {
886        self.local_base_of(def_id) + local
887    }
888
889    /// Assign a fresh local-namespace base for `def_id` (or return its existing
890    /// base if the function has already been inlined).
891    fn assign_local_base(&mut self, def_id: DefId) -> usize {
892        if let Some(&base) = self.local_bases.get(&def_id) {
893            return base;
894        }
895        let base = self.next_local_base;
896        let local_count = self.tcx().optimized_mir(def_id).local_decls.len();
897        self.local_bases.insert(def_id, base);
898        self.next_local_base = base + local_count;
899        base
900    }
901
902    /// Get the number of variants for a constraint local (in `def_id`'s local
903    /// namespace). First checks the pre-populated `variant_count_of` hashmap,
904    /// then falls back to the local's declared type (for ADT locals that gained
905    /// their type through field projections in nested destructuring patterns
906    /// rather than explicit construction).
907    fn get_variant_count(&self, local: usize, def_id: DefId) -> Option<usize> {
908        if let Some(&count) = self.disc_info.variant_count_of.get(&local) {
909            return Some(count);
910        }
911        let body = self.cfg.tcx.optimized_mir(def_id);
912        let original = local - self.local_base_of(def_id);
913        let mut ty = body.local_decls[Local::from_usize(original)].ty;
914        while let TyKind::Ref(_, inner_ty, _) | TyKind::RawPtr(inner_ty, _) = ty.kind() {
915            ty = *inner_ty;
916        }
917        match ty.kind() {
918            TyKind::Adt(adt_def, _) if adt_def.is_enum() => Some(adt_def.variants().len()),
919            _ => None,
920        }
921    }
922
923    /// Check a single transition `cur -> next` for reachability and update
924    /// discriminant constraints. Returns `false` if the transition is
925    /// provably unreachable.
926    pub fn check_transition(
927        &self,
928        cur: usize,
929        next: usize,
930        constraints: &mut FxHashMap<usize, usize>,
931    ) -> bool {
932        if cur >= self.cfg.blocks.len() || next >= self.cfg.blocks.len() {
933            return false;
934        }
935
936        if let Some(info) = self.block_info.get(cur) {
937            for local in &info.assigned_locals {
938                if let Some(&src) = info.constraint_copies.get(local) {
939                    if let Some(val) = self.resolve_local_value(src, constraints) {
940                        constraints.insert(*local, val);
941                        continue;
942                    }
943                    if let Some(&dst_val) = constraints.get(local) {
944                        constraints.insert(src, dst_val);
945                        constraints.insert(*local, dst_val);
946                        continue;
947                    }
948                }
949                if let Some(&val) = info.constants.get(local) {
950                    constraints.insert(*local, val);
951                    continue;
952                }
953                constraints.remove(local);
954            }
955            for local in &info.known_nonnull_locals {
956                constraints.insert(*local, usize::MAX);
957            }
958        }
959
960        // Also clear constraints for locals assigned by the terminator
961        // (e.g. _13 = Iterator::next() in a Call terminator). The block's
962        // assigned_locals only covers statement-level assignments, so
963        // terminator-side assignments are handled here.
964        if let Some(terminator) = self.terminator(cur) {
965            let local_base = self.local_base_of(self.cfg.block(cur).def_id);
966            let assigned = match &terminator.kind {
967                TerminatorKind::Call { destination, .. } => {
968                    Some(local_base + destination.local.as_usize())
969                }
970                TerminatorKind::Yield { resume_arg, .. } => {
971                    Some(local_base + resume_arg.local.as_usize())
972                }
973                _ => None,
974            };
975            if let Some(local) = assigned {
976                // Propagate known nullness from BlockConstantInfo.
977                if let Some(block_info) = self.block_info.get(cur) {
978                    if let Some(&val) = block_info.constants.get(&local) {
979                        constraints.insert(local, val);
980                    } else if block_info.known_nonnull_locals.contains(&local) {
981                        constraints.insert(local, usize::MAX);
982                    } else {
983                        constraints.remove(&local);
984                    }
985                } else {
986                    constraints.remove(&local);
987                }
988            }
989        }
990
991        let successors = &self.cfg.block(cur).next;
992        if !successors.contains(&next) {
993            if !self.is_unwind_target(cur, next) {
994                return false;
995            }
996        }
997
998        if !self.check_switch_transition(cur, next, constraints) {
999            return false;
1000        }
1001
1002        if !self.check_assert_transition(cur, next, constraints) {
1003            return false;
1004        }
1005
1006        true
1007    }
1008
1009    fn check_assert_transition(
1010        &self,
1011        cur: usize,
1012        next: usize,
1013        constraints: &FxHashMap<usize, usize>,
1014    ) -> bool {
1015        let Some(terminator) = self.cfg.terminator(cur) else {
1016            return true;
1017        };
1018        let TerminatorKind::Assert { cond, target, .. } = &terminator.kind else {
1019            return true;
1020        };
1021        let base = cur - self.cfg.block(cur).local_index;
1022        if next != base + target.as_usize() {
1023            return true;
1024        }
1025        let cond_local = match cond {
1026            Operand::Copy(p) | Operand::Move(p) => {
1027                self.remap_local(self.cfg.block(cur).def_id, p.local.as_usize())
1028            }
1029            Operand::Constant(c) => {
1030                let typing_env =
1031                    rustc_middle::ty::TypingEnv::post_analysis(self.cfg.tcx, self.cfg.def_id);
1032                return c
1033                    .const_
1034                    .try_eval_bool(self.cfg.tcx, typing_env)
1035                    .unwrap_or(true);
1036            }
1037            #[cfg(rapx_ge_95)]
1038            Operand::RuntimeChecks(_) => return true,
1039        };
1040        self.resolve_bool_local(cond_local, constraints)
1041            .is_none_or(|v| v == 1)
1042    }
1043
1044    fn resolve_simple_bool(
1045        &self,
1046        local: usize,
1047        constraints: &FxHashMap<usize, usize>,
1048    ) -> Option<usize> {
1049        if let Some(&val) = constraints.get(&local)
1050            && val <= 1
1051        {
1052            return Some(val);
1053        }
1054        for info in &self.block_info {
1055            if let Some(&val) = info.constants.get(&local)
1056                && val <= 1
1057            {
1058                return Some(val);
1059            }
1060            if let Some(cmp) = info.comparison_sources.get(&local) {
1061                if matches!(cmp.op, BinOp::Eq | BinOp::Ne) {
1062                    let is_eq = matches!(cmp.op, BinOp::Eq);
1063                    if cmp.rhs_is_constant {
1064                        if let Some(lhs_val) = self.resolve_local_value(cmp.lhs_local, constraints)
1065                        {
1066                            return Some(if is_eq {
1067                                if lhs_val == cmp.rhs_local { 1 } else { 0 }
1068                            } else {
1069                                if lhs_val != cmp.rhs_local { 1 } else { 0 }
1070                            });
1071                        }
1072                    } else {
1073                        let lhs_val = self.resolve_local_value(cmp.lhs_local, constraints);
1074                        let rhs_val = self.resolve_local_value(cmp.rhs_local, constraints);
1075                        if let Some(lhs_val) = lhs_val {
1076                            if let Some(rhs_val) = rhs_val {
1077                                return Some(if is_eq {
1078                                    if lhs_val == rhs_val { 1 } else { 0 }
1079                                } else {
1080                                    if lhs_val != rhs_val { 1 } else { 0 }
1081                                });
1082                            }
1083                        } else if let Some(rhs_val) = rhs_val {
1084                            return Some(if is_eq {
1085                                if cmp.lhs_local == rhs_val { 1 } else { 0 }
1086                            } else {
1087                                if cmp.lhs_local != rhs_val { 1 } else { 0 }
1088                            });
1089                        }
1090                    }
1091                }
1092            }
1093        }
1094        None
1095    }
1096
1097    fn resolve_bool_local(
1098        &self,
1099        local: usize,
1100        constraints: &FxHashMap<usize, usize>,
1101    ) -> Option<usize> {
1102        let mut stack = vec![(local, false)];
1103        let mut seen = FxHashSet::default();
1104        while let Some((cur, negated)) = stack.pop() {
1105            let key = if negated { cur | (1 << 31) } else { cur };
1106            if !seen.insert(key) {
1107                continue;
1108            }
1109            if let Some(v) = self.resolve_simple_bool(cur, constraints) {
1110                return Some(if negated { 1 - v } else { v });
1111            }
1112            for info in &self.block_info {
1113                if let Some(&src) = info.constraint_copies.get(&cur) {
1114                    stack.push((src, negated));
1115                }
1116                if let Some(&src) = info.negation_sources.get(&cur) {
1117                    stack.push((src, !negated));
1118                }
1119                if let Some(&(lhs, rhs)) = info.and_sources.get(&cur) {
1120                    let a = self.resolve_simple_bool(lhs, constraints);
1121                    let b = self.resolve_simple_bool(rhs, constraints);
1122                    if let (Some(a), Some(b)) = (a, b) {
1123                        let r = if a == 1 && b == 1 { 1 } else { 0 };
1124                        return Some(if negated { 1 - r } else { r });
1125                    }
1126                }
1127            }
1128            if let Some(&src) = self.cast_chains.get(&cur) {
1129                stack.push((src, negated));
1130            }
1131        }
1132        None
1133    }
1134
1135    /// Check whether `cur → next` is a valid `SwitchInt` transition given
1136    /// current discriminant constraints. Returns `false` when the transition
1137    /// contradicts a known discriminant value. Also records newly learned
1138    /// constraints from the taken branch into `constraints`.
1139    fn check_switch_transition(
1140        &self,
1141        cur: usize,
1142        next: usize,
1143        constraints: &mut FxHashMap<usize, usize>,
1144    ) -> bool {
1145        let Some(terminator) = self.cfg.terminator(cur) else {
1146            return true;
1147        };
1148        // Base offset of the function owning `cur` (0 for the caller, the
1149        // callee's global start for inlined blocks). Switch targets are local
1150        // MIR block indices, so they must be shifted into the global space.
1151        let base = cur - self.cfg.block(cur).local_index;
1152
1153        match &terminator.kind {
1154            TerminatorKind::SwitchInt { discr, targets } => {
1155                // The discriminant local is remapped into its function's
1156                // namespace so it matches `disc_info`/`constraints` keys.
1157                let discr_local = discr
1158                    .place()
1159                    .map(|p| self.remap_local(self.cfg.block(cur).def_id, p.local.as_usize()));
1160                let constraint_local = discr_local
1161                    .and_then(|l| self.disc_info.source_of.get(&l).copied())
1162                    .or(discr_local);
1163
1164                // Collect all possible successor blocks for this switch.
1165                let all_targets: FxHashSet<usize> = targets
1166                    .iter()
1167                    .map(|(_, bb)| base + bb.as_usize())
1168                    .chain(std::iter::once(base + targets.otherwise().as_usize()))
1169                    .collect();
1170
1171                if !all_targets.contains(&next) {
1172                    return false;
1173                }
1174
1175                // Try to evaluate a concrete constant for the discriminant.
1176                let const_val = match discr {
1177                    Operand::Constant(c) => c
1178                        .const_
1179                        .try_eval_target_usize(
1180                            self.cfg.tcx,
1181                            TypingEnv::post_analysis(self.cfg.tcx, self.cfg.def_id),
1182                        )
1183                        .map(|v| v as usize),
1184                    _ => None,
1185                };
1186
1187                if let Some(val) = const_val {
1188                    // Discriminant is a literal constant — only one target is
1189                    // reachable.
1190                    let expected = resolve_switch_target(targets, val as u128);
1191                    if next != base + expected {
1192                        return false;
1193                    }
1194                    if let Some(local) = constraint_local {
1195                        constraints.insert(local, val);
1196                    }
1197                    return true;
1198                }
1199
1200                if let Some(local) = constraint_local {
1201                    if let Some(&known_val) = constraints.get(&local) {
1202                        let expected = resolve_switch_target(targets, known_val as u128);
1203                        if next != base + expected {
1204                            return false;
1205                        }
1206                        return true;
1207                    }
1208                }
1209
1210                // Try to infer the discriminant from a comparison source
1211                // (e.g. `_X = Ne(ptr, 0)`) when we know whether `ptr` is null.
1212                if let Some(discr_local) = discr_local
1213                    && let Some(info) = self.block_info.get(cur)
1214                    && let Some(cmp) = info.comparison_sources.get(&discr_local)
1215                    && matches!(cmp.op, BinOp::Ne | BinOp::Eq)
1216                {
1217                    let is_ne = matches!(cmp.op, BinOp::Ne);
1218                    let pointer_is_nonnull =
1219                        self.local_is_known_nonnull(constraints, cmp.lhs_local);
1220                    let pointer_is_null = self.local_is_known_null(constraints, cmp.lhs_local);
1221                    if pointer_is_nonnull {
1222                        let expected_val = if is_ne { 1 } else { 0 };
1223                        let expected = resolve_switch_target(targets, expected_val);
1224                        let val = expected_val as usize;
1225                        constraints.insert(discr_local, val);
1226                        if let Some(local) = constraint_local {
1227                            constraints.insert(local, val);
1228                        }
1229                        if next != base + expected {
1230                            return false;
1231                        }
1232                        return true;
1233                    }
1234                    if pointer_is_null {
1235                        let expected_val = if is_ne { 0 } else { 1 };
1236                        let expected = resolve_switch_target(targets, expected_val);
1237                        let val = expected_val as usize;
1238                        constraints.insert(discr_local, val);
1239                        if let Some(local) = constraint_local {
1240                            constraints.insert(local, val);
1241                        }
1242                        if next != base + expected {
1243                            return false;
1244                        }
1245                        return true;
1246                    }
1247
1248                    // General Eq/Ne integer comparison: resolve both operands
1249                    // and prune the infeasible branch when both are known.
1250                    let is_eq = matches!(cmp.op, BinOp::Eq);
1251                    let lhs_val = self.resolve_local_value(cmp.lhs_local, constraints);
1252                    let rhs_val = if cmp.rhs_is_constant {
1253                        Some(cmp.rhs_local)
1254                    } else {
1255                        self.resolve_local_value(cmp.rhs_local, constraints)
1256                    };
1257                    if let (Some(lhs), Some(rhs)) = (lhs_val, rhs_val) {
1258                        let val = if is_eq {
1259                            if lhs == rhs { 1 } else { 0 }
1260                        } else {
1261                            if lhs != rhs { 1 } else { 0 }
1262                        };
1263                        let expected = resolve_switch_target(targets, val as u128);
1264                        if let Some(local) = constraint_local {
1265                            constraints.insert(local, val);
1266                        }
1267                        if next != base + expected {
1268                            return false;
1269                        }
1270                        return true;
1271                    }
1272                }
1273
1274                // Try to resolve a boolean comparison with concrete-value
1275                // reasoning (handles Lt/Le/Gt/Ge with fully known operands).
1276                // Only filter when the discriminant resolves to a definite value
1277                // from a comparison with BOTH operands as known constants
1278                // (not derived via increments, which may be imprecise across
1279                // SCC boundaries).
1280                if let Some(discr_local) = discr_local
1281                    && let Some(info) = self.block_info.get(cur)
1282                    && let Some(cmp) = info.comparison_sources.get(&discr_local)
1283                    && matches!(cmp.op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge)
1284                {
1285                    // Resolve LHS directly from constants (no increment chains)
1286                    let lhs_val = self.resolve_local_value_direct(cmp.lhs_local, constraints);
1287                    let rhs_val = if cmp.rhs_is_constant {
1288                        Some(cmp.rhs_local)
1289                    } else {
1290                        self.resolve_local_value_direct(cmp.rhs_local, constraints)
1291                    };
1292                    if let (Some(lhs), Some(rhs)) = (lhs_val, rhs_val) {
1293                        let val = match cmp.op {
1294                            BinOp::Lt => {
1295                                if lhs < rhs {
1296                                    1
1297                                } else {
1298                                    0
1299                                }
1300                            }
1301                            BinOp::Le => {
1302                                if lhs <= rhs {
1303                                    1
1304                                } else {
1305                                    0
1306                                }
1307                            }
1308                            BinOp::Gt => {
1309                                if lhs > rhs {
1310                                    1
1311                                } else {
1312                                    0
1313                                }
1314                            }
1315                            BinOp::Ge => {
1316                                if lhs >= rhs {
1317                                    1
1318                                } else {
1319                                    0
1320                                }
1321                            }
1322                            _ => unreachable!(),
1323                        };
1324                        let expected = resolve_switch_target(targets, val as u128);
1325                        if let Some(local) = constraint_local {
1326                            constraints.insert(local, val);
1327                        }
1328                        if next != base + expected {
1329                            return false;
1330                        }
1331                        return true;
1332                    }
1333                }
1334
1335                // No prior constraint — conservatively allow any valid target
1336                // and record the newly learned constraint from the taken branch.
1337                if next == targets.otherwise().as_usize() {
1338                    if let Some(local) = constraint_local {
1339                        if let Some(num_variants) =
1340                            self.get_variant_count(local, self.cfg.block(cur).def_id)
1341                        {
1342                            let all_covered = (0..num_variants)
1343                                .all(|v| targets.iter().any(|(tv, _)| tv == v as u128));
1344                            if all_covered {
1345                                return false;
1346                            }
1347                        }
1348                    }
1349                }
1350
1351                self.learn_constraint_with_backprop(
1352                    cur,
1353                    constraint_local,
1354                    targets,
1355                    next,
1356                    constraints,
1357                );
1358
1359                true
1360            }
1361            _ => true,
1362        }
1363    }
1364
1365    /// After learning a constraint for a discriminant local, propagate the
1366    /// constraint backward through the copy chain so that source locals also
1367    /// receive the value. This prevents losing track of the constraint when
1368    /// the destination temporary is reassigned on loop back-edges.
1369    fn learn_constraint_with_backprop(
1370        &self,
1371        cur: usize,
1372        constraint_local: Option<usize>,
1373        targets: &SwitchTargets,
1374        next: usize,
1375        constraints: &mut FxHashMap<usize, usize>,
1376    ) {
1377        let Some(local) = constraint_local else {
1378            return;
1379        };
1380        let Some((val, _)) = targets.iter().find(|(_, bb)| bb.as_usize() == next) else {
1381            if let Some(inferred) = self.infer_otherwise_value(cur, targets, local) {
1382                constraints.insert(local, inferred);
1383                self.backprop_constraint(cur, local, inferred, constraints);
1384            }
1385            return;
1386        };
1387        let val = val as usize;
1388        constraints.insert(local, val);
1389        self.backprop_constraint(cur, local, val, constraints);
1390    }
1391
1392    fn backprop_constraint(
1393        &self,
1394        cur: usize,
1395        local: usize,
1396        val: usize,
1397        constraints: &mut FxHashMap<usize, usize>,
1398    ) {
1399        let Some(info) = self.block_info.get(cur) else {
1400            return;
1401        };
1402        let mut current = local;
1403        while let Some(&src) = info.constraint_copies.get(&current) {
1404            if current == src {
1405                break;
1406            }
1407            constraints.insert(src, val);
1408            current = src;
1409        }
1410    }
1411
1412    /// Recursively resolve a local's value through constraint copies,
1413    /// cast chains, field projections, and aggregate sources (global maps).
1414    /// Like `resolve_local_value` but does NOT follow increment chains.
1415    /// Used in conservative path filtering where we need high confidence.
1416    fn resolve_local_value_direct(
1417        &self,
1418        local: usize,
1419        constraints: &FxHashMap<usize, usize>,
1420    ) -> Option<usize> {
1421        let mut stack = vec![local];
1422        let mut seen = FxHashSet::default();
1423        while let Some(cur) = stack.pop() {
1424            if !seen.insert(cur) {
1425                continue;
1426            }
1427            if let Some(&val) = constraints.get(&cur) {
1428                if val != usize::MAX {
1429                    return Some(val);
1430                }
1431            }
1432            for info in &self.block_info {
1433                if let Some(&src) = info.constraint_copies.get(&cur) {
1434                    stack.push(src);
1435                }
1436                if let Some(&val) = info.constants.get(&cur) {
1437                    return Some(val);
1438                }
1439            }
1440            if let Some(&cast_src) = self.cast_chains.get(&cur) {
1441                stack.push(cast_src);
1442            }
1443        }
1444        None
1445    }
1446
1447    fn resolve_local_value(
1448        &self,
1449        local: usize,
1450        constraints: &FxHashMap<usize, usize>,
1451    ) -> Option<usize> {
1452        let mut stack = vec![(local, 0isize)];
1453        let mut seen = FxHashSet::default();
1454        while let Some((cur, offset)) = stack.pop() {
1455            if !seen.insert(cur) {
1456                continue;
1457            }
1458            if let Some(&val) = constraints.get(&cur) {
1459                if val == usize::MAX {
1460                    // Sentinel for known-nonnull; not a concrete integer value.
1461                } else if offset >= 0 {
1462                    return Some(val + offset as usize);
1463                } else {
1464                    return val.checked_sub((-offset) as usize);
1465                }
1466            }
1467            // Follow constraint copies in any block (per-block metadata).
1468            for info in &self.block_info {
1469                if let Some(&src) = info.constraint_copies.get(&cur) {
1470                    stack.push((src, offset));
1471                }
1472                if let Some(&val) = info.constants.get(&cur) {
1473                    if offset >= 0 {
1474                        return Some(val + offset as usize);
1475                    } else {
1476                        return val.checked_sub((-offset) as usize);
1477                    }
1478                }
1479                // Follow increments: if cur = src + incr, the effective value
1480                // of cur is value_of(src) + incr + offset.
1481                if let Some(&(incr_src, incr_amt)) = info.increments.get(&cur) {
1482                    stack.push((incr_src, offset + incr_amt as isize));
1483                }
1484                // Follow remainder: cur = rem_src % rem_div.
1485                // Resolve rem_src recursively, compute remainder, then apply offset.
1486                if let Some(&(rem_src, rem_div)) = info.remainders.get(&cur) {
1487                    if let Some(src_val) = self.resolve_local_value(rem_src, constraints) {
1488                        let rem = src_val % rem_div;
1489                        let result = if offset >= 0 {
1490                            Some(rem + offset as usize)
1491                        } else {
1492                            rem.checked_sub((-offset) as usize)
1493                        };
1494                        if let Some(v) = result {
1495                            return Some(v);
1496                        }
1497                    }
1498                }
1499            }
1500            // Follow global cast chains.
1501            if let Some(&cast_src) = self.cast_chains.get(&cur) {
1502                stack.push((cast_src, offset));
1503            }
1504            // Follow field projection -> aggregate source.
1505            if let Some(&encoded) = self.field_projection_source.get(&cur) {
1506                if let Some((agg_local, field_idx)) = decode_aggregate_field(encoded) {
1507                    let key = encode_aggregate_field(agg_local, field_idx);
1508                    if let Some(&source) = self.aggregate_field_sources.get(&key) {
1509                        stack.push((source, offset));
1510                    }
1511                }
1512            }
1513        }
1514        None
1515    }
1516
1517    /// For the "otherwise" branch of a `SwitchInt`, try to infer the single
1518    /// concrete value that the discriminant must have (because all other
1519    /// possible values are covered by explicit targets).
1520    fn infer_otherwise_value(
1521        &self,
1522        cur: usize,
1523        targets: &SwitchTargets,
1524        discr_local: usize,
1525    ) -> Option<usize> {
1526        let body = self.cfg.tcx.optimized_mir(self.cfg.block(cur).def_id);
1527        let def_id = self.cfg.block(cur).def_id;
1528        let original = discr_local - self.local_base_of(def_id);
1529        let mut discr_ty = body.local_decls[Local::from_usize(original)].ty;
1530        while let TyKind::Ref(_, inner, _) | TyKind::RawPtr(inner, _) = discr_ty.kind() {
1531            discr_ty = *inner;
1532        }
1533
1534        let possible_values: Vec<usize> = match discr_ty.kind() {
1535            TyKind::Bool => vec![0, 1],
1536            TyKind::Adt(adt_def, _) if adt_def.is_enum() => (0..adt_def.variants().len()).collect(),
1537            _ => return None,
1538        };
1539
1540        let explicit_values: FxHashSet<usize> = targets.iter().map(|(v, _)| v as usize).collect();
1541        let remaining: Vec<usize> = possible_values
1542            .into_iter()
1543            .filter(|v| !explicit_values.contains(v))
1544            .collect();
1545
1546        if remaining.len() == 1 {
1547            Some(remaining[0])
1548        } else {
1549            None
1550        }
1551    }
1552
1553    /// Check whether `next` is an unwind target reachable from `cur` via a
1554    /// call or drop terminator (may not be recorded as a normal CFG successor).
1555    fn is_unwind_target(&self, cur: usize, next: usize) -> bool {
1556        let Some(terminator) = self.cfg.terminator(cur) else {
1557            return false;
1558        };
1559
1560        let unwind = match &terminator.kind {
1561            TerminatorKind::Call { unwind, .. }
1562            | TerminatorKind::Drop { unwind, .. }
1563            | TerminatorKind::Assert { unwind, .. } => unwind,
1564            _ => return false,
1565        };
1566
1567        if let UnwindAction::Cleanup(target) = unwind {
1568            let base = cur - self.cfg.block(cur).local_index;
1569            return base + target.as_usize() == next;
1570        }
1571        false
1572    }
1573
1574    /// Return true if `local` is known to be a non-null pointer.
1575    fn local_is_known_nonnull(&self, constraints: &FxHashMap<usize, usize>, local: usize) -> bool {
1576        let Some(&val) = constraints.get(&local) else {
1577            return false;
1578        };
1579        val > 0
1580    }
1581
1582    /// Return true if `local` is known to be a null pointer.
1583    fn local_is_known_null(&self, constraints: &FxHashMap<usize, usize>, local: usize) -> bool {
1584        let Some(&val) = constraints.get(&local) else {
1585            return false;
1586        };
1587        val == 0
1588    }
1589
1590    /// Populate the `child_sccs` field for a given SCC entry block, then
1591    /// recurse into those child SCCs. Called eagerly from `find_scc()` so
1592    /// that enumeration can be purely read-only on the graph.
1593    fn populate_child_sccs(&mut self, enter: usize) {
1594        let nodes: Vec<usize> = self.cfg.block(enter).scc.nodes.iter().cloned().collect();
1595        let mut child_enters = Vec::new();
1596        let mut seen = FxHashSet::default();
1597
1598        for node in nodes {
1599            if let Some(block) = self.cfg.blocks.get(node) {
1600                let node_enter = block.scc.enter;
1601                let non_trivial = !block.scc.nodes.is_empty();
1602                if node_enter != enter && non_trivial && seen.insert(node_enter) {
1603                    child_enters.push(node_enter);
1604                }
1605            }
1606        }
1607
1608        self.cfg.block_mut(enter).scc.child_sccs = child_enters;
1609
1610        let child_count = self.cfg.block(enter).scc.child_sccs.len();
1611        for i in 0..child_count {
1612            let child_enter = self.cfg.block(enter).scc.child_sccs[i];
1613            self.populate_child_sccs(child_enter);
1614        }
1615    }
1616
1617    fn populate_all_child_sccs(&mut self) {
1618        let mut visited = FxHashSet::default();
1619        let block_count = self.cfg.blocks.len();
1620        for i in 0..block_count {
1621            let scc = &self.cfg.block(i).scc;
1622            let enter = scc.enter;
1623            if scc.nodes.is_empty() || !visited.insert(enter) {
1624                continue;
1625            }
1626            self.populate_child_sccs(enter);
1627        }
1628    }
1629}
1630
1631/// Hash of the constraint state accumulated along a path prefix.
1632///
1633/// Computed by walking the prefix, collecting `(local → constant_value)`
1634/// bindings from each block's [`BlockConstantInfo`], sorting them, and
1635/// hashing the result.  Used by [`PathEnumerator::visited_sccs`] to
1636/// skip redundant re-entries into the same SCC with the same state.
1637#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
1638pub struct ConstraintHash(u64);
1639
1640impl ConstraintHash {
1641    fn from_path(path: &[usize], graph: &PathGraph<'_>) -> Self {
1642        let mut hasher = DefaultHasher::new();
1643        let mut constraints: FxHashMap<usize, usize> = FxHashMap::default();
1644
1645        for &block in path.iter() {
1646            if let Some(info) = graph.block_info.get(block) {
1647                for local in &info.assigned_locals {
1648                    if let Some(&src) = info.constraint_copies.get(local) {
1649                        if let Some(&src_val) = constraints.get(&src) {
1650                            constraints.insert(*local, src_val);
1651                            continue;
1652                        }
1653                        if let Some(&dst_val) = constraints.get(local) {
1654                            constraints.insert(src, dst_val);
1655                            constraints.insert(*local, dst_val);
1656                            continue;
1657                        }
1658                    }
1659                    if let Some(&val) = info.constants.get(local) {
1660                        constraints.insert(*local, val);
1661                        continue;
1662                    }
1663                    constraints.remove(local);
1664                }
1665            }
1666        }
1667
1668        let mut entries: Vec<(usize, usize)> = constraints.into_iter().collect();
1669        entries.sort();
1670        entries.hash(&mut hasher);
1671        ConstraintHash(hasher.finish())
1672    }
1673}
1674
1675/// Key for [`PathEnumerator::scc_paths`] and [`PathEnumerator::visited_sccs`] and [`PathEnumerator::visited_sccs`]:
1676/// which SCC entry block, with what constraint state, and how many
1677/// additional postfix repeats are allowed.
1678///
1679/// In `scc_paths` the `constraint` field is unused (cache keyed by
1680/// `entry` + `repeat`); in `visited_sccs` the `repeat` field is
1681/// unused (deduplicated by `entry` + `constraint`).
1682#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1683pub struct SccKey {
1684    pub entry: usize,
1685    pub repeat: usize,
1686    pub constraint: ConstraintHash,
1687}
1688
1689/// Builds a [`PathTree`] by depth-first enumeration of whole-CFG paths.
1690///
1691/// The enumerator holds a `&PathGraph` (which must have `find_scc()` called
1692/// beforehand) and two caches:
1693///
1694/// The **constraint hash** used by `visited_sccs` is a
1695/// [`ConstraintHash`]: it walks the current path prefix, accumulates
1696/// `(local → constant_value)` bindings from each block's
1697/// [`BlockConstantInfo`], sorts them, and hashes the result.  Different
1698/// constraint states (e.g. a loop variable changing from `true` to
1699/// `false`) produce different hashes.
1700///
1701/// 1. `scc_paths` — maps [`SccKey`] to a list of acyclic paths through
1702///    that SCC.  Reused across repeated enumerations of the same function
1703///    with different repeat counts.
1704///
1705/// 2. `visited_sccs` — set of [`SccKey`] entries already explored
1706///    (matched by `entry` + `constraint`, ignoring `repeat`).
1707///    When the same SCC is reached with the same constraint state, its
1708///    sub-paths are identical, so the re-entry is skipped.
1709///
1710/// Constraint-based filtering runs incrementally during DFS via
1711/// [`PathGraph::check_transition`], so only feasible paths are inserted
1712/// into the resulting tree.
1713pub struct PathEnumerator<'g, 'tcx> {
1714    graph: &'g PathGraph<'tcx>,
1715    scc_paths: FxHashMap<SccKey, Vec<SccPath>>,
1716    visited_sccs: FxHashSet<SccKey>,
1717}
1718
1719impl<'g, 'tcx> PathEnumerator<'g, 'tcx> {
1720    pub fn new(graph: &'g PathGraph<'tcx>) -> Self {
1721        PathEnumerator {
1722            graph,
1723            scc_paths: FxHashMap::default(),
1724            visited_sccs: FxHashSet::default(),
1725        }
1726    }
1727
1728    /// Enumerate all whole-CFG paths, pruning infeasible transitions via
1729    /// incremental constraint-based filtering during DFS.
1730    ///
1731    /// SCC regions are flattened into a bounded set of acyclic paths.
1732    pub fn enumerate_paths(&mut self) -> PathTree {
1733        self.enumerate_paths_repeat(0)
1734    }
1735
1736    /// Enumerate whole-CFG paths allowing each SCC postfix segment to repeat
1737    /// up to `postfix_repeat` additional times. `postfix_repeat = 0` gives
1738    /// the same result as `enumerate_paths`.
1739    ///
1740    /// Enumeration stops once [`crate::limit::path_limit`] paths are
1741    /// collected and the tree is marked truncated.
1742    pub fn enumerate_paths_repeat(&mut self, postfix_repeat: usize) -> PathTree {
1743        let mut tree = PathTree::new();
1744
1745        if self.graph.cfg.blocks.is_empty() {
1746            return tree;
1747        }
1748
1749        self.collect_whole_cfg_paths(
1750            0,
1751            &mut vec![0],
1752            &mut tree,
1753            0,
1754            postfix_repeat,
1755            &FxHashMap::default(),
1756        );
1757
1758        tree
1759    }
1760
1761    /// Enumerate all acyclic paths through `scc` starting at `start`,
1762    /// allowing each postfix segment to repeat up to `postfix_repeat`
1763    /// additional times.
1764    ///
1765    /// Results are cached per `(def_id, scc_enter, postfix_repeat)`.
1766    pub fn find_scc_paths_repeat(
1767        &mut self,
1768        start: usize,
1769        scc: &SccInfo,
1770        postfix_repeat: usize,
1771    ) -> Vec<SccPath> {
1772        let cache_key = SccKey {
1773            entry: scc.enter,
1774            repeat: postfix_repeat,
1775            constraint: ConstraintHash::default(),
1776        };
1777        if let Some(cached) = self.scc_paths.get(&cache_key) {
1778            return cached.clone();
1779        }
1780
1781        let mut out = Vec::new();
1782        let mut seen: FxHashSet<Vec<usize>> = FxHashSet::default();
1783        let mut path = vec![start];
1784        let mut segment_counts = FxHashMap::default();
1785
1786        self.dfs_scc_tree(
1787            scc,
1788            start,
1789            &mut path,
1790            &mut segment_counts,
1791            postfix_repeat,
1792            &mut out,
1793            &mut seen,
1794            0,
1795        );
1796
1797        if self.scc_paths.len() >= SCC_PATH_CACHE_LIMIT {
1798            self.scc_paths.clear();
1799        }
1800        self.scc_paths.insert(cache_key, out.clone());
1801
1802        out
1803    }
1804
1805    /// Recursive DFS through one level of the SCC tree.
1806    ///
1807    /// Enumerates structurally possible paths through the SCC to exit points.
1808    /// No constraint tracking — `check_postfix_segment` prunes repeated
1809    /// loop-body segments purely by block-id sequence.
1810    ///
1811    /// When `postfix_repeat > 0`, allows the same postfix segment to repeat
1812    /// up to `postfix_repeat` additional times beyond the first occurrence.
1813    ///
1814    /// Child SCC paths are pre-enumerated via `find_scc_paths_repeat` and treated as
1815    /// atomic building blocks (no recursive descent into child SCC internals).
1816    #[allow(clippy::too_many_arguments)]
1817    fn dfs_scc_tree(
1818        &mut self,
1819        scc: &SccInfo,
1820        cur: usize,
1821        path: &mut Vec<usize>,
1822        segment_counts: &mut FxHashMap<Vec<usize>, usize>,
1823        postfix_repeat: usize,
1824        out: &mut Vec<SccPath>,
1825        seen_paths: &mut FxHashSet<Vec<usize>>,
1826        depth: usize,
1827    ) {
1828        if depth > SCC_MAX_DEPTH {
1829            return;
1830        }
1831        if out.len() >= SCC_MAX_SEEN_PATHS {
1832            return;
1833        }
1834        if path.len() > SCC_MAX_PATH_LEN {
1835            return;
1836        }
1837        if cur != scc.enter && !scc.nodes.contains(&cur) {
1838            return;
1839        }
1840
1841        if cur == scc.enter && path.len() > 1 {
1842            if !check_postfix_segment(path, scc.enter, segment_counts, postfix_repeat) {
1843                if (postfix_repeat > 0 || segment_counts.len() > 1)
1844                    && scc.exits.iter().any(|e| e.exit == cur)
1845                {
1846                    self.record_unique_path(path, scc, out, seen_paths);
1847                }
1848                return;
1849            }
1850        }
1851
1852        if scc.exits.iter().any(|e| e.exit == cur) {
1853            self.record_unique_path(path, scc, out, seen_paths);
1854        }
1855
1856        let is_child = scc.child_sccs.contains(&cur);
1857
1858        if is_child {
1859            let ctx = self.constraint_context(path);
1860            if !self.visited_sccs.insert(SccKey {
1861                entry: cur,
1862                repeat: 0,
1863                constraint: ctx,
1864            }) {
1865                return;
1866            }
1867
1868            let child_scc = self.graph.cfg_block(cur).scc.clone();
1869            let child_paths = self.find_scc_paths_repeat(cur, &child_scc, postfix_repeat);
1870
1871            for child_path in &child_paths {
1872                let orig_len = path.len();
1873                if child_path.blocks.len() > 1 {
1874                    path.extend(&child_path.blocks[1..]);
1875                }
1876
1877                let mut branch_counts = segment_counts.clone();
1878                for &next in &child_path.exit_successors {
1879                    path.push(next);
1880                    self.dfs_scc_tree(
1881                        scc,
1882                        next,
1883                        path,
1884                        &mut branch_counts,
1885                        postfix_repeat,
1886                        out,
1887                        seen_paths,
1888                        depth + 1,
1889                    );
1890                    path.pop();
1891                }
1892                path.truncate(orig_len);
1893            }
1894            return;
1895        }
1896
1897        let successors: Vec<usize> = self.graph.cfg.block(cur).next.iter().copied().collect();
1898        let saved_counts = segment_counts.clone();
1899        for next in successors {
1900            if next != scc.enter && !scc.nodes.contains(&next) {
1901                self.record_unique_path(path, scc, out, seen_paths);
1902                continue;
1903            }
1904            let mut branch_counts = saved_counts.clone();
1905            path.push(next);
1906            self.dfs_scc_tree(
1907                scc,
1908                next,
1909                path,
1910                &mut branch_counts,
1911                postfix_repeat,
1912                out,
1913                seen_paths,
1914                depth + 1,
1915            );
1916            path.pop();
1917        }
1918    }
1919
1920    /// Build a [`ConstraintHash`] from the constraint state along `path`.
1921    fn constraint_context(&self, path: &[usize]) -> ConstraintHash {
1922        ConstraintHash::from_path(path, self.graph)
1923    }
1924
1925    /// Depth-first enumeration of all CFG paths from `current` to a terminator.
1926    ///
1927    /// SCC nodes are flattened via `find_scc_paths_repeat`; non-SCC blocks are followed
1928    /// one by one.  No cycle detection is needed because the post-SCC CFG is a DAG.
1929    /// Constraints are maintained incrementally — each transition is checked
1930    /// via `PathGraph::check_transition` before recursing, and infeasible
1931    /// branches are pruned early.
1932    fn collect_whole_cfg_paths(
1933        &mut self,
1934        current: usize,
1935        path: &mut Vec<usize>,
1936        tree: &mut PathTree,
1937        depth: usize,
1938        postfix_repeat: usize,
1939        constraints: &FxHashMap<usize, usize>,
1940    ) {
1941        if current >= self.graph.cfg.blocks.len() {
1942            return;
1943        }
1944        if depth > WHOLE_CFG_PATH_DEPTH_LIMIT
1945            || tree.len() >= crate::limit::path_limit()
1946        {
1947            tree.mark_truncated();
1948            return;
1949        }
1950
1951        let scc_info = self.graph.cfg_block(current).scc.clone();
1952        let is_scc = current == scc_info.enter && !scc_info.nodes.is_empty();
1953        if is_scc {
1954            let scc = self.sort_scc_tree(&scc_info);
1955            let segments = self.find_scc_paths_repeat(current, &scc, postfix_repeat);
1956
1957            if segments.is_empty() {
1958                tree.insert(path);
1959                return;
1960            }
1961
1962            for seg in segments {
1963                if tree.len() >= crate::limit::path_limit() {
1964                    tree.mark_truncated();
1965                    break;
1966                }
1967
1968                let orig_len = path.len();
1969                let mut seg_constraints = constraints.clone();
1970                let mut reachable = true;
1971
1972                if seg.blocks.len() > 1 {
1973                    for i in 0..seg.blocks.len() - 1 {
1974                        if !self.graph.check_transition(
1975                            seg.blocks[i],
1976                            seg.blocks[i + 1],
1977                            &mut seg_constraints,
1978                        ) {
1979                            reachable = false;
1980                            break;
1981                        }
1982                    }
1983                    if reachable {
1984                        path.extend_from_slice(&seg.blocks[1..]);
1985                    }
1986                }
1987
1988                if reachable {
1989                    if seg.exit_successors.is_empty() {
1990                        tree.insert(path);
1991                    } else {
1992                        for &next in &seg.exit_successors {
1993                            let mut next_constraints = seg_constraints.clone();
1994                            let last = *path.last().unwrap();
1995                            if self
1996                                .graph
1997                                .check_transition(last, next, &mut next_constraints)
1998                            {
1999                                path.push(next);
2000                                self.collect_whole_cfg_paths(
2001                                    next,
2002                                    path,
2003                                    tree,
2004                                    depth + 1,
2005                                    postfix_repeat,
2006                                    &next_constraints,
2007                                );
2008                                path.pop();
2009                            }
2010                        }
2011                    }
2012                }
2013
2014                path.truncate(orig_len);
2015            }
2016            return;
2017        }
2018
2019        // Non-SCC block: follow CFG successors.
2020        let successors: Vec<usize> = self.graph.cfg_block(current).next.iter().copied().collect();
2021        if successors.is_empty() {
2022            tree.insert(path);
2023            return;
2024        }
2025
2026        for next in successors {
2027            let mut next_constraints = constraints.clone();
2028            if self
2029                .graph
2030                .check_transition(current, next, &mut next_constraints)
2031            {
2032                path.push(next);
2033                self.collect_whole_cfg_paths(
2034                    next,
2035                    path,
2036                    tree,
2037                    depth + 1,
2038                    postfix_repeat,
2039                    &next_constraints,
2040                );
2041                path.pop();
2042            }
2043        }
2044    }
2045
2046    fn sort_scc_tree(&self, scc: &SccInfo) -> SccInfo {
2047        self.graph.cfg_block(scc.enter).scc.clone()
2048    }
2049
2050    fn record_unique_path(
2051        &self,
2052        path: &[usize],
2053        scc: &SccInfo,
2054        out: &mut Vec<SccPath>,
2055        seen_paths: &mut FxHashSet<Vec<usize>>,
2056    ) {
2057        if !seen_paths.insert(path.to_vec()) {
2058            return;
2059        }
2060        let exit_successors = self.compute_exit_successors(path, scc);
2061        out.push(SccPath {
2062            blocks: path.to_vec(),
2063            exit_successors,
2064        });
2065    }
2066
2067    fn compute_exit_successors(&self, path: &[usize], scc: &SccInfo) -> Vec<usize> {
2068        let Some(&last) = path.last() else {
2069            return vec![];
2070        };
2071        scc.exits
2072            .iter()
2073            .filter(|e| e.exit == last)
2074            .map(|e| e.to)
2075            .filter(|&n| {
2076                !scc.child_sccs
2077                    .contains(&self.graph.cfg.block(n).scc.enter())
2078            })
2079            .collect()
2080    }
2081}
2082
2083/// Resolve a concrete discriminant value to the corresponding `SwitchInt`
2084/// successor block index.
2085fn resolve_switch_target(targets: &SwitchTargets, val: u128) -> usize {
2086    targets
2087        .iter()
2088        .find(|(v, _)| *v == val)
2089        .map(|(_, bb)| bb.as_usize())
2090        .unwrap_or_else(|| targets.otherwise().as_usize())
2091}