1use std::collections::{HashMap, HashSet, VecDeque};
22
23use crate::compiled::CompiledGraph;
24use crate::state::GraphState;
25use crate::viz::extract_edges;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct GraphAnalysis {
30 pub node_count: usize,
32 pub edge_count: usize,
34 pub has_cycle: bool,
37 pub depth: usize,
41 pub unreachable: Vec<String>,
44}
45
46impl<S: GraphState> CompiledGraph<S> {
47 pub fn analyze(&self) -> GraphAnalysis {
49 let nodes: Vec<&String> = self.graph.nodes.keys().collect();
50 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
51 for node in &nodes {
52 adj.insert(node.as_str(), Vec::new());
53 }
54 let edges = extract_edges(self);
55 for e in &edges {
56 adj.entry(e.from.as_str()).or_default().push(e.to.as_str());
57 }
58
59 let start = self.graph.start.as_deref();
60
61 let mut reachable: HashSet<&str> = HashSet::new();
64 if let Some(s) = start {
65 let mut q: VecDeque<&str> = VecDeque::from([s]);
66 reachable.insert(s);
67 while let Some(n) = q.pop_front() {
68 if let Some(nexts) = adj.get(n) {
69 for &m in nexts {
70 if m == "__END__" {
71 continue;
72 }
73 if reachable.insert(m) {
74 q.push_back(m);
75 }
76 }
77 }
78 }
79 }
80 let mut unreachable: Vec<String> = nodes
81 .iter()
82 .filter(|n| !reachable.contains(n.as_str()))
83 .map(|n| (*n).clone())
84 .collect();
85 unreachable.sort();
86
87 let has_cycle = match start {
89 Some(s) => detect_cycle(s, &adj, &reachable),
90 None => false,
91 };
92
93 let depth = match start {
95 Some(s) if !has_cycle => longest_path(s, &adj, &reachable),
96 _ => 0,
97 };
98
99 GraphAnalysis {
100 node_count: self.graph.nodes.len(),
101 edge_count: edges.len(),
102 has_cycle,
103 depth,
104 unreachable,
105 }
106 }
107}
108
109fn detect_cycle<'a>(
110 start: &'a str,
111 adj: &HashMap<&'a str, Vec<&'a str>>,
112 reachable: &HashSet<&'a str>,
113) -> bool {
114 enum Color {
115 White,
116 Gray,
117 Black,
118 }
119 let mut color: HashMap<&str, Color> = HashMap::new();
120 for &n in reachable {
121 color.insert(n, Color::White);
122 }
123 fn visit<'a>(
124 node: &'a str,
125 adj: &HashMap<&'a str, Vec<&'a str>>,
126 reachable: &HashSet<&'a str>,
127 color: &mut HashMap<&'a str, Color>,
128 ) -> bool {
129 color.insert(node, Color::Gray);
130 if let Some(nexts) = adj.get(node) {
131 for &m in nexts {
132 if m == "__END__" || !reachable.contains(m) {
133 continue;
134 }
135 let recurse = match color.get(m) {
136 Some(Color::Gray) => return true, Some(Color::White) => true,
138 _ => false,
139 };
140 if recurse && visit(m, adj, reachable, color) {
141 return true;
142 }
143 }
144 }
145 color.insert(node, Color::Black);
146 false
147 }
148 visit(start, adj, reachable, &mut color)
149}
150
151fn longest_path<'a>(
152 start: &'a str,
153 adj: &HashMap<&'a str, Vec<&'a str>>,
154 reachable: &HashSet<&'a str>,
155) -> usize {
156 fn dfs<'a>(
158 node: &'a str,
159 adj: &HashMap<&'a str, Vec<&'a str>>,
160 reachable: &HashSet<&'a str>,
161 memo: &mut HashMap<&'a str, usize>,
162 ) -> usize {
163 if let Some(&d) = memo.get(node) {
164 return d;
165 }
166 let mut best = 0;
167 if let Some(nexts) = adj.get(node) {
168 for &m in nexts {
169 if m == "__END__" || !reachable.contains(m) {
170 continue;
171 }
172 let d = 1 + dfs(m, adj, reachable, memo);
173 if d > best {
174 best = d;
175 }
176 }
177 }
178 memo.insert(node, best);
179 best
180 }
181 let mut memo: HashMap<&str, usize> = HashMap::new();
182 dfs(start, adj, reachable, &mut memo)
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use crate::builder::Graph;
189 use crate::goto::Goto;
190 use crate::node::{node_fn, NodeOut};
191
192 #[derive(Default, Clone)]
193 struct S;
194 #[derive(Default)]
195 struct SU;
196 impl GraphState for S {
197 type Update = SU;
198 fn apply(&mut self, _: Self::Update) {}
199 }
200
201 fn nf(name: &'static str) -> impl crate::Node<S> + 'static {
202 node_fn::<S, _, _>(name, |_, _| async move {
203 Ok(NodeOut {
204 update: SU,
205 goto: Goto::end(),
206 })
207 })
208 }
209
210 #[test]
211 fn linear_three_node_chain_depth_2() {
212 let g = Graph::<S>::new()
214 .node("a", nf("a"))
215 .node("b", nf("b"))
216 .node("c", nf("c"))
217 .edge("a", "b")
218 .edge("b", "c")
219 .start_at("a")
220 .compile()
221 .unwrap();
222 let r = g.analyze();
223 assert_eq!(r.node_count, 3);
224 assert_eq!(r.edge_count, 2);
225 assert!(!r.has_cycle);
226 assert_eq!(r.depth, 2);
227 assert!(r.unreachable.is_empty());
228 }
229
230 #[test]
231 fn diamond_depth_is_2() {
232 let g = Graph::<S>::new()
235 .node("a", nf("a"))
236 .node("b", nf("b"))
237 .node("c", nf("c"))
238 .node("d", nf("d"))
239 .edge("a", "b")
240 .edge("a", "c")
241 .edge("b", "d")
242 .edge("c", "d")
243 .start_at("a")
244 .compile()
245 .unwrap();
246 let r = g.analyze();
247 assert!(!r.has_cycle);
248 assert_eq!(r.depth, 2);
249 }
250
251 #[test]
252 fn self_loop_is_a_cycle() {
253 let g = Graph::<S>::new()
255 .node("a", nf("a"))
256 .edge("a", "a")
257 .start_at("a")
258 .compile()
259 .unwrap();
260 let r = g.analyze();
261 assert!(r.has_cycle);
262 assert_eq!(r.depth, 0, "depth undefined for graphs with cycles");
263 }
264
265 #[test]
266 fn detects_back_edge_cycle() {
267 let g = Graph::<S>::new()
269 .node("a", nf("a"))
270 .node("b", nf("b"))
271 .node("c", nf("c"))
272 .edge("a", "b")
273 .edge("b", "c")
274 .edge("c", "a")
275 .start_at("a")
276 .compile()
277 .unwrap();
278 let r = g.analyze();
279 assert!(r.has_cycle);
280 }
281
282 #[test]
283 fn unreachable_nodes_listed() {
284 let g = Graph::<S>::new()
286 .node("a", nf("a"))
287 .node("b", nf("b"))
288 .node("c", nf("c"))
289 .edge("a", "b")
290 .start_at("a")
291 .compile()
292 .unwrap();
293 let r = g.analyze();
294 assert_eq!(r.unreachable, vec!["c".to_string()]);
295 assert!(!r.has_cycle);
296 assert_eq!(r.depth, 1);
297 }
298}