use std::collections::{HashMap, HashSet};
use bock_ast::{ImportDecl, Module, ModulePath};
pub type ModuleId = String;
#[derive(Debug, Clone, Default)]
pub struct DepGraph {
edges: HashMap<ModuleId, HashSet<ModuleId>>,
reverse_edges: HashMap<ModuleId, HashSet<ModuleId>>,
}
impl DepGraph {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_modules(modules: &[Module]) -> Self {
let mut graph = Self::new();
for (i, module) in modules.iter().enumerate() {
let module_id = module_id_from_module(module, i);
let deps = extract_dependencies(&module.imports);
graph.add_module_with_deps(module_id, deps);
}
graph
}
pub fn add_module_with_deps(&mut self, module_id: ModuleId, deps: HashSet<ModuleId>) {
for dep in &deps {
self.reverse_edges
.entry(dep.clone())
.or_default()
.insert(module_id.clone());
}
self.edges.insert(module_id, deps);
}
pub fn add_module(&mut self, module_id: ModuleId) {
self.edges.entry(module_id).or_default();
}
#[must_use]
pub fn dependencies(&self, module_id: &str) -> Option<&HashSet<ModuleId>> {
self.edges.get(module_id)
}
#[must_use]
pub fn dependents(&self, module_id: &str) -> Option<&HashSet<ModuleId>> {
self.reverse_edges.get(module_id)
}
#[must_use]
pub fn modules(&self) -> Vec<&ModuleId> {
self.edges.keys().collect()
}
#[must_use]
pub fn module_count(&self) -> usize {
self.edges.len()
}
#[must_use]
pub fn has_cycle(&self) -> bool {
let mut visited = HashSet::new();
let mut in_stack = HashSet::new();
for module_id in self.sorted_module_ids() {
if self.dfs_cycle_check(module_id, &mut visited, &mut in_stack) {
return true;
}
}
false
}
#[must_use]
pub fn topological_order(&self) -> Option<Vec<ModuleId>> {
let mut visited = HashSet::new();
let mut in_stack = HashSet::new();
let mut order = Vec::new();
for module_id in self.sorted_module_ids() {
if !self.topo_dfs(module_id, &mut visited, &mut in_stack, &mut order) {
return None;
}
}
Some(order)
}
fn sorted_module_ids(&self) -> Vec<&ModuleId> {
let mut ids: Vec<&ModuleId> = self.edges.keys().collect();
ids.sort_unstable();
ids
}
fn dfs_cycle_check(
&self,
node: &str,
visited: &mut HashSet<String>,
in_stack: &mut HashSet<String>,
) -> bool {
if in_stack.contains(node) {
return true;
}
if visited.contains(node) {
return false;
}
visited.insert(node.to_string());
in_stack.insert(node.to_string());
for dep in sorted_deps(self.edges.get(node)) {
if self.dfs_cycle_check(dep, visited, in_stack) {
return true;
}
}
in_stack.remove(node);
false
}
fn topo_dfs(
&self,
node: &str,
visited: &mut HashSet<String>,
in_stack: &mut HashSet<String>,
order: &mut Vec<String>,
) -> bool {
if in_stack.contains(node) {
return false; }
if visited.contains(node) {
return true;
}
visited.insert(node.to_string());
in_stack.insert(node.to_string());
for dep in sorted_deps(self.edges.get(node)) {
if !self.topo_dfs(dep, visited, in_stack, order) {
return false;
}
}
in_stack.remove(node);
order.push(node.to_string());
true
}
}
fn sorted_deps(deps: Option<&HashSet<ModuleId>>) -> Vec<&ModuleId> {
let mut deps: Vec<&ModuleId> = deps.into_iter().flatten().collect();
deps.sort_unstable();
deps
}
#[must_use]
pub fn module_id_from_module(module: &Module, index: usize) -> ModuleId {
module
.path
.as_ref()
.map(module_path_to_id)
.unwrap_or_else(|| format!("<anonymous_{index}>"))
}
#[must_use]
pub fn module_path_to_id(path: &ModulePath) -> ModuleId {
path.segments
.iter()
.map(|ident| ident.name.as_str())
.collect::<Vec<_>>()
.join(".")
}
#[must_use]
pub fn extract_dependencies(imports: &[ImportDecl]) -> HashSet<ModuleId> {
imports
.iter()
.map(|import| module_path_to_id(&import.path))
.collect()
}
pub fn add_prelude_deps(deps: &mut HashSet<ModuleId>, self_id: &str, core_module_ids: &[ModuleId]) {
for core_id in core_module_ids {
if core_id != self_id {
deps.insert(core_id.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_graph() {
let graph = DepGraph::new();
assert_eq!(graph.module_count(), 0);
assert!(!graph.has_cycle());
assert_eq!(graph.topological_order(), Some(vec![]));
}
#[test]
fn single_module_no_deps() {
let mut graph = DepGraph::new();
graph.add_module("Main".to_string());
assert_eq!(graph.module_count(), 1);
assert!(graph.dependencies("Main").unwrap().is_empty());
assert!(!graph.has_cycle());
}
#[test]
fn linear_dependency_chain() {
let mut graph = DepGraph::new();
graph.add_module_with_deps("App".to_string(), HashSet::from(["Lib".to_string()]));
graph.add_module_with_deps("Lib".to_string(), HashSet::from(["Core".to_string()]));
graph.add_module("Core".to_string());
assert_eq!(graph.module_count(), 3);
assert!(!graph.has_cycle());
let order = graph.topological_order().unwrap();
let core_pos = order.iter().position(|m| m == "Core").unwrap();
let lib_pos = order.iter().position(|m| m == "Lib").unwrap();
let app_pos = order.iter().position(|m| m == "App").unwrap();
assert!(core_pos < lib_pos);
assert!(lib_pos < app_pos);
}
#[test]
fn cycle_detection() {
let mut graph = DepGraph::new();
graph.add_module_with_deps("A".to_string(), HashSet::from(["B".to_string()]));
graph.add_module_with_deps("B".to_string(), HashSet::from(["A".to_string()]));
assert!(graph.has_cycle());
assert!(graph.topological_order().is_none());
}
#[test]
fn reverse_dependencies() {
let mut graph = DepGraph::new();
graph.add_module_with_deps("App".to_string(), HashSet::from(["Lib".to_string()]));
graph.add_module_with_deps("Tests".to_string(), HashSet::from(["Lib".to_string()]));
graph.add_module("Lib".to_string());
let dependents = graph.dependents("Lib").unwrap();
assert!(dependents.contains("App"));
assert!(dependents.contains("Tests"));
assert_eq!(dependents.len(), 2);
}
#[test]
fn diamond_dependency() {
let mut graph = DepGraph::new();
graph.add_module_with_deps(
"App".to_string(),
HashSet::from(["Left".to_string(), "Right".to_string()]),
);
graph.add_module_with_deps("Left".to_string(), HashSet::from(["Base".to_string()]));
graph.add_module_with_deps("Right".to_string(), HashSet::from(["Base".to_string()]));
graph.add_module("Base".to_string());
assert!(!graph.has_cycle());
let order = graph.topological_order().unwrap();
let base_pos = order.iter().position(|m| m == "Base").unwrap();
let app_pos = order.iter().position(|m| m == "App").unwrap();
assert!(base_pos < app_pos);
}
fn build_graph(modules: &[(&str, &[&str])]) -> DepGraph {
let mut graph = DepGraph::new();
for (id, deps) in modules {
let dep_set: HashSet<ModuleId> = deps.iter().map(|d| (*d).to_string()).collect();
graph.add_module_with_deps((*id).to_string(), dep_set);
}
graph
}
#[test]
fn topological_order_is_stable_across_repeated_calls() {
let graph = build_graph(&[
("core.iter", &[]),
("core.option", &[]),
("core.result", &[]),
("core.cmp", &[]),
("core.convert", &[]),
("core.num", &[]),
("core.str", &[]),
]);
let first = graph.topological_order().unwrap();
for _ in 0..64 {
assert_eq!(graph.topological_order().unwrap(), first);
}
let mut sorted = first.clone();
sorted.sort();
assert_eq!(first, sorted);
}
#[test]
fn topological_order_is_independent_of_input_permutation() {
let permutations: &[&[(&str, &[&str])]] = &[
&[
("a", &[]),
("b", &[]),
("c", &[]),
("d", &["a", "b"]),
("e", &["b", "c"]),
],
&[
("e", &["c", "b"]),
("d", &["b", "a"]),
("c", &[]),
("b", &[]),
("a", &[]),
],
&[
("c", &[]),
("a", &[]),
("e", &["b", "c"]),
("b", &[]),
("d", &["a", "b"]),
],
&[
("b", &[]),
("d", &["b", "a"]),
("a", &[]),
("e", &["c", "b"]),
("c", &[]),
],
];
let expected = build_graph(permutations[0]).topological_order().unwrap();
for perm in permutations {
let order = build_graph(perm).topological_order().unwrap();
assert_eq!(
order, expected,
"topological order changed with input permutation {perm:?}"
);
let pos = |m: &str| order.iter().position(|x| x == m).unwrap();
assert!(pos("a") < pos("d"));
assert!(pos("b") < pos("d"));
assert!(pos("b") < pos("e"));
assert!(pos("c") < pos("e"));
}
}
#[test]
fn has_cycle_is_stable_across_permutations() {
let acyclic: &[(&str, &[&str])] = &[("a", &[]), ("b", &["a"]), ("c", &["a", "b"])];
let cyclic: &[(&str, &[&str])] = &[("a", &["c"]), ("b", &["a"]), ("c", &["b"])];
for _ in 0..16 {
assert!(!build_graph(acyclic).has_cycle());
assert!(build_graph(cyclic).has_cycle());
}
}
}