kcl_lib/walk/
import_graph.rs

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
17/// Specific dependency between two modules. The 0th element of this info
18/// is the "importing" module, the 1st is the "imported" module. The 0th
19/// module *depends on* the 1st module.
20type 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/// Process a number of programs, returning the graph of dependencies.
29///
30/// This will (currently) return a list of lists of IDs that can be safely
31/// run concurrently. Each "stage" is blocking in this model, which will
32/// change in the future. Don't use this function widely, yet.
33#[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    // dep_map now contains reverse dependencies. For each module, it's a
64    // list of what things are "waiting on it". A non-empty value for a key
65    // means it's currently blocked.
66
67    let mut waiting_modules = all_modules.to_owned();
68    let mut order = vec![];
69
70    loop {
71        // Each pass through we need to find any modules which have nothing
72        // "pointing at it" -- so-called reverse dependencies. This is an entry
73        // that is either not in the dep_map OR an empty list.
74
75        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                // if it's None or empty, this is a node that we can process,
81                // and remove from the graph.
82                stage_modules.push(module.to_string());
83            }
84        }
85
86        for stage_module in &stage_modules {
87            // remove the ready-to-run module from the waiting list
88            waiting_modules.retain(|v| *v != stage_module.as_str());
89
90            // remove any dependencies for the next run
91            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                // TODO: we can get the right import lines from the AST, but we don't
102                source_ranges: vec![SourceRange::default()],
103            }));
104        }
105
106        // not strictly needed here, but perhaps helpful to avoid thinking
107        // there's any implied ordering as well as helping to make tests
108        // easier.
109        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        // It has no dependencies, so return an empty list.
126        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            // We only care about Kcl and Foreign imports for now.
133            let resolved_path = ModulePath::from_import_path(&is.path, &ctx.settings.project_directory);
134            match &is.path {
135                ImportPath::Kcl { filename } => {
136                    // We need to lock the mutex to push the dependency.
137                    // This is a bit of a hack, but it works for now.
138                    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 { .. } => { // do nothing
158                }
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
181/// Mutates the `out` universe with the imported modules. Returns the imports of
182/// only `repr`'s non-transitive imports.
183pub(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                // We only care about what the root module imports.
195            }
196            ModulePath::Local { value, .. } => {
197                module_imports.insert(value.clone(), import_stmt.clone());
198            }
199            ModulePath::Std { .. } => {
200                // We don't care about std imports.
201            }
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}