ascii_dag/
cycles.rs

1//! Cycle detection algorithms for directed graphs.
2//!
3//! This module provides functionality to detect cycles in a DAG,
4//! which would make it not a valid DAG.
5//!
6//! ## Generic Cycle Detection
7//!
8//! For more flexibility, see the [`generic`] submodule which provides
9//! cycle detection that works with any data structure through higher-order
10//! functions or traits.
11//!
12//! ```
13//! # #[cfg(feature = "generic")]
14//! # {
15//! use ascii_dag::cycles::generic::detect_cycle_fn;
16//!
17//! // Example: Error chain
18//! let get_caused_by = |error_id: &usize| -> Vec<usize> {
19//!     match error_id {
20//!         1 => vec![2],
21//!         2 => vec![3],
22//!         3 => vec![],
23//!         _ => vec![],
24//!     }
25//! };
26//!
27//! let all_errors = vec![1, 2, 3];
28//! let cycle = detect_cycle_fn(&all_errors, get_caused_by);
29//! assert!(cycle.is_none()); // No cycle
30//! # }
31//! ```
32
33#[cfg(feature = "generic")]
34pub mod generic;
35
36use crate::graph::DAG;
37use alloc::{vec, vec::Vec};
38
39impl<'a> DAG<'a> {
40    /// Check if the graph contains cycles (making it not a valid DAG).
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use ascii_dag::graph::DAG;
46    ///
47    /// let mut dag = DAG::new();
48    /// dag.add_node(1, "A");
49    /// dag.add_node(2, "B");
50    /// dag.add_edge(1, 2);
51    /// dag.add_edge(2, 1);  // Creates a cycle!
52    ///
53    /// assert!(dag.has_cycle());
54    /// ```
55    pub fn has_cycle(&self) -> bool {
56        let mut visited = vec![false; self.nodes.len()];
57        let mut rec_stack = vec![false; self.nodes.len()];
58
59        for i in 0..self.nodes.len() {
60            if self.has_cycle_util(i, &mut visited, &mut rec_stack) {
61                return true;
62            }
63        }
64        false
65    }
66
67    /// Helper function for cycle detection using DFS.
68    fn has_cycle_util(&self, idx: usize, visited: &mut [bool], rec_stack: &mut [bool]) -> bool {
69        if rec_stack[idx] {
70            return true;
71        }
72        if visited[idx] {
73            return false;
74        }
75
76        visited[idx] = true;
77        rec_stack[idx] = true;
78
79        let node_id = self.nodes[idx].0;
80        for &(from, to) in &self.edges {
81            if from == node_id {
82                // O(1) HashMap lookup instead of O(n) scan
83                if let Some(child_idx) = self.node_index(to) {
84                    if self.has_cycle_util(child_idx, visited, rec_stack) {
85                        return true;
86                    }
87                }
88            }
89        }
90
91        rec_stack[idx] = false;
92        false
93    }
94
95    /// Find a cycle path in the graph.
96    ///
97    /// Returns the node IDs that form a cycle, if one exists.
98    pub(crate) fn find_cycle_path(&self) -> Option<Vec<usize>> {
99        for i in 0..self.nodes.len() {
100            let mut visited = vec![false; self.nodes.len()];
101            let mut path = Vec::new();
102
103            if let Some(cycle) = self.find_cycle_from(i, &mut visited, &mut path) {
104                return Some(cycle);
105            }
106        }
107        None
108    }
109
110    /// Helper function to find a cycle starting from a specific node.
111    fn find_cycle_from(
112        &self,
113        start_idx: usize,
114        visited: &mut [bool],
115        path: &mut Vec<usize>,
116    ) -> Option<Vec<usize>> {
117        if visited[start_idx] {
118            // Found a cycle - extract it from path
119            if let Some(cycle_start) = path.iter().position(|&idx| idx == start_idx) {
120                return Some(
121                    path[cycle_start..]
122                        .iter()
123                        .map(|&idx| self.nodes[idx].0)
124                        .collect(),
125                );
126            }
127            return None;
128        }
129
130        visited[start_idx] = true;
131        path.push(start_idx);
132
133        let node_id = self.nodes[start_idx].0;
134        for &(from, to) in &self.edges {
135            if from == node_id {
136                // O(1) HashMap lookup instead of O(n) scan
137                if let Some(child_idx) = self.node_index(to) {
138                    if let Some(cycle) = self.find_cycle_from(child_idx, visited, path) {
139                        return Some(cycle);
140                    }
141                }
142            }
143        }
144
145        path.pop();
146        None
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use crate::graph::DAG;
153
154    #[test]
155    fn test_cycle_detection() {
156        let mut dag = DAG::new();
157        dag.add_node(1, "A");
158        dag.add_node(2, "B");
159        dag.add_edge(1, 2);
160        dag.add_edge(2, 1); // Cycle!
161
162        assert!(dag.has_cycle());
163    }
164
165    #[test]
166    fn test_no_cycle() {
167        let dag = DAG::from_edges(&[(1, "A"), (2, "B")], &[(1, 2)]);
168
169        assert!(!dag.has_cycle());
170    }
171
172    #[test]
173    fn test_cycle_with_auto_created_nodes() {
174        let mut dag = DAG::new();
175        dag.add_node(1, "A");
176        // Node 2 will be auto-created
177        dag.add_edge(1, 2);
178        dag.add_edge(2, 1); // Creates cycle
179
180        assert!(dag.has_cycle());
181    }
182}