1use rayon::prelude::*;
2use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use diagnostics::{LocalSink, SemanticResult};
7use syntax::ast::Expression;
8use syntax::program::{File, ModuleInfo, MutationInfo, UnusedInfo};
9
10use deps::TypedefLocator;
11
12use crate::cache::{
13 CompiledModule, EmitStamp, compute_emit_artifact_hash, compute_module_hash,
14 get_dependency_module_hashes,
15 go_stdlib::{self, load_cached_go_module},
16 hash_module_sources, is_cache_disabled, prelude as prelude_cache, register_cached_module,
17 save_module_cache, try_load_cache,
18};
19use crate::checker::TaskState;
20use crate::checker::infer::InferCtx;
21use crate::diagnostics::emit_for_locator_result;
22use crate::facts::{BindingIdAllocator, Facts};
23use crate::loader::Loader;
24use crate::module_graph::build_module_graph;
25use crate::passes;
26use crate::prelude::parse_and_register_prelude;
27use crate::store::{ENTRY_MODULE_ID, Store};
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
30pub enum CompilePhase {
31 #[default]
32 Check,
33 Emit,
34}
35
36#[derive(Debug, Clone, Default)]
37pub struct SemanticConfig {
38 pub run_lints: bool,
39 pub standalone_mode: bool,
40 pub load_siblings: bool,
41}
42
43pub struct AnalyzeInput<'a> {
44 pub config: SemanticConfig,
45 pub loader: &'a dyn Loader,
46 pub source: String,
47 pub filename: String,
49 pub display_path: String,
52 pub ast: Vec<Expression>,
53 pub project_root: Option<PathBuf>,
54 pub compile_phase: CompilePhase,
55 pub locator: TypedefLocator,
56 pub go_module: String,
59 pub disable_cache: bool,
62}
63
64pub struct AnalyzeOutput {
67 pub result: SemanticResult,
68 pub facts: Facts,
69 pub emit_stamps: Vec<EmitStamp>,
70}
71
72pub fn analyze(input: AnalyzeInput) -> AnalyzeOutput {
73 let mut store = Store::new();
74
75 store.init_entry_module();
76 store.store_entry_file(
77 &input.filename,
78 &input.display_path,
79 &input.source,
80 input.ast,
81 );
82
83 let sink = LocalSink::new();
84
85 if input.config.load_siblings {
86 for (filename, content) in input.loader.scan_folder(ENTRY_MODULE_ID) {
87 if filename == input.filename
88 || !filename.ends_with(".lis")
89 || filename.ends_with(".d.lis")
90 {
91 continue;
92 }
93 let file_id = store.new_file_id();
94 let result = syntax::build_ast(&content.source, file_id);
95 sink.extend_parse_errors(result.errors);
96 store.store_file(
97 ENTRY_MODULE_ID,
98 File::new(
99 ENTRY_MODULE_ID,
100 &filename,
101 &content.display_path,
102 &content.source,
103 result.ast,
104 file_id,
105 ),
106 );
107 }
108 }
109
110 let entry_module = store.entry_module_id().to_string();
111 let mut graph_result = build_module_graph(
112 &mut store,
113 Some(input.loader),
114 &entry_module,
115 &sink,
116 input.config.standalone_mode,
117 &input.locator,
118 );
119
120 for cycle in &graph_result.cycles {
121 sink.push(diagnostics::module_graph::import_cycle(cycle));
122 }
123
124 let has_pre_check_errors = sink.has_errors();
125
126 let cache_disabled = is_cache_disabled();
127
128 let prelude_cache_hit = if cache_disabled {
129 false
130 } else if let Some(cached) = prelude_cache::try_load_prelude_cache() {
131 prelude_cache::register_cached_prelude(&mut store, cached);
132 true
133 } else {
134 false
135 };
136
137 if !prelude_cache_hit {
138 parse_and_register_prelude(&mut store, &sink);
139 }
140
141 let cache_enabled = input.project_root.is_some() && !cache_disabled && !input.disable_cache;
142 let check_go_files = input.compile_phase == CompilePhase::Emit;
143
144 let binding_ids = Arc::new(BindingIdAllocator::new());
145
146 let (mut facts, cached_modules, compiled_modules, ufcs_methods) = {
147 let mut checker = TaskState::new(&sink, binding_ids.clone());
148 checker
149 .ufcs_methods
150 .extend(crate::prelude::compute_prelude_ufcs(&store));
151
152 let mut module_hashes: HashMap<String, u64> = HashMap::default();
153 let mut cached_modules: HashSet<String> = HashSet::default();
154 let mut compiled_modules: Vec<CompiledModule> = vec![];
155
156 let order = std::mem::take(&mut graph_result.order);
157 let edges = &graph_result.edges;
158
159 let go_cache = if cache_disabled {
160 None
161 } else {
162 go_stdlib::try_load_go_stdlib_cache(input.locator.target())
163 };
164
165 let mut to_infer: Vec<String> = Vec::new();
166
167 for module_id in order {
168 if let Some(go_pkg) = module_id.strip_prefix("go:") {
169 if graph_result.link_only_modules.contains(&module_id) {
170 continue;
171 }
172
173 if deps::is_stdlib(go_pkg)
174 && let Some(ref cache) = go_cache
175 {
176 load_cached_go_module(&mut store, &module_id, cache, input.locator.target());
177 if store.is_visited(&module_id) {
178 continue;
179 }
180 }
181
182 match input.locator.find_typedef_content(go_pkg) {
183 deps::TypedefLocatorResult::Found { content, origin } => {
184 checker.parse_and_register_go_module(
185 &mut store,
186 &module_id,
187 content.as_ref(),
188 origin.into_cache_path(),
189 &input.locator,
190 );
191 }
192 other => {
193 emit_for_locator_result(
194 &other,
195 &module_id,
196 go_pkg,
197 None,
198 input.locator.target(),
199 input.config.standalone_mode,
200 &sink,
201 );
202 }
203 }
204 continue;
205 }
206
207 if store.is_visited(&module_id) {
208 continue;
209 }
210
211 let files = graph_result.files.remove(&module_id).unwrap_or_default();
212 let source_hash = hash_module_sources(&files);
213
214 let dep_hashes = get_dependency_module_hashes(&module_id, edges, &module_hashes);
215 let module_hash = compute_module_hash(source_hash, &dep_hashes);
216 module_hashes.insert(module_id.clone(), module_hash);
217
218 let is_entry = module_id == ENTRY_MODULE_ID;
219
220 let expected_artifact_hash =
221 check_go_files.then(|| compute_emit_artifact_hash(source_hash, &input.go_module));
222
223 if cache_enabled
224 && !is_entry
225 && let Some(ref project_root) = input.project_root
226 && let Some(cached) = try_load_cache(
227 &module_id,
228 source_hash,
229 &dep_hashes,
230 expected_artifact_hash,
231 project_root,
232 check_go_files,
233 )
234 {
235 checker
236 .ufcs_methods
237 .extend(cached.ufcs_methods.iter().cloned());
238 register_cached_module(&mut store, &module_id, cached, project_root);
239 cached_modules.insert(module_id.clone());
240 continue;
241 }
242
243 store.store_module(&module_id, files);
244 checker.register_module(&mut store, &module_id);
245
246 if !is_entry {
247 compiled_modules.push(CompiledModule {
248 module_id: module_id.clone(),
249 source_hash,
250 dep_hashes,
251 });
252 }
253
254 to_infer.push(module_id);
255 }
256
257 let module_files: Vec<(String, Vec<File>)> = to_infer
258 .iter()
259 .map(|module_id| {
260 let files = checker.take_module_files(&mut store, module_id);
261 (module_id.clone(), files)
262 })
263 .collect();
264
265 const PARALLEL_THRESHOLD: usize = 4;
269
270 if module_files.len() < PARALLEL_THRESHOLD {
271 for (module_id, files) in module_files {
272 InferCtx::new(&mut checker, &store).infer_module(&module_id, files);
273 }
274 } else {
275 let allocator = binding_ids.clone();
276 let ufcs_shared = Arc::new(std::mem::take(&mut checker.ufcs_methods));
277 let store_ref: &Store = &store;
278
279 type WorkerOutput = (Vec<(String, File)>, Facts, LocalSink);
280 let outputs: Vec<WorkerOutput> = module_files
281 .into_par_iter()
282 .map(|(module_id, files)| {
283 let local_sink = LocalSink::new();
284 let mut worker = TaskState::new(&local_sink, allocator.clone());
285 worker.ufcs_shared = Some(ufcs_shared.clone());
286 InferCtx::new(&mut worker, store_ref).infer_module(&module_id, files);
287 let typed_files = std::mem::take(&mut worker.typed_files);
288 let facts = std::mem::replace(&mut worker.facts, Facts::new(allocator.clone()));
289 (typed_files, facts, local_sink)
290 })
291 .collect();
292
293 checker.ufcs_methods =
294 Arc::try_unwrap(ufcs_shared).unwrap_or_else(|arc| (*arc).clone());
295
296 let mut worker_sinks: Vec<LocalSink> = Vec::with_capacity(outputs.len());
297 for (typed_files, facts, sink_local) in outputs {
298 checker.typed_files.extend(typed_files);
299 checker.facts.merge(facts);
300 worker_sinks.push(sink_local);
301 }
302 sink.extend(LocalSink::merge(worker_sinks));
303 }
304
305 for (module_id, typed_file) in std::mem::take(&mut checker.typed_files) {
306 store.store_file(&module_id, typed_file);
307 }
308
309 if !cache_disabled {
311 let all_go_modules: Vec<String> = store
312 .modules
313 .keys()
314 .filter(|id| id.strip_prefix("go:").is_some_and(deps::is_stdlib))
315 .cloned()
316 .collect();
317 let needs_save = !all_go_modules.is_empty()
318 && go_cache.as_ref().is_none_or(|c| {
319 all_go_modules.len() != c.modules.len()
320 || all_go_modules.iter().any(|id| !c.modules.contains_key(id))
321 });
322 if needs_save {
323 go_stdlib::save_go_stdlib_cache(&store, &all_go_modules, input.locator.target());
324 }
325 }
326
327 if !cache_disabled && !prelude_cache_hit {
328 prelude_cache::save_prelude_cache(&store);
329 }
330
331 (
332 checker.facts,
333 cached_modules,
334 compiled_modules,
335 checker.ufcs_methods,
336 )
337 };
338
339 store.build_closed_domains();
340
341 let analysis = crate::context::AnalysisContext::new(&store, &ufcs_methods);
342
343 let mut unused = UnusedInfo::default();
344 if !has_pre_check_errors {
345 passes::run(
346 &analysis,
347 &mut facts,
348 &sink,
349 &mut unused,
350 input.config.run_lints,
351 );
352 }
353
354 let mut mutations = MutationInfo::default();
355 for (&binding_id, b) in facts.bindings.iter() {
356 if b.mutated {
357 mutations.mark_binding_mutated(binding_id);
358 }
359 }
360
361 let mut all_diagnostics = sink.take();
364 all_diagnostics.sort_by(diagnostics::LisetteDiagnostic::sort_key);
365 let (errors, lints): (Vec<_>, Vec<_>) = all_diagnostics.into_iter().partition(|d| d.is_error());
366
367 let emit_stamps: Vec<EmitStamp> = compiled_modules
368 .iter()
369 .map(|c| EmitStamp {
370 module_id: c.module_id.clone(),
371 artifact_hash: compute_emit_artifact_hash(c.source_hash, &input.go_module),
372 })
373 .collect();
374
375 if cache_enabled && let Some(ref project_root) = input.project_root {
376 let has_errors = errors.iter().any(|e| e.is_error());
377 if !has_errors {
378 for compiled in compiled_modules {
379 let file_ids: HashSet<u32> = store
380 .get_module(&compiled.module_id)
381 .map(|m| m.file_ids().collect())
382 .unwrap_or_default();
383
384 let has_module_lints = lints.iter().any(|lint| {
385 lint.file_id()
386 .map(|fid| file_ids.contains(&fid))
387 .unwrap_or(true)
388 });
389 if !has_module_lints
390 && let Err(e) =
391 save_module_cache(&compiled, &store, project_root, &ufcs_methods)
392 {
393 eprintln!(
394 "warning: failed to write cache for {}: {e}",
395 compiled.module_id
396 );
397 }
398 }
399 }
400 }
401
402 let mut files = HashMap::default();
403 let mut definitions = HashMap::default();
404 let mut modules = HashMap::default();
405
406 let go_module_ids: HashSet<String> = store
407 .modules
408 .keys()
409 .filter(|id| id.starts_with(syntax::types::GO_IMPORT_PREFIX))
410 .cloned()
411 .collect();
412
413 for (mod_id, module) in store.modules {
414 let is_internal = module.is_internal();
415
416 definitions.extend(module.definitions);
417
418 if is_internal {
422 files.extend(module.typedefs);
423 continue;
424 }
425
426 modules.insert(
427 mod_id,
428 ModuleInfo {
429 file_ids: module.files.keys().copied().collect(),
430 typedef_ids: module.typedefs.keys().copied().collect(),
431 id: module.id.clone(),
432 path: module.id,
433 },
434 );
435
436 files.extend(module.files);
437 files.extend(module.typedefs);
438 }
439
440 let result = SemanticResult {
441 files,
442 definitions,
443 modules,
444 errors,
445 lints,
446 entry_module_id: ENTRY_MODULE_ID.to_string(),
447 unused,
448 mutations,
449 cached_modules,
450 ufcs_methods,
451 typedef_paths: store.typedef_paths,
452 go_package_names: store.go_package_names,
453 go_module_ids,
454 };
455
456 AnalyzeOutput {
457 result,
458 facts,
459 emit_stamps,
460 }
461}