1use std::{
2 collections::HashMap,
3 sync::{Arc, Mutex},
4};
5
6use anyhow::Result;
7
8use crate::{
9 errors::KclErrorDetails,
10 execution::typed_path::TypedPath,
11 modules::{ModulePath, ModuleRepr},
12 parsing::ast::types::{ImportPath, ImportStatement, Node as AstNode},
13 walk::{Node, Visitable},
14 ExecState, ExecutorContext, KclError, ModuleId, SourceRange,
15};
16
17type Dependency = (String, String);
21
22type Graph = Vec<Dependency>;
23
24pub(crate) type DependencyInfo = (AstNode<ImportStatement>, ModuleId, ModulePath, ModuleRepr);
25pub(crate) type UniverseMap = HashMap<TypedPath, AstNode<ImportStatement>>;
26pub(crate) type Universe = HashMap<String, DependencyInfo>;
27
28#[allow(clippy::iter_over_hash_type)]
34pub fn import_graph(progs: &Universe, ctx: &ExecutorContext) -> Result<Vec<Vec<String>>, KclError> {
35 let mut graph = Graph::new();
36
37 for (name, (_, _, _, repr)) in progs.iter() {
38 graph.extend(
39 import_dependencies(repr, ctx)?
40 .into_iter()
41 .map(|(dependency, _, _)| (name.clone(), dependency))
42 .collect::<Vec<_>>(),
43 );
44 }
45
46 let all_modules: Vec<&str> = progs.keys().map(|v| v.as_str()).collect();
47 topsort(&all_modules, graph)
48}
49
50#[allow(clippy::iter_over_hash_type)]
51fn topsort(all_modules: &[&str], graph: Graph) -> Result<Vec<Vec<String>>, KclError> {
52 if all_modules.is_empty() {
53 return Ok(vec![]);
54 }
55 let mut dep_map = HashMap::<String, Vec<String>>::new();
56
57 for (dependent, dependency) in graph.iter() {
58 let mut dependencies = dep_map.remove(dependent).unwrap_or_default();
59 dependencies.push(dependency.to_owned());
60 dep_map.insert(dependent.to_owned(), dependencies);
61 }
62
63 let mut waiting_modules = all_modules.to_owned();
68 let mut order = vec![];
69
70 loop {
71 let mut stage_modules: Vec<String> = vec![];
76
77 for module in &waiting_modules {
78 let module = module.to_string();
79 if dep_map.get(&module).map(|v| v.len()).unwrap_or(0) == 0 {
80 stage_modules.push(module.to_string());
83 }
84 }
85
86 for stage_module in &stage_modules {
87 waiting_modules.retain(|v| *v != stage_module.as_str());
89
90 for (_, waiting_for) in dep_map.iter_mut() {
92 waiting_for.retain(|v| v != stage_module);
93 }
94 }
95
96 if stage_modules.is_empty() {
97 waiting_modules.sort();
98
99 return Err(KclError::ImportCycle(KclErrorDetails {
100 message: format!("circular import of modules not allowed: {}", waiting_modules.join(", ")),
101 source_ranges: vec![SourceRange::default()],
103 }));
104 }
105
106 stage_modules.sort();
110
111 order.push(stage_modules);
112
113 if waiting_modules.is_empty() {
114 break;
115 }
116 }
117
118 Ok(order)
119}
120
121type ImportDependencies = Vec<(String, AstNode<ImportStatement>, ModulePath)>;
122
123pub(crate) fn import_dependencies(repr: &ModuleRepr, ctx: &ExecutorContext) -> Result<ImportDependencies, KclError> {
124 let ModuleRepr::Kcl(prog, _) = repr else {
125 return Ok(vec![]);
127 };
128
129 let ret = Arc::new(Mutex::new(vec![]));
130 fn walk(ret: Arc<Mutex<ImportDependencies>>, node: Node<'_>, ctx: &ExecutorContext) -> Result<(), KclError> {
131 if let Node::ImportStatement(is) = node {
132 let resolved_path = ModulePath::from_import_path(&is.path, &ctx.settings.project_directory);
134 match &is.path {
135 ImportPath::Kcl { filename } => {
136 ret.lock()
139 .map_err(|err| {
140 KclError::Internal(KclErrorDetails {
141 message: format!("Failed to lock mutex: {}", err),
142 source_ranges: Default::default(),
143 })
144 })?
145 .push((filename.to_string(), is.clone(), resolved_path));
146 }
147 ImportPath::Foreign { path } => {
148 ret.lock()
149 .map_err(|err| {
150 KclError::Internal(KclErrorDetails {
151 message: format!("Failed to lock mutex: {}", err),
152 source_ranges: Default::default(),
153 })
154 })?
155 .push((path.to_string(), is.clone(), resolved_path));
156 }
157 ImportPath::Std { .. } => { }
159 }
160 }
161
162 for child in node.children().iter() {
163 walk(ret.clone(), *child, ctx)?;
164 }
165
166 Ok(())
167 }
168
169 walk(ret.clone(), prog.into(), ctx)?;
170
171 let ret = ret.lock().map_err(|err| {
172 KclError::Internal(KclErrorDetails {
173 message: format!("Failed to lock mutex: {}", err),
174 source_ranges: Default::default(),
175 })
176 })?;
177
178 Ok(ret.clone())
179}
180
181pub(crate) async fn import_universe(
184 ctx: &ExecutorContext,
185 repr: &ModuleRepr,
186 out: &mut Universe,
187 exec_state: &mut ExecState,
188) -> Result<UniverseMap, KclError> {
189 let modules = import_dependencies(repr, ctx)?;
190 let mut module_imports = HashMap::new();
191 for (filename, import_stmt, module_path) in modules {
192 match &module_path {
193 ModulePath::Main => {
194 }
196 ModulePath::Local { value, .. } => {
197 module_imports.insert(value.clone(), import_stmt.clone());
198 }
199 ModulePath::Std { .. } => {
200 }
202 }
203
204 if out.contains_key(&filename) {
205 continue;
206 }
207
208 let source_range = SourceRange::from(&import_stmt);
209 let attrs = &import_stmt.outer_attrs;
210 let module_id = ctx
211 .open_module(&import_stmt.path, attrs, exec_state, source_range)
212 .await?;
213
214 let repr = {
215 let Some(module_info) = exec_state.get_module(module_id) else {
216 return Err(KclError::Internal(KclErrorDetails {
217 message: format!("Module {} not found", module_id),
218 source_ranges: vec![import_stmt.into()],
219 }));
220 };
221 module_info.repr.clone()
222 };
223
224 out.insert(filename, (import_stmt, module_id, module_path, repr.clone()));
225 Box::pin(import_universe(ctx, &repr, out, exec_state)).await?;
226 }
227
228 Ok(module_imports)
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::parsing::ast::types::{ImportSelector, Program};
235
236 macro_rules! kcl {
237 ( $kcl:expr ) => {{
238 $crate::parsing::top_level_parse($kcl).unwrap()
239 }};
240 }
241
242 fn into_module_info(program: AstNode<Program>) -> DependencyInfo {
243 (
244 AstNode::no_src(ImportStatement {
245 selector: ImportSelector::None { alias: None },
246 path: ImportPath::Kcl { filename: "".into() },
247 visibility: Default::default(),
248 digest: None,
249 }),
250 ModuleId::default(),
251 ModulePath::Local { value: "".into() },
252 ModuleRepr::Kcl(program, None),
253 )
254 }
255
256 #[tokio::test]
257 async fn order_imports() {
258 let mut modules = HashMap::new();
259
260 let a = kcl!("");
261 modules.insert("a.kcl".to_owned(), into_module_info(a));
262
263 let b = kcl!(
264 "
265import \"a.kcl\"
266"
267 );
268 modules.insert("b.kcl".to_owned(), into_module_info(b));
269
270 let ctx = ExecutorContext::new_mock(None).await;
271 let order = import_graph(&modules, &ctx).unwrap();
272 assert_eq!(vec![vec!["a.kcl".to_owned()], vec!["b.kcl".to_owned()]], order);
273 }
274
275 #[tokio::test]
276 async fn order_imports_none() {
277 let mut modules = HashMap::new();
278
279 let a = kcl!(
280 "
281y = 2
282"
283 );
284 modules.insert("a.kcl".to_owned(), into_module_info(a));
285
286 let b = kcl!(
287 "
288x = 1
289"
290 );
291 modules.insert("b.kcl".to_owned(), into_module_info(b));
292
293 let ctx = ExecutorContext::new_mock(None).await;
294 let order = import_graph(&modules, &ctx).unwrap();
295 assert_eq!(vec![vec!["a.kcl".to_owned(), "b.kcl".to_owned()]], order);
296 }
297
298 #[tokio::test]
299 async fn order_imports_2() {
300 let mut modules = HashMap::new();
301
302 let a = kcl!("");
303 modules.insert("a.kcl".to_owned(), into_module_info(a));
304
305 let b = kcl!(
306 "
307import \"a.kcl\"
308"
309 );
310 modules.insert("b.kcl".to_owned(), into_module_info(b));
311
312 let c = kcl!(
313 "
314import \"a.kcl\"
315"
316 );
317 modules.insert("c.kcl".to_owned(), into_module_info(c));
318
319 let ctx = ExecutorContext::new_mock(None).await;
320 let order = import_graph(&modules, &ctx).unwrap();
321 assert_eq!(
322 vec![vec!["a.kcl".to_owned()], vec!["b.kcl".to_owned(), "c.kcl".to_owned()]],
323 order
324 );
325 }
326
327 #[tokio::test]
328 async fn order_imports_cycle() {
329 let mut modules = HashMap::new();
330
331 let a = kcl!(
332 "
333import \"b.kcl\"
334"
335 );
336 modules.insert("a.kcl".to_owned(), into_module_info(a));
337
338 let b = kcl!(
339 "
340import \"a.kcl\"
341"
342 );
343 modules.insert("b.kcl".to_owned(), into_module_info(b));
344
345 let ctx = ExecutorContext::new_mock(None).await;
346 import_graph(&modules, &ctx).unwrap_err();
347 }
348}