Skip to main content

lisette_semantics/
analyze.rs

1use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
2use std::path::PathBuf;
3
4use diagnostics::{DiagnosticSink, SemanticResult};
5use syntax::ast::Expression;
6use syntax::program::{File, ModuleInfo, MutationInfo, UnusedInfo};
7
8use crate::cache::{
9    CompiledModule, compute_module_hash, get_dependency_module_hashes,
10    go_stdlib::{self, load_cached_go_module},
11    hash_module_sources, is_cache_disabled, prelude as prelude_cache, register_cached_module,
12    save_module_cache, try_load_cache,
13};
14use crate::checker::Checker;
15use crate::checker::infer::checks::check_interface_visibility;
16use crate::facts::Facts;
17use crate::lint;
18use crate::loader::Loader;
19use crate::module_graph::build_module_graph;
20use crate::pattern_analysis;
21use crate::prelude::parse_and_register_prelude;
22use crate::store::{ENTRY_MODULE_ID, Store};
23use stdlib::get_go_stdlib_typedef;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum CompilePhase {
27    #[default]
28    Check,
29    Emit,
30}
31
32#[derive(Debug, Clone, Default)]
33pub struct SemanticConfig {
34    pub run_lints: bool,
35    pub standalone_mode: bool,
36    pub load_siblings: bool,
37}
38
39pub struct AnalyzeInput<'a> {
40    pub config: SemanticConfig,
41    pub loader: &'a dyn Loader,
42    pub source: String,
43    pub filename: String,
44    pub ast: Vec<Expression>,
45    pub project_root: Option<PathBuf>,
46    pub compile_phase: CompilePhase,
47}
48
49pub fn analyze(input: AnalyzeInput) -> (SemanticResult, Facts) {
50    let mut store = Store::new();
51
52    store.init_entry_module();
53    store.store_entry_file(&input.filename, &input.source, input.ast);
54
55    let sink = DiagnosticSink::new();
56
57    if input.config.load_siblings {
58        for (filename, source) in input.loader.scan_folder(ENTRY_MODULE_ID) {
59            if filename == input.filename
60                || !filename.ends_with(".lis")
61                || filename.ends_with(".d.lis")
62            {
63                continue;
64            }
65            let file_id = store.new_file_id();
66            let result = syntax::build_ast(&source, file_id);
67            sink.extend_parse_errors(result.errors);
68            store.store_file(
69                ENTRY_MODULE_ID,
70                File::new(ENTRY_MODULE_ID, &filename, &source, result.ast, file_id),
71            );
72        }
73    }
74
75    let entry_module = store.entry_module_id().to_string();
76    let mut graph_result = build_module_graph(
77        &mut store,
78        Some(input.loader),
79        &entry_module,
80        &sink,
81        input.config.standalone_mode,
82    );
83
84    for cycle in &graph_result.cycles {
85        sink.push(diagnostics::module_graph::import_cycle(cycle));
86    }
87
88    let has_pre_check_errors = sink.has_errors();
89
90    let cache_disabled = is_cache_disabled();
91
92    let prelude_cache_hit = if cache_disabled {
93        false
94    } else if let Some(cached) = prelude_cache::try_load_prelude_cache() {
95        prelude_cache::register_cached_prelude(&mut store, cached);
96        true
97    } else {
98        false
99    };
100
101    if !prelude_cache_hit {
102        parse_and_register_prelude(&mut store, &sink);
103    }
104
105    let cache_enabled = input.project_root.is_some() && !cache_disabled;
106    let check_go_files = input.compile_phase == CompilePhase::Emit;
107
108    let (mut facts, coercions, resolutions, cached_modules, compiled_modules, ufcs_methods) = {
109        let mut checker = Checker::new(&mut store, &sink);
110        checker
111            .ufcs_methods
112            .extend(crate::prelude::compute_prelude_ufcs(checker.store));
113
114        let mut module_hashes: HashMap<String, u64> = HashMap::default();
115        let mut cached_modules: HashSet<String> = HashSet::default();
116        let mut compiled_modules: Vec<CompiledModule> = vec![];
117
118        let order = std::mem::take(&mut graph_result.order);
119        let edges = &graph_result.edges;
120
121        let go_cache = if cache_disabled {
122            None
123        } else {
124            go_stdlib::try_load_go_stdlib_cache()
125        };
126
127        for module_id in order {
128            if let Some(go_pkg) = module_id.strip_prefix("go:") {
129                if let Some(ref cache) = go_cache {
130                    // Load this module + transitive deps from cache
131                    load_cached_go_module(checker.store, &module_id, cache);
132                    if checker.store.is_visited(&module_id) {
133                        continue;
134                    }
135                }
136                // Cache miss: parse and register
137                if let Some(typedef) = get_go_stdlib_typedef(go_pkg) {
138                    checker.parse_and_register_go_module(&module_id, typedef);
139                }
140                continue;
141            }
142
143            if checker.store.is_visited(&module_id) {
144                continue;
145            }
146
147            let files = graph_result.files.remove(&module_id).unwrap_or_default();
148            let source_hash = hash_module_sources(&files);
149
150            let dep_hashes = get_dependency_module_hashes(&module_id, edges, &module_hashes);
151            let module_hash = compute_module_hash(source_hash, &dep_hashes);
152            module_hashes.insert(module_id.clone(), module_hash);
153
154            let is_entry = module_id == ENTRY_MODULE_ID;
155
156            if cache_enabled
157                && !is_entry
158                && let Some(ref project_root) = input.project_root
159                && let Some(cached) = try_load_cache(
160                    &module_id,
161                    source_hash,
162                    &dep_hashes,
163                    project_root,
164                    check_go_files,
165                )
166            {
167                checker
168                    .ufcs_methods
169                    .extend(cached.ufcs_methods.iter().cloned());
170                register_cached_module(checker.store, &module_id, cached);
171                cached_modules.insert(module_id.clone());
172                continue;
173            }
174
175            let prev_module_id = checker.cursor.module_id.clone();
176            checker.cursor.module_id = module_id.to_string();
177
178            checker.store.store_module(&module_id, files);
179            checker.register_module(&module_id);
180            checker.infer_module(&module_id);
181            check_interface_visibility(checker.store, &module_id, &sink);
182
183            checker.cursor.module_id = prev_module_id;
184
185            if cache_enabled && !is_entry {
186                compiled_modules.push(CompiledModule {
187                    module_id: module_id.clone(),
188                    source_hash,
189                    dep_hashes,
190                });
191            }
192        }
193
194        // Save Go stdlib cache if store has Go modules not already in cache
195        if !cache_disabled {
196            let all_go_modules: Vec<String> = checker
197                .store
198                .modules
199                .keys()
200                .filter(|id| id.starts_with("go:"))
201                .cloned()
202                .collect();
203            let needs_save = !all_go_modules.is_empty()
204                && go_cache.as_ref().is_none_or(|c| {
205                    all_go_modules.len() != c.modules.len()
206                        || all_go_modules.iter().any(|id| !c.modules.contains_key(id))
207                });
208            if needs_save {
209                go_stdlib::save_go_stdlib_cache(checker.store, &all_go_modules);
210            }
211        }
212
213        if !cache_disabled && !prelude_cache_hit {
214            prelude_cache::save_prelude_cache(checker.store);
215        }
216
217        (
218            checker.facts,
219            checker.coercions,
220            checker.resolutions,
221            cached_modules,
222            compiled_modules,
223            checker.ufcs_methods,
224        )
225    };
226
227    let pattern_ctx = pattern_analysis::Context::new(&store, &facts.or_pattern_error_spans);
228    for module in store.modules.values() {
229        for file in module.files.values() {
230            for expression in &file.items {
231                pattern_analysis::check(expression, &pattern_ctx, &sink);
232            }
233        }
234    }
235    facts.pattern_issues = pattern_ctx.take_issues();
236
237    let errors = sink.take();
238
239    let unused = if input.config.run_lints && !has_pre_check_errors {
240        lint::lint_all_modules(&store, &facts, &sink)
241    } else {
242        UnusedInfo::default()
243    };
244
245    let mut mutations = MutationInfo::default();
246    for (&binding_id, b) in facts.bindings.iter() {
247        if b.mutated {
248            mutations.mark_binding_mutated(binding_id);
249        }
250    }
251
252    let lints = sink.take();
253
254    if cache_enabled && let Some(ref project_root) = input.project_root {
255        let has_errors = errors.iter().any(|e| e.is_error());
256        if !has_errors {
257            for compiled in compiled_modules {
258                let file_ids: HashSet<u32> = store
259                    .get_module(&compiled.module_id)
260                    .map(|m| m.file_ids().collect())
261                    .unwrap_or_default();
262
263                let has_module_warnings = lints.iter().any(|lint| {
264                    lint.file_id()
265                        .map(|fid| file_ids.contains(&fid))
266                        .unwrap_or(false)
267                });
268                if !has_module_warnings
269                    && let Err(e) =
270                        save_module_cache(&compiled, &store, project_root, &ufcs_methods)
271                {
272                    eprintln!(
273                        "warning: failed to write cache for {}: {e}",
274                        compiled.module_id
275                    );
276                }
277            }
278        }
279    }
280
281    let mut files = HashMap::default();
282    let mut definitions = HashMap::default();
283    let mut modules = HashMap::default();
284
285    for (mod_id, module) in store.modules {
286        let is_internal = module.is_internal();
287
288        definitions.extend(module.definitions);
289
290        if is_internal {
291            continue;
292        }
293
294        modules.insert(
295            mod_id,
296            ModuleInfo {
297                file_ids: module.files.keys().copied().collect(),
298                typedef_ids: module.typedefs.keys().copied().collect(),
299                id: module.id.clone(),
300                path: module.id,
301            },
302        );
303
304        files.extend(module.files);
305        files.extend(module.typedefs);
306    }
307
308    let result = SemanticResult {
309        files,
310        definitions,
311        modules,
312        errors,
313        lints,
314        entry_module_id: ENTRY_MODULE_ID.to_string(),
315        unused,
316        mutations,
317        coercions,
318        resolutions,
319        cached_modules,
320        ufcs_methods,
321    };
322
323    (result, facts)
324}