1use std::ops::Range;
4
5use fixedbitset::FixedBitSet;
6use rustc_hash::FxHashSet;
7
8use fallow_types::discover::FileId;
9
10use super::ModuleGraph;
11use super::types::ModuleNode;
12
13impl ModuleGraph {
14 #[expect(clippy::excessive_nesting)]
23 pub fn find_cycles(&self) -> Vec<Vec<FileId>> {
24 let n = self.modules.len();
25 if n == 0 {
26 return Vec::new();
27 }
28
29 let mut index_counter: u32 = 0;
31 let mut indices: Vec<u32> = vec![u32::MAX; n]; let mut lowlinks: Vec<u32> = vec![0; n];
33 let mut on_stack = FixedBitSet::with_capacity(n);
34 let mut stack: Vec<usize> = Vec::new();
35 let mut sccs: Vec<Vec<FileId>> = Vec::new();
36
37 struct Frame {
39 node: usize,
40 succ_pos: usize,
41 succ_end: usize,
42 }
43
44 let mut all_succs: Vec<usize> = Vec::with_capacity(self.edges.len());
46 let mut succ_ranges: Vec<Range<usize>> = Vec::with_capacity(n);
47 let mut seen_set = FxHashSet::default();
48 for module in &self.modules {
49 let start = all_succs.len();
50 seen_set.clear();
51 for edge in &self.edges[module.edge_range.clone()] {
52 let target = edge.target.0 as usize;
53 if target < n && seen_set.insert(target) {
54 all_succs.push(target);
55 }
56 }
57 let end = all_succs.len();
58 succ_ranges.push(start..end);
59 }
60
61 let mut dfs_stack: Vec<Frame> = Vec::new();
62
63 for start_node in 0..n {
64 if indices[start_node] != u32::MAX {
65 continue;
66 }
67
68 indices[start_node] = index_counter;
70 lowlinks[start_node] = index_counter;
71 index_counter += 1;
72 on_stack.insert(start_node);
73 stack.push(start_node);
74
75 let range = &succ_ranges[start_node];
76 dfs_stack.push(Frame {
77 node: start_node,
78 succ_pos: range.start,
79 succ_end: range.end,
80 });
81
82 while let Some(frame) = dfs_stack.last_mut() {
83 if frame.succ_pos < frame.succ_end {
84 let w = all_succs[frame.succ_pos];
85 frame.succ_pos += 1;
86
87 if indices[w] == u32::MAX {
88 indices[w] = index_counter;
90 lowlinks[w] = index_counter;
91 index_counter += 1;
92 on_stack.insert(w);
93 stack.push(w);
94
95 let range = &succ_ranges[w];
96 dfs_stack.push(Frame {
97 node: w,
98 succ_pos: range.start,
99 succ_end: range.end,
100 });
101 } else if on_stack.contains(w) {
102 let v = frame.node;
104 lowlinks[v] = lowlinks[v].min(indices[w]);
105 }
106 } else {
107 let v = frame.node;
109 let v_lowlink = lowlinks[v];
110 let v_index = indices[v];
111 dfs_stack.pop();
112
113 if let Some(parent) = dfs_stack.last_mut() {
115 lowlinks[parent.node] = lowlinks[parent.node].min(v_lowlink);
116 }
117
118 if v_lowlink == v_index {
120 let mut scc = Vec::new();
121 loop {
122 let w = stack.pop().expect("SCC stack should not be empty");
123 on_stack.set(w, false);
124 scc.push(FileId(w as u32));
125 if w == v {
126 break;
127 }
128 }
129 if scc.len() >= 2 {
131 sccs.push(scc);
132 }
133 }
134 }
135 }
136 }
137
138 const MAX_CYCLES_PER_SCC: usize = 20;
142
143 let succs = SuccessorMap {
144 all_succs: &all_succs,
145 succ_ranges: &succ_ranges,
146 modules: &self.modules,
147 };
148
149 let mut result: Vec<Vec<FileId>> = Vec::new();
150 let mut seen_cycles: FxHashSet<Vec<u32>> = FxHashSet::default();
151
152 for scc in &sccs {
153 if scc.len() == 2 {
154 let mut cycle = vec![scc[0].0 as usize, scc[1].0 as usize];
155 if self.modules[cycle[1]].path < self.modules[cycle[0]].path {
157 cycle.swap(0, 1);
158 }
159 let key: Vec<u32> = cycle.iter().map(|&n| n as u32).collect();
160 if seen_cycles.insert(key) {
161 result.push(cycle.into_iter().map(|n| FileId(n as u32)).collect());
162 }
163 continue;
164 }
165
166 let scc_nodes: Vec<usize> = scc.iter().map(|id| id.0 as usize).collect();
167 let elementary = enumerate_elementary_cycles(&scc_nodes, &succs, MAX_CYCLES_PER_SCC);
168
169 for cycle in elementary {
170 let key: Vec<u32> = cycle.iter().map(|&n| n as u32).collect();
171 if seen_cycles.insert(key) {
172 result.push(cycle.into_iter().map(|n| FileId(n as u32)).collect());
173 }
174 }
175 }
176
177 result.sort_by(|a, b| {
179 a.len().cmp(&b.len()).then_with(|| {
180 self.modules[a[0].0 as usize]
181 .path
182 .cmp(&self.modules[b[0].0 as usize].path)
183 })
184 });
185
186 result
187 }
188}
189
190fn canonical_cycle(cycle: &[usize], modules: &[ModuleNode]) -> Vec<usize> {
192 if cycle.is_empty() {
193 return Vec::new();
194 }
195 let min_pos = cycle
196 .iter()
197 .enumerate()
198 .min_by(|(_, a), (_, b)| modules[**a].path.cmp(&modules[**b].path))
199 .map_or(0, |(i, _)| i);
200 let mut result = cycle[min_pos..].to_vec();
201 result.extend_from_slice(&cycle[..min_pos]);
202 result
203}
204
205struct CycleFrame {
207 succ_pos: usize,
208 succ_end: usize,
209}
210
211struct SuccessorMap<'a> {
213 all_succs: &'a [usize],
214 succ_ranges: &'a [Range<usize>],
215 modules: &'a [ModuleNode],
216}
217
218fn try_record_cycle(
220 path: &[usize],
221 modules: &[ModuleNode],
222 seen: &mut FxHashSet<Vec<u32>>,
223 cycles: &mut Vec<Vec<usize>>,
224) {
225 let canonical = canonical_cycle(path, modules);
226 let key: Vec<u32> = canonical.iter().map(|&n| n as u32).collect();
227 if seen.insert(key) {
228 cycles.push(canonical);
229 }
230}
231
232fn dfs_find_cycles_from(
237 start: usize,
238 depth_limit: usize,
239 scc_set: &FxHashSet<usize>,
240 succs: &SuccessorMap<'_>,
241 max_cycles: usize,
242 seen: &mut FxHashSet<Vec<u32>>,
243 cycles: &mut Vec<Vec<usize>>,
244) {
245 let mut path: Vec<usize> = vec![start];
246 let mut path_set = FixedBitSet::with_capacity(succs.modules.len());
247 path_set.insert(start);
248
249 let range = &succs.succ_ranges[start];
250 let mut dfs: Vec<CycleFrame> = vec![CycleFrame {
251 succ_pos: range.start,
252 succ_end: range.end,
253 }];
254
255 while let Some(frame) = dfs.last_mut() {
256 if cycles.len() >= max_cycles {
257 return;
258 }
259
260 if frame.succ_pos >= frame.succ_end {
261 dfs.pop();
263 if path.len() > 1 {
264 let removed = path.pop().unwrap();
265 path_set.set(removed, false);
266 }
267 continue;
268 }
269
270 let w = succs.all_succs[frame.succ_pos];
271 frame.succ_pos += 1;
272
273 if !scc_set.contains(&w) {
275 continue;
276 }
277
278 if w == start && path.len() >= 2 && path.len() == depth_limit {
280 try_record_cycle(&path, succs.modules, seen, cycles);
281 continue;
282 }
283
284 if path_set.contains(w) || path.len() >= depth_limit {
286 continue;
287 }
288
289 path.push(w);
291 path_set.insert(w);
292
293 let range = &succs.succ_ranges[w];
294 dfs.push(CycleFrame {
295 succ_pos: range.start,
296 succ_end: range.end,
297 });
298 }
299}
300
301fn enumerate_elementary_cycles(
307 scc_nodes: &[usize],
308 succs: &SuccessorMap<'_>,
309 max_cycles: usize,
310) -> Vec<Vec<usize>> {
311 let scc_set: FxHashSet<usize> = scc_nodes.iter().copied().collect();
312 let mut cycles: Vec<Vec<usize>> = Vec::new();
313 let mut seen: FxHashSet<Vec<u32>> = FxHashSet::default();
314
315 let mut sorted_nodes: Vec<usize> = scc_nodes.to_vec();
317 sorted_nodes.sort_by(|a, b| succs.modules[*a].path.cmp(&succs.modules[*b].path));
318
319 let max_depth = scc_nodes.len().min(12); for depth_limit in 2..=max_depth {
322 if cycles.len() >= max_cycles {
323 break;
324 }
325
326 for &start in &sorted_nodes {
327 if cycles.len() >= max_cycles {
328 break;
329 }
330
331 dfs_find_cycles_from(
332 start,
333 depth_limit,
334 &scc_set,
335 succs,
336 max_cycles,
337 &mut seen,
338 &mut cycles,
339 );
340 }
341 }
342
343 cycles
344}
345
346#[cfg(test)]
347mod tests {
348 use std::ops::Range;
349 use std::path::PathBuf;
350
351 use rustc_hash::FxHashSet;
352
353 use crate::graph::types::ModuleNode;
354 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule};
355 use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
356 use fallow_types::extract::{ExportName, ImportInfo, ImportedName};
357
358 use super::{
359 ModuleGraph, SuccessorMap, canonical_cycle, dfs_find_cycles_from,
360 enumerate_elementary_cycles, try_record_cycle,
361 };
362
363 fn build_cycle_graph(file_count: usize, edges_spec: &[(u32, u32)]) -> ModuleGraph {
365 let files: Vec<DiscoveredFile> = (0..file_count)
366 .map(|i| DiscoveredFile {
367 id: FileId(i as u32),
368 path: PathBuf::from(format!("/project/file{i}.ts")),
369 size_bytes: 100,
370 })
371 .collect();
372
373 let resolved_modules: Vec<ResolvedModule> = (0..file_count)
374 .map(|i| {
375 let imports: Vec<ResolvedImport> = edges_spec
376 .iter()
377 .filter(|(src, _)| *src == i as u32)
378 .map(|(_, tgt)| ResolvedImport {
379 info: ImportInfo {
380 source: format!("./file{tgt}"),
381 imported_name: ImportedName::Named("x".to_string()),
382 local_name: "x".to_string(),
383 is_type_only: false,
384 span: oxc_span::Span::new(0, 10),
385 source_span: oxc_span::Span::default(),
386 },
387 target: ResolveResult::InternalModule(FileId(*tgt)),
388 })
389 .collect();
390
391 ResolvedModule {
392 file_id: FileId(i as u32),
393 path: PathBuf::from(format!("/project/file{i}.ts")),
394 exports: vec![fallow_types::extract::ExportInfo {
395 name: ExportName::Named("x".to_string()),
396 local_name: Some("x".to_string()),
397 is_type_only: false,
398 is_public: false,
399 span: oxc_span::Span::new(0, 20),
400 members: vec![],
401 }],
402 re_exports: vec![],
403 resolved_imports: imports,
404 resolved_dynamic_imports: vec![],
405 resolved_dynamic_patterns: vec![],
406 member_accesses: vec![],
407 whole_object_uses: vec![],
408 has_cjs_exports: false,
409 unused_import_bindings: vec![],
410 }
411 })
412 .collect();
413
414 let entry_points = vec![EntryPoint {
415 path: PathBuf::from("/project/file0.ts"),
416 source: EntryPointSource::PackageJsonMain,
417 }];
418
419 ModuleGraph::build(&resolved_modules, &entry_points, &files)
420 }
421
422 #[test]
423 fn find_cycles_empty_graph() {
424 let graph = ModuleGraph::build(&[], &[], &[]);
425 assert!(graph.find_cycles().is_empty());
426 }
427
428 #[test]
429 fn find_cycles_no_cycles() {
430 let graph = build_cycle_graph(3, &[(0, 1), (1, 2)]);
432 assert!(graph.find_cycles().is_empty());
433 }
434
435 #[test]
436 fn find_cycles_simple_two_node_cycle() {
437 let graph = build_cycle_graph(2, &[(0, 1), (1, 0)]);
439 let cycles = graph.find_cycles();
440 assert_eq!(cycles.len(), 1);
441 assert_eq!(cycles[0].len(), 2);
442 }
443
444 #[test]
445 fn find_cycles_three_node_cycle() {
446 let graph = build_cycle_graph(3, &[(0, 1), (1, 2), (2, 0)]);
448 let cycles = graph.find_cycles();
449 assert_eq!(cycles.len(), 1);
450 assert_eq!(cycles[0].len(), 3);
451 }
452
453 #[test]
454 fn find_cycles_self_import_ignored() {
455 let graph = build_cycle_graph(1, &[(0, 0)]);
459 let cycles = graph.find_cycles();
460 assert!(
461 cycles.is_empty(),
462 "self-imports should not be reported as cycles"
463 );
464 }
465
466 #[test]
467 fn find_cycles_multiple_independent_cycles() {
468 let graph = build_cycle_graph(4, &[(0, 1), (1, 0), (2, 3), (3, 2)]);
472 let cycles = graph.find_cycles();
473 assert_eq!(cycles.len(), 2);
474 assert!(cycles.iter().all(|c| c.len() == 2));
476 }
477
478 #[test]
479 fn find_cycles_linear_chain_with_back_edge() {
480 let graph = build_cycle_graph(4, &[(0, 1), (1, 2), (2, 3), (3, 1)]);
482 let cycles = graph.find_cycles();
483 assert_eq!(cycles.len(), 1);
484 assert_eq!(cycles[0].len(), 3);
485 let ids: Vec<u32> = cycles[0].iter().map(|f| f.0).collect();
487 assert!(ids.contains(&1));
488 assert!(ids.contains(&2));
489 assert!(ids.contains(&3));
490 assert!(!ids.contains(&0));
491 }
492
493 #[test]
494 fn find_cycles_overlapping_cycles_enumerated() {
495 let graph = build_cycle_graph(3, &[(0, 1), (1, 0), (1, 2), (2, 1)]);
497 let cycles = graph.find_cycles();
498 assert_eq!(
499 cycles.len(),
500 2,
501 "should find 2 elementary cycles, not 1 SCC"
502 );
503 assert!(
504 cycles.iter().all(|c| c.len() == 2),
505 "both cycles should have length 2"
506 );
507 }
508
509 #[test]
510 fn find_cycles_deterministic_ordering() {
511 let graph1 = build_cycle_graph(3, &[(0, 1), (1, 2), (2, 0)]);
513 let graph2 = build_cycle_graph(3, &[(0, 1), (1, 2), (2, 0)]);
514 let cycles1 = graph1.find_cycles();
515 let cycles2 = graph2.find_cycles();
516 assert_eq!(cycles1.len(), cycles2.len());
517 for (c1, c2) in cycles1.iter().zip(cycles2.iter()) {
518 let paths1: Vec<&PathBuf> = c1
519 .iter()
520 .map(|f| &graph1.modules[f.0 as usize].path)
521 .collect();
522 let paths2: Vec<&PathBuf> = c2
523 .iter()
524 .map(|f| &graph2.modules[f.0 as usize].path)
525 .collect();
526 assert_eq!(paths1, paths2);
527 }
528 }
529
530 #[test]
531 fn find_cycles_sorted_by_length() {
532 let graph = build_cycle_graph(5, &[(0, 1), (1, 0), (2, 3), (3, 4), (4, 2)]);
534 let cycles = graph.find_cycles();
535 assert_eq!(cycles.len(), 2);
536 assert!(
537 cycles[0].len() <= cycles[1].len(),
538 "cycles should be sorted by length"
539 );
540 }
541
542 #[test]
543 fn find_cycles_large_cycle() {
544 let edges: Vec<(u32, u32)> = (0..10).map(|i| (i, (i + 1) % 10)).collect();
546 let graph = build_cycle_graph(10, &edges);
547 let cycles = graph.find_cycles();
548 assert_eq!(cycles.len(), 1);
549 assert_eq!(cycles[0].len(), 10);
550 }
551
552 #[test]
553 fn find_cycles_complex_scc_multiple_elementary() {
554 let graph = build_cycle_graph(4, &[(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]);
557 let cycles = graph.find_cycles();
558 assert!(
560 cycles.len() >= 2,
561 "should find at least 2 elementary cycles, got {}",
562 cycles.len()
563 );
564 assert!(cycles.iter().all(|c| c.len() <= 4));
566 }
567
568 #[test]
569 fn find_cycles_no_duplicate_cycles() {
570 let graph = build_cycle_graph(3, &[(0, 1), (1, 2), (2, 0)]);
573 let cycles = graph.find_cycles();
574 assert_eq!(cycles.len(), 1, "triangle should produce exactly 1 cycle");
575 assert_eq!(cycles[0].len(), 3);
576 }
577
578 fn build_test_succs(
587 file_count: usize,
588 edges_spec: &[(usize, usize)],
589 ) -> (Vec<ModuleNode>, Vec<usize>, Vec<Range<usize>>) {
590 let modules: Vec<ModuleNode> = (0..file_count)
591 .map(|i| ModuleNode {
592 file_id: FileId(i as u32),
593 path: PathBuf::from(format!("/project/file{i}.ts")),
594 edge_range: 0..0,
595 exports: vec![],
596 re_exports: vec![],
597 is_entry_point: i == 0,
598 is_reachable: true,
599 has_cjs_exports: false,
600 })
601 .collect();
602
603 let mut all_succs: Vec<usize> = Vec::new();
604 let mut succ_ranges: Vec<Range<usize>> = Vec::with_capacity(file_count);
605 for src in 0..file_count {
606 let start = all_succs.len();
607 let mut seen = FxHashSet::default();
608 for &(s, t) in edges_spec {
609 if s == src && t < file_count && seen.insert(t) {
610 all_succs.push(t);
611 }
612 }
613 let end = all_succs.len();
614 succ_ranges.push(start..end);
615 }
616
617 (modules, all_succs, succ_ranges)
618 }
619
620 #[test]
625 fn canonical_cycle_empty() {
626 let modules: Vec<ModuleNode> = vec![];
627 assert!(canonical_cycle(&[], &modules).is_empty());
628 }
629
630 #[test]
631 fn canonical_cycle_rotates_to_smallest_path() {
632 let (modules, _, _) = build_test_succs(3, &[]);
633 let result = canonical_cycle(&[2, 0, 1], &modules);
635 assert_eq!(result, vec![0, 1, 2]);
636 }
637
638 #[test]
639 fn canonical_cycle_already_canonical() {
640 let (modules, _, _) = build_test_succs(3, &[]);
641 let result = canonical_cycle(&[0, 1, 2], &modules);
642 assert_eq!(result, vec![0, 1, 2]);
643 }
644
645 #[test]
646 fn canonical_cycle_single_node() {
647 let (modules, _, _) = build_test_succs(1, &[]);
648 let result = canonical_cycle(&[0], &modules);
649 assert_eq!(result, vec![0]);
650 }
651
652 #[test]
657 fn try_record_cycle_inserts_new_cycle() {
658 let (modules, _, _) = build_test_succs(3, &[]);
659 let mut seen = FxHashSet::default();
660 let mut cycles = Vec::new();
661
662 try_record_cycle(&[0, 1, 2], &modules, &mut seen, &mut cycles);
663 assert_eq!(cycles.len(), 1);
664 assert_eq!(cycles[0], vec![0, 1, 2]);
665 }
666
667 #[test]
668 fn try_record_cycle_deduplicates_rotated_cycle() {
669 let (modules, _, _) = build_test_succs(3, &[]);
672 let mut seen = FxHashSet::default();
673 let mut cycles = Vec::new();
674
675 try_record_cycle(&[0, 1, 2], &modules, &mut seen, &mut cycles);
676 try_record_cycle(&[1, 2, 0], &modules, &mut seen, &mut cycles);
677 try_record_cycle(&[2, 0, 1], &modules, &mut seen, &mut cycles);
678
679 assert_eq!(
680 cycles.len(),
681 1,
682 "rotations of the same cycle should be deduped"
683 );
684 }
685
686 #[test]
687 fn try_record_cycle_single_node_self_loop() {
688 let (modules, _, _) = build_test_succs(1, &[]);
690 let mut seen = FxHashSet::default();
691 let mut cycles = Vec::new();
692
693 try_record_cycle(&[0], &modules, &mut seen, &mut cycles);
694 assert_eq!(cycles.len(), 1);
695 assert_eq!(cycles[0], vec![0]);
696 }
697
698 #[test]
699 fn try_record_cycle_distinct_cycles_both_recorded() {
700 let (modules, _, _) = build_test_succs(4, &[]);
702 let mut seen = FxHashSet::default();
703 let mut cycles = Vec::new();
704
705 try_record_cycle(&[0, 1], &modules, &mut seen, &mut cycles);
706 try_record_cycle(&[2, 3], &modules, &mut seen, &mut cycles);
707
708 assert_eq!(cycles.len(), 2);
709 }
710
711 #[test]
716 fn successor_map_empty_graph() {
717 let (modules, all_succs, succ_ranges) = build_test_succs(0, &[]);
718 let succs = SuccessorMap {
719 all_succs: &all_succs,
720 succ_ranges: &succ_ranges,
721 modules: &modules,
722 };
723 assert!(succs.all_succs.is_empty());
724 assert!(succs.succ_ranges.is_empty());
725 }
726
727 #[test]
728 fn successor_map_single_node_self_edge() {
729 let (modules, all_succs, succ_ranges) = build_test_succs(1, &[(0, 0)]);
730 let succs = SuccessorMap {
731 all_succs: &all_succs,
732 succ_ranges: &succ_ranges,
733 modules: &modules,
734 };
735 assert_eq!(succs.all_succs.len(), 1);
736 assert_eq!(succs.all_succs[0], 0);
737 assert_eq!(succs.succ_ranges[0], 0..1);
738 }
739
740 #[test]
741 fn successor_map_deduplicates_edges() {
742 let (modules, all_succs, succ_ranges) = build_test_succs(2, &[(0, 1), (0, 1)]);
744 let succs = SuccessorMap {
745 all_succs: &all_succs,
746 succ_ranges: &succ_ranges,
747 modules: &modules,
748 };
749 let range = &succs.succ_ranges[0];
750 assert_eq!(
751 range.end - range.start,
752 1,
753 "duplicate edges should be deduped"
754 );
755 }
756
757 #[test]
758 fn successor_map_multiple_successors() {
759 let (modules, all_succs, succ_ranges) = build_test_succs(4, &[(0, 1), (0, 2), (0, 3)]);
760 let succs = SuccessorMap {
761 all_succs: &all_succs,
762 succ_ranges: &succ_ranges,
763 modules: &modules,
764 };
765 let range = &succs.succ_ranges[0];
766 assert_eq!(range.end - range.start, 3);
767 for i in 1..4 {
769 let r = &succs.succ_ranges[i];
770 assert_eq!(r.end - r.start, 0);
771 }
772 }
773
774 #[test]
779 fn dfs_find_cycles_from_isolated_node() {
780 let (modules, all_succs, succ_ranges) = build_test_succs(1, &[]);
782 let succs = SuccessorMap {
783 all_succs: &all_succs,
784 succ_ranges: &succ_ranges,
785 modules: &modules,
786 };
787 let scc_set: FxHashSet<usize> = [0].into_iter().collect();
788 let mut seen = FxHashSet::default();
789 let mut cycles = Vec::new();
790
791 dfs_find_cycles_from(0, 2, &scc_set, &succs, 10, &mut seen, &mut cycles);
792 assert!(cycles.is_empty(), "isolated node should have no cycles");
793 }
794
795 #[test]
796 fn dfs_find_cycles_from_simple_two_cycle() {
797 let (modules, all_succs, succ_ranges) = build_test_succs(2, &[(0, 1), (1, 0)]);
799 let succs = SuccessorMap {
800 all_succs: &all_succs,
801 succ_ranges: &succ_ranges,
802 modules: &modules,
803 };
804 let scc_set: FxHashSet<usize> = [0, 1].into_iter().collect();
805 let mut seen = FxHashSet::default();
806 let mut cycles = Vec::new();
807
808 dfs_find_cycles_from(0, 2, &scc_set, &succs, 10, &mut seen, &mut cycles);
809 assert_eq!(cycles.len(), 1);
810 assert_eq!(cycles[0].len(), 2);
811 }
812
813 #[test]
814 fn dfs_find_cycles_from_diamond_graph() {
815 let (modules, all_succs, succ_ranges) =
819 build_test_succs(4, &[(0, 1), (0, 2), (1, 3), (2, 3), (3, 0)]);
820 let succs = SuccessorMap {
821 all_succs: &all_succs,
822 succ_ranges: &succ_ranges,
823 modules: &modules,
824 };
825 let scc_set: FxHashSet<usize> = [0, 1, 2, 3].into_iter().collect();
826 let mut seen = FxHashSet::default();
827 let mut cycles = Vec::new();
828
829 dfs_find_cycles_from(0, 3, &scc_set, &succs, 10, &mut seen, &mut cycles);
831 assert_eq!(cycles.len(), 2, "diamond should have two 3-node cycles");
832 assert!(cycles.iter().all(|c| c.len() == 3));
833 }
834
835 #[test]
836 fn dfs_find_cycles_from_depth_limit_prevents_longer_cycles() {
837 let (modules, all_succs, succ_ranges) =
840 build_test_succs(4, &[(0, 1), (1, 2), (2, 3), (3, 0)]);
841 let succs = SuccessorMap {
842 all_succs: &all_succs,
843 succ_ranges: &succ_ranges,
844 modules: &modules,
845 };
846 let scc_set: FxHashSet<usize> = [0, 1, 2, 3].into_iter().collect();
847 let mut seen = FxHashSet::default();
848 let mut cycles = Vec::new();
849
850 dfs_find_cycles_from(0, 3, &scc_set, &succs, 10, &mut seen, &mut cycles);
851 assert!(
852 cycles.is_empty(),
853 "depth_limit=3 should prevent finding a 4-node cycle"
854 );
855 }
856
857 #[test]
858 fn dfs_find_cycles_from_depth_limit_exact_match() {
859 let (modules, all_succs, succ_ranges) =
862 build_test_succs(4, &[(0, 1), (1, 2), (2, 3), (3, 0)]);
863 let succs = SuccessorMap {
864 all_succs: &all_succs,
865 succ_ranges: &succ_ranges,
866 modules: &modules,
867 };
868 let scc_set: FxHashSet<usize> = [0, 1, 2, 3].into_iter().collect();
869 let mut seen = FxHashSet::default();
870 let mut cycles = Vec::new();
871
872 dfs_find_cycles_from(0, 4, &scc_set, &succs, 10, &mut seen, &mut cycles);
873 assert_eq!(
874 cycles.len(),
875 1,
876 "depth_limit=4 should find the 4-node cycle"
877 );
878 assert_eq!(cycles[0].len(), 4);
879 }
880
881 #[test]
882 fn dfs_find_cycles_from_respects_max_cycles() {
883 let edges: Vec<(usize, usize)> = (0..4)
885 .flat_map(|i| (0..4).filter(move |&j| i != j).map(move |j| (i, j)))
886 .collect();
887 let (modules, all_succs, succ_ranges) = build_test_succs(4, &edges);
888 let succs = SuccessorMap {
889 all_succs: &all_succs,
890 succ_ranges: &succ_ranges,
891 modules: &modules,
892 };
893 let scc_set: FxHashSet<usize> = (0..4).collect();
894 let mut seen = FxHashSet::default();
895 let mut cycles = Vec::new();
896
897 dfs_find_cycles_from(0, 2, &scc_set, &succs, 2, &mut seen, &mut cycles);
899 assert!(
900 cycles.len() <= 2,
901 "should respect max_cycles limit, got {}",
902 cycles.len()
903 );
904 }
905
906 #[test]
907 fn dfs_find_cycles_from_ignores_nodes_outside_scc() {
908 let (modules, all_succs, succ_ranges) = build_test_succs(3, &[(0, 1), (1, 2), (2, 0)]);
910 let succs = SuccessorMap {
911 all_succs: &all_succs,
912 succ_ranges: &succ_ranges,
913 modules: &modules,
914 };
915 let scc_set: FxHashSet<usize> = [0, 1].into_iter().collect();
916 let mut seen = FxHashSet::default();
917 let mut cycles = Vec::new();
918
919 for depth in 2..=3 {
920 dfs_find_cycles_from(0, depth, &scc_set, &succs, 10, &mut seen, &mut cycles);
921 }
922 assert!(
923 cycles.is_empty(),
924 "should not find cycles through nodes outside the SCC set"
925 );
926 }
927
928 #[test]
933 fn enumerate_elementary_cycles_empty_scc() {
934 let (modules, all_succs, succ_ranges) = build_test_succs(0, &[]);
935 let succs = SuccessorMap {
936 all_succs: &all_succs,
937 succ_ranges: &succ_ranges,
938 modules: &modules,
939 };
940 let cycles = enumerate_elementary_cycles(&[], &succs, 10);
941 assert!(cycles.is_empty());
942 }
943
944 #[test]
945 fn enumerate_elementary_cycles_max_cycles_limit() {
946 let edges: Vec<(usize, usize)> = (0..4)
948 .flat_map(|i| (0..4).filter(move |&j| i != j).map(move |j| (i, j)))
949 .collect();
950 let (modules, all_succs, succ_ranges) = build_test_succs(4, &edges);
951 let succs = SuccessorMap {
952 all_succs: &all_succs,
953 succ_ranges: &succ_ranges,
954 modules: &modules,
955 };
956 let scc_nodes: Vec<usize> = (0..4).collect();
957
958 let cycles = enumerate_elementary_cycles(&scc_nodes, &succs, 3);
959 assert!(
960 cycles.len() <= 3,
961 "should respect max_cycles=3 limit, got {}",
962 cycles.len()
963 );
964 }
965
966 #[test]
967 fn enumerate_elementary_cycles_finds_all_in_triangle() {
968 let (modules, all_succs, succ_ranges) = build_test_succs(3, &[(0, 1), (1, 2), (2, 0)]);
970 let succs = SuccessorMap {
971 all_succs: &all_succs,
972 succ_ranges: &succ_ranges,
973 modules: &modules,
974 };
975 let scc_nodes: Vec<usize> = vec![0, 1, 2];
976
977 let cycles = enumerate_elementary_cycles(&scc_nodes, &succs, 20);
978 assert_eq!(cycles.len(), 1);
979 assert_eq!(cycles[0].len(), 3);
980 }
981
982 #[test]
983 fn enumerate_elementary_cycles_iterative_deepening_order() {
984 let (modules, all_succs, succ_ranges) =
987 build_test_succs(3, &[(0, 1), (1, 0), (1, 2), (2, 0)]);
988 let succs = SuccessorMap {
989 all_succs: &all_succs,
990 succ_ranges: &succ_ranges,
991 modules: &modules,
992 };
993 let scc_nodes: Vec<usize> = vec![0, 1, 2];
994
995 let cycles = enumerate_elementary_cycles(&scc_nodes, &succs, 20);
996 assert!(cycles.len() >= 2, "should find at least 2 cycles");
997 assert!(
999 cycles[0].len() <= cycles[cycles.len() - 1].len(),
1000 "shorter cycles should be found before longer ones"
1001 );
1002 }
1003
1004 #[test]
1009 fn find_cycles_max_cycles_per_scc_respected() {
1010 let edges: Vec<(u32, u32)> = (0..5)
1012 .flat_map(|i| (0..5).filter(move |&j| i != j).map(move |j| (i, j)))
1013 .collect();
1014 let graph = build_cycle_graph(5, &edges);
1015 let cycles = graph.find_cycles();
1016 assert!(
1018 cycles.len() <= 20,
1019 "should cap at MAX_CYCLES_PER_SCC, got {}",
1020 cycles.len()
1021 );
1022 assert!(
1023 !cycles.is_empty(),
1024 "dense graph should still find some cycles"
1025 );
1026 }
1027
1028 #[test]
1029 fn find_cycles_graph_with_no_cycles_returns_empty() {
1030 let graph = build_cycle_graph(5, &[(0, 1), (0, 2), (0, 3), (0, 4)]);
1032 assert!(graph.find_cycles().is_empty());
1033 }
1034
1035 #[test]
1036 fn find_cycles_diamond_no_cycle() {
1037 let graph = build_cycle_graph(4, &[(0, 1), (0, 2), (1, 3), (2, 3)]);
1039 assert!(graph.find_cycles().is_empty());
1040 }
1041
1042 #[test]
1043 fn find_cycles_diamond_with_back_edge() {
1044 let graph = build_cycle_graph(4, &[(0, 1), (0, 2), (1, 3), (2, 3), (3, 0)]);
1046 let cycles = graph.find_cycles();
1047 assert!(
1048 cycles.len() >= 2,
1049 "diamond with back-edge should have at least 2 elementary cycles, got {}",
1050 cycles.len()
1051 );
1052 assert_eq!(cycles[0].len(), 3);
1054 }
1055
1056 #[test]
1061 fn canonical_cycle_non_sequential_indices() {
1062 let (modules, _, _) = build_test_succs(5, &[]);
1064 let result = canonical_cycle(&[3, 1, 4], &modules);
1065 assert_eq!(result, vec![1, 4, 3]);
1067 }
1068
1069 #[test]
1070 fn canonical_cycle_different_starting_points_same_result() {
1071 let (modules, _, _) = build_test_succs(4, &[]);
1074 let r1 = canonical_cycle(&[0, 1, 2, 3], &modules);
1075 let r2 = canonical_cycle(&[1, 2, 3, 0], &modules);
1076 let r3 = canonical_cycle(&[2, 3, 0, 1], &modules);
1077 let r4 = canonical_cycle(&[3, 0, 1, 2], &modules);
1078 assert_eq!(r1, r2);
1079 assert_eq!(r2, r3);
1080 assert_eq!(r3, r4);
1081 assert_eq!(r1, vec![0, 1, 2, 3]);
1082 }
1083
1084 #[test]
1085 fn canonical_cycle_two_node_both_rotations() {
1086 let (modules, _, _) = build_test_succs(2, &[]);
1088 assert_eq!(canonical_cycle(&[0, 1], &modules), vec![0, 1]);
1089 assert_eq!(canonical_cycle(&[1, 0], &modules), vec![0, 1]);
1090 }
1091
1092 #[test]
1097 fn dfs_find_cycles_from_self_loop_not_found() {
1098 let (modules, all_succs, succ_ranges) = build_test_succs(1, &[(0, 0)]);
1101 let succs = SuccessorMap {
1102 all_succs: &all_succs,
1103 succ_ranges: &succ_ranges,
1104 modules: &modules,
1105 };
1106 let scc_set: FxHashSet<usize> = [0].into_iter().collect();
1107 let mut seen = FxHashSet::default();
1108 let mut cycles = Vec::new();
1109
1110 for depth in 1..=3 {
1111 dfs_find_cycles_from(0, depth, &scc_set, &succs, 10, &mut seen, &mut cycles);
1112 }
1113 assert!(
1114 cycles.is_empty(),
1115 "self-loop should not be detected as a cycle by dfs_find_cycles_from"
1116 );
1117 }
1118
1119 #[test]
1120 fn enumerate_elementary_cycles_self_loop_not_found() {
1121 let (modules, all_succs, succ_ranges) = build_test_succs(1, &[(0, 0)]);
1123 let succs = SuccessorMap {
1124 all_succs: &all_succs,
1125 succ_ranges: &succ_ranges,
1126 modules: &modules,
1127 };
1128 let cycles = enumerate_elementary_cycles(&[0], &succs, 20);
1129 assert!(
1130 cycles.is_empty(),
1131 "self-loop should not produce elementary cycles"
1132 );
1133 }
1134
1135 #[test]
1140 fn find_cycles_two_cycles_sharing_edge() {
1141 let graph = build_cycle_graph(4, &[(0, 1), (1, 2), (2, 0), (1, 3), (3, 0)]);
1144 let cycles = graph.find_cycles();
1145 assert_eq!(
1146 cycles.len(),
1147 2,
1148 "two cycles sharing edge A->B should both be found, got {}",
1149 cycles.len()
1150 );
1151 assert!(
1152 cycles.iter().all(|c| c.len() == 3),
1153 "both cycles should have length 3"
1154 );
1155 }
1156
1157 #[test]
1158 fn enumerate_elementary_cycles_shared_edge() {
1159 let (modules, all_succs, succ_ranges) =
1161 build_test_succs(4, &[(0, 1), (1, 2), (2, 0), (1, 3), (3, 0)]);
1162 let succs = SuccessorMap {
1163 all_succs: &all_succs,
1164 succ_ranges: &succ_ranges,
1165 modules: &modules,
1166 };
1167 let scc_nodes: Vec<usize> = vec![0, 1, 2, 3];
1168 let cycles = enumerate_elementary_cycles(&scc_nodes, &succs, 20);
1169 assert_eq!(
1170 cycles.len(),
1171 2,
1172 "should find exactly 2 elementary cycles sharing edge 0->1, got {}",
1173 cycles.len()
1174 );
1175 }
1176
1177 #[test]
1182 fn enumerate_elementary_cycles_pentagon_with_chords() {
1183 let (modules, all_succs, succ_ranges) =
1191 build_test_succs(5, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 2), (0, 3)]);
1192 let succs = SuccessorMap {
1193 all_succs: &all_succs,
1194 succ_ranges: &succ_ranges,
1195 modules: &modules,
1196 };
1197 let scc_nodes: Vec<usize> = vec![0, 1, 2, 3, 4];
1198 let cycles = enumerate_elementary_cycles(&scc_nodes, &succs, 20);
1199
1200 assert!(
1202 cycles.len() >= 3,
1203 "pentagon with chords should have at least 3 elementary cycles, got {}",
1204 cycles.len()
1205 );
1206 let unique: FxHashSet<Vec<usize>> = cycles.iter().cloned().collect();
1208 assert_eq!(
1209 unique.len(),
1210 cycles.len(),
1211 "all enumerated cycles should be unique"
1212 );
1213 assert_eq!(
1215 cycles[0].len(),
1216 3,
1217 "shortest cycle in pentagon with chords should be length 3"
1218 );
1219 }
1220
1221 #[test]
1222 fn find_cycles_large_scc_complete_graph_k6() {
1223 let edges: Vec<(u32, u32)> = (0..6)
1226 .flat_map(|i| (0..6).filter(move |&j| i != j).map(move |j| (i, j)))
1227 .collect();
1228 let graph = build_cycle_graph(6, &edges);
1229 let cycles = graph.find_cycles();
1230
1231 assert!(
1233 cycles.len() <= 20,
1234 "should cap at MAX_CYCLES_PER_SCC (20), got {}",
1235 cycles.len()
1236 );
1237 assert_eq!(
1238 cycles.len(),
1239 20,
1240 "K6 has far more than 20 elementary cycles, so we should hit the cap"
1241 );
1242 assert_eq!(cycles[0].len(), 2, "shortest cycles in K6 should be 2-node");
1244 }
1245
1246 #[test]
1251 fn enumerate_elementary_cycles_respects_depth_cap_of_12() {
1252 let edges: Vec<(usize, usize)> = (0..15).map(|i| (i, (i + 1) % 15)).collect();
1256 let (modules, all_succs, succ_ranges) = build_test_succs(15, &edges);
1257 let succs = SuccessorMap {
1258 all_succs: &all_succs,
1259 succ_ranges: &succ_ranges,
1260 modules: &modules,
1261 };
1262 let scc_nodes: Vec<usize> = (0..15).collect();
1263 let cycles = enumerate_elementary_cycles(&scc_nodes, &succs, 20);
1264
1265 assert!(
1266 cycles.is_empty(),
1267 "a pure 15-node cycle should not be found with depth cap of 12, got {} cycles",
1268 cycles.len()
1269 );
1270 }
1271
1272 #[test]
1273 fn enumerate_elementary_cycles_finds_cycle_at_depth_cap_boundary() {
1274 let edges: Vec<(usize, usize)> = (0..12).map(|i| (i, (i + 1) % 12)).collect();
1277 let (modules, all_succs, succ_ranges) = build_test_succs(12, &edges);
1278 let succs = SuccessorMap {
1279 all_succs: &all_succs,
1280 succ_ranges: &succ_ranges,
1281 modules: &modules,
1282 };
1283 let scc_nodes: Vec<usize> = (0..12).collect();
1284 let cycles = enumerate_elementary_cycles(&scc_nodes, &succs, 20);
1285
1286 assert_eq!(
1287 cycles.len(),
1288 1,
1289 "a pure 12-node cycle should be found at the depth cap boundary"
1290 );
1291 assert_eq!(cycles[0].len(), 12);
1292 }
1293
1294 #[test]
1295 fn enumerate_elementary_cycles_13_node_pure_cycle_not_found() {
1296 let edges: Vec<(usize, usize)> = (0..13).map(|i| (i, (i + 1) % 13)).collect();
1298 let (modules, all_succs, succ_ranges) = build_test_succs(13, &edges);
1299 let succs = SuccessorMap {
1300 all_succs: &all_succs,
1301 succ_ranges: &succ_ranges,
1302 modules: &modules,
1303 };
1304 let scc_nodes: Vec<usize> = (0..13).collect();
1305 let cycles = enumerate_elementary_cycles(&scc_nodes, &succs, 20);
1306
1307 assert!(
1308 cycles.is_empty(),
1309 "13-node pure cycle exceeds depth cap of 12"
1310 );
1311 }
1312
1313 #[test]
1318 fn find_cycles_max_cycles_per_scc_enforced_on_k7() {
1319 let edges: Vec<(u32, u32)> = (0..7)
1322 .flat_map(|i| (0..7).filter(move |&j| i != j).map(move |j| (i, j)))
1323 .collect();
1324 let graph = build_cycle_graph(7, &edges);
1325 let cycles = graph.find_cycles();
1326
1327 assert!(
1328 cycles.len() <= 20,
1329 "K7 should cap at MAX_CYCLES_PER_SCC (20), got {}",
1330 cycles.len()
1331 );
1332 assert_eq!(
1333 cycles.len(),
1334 20,
1335 "K7 has far more than 20 elementary cycles, should hit the cap exactly"
1336 );
1337 }
1338
1339 #[test]
1340 fn find_cycles_two_dense_sccs_each_capped() {
1341 let mut edges: Vec<(u32, u32)> = Vec::new();
1344 for i in 0..4 {
1346 for j in 0..4 {
1347 if i != j {
1348 edges.push((i, j));
1349 }
1350 }
1351 }
1352 for i in 4..8 {
1354 for j in 4..8 {
1355 if i != j {
1356 edges.push((i, j));
1357 }
1358 }
1359 }
1360 let graph = build_cycle_graph(8, &edges);
1361 let cycles = graph.find_cycles();
1362
1363 assert!(!cycles.is_empty(), "two dense SCCs should produce cycles");
1366 assert!(
1370 cycles.len() > 2,
1371 "should find multiple cycles across both SCCs, got {}",
1372 cycles.len()
1373 );
1374 }
1375}