1use crate::storage::CsrGraph;
26use crate::NodeId;
27use anyhow::{anyhow, Result};
28
29#[derive(Clone, Copy, PartialEq, Eq)]
31enum NodeState {
32 Unvisited,
34 InStack,
36 Finished,
38}
39
40#[must_use]
76pub fn is_cyclic(graph: &CsrGraph) -> bool {
77 let n = graph.num_nodes();
78 if n == 0 {
79 return false;
80 }
81
82 let mut state = vec![NodeState::Unvisited; n];
83
84 for start in 0..n {
86 if state[start] == NodeState::Unvisited && has_cycle_from(graph, start, &mut state) {
87 return true;
88 }
89 }
90
91 false
92}
93
94fn has_cycle_from(graph: &CsrGraph, node: usize, state: &mut [NodeState]) -> bool {
96 state[node] = NodeState::InStack;
97
98 #[allow(clippy::cast_possible_truncation)]
100 let neighbors = graph.outgoing_neighbors(NodeId(node as u32));
101
102 if let Ok(neighbors) = neighbors {
103 for &neighbor in neighbors {
104 let neighbor_idx = neighbor as usize;
105
106 match state[neighbor_idx] {
107 NodeState::InStack => {
108 return true;
110 }
111 NodeState::Unvisited => {
112 if has_cycle_from(graph, neighbor_idx, state) {
113 return true;
114 }
115 }
116 NodeState::Finished => {
117 }
119 }
120 }
121 }
122
123 state[node] = NodeState::Finished;
124 false
125}
126
127pub fn toposort(graph: &CsrGraph) -> Result<Vec<NodeId>> {
169 let n = graph.num_nodes();
170 if n == 0 {
171 return Ok(Vec::new());
172 }
173
174 let mut state = vec![NodeState::Unvisited; n];
175 let mut result = Vec::with_capacity(n);
176
177 for start in 0..n {
179 if state[start] == NodeState::Unvisited {
180 toposort_dfs(graph, start, &mut state, &mut result)?;
181 }
182 }
183
184 result.reverse();
186 Ok(result)
187}
188
189fn toposort_dfs(
191 graph: &CsrGraph,
192 node: usize,
193 state: &mut [NodeState],
194 result: &mut Vec<NodeId>,
195) -> Result<()> {
196 state[node] = NodeState::InStack;
197
198 #[allow(clippy::cast_possible_truncation)]
200 let neighbors = graph.outgoing_neighbors(NodeId(node as u32))?;
201
202 for &neighbor in neighbors {
203 let neighbor_idx = neighbor as usize;
204
205 match state[neighbor_idx] {
206 NodeState::InStack => {
207 return Err(anyhow!("Cycle detected: cannot compute topological order"));
209 }
210 NodeState::Unvisited => {
211 toposort_dfs(graph, neighbor_idx, state, result)?;
212 }
213 NodeState::Finished => {
214 }
216 }
217 }
218
219 state[node] = NodeState::Finished;
220
221 #[allow(clippy::cast_possible_truncation)]
222 result.push(NodeId(node as u32));
223
224 Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_empty_graph_not_cyclic() {
233 let graph = CsrGraph::new();
234 assert!(!is_cyclic(&graph));
235 }
236
237 #[test]
238 fn test_empty_graph_toposort() {
239 let graph = CsrGraph::new();
240 let order = toposort(&graph).unwrap();
241 assert!(order.is_empty());
242 }
243
244 #[test]
245 fn test_single_node_not_cyclic() {
246 let mut graph = CsrGraph::new();
247 graph.add_edge(NodeId(0), NodeId(1), 1.0).unwrap();
248 assert!(!is_cyclic(&graph));
250 }
251
252 #[test]
253 fn test_self_loop_is_cyclic() {
254 let edges = vec![(NodeId(0), NodeId(0), 1.0)]; let graph = CsrGraph::from_edge_list(&edges).unwrap();
256 assert!(is_cyclic(&graph));
257 }
258
259 #[test]
260 fn test_simple_dag_not_cyclic() {
261 let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(2), 1.0)];
263 let graph = CsrGraph::from_edge_list(&edges).unwrap();
264 assert!(!is_cyclic(&graph));
265 }
266
267 #[test]
268 fn test_simple_cycle() {
269 let edges = vec![
271 (NodeId(0), NodeId(1), 1.0),
272 (NodeId(1), NodeId(2), 1.0),
273 (NodeId(2), NodeId(0), 1.0),
274 ];
275 let graph = CsrGraph::from_edge_list(&edges).unwrap();
276 assert!(is_cyclic(&graph));
277 }
278
279 #[test]
280 fn test_diamond_dag_not_cyclic() {
281 let edges = vec![
283 (NodeId(0), NodeId(1), 1.0),
284 (NodeId(0), NodeId(2), 1.0),
285 (NodeId(1), NodeId(3), 1.0),
286 (NodeId(2), NodeId(3), 1.0),
287 ];
288 let graph = CsrGraph::from_edge_list(&edges).unwrap();
289 assert!(!is_cyclic(&graph));
290 }
291
292 #[test]
293 fn test_toposort_simple_chain() {
294 let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(2), 1.0)];
296 let graph = CsrGraph::from_edge_list(&edges).unwrap();
297
298 let order = toposort(&graph).unwrap();
299 assert_eq!(order, vec![NodeId(0), NodeId(1), NodeId(2)]);
300 }
301
302 #[test]
303 fn test_toposort_diamond() {
304 let edges = vec![
306 (NodeId(0), NodeId(1), 1.0),
307 (NodeId(0), NodeId(2), 1.0),
308 (NodeId(1), NodeId(3), 1.0),
309 (NodeId(2), NodeId(3), 1.0),
310 ];
311 let graph = CsrGraph::from_edge_list(&edges).unwrap();
312
313 let order = toposort(&graph).unwrap();
314
315 let pos = |n: u32| order.iter().position(|&x| x == NodeId(n)).unwrap();
317
318 assert!(pos(0) < pos(1), "0 must come before 1");
319 assert!(pos(0) < pos(2), "0 must come before 2");
320 assert!(pos(1) < pos(3), "1 must come before 3");
321 assert!(pos(2) < pos(3), "2 must come before 3");
322 }
323
324 #[test]
325 fn test_toposort_fails_on_cycle() {
326 let edges = vec![
328 (NodeId(0), NodeId(1), 1.0),
329 (NodeId(1), NodeId(2), 1.0),
330 (NodeId(2), NodeId(0), 1.0),
331 ];
332 let graph = CsrGraph::from_edge_list(&edges).unwrap();
333
334 let result = toposort(&graph);
335 assert!(result.is_err());
336 assert!(result.unwrap_err().to_string().contains("Cycle"));
337 }
338
339 #[test]
340 fn test_disconnected_components() {
341 let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(2), NodeId(3), 1.0)];
343 let graph = CsrGraph::from_edge_list(&edges).unwrap();
344
345 assert!(!is_cyclic(&graph));
346
347 let order = toposort(&graph).unwrap();
348 assert_eq!(order.len(), 4);
349
350 let pos = |n: u32| order.iter().position(|&x| x == NodeId(n)).unwrap();
352 assert!(pos(0) < pos(1));
353 assert!(pos(2) < pos(3));
354 }
355
356 #[test]
357 fn test_complex_dag() {
358 let edges = vec![
365 (NodeId(0), NodeId(1), 1.0),
366 (NodeId(0), NodeId(2), 1.0),
367 (NodeId(0), NodeId(3), 1.0),
368 (NodeId(1), NodeId(4), 1.0),
369 (NodeId(2), NodeId(4), 1.0),
370 (NodeId(3), NodeId(4), 1.0),
371 ];
372 let graph = CsrGraph::from_edge_list(&edges).unwrap();
373
374 assert!(!is_cyclic(&graph));
375
376 let order = toposort(&graph).unwrap();
377
378 assert_eq!(order[0], NodeId(0));
380 assert_eq!(order[4], NodeId(4));
381 }
382
383 #[test]
384 fn test_two_node_cycle() {
385 let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(0), 1.0)];
387 let graph = CsrGraph::from_edge_list(&edges).unwrap();
388 assert!(is_cyclic(&graph));
389 }
390
391 #[test]
392 fn test_cycle_in_subgraph() {
393 let edges = vec![
395 (NodeId(0), NodeId(1), 1.0),
396 (NodeId(1), NodeId(2), 1.0),
397 (NodeId(2), NodeId(1), 1.0), (NodeId(3), NodeId(4), 1.0),
399 ];
400 let graph = CsrGraph::from_edge_list(&edges).unwrap();
401 assert!(is_cyclic(&graph));
402 }
403}