Skip to main content

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