Skip to main content

trait_kit/kit/
graph.rs

1// Copyright (c) 2026 Kirky.X
2// SPDX-License-Identifier: MIT
3//! Dependency graph with topological sort and cycle detection.
4
5use std::any::TypeId;
6use std::collections::{HashMap, VecDeque};
7
8/// A node in the dependency graph.
9pub struct ModuleEntry {
10    /// The module's `TypeId`.
11    pub type_id: TypeId,
12    /// The module's diagnostic name.
13    pub name: &'static str,
14    /// (name, `TypeId`) pairs of modules this module depends on.
15    pub dependencies: Vec<(&'static str, TypeId)>,
16}
17
18/// Dependency graph for topological sort and cycle detection.
19pub struct DependencyGraph {
20    entries: Vec<ModuleEntry>,
21    index: HashMap<TypeId, usize>,
22}
23
24impl DependencyGraph {
25    /// Create an empty graph.
26    #[must_use]
27    pub fn new() -> Self {
28        DependencyGraph {
29            entries: Vec::new(),
30            index: HashMap::new(),
31        }
32    }
33
34    /// Add a module to the graph.
35    ///
36    /// # Errors
37    ///
38    /// Returns the module's name if it is already registered.
39    pub fn add(&mut self, entry: ModuleEntry) -> Result<(), &'static str> {
40        if self.index.contains_key(&entry.type_id) {
41            return Err(entry.name);
42        }
43        let idx = self.entries.len();
44        self.index.insert(entry.type_id, idx);
45        self.entries.push(entry);
46        Ok(())
47    }
48
49    /// Validate the graph: check for missing dependencies and cycles.
50    /// Returns the topologically sorted `TypeIds` on success.
51    ///
52    /// # Errors
53    ///
54    /// Returns `GraphError::DependencyMissing` if a module depends on an unregistered module.
55    /// Returns `GraphError::CycleDetected` if a dependency cycle is found.
56    pub fn validate(&self) -> Result<Vec<TypeId>, GraphError> {
57        // Check for missing dependencies
58        for entry in &self.entries {
59            for (dep_name, dep_id) in &entry.dependencies {
60                if !self.index.contains_key(dep_id) {
61                    return Err(GraphError::DependencyMissing {
62                        module: entry.name,
63                        missing: dep_name,
64                    });
65                }
66            }
67        }
68
69        // Kahn's algorithm for topological sort + cycle detection
70        let n = self.entries.len();
71        let mut in_degree = vec![0usize; n];
72        let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
73
74        for (i, entry) in self.entries.iter().enumerate() {
75            for (_dep_name, dep_id) in &entry.dependencies {
76                if let Some(&dep_idx) = self.index.get(dep_id) {
77                    adj[dep_idx].push(i);
78                    in_degree[i] += 1;
79                }
80            }
81        }
82
83        let mut queue: VecDeque<usize> = VecDeque::new();
84        for (i, deg) in in_degree.iter().enumerate().take(n) {
85            if *deg == 0 {
86                queue.push_back(i);
87            }
88        }
89
90        let mut sorted = Vec::with_capacity(n);
91        while let Some(node) = queue.pop_front() {
92            sorted.push(self.entries[node].type_id);
93            for &neighbor in &adj[node] {
94                in_degree[neighbor] -= 1;
95                if in_degree[neighbor] == 0 {
96                    queue.push_back(neighbor);
97                }
98            }
99        }
100
101        if sorted.len() != n {
102            // Cycle detected — find the cycle for a useful error message
103            let cycle = self.find_cycle();
104            return Err(GraphError::CycleDetected { cycle });
105        }
106
107        Ok(sorted)
108    }
109
110    /// Find a cycle in the graph using DFS (for error reporting).
111    fn find_cycle(&self) -> Vec<&'static str> {
112        fn dfs(
113            node: usize,
114            entries: &[ModuleEntry],
115            index: &HashMap<TypeId, usize>,
116            visited: &mut [u8],
117            stack: &mut Vec<usize>,
118            cycle_names: &mut Vec<&'static str>,
119        ) -> bool {
120            visited[node] = 1;
121            stack.push(node);
122
123            for (_dep_name, dep_id) in &entries[node].dependencies {
124                if let Some(&dep_idx) = index.get(dep_id) {
125                    if visited[dep_idx] == 1 {
126                        // Found cycle — extract it
127                        let start = stack
128                            .iter()
129                            .position(|&x| x == dep_idx)
130                            .expect("invariant: dep_idx must be in stack (visited[dep_idx] == 1)");
131                        for &idx in &stack[start..] {
132                            cycle_names.push(entries[idx].name);
133                        }
134                        cycle_names.push(entries[dep_idx].name);
135                        return true;
136                    }
137                    if visited[dep_idx] == 0
138                        && dfs(dep_idx, entries, index, visited, stack, cycle_names)
139                    {
140                        return true;
141                    }
142                }
143            }
144
145            stack.pop();
146            visited[node] = 2;
147            false
148        }
149
150        let n = self.entries.len();
151        let mut visited = vec![0u8; n]; // 0=unvisited, 1=in-stack, 2=done
152        let mut stack = Vec::new();
153        let mut cycle_names = Vec::new();
154
155        for i in 0..n {
156            if visited[i] == 0
157                && dfs(
158                    i,
159                    &self.entries,
160                    &self.index,
161                    &mut visited,
162                    &mut stack,
163                    &mut cycle_names,
164                )
165            {
166                return cycle_names;
167            }
168        }
169
170        vec!["<unknown cycle>"]
171    }
172
173    /// Get the registered names of all dependencies for a module.
174    #[must_use]
175    pub fn dependency_names(&self, type_id: TypeId) -> Vec<&'static str> {
176        if let Some(&idx) = self.index.get(&type_id) {
177            self.entries[idx]
178                .dependencies
179                .iter()
180                .map(|(name, _)| *name)
181                .collect()
182        } else {
183            Vec::new()
184        }
185    }
186
187    /// Get all entries in registration order.
188    #[must_use]
189    pub fn entries(&self) -> &[ModuleEntry] {
190        &self.entries
191    }
192
193    /// Look up a module's diagnostic name by `TypeId` in O(1).
194    #[must_use]
195    pub fn name_of(&self, type_id: TypeId) -> Option<&'static str> {
196        self.index.get(&type_id).map(|&idx| self.entries[idx].name)
197    }
198}
199
200impl Default for DependencyGraph {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206/// Errors from graph validation.
207#[derive(Debug)]
208pub enum GraphError {
209    /// A module depends on an unregistered module.
210    DependencyMissing {
211        module: &'static str,
212        missing: &'static str,
213    },
214    /// A dependency cycle was detected.
215    CycleDetected { cycle: Vec<&'static str> },
216}