1#[derive(Debug, Clone, PartialEq, Eq)]
21pub(crate) struct LiveGraphAnalysis {
22 pub in_cycle: Vec<bool>,
25 pub cycle_count: usize,
27 pub topo: Vec<u32>,
32}
33
34impl LiveGraphAnalysis {
35 pub fn topo_positions(&self) -> Vec<u32> {
37 let mut pos = vec![0u32; self.topo.len()];
38 for (p, &i) in self.topo.iter().enumerate() {
39 pos[i as usize] = p as u32;
40 }
41 pos
42 }
43}
44
45pub(crate) fn analyze_live_graph(n: usize, edges: &[(u32, u32)]) -> LiveGraphAnalysis {
48 debug_assert!(
49 edges.is_sorted(),
50 "live edges must be sorted for determinism"
51 );
52
53 let mut adj_start = vec![0usize; n + 1];
55 for &(from, _) in edges {
56 adj_start[from as usize + 1] += 1;
57 }
58 for i in 0..n {
59 adj_start[i + 1] += adj_start[i];
60 }
61 let adj = |i: usize| -> &[(u32, u32)] { &edges[adj_start[i]..adj_start[i + 1]] };
63
64 const UNVISITED: u32 = u32::MAX;
65 let mut index = vec![UNVISITED; n];
66 let mut lowlink = vec![0u32; n];
67 let mut on_stack = vec![false; n];
68 let mut stack: Vec<u32> = Vec::new();
69 let mut next_index = 0u32;
70
71 let mut in_cycle = vec![false; n];
72 let mut cycle_count = 0usize;
73 let mut topo: Vec<u32> = Vec::with_capacity(n);
76
77 let mut frames: Vec<(u32, usize)> = Vec::new();
79 for root in 0..n as u32 {
80 if index[root as usize] != UNVISITED {
81 continue;
82 }
83 frames.push((root, 0));
84 index[root as usize] = next_index;
85 lowlink[root as usize] = next_index;
86 next_index += 1;
87 stack.push(root);
88 on_stack[root as usize] = true;
89
90 while let Some(&(v, next_edge)) = frames.last() {
91 let vu = v as usize;
92 if let Some(&(_, w)) = adj(vu).get(next_edge) {
93 frames.last_mut().expect("frame exists").1 += 1;
94 let wu = w as usize;
95 if index[wu] == UNVISITED {
96 index[wu] = next_index;
97 lowlink[wu] = next_index;
98 next_index += 1;
99 stack.push(w);
100 on_stack[wu] = true;
101 frames.push((w, 0));
102 } else if on_stack[wu] {
103 lowlink[vu] = lowlink[vu].min(index[wu]);
104 }
105 } else {
106 if lowlink[vu] == index[vu] {
108 let scc_start = topo.len();
109 loop {
110 let w = stack.pop().expect("tarjan stack underflow");
111 on_stack[w as usize] = false;
112 topo.push(w);
113 if w == v {
114 break;
115 }
116 }
117 let members = &mut topo[scc_start..];
118 members.sort_unstable();
120 let cyclic = members.len() > 1 || adj(vu).iter().any(|&(_, w)| w == v); if cyclic {
122 cycle_count += 1;
123 for &m in topo[scc_start..].iter() {
124 in_cycle[m as usize] = true;
125 }
126 }
127 }
128 frames.pop();
129 if let Some(&(parent, _)) = frames.last() {
130 let pu = parent as usize;
131 lowlink[pu] = lowlink[pu].min(lowlink[vu]);
132 }
133 }
134 }
135 }
136
137 LiveGraphAnalysis {
138 in_cycle,
139 cycle_count,
140 topo,
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn analyze(n: usize, mut edges: Vec<(u32, u32)>) -> LiveGraphAnalysis {
149 edges.sort_unstable();
150 edges.dedup();
151 analyze_live_graph(n, &edges)
152 }
153
154 fn assert_topo_consistent(a: &LiveGraphAnalysis, n: usize, edges: &[(u32, u32)]) {
155 assert_eq!(a.topo.len(), n);
156 let pos = a.topo_positions();
157 for &(from, to) in edges {
158 if from == to {
159 continue;
160 }
161 if a.in_cycle[from as usize] && a.in_cycle[to as usize] {
163 continue;
164 }
165 assert!(
166 pos[to as usize] < pos[from as usize],
167 "dependency {to} must precede reader {from} in topo {:?}",
168 a.topo
169 );
170 }
171 }
172
173 #[test]
174 fn empty_graph_is_acyclic() {
175 let a = analyze(3, vec![]);
176 assert_eq!(a.cycle_count, 0);
177 assert_eq!(a.in_cycle, vec![false; 3]);
178 assert_eq!(a.topo.len(), 3);
179 }
180
181 #[test]
182 fn self_loop_is_a_cycle() {
183 let a = analyze(2, vec![(0, 0)]);
184 assert_eq!(a.cycle_count, 1);
185 assert_eq!(a.in_cycle, vec![true, false]);
186 }
187
188 #[test]
189 fn two_cycle_detected() {
190 let edges = vec![(0, 1), (1, 0)];
191 let a = analyze(2, edges.clone());
192 assert_eq!(a.cycle_count, 1);
193 assert_eq!(a.in_cycle, vec![true, true]);
194 assert_topo_consistent(&a, 2, &edges);
195 }
196
197 #[test]
198 fn chain_is_acyclic_with_deps_first_topo() {
199 let edges = vec![(0, 1), (1, 2)];
201 let a = analyze(3, edges.clone());
202 assert_eq!(a.cycle_count, 0);
203 assert_eq!(a.in_cycle, vec![false; 3]);
204 assert_eq!(a.topo, vec![2, 1, 0]);
205 assert_topo_consistent(&a, 3, &edges);
206 }
207
208 #[test]
209 fn disjoint_components_cycle_and_chain() {
210 let edges = vec![(0, 1), (1, 0), (2, 3)];
213 let a = analyze(5, edges.clone());
214 assert_eq!(a.cycle_count, 1);
215 assert_eq!(a.in_cycle, vec![true, true, false, false, false]);
216 assert_topo_consistent(&a, 5, &edges);
217 }
218
219 #[test]
220 fn cycle_with_downstream_reader() {
221 let edges = vec![(0, 1), (1, 0), (2, 0), (3, 2)];
223 let a = analyze(4, edges.clone());
224 assert_eq!(a.cycle_count, 1);
225 assert_eq!(a.in_cycle, vec![true, true, false, false]);
226 let pos = a.topo_positions();
227 assert!(pos[0] < pos[2] && pos[1] < pos[2] && pos[2] < pos[3]);
228 }
229
230 #[test]
231 fn two_distinct_cycles_counted() {
232 let edges = vec![(0, 1), (1, 0), (2, 2)];
233 let a = analyze(3, edges);
234 assert_eq!(a.cycle_count, 2);
235 assert_eq!(a.in_cycle, vec![true, true, true]);
236 }
237
238 #[test]
239 fn deterministic_for_same_input() {
240 let edges = vec![(0, 2), (1, 2), (2, 3), (4, 0), (4, 1)];
241 let a1 = analyze(5, edges.clone());
242 let a2 = analyze(5, edges);
243 assert_eq!(a1, a2);
244 }
245}