lisette_semantics/module_graph/
mod.rs1pub 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 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:"); 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 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 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 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 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 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}