Skip to main content

lisette_semantics/module_graph/
mod.rs

1pub mod kahn;
2
3use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
4
5use syntax::ast::Span;
6use syntax::program::File;
7
8use crate::loader::Loader;
9use crate::store::Store;
10use diagnostics::DiagnosticSink;
11use stdlib::get_go_stdlib_typedef;
12
13pub type ModuleId = String;
14
15#[derive(Debug)]
16pub struct ModuleGraphResult {
17    pub order: Vec<ModuleId>,
18    pub cycles: Vec<Vec<ModuleId>>,
19    pub files: HashMap<ModuleId, Vec<File>>,
20    /// Direct dependencies of each module (module_id -> set of dependency module_ids).
21    /// Used for transitive cache invalidation.
22    pub edges: HashMap<ModuleId, HashSet<ModuleId>>,
23}
24
25pub fn build_module_graph(
26    store: &mut Store,
27    loader: Option<&dyn Loader>,
28    entry_module: &str,
29    sink: &DiagnosticSink,
30    standalone_mode: bool,
31) -> ModuleGraphResult {
32    let mut edges: HashMap<ModuleId, HashSet<ModuleId>> = HashMap::default();
33    let mut to_visit = vec![entry_module.to_string()];
34    let mut visited = HashSet::default();
35    let mut files: HashMap<ModuleId, Vec<File>> = HashMap::default();
36    let mut import_spans: HashMap<ModuleId, Span> = HashMap::default();
37
38    while let Some(module_id) = to_visit.pop() {
39        if visited.contains(&module_id) {
40            continue;
41        }
42        visited.insert(module_id.clone());
43
44        let (imports_with_spans, module_files) =
45            collect_imports(&module_id, store, loader, sink, standalone_mode);
46
47        let module_exists = !module_files.is_empty()
48            || store.has(&module_id)
49            || module_id == entry_module
50            || module_id.starts_with("go:"); // go modules are virtual
51
52        if !module_exists {
53            if let Some(span) = import_spans.get(&module_id) {
54                let is_go_stdlib = get_go_stdlib_typedef(&module_id).is_some();
55
56                let src_prefix_hint = module_id
57                    .strip_prefix("src/")
58                    .filter(|stripped| {
59                        loader.is_some_and(|fs| !fs.scan_folder(stripped).is_empty())
60                    })
61                    .map(String::from);
62
63                sink.push(diagnostics::module_graph::module_not_found(
64                    &module_id,
65                    *span,
66                    is_go_stdlib,
67                    standalone_mode,
68                    src_prefix_hint,
69                ));
70            }
71            continue;
72        }
73
74        files.insert(module_id.clone(), module_files);
75
76        let imports: HashSet<_> = imports_with_spans.keys().cloned().collect();
77
78        for (import, span) in imports_with_spans {
79            if !visited.contains(&import) {
80                to_visit.push(import.clone());
81            }
82            import_spans.entry(import).or_insert(span);
83        }
84
85        edges.insert(module_id, imports);
86    }
87
88    let (order, cycles) = kahn::topological_sort(&edges);
89
90    ModuleGraphResult {
91        order,
92        cycles,
93        files,
94        edges,
95    }
96}
97
98fn parse_module_files(
99    module_id: &ModuleId,
100    store: &mut Store,
101    loader: Option<&dyn Loader>,
102    sink: &DiagnosticSink,
103) -> Vec<File> {
104    let Some(fs) = loader else {
105        return vec![];
106    };
107    let mut files = Vec::new();
108    for (filename, source) in fs.scan_folder(module_id) {
109        if filename.ends_with("_test.lis") {
110            sink.push(diagnostics::module_graph::test_file_not_supported(
111                &filename,
112            ));
113            continue;
114        }
115        // Ensure the module exists in the store before adding the first file
116        if files.is_empty() {
117            store.add_module(module_id);
118        }
119        let file_id = store.new_file_id();
120        let result = syntax::build_ast(&source, file_id);
121        sink.extend_parse_errors(result.errors);
122        let file = File::new(module_id, &filename, &source, result.ast, file_id);
123        // Register the file immediately so diagnostic rendering works
124        store.store_file(module_id, file.clone());
125        files.push(file);
126    }
127    files
128}
129
130fn collect_imports(
131    module_id: &ModuleId,
132    store: &mut Store,
133    loader: Option<&dyn Loader>,
134    sink: &DiagnosticSink,
135    standalone_mode: bool,
136) -> (HashMap<ModuleId, Span>, Vec<File>) {
137    let mut imports = HashMap::default();
138
139    let (files, file_imports): (Vec<File>, Vec<_>) =
140        if let Some(module) = store.get_module(module_id) {
141            // Module already in store (entry module or prelude): get imports from stored files
142            let lis_imports = module.files.values().flat_map(|f| f.imports());
143            let typedef_imports = module.all_typedefs().flat_map(|f| f.imports());
144            let all_imports: Vec<_> = lis_imports.chain(typedef_imports).collect();
145            (vec![], all_imports)
146        } else {
147            // Module not in store: parse from filesystem
148            let parsed = parse_module_files(module_id, store, loader, sink);
149            let file_imports = parsed.iter().flat_map(|f| f.imports()).collect();
150            (parsed, file_imports)
151        };
152
153    for file_import in file_imports {
154        if file_import.name == "prelude" {
155            sink.push(diagnostics::module_graph::cannot_import_prelude(
156                file_import.span,
157            ));
158            continue;
159        }
160
161        if let Some(go_pkg) = file_import.name.strip_prefix("go:") {
162            if get_go_stdlib_typedef(go_pkg).is_some() {
163                imports.insert(file_import.name.to_string(), file_import.name_span);
164                continue;
165            }
166
167            // @TODO: Check cache at ~/.lisette (and other spots)
168
169            sink.push(diagnostics::module_graph::module_not_found(
170                &file_import.name,
171                file_import.name_span,
172                false,
173                standalone_mode,
174                None,
175            ));
176            continue;
177        }
178
179        imports
180            .entry(file_import.name.to_string())
181            .or_insert(file_import.name_span);
182    }
183
184    (imports, files)
185}