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()
155 .find(|&idx| cfg[idx].id == block_id)
156}
157
158pub fn is_feasible_path(cfg: &Cfg, blocks: &[BlockId]) -> bool {
203 use crate::cfg::BlockKind;
204
205 if blocks.is_empty() {
207 return false;
208 }
209
210 let first_idx = match find_node_by_block_id(cfg, blocks[0]) {
212 Some(idx) => idx,
213 None => return false, };
215 if cfg[first_idx].kind != BlockKind::Entry {
216 return false;
217 }
218
219 let last_idx = match find_node_by_block_id(cfg, *blocks.last().unwrap()) {
221 Some(idx) => idx,
222 None => return false, };
224
225 match &cfg[last_idx].terminator {
226 Terminator::Return => {}, Terminator::Abort(_) => {}, Terminator::Call { unwind: None, .. } => {}, Terminator::Call { unwind: Some(_), target: Some(_) } => {}, Terminator::Unreachable |
232 Terminator::Goto { .. } |
233 Terminator::SwitchInt { .. } |
234 Terminator::Call { unwind: Some(_), target: None } => {
235 return false;
236 }
237 }
238
239 for &block_id in blocks.iter().skip(1).take(blocks.len().saturating_sub(2)) {
241 if find_node_by_block_id(cfg, block_id).is_none() {
242 return false;
243 }
244 }
245
246 true
247}
248
249pub fn is_feasible_path_precomputed(
295 cfg: &Cfg,
296 blocks: &[BlockId],
297 reachable_blocks: &HashSet<BlockId>,
298) -> bool {
299 use crate::cfg::BlockKind;
300
301 if blocks.is_empty() {
303 return false;
304 }
305
306 let first_idx = match find_node_by_block_id(cfg, blocks[0]) {
308 Some(idx) => idx,
309 None => return false, };
311 if cfg[first_idx].kind != BlockKind::Entry {
312 return false;
313 }
314
315 for &block_id in blocks {
317 if !reachable_blocks.contains(&block_id) {
318 return false;
319 }
320 }
321
322 let last_idx = match find_node_by_block_id(cfg, *blocks.last().unwrap()) {
324 Some(idx) => idx,
325 None => return false, };
327
328 match &cfg[last_idx].terminator {
329 Terminator::Return => {}, Terminator::Abort(_) => {}, Terminator::Call { unwind: None, .. } => {}, Terminator::Call { unwind: Some(_), target: Some(_) } => {}, Terminator::Unreachable |
335 Terminator::Goto { .. } |
336 Terminator::SwitchInt { .. } |
337 Terminator::Call { unwind: Some(_), target: None } => {
338 return false;
339 }
340 }
341
342 for &block_id in blocks.iter().skip(1).take(blocks.len().saturating_sub(2)) {
344 if find_node_by_block_id(cfg, block_id).is_none() {
345 return false;
346 }
347 }
348
349 true
350}
351
352pub fn classify_path(cfg: &Cfg, blocks: &[BlockId]) -> PathKind {
370 use crate::cfg::reachability::is_reachable_from_entry;
371
372 if blocks.is_empty() {
374 return PathKind::Degenerate;
375 }
376
377 for &block_id in blocks {
379 let node_idx = match find_node_by_block_id(cfg, block_id) {
380 Some(idx) => idx,
381 None => return PathKind::Degenerate, };
383
384 if !is_reachable_from_entry(cfg, node_idx) {
386 return PathKind::Unreachable;
387 }
388
389 let terminator = &cfg[node_idx].terminator;
391
392 match terminator {
394 Terminator::Abort(_) => return PathKind::Error,
395 Terminator::Call { unwind: Some(_), .. } => return PathKind::Error,
396 _ => {}
397 }
398
399 if matches!(terminator, Terminator::Unreachable) {
401 return PathKind::Degenerate;
402 }
403 }
404
405 if let Some(&last_block_id) = blocks.last() {
407 if let Some(node_idx) = find_node_by_block_id(cfg, last_block_id) {
408 let terminator = &cfg[node_idx].terminator;
409 if !matches!(terminator, Terminator::Return) {
411 return PathKind::Degenerate;
413 }
414 }
415 }
416
417 PathKind::Normal
419}
420
421pub fn classify_path_precomputed(
462 cfg: &Cfg,
463 blocks: &[BlockId],
464 reachable_blocks: &HashSet<BlockId>,
465) -> PathKind {
466 if blocks.is_empty() {
468 return PathKind::Degenerate;
469 }
470
471 for &block_id in blocks {
473 if !reachable_blocks.contains(&block_id) {
474 return PathKind::Unreachable;
475 }
476 }
477
478 for &block_id in blocks {
480 let node_idx = match find_node_by_block_id(cfg, block_id) {
481 Some(idx) => idx,
482 None => return PathKind::Degenerate, };
484
485 let terminator = &cfg[node_idx].terminator;
486
487 match terminator {
489 Terminator::Abort(_) => return PathKind::Error,
490 Terminator::Call { unwind: Some(_), .. } => return PathKind::Error,
491 _ => {}
492 }
493
494 if matches!(terminator, Terminator::Unreachable) {
496 return PathKind::Degenerate;
497 }
498 }
499
500 if !is_feasible_path_precomputed(cfg, blocks, reachable_blocks) {
503 return PathKind::Degenerate;
504 }
505
506 PathKind::Normal
508}
509
510impl PathKind {
511 pub fn is_normal(&self) -> bool {
513 matches!(self, Self::Normal)
514 }
515
516 pub fn is_error(&self) -> bool {
518 matches!(self, Self::Error)
519 }
520
521 pub fn is_degenerate(&self) -> bool {
523 matches!(self, Self::Degenerate)
524 }
525
526 pub fn is_unreachable(&self) -> bool {
528 matches!(self, Self::Unreachable)
529 }
530}
531
532#[derive(Debug, Clone, PartialEq, Eq)]
537pub struct PathLimits {
538 pub max_length: usize,
540 pub max_paths: usize,
542 pub loop_unroll_limit: usize,
544}
545
546impl Default for PathLimits {
547 fn default() -> Self {
548 Self {
549 max_length: 1000,
550 max_paths: 10000,
551 loop_unroll_limit: 3,
552 }
553 }
554}
555
556impl PathLimits {
557 pub fn new(max_length: usize, max_paths: usize, loop_unroll_limit: usize) -> Self {
559 Self {
560 max_length,
561 max_paths,
562 loop_unroll_limit,
563 }
564 }
565
566 pub fn with_max_length(mut self, max_length: usize) -> Self {
568 self.max_length = max_length;
569 self
570 }
571
572 pub fn with_max_paths(mut self, max_paths: usize) -> Self {
574 self.max_paths = max_paths;
575 self
576 }
577
578 pub fn with_loop_unroll_limit(mut self, loop_unroll_limit: usize) -> Self {
580 self.loop_unroll_limit = loop_unroll_limit;
581 self
582 }
583
584 pub fn quick_analysis() -> Self {
596 Self {
597 max_length: 100,
598 max_paths: 1000,
599 loop_unroll_limit: 2,
600 }
601 }
602
603 pub fn thorough() -> Self {
615 Self {
616 max_length: 10000,
617 max_paths: 100000,
618 loop_unroll_limit: 5,
619 }
620 }
621}
622
623pub fn hash_path(blocks: &[BlockId]) -> String {
637 let mut hasher = blake3::Hasher::new();
638
639 hasher.update(&blocks.len().to_le_bytes());
641
642 for &block_id in blocks {
644 hasher.update(&block_id.to_le_bytes());
645 }
646
647 hasher.finalize().to_hex().to_string()
648}
649
650#[derive(Debug, Clone)]
674pub struct EnumerationContext {
675 pub reachable_blocks: HashSet<BlockId>,
677 pub loop_headers: HashSet<NodeIndex>,
679 pub exits: HashSet<NodeIndex>,
681}
682
683impl EnumerationContext {
684 pub fn new(cfg: &Cfg) -> Self {
704 let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
706 let reachable_blocks: HashSet<BlockId> = reachable_nodes
707 .iter()
708 .map(|&idx| cfg[idx].id)
709 .collect();
710
711 let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
713
714 let exits = crate::cfg::analysis::find_exits(cfg)
716 .into_iter()
717 .collect();
718
719 Self {
720 reachable_blocks,
721 loop_headers,
722 exits,
723 }
724 }
725
726 pub fn reachable_count(&self) -> usize {
728 self.reachable_blocks.len()
729 }
730
731 pub fn loop_count(&self) -> usize {
733 self.loop_headers.len()
734 }
735
736 pub fn exit_count(&self) -> usize {
738 self.exits.len()
739 }
740
741 pub fn is_reachable(&self, block_id: BlockId) -> bool {
743 self.reachable_blocks.contains(&block_id)
744 }
745
746 pub fn is_loop_header(&self, node: NodeIndex) -> bool {
748 self.loop_headers.contains(&node)
749 }
750
751 pub fn is_exit(&self, node: NodeIndex) -> bool {
753 self.exits.contains(&node)
754 }
755}
756
757pub fn enumerate_paths_with_context(
790 cfg: &Cfg,
791 limits: &PathLimits,
792 ctx: &EnumerationContext,
793) -> Vec<Path> {
794 let entry = match crate::cfg::analysis::find_entry(cfg) {
796 Some(e) => e,
797 None => return vec![], };
799
800 if ctx.exits.is_empty() {
801 return vec![]; }
803
804 let mut paths = Vec::new();
806 let mut current_path = Vec::new();
807 let mut visited = HashSet::new();
808 let mut loop_iterations: HashMap<NodeIndex, usize> = HashMap::new();
809
810 dfs_enumerate_with_context(
812 cfg,
813 entry,
814 limits,
815 &mut paths,
816 &mut current_path,
817 &mut visited,
818 ctx,
819 &mut loop_iterations,
820 );
821
822 paths
823}
824
825fn dfs_enumerate_with_context(
827 cfg: &Cfg,
828 current: NodeIndex,
829 limits: &PathLimits,
830 paths: &mut Vec<Path>,
831 current_path: &mut Vec<BlockId>,
832 visited: &mut HashSet<NodeIndex>,
833 ctx: &EnumerationContext,
834 loop_iterations: &mut HashMap<NodeIndex, usize>,
835) {
836 let block_id = match cfg.node_weight(current) {
838 Some(block) => block.id,
839 None => return,
840 };
841
842 current_path.push(block_id);
844
845 if current_path.len() > limits.max_length {
847 current_path.pop();
848 return;
849 }
850
851 if ctx.is_exit(current) {
853 let kind = classify_path_precomputed(cfg, current_path, &ctx.reachable_blocks);
855 let path = Path::new(current_path.clone(), kind);
856 paths.push(path);
857 current_path.pop();
858 return;
859 }
860
861 if paths.len() >= limits.max_paths {
863 current_path.pop();
864 return;
865 }
866
867 if visited.contains(¤t) && !ctx.is_loop_header(current) {
870 current_path.pop();
871 return;
872 }
873
874 visited.insert(current);
876
877 let is_loop_header = ctx.is_loop_header(current);
879 if is_loop_header {
880 let count = loop_iterations.entry(current).or_insert(0);
881 if *count >= limits.loop_unroll_limit {
882 visited.remove(¤t);
883 current_path.pop();
884 return;
885 }
886 *count += 1;
887 }
888
889 let neighbors: Vec<_> = cfg
891 .neighbors(current)
892 .collect();
893
894 for next in neighbors {
895 dfs_enumerate_with_context(
896 cfg,
897 next,
898 limits,
899 paths,
900 current_path,
901 visited,
902 ctx,
903 loop_iterations,
904 );
905 }
906
907 if is_loop_header {
909 if let Some(count) = loop_iterations.get_mut(¤t) {
910 *count = count.saturating_sub(1);
911 }
912 }
913
914 visited.remove(¤t);
915 current_path.pop();
916}
917
918pub fn enumerate_paths(cfg: &Cfg, limits: &PathLimits) -> Vec<Path> {
945 let entry = match crate::cfg::analysis::find_entry(cfg) {
947 Some(e) => e,
948 None => return vec![], };
950
951 let exits: HashSet<NodeIndex> = crate::cfg::analysis::find_exits(cfg)
953 .into_iter()
954 .collect();
955
956 if exits.is_empty() {
957 return vec![]; }
959
960 let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
962 let reachable_blocks: HashSet<BlockId> = reachable_nodes
963 .iter()
964 .map(|&idx| cfg[idx].id)
965 .collect();
966
967 let mut paths = Vec::new();
969 let mut current_path = Vec::new();
970 let mut visited = HashSet::new();
971
972 let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
974 let mut loop_iterations: HashMap<NodeIndex, usize> = HashMap::new();
975
976 dfs_enumerate(
978 cfg,
979 entry,
980 &exits,
981 limits,
982 &mut paths,
983 &mut current_path,
984 &mut visited,
985 &loop_headers,
986 &mut loop_iterations,
987 &reachable_blocks,
988 );
989
990 paths
991}
992
993fn dfs_enumerate(
999 cfg: &Cfg,
1000 current: NodeIndex,
1001 exits: &HashSet<NodeIndex>,
1002 limits: &PathLimits,
1003 paths: &mut Vec<Path>,
1004 current_path: &mut Vec<BlockId>,
1005 visited: &mut HashSet<NodeIndex>,
1006 loop_headers: &HashSet<NodeIndex>,
1007 loop_iterations: &mut HashMap<NodeIndex, usize>,
1008 reachable_blocks: &HashSet<BlockId>,
1009) {
1010 let block_id = match cfg.node_weight(current) {
1012 Some(block) => block.id,
1013 None => return,
1014 };
1015
1016 current_path.push(block_id);
1018
1019 if current_path.len() > limits.max_length {
1021 current_path.pop();
1022 return;
1023 }
1024
1025 if exits.contains(¤t) {
1027 let kind = classify_path_precomputed(cfg, current_path, reachable_blocks);
1029 let path = Path::new(current_path.clone(), kind);
1030 paths.push(path);
1031 current_path.pop();
1032 return;
1033 }
1034
1035 if paths.len() >= limits.max_paths {
1037 current_path.pop();
1038 return;
1039 }
1040
1041 let is_loop_header = loop_headers.contains(¤t);
1043 if is_loop_header {
1044 let count = loop_iterations.entry(current).or_insert(0);
1045 if *count >= limits.loop_unroll_limit {
1046 current_path.pop();
1048 return;
1049 }
1050 *count += 1;
1051 }
1052
1053 let was_visited = visited.insert(current);
1055
1056 let mut successors: Vec<NodeIndex> = cfg.neighbors(current).collect();
1058 successors.sort_by_key(|n| n.index()); if successors.is_empty() {
1061 let kind = classify_path_precomputed(cfg, current_path, reachable_blocks);
1064 let path = Path::new(current_path.clone(), kind);
1065 paths.push(path);
1066 } else {
1067 for succ in successors {
1068 let is_back_edge = loop_headers.contains(&succ) && loop_iterations.contains_key(&succ);
1071 if visited.contains(&succ) && !is_back_edge {
1072 continue;
1073 }
1074
1075 if is_back_edge {
1077 let count = loop_iterations.get(&succ).copied().unwrap_or(0);
1078 if count >= limits.loop_unroll_limit {
1079 continue; }
1081 }
1082
1083 dfs_enumerate(
1085 cfg,
1086 succ,
1087 exits,
1088 limits,
1089 paths,
1090 current_path,
1091 visited,
1092 loop_headers,
1093 loop_iterations,
1094 reachable_blocks,
1095 );
1096
1097 if paths.len() >= limits.max_paths {
1099 break;
1100 }
1101 }
1102 }
1103
1104 if was_visited {
1106 visited.remove(¤t);
1107 }
1108
1109 if is_loop_header {
1111 loop_iterations.entry(current).and_modify(|c| *c -= 1);
1112 }
1113
1114 current_path.pop();
1116}
1117
1118pub fn enumerate_paths_iterative(cfg: &Cfg, limits: &PathLimits) -> Vec<Path> {
1157 use std::collections::BTreeSet;
1158
1159 let entry = match crate::cfg::analysis::find_entry(cfg) {
1161 Some(e) => e,
1162 None => return vec![],
1163 };
1164
1165 let exits: HashSet<NodeIndex> = crate::cfg::analysis::find_exits(cfg)
1167 .into_iter()
1168 .collect();
1169
1170 if exits.is_empty() {
1171 return vec![]; }
1173
1174 let reachable_nodes = crate::cfg::reachability::find_reachable(cfg);
1176 let reachable_blocks: HashSet<BlockId> = reachable_nodes
1177 .iter()
1178 .map(|&idx| cfg[idx].id)
1179 .collect();
1180 let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
1181
1182 let mut seen_paths: BTreeSet<Vec<BlockId>> = BTreeSet::new();
1185
1186 struct StackFrame {
1188 node: NodeIndex,
1189 path: Vec<BlockId>,
1190 visited: HashSet<NodeIndex>,
1191 loop_iterations: HashMap<NodeIndex, usize>,
1192 }
1193
1194 let mut stack = Vec::new();
1195 let mut paths = Vec::new();
1196
1197 let entry_block_id = cfg[entry].id;
1199 let mut initial_visited = HashSet::new();
1200 initial_visited.insert(entry);
1201
1202 stack.push(StackFrame {
1203 node: entry,
1204 path: vec![entry_block_id],
1205 visited: initial_visited,
1206 loop_iterations: HashMap::new(),
1207 });
1208
1209 while let Some(frame) = stack.pop() {
1210 let StackFrame {
1211 node: current,
1212 path: current_path,
1213 visited: current_visited,
1214 mut loop_iterations,
1215 } = frame;
1216
1217 if current_path.len() > limits.max_length {
1219 continue;
1220 }
1221
1222 if exits.contains(¤t) {
1224 if seen_paths.insert(current_path.clone()) {
1226 let kind = classify_path_precomputed(cfg, ¤t_path, &reachable_blocks);
1227 let path = Path::new(current_path, kind);
1228 paths.push(path);
1229 }
1230 continue;
1231 }
1232
1233 if paths.len() >= limits.max_paths {
1235 break;
1236 }
1237
1238 let mut successors: Vec<NodeIndex> = cfg.neighbors(current).collect();
1240 successors.sort_by_key(|n| n.index()); for succ in successors {
1243 let is_back_edge = loop_headers.contains(&succ)
1246 && loop_iterations.get(&succ).copied().unwrap_or(0) < limits.loop_unroll_limit;
1247
1248 if current_visited.contains(&succ) && !is_back_edge {
1249 continue;
1250 }
1251
1252 if is_back_edge {
1254 let count = loop_iterations.entry(succ).or_insert(0);
1255 if *count >= limits.loop_unroll_limit {
1256 continue;
1257 }
1258 *count += 1;
1259 }
1260
1261 let mut new_path = current_path.clone();
1263 let block_id = cfg[succ].id;
1264 new_path.push(block_id);
1265
1266 let mut new_visited = current_visited.clone();
1268 new_visited.insert(succ);
1269
1270 stack.push(StackFrame {
1272 node: succ,
1273 path: new_path,
1274 visited: new_visited,
1275 loop_iterations: loop_iterations.clone(),
1276 });
1277 }
1278 }
1279
1280 paths
1281}
1282
1283#[derive(Debug, Clone)]
1288pub struct PathEnumerationResult {
1289 pub paths: Vec<Path>,
1291 pub limits_hit: LimitsHit,
1293 pub stats: EnumerationStats,
1295}
1296
1297#[derive(Debug, Clone, PartialEq, Eq)]
1299pub enum LimitsHit {
1300 None,
1302 MaxLength,
1304 MaxPaths,
1306 LoopUnroll,
1308 Multiple,
1310}
1311
1312#[derive(Debug, Clone)]
1314pub struct EnumerationStats {
1315 pub total_paths: usize,
1317 pub normal_paths: usize,
1319 pub error_paths: usize,
1321 pub degenerate_paths: usize,
1323 pub unreachable_paths: usize,
1325 pub avg_path_length: f64,
1327 pub max_path_length: usize,
1329 pub loop_count: usize,
1331}
1332
1333pub fn enumerate_paths_with_metadata(cfg: &Cfg, limits: &PathLimits) -> PathEnumerationResult {
1367 let paths = enumerate_paths_iterative(cfg, limits);
1368
1369 let mut stats = EnumerationStats {
1370 total_paths: paths.len(),
1371 normal_paths: 0,
1372 error_paths: 0,
1373 degenerate_paths: 0,
1374 unreachable_paths: 0,
1375 avg_path_length: 0.0,
1376 max_path_length: 0,
1377 loop_count: 0,
1378 };
1379
1380 if paths.is_empty() {
1381 return PathEnumerationResult {
1382 paths,
1383 limits_hit: LimitsHit::None,
1384 stats,
1385 };
1386 }
1387
1388 let mut total_length = 0;
1389
1390 for path in &paths {
1391 match path.kind {
1392 PathKind::Normal => stats.normal_paths += 1,
1393 PathKind::Error => stats.error_paths += 1,
1394 PathKind::Degenerate => stats.degenerate_paths += 1,
1395 PathKind::Unreachable => stats.unreachable_paths += 1,
1396 }
1397
1398 let len = path.len();
1399 total_length += len;
1400 if len > stats.max_path_length {
1401 stats.max_path_length = len;
1402 }
1403 }
1404
1405 stats.avg_path_length = total_length as f64 / paths.len() as f64;
1406
1407 stats.loop_count = crate::cfg::loops::find_loop_headers(cfg).len();
1409
1410 let limits_hit = if paths.len() >= limits.max_paths {
1412 LimitsHit::MaxPaths
1413 } else if stats.max_path_length >= limits.max_length {
1414 LimitsHit::MaxLength
1415 } else {
1416 LimitsHit::None
1417 };
1418
1419 PathEnumerationResult {
1420 paths,
1421 limits_hit,
1422 stats,
1423 }
1424}
1425
1426pub fn get_or_enumerate_paths(
1472 cfg: &Cfg,
1473 function_id: i64,
1474 function_hash: &str,
1475 limits: &PathLimits,
1476 db_conn: &mut rusqlite::Connection,
1477) -> Result<Vec<Path>, String> {
1478 use crate::storage::paths::{get_cached_paths, invalidate_function_paths, store_paths};
1479
1480 let current_hash: Option<String> = db_conn.query_row(
1482 "SELECT function_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
1483 rusqlite::params![function_id],
1484 |row| row.get(0),
1485 ).unwrap_or(None);
1486
1487 if let Some(ref hash) = current_hash {
1489 if hash == function_hash {
1490 let paths = get_cached_paths(db_conn, function_id)
1492 .map_err(|e| format!("Failed to retrieve cached paths: {}", e))?;
1493 return Ok(paths);
1494 }
1495 }
1496
1497 let paths = enumerate_paths(cfg, limits);
1499
1500 let _ = invalidate_function_paths(db_conn, function_id);
1502
1503 store_paths(db_conn, function_id, &paths)
1505 .map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
1506
1507 Ok(paths)
1511}
1512
1513pub fn enumerate_paths_cached(
1572 cfg: &Cfg,
1573 function_id: i64,
1574 function_hash: &str,
1575 limits: &PathLimits,
1576 db_conn: &mut rusqlite::Connection,
1577) -> Result<Vec<Path>, String> {
1578 use crate::storage::paths::{get_cached_paths, update_function_paths_if_changed};
1579
1580 let current_hash: Option<String> = db_conn.query_row(
1585 "SELECT function_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
1586 rusqlite::params![function_id],
1587 |row| row.get(0),
1588 ).unwrap_or(None);
1589
1590 if let Some(ref hash) = current_hash {
1592 if hash == function_hash {
1593 let paths = get_cached_paths(db_conn, function_id)
1595 .map_err(|e| format!("Failed to retrieve cached paths: {}", e))?;
1596 return Ok(paths);
1597 }
1598 }
1599
1600 let ctx = EnumerationContext::new(cfg);
1602 let paths = enumerate_paths_with_context(cfg, limits, &ctx);
1603
1604 update_function_paths_if_changed(db_conn, function_id, function_hash, &paths)
1606 .map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
1607
1608 Ok(paths)
1609}
1610
1611pub fn enumerate_paths_cached_with_context(
1634 cfg: &Cfg,
1635 function_id: i64,
1636 function_hash: &str,
1637 limits: &PathLimits,
1638 ctx: &EnumerationContext,
1639 db_conn: &mut rusqlite::Connection,
1640) -> Result<Vec<Path>, String> {
1641 use crate::storage::paths::{get_cached_paths, update_function_paths_if_changed};
1642
1643 let current_hash: Option<String> = db_conn.query_row(
1645 "SELECT function_hash FROM cfg_blocks WHERE function_id = ?1 LIMIT 1",
1646 rusqlite::params![function_id],
1647 |row| row.get(0),
1648 ).unwrap_or(None);
1649
1650 if let Some(ref hash) = current_hash {
1651 if hash == function_hash {
1652 return get_cached_paths(db_conn, function_id)
1653 .map_err(|e| format!("Failed to retrieve cached paths: {}", e));
1654 }
1655 }
1656
1657 let paths = enumerate_paths_with_context(cfg, limits, ctx);
1659
1660 update_function_paths_if_changed(db_conn, function_id, function_hash, &paths)
1661 .map_err(|e| format!("Failed to store enumerated paths: {}", e))?;
1662
1663 Ok(paths)
1664}
1665
1666pub fn estimate_path_count(cfg: &Cfg, loop_unroll_limit: usize) -> usize {
1704 let loop_headers = crate::cfg::loops::find_loop_headers(cfg);
1706 let loop_count = loop_headers.len();
1707
1708 let mut branch_count = 0;
1710 for node in cfg.node_indices() {
1711 if loop_headers.contains(&node) {
1712 continue; }
1714 let out_degree = cfg.neighbors_directed(node, petgraph::Direction::Outgoing).count();
1715 if out_degree > 1 {
1716 branch_count += out_degree - 1; }
1718 }
1719
1720 if loop_count == 0 && branch_count == 0 {
1722 return 1; }
1724
1725 let unroll_factor = loop_unroll_limit + 1;
1728
1729 let branch_factor = if branch_count < 31 {
1732 2_usize.pow(branch_count as u32)
1733 } else {
1734 usize::MAX / 2 };
1736
1737 let loop_factor = if loop_count < 31 {
1738 unroll_factor.pow(loop_count as u32)
1739 } else {
1740 usize::MAX / 2 };
1742
1743 branch_factor.saturating_mul(loop_factor)
1745}
1746
1747pub fn check_path_explosion(cfg: &Cfg, limits: &PathLimits) -> Option<usize> {
1758 let estimate = estimate_path_count(cfg, limits.loop_unroll_limit);
1759 if estimate > limits.max_paths {
1760 Some(estimate)
1761 } else {
1762 None
1763 }
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768 use super::*;
1769 use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
1770 use petgraph::graph::DiGraph;
1771
1772 fn create_linear_cfg() -> Cfg {
1774 let mut g = DiGraph::new();
1775
1776 let b0 = g.add_node(BasicBlock {
1777 id: 0,
1778 kind: BlockKind::Entry,
1779 statements: vec![],
1780 terminator: Terminator::Goto { target: 1 },
1781 source_location: None,
1782 });
1783
1784 let b1 = g.add_node(BasicBlock {
1785 id: 1,
1786 kind: BlockKind::Normal,
1787 statements: vec![],
1788 terminator: Terminator::Goto { target: 2 },
1789 source_location: None,
1790 });
1791
1792 let b2 = g.add_node(BasicBlock {
1793 id: 2,
1794 kind: BlockKind::Exit,
1795 statements: vec![],
1796 terminator: Terminator::Return,
1797 source_location: None,
1798 });
1799
1800 g.add_edge(b0, b1, EdgeType::Fallthrough);
1801 g.add_edge(b1, b2, EdgeType::Fallthrough);
1802
1803 g
1804 }
1805
1806 fn create_diamond_cfg() -> Cfg {
1808 let mut g = DiGraph::new();
1809
1810 let b0 = g.add_node(BasicBlock {
1811 id: 0,
1812 kind: BlockKind::Entry,
1813 statements: vec![],
1814 terminator: Terminator::SwitchInt {
1815 targets: vec![1],
1816 otherwise: 2,
1817 },
1818 source_location: None,
1819 });
1820
1821 let b1 = g.add_node(BasicBlock {
1822 id: 1,
1823 kind: BlockKind::Normal,
1824 statements: vec![],
1825 terminator: Terminator::Goto { target: 3 },
1826 source_location: None,
1827 });
1828
1829 let b2 = g.add_node(BasicBlock {
1830 id: 2,
1831 kind: BlockKind::Normal,
1832 statements: vec![],
1833 terminator: Terminator::Goto { target: 3 },
1834 source_location: None,
1835 });
1836
1837 let b3 = g.add_node(BasicBlock {
1838 id: 3,
1839 kind: BlockKind::Exit,
1840 statements: vec![],
1841 terminator: Terminator::Return,
1842 source_location: None,
1843 });
1844
1845 g.add_edge(b0, b1, EdgeType::TrueBranch);
1846 g.add_edge(b0, b2, EdgeType::FalseBranch);
1847 g.add_edge(b1, b3, EdgeType::Fallthrough);
1848 g.add_edge(b2, b3, EdgeType::Fallthrough);
1849
1850 g
1851 }
1852
1853 fn create_loop_cfg() -> Cfg {
1855 let mut g = DiGraph::new();
1856
1857 let b0 = g.add_node(BasicBlock {
1858 id: 0,
1859 kind: BlockKind::Entry,
1860 statements: vec![],
1861 terminator: Terminator::Goto { target: 1 },
1862 source_location: None,
1863 });
1864
1865 let b1 = g.add_node(BasicBlock {
1866 id: 1,
1867 kind: BlockKind::Normal,
1868 statements: vec![],
1869 terminator: Terminator::SwitchInt {
1870 targets: vec![2],
1871 otherwise: 3,
1872 },
1873 source_location: None,
1874 });
1875
1876 let b2 = g.add_node(BasicBlock {
1877 id: 2,
1878 kind: BlockKind::Normal,
1879 statements: vec![],
1880 terminator: Terminator::Goto { target: 1 },
1881 source_location: None,
1882 });
1883
1884 let b3 = g.add_node(BasicBlock {
1885 id: 3,
1886 kind: BlockKind::Exit,
1887 statements: vec![],
1888 terminator: Terminator::Return,
1889 source_location: None,
1890 });
1891
1892 g.add_edge(b0, b1, EdgeType::Fallthrough);
1893 g.add_edge(b1, b2, EdgeType::TrueBranch);
1894 g.add_edge(b1, b3, EdgeType::FalseBranch);
1895 g.add_edge(b2, b1, EdgeType::LoopBack);
1896
1897 g
1898 }
1899
1900 #[test]
1901 fn test_hash_path_deterministic() {
1902 let blocks = vec![0, 1, 2];
1903 let hash1 = hash_path(&blocks);
1904 let hash2 = hash_path(&blocks);
1905
1906 assert_eq!(hash1, hash2, "Same input should produce same hash");
1907 }
1908
1909 #[test]
1910 fn test_hash_path_different_sequences() {
1911 let blocks1 = vec![0, 1, 2];
1912 let blocks2 = vec![0, 2, 1];
1913
1914 assert_ne!(hash_path(&blocks1), hash_path(&blocks2));
1915 }
1916
1917 #[test]
1918 fn test_hash_path_length_collision_protection() {
1919 let blocks1 = vec![1, 2, 3];
1920 let blocks2 = vec![1, 2, 3, 0];
1921
1922 assert_ne!(hash_path(&blocks1), hash_path(&blocks2));
1923 }
1924
1925 #[test]
1926 fn test_path_new() {
1927 let blocks = vec![0, 1, 2];
1928 let path = Path::new(blocks.clone(), PathKind::Normal);
1929
1930 assert_eq!(path.blocks, blocks);
1931 assert_eq!(path.entry, 0);
1932 assert_eq!(path.exit, 2);
1933 assert_eq!(path.kind, PathKind::Normal);
1934 assert!(!path.path_id.is_empty());
1935 }
1936
1937 #[test]
1938 fn test_path_len() {
1939 let blocks = vec![0, 1, 2];
1940 let path = Path::new(blocks, PathKind::Normal);
1941
1942 assert_eq!(path.len(), 3);
1943 assert!(!path.is_empty());
1944 }
1945
1946 #[test]
1947 fn test_path_contains() {
1948 let blocks = vec![0, 1, 2];
1949 let path = Path::new(blocks, PathKind::Normal);
1950
1951 assert!(path.contains(0));
1952 assert!(path.contains(1));
1953 assert!(path.contains(2));
1954 assert!(!path.contains(3));
1955 }
1956
1957 #[test]
1958 fn test_path_limits_default() {
1959 let limits = PathLimits::default();
1960
1961 assert_eq!(limits.max_length, 1000);
1962 assert_eq!(limits.max_paths, 10000);
1963 assert_eq!(limits.loop_unroll_limit, 3);
1964 }
1965
1966 #[test]
1967 fn test_path_limits_custom() {
1968 let limits = PathLimits::new(100, 500, 5);
1969
1970 assert_eq!(limits.max_length, 100);
1971 assert_eq!(limits.max_paths, 500);
1972 assert_eq!(limits.loop_unroll_limit, 5);
1973 }
1974
1975 #[test]
1976 fn test_path_limits_builder() {
1977 let limits = PathLimits::default()
1978 .with_max_length(200)
1979 .with_max_paths(1000)
1980 .with_loop_unroll_limit(10);
1981
1982 assert_eq!(limits.max_length, 200);
1983 assert_eq!(limits.max_paths, 1000);
1984 assert_eq!(limits.loop_unroll_limit, 10);
1985 }
1986
1987 #[test]
1988 fn test_path_kind_is_normal() {
1989 assert!(PathKind::Normal.is_normal());
1990 assert!(!PathKind::Error.is_normal());
1991 assert!(!PathKind::Degenerate.is_normal());
1992 assert!(!PathKind::Unreachable.is_normal());
1993 }
1994
1995 #[test]
1996 fn test_path_kind_is_error() {
1997 assert!(PathKind::Error.is_error());
1998 assert!(!PathKind::Normal.is_error());
1999 }
2000
2001 #[test]
2002 fn test_path_kind_is_degenerate() {
2003 assert!(PathKind::Degenerate.is_degenerate());
2004 assert!(!PathKind::Normal.is_degenerate());
2005 }
2006
2007 #[test]
2008 fn test_path_kind_is_unreachable() {
2009 assert!(PathKind::Unreachable.is_unreachable());
2010 assert!(!PathKind::Normal.is_unreachable());
2011 }
2012
2013 #[test]
2016 fn test_find_node_by_block_id_existing() {
2017 let cfg = create_linear_cfg();
2018
2019 let b0 = find_node_by_block_id(&cfg, 0);
2021 let b1 = find_node_by_block_id(&cfg, 1);
2022 let b2 = find_node_by_block_id(&cfg, 2);
2023
2024 assert!(b0.is_some());
2025 assert!(b1.is_some());
2026 assert!(b2.is_some());
2027
2028 assert_eq!(b0.unwrap().index(), 0);
2030 assert_eq!(b1.unwrap().index(), 1);
2031 assert_eq!(b2.unwrap().index(), 2);
2032 }
2033
2034 #[test]
2035 fn test_find_node_by_block_id_nonexistent() {
2036 let cfg = create_linear_cfg();
2037
2038 let b99 = find_node_by_block_id(&cfg, 99);
2040 assert!(b99.is_none());
2041 }
2042
2043 #[test]
2044 fn test_find_node_by_block_id_empty_cfg() {
2045 let cfg: Cfg = DiGraph::new();
2046
2047 let b0 = find_node_by_block_id(&cfg, 0);
2049 assert!(b0.is_none());
2050 }
2051
2052 fn create_error_cfg() -> Cfg {
2056 let mut g = DiGraph::new();
2057
2058 let b0 = g.add_node(BasicBlock {
2059 id: 0,
2060 kind: BlockKind::Entry,
2061 statements: vec![],
2062 terminator: Terminator::Goto { target: 1 },
2063 source_location: None,
2064 });
2065
2066 let b1 = g.add_node(BasicBlock {
2067 id: 1,
2068 kind: BlockKind::Exit,
2069 statements: vec![],
2070 terminator: Terminator::Abort("panic!".to_string()),
2071 source_location: None,
2072 });
2073
2074 g.add_edge(b0, b1, EdgeType::Fallthrough);
2075
2076 g
2077 }
2078
2079 fn create_unreachable_term_cfg() -> Cfg {
2081 let mut g = DiGraph::new();
2082
2083 let b0 = g.add_node(BasicBlock {
2084 id: 0,
2085 kind: BlockKind::Entry,
2086 statements: vec![],
2087 terminator: Terminator::Goto { target: 1 },
2088 source_location: None,
2089 });
2090
2091 let b1 = g.add_node(BasicBlock {
2092 id: 1,
2093 kind: BlockKind::Exit,
2094 statements: vec![],
2095 terminator: Terminator::Unreachable,
2096 source_location: None,
2097 });
2098
2099 g.add_edge(b0, b1, EdgeType::Fallthrough);
2100
2101 g
2102 }
2103
2104 fn create_dead_code_cfg() -> Cfg {
2106 let mut g = DiGraph::new();
2107
2108 let _b0 = g.add_node(BasicBlock {
2109 id: 0,
2110 kind: BlockKind::Entry,
2111 statements: vec![],
2112 terminator: Terminator::Return,
2113 source_location: None,
2114 });
2115
2116 let _b1 = g.add_node(BasicBlock {
2118 id: 1,
2119 kind: BlockKind::Exit,
2120 statements: vec![],
2121 terminator: Terminator::Return,
2122 source_location: None,
2123 });
2124
2125 g
2126 }
2127
2128 fn create_call_unwind_cfg() -> Cfg {
2130 let mut g = DiGraph::new();
2131
2132 let b0 = g.add_node(BasicBlock {
2133 id: 0,
2134 kind: BlockKind::Entry,
2135 statements: vec![],
2136 terminator: Terminator::Call {
2137 target: Some(1),
2138 unwind: Some(2), },
2140 source_location: None,
2141 });
2142
2143 let b1 = g.add_node(BasicBlock {
2144 id: 1,
2145 kind: BlockKind::Exit,
2146 statements: vec![],
2147 terminator: Terminator::Return,
2148 source_location: None,
2149 });
2150
2151 let _b2 = g.add_node(BasicBlock {
2152 id: 2,
2153 kind: BlockKind::Exit,
2154 statements: vec![],
2155 terminator: Terminator::Return,
2156 source_location: None,
2157 });
2158
2159 g.add_edge(b0, b1, EdgeType::Fallthrough);
2160
2161 g
2162 }
2163
2164 #[test]
2165 fn test_classify_path_normal_return() {
2166 let cfg = create_linear_cfg();
2167 let path = vec![0, 1, 2];
2168
2169 let kind = classify_path(&cfg, &path);
2170 assert_eq!(kind, PathKind::Normal);
2171 }
2172
2173 #[test]
2174 fn test_classify_path_error_abort() {
2175 let cfg = create_error_cfg();
2176 let path = vec![0, 1];
2177
2178 let kind = classify_path(&cfg, &path);
2179 assert_eq!(kind, PathKind::Error);
2180 }
2181
2182 #[test]
2183 fn test_classify_path_degenerate_unreachable_terminator() {
2184 let cfg = create_unreachable_term_cfg();
2185 let path = vec![0, 1];
2186
2187 let kind = classify_path(&cfg, &path);
2188 assert_eq!(kind, PathKind::Degenerate);
2189 }
2190
2191 #[test]
2192 fn test_classify_path_unreachable_block() {
2193 let cfg = create_dead_code_cfg();
2194 let path = vec![1]; let kind = classify_path(&cfg, &path);
2198 assert_eq!(kind, PathKind::Unreachable);
2199 }
2200
2201 #[test]
2202 fn test_classify_path_error_call_unwind() {
2203 let cfg = create_call_unwind_cfg();
2204 let path = vec![0, 1]; let kind = classify_path(&cfg, &path);
2207 assert_eq!(kind, PathKind::Error);
2208 }
2209
2210 #[test]
2211 fn test_classify_path_empty() {
2212 let cfg = create_linear_cfg();
2213 let path: Vec<BlockId> = vec![];
2214
2215 let kind = classify_path(&cfg, &path);
2216 assert_eq!(kind, PathKind::Degenerate);
2217 }
2218
2219 #[test]
2220 fn test_classify_path_single_block() {
2221 let mut g = DiGraph::new();
2222
2223 let _b0 = g.add_node(BasicBlock {
2224 id: 0,
2225 kind: BlockKind::Entry,
2226 statements: vec![],
2227 terminator: Terminator::Return,
2228 source_location: None,
2229 });
2230
2231 let path = vec![0];
2232 let kind = classify_path(&g, &path);
2233 assert_eq!(kind, PathKind::Normal);
2234 }
2235
2236 #[test]
2237 fn test_classify_path_nonexistent_block() {
2238 let cfg = create_linear_cfg();
2239 let path = vec![0, 99]; let kind = classify_path(&cfg, &path);
2242 assert_eq!(kind, PathKind::Degenerate);
2243 }
2244
2245 #[test]
2248 fn test_classify_path_precomputed_matches_classify_path() {
2249 let cfg = create_diamond_cfg();
2250
2251 use crate::cfg::reachability::find_reachable;
2253 let reachable_nodes = find_reachable(&cfg);
2254 let reachable_blocks: HashSet<BlockId> = reachable_nodes
2255 .iter()
2256 .map(|&idx| cfg[idx].id)
2257 .collect();
2258
2259 let test_paths = vec![
2261 vec![0, 1, 3],
2262 vec![0, 2, 3],
2263 vec![0, 1],
2264 vec![0],
2265 ];
2266
2267 for path in test_paths {
2268 let kind1 = classify_path(&cfg, &path);
2269 let kind2 = classify_path_precomputed(&cfg, &path, &reachable_blocks);
2270 assert_eq!(
2271 kind1, kind2,
2272 "classify_path_precomputed should match classify_path for path {:?}",
2273 path
2274 );
2275 }
2276 }
2277
2278 #[test]
2279 fn test_classify_path_precomputed_unreachable() {
2280 let cfg = create_dead_code_cfg();
2281
2282 use crate::cfg::reachability::find_reachable;
2284 let reachable_nodes = find_reachable(&cfg);
2285 let reachable_blocks: HashSet<BlockId> = reachable_nodes
2286 .iter()
2287 .map(|&idx| cfg[idx].id)
2288 .collect();
2289
2290 let path = vec![1];
2292 let kind = classify_path_precomputed(&cfg, &path, &reachable_blocks);
2293 assert_eq!(kind, PathKind::Unreachable);
2294 }
2295
2296 #[test]
2297 fn test_classify_path_precomputed_performance() {
2298 use crate::cfg::reachability::find_reachable;
2299 use std::time::Instant;
2300
2301 let cfg = create_diamond_cfg();
2302
2303 let reachable_nodes = find_reachable(&cfg);
2305 let reachable_blocks: HashSet<BlockId> = reachable_nodes
2306 .iter()
2307 .map(|&idx| cfg[idx].id)
2308 .collect();
2309
2310 let test_paths: Vec<Vec<BlockId>> = (0..1000)
2312 .map(|_| vec![0, 1, 3])
2313 .collect();
2314
2315 let start = Instant::now();
2317 for path in &test_paths {
2318 let _ = classify_path_precomputed(&cfg, path, &reachable_blocks);
2319 }
2320 let precomputed_duration = start.elapsed();
2321
2322 assert!(
2324 precomputed_duration.as_millis() < 10,
2325 "classify_path_precomputed should classify 1000 paths in <10ms, took {}ms",
2326 precomputed_duration.as_millis()
2327 );
2328 }
2329
2330 #[test]
2331 fn test_classify_path_precomputed_all_kinds() {
2332 use crate::cfg::reachability::find_reachable;
2333
2334 let cfg_normal = create_linear_cfg();
2336 let reachable = find_reachable(&cfg_normal)
2337 .iter()
2338 .map(|&idx| cfg_normal[idx].id)
2339 .collect();
2340 assert_eq!(
2341 classify_path_precomputed(&cfg_normal, &[0, 1, 2], &reachable),
2342 PathKind::Normal
2343 );
2344
2345 let cfg_error = create_error_cfg();
2347 let reachable = find_reachable(&cfg_error)
2348 .iter()
2349 .map(|&idx| cfg_error[idx].id)
2350 .collect();
2351 assert_eq!(
2352 classify_path_precomputed(&cfg_error, &[0, 1], &reachable),
2353 PathKind::Error
2354 );
2355
2356 let cfg_degen = create_unreachable_term_cfg();
2358 let reachable = find_reachable(&cfg_degen)
2359 .iter()
2360 .map(|&idx| cfg_degen[idx].id)
2361 .collect();
2362 assert_eq!(
2363 classify_path_precomputed(&cfg_degen, &[0, 1], &reachable),
2364 PathKind::Degenerate
2365 );
2366 }
2367
2368 #[test]
2371 fn test_enumerate_paths_linear_cfg() {
2372 let cfg = create_linear_cfg();
2373 let paths = enumerate_paths(&cfg, &PathLimits::default());
2374
2375 assert_eq!(paths.len(), 1);
2377 assert_eq!(paths[0].blocks, vec![0, 1, 2]);
2378 assert_eq!(paths[0].entry, 0);
2379 assert_eq!(paths[0].exit, 2);
2380 assert_eq!(paths[0].kind, PathKind::Normal);
2381 }
2382
2383 #[test]
2384 fn test_enumerate_paths_diamond_cfg() {
2385 let cfg = create_diamond_cfg();
2386 let paths = enumerate_paths(&cfg, &PathLimits::default());
2387
2388 assert_eq!(paths.len(), 2);
2390
2391 for path in &paths {
2393 assert_eq!(path.entry, 0);
2394 assert_eq!(path.exit, 3);
2395 assert_eq!(path.kind, PathKind::Normal);
2396 }
2397
2398 let path_blocks: Vec<_> = paths.iter().map(|p| p.blocks.clone()).collect();
2400 assert!(path_blocks.contains(&vec![0, 1, 3]));
2401 assert!(path_blocks.contains(&vec![0, 2, 3]));
2402 }
2403
2404 #[test]
2405 fn test_enumerate_paths_loop_with_unroll_limit() {
2406 let cfg = create_loop_cfg();
2407
2408 let limits = PathLimits::default().with_loop_unroll_limit(3);
2415 let paths = enumerate_paths(&cfg, &limits);
2416
2417 assert!(paths.len() >= 2, "Should have at least direct exit and one loop iteration");
2421 assert!(paths.len() <= 5, "Should be bounded by loop unroll limit");
2422
2423 for path in &paths {
2425 assert_eq!(path.kind, PathKind::Normal);
2426 assert_eq!(path.entry, 0);
2427 assert_eq!(path.exit, 3);
2428 }
2429
2430 assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 3]));
2432 }
2433
2434 #[test]
2435 fn test_enumerate_paths_empty_cfg() {
2436 let cfg: Cfg = DiGraph::new();
2437 let paths = enumerate_paths(&cfg, &PathLimits::default());
2438
2439 assert_eq!(paths.len(), 0);
2441 }
2442
2443 #[test]
2444 fn test_enumerate_paths_max_paths_limit() {
2445 let cfg = create_diamond_cfg();
2446
2447 let limits = PathLimits::default().with_max_paths(1);
2449 let paths = enumerate_paths(&cfg, &limits);
2450
2451 assert_eq!(paths.len(), 1);
2453 }
2454
2455 #[test]
2456 fn test_enumerate_paths_max_length_limit() {
2457 let cfg = create_diamond_cfg();
2458
2459 let limits = PathLimits::default().with_max_length(2);
2461 let paths = enumerate_paths(&cfg, &limits);
2462
2463 assert_eq!(paths.len(), 0);
2465 }
2466
2467 #[test]
2468 fn test_enumerate_paths_single_block_cfg() {
2469 let mut g = DiGraph::new();
2470
2471 let _b0 = g.add_node(BasicBlock {
2472 id: 0,
2473 kind: BlockKind::Entry,
2474 statements: vec![],
2475 terminator: Terminator::Return,
2476 source_location: None,
2477 });
2478
2479 let paths = enumerate_paths(&g, &PathLimits::default());
2481
2482 assert_eq!(paths.len(), 1);
2483 assert_eq!(paths[0].blocks, vec![0]);
2484 assert_eq!(paths[0].entry, 0);
2485 assert_eq!(paths[0].exit, 0);
2486 }
2487
2488 #[test]
2489 fn test_enumerate_paths_with_unreachable_exit() {
2490 let mut g = DiGraph::new();
2491
2492 let b0 = g.add_node(BasicBlock {
2494 id: 0,
2495 kind: BlockKind::Entry,
2496 statements: vec![],
2497 terminator: Terminator::Goto { target: 1 },
2498 source_location: None,
2499 });
2500
2501 let b1 = g.add_node(BasicBlock {
2503 id: 1,
2504 kind: BlockKind::Exit,
2505 statements: vec![],
2506 terminator: Terminator::Return,
2507 source_location: None,
2508 });
2509
2510 let _b2 = g.add_node(BasicBlock {
2512 id: 2,
2513 kind: BlockKind::Exit,
2514 statements: vec![],
2515 terminator: Terminator::Unreachable,
2516 source_location: None,
2517 });
2518
2519 g.add_edge(b0, b1, EdgeType::Fallthrough);
2520
2521 let paths = enumerate_paths(&g, &PathLimits::default());
2522
2523 assert_eq!(paths.len(), 1);
2525 assert_eq!(paths[0].blocks, vec![0, 1]);
2526 }
2527
2528 #[test]
2531 fn test_enumerate_paths_classification_diamond() {
2532 let cfg = create_diamond_cfg();
2533 let paths = enumerate_paths(&cfg, &PathLimits::default());
2534
2535 assert_eq!(paths.len(), 2);
2537 for path in &paths {
2538 assert_eq!(
2539 path.kind,
2540 PathKind::Normal,
2541 "Diamond CFG should only have Normal paths, got {:?} for {:?}",
2542 path.kind,
2543 path.blocks
2544 );
2545 }
2546 }
2547
2548 #[test]
2549 fn test_enumerate_paths_classification_with_error() {
2550 let cfg = create_error_cfg();
2552 let paths = enumerate_paths(&cfg, &PathLimits::default());
2553
2554 assert_eq!(paths.len(), 1);
2556 assert_eq!(paths[0].kind, PathKind::Error);
2557 assert_eq!(paths[0].blocks, vec![0, 1]);
2558 }
2559
2560 #[test]
2561 fn test_enumerate_paths_classification_with_unreachable() {
2562 let cfg = create_dead_code_cfg();
2564 let paths = enumerate_paths(&cfg, &PathLimits::default());
2565
2566 assert_eq!(paths.len(), 1);
2568 assert_eq!(paths[0].blocks, vec![0]);
2570 assert_eq!(paths[0].kind, PathKind::Normal);
2571 }
2572
2573 #[test]
2574 fn test_enumerate_paths_classification_mixed() {
2575 let mut g = DiGraph::new();
2577
2578 let b0 = g.add_node(BasicBlock {
2580 id: 0,
2581 kind: BlockKind::Entry,
2582 statements: vec![],
2583 terminator: Terminator::SwitchInt { targets: vec![1], otherwise: 2 },
2584 source_location: None,
2585 });
2586
2587 let b1 = g.add_node(BasicBlock {
2589 id: 1,
2590 kind: BlockKind::Exit,
2591 statements: vec![],
2592 terminator: Terminator::Return,
2593 source_location: None,
2594 });
2595
2596 let b2 = g.add_node(BasicBlock {
2598 id: 2,
2599 kind: BlockKind::Exit,
2600 statements: vec![],
2601 terminator: Terminator::Abort("panic!".to_string()),
2602 source_location: None,
2603 });
2604
2605 g.add_edge(b0, b1, EdgeType::TrueBranch);
2606 g.add_edge(b0, b2, EdgeType::FalseBranch);
2607
2608 let paths = enumerate_paths(&g, &PathLimits::default());
2609
2610 assert_eq!(paths.len(), 2);
2612
2613 let normal_count = paths.iter().filter(|p| p.kind == PathKind::Normal).count();
2614 let error_count = paths.iter().filter(|p| p.kind == PathKind::Error).count();
2615
2616 assert_eq!(normal_count, 1, "Should have 1 Normal path");
2617 assert_eq!(error_count, 1, "Should have 1 Error path");
2618 }
2619
2620 #[test]
2621 fn test_enumerate_paths_classification_correctness() {
2622 let cfg = create_diamond_cfg();
2624 let paths = enumerate_paths(&cfg, &PathLimits::default());
2625
2626 use crate::cfg::reachability::find_reachable;
2628 let reachable_nodes = find_reachable(&cfg);
2629 let reachable_blocks: HashSet<BlockId> = reachable_nodes
2630 .iter()
2631 .map(|&idx| cfg[idx].id)
2632 .collect();
2633
2634 for path in &paths {
2636 let expected_kind = classify_path_precomputed(&cfg, &path.blocks, &reachable_blocks);
2637 assert_eq!(
2638 path.kind, expected_kind,
2639 "Path kind mismatch for {:?}: got {:?}, expected {:?}",
2640 path.blocks, path.kind, expected_kind
2641 );
2642 }
2643 }
2644
2645 #[test]
2648 fn test_path_limits_max_length_long_path() {
2649 let mut g = DiGraph::new();
2651
2652 let b0 = g.add_node(BasicBlock {
2653 id: 0,
2654 kind: BlockKind::Entry,
2655 statements: vec![],
2656 terminator: Terminator::Goto { target: 1 },
2657 source_location: None,
2658 });
2659
2660 let b1 = g.add_node(BasicBlock {
2661 id: 1,
2662 kind: BlockKind::Normal,
2663 statements: vec![],
2664 terminator: Terminator::Goto { target: 2 },
2665 source_location: None,
2666 });
2667
2668 let b2 = g.add_node(BasicBlock {
2669 id: 2,
2670 kind: BlockKind::Normal,
2671 statements: vec![],
2672 terminator: Terminator::Goto { target: 3 },
2673 source_location: None,
2674 });
2675
2676 let b3 = g.add_node(BasicBlock {
2677 id: 3,
2678 kind: BlockKind::Normal,
2679 statements: vec![],
2680 terminator: Terminator::Goto { target: 4 },
2681 source_location: None,
2682 });
2683
2684 let b4 = g.add_node(BasicBlock {
2685 id: 4,
2686 kind: BlockKind::Exit,
2687 statements: vec![],
2688 terminator: Terminator::Return,
2689 source_location: None,
2690 });
2691
2692 g.add_edge(b0, b1, EdgeType::Fallthrough);
2693 g.add_edge(b1, b2, EdgeType::Fallthrough);
2694 g.add_edge(b2, b3, EdgeType::Fallthrough);
2695 g.add_edge(b3, b4, EdgeType::Fallthrough);
2696
2697 let limits = PathLimits::default().with_max_length(3);
2699 let paths = enumerate_paths(&g, &limits);
2700
2701 assert_eq!(paths.len(), 0, "Path exceeds max_length, should return 0 paths");
2703 }
2704
2705 #[test]
2706 fn test_path_limits_max_paths_exact() {
2707 let cfg = create_diamond_cfg();
2708
2709 let limits = PathLimits::default().with_max_paths(1);
2711 let paths = enumerate_paths(&cfg, &limits);
2712
2713 assert_eq!(paths.len(), 1, "Should stop at max_paths=1");
2715
2716 assert_eq!(paths[0].entry, 0);
2718 assert_eq!(paths[0].exit, 3);
2719 }
2720
2721 #[test]
2722 fn test_path_limits_loop_unroll_exact() {
2723 let cfg = create_loop_cfg();
2724
2725 let limits = PathLimits::default().with_loop_unroll_limit(1);
2729 let paths = enumerate_paths(&cfg, &limits);
2730
2731 assert_eq!(paths.len(), 1, "Should have exactly 1 path with loop_unroll_limit=1");
2735
2736 assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 3]),
2738 "Direct exit path should exist");
2739 }
2740
2741 #[test]
2742 fn test_path_limits_loop_unroll_limit_2() {
2743 let cfg = create_loop_cfg();
2744
2745 let limits = PathLimits::default().with_loop_unroll_limit(2);
2751 let paths = enumerate_paths(&cfg, &limits);
2752
2753 assert_eq!(paths.len(), 2, "Should have exactly 2 paths with loop_unroll_limit=2");
2755
2756 assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 3]),
2758 "Direct exit path should exist");
2759
2760 assert!(paths.iter().any(|p| p.blocks == vec![0, 1, 2, 1, 3]),
2762 "One iteration path should exist");
2763 }
2764
2765 fn create_self_loop_cfg() -> Cfg {
2769 let mut g = DiGraph::new();
2770
2771 let b0 = g.add_node(BasicBlock {
2772 id: 0,
2773 kind: BlockKind::Entry,
2774 statements: vec![],
2775 terminator: Terminator::Goto { target: 1 },
2776 source_location: None,
2777 });
2778
2779 let b1 = g.add_node(BasicBlock {
2780 id: 1,
2781 kind: BlockKind::Normal,
2782 statements: vec![],
2783 terminator: Terminator::Goto { target: 1 },
2785 source_location: None,
2786 });
2787
2788 g.add_edge(b0, b1, EdgeType::Fallthrough);
2789
2790 g
2791 }
2792
2793 #[test]
2794 fn test_self_loop_terminates() {
2795 let cfg = create_self_loop_cfg();
2796
2797 let limits = PathLimits::default();
2799 let paths = enumerate_paths(&cfg, &limits);
2800
2801 assert!(paths.len() <= limits.loop_unroll_limit + 1,
2804 "Self-loop should be bounded by loop_unroll_limit");
2805 }
2806
2807 #[test]
2808 fn test_self_loop_with_low_limit() {
2809 let cfg = create_self_loop_cfg();
2810
2811 let limits = PathLimits::default().with_loop_unroll_limit(1);
2813 let paths = enumerate_paths(&cfg, &limits);
2814
2815 assert!(paths.len() <= 2, "Self-loop with low limit should have few paths");
2817 }
2818
2819 fn create_nested_loop_cfg() -> Cfg {
2827 let mut g = DiGraph::new();
2828
2829 let b0 = g.add_node(BasicBlock {
2830 id: 0,
2831 kind: BlockKind::Entry,
2832 statements: vec![],
2833 terminator: Terminator::Goto { target: 1 },
2834 source_location: None,
2835 });
2836
2837 let b1 = g.add_node(BasicBlock {
2838 id: 1,
2839 kind: BlockKind::Normal,
2840 statements: vec![],
2841 terminator: Terminator::SwitchInt { targets: vec![2], otherwise: 4 },
2842 source_location: None,
2843 });
2844
2845 let b2 = g.add_node(BasicBlock {
2846 id: 2,
2847 kind: BlockKind::Normal,
2848 statements: vec![],
2849 terminator: Terminator::SwitchInt { targets: vec![3], otherwise: 1 },
2850 source_location: None,
2851 });
2852
2853 let b3 = g.add_node(BasicBlock {
2854 id: 3,
2855 kind: BlockKind::Normal,
2856 statements: vec![],
2857 terminator: Terminator::Goto { target: 2 },
2858 source_location: None,
2859 });
2860
2861 let b4 = g.add_node(BasicBlock {
2862 id: 4,
2863 kind: BlockKind::Exit,
2864 statements: vec![],
2865 terminator: Terminator::Return,
2866 source_location: None,
2867 });
2868
2869 g.add_edge(b0, b1, EdgeType::Fallthrough);
2870 g.add_edge(b1, b2, EdgeType::TrueBranch);
2871 g.add_edge(b1, b4, EdgeType::FalseBranch);
2872 g.add_edge(b2, b3, EdgeType::TrueBranch);
2873 g.add_edge(b2, b1, EdgeType::LoopBack); g.add_edge(b3, b2, EdgeType::LoopBack); g
2877 }
2878
2879 #[test]
2880 fn test_nested_loop_bounding() {
2881 let cfg = create_nested_loop_cfg();
2882
2883 let limits = PathLimits::default().with_loop_unroll_limit(2);
2886 let paths = enumerate_paths(&cfg, &limits);
2887
2888 assert!(paths.len() <= 9, "Nested loops should be bounded: got {} paths", paths.len());
2891 assert!(paths.len() > 0, "Should have at least some paths");
2892 }
2893
2894 #[test]
2895 fn test_nested_loop_bounding_three_levels() {
2896 let mut g = DiGraph::new();
2898
2899 let b0 = g.add_node(BasicBlock {
2900 id: 0,
2901 kind: BlockKind::Entry,
2902 statements: vec![],
2903 terminator: Terminator::Goto { target: 1 },
2904 source_location: None,
2905 });
2906
2907 let b1 = g.add_node(BasicBlock {
2909 id: 1,
2910 kind: BlockKind::Normal,
2911 statements: vec![],
2912 terminator: Terminator::SwitchInt { targets: vec![2], otherwise: 6 },
2913 source_location: None,
2914 });
2915
2916 let b2 = g.add_node(BasicBlock {
2918 id: 2,
2919 kind: BlockKind::Normal,
2920 statements: vec![],
2921 terminator: Terminator::SwitchInt { targets: vec![3], otherwise: 1 },
2922 source_location: None,
2923 });
2924
2925 let b3 = g.add_node(BasicBlock {
2927 id: 3,
2928 kind: BlockKind::Normal,
2929 statements: vec![],
2930 terminator: Terminator::SwitchInt { targets: vec![4], otherwise: 2 },
2931 source_location: None,
2932 });
2933
2934 let b4 = g.add_node(BasicBlock {
2935 id: 4,
2936 kind: BlockKind::Normal,
2937 statements: vec![],
2938 terminator: Terminator::Goto { target: 3 },
2939 source_location: None,
2940 });
2941
2942 let b6 = g.add_node(BasicBlock {
2943 id: 6,
2944 kind: BlockKind::Exit,
2945 statements: vec![],
2946 terminator: Terminator::Return,
2947 source_location: None,
2948 });
2949
2950 g.add_edge(b0, b1, EdgeType::Fallthrough);
2951 g.add_edge(b1, b2, EdgeType::TrueBranch);
2952 g.add_edge(b1, b6, EdgeType::FalseBranch);
2953 g.add_edge(b2, b3, EdgeType::TrueBranch);
2954 g.add_edge(b2, b1, EdgeType::LoopBack); g.add_edge(b3, b4, EdgeType::TrueBranch);
2956 g.add_edge(b3, b2, EdgeType::LoopBack); g.add_edge(b4, b3, EdgeType::LoopBack); let limits = PathLimits::default().with_loop_unroll_limit(2);
2962 let paths = enumerate_paths(&g, &limits);
2963
2964 assert!(paths.len() <= 27, "3-level nested loops should be bounded by 27");
2965 }
2966
2967 #[test]
2968 fn test_nested_loop_independent_counters() {
2969 let cfg = create_nested_loop_cfg();
2970
2971 let loop_headers = crate::cfg::loops::find_loop_headers(&cfg);
2973
2974 assert_eq!(loop_headers.len(), 2, "Should detect 2 loop headers");
2976
2977 let limits = PathLimits::default().with_loop_unroll_limit(2);
2979 let paths = enumerate_paths(&cfg, &limits);
2980
2981 assert!(paths.len() > 0, "Should have some paths");
2983 assert!(paths.len() <= 9, "Should be bounded by (limit+1)^nesting_depth");
2984 }
2985
2986 #[test]
2989 fn test_path_limits_quick_analysis() {
2990 let limits = PathLimits::quick_analysis();
2991
2992 assert_eq!(limits.max_length, 100);
2993 assert_eq!(limits.max_paths, 1000);
2994 assert_eq!(limits.loop_unroll_limit, 2);
2995 }
2996
2997 #[test]
2998 fn test_path_limits_thorough() {
2999 let limits = PathLimits::thorough();
3000
3001 assert_eq!(limits.max_length, 10000);
3002 assert_eq!(limits.max_paths, 100000);
3003 assert_eq!(limits.loop_unroll_limit, 5);
3004 }
3005
3006 #[test]
3007 fn test_path_limits_builder_chaining() {
3008 let limits = PathLimits::quick_analysis()
3010 .with_max_length(200)
3011 .with_max_paths(5000)
3012 .with_loop_unroll_limit(3);
3013
3014 assert_eq!(limits.max_length, 200);
3015 assert_eq!(limits.max_paths, 5000);
3016 assert_eq!(limits.loop_unroll_limit, 3);
3017 }
3018
3019 #[test]
3020 fn test_path_limits_presets_differ_from_default() {
3021 let default = PathLimits::default();
3022 let quick = PathLimits::quick_analysis();
3023 let thorough = PathLimits::thorough();
3024
3025 assert!(quick.max_length < default.max_length);
3027 assert!(quick.max_paths < default.max_paths);
3028 assert!(quick.loop_unroll_limit < default.loop_unroll_limit);
3029
3030 assert!(thorough.max_length > default.max_length);
3032 assert!(thorough.max_paths > default.max_paths);
3033 assert!(thorough.loop_unroll_limit > default.loop_unroll_limit);
3034 }
3035
3036 #[test]
3037 fn test_path_limits_quick_vs_thorough_on_loop() {
3038 let cfg = create_loop_cfg();
3039
3040 let quick_paths = enumerate_paths(&cfg, &PathLimits::quick_analysis());
3042 let thorough_paths = enumerate_paths(&cfg, &PathLimits::thorough());
3043
3044 assert!(thorough_paths.len() >= quick_paths.len(),
3046 "Thorough analysis should find at least as many paths as quick");
3047 }
3048
3049 #[test]
3052 fn test_is_feasible_path_empty_path() {
3053 let cfg = create_linear_cfg();
3054 let empty_path: Vec<BlockId> = vec![];
3055
3056 assert!(!is_feasible_path(&cfg, &empty_path),
3057 "Empty path should be infeasible");
3058 }
3059
3060 #[test]
3061 fn test_is_feasible_path_non_entry_first_block() {
3062 let cfg = create_diamond_cfg();
3063 let path = vec![1, 3];
3065
3066 assert!(!is_feasible_path(&cfg, &path),
3067 "Path starting from non-entry block should be infeasible");
3068 }
3069
3070 #[test]
3071 fn test_is_feasible_path_dead_end_goto() {
3072 let cfg = create_linear_cfg();
3073 let path = vec![0, 1]; assert!(!is_feasible_path(&cfg, &path),
3077 "Path ending in Goto should be infeasible (dead end)");
3078 }
3079
3080 #[test]
3081 fn test_is_feasible_path_valid_return() {
3082 let cfg = create_linear_cfg();
3083 let path = vec![0, 1, 2];
3085
3086 assert!(is_feasible_path(&cfg, &path),
3087 "Complete path ending in Return should be feasible");
3088 }
3089
3090 #[test]
3091 fn test_is_feasible_path_abort_is_feasible() {
3092 let cfg = create_error_cfg();
3093 let path = vec![0, 1];
3095
3096 assert!(is_feasible_path(&cfg, &path),
3097 "Path ending in Abort should be feasible (error path but reachable)");
3098 }
3099
3100 #[test]
3101 fn test_is_feasible_path_unreachable_terminator() {
3102 let cfg = create_unreachable_term_cfg();
3103 let path = vec![0, 1];
3105
3106 assert!(!is_feasible_path(&cfg, &path),
3107 "Path ending in Unreachable should be infeasible");
3108 }
3109
3110 #[test]
3111 fn test_is_feasible_path_nonexistent_block() {
3112 let cfg = create_linear_cfg();
3113 let path = vec![0, 99]; assert!(!is_feasible_path(&cfg, &path),
3117 "Path with nonexistent block should be infeasible");
3118 }
3119
3120 #[test]
3121 fn test_is_feasible_path_switch_int_dead_end() {
3122 let cfg = create_diamond_cfg();
3123 let path = vec![0]; assert!(!is_feasible_path(&cfg, &path),
3127 "Path ending in SwitchInt should be infeasible (dead end)");
3128 }
3129
3130 #[test]
3131 fn test_is_feasible_path_complete_diamond() {
3132 let cfg = create_diamond_cfg();
3133 let path1 = vec![0, 1, 3]; let path2 = vec![0, 2, 3]; assert!(is_feasible_path(&cfg, &path1),
3138 "Complete diamond path 0->1->3 should be feasible");
3139 assert!(is_feasible_path(&cfg, &path2),
3140 "Complete diamond path 0->2->3 should be feasible");
3141 }
3142
3143 #[test]
3144 fn test_is_feasible_path_call_no_unwind() {
3145 let mut g = DiGraph::new();
3146
3147 let b0 = g.add_node(BasicBlock {
3148 id: 0,
3149 kind: BlockKind::Entry,
3150 statements: vec![],
3151 terminator: Terminator::Call {
3152 target: Some(1),
3153 unwind: None,
3154 },
3155 source_location: None,
3156 });
3157
3158 let b1 = g.add_node(BasicBlock {
3159 id: 1,
3160 kind: BlockKind::Exit,
3161 statements: vec![],
3162 terminator: Terminator::Return,
3163 source_location: None,
3164 });
3165
3166 g.add_edge(b0, b1, EdgeType::Fallthrough);
3167
3168 let path = vec![0];
3170
3171 assert!(is_feasible_path(&g, &path),
3172 "Path ending in Call with no unwind should be feasible");
3173 }
3174
3175 #[test]
3176 fn test_is_feasible_path_call_with_unwind_and_target() {
3177 let cfg = create_call_unwind_cfg();
3178 let path = vec![0]; assert!(is_feasible_path(&cfg, &path),
3182 "Path ending in Call with both unwind and target should be feasible");
3183 }
3184
3185 #[test]
3186 fn test_is_feasible_path_call_always_unwinds() {
3187 let mut g = DiGraph::new();
3188
3189 let b0 = g.add_node(BasicBlock {
3190 id: 0,
3191 kind: BlockKind::Entry,
3192 statements: vec![],
3193 terminator: Terminator::Call {
3194 target: None, unwind: Some(1),
3196 },
3197 source_location: None,
3198 });
3199
3200 let b1 = g.add_node(BasicBlock {
3201 id: 1,
3202 kind: BlockKind::Exit,
3203 statements: vec![],
3204 terminator: Terminator::Return,
3205 source_location: None,
3206 });
3207
3208 g.add_edge(b0, b1, EdgeType::Fallthrough);
3209
3210 let path = vec![0];
3212
3213 assert!(!is_feasible_path(&g, &path),
3214 "Path ending in Call with only unwind (no target) should be infeasible");
3215 }
3216
3217 #[test]
3220 fn test_is_feasible_path_precomputed_matches_basic() {
3221 let cfg = create_diamond_cfg();
3222
3223 use crate::cfg::reachability::find_reachable;
3225 let reachable_nodes = find_reachable(&cfg);
3226 let reachable_blocks: HashSet<BlockId> = reachable_nodes
3227 .iter()
3228 .map(|&idx| cfg[idx].id)
3229 .collect();
3230
3231 let test_paths = vec![
3233 vec![0, 1, 3], vec![0, 2, 3], vec![0, 1], vec![], ];
3238
3239 for path in test_paths {
3240 let basic = is_feasible_path(&cfg, &path);
3241 let precomputed = is_feasible_path_precomputed(&cfg, &path, &reachable_blocks);
3242 assert_eq!(
3243 basic, precomputed,
3244 "is_feasible_path_precomputed should match is_feasible_path for {:?}",
3245 path
3246 );
3247 }
3248 }
3249
3250 #[test]
3251 fn test_is_feasible_path_precomputed_unreachable_block() {
3252 let cfg = create_dead_code_cfg();
3253
3254 use crate::cfg::reachability::find_reachable;
3256 let reachable_nodes = find_reachable(&cfg);
3257 let reachable_blocks: HashSet<BlockId> = reachable_nodes
3258 .iter()
3259 .map(|&idx| cfg[idx].id)
3260 .collect();
3261
3262 let path = vec![1]; assert!(!is_feasible_path_precomputed(&cfg, &path, &reachable_blocks),
3266 "Path with unreachable block should be infeasible");
3267 }
3268
3269 #[test]
3270 fn test_is_feasible_path_precomputed_performance() {
3271 use crate::cfg::reachability::find_reachable;
3272 use std::time::Instant;
3273
3274 let cfg = create_diamond_cfg();
3275
3276 let reachable_nodes = find_reachable(&cfg);
3278 let reachable_blocks: HashSet<BlockId> = reachable_nodes
3279 .iter()
3280 .map(|&idx| cfg[idx].id)
3281 .collect();
3282
3283 let test_paths: Vec<Vec<BlockId>> = (0..1000)
3285 .map(|_| vec![0, 1, 3])
3286 .collect();
3287
3288 let start = Instant::now();
3290 for path in &test_paths {
3291 let _ = is_feasible_path_precomputed(&cfg, path, &reachable_blocks);
3292 }
3293 let precomputed_duration = start.elapsed();
3294
3295 assert!(
3297 precomputed_duration.as_millis() < 5,
3298 "is_feasible_path_precomputed should check 1000 paths in <5ms, took {}ms",
3299 precomputed_duration.as_millis()
3300 );
3301 }
3302
3303 #[test]
3304 fn test_is_feasible_path_precomputed_all_criteria() {
3305 use crate::cfg::reachability::find_reachable;
3306
3307 let cfg_normal = create_linear_cfg();
3309 let reachable_normal = find_reachable(&cfg_normal)
3310 .iter()
3311 .map(|&idx| cfg_normal[idx].id)
3312 .collect();
3313 assert!(
3314 is_feasible_path_precomputed(&cfg_normal, &[0, 1, 2], &reachable_normal),
3315 "Complete linear path should be feasible"
3316 );
3317
3318 let cfg_error = create_error_cfg();
3320 let reachable_error = find_reachable(&cfg_error)
3321 .iter()
3322 .map(|&idx| cfg_error[idx].id)
3323 .collect();
3324 assert!(
3325 is_feasible_path_precomputed(&cfg_error, &[0, 1], &reachable_error),
3326 "Path ending in Abort should be feasible (error path but reachable)"
3327 );
3328
3329 let cfg_degen = create_unreachable_term_cfg();
3331 let reachable_degen = find_reachable(&cfg_degen)
3332 .iter()
3333 .map(|&idx| cfg_degen[idx].id)
3334 .collect();
3335 assert!(
3336 !is_feasible_path_precomputed(&cfg_degen, &[0, 1], &reachable_degen),
3337 "Path ending in Unreachable should be infeasible"
3338 );
3339 }
3340
3341 #[test]
3344 fn test_classify_with_feasibility_dead_end() {
3345 use crate::cfg::reachability::find_reachable;
3346
3347 let cfg = create_linear_cfg();
3348 let reachable = find_reachable(&cfg)
3349 .iter()
3350 .map(|&idx| cfg[idx].id)
3351 .collect();
3352
3353 let path = vec![0, 1]; let kind = classify_path_precomputed(&cfg, &path, &reachable);
3356 assert_eq!(kind, PathKind::Degenerate,
3357 "Path ending in Goto should be Degenerate");
3358 }
3359
3360 #[test]
3361 fn test_classify_with_feasibility_valid_exit() {
3362 use crate::cfg::reachability::find_reachable;
3363
3364 let cfg = create_linear_cfg();
3365 let reachable = find_reachable(&cfg)
3366 .iter()
3367 .map(|&idx| cfg[idx].id)
3368 .collect();
3369
3370 let path = vec![0, 1, 2];
3372 let kind = classify_path_precomputed(&cfg, &path, &reachable);
3373 assert_eq!(kind, PathKind::Normal,
3374 "Complete path with Return should be Normal");
3375 }
3376
3377 #[test]
3378 fn test_classify_with_feasibility_error_path() {
3379 use crate::cfg::reachability::find_reachable;
3380
3381 let cfg = create_error_cfg();
3382 let reachable = find_reachable(&cfg)
3383 .iter()
3384 .map(|&idx| cfg[idx].id)
3385 .collect();
3386
3387 let path = vec![0, 1];
3389 let kind = classify_path_precomputed(&cfg, &path, &reachable);
3390 assert_eq!(kind, PathKind::Error,
3391 "Path ending in Abort should be Error");
3392 }
3393
3394 #[test]
3395 fn test_classify_with_feasibility_switch_int_dead_end() {
3396 use crate::cfg::reachability::find_reachable;
3397
3398 let cfg = create_diamond_cfg();
3399 let reachable = find_reachable(&cfg)
3400 .iter()
3401 .map(|&idx| cfg[idx].id)
3402 .collect();
3403
3404 let path = vec![0]; let kind = classify_path_precomputed(&cfg, &path, &reachable);
3407 assert_eq!(kind, PathKind::Degenerate,
3408 "Path ending in SwitchInt should be Degenerate");
3409 }
3410
3411 #[test]
3412 fn test_classify_with_feasibility_priority_order() {
3413 use crate::cfg::reachability::find_reachable;
3414
3415 let cfg = create_dead_code_cfg();
3417 let reachable = find_reachable(&cfg)
3418 .iter()
3419 .map(|&idx| cfg[idx].id)
3420 .collect();
3421
3422 let path = vec![1];
3424 let kind = classify_path_precomputed(&cfg, &path, &reachable);
3425 assert_eq!(kind, PathKind::Unreachable,
3426 "Unreachable should be prioritized over feasibility");
3427 }
3428
3429 #[test]
3430 fn test_classify_with_feasibility_complete_paths() {
3431 use crate::cfg::reachability::find_reachable;
3432
3433 let cfg = create_diamond_cfg();
3434 let reachable = find_reachable(&cfg)
3435 .iter()
3436 .map(|&idx| cfg[idx].id)
3437 .collect();
3438
3439 let path1 = vec![0, 1, 3];
3441 let path2 = vec![0, 2, 3];
3442
3443 assert_eq!(classify_path_precomputed(&cfg, &path1, &reachable), PathKind::Normal,
3444 "Complete diamond path 0->1->3 should be Normal");
3445 assert_eq!(classify_path_precomputed(&cfg, &path2, &reachable), PathKind::Normal,
3446 "Complete diamond path 0->2->3 should be Normal");
3447 }
3448
3449 #[test]
3450 fn test_classify_with_feasibility_call_terminator() {
3451 use crate::cfg::reachability::find_reachable;
3452
3453 let mut g = DiGraph::new();
3454
3455 let b0 = g.add_node(BasicBlock {
3456 id: 0,
3457 kind: BlockKind::Entry,
3458 statements: vec![],
3459 terminator: Terminator::Call {
3460 target: Some(1),
3461 unwind: None,
3462 },
3463 source_location: None,
3464 });
3465
3466 let b1 = g.add_node(BasicBlock {
3467 id: 1,
3468 kind: BlockKind::Exit,
3469 statements: vec![],
3470 terminator: Terminator::Return,
3471 source_location: None,
3472 });
3473
3474 g.add_edge(b0, b1, EdgeType::Fallthrough);
3475
3476 let reachable = find_reachable(&g)
3477 .iter()
3478 .map(|&idx| g[idx].id)
3479 .collect();
3480
3481 let path = vec![0];
3483 let kind = classify_path_precomputed(&g, &path, &reachable);
3484 assert_eq!(kind, PathKind::Normal,
3485 "Path ending in Call with target should be Normal");
3486 }
3487
3488 fn create_conflicting_conditions_cfg() -> Cfg {
3497 let mut g = DiGraph::new();
3498
3499 let b0 = g.add_node(BasicBlock {
3501 id: 0,
3502 kind: BlockKind::Entry,
3503 statements: vec![],
3504 terminator: Terminator::SwitchInt { targets: vec![1], otherwise: 2 },
3505 source_location: None,
3506 });
3507
3508 let b1 = g.add_node(BasicBlock {
3510 id: 1,
3511 kind: BlockKind::Normal,
3512 statements: vec![],
3513 terminator: Terminator::SwitchInt { targets: vec![3], otherwise: 3 },
3514 source_location: None,
3515 });
3516
3517 let b2 = g.add_node(BasicBlock {
3519 id: 2,
3520 kind: BlockKind::Exit,
3521 statements: vec![],
3522 terminator: Terminator::Return,
3523 source_location: None,
3524 });
3525
3526 let b3 = g.add_node(BasicBlock {
3528 id: 3,
3529 kind: BlockKind::Exit,
3530 statements: vec![],
3531 terminator: Terminator::Return,
3532 source_location: None,
3533 });
3534
3535 g.add_edge(b0, b1, EdgeType::TrueBranch);
3536 g.add_edge(b0, b2, EdgeType::FalseBranch);
3537 g.add_edge(b1, b3, EdgeType::Fallthrough);
3538
3539 g
3540 }
3541
3542 #[test]
3543 fn test_feasibility_limitation_conflicting_conditions() {
3544 use crate::cfg::reachability::find_reachable;
3545
3546 let cfg = create_conflicting_conditions_cfg();
3547 let reachable = find_reachable(&cfg)
3548 .iter()
3549 .map(|&idx| cfg[idx].id)
3550 .collect();
3551
3552 let path = vec![0, 1, 3];
3556
3557 assert!(is_feasible_path_precomputed(&cfg, &path, &reachable),
3559 "Static analysis marks conflicting path as feasible (limitation)");
3560
3561 assert_eq!(classify_path_precomputed(&cfg, &path, &reachable),
3563 PathKind::Normal,
3564 "Conflicting path is classified as Normal (static limitation)");
3565
3566 }
3569
3570 #[test]
3571 fn test_feasibility_documentation_accuracy() {
3572 use crate::cfg::reachability::find_reachable;
3573
3574 let cfg = create_linear_cfg();
3577 let reachable = find_reachable(&cfg)
3578 .iter()
3579 .map(|&idx| cfg[idx].id)
3580 .collect();
3581
3582 let non_entry_path = vec![1, 2];
3584 assert!(!is_feasible_path_precomputed(&cfg, &non_entry_path, &reachable),
3585 "Entry check works as documented");
3586
3587 let dead_end_path = vec![0, 1];
3589 assert!(!is_feasible_path_precomputed(&cfg, &dead_end_path, &reachable),
3590 "Dead-end detection works as documented");
3591
3592 assert!(is_feasible_path_precomputed(&cfg, &[0, 1, 2], &reachable),
3594 "Reachable path is feasible as documented");
3595 }
3596
3597 #[test]
3600 fn test_get_or_enumerate_paths_cache_miss_enumerates() {
3601 use crate::storage::create_schema;
3602
3603 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
3605
3606 conn.execute(
3608 "CREATE TABLE magellan_meta (
3609 id INTEGER PRIMARY KEY CHECK (id = 1),
3610 magellan_schema_version INTEGER NOT NULL,
3611 sqlitegraph_schema_version INTEGER NOT NULL,
3612 created_at INTEGER NOT NULL
3613 )",
3614 [],
3615 ).unwrap();
3616
3617 conn.execute(
3618 "CREATE TABLE graph_entities (
3619 id INTEGER PRIMARY KEY AUTOINCREMENT,
3620 kind TEXT NOT NULL,
3621 name TEXT NOT NULL,
3622 file_path TEXT,
3623 data TEXT NOT NULL
3624 )",
3625 [],
3626 ).unwrap();
3627
3628 conn.execute(
3629 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3630 VALUES (1, 4, 3, 0)",
3631 [],
3632 ).unwrap();
3633
3634 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3636
3637 conn.execute(
3639 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3640 rusqlite::params!("function", "test_func", "test.rs", "{}"),
3641 ).unwrap();
3642 let function_id: i64 = 1;
3643
3644 let cfg = create_linear_cfg();
3646 let function_hash = "test_hash_123";
3647 let limits = PathLimits::default();
3648
3649 let paths = get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
3651
3652 assert_eq!(paths.len(), 1);
3654 assert_eq!(paths[0].blocks, vec![0, 1, 2]);
3655 }
3656
3657 #[test]
3658 fn test_get_or_enumerate_paths_cache_hit_retrieves() {
3659 use crate::storage::create_schema;
3660
3661 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
3662
3663 conn.execute(
3665 "CREATE TABLE magellan_meta (
3666 id INTEGER PRIMARY KEY CHECK (id = 1),
3667 magellan_schema_version INTEGER NOT NULL,
3668 sqlitegraph_schema_version INTEGER NOT NULL,
3669 created_at INTEGER NOT NULL
3670 )",
3671 [],
3672 ).unwrap();
3673
3674 conn.execute(
3675 "CREATE TABLE graph_entities (
3676 id INTEGER PRIMARY KEY AUTOINCREMENT,
3677 kind TEXT NOT NULL,
3678 name TEXT NOT NULL,
3679 file_path TEXT,
3680 data TEXT NOT NULL
3681 )",
3682 [],
3683 ).unwrap();
3684
3685 conn.execute(
3686 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3687 VALUES (1, 4, 3, 0)",
3688 [],
3689 ).unwrap();
3690
3691 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3692
3693 conn.execute(
3694 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3695 rusqlite::params!("function", "test_func", "test.rs", "{}"),
3696 ).unwrap();
3697 let function_id: i64 = 1;
3698
3699 let cfg = create_linear_cfg();
3700 let function_hash = "test_hash_456";
3701 let limits = PathLimits::default();
3702
3703 let paths1 = get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
3705 assert_eq!(paths1.len(), 1);
3706
3707 let paths2 = get_or_enumerate_paths(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
3709 assert_eq!(paths2.len(), 1);
3710 assert_eq!(paths2[0].blocks, vec![0, 1, 2]);
3711 }
3712
3713 #[test]
3714 fn test_get_or_enumerate_paths_hash_change_invalidates() {
3715 use crate::storage::create_schema;
3716
3717 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
3718
3719 conn.execute(
3721 "CREATE TABLE magellan_meta (
3722 id INTEGER PRIMARY KEY CHECK (id = 1),
3723 magellan_schema_version INTEGER NOT NULL,
3724 sqlitegraph_schema_version INTEGER NOT NULL,
3725 created_at INTEGER NOT NULL
3726 )",
3727 [],
3728 ).unwrap();
3729
3730 conn.execute(
3731 "CREATE TABLE graph_entities (
3732 id INTEGER PRIMARY KEY AUTOINCREMENT,
3733 kind TEXT NOT NULL,
3734 name TEXT NOT NULL,
3735 file_path TEXT,
3736 data TEXT NOT NULL
3737 )",
3738 [],
3739 ).unwrap();
3740
3741 conn.execute(
3742 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3743 VALUES (1, 4, 3, 0)",
3744 [],
3745 ).unwrap();
3746
3747 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
3748
3749 conn.execute(
3750 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
3751 rusqlite::params!("function", "test_func", "test.rs", "{}"),
3752 ).unwrap();
3753 let function_id: i64 = 1;
3754
3755 let cfg = create_linear_cfg();
3756 let hash1 = "test_hash_v1";
3757 let hash2 = "test_hash_v3";
3758 let limits = PathLimits::default();
3759
3760 let paths1 = get_or_enumerate_paths(&cfg, function_id, hash1, &limits, &mut conn).unwrap();
3762 assert_eq!(paths1.len(), 1);
3763
3764 let paths2 = get_or_enumerate_paths(&cfg, function_id, hash2, &limits, &mut conn).unwrap();
3766 assert_eq!(paths2.len(), 1);
3767
3768 assert_eq!(paths1[0].blocks, paths2[0].blocks);
3770 }
3771
3772 #[test]
3775 fn test_enumeration_context_new() {
3776 use super::super::EnumerationContext;
3777
3778 let cfg = create_linear_cfg();
3779 let ctx = EnumerationContext::new(&cfg);
3780
3781 assert_eq!(ctx.reachable_count(), 3);
3783 assert_eq!(ctx.loop_count(), 0);
3784 assert_eq!(ctx.exit_count(), 1);
3785 }
3786
3787 #[test]
3788 fn test_enumeration_context_with_loop() {
3789 use super::super::EnumerationContext;
3790
3791 let cfg = create_loop_cfg();
3792 let ctx = EnumerationContext::new(&cfg);
3793
3794 assert_eq!(ctx.loop_count(), 1);
3796 assert!(ctx.reachable_count() > 0);
3797 assert!(ctx.exit_count() > 0);
3798 }
3799
3800 #[test]
3801 fn test_enumeration_context_diamond_cfg() {
3802 use super::super::EnumerationContext;
3803
3804 let cfg = create_diamond_cfg();
3805 let ctx = EnumerationContext::new(&cfg);
3806
3807 assert_eq!(ctx.loop_count(), 0);
3809 assert_eq!(ctx.reachable_count(), 4); assert_eq!(ctx.exit_count(), 1); }
3812
3813 #[test]
3814 fn test_enumeration_context_is_reachable() {
3815 use super::super::EnumerationContext;
3816
3817 let cfg = create_dead_code_cfg();
3818 let ctx = EnumerationContext::new(&cfg);
3819
3820 assert!(ctx.is_reachable(0));
3822 assert!(!ctx.is_reachable(1));
3824 }
3825
3826 #[test]
3827 fn test_enumeration_context_is_loop_header() {
3828 use super::super::EnumerationContext;
3829 use petgraph::graph::NodeIndex;
3830
3831 let cfg = create_loop_cfg();
3832 let ctx = EnumerationContext::new(&cfg);
3833
3834 assert!(ctx.is_loop_header(NodeIndex::new(1)));
3836 assert!(!ctx.is_loop_header(NodeIndex::new(0)));
3838 }
3839
3840 #[test]
3841 fn test_enumeration_context_is_exit() {
3842 use super::super::EnumerationContext;
3843 use petgraph::graph::NodeIndex;
3844
3845 let cfg = create_diamond_cfg();
3846 let ctx = EnumerationContext::new(&cfg);
3847
3848 assert!(ctx.is_exit(NodeIndex::new(3)));
3850 assert!(!ctx.is_exit(NodeIndex::new(0)));
3852 }
3853
3854 #[test]
3855 fn test_enumerate_paths_with_context_matches_basic() {
3856 use super::super::{enumerate_paths, enumerate_paths_with_context, EnumerationContext};
3857
3858 let cfg = create_diamond_cfg();
3859 let limits = PathLimits::default();
3860 let ctx = EnumerationContext::new(&cfg);
3861
3862 let paths_basic = enumerate_paths(&cfg, &limits);
3864 let paths_context = enumerate_paths_with_context(&cfg, &limits, &ctx);
3865
3866 assert_eq!(paths_basic.len(), paths_context.len());
3867
3868 let mut sorted_basic: Vec<_> = paths_basic.iter().collect();
3870 let mut sorted_context: Vec<_> = paths_context.iter().collect();
3871 sorted_basic.sort_by_key(|p| p.blocks.clone());
3872 sorted_context.sort_by_key(|p| p.blocks.clone());
3873
3874 for (basic, context) in sorted_basic.iter().zip(sorted_context.iter()) {
3875 assert_eq!(basic.blocks, context.blocks);
3876 assert_eq!(basic.kind, context.kind);
3877 }
3878 }
3879
3880 #[test]
3881 fn test_enumerate_paths_with_context_linear_cfg() {
3882 use super::super::{enumerate_paths_with_context, EnumerationContext};
3883
3884 let cfg = create_linear_cfg();
3885 let limits = PathLimits::default();
3886 let ctx = EnumerationContext::new(&cfg);
3887
3888 let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
3889
3890 assert_eq!(paths.len(), 1);
3891 assert_eq!(paths[0].blocks, vec![0, 1, 2]);
3892 }
3893
3894 #[test]
3895 fn test_enumerate_paths_with_context_with_loop() {
3896 use super::super::{enumerate_paths_with_context, EnumerationContext};
3897
3898 let cfg = create_loop_cfg();
3899 let limits = PathLimits::default();
3900 let ctx = EnumerationContext::new(&cfg);
3901
3902 let paths = enumerate_paths_with_context(&cfg, &limits, &ctx);
3903
3904 assert!(paths.len() > 0);
3906 assert!(paths.len() <= 4); }
3908
3909 #[test]
3910 fn test_enumerate_paths_with_context_performance() {
3911 use super::super::{enumerate_paths, enumerate_paths_with_context, EnumerationContext};
3912 use std::time::Instant;
3913
3914 let cfg = create_diamond_cfg();
3915 let limits = PathLimits::default();
3916
3917 let start = Instant::now();
3919 let _paths1 = enumerate_paths(&cfg, &limits);
3920 let basic_time = start.elapsed();
3921
3922 let ctx_start = Instant::now();
3924 let ctx = EnumerationContext::new(&cfg);
3925 let ctx_creation_time = ctx_start.elapsed();
3926
3927 let start = Instant::now();
3928 let _paths2 = enumerate_paths_with_context(&cfg, &limits, &ctx);
3929 let context_time = start.elapsed();
3930
3931 println!("Basic: {:?}, Context creation: {:?}, Context enum: {:?}",
3935 basic_time, ctx_creation_time, context_time);
3936
3937 let start = Instant::now();
3939 let _paths3 = enumerate_paths_with_context(&cfg, &limits, &ctx);
3940 let context_time2 = start.elapsed();
3941
3942 println!("Second context call: {:?}", context_time2);
3943
3944 assert!(context_time2.as_millis() < 100);
3946 }
3947
3948 #[test]
3949 fn test_enumerate_paths_with_context_multiple_calls() {
3950 use super::super::{enumerate_paths_with_context, EnumerationContext};
3951
3952 let cfg = create_diamond_cfg();
3953 let ctx = EnumerationContext::new(&cfg);
3954
3955 let limits1 = PathLimits::default().with_max_paths(10);
3957 let limits2 = PathLimits::default().with_max_paths(100);
3958
3959 let paths1 = enumerate_paths_with_context(&cfg, &limits1, &ctx);
3960 let paths2 = enumerate_paths_with_context(&cfg, &limits2, &ctx);
3961
3962 assert_eq!(paths1.len(), paths2.len());
3964 }
3965
3966 #[test]
3969 fn test_enumerate_paths_cached_cache_miss_enumerates() {
3970 use crate::storage::create_schema;
3971 use super::super::enumerate_paths_cached;
3972
3973 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
3974
3975 conn.execute(
3977 "CREATE TABLE magellan_meta (
3978 id INTEGER PRIMARY KEY CHECK (id = 1),
3979 magellan_schema_version INTEGER NOT NULL,
3980 sqlitegraph_schema_version INTEGER NOT NULL,
3981 created_at INTEGER NOT NULL
3982 )",
3983 [],
3984 ).unwrap();
3985
3986 conn.execute(
3987 "CREATE TABLE graph_entities (
3988 id INTEGER PRIMARY KEY AUTOINCREMENT,
3989 kind TEXT NOT NULL,
3990 name TEXT NOT NULL,
3991 file_path TEXT,
3992 data TEXT NOT NULL
3993 )",
3994 [],
3995 ).unwrap();
3996
3997 conn.execute(
3998 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
3999 VALUES (1, 4, 3, 0)",
4000 [],
4001 ).unwrap();
4002
4003 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4004
4005 conn.execute(
4006 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
4007 rusqlite::params!("function", "test_func", "test.rs", "{}"),
4008 ).unwrap();
4009 let function_id: i64 = 1;
4010
4011 let cfg = create_linear_cfg();
4012 let function_hash = "test_hash_123";
4013 let limits = PathLimits::default();
4014
4015 let paths = enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4017
4018 assert_eq!(paths.len(), 1);
4019 assert_eq!(paths[0].blocks, vec![0, 1, 2]);
4020 }
4021
4022 #[test]
4023 fn test_enumerate_paths_cached_cache_hit_retrieves() {
4024 use crate::storage::create_schema;
4025 use super::super::enumerate_paths_cached;
4026
4027 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
4028
4029 conn.execute(
4031 "CREATE TABLE magellan_meta (
4032 id INTEGER PRIMARY KEY CHECK (id = 1),
4033 magellan_schema_version INTEGER NOT NULL,
4034 sqlitegraph_schema_version INTEGER NOT NULL,
4035 created_at INTEGER NOT NULL
4036 )",
4037 [],
4038 ).unwrap();
4039
4040 conn.execute(
4041 "CREATE TABLE graph_entities (
4042 id INTEGER PRIMARY KEY AUTOINCREMENT,
4043 kind TEXT NOT NULL,
4044 name TEXT NOT NULL,
4045 file_path TEXT,
4046 data TEXT NOT NULL
4047 )",
4048 [],
4049 ).unwrap();
4050
4051 conn.execute(
4052 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
4053 VALUES (1, 4, 3, 0)",
4054 [],
4055 ).unwrap();
4056
4057 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4058
4059 conn.execute(
4060 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
4061 rusqlite::params!("function", "test_func", "test.rs", "{}"),
4062 ).unwrap();
4063 let function_id: i64 = 1;
4064
4065 let cfg = create_linear_cfg();
4066 let function_hash = "test_hash_456";
4067 let limits = PathLimits::default();
4068
4069 let paths1 = enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4071 assert_eq!(paths1.len(), 1);
4072
4073 let paths2 = enumerate_paths_cached(&cfg, function_id, function_hash, &limits, &mut conn).unwrap();
4075 assert_eq!(paths2.len(), 1);
4076 assert_eq!(paths2[0].blocks, vec![0, 1, 2]);
4077 }
4078
4079 #[test]
4080 fn test_enumerate_paths_cached_hash_change_invalidates() {
4081 use crate::storage::create_schema;
4082 use super::super::enumerate_paths_cached;
4083
4084 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
4085
4086 conn.execute(
4088 "CREATE TABLE magellan_meta (
4089 id INTEGER PRIMARY KEY CHECK (id = 1),
4090 magellan_schema_version INTEGER NOT NULL,
4091 sqlitegraph_schema_version INTEGER NOT NULL,
4092 created_at INTEGER NOT NULL
4093 )",
4094 [],
4095 ).unwrap();
4096
4097 conn.execute(
4098 "CREATE TABLE graph_entities (
4099 id INTEGER PRIMARY KEY AUTOINCREMENT,
4100 kind TEXT NOT NULL,
4101 name TEXT NOT NULL,
4102 file_path TEXT,
4103 data TEXT NOT NULL
4104 )",
4105 [],
4106 ).unwrap();
4107
4108 conn.execute(
4109 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
4110 VALUES (1, 4, 3, 0)",
4111 [],
4112 ).unwrap();
4113
4114 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4115
4116 conn.execute(
4117 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
4118 rusqlite::params!("function", "test_func", "test.rs", "{}"),
4119 ).unwrap();
4120 let function_id: i64 = 1;
4121
4122 let cfg = create_linear_cfg();
4123 let hash1 = "test_hash_v1";
4124 let hash2 = "test_hash_v3";
4125 let limits = PathLimits::default();
4126
4127 let paths1 = enumerate_paths_cached(&cfg, function_id, hash1, &limits, &mut conn).unwrap();
4129 assert_eq!(paths1.len(), 1);
4130
4131 let paths2 = enumerate_paths_cached(&cfg, function_id, hash2, &limits, &mut conn).unwrap();
4133 assert_eq!(paths2.len(), 1);
4134
4135 assert_eq!(paths1[0].blocks, paths2[0].blocks);
4137 }
4138
4139 #[test]
4140 fn test_enumerate_paths_cached_with_context() {
4141 use crate::storage::create_schema;
4142 use super::super::{enumerate_paths_cached_with_context, EnumerationContext};
4143
4144 let mut conn = rusqlite::Connection::open_in_memory().unwrap();
4145
4146 conn.execute(
4148 "CREATE TABLE magellan_meta (
4149 id INTEGER PRIMARY KEY CHECK (id = 1),
4150 magellan_schema_version INTEGER NOT NULL,
4151 sqlitegraph_schema_version INTEGER NOT NULL,
4152 created_at INTEGER NOT NULL
4153 )",
4154 [],
4155 ).unwrap();
4156
4157 conn.execute(
4158 "CREATE TABLE graph_entities (
4159 id INTEGER PRIMARY KEY AUTOINCREMENT,
4160 kind TEXT NOT NULL,
4161 name TEXT NOT NULL,
4162 file_path TEXT,
4163 data TEXT NOT NULL
4164 )",
4165 [],
4166 ).unwrap();
4167
4168 conn.execute(
4169 "INSERT INTO magellan_meta (id, magellan_schema_version, sqlitegraph_schema_version, created_at)
4170 VALUES (1, 4, 3, 0)",
4171 [],
4172 ).unwrap();
4173
4174 create_schema(&mut conn, crate::storage::TEST_MAGELLAN_SCHEMA_VERSION).unwrap();
4175
4176 conn.execute(
4177 "INSERT INTO graph_entities (kind, name, file_path, data) VALUES (?, ?, ?, ?)",
4178 rusqlite::params!("function", "test_func", "test.rs", "{}"),
4179 ).unwrap();
4180 let function_id: i64 = 1;
4181
4182 let cfg = create_diamond_cfg();
4183 let function_hash = "test_hash_ctx";
4184 let limits = PathLimits::default();
4185 let ctx = EnumerationContext::new(&cfg);
4186
4187 let paths1 = enumerate_paths_cached_with_context(
4189 &cfg, function_id, function_hash, &limits, &ctx, &mut conn
4190 ).unwrap();
4191 assert_eq!(paths1.len(), 2); let paths2 = enumerate_paths_cached_with_context(
4195 &cfg, function_id, function_hash, &limits, &ctx, &mut conn
4196 ).unwrap();
4197 assert_eq!(paths2.len(), 2);
4198 }
4199
4200 #[test]
4203 fn test_estimate_path_count_linear_cfg() {
4204 let cfg = create_linear_cfg();
4206 let estimate = estimate_path_count(&cfg, 3);
4207
4208 assert_eq!(estimate, 1);
4210 }
4211
4212 #[test]
4213 fn test_estimate_path_count_diamond_cfg() {
4214 let cfg = create_diamond_cfg();
4216 let estimate = estimate_path_count(&cfg, 3);
4217
4218 assert_eq!(estimate, 2);
4221 }
4222
4223 #[test]
4224 fn test_estimate_path_count_single_loop() {
4225 let cfg = create_loop_cfg();
4227 let estimate = estimate_path_count(&cfg, 3);
4228
4229 assert_eq!(estimate, 4);
4231 }
4232
4233 #[test]
4234 fn test_estimate_path_count_loop_formula() {
4235 let cfg = create_loop_cfg();
4237
4238 let estimate = estimate_path_count(&cfg, 3);
4241 assert!(estimate >= 4); }
4243
4244 #[test]
4245 fn test_estimate_path_count_increases_with_loop_limit() {
4246 let cfg = create_loop_cfg();
4247
4248 let estimate3 = estimate_path_count(&cfg, 3);
4249 let estimate5 = estimate_path_count(&cfg, 5);
4250
4251 assert!(estimate5 >= estimate3);
4253 }
4254
4255 #[test]
4256 fn test_estimate_path_count_monotonic_with_complexity() {
4257 let linear = create_linear_cfg();
4259 let diamond = create_diamond_cfg();
4260 let loop_cfg = create_loop_cfg();
4261
4262 let linear_estimate = estimate_path_count(&linear, 3);
4263 let diamond_estimate = estimate_path_count(&diamond, 3);
4264 let _loop_estimate = estimate_path_count(&loop_cfg, 3);
4265
4266 assert!(linear_estimate <= diamond_estimate);
4268 }
4270
4271 #[test]
4272 fn test_check_path_explosion_safe() {
4273 let cfg = create_linear_cfg();
4274 let limits = PathLimits::default();
4275
4276 let result = check_path_explosion(&cfg, &limits);
4278 assert!(result.is_none(), "Linear CFG should be safe");
4279 }
4280
4281 #[test]
4282 fn test_check_path_explosion_exceeds_limit() {
4283 let cfg = create_diamond_cfg();
4284 let limits = PathLimits {
4285 max_length: 100,
4286 max_paths: 1, loop_unroll_limit: 3,
4288 };
4289
4290 let result = check_path_explosion(&cfg, &limits);
4292 assert!(result.is_some(), "Should warn about path explosion");
4294 if let Some(estimate) = result {
4295 assert!(estimate > 1);
4296 }
4297 }
4298
4299 #[test]
4300 fn test_estimate_path_count_no_overflow() {
4301 let cfg = create_loop_cfg();
4303
4304 let estimate = estimate_path_count(&cfg, 1000);
4306
4307 assert!(estimate > 0);
4309 assert!(estimate < usize::MAX);
4310 }
4311
4312 fn create_large_linear_cfg(size: usize) -> Cfg {
4316 let mut g = DiGraph::new();
4317
4318 for i in 0..size {
4319 let kind = if i == 0 {
4320 BlockKind::Entry
4321 } else if i == size - 1 {
4322 BlockKind::Exit
4323 } else {
4324 BlockKind::Normal
4325 };
4326
4327 let terminator = if i == size - 1 {
4328 Terminator::Return
4329 } else {
4330 Terminator::Goto { target: i + 1 }
4331 };
4332
4333 let _node = g.add_node(BasicBlock {
4334 id: i,
4335 kind,
4336 statements: vec![],
4337 terminator,
4338 source_location: None,
4339 });
4340 }
4341
4342 for i in 0..size - 1 {
4344 let from = NodeIndex::new(i);
4345 let to = NodeIndex::new(i + 1);
4346 g.add_edge(from, to, EdgeType::Fallthrough);
4347 }
4348
4349 g
4350 }
4351
4352 fn create_large_diamond_cfg() -> Cfg {
4354 let mut g = DiGraph::new();
4355
4356 let mut nodes = Vec::new();
4358
4359 for i in 0..21 {
4360 let kind = if i == 0 {
4361 BlockKind::Entry
4362 } else if i % 2 == 0 && i > 0 {
4363 BlockKind::Normal
4365 } else if i == 20 {
4366 BlockKind::Exit
4367 } else {
4368 BlockKind::Normal
4369 };
4370
4371 let terminator = if i == 20 {
4372 Terminator::Return
4373 } else if i % 2 == 0 {
4374 let target1 = i + 1;
4376 let target2 = i + 2;
4377 Terminator::SwitchInt { targets: vec![target1], otherwise: target2 }
4378 } else {
4379 let merge = i + 1;
4381 Terminator::Goto { target: merge }
4382 };
4383
4384 let node = g.add_node(BasicBlock {
4385 id: i,
4386 kind,
4387 statements: vec![],
4388 terminator,
4389 source_location: None,
4390 });
4391 nodes.push(node);
4392 }
4393
4394 for i in (0..20).step_by(2) {
4396 let from = nodes[i];
4397 let to1 = nodes[i + 1];
4398 let to2 = nodes[i + 2];
4399 g.add_edge(from, to1, EdgeType::TrueBranch);
4400 g.add_edge(from, to2, EdgeType::FalseBranch);
4401 }
4402
4403 for i in (1..20).filter(|x| x % 2 == 1) {
4405 let from = nodes[i];
4406 let to = nodes[i + 1];
4407 g.add_edge(from, to, EdgeType::Fallthrough);
4408 }
4409
4410 g
4411 }
4412
4413 #[test]
4414 #[ignore = "benchmark test - run with cargo test -- --ignored"]
4415 fn test_perf_linear_cfg_10_blocks() {
4416 use std::time::Instant;
4417
4418 let cfg = create_large_linear_cfg(10);
4419 let limits = PathLimits::default();
4420
4421 let start = Instant::now();
4422 let paths = enumerate_paths(&cfg, &limits);
4423 let duration = start.elapsed();
4424
4425 assert_eq!(paths.len(), 1);
4426 assert!(duration < Duration::from_millis(10),
4427 "Linear 10-block CFG should be <10ms, took {:?}", duration);
4428 println!("Linear 10 blocks: {:?}", duration);
4429 }
4430
4431 #[test]
4432 #[ignore = "benchmark test - run with cargo test -- --ignored"]
4433 fn test_perf_linear_cfg_100_blocks() {
4434 use std::time::Instant;
4435
4436 let cfg = create_large_linear_cfg(100);
4437 let limits = PathLimits::default();
4438
4439 let start = Instant::now();
4440 let paths = enumerate_paths(&cfg, &limits);
4441 let duration = start.elapsed();
4442
4443 assert_eq!(paths.len(), 1);
4444 assert!(duration < Duration::from_millis(100),
4445 "Linear 100-block CFG should be <100ms, took {:?}", duration);
4446 println!("Linear 100 blocks: {:?}", duration);
4447 }
4448
4449 #[test]
4450 #[ignore = "benchmark test - run with cargo test -- --ignored"]
4451 fn test_perf_diamond_cfg_10_branches() {
4452 use std::time::Instant;
4453
4454 let cfg = create_large_diamond_cfg();
4455 let limits = PathLimits::default();
4456
4457 let start = Instant::now();
4458 let paths = enumerate_paths(&cfg, &limits);
4459 let duration = start.elapsed();
4460
4461 assert!(paths.len() > 0);
4463 assert!(duration < Duration::from_millis(50),
4464 "Diamond CFG should be <50ms, took {:?}", duration);
4465 println!("Diamond 10 branches: {} paths in {:?}", paths.len(), duration);
4466 }
4467
4468 #[test]
4469 #[ignore = "benchmark test - run with cargo test -- --ignored"]
4470 fn test_perf_single_loop_unroll_3() {
4471 use std::time::Instant;
4472
4473 let cfg = create_loop_cfg();
4474 let limits = PathLimits::default().with_loop_unroll_limit(3);
4475
4476 let start = Instant::now();
4477 let paths = enumerate_paths(&cfg, &limits);
4478 let duration = start.elapsed();
4479
4480 assert!(paths.len() > 0);
4481 assert!(duration < Duration::from_millis(100),
4482 "Single loop should be <100ms, took {:?}", duration);
4483 println!("Single loop (unroll=3): {} paths in {:?}", paths.len(), duration);
4484 }
4485
4486 #[test]
4487 #[ignore = "benchmark test - run with cargo test -- --ignored"]
4488 fn test_perf_nested_loops() {
4489 use std::time::Instant;
4490 use crate::cfg::{BasicBlock, BlockKind, EdgeType, Terminator};
4491
4492 let mut g = DiGraph::new();
4493
4494 let b0 = g.add_node(BasicBlock {
4496 id: 0,
4497 kind: BlockKind::Entry,
4498 statements: vec![],
4499 terminator: Terminator::Goto { target: 1 },
4500 source_location: None,
4501 });
4502
4503 let b1 = g.add_node(BasicBlock {
4504 id: 1,
4505 kind: BlockKind::Normal,
4506 statements: vec![],
4507 terminator: Terminator::SwitchInt { targets: vec![2], otherwise: 5 },
4508 source_location: None,
4509 });
4510
4511 let b2 = g.add_node(BasicBlock {
4512 id: 2,
4513 kind: BlockKind::Normal,
4514 statements: vec![],
4515 terminator: Terminator::SwitchInt { targets: vec![3], otherwise: 1 },
4516 source_location: None,
4517 });
4518
4519 let b3 = g.add_node(BasicBlock {
4520 id: 3,
4521 kind: BlockKind::Normal,
4522 statements: vec![],
4523 terminator: Terminator::Goto { target: 2 },
4524 source_location: None,
4525 });
4526
4527 let b5 = g.add_node(BasicBlock {
4528 id: 5,
4529 kind: BlockKind::Exit,
4530 statements: vec![],
4531 terminator: Terminator::Return,
4532 source_location: None,
4533 });
4534
4535 g.add_edge(b0, b1, EdgeType::Fallthrough);
4536 g.add_edge(b1, b2, EdgeType::TrueBranch);
4537 g.add_edge(b1, b5, EdgeType::FalseBranch);
4538 g.add_edge(b2, b3, EdgeType::TrueBranch);
4539 g.add_edge(b2, b1, EdgeType::LoopBack);
4540 g.add_edge(b3, b2, EdgeType::LoopBack);
4541
4542 let limits = PathLimits::default().with_loop_unroll_limit(2);
4543
4544 let start = Instant::now();
4545 let paths = enumerate_paths(&g, &limits);
4546 let duration = start.elapsed();
4547
4548 assert!(paths.len() > 0);
4549 assert!(duration < Duration::from_millis(500),
4550 "Nested loops should be <500ms, took {:?}", duration);
4551 println!("Nested 2 loops (unroll=2): {} paths in {:?}", paths.len(), duration);
4552 }
4553
4554 #[test]
4555 #[ignore = "benchmark test - run with cargo test -- --ignored"]
4556 fn test_perf_enumeration_context_reuse() {
4557 use std::time::Instant;
4558 use super::super::EnumerationContext;
4559
4560 let cfg = create_diamond_cfg();
4561 let ctx = EnumerationContext::new(&cfg);
4562
4563 let limits = PathLimits::default();
4565
4566 let start = Instant::now();
4567 for _ in 0..100 {
4568 let _ = enumerate_paths_with_context(&cfg, &limits, &ctx);
4569 }
4570 let duration = start.elapsed();
4571
4572 println!("100 calls with context: {:?}", duration);
4573 assert!(duration < Duration::from_millis(100),
4574 "100 cached calls should be <100ms, took {:?}", duration);
4575 }
4576
4577 #[test]
4578 #[ignore = "benchmark test - run with cargo test -- --ignored"]
4579 fn test_perf_estimation_vs_actual() {
4580 use std::time::Instant;
4581
4582 let cfg = create_diamond_cfg();
4583
4584 let start = Instant::now();
4586 let estimate = estimate_path_count(&cfg, 3);
4587 let est_duration = start.elapsed();
4588
4589 let start = Instant::now();
4591 let limits = PathLimits::default();
4592 let paths = enumerate_paths(&cfg, &limits);
4593 let enum_duration = start.elapsed();
4594
4595 println!("Estimation: {} paths in {:?}", estimate, est_duration);
4596 println!("Enumeration: {} paths in {:?}", paths.len(), enum_duration);
4597
4598 assert!(est_duration.as_micros() < 1000,
4600 "Estimation should be fast: {:?}", est_duration);
4601 assert!(enum_duration.as_micros() < 1000,
4602 "Enumeration should be fast: {:?}", enum_duration);
4603 }
4604}
4605
4606#[derive(Debug, Clone)]
4611pub struct IncrementalPathsResult {
4612 pub analyzed_functions: usize,
4614 pub skipped_functions: usize,
4616 pub paths: Vec<Path>,
4618}
4619
4620pub fn enumerate_paths_incremental(
4659 function_id: &str,
4660 backend: &crate::storage::MirageDb,
4661 repo_path: &std::path::Path,
4662 since_revision: &str,
4663 max_length: Option<usize>,
4664) -> anyhow::Result<IncrementalPathsResult> {
4665 use crate::cfg::{load_cfg_from_db, PathLimits};
4666
4667 if function_id != "all" {
4669 let fid = function_id.parse::<i64>()
4670 .map_err(|_| anyhow::anyhow!("Invalid function ID: {}", function_id))?;
4671
4672 let cfg = load_cfg_from_db(backend, fid)?;
4673 let limits = PathLimits {
4674 max_length: max_length.unwrap_or(1000),
4675 ..Default::default()
4676 };
4677 let paths = enumerate_paths(&cfg, &limits);
4678
4679 return Ok(IncrementalPathsResult {
4680 analyzed_functions: 1,
4681 skipped_functions: 0,
4682 paths,
4683 });
4684 }
4685
4686 let changed_function_ids = crate::cfg::git_utils::get_changed_functions(
4688 backend,
4689 repo_path,
4690 since_revision,
4691 )?;
4692
4693 if changed_function_ids.is_empty() {
4694 return Ok(IncrementalPathsResult {
4695 analyzed_functions: 0,
4696 skipped_functions: 0, paths: vec![],
4698 });
4699 }
4700
4701 let mut all_paths = Vec::new();
4703 let mut analyzed = 0;
4704
4705 for fid in changed_function_ids {
4706 match load_cfg_from_db(backend, fid) {
4707 Ok(cfg) => {
4708 let limits = PathLimits {
4709 max_length: max_length.unwrap_or(1000),
4710 ..Default::default()
4711 };
4712 let paths = enumerate_paths(&cfg, &limits);
4713 all_paths.extend(paths);
4714 analyzed += 1;
4715 }
4716 Err(e) => {
4717 tracing::warn!("Failed to load CFG for function {}: {}", fid, e);
4718 }
4720 }
4721 }
4722
4723 Ok(IncrementalPathsResult {
4724 analyzed_functions: analyzed,
4725 skipped_functions: 0, paths: all_paths,
4727 })
4728}
4729
4730#[cfg(test)]
4733mod iterative_tests {
4734 use super::*;
4735 use crate::cfg::{BlockKind, BasicBlock, Cfg, EdgeType, Terminator};
4736 use petgraph::graph::DiGraph;
4737
4738 fn create_simple_diamond_cfg() -> Cfg {
4739 let mut g = DiGraph::new();
4740
4741 let b0 = g.add_node(BasicBlock {
4742 id: 0,
4743 kind: BlockKind::Entry,
4744 statements: vec![],
4745 terminator: Terminator::SwitchInt {
4746 targets: vec![1],
4747 otherwise: 2,
4748 },
4749 source_location: None,
4750 });
4751
4752 let b1 = g.add_node(BasicBlock {
4753 id: 1,
4754 kind: BlockKind::Normal,
4755 statements: vec![],
4756 terminator: Terminator::Goto { target: 3 },
4757 source_location: None,
4758 });
4759
4760 let b2 = g.add_node(BasicBlock {
4761 id: 2,
4762 kind: BlockKind::Normal,
4763 statements: vec![],
4764 terminator: Terminator::Goto { target: 3 },
4765 source_location: None,
4766 });
4767
4768 let b3 = g.add_node(BasicBlock {
4769 id: 3,
4770 kind: BlockKind::Exit,
4771 statements: vec![],
4772 terminator: Terminator::Return,
4773 source_location: None,
4774 });
4775
4776 g.add_edge(b0, b1, EdgeType::TrueBranch);
4777 g.add_edge(b0, b2, EdgeType::FalseBranch);
4778 g.add_edge(b1, b3, EdgeType::Fallthrough);
4779 g.add_edge(b2, b3, EdgeType::Fallthrough);
4780
4781 g
4782 }
4783
4784 #[test]
4785 fn test_enumerate_paths_iterative_diamond_cfg() {
4786 let cfg = create_simple_diamond_cfg();
4787 let limits = PathLimits::default();
4788 let paths = enumerate_paths_iterative(&cfg, &limits);
4789
4790 assert_eq!(paths.len(), 2);
4792
4793 let path1 = &paths[0];
4795 let path2 = &paths[1];
4796
4797 assert_ne!(path1.blocks, path2.blocks);
4798
4799 assert_eq!(path1.entry, 0);
4801 assert_eq!(path1.exit, 3);
4802 assert_eq!(path2.entry, 0);
4803 assert_eq!(path2.exit, 3);
4804 }
4805
4806 #[test]
4807 fn test_enumerate_paths_iterative_produces_same_results_as_recursive() {
4808 let cfg = create_simple_diamond_cfg();
4809 let limits = PathLimits::default();
4810
4811 let recursive_paths = enumerate_paths(&cfg, &limits);
4812 let iterative_paths = enumerate_paths_iterative(&cfg, &limits);
4813
4814 assert_eq!(recursive_paths.len(), iterative_paths.len());
4816
4817 let recursive_set: std::collections::HashSet<Vec<_>> = recursive_paths
4819 .iter()
4820 .map(|p| p.blocks.clone())
4821 .collect();
4822 let iterative_set: std::collections::HashSet<Vec<_>> = iterative_paths
4823 .iter()
4824 .map(|p| p.blocks.clone())
4825 .collect();
4826
4827 assert_eq!(recursive_set, iterative_set);
4828 }
4829
4830 #[test]
4831 fn test_enumerate_paths_with_metadata_diamond() {
4832 let cfg = create_simple_diamond_cfg();
4833 let result = enumerate_paths_with_metadata(&cfg, &PathLimits::default());
4834
4835 assert_eq!(result.stats.total_paths, 2);
4837
4838 assert_eq!(result.stats.normal_paths, 2);
4840 assert_eq!(result.stats.error_paths, 0);
4841
4842 assert_eq!(result.stats.avg_path_length, 3.0);
4844
4845 assert_eq!(result.stats.max_path_length, 3);
4847
4848 assert_eq!(result.limits_hit, LimitsHit::None);
4850 }
4851
4852 #[test]
4853 fn test_enumerate_paths_with_metadata_limits_hit() {
4854 let cfg = create_simple_diamond_cfg();
4855 let limits = PathLimits {
4856 max_paths: 1,
4857 ..Default::default()
4858 };
4859 let result = enumerate_paths_with_metadata(&cfg, &limits);
4860
4861 assert_eq!(result.limits_hit, LimitsHit::MaxPaths);
4863
4864 assert_eq!(result.stats.total_paths, 1);
4866 }
4867
4868 #[test]
4869 fn test_enumerate_paths_iterative_with_loop() {
4870 let mut g = DiGraph::new();
4872
4873 let b0 = g.add_node(BasicBlock {
4874 id: 0,
4875 kind: BlockKind::Entry,
4876 statements: vec![],
4877 terminator: Terminator::SwitchInt {
4878 targets: vec![1],
4879 otherwise: 2,
4880 },
4881 source_location: None,
4882 });
4883
4884 let b1 = g.add_node(BasicBlock {
4885 id: 1,
4886 kind: BlockKind::Normal,
4887 statements: vec![],
4888 terminator: Terminator::Goto { target: 0 }, source_location: None,
4890 });
4891
4892 let b2 = g.add_node(BasicBlock {
4893 id: 2,
4894 kind: BlockKind::Exit,
4895 statements: vec![],
4896 terminator: Terminator::Return,
4897 source_location: None,
4898 });
4899
4900 g.add_edge(b0, b1, EdgeType::TrueBranch);
4901 g.add_edge(b0, b2, EdgeType::FalseBranch);
4902 g.add_edge(b1, b0, EdgeType::LoopBack);
4903
4904 let limits = PathLimits {
4905 loop_unroll_limit: 2,
4906 ..Default::default()
4907 };
4908 let paths = enumerate_paths_iterative(&g, &limits);
4909
4910 assert!(paths.len() >= 2);
4912 }
4913
4914 #[test]
4915 fn test_enumerate_paths_iterative_empty_cfg() {
4916 let cfg = DiGraph::new();
4917 let paths = enumerate_paths_iterative(&cfg, &PathLimits::default());
4918
4919 assert_eq!(paths.len(), 0);
4920 }
4921}