rapx/analysis/path_analysis/
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, Rvalue, StatementKind, SwitchTargets,
10        Terminator, TerminatorKind, 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 boolean local (e.g., a guard result) to the binary comparison
81    /// that produced it: `(op, lhs_local, rhs_kind)`.
82    pub comparison_sources: FxHashMap<usize, ComparisonSource>,
83}
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}
93
94/// Enum discriminant metadata used by [`check_switch_transition`].
95///
96/// `source_of` maps a discriminant local to the ADT local it was read from
97/// (via `Rvalue::Discriminant`).  `variant_count_of` tracks the number of
98/// variants for each ADT local, used to determine whether a `SwitchInt`
99/// otherwise-target is uniquely determined.
100#[derive(Clone, Debug, Default)]
101pub struct DiscriminantInfo {
102    pub source_of: FxHashMap<usize, usize>,
103    pub variant_count_of: FxHashMap<usize, usize>,
104}
105
106/// CFG augmented with per-block constant info and discriminant metadata
107/// for path reachability analysis.
108///
109/// `PathGraph` wraps a `ControlFlowGraph` and adds block-indexed data
110/// that track assignments, constants, and copy chains.  These are used by
111/// [`check_transition`](PathGraph::check_transition) to update a set of
112/// discriminant constraints while traversing the CFG, enabling early
113/// pruning of infeasible `SwitchInt` branches during path enumeration.
114#[derive(Clone)]
115pub struct PathGraph<'tcx> {
116    pub cfg: ControlFlowGraph<'tcx>,
117    pub block_info: Vec<BlockConstantInfo>,
118    pub disc_info: DiscriminantInfo,
119}
120
121impl<'tcx> PathGraph<'tcx> {
122    pub fn new(tcx: TyCtxt<'tcx>, def_id: DefId) -> PathGraph<'tcx> {
123        let body = tcx.optimized_mir(def_id);
124        let basicblocks = &body.basic_blocks;
125        let mut cfg_blocks = Vec::<CfgBlock>::new();
126        let mut block_info = Vec::new();
127        let mut disc_info = DiscriminantInfo::default();
128
129        for i in 0..basicblocks.len() {
130            let bb = &basicblocks[BasicBlock::from(i)];
131            let mut cfg_block = CfgBlock::new(i, bb.is_cleanup);
132            let mut info = BlockConstantInfo::default();
133
134            for stmt in &bb.statements {
135                if let StatementKind::Assign(assign) = &stmt.kind {
136                    let (place, rvalue) = &**assign;
137                    let dest = place.local.as_usize();
138                    info.assigned_locals.insert(dest);
139                    match rvalue {
140                        Rvalue::Use(Operand::Constant(c), ..) => {
141                            let typing_env = TypingEnv::post_analysis(tcx, def_id);
142                            let val = match c.const_.ty().kind() {
143                                TyKind::Bool => c
144                                    .const_
145                                    .try_eval_bool(tcx, typing_env)
146                                    .map(|b| if b { 1 } else { 0 }),
147                                TyKind::Int(_) | TyKind::Uint(_) => {
148                                    c.const_.try_eval_bits(tcx, typing_env).map(|v| v as usize)
149                                }
150                                _ => None,
151                            };
152                            if let Some(val) = val {
153                                info.constants.insert(dest, val);
154                            }
155                        }
156                        Rvalue::Use(Operand::Copy(src) | Operand::Move(src), ..) => {
157                            info.constraint_copies.insert(dest, src.local.as_usize());
158                        }
159                        Rvalue::Discriminant(rv_place) => {
160                            disc_info.source_of.insert(dest, rv_place.local.as_usize());
161                            let src_local = rv_place.local.as_usize();
162                            if !disc_info.variant_count_of.contains_key(&src_local) {
163                                let src_ty = body.local_decls[rv_place.local].ty;
164                                if let TyKind::Adt(adt_def, _) = src_ty.kind() {
165                                    let num = adt_def.variants().len();
166                                    if num > 0 {
167                                        disc_info.variant_count_of.insert(src_local, num);
168                                    }
169                                }
170                            }
171                        }
172                        Rvalue::Aggregate(kind, _) => {
173                            if let AggregateKind::Adt(_, variant_idx, _, _, _) = kind.as_ref() {
174                                let discr = variant_idx.as_usize();
175                                info.constants.insert(dest, discr);
176                                if !disc_info.variant_count_of.contains_key(&dest) {
177                                    let dest_ty = body.local_decls[place.local].ty;
178                                    if let TyKind::Adt(adt_def, _) = dest_ty.kind() {
179                                        let num = adt_def.variants().len();
180                                        if num > 0 {
181                                            disc_info.variant_count_of.insert(dest, num);
182                                        }
183                                    }
184                                }
185                            }
186                        }
187                        Rvalue::BinaryOp(op, operands)
188                            if matches!(
189                                op,
190                                BinOp::Lt
191                                    | BinOp::Le
192                                    | BinOp::Gt
193                                    | BinOp::Ge
194                                    | BinOp::Eq
195                                    | BinOp::Ne
196                            ) =>
197                        {
198                            let (lhs, rhs): (&Operand<'_>, &Operand<'_>) = (&operands.0, &operands.1);
199                            let (lhs_local, rhs_local) =
200                                match (lhs, rhs) {
201                                    (Operand::Copy(l) | Operand::Move(l), Operand::Copy(r) | Operand::Move(r)) => {
202                                        (l.local, r.local)
203                                    }
204                                    _ => {
205                                        continue;
206                                    }
207                                };
208                            let lhs_local = lhs_local.as_usize();
209                            let rhs_local = rhs_local.as_usize();
210                            info.comparison_sources.insert(
211                                dest,
212                                ComparisonSource {
213                                    op: *op,
214                                    lhs_local,
215                                    rhs_local,
216                                },
217                            );
218                        }
219                        _ => {}  // close match rvalue
220                    }
221                }
222            }
223
224            let Some(terminator) = &bb.terminator else {
225                continue;
226            };
227
228            match terminator.kind.clone() {
229                TerminatorKind::Goto { ref target } => {
230                    cfg_block.add_next(target.as_usize());
231                }
232                TerminatorKind::SwitchInt {
233                    discr: _,
234                    ref targets,
235                } => {
236                    for (_, ref target) in targets.iter() {
237                        cfg_block.add_next(target.as_usize());
238                    }
239                    cfg_block.add_next(targets.otherwise().as_usize());
240                }
241                TerminatorKind::Drop {
242                    place: _,
243                    target,
244                    unwind,
245                    replace: _,
246                    drop: _,
247                    #[cfg(not(rapx_rustc_ge_198))]
248                        async_fut: _,
249                } => {
250                    cfg_block.add_next(target.as_usize());
251                    if let UnwindAction::Cleanup(target) = unwind {
252                        cfg_block.add_next(target.as_usize());
253                    }
254                }
255                TerminatorKind::Call {
256                    ref target,
257                    ref unwind,
258                    ..
259                } => {
260                    if let Some(tt) = target {
261                        cfg_block.add_next(tt.as_usize());
262                    }
263                    if let UnwindAction::Cleanup(tt) = unwind {
264                        cfg_block.add_next(tt.as_usize());
265                    }
266                }
267                TerminatorKind::Assert {
268                    cond: _,
269                    expected: _,
270                    msg: _,
271                    ref target,
272                    ref unwind,
273                } => {
274                    cfg_block.add_next(target.as_usize());
275                    if let UnwindAction::Cleanup(target) = unwind {
276                        cfg_block.add_next(target.as_usize());
277                    }
278                }
279                TerminatorKind::Yield {
280                    value: _,
281                    ref resume,
282                    resume_arg: _,
283                    ref drop,
284                } => {
285                    cfg_block.add_next(resume.as_usize());
286                    if let Some(target) = drop {
287                        cfg_block.add_next(target.as_usize());
288                    }
289                }
290                TerminatorKind::FalseEdge {
291                    ref real_target,
292                    imaginary_target: _,
293                } => {
294                    cfg_block.add_next(real_target.as_usize());
295                }
296                TerminatorKind::FalseUnwind {
297                    ref real_target,
298                    unwind: _,
299                } => {
300                    cfg_block.add_next(real_target.as_usize());
301                }
302                TerminatorKind::InlineAsm {
303                    template: _,
304                    operands: _,
305                    options: _,
306                    line_spans: _,
307                    ref unwind,
308                    targets,
309                    asm_macro: _,
310                } => {
311                    for target in targets {
312                        cfg_block.add_next(target.as_usize());
313                    }
314                    if let UnwindAction::Cleanup(target) = unwind {
315                        cfg_block.add_next(target.as_usize());
316                    }
317                }
318                _ => {}
319            }
320
321            cfg_blocks.push(cfg_block);
322            block_info.push(info);
323        }
324
325        let cfg = ControlFlowGraph::new(def_id, tcx, cfg_blocks);
326
327        PathGraph {
328            cfg,
329            block_info,
330            disc_info,
331        }
332    }
333
334    pub fn find_scc(&mut self) {
335        self.cfg.find_scc();
336        self.populate_all_child_sccs();
337    }
338
339    pub fn def_id(&self) -> DefId {
340        self.cfg.def_id
341    }
342
343    pub fn tcx(&self) -> TyCtxt<'tcx> {
344        self.cfg.tcx
345    }
346
347    pub fn cfg_block(&self, index: usize) -> &CfgBlock {
348        self.cfg.block(index)
349    }
350
351    pub fn cfg_block_mut(&mut self, index: usize) -> &mut CfgBlock {
352        self.cfg.block_mut(index)
353    }
354
355    /// Retrieve the MIR terminator for the block at `index` on demand.
356    pub fn terminator(&self, index: usize) -> Option<&Terminator<'tcx>> {
357        self.cfg.terminator(index)
358    }
359
360    pub fn is_cleanup_block(&self, index: usize) -> bool {
361        self.cfg
362            .blocks
363            .get(index)
364            .map(|b| b.is_cleanup)
365            .unwrap_or(false)
366    }
367
368    /// Get the number of variants for a constraint local.
369    /// First checks the pre-populated `variant_count_of` hashmap,
370    /// then falls back to the local's declared type (for ADT locals
371    /// that gained their type through field projections in nested
372    /// destructuring patterns rather than explicit construction).
373    fn get_variant_count(&self, local: usize) -> Option<usize> {
374        if let Some(&count) = self.disc_info.variant_count_of.get(&local) {
375            return Some(count);
376        }
377        let body = self.cfg.tcx.optimized_mir(self.cfg.def_id);
378        let mut ty = body.local_decls[Local::from_usize(local)].ty;
379        while let TyKind::Ref(_, inner_ty, _) | TyKind::RawPtr(inner_ty, _) = ty.kind() {
380            ty = *inner_ty;
381        }
382        match ty.kind() {
383            TyKind::Adt(adt_def, _) if adt_def.is_enum() => Some(adt_def.variants().len()),
384            _ => None,
385        }
386    }
387
388    /// Check a single transition `cur -> next` for reachability and update
389    /// discriminant constraints. Returns `false` if the transition is
390    /// provably unreachable.
391    pub fn check_transition(
392        &self,
393        cur: usize,
394        next: usize,
395        constraints: &mut FxHashMap<usize, usize>,
396    ) -> bool {
397        if cur >= self.cfg.blocks.len() || next >= self.cfg.blocks.len() {
398            return false;
399        }
400
401        if let Some(info) = self.block_info.get(cur) {
402            for local in &info.assigned_locals {
403                if let Some(&src) = info.constraint_copies.get(local) {
404                    if let Some(&src_val) = constraints.get(&src) {
405                        constraints.insert(*local, src_val);
406                        continue;
407                    }
408                    if let Some(&dst_val) = constraints.get(local) {
409                        constraints.insert(src, dst_val);
410                        constraints.insert(*local, dst_val);
411                        continue;
412                    }
413                }
414                if let Some(&val) = info.constants.get(local) {
415                    constraints.insert(*local, val);
416                    continue;
417                }
418                constraints.remove(local);
419            }
420        }
421
422        // Also clear constraints for locals assigned by the terminator
423        // (e.g. _13 = Iterator::next() in a Call terminator). The block's
424        // assigned_locals only covers statement-level assignments, so
425        // terminator-side assignments are handled here.
426        if let Some(terminator) = self.terminator(cur) {
427            let assigned = match &terminator.kind {
428                TerminatorKind::Call {
429                    destination, ..
430                } => Some(destination.local.as_usize()),
431                TerminatorKind::Yield {
432                    resume_arg, ..
433                } => Some(resume_arg.local.as_usize()),
434                _ => None,
435            };
436            if let Some(local) = assigned {
437                constraints.remove(&local);
438            }
439        }
440
441        let successors = &self.cfg.block(cur).next;
442        if !successors.contains(&next) {
443            if !self.is_unwind_target(cur, next) {
444                return false;
445            }
446        }
447
448        if !self.check_switch_transition(cur, next, constraints) {
449            return false;
450        }
451
452        true
453    }
454
455    /// Check whether `cur → next` is a valid `SwitchInt` transition given
456    /// current discriminant constraints. Returns `false` when the transition
457    /// contradicts a known discriminant value. Also records newly learned
458    /// constraints from the taken branch into `constraints`.
459    fn check_switch_transition(
460        &self,
461        cur: usize,
462        next: usize,
463        constraints: &mut FxHashMap<usize, usize>,
464    ) -> bool {
465        let Some(terminator) = self.cfg.terminator(cur) else {
466            return true;
467        };
468
469        match &terminator.kind {
470            TerminatorKind::SwitchInt { discr, targets } => {
471                let discr_local = discr.place().map(|p| p.local.as_usize());
472                let constraint_local = discr_local
473                    .and_then(|l| self.disc_info.source_of.get(&l).copied())
474                    .or(discr_local);
475
476                // Collect all possible successor blocks for this switch.
477                let all_targets: FxHashSet<usize> = targets
478                    .iter()
479                    .map(|(_, bb)| bb.as_usize())
480                    .chain(std::iter::once(targets.otherwise().as_usize()))
481                    .collect();
482
483                if !all_targets.contains(&next) {
484                    return false;
485                }
486
487                // Try to evaluate a concrete constant for the discriminant.
488                let const_val = match discr {
489                    Operand::Constant(c) => c
490                        .const_
491                        .try_eval_target_usize(
492                            self.cfg.tcx,
493                            TypingEnv::post_analysis(self.cfg.tcx, self.cfg.def_id),
494                        )
495                        .map(|v| v as usize),
496                    _ => None,
497                };
498
499                if let Some(val) = const_val {
500                    // Discriminant is a literal constant — only one target is
501                    // reachable.
502                    let expected = resolve_switch_target(targets, val as u128);
503                    if next != expected {
504                        return false;
505                    }
506                    if let Some(local) = constraint_local {
507                        constraints.insert(local, val);
508                    }
509                    return true;
510                }
511
512                if let Some(local) = constraint_local {
513                    if let Some(&known_val) = constraints.get(&local) {
514                        let expected = resolve_switch_target(targets, known_val as u128);
515                        if next != expected {
516                            return false;
517                        }
518                        return true;
519                    }
520                }
521
522                // No prior constraint — conservatively allow any valid target
523                // and record the newly learned constraint from the taken branch.
524                if next == targets.otherwise().as_usize() {
525                    if let Some(local) = constraint_local {
526                        if let Some(num_variants) = self.get_variant_count(local) {
527                            let all_covered = (0..num_variants)
528                                .all(|v| targets.iter().any(|(tv, _)| tv == v as u128));
529                            if all_covered {
530                                return false;
531                            }
532                        }
533                    }
534                }
535
536                self.learn_constraint_with_backprop(
537                    cur,
538                    constraint_local,
539                    &targets,
540                    next,
541                    constraints,
542                );
543
544                true
545            }
546            _ => true,
547        }
548    }
549
550    /// After learning a constraint for a discriminant local, propagate the
551    /// constraint backward through the copy chain so that source locals also
552    /// receive the value. This prevents losing track of the constraint when
553    /// the destination temporary is reassigned on loop back-edges.
554    fn learn_constraint_with_backprop(
555        &self,
556        cur: usize,
557        constraint_local: Option<usize>,
558        targets: &SwitchTargets,
559        next: usize,
560        constraints: &mut FxHashMap<usize, usize>,
561    ) {
562        let Some(local) = constraint_local else {
563            return;
564        };
565        let Some((val, _)) = targets.iter().find(|(_, bb)| bb.as_usize() == next) else {
566            if let Some(inferred) = self.infer_otherwise_value(targets, local) {
567                constraints.insert(local, inferred);
568                self.backprop_constraint(cur, local, inferred, constraints);
569            }
570            return;
571        };
572        let val = val as usize;
573        constraints.insert(local, val);
574        self.backprop_constraint(cur, local, val, constraints);
575    }
576
577    fn backprop_constraint(
578        &self,
579        cur: usize,
580        local: usize,
581        val: usize,
582        constraints: &mut FxHashMap<usize, usize>,
583    ) {
584        let Some(info) = self.block_info.get(cur) else {
585            return;
586        };
587        let mut current = local;
588        while let Some(&src) = info.constraint_copies.get(&current) {
589            if current == src {
590                break;
591            }
592            constraints.insert(src, val);
593            current = src;
594        }
595    }
596
597    /// For the "otherwise" branch of a `SwitchInt`, try to infer the single
598    /// concrete value that the discriminant must have (because all other
599    /// possible values are covered by explicit targets).
600    fn infer_otherwise_value(&self, targets: &SwitchTargets, discr_local: usize) -> Option<usize> {
601        let body = self.cfg.tcx.optimized_mir(self.cfg.def_id);
602        let mut discr_ty = body.local_decls[Local::from_usize(discr_local)].ty;
603        while let TyKind::Ref(_, inner, _) | TyKind::RawPtr(inner, _) = discr_ty.kind() {
604            discr_ty = *inner;
605        }
606
607        let possible_values: Vec<usize> = match discr_ty.kind() {
608            TyKind::Bool => vec![0, 1],
609            TyKind::Adt(adt_def, _) if adt_def.is_enum() => (0..adt_def.variants().len()).collect(),
610            _ => return None,
611        };
612
613        let explicit_values: FxHashSet<usize> = targets.iter().map(|(v, _)| v as usize).collect();
614        let remaining: Vec<usize> = possible_values
615            .into_iter()
616            .filter(|v| !explicit_values.contains(v))
617            .collect();
618
619        if remaining.len() == 1 {
620            Some(remaining[0])
621        } else {
622            None
623        }
624    }
625
626    /// Check whether `next` is an unwind target reachable from `cur` via a
627    /// call or drop terminator (may not be recorded as a normal CFG successor).
628    fn is_unwind_target(&self, cur: usize, next: usize) -> bool {
629        let Some(terminator) = self.cfg.terminator(cur) else {
630            return false;
631        };
632
633        let unwind = match &terminator.kind {
634            TerminatorKind::Call { unwind, .. }
635            | TerminatorKind::Drop { unwind, .. }
636            | TerminatorKind::Assert { unwind, .. } => unwind,
637            _ => return false,
638        };
639
640        if let UnwindAction::Cleanup(target) = unwind {
641            return target.as_usize() == next;
642        }
643        false
644    }
645
646    /// Populate the `child_sccs` field for a given SCC entry block, then
647    /// recurse into those child SCCs. Called eagerly from `find_scc()` so
648    /// that enumeration can be purely read-only on the graph.
649    fn populate_child_sccs(&mut self, enter: usize) {
650        let nodes: Vec<usize> = self.cfg.block(enter).scc.nodes.iter().cloned().collect();
651        let mut child_enters = Vec::new();
652        let mut seen = FxHashSet::default();
653
654        for node in nodes {
655            if let Some(block) = self.cfg.blocks.get(node) {
656                let node_enter = block.scc.enter;
657                let non_trivial = !block.scc.nodes.is_empty();
658                if node_enter != enter && non_trivial && seen.insert(node_enter) {
659                    child_enters.push(node_enter);
660                }
661            }
662        }
663
664        self.cfg.block_mut(enter).scc.child_sccs = child_enters;
665
666        let child_count = self.cfg.block(enter).scc.child_sccs.len();
667        for i in 0..child_count {
668            let child_enter = self.cfg.block(enter).scc.child_sccs[i];
669            self.populate_child_sccs(child_enter);
670        }
671    }
672
673    fn populate_all_child_sccs(&mut self) {
674        let mut visited = FxHashSet::default();
675        let block_count = self.cfg.blocks.len();
676        for i in 0..block_count {
677            let scc = &self.cfg.block(i).scc;
678            let enter = scc.enter;
679            if scc.nodes.is_empty() || !visited.insert(enter) {
680                continue;
681            }
682            self.populate_child_sccs(enter);
683        }
684    }
685}
686
687/// Hash of the constraint state accumulated along a path prefix.
688///
689/// Computed by walking the prefix, collecting `(local → constant_value)`
690/// bindings from each block's [`BlockConstantInfo`], sorting them, and
691/// hashing the result.  Used by [`PathEnumerator::visited_sccs`] to
692/// skip redundant re-entries into the same SCC with the same state.
693#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
694pub struct ConstraintHash(u64);
695
696impl ConstraintHash {
697    fn from_path(path: &[usize], graph: &PathGraph<'_>) -> Self {
698        let mut hasher = DefaultHasher::new();
699        let mut constraints: FxHashMap<usize, usize> = FxHashMap::default();
700
701        for &block in path.iter() {
702            if let Some(info) = graph.block_info.get(block) {
703                for local in &info.assigned_locals {
704                    if let Some(&src) = info.constraint_copies.get(local) {
705                        if let Some(&src_val) = constraints.get(&src) {
706                            constraints.insert(*local, src_val);
707                            continue;
708                        }
709                        if let Some(&dst_val) = constraints.get(local) {
710                            constraints.insert(src, dst_val);
711                            constraints.insert(*local, dst_val);
712                            continue;
713                        }
714                    }
715                    if let Some(&val) = info.constants.get(local) {
716                        constraints.insert(*local, val);
717                        continue;
718                    }
719                    constraints.remove(local);
720                }
721            }
722        }
723
724        let mut entries: Vec<(usize, usize)> = constraints.into_iter().collect();
725        entries.sort();
726        entries.hash(&mut hasher);
727        ConstraintHash(hasher.finish())
728    }
729}
730
731/// Key for [`PathEnumerator::scc_paths`] and [`PathEnumerator::visited_sccs`] and [`PathEnumerator::visited_sccs`]:
732/// which SCC entry block, with what constraint state, and how many
733/// additional postfix repeats are allowed.
734///
735/// In `scc_paths` the `constraint` field is unused (cache keyed by
736/// `entry` + `repeat`); in `visited_sccs` the `repeat` field is
737/// unused (deduplicated by `entry` + `constraint`).
738#[derive(Debug, Clone, Hash, PartialEq, Eq)]
739pub struct SccKey {
740    pub entry: usize,
741    pub repeat: usize,
742    pub constraint: ConstraintHash,
743}
744
745/// Builds a [`PathTree`] by depth-first enumeration of whole-CFG paths.
746///
747/// The enumerator holds a `&PathGraph` (which must have `find_scc()` called
748/// beforehand) and two caches:
749///
750/// The **constraint hash** used by `visited_sccs` is a
751/// [`ConstraintHash`]: it walks the current path prefix, accumulates
752/// `(local → constant_value)` bindings from each block's
753/// [`BlockConstantInfo`], sorts them, and hashes the result.  Different
754/// constraint states (e.g. a loop variable changing from `true` to
755/// `false`) produce different hashes.
756///
757/// 1. `scc_paths` — maps [`SccKey`] to a list of acyclic paths through
758///    that SCC.  Reused across repeated enumerations of the same function
759///    with different repeat counts.
760///
761/// 2. `visited_sccs` — set of [`SccKey`] entries already explored
762///    (matched by `entry` + `constraint`, ignoring `repeat`).
763///    When the same SCC is reached with the same constraint state, its
764///    sub-paths are identical, so the re-entry is skipped.
765///
766/// Constraint-based filtering runs incrementally during DFS via
767/// [`PathGraph::check_transition`], so only feasible paths are inserted
768/// into the resulting tree.
769pub struct PathEnumerator<'g, 'tcx> {
770    graph: &'g PathGraph<'tcx>,
771    scc_paths: FxHashMap<SccKey, Vec<SccPath>>,
772    visited_sccs: FxHashSet<SccKey>,
773}
774
775impl<'g, 'tcx> PathEnumerator<'g, 'tcx> {
776    pub fn new(graph: &'g PathGraph<'tcx>) -> Self {
777        PathEnumerator {
778            graph,
779            scc_paths: FxHashMap::default(),
780            visited_sccs: FxHashSet::default(),
781        }
782    }
783
784    /// Enumerate all whole-CFG paths, pruning infeasible transitions via
785    /// incremental constraint-based filtering during DFS.
786    ///
787    /// SCC regions are flattened into a bounded set of acyclic paths.
788    pub fn enumerate_paths(&mut self) -> PathTree {
789        self.enumerate_paths_repeat(0)
790    }
791
792    /// Enumerate whole-CFG paths allowing each SCC postfix segment to repeat
793    /// up to `postfix_repeat` additional times. `postfix_repeat = 0` gives
794    /// the same result as `enumerate_paths`.
795    pub fn enumerate_paths_repeat(&mut self, postfix_repeat: usize) -> PathTree {
796        let mut tree = PathTree::new();
797
798        if self.graph.cfg.blocks.is_empty() {
799            return tree;
800        }
801
802        self.collect_whole_cfg_paths(
803            0,
804            &mut vec![0],
805            &mut tree,
806            0,
807            postfix_repeat,
808            &FxHashMap::default(),
809        );
810
811        tree
812    }
813
814    /// Enumerate all acyclic paths through `scc` starting at `start`,
815    /// allowing each postfix segment to repeat up to `postfix_repeat`
816    /// additional times.
817    ///
818    /// Results are cached per `(def_id, scc_enter, postfix_repeat)`.
819    pub fn find_scc_paths_repeat(
820        &mut self,
821        start: usize,
822        scc: &SccInfo,
823        postfix_repeat: usize,
824    ) -> Vec<SccPath> {
825        let cache_key = SccKey {
826            entry: scc.enter,
827            repeat: postfix_repeat,
828            constraint: ConstraintHash::default(),
829        };
830        if let Some(cached) = self.scc_paths.get(&cache_key) {
831            return cached.clone();
832        }
833
834        let mut out = Vec::new();
835        let mut seen: FxHashSet<Vec<usize>> = FxHashSet::default();
836        let mut path = vec![start];
837        let mut segment_counts = FxHashMap::default();
838
839        self.dfs_scc_tree(
840            scc,
841            start,
842            &mut path,
843            &mut segment_counts,
844            postfix_repeat,
845            &mut out,
846            &mut seen,
847            0,
848        );
849
850        if self.scc_paths.len() >= SCC_PATH_CACHE_LIMIT {
851            self.scc_paths.clear();
852        }
853        self.scc_paths.insert(cache_key, out.clone());
854
855        out
856    }
857
858    /// Recursive DFS through one level of the SCC tree.
859    ///
860    /// Enumerates structurally possible paths through the SCC to exit points.
861    /// No constraint tracking — `check_postfix_segment` prunes repeated
862    /// loop-body segments purely by block-id sequence.
863    ///
864    /// When `postfix_repeat > 0`, allows the same postfix segment to repeat
865    /// up to `postfix_repeat` additional times beyond the first occurrence.
866    ///
867    /// Child SCC paths are pre-enumerated via `find_scc_paths_repeat` and treated as
868    /// atomic building blocks (no recursive descent into child SCC internals).
869    #[allow(clippy::too_many_arguments)]
870    fn dfs_scc_tree(
871        &mut self,
872        scc: &SccInfo,
873        cur: usize,
874        path: &mut Vec<usize>,
875        segment_counts: &mut FxHashMap<Vec<usize>, usize>,
876        postfix_repeat: usize,
877        out: &mut Vec<SccPath>,
878        seen_paths: &mut FxHashSet<Vec<usize>>,
879        depth: usize,
880    ) {
881        if depth > SCC_MAX_DEPTH {
882            return;
883        }
884        if out.len() >= SCC_MAX_SEEN_PATHS {
885            return;
886        }
887        if path.len() > SCC_MAX_PATH_LEN {
888            return;
889        }
890        if cur != scc.enter && !scc.nodes.contains(&cur) {
891            return;
892        }
893
894        if cur == scc.enter && path.len() > 1 {
895            if !check_postfix_segment(path, scc.enter, segment_counts, postfix_repeat) {
896                if (postfix_repeat > 0 || segment_counts.len() > 1)
897                    && scc.exits.iter().any(|e| e.exit == cur)
898                {
899                    self.record_unique_path(path, scc, out, seen_paths);
900                }
901                return;
902            }
903        }
904
905        if scc.exits.iter().any(|e| e.exit == cur) {
906            self.record_unique_path(path, scc, out, seen_paths);
907        }
908
909        let is_child = scc.child_sccs.contains(&cur);
910
911        if is_child {
912            let ctx = self.constraint_context(path);
913            if !self.visited_sccs.insert(SccKey {
914                entry: cur,
915                repeat: 0,
916                constraint: ctx,
917            }) {
918                return;
919            }
920
921            let child_scc = self.graph.cfg_block(cur).scc.clone();
922            let child_paths = self.find_scc_paths_repeat(cur, &child_scc, postfix_repeat);
923
924            for child_path in &child_paths {
925                let orig_len = path.len();
926                if child_path.blocks.len() > 1 {
927                    path.extend(&child_path.blocks[1..]);
928                }
929
930                let mut branch_counts = segment_counts.clone();
931                for &next in &child_path.exit_successors {
932                    path.push(next);
933                    self.dfs_scc_tree(
934                        scc,
935                        next,
936                        path,
937                        &mut branch_counts,
938                        postfix_repeat,
939                        out,
940                        seen_paths,
941                        depth + 1,
942                    );
943                    path.pop();
944                }
945                path.truncate(orig_len);
946            }
947            return;
948        }
949
950        let successors: Vec<usize> = self.graph.cfg.block(cur).next.iter().copied().collect();
951        let saved_counts = segment_counts.clone();
952        for next in successors {
953            if next != scc.enter && !scc.nodes.contains(&next) {
954                self.record_unique_path(path, scc, out, seen_paths);
955                continue;
956            }
957            let mut branch_counts = saved_counts.clone();
958            path.push(next);
959            self.dfs_scc_tree(
960                scc,
961                next,
962                path,
963                &mut branch_counts,
964                postfix_repeat,
965                out,
966                seen_paths,
967                depth + 1,
968            );
969            path.pop();
970        }
971    }
972
973    /// Build a [`ConstraintHash`] from the constraint state along `path`.
974    fn constraint_context(&self, path: &[usize]) -> ConstraintHash {
975        ConstraintHash::from_path(path, self.graph)
976    }
977
978    /// Depth-first enumeration of all CFG paths from `current` to a terminator.
979    ///
980    /// SCC nodes are flattened via `find_scc_paths_repeat`; non-SCC blocks are followed
981    /// one by one.  No cycle detection is needed because the post-SCC CFG is a DAG.
982    /// Constraints are maintained incrementally — each transition is checked
983    /// via `PathGraph::check_transition` before recursing, and infeasible
984    /// branches are pruned early.
985    fn collect_whole_cfg_paths(
986        &mut self,
987        current: usize,
988        path: &mut Vec<usize>,
989        tree: &mut PathTree,
990        depth: usize,
991        postfix_repeat: usize,
992        constraints: &FxHashMap<usize, usize>,
993    ) {
994        if current >= self.graph.cfg.blocks.len() {
995            return;
996        }
997        if depth > WHOLE_CFG_PATH_DEPTH_LIMIT || tree.len() >= WHOLE_CFG_PATH_LIMIT {
998            return;
999        }
1000
1001        let scc_info = self.graph.cfg_block(current).scc.clone();
1002        let is_scc = current == scc_info.enter && !scc_info.nodes.is_empty();
1003        if is_scc {
1004            let scc = self.sort_scc_tree(&scc_info);
1005            let segments = self.find_scc_paths_repeat(current, &scc, postfix_repeat);
1006
1007            if segments.is_empty() {
1008                tree.insert(path);
1009                return;
1010            }
1011
1012            for seg in segments {
1013                if tree.len() >= WHOLE_CFG_PATH_LIMIT {
1014                    break;
1015                }
1016
1017                let orig_len = path.len();
1018                let mut seg_constraints = constraints.clone();
1019                let mut reachable = true;
1020
1021                if seg.blocks.len() > 1 {
1022                    for i in 0..seg.blocks.len() - 1 {
1023                        if !self.graph.check_transition(
1024                            seg.blocks[i],
1025                            seg.blocks[i + 1],
1026                            &mut seg_constraints,
1027                        ) {
1028                            reachable = false;
1029                            break;
1030                        }
1031                    }
1032                    if reachable {
1033                        path.extend_from_slice(&seg.blocks[1..]);
1034                    }
1035                }
1036
1037                if reachable {
1038                    if seg.exit_successors.is_empty() {
1039                        tree.insert(path);
1040                    } else {
1041                        for &next in &seg.exit_successors {
1042                            let mut next_constraints = seg_constraints.clone();
1043                            let last = *path.last().unwrap();
1044                            if self
1045                                .graph
1046                                .check_transition(last, next, &mut next_constraints)
1047                            {
1048                                path.push(next);
1049                                self.collect_whole_cfg_paths(
1050                                    next,
1051                                    path,
1052                                    tree,
1053                                    depth + 1,
1054                                    postfix_repeat,
1055                                    &next_constraints,
1056                                );
1057                                path.pop();
1058                            }
1059                        }
1060                    }
1061                }
1062
1063                path.truncate(orig_len);
1064            }
1065            return;
1066        }
1067
1068        // Non-SCC block: follow CFG successors.
1069        let successors: Vec<usize> = self.graph.cfg_block(current).next.iter().copied().collect();
1070        if successors.is_empty() {
1071            tree.insert(path);
1072            return;
1073        }
1074
1075        for next in successors {
1076            let mut next_constraints = constraints.clone();
1077            if self
1078                .graph
1079                .check_transition(current, next, &mut next_constraints)
1080            {
1081                path.push(next);
1082                self.collect_whole_cfg_paths(
1083                    next,
1084                    path,
1085                    tree,
1086                    depth + 1,
1087                    postfix_repeat,
1088                    &next_constraints,
1089                );
1090                path.pop();
1091            }
1092        }
1093    }
1094
1095    fn sort_scc_tree(&self, scc: &SccInfo) -> SccInfo {
1096        self.graph.cfg_block(scc.enter).scc.clone()
1097    }
1098
1099    fn record_unique_path(
1100        &self,
1101        path: &[usize],
1102        scc: &SccInfo,
1103        out: &mut Vec<SccPath>,
1104        seen_paths: &mut FxHashSet<Vec<usize>>,
1105    ) {
1106        if !seen_paths.insert(path.to_vec()) {
1107            return;
1108        }
1109        let exit_successors = self.compute_exit_successors(path, scc);
1110        out.push(SccPath {
1111            blocks: path.to_vec(),
1112            exit_successors,
1113        });
1114    }
1115
1116    fn compute_exit_successors(&self, path: &[usize], scc: &SccInfo) -> Vec<usize> {
1117        let Some(&last) = path.last() else {
1118            return vec![];
1119        };
1120        scc.exits
1121            .iter()
1122            .filter(|e| e.exit == last)
1123            .map(|e| e.to)
1124            .filter(|&n| {
1125                !scc.child_sccs
1126                    .contains(&self.graph.cfg.block(n).scc.enter())
1127            })
1128            .collect()
1129    }
1130}
1131
1132/// Resolve a concrete discriminant value to the corresponding `SwitchInt`
1133/// successor block index.
1134fn resolve_switch_target(targets: &SwitchTargets, val: u128) -> usize {
1135    targets
1136        .iter()
1137        .find(|(v, _)| *v == val)
1138        .map(|(_, bb)| bb.as_usize())
1139        .unwrap_or_else(|| targets.otherwise().as_usize())
1140}