1use crate::ast::Module;
12use std::collections::{HashMap, HashSet};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ImportCycle {
22 pub modules: Vec<String>,
24}
25
26impl ImportCycle {
27 pub fn display(&self) -> String {
30 self.modules.join(" → ")
31 }
32}
33
34pub fn detect_cycles(modules: &[Module]) -> Vec<ImportCycle> {
52 let known: HashSet<&str> = modules.iter().map(|m| m.name.as_str()).collect();
54
55 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
58 for m in modules {
59 let deps: Vec<&str> = m
60 .imports
61 .iter()
62 .filter_map(|imp| {
63 let n = imp.module_name.as_str();
64 if known.contains(n) {
65 Some(n)
66 } else {
67 None
68 }
69 })
70 .collect();
71 adj.insert(m.name.as_str(), deps);
72 }
73
74 let mut globally_visited: HashSet<&str> = HashSet::new();
75 let mut path: Vec<&str> = Vec::new();
76 let mut path_set: HashSet<&str> = HashSet::new();
77 let mut cycles: Vec<ImportCycle> = Vec::new();
78 let mut seen: HashSet<Vec<String>> = HashSet::new();
80
81 for m in modules {
82 let name = m.name.as_str();
83 if !globally_visited.contains(name) {
84 dfs(
85 name,
86 &adj,
87 &mut globally_visited,
88 &mut path,
89 &mut path_set,
90 &mut cycles,
91 &mut seen,
92 );
93 }
94 }
95
96 cycles
97}
98
99pub fn topological_order(modules: &[Module]) -> Vec<usize> {
124 let name_to_idx: HashMap<&str, usize> = modules
126 .iter()
127 .enumerate()
128 .map(|(i, m)| (m.name.as_str(), i))
129 .collect();
130
131 let mut in_degree = vec![0usize; modules.len()];
133 let mut rev_adj: Vec<Vec<usize>> = vec![Vec::new(); modules.len()];
135
136 for (i, m) in modules.iter().enumerate() {
137 for imp in &m.imports {
138 if let Some(&j) = name_to_idx.get(imp.module_name.as_str()) {
139 in_degree[i] += 1;
141 rev_adj[j].push(i);
142 }
143 }
144 }
145
146 let mut queue: std::collections::VecDeque<usize> = in_degree
148 .iter()
149 .enumerate()
150 .filter(|(_, &d)| d == 0)
151 .map(|(i, _)| i)
152 .collect();
153
154 let mut order = Vec::with_capacity(modules.len());
155 while let Some(idx) = queue.pop_front() {
156 order.push(idx);
157 for &dependent in &rev_adj[idx] {
158 in_degree[dependent] -= 1;
159 if in_degree[dependent] == 0 {
160 queue.push_back(dependent);
161 }
162 }
163 }
164
165 if order.len() < modules.len() {
169 let emitted: HashSet<usize> = order.iter().copied().collect();
170 for i in 0..modules.len() {
171 if !emitted.contains(&i) {
172 order.push(i);
173 }
174 }
175 }
176
177 order
178}
179
180fn dfs<'a>(
185 node: &'a str,
186 adj: &HashMap<&'a str, Vec<&'a str>>,
187 globally_visited: &mut HashSet<&'a str>,
188 path: &mut Vec<&'a str>,
189 path_set: &mut HashSet<&'a str>,
190 cycles: &mut Vec<ImportCycle>,
191 seen: &mut HashSet<Vec<String>>,
192) {
193 if path_set.contains(node) {
194 let start = path.iter().position(|&n| n == node).unwrap();
196 let raw: Vec<&str> = path[start..].to_vec();
197
198 let min_pos = raw
201 .iter()
202 .enumerate()
203 .min_by_key(|(_, &s)| s)
204 .map(|(i, _)| i)
205 .unwrap_or(0);
206 let mut canonical: Vec<String> = raw[min_pos..]
207 .iter()
208 .chain(raw[..min_pos].iter())
209 .map(|&s| s.to_string())
210 .collect();
211 canonical.push(canonical[0].clone()); if seen.insert(canonical.clone()) {
214 cycles.push(ImportCycle { modules: canonical });
215 }
216 return;
217 }
218
219 if globally_visited.contains(node) {
220 return;
222 }
223
224 path.push(node);
226 path_set.insert(node);
227
228 if let Some(neighbors) = adj.get(node) {
229 for &neighbor in neighbors {
230 dfs(
231 neighbor,
232 adj,
233 globally_visited,
234 path,
235 path_set,
236 cycles,
237 seen,
238 );
239 }
240 }
241
242 path.pop();
244 path_set.remove(node);
245 globally_visited.insert(node);
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::parse;
252
253 fn make_module(name: &str, imports_from: &[&str]) -> Module {
254 let import_block = if imports_from.is_empty() {
255 String::new()
256 } else {
257 let clauses: Vec<String> = imports_from
258 .iter()
259 .map(|m| format!("Dummy FROM {}", m))
260 .collect();
261 format!("IMPORTS {};", clauses.join(", "))
262 };
263 let src = format!("{} DEFINITIONS ::= BEGIN {} END", name, import_block);
264 parse(&src).unwrap()
265 }
266
267 #[test]
268 fn test_no_cycles_single_module() {
269 let m = make_module("Alpha", &[]);
270 assert!(detect_cycles(&[m]).is_empty());
271 }
272
273 #[test]
274 fn test_no_cycles_linear_chain() {
275 let a = make_module("Alpha", &["Beta"]);
277 let b = make_module("Beta", &["Gamma"]);
278 let c = make_module("Gamma", &[]);
279 assert!(detect_cycles(&[a, b, c]).is_empty());
280 }
281
282 #[test]
283 fn test_direct_cycle_two_modules() {
284 let a = make_module("Alpha", &["Beta"]);
286 let b = make_module("Beta", &["Alpha"]);
287 let cycles = detect_cycles(&[a, b]);
288 assert_eq!(cycles.len(), 1);
289 assert_eq!(cycles[0].modules, vec!["Alpha", "Beta", "Alpha"]);
291 }
292
293 #[test]
294 fn test_three_module_cycle() {
295 let a = make_module("Alpha", &["Beta"]);
297 let b = make_module("Beta", &["Gamma"]);
298 let c = make_module("Gamma", &["Alpha"]);
299 let cycles = detect_cycles(&[a, b, c]);
300 assert_eq!(cycles.len(), 1);
301 assert_eq!(cycles[0].modules, vec!["Alpha", "Beta", "Gamma", "Alpha"]);
302 }
303
304 #[test]
305 fn test_unknown_import_ignored() {
306 let a = make_module("Alpha", &["Unknown"]);
308 assert!(detect_cycles(&[a]).is_empty());
309 }
310
311 #[test]
312 fn test_self_import_is_cycle() {
313 let a = make_module("Alpha", &["Alpha"]);
315 let cycles = detect_cycles(&[a]);
316 assert_eq!(cycles.len(), 1);
317 assert_eq!(cycles[0].modules, vec!["Alpha", "Alpha"]);
318 }
319
320 #[test]
321 fn test_display_format() {
322 let cycle = ImportCycle {
323 modules: vec!["Alpha".to_string(), "Beta".to_string(), "Alpha".to_string()],
324 };
325 assert_eq!(cycle.display(), "Alpha → Beta → Alpha");
326 }
327
328 #[test]
329 fn test_cycle_reported_once() {
330 let a = make_module("Alpha", &["Beta"]);
333 let b = make_module("Beta", &["Alpha"]);
334 let cycles1 = detect_cycles(&[a.clone(), b.clone()]);
336 let cycles2 = detect_cycles(&[b.clone(), a.clone()]);
337 assert_eq!(cycles1.len(), 1);
338 assert_eq!(cycles2.len(), 1);
339 assert_eq!(cycles1[0].modules, cycles2[0].modules);
340 }
341
342 #[test]
345 fn test_topo_single_module() {
346 let m = make_module("Alpha", &[]);
347 assert_eq!(topological_order(&[m]), vec![0]);
348 }
349
350 #[test]
351 fn test_topo_linear_chain() {
352 let a = make_module("Alpha", &["Beta"]);
354 let b = make_module("Beta", &["Gamma"]);
355 let c = make_module("Gamma", &[]);
356 let order = topological_order(&[a, b, c]);
357 assert_eq!(order, vec![2, 1, 0]);
359 }
360
361 #[test]
362 fn test_topo_multiple_independent_leaves() {
363 let a = make_module("Alpha", &["Beta"]);
367 let b = make_module("Beta", &[]);
368 let c = make_module("Gamma", &[]);
369 let order = topological_order(&[a, b, c]);
370 let alpha_pos = order.iter().position(|&i| i == 0).unwrap();
371 let beta_pos = order.iter().position(|&i| i == 1).unwrap();
372 assert!(beta_pos < alpha_pos, "Beta must be generated before Alpha");
373 }
374
375 #[test]
376 fn test_topo_no_cross_deps() {
377 let a = make_module("Alpha", &[]);
379 let b = make_module("Beta", &[]);
380 let order = topological_order(&[a, b]);
381 assert_eq!(order, vec![0, 1]);
382 }
383}