1use std::collections::{BTreeMap, BTreeSet, VecDeque};
7
8#[derive(Debug, Clone)]
10pub struct DependencyGraph {
11 pub edges: BTreeMap<String, Vec<String>>,
13 pub nodes: BTreeSet<String>,
15 pub topo_order: Vec<String>,
17 pub cycles: Vec<Vec<String>>,
19}
20
21#[derive(Debug, Clone)]
23pub struct GraphNode {
24 pub stem: String,
26 pub dependents: usize,
28 pub dependencies: usize,
30}
31
32pub fn dependency_graph(contracts: &[(String, &crate::schema::Contract)]) -> DependencyGraph {
37 let mut edges: BTreeMap<String, Vec<String>> = BTreeMap::new();
38 let mut nodes = BTreeSet::new();
39
40 for (stem, contract) in contracts {
41 nodes.insert(stem.clone());
42 let deps = &contract.metadata.depends_on;
43 edges.insert(stem.clone(), deps.clone());
44 for dep in deps {
45 nodes.insert(dep.clone());
46 }
47 }
48
49 for node in &nodes {
51 edges.entry(node.clone()).or_default();
52 }
53
54 let cycles = detect_cycles(&edges);
55 let topo_order = if cycles.is_empty() {
56 topological_sort(&edges, &nodes)
57 } else {
58 Vec::new()
59 };
60
61 DependencyGraph {
62 edges,
63 nodes,
64 topo_order,
65 cycles,
66 }
67}
68
69pub fn graph_nodes(graph: &DependencyGraph) -> Vec<GraphNode> {
71 let mut dependents_count: BTreeMap<&str, usize> = BTreeMap::new();
72 for deps in graph.edges.values() {
73 for dep in deps {
74 *dependents_count.entry(dep.as_str()).or_default() += 1;
75 }
76 }
77
78 graph
79 .nodes
80 .iter()
81 .map(|stem| GraphNode {
82 stem: stem.clone(),
83 dependents: dependents_count.get(stem.as_str()).copied().unwrap_or(0),
84 dependencies: graph.edges.get(stem).map_or(0, Vec::len),
85 })
86 .collect()
87}
88
89fn topological_sort(
91 edges: &BTreeMap<String, Vec<String>>,
92 nodes: &BTreeSet<String>,
93) -> Vec<String> {
94 let mut reverse_edges: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
96 let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
97 for node in nodes {
98 in_degree.insert(node.as_str(), 0);
99 reverse_edges.entry(node.as_str()).or_default();
100 }
101 for (node, deps) in edges {
103 *in_degree.entry(node.as_str()).or_default() = deps.len();
104 for dep in deps {
105 reverse_edges
106 .entry(dep.as_str())
107 .or_default()
108 .push(node.as_str());
109 }
110 }
111
112 let mut queue: VecDeque<String> = nodes
114 .iter()
115 .filter(|n| in_degree.get(n.as_str()) == Some(&0))
116 .cloned()
117 .collect();
118
119 let mut result = Vec::new();
120 while let Some(node) = queue.pop_front() {
121 result.push(node.clone());
122 if let Some(dependents) = reverse_edges.get(node.as_str()) {
124 for &dependent in dependents {
125 if let Some(deg) = in_degree.get_mut(dependent) {
126 *deg = deg.saturating_sub(1);
127 if *deg == 0 {
128 queue.push_back(dependent.to_string());
129 }
130 }
131 }
132 }
133 }
134
135 result
136}
137
138#[derive(Clone, Copy, PartialEq)]
139enum DfsColor {
140 White,
141 Gray,
142 Black,
143}
144
145fn dfs_visit<'a>(
146 node: &'a str,
147 edges: &'a BTreeMap<String, Vec<String>>,
148 color: &mut BTreeMap<&'a str, DfsColor>,
149 path: &mut Vec<String>,
150 cycles: &mut Vec<Vec<String>>,
151) {
152 color.insert(node, DfsColor::Gray);
153 path.push(node.to_string());
154
155 if let Some(deps) = edges.get(node) {
156 for dep in deps {
157 match color.get(dep.as_str()) {
158 Some(DfsColor::Gray) => {
159 if let Some(pos) = path.iter().position(|n| n == dep) {
160 let cycle: Vec<String> = path[pos..].to_vec();
161 cycles.push(cycle);
162 }
163 }
164 Some(DfsColor::White) | None => {
165 dfs_visit(dep.as_str(), edges, color, path, cycles);
166 }
167 Some(DfsColor::Black) => {}
168 }
169 }
170 }
171
172 path.pop();
173 color.insert(node, DfsColor::Black);
174}
175
176fn detect_cycles(edges: &BTreeMap<String, Vec<String>>) -> Vec<Vec<String>> {
178 let mut color: BTreeMap<&str, DfsColor> = BTreeMap::new();
179 for key in edges.keys() {
180 color.insert(key.as_str(), DfsColor::White);
181 }
182
183 let mut cycles = Vec::new();
184 let keys: Vec<String> = edges.keys().cloned().collect();
185 let mut path = Vec::new();
186 for node in &keys {
187 if color.get(node.as_str()) == Some(&DfsColor::White) {
188 dfs_visit(node.as_str(), edges, &mut color, &mut path, &mut cycles);
189 }
190 }
191
192 cycles
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::schema::parse_contract_str;
199
200 fn contract_with_deps(deps: &[&str]) -> crate::schema::Contract {
201 let deps_yaml = if deps.is_empty() {
202 String::new()
203 } else {
204 let items: Vec<String> = deps.iter().map(|d| format!(" - \"{d}\"")).collect();
205 format!(" depends_on:\n{}", items.join("\n"))
206 };
207 parse_contract_str(&format!(
208 r#"
209metadata:
210 version: "1.0.0"
211 description: "Test"
212 references: ["Paper"]
213{deps_yaml}
214equations:
215 f:
216 formula: "f(x) = x"
217falsification_tests: []
218"#
219 ))
220 .unwrap()
221 }
222
223 #[test]
224 fn empty_graph() {
225 let graph = dependency_graph(&[]);
226 assert!(graph.nodes.is_empty());
227 assert!(graph.edges.is_empty());
228 assert!(graph.cycles.is_empty());
229 }
230
231 #[test]
232 fn single_node_no_deps() {
233 let c = contract_with_deps(&[]);
234 let graph = dependency_graph(&[("softmax".to_string(), &c)]);
235 assert_eq!(graph.nodes.len(), 1);
236 assert!(graph.cycles.is_empty());
237 assert_eq!(graph.topo_order.len(), 1);
238 }
239
240 #[test]
241 fn linear_chain() {
242 let a = contract_with_deps(&["b"]);
243 let b = contract_with_deps(&["c"]);
244 let c = contract_with_deps(&[]);
245 let graph = dependency_graph(&[
246 ("a".to_string(), &a),
247 ("b".to_string(), &b),
248 ("c".to_string(), &c),
249 ]);
250 assert_eq!(graph.nodes.len(), 3);
251 assert!(graph.cycles.is_empty());
252 assert_eq!(graph.topo_order.len(), 3);
253 }
254
255 #[test]
256 fn cycle_detected() {
257 let a = contract_with_deps(&["b"]);
258 let b = contract_with_deps(&["a"]);
259 let graph = dependency_graph(&[("a".to_string(), &a), ("b".to_string(), &b)]);
260 assert!(!graph.cycles.is_empty());
261 assert!(graph.topo_order.is_empty());
262 }
263
264 #[test]
265 fn diamond_dependency() {
266 let a = contract_with_deps(&["b", "c"]);
267 let b = contract_with_deps(&["d"]);
268 let c = contract_with_deps(&["d"]);
269 let d = contract_with_deps(&[]);
270 let graph = dependency_graph(&[
271 ("a".to_string(), &a),
272 ("b".to_string(), &b),
273 ("c".to_string(), &c),
274 ("d".to_string(), &d),
275 ]);
276 assert_eq!(graph.nodes.len(), 4);
277 assert!(graph.cycles.is_empty());
278 assert_eq!(graph.topo_order.len(), 4);
279 }
280
281 #[test]
282 fn graph_nodes_metadata() {
283 let a = contract_with_deps(&["b"]);
284 let b = contract_with_deps(&[]);
285 let graph = dependency_graph(&[("a".to_string(), &a), ("b".to_string(), &b)]);
286 let nodes = graph_nodes(&graph);
287 assert_eq!(nodes.len(), 2);
288
289 let a_node = nodes.iter().find(|n| n.stem == "a").unwrap();
290 assert_eq!(a_node.dependencies, 1);
291 assert_eq!(a_node.dependents, 0);
292
293 let b_node = nodes.iter().find(|n| n.stem == "b").unwrap();
294 assert_eq!(b_node.dependencies, 0);
295 assert_eq!(b_node.dependents, 1);
296 }
297
298 #[test]
299 fn external_dependency_added_to_nodes() {
300 let a = contract_with_deps(&["external-crate"]);
301 let graph = dependency_graph(&[("a".to_string(), &a)]);
302 assert!(graph.nodes.contains("external-crate"));
303 assert_eq!(graph.nodes.len(), 2);
304 }
305
306 #[test]
307 fn topo_order_respects_deps() {
308 let a = contract_with_deps(&["b"]);
309 let b = contract_with_deps(&[]);
310 let graph = dependency_graph(&[("a".to_string(), &a), ("b".to_string(), &b)]);
311 let a_pos = graph.topo_order.iter().position(|n| n == "a").unwrap();
312 let b_pos = graph.topo_order.iter().position(|n| n == "b").unwrap();
313 assert!(b_pos < a_pos);
316 }
317}