lisette_semantics/module_graph/
mod.rs1pub mod kahn;
2
3use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
4
5use deps::{TypedefLocator, TypedefLocatorResult};
6use syntax::ast::Span;
7use syntax::program::File;
8
9use crate::loader::Loader;
10use crate::store::Store;
11use diagnostics::LocalSink;
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: &LocalSink,
30 standalone_mode: bool,
31 locator: &TypedefLocator,
32) -> ModuleGraphResult {
33 let mut edges: HashMap<ModuleId, HashSet<ModuleId>> = HashMap::default();
34 let mut to_visit = vec![entry_module.to_string()];
35 let mut visited = HashSet::default();
36 let mut files: HashMap<ModuleId, Vec<File>> = HashMap::default();
37 let mut import_spans: HashMap<ModuleId, Span> = HashMap::default();
38
39 while let Some(module_id) = to_visit.pop() {
40 if visited.contains(&module_id) {
41 continue;
42 }
43 visited.insert(module_id.clone());
44
45 let (imports_with_spans, module_files) =
46 collect_imports(&module_id, store, loader, sink, standalone_mode, locator);
47
48 let module_exists = !module_files.is_empty()
49 || store.has(&module_id)
50 || module_id == entry_module
51 || module_id.starts_with("go:"); if !module_exists {
54 if let Some(span) = import_spans.get(&module_id) {
55 let is_go_stdlib = stdlib::get_go_stdlib_typedef(&module_id).is_some();
56
57 let src_prefix_hint = module_id
58 .strip_prefix("src/")
59 .filter(|stripped| {
60 loader.is_some_and(|fs| !fs.scan_folder(stripped).is_empty())
61 })
62 .map(String::from);
63
64 sink.push(diagnostics::module_graph::module_not_found(
65 &module_id,
66 *span,
67 is_go_stdlib,
68 standalone_mode,
69 src_prefix_hint,
70 ));
71 }
72 continue;
73 }
74
75 files.insert(module_id.clone(), module_files);
76
77 let imports: HashSet<_> = imports_with_spans.keys().cloned().collect();
78
79 for (import, span) in imports_with_spans {
80 if !visited.contains(&import) {
81 to_visit.push(import.clone());
82 }
83 import_spans.entry(import).or_insert(span);
84 }
85
86 edges.insert(module_id, imports);
87 }
88
89 let (order, cycles) = kahn::topological_sort(&edges);
90
91 ModuleGraphResult {
92 order,
93 cycles,
94 files,
95 edges,
96 }
97}
98
99fn parse_module_files(
100 module_id: &ModuleId,
101 store: &mut Store,
102 loader: Option<&dyn Loader>,
103 sink: &LocalSink,
104) -> Vec<File> {
105 let Some(fs) = loader else {
106 return vec![];
107 };
108 let mut files = Vec::new();
109 for (filename, source) in fs.scan_folder(module_id) {
110 if filename.ends_with("_test.lis") {
111 sink.push(diagnostics::module_graph::test_file_not_supported(
112 &filename,
113 ));
114 continue;
115 }
116 if files.is_empty() {
118 store.add_module(module_id);
119 }
120 let file_id = store.new_file_id();
121 let result = syntax::build_ast(&source, file_id);
122 sink.extend_parse_errors(result.errors);
123 let file = File::new(module_id, &filename, &source, result.ast, file_id);
124 store.store_file(module_id, file.clone());
126 files.push(file);
127 }
128 files
129}
130
131fn collect_imports(
132 module_id: &ModuleId,
133 store: &mut Store,
134 loader: Option<&dyn Loader>,
135 sink: &LocalSink,
136 standalone_mode: bool,
137 locator: &TypedefLocator,
138) -> (HashMap<ModuleId, Span>, Vec<File>) {
139 let mut imports = HashMap::default();
140
141 let (files, file_imports): (Vec<File>, Vec<_>) =
142 if let Some(module) = store.get_module(module_id) {
143 let lis_imports = module.files.values().flat_map(|f| f.imports());
145 let typedef_imports = module.all_typedefs().flat_map(|f| f.imports());
146 let all_imports: Vec<_> = lis_imports.chain(typedef_imports).collect();
147 (vec![], all_imports)
148 } else {
149 let parsed = parse_module_files(module_id, store, loader, sink);
151 let file_imports = parsed.iter().flat_map(|f| f.imports()).collect();
152 (parsed, file_imports)
153 };
154
155 for file_import in file_imports {
156 if file_import.name == "prelude" {
157 sink.push(diagnostics::module_graph::cannot_import_prelude(
158 file_import.span,
159 ));
160 continue;
161 }
162
163 if let Some(go_pkg) = file_import.name.strip_prefix("go:") {
164 match locator.find_typedef_content(go_pkg) {
165 TypedefLocatorResult::Found { .. } => {
166 imports.insert(file_import.name.to_string(), file_import.name_span);
167 }
168 TypedefLocatorResult::UnknownStdlib => {
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 }
177 TypedefLocatorResult::UndeclaredImport => {
178 if standalone_mode {
179 sink.push(diagnostics::module_graph::module_not_found(
180 &file_import.name,
181 file_import.name_span,
182 false,
183 true,
184 None,
185 ));
186 } else {
187 sink.push(diagnostics::module_graph::undeclared_go_import(
188 go_pkg,
189 file_import.name_span,
190 ));
191 }
192 }
193 TypedefLocatorResult::MissingTypedef { module, version } => {
194 sink.push(diagnostics::module_graph::missing_go_typedef(
195 go_pkg,
196 &module,
197 &version,
198 file_import.name_span,
199 ));
200 }
201 TypedefLocatorResult::UnreadableTypedef { path, error } => {
202 sink.push(diagnostics::module_graph::unreadable_go_typedef(
203 &path,
204 &error,
205 file_import.name_span,
206 ));
207 }
208 }
209 continue;
210 }
211
212 if file_import.name.contains('.') {
213 sink.push(diagnostics::module_graph::invalid_module_path(
214 &file_import.name,
215 file_import.name_span,
216 ));
217 continue;
218 }
219
220 imports
221 .entry(file_import.name.to_string())
222 .or_insert(file_import.name_span);
223 }
224
225 (imports, files)
226}