Skip to main content

trait_kit/kit/
graph.rs

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