1use 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
52pub struct Path {
53 pub path_id: String,
55 pub blocks: Vec<BlockId>,
57 pub kind: PathKind,
59 pub entry: BlockId,
61 pub exit: BlockId,
63}
64
65impl Path {
66 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 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 pub fn len(&self) -> usize {
108 self.blocks.len()
109 }
110
111 pub fn is_empty(&self) -> bool {
113 self.blocks.is_empty()
114 }
115
116 pub fn iter(&self) -> impl Iterator<Item = &BlockId> {
118 self.blocks.iter()
119 }
120
121 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
138pub enum PathKind {
139 Normal,
141 Error,
143 Degenerate,
145 Unreachable,
147}
148
149fn 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
157pub fn is_feasible_path(cfg: &Cfg, blocks: &[BlockId]) -> bool {
202 use crate::cfg::BlockKind;
203
204 if blocks.is_empty() {
206 return false;
207 }
208
209 let first_idx = match find_node_by_block_id(cfg, blocks[0]) {
211 Some(idx) => idx,
212 None => return false, };
214 if cfg[first_idx].kind != BlockKind::Entry {
215 return false;
216 }
217
218 let last_idx = match find_node_by_block_id(cfg, *blocks.last().unwrap()) {
220 Some(idx) => idx,
221 None => return false, };
223
224 match &cfg[last_idx].terminator {
225 Terminator::Return => {} Terminator::Abort(_) => {} Terminator::Call { unwind: None, .. } => {} Terminator::Call {
229 unwind: Some(_),
230 target: Some(_),
231 } => {} 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 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
254pub 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 if blocks.is_empty() {
308 return false;
309 }
310
311 let first_idx = match find_node_by_block_id(cfg, blocks[0]) {
313 Some(idx) => idx,
314 None => return false, };
316 if cfg[first_idx].kind != BlockKind::Entry {
317 return false;
318 }
319
320 for &block_id in blocks {
322 if !reachable_blocks.contains(&block_id) {
323 return false;
324 }
325 }
326
327 let last_idx = match find_node_by_block_id(cfg, *blocks.last().unwrap()) {
329 Some(idx) => idx,
330 None => return false, };
332
333 match &cfg[last_idx].terminator {
334 Terminator::Return => {} Terminator::Abort(_) => {} Terminator::Call { unwind: None, .. } => {} Terminator::Call {
338 unwind: Some(_),
339 target: Some(_),
340 } => {} 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 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
363pub fn classify_path(cfg: &Cfg, blocks: &[BlockId]) -> PathKind {
381 use crate::cfg::reachability::is_reachable_from_entry;
382
383 if blocks.is_empty() {
385 return PathKind::Degenerate;
386 }
387
388 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, };
394
395 if !is_reachable_from_entry(cfg, node_idx) {
397 return PathKind::Unreachable;
398 }
399
400 let terminator = &cfg[node_idx].terminator;
402
403 match terminator {
405 Terminator::Abort(_) => return PathKind::Error,
406 Terminator::Call {
407 unwind: Some(_), ..
408 } => return PathKind::Error,
409 _ => {}
410 }
411
412 if matches!(terminator, Terminator::Unreachable) {
414 return PathKind::Degenerate;
415 }
416 }
417
418 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 if !matches!(terminator, Terminator::Return) {
424 return PathKind::Degenerate;
426 }
427 }
428 }
429
430 PathKind::Normal
432}
433
434pub fn classify_path_precomputed(
475 cfg: &Cfg,
476 blocks: &[BlockId],
477 reachable_blocks: &HashSet<BlockId>,
478) -> PathKind {
479 if blocks.is_empty() {
481 return PathKind::Degenerate;
482 }
483
484 for &block_id in blocks {
486 if !reachable_blocks.contains(&block_id) {
487 return PathKind::Unreachable;
488 }
489 }
490
491 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, };
497
498 let terminator = &cfg[node_idx].terminator;
499
500 match terminator {
502 Terminator::Abort(_) => return PathKind::Error,
503 Terminator::Call {
504 unwind: Some(_), ..
505 } => return PathKind::Error,
506 _ => {}
507 }
508
509 if matches!(terminator, Terminator::Unreachable) {
511 return PathKind::Degenerate;
512 }
513 }
514
515 if !is_feasible_path_precomputed(cfg, blocks, reachable_blocks) {
518 return PathKind::Degenerate;
519 }
520
521 PathKind::Normal
523}
524
525impl PathKind {
526 pub fn is_normal(&self) -> bool {
528 matches!(self, Self::Normal)
529 }
530
531 pub fn is_error(&self) -> bool {
533 matches!(self, Self::Error)
534 }
535
536 pub fn is_degenerate(&self) -> bool {
538 matches!(self, Self::Degenerate)
539 }
540
541 pub fn is_unreachable(&self) -> bool {
543 matches!(self, Self::Unreachable)
544 }
545}
546
547#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct PathLimits {
553 pub max_length: usize,
555 pub max_paths: usize,
557 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 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 pub fn with_max_length(mut self, max_length: usize) -> Self {
583 self.max_length = max_length;
584 self
585 }
586
587 pub fn with_max_paths(mut self, max_paths: usize) -> Self {
589 self.max_paths = max_paths;
590 self
591 }
592
593 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 pub fn quick_analysis() -> Self {
611 Self {
612 max_length: 100,
613 max_paths: 1000,
614 loop_unroll_limit: 2,
615 }
616 }
617
618 pub fn thorough() -> Self {
630 Self {
631 max_length: 10000,
632 max_paths: 100000,
633 loop_unroll_limit: 5,
634 }
635 }
636}
637
638pub fn hash_path(blocks: &[BlockId]) -> String {
652 let mut hasher = blake3::Hasher::new();
653
654 hasher.update(&blocks.len().to_le_bytes());
656
657 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#[derive(Debug, Clone)]
689pub struct EnumerationContext {
690 pub reachable_blocks: HashSet<BlockId>,
692 pub loop_headers: HashSet<NodeIndex>,
694 pub exits: HashSet<NodeIndex>,
696}
697
698impl EnumerationContext {
699 pub fn new(cfg: &Cfg) -> Self {
719 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 let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
726
727 let mut exits: HashSet<NodeIndex> =
729 crate::cfg::analysis::find_exits(cfg).into_iter().collect();
730
731 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 pub fn reachable_count(&self) -> usize {
750 self.reachable_blocks.len()
751 }
752
753 pub fn loop_count(&self) -> usize {
755 self.loop_headers.len()
756 }
757
758 pub fn exit_count(&self) -> usize {
760 self.exits.len()
761 }
762
763 pub fn is_reachable(&self, block_id: BlockId) -> bool {
765 self.reachable_blocks.contains(&block_id)
766 }
767
768 pub fn is_loop_header(&self, node: NodeIndex) -> bool {
770 self.loop_headers.contains(&node)
771 }
772
773 pub fn is_exit(&self, node: NodeIndex) -> bool {
775 self.exits.contains(&node)
776 }
777}
778
779pub fn enumerate_paths_with_context(
812 cfg: &Cfg,
813 limits: &PathLimits,
814 ctx: &EnumerationContext,
815) -> Vec<Path> {
816 let entry = match crate::cfg::analysis::find_entry(cfg) {
818 Some(e) => e,
819 None => return vec![], };
821
822 if ctx.exits.is_empty() {
823 return vec![]; }
825
826 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 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
847fn 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 let block_id = match cfg.node_weight(current) {
860 Some(block) => block.id,
861 None => return,
862 };
863
864 current_path.push(block_id);
866
867 if current_path.len() > limits.max_length {
869 current_path.pop();
870 return;
871 }
872
873 if ctx.is_exit(current) {
875 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 if paths.len() >= limits.max_paths {
885 current_path.pop();
886 return;
887 }
888
889 if visited.contains(¤t) && !ctx.is_loop_header(current) {
892 current_path.pop();
893 return;
894 }
895
896 visited.insert(current);
898
899 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(¤t);
905 current_path.pop();
906 return;
907 }
908 *count += 1;
909 }
910
911 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 if is_loop_header {
929 if let Some(count) = loop_iterations.get_mut(¤t) {
930 *count = count.saturating_sub(1);
931 }
932 }
933
934 visited.remove(¤t);
935 current_path.pop();
936}
937
938pub fn enumerate_paths(cfg: &Cfg, limits: &PathLimits) -> Vec<Path> {
965 let entry = match crate::cfg::analysis::find_entry(cfg) {
967 Some(e) => e,
968 None => return vec![], };
970
971 let mut exits: HashSet<NodeIndex> = crate::cfg::analysis::find_exits(cfg).into_iter().collect();
973
974 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![]; }
989
990 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 let mut paths = Vec::new();
997 let mut current_path = Vec::new();
998 let mut visited = HashSet::new();
999
1000 let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
1002 let mut loop_iterations: HashMap<NodeIndex, usize> = HashMap::new();
1003
1004 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
1021fn 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 let block_id = match cfg.node_weight(current) {
1040 Some(block) => block.id,
1041 None => return,
1042 };
1043
1044 current_path.push(block_id);
1046
1047 if current_path.len() > limits.max_length {
1049 current_path.pop();
1050 return;
1051 }
1052
1053 if exits.contains(¤t) {
1055 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 if paths.len() >= limits.max_paths {
1065 current_path.pop();
1066 return;
1067 }
1068
1069 let is_loop_header = loop_headers.contains(¤t);
1071 if is_loop_header {
1072 let count = loop_iterations.entry(current).or_insert(0);
1073 if *count >= limits.loop_unroll_limit {
1074 current_path.pop();
1076 return;
1077 }
1078 *count += 1;
1079 }
1080
1081 let was_visited = visited.insert(current);
1083
1084 let mut successors: Vec<NodeIndex> = cfg.neighbors(current).collect();
1086 successors.sort_by_key(|n| n.index()); if successors.is_empty() {
1089 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 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 if is_back_edge {
1105 let count = loop_iterations.get(&succ).copied().unwrap_or(0);
1106 if count >= limits.loop_unroll_limit {
1107 continue; }
1109 }
1110
1111 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 if paths.len() >= limits.max_paths {
1127 break;
1128 }
1129 }
1130 }
1131
1132 if was_visited {
1134 visited.remove(¤t);
1135 }
1136
1137 if is_loop_header {
1139 loop_iterations.entry(current).and_modify(|c| *c -= 1);
1140 }
1141
1142 current_path.pop();
1144}
1145
1146pub fn enumerate_paths_iterative(cfg: &Cfg, limits: &PathLimits) -> Vec<Path> {
1185 use std::collections::BTreeSet;
1186
1187 let entry = match crate::cfg::analysis::find_entry(cfg) {
1189 Some(e) => e,
1190 None => return vec![],
1191 };
1192
1193 let mut exits: HashSet<NodeIndex> = crate::cfg::analysis::find_exits(cfg).into_iter().collect();
1195
1196 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![]; }
1211
1212 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 let mut seen_paths: BTreeSet<Vec<BlockId>> = BTreeSet::new();
1221
1222 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 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 if current_path.len() > limits.max_length {
1255 continue;
1256 }
1257
1258 if exits.contains(¤t) {
1260 if seen_paths.insert(current_path.clone()) {
1262 let kind = classify_path_precomputed(cfg, ¤t_path, &reachable_blocks);
1263 let path = Path::new(current_path, kind);
1264 paths.push(path);
1265 }
1266 continue;
1267 }
1268
1269 if paths.len() >= limits.max_paths {
1271 break;
1272 }
1273
1274 let mut successors: Vec<NodeIndex> = cfg.neighbors(current).collect();
1276 successors.sort_by_key(|n| n.index()); for succ in successors {
1279 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 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 let mut new_path = current_path.clone();
1299 let block_id = cfg[succ].id;
1300 new_path.push(block_id);
1301
1302 let mut new_visited = current_visited.clone();
1304 new_visited.insert(succ);
1305
1306 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#[derive(Debug, Clone)]
1324pub struct PathEnumerationResult {
1325 pub paths: Vec<Path>,
1327 pub limits_hit: LimitsHit,
1329 pub stats: EnumerationStats,
1331}
1332
1333#[derive(Debug, Clone, PartialEq, Eq)]
1335pub enum LimitsHit {
1336 None,
1338 MaxLength,
1340 MaxPaths,
1342 LoopUnroll,
1344 Multiple,
1346}
1347
1348#[derive(Debug, Clone)]
1350pub struct EnumerationStats {
1351 pub total_paths: usize,
1353 pub normal_paths: usize,
1355 pub error_paths: usize,
1357 pub degenerate_paths: usize,
1359 pub unreachable_paths: usize,
1361 pub avg_path_length: f64,
1363 pub max_path_length: usize,
1365 pub loop_count: usize,
1367}
1368
1369pub 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 stats.loop_count = crate::cfg::loops::find_loop_headers(cfg).len();
1445
1446 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
1462pub 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 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 let Some(ref hash) = current_hash {
1527 if hash == function_hash {
1528 let paths = get_cached_paths(db_conn, function_id)
1530 .map_err(|e| format!("Failed to retrieve cached paths: {}", e))?;
1531 if !paths.is_empty() {
1534 return Ok(paths);
1535 }
1536 }
1537 }
1538
1539 let paths = enumerate_paths(cfg, limits);
1541
1542 let _ = invalidate_function_paths(db_conn, function_id);
1544
1545 store_paths(db_conn, function_id, &paths)
1547 .map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
1548
1549 Ok(paths)
1553}
1554
1555pub 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 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 if let Some(ref hash) = current_hash {
1636 if hash == function_hash {
1637 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 let ctx = EnumerationContext::new(cfg);
1646 let paths = enumerate_paths_with_context(cfg, limits, &ctx);
1647
1648 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
1655pub 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 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 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
1712pub fn estimate_path_count(cfg: &Cfg, loop_unroll_limit: usize) -> usize {
1750 let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
1752 let loop_count = loop_headers.len();
1753
1754 let mut branch_count = 0;
1756 for node in cfg.node_indices() {
1757 if loop_headers.contains(&node) {
1758 continue; }
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; }
1766 }
1767
1768 if loop_count == 0 && branch_count == 0 {
1770 return 1; }
1772
1773 let unroll_factor = loop_unroll_limit + 1;
1776
1777 let branch_factor = if branch_count < 31 {
1780 2_usize.pow(branch_count as u32)
1781 } else {
1782 usize::MAX / 2 };
1784
1785 let loop_factor = if loop_count < 31 {
1786 unroll_factor.pow(loop_count as u32)
1787 } else {
1788 usize::MAX / 2 };
1790
1791 branch_factor.saturating_mul(loop_factor)
1793}
1794
1795pub 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 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 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 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 #[test]
2108 fn test_find_node_by_block_id_existing() {
2109 let cfg = create_linear_cfg();
2110
2111 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 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 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 let b0 = find_node_by_block_id(&cfg, 0);
2141 assert!(b0.is_none());
2142 }
2143
2144 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 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 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 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 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), },
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 let path = vec![1]; 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]; 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]; let kind = classify_path(&cfg, &path);
2374 assert_eq!(kind, PathKind::Degenerate);
2375 }
2376
2377 #[test]
2380 fn test_classify_path_precomputed_matches_classify_path() {
2381 let cfg = create_diamond_cfg();
2382
2383 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 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 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 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 let reachable_nodes = find_reachable(&cfg);
2428 let reachable_blocks: HashSet<BlockId> =
2429 reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
2430
2431 let test_paths: Vec<Vec<BlockId>> = (0..1000).map(|_| vec![0, 1, 3]).collect();
2433
2434 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 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 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 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 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 #[test]
2490 fn test_enumerate_paths_linear_cfg() {
2491 let cfg = create_linear_cfg();
2492 let paths = enumerate_paths(&cfg, &PathLimits::default());
2493
2494 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 assert_eq!(paths.len(), 2);
2509
2510 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 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 let limits = PathLimits::default().with_loop_unroll_limit(3);
2534 let paths = enumerate_paths(&cfg, &limits);
2535
2536 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 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 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 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 let limits = PathLimits::default().with_max_paths(1);
2571 let paths = enumerate_paths(&cfg, &limits);
2572
2573 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 let limits = PathLimits::default().with_max_length(2);
2583 let paths = enumerate_paths(&cfg, &limits);
2584
2585 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 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 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 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 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 assert_eq!(paths.len(), 1);
2663 assert_eq!(paths[0].blocks, vec![0, 1]);
2664 }
2665
2666 #[test]
2669 fn test_enumerate_paths_classification_diamond() {
2670 let cfg = create_diamond_cfg();
2671 let paths = enumerate_paths(&cfg, &PathLimits::default());
2672
2673 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 let cfg = create_error_cfg();
2690 let paths = enumerate_paths(&cfg, &PathLimits::default());
2691
2692 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 let cfg = create_dead_code_cfg();
2702 let paths = enumerate_paths(&cfg, &PathLimits::default());
2703
2704 assert_eq!(paths.len(), 1);
2706 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 let mut g = DiGraph::new();
2715
2716 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 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 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 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 let cfg = create_diamond_cfg();
2777 let paths = enumerate_paths(&cfg, &PathLimits::default());
2778
2779 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 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 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); g.add_edge(b1, b2, EdgeType::Fallthrough); g.add_edge(b2, b3, EdgeType::Fallthrough);
2859
2860 let paths = enumerate_paths(&g, &PathLimits::default());
2861
2862 assert!(
2865 !paths.is_empty(),
2866 "Should find at least one path through try operator CFG, found 0"
2867 );
2868
2869 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 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 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 }, 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 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 #[test]
2938 fn test_path_limits_max_length_long_path() {
2939 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 let limits = PathLimits::default().with_max_length(3);
3009 let paths = enumerate_paths(&g, &limits);
3010
3011 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 let limits = PathLimits::default().with_max_paths(1);
3025 let paths = enumerate_paths(&cfg, &limits);
3026
3027 assert_eq!(paths.len(), 1, "Should stop at max_paths=1");
3029
3030 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 let limits = PathLimits::default().with_loop_unroll_limit(1);
3043 let paths = enumerate_paths(&cfg, &limits);
3044
3045 assert_eq!(
3049 paths.len(),
3050 1,
3051 "Should have exactly 1 path with loop_unroll_limit=1"
3052 );
3053
3054 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 let limits = PathLimits::default().with_loop_unroll_limit(2);
3071 let paths = enumerate_paths(&cfg, &limits);
3072
3073 assert_eq!(
3075 paths.len(),
3076 2,
3077 "Should have exactly 2 paths with loop_unroll_limit=2"
3078 );
3079
3080 assert!(
3082 paths.iter().any(|p| p.blocks == vec![0, 1, 3]),
3083 "Direct exit path should exist"
3084 );
3085
3086 assert!(
3088 paths.iter().any(|p| p.blocks == vec![0, 1, 2, 1, 3]),
3089 "One iteration path should exist"
3090 );
3091 }
3092
3093 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 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 let limits = PathLimits::default();
3135 let paths = enumerate_paths(&cfg, &limits);
3136
3137 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 let limits = PathLimits::default().with_loop_unroll_limit(1);
3151 let paths = enumerate_paths(&cfg, &limits);
3152
3153 assert!(
3155 paths.len() <= 2,
3156 "Self-loop with low limit should have few paths"
3157 );
3158 }
3159
3160 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); g.add_edge(b3, b2, EdgeType::LoopBack); g
3244 }
3245
3246 #[test]
3247 fn test_nested_loop_bounding() {
3248 let cfg = create_nested_loop_cfg();
3249
3250 let limits = PathLimits::default().with_loop_unroll_limit(2);
3253 let paths = enumerate_paths(&cfg, &limits);
3254
3255 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 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 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 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 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); g.add_edge(b3, b4, EdgeType::TrueBranch);
3360 g.add_edge(b3, b2, EdgeType::LoopBack); g.add_edge(b4, b3, EdgeType::LoopBack); 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 let loop_headers = crate::cfg::loops::find_loop_headers(&cfg);
3380
3381 assert_eq!(loop_headers.len(), 2, "Should detect 2 loop headers");
3383
3384 let limits = PathLimits::default().with_loop_unroll_limit(2);
3386 let paths = enumerate_paths(&cfg, &limits);
3387
3388 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 #[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 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 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 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 let quick_paths = enumerate_paths(&cfg, &PathLimits::quick_analysis());
3452 let thorough_paths = enumerate_paths(&cfg, &PathLimits::thorough());
3453
3454 assert!(
3456 thorough_paths.len() >= quick_paths.len(),
3457 "Thorough analysis should find at least as many paths as quick"
3458 );
3459 }
3460
3461 #[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 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 let path = vec![0, 1]; 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 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 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 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 let path = vec![0, 99]; 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 let path = vec![0]; 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 let path1 = vec![0, 1, 3]; let path2 = vec![0, 2, 3]; 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 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 let path = vec![0]; 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, 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 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 #[test]
3674 fn test_is_feasible_path_precomputed_matches_basic() {
3675 let cfg = create_diamond_cfg();
3676
3677 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 let test_paths = vec![
3685 vec![0, 1, 3], vec![0, 2, 3], vec![0, 1], vec![], ];
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 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 let path = vec![1]; 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 let reachable_nodes = find_reachable(&cfg);
3730 let reachable_blocks: HashSet<BlockId> =
3731 reachable_nodes.iter().map(|&idx| cfg[idx].id).collect();
3732
3733 let test_paths: Vec<Vec<BlockId>> = (0..1000).map(|_| vec![0, 1, 3]).collect();
3735
3736 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 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 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 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 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 #[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 let path = vec![0, 1]; 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 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 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 let path = vec![0]; 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 let cfg = create_dead_code_cfg();
3877 let reachable = find_reachable(&cfg)
3878 .iter()
3879 .map(|&idx| cfg[idx].id)
3880 .collect();
3881
3882 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 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 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 fn create_conflicting_conditions_cfg() -> Cfg {
3974 let mut g = DiGraph::new();
3975
3976 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 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 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 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 let path = vec![0, 1, 3];
4055
4056 assert!(
4058 is_feasible_path_precomputed(&cfg, &path, &reachable),
4059 "Static analysis marks conflicting path as feasible (limitation)"
4060 );
4061
4062 assert_eq!(
4064 classify_path_precomputed(&cfg, &path, &reachable),
4065 PathKind::Normal,
4066 "Conflicting path is classified as Normal (static limitation)"
4067 );
4068
4069 }
4072
4073 #[test]
4074 fn test_feasibility_documentation_accuracy() {
4075 use crate::cfg::reachability::find_reachable;
4076
4077 let cfg = create_linear_cfg();
4080 let reachable = find_reachable(&cfg)
4081 .iter()
4082 .map(|&idx| cfg[idx].id)
4083 .collect();
4084
4085 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 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 assert!(
4101 is_feasible_path_precomputed(&cfg, &[0, 1, 2], &reachable),
4102 "Reachable path is feasible as documented"
4103 );
4104 }
4105
4106 #[test]
4109 fn test_get_or_enumerate_paths_cache_miss_enumerates() {
4110 use crate::storage::create_schema;
4111
4112 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
4114
4115 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_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4147
4148 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 let cfg = create_linear_cfg();
4158 let function_hash = "test_hash_123";
4159 let limits = PathLimits::default();
4160
4161 let paths =
4163 get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4164
4165 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 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 let paths1 =
4221 get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4222 assert_eq!(paths1.len(), 1);
4223
4224 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 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 let paths1 = get_or_enumerate_paths(&cfg, function_id, hash1, &limits, &mut conn).unwrap();
4283 assert_eq!(paths1.len(), 1);
4284
4285 let paths2 = get_or_enumerate_paths(&cfg, function_id, hash2, &limits, &mut conn).unwrap();
4287 assert_eq!(paths2.len(), 1);
4288
4289 assert_eq!(paths1[0].blocks, paths2[0].blocks);
4291 }
4292
4293 #[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 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 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 assert_eq!(ctx.loop_count(), 0);
4330 assert_eq!(ctx.reachable_count(), 4); assert_eq!(ctx.exit_count(), 1); }
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 assert!(ctx.is_reachable(0));
4343 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 assert!(ctx.is_loop_header(NodeIndex::new(1)));
4357 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 assert!(ctx.is_exit(NodeIndex::new(3)));
4371 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 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 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 assert!(paths.len() > 0);
4427 assert!(paths.len() <= 4); }
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 let start = Instant::now();
4440 let _paths1 = enumerate_paths(&cfg, &limits);
4441 let basic_time = start.elapsed();
4442
4443 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 println!(
4456 "Basic: {:?}, Context creation: {:?}, Context enum: {:?}",
4457 basic_time, ctx_creation_time, context_time
4458 );
4459
4460 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 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 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 assert_eq!(paths1.len(), paths2.len());
4487 }
4488
4489 #[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 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 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 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 let paths1 =
4601 enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4602 assert_eq!(paths1.len(), 1);
4603
4604 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 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 let paths1 = enumerate_paths_cached(&cfg, function_id, hash1, &limits, &mut conn).unwrap();
4664 assert_eq!(paths1.len(), 1);
4665
4666 let paths2 = enumerate_paths_cached(&cfg, function_id, hash2, &limits, &mut conn).unwrap();
4668 assert_eq!(paths2.len(), 1);
4669
4670 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 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 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); 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 #[test]
4753 fn test_estimate_path_count_linear_cfg() {
4754 let cfg = create_linear_cfg();
4756 let estimate = estimate_path_count(&cfg, 3);
4757
4758 assert_eq!(estimate, 1);
4760 }
4761
4762 #[test]
4763 fn test_estimate_path_count_diamond_cfg() {
4764 let cfg = create_diamond_cfg();
4766 let estimate = estimate_path_count(&cfg, 3);
4767
4768 assert_eq!(estimate, 2);
4771 }
4772
4773 #[test]
4774 fn test_estimate_path_count_single_loop() {
4775 let cfg = create_loop_cfg();
4777 let estimate = estimate_path_count(&cfg, 3);
4778
4779 assert_eq!(estimate, 4);
4781 }
4782
4783 #[test]
4784 fn test_estimate_path_count_loop_formula() {
4785 let cfg = create_loop_cfg();
4787
4788 let estimate = estimate_path_count(&cfg, 3);
4791 assert!(estimate >= 4); }
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 assert!(estimate5 >= estimate3);
4803 }
4804
4805 #[test]
4806 fn test_estimate_path_count_monotonic_with_complexity() {
4807 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 assert!(linear_estimate <= diamond_estimate);
4818 }
4820
4821 #[test]
4822 fn test_check_path_explosion_safe() {
4823 let cfg = create_linear_cfg();
4824 let limits = PathLimits::default();
4825
4826 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, loop_unroll_limit: 3,
4838 };
4839
4840 let result = check_path_explosion(&cfg, &limits);
4842 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 let cfg = create_loop_cfg();
4853
4854 let estimate = estimate_path_count(&cfg, 1000);
4856
4857 assert!(estimate > 0);
4859 assert!(estimate < usize::MAX);
4860 }
4861
4862 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 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 fn create_large_diamond_cfg() -> Cfg {
4908 let mut g = DiGraph::new();
4909
4910 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 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 let target1 = i + 1;
4930 let target2 = i + 2;
4931 Terminator::SwitchInt {
4932 targets: vec![target1],
4933 otherwise: target2,
4934 }
4935 } else {
4936 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 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 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 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 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 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 let start = Instant::now();
5203 let estimate = estimate_path_count(&cfg, 3);
5204 let est_duration = start.elapsed();
5205
5206 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 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#[derive(Debug, Clone)]
5234pub struct IncrementalPathsResult {
5235 pub analyzed_functions: usize,
5237 pub skipped_functions: usize,
5239 pub paths: Vec<Path>,
5241}
5242
5243pub 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 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 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, paths: vec![],
5319 });
5320 }
5321
5322 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 }
5341 }
5342 }
5343
5344 Ok(IncrementalPathsResult {
5345 analyzed_functions: analyzed,
5346 skipped_functions: 0, paths: all_paths,
5348 })
5349}
5350
5351#[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 assert_eq!(paths.len(), 2);
5429
5430 let path1 = &paths[0];
5432 let path2 = &paths[1];
5433
5434 assert_ne!(path1.blocks, path2.blocks);
5435
5436 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 assert_eq!(recursive_paths.len(), iterative_paths.len());
5453
5454 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 assert_eq!(result.stats.total_paths, 2);
5470
5471 assert_eq!(result.stats.normal_paths, 2);
5473 assert_eq!(result.stats.error_paths, 0);
5474
5475 assert_eq!(result.stats.avg_path_length, 3.0);
5477
5478 assert_eq!(result.stats.max_path_length, 3);
5480
5481 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 assert_eq!(result.limits_hit, LimitsHit::MaxPaths);
5496
5497 assert_eq!(result.stats.total_paths, 1);
5499 }
5500
5501 #[test]
5502 fn test_enumerate_paths_iterative_with_loop() {
5503 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 }, 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 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}