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
18/// Maximum number of whole-CFG paths collected before stopping enumeration.
19const WHOLE_CFG_PATH_LIMIT: usize = 4000;
20/// Maximum DFS depth for whole-CFG path enumeration.
21const WHOLE_CFG_PATH_DEPTH_LIMIT: usize = 256;
22/// Bounded cache size for SCC path enumeration.
23const SCC_PATH_CACHE_LIMIT: usize = 2048;
24/// Maximum DFS depth for intra-SCC path enumeration.
25const SCC_MAX_DEPTH: usize = 128;
26/// Maximum number of distinct paths collected per SCC.
27const SCC_MAX_SEEN_PATHS: usize = 128;
28/// Maximum path length within an SCC traversal.
29const SCC_MAX_PATH_LEN: usize = 200;
30
31/// Check whether the current entry→entry sub-path introduces a new block
32/// *sequence* (not just new blocks).  Different branch choices inside the SCC
33/// produce different sequences even when all block IDs have already been seen,
34/// e.g. `if i % 2 == 0 { A } else { B }` alternates between two paths through
35/// the same set of blocks on successive loop iterations.
36fn check_postfix_segment(
37    path: &[usize],
38    enter: usize,
39    segment_counts: &mut FxHashMap<Vec<usize>, usize>,
40    max_repeats: usize,
41) -> bool {
42    let segment = extract_segment(path, enter);
43    let count = segment_counts.entry(segment).or_insert(0);
44    *count += 1;
45    *count == 1 || *count - 1 <= max_repeats
46}
47
48fn extract_segment(path: &[usize], enter: usize) -> Vec<usize> {
49    let prev_pos = path[..path.len() - 1]
50        .iter()
51        .rposition(|&node| node == enter)
52        .unwrap_or(0);
53    path[prev_pos + 1..path.len() - 1].to_vec()
54}
55
56#[derive(Clone, Debug)]
57/// A single enumerated acyclic path through an SCC region.
58///
59/// `blocks` is the ordered sequence of MIR block indices from the SCC entry
60/// to the last block before exiting. The last block may have multiple CFG
61/// successors outside the SCC (e.g. a `SwitchInt` branching to different
62/// out-of-SCC targets), which are stored in `exit_successors`.
63///
64/// For example, in a loop with `switch x { A => loop_body, B => done1, C => done2 }`,
65/// the corresponding `SccPath` would have `exit_successors = [done1, done2]`
66/// — the DFS forks recursively into each of these when constructing whole-CFG paths.
67pub struct SccPath {
68    pub blocks: Vec<usize>,
69    pub exit_successors: Vec<usize>,
70}
71
72/// Per-block info collected during construction for path reachability
73/// analysis.  Each block's assignments, constants, and copy chains are
74/// stored together so they can be read with a single index lookup.
75#[derive(Clone, Debug, Default)]
76pub struct BlockConstantInfo {
77    pub assigned_locals: FxHashSet<usize>,
78    pub constants: FxHashMap<usize, usize>,
79    pub constraint_copies: FxHashMap<usize, usize>,
80    /// Maps a local assigned by `AddWithOverflow(src, const)` to `(src, const)`.
81    pub increments: FxHashMap<usize, (usize, usize)>,
82    /// Maps a local assigned by `Rem(src, const)` to `(src, divisor)`.
83    pub remainders: FxHashMap<usize, (usize, usize)>,
84    /// Maps a boolean local (e.g., a guard result) to the binary comparison
85    /// that produced it: `(op, lhs_local, rhs_kind)`.
86    pub comparison_sources: FxHashMap<usize, ComparisonSource>,
87    /// Set of locals that are provably non-null (e.g. assigned from
88    /// `Box::into_raw`).  Used for path reachability pruning.
89    pub known_nonnull_locals: FxHashSet<usize>,
90    pub negation_sources: FxHashMap<usize, usize>,
91    pub and_sources: FxHashMap<usize, (usize, usize)>,
92}
93/// Records the origin of a boolean temporary produced by a binary
94/// comparison during guard-clause evaluation.
95#[derive(Clone, Debug)]
96pub struct ComparisonSource {
97    pub op: rustc_middle::mir::BinOp,
98    pub lhs_local: usize,
99    pub rhs_local: usize,
100    pub rhs_is_constant: bool,
101}
102
103/// Encode a `(local, field_index)` pair into a single `usize`.
104const AGGREGATE_FIELD_MULT: usize = 256;
105const AGGREGATE_FIELD_SENTINEL: usize = 1 << (usize::BITS as usize - 1);
106
107fn encode_aggregate_field(local: usize, field: usize) -> usize {
108    debug_assert!(field < AGGREGATE_FIELD_MULT);
109    AGGREGATE_FIELD_SENTINEL | (local * AGGREGATE_FIELD_MULT + field)
110}
111
112fn decode_aggregate_field(encoded: usize) -> Option<(usize, usize)> {
113    if encoded & AGGREGATE_FIELD_SENTINEL == 0 {
114        return None;
115    }
116    let raw = encoded & !AGGREGATE_FIELD_SENTINEL;
117    Some((raw / AGGREGATE_FIELD_MULT, raw % AGGREGATE_FIELD_MULT))
118}
119
120fn first_field_projection(place: &rustc_middle::mir::Place<'_>) -> Option<usize> {
121    for proj in place.projection.iter() {
122        if let rustc_middle::mir::ProjectionElem::Field(field, _) = proj {
123            return Some(field.as_usize());
124        }
125    }
126    None
127}
128
129/// Enum discriminant metadata used by [`check_switch_transition`].
130///
131/// `source_of` maps a discriminant local to the ADT local it was read from
132/// (via `Rvalue::Discriminant`).  `variant_count_of` tracks the number of
133/// variants for each ADT local, used to determine whether a `SwitchInt`
134/// otherwise-target is uniquely determined.
135#[derive(Clone, Debug, Default)]
136pub struct DiscriminantInfo {
137    pub source_of: FxHashMap<usize, usize>,
138    pub variant_count_of: FxHashMap<usize, usize>,
139}
140
141/// CFG augmented with per-block constant info and discriminant metadata
142/// for path reachability analysis.
143///
144/// `PathGraph` wraps a `ControlFlowGraph` and adds block-indexed data
145/// that track assignments, constants, and copy chains.  These are used by
146/// [`check_transition`](PathGraph::check_transition) to update a set of
147/// discriminant constraints while traversing the CFG, enabling early
148/// pruning of infeasible `SwitchInt` branches during path enumeration.
149#[derive(Clone)]
150pub struct PathGraph<'tcx> {
151    pub cfg: ControlFlowGraph<'tcx>,
152    pub block_info: Vec<BlockConstantInfo>,
153    pub disc_info: DiscriminantInfo,
154    /// Global store: maps encoded `(local, field_idx)` to source local from Aggregates.
155    pub aggregate_field_sources: FxHashMap<usize, usize>,
156    /// Global store: maps `dest` to `src` for pointer-cast chains.
157    pub cast_chains: FxHashMap<usize, usize>,
158    /// Global store: maps `_dest` to encoded `(base, field_idx)` from field-projection copies.
159    pub field_projection_source: FxHashMap<usize, usize>,
160}
161
162impl<'tcx> PathGraph<'tcx> {
163    pub fn new(tcx: TyCtxt<'tcx>, def_id: DefId) -> PathGraph<'tcx> {
164        let body = tcx.optimized_mir(def_id);
165        let basicblocks = &body.basic_blocks;
166        let mut cfg_blocks = Vec::<CfgBlock>::new();
167        let mut block_info = Vec::new();
168        let mut disc_info = DiscriminantInfo::default();
169        let mut aggregate_field_sources: FxHashMap<usize, usize> = FxHashMap::default();
170        let mut field_projection_source: FxHashMap<usize, usize> = FxHashMap::default();
171        let mut cast_chains: FxHashMap<usize, usize> = FxHashMap::default();
172
173        for i in 0..basicblocks.len() {
174            let bb = &basicblocks[BasicBlock::from(i)];
175            let mut cfg_block = CfgBlock::new(i, bb.is_cleanup);
176            let mut info = BlockConstantInfo::default();
177
178            for stmt in &bb.statements {
179                if let StatementKind::Assign(assign) = &stmt.kind {
180                    let (place, rvalue) = &**assign;
181                    let dest = place.local.as_usize();
182                    // Writing through (*ptr).field doesn't reassign ptr itself,
183                    // so ptr's constraint should not be cleared.
184                    let is_deref = place
185                        .projection
186                        .iter()
187                        .any(|p| matches!(p, ProjectionElem::Deref));
188                    if !is_deref {
189                        info.assigned_locals.insert(dest);
190                    }
191                    match rvalue {
192                        Rvalue::Use(Operand::Constant(c), ..) => {
193                            let typing_env = TypingEnv::post_analysis(tcx, def_id);
194                            let val = match c.const_.ty().kind() {
195                                TyKind::Bool => c
196                                    .const_
197                                    .try_eval_bool(tcx, typing_env)
198                                    .map(|b| if b { 1 } else { 0 }),
199                                TyKind::Int(_) | TyKind::Uint(_) => {
200                                    c.const_.try_eval_bits(tcx, typing_env).map(|v| v as usize)
201                                }
202                                _ => None,
203                            };
204                            if let Some(val) = val {
205                                info.constants.insert(dest, val);
206                            }
207                        }
208                        Rvalue::Use(Operand::Copy(src) | Operand::Move(src), ..) => {
209                            let src_local = src.local.as_usize();
210                            if let Some(field_proj) = first_field_projection(src) {
211                                let encoded = encode_aggregate_field(src_local, field_proj);
212                                field_projection_source.insert(dest, encoded);
213                            }
214                            info.constraint_copies.insert(dest, src_local);
215                        }
216                        Rvalue::Discriminant(rv_place) => {
217                            disc_info.source_of.insert(dest, rv_place.local.as_usize());
218                            let src_local = rv_place.local.as_usize();
219                            if !disc_info.variant_count_of.contains_key(&src_local) {
220                                let src_ty = body.local_decls[rv_place.local].ty;
221                                if let TyKind::Adt(adt_def, _) = src_ty.kind() {
222                                    let num = adt_def.variants().len();
223                                    if num > 0 {
224                                        disc_info.variant_count_of.insert(src_local, num);
225                                    }
226                                }
227                            }
228                        }
229                        Rvalue::Aggregate(kind, operands) => {
230                            if let AggregateKind::Adt(_, _, _, _, _) = kind.as_ref() {
231                                let agg_local = place.local.as_usize();
232                                for (field_idx, operand) in operands.iter().enumerate() {
233                                    if let Operand::Copy(src) | Operand::Move(src) = operand {
234                                        let key = encode_aggregate_field(agg_local, field_idx);
235                                        let src_local = src.local.as_usize();
236                                        aggregate_field_sources.insert(key, src_local);
237                                    }
238                                }
239                            }
240                            let discr = match kind.as_ref() {
241                                AggregateKind::Adt(_, variant_idx, _, _, _) => {
242                                    Some(variant_idx.as_usize())
243                                }
244                                _ => None,
245                            };
246                            if let Some(discr) = discr {
247                                info.constants.insert(dest, discr);
248                                if !disc_info.variant_count_of.contains_key(&dest) {
249                                    let dest_ty = body.local_decls[place.local].ty;
250                                    if let TyKind::Adt(adt_def, _) = dest_ty.kind() {
251                                        let num = adt_def.variants().len();
252                                        if num > 0 {
253                                            disc_info.variant_count_of.insert(dest, num);
254                                        }
255                                    }
256                                }
257                            }
258                        }
259                        Rvalue::BinaryOp(op, operands)
260                            if matches!(
261                                op,
262                                BinOp::AddWithOverflow
263                            ) =>
264                        {
265                            let (lhs, rhs): (&Operand<'_>, &Operand<'_>) =
266                                (&operands.0, &operands.1);
267                            if let Some(lhs_local) = match lhs {
268                                Operand::Copy(l) | Operand::Move(l)
269                                    if l.projection.is_empty() =>
270                                {
271                                    Some(l.local.as_usize())
272                                }
273                                _ => None,
274                            } {
275                                let incr = match rhs {
276                                    Operand::Constant(c) => {
277                                        let typing_env =
278                                            TypingEnv::post_analysis(tcx, def_id);
279                                        c.const_
280                                            .try_eval_bits(tcx, typing_env)
281                                            .map(|v| v as usize)
282                                    }
283                                    _ => None,
284                                };
285                                if let Some(incr) = incr {
286                                    info.increments.insert(dest, (lhs_local, incr));
287                                }
288                            }
289                        }
290                        Rvalue::BinaryOp(op, operands)
291                            if matches!(
292                                op,
293                                BinOp::Lt
294                                    | BinOp::Le
295                                    | BinOp::Gt
296                                    | BinOp::Ge
297                                    | BinOp::Eq
298                                    | BinOp::Ne
299                                    | BinOp::BitAnd
300                            ) =>
301                        {
302                            let (lhs, rhs): (&Operand<'_>, &Operand<'_>) =
303                                (&operands.0, &operands.1);
304                            let lhs_local = match lhs {
305                                Operand::Copy(l) | Operand::Move(l)
306                                    if l.projection.is_empty() =>
307                                {
308                                    Some(l.local.as_usize())
309                                }
310                                _ => None,
311                            };
312                            if let Some(lhs_local) = lhs_local {
313                                let rhs_eval = match rhs {
314                                    Operand::Constant(c) => {
315                                        let typing_env =
316                                            TypingEnv::post_analysis(tcx, def_id);
317                                        c.const_
318                                            .try_eval_bits(tcx, typing_env)
319                                            .map(|v| (v as usize, true))
320                                    }
321                                    Operand::Copy(r) | Operand::Move(r)
322                                        if r.projection.is_empty() =>
323                                    {
324                                        Some((r.local.as_usize(), false))
325                                    }
326                                    _ => None,
327                                };
328                                let Some((rhs_local, rhs_is_constant)) = rhs_eval else {
329                                    continue;
330                                };
331                                info.comparison_sources.insert(
332                                    dest,
333                                    ComparisonSource {
334                                        op: *op,
335                                        lhs_local,
336                                        rhs_local,
337                                        rhs_is_constant,
338                                    },
339                                );
340                                if matches!(op, BinOp::BitAnd)
341                                    && matches!(
342                                        body.local_decls[place.local].ty.kind(),
343                                        TyKind::Bool
344                                    )
345                                {
346                                    info.and_sources.insert(dest, (lhs_local, rhs_local));
347                                }
348                            }
349                        }
350                        Rvalue::BinaryOp(op, operands) if matches!(op, BinOp::Rem) => {
351                            let (lhs, rhs): (&Operand<'_>, &Operand<'_>) = (&operands.0, &operands.1);
352                            if let Some(lhs_local) = match lhs {
353                                Operand::Copy(l) | Operand::Move(l) if l.projection.is_empty() => {
354                                    Some(l.local.as_usize())
355                                }
356                                _ => None,
357                            } {
358                                let divisor = match rhs {
359                                    Operand::Constant(c) => {
360                                        let typing_env = TypingEnv::post_analysis(tcx, def_id);
361                                        c.const_
362                                            .try_eval_bits(tcx, typing_env)
363                                            .map(|v| v as usize)
364                                    }
365                                    _ => None,
366                                };
367                                if let Some(divisor) = divisor {
368                                    if divisor != 0 {
369                                        info.remainders.insert(dest, (lhs_local, divisor));
370                                    }
371                                }
372                            }
373                        }
374                        Rvalue::UnaryOp(unop, operand) => {
375                            if matches!(unop, UnOp::Not)
376                                && let Operand::Copy(src) | Operand::Move(src) = operand
377                            {
378                                info.negation_sources.insert(dest, src.local.as_usize());
379                            }
380                        }
381                        Rvalue::Cast(_, operand, _) => {
382                            if let Operand::Copy(src) | Operand::Move(src) = operand
383                                && matches!(
384                                    body.local_decls[place.local].ty.kind(),
385                                    TyKind::RawPtr(..) | TyKind::Int(..) | TyKind::Uint(..)
386                                )
387                            {
388                                cast_chains.insert(dest, src.local.as_usize());
389                            }
390                        }
391                        _ => {} // close match rvalue
392                    }
393                }
394            }
395
396            let Some(terminator) = &bb.terminator else {
397                cfg_blocks.push(cfg_block);
398                block_info.push(info);
399                continue;
400            };
401
402            if let TerminatorKind::Call {
403                destination,
404                ref func,
405                ..
406            } = terminator.kind
407            {
408                let name = crate::helpers::mir_utils::call_name(tcx, func);
409                if name.contains("::into_raw")
410                    || (name.contains("::new") && name.contains("Box"))
411                    || name.contains("::as_mut_ptr")
412                    || name.contains("::as_ptr")
413                {
414                    info.known_nonnull_locals
415                        .insert(destination.local.as_usize());
416                }
417                if name.contains("null_mut") || (name.contains("null") && name.contains("ptr::")) {
418                    info.constants.insert(destination.local.as_usize(), 0);
419                }
420            }
421
422            match terminator.kind.clone() {
423                TerminatorKind::Goto { ref target } => {
424                    cfg_block.add_next(target.as_usize());
425                }
426                TerminatorKind::SwitchInt {
427                    discr: _,
428                    ref targets,
429                } => {
430                    for (_, ref target) in targets.iter() {
431                        cfg_block.add_next(target.as_usize());
432                    }
433                    cfg_block.add_next(targets.otherwise().as_usize());
434                }
435                TerminatorKind::Drop {
436                    place: _,
437                    target,
438                    unwind,
439                    replace: _,
440                    drop: _,
441                    #[cfg(not(rapx_ge_99))]
442                        async_fut: _,
443                } => {
444                    cfg_block.add_next(target.as_usize());
445                    if let UnwindAction::Cleanup(target) = unwind {
446                        cfg_block.add_next(target.as_usize());
447                    }
448                }
449                TerminatorKind::Call {
450                    ref target,
451                    ref unwind,
452                    ..
453                } => {
454                    if let Some(tt) = target {
455                        cfg_block.add_next(tt.as_usize());
456                    }
457                    if let UnwindAction::Cleanup(tt) = unwind {
458                        cfg_block.add_next(tt.as_usize());
459                    }
460                }
461                TerminatorKind::Assert {
462                    cond: _,
463                    expected: _,
464                    msg: _,
465                    ref target,
466                    ref unwind,
467                } => {
468                    cfg_block.add_next(target.as_usize());
469                    if let UnwindAction::Cleanup(target) = unwind {
470                        cfg_block.add_next(target.as_usize());
471                    }
472                }
473                TerminatorKind::Yield {
474                    value: _,
475                    ref resume,
476                    resume_arg: _,
477                    ref drop,
478                } => {
479                    cfg_block.add_next(resume.as_usize());
480                    if let Some(target) = drop {
481                        cfg_block.add_next(target.as_usize());
482                    }
483                }
484                TerminatorKind::FalseEdge {
485                    ref real_target,
486                    imaginary_target: _,
487                } => {
488                    cfg_block.add_next(real_target.as_usize());
489                }
490                TerminatorKind::FalseUnwind {
491                    ref real_target,
492                    unwind: _,
493                } => {
494                    cfg_block.add_next(real_target.as_usize());
495                }
496                TerminatorKind::InlineAsm {
497                    template: _,
498                    operands: _,
499                    options: _,
500                    line_spans: _,
501                    ref unwind,
502                    targets,
503                    asm_macro: _,
504                } => {
505                    for target in targets {
506                        cfg_block.add_next(target.as_usize());
507                    }
508                    if let UnwindAction::Cleanup(target) = unwind {
509                        cfg_block.add_next(target.as_usize());
510                    }
511                }
512                _ => {}
513            }
514
515            cfg_blocks.push(cfg_block);
516            block_info.push(info);
517        }
518
519        let cfg = ControlFlowGraph::new(def_id, tcx, cfg_blocks);
520
521        PathGraph {
522            cfg,
523            block_info,
524            disc_info,
525            aggregate_field_sources,
526            field_projection_source,
527            cast_chains,
528        }
529    }
530
531    pub fn find_scc(&mut self) {
532        self.cfg.find_scc();
533        self.populate_all_child_sccs();
534    }
535
536    pub fn def_id(&self) -> DefId {
537        self.cfg.def_id
538    }
539
540    pub fn tcx(&self) -> TyCtxt<'tcx> {
541        self.cfg.tcx
542    }
543
544    pub fn cfg_block(&self, index: usize) -> &CfgBlock {
545        self.cfg.block(index)
546    }
547
548    pub fn cfg_block_mut(&mut self, index: usize) -> &mut CfgBlock {
549        self.cfg.block_mut(index)
550    }
551
552    /// Retrieve the MIR terminator for the block at `index` on demand.
553    pub fn terminator(&self, index: usize) -> Option<&Terminator<'tcx>> {
554        self.cfg.terminator(index)
555    }
556
557    pub fn is_cleanup_block(&self, index: usize) -> bool {
558        self.cfg
559            .blocks
560            .get(index)
561            .map(|b| b.is_cleanup)
562            .unwrap_or(false)
563    }
564
565    /// Get the number of variants for a constraint local.
566    /// First checks the pre-populated `variant_count_of` hashmap,
567    /// then falls back to the local's declared type (for ADT locals
568    /// that gained their type through field projections in nested
569    /// destructuring patterns rather than explicit construction).
570    fn get_variant_count(&self, local: usize) -> Option<usize> {
571        if let Some(&count) = self.disc_info.variant_count_of.get(&local) {
572            return Some(count);
573        }
574        let body = self.cfg.tcx.optimized_mir(self.cfg.def_id);
575        let mut ty = body.local_decls[Local::from_usize(local)].ty;
576        while let TyKind::Ref(_, inner_ty, _) | TyKind::RawPtr(inner_ty, _) = ty.kind() {
577            ty = *inner_ty;
578        }
579        match ty.kind() {
580            TyKind::Adt(adt_def, _) if adt_def.is_enum() => Some(adt_def.variants().len()),
581            _ => None,
582        }
583    }
584
585    /// Check a single transition `cur -> next` for reachability and update
586    /// discriminant constraints. Returns `false` if the transition is
587    /// provably unreachable.
588    pub fn check_transition(
589        &self,
590        cur: usize,
591        next: usize,
592        constraints: &mut FxHashMap<usize, usize>,
593    ) -> bool {
594        if cur >= self.cfg.blocks.len() || next >= self.cfg.blocks.len() {
595            return false;
596        }
597
598        if let Some(info) = self.block_info.get(cur) {
599            for local in &info.assigned_locals {
600                if let Some(&src) = info.constraint_copies.get(local) {
601                    if let Some(val) = self.resolve_local_value(src, constraints) {
602                        constraints.insert(*local, val);
603                        continue;
604                    }
605                    if let Some(&dst_val) = constraints.get(local) {
606                        constraints.insert(src, dst_val);
607                        constraints.insert(*local, dst_val);
608                        continue;
609                    }
610                }
611                if let Some(&val) = info.constants.get(local) {
612                    constraints.insert(*local, val);
613                    continue;
614                }
615                constraints.remove(local);
616            }
617            for local in &info.known_nonnull_locals {
618                constraints.insert(*local, usize::MAX);
619            }
620        }
621
622        // Also clear constraints for locals assigned by the terminator
623        // (e.g. _13 = Iterator::next() in a Call terminator). The block's
624        // assigned_locals only covers statement-level assignments, so
625        // terminator-side assignments are handled here.
626        if let Some(terminator) = self.terminator(cur) {
627            let assigned = match &terminator.kind {
628                TerminatorKind::Call { destination, .. } => Some(destination.local.as_usize()),
629                TerminatorKind::Yield { resume_arg, .. } => Some(resume_arg.local.as_usize()),
630                _ => None,
631            };
632            if let Some(local) = assigned {
633                // Propagate known nullness from BlockConstantInfo.
634                if let Some(block_info) = self.block_info.get(cur) {
635                    if let Some(&val) = block_info.constants.get(&local) {
636                        constraints.insert(local, val);
637                    } else if block_info.known_nonnull_locals.contains(&local) {
638                        constraints.insert(local, usize::MAX);
639                    } else {
640                        constraints.remove(&local);
641                    }
642                } else {
643                    constraints.remove(&local);
644                }
645            }
646        }
647
648        let successors = &self.cfg.block(cur).next;
649        if !successors.contains(&next) {
650            if !self.is_unwind_target(cur, next) {
651                return false;
652            }
653        }
654
655        if !self.check_switch_transition(cur, next, constraints) {
656            return false;
657        }
658
659        if !self.check_assert_transition(cur, next, constraints) {
660            return false;
661        }
662
663        true
664    }
665
666    fn check_assert_transition(
667        &self,
668        cur: usize,
669        next: usize,
670        constraints: &FxHashMap<usize, usize>,
671    ) -> bool {
672        let Some(terminator) = self.cfg.terminator(cur) else {
673            return true;
674        };
675        let TerminatorKind::Assert { cond, target, .. } = &terminator.kind else {
676            return true;
677        };
678        if next != target.as_usize() {
679            return true;
680        }
681        let cond_local = match cond {
682            Operand::Copy(p) | Operand::Move(p) => p.local.as_usize(),
683            Operand::Constant(c) => {
684                let typing_env =
685                    rustc_middle::ty::TypingEnv::post_analysis(self.cfg.tcx, self.cfg.def_id);
686                return c
687                    .const_
688                    .try_eval_bool(self.cfg.tcx, typing_env)
689                    .unwrap_or(true);
690            }
691            #[cfg(rapx_ge_99)]
692            Operand::RuntimeChecks(_) => return true,
693        };
694        self.resolve_bool_local(cond_local, constraints)
695            .map_or(true, |v| v == 1)
696    }
697
698    fn resolve_simple_bool(
699        &self,
700        local: usize,
701        constraints: &FxHashMap<usize, usize>,
702    ) -> Option<usize> {
703        if let Some(&val) = constraints.get(&local)
704            && val <= 1
705        {
706            return Some(val);
707        }
708        for info in &self.block_info {
709            if let Some(&val) = info.constants.get(&local)
710                && val <= 1
711            {
712                return Some(val);
713            }
714            if let Some(cmp) = info.comparison_sources.get(&local) {
715                if matches!(cmp.op, BinOp::Eq | BinOp::Ne) {
716                    let is_eq = matches!(cmp.op, BinOp::Eq);
717                    if cmp.rhs_is_constant {
718                        if let Some(lhs_val) = self.resolve_local_value(cmp.lhs_local, constraints)
719                        {
720                            return Some(if is_eq {
721                                if lhs_val == cmp.rhs_local { 1 } else { 0 }
722                            } else {
723                                if lhs_val != cmp.rhs_local { 1 } else { 0 }
724                            });
725                        }
726                    } else {
727                        let lhs_val = self.resolve_local_value(cmp.lhs_local, constraints);
728                        let rhs_val = self.resolve_local_value(cmp.rhs_local, constraints);
729                        if let Some(lhs_val) = lhs_val {
730                            if let Some(rhs_val) = rhs_val {
731                                return Some(if is_eq {
732                                    if lhs_val == rhs_val { 1 } else { 0 }
733                                } else {
734                                    if lhs_val != rhs_val { 1 } else { 0 }
735                                });
736                            }
737                        } else if let Some(rhs_val) = rhs_val {
738                            return Some(if is_eq {
739                                if cmp.lhs_local == rhs_val { 1 } else { 0 }
740                            } else {
741                                if cmp.lhs_local != rhs_val { 1 } else { 0 }
742                            });
743                        }
744                    }
745                }
746            }
747        }
748        None
749    }
750
751    fn resolve_bool_local(
752        &self,
753        local: usize,
754        constraints: &FxHashMap<usize, usize>,
755    ) -> Option<usize> {
756        let mut stack = vec![(local, false)];
757        let mut seen = FxHashSet::default();
758        while let Some((cur, negated)) = stack.pop() {
759            let key = if negated { cur | (1 << 31) } else { cur };
760            if !seen.insert(key) {
761                continue;
762            }
763            if let Some(v) = self.resolve_simple_bool(cur, constraints) {
764                return Some(if negated { 1 - v } else { v });
765            }
766            for info in &self.block_info {
767                if let Some(&src) = info.constraint_copies.get(&cur) {
768                    stack.push((src, negated));
769                }
770                if let Some(&src) = info.negation_sources.get(&cur) {
771                    stack.push((src, !negated));
772                }
773                if let Some(&(lhs, rhs)) = info.and_sources.get(&cur) {
774                    let a = self.resolve_simple_bool(lhs, constraints);
775                    let b = self.resolve_simple_bool(rhs, constraints);
776                    if let (Some(a), Some(b)) = (a, b) {
777                        let r = if a == 1 && b == 1 { 1 } else { 0 };
778                        return Some(if negated { 1 - r } else { r });
779                    }
780                }
781            }
782            if let Some(&src) = self.cast_chains.get(&cur) {
783                stack.push((src, negated));
784            }
785        }
786        None
787    }
788
789    /// Check whether `cur → next` is a valid `SwitchInt` transition given
790    /// current discriminant constraints. Returns `false` when the transition
791    /// contradicts a known discriminant value. Also records newly learned
792    /// constraints from the taken branch into `constraints`.
793    fn check_switch_transition(
794        &self,
795        cur: usize,
796        next: usize,
797        constraints: &mut FxHashMap<usize, usize>,
798    ) -> bool {
799        let Some(terminator) = self.cfg.terminator(cur) else {
800            return true;
801        };
802
803        match &terminator.kind {
804            TerminatorKind::SwitchInt { discr, targets } => {
805                let discr_local = discr.place().map(|p| p.local.as_usize());
806                let constraint_local = discr_local
807                    .and_then(|l| self.disc_info.source_of.get(&l).copied())
808                    .or(discr_local);
809
810                // Collect all possible successor blocks for this switch.
811                let all_targets: FxHashSet<usize> = targets
812                    .iter()
813                    .map(|(_, bb)| bb.as_usize())
814                    .chain(std::iter::once(targets.otherwise().as_usize()))
815                    .collect();
816
817                if !all_targets.contains(&next) {
818                    return false;
819                }
820
821                // Try to evaluate a concrete constant for the discriminant.
822                let const_val = match discr {
823                    Operand::Constant(c) => c
824                        .const_
825                        .try_eval_target_usize(
826                            self.cfg.tcx,
827                            TypingEnv::post_analysis(self.cfg.tcx, self.cfg.def_id),
828                        )
829                        .map(|v| v as usize),
830                    _ => None,
831                };
832
833                if let Some(val) = const_val {
834                    // Discriminant is a literal constant — only one target is
835                    // reachable.
836                    let expected = resolve_switch_target(targets, val as u128);
837                    if next != expected {
838                        return false;
839                    }
840                    if let Some(local) = constraint_local {
841                        constraints.insert(local, val);
842                    }
843                    return true;
844                }
845
846                if let Some(local) = constraint_local {
847                    if let Some(&known_val) = constraints.get(&local) {
848                        let expected = resolve_switch_target(targets, known_val as u128);
849                        if next != expected {
850                            return false;
851                        }
852                        return true;
853                    }
854                }
855
856                // Try to infer the discriminant from a comparison source
857                // (e.g. `_X = Ne(ptr, 0)`) when we know whether `ptr` is null.
858                if let Some(discr_local) = discr_local
859                    && let Some(info) = self.block_info.get(cur)
860                    && let Some(cmp) = info.comparison_sources.get(&discr_local)
861                    && matches!(cmp.op, BinOp::Ne | BinOp::Eq)
862                {
863                    let is_ne = matches!(cmp.op, BinOp::Ne);
864                    let pointer_is_nonnull =
865                        self.local_is_known_nonnull(constraints, cmp.lhs_local);
866                    let pointer_is_null = self.local_is_known_null(constraints, cmp.lhs_local);
867                    if pointer_is_nonnull {
868                        let expected_val = if is_ne { 1 } else { 0 };
869                        let expected = resolve_switch_target(targets, expected_val);
870                        let val = expected_val as usize;
871                        constraints.insert(discr_local, val);
872                        if let Some(local) = constraint_local {
873                            constraints.insert(local, val);
874                        }
875                        if next != expected {
876                            return false;
877                        }
878                        return true;
879                    }
880                    if pointer_is_null {
881                        let expected_val = if is_ne { 0 } else { 1 };
882                        let expected = resolve_switch_target(targets, expected_val);
883                        let val = expected_val as usize;
884                        constraints.insert(discr_local, val);
885                        if let Some(local) = constraint_local {
886                            constraints.insert(local, val);
887                        }
888                        if next != expected {
889                            return false;
890                        }
891                        return true;
892                    }
893
894                    // General Eq/Ne integer comparison: resolve both operands
895                    // and prune the infeasible branch when both are known.
896                    let is_eq = matches!(cmp.op, BinOp::Eq);
897                    let lhs_val = self.resolve_local_value(cmp.lhs_local, constraints);
898                    let rhs_val = if cmp.rhs_is_constant {
899                        Some(cmp.rhs_local)
900                    } else {
901                        self.resolve_local_value(cmp.rhs_local, constraints)
902                    };
903                    if let (Some(lhs), Some(rhs)) = (lhs_val, rhs_val) {
904                        let val = if is_eq {
905                            if lhs == rhs { 1 } else { 0 }
906                        } else {
907                            if lhs != rhs { 1 } else { 0 }
908                        };
909                        let expected = resolve_switch_target(targets, val as u128);
910                        if let Some(local) = constraint_local {
911                            constraints.insert(local, val);
912                        }
913                        if next != expected {
914                            return false;
915                        }
916                        return true;
917                    }
918                }
919
920                // Try to resolve a boolean comparison with concrete-value
921                // reasoning (handles Lt/Le/Gt/Ge with fully known operands).
922                // Only filter when the discriminant resolves to a definite value
923                // from a comparison with BOTH operands as known constants
924                // (not derived via increments, which may be imprecise across
925                // SCC boundaries).
926                if let Some(discr_local) = discr_local
927                    && let Some(info) = self.block_info.get(cur)
928                    && let Some(cmp) = info.comparison_sources.get(&discr_local)
929                    && matches!(cmp.op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge)
930                {
931                    // Resolve LHS directly from constants (no increment chains)
932                    let lhs_val = self.resolve_local_value_direct(cmp.lhs_local, constraints);
933                    let rhs_val = if cmp.rhs_is_constant {
934                        Some(cmp.rhs_local)
935                    } else {
936                        self.resolve_local_value_direct(cmp.rhs_local, constraints)
937                    };
938                    if let (Some(lhs), Some(rhs)) = (lhs_val, rhs_val) {
939                        let val = match cmp.op {
940                            BinOp::Lt => if lhs < rhs { 1 } else { 0 },
941                            BinOp::Le => if lhs <= rhs { 1 } else { 0 },
942                            BinOp::Gt => if lhs > rhs { 1 } else { 0 },
943                            BinOp::Ge => if lhs >= rhs { 1 } else { 0 },
944                            _ => unreachable!(),
945                        };
946                        let expected = resolve_switch_target(targets, val as u128);
947                        if let Some(local) = constraint_local {
948                            constraints.insert(local, val);
949                        }
950                        if next != expected {
951                            return false;
952                        }
953                        return true;
954                    }
955                }
956
957                // No prior constraint — conservatively allow any valid target
958                // and record the newly learned constraint from the taken branch.
959                if next == targets.otherwise().as_usize() {
960                    if let Some(local) = constraint_local {
961                        if let Some(num_variants) = self.get_variant_count(local) {
962                            let all_covered = (0..num_variants)
963                                .all(|v| targets.iter().any(|(tv, _)| tv == v as u128));
964                            if all_covered {
965                                return false;
966                            }
967                        }
968                    }
969                }
970
971                self.learn_constraint_with_backprop(
972                    cur,
973                    constraint_local,
974                    &targets,
975                    next,
976                    constraints,
977                );
978
979                true
980            }
981            _ => true,
982        }
983    }
984
985    /// After learning a constraint for a discriminant local, propagate the
986    /// constraint backward through the copy chain so that source locals also
987    /// receive the value. This prevents losing track of the constraint when
988    /// the destination temporary is reassigned on loop back-edges.
989    fn learn_constraint_with_backprop(
990        &self,
991        cur: usize,
992        constraint_local: Option<usize>,
993        targets: &SwitchTargets,
994        next: usize,
995        constraints: &mut FxHashMap<usize, usize>,
996    ) {
997        let Some(local) = constraint_local else {
998            return;
999        };
1000        let Some((val, _)) = targets.iter().find(|(_, bb)| bb.as_usize() == next) else {
1001            if let Some(inferred) = self.infer_otherwise_value(targets, local) {
1002                constraints.insert(local, inferred);
1003                self.backprop_constraint(cur, local, inferred, constraints);
1004            }
1005            return;
1006        };
1007        let val = val as usize;
1008        constraints.insert(local, val);
1009        self.backprop_constraint(cur, local, val, constraints);
1010    }
1011
1012    fn backprop_constraint(
1013        &self,
1014        cur: usize,
1015        local: usize,
1016        val: usize,
1017        constraints: &mut FxHashMap<usize, usize>,
1018    ) {
1019        let Some(info) = self.block_info.get(cur) else {
1020            return;
1021        };
1022        let mut current = local;
1023        while let Some(&src) = info.constraint_copies.get(&current) {
1024            if current == src {
1025                break;
1026            }
1027            constraints.insert(src, val);
1028            current = src;
1029        }
1030    }
1031
1032    /// Recursively resolve a local's value through constraint copies,
1033    /// cast chains, field projections, and aggregate sources (global maps).
1034    /// Like `resolve_local_value` but does NOT follow increment chains.
1035    /// Used in conservative path filtering where we need high confidence.
1036    fn resolve_local_value_direct(
1037        &self,
1038        local: usize,
1039        constraints: &FxHashMap<usize, usize>,
1040    ) -> Option<usize> {
1041        let mut stack = vec![local];
1042        let mut seen = FxHashSet::default();
1043        while let Some(cur) = stack.pop() {
1044            if !seen.insert(cur) {
1045                continue;
1046            }
1047            if let Some(&val) = constraints.get(&cur) {
1048                if val != usize::MAX {
1049                    return Some(val);
1050                }
1051            }
1052            for info in &self.block_info {
1053                if let Some(&src) = info.constraint_copies.get(&cur) {
1054                    stack.push(src);
1055                }
1056                if let Some(&val) = info.constants.get(&cur) {
1057                    return Some(val);
1058                }
1059            }
1060            if let Some(&cast_src) = self.cast_chains.get(&cur) {
1061                stack.push(cast_src);
1062            }
1063        }
1064        None
1065    }
1066
1067    fn resolve_local_value(
1068        &self,
1069        local: usize,
1070        constraints: &FxHashMap<usize, usize>,
1071    ) -> Option<usize> {
1072        let mut stack = vec![(local, 0isize)];
1073        let mut seen = FxHashSet::default();
1074        while let Some((cur, offset)) = stack.pop() {
1075            if !seen.insert(cur) {
1076                continue;
1077            }
1078            if let Some(&val) = constraints.get(&cur) {
1079                if val == usize::MAX {
1080                    // Sentinel for known-nonnull; not a concrete integer value.
1081                } else if offset >= 0 {
1082                    return Some(val + offset as usize);
1083                } else {
1084                    return val.checked_sub((-offset) as usize);
1085                }
1086            }
1087            // Follow constraint copies in any block (per-block metadata).
1088            for info in &self.block_info {
1089                if let Some(&src) = info.constraint_copies.get(&cur) {
1090                    stack.push((src, offset));
1091                }
1092                if let Some(&val) = info.constants.get(&cur) {
1093                    if offset >= 0 {
1094                        return Some(val + offset as usize);
1095                    } else {
1096                        return val.checked_sub((-offset) as usize);
1097                    }
1098                }
1099                // Follow increments: if cur = src + incr, the effective value
1100                // of cur is value_of(src) + incr + offset.
1101                if let Some(&(incr_src, incr_amt)) = info.increments.get(&cur) {
1102                    stack.push((incr_src, offset + incr_amt as isize));
1103                }
1104                // Follow remainder: cur = rem_src % rem_div.
1105                // Resolve rem_src recursively, compute remainder, then apply offset.
1106                if let Some(&(rem_src, rem_div)) = info.remainders.get(&cur) {
1107                    if let Some(src_val) = self.resolve_local_value(rem_src, constraints) {
1108                        let rem = src_val % rem_div;
1109                        let result = if offset >= 0 {
1110                            Some(rem + offset as usize)
1111                        } else {
1112                            rem.checked_sub((-offset) as usize)
1113                        };
1114                        if let Some(v) = result {
1115                            return Some(v);
1116                        }
1117                    }
1118                }
1119            }
1120            // Follow global cast chains.
1121            if let Some(&cast_src) = self.cast_chains.get(&cur) {
1122                stack.push((cast_src, offset));
1123            }
1124            // Follow field projection -> aggregate source.
1125            if let Some(&encoded) = self.field_projection_source.get(&cur) {
1126                if let Some((agg_local, field_idx)) = decode_aggregate_field(encoded) {
1127                    let key = encode_aggregate_field(agg_local, field_idx);
1128                    if let Some(&source) = self.aggregate_field_sources.get(&key) {
1129                        stack.push((source, offset));
1130                    }
1131                }
1132            }
1133        }
1134        None
1135    }
1136
1137    /// For the "otherwise" branch of a `SwitchInt`, try to infer the single
1138    /// concrete value that the discriminant must have (because all other
1139    /// possible values are covered by explicit targets).
1140    fn infer_otherwise_value(&self, targets: &SwitchTargets, discr_local: usize) -> Option<usize> {
1141        let body = self.cfg.tcx.optimized_mir(self.cfg.def_id);
1142        let mut discr_ty = body.local_decls[Local::from_usize(discr_local)].ty;
1143        while let TyKind::Ref(_, inner, _) | TyKind::RawPtr(inner, _) = discr_ty.kind() {
1144            discr_ty = *inner;
1145        }
1146
1147        let possible_values: Vec<usize> = match discr_ty.kind() {
1148            TyKind::Bool => vec![0, 1],
1149            TyKind::Adt(adt_def, _) if adt_def.is_enum() => (0..adt_def.variants().len()).collect(),
1150            _ => return None,
1151        };
1152
1153        let explicit_values: FxHashSet<usize> = targets.iter().map(|(v, _)| v as usize).collect();
1154        let remaining: Vec<usize> = possible_values
1155            .into_iter()
1156            .filter(|v| !explicit_values.contains(v))
1157            .collect();
1158
1159        if remaining.len() == 1 {
1160            Some(remaining[0])
1161        } else {
1162            None
1163        }
1164    }
1165
1166    /// Check whether `next` is an unwind target reachable from `cur` via a
1167    /// call or drop terminator (may not be recorded as a normal CFG successor).
1168    fn is_unwind_target(&self, cur: usize, next: usize) -> bool {
1169        let Some(terminator) = self.cfg.terminator(cur) else {
1170            return false;
1171        };
1172
1173        let unwind = match &terminator.kind {
1174            TerminatorKind::Call { unwind, .. }
1175            | TerminatorKind::Drop { unwind, .. }
1176            | TerminatorKind::Assert { unwind, .. } => unwind,
1177            _ => return false,
1178        };
1179
1180        if let UnwindAction::Cleanup(target) = unwind {
1181            return target.as_usize() == next;
1182        }
1183        false
1184    }
1185
1186    /// Return true if `local` is known to be a non-null pointer.
1187    fn local_is_known_nonnull(&self, constraints: &FxHashMap<usize, usize>, local: usize) -> bool {
1188        let Some(&val) = constraints.get(&local) else {
1189            return false;
1190        };
1191        val > 0
1192    }
1193
1194    /// Return true if `local` is known to be a null pointer.
1195    fn local_is_known_null(&self, constraints: &FxHashMap<usize, usize>, local: usize) -> bool {
1196        let Some(&val) = constraints.get(&local) else {
1197            return false;
1198        };
1199        val == 0
1200    }
1201
1202    /// Populate the `child_sccs` field for a given SCC entry block, then
1203    /// recurse into those child SCCs. Called eagerly from `find_scc()` so
1204    /// that enumeration can be purely read-only on the graph.
1205    fn populate_child_sccs(&mut self, enter: usize) {
1206        let nodes: Vec<usize> = self.cfg.block(enter).scc.nodes.iter().cloned().collect();
1207        let mut child_enters = Vec::new();
1208        let mut seen = FxHashSet::default();
1209
1210        for node in nodes {
1211            if let Some(block) = self.cfg.blocks.get(node) {
1212                let node_enter = block.scc.enter;
1213                let non_trivial = !block.scc.nodes.is_empty();
1214                if node_enter != enter && non_trivial && seen.insert(node_enter) {
1215                    child_enters.push(node_enter);
1216                }
1217            }
1218        }
1219
1220        self.cfg.block_mut(enter).scc.child_sccs = child_enters;
1221
1222        let child_count = self.cfg.block(enter).scc.child_sccs.len();
1223        for i in 0..child_count {
1224            let child_enter = self.cfg.block(enter).scc.child_sccs[i];
1225            self.populate_child_sccs(child_enter);
1226        }
1227    }
1228
1229    fn populate_all_child_sccs(&mut self) {
1230        let mut visited = FxHashSet::default();
1231        let block_count = self.cfg.blocks.len();
1232        for i in 0..block_count {
1233            let scc = &self.cfg.block(i).scc;
1234            let enter = scc.enter;
1235            if scc.nodes.is_empty() || !visited.insert(enter) {
1236                continue;
1237            }
1238            self.populate_child_sccs(enter);
1239        }
1240    }
1241}
1242
1243/// Hash of the constraint state accumulated along a path prefix.
1244///
1245/// Computed by walking the prefix, collecting `(local → constant_value)`
1246/// bindings from each block's [`BlockConstantInfo`], sorting them, and
1247/// hashing the result.  Used by [`PathEnumerator::visited_sccs`] to
1248/// skip redundant re-entries into the same SCC with the same state.
1249#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
1250pub struct ConstraintHash(u64);
1251
1252impl ConstraintHash {
1253    fn from_path(path: &[usize], graph: &PathGraph<'_>) -> Self {
1254        let mut hasher = DefaultHasher::new();
1255        let mut constraints: FxHashMap<usize, usize> = FxHashMap::default();
1256
1257        for &block in path.iter() {
1258            if let Some(info) = graph.block_info.get(block) {
1259                for local in &info.assigned_locals {
1260                    if let Some(&src) = info.constraint_copies.get(local) {
1261                        if let Some(&src_val) = constraints.get(&src) {
1262                            constraints.insert(*local, src_val);
1263                            continue;
1264                        }
1265                        if let Some(&dst_val) = constraints.get(local) {
1266                            constraints.insert(src, dst_val);
1267                            constraints.insert(*local, dst_val);
1268                            continue;
1269                        }
1270                    }
1271                    if let Some(&val) = info.constants.get(local) {
1272                        constraints.insert(*local, val);
1273                        continue;
1274                    }
1275                    constraints.remove(local);
1276                }
1277            }
1278        }
1279
1280        let mut entries: Vec<(usize, usize)> = constraints.into_iter().collect();
1281        entries.sort();
1282        entries.hash(&mut hasher);
1283        ConstraintHash(hasher.finish())
1284    }
1285}
1286
1287/// Key for [`PathEnumerator::scc_paths`] and [`PathEnumerator::visited_sccs`] and [`PathEnumerator::visited_sccs`]:
1288/// which SCC entry block, with what constraint state, and how many
1289/// additional postfix repeats are allowed.
1290///
1291/// In `scc_paths` the `constraint` field is unused (cache keyed by
1292/// `entry` + `repeat`); in `visited_sccs` the `repeat` field is
1293/// unused (deduplicated by `entry` + `constraint`).
1294#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1295pub struct SccKey {
1296    pub entry: usize,
1297    pub repeat: usize,
1298    pub constraint: ConstraintHash,
1299}
1300
1301/// Builds a [`PathTree`] by depth-first enumeration of whole-CFG paths.
1302///
1303/// The enumerator holds a `&PathGraph` (which must have `find_scc()` called
1304/// beforehand) and two caches:
1305///
1306/// The **constraint hash** used by `visited_sccs` is a
1307/// [`ConstraintHash`]: it walks the current path prefix, accumulates
1308/// `(local → constant_value)` bindings from each block's
1309/// [`BlockConstantInfo`], sorts them, and hashes the result.  Different
1310/// constraint states (e.g. a loop variable changing from `true` to
1311/// `false`) produce different hashes.
1312///
1313/// 1. `scc_paths` — maps [`SccKey`] to a list of acyclic paths through
1314///    that SCC.  Reused across repeated enumerations of the same function
1315///    with different repeat counts.
1316///
1317/// 2. `visited_sccs` — set of [`SccKey`] entries already explored
1318///    (matched by `entry` + `constraint`, ignoring `repeat`).
1319///    When the same SCC is reached with the same constraint state, its
1320///    sub-paths are identical, so the re-entry is skipped.
1321///
1322/// Constraint-based filtering runs incrementally during DFS via
1323/// [`PathGraph::check_transition`], so only feasible paths are inserted
1324/// into the resulting tree.
1325pub struct PathEnumerator<'g, 'tcx> {
1326    graph: &'g PathGraph<'tcx>,
1327    scc_paths: FxHashMap<SccKey, Vec<SccPath>>,
1328    visited_sccs: FxHashSet<SccKey>,
1329}
1330
1331impl<'g, 'tcx> PathEnumerator<'g, 'tcx> {
1332    pub fn new(graph: &'g PathGraph<'tcx>) -> Self {
1333        PathEnumerator {
1334            graph,
1335            scc_paths: FxHashMap::default(),
1336            visited_sccs: FxHashSet::default(),
1337        }
1338    }
1339
1340    /// Enumerate all whole-CFG paths, pruning infeasible transitions via
1341    /// incremental constraint-based filtering during DFS.
1342    ///
1343    /// SCC regions are flattened into a bounded set of acyclic paths.
1344    pub fn enumerate_paths(&mut self) -> PathTree {
1345        self.enumerate_paths_repeat(0)
1346    }
1347
1348    /// Enumerate whole-CFG paths allowing each SCC postfix segment to repeat
1349    /// up to `postfix_repeat` additional times. `postfix_repeat = 0` gives
1350    /// the same result as `enumerate_paths`.
1351    pub fn enumerate_paths_repeat(&mut self, postfix_repeat: usize) -> PathTree {
1352        let mut tree = PathTree::new();
1353
1354        if self.graph.cfg.blocks.is_empty() {
1355            return tree;
1356        }
1357
1358        self.collect_whole_cfg_paths(
1359            0,
1360            &mut vec![0],
1361            &mut tree,
1362            0,
1363            postfix_repeat,
1364            &FxHashMap::default(),
1365        );
1366
1367        tree
1368    }
1369
1370    /// Enumerate all acyclic paths through `scc` starting at `start`,
1371    /// allowing each postfix segment to repeat up to `postfix_repeat`
1372    /// additional times.
1373    ///
1374    /// Results are cached per `(def_id, scc_enter, postfix_repeat)`.
1375    pub fn find_scc_paths_repeat(
1376        &mut self,
1377        start: usize,
1378        scc: &SccInfo,
1379        postfix_repeat: usize,
1380    ) -> Vec<SccPath> {
1381        let cache_key = SccKey {
1382            entry: scc.enter,
1383            repeat: postfix_repeat,
1384            constraint: ConstraintHash::default(),
1385        };
1386        if let Some(cached) = self.scc_paths.get(&cache_key) {
1387            return cached.clone();
1388        }
1389
1390        let mut out = Vec::new();
1391        let mut seen: FxHashSet<Vec<usize>> = FxHashSet::default();
1392        let mut path = vec![start];
1393        let mut segment_counts = FxHashMap::default();
1394
1395        self.dfs_scc_tree(
1396            scc,
1397            start,
1398            &mut path,
1399            &mut segment_counts,
1400            postfix_repeat,
1401            &mut out,
1402            &mut seen,
1403            0,
1404        );
1405
1406        if self.scc_paths.len() >= SCC_PATH_CACHE_LIMIT {
1407            self.scc_paths.clear();
1408        }
1409        self.scc_paths.insert(cache_key, out.clone());
1410
1411        out
1412    }
1413
1414    /// Recursive DFS through one level of the SCC tree.
1415    ///
1416    /// Enumerates structurally possible paths through the SCC to exit points.
1417    /// No constraint tracking — `check_postfix_segment` prunes repeated
1418    /// loop-body segments purely by block-id sequence.
1419    ///
1420    /// When `postfix_repeat > 0`, allows the same postfix segment to repeat
1421    /// up to `postfix_repeat` additional times beyond the first occurrence.
1422    ///
1423    /// Child SCC paths are pre-enumerated via `find_scc_paths_repeat` and treated as
1424    /// atomic building blocks (no recursive descent into child SCC internals).
1425    #[allow(clippy::too_many_arguments)]
1426    fn dfs_scc_tree(
1427        &mut self,
1428        scc: &SccInfo,
1429        cur: usize,
1430        path: &mut Vec<usize>,
1431        segment_counts: &mut FxHashMap<Vec<usize>, usize>,
1432        postfix_repeat: usize,
1433        out: &mut Vec<SccPath>,
1434        seen_paths: &mut FxHashSet<Vec<usize>>,
1435        depth: usize,
1436    ) {
1437        if depth > SCC_MAX_DEPTH {
1438            return;
1439        }
1440        if out.len() >= SCC_MAX_SEEN_PATHS {
1441            return;
1442        }
1443        if path.len() > SCC_MAX_PATH_LEN {
1444            return;
1445        }
1446        if cur != scc.enter && !scc.nodes.contains(&cur) {
1447            return;
1448        }
1449
1450        if cur == scc.enter && path.len() > 1 {
1451            if !check_postfix_segment(path, scc.enter, segment_counts, postfix_repeat) {
1452                if (postfix_repeat > 0 || segment_counts.len() > 1)
1453                    && scc.exits.iter().any(|e| e.exit == cur)
1454                {
1455                    self.record_unique_path(path, scc, out, seen_paths);
1456                }
1457                return;
1458            }
1459        }
1460
1461        if scc.exits.iter().any(|e| e.exit == cur) {
1462            self.record_unique_path(path, scc, out, seen_paths);
1463        }
1464
1465        let is_child = scc.child_sccs.contains(&cur);
1466
1467        if is_child {
1468            let ctx = self.constraint_context(path);
1469            if !self.visited_sccs.insert(SccKey {
1470                entry: cur,
1471                repeat: 0,
1472                constraint: ctx,
1473            }) {
1474                return;
1475            }
1476
1477            let child_scc = self.graph.cfg_block(cur).scc.clone();
1478            let child_paths = self.find_scc_paths_repeat(cur, &child_scc, postfix_repeat);
1479
1480            for child_path in &child_paths {
1481                let orig_len = path.len();
1482                if child_path.blocks.len() > 1 {
1483                    path.extend(&child_path.blocks[1..]);
1484                }
1485
1486                let mut branch_counts = segment_counts.clone();
1487                for &next in &child_path.exit_successors {
1488                    path.push(next);
1489                    self.dfs_scc_tree(
1490                        scc,
1491                        next,
1492                        path,
1493                        &mut branch_counts,
1494                        postfix_repeat,
1495                        out,
1496                        seen_paths,
1497                        depth + 1,
1498                    );
1499                    path.pop();
1500                }
1501                path.truncate(orig_len);
1502            }
1503            return;
1504        }
1505
1506        let successors: Vec<usize> = self.graph.cfg.block(cur).next.iter().copied().collect();
1507        let saved_counts = segment_counts.clone();
1508        for next in successors {
1509            if next != scc.enter && !scc.nodes.contains(&next) {
1510                self.record_unique_path(path, scc, out, seen_paths);
1511                continue;
1512            }
1513            let mut branch_counts = saved_counts.clone();
1514            path.push(next);
1515            self.dfs_scc_tree(
1516                scc,
1517                next,
1518                path,
1519                &mut branch_counts,
1520                postfix_repeat,
1521                out,
1522                seen_paths,
1523                depth + 1,
1524            );
1525            path.pop();
1526        }
1527    }
1528
1529    /// Build a [`ConstraintHash`] from the constraint state along `path`.
1530    fn constraint_context(&self, path: &[usize]) -> ConstraintHash {
1531        ConstraintHash::from_path(path, self.graph)
1532    }
1533
1534    /// Depth-first enumeration of all CFG paths from `current` to a terminator.
1535    ///
1536    /// SCC nodes are flattened via `find_scc_paths_repeat`; non-SCC blocks are followed
1537    /// one by one.  No cycle detection is needed because the post-SCC CFG is a DAG.
1538    /// Constraints are maintained incrementally — each transition is checked
1539    /// via `PathGraph::check_transition` before recursing, and infeasible
1540    /// branches are pruned early.
1541    fn collect_whole_cfg_paths(
1542        &mut self,
1543        current: usize,
1544        path: &mut Vec<usize>,
1545        tree: &mut PathTree,
1546        depth: usize,
1547        postfix_repeat: usize,
1548        constraints: &FxHashMap<usize, usize>,
1549    ) {
1550        if current >= self.graph.cfg.blocks.len() {
1551            return;
1552        }
1553        if depth > WHOLE_CFG_PATH_DEPTH_LIMIT || tree.len() >= WHOLE_CFG_PATH_LIMIT {
1554            return;
1555        }
1556
1557        let scc_info = self.graph.cfg_block(current).scc.clone();
1558        let is_scc = current == scc_info.enter && !scc_info.nodes.is_empty();
1559        if is_scc {
1560            let scc = self.sort_scc_tree(&scc_info);
1561            let segments = self.find_scc_paths_repeat(current, &scc, postfix_repeat);
1562
1563            if segments.is_empty() {
1564                tree.insert(path);
1565                return;
1566            }
1567
1568            for seg in segments {
1569                if tree.len() >= WHOLE_CFG_PATH_LIMIT {
1570                    break;
1571                }
1572
1573                let orig_len = path.len();
1574                let mut seg_constraints = constraints.clone();
1575                let mut reachable = true;
1576
1577                if seg.blocks.len() > 1 {
1578                    for i in 0..seg.blocks.len() - 1 {
1579                        if !self.graph.check_transition(
1580                            seg.blocks[i],
1581                            seg.blocks[i + 1],
1582                            &mut seg_constraints,
1583                        ) {
1584                            reachable = false;
1585                            break;
1586                        }
1587                    }
1588                    if reachable {
1589                        path.extend_from_slice(&seg.blocks[1..]);
1590                    }
1591                }
1592
1593                if reachable {
1594                    if seg.exit_successors.is_empty() {
1595                        tree.insert(path);
1596                    } else {
1597                        for &next in &seg.exit_successors {
1598                            let mut next_constraints = seg_constraints.clone();
1599                            let last = *path.last().unwrap();
1600                            if self
1601                                .graph
1602                                .check_transition(last, next, &mut next_constraints)
1603                            {
1604                                path.push(next);
1605                                self.collect_whole_cfg_paths(
1606                                    next,
1607                                    path,
1608                                    tree,
1609                                    depth + 1,
1610                                    postfix_repeat,
1611                                    &next_constraints,
1612                                );
1613                                path.pop();
1614                            }
1615                        }
1616                    }
1617                }
1618
1619                path.truncate(orig_len);
1620            }
1621            return;
1622        }
1623
1624        // Non-SCC block: follow CFG successors.
1625        let successors: Vec<usize> = self.graph.cfg_block(current).next.iter().copied().collect();
1626        if successors.is_empty() {
1627            tree.insert(path);
1628            return;
1629        }
1630
1631        for next in successors {
1632            let mut next_constraints = constraints.clone();
1633            if self
1634                .graph
1635                .check_transition(current, next, &mut next_constraints)
1636            {
1637                path.push(next);
1638                self.collect_whole_cfg_paths(
1639                    next,
1640                    path,
1641                    tree,
1642                    depth + 1,
1643                    postfix_repeat,
1644                    &next_constraints,
1645                );
1646                path.pop();
1647            }
1648        }
1649    }
1650
1651    fn sort_scc_tree(&self, scc: &SccInfo) -> SccInfo {
1652        self.graph.cfg_block(scc.enter).scc.clone()
1653    }
1654
1655    fn record_unique_path(
1656        &self,
1657        path: &[usize],
1658        scc: &SccInfo,
1659        out: &mut Vec<SccPath>,
1660        seen_paths: &mut FxHashSet<Vec<usize>>,
1661    ) {
1662        if !seen_paths.insert(path.to_vec()) {
1663            return;
1664        }
1665        let exit_successors = self.compute_exit_successors(path, scc);
1666        out.push(SccPath {
1667            blocks: path.to_vec(),
1668            exit_successors,
1669        });
1670    }
1671
1672    fn compute_exit_successors(&self, path: &[usize], scc: &SccInfo) -> Vec<usize> {
1673        let Some(&last) = path.last() else {
1674            return vec![];
1675        };
1676        scc.exits
1677            .iter()
1678            .filter(|e| e.exit == last)
1679            .map(|e| e.to)
1680            .filter(|&n| {
1681                !scc.child_sccs
1682                    .contains(&self.graph.cfg.block(n).scc.enter())
1683            })
1684            .collect()
1685    }
1686}
1687
1688/// Resolve a concrete discriminant value to the corresponding `SwitchInt`
1689/// successor block index.
1690fn resolve_switch_target(targets: &SwitchTargets, val: u128) -> usize {
1691    targets
1692        .iter()
1693        .find(|(v, _)| *v == val)
1694        .map(|(_, bb)| bb.as_usize())
1695        .unwrap_or_else(|| targets.otherwise().as_usize())
1696}