Skip to main content

mirage_analyzer/cfg/
paths.rs

1//! Path enumeration for CFG analysis
2//!
3//! This module provides data structures and algorithms for discovering
4//! all execution paths through a function's control flow graph from entry
5//! to exit. Paths are discovered using depth-first search with cycle
6//! detection and loop bounding to prevent infinite recursion.
7//!
8//! ## Feasibility Checking
9//!
10//! This module provides **STATIC** feasibility checking only. It does NOT
11//! perform symbolic execution or data flow analysis.
12//!
13//! ### What we check:
14//! - Entry block is actually Entry kind
15//! - Exit block has valid terminator (Return, Abort, Call with target)
16//! - All blocks are reachable from entry
17//! - No dead ends (Goto/SwitchInt as last terminator)
18//!
19//! ### What we DON'T check (requires symbolic execution):
20//! - Conflicting branch conditions (e.g., `if x > 5 && x < 3`)
21//! - Data-dependent constraints (array bounds, divide by zero)
22//! - Runtime panic conditions
23//!
24//! ### Tradeoff:
25//! Static checking is fast (O(n)) and sound (never falsely claims feasible).
26//! Symbolic execution is precise but slow (>100x) and complex.
27//!
28//! For most code intelligence queries, static checking is sufficient.
29//! Future work may add symbolic execution for specific paths.
30//!
31//! ## Path Classification
32//!
33//! Paths are categorized based on their structure and content:
34//! - **Normal:** Standard entry → return path
35//! - **Error:** Contains panic, abort, or error propagation
36//! - **Degenerate:** Dead end, infinite loop, or infeasible path
37//! - **Unreachable:** Statically unreachable code path
38
39use crate::cfg::{BlockId, Cfg, Terminator};
40use petgraph::graph::NodeIndex;
41use serde::{Deserialize, Serialize};
42use std::collections::{HashMap, HashSet};
43#[cfg(test)]
44use std::time::Duration;
45
46/// Execution path through a CFG
47///
48/// Represents a sequence of basic blocks from an entry block to an exit block.
49/// Each path has a unique identifier derived from a BLAKE3 hash of the block
50/// sequence for deduplication and comparison.
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub struct Path {
53    /// Unique identifier (BLAKE3 hash of block sequence)
54    pub path_id: String,
55    /// Ordered block IDs in execution order
56    pub blocks: Vec<BlockId>,
57    /// Classification of this path
58    pub kind: PathKind,
59    /// First block (entry)
60    pub entry: BlockId,
61    /// Last block (exit)
62    pub exit: BlockId,
63}
64
65impl Path {
66    /// Create a new path from a block sequence
67    pub fn new(blocks: Vec<BlockId>, kind: PathKind) -> Self {
68        let entry = *blocks.first().unwrap_or(&0);
69        let exit = *blocks.last().unwrap_or(&0);
70        let path_id = hash_path(&blocks);
71
72        Self {
73            path_id,
74            blocks,
75            kind,
76            entry,
77            exit,
78        }
79    }
80
81    /// Create a path with a pre-existing path_id (for loading from cache)
82    ///
83    /// # Arguments
84    ///
85    /// * `path_id` - The stored path identifier
86    /// * `blocks` - Ordered block IDs in execution order
87    /// * `kind` - Path classification
88    ///
89    /// # Note
90    ///
91    /// This bypasses the normal path_id computation. Use only when loading
92    /// previously stored paths where the path_id was already computed.
93    pub fn with_id(path_id: String, blocks: Vec<BlockId>, kind: PathKind) -> Self {
94        let entry = *blocks.first().unwrap_or(&0);
95        let exit = *blocks.last().unwrap_or(&0);
96
97        Self {
98            path_id,
99            blocks,
100            kind,
101            entry,
102            exit,
103        }
104    }
105
106    /// Get the length of this path (number of blocks)
107    pub fn len(&self) -> usize {
108        self.blocks.len()
109    }
110
111    /// Check if this path is empty
112    pub fn is_empty(&self) -> bool {
113        self.blocks.is_empty()
114    }
115
116    /// Get an iterator over the blocks in this path
117    pub fn iter(&self) -> impl Iterator<Item = &BlockId> {
118        self.blocks.iter()
119    }
120
121    /// Check if this path contains a specific block
122    pub fn contains(&self, block_id: BlockId) -> bool {
123        self.blocks.contains(&block_id)
124    }
125}
126
127impl std::fmt::Display for Path {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        write!(f, "{}", self.path_id)
130    }
131}
132
133/// Classification of execution paths
134///
135/// Paths are categorized based on their structure and content.
136/// Classification is used for analysis and reporting.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
138pub enum PathKind {
139    /// Standard entry -> return path
140    Normal,
141    /// Contains panic, abort, or error propagation
142    Error,
143    /// Dead end or infinite loop (no valid exit)
144    Degenerate,
145    /// Statically unreachable code path
146    Unreachable,
147}
148
149/// Find the NodeIndex for a given BlockId
150///
151/// Helper function to convert BlockIds from paths to NodeIndices for CFG queries.
152/// Returns None if the block ID doesn't exist in the CFG.
153fn find_node_by_block_id(cfg: &Cfg, block_id: BlockId) -> Option<NodeIndex> {
154    cfg.node_indices()
155        .find(|&idx| cfg[idx].id == block_id)
156}
157
158/// Check if a path is statically feasible
159///
160/// A path is feasible if it represents a viable execution path from entry to exit.
161/// This is a STATIC check based on terminator analysis only - it does NOT perform
162/// symbolic execution or data flow analysis.
163///
164/// **Feasibility criteria (all must pass):**
165///
166/// 1. **Non-empty:** Path must have at least one block
167/// 2. **Valid entry:** First block must be Entry kind
168/// 3. **Valid exit:** Last block must have valid exit terminator:
169///    - `Terminator::Return` -> feasible (normal exit)
170///    - `Terminator::Abort(_)` -> feasible (error path, but reachable)
171///    - `Terminator::Unreachable` -> infeasible (cannot execute)
172///    - `Terminator::Call { unwind: None, .. }` -> feasible (no unwind)
173///    - `Terminator::Call { unwind: Some(_), target: Some(_), .. }` -> feasible
174///    - `Terminator::Call { unwind: Some(_), target: None }` -> infeasible (always unwinds)
175///    - `Terminator::Goto` / `Terminator::SwitchInt` -> infeasible (dead end if last block)
176///
177/// 4. **All blocks exist:** Every block ID in the path must exist in the CFG
178///
179/// **What we DON'T check (requires symbolic execution):**
180/// - Conflicting branch conditions (e.g., `if x > 5 && x < 3`)
181/// - Data-dependent constraints (array bounds, divide by zero)
182/// - Runtime panic conditions
183///
184/// # Arguments
185///
186/// * `cfg` - Control flow graph to analyze
187/// * `blocks` - Block IDs in execution order
188///
189/// # Returns
190///
191/// `true` if the path is statically feasible, `false` otherwise
192///
193/// # Examples
194///
195/// ```rust,no_run
196/// # use mirage_analyzer::cfg::paths::is_feasible_path;
197/// # use mirage_analyzer::cfg::Cfg;
198/// # let graph: Cfg = unimplemented!();
199/// let feasible = is_feasible_path(&graph, &[0, 1, 2]);  // entry -> goto -> return
200/// let infeasible = is_feasible_path(&graph, &[0, 1]);    // entry -> goto (dead end)
201/// ```
202pub fn is_feasible_path(cfg: &Cfg, blocks: &[BlockId]) -> bool {
203    use crate::cfg::BlockKind;
204
205    // Criterion 1: Path must be non-empty
206    if blocks.is_empty() {
207        return false;
208    }
209
210    // Criterion 2: First block must be Entry kind
211    let first_idx = match find_node_by_block_id(cfg, blocks[0]) {
212        Some(idx) => idx,
213        None => return false, // Block doesn't exist
214    };
215    if cfg[first_idx].kind != BlockKind::Entry {
216        return false;
217    }
218
219    // Criterion 3: Last block must have valid exit terminator
220    let last_idx = match find_node_by_block_id(cfg, *blocks.last().unwrap()) {
221        Some(idx) => idx,
222        None => return false, // Block doesn't exist
223    };
224
225    match &cfg[last_idx].terminator {
226        Terminator::Return => {}, // Feasible: normal exit
227        Terminator::Abort(_) => {}, // Feasible: error path but reachable
228        Terminator::Call { unwind: None, .. } => {}, // Feasible: no unwind
229        Terminator::Call { unwind: Some(_), target: Some(_) } => {}, // Feasible: has target
230        // Infeasible terminators (dead ends)
231        Terminator::Unreachable |
232        Terminator::Goto { .. } |
233        Terminator::SwitchInt { .. } |
234        Terminator::Call { unwind: Some(_), target: None } => {
235            return false;
236        }
237    }
238
239    // Criterion 4: All intermediate blocks must exist
240    for &block_id in blocks.iter().skip(1).take(blocks.len().saturating_sub(2)) {
241        if find_node_by_block_id(cfg, block_id).is_none() {
242            return false;
243        }
244    }
245
246    true
247}
248
249/// Check if a path is statically feasible using pre-computed reachable set
250///
251/// This is an optimized version of `is_feasible_path` for batch operations.
252/// Instead of calling `is_reachable_from_entry` for each block (which is O(n)
253/// per call), we use a pre-computed HashSet of reachable block IDs, making
254/// reachability checks O(1).
255///
256/// **Why:** O(n) batch feasibility checking vs O(n²) for repeated individual calls.
257/// Pre-compute reachable set once with `find_reachable()`, reuse for all paths.
258///
259/// **Additional criterion over is_feasible_path:**
260/// 5. **All blocks reachable:** Every block in the path must be reachable from entry
261///    - Uses pre-computed HashSet for O(1) lookup
262///
263/// # Arguments
264///
265/// * `cfg` - Control flow graph to analyze
266/// * `blocks` - Block IDs in execution order
267/// * `reachable_blocks` - Pre-computed set of reachable BlockIds
268///
269/// # Returns
270///
271/// `true` if the path is statically feasible, `false` otherwise
272///
273/// # Examples
274///
275/// ```rust,no_run
276/// # use mirage_analyzer::cfg::paths::is_feasible_path_precomputed;
277/// # use mirage_analyzer::cfg::reachability::find_reachable;
278/// # use mirage_analyzer::cfg::Cfg;
279/// # use std::collections::HashSet;
280/// # use mirage_analyzer::cfg::BlockId;
281/// # use mirage_analyzer::cfg::Path;
282/// # let graph: Cfg = unimplemented!();
283/// # let paths: Vec<Path> = vec![];
284/// let reachable_nodes = find_reachable(&graph);
285/// let reachable_blocks: HashSet<BlockId> = reachable_nodes
286///     .iter()
287///     .map(|&idx| graph[idx].id)
288///     .collect();
289///
290/// for path in paths {
291///     let feasible = is_feasible_path_precomputed(&graph, &path.blocks, &reachable_blocks);
292/// }
293/// ```
294pub fn is_feasible_path_precomputed(
295    cfg: &Cfg,
296    blocks: &[BlockId],
297    reachable_blocks: &HashSet<BlockId>,
298) -> bool {
299    use crate::cfg::BlockKind;
300
301    // Criterion 1: Path must be non-empty
302    if blocks.is_empty() {
303        return false;
304    }
305
306    // Criterion 2: First block must be Entry kind
307    let first_idx = match find_node_by_block_id(cfg, blocks[0]) {
308        Some(idx) => idx,
309        None => return false, // Block doesn't exist
310    };
311    if cfg[first_idx].kind != BlockKind::Entry {
312        return false;
313    }
314
315    // Criterion 5: All blocks must be reachable (O(1) per block)
316    for &block_id in blocks {
317        if !reachable_blocks.contains(&block_id) {
318            return false;
319        }
320    }
321
322    // Criterion 3: Last block must have valid exit terminator
323    let last_idx = match find_node_by_block_id(cfg, *blocks.last().unwrap()) {
324        Some(idx) => idx,
325        None => return false, // Block doesn't exist
326    };
327
328    match &cfg[last_idx].terminator {
329        Terminator::Return => {}, // Feasible: normal exit
330        Terminator::Abort(_) => {}, // Feasible: error path but reachable
331        Terminator::Call { unwind: None, .. } => {}, // Feasible: no unwind
332        Terminator::Call { unwind: Some(_), target: Some(_) } => {}, // Feasible: has target
333        // Infeasible terminators (dead ends)
334        Terminator::Unreachable |
335        Terminator::Goto { .. } |
336        Terminator::SwitchInt { .. } |
337        Terminator::Call { unwind: Some(_), target: None } => {
338            return false;
339        }
340    }
341
342    // Criterion 4: All intermediate blocks must exist
343    for &block_id in blocks.iter().skip(1).take(blocks.len().saturating_sub(2)) {
344        if find_node_by_block_id(cfg, block_id).is_none() {
345            return false;
346        }
347    }
348
349    true
350}
351
352/// Classify a path based on its terminators and reachability
353///
354/// **Classification rules (in priority order):**
355///
356/// 1. **Unreachable:** Any block in path is unreachable from entry
357/// 2. **Error:** Path contains error terminators (Abort, Call with unwind)
358/// 3. **Degenerate:** Path ends abnormally or has unreachable terminator
359/// 4. **Normal:** Default classification (entry -> return path)
360///
361/// # Arguments
362///
363/// * `cfg` - Control flow graph to analyze
364/// * `blocks` - Block IDs in execution order
365///
366/// # Returns
367///
368/// The classified PathKind for this path
369pub fn classify_path(cfg: &Cfg, blocks: &[BlockId]) -> PathKind {
370    use crate::cfg::reachability::is_reachable_from_entry;
371
372    // Empty path is degenerate
373    if blocks.is_empty() {
374        return PathKind::Degenerate;
375    }
376
377    // Check each block in the path
378    for &block_id in blocks {
379        let node_idx = match find_node_by_block_id(cfg, block_id) {
380            Some(idx) => idx,
381            None => return PathKind::Degenerate, // Block doesn't exist
382        };
383
384        // Priority 1: Check if block is unreachable from entry
385        if !is_reachable_from_entry(cfg, node_idx) {
386            return PathKind::Unreachable;
387        }
388
389        // Get the block's terminator
390        let terminator = &cfg[node_idx].terminator;
391
392        // Priority 2: Check for error terminators
393        match terminator {
394            Terminator::Abort(_) => return PathKind::Error,
395            Terminator::Call { unwind: Some(_), .. } => return PathKind::Error,
396            _ => {}
397        }
398
399        // Priority 3: Check for unreachable terminator (anywhere in path)
400        if matches!(terminator, Terminator::Unreachable) {
401            return PathKind::Degenerate;
402        }
403    }
404
405    // Check last block terminator - if not Return, it's degenerate
406    if let Some(&last_block_id) = blocks.last() {
407        if let Some(node_idx) = find_node_by_block_id(cfg, last_block_id) {
408            let terminator = &cfg[node_idx].terminator;
409            // Non-Return terminators at end are degenerate
410            if !matches!(terminator, Terminator::Return) {
411                // Already caught Unreachable above, so check other cases
412                return PathKind::Degenerate;
413            }
414        }
415    }
416
417    // Default: Normal path
418    PathKind::Normal
419}
420
421/// Classify a path using a pre-computed reachable set for O(n) batch classification
422///
423/// This version is optimized for classifying many paths. Instead of calling
424/// `is_reachable_from_entry` for each block (which is O(n) per call), we use
425/// a pre-computed HashSet of reachable block IDs, making reachability checks O(1).
426///
427/// **Why:** O(n) classification vs O(n²) for repeated `is_reachable_from_entry` calls.
428/// Pre-compute reachable set once with `find_reachable()`, reuse for all paths.
429///
430/// # Arguments
431///
432/// * `cfg` - Control flow graph to analyze
433/// * `blocks` - Block IDs in execution order
434/// * `reachable_blocks` - Pre-computed set of reachable BlockIds
435///
436/// # Returns
437///
438/// The classified PathKind for this path
439///
440/// # Example
441///
442/// ```rust,no_run
443/// # use mirage_analyzer::cfg::paths::classify_path_precomputed;
444/// # use mirage_analyzer::cfg::reachability::find_reachable;
445/// # use mirage_analyzer::cfg::Cfg;
446/// # use std::collections::HashSet;
447/// # use mirage_analyzer::cfg::BlockId;
448/// # use mirage_analyzer::cfg::Path;
449/// # let graph: Cfg = unimplemented!();
450/// # let paths: Vec<Path> = vec![];
451/// let reachable_nodes = find_reachable(&graph);
452/// let reachable_blocks: HashSet<BlockId> = reachable_nodes
453///     .iter()
454///     .map(|&idx| graph[idx].id)
455///     .collect();
456///
457/// for path in paths {
458///     let kind = classify_path_precomputed(&graph, &path.blocks, &reachable_blocks);
459/// }
460/// ```
461pub fn classify_path_precomputed(
462    cfg: &Cfg,
463    blocks: &[BlockId],
464    reachable_blocks: &HashSet<BlockId>,
465) -> PathKind {
466    // Empty path is degenerate
467    if blocks.is_empty() {
468        return PathKind::Degenerate;
469    }
470
471    // Priority 1: Check if any block is unreachable (O(1) lookup)
472    for &block_id in blocks {
473        if !reachable_blocks.contains(&block_id) {
474            return PathKind::Unreachable;
475        }
476    }
477
478    // Priority 2: Check for error terminators in the path
479    for &block_id in blocks {
480        let node_idx = match find_node_by_block_id(cfg, block_id) {
481            Some(idx) => idx,
482            None => return PathKind::Degenerate, // Block doesn't exist
483        };
484
485        let terminator = &cfg[node_idx].terminator;
486
487        // Check for error terminators
488        match terminator {
489            Terminator::Abort(_) => return PathKind::Error,
490            Terminator::Call { unwind: Some(_), .. } => return PathKind::Error,
491            _ => {}
492        }
493
494        // Check for unreachable terminator (anywhere in path)
495        if matches!(terminator, Terminator::Unreachable) {
496            return PathKind::Degenerate;
497        }
498    }
499
500    // Priority 3: Check static feasibility (dead-end detection)
501    // This identifies paths that end in Goto, SwitchInt, or other invalid terminators
502    if !is_feasible_path_precomputed(cfg, blocks, reachable_blocks) {
503        return PathKind::Degenerate;
504    }
505
506    // Default: Normal path
507    PathKind::Normal
508}
509
510impl PathKind {
511    /// Check if this path represents a normal execution
512    pub fn is_normal(&self) -> bool {
513        matches!(self, Self::Normal)
514    }
515
516    /// Check if this path represents an error condition
517    pub fn is_error(&self) -> bool {
518        matches!(self, Self::Error)
519    }
520
521    /// Check if this path is degenerate (abnormal structure)
522    pub fn is_degenerate(&self) -> bool {
523        matches!(self, Self::Degenerate)
524    }
525
526    /// Check if this path is unreachable
527    pub fn is_unreachable(&self) -> bool {
528        matches!(self, Self::Unreachable)
529    }
530}
531
532/// Configurable limits for path enumeration
533///
534/// Prevents exponential explosion of paths in complex CFGs and
535/// ensures termination in the presence of loops.
536#[derive(Debug, Clone, PartialEq, Eq)]
537pub struct PathLimits {
538    /// Maximum number of blocks per path
539    pub max_length: usize,
540    /// Maximum number of paths to enumerate
541    pub max_paths: usize,
542    /// Loop iterations to unroll before stopping
543    pub loop_unroll_limit: usize,
544}
545
546impl Default for PathLimits {
547    fn default() -> Self {
548        Self {
549            max_length: 1000,
550            max_paths: 10000,
551            loop_unroll_limit: 3,
552        }
553    }
554}
555
556impl PathLimits {
557    /// Create new path limits with custom values
558    pub fn new(max_length: usize, max_paths: usize, loop_unroll_limit: usize) -> Self {
559        Self {
560            max_length,
561            max_paths,
562            loop_unroll_limit,
563        }
564    }
565
566    /// Create limits with a custom maximum path length
567    pub fn with_max_length(mut self, max_length: usize) -> Self {
568        self.max_length = max_length;
569        self
570    }
571
572    /// Create limits with a custom maximum path count
573    pub fn with_max_paths(mut self, max_paths: usize) -> Self {
574        self.max_paths = max_paths;
575        self
576    }
577
578    /// Create limits with a custom loop unroll limit
579    pub fn with_loop_unroll_limit(mut self, loop_unroll_limit: usize) -> Self {
580        self.loop_unroll_limit = loop_unroll_limit;
581        self
582    }
583
584    /// Quick analysis preset for fast, approximate path enumeration
585    ///
586    /// Use this for:
587    /// - Initial code exploration
588    /// - IDE/integration features where responsiveness matters
589    /// - Large codebases where thorough analysis would be too slow
590    ///
591    /// Tradeoffs:
592    /// - May miss some paths due to lower limits
593    /// - Loop unrolling is minimal (2 iterations)
594    /// - Completes in <100ms for typical functions
595    pub fn quick_analysis() -> Self {
596        Self {
597            max_length: 100,
598            max_paths: 1000,
599            loop_unroll_limit: 2,
600        }
601    }
602
603    /// Thorough analysis preset for comprehensive path enumeration
604    ///
605    /// Use this for:
606    /// - Final analysis before deployment
607    /// - Security-critical code paths
608    /// - Test coverage validation
609    ///
610    /// Tradeoffs:
611    /// - Higher limits produce more complete results
612    /// - May take several seconds on complex functions
613    /// - Still bounded to prevent infinite loops
614    pub fn thorough() -> Self {
615        Self {
616            max_length: 10000,
617            max_paths: 100000,
618            loop_unroll_limit: 5,
619        }
620    }
621}
622
623/// Compute BLAKE3 hash of a block sequence
624///
625/// Used to generate unique identifiers for paths. The hash includes
626/// the path length to prevent collisions between different sequences
627/// that might otherwise hash to the same value.
628///
629/// # Arguments
630///
631/// * `blocks` - Slice of block IDs in execution order
632///
633/// # Returns
634///
635/// Hex string representing the BLAKE3 hash
636pub fn hash_path(blocks: &[BlockId]) -> String {
637    let mut hasher = blake3::Hasher::new();
638
639    // Include length to prevent collisions
640    hasher.update(&blocks.len().to_le_bytes());
641
642    // Hash each block ID with consistent endianness
643    for &block_id in blocks {
644        hasher.update(&block_id.to_le_bytes());
645    }
646
647    hasher.finalize().to_hex().to_string()
648}
649
650/// Pre-computed context for path enumeration
651///
652/// Contains analysis results that are shared across all path enumerations.
653/// Computing this context once and reusing it is much more efficient than
654/// recomputing for each enumeration call.
655///
656/// **Benefits:**
657/// - Reachable blocks computed once: O(n) instead of O(n²) for n paths
658/// - Loop headers computed once: O(e) instead of O(e) per call
659/// - Exit nodes computed once: O(v) instead of O(v) per call
660///
661/// **Use case:**
662/// ```rust,no_run
663/// # use mirage_analyzer::cfg::{PathLimits, enumerate_paths_with_context, EnumerationContext};
664/// # use mirage_analyzer::cfg::Cfg;
665/// # let graph: Cfg = unimplemented!();
666/// # let limits1 = PathLimits::default();
667/// # let limits2 = PathLimits::default();
668/// let ctx = EnumerationContext::new(&graph);
669/// let paths1 = enumerate_paths_with_context(&graph, &limits1, &ctx);
670/// let paths2 = enumerate_paths_with_context(&graph, &limits2, &ctx);
671/// // No redundant analysis computations
672/// ```
673#[derive(Debug, Clone)]
674pub struct EnumerationContext {
675    /// Blocks reachable from the entry node
676    pub reachable_blocks: HashSet<BlockId>,
677    /// Loop header nodes (for bounding loop iterations)
678    pub loop_headers: HashSet<NodeIndex>,
679    /// Exit nodes (valid path termination points)
680    pub exits: HashSet<NodeIndex>,
681}
682
683impl EnumerationContext {
684    /// Create a new enumeration context by analyzing the CFG
685    ///
686    /// Performs three analyses:
687    /// 1. Reachability: Find all blocks reachable from entry
688    /// 2. Loop detection: Find all loop headers via dominance analysis
689    /// 3. Exit detection: Find all blocks with return/abort/unreachable terminators
690    ///
691    /// **Time complexity:** O(v + e) where v = vertices, e = edges
692    /// **Space complexity:** O(v) for storing the analysis results
693    ///
694    /// # Example
695    ///
696    /// ```rust,no_run
697    /// # use mirage_analyzer::cfg::paths::EnumerationContext;
698    /// # use mirage_analyzer::cfg::Cfg;
699    /// # let graph: Cfg = unimplemented!();
700    /// let ctx = EnumerationContext::new(&graph);
701    /// println!("Found {} loop headers", ctx.loop_headers.len());
702    /// ```
703    pub fn new(cfg: &Cfg) -> Self {
704        // Compute reachable blocks
705        let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
706        let reachable_blocks: HashSet<BlockId> = reachable_nodes
707            .iter()
708            .map(|&idx| cfg[idx].id)
709            .collect();
710
711        // Compute loop headers
712        let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
713
714        // Compute exit nodes
715        let exits = crate::cfg::analysis::find_exits(cfg)
716            .into_iter()
717            .collect();
718
719        Self {
720            reachable_blocks,
721            loop_headers,
722            exits,
723        }
724    }
725
726    /// Get the number of reachable blocks
727    pub fn reachable_count(&self) -> usize {
728        self.reachable_blocks.len()
729    }
730
731    /// Get the number of loop headers
732    pub fn loop_count(&self) -> usize {
733        self.loop_headers.len()
734    }
735
736    /// Get the number of exit nodes
737    pub fn exit_count(&self) -> usize {
738        self.exits.len()
739    }
740
741    /// Check if a block is reachable
742    pub fn is_reachable(&self, block_id: BlockId) -> bool {
743        self.reachable_blocks.contains(&block_id)
744    }
745
746    /// Check if a node is a loop header
747    pub fn is_loop_header(&self, node: NodeIndex) -> bool {
748        self.loop_headers.contains(&node)
749    }
750
751    /// Check if a node is an exit
752    pub fn is_exit(&self, node: NodeIndex) -> bool {
753        self.exits.contains(&node)
754    }
755}
756
757/// Enumerate all execution paths through a CFG using pre-computed context
758///
759/// This is an optimized version of `enumerate_paths` that uses pre-computed
760/// analysis results. Use this when:
761/// - Performing multiple enumerations on the same CFG
762/// - You need to avoid redundant analysis computations
763///
764/// **Performance:**
765/// - Context creation: O(v + e) - done once
766/// - Enumeration per call: O(p * l) where p = paths, l = avg length
767/// - Versus O(v + e + p * l) for enumerate_paths (redundant analysis)
768///
769/// # Arguments
770///
771/// * `cfg` - Control flow graph to analyze
772/// * `limits` - Path enumeration limits
773/// * `ctx` - Pre-computed enumeration context
774///
775/// # Returns
776///
777/// Vector of all enumerated execution paths
778///
779/// # Example
780///
781/// ```rust,no_run
782/// # use mirage_analyzer::cfg::{PathLimits, enumerate_paths_with_context, EnumerationContext};
783/// # use mirage_analyzer::cfg::Cfg;
784/// # let graph: Cfg = unimplemented!();
785/// let ctx = EnumerationContext::new(&graph);
786/// let limits = PathLimits::default();
787/// let paths = enumerate_paths_with_context(&graph, &limits, &ctx);
788/// ```
789pub fn enumerate_paths_with_context(
790    cfg: &Cfg,
791    limits: &PathLimits,
792    ctx: &EnumerationContext,
793) -> Vec<Path> {
794    // Get entry block
795    let entry = match crate::cfg::analysis::find_entry(cfg) {
796        Some(e) => e,
797        None => return vec![], // Empty CFG
798    };
799
800    if ctx.exits.is_empty() {
801        return vec![]; // No exits means no complete paths
802    }
803
804    // Initialize traversal state
805    let mut paths = Vec::new();
806    let mut current_path = Vec::new();
807    let mut visited = HashSet::new();
808    let mut loop_iterations: HashMap<NodeIndex, usize> = HashMap::new();
809
810    // Start DFS from entry
811    dfs_enumerate_with_context(
812        cfg,
813        entry,
814        limits,
815        &mut paths,
816        &mut current_path,
817        &mut visited,
818        ctx,
819        &mut loop_iterations,
820    );
821
822    paths
823}
824
825/// Recursive DFS helper for path enumeration with pre-computed context
826fn dfs_enumerate_with_context(
827    cfg: &Cfg,
828    current: NodeIndex,
829    limits: &PathLimits,
830    paths: &mut Vec<Path>,
831    current_path: &mut Vec<BlockId>,
832    visited: &mut HashSet<NodeIndex>,
833    ctx: &EnumerationContext,
834    loop_iterations: &mut HashMap<NodeIndex, usize>,
835) {
836    // Get current block ID
837    let block_id = match cfg.node_weight(current) {
838        Some(block) => block.id,
839        None => return,
840    };
841
842    // Add current block to path
843    current_path.push(block_id);
844
845    // Check path length limit
846    if current_path.len() > limits.max_length {
847        current_path.pop();
848        return;
849    }
850
851    // Check if we've reached an exit
852    if ctx.is_exit(current) {
853        // Classify the path using pre-computed reachable set
854        let kind = classify_path_precomputed(cfg, current_path, &ctx.reachable_blocks);
855        let path = Path::new(current_path.clone(), kind);
856        paths.push(path);
857        current_path.pop();
858        return;
859    }
860
861    // Check path count limit
862    if paths.len() >= limits.max_paths {
863        current_path.pop();
864        return;
865    }
866
867    // Check if already visited (cycle detection)
868    // Loop headers are exempt - we track loop iterations separately
869    if visited.contains(&current) && !ctx.is_loop_header(current) {
870        current_path.pop();
871        return;
872    }
873
874    // Mark as visited
875    visited.insert(current);
876
877    // Track loop iterations for loop headers
878    let is_loop_header = ctx.is_loop_header(current);
879    if is_loop_header {
880        let count = loop_iterations.entry(current).or_insert(0);
881        if *count >= limits.loop_unroll_limit {
882            visited.remove(&current);
883            current_path.pop();
884            return;
885        }
886        *count += 1;
887    }
888
889    // Explore neighbors
890    let neighbors: Vec<_> = cfg
891        .neighbors(current)
892        .collect();
893
894    for next in neighbors {
895        dfs_enumerate_with_context(
896            cfg,
897            next,
898            limits,
899            paths,
900            current_path,
901            visited,
902            ctx,
903            loop_iterations,
904        );
905    }
906
907    // Backtrack: decrement loop iteration count if this was a loop header
908    if is_loop_header {
909        if let Some(count) = loop_iterations.get_mut(&current) {
910            *count = count.saturating_sub(1);
911        }
912    }
913
914    visited.remove(&current);
915    current_path.pop();
916}
917
918/// Enumerate all execution paths through a CFG
919///
920/// Performs depth-first search from the entry block to all exit blocks,
921/// collecting complete paths. Cycle detection prevents infinite recursion
922/// on back-edges, and loop bounding limits exploration of cyclic paths.
923///
924/// Paths are classified using `classify_path_precomputed` for efficiency.
925///
926/// # Arguments
927///
928/// * `cfg` - Control flow graph to analyze
929/// * `limits` - Limits on path enumeration
930///
931/// # Returns
932///
933/// Vector of all discovered paths from entry to exit
934///
935/// # Examples
936///
937/// ```rust,no_run
938/// # use mirage_analyzer::cfg::{enumerate_paths, PathLimits};
939/// # use mirage_analyzer::cfg::Cfg;
940/// # let graph: Cfg = unimplemented!();
941/// let paths = enumerate_paths(&graph, &PathLimits::default());
942/// println!("Found {} paths", paths.len());
943/// ```
944pub fn enumerate_paths(cfg: &Cfg, limits: &PathLimits) -> Vec<Path> {
945    // Get entry block
946    let entry = match crate::cfg::analysis::find_entry(cfg) {
947        Some(e) => e,
948        None => return vec![], // Empty CFG
949    };
950
951    // Get exit blocks
952    let exits: HashSet<NodeIndex> = crate::cfg::analysis::find_exits(cfg)
953        .into_iter()
954        .collect();
955
956    if exits.is_empty() {
957        return vec![]; // No exits means no complete paths
958    }
959
960    // Pre-compute reachable blocks for efficient classification
961    let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
962    let reachable_blocks: HashSet<BlockId> = reachable_nodes
963        .iter()
964        .map(|&idx| cfg[idx].id)
965        .collect();
966
967    // Initialize traversal state
968    let mut paths = Vec::new();
969    let mut current_path = Vec::new();
970    let mut visited = HashSet::new();
971
972    // Get loop headers for bounding
973    let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
974    let mut loop_iterations: HashMap<NodeIndex, usize> = HashMap::new();
975
976    // Start DFS from entry
977    dfs_enumerate(
978        cfg,
979        entry,
980        &exits,
981        limits,
982        &mut paths,
983        &mut current_path,
984        &mut visited,
985        &loop_headers,
986        &mut loop_iterations,
987        &reachable_blocks,
988    );
989
990    paths
991}
992
993/// Recursive DFS helper for path enumeration
994///
995/// Explores all paths from the current node to exit blocks, tracking
996/// visited nodes to prevent cycles and respecting loop unroll limits.
997/// Uses pre-computed reachable set for efficient path classification.
998fn dfs_enumerate(
999    cfg: &Cfg,
1000    current: NodeIndex,
1001    exits: &HashSet<NodeIndex>,
1002    limits: &PathLimits,
1003    paths: &mut Vec<Path>,
1004    current_path: &mut Vec<BlockId>,
1005    visited: &mut HashSet<NodeIndex>,
1006    loop_headers: &HashSet<NodeIndex>,
1007    loop_iterations: &mut HashMap<NodeIndex, usize>,
1008    reachable_blocks: &HashSet<BlockId>,
1009) {
1010    // Get current block ID
1011    let block_id = match cfg.node_weight(current) {
1012        Some(block) => block.id,
1013        None => return,
1014    };
1015
1016    // Add current block to path
1017    current_path.push(block_id);
1018
1019    // Check path length limit
1020    if current_path.len() > limits.max_length {
1021        current_path.pop();
1022        return;
1023    }
1024
1025    // Check if we've reached an exit
1026    if exits.contains(&current) {
1027        // Classify the path using pre-computed reachable set
1028        let kind = classify_path_precomputed(cfg, current_path, reachable_blocks);
1029        let path = Path::new(current_path.clone(), kind);
1030        paths.push(path);
1031        current_path.pop();
1032        return;
1033    }
1034
1035    // Check path count limit
1036    if paths.len() >= limits.max_paths {
1037        current_path.pop();
1038        return;
1039    }
1040
1041    // Track loop iterations
1042    let is_loop_header = loop_headers.contains(&current);
1043    if is_loop_header {
1044        let count = loop_iterations.entry(current).or_insert(0);
1045        if *count >= limits.loop_unroll_limit {
1046            // Exceeded unroll limit, stop this branch
1047            current_path.pop();
1048            return;
1049        }
1050        *count += 1;
1051    }
1052
1053    // Mark as visited for cycle detection
1054    let was_visited = visited.insert(current);
1055
1056    // Explore all successors
1057    let mut successors: Vec<NodeIndex> = cfg.neighbors(current).collect();
1058    successors.sort_by_key(|n| n.index()); // Deterministic order
1059
1060    if successors.is_empty() {
1061        // Dead end (not an exit but no successors)
1062        // Use classification to determine path kind
1063        let kind = classify_path_precomputed(cfg, current_path, reachable_blocks);
1064        let path = Path::new(current_path.clone(), kind);
1065        paths.push(path);
1066    } else {
1067        for succ in successors {
1068            // Skip already visited nodes UNLESS it's a back-edge to a loop header
1069            // Loop headers can be revisited (bounded by loop_iterations)
1070            let is_back_edge = loop_headers.contains(&succ) && loop_iterations.contains_key(&succ);
1071            if visited.contains(&succ) && !is_back_edge {
1072                continue;
1073            }
1074
1075            // For back-edges to loop headers, check iteration limit
1076            if is_back_edge {
1077                let count = loop_iterations.get(&succ).copied().unwrap_or(0);
1078                if count >= limits.loop_unroll_limit {
1079                    continue; // Exceeded loop unroll limit
1080                }
1081            }
1082
1083            // Recurse into successor
1084            dfs_enumerate(
1085                cfg,
1086                succ,
1087                exits,
1088                limits,
1089                paths,
1090                current_path,
1091                visited,
1092                loop_headers,
1093                loop_iterations,
1094                reachable_blocks,
1095            );
1096
1097            // Check path count limit after each recursive call
1098            if paths.len() >= limits.max_paths {
1099                break;
1100            }
1101        }
1102    }
1103
1104    // Unmark visited (backtrack)
1105    if was_visited {
1106        visited.remove(&current);
1107    }
1108
1109    // Clean up loop iteration count
1110    if is_loop_header {
1111        loop_iterations.entry(current).and_modify(|c| *c -= 1);
1112    }
1113
1114    // Remove current block from path
1115    current_path.pop();
1116}
1117
1118/// Iterative DFS path enumeration (stack-based, no recursion)
1119///
1120/// Improved version of `enumerate_paths` that:
1121/// - Uses an explicit stack instead of recursion (prevents stack overflow)
1122/// - Performs early path deduplication (no duplicate paths stored)
1123/// - Tracks more path metadata during enumeration
1124///
1125/// **Advantages over recursive version:**
1126/// - No stack overflow on deeply nested CFGs
1127/// - Better memory locality (stack array vs recursive calls)
1128/// - Easier to add path metadata tracking
1129/// - Early deduplication saves memory/time
1130///
1131/// **Algorithm:**
1132/// 1. Push entry block onto work stack
1133/// 2. While stack not empty:
1134///    a. Pop current state
1135///    b. If at exit, record path (if unique)
1136///    c. Otherwise, push successors
1137/// 3. Track visited nodes per-path for cycle detection
1138///
1139/// # Arguments
1140///
1141/// * `cfg` - Control flow graph to analyze
1142/// * `limits` - Path enumeration limits
1143///
1144/// # Returns
1145///
1146/// Vector of unique paths through the CFG
1147///
1148/// # Example
1149///
1150/// ```rust,no_run
1151/// # use mirage_analyzer::cfg::{enumerate_paths_iterative, PathLimits, Cfg};
1152/// # let graph: Cfg = unimplemented!();
1153/// let paths = enumerate_paths_iterative(&graph, &PathLimits::default());
1154/// println!("Found {} unique paths", paths.len());
1155/// ```
1156pub fn enumerate_paths_iterative(cfg: &Cfg, limits: &PathLimits) -> Vec<Path> {
1157    use std::collections::BTreeSet;
1158
1159    // Get entry block
1160    let entry = match crate::cfg::analysis::find_entry(cfg) {
1161        Some(e) => e,
1162        None => return vec![],
1163    };
1164
1165    // Get exit blocks
1166    let exits: HashSet<NodeIndex> = crate::cfg::analysis::find_exits(cfg)
1167        .into_iter()
1168        .collect();
1169
1170    if exits.is_empty() {
1171        return vec![]; // No exits means no complete paths
1172    }
1173
1174    // Pre-compute analysis
1175    let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
1176    let reachable_blocks: HashSet<BlockId> = reachable_nodes
1177        .iter()
1178        .map(|&idx| cfg[idx].id)
1179        .collect();
1180    let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
1181
1182    // Path deduplication: use BTreeSet keyed by block sequence
1183    // This prevents storing duplicate paths during enumeration
1184    let mut seen_paths: BTreeSet<Vec<BlockId>> = BTreeSet::new();
1185
1186    // Work stack: each element is (current_node, current_path, visited_set, loop_iteration_count)
1187    struct StackFrame {
1188        node: NodeIndex,
1189        path: Vec<BlockId>,
1190        visited: HashSet<NodeIndex>,
1191        loop_iterations: HashMap<NodeIndex, usize>,
1192    }
1193
1194    let mut stack = Vec::new();
1195    let mut paths = Vec::new();
1196
1197    // Initialize with entry block
1198    let entry_block_id = cfg[entry].id;
1199    let mut initial_visited = HashSet::new();
1200    initial_visited.insert(entry);
1201
1202    stack.push(StackFrame {
1203        node: entry,
1204        path: vec![entry_block_id],
1205        visited: initial_visited,
1206        loop_iterations: HashMap::new(),
1207    });
1208
1209    while let Some(frame) = stack.pop() {
1210        let StackFrame {
1211            node: current,
1212            path: current_path,
1213            visited: current_visited,
1214            mut loop_iterations,
1215        } = frame;
1216
1217        // Check path length limit
1218        if current_path.len() > limits.max_length {
1219            continue;
1220        }
1221
1222        // Check if we've reached an exit
1223        if exits.contains(&current) {
1224            // Deduplicate: only add if we haven't seen this exact path
1225            if seen_paths.insert(current_path.clone()) {
1226                let kind = classify_path_precomputed(cfg, &current_path, &reachable_blocks);
1227                let path = Path::new(current_path, kind);
1228                paths.push(path);
1229            }
1230            continue;
1231        }
1232
1233        // Check path count limit
1234        if paths.len() >= limits.max_paths {
1235            break;
1236        }
1237
1238        // Get successors
1239        let mut successors: Vec<NodeIndex> = cfg.neighbors(current).collect();
1240        successors.sort_by_key(|n| n.index()); // Deterministic order
1241
1242        for succ in successors {
1243            // Skip already visited nodes (cycle detection)
1244            // Exception: back-edges to loop headers are allowed (bounded)
1245            let is_back_edge = loop_headers.contains(&succ)
1246                && loop_iterations.get(&succ).copied().unwrap_or(0) < limits.loop_unroll_limit;
1247
1248            if current_visited.contains(&succ) && !is_back_edge {
1249                continue;
1250            }
1251
1252            // Check loop iteration limit for back-edges
1253            if is_back_edge {
1254                let count = loop_iterations.entry(succ).or_insert(0);
1255                if *count >= limits.loop_unroll_limit {
1256                    continue;
1257                }
1258                *count += 1;
1259            }
1260
1261            // Create new path with successor
1262            let mut new_path = current_path.clone();
1263            let block_id = cfg[succ].id;
1264            new_path.push(block_id);
1265
1266            // Create new visited set
1267            let mut new_visited = current_visited.clone();
1268            new_visited.insert(succ);
1269
1270            // Push onto stack
1271            stack.push(StackFrame {
1272                node: succ,
1273                path: new_path,
1274                visited: new_visited,
1275                loop_iterations: loop_iterations.clone(),
1276            });
1277        }
1278    }
1279
1280    paths
1281}
1282
1283/// Path enumeration result with metadata
1284///
1285/// Enhanced result that includes statistics about the enumeration
1286/// to help AI agents understand the analysis quality.
1287#[derive(Debug, Clone)]
1288pub struct PathEnumerationResult {
1289    /// The enumerated paths
1290    pub paths: Vec<Path>,
1291    /// Whether the enumeration hit any limits
1292    pub limits_hit: LimitsHit,
1293    /// Statistics about the enumeration
1294    pub stats: EnumerationStats,
1295}
1296
1297/// Which limits were hit during enumeration
1298#[derive(Debug, Clone, PartialEq, Eq)]
1299pub enum LimitsHit {
1300    /// No limits hit - complete enumeration
1301    None,
1302    /// Hit max path length limit
1303    MaxLength,
1304    /// Hit max path count limit
1305    MaxPaths,
1306    /// Hit loop unroll limit
1307    LoopUnroll,
1308    /// Multiple limits hit
1309    Multiple,
1310}
1311
1312/// Statistics about path enumeration
1313#[derive(Debug, Clone)]
1314pub struct EnumerationStats {
1315    /// Total paths found
1316    pub total_paths: usize,
1317    /// Normal paths (entry → return)
1318    pub normal_paths: usize,
1319    /// Error paths (contains panic/abort)
1320    pub error_paths: usize,
1321    /// Degenerate paths (dead ends, infinite loops)
1322    pub degenerate_paths: usize,
1323    /// Unreachable paths
1324    pub unreachable_paths: usize,
1325    /// Average path length
1326    pub avg_path_length: f64,
1327    /// Maximum path length found
1328    pub max_path_length: usize,
1329    /// Number of loops detected
1330    pub loop_count: usize,
1331}
1332
1333/// Enumerate paths with detailed metadata
1334///
1335/// Returns both the paths and information about enumeration quality.
1336/// This helps AI agents understand if results are complete or truncated.
1337///
1338/// **Use case:** When you need to know if enumeration was complete
1339///
1340/// # Arguments
1341///
1342/// * `cfg` - Control flow graph to analyze
1343/// * `limits` - Path enumeration limits
1344///
1345/// # Returns
1346///
1347/// `PathEnumerationResult` with paths and metadata
1348///
1349/// # Example
1350///
1351/// ```rust,no_run
1352/// # use mirage_analyzer::cfg::{enumerate_paths_with_metadata, PathLimits, Cfg};
1353/// # let graph: Cfg = unimplemented!();
1354/// let result = enumerate_paths_with_metadata(&graph, &PathLimits::default());
1355///
1356/// match result.limits_hit {
1357///     mirage_analyzer::cfg::paths::LimitsHit::None => {
1358///         println!("Complete enumeration: {} paths", result.stats.total_paths);
1359///     }
1360///     mirage_analyzer::cfg::paths::LimitsHit::MaxPaths => {
1361///         println!("Hit path limit - results may be truncated");
1362///     }
1363///     _ => {}
1364/// }
1365/// ```
1366pub fn enumerate_paths_with_metadata(cfg: &Cfg, limits: &PathLimits) -> PathEnumerationResult {
1367    let paths = enumerate_paths_iterative(cfg, limits);
1368
1369    let mut stats = EnumerationStats {
1370        total_paths: paths.len(),
1371        normal_paths: 0,
1372        error_paths: 0,
1373        degenerate_paths: 0,
1374        unreachable_paths: 0,
1375        avg_path_length: 0.0,
1376        max_path_length: 0,
1377        loop_count: 0,
1378    };
1379
1380    if paths.is_empty() {
1381        return PathEnumerationResult {
1382            paths,
1383            limits_hit: LimitsHit::None,
1384            stats,
1385        };
1386    }
1387
1388    let mut total_length = 0;
1389
1390    for path in &paths {
1391        match path.kind {
1392            PathKind::Normal => stats.normal_paths += 1,
1393            PathKind::Error => stats.error_paths += 1,
1394            PathKind::Degenerate => stats.degenerate_paths += 1,
1395            PathKind::Unreachable => stats.unreachable_paths += 1,
1396        }
1397
1398        let len = path.len();
1399        total_length += len;
1400        if len > stats.max_path_length {
1401            stats.max_path_length = len;
1402        }
1403    }
1404
1405    stats.avg_path_length = total_length as f64 / paths.len() as f64;
1406
1407    // Count loops
1408    stats.loop_count = crate::cfg::loops::find_loop_headers(cfg).len();
1409
1410    // Determine if limits were hit
1411    let limits_hit = if paths.len() >= limits.max_paths {
1412        LimitsHit::MaxPaths
1413    } else if stats.max_path_length >= limits.max_length {
1414        LimitsHit::MaxLength
1415    } else {
1416        LimitsHit::None
1417    };
1418
1419    PathEnumerationResult {
1420        paths,
1421        limits_hit,
1422        stats,
1423    }
1424}
1425
1426/// Get paths from cache or enumerate them
1427///
1428/// This bridge function connects the caching layer to path enumeration.
1429/// It checks if cached paths exist for the given function and hash,
1430/// and only enumerates if the cache is stale or empty.
1431///
1432/// # Arguments
1433///
1434/// * `cfg` - Control flow graph to analyze (for enumeration)
1435/// * `function_id` - Database ID of the function
1436/// * `function_hash` - Hash of the function content for cache validation
1437/// * `limits` - Limits for path enumeration (if enumeration is needed)
1438/// * `db_conn` - Database connection for caching
1439///
1440/// # Returns
1441///
1442/// Vector of paths, either from cache or freshly enumerated
1443///
1444/// # Algorithm
1445///
1446/// 1. Call `update_function_paths_if_changed` with empty paths to check hash
1447/// 2. If returns false (cache hit), retrieve cached paths
1448/// 3. If returns true (cache miss):
1449///    - Enumerate paths via `enumerate_paths`
1450///    - Store via `update_function_paths_if_changed` with actual paths
1451///    - Return paths
1452///
1453/// # Example
1454///
1455/// ```rust,no_run
1456/// # use mirage_analyzer::cfg::paths::get_or_enumerate_paths;
1457/// # use mirage_analyzer::cfg::{PathLimits, Cfg};
1458/// # let graph: Cfg = unimplemented!();
1459/// # let function_id: i64 = 0;
1460/// # let function_hash = "hash";
1461/// # let mut conn = unimplemented!();
1462/// let paths = get_or_enumerate_paths(
1463///     &graph,
1464///     function_id,
1465///     function_hash,
1466///     &PathLimits::default(),
1467///     &mut conn,
1468/// )?;
1469/// # Ok::<(), Box<dyn std::error::Error>>(())
1470/// ```
1471pub fn get_or_enumerate_paths(
1472    cfg: &Cfg,
1473    function_id: i64,
1474    function_hash: &str,
1475    limits: &PathLimits,
1476    db_conn: &mut rusqlite::Connection,
1477) -> Result<Vec<Path>, String> {
1478    use crate::storage::paths::{get_cached_paths, invalidate_function_paths, store_paths};
1479
1480    // Check current hash in cfg_blocks
1481    let current_hash: Option<String> = db_conn.query_row(
1482        "SELECT function_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
1483        rusqlite::params![function_id],
1484        |row| row.get(0),
1485    ).unwrap_or(None);
1486
1487    // If hash matches, return cached paths
1488    if let Some(ref hash) = current_hash {
1489        if hash == function_hash {
1490            // Cache hit - retrieve stored paths
1491            let paths = get_cached_paths(db_conn, function_id)
1492                .map_err(|e| format!("Failed to retrieve cached paths: {}", e))?;
1493            return Ok(paths);
1494        }
1495    }
1496
1497    // Cache miss or hash changed - enumerate and store paths
1498    let paths = enumerate_paths(cfg, limits);
1499
1500    // Invalidate old paths if any
1501    let _ = invalidate_function_paths(db_conn, function_id);
1502
1503    // Store the enumerated paths
1504    store_paths(db_conn, function_id, &paths)
1505        .map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
1506
1507    // Note: function_hash tracking removed - not available in Magellan's cfg_blocks schema
1508    // Magellan manages its own caching and re-indexing when source files change
1509
1510    Ok(paths)
1511}
1512
1513/// Enumerate paths with integrated caching and pre-computed context
1514///
1515/// This is the highest-level path enumeration function that combines:
1516/// 1. Hash-based cache invalidation (via update_function_paths_if_changed)
1517/// 2. Pre-computed enumeration context (avoid redundant analysis)
1518/// 3. Optimized batch storage (via store_paths_batch)
1519///
1520/// **Use this when:**
1521/// - You need the fastest path enumeration with caching
1522/// - Performing multiple enumerations on the same CFG
1523/// - You want automatic cache invalidation on function changes
1524///
1525/// **Algorithm:**
1526/// 1. Check cache via hash comparison (update_function_paths_if_changed)
1527/// 2. If cache hit: retrieve stored paths
1528/// 3. If cache miss:
1529///    - Create EnumerationContext
1530///    - Enumerate paths with context
1531///    - Store paths with batch insert
1532/// 4. Return paths
1533///
1534/// **Performance:**
1535/// - Cache hit: O(p) retrieval (p = stored path count)
1536/// - Cache miss: O(v+e + n*l) where v=vertices, e=edges, n=paths, l=avg length
1537/// - Subsequent calls with same CFG: O(v+e) for context only
1538///
1539/// # Arguments
1540///
1541/// * `cfg` - Control flow graph to analyze
1542/// * `function_id` - Database ID of the function
1543/// * `function_hash` - BLAKE3 hash of function content for cache validation
1544/// * `limits` - Path enumeration limits
1545/// * `db_conn` - Database connection for cache operations
1546///
1547/// # Returns
1548///
1549/// `Ok(Vec<Path>)` - Enumerated or cached paths
1550/// `Err(String)` - Error message if operation fails
1551///
1552/// # Example
1553///
1554/// ```rust,no_run
1555/// # use mirage_analyzer::cfg::{enumerate_paths_cached, PathLimits, EnumerationContext};
1556/// # use mirage_analyzer::cfg::Cfg;
1557/// # let graph: Cfg = unimplemented!();
1558/// # let function_bytes: Vec<u8> = vec![];
1559/// # let function_id: i64 = 0;
1560/// # let mut conn = unimplemented!();
1561/// let hash = blake3::hash(&function_bytes).to_hex().to_string();
1562/// let paths = enumerate_paths_cached(
1563///     &graph,
1564///     function_id,
1565///     &hash,
1566///     &PathLimits::default(),
1567///     &mut conn,
1568/// )?;
1569/// # Ok::<(), Box<dyn std::error::Error>>(())
1570/// ```
1571pub fn enumerate_paths_cached(
1572    cfg: &Cfg,
1573    function_id: i64,
1574    function_hash: &str,
1575    limits: &PathLimits,
1576    db_conn: &mut rusqlite::Connection,
1577) -> Result<Vec<Path>, String> {
1578    use crate::storage::paths::{get_cached_paths, update_function_paths_if_changed};
1579
1580    // Try to get cached paths first
1581    // update_function_paths_if_changed handles hash comparison and returns:
1582    // - Ok(false) if hash matched (cache hit)
1583    // - Ok(true) if paths were updated (cache miss)
1584    let current_hash: Option<String> = db_conn.query_row(
1585        "SELECT function_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
1586        rusqlite::params![function_id],
1587        |row| row.get(0),
1588    ).unwrap_or(None);
1589
1590    // Check if we have a cache hit
1591    if let Some(ref hash) = current_hash {
1592        if hash == function_hash {
1593            // Cache hit - retrieve stored paths
1594            let paths = get_cached_paths(db_conn, function_id)
1595                .map_err(|e| format!("Failed to retrieve cached paths: {}", e))?;
1596            return Ok(paths);
1597        }
1598    }
1599
1600    // Cache miss - enumerate paths with pre-computed context
1601    let ctx = EnumerationContext::new(cfg);
1602    let paths = enumerate_paths_with_context(cfg, limits, &ctx);
1603
1604    // Store paths using batch insert for performance
1605    update_function_paths_if_changed(db_conn, function_id, function_hash, &paths)
1606        .map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
1607
1608    Ok(paths)
1609}
1610
1611/// Enumerate paths with external context and integrated caching
1612///
1613/// Similar to `enumerate_paths_cached` but allows you to provide a pre-computed
1614/// EnumerationContext. Use this when performing multiple operations on the same CFG.
1615///
1616/// **Benefits over enumerate_paths_cached:**
1617/// - Reuse context across multiple calls
1618/// - Useful when you also need context for other analyses
1619///
1620/// # Arguments
1621///
1622/// * `cfg` - Control flow graph to analyze
1623/// * `function_id` - Database ID of the function
1624/// * `function_hash` - BLAKE3 hash of function content for cache validation
1625/// * `limits` - Path enumeration limits
1626/// * `ctx` - Pre-computed enumeration context
1627/// * `db_conn` - Database connection for cache operations
1628///
1629/// # Returns
1630///
1631/// `Ok(Vec<Path>)` - Enumerated or cached paths
1632/// `Err(String)` - Error message if operation fails
1633pub fn enumerate_paths_cached_with_context(
1634    cfg: &Cfg,
1635    function_id: i64,
1636    function_hash: &str,
1637    limits: &PathLimits,
1638    ctx: &EnumerationContext,
1639    db_conn: &mut rusqlite::Connection,
1640) -> Result<Vec<Path>, String> {
1641    use crate::storage::paths::{get_cached_paths, update_function_paths_if_changed};
1642
1643    // Try cache first
1644    let current_hash: Option<String> = db_conn.query_row(
1645        "SELECT function_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
1646        rusqlite::params![function_id],
1647        |row| row.get(0),
1648    ).unwrap_or(None);
1649
1650    if let Some(ref hash) = current_hash {
1651        if hash == function_hash {
1652            return get_cached_paths(db_conn, function_id)
1653                .map_err(|e| format!("Failed to retrieve cached paths: {}", e));
1654        }
1655    }
1656
1657    // Cache miss - use provided context
1658    let paths = enumerate_paths_with_context(cfg, limits, ctx);
1659
1660    update_function_paths_if_changed(db_conn, function_id, function_hash, &paths)
1661        .map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
1662
1663    Ok(paths)
1664}
1665
1666/// Estimate the number of paths in a CFG before enumeration
1667///
1668/// This provides an upper bound on path count using cyclomatic complexity
1669/// and loop structure analysis. Use this to warn users about potential
1670/// path explosion before running expensive enumeration.
1671///
1672/// **Algorithm:**
1673/// - Count loop headers (each contributes multiplicative complexity)
1674/// - Estimate: 2^(branches + loops) * (loop_unroll_limit + 1)^loop_count
1675/// - Cap at max_paths to avoid overflow
1676///
1677/// **Usage:**
1678/// ```rust,no_run
1679/// # use mirage_analyzer::cfg::paths::estimate_path_count;
1680/// # use mirage_analyzer::cfg::Cfg;
1681/// # use mirage_analyzer::cfg::PathLimits;
1682/// # let graph: Cfg = unimplemented!();
1683/// # let limits = PathLimits::default();
1684/// let estimated = estimate_path_count(&graph, 3);
1685/// if estimated > limits.max_paths {
1686///     // Warn user about path explosion
1687/// }
1688/// ```
1689///
1690/// **Limitations:**
1691/// - This is an estimate, not exact count
1692/// - Assumes worst-case (all paths are independent)
1693/// - Actual count may be lower due to dominance constraints
1694///
1695/// # Arguments
1696///
1697/// * `cfg` - Control flow graph to analyze
1698/// * `loop_unroll_limit` - Maximum loop iterations to account for
1699///
1700/// # Returns
1701///
1702/// Estimated maximum path count (upper bound)
1703pub fn estimate_path_count(cfg: &Cfg, loop_unroll_limit: usize) -> usize {
1704    // Count loop headers
1705    let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
1706    let loop_count = loop_headers.len();
1707
1708    // Count branch points (excluding loop back edges)
1709    let mut branch_count = 0;
1710    for node in cfg.node_indices() {
1711        if loop_headers.contains(&node) {
1712            continue; // Skip loop headers, counted separately
1713        }
1714        let out_degree = cfg.neighbors_directed(node, petgraph::Direction::Outgoing).count();
1715        if out_degree > 1 {
1716            branch_count += out_degree - 1; // Each extra edge adds complexity
1717        }
1718    }
1719
1720    // Base estimate: at least 1 path for acyclic CFG
1721    if loop_count == 0 && branch_count == 0 {
1722        return 1; // Single path (linear CFG)
1723    }
1724
1725    // Each branch roughly doubles path count
1726    // Each loop multiplies by (unroll_limit + 1)
1727    let unroll_factor = loop_unroll_limit + 1;
1728
1729    // Calculate: 2^branch_count * unroll_factor^loop_count
1730    // Use saturating operations to avoid overflow
1731    let branch_factor = if branch_count < 31 {
1732        2_usize.pow(branch_count as u32)
1733    } else {
1734        usize::MAX / 2 // Cap to avoid overflow
1735    };
1736
1737    let loop_factor = if loop_count < 31 {
1738        unroll_factor.pow(loop_count as u32)
1739    } else {
1740        usize::MAX / 2 // Cap to avoid overflow
1741    };
1742
1743    // Multiply with overflow protection
1744    branch_factor.saturating_mul(loop_factor)
1745}
1746
1747/// Check if path enumeration may exceed limits
1748///
1749/// This is a convenience wrapper around `estimate_path_count` that
1750/// compares the estimate against a limit and returns a warning if
1751/// the estimate suggests explosion.
1752///
1753/// # Returns
1754///
1755/// * `None` - Enumeration should be safe (estimate <= max_paths)
1756/// * `Some(estimate)` - Enumeration may exceed limit (estimate > max_paths)
1757pub fn check_path_explosion(cfg: &Cfg, limits: &PathLimits) -> Option<usize> {
1758    let estimate = estimate_path_count(cfg, limits.loop_unroll_limit);
1759    if estimate > limits.max_paths {
1760        Some(estimate)
1761    } else {
1762        None
1763    }
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768    use super::*;
1769    use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
1770    use petgraph::graph::DiGraph;
1771
1772    /// Create a simple linear CFG: 0 -> 1 -> 2
1773    fn create_linear_cfg() -> Cfg {
1774        let mut g = DiGraph::new();
1775
1776        let b0 = g.add_node(BasicBlock {
1777            id: 0,
1778            kind: BlockKind::Entry,
1779            statements: vec![],
1780            terminator: Terminator::Goto { target: 1 },
1781            source_location: None,
1782        });
1783
1784        let b1 = g.add_node(BasicBlock {
1785            id: 1,
1786            kind: BlockKind::Normal,
1787            statements: vec![],
1788            terminator: Terminator::Goto { target: 2 },
1789            source_location: None,
1790        });
1791
1792        let b2 = g.add_node(BasicBlock {
1793            id: 2,
1794            kind: BlockKind::Exit,
1795            statements: vec![],
1796            terminator: Terminator::Return,
1797            source_location: None,
1798        });
1799
1800        g.add_edge(b0, b1, EdgeType::Fallthrough);
1801        g.add_edge(b1, b2, EdgeType::Fallthrough);
1802
1803        g
1804    }
1805
1806    /// Create a diamond CFG: 0 -> (1, 2) -> 3
1807    fn create_diamond_cfg() -> Cfg {
1808        let mut g = DiGraph::new();
1809
1810        let b0 = g.add_node(BasicBlock {
1811            id: 0,
1812            kind: BlockKind::Entry,
1813            statements: vec![],
1814            terminator: Terminator::SwitchInt {
1815                targets: vec![1],
1816                otherwise: 2,
1817            },
1818            source_location: None,
1819        });
1820
1821        let b1 = g.add_node(BasicBlock {
1822            id: 1,
1823            kind: BlockKind::Normal,
1824            statements: vec![],
1825            terminator: Terminator::Goto { target: 3 },
1826            source_location: None,
1827        });
1828
1829        let b2 = g.add_node(BasicBlock {
1830            id: 2,
1831            kind: BlockKind::Normal,
1832            statements: vec![],
1833            terminator: Terminator::Goto { target: 3 },
1834            source_location: None,
1835        });
1836
1837        let b3 = g.add_node(BasicBlock {
1838            id: 3,
1839            kind: BlockKind::Exit,
1840            statements: vec![],
1841            terminator: Terminator::Return,
1842            source_location: None,
1843        });
1844
1845        g.add_edge(b0, b1, EdgeType::TrueBranch);
1846        g.add_edge(b0, b2, EdgeType::FalseBranch);
1847        g.add_edge(b1, b3, EdgeType::Fallthrough);
1848        g.add_edge(b2, b3, EdgeType::Fallthrough);
1849
1850        g
1851    }
1852
1853    /// Create a simple loop CFG: 0 -> 1 <-> 2 -> 3
1854    fn create_loop_cfg() -> Cfg {
1855        let mut g = DiGraph::new();
1856
1857        let b0 = g.add_node(BasicBlock {
1858            id: 0,
1859            kind: BlockKind::Entry,
1860            statements: vec![],
1861            terminator: Terminator::Goto { target: 1 },
1862            source_location: None,
1863        });
1864
1865        let b1 = g.add_node(BasicBlock {
1866            id: 1,
1867            kind: BlockKind::Normal,
1868            statements: vec![],
1869            terminator: Terminator::SwitchInt {
1870                targets: vec![2],
1871                otherwise: 3,
1872            },
1873            source_location: None,
1874        });
1875
1876        let b2 = g.add_node(BasicBlock {
1877            id: 2,
1878            kind: BlockKind::Normal,
1879            statements: vec![],
1880            terminator: Terminator::Goto { target: 1 },
1881            source_location: None,
1882        });
1883
1884        let b3 = g.add_node(BasicBlock {
1885            id: 3,
1886            kind: BlockKind::Exit,
1887            statements: vec![],
1888            terminator: Terminator::Return,
1889            source_location: None,
1890        });
1891
1892        g.add_edge(b0, b1, EdgeType::Fallthrough);
1893        g.add_edge(b1, b2, EdgeType::TrueBranch);
1894        g.add_edge(b1, b3, EdgeType::FalseBranch);
1895        g.add_edge(b2, b1, EdgeType::LoopBack);
1896
1897        g
1898    }
1899
1900    #[test]
1901    fn test_hash_path_deterministic() {
1902        let blocks = vec![0, 1, 2];
1903        let hash1 = hash_path(&blocks);
1904        let hash2 = hash_path(&blocks);
1905
1906        assert_eq!(hash1, hash2, "Same input should produce same hash");
1907    }
1908
1909    #[test]
1910    fn test_hash_path_different_sequences() {
1911        let blocks1 = vec![0, 1, 2];
1912        let blocks2 = vec![0, 2, 1];
1913
1914        assert_ne!(hash_path(&blocks1), hash_path(&blocks2));
1915    }
1916
1917    #[test]
1918    fn test_hash_path_length_collision_protection() {
1919        let blocks1 = vec![1, 2, 3];
1920        let blocks2 = vec![1, 2, 3, 0];
1921
1922        assert_ne!(hash_path(&blocks1), hash_path(&blocks2));
1923    }
1924
1925    #[test]
1926    fn test_path_new() {
1927        let blocks = vec![0, 1, 2];
1928        let path = Path::new(blocks.clone(), PathKind::Normal);
1929
1930        assert_eq!(path.blocks, blocks);
1931        assert_eq!(path.entry, 0);
1932        assert_eq!(path.exit, 2);
1933        assert_eq!(path.kind, PathKind::Normal);
1934        assert!(!path.path_id.is_empty());
1935    }
1936
1937    #[test]
1938    fn test_path_len() {
1939        let blocks = vec![0, 1, 2];
1940        let path = Path::new(blocks, PathKind::Normal);
1941
1942        assert_eq!(path.len(), 3);
1943        assert!(!path.is_empty());
1944    }
1945
1946    #[test]
1947    fn test_path_contains() {
1948        let blocks = vec![0, 1, 2];
1949        let path = Path::new(blocks, PathKind::Normal);
1950
1951        assert!(path.contains(0));
1952        assert!(path.contains(1));
1953        assert!(path.contains(2));
1954        assert!(!path.contains(3));
1955    }
1956
1957    #[test]
1958    fn test_path_limits_default() {
1959        let limits = PathLimits::default();
1960
1961        assert_eq!(limits.max_length, 1000);
1962        assert_eq!(limits.max_paths, 10000);
1963        assert_eq!(limits.loop_unroll_limit, 3);
1964    }
1965
1966    #[test]
1967    fn test_path_limits_custom() {
1968        let limits = PathLimits::new(100, 500, 5);
1969
1970        assert_eq!(limits.max_length, 100);
1971        assert_eq!(limits.max_paths, 500);
1972        assert_eq!(limits.loop_unroll_limit, 5);
1973    }
1974
1975    #[test]
1976    fn test_path_limits_builder() {
1977        let limits = PathLimits::default()
1978            .with_max_length(200)
1979            .with_max_paths(1000)
1980            .with_loop_unroll_limit(10);
1981
1982        assert_eq!(limits.max_length, 200);
1983        assert_eq!(limits.max_paths, 1000);
1984        assert_eq!(limits.loop_unroll_limit, 10);
1985    }
1986
1987    #[test]
1988    fn test_path_kind_is_normal() {
1989        assert!(PathKind::Normal.is_normal());
1990        assert!(!PathKind::Error.is_normal());
1991        assert!(!PathKind::Degenerate.is_normal());
1992        assert!(!PathKind::Unreachable.is_normal());
1993    }
1994
1995    #[test]
1996    fn test_path_kind_is_error() {
1997        assert!(PathKind::Error.is_error());
1998        assert!(!PathKind::Normal.is_error());
1999    }
2000
2001    #[test]
2002    fn test_path_kind_is_degenerate() {
2003        assert!(PathKind::Degenerate.is_degenerate());
2004        assert!(!PathKind::Normal.is_degenerate());
2005    }
2006
2007    #[test]
2008    fn test_path_kind_is_unreachable() {
2009        assert!(PathKind::Unreachable.is_unreachable());
2010        assert!(!PathKind::Normal.is_unreachable());
2011    }
2012
2013    // find_node_by_block_id tests
2014
2015    #[test]
2016    fn test_find_node_by_block_id_existing() {
2017        let cfg = create_linear_cfg();
2018
2019        // Find existing blocks
2020        let b0 = find_node_by_block_id(&cfg, 0);
2021        let b1 = find_node_by_block_id(&cfg, 1);
2022        let b2 = find_node_by_block_id(&cfg, 2);
2023
2024        assert!(b0.is_some());
2025        assert!(b1.is_some());
2026        assert!(b2.is_some());
2027
2028        // Verify the NodeIndices are correct
2029        assert_eq!(b0.unwrap().index(), 0);
2030        assert_eq!(b1.unwrap().index(), 1);
2031        assert_eq!(b2.unwrap().index(), 2);
2032    }
2033
2034    #[test]
2035    fn test_find_node_by_block_id_nonexistent() {
2036        let cfg = create_linear_cfg();
2037
2038        // Find non-existent block
2039        let b99 = find_node_by_block_id(&cfg, 99);
2040        assert!(b99.is_none());
2041    }
2042
2043    #[test]
2044    fn test_find_node_by_block_id_empty_cfg() {
2045        let cfg: Cfg = DiGraph::new();
2046
2047        // Empty CFG has no blocks
2048        let b0 = find_node_by_block_id(&cfg, 0);
2049        assert!(b0.is_none());
2050    }
2051
2052    // classify_path tests
2053
2054    /// Create a CFG with an Abort terminator (error path)
2055    fn create_error_cfg() -> Cfg {
2056        let mut g = DiGraph::new();
2057
2058        let b0 = g.add_node(BasicBlock {
2059            id: 0,
2060            kind: BlockKind::Entry,
2061            statements: vec![],
2062            terminator: Terminator::Goto { target: 1 },
2063            source_location: None,
2064        });
2065
2066        let b1 = g.add_node(BasicBlock {
2067            id: 1,
2068            kind: BlockKind::Exit,
2069            statements: vec![],
2070            terminator: Terminator::Abort("panic!".to_string()),
2071            source_location: None,
2072        });
2073
2074        g.add_edge(b0, b1, EdgeType::Fallthrough);
2075
2076        g
2077    }
2078
2079    /// Create a CFG with unreachable terminator (degenerate path)
2080    fn create_unreachable_term_cfg() -> Cfg {
2081        let mut g = DiGraph::new();
2082
2083        let b0 = g.add_node(BasicBlock {
2084            id: 0,
2085            kind: BlockKind::Entry,
2086            statements: vec![],
2087            terminator: Terminator::Goto { target: 1 },
2088            source_location: None,
2089        });
2090
2091        let b1 = g.add_node(BasicBlock {
2092            id: 1,
2093            kind: BlockKind::Exit,
2094            statements: vec![],
2095            terminator: Terminator::Unreachable,
2096            source_location: None,
2097        });
2098
2099        g.add_edge(b0, b1, EdgeType::Fallthrough);
2100
2101        g
2102    }
2103
2104    /// Create a CFG with an unreachable block (dead code)
2105    fn create_dead_code_cfg() -> Cfg {
2106        let mut g = DiGraph::new();
2107
2108        let _b0 = g.add_node(BasicBlock {
2109            id: 0,
2110            kind: BlockKind::Entry,
2111            statements: vec![],
2112            terminator: Terminator::Return,
2113            source_location: None,
2114        });
2115
2116        // Block 1 is not reachable from entry
2117        let _b1 = g.add_node(BasicBlock {
2118            id: 1,
2119            kind: BlockKind::Exit,
2120            statements: vec![],
2121            terminator: Terminator::Return,
2122            source_location: None,
2123        });
2124
2125        g
2126    }
2127
2128    /// Create a CFG with Call that has unwind (error path)
2129    fn create_call_unwind_cfg() -> Cfg {
2130        let mut g = DiGraph::new();
2131
2132        let b0 = g.add_node(BasicBlock {
2133            id: 0,
2134            kind: BlockKind::Entry,
2135            statements: vec![],
2136            terminator: Terminator::Call {
2137                target: Some(1),
2138                unwind: Some(2), // Has unwind -> Error path
2139            },
2140            source_location: None,
2141        });
2142
2143        let b1 = g.add_node(BasicBlock {
2144            id: 1,
2145            kind: BlockKind::Exit,
2146            statements: vec![],
2147            terminator: Terminator::Return,
2148            source_location: None,
2149        });
2150
2151        let _b2 = g.add_node(BasicBlock {
2152            id: 2,
2153            kind: BlockKind::Exit,
2154            statements: vec![],
2155            terminator: Terminator::Return,
2156            source_location: None,
2157        });
2158
2159        g.add_edge(b0, b1, EdgeType::Fallthrough);
2160
2161        g
2162    }
2163
2164    #[test]
2165    fn test_classify_path_normal_return() {
2166        let cfg = create_linear_cfg();
2167        let path = vec![0, 1, 2];
2168
2169        let kind = classify_path(&cfg, &path);
2170        assert_eq!(kind, PathKind::Normal);
2171    }
2172
2173    #[test]
2174    fn test_classify_path_error_abort() {
2175        let cfg = create_error_cfg();
2176        let path = vec![0, 1];
2177
2178        let kind = classify_path(&cfg, &path);
2179        assert_eq!(kind, PathKind::Error);
2180    }
2181
2182    #[test]
2183    fn test_classify_path_degenerate_unreachable_terminator() {
2184        let cfg = create_unreachable_term_cfg();
2185        let path = vec![0, 1];
2186
2187        let kind = classify_path(&cfg, &path);
2188        assert_eq!(kind, PathKind::Degenerate);
2189    }
2190
2191    #[test]
2192    fn test_classify_path_unreachable_block() {
2193        let cfg = create_dead_code_cfg();
2194        // Path includes unreachable block
2195        let path = vec![1]; // Block 1 is not reachable from entry
2196
2197        let kind = classify_path(&cfg, &path);
2198        assert_eq!(kind, PathKind::Unreachable);
2199    }
2200
2201    #[test]
2202    fn test_classify_path_error_call_unwind() {
2203        let cfg = create_call_unwind_cfg();
2204        let path = vec![0, 1]; // Goes through Call with unwind
2205
2206        let kind = classify_path(&cfg, &path);
2207        assert_eq!(kind, PathKind::Error);
2208    }
2209
2210    #[test]
2211    fn test_classify_path_empty() {
2212        let cfg = create_linear_cfg();
2213        let path: Vec<BlockId> = vec![];
2214
2215        let kind = classify_path(&cfg, &path);
2216        assert_eq!(kind, PathKind::Degenerate);
2217    }
2218
2219    #[test]
2220    fn test_classify_path_single_block() {
2221        let mut g = DiGraph::new();
2222
2223        let _b0 = g.add_node(BasicBlock {
2224            id: 0,
2225            kind: BlockKind::Entry,
2226            statements: vec![],
2227            terminator: Terminator::Return,
2228            source_location: None,
2229        });
2230
2231        let path = vec![0];
2232        let kind = classify_path(&g, &path);
2233        assert_eq!(kind, PathKind::Normal);
2234    }
2235
2236    #[test]
2237    fn test_classify_path_nonexistent_block() {
2238        let cfg = create_linear_cfg();
2239        let path = vec![0, 99]; // Block 99 doesn't exist
2240
2241        let kind = classify_path(&cfg, &path);
2242        assert_eq!(kind, PathKind::Degenerate);
2243    }
2244
2245    // classify_path_precomputed tests
2246
2247    #[test]
2248    fn test_classify_path_precomputed_matches_classify_path() {
2249        let cfg = create_diamond_cfg();
2250
2251        // Pre-compute reachable set
2252        use crate::cfg::reachability::find_reachable;
2253        let reachable_nodes = find_reachable(&cfg);
2254        let reachable_blocks: HashSet<BlockId> = reachable_nodes
2255            .iter()
2256            .map(|&idx| cfg[idx].id)
2257            .collect();
2258
2259        // Test multiple paths
2260        let test_paths = vec![
2261            vec![0, 1, 3],
2262            vec![0, 2, 3],
2263            vec![0, 1],
2264            vec![0],
2265        ];
2266
2267        for path in test_paths {
2268            let kind1 = classify_path(&cfg, &path);
2269            let kind2 = classify_path_precomputed(&cfg, &path, &reachable_blocks);
2270            assert_eq!(
2271                kind1, kind2,
2272                "classify_path_precomputed should match classify_path for path {:?}",
2273                path
2274            );
2275        }
2276    }
2277
2278    #[test]
2279    fn test_classify_path_precomputed_unreachable() {
2280        let cfg = create_dead_code_cfg();
2281
2282        // Pre-compute reachable set (only block 0 is reachable)
2283        use crate::cfg::reachability::find_reachable;
2284        let reachable_nodes = find_reachable(&cfg);
2285        let reachable_blocks: HashSet<BlockId> = reachable_nodes
2286            .iter()
2287            .map(|&idx| cfg[idx].id)
2288            .collect();
2289
2290        // Block 1 is unreachable
2291        let path = vec![1];
2292        let kind = classify_path_precomputed(&cfg, &path, &reachable_blocks);
2293        assert_eq!(kind, PathKind::Unreachable);
2294    }
2295
2296    #[test]
2297    fn test_classify_path_precomputed_performance() {
2298        use crate::cfg::reachability::find_reachable;
2299        use std::time::Instant;
2300
2301        let cfg = create_diamond_cfg();
2302
2303        // Pre-compute reachable set once
2304        let reachable_nodes = find_reachable(&cfg);
2305        let reachable_blocks: HashSet<BlockId> = reachable_nodes
2306            .iter()
2307            .map(|&idx| cfg[idx].id)
2308            .collect();
2309
2310        // Create many test paths
2311        let test_paths: Vec<Vec<BlockId>> = (0..1000)
2312            .map(|_| vec![0, 1, 3])
2313            .collect();
2314
2315        // Time the precomputed version
2316        let start = Instant::now();
2317        for path in &test_paths {
2318            let _ = classify_path_precomputed(&cfg, path, &reachable_blocks);
2319        }
2320        let precomputed_duration = start.elapsed();
2321
2322        // Should be very fast (< 10ms for 1000 paths)
2323        assert!(
2324            precomputed_duration.as_millis() < 10,
2325            "classify_path_precomputed should classify 1000 paths in <10ms, took {}ms",
2326            precomputed_duration.as_millis()
2327        );
2328    }
2329
2330    #[test]
2331    fn test_classify_path_precomputed_all_kinds() {
2332        use crate::cfg::reachability::find_reachable;
2333
2334        // Test with normal path
2335        let cfg_normal = create_linear_cfg();
2336        let reachable = find_reachable(&cfg_normal)
2337            .iter()
2338            .map(|&idx| cfg_normal[idx].id)
2339            .collect();
2340        assert_eq!(
2341            classify_path_precomputed(&cfg_normal, &[0, 1, 2], &reachable),
2342            PathKind::Normal
2343        );
2344
2345        // Test with error path
2346        let cfg_error = create_error_cfg();
2347        let reachable = find_reachable(&cfg_error)
2348            .iter()
2349            .map(|&idx| cfg_error[idx].id)
2350            .collect();
2351        assert_eq!(
2352            classify_path_precomputed(&cfg_error, &[0, 1], &reachable),
2353            PathKind::Error
2354        );
2355
2356        // Test with degenerate path
2357        let cfg_degen = create_unreachable_term_cfg();
2358        let reachable = find_reachable(&cfg_degen)
2359            .iter()
2360            .map(|&idx| cfg_degen[idx].id)
2361            .collect();
2362        assert_eq!(
2363            classify_path_precomputed(&cfg_degen, &[0, 1], &reachable),
2364            PathKind::Degenerate
2365        );
2366    }
2367
2368    // enumerate_paths tests
2369
2370    #[test]
2371    fn test_enumerate_paths_linear_cfg() {
2372        let cfg = create_linear_cfg();
2373        let paths = enumerate_paths(&cfg, &PathLimits::default());
2374
2375        // Linear CFG produces exactly 1 path
2376        assert_eq!(paths.len(), 1);
2377        assert_eq!(paths[0].blocks, vec![0, 1, 2]);
2378        assert_eq!(paths[0].entry, 0);
2379        assert_eq!(paths[0].exit, 2);
2380        assert_eq!(paths[0].kind, PathKind::Normal);
2381    }
2382
2383    #[test]
2384    fn test_enumerate_paths_diamond_cfg() {
2385        let cfg = create_diamond_cfg();
2386        let paths = enumerate_paths(&cfg, &PathLimits::default());
2387
2388        // Diamond CFG produces 2 paths: 0->1->3 and 0->2->3
2389        assert_eq!(paths.len(), 2);
2390
2391        // Check that both paths start at entry and end at exit
2392        for path in &paths {
2393            assert_eq!(path.entry, 0);
2394            assert_eq!(path.exit, 3);
2395            assert_eq!(path.kind, PathKind::Normal);
2396        }
2397
2398        // Check that we have both distinct paths
2399        let path_blocks: Vec<_> = paths.iter().map(|p| p.blocks.clone()).collect();
2400        assert!(path_blocks.contains(&vec![0, 1, 3]));
2401        assert!(path_blocks.contains(&vec![0, 2, 3]));
2402    }
2403
2404    #[test]
2405    fn test_enumerate_paths_loop_with_unroll_limit() {
2406        let cfg = create_loop_cfg();
2407
2408        // With unroll_limit=3, we get bounded paths
2409        // With loop unroll limit of 3, we get:
2410        // - Direct exit: 0->1->3
2411        // - 1 iteration: 0->1->2->1->3
2412        // - 2 iterations: 0->1->2->1->2->1->3
2413        // - 3 iterations: 0->1->2->1->2->1->2->1->3
2414        let limits = PathLimits::default().with_loop_unroll_limit(3);
2415        let paths = enumerate_paths(&cfg, &limits);
2416
2417        // Should have 4 paths (0, 1, 2, 3 loop iterations)
2418        // Or possibly 2 paths depending on how loop iteration is counted
2419        // The key is that loop is bounded and doesn't cause infinite paths
2420        assert!(paths.len() >= 2, "Should have at least direct exit and one loop iteration");
2421        assert!(paths.len() <= 5, "Should be bounded by loop unroll limit");
2422
2423        // All paths should be normal
2424        for path in &paths {
2425            assert_eq!(path.kind, PathKind::Normal);
2426            assert_eq!(path.entry, 0);
2427            assert_eq!(path.exit, 3);
2428        }
2429
2430        // Direct exit path should exist
2431        assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 3]));
2432    }
2433
2434    #[test]
2435    fn test_enumerate_paths_empty_cfg() {
2436        let cfg: Cfg = DiGraph::new();
2437        let paths = enumerate_paths(&cfg, &PathLimits::default());
2438
2439        // Empty CFG produces 0 paths (no crash)
2440        assert_eq!(paths.len(), 0);
2441    }
2442
2443    #[test]
2444    fn test_enumerate_paths_max_paths_limit() {
2445        let cfg = create_diamond_cfg();
2446
2447        // Set very low max_paths limit
2448        let limits = PathLimits::default().with_max_paths(1);
2449        let paths = enumerate_paths(&cfg, &limits);
2450
2451        // Should stop at 1 path even though diamond has 2
2452        assert_eq!(paths.len(), 1);
2453    }
2454
2455    #[test]
2456    fn test_enumerate_paths_max_length_limit() {
2457        let cfg = create_diamond_cfg();
2458
2459        // Set very low max_length limit
2460        let limits = PathLimits::default().with_max_length(2);
2461        let paths = enumerate_paths(&cfg, &limits);
2462
2463        // Should return 0 paths because all paths exceed length 2
2464        assert_eq!(paths.len(), 0);
2465    }
2466
2467    #[test]
2468    fn test_enumerate_paths_single_block_cfg() {
2469        let mut g = DiGraph::new();
2470
2471        let _b0 = g.add_node(BasicBlock {
2472            id: 0,
2473            kind: BlockKind::Entry,
2474            statements: vec![],
2475            terminator: Terminator::Return,
2476            source_location: None,
2477        });
2478
2479        // A single block that is both entry and exit
2480        let paths = enumerate_paths(&g, &PathLimits::default());
2481
2482        assert_eq!(paths.len(), 1);
2483        assert_eq!(paths[0].blocks, vec![0]);
2484        assert_eq!(paths[0].entry, 0);
2485        assert_eq!(paths[0].exit, 0);
2486    }
2487
2488    #[test]
2489    fn test_enumerate_paths_with_unreachable_exit() {
2490        let mut g = DiGraph::new();
2491
2492        // Block 0: entry
2493        let b0 = g.add_node(BasicBlock {
2494            id: 0,
2495            kind: BlockKind::Entry,
2496            statements: vec![],
2497            terminator: Terminator::Goto { target: 1 },
2498            source_location: None,
2499        });
2500
2501        // Block 1: return
2502        let b1 = g.add_node(BasicBlock {
2503            id: 1,
2504            kind: BlockKind::Exit,
2505            statements: vec![],
2506            terminator: Terminator::Return,
2507            source_location: None,
2508        });
2509
2510        // Block 2: unreachable (not connected)
2511        let _b2 = g.add_node(BasicBlock {
2512            id: 2,
2513            kind: BlockKind::Exit,
2514            statements: vec![],
2515            terminator: Terminator::Unreachable,
2516            source_location: None,
2517        });
2518
2519        g.add_edge(b0, b1, EdgeType::Fallthrough);
2520
2521        let paths = enumerate_paths(&g, &PathLimits::default());
2522
2523        // Only reachable exit produces a path
2524        assert_eq!(paths.len(), 1);
2525        assert_eq!(paths[0].blocks, vec![0, 1]);
2526    }
2527
2528    // enumerate_paths_classification integration tests
2529
2530    #[test]
2531    fn test_enumerate_paths_classification_diamond() {
2532        let cfg = create_diamond_cfg();
2533        let paths = enumerate_paths(&cfg, &PathLimits::default());
2534
2535        // Diamond CFG: all paths should be Normal
2536        assert_eq!(paths.len(), 2);
2537        for path in &paths {
2538            assert_eq!(
2539                path.kind,
2540                PathKind::Normal,
2541                "Diamond CFG should only have Normal paths, got {:?} for {:?}",
2542                path.kind,
2543                path.blocks
2544            );
2545        }
2546    }
2547
2548    #[test]
2549    fn test_enumerate_paths_classification_with_error() {
2550        // Create CFG with error path
2551        let cfg = create_error_cfg();
2552        let paths = enumerate_paths(&cfg, &PathLimits::default());
2553
2554        // Should have one path that ends in Abort (Error)
2555        assert_eq!(paths.len(), 1);
2556        assert_eq!(paths[0].kind, PathKind::Error);
2557        assert_eq!(paths[0].blocks, vec![0, 1]);
2558    }
2559
2560    #[test]
2561    fn test_enumerate_paths_classification_with_unreachable() {
2562        // Create CFG with unreachable block
2563        let cfg = create_dead_code_cfg();
2564        let paths = enumerate_paths(&cfg, &PathLimits::default());
2565
2566        // Only the reachable path should be enumerated
2567        assert_eq!(paths.len(), 1);
2568        // The path goes through block 0 which is reachable
2569        assert_eq!(paths[0].blocks, vec![0]);
2570        assert_eq!(paths[0].kind, PathKind::Normal);
2571    }
2572
2573    #[test]
2574    fn test_enumerate_paths_classification_mixed() {
2575        // Create a CFG with both normal and error paths
2576        let mut g = DiGraph::new();
2577
2578        // Entry block with conditional
2579        let b0 = g.add_node(BasicBlock {
2580            id: 0,
2581            kind: BlockKind::Entry,
2582            statements: vec![],
2583            terminator: Terminator::SwitchInt { targets: vec![1], otherwise: 2 },
2584            source_location: None,
2585        });
2586
2587        // Normal branch
2588        let b1 = g.add_node(BasicBlock {
2589            id: 1,
2590            kind: BlockKind::Exit,
2591            statements: vec![],
2592            terminator: Terminator::Return,
2593            source_location: None,
2594        });
2595
2596        // Error branch (panic)
2597        let b2 = g.add_node(BasicBlock {
2598            id: 2,
2599            kind: BlockKind::Exit,
2600            statements: vec![],
2601            terminator: Terminator::Abort("panic!".to_string()),
2602            source_location: None,
2603        });
2604
2605        g.add_edge(b0, b1, EdgeType::TrueBranch);
2606        g.add_edge(b0, b2, EdgeType::FalseBranch);
2607
2608        let paths = enumerate_paths(&g, &PathLimits::default());
2609
2610        // Should have 2 paths: one Normal, one Error
2611        assert_eq!(paths.len(), 2);
2612
2613        let normal_count = paths.iter().filter(|p| p.kind == PathKind::Normal).count();
2614        let error_count = paths.iter().filter(|p| p.kind == PathKind::Error).count();
2615
2616        assert_eq!(normal_count, 1, "Should have 1 Normal path");
2617        assert_eq!(error_count, 1, "Should have 1 Error path");
2618    }
2619
2620    #[test]
2621    fn test_enumerate_paths_classification_correctness() {
2622        // Verify that classification is correctly applied during enumeration
2623        let cfg = create_diamond_cfg();
2624        let paths = enumerate_paths(&cfg, &PathLimits::default());
2625
2626        // Use the same reachable set for manual classification
2627        use crate::cfg::reachability::find_reachable;
2628        let reachable_nodes = find_reachable(&cfg);
2629        let reachable_blocks: HashSet<BlockId> = reachable_nodes
2630            .iter()
2631            .map(|&idx| cfg[idx].id)
2632            .collect();
2633
2634        // Verify each path's kind matches manual classification
2635        for path in &paths {
2636            let expected_kind = classify_path_precomputed(&cfg, &path.blocks, &reachable_blocks);
2637            assert_eq!(
2638                path.kind, expected_kind,
2639                "Path kind mismatch for {:?}: got {:?}, expected {:?}",
2640                path.blocks, path.kind, expected_kind
2641            );
2642        }
2643    }
2644
2645    // Task 1: PathLimits enforcement tests
2646
2647    #[test]
2648    fn test_path_limits_max_length_long_path() {
2649        // Create a 5-block linear path: 0 -> 1 -> 2 -> 3 -> 4
2650        let mut g = DiGraph::new();
2651
2652        let b0 = g.add_node(BasicBlock {
2653            id: 0,
2654            kind: BlockKind::Entry,
2655            statements: vec![],
2656            terminator: Terminator::Goto { target: 1 },
2657            source_location: None,
2658        });
2659
2660        let b1 = g.add_node(BasicBlock {
2661            id: 1,
2662            kind: BlockKind::Normal,
2663            statements: vec![],
2664            terminator: Terminator::Goto { target: 2 },
2665            source_location: None,
2666        });
2667
2668        let b2 = g.add_node(BasicBlock {
2669            id: 2,
2670            kind: BlockKind::Normal,
2671            statements: vec![],
2672            terminator: Terminator::Goto { target: 3 },
2673            source_location: None,
2674        });
2675
2676        let b3 = g.add_node(BasicBlock {
2677            id: 3,
2678            kind: BlockKind::Normal,
2679            statements: vec![],
2680            terminator: Terminator::Goto { target: 4 },
2681            source_location: None,
2682        });
2683
2684        let b4 = g.add_node(BasicBlock {
2685            id: 4,
2686            kind: BlockKind::Exit,
2687            statements: vec![],
2688            terminator: Terminator::Return,
2689            source_location: None,
2690        });
2691
2692        g.add_edge(b0, b1, EdgeType::Fallthrough);
2693        g.add_edge(b1, b2, EdgeType::Fallthrough);
2694        g.add_edge(b2, b3, EdgeType::Fallthrough);
2695        g.add_edge(b3, b4, EdgeType::Fallthrough);
2696
2697        // With max_length=3, the 5-block path exceeds limit
2698        let limits = PathLimits::default().with_max_length(3);
2699        let paths = enumerate_paths(&g, &limits);
2700
2701        // No paths should be found because the only path has length 5 > 3
2702        assert_eq!(paths.len(), 0, "Path exceeds max_length, should return 0 paths");
2703    }
2704
2705    #[test]
2706    fn test_path_limits_max_paths_exact() {
2707        let cfg = create_diamond_cfg();
2708
2709        // Diamond has 2 paths, limit to 1
2710        let limits = PathLimits::default().with_max_paths(1);
2711        let paths = enumerate_paths(&cfg, &limits);
2712
2713        // Should get exactly 1 path (stops early)
2714        assert_eq!(paths.len(), 1, "Should stop at max_paths=1");
2715
2716        // Path should be valid (entry to exit)
2717        assert_eq!(paths[0].entry, 0);
2718        assert_eq!(paths[0].exit, 3);
2719    }
2720
2721    #[test]
2722    fn test_path_limits_loop_unroll_exact() {
2723        let cfg = create_loop_cfg();
2724
2725        // With loop_unroll_limit=1, we should get:
2726        // - Direct exit: 0->1->3
2727        // - 1 iteration: 0->1->2->1->3
2728        let limits = PathLimits::default().with_loop_unroll_limit(1);
2729        let paths = enumerate_paths(&cfg, &limits);
2730
2731        // With limit=1, we get 1 path (direct exit only, 0 loop iterations)
2732        // The loop iteration counter is incremented on first visit (0->1), so
2733        // back-edge attempts to visit again with count=1 which is >= limit
2734        assert_eq!(paths.len(), 1, "Should have exactly 1 path with loop_unroll_limit=1");
2735
2736        // Verify direct exit exists
2737        assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 3]),
2738                "Direct exit path should exist");
2739    }
2740
2741    #[test]
2742    fn test_path_limits_loop_unroll_limit_2() {
2743        let cfg = create_loop_cfg();
2744
2745        // With loop_unroll_limit=2:
2746        // - First entry: count=0 -> 1
2747        // - Second entry (via back-edge): count=1 -> 2 (allowed)
2748        // - Third entry: count=2 >= limit, stopped
2749        // So we get: direct exit + 1 iteration path = 2 paths
2750        let limits = PathLimits::default().with_loop_unroll_limit(2);
2751        let paths = enumerate_paths(&cfg, &limits);
2752
2753        // With limit=2, we should get 2 paths (direct exit + 1 loop iteration)
2754        assert_eq!(paths.len(), 2, "Should have exactly 2 paths with loop_unroll_limit=2");
2755
2756        // Verify direct exit exists
2757        assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 3]),
2758                "Direct exit path should exist");
2759
2760        // Verify one iteration path exists
2761        assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 2, 1, 3]),
2762                "One iteration path should exist");
2763    }
2764
2765    // Task 2: Self-loop cycle detection tests
2766
2767    /// Create a CFG with a self-loop: 0 -> 1 -> 1 (self-loop)
2768    fn create_self_loop_cfg() -> Cfg {
2769        let mut g = DiGraph::new();
2770
2771        let b0 = g.add_node(BasicBlock {
2772            id: 0,
2773            kind: BlockKind::Entry,
2774            statements: vec![],
2775            terminator: Terminator::Goto { target: 1 },
2776            source_location: None,
2777        });
2778
2779        let b1 = g.add_node(BasicBlock {
2780            id: 1,
2781            kind: BlockKind::Normal,
2782            statements: vec![],
2783            // Self-loop: always goes back to itself
2784            terminator: Terminator::Goto { target: 1 },
2785            source_location: None,
2786        });
2787
2788        g.add_edge(b0, b1, EdgeType::Fallthrough);
2789
2790        g
2791    }
2792
2793    #[test]
2794    fn test_self_loop_terminates() {
2795        let cfg = create_self_loop_cfg();
2796
2797        // This should terminate without hanging
2798        let limits = PathLimits::default();
2799        let paths = enumerate_paths(&cfg, &limits);
2800
2801        // Should have a bounded number of paths (not infinite)
2802        // The self-loop is bounded by loop_unroll_limit
2803        assert!(paths.len() <= limits.loop_unroll_limit + 1,
2804                "Self-loop should be bounded by loop_unroll_limit");
2805    }
2806
2807    #[test]
2808    fn test_self_loop_with_low_limit() {
2809        let cfg = create_self_loop_cfg();
2810
2811        // With a very low unroll limit, we should get minimal paths
2812        let limits = PathLimits::default().with_loop_unroll_limit(1);
2813        let paths = enumerate_paths(&cfg, &limits);
2814
2815        // Should have exactly 1 path (direct to self-loop block, then bounded)
2816        assert!(paths.len() <= 2, "Self-loop with low limit should have few paths");
2817    }
2818
2819    // Task 3: Nested loop bounding tests
2820
2821    /// Create a CFG with nested loops:
2822    /// 0 -> 1 (outer header) -> 2 (inner header) -> 3 -> 2 (back to inner)
2823    /// 1 -> 4 (outer exit)
2824    /// 2 -> 4 (inner exit to outer)
2825    /// 4 -> 5 (final exit)
2826    fn create_nested_loop_cfg() -> Cfg {
2827        let mut g = DiGraph::new();
2828
2829        let b0 = g.add_node(BasicBlock {
2830            id: 0,
2831            kind: BlockKind::Entry,
2832            statements: vec![],
2833            terminator: Terminator::Goto { target: 1 },
2834            source_location: None,
2835        });
2836
2837        let b1 = g.add_node(BasicBlock {
2838            id: 1,
2839            kind: BlockKind::Normal,
2840            statements: vec![],
2841            terminator: Terminator::SwitchInt { targets: vec![2], otherwise: 4 },
2842            source_location: None,
2843        });
2844
2845        let b2 = g.add_node(BasicBlock {
2846            id: 2,
2847            kind: BlockKind::Normal,
2848            statements: vec![],
2849            terminator: Terminator::SwitchInt { targets: vec![3], otherwise: 1 },
2850            source_location: None,
2851        });
2852
2853        let b3 = g.add_node(BasicBlock {
2854            id: 3,
2855            kind: BlockKind::Normal,
2856            statements: vec![],
2857            terminator: Terminator::Goto { target: 2 },
2858            source_location: None,
2859        });
2860
2861        let b4 = g.add_node(BasicBlock {
2862            id: 4,
2863            kind: BlockKind::Exit,
2864            statements: vec![],
2865            terminator: Terminator::Return,
2866            source_location: None,
2867        });
2868
2869        g.add_edge(b0, b1, EdgeType::Fallthrough);
2870        g.add_edge(b1, b2, EdgeType::TrueBranch);
2871        g.add_edge(b1, b4, EdgeType::FalseBranch);
2872        g.add_edge(b2, b3, EdgeType::TrueBranch);
2873        g.add_edge(b2, b1, EdgeType::LoopBack); // Outer back edge
2874        g.add_edge(b3, b2, EdgeType::LoopBack); // Inner back edge
2875
2876        g
2877    }
2878
2879    #[test]
2880    fn test_nested_loop_bounding() {
2881        let cfg = create_nested_loop_cfg();
2882
2883        // With loop_unroll_limit=2, each loop can iterate 0, 1, or 2 times
2884        // For 2 nested loops, max paths should be bounded by (limit+1)^2 = 9
2885        let limits = PathLimits::default().with_loop_unroll_limit(2);
2886        let paths = enumerate_paths(&cfg, &limits);
2887
2888        // With 2 nested loops and limit 2, we get at most 9 paths
2889        // (3 outer iterations * 3 inner iterations each)
2890        assert!(paths.len() <= 9, "Nested loops should be bounded: got {} paths", paths.len());
2891        assert!(paths.len() > 0, "Should have at least some paths");
2892    }
2893
2894    #[test]
2895    fn test_nested_loop_bounding_three_levels() {
2896        // Create 3-level nested loop
2897        let mut g = DiGraph::new();
2898
2899        let b0 = g.add_node(BasicBlock {
2900            id: 0,
2901            kind: BlockKind::Entry,
2902            statements: vec![],
2903            terminator: Terminator::Goto { target: 1 },
2904            source_location: None,
2905        });
2906
2907        // Outer loop header
2908        let b1 = g.add_node(BasicBlock {
2909            id: 1,
2910            kind: BlockKind::Normal,
2911            statements: vec![],
2912            terminator: Terminator::SwitchInt { targets: vec![2], otherwise: 6 },
2913            source_location: None,
2914        });
2915
2916        // Middle loop header
2917        let b2 = g.add_node(BasicBlock {
2918            id: 2,
2919            kind: BlockKind::Normal,
2920            statements: vec![],
2921            terminator: Terminator::SwitchInt { targets: vec![3], otherwise: 1 },
2922            source_location: None,
2923        });
2924
2925        // Inner loop header
2926        let b3 = g.add_node(BasicBlock {
2927            id: 3,
2928            kind: BlockKind::Normal,
2929            statements: vec![],
2930            terminator: Terminator::SwitchInt { targets: vec![4], otherwise: 2 },
2931            source_location: None,
2932        });
2933
2934        let b4 = g.add_node(BasicBlock {
2935            id: 4,
2936            kind: BlockKind::Normal,
2937            statements: vec![],
2938            terminator: Terminator::Goto { target: 3 },
2939            source_location: None,
2940        });
2941
2942        let b6 = g.add_node(BasicBlock {
2943            id: 6,
2944            kind: BlockKind::Exit,
2945            statements: vec![],
2946            terminator: Terminator::Return,
2947            source_location: None,
2948        });
2949
2950        g.add_edge(b0, b1, EdgeType::Fallthrough);
2951        g.add_edge(b1, b2, EdgeType::TrueBranch);
2952        g.add_edge(b1, b6, EdgeType::FalseBranch);
2953        g.add_edge(b2, b3, EdgeType::TrueBranch);
2954        g.add_edge(b2, b1, EdgeType::LoopBack); // Outer back edge
2955        g.add_edge(b3, b4, EdgeType::TrueBranch);
2956        g.add_edge(b3, b2, EdgeType::LoopBack); // Middle back edge
2957        g.add_edge(b4, b3, EdgeType::LoopBack); // Inner back edge
2958
2959        // With loop_unroll_limit=2 and 3 nested loops:
2960        // Max paths = (limit+1)^3 = 27
2961        let limits = PathLimits::default().with_loop_unroll_limit(2);
2962        let paths = enumerate_paths(&g, &limits);
2963
2964        assert!(paths.len() <= 27, "3-level nested loops should be bounded by 27");
2965    }
2966
2967    #[test]
2968    fn test_nested_loop_independent_counters() {
2969        let cfg = create_nested_loop_cfg();
2970
2971        // Verify that each loop header is tracked independently
2972        let loop_headers = crate::cfg::loops::find_loop_headers(&cfg);
2973
2974        // Should have 2 loop headers (outer and inner)
2975        assert_eq!(loop_headers.len(), 2, "Should detect 2 loop headers");
2976
2977        // With limit=2, we should get reasonable bounded paths
2978        let limits = PathLimits::default().with_loop_unroll_limit(2);
2979        let paths = enumerate_paths(&cfg, &limits);
2980
2981        // Verify paths are bounded (not exponential explosion)
2982        assert!(paths.len() > 0, "Should have some paths");
2983        assert!(paths.len() <= 9, "Should be bounded by (limit+1)^nesting_depth");
2984    }
2985
2986    // Task 4: PathLimits builder and preset tests
2987
2988    #[test]
2989    fn test_path_limits_quick_analysis() {
2990        let limits = PathLimits::quick_analysis();
2991
2992        assert_eq!(limits.max_length, 100);
2993        assert_eq!(limits.max_paths, 1000);
2994        assert_eq!(limits.loop_unroll_limit, 2);
2995    }
2996
2997    #[test]
2998    fn test_path_limits_thorough() {
2999        let limits = PathLimits::thorough();
3000
3001        assert_eq!(limits.max_length, 10000);
3002        assert_eq!(limits.max_paths, 100000);
3003        assert_eq!(limits.loop_unroll_limit, 5);
3004    }
3005
3006    #[test]
3007    fn test_path_limits_builder_chaining() {
3008        // Start with quick_analysis and customize further
3009        let limits = PathLimits::quick_analysis()
3010            .with_max_length(200)
3011            .with_max_paths(5000)
3012            .with_loop_unroll_limit(3);
3013
3014        assert_eq!(limits.max_length, 200);
3015        assert_eq!(limits.max_paths, 5000);
3016        assert_eq!(limits.loop_unroll_limit, 3);
3017    }
3018
3019    #[test]
3020    fn test_path_limits_presets_differ_from_default() {
3021        let default = PathLimits::default();
3022        let quick = PathLimits::quick_analysis();
3023        let thorough = PathLimits::thorough();
3024
3025        // Quick should be more restrictive than default
3026        assert!(quick.max_length < default.max_length);
3027        assert!(quick.max_paths < default.max_paths);
3028        assert!(quick.loop_unroll_limit < default.loop_unroll_limit);
3029
3030        // Thorough should be less restrictive than default
3031        assert!(thorough.max_length > default.max_length);
3032        assert!(thorough.max_paths > default.max_paths);
3033        assert!(thorough.loop_unroll_limit > default.loop_unroll_limit);
3034    }
3035
3036    #[test]
3037    fn test_path_limits_quick_vs_thorough_on_loop() {
3038        let cfg = create_loop_cfg();
3039
3040        // Quick analysis should find fewer paths
3041        let quick_paths = enumerate_paths(&cfg, &PathLimits::quick_analysis());
3042        let thorough_paths = enumerate_paths(&cfg, &PathLimits::thorough());
3043
3044        // Thorough should find at least as many paths as quick
3045        assert!(thorough_paths.len() >= quick_paths.len(),
3046                "Thorough analysis should find at least as many paths as quick");
3047    }
3048
3049    // Task 1: is_feasible_path tests
3050
3051    #[test]
3052    fn test_is_feasible_path_empty_path() {
3053        let cfg = create_linear_cfg();
3054        let empty_path: Vec<BlockId> = vec![];
3055
3056        assert!(!is_feasible_path(&cfg, &empty_path),
3057                "Empty path should be infeasible");
3058    }
3059
3060    #[test]
3061    fn test_is_feasible_path_non_entry_first_block() {
3062        let cfg = create_diamond_cfg();
3063        // Path starting from block 1 (not entry)
3064        let path = vec![1, 3];
3065
3066        assert!(!is_feasible_path(&cfg, &path),
3067                "Path starting from non-entry block should be infeasible");
3068    }
3069
3070    #[test]
3071    fn test_is_feasible_path_dead_end_goto() {
3072        let cfg = create_linear_cfg();
3073        // Path ending in Goto (dead end)
3074        let path = vec![0, 1]; // Block 1 has Goto to 2
3075
3076        assert!(!is_feasible_path(&cfg, &path),
3077                "Path ending in Goto should be infeasible (dead end)");
3078    }
3079
3080    #[test]
3081    fn test_is_feasible_path_valid_return() {
3082        let cfg = create_linear_cfg();
3083        // Complete path: entry -> goto -> return
3084        let path = vec![0, 1, 2];
3085
3086        assert!(is_feasible_path(&cfg, &path),
3087                "Complete path ending in Return should be feasible");
3088    }
3089
3090    #[test]
3091    fn test_is_feasible_path_abort_is_feasible() {
3092        let cfg = create_error_cfg();
3093        // Path ending in Abort (error but reachable)
3094        let path = vec![0, 1];
3095
3096        assert!(is_feasible_path(&cfg, &path),
3097                "Path ending in Abort should be feasible (error path but reachable)");
3098    }
3099
3100    #[test]
3101    fn test_is_feasible_path_unreachable_terminator() {
3102        let cfg = create_unreachable_term_cfg();
3103        // Path ending in Unreachable
3104        let path = vec![0, 1];
3105
3106        assert!(!is_feasible_path(&cfg, &path),
3107                "Path ending in Unreachable should be infeasible");
3108    }
3109
3110    #[test]
3111    fn test_is_feasible_path_nonexistent_block() {
3112        let cfg = create_linear_cfg();
3113        // Path with nonexistent block
3114        let path = vec![0, 99]; // Block 99 doesn't exist
3115
3116        assert!(!is_feasible_path(&cfg, &path),
3117                "Path with nonexistent block should be infeasible");
3118    }
3119
3120    #[test]
3121    fn test_is_feasible_path_switch_int_dead_end() {
3122        let cfg = create_diamond_cfg();
3123        // Path ending in SwitchInt (block 0)
3124        let path = vec![0]; // Block 0 has SwitchInt
3125
3126        assert!(!is_feasible_path(&cfg, &path),
3127                "Path ending in SwitchInt should be infeasible (dead end)");
3128    }
3129
3130    #[test]
3131    fn test_is_feasible_path_complete_diamond() {
3132        let cfg = create_diamond_cfg();
3133        // Complete path through diamond
3134        let path1 = vec![0, 1, 3]; // Through true branch
3135        let path2 = vec![0, 2, 3]; // Through false branch
3136
3137        assert!(is_feasible_path(&cfg, &path1),
3138                "Complete diamond path 0->1->3 should be feasible");
3139        assert!(is_feasible_path(&cfg, &path2),
3140                "Complete diamond path 0->2->3 should be feasible");
3141    }
3142
3143    #[test]
3144    fn test_is_feasible_path_call_no_unwind() {
3145        let mut g = DiGraph::new();
3146
3147        let b0 = g.add_node(BasicBlock {
3148            id: 0,
3149            kind: BlockKind::Entry,
3150            statements: vec![],
3151            terminator: Terminator::Call {
3152                target: Some(1),
3153                unwind: None,
3154            },
3155            source_location: None,
3156        });
3157
3158        let b1 = g.add_node(BasicBlock {
3159            id: 1,
3160            kind: BlockKind::Exit,
3161            statements: vec![],
3162            terminator: Terminator::Return,
3163            source_location: None,
3164        });
3165
3166        g.add_edge(b0, b1, EdgeType::Fallthrough);
3167
3168        // Path ending in Call with no unwind
3169        let path = vec![0];
3170
3171        assert!(is_feasible_path(&g, &path),
3172                "Path ending in Call with no unwind should be feasible");
3173    }
3174
3175    #[test]
3176    fn test_is_feasible_path_call_with_unwind_and_target() {
3177        let cfg = create_call_unwind_cfg();
3178        // Path ending in Call with unwind and target
3179        let path = vec![0]; // Block 0 has Call with unwind: Some(2), target: Some(1)
3180
3181        assert!(is_feasible_path(&cfg, &path),
3182                "Path ending in Call with both unwind and target should be feasible");
3183    }
3184
3185    #[test]
3186    fn test_is_feasible_path_call_always_unwinds() {
3187        let mut g = DiGraph::new();
3188
3189        let b0 = g.add_node(BasicBlock {
3190            id: 0,
3191            kind: BlockKind::Entry,
3192            statements: vec![],
3193            terminator: Terminator::Call {
3194                target: None, // No target - always unwinds
3195                unwind: Some(1),
3196            },
3197            source_location: None,
3198        });
3199
3200        let b1 = g.add_node(BasicBlock {
3201            id: 1,
3202            kind: BlockKind::Exit,
3203            statements: vec![],
3204            terminator: Terminator::Return,
3205            source_location: None,
3206        });
3207
3208        g.add_edge(b0, b1, EdgeType::Fallthrough);
3209
3210        // Path ending in Call that always unwinds
3211        let path = vec![0];
3212
3213        assert!(!is_feasible_path(&g, &path),
3214                "Path ending in Call with only unwind (no target) should be infeasible");
3215    }
3216
3217    // Task 2: is_feasible_path_precomputed tests
3218
3219    #[test]
3220    fn test_is_feasible_path_precomputed_matches_basic() {
3221        let cfg = create_diamond_cfg();
3222
3223        // Pre-compute reachable set
3224        use crate::cfg::reachability::find_reachable;
3225        let reachable_nodes = find_reachable(&cfg);
3226        let reachable_blocks: HashSet<BlockId> = reachable_nodes
3227            .iter()
3228            .map(|&idx| cfg[idx].id)
3229            .collect();
3230
3231        // Test multiple paths - both should give same result
3232        let test_paths = vec![
3233            vec![0, 1, 3],      // Complete path - feasible
3234            vec![0, 2, 3],      // Complete path - feasible
3235            vec![0, 1],         // Dead end - infeasible
3236            vec![],             // Empty - infeasible
3237        ];
3238
3239        for path in test_paths {
3240            let basic = is_feasible_path(&cfg, &path);
3241            let precomputed = is_feasible_path_precomputed(&cfg, &path, &reachable_blocks);
3242            assert_eq!(
3243                basic, precomputed,
3244                "is_feasible_path_precomputed should match is_feasible_path for {:?}",
3245                path
3246            );
3247        }
3248    }
3249
3250    #[test]
3251    fn test_is_feasible_path_precomputed_unreachable_block() {
3252        let cfg = create_dead_code_cfg();
3253
3254        // Pre-compute reachable set (only block 0 is reachable)
3255        use crate::cfg::reachability::find_reachable;
3256        let reachable_nodes = find_reachable(&cfg);
3257        let reachable_blocks: HashSet<BlockId> = reachable_nodes
3258            .iter()
3259            .map(|&idx| cfg[idx].id)
3260            .collect();
3261
3262        // Path with unreachable block
3263        let path = vec![1]; // Block 1 is not reachable from entry
3264
3265        assert!(!is_feasible_path_precomputed(&cfg, &path, &reachable_blocks),
3266                "Path with unreachable block should be infeasible");
3267    }
3268
3269    #[test]
3270    fn test_is_feasible_path_precomputed_performance() {
3271        use crate::cfg::reachability::find_reachable;
3272        use std::time::Instant;
3273
3274        let cfg = create_diamond_cfg();
3275
3276        // Pre-compute reachable set once
3277        let reachable_nodes = find_reachable(&cfg);
3278        let reachable_blocks: HashSet<BlockId> = reachable_nodes
3279            .iter()
3280            .map(|&idx| cfg[idx].id)
3281            .collect();
3282
3283        // Create many test paths
3284        let test_paths: Vec<Vec<BlockId>> = (0..1000)
3285            .map(|_| vec![0, 1, 3])
3286            .collect();
3287
3288        // Time the precomputed version
3289        let start = Instant::now();
3290        for path in &test_paths {
3291            let _ = is_feasible_path_precomputed(&cfg, path, &reachable_blocks);
3292        }
3293        let precomputed_duration = start.elapsed();
3294
3295        // Should be very fast (< 5ms for 1000 paths)
3296        assert!(
3297            precomputed_duration.as_millis() < 5,
3298            "is_feasible_path_precomputed should check 1000 paths in <5ms, took {}ms",
3299            precomputed_duration.as_millis()
3300        );
3301    }
3302
3303    #[test]
3304    fn test_is_feasible_path_precomputed_all_criteria() {
3305        use crate::cfg::reachability::find_reachable;
3306
3307        // Test with normal path (feasible)
3308        let cfg_normal = create_linear_cfg();
3309        let reachable_normal = find_reachable(&cfg_normal)
3310            .iter()
3311            .map(|&idx| cfg_normal[idx].id)
3312            .collect();
3313        assert!(
3314            is_feasible_path_precomputed(&cfg_normal, &[0, 1, 2], &reachable_normal),
3315            "Complete linear path should be feasible"
3316        );
3317
3318        // Test with error path (feasible)
3319        let cfg_error = create_error_cfg();
3320        let reachable_error = find_reachable(&cfg_error)
3321            .iter()
3322            .map(|&idx| cfg_error[idx].id)
3323            .collect();
3324        assert!(
3325            is_feasible_path_precomputed(&cfg_error, &[0, 1], &reachable_error),
3326            "Path ending in Abort should be feasible (error path but reachable)"
3327        );
3328
3329        // Test with degenerate path (infeasible)
3330        let cfg_degen = create_unreachable_term_cfg();
3331        let reachable_degen = find_reachable(&cfg_degen)
3332            .iter()
3333            .map(|&idx| cfg_degen[idx].id)
3334            .collect();
3335        assert!(
3336            !is_feasible_path_precomputed(&cfg_degen, &[0, 1], &reachable_degen),
3337            "Path ending in Unreachable should be infeasible"
3338        );
3339    }
3340
3341    // Task 3: classify_with_feasibility tests
3342
3343    #[test]
3344    fn test_classify_with_feasibility_dead_end() {
3345        use crate::cfg::reachability::find_reachable;
3346
3347        let cfg = create_linear_cfg();
3348        let reachable = find_reachable(&cfg)
3349            .iter()
3350            .map(|&idx| cfg[idx].id)
3351            .collect();
3352
3353        // Path ending in Goto (dead end) should be Degenerate
3354        let path = vec![0, 1]; // Block 1 has Goto to 2
3355        let kind = classify_path_precomputed(&cfg, &path, &reachable);
3356        assert_eq!(kind, PathKind::Degenerate,
3357                   "Path ending in Goto should be Degenerate");
3358    }
3359
3360    #[test]
3361    fn test_classify_with_feasibility_valid_exit() {
3362        use crate::cfg::reachability::find_reachable;
3363
3364        let cfg = create_linear_cfg();
3365        let reachable = find_reachable(&cfg)
3366            .iter()
3367            .map(|&idx| cfg[idx].id)
3368            .collect();
3369
3370        // Complete path with Return should be Normal
3371        let path = vec![0, 1, 2];
3372        let kind = classify_path_precomputed(&cfg, &path, &reachable);
3373        assert_eq!(kind, PathKind::Normal,
3374                   "Complete path with Return should be Normal");
3375    }
3376
3377    #[test]
3378    fn test_classify_with_feasibility_error_path() {
3379        use crate::cfg::reachability::find_reachable;
3380
3381        let cfg = create_error_cfg();
3382        let reachable = find_reachable(&cfg)
3383            .iter()
3384            .map(|&idx| cfg[idx].id)
3385            .collect();
3386
3387        // Path ending in Abort should be Error (feasible but error path)
3388        let path = vec![0, 1];
3389        let kind = classify_path_precomputed(&cfg, &path, &reachable);
3390        assert_eq!(kind, PathKind::Error,
3391                   "Path ending in Abort should be Error");
3392    }
3393
3394    #[test]
3395    fn test_classify_with_feasibility_switch_int_dead_end() {
3396        use crate::cfg::reachability::find_reachable;
3397
3398        let cfg = create_diamond_cfg();
3399        let reachable = find_reachable(&cfg)
3400            .iter()
3401            .map(|&idx| cfg[idx].id)
3402            .collect();
3403
3404        // Path ending in SwitchInt (dead end)
3405        let path = vec![0]; // Block 0 has SwitchInt
3406        let kind = classify_path_precomputed(&cfg, &path, &reachable);
3407        assert_eq!(kind, PathKind::Degenerate,
3408                   "Path ending in SwitchInt should be Degenerate");
3409    }
3410
3411    #[test]
3412    fn test_classify_with_feasibility_priority_order() {
3413        use crate::cfg::reachability::find_reachable;
3414
3415        // Test that unreachable takes priority over feasibility
3416        let cfg = create_dead_code_cfg();
3417        let reachable = find_reachable(&cfg)
3418            .iter()
3419            .map(|&idx| cfg[idx].id)
3420            .collect();
3421
3422        // Path with unreachable block
3423        let path = vec![1];
3424        let kind = classify_path_precomputed(&cfg, &path, &reachable);
3425        assert_eq!(kind, PathKind::Unreachable,
3426                   "Unreachable should be prioritized over feasibility");
3427    }
3428
3429    #[test]
3430    fn test_classify_with_feasibility_complete_paths() {
3431        use crate::cfg::reachability::find_reachable;
3432
3433        let cfg = create_diamond_cfg();
3434        let reachable = find_reachable(&cfg)
3435            .iter()
3436            .map(|&idx| cfg[idx].id)
3437            .collect();
3438
3439        // Both complete paths should be Normal
3440        let path1 = vec![0, 1, 3];
3441        let path2 = vec![0, 2, 3];
3442
3443        assert_eq!(classify_path_precomputed(&cfg, &path1, &reachable), PathKind::Normal,
3444                   "Complete diamond path 0->1->3 should be Normal");
3445        assert_eq!(classify_path_precomputed(&cfg, &path2, &reachable), PathKind::Normal,
3446                   "Complete diamond path 0->2->3 should be Normal");
3447    }
3448
3449    #[test]
3450    fn test_classify_with_feasibility_call_terminator() {
3451        use crate::cfg::reachability::find_reachable;
3452
3453        let mut g = DiGraph::new();
3454
3455        let b0 = g.add_node(BasicBlock {
3456            id: 0,
3457            kind: BlockKind::Entry,
3458            statements: vec![],
3459            terminator: Terminator::Call {
3460                target: Some(1),
3461                unwind: None,
3462            },
3463            source_location: None,
3464        });
3465
3466        let b1 = g.add_node(BasicBlock {
3467            id: 1,
3468            kind: BlockKind::Exit,
3469            statements: vec![],
3470            terminator: Terminator::Return,
3471            source_location: None,
3472        });
3473
3474        g.add_edge(b0, b1, EdgeType::Fallthrough);
3475
3476        let reachable = find_reachable(&g)
3477            .iter()
3478            .map(|&idx| g[idx].id)
3479            .collect();
3480
3481        // Path ending in Call with target should be Normal (feasible)
3482        let path = vec![0];
3483        let kind = classify_path_precomputed(&g, &path, &reachable);
3484        assert_eq!(kind, PathKind::Normal,
3485                   "Path ending in Call with target should be Normal");
3486    }
3487
3488    // Task 4: Feasibility limitation demonstration
3489
3490    /// Create a CFG with conflicting conditions that static analysis can't detect
3491    ///
3492    /// This demonstrates the limitation of static feasibility checking:
3493    /// The path might have conflicting conditions (e.g., x > 5 && x < 3)
3494    /// but static analysis doesn't perform symbolic execution, so it's still
3495    /// marked as feasible.
3496    fn create_conflicting_conditions_cfg() -> Cfg {
3497        let mut g = DiGraph::new();
3498
3499        // Entry: check x > 5
3500        let b0 = g.add_node(BasicBlock {
3501            id: 0,
3502            kind: BlockKind::Entry,
3503            statements: vec![],
3504            terminator: Terminator::SwitchInt { targets: vec![1], otherwise: 2 },
3505            source_location: None,
3506        });
3507
3508        // True branch: check x < 3 (conflicts with x > 5)
3509        let b1 = g.add_node(BasicBlock {
3510            id: 1,
3511            kind: BlockKind::Normal,
3512            statements: vec![],
3513            terminator: Terminator::SwitchInt { targets: vec![3], otherwise: 3 },
3514            source_location: None,
3515        });
3516
3517        // False branch: just return
3518        let b2 = g.add_node(BasicBlock {
3519            id: 2,
3520            kind: BlockKind::Exit,
3521            statements: vec![],
3522            terminator: Terminator::Return,
3523            source_location: None,
3524        });
3525
3526        // After conflicting check: return
3527        let b3 = g.add_node(BasicBlock {
3528            id: 3,
3529            kind: BlockKind::Exit,
3530            statements: vec![],
3531            terminator: Terminator::Return,
3532            source_location: None,
3533        });
3534
3535        g.add_edge(b0, b1, EdgeType::TrueBranch);
3536        g.add_edge(b0, b2, EdgeType::FalseBranch);
3537        g.add_edge(b1, b3, EdgeType::Fallthrough);
3538
3539        g
3540    }
3541
3542    #[test]
3543    fn test_feasibility_limitation_conflicting_conditions() {
3544        use crate::cfg::reachability::find_reachable;
3545
3546        let cfg = create_conflicting_conditions_cfg();
3547        let reachable = find_reachable(&cfg)
3548            .iter()
3549            .map(|&idx| cfg[idx].id)
3550            .collect();
3551
3552        // Path 0 -> 1 -> 3 has conflicting conditions (x > 5 && x < 3)
3553        // This is dynamically infeasible (no value can satisfy both)
3554        // But static analysis doesn't detect this
3555        let path = vec![0, 1, 3];
3556
3557        // The path is marked feasible by static check
3558        assert!(is_feasible_path_precomputed(&cfg, &path, &reachable),
3559                "Static analysis marks conflicting path as feasible (limitation)");
3560
3561        // And classified as Normal
3562        assert_eq!(classify_path_precomputed(&cfg, &path, &reachable),
3563                   PathKind::Normal,
3564                   "Conflicting path is classified as Normal (static limitation)");
3565
3566        // This test documents the limitation: symbolic execution would be needed
3567        // to detect that x > 5 and x < 3 cannot both be true
3568    }
3569
3570    #[test]
3571    fn test_feasibility_documentation_accuracy() {
3572        use crate::cfg::reachability::find_reachable;
3573
3574        // Verify that the documented behavior is accurate
3575
3576        let cfg = create_linear_cfg();
3577        let reachable = find_reachable(&cfg)
3578            .iter()
3579            .map(|&idx| cfg[idx].id)
3580            .collect();
3581
3582        // What we check: Entry kind
3583        let non_entry_path = vec![1, 2];
3584        assert!(!is_feasible_path_precomputed(&cfg, &non_entry_path, &reachable),
3585                "Entry check works as documented");
3586
3587        // What we check: Valid exit terminator
3588        let dead_end_path = vec![0, 1];
3589        assert!(!is_feasible_path_precomputed(&cfg, &dead_end_path, &reachable),
3590                "Dead-end detection works as documented");
3591
3592        // What we check: Reachable blocks
3593        assert!(is_feasible_path_precomputed(&cfg, &[0, 1, 2], &reachable),
3594                "Reachable path is feasible as documented");
3595    }
3596
3597    // Task 7: get_or_enumerate_paths tests
3598
3599    #[test]
3600    fn test_get_or_enumerate_paths_cache_miss_enumerates() {
3601        use crate::storage::create_schema;
3602
3603        // Create in-memory database with schema
3604        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
3605
3606        // Create Magellan tables (simplified)
3607        conn.execute(
3608            "CREATE TABLE magellan_meta (
3609                id INTEGER PRIMARY KEY CHECK (id = 1),
3610                magellan_schema_version INTEGER NOT NULL,
3611                sqlitegraph_schema_version INTEGER NOT NULL,
3612                created_at INTEGER NOT NULL
3613            )",
3614            [],
3615        ).unwrap();
3616
3617        conn.execute(
3618            "CREATE TABLE graph_entities (
3619                id INTEGER PRIMARY KEY AUTOINCREMENT,
3620                kind TEXT NOT NULL,
3621                name TEXT NOT NULL,
3622                file_path TEXT,
3623                data TEXT NOT NULL
3624            )",
3625            [],
3626        ).unwrap();
3627
3628        conn.execute(
3629            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3630             VALUES (1, 4, 3, 0)",
3631            [],
3632        ).unwrap();
3633
3634        // Create Mirage schema
3635        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3636
3637        // Insert a test function
3638        conn.execute(
3639            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3640            rusqlite::params!("function", "test_func", "test.rs", "{}"),
3641        ).unwrap();
3642        let function_id: i64 = 1;
3643
3644        // Create test CFG
3645        let cfg = create_linear_cfg();
3646        let function_hash = "test_hash_123";
3647        let limits = PathLimits::default();
3648
3649        // First call should enumerate (cache miss)
3650        let paths = get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
3651
3652        // Linear CFG has exactly 1 path
3653        assert_eq!(paths.len(), 1);
3654        assert_eq!(paths[0].blocks, vec![0, 1, 2]);
3655    }
3656
3657    #[test]
3658    fn test_get_or_enumerate_paths_cache_hit_retrieves() {
3659        use crate::storage::create_schema;
3660
3661        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
3662
3663        // Create Magellan tables
3664        conn.execute(
3665            "CREATE TABLE magellan_meta (
3666                id INTEGER PRIMARY KEY CHECK (id = 1),
3667                magellan_schema_version INTEGER NOT NULL,
3668                sqlitegraph_schema_version INTEGER NOT NULL,
3669                created_at INTEGER NOT NULL
3670            )",
3671            [],
3672        ).unwrap();
3673
3674        conn.execute(
3675            "CREATE TABLE graph_entities (
3676                id INTEGER PRIMARY KEY AUTOINCREMENT,
3677                kind TEXT NOT NULL,
3678                name TEXT NOT NULL,
3679                file_path TEXT,
3680                data TEXT NOT NULL
3681            )",
3682            [],
3683        ).unwrap();
3684
3685        conn.execute(
3686            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3687             VALUES (1, 4, 3, 0)",
3688            [],
3689        ).unwrap();
3690
3691        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3692
3693        conn.execute(
3694            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3695            rusqlite::params!("function", "test_func", "test.rs", "{}"),
3696        ).unwrap();
3697        let function_id: i64 = 1;
3698
3699        let cfg = create_linear_cfg();
3700        let function_hash = "test_hash_456";
3701        let limits = PathLimits::default();
3702
3703        // First call - cache miss, enumerates and stores
3704        let paths1 = get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
3705        assert_eq!(paths1.len(), 1);
3706
3707        // Second call with same hash - cache hit, retrieves without enumeration
3708        let paths2 = get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
3709        assert_eq!(paths2.len(), 1);
3710        assert_eq!(paths2[0].blocks, vec![0, 1, 2]);
3711    }
3712
3713    #[test]
3714    fn test_get_or_enumerate_paths_hash_change_invalidates() {
3715        use crate::storage::create_schema;
3716
3717        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
3718
3719        // Create Magellan tables
3720        conn.execute(
3721            "CREATE TABLE magellan_meta (
3722                id INTEGER PRIMARY KEY CHECK (id = 1),
3723                magellan_schema_version INTEGER NOT NULL,
3724                sqlitegraph_schema_version INTEGER NOT NULL,
3725                created_at INTEGER NOT NULL
3726            )",
3727            [],
3728        ).unwrap();
3729
3730        conn.execute(
3731            "CREATE TABLE graph_entities (
3732                id INTEGER PRIMARY KEY AUTOINCREMENT,
3733                kind TEXT NOT NULL,
3734                name TEXT NOT NULL,
3735                file_path TEXT,
3736                data TEXT NOT NULL
3737            )",
3738            [],
3739        ).unwrap();
3740
3741        conn.execute(
3742            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3743             VALUES (1, 4, 3, 0)",
3744            [],
3745        ).unwrap();
3746
3747        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3748
3749        conn.execute(
3750            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3751            rusqlite::params!("function", "test_func", "test.rs", "{}"),
3752        ).unwrap();
3753        let function_id: i64 = 1;
3754
3755        let cfg = create_linear_cfg();
3756        let hash1 = "test_hash_v1";
3757        let hash2 = "test_hash_v3";
3758        let limits = PathLimits::default();
3759
3760        // Call with hash1 - stores paths
3761        let paths1 = get_or_enumerate_paths(&cfg, function_id, hash1, &limits, &mut conn).unwrap();
3762        assert_eq!(paths1.len(), 1);
3763
3764        // Call with hash2 - should invalidate and re-enumerate
3765        let paths2 = get_or_enumerate_paths(&cfg, function_id, hash2, &limits, &mut conn).unwrap();
3766        assert_eq!(paths2.len(), 1);
3767
3768        // Both should return the same path content (same CFG)
3769        assert_eq!(paths1[0].blocks, paths2[0].blocks);
3770    }
3771
3772    // Task 05-06-2: EnumerationContext tests
3773
3774    #[test]
3775    fn test_enumeration_context_new() {
3776        use super::super::EnumerationContext;
3777
3778        let cfg = create_linear_cfg();
3779        let ctx = EnumerationContext::new(&cfg);
3780
3781        // Linear CFG: 3 blocks all reachable
3782        assert_eq!(ctx.reachable_count(), 3);
3783        assert_eq!(ctx.loop_count(), 0);
3784        assert_eq!(ctx.exit_count(), 1);
3785    }
3786
3787    #[test]
3788    fn test_enumeration_context_with_loop() {
3789        use super::super::EnumerationContext;
3790
3791        let cfg = create_loop_cfg();
3792        let ctx = EnumerationContext::new(&cfg);
3793
3794        // Loop CFG: should have 1 loop header
3795        assert_eq!(ctx.loop_count(), 1);
3796        assert!(ctx.reachable_count() > 0);
3797        assert!(ctx.exit_count() > 0);
3798    }
3799
3800    #[test]
3801    fn test_enumeration_context_diamond_cfg() {
3802        use super::super::EnumerationContext;
3803
3804        let cfg = create_diamond_cfg();
3805        let ctx = EnumerationContext::new(&cfg);
3806
3807        // Diamond CFG: no loops
3808        assert_eq!(ctx.loop_count(), 0);
3809        assert_eq!(ctx.reachable_count(), 4); // All 4 blocks reachable
3810        assert_eq!(ctx.exit_count(), 1); // Single merge point exit
3811    }
3812
3813    #[test]
3814    fn test_enumeration_context_is_reachable() {
3815        use super::super::EnumerationContext;
3816
3817        let cfg = create_dead_code_cfg();
3818        let ctx = EnumerationContext::new(&cfg);
3819
3820        // Block 0 is reachable (entry)
3821        assert!(ctx.is_reachable(0));
3822        // Block 1 is not reachable (dead code)
3823        assert!(!ctx.is_reachable(1));
3824    }
3825
3826    #[test]
3827    fn test_enumeration_context_is_loop_header() {
3828        use super::super::EnumerationContext;
3829        use petgraph::graph::NodeIndex;
3830
3831        let cfg = create_loop_cfg();
3832        let ctx = EnumerationContext::new(&cfg);
3833
3834        // Block 1 is the loop header
3835        assert!(ctx.is_loop_header(NodeIndex::new(1)));
3836        // Block 0 is not a loop header
3837        assert!(!ctx.is_loop_header(NodeIndex::new(0)));
3838    }
3839
3840    #[test]
3841    fn test_enumeration_context_is_exit() {
3842        use super::super::EnumerationContext;
3843        use petgraph::graph::NodeIndex;
3844
3845        let cfg = create_diamond_cfg();
3846        let ctx = EnumerationContext::new(&cfg);
3847
3848        // Block 3 is the exit
3849        assert!(ctx.is_exit(NodeIndex::new(3)));
3850        // Block 0 is not an exit
3851        assert!(!ctx.is_exit(NodeIndex::new(0)));
3852    }
3853
3854    #[test]
3855    fn test_enumerate_paths_with_context_matches_basic() {
3856        use super::super::{enumerate_paths, enumerate_paths_with_context, EnumerationContext};
3857
3858        let cfg = create_diamond_cfg();
3859        let limits = PathLimits::default();
3860        let ctx = EnumerationContext::new(&cfg);
3861
3862        // Both methods should return same paths
3863        let paths_basic = enumerate_paths(&cfg, &limits);
3864        let paths_context = enumerate_paths_with_context(&cfg, &limits, &ctx);
3865
3866        assert_eq!(paths_basic.len(), paths_context.len());
3867
3868        // Sort and compare
3869        let mut sorted_basic: Vec<_> = paths_basic.iter().collect();
3870        let mut sorted_context: Vec<_> = paths_context.iter().collect();
3871        sorted_basic.sort_by_key(|p| p.blocks.clone());
3872        sorted_context.sort_by_key(|p| p.blocks.clone());
3873
3874        for (basic, context) in sorted_basic.iter().zip(sorted_context.iter()) {
3875            assert_eq!(basic.blocks, context.blocks);
3876            assert_eq!(basic.kind, context.kind);
3877        }
3878    }
3879
3880    #[test]
3881    fn test_enumerate_paths_with_context_linear_cfg() {
3882        use super::super::{enumerate_paths_with_context, EnumerationContext};
3883
3884        let cfg = create_linear_cfg();
3885        let limits = PathLimits::default();
3886        let ctx = EnumerationContext::new(&cfg);
3887
3888        let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
3889
3890        assert_eq!(paths.len(), 1);
3891        assert_eq!(paths[0].blocks, vec![0, 1, 2]);
3892    }
3893
3894    #[test]
3895    fn test_enumerate_paths_with_context_with_loop() {
3896        use super::super::{enumerate_paths_with_context, EnumerationContext};
3897
3898        let cfg = create_loop_cfg();
3899        let limits = PathLimits::default();
3900        let ctx = EnumerationContext::new(&cfg);
3901
3902        let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
3903
3904        // With loop_unroll_limit=3, should have bounded paths
3905        assert!(paths.len() > 0);
3906        assert!(paths.len() <= 4); // Entry + up to 3 loop iterations
3907    }
3908
3909    #[test]
3910    fn test_enumerate_paths_with_context_performance() {
3911        use super::super::{enumerate_paths, enumerate_paths_with_context, EnumerationContext};
3912        use std::time::Instant;
3913
3914        let cfg = create_diamond_cfg();
3915        let limits = PathLimits::default();
3916
3917        // Time basic enumeration
3918        let start = Instant::now();
3919        let _paths1 = enumerate_paths(&cfg, &limits);
3920        let basic_time = start.elapsed();
3921
3922        // Create context and time context enumeration
3923        let ctx_start = Instant::now();
3924        let ctx = EnumerationContext::new(&cfg);
3925        let ctx_creation_time = ctx_start.elapsed();
3926
3927        let start = Instant::now();
3928        let _paths2 = enumerate_paths_with_context(&cfg, &limits, &ctx);
3929        let context_time = start.elapsed();
3930
3931        // First call with context should be faster than basic
3932        // (basic recomputes everything, context reuses)
3933        // Note: This is a micro-benchmark and may vary
3934        println!("Basic: {:?}, Context creation: {:?}, Context enum: {:?}",
3935                 basic_time, ctx_creation_time, context_time);
3936
3937        // Second call with same context should be much faster
3938        let start = Instant::now();
3939        let _paths3 = enumerate_paths_with_context(&cfg, &limits, &ctx);
3940        let context_time2 = start.elapsed();
3941
3942        println!("Second context call: {:?}", context_time2);
3943
3944        // Context enumeration should be complete
3945        assert!(context_time2.as_millis() < 100);
3946    }
3947
3948    #[test]
3949    fn test_enumerate_paths_with_context_multiple_calls() {
3950        use super::super::{enumerate_paths_with_context, EnumerationContext};
3951
3952        let cfg = create_diamond_cfg();
3953        let ctx = EnumerationContext::new(&cfg);
3954
3955        // Multiple calls with different limits should reuse context
3956        let limits1 = PathLimits::default().with_max_paths(10);
3957        let limits2 = PathLimits::default().with_max_paths(100);
3958
3959        let paths1 = enumerate_paths_with_context(&cfg, &limits1, &ctx);
3960        let paths2 = enumerate_paths_with_context(&cfg, &limits2, &ctx);
3961
3962        // Both should return the same paths (10 is enough for diamond)
3963        assert_eq!(paths1.len(), paths2.len());
3964    }
3965
3966    // Task 05-06-3: enumerate_paths_cached tests
3967
3968    #[test]
3969    fn test_enumerate_paths_cached_cache_miss_enumerates() {
3970        use crate::storage::create_schema;
3971        use super::super::enumerate_paths_cached;
3972
3973        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
3974
3975        // Create Magellan tables
3976        conn.execute(
3977            "CREATE TABLE magellan_meta (
3978                id INTEGER PRIMARY KEY CHECK (id = 1),
3979                magellan_schema_version INTEGER NOT NULL,
3980                sqlitegraph_schema_version INTEGER NOT NULL,
3981                created_at INTEGER NOT NULL
3982            )",
3983            [],
3984        ).unwrap();
3985
3986        conn.execute(
3987            "CREATE TABLE graph_entities (
3988                id INTEGER PRIMARY KEY AUTOINCREMENT,
3989                kind TEXT NOT NULL,
3990                name TEXT NOT NULL,
3991                file_path TEXT,
3992                data TEXT NOT NULL
3993            )",
3994            [],
3995        ).unwrap();
3996
3997        conn.execute(
3998            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3999             VALUES (1, 4, 3, 0)",
4000            [],
4001        ).unwrap();
4002
4003        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4004
4005        conn.execute(
4006            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
4007            rusqlite::params!("function", "test_func", "test.rs", "{}"),
4008        ).unwrap();
4009        let function_id: i64 = 1;
4010
4011        let cfg = create_linear_cfg();
4012        let function_hash = "test_hash_123";
4013        let limits = PathLimits::default();
4014
4015        // First call should enumerate (cache miss)
4016        let paths = enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4017
4018        assert_eq!(paths.len(), 1);
4019        assert_eq!(paths[0].blocks, vec![0, 1, 2]);
4020    }
4021
4022    #[test]
4023    fn test_enumerate_paths_cached_cache_hit_retrieves() {
4024        use crate::storage::create_schema;
4025        use super::super::enumerate_paths_cached;
4026
4027        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
4028
4029        // Create Magellan tables
4030        conn.execute(
4031            "CREATE TABLE magellan_meta (
4032                id INTEGER PRIMARY KEY CHECK (id = 1),
4033                magellan_schema_version INTEGER NOT NULL,
4034                sqlitegraph_schema_version INTEGER NOT NULL,
4035                created_at INTEGER NOT NULL
4036            )",
4037            [],
4038        ).unwrap();
4039
4040        conn.execute(
4041            "CREATE TABLE graph_entities (
4042                id INTEGER PRIMARY KEY AUTOINCREMENT,
4043                kind TEXT NOT NULL,
4044                name TEXT NOT NULL,
4045                file_path TEXT,
4046                data TEXT NOT NULL
4047            )",
4048            [],
4049        ).unwrap();
4050
4051        conn.execute(
4052            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
4053             VALUES (1, 4, 3, 0)",
4054            [],
4055        ).unwrap();
4056
4057        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4058
4059        conn.execute(
4060            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
4061            rusqlite::params!("function", "test_func", "test.rs", "{}"),
4062        ).unwrap();
4063        let function_id: i64 = 1;
4064
4065        let cfg = create_linear_cfg();
4066        let function_hash = "test_hash_456";
4067        let limits = PathLimits::default();
4068
4069        // First call - cache miss, enumerates and stores
4070        let paths1 = enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4071        assert_eq!(paths1.len(), 1);
4072
4073        // Second call with same hash - cache hit, retrieves without enumeration
4074        let paths2 = enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4075        assert_eq!(paths2.len(), 1);
4076        assert_eq!(paths2[0].blocks, vec![0, 1, 2]);
4077    }
4078
4079    #[test]
4080    fn test_enumerate_paths_cached_hash_change_invalidates() {
4081        use crate::storage::create_schema;
4082        use super::super::enumerate_paths_cached;
4083
4084        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
4085
4086        // Create Magellan tables
4087        conn.execute(
4088            "CREATE TABLE magellan_meta (
4089                id INTEGER PRIMARY KEY CHECK (id = 1),
4090                magellan_schema_version INTEGER NOT NULL,
4091                sqlitegraph_schema_version INTEGER NOT NULL,
4092                created_at INTEGER NOT NULL
4093            )",
4094            [],
4095        ).unwrap();
4096
4097        conn.execute(
4098            "CREATE TABLE graph_entities (
4099                id INTEGER PRIMARY KEY AUTOINCREMENT,
4100                kind TEXT NOT NULL,
4101                name TEXT NOT NULL,
4102                file_path TEXT,
4103                data TEXT NOT NULL
4104            )",
4105            [],
4106        ).unwrap();
4107
4108        conn.execute(
4109            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
4110             VALUES (1, 4, 3, 0)",
4111            [],
4112        ).unwrap();
4113
4114        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4115
4116        conn.execute(
4117            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
4118            rusqlite::params!("function", "test_func", "test.rs", "{}"),
4119        ).unwrap();
4120        let function_id: i64 = 1;
4121
4122        let cfg = create_linear_cfg();
4123        let hash1 = "test_hash_v1";
4124        let hash2 = "test_hash_v3";
4125        let limits = PathLimits::default();
4126
4127        // Call with hash1 - stores paths
4128        let paths1 = enumerate_paths_cached(&cfg, function_id, hash1, &limits, &mut conn).unwrap();
4129        assert_eq!(paths1.len(), 1);
4130
4131        // Call with hash2 - should invalidate and re-enumerate
4132        let paths2 = enumerate_paths_cached(&cfg, function_id, hash2, &limits, &mut conn).unwrap();
4133        assert_eq!(paths2.len(), 1);
4134
4135        // Both should return the same path content (same CFG)
4136        assert_eq!(paths1[0].blocks, paths2[0].blocks);
4137    }
4138
4139    #[test]
4140    fn test_enumerate_paths_cached_with_context() {
4141        use crate::storage::create_schema;
4142        use super::super::{enumerate_paths_cached_with_context, EnumerationContext};
4143
4144        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
4145
4146        // Create Magellan tables
4147        conn.execute(
4148            "CREATE TABLE magellan_meta (
4149                id INTEGER PRIMARY KEY CHECK (id = 1),
4150                magellan_schema_version INTEGER NOT NULL,
4151                sqlitegraph_schema_version INTEGER NOT NULL,
4152                created_at INTEGER NOT NULL
4153            )",
4154            [],
4155        ).unwrap();
4156
4157        conn.execute(
4158            "CREATE TABLE graph_entities (
4159                id INTEGER PRIMARY KEY AUTOINCREMENT,
4160                kind TEXT NOT NULL,
4161                name TEXT NOT NULL,
4162                file_path TEXT,
4163                data TEXT NOT NULL
4164            )",
4165            [],
4166        ).unwrap();
4167
4168        conn.execute(
4169            "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
4170             VALUES (1, 4, 3, 0)",
4171            [],
4172        ).unwrap();
4173
4174        create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4175
4176        conn.execute(
4177            "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
4178            rusqlite::params!("function", "test_func", "test.rs", "{}"),
4179        ).unwrap();
4180        let function_id: i64 = 1;
4181
4182        let cfg = create_diamond_cfg();
4183        let function_hash = "test_hash_ctx";
4184        let limits = PathLimits::default();
4185        let ctx = EnumerationContext::new(&cfg);
4186
4187        // First call - cache miss
4188        let paths1 = enumerate_paths_cached_with_context(
4189            &cfg, function_id, function_hash, &limits, &ctx, &mut conn
4190        ).unwrap();
4191        assert_eq!(paths1.len(), 2); // Diamond has 2 paths
4192
4193        // Second call - cache hit
4194        let paths2 = enumerate_paths_cached_with_context(
4195            &cfg, function_id, function_hash, &limits, &ctx, &mut conn
4196        ).unwrap();
4197        assert_eq!(paths2.len(), 2);
4198    }
4199
4200    // Task 05-06-4: estimate_path_count tests
4201
4202    #[test]
4203    fn test_estimate_path_count_linear_cfg() {
4204        // Linear CFG: 0 -> 1 -> 2
4205        let cfg = create_linear_cfg();
4206        let estimate = estimate_path_count(&cfg, 3);
4207
4208        // Linear CFG has no branches or loops, so 1 path
4209        assert_eq!(estimate, 1);
4210    }
4211
4212    #[test]
4213    fn test_estimate_path_count_diamond_cfg() {
4214        // Diamond: 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3
4215        let cfg = create_diamond_cfg();
4216        let estimate = estimate_path_count(&cfg, 3);
4217
4218        // Diamond has 1 branch point (2 outgoing from block 0)
4219        // With loop_unroll_limit=3: 2^1 = 2
4220        assert_eq!(estimate, 2);
4221    }
4222
4223    #[test]
4224    fn test_estimate_path_count_single_loop() {
4225        // Single loop with unroll_limit=3
4226        let cfg = create_loop_cfg();
4227        let estimate = estimate_path_count(&cfg, 3);
4228
4229        // 1 loop, unroll_limit=3: (3+1)^1 = 4
4230        assert_eq!(estimate, 4);
4231    }
4232
4233    #[test]
4234    fn test_estimate_path_count_loop_formula() {
4235        // Single loop, verify formula: (unroll_limit + 1) * 2
4236        let cfg = create_loop_cfg();
4237
4238        // With unroll_limit=3: (3+1) * 2 = 8
4239        // (2 because there's also a branch at the loop header)
4240        let estimate = estimate_path_count(&cfg, 3);
4241        assert!(estimate >= 4); // At minimum, loop contributes 4 paths
4242    }
4243
4244    #[test]
4245    fn test_estimate_path_count_increases_with_loop_limit() {
4246        let cfg = create_loop_cfg();
4247
4248        let estimate3 = estimate_path_count(&cfg, 3);
4249        let estimate5 = estimate_path_count(&cfg, 5);
4250
4251        // Higher unroll limit should give higher estimate
4252        assert!(estimate5 >= estimate3);
4253    }
4254
4255    #[test]
4256    fn test_estimate_path_count_monotonic_with_complexity() {
4257        // Create increasingly complex CFGs and verify monotonic growth
4258        let linear = create_linear_cfg();
4259        let diamond = create_diamond_cfg();
4260        let loop_cfg = create_loop_cfg();
4261
4262        let linear_estimate = estimate_path_count(&linear, 3);
4263        let diamond_estimate = estimate_path_count(&diamond, 3);
4264        let _loop_estimate = estimate_path_count(&loop_cfg, 3);
4265
4266        // Linear < Diamond < Loop (in terms of complexity)
4267        assert!(linear_estimate <= diamond_estimate);
4268        // Diamond might be less than loop depending on structure
4269    }
4270
4271    #[test]
4272    fn test_check_path_explosion_safe() {
4273        let cfg = create_linear_cfg();
4274        let limits = PathLimits::default();
4275
4276        // Linear CFG with default limits should be safe
4277        let result = check_path_explosion(&cfg, &limits);
4278        assert!(result.is_none(), "Linear CFG should be safe");
4279    }
4280
4281    #[test]
4282    fn test_check_path_explosion_exceeds_limit() {
4283        let cfg = create_diamond_cfg();
4284        let limits = PathLimits {
4285            max_length: 100,
4286            max_paths: 1, // Very low limit
4287            loop_unroll_limit: 3,
4288        };
4289
4290        // Diamond might exceed very low limit
4291        let result = check_path_explosion(&cfg, &limits);
4292        // Diamond produces 2 paths, so limit of 1 should trigger warning
4293        assert!(result.is_some(), "Should warn about path explosion");
4294        if let Some(estimate) = result {
4295            assert!(estimate > 1);
4296        }
4297    }
4298
4299    #[test]
4300    fn test_estimate_path_count_no_overflow() {
4301        // Even with high unroll limit, should not overflow
4302        let cfg = create_loop_cfg();
4303
4304        // Very high unroll limit
4305        let estimate = estimate_path_count(&cfg, 1000);
4306
4307        // Should return a reasonable number, not overflow
4308        assert!(estimate > 0);
4309        assert!(estimate < usize::MAX);
4310    }
4311
4312    // Task 05-06-5: Performance benchmark tests
4313
4314    /// Create a large linear CFG (100 blocks)
4315    fn create_large_linear_cfg(size: usize) -> Cfg {
4316        let mut g = DiGraph::new();
4317
4318        for i in 0..size {
4319            let kind = if i == 0 {
4320                BlockKind::Entry
4321            } else if i == size - 1 {
4322                BlockKind::Exit
4323            } else {
4324                BlockKind::Normal
4325            };
4326
4327            let terminator = if i == size - 1 {
4328                Terminator::Return
4329            } else {
4330                Terminator::Goto { target: i + 1 }
4331            };
4332
4333            let _node = g.add_node(BasicBlock {
4334                id: i,
4335                kind,
4336                statements: vec![],
4337                terminator,
4338                source_location: None,
4339            });
4340        }
4341
4342        // Add edges
4343        for i in 0..size - 1 {
4344            let from = NodeIndex::new(i);
4345            let to = NodeIndex::new(i + 1);
4346            g.add_edge(from, to, EdgeType::Fallthrough);
4347        }
4348
4349        g
4350    }
4351
4352    /// Create a large diamond CFG (10 sequential branches)
4353    fn create_large_diamond_cfg() -> Cfg {
4354        let mut g = DiGraph::new();
4355
4356        // Create a chain of diamond patterns
4357        let mut nodes = Vec::new();
4358
4359        for i in 0..21 {
4360            let kind = if i == 0 {
4361                BlockKind::Entry
4362            } else if i % 2 == 0 && i > 0 {
4363                // Merge points
4364                BlockKind::Normal
4365            } else if i == 20 {
4366                BlockKind::Exit
4367            } else {
4368                BlockKind::Normal
4369            };
4370
4371            let terminator = if i == 20 {
4372                Terminator::Return
4373            } else if i % 2 == 0 {
4374                // Branch point (even numbers after 0)
4375                let target1 = i + 1;
4376                let target2 = i + 2;
4377                Terminator::SwitchInt { targets: vec![target1], otherwise: target2 }
4378            } else {
4379                // Fallthrough to merge
4380                let merge = i + 1;
4381                Terminator::Goto { target: merge }
4382            };
4383
4384            let node = g.add_node(BasicBlock {
4385                id: i,
4386                kind,
4387                statements: vec![],
4388                terminator,
4389                source_location: None,
4390            });
4391            nodes.push(node);
4392        }
4393
4394        // Add edges for branch points
4395        for i in (0..20).step_by(2) {
4396            let from = nodes[i];
4397            let to1 = nodes[i + 1];
4398            let to2 = nodes[i + 2];
4399            g.add_edge(from, to1, EdgeType::TrueBranch);
4400            g.add_edge(from, to2, EdgeType::FalseBranch);
4401        }
4402
4403        // Add edges for fallthroughs
4404        for i in (1..20).filter(|x| x % 2 == 1) {
4405            let from = nodes[i];
4406            let to = nodes[i + 1];
4407            g.add_edge(from, to, EdgeType::Fallthrough);
4408        }
4409
4410        g
4411    }
4412
4413    #[test]
4414    #[ignore = "benchmark test - run with cargo test -- --ignored"]
4415    fn test_perf_linear_cfg_10_blocks() {
4416        use std::time::Instant;
4417
4418        let cfg = create_large_linear_cfg(10);
4419        let limits = PathLimits::default();
4420
4421        let start = Instant::now();
4422        let paths = enumerate_paths(&cfg, &limits);
4423        let duration = start.elapsed();
4424
4425        assert_eq!(paths.len(), 1);
4426        assert!(duration < Duration::from_millis(10),
4427                "Linear 10-block CFG should be <10ms, took {:?}", duration);
4428        println!("Linear 10 blocks: {:?}", duration);
4429    }
4430
4431    #[test]
4432    #[ignore = "benchmark test - run with cargo test -- --ignored"]
4433    fn test_perf_linear_cfg_100_blocks() {
4434        use std::time::Instant;
4435
4436        let cfg = create_large_linear_cfg(100);
4437        let limits = PathLimits::default();
4438
4439        let start = Instant::now();
4440        let paths = enumerate_paths(&cfg, &limits);
4441        let duration = start.elapsed();
4442
4443        assert_eq!(paths.len(), 1);
4444        assert!(duration < Duration::from_millis(100),
4445                "Linear 100-block CFG should be <100ms, took {:?}", duration);
4446        println!("Linear 100 blocks: {:?}", duration);
4447    }
4448
4449    #[test]
4450    #[ignore = "benchmark test - run with cargo test -- --ignored"]
4451    fn test_perf_diamond_cfg_10_branches() {
4452        use std::time::Instant;
4453
4454        let cfg = create_large_diamond_cfg();
4455        let limits = PathLimits::default();
4456
4457        let start = Instant::now();
4458        let paths = enumerate_paths(&cfg, &limits);
4459        let duration = start.elapsed();
4460
4461        // 10 branches = 2^10 = 1024 paths
4462        assert!(paths.len() > 0);
4463        assert!(duration < Duration::from_millis(50),
4464                "Diamond CFG should be <50ms, took {:?}", duration);
4465        println!("Diamond 10 branches: {} paths in {:?}", paths.len(), duration);
4466    }
4467
4468    #[test]
4469    #[ignore = "benchmark test - run with cargo test -- --ignored"]
4470    fn test_perf_single_loop_unroll_3() {
4471        use std::time::Instant;
4472
4473        let cfg = create_loop_cfg();
4474        let limits = PathLimits::default().with_loop_unroll_limit(3);
4475
4476        let start = Instant::now();
4477        let paths = enumerate_paths(&cfg, &limits);
4478        let duration = start.elapsed();
4479
4480        assert!(paths.len() > 0);
4481        assert!(duration < Duration::from_millis(100),
4482                "Single loop should be <100ms, took {:?}", duration);
4483        println!("Single loop (unroll=3): {} paths in {:?}", paths.len(), duration);
4484    }
4485
4486    #[test]
4487    #[ignore = "benchmark test - run with cargo test -- --ignored"]
4488    fn test_perf_nested_loops() {
4489        use std::time::Instant;
4490        use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
4491
4492        let mut g = DiGraph::new();
4493
4494        // Create 2-level nested loop
4495        let b0 = g.add_node(BasicBlock {
4496            id: 0,
4497            kind: BlockKind::Entry,
4498            statements: vec![],
4499            terminator: Terminator::Goto { target: 1 },
4500            source_location: None,
4501        });
4502
4503        let b1 = g.add_node(BasicBlock {
4504            id: 1,
4505            kind: BlockKind::Normal,
4506            statements: vec![],
4507            terminator: Terminator::SwitchInt { targets: vec![2], otherwise: 5 },
4508            source_location: None,
4509        });
4510
4511        let b2 = g.add_node(BasicBlock {
4512            id: 2,
4513            kind: BlockKind::Normal,
4514            statements: vec![],
4515            terminator: Terminator::SwitchInt { targets: vec![3], otherwise: 1 },
4516            source_location: None,
4517        });
4518
4519        let b3 = g.add_node(BasicBlock {
4520            id: 3,
4521            kind: BlockKind::Normal,
4522            statements: vec![],
4523            terminator: Terminator::Goto { target: 2 },
4524            source_location: None,
4525        });
4526
4527        let b5 = g.add_node(BasicBlock {
4528            id: 5,
4529            kind: BlockKind::Exit,
4530            statements: vec![],
4531            terminator: Terminator::Return,
4532            source_location: None,
4533        });
4534
4535        g.add_edge(b0, b1, EdgeType::Fallthrough);
4536        g.add_edge(b1, b2, EdgeType::TrueBranch);
4537        g.add_edge(b1, b5, EdgeType::FalseBranch);
4538        g.add_edge(b2, b3, EdgeType::TrueBranch);
4539        g.add_edge(b2, b1, EdgeType::LoopBack);
4540        g.add_edge(b3, b2, EdgeType::LoopBack);
4541
4542        let limits = PathLimits::default().with_loop_unroll_limit(2);
4543
4544        let start = Instant::now();
4545        let paths = enumerate_paths(&g, &limits);
4546        let duration = start.elapsed();
4547
4548        assert!(paths.len() > 0);
4549        assert!(duration < Duration::from_millis(500),
4550                "Nested loops should be <500ms, took {:?}", duration);
4551        println!("Nested 2 loops (unroll=2): {} paths in {:?}", paths.len(), duration);
4552    }
4553
4554    #[test]
4555    #[ignore = "benchmark test - run with cargo test -- --ignored"]
4556    fn test_perf_enumeration_context_reuse() {
4557        use std::time::Instant;
4558        use super::super::EnumerationContext;
4559
4560        let cfg = create_diamond_cfg();
4561        let ctx = EnumerationContext::new(&cfg);
4562
4563        // Time multiple calls with same context
4564        let limits = PathLimits::default();
4565
4566        let start = Instant::now();
4567        for _ in 0..100 {
4568            let _ = enumerate_paths_with_context(&cfg, &limits, &ctx);
4569        }
4570        let duration = start.elapsed();
4571
4572        println!("100 calls with context: {:?}", duration);
4573        assert!(duration < Duration::from_millis(100),
4574                "100 cached calls should be <100ms, took {:?}", duration);
4575    }
4576
4577    #[test]
4578    #[ignore = "benchmark test - run with cargo test -- --ignored"]
4579    fn test_perf_estimation_vs_actual() {
4580        use std::time::Instant;
4581
4582        let cfg = create_diamond_cfg();
4583
4584        // Time estimation
4585        let start = Instant::now();
4586        let estimate = estimate_path_count(&cfg, 3);
4587        let est_duration = start.elapsed();
4588
4589        // Time actual enumeration
4590        let start = Instant::now();
4591        let limits = PathLimits::default();
4592        let paths = enumerate_paths(&cfg, &limits);
4593        let enum_duration = start.elapsed();
4594
4595        println!("Estimation: {} paths in {:?}", estimate, est_duration);
4596        println!("Enumeration: {} paths in {:?}", paths.len(), enum_duration);
4597
4598        // Both should be fast for simple CFGs
4599        assert!(est_duration.as_micros() < 1000,
4600                "Estimation should be fast: {:?}", est_duration);
4601        assert!(enum_duration.as_micros() < 1000,
4602                "Enumeration should be fast: {:?}", enum_duration);
4603    }
4604}
4605
4606/// Result of incremental path enumeration
4607///
4608/// Contains the enumerated paths along with statistics about
4609/// how many functions were analyzed vs skipped.
4610#[derive(Debug, Clone)]
4611pub struct IncrementalPathsResult {
4612    /// Number of functions that were analyzed
4613    pub analyzed_functions: usize,
4614    /// Number of functions that were skipped (not changed)
4615    pub skipped_functions: usize,
4616    /// All enumerated paths from analyzed functions
4617    pub paths: Vec<Path>,
4618}
4619
4620/// Enumerate paths incrementally - only for functions in changed files
4621///
4622/// This function enables incremental analysis by using git diff to detect
4623/// changed files and only analyzing functions in those files.
4624///
4625/// # Arguments
4626///
4627/// * `function_id` - Function to analyze (can be "all" for project-wide)
4628/// * `backend` - MirageDb backend for database access
4629/// * `repo_path` - Path to git repository
4630/// * `since_revision` - Git revision (e.g., "HEAD~1")
4631/// * `max_length` - Optional maximum path length for pruning
4632///
4633/// # Returns
4634///
4635/// * `Ok(IncrementalPathsResult)` - Results with analyzed/skipped counts and paths
4636///
4637/// # Examples
4638///
4639/// ```no_run
4640/// use mirage_analyzer::cfg::paths::enumerate_paths_incremental;
4641/// use std::path::Path;
4642///
4643/// # let backend = unimplemented!();
4644/// let result = enumerate_paths_incremental(
4645///     "all",
4646///     &backend,
4647///     Path::new("."),
4648///     "HEAD~1",
4649///     Some(100)
4650/// ).unwrap();
4651///
4652/// println!("Analyzed: {}, Skipped: {}, Paths: {}",
4653///     result.analyzed_functions,
4654///     result.skipped_functions,
4655///     result.paths.len()
4656/// );
4657/// ```
4658pub fn enumerate_paths_incremental(
4659    function_id: &str,
4660    backend: &crate::storage::MirageDb,
4661    repo_path: &std::path::Path,
4662    since_revision: &str,
4663    max_length: Option<usize>,
4664) -> anyhow::Result<IncrementalPathsResult> {
4665    use crate::cfg::{load_cfg_from_db, PathLimits};
4666
4667    // If specific function requested, analyze regardless of changes
4668    if function_id != "all" {
4669        let fid = function_id.parse::<i64>()
4670            .map_err(|_| anyhow::anyhow!("Invalid function ID: {}", function_id))?;
4671
4672        let cfg = load_cfg_from_db(backend, fid)?;
4673        let limits = PathLimits {
4674            max_length: max_length.unwrap_or(1000),
4675            ..Default::default()
4676        };
4677        let paths = enumerate_paths(&cfg, &limits);
4678
4679        return Ok(IncrementalPathsResult {
4680            analyzed_functions: 1,
4681            skipped_functions: 0,
4682            paths,
4683        });
4684    }
4685
4686    // Project-wide: use git to detect changed functions
4687    let changed_function_ids = crate::cfg::git_utils::get_changed_functions(
4688        backend,
4689        repo_path,
4690        since_revision,
4691    )?;
4692
4693    if changed_function_ids.is_empty() {
4694        return Ok(IncrementalPathsResult {
4695            analyzed_functions: 0,
4696            skipped_functions: 0,  // Cannot determine total without scanning all
4697            paths: vec![],
4698        });
4699    }
4700
4701    // Analyze each changed function
4702    let mut all_paths = Vec::new();
4703    let mut analyzed = 0;
4704
4705    for fid in changed_function_ids {
4706        match load_cfg_from_db(backend, fid) {
4707            Ok(cfg) => {
4708                let limits = PathLimits {
4709                    max_length: max_length.unwrap_or(1000),
4710                    ..Default::default()
4711                };
4712                let paths = enumerate_paths(&cfg, &limits);
4713                all_paths.extend(paths);
4714                analyzed += 1;
4715            }
4716            Err(e) => {
4717                tracing::warn!("Failed to load CFG for function {}: {}", fid, e);
4718                // Continue with other functions
4719            }
4720        }
4721    }
4722
4723    Ok(IncrementalPathsResult {
4724        analyzed_functions: analyzed,
4725        skipped_functions: 0,  // TODO: Would require scanning all functions
4726        paths: all_paths,
4727    })
4728}
4729
4730// Tests for new iterative path enumeration
4731
4732#[cfg(test)]
4733mod iterative_tests {
4734    use super::*;
4735    use crate::cfg::{BlockKind, BasicBlock, Cfg, EdgeType, Terminator};
4736    use petgraph::graph::DiGraph;
4737
4738    fn create_simple_diamond_cfg() -> Cfg {
4739        let mut g = DiGraph::new();
4740
4741        let b0 = g.add_node(BasicBlock {
4742            id: 0,
4743            kind: BlockKind::Entry,
4744            statements: vec![],
4745            terminator: Terminator::SwitchInt {
4746                targets: vec![1],
4747                otherwise: 2,
4748            },
4749            source_location: None,
4750        });
4751
4752        let b1 = g.add_node(BasicBlock {
4753            id: 1,
4754            kind: BlockKind::Normal,
4755            statements: vec![],
4756            terminator: Terminator::Goto { target: 3 },
4757            source_location: None,
4758        });
4759
4760        let b2 = g.add_node(BasicBlock {
4761            id: 2,
4762            kind: BlockKind::Normal,
4763            statements: vec![],
4764            terminator: Terminator::Goto { target: 3 },
4765            source_location: None,
4766        });
4767
4768        let b3 = g.add_node(BasicBlock {
4769            id: 3,
4770            kind: BlockKind::Exit,
4771            statements: vec![],
4772            terminator: Terminator::Return,
4773            source_location: None,
4774        });
4775
4776        g.add_edge(b0, b1, EdgeType::TrueBranch);
4777        g.add_edge(b0, b2, EdgeType::FalseBranch);
4778        g.add_edge(b1, b3, EdgeType::Fallthrough);
4779        g.add_edge(b2, b3, EdgeType::Fallthrough);
4780
4781        g
4782    }
4783
4784    #[test]
4785    fn test_enumerate_paths_iterative_diamond_cfg() {
4786        let cfg = create_simple_diamond_cfg();
4787        let limits = PathLimits::default();
4788        let paths = enumerate_paths_iterative(&cfg, &limits);
4789
4790        // Diamond CFG produces 2 paths
4791        assert_eq!(paths.len(), 2);
4792
4793        // Both paths should be unique
4794        let path1 = &paths[0];
4795        let path2 = &paths[1];
4796
4797        assert_ne!(path1.blocks, path2.blocks);
4798
4799        // Both should start at 0 and end at 3
4800        assert_eq!(path1.entry, 0);
4801        assert_eq!(path1.exit, 3);
4802        assert_eq!(path2.entry, 0);
4803        assert_eq!(path2.exit, 3);
4804    }
4805
4806    #[test]
4807    fn test_enumerate_paths_iterative_produces_same_results_as_recursive() {
4808        let cfg = create_simple_diamond_cfg();
4809        let limits = PathLimits::default();
4810
4811        let recursive_paths = enumerate_paths(&cfg, &limits);
4812        let iterative_paths = enumerate_paths_iterative(&cfg, &limits);
4813
4814        // Should produce same number of paths
4815        assert_eq!(recursive_paths.len(), iterative_paths.len());
4816
4817        // Paths should be identical (as sets)
4818        let recursive_set: std::collections::HashSet<Vec<_>> = recursive_paths
4819            .iter()
4820            .map(|p| p.blocks.clone())
4821            .collect();
4822        let iterative_set: std::collections::HashSet<Vec<_>> = iterative_paths
4823            .iter()
4824            .map(|p| p.blocks.clone())
4825            .collect();
4826
4827        assert_eq!(recursive_set, iterative_set);
4828    }
4829
4830    #[test]
4831    fn test_enumerate_paths_with_metadata_diamond() {
4832        let cfg = create_simple_diamond_cfg();
4833        let result = enumerate_paths_with_metadata(&cfg, &PathLimits::default());
4834
4835        // Should have 2 paths
4836        assert_eq!(result.stats.total_paths, 2);
4837
4838        // Both should be normal paths
4839        assert_eq!(result.stats.normal_paths, 2);
4840        assert_eq!(result.stats.error_paths, 0);
4841
4842        // Average path length should be 3 blocks ([0,1,3] and [0,2,3])
4843        assert_eq!(result.stats.avg_path_length, 3.0);
4844
4845        // Max path length
4846        assert_eq!(result.stats.max_path_length, 3);
4847
4848        // No limits hit
4849        assert_eq!(result.limits_hit, LimitsHit::None);
4850    }
4851
4852    #[test]
4853    fn test_enumerate_paths_with_metadata_limits_hit() {
4854        let cfg = create_simple_diamond_cfg();
4855        let limits = PathLimits {
4856            max_paths: 1,
4857            ..Default::default()
4858        };
4859        let result = enumerate_paths_with_metadata(&cfg, &limits);
4860
4861        // Should hit max_paths limit
4862        assert_eq!(result.limits_hit, LimitsHit::MaxPaths);
4863
4864        // Should only return 1 path
4865        assert_eq!(result.stats.total_paths, 1);
4866    }
4867
4868    #[test]
4869    fn test_enumerate_paths_iterative_with_loop() {
4870        // Create a simple loop CFG
4871        let mut g = DiGraph::new();
4872
4873        let b0 = g.add_node(BasicBlock {
4874            id: 0,
4875            kind: BlockKind::Entry,
4876            statements: vec![],
4877            terminator: Terminator::SwitchInt {
4878                targets: vec![1],
4879                otherwise: 2,
4880            },
4881            source_location: None,
4882        });
4883
4884        let b1 = g.add_node(BasicBlock {
4885            id: 1,
4886            kind: BlockKind::Normal,
4887            statements: vec![],
4888            terminator: Terminator::Goto { target: 0 }, // Back edge to entry
4889            source_location: None,
4890        });
4891
4892        let b2 = g.add_node(BasicBlock {
4893            id: 2,
4894            kind: BlockKind::Exit,
4895            statements: vec![],
4896            terminator: Terminator::Return,
4897            source_location: None,
4898        });
4899
4900        g.add_edge(b0, b1, EdgeType::TrueBranch);
4901        g.add_edge(b0, b2, EdgeType::FalseBranch);
4902        g.add_edge(b1, b0, EdgeType::LoopBack);
4903
4904        let limits = PathLimits {
4905            loop_unroll_limit: 2,
4906            ..Default::default()
4907        };
4908        let paths = enumerate_paths_iterative(&g, &limits);
4909
4910        // Should produce at least 2 paths (0->2, and 0->1->0->2 with loop unrolled once)
4911        assert!(paths.len() >= 2);
4912    }
4913
4914    #[test]
4915    fn test_enumerate_paths_iterative_empty_cfg() {
4916        let cfg = DiGraph::new();
4917        let paths = enumerate_paths_iterative(&cfg, &PathLimits::default());
4918
4919        assert_eq!(paths.len(), 0);
4920    }
4921}