use crate::ast::Module;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportCycle {
pub modules: Vec<String>,
}
impl ImportCycle {
pub fn display(&self) -> String {
self.modules.join(" → ")
}
}
pub fn detect_cycles(modules: &[Module]) -> Vec<ImportCycle> {
let known: HashSet<&str> = modules.iter().map(|m| m.name.as_str()).collect();
let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
for m in modules {
let deps: Vec<&str> = m
.imports
.iter()
.filter_map(|imp| {
let n = imp.module_name.as_str();
if known.contains(n) {
Some(n)
} else {
None
}
})
.collect();
adj.insert(m.name.as_str(), deps);
}
let mut globally_visited: HashSet<&str> = HashSet::new();
let mut path: Vec<&str> = Vec::new();
let mut path_set: HashSet<&str> = HashSet::new();
let mut cycles: Vec<ImportCycle> = Vec::new();
let mut seen: HashSet<Vec<String>> = HashSet::new();
for m in modules {
let name = m.name.as_str();
if !globally_visited.contains(name) {
dfs(
name,
&adj,
&mut globally_visited,
&mut path,
&mut path_set,
&mut cycles,
&mut seen,
);
}
}
cycles
}
pub fn topological_order(modules: &[Module]) -> Vec<usize> {
let name_to_idx: HashMap<&str, usize> = modules
.iter()
.enumerate()
.map(|(i, m)| (m.name.as_str(), i))
.collect();
let mut in_degree = vec![0usize; modules.len()];
let mut rev_adj: Vec<Vec<usize>> = vec![Vec::new(); modules.len()];
for (i, m) in modules.iter().enumerate() {
for imp in &m.imports {
if let Some(&j) = name_to_idx.get(imp.module_name.as_str()) {
in_degree[i] += 1;
rev_adj[j].push(i);
}
}
}
let mut queue: std::collections::VecDeque<usize> = in_degree
.iter()
.enumerate()
.filter(|(_, &d)| d == 0)
.map(|(i, _)| i)
.collect();
let mut order = Vec::with_capacity(modules.len());
while let Some(idx) = queue.pop_front() {
order.push(idx);
for &dependent in &rev_adj[idx] {
in_degree[dependent] -= 1;
if in_degree[dependent] == 0 {
queue.push_back(dependent);
}
}
}
if order.len() < modules.len() {
let emitted: HashSet<usize> = order.iter().copied().collect();
for i in 0..modules.len() {
if !emitted.contains(&i) {
order.push(i);
}
}
}
order
}
fn dfs<'a>(
node: &'a str,
adj: &HashMap<&'a str, Vec<&'a str>>,
globally_visited: &mut HashSet<&'a str>,
path: &mut Vec<&'a str>,
path_set: &mut HashSet<&'a str>,
cycles: &mut Vec<ImportCycle>,
seen: &mut HashSet<Vec<String>>,
) {
if path_set.contains(node) {
let start = path.iter().position(|&n| n == node).unwrap();
let raw: Vec<&str> = path[start..].to_vec();
let min_pos = raw
.iter()
.enumerate()
.min_by_key(|(_, &s)| s)
.map(|(i, _)| i)
.unwrap_or(0);
let mut canonical: Vec<String> = raw[min_pos..]
.iter()
.chain(raw[..min_pos].iter())
.map(|&s| s.to_string())
.collect();
canonical.push(canonical[0].clone());
if seen.insert(canonical.clone()) {
cycles.push(ImportCycle { modules: canonical });
}
return;
}
if globally_visited.contains(node) {
return;
}
path.push(node);
path_set.insert(node);
if let Some(neighbors) = adj.get(node) {
for &neighbor in neighbors {
dfs(
neighbor,
adj,
globally_visited,
path,
path_set,
cycles,
seen,
);
}
}
path.pop();
path_set.remove(node);
globally_visited.insert(node);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
fn make_module(name: &str, imports_from: &[&str]) -> Module {
let import_block = if imports_from.is_empty() {
String::new()
} else {
let clauses: Vec<String> = imports_from
.iter()
.map(|m| format!("Dummy FROM {}", m))
.collect();
format!("IMPORTS {};", clauses.join(", "))
};
let src = format!("{} DEFINITIONS ::= BEGIN {} END", name, import_block);
parse(&src).unwrap()
}
#[test]
fn test_no_cycles_single_module() {
let m = make_module("Alpha", &[]);
assert!(detect_cycles(&[m]).is_empty());
}
#[test]
fn test_no_cycles_linear_chain() {
let a = make_module("Alpha", &["Beta"]);
let b = make_module("Beta", &["Gamma"]);
let c = make_module("Gamma", &[]);
assert!(detect_cycles(&[a, b, c]).is_empty());
}
#[test]
fn test_direct_cycle_two_modules() {
let a = make_module("Alpha", &["Beta"]);
let b = make_module("Beta", &["Alpha"]);
let cycles = detect_cycles(&[a, b]);
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0].modules, vec!["Alpha", "Beta", "Alpha"]);
}
#[test]
fn test_three_module_cycle() {
let a = make_module("Alpha", &["Beta"]);
let b = make_module("Beta", &["Gamma"]);
let c = make_module("Gamma", &["Alpha"]);
let cycles = detect_cycles(&[a, b, c]);
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0].modules, vec!["Alpha", "Beta", "Gamma", "Alpha"]);
}
#[test]
fn test_unknown_import_ignored() {
let a = make_module("Alpha", &["Unknown"]);
assert!(detect_cycles(&[a]).is_empty());
}
#[test]
fn test_self_import_is_cycle() {
let a = make_module("Alpha", &["Alpha"]);
let cycles = detect_cycles(&[a]);
assert_eq!(cycles.len(), 1);
assert_eq!(cycles[0].modules, vec!["Alpha", "Alpha"]);
}
#[test]
fn test_display_format() {
let cycle = ImportCycle {
modules: vec!["Alpha".to_string(), "Beta".to_string(), "Alpha".to_string()],
};
assert_eq!(cycle.display(), "Alpha → Beta → Alpha");
}
#[test]
fn test_cycle_reported_once() {
let a = make_module("Alpha", &["Beta"]);
let b = make_module("Beta", &["Alpha"]);
let cycles1 = detect_cycles(&[a.clone(), b.clone()]);
let cycles2 = detect_cycles(&[b.clone(), a.clone()]);
assert_eq!(cycles1.len(), 1);
assert_eq!(cycles2.len(), 1);
assert_eq!(cycles1[0].modules, cycles2[0].modules);
}
#[test]
fn test_topo_single_module() {
let m = make_module("Alpha", &[]);
assert_eq!(topological_order(&[m]), vec![0]);
}
#[test]
fn test_topo_linear_chain() {
let a = make_module("Alpha", &["Beta"]);
let b = make_module("Beta", &["Gamma"]);
let c = make_module("Gamma", &[]);
let order = topological_order(&[a, b, c]);
assert_eq!(order, vec![2, 1, 0]);
}
#[test]
fn test_topo_multiple_independent_leaves() {
let a = make_module("Alpha", &["Beta"]);
let b = make_module("Beta", &[]);
let c = make_module("Gamma", &[]);
let order = topological_order(&[a, b, c]);
let alpha_pos = order.iter().position(|&i| i == 0).unwrap();
let beta_pos = order.iter().position(|&i| i == 1).unwrap();
assert!(beta_pos < alpha_pos, "Beta must be generated before Alpha");
}
#[test]
fn test_topo_no_cross_deps() {
let a = make_module("Alpha", &[]);
let b = make_module("Beta", &[]);
let order = topological_order(&[a, b]);
assert_eq!(order, vec![0, 1]);
}
}