Skip to main content

microcad_lang/resolve/
resolve_context.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Resolve Context
5
6#[cfg(test)]
7use std::rc::Rc;
8
9use microcad_lang_base::{
10    Diag, DiagHandler, DiagResult, Diagnostic, GetSourceStrByHash, PushDiag, SrcReferrer,
11    TreeDisplay, TreeState, WriteToFile,
12};
13
14use crate::{
15    resolve::*,
16    symbol::{Symbol, Symbols},
17    syntax::*,
18};
19
20/// Resolve Context
21#[derive(Default)]
22pub struct ResolveContext {
23    /// Symbol table.
24    pub root: Symbol,
25    /// Source file cache.
26    pub(crate) sources: Sources,
27    /// Diagnostic handler.
28    pub(crate) diag: DiagHandler,
29    /// Unchecked symbols.
30    ///
31    /// Filled by [check()] with symbols which are not in use in ANY checked code.
32    unchecked: Option<Symbols>,
33    /// Signals resolve stage.
34    mode: ResolveMode,
35}
36
37/// Select what {ResolveContext::create()] automatically does.
38#[derive(Default, PartialEq, PartialOrd)]
39pub enum ResolveMode {
40    /// Failed context.
41    Failed,
42    /// Only load the sources.
43    #[default]
44    Loaded,
45    /// Create symbol table.
46    Symbolized,
47    /// Resolve symbol table.
48    Resolved,
49    /// Check symbol table.
50    Checked,
51}
52
53impl ResolveContext {
54    /// Load resolve and check a source file and referenced files.
55    pub fn create(
56        root: std::rc::Rc<SourceFile>,
57        search_paths: &[impl AsRef<std::path::Path>],
58        builtin: Option<Symbol>,
59        diag: DiagHandler,
60    ) -> ResolveResult<Self> {
61        let mut context = Self {
62            sources: Sources::load(root.clone(), search_paths)?,
63            diag,
64            ..Default::default()
65        };
66        match context.load(builtin, ResolveMode::Checked) {
67            Ok(()) => Ok(context),
68            Err(err) => {
69                context.error(&err.src_ref(), err)?;
70                Ok(context)
71            }
72        }
73    }
74
75    fn load(&mut self, builtin: Option<Symbol>, mode: ResolveMode) -> ResolveResult<()> {
76        self.symbolize()?;
77        log::trace!("Symbolized Context:\n{self:?}");
78        if let Some(builtin) = builtin {
79            log::trace!("Added builtin library {id}.", id = builtin.id());
80            self.root.add_symbol(builtin)?;
81        }
82        if matches!(mode, ResolveMode::Resolved | ResolveMode::Checked) {
83            self.resolve()?;
84        }
85        Ok(())
86    }
87
88    #[cfg(test)]
89    pub(crate) fn test_create(root: Rc<SourceFile>, mode: ResolveMode) -> ResolveResult<Self> {
90        let mut context = Self {
91            sources: Sources::load(root.clone(), &[] as &[std::path::PathBuf])?,
92            ..Default::default()
93        };
94        context.load(None, mode)?;
95        Ok(context)
96    }
97
98    pub(crate) fn symbolize(&mut self) -> ResolveResult<()> {
99        assert!(matches!(self.mode, ResolveMode::Loaded));
100        self.mode = ResolveMode::Failed;
101
102        let named_symbols = self
103            .sources
104            .clone()
105            .iter()
106            .map(|source| {
107                match (
108                    self.sources.generate_name_from_path(&source.filename()),
109                    source.symbolize(Visibility::Public, self),
110                ) {
111                    (Ok(name), Ok(symbol)) => Ok((name, symbol)),
112                    (_, Err(err)) | (Err(err), _) => Err(err),
113                }
114            })
115            .collect::<ResolveResult<Vec<_>>>()?;
116        for (name, symbol) in named_symbols {
117            if let Some(id) = name.single_identifier() {
118                self.root.insert_symbol(id.clone(), symbol)?;
119            } else {
120                unreachable!("name is not an id")
121            }
122        }
123
124        self.mode = ResolveMode::Symbolized;
125
126        Ok(())
127    }
128
129    pub(super) fn resolve(&mut self) -> ResolveResult<()> {
130        assert!(matches!(self.mode, ResolveMode::Symbolized));
131        self.mode = ResolveMode::Failed;
132
133        // resolve std as first
134        if let Some(std) = self.root.get_child(&Identifier::no_ref("std")) {
135            std.resolve(self)?;
136        }
137
138        // multi pass resolve
139        const MAX_PASSES: usize = 3;
140        let mut passes_needed = 0;
141        let mut resolved = false;
142        for _ in 0..MAX_PASSES {
143            self.root
144                .iter()
145                .filter(|child| child.is_resolvable())
146                .map(|child| child.resolve(self))
147                .collect::<Result<Vec<_>, _>>()?;
148            passes_needed += 1;
149            if !self.has_links() {
150                resolved = true;
151                break;
152            }
153            self.diag.clear()
154        }
155
156        if resolved {
157            log::info!("Resolve OK ({passes_needed} passes).");
158        } else {
159            log::info!("Resolve failed after {passes_needed} passes.");
160        }
161        log::debug!("Resolved symbol table:\n{self:?}");
162
163        self.mode = ResolveMode::Resolved;
164
165        Ok(())
166    }
167
168    fn has_links(&self) -> bool {
169        self.root
170            .iter()
171            .filter(|symbol| !symbol.is_deleted())
172            .any(|symbol| symbol.has_links())
173    }
174
175    /// Load file into source cache and symbolize it into a symbol.
176    pub fn symbolize_file(
177        &mut self,
178        visibility: Visibility,
179        parent_path: impl AsRef<std::path::Path>,
180        id: &Identifier,
181    ) -> ResolveResult<Symbol> {
182        let mut symbol = self
183            .sources
184            .load_mod_file(parent_path, id)?
185            .symbolize(visibility, self)?;
186        symbol.set_src_ref(id.src_ref());
187        Ok(symbol)
188    }
189
190    /// Create a symbol out of all sources (without resolving them)
191    /// Return `true` if context has been resolved (or checked as well)
192    pub fn is_checked(&self) -> bool {
193        self.mode >= ResolveMode::Checked
194    }
195
196    /// Reload one or more existing files and re-resolve the symbol table.
197    pub fn reload_files(&mut self, files: &[impl AsRef<std::path::Path>]) -> ResolveResult<()> {
198        // prepare for any error
199        self.mode = ResolveMode::Failed;
200
201        // reload existing source files in source cache
202        let replaced = files
203            .iter()
204            .map(|path| self.sources.update_file(path.as_ref()))
205            .collect::<ResolveResult<Vec<_>>>()?;
206
207        // replace the source file within the symbol table
208        replaced.iter().try_for_each(|rep| {
209            self.root
210                .riter()
211                .filter(|symbol| symbol.is_source())
212                .find(|symbol| symbol.source_hash() == rep.old.hash)
213                .map(|mut symbol| -> ResolveResult<()> {
214                    symbol.replace(rep.new.symbolize(Visibility::Public, self)?);
215                    Ok(())
216                })
217                .expect("symbol of file could not be found")
218        })?;
219
220        // search for aliases and symbols from the replaced file
221        replaced.iter().for_each(|rep| {
222            self.root.riter().for_each(|symbol| {
223                // check for alias or use-all (from use statements)
224                if let Some(link) = symbol.get_link() {
225                    // check if it points into the old source file
226                    if link.starts_with(&rep.old.name) {
227                        // then reset visibility so that it will get re-resolved.
228                        symbol.reset_visibility();
229                    }
230                } else if symbol.source_hash() == rep.old.hash {
231                    // delete any non-link symbol which is related to the old source file
232                    symbol.delete();
233                }
234            });
235        });
236
237        // re-resolve
238        self.mode = ResolveMode::Symbolized;
239        self.resolve()
240    }
241}
242
243#[test]
244fn test_update_sub_mod() {
245    use crate::eval::*;
246
247    std::fs::copy(
248        "../../tests/test_files/update_files/sub/sub_0.µcad",
249        "../../tests/test_files/update_files/sub/sub.µcad",
250    )
251    .expect("test error");
252
253    let root =
254        SourceFile::load("../../tests/test_files/update_files/sub/top.µcad").expect("test error");
255    let mut context = ResolveContext::test_create(root, ResolveMode::Checked).expect("test error");
256
257    eprintln!("{context:?}");
258
259    std::fs::copy(
260        "../../tests/test_files/update_files/sub/sub_1.µcad",
261        "../../tests/test_files/update_files/sub/sub.µcad",
262    )
263    .expect("test error");
264
265    context
266        .reload_files(&["../../tests/test_files/update_files/sub/sub.µcad"])
267        .expect("test error");
268
269    eprintln!("{context:?}");
270
271    let mut context = EvalContext::new(
272        context,
273        Stdout::new(),
274        Default::default(),
275        Default::default(),
276    );
277    context.eval().expect("test error");
278    assert!(!context.has_errors());
279}
280
281#[test]
282fn test_update_top_mod() {
283    use crate::eval::*;
284
285    let _ = env_logger::try_init();
286
287    std::fs::copy(
288        "../../tests/test_files/update_files/top/top_0.µcad",
289        "../../tests/test_files/update_files/top/top.µcad",
290    )
291    .expect("test error");
292
293    let root =
294        SourceFile::load("../../tests/test_files/update_files/top/top.µcad").expect("test error");
295    let mut context = ResolveContext::test_create(root, ResolveMode::Checked).expect("test error");
296
297    eprintln!("{context:?}");
298
299    std::fs::copy(
300        "../../tests/test_files/update_files/top/top_1.µcad",
301        "../../tests/test_files/update_files/top/top.µcad",
302    )
303    .expect("test error");
304
305    context
306        .reload_files(&["../../tests/test_files/update_files/top/top.µcad"])
307        .expect("test error");
308
309    eprintln!("{context:?}");
310
311    let mut context = EvalContext::new(
312        context,
313        Stdout::new(),
314        Default::default(),
315        Default::default(),
316    );
317    context.eval().expect("test error");
318    eprintln!("{}", context.diagnosis());
319    assert!(!context.has_errors());
320}
321
322impl WriteToFile for ResolveContext {}
323
324impl PushDiag for ResolveContext {
325    fn push_diag(&mut self, diag: Diagnostic) -> DiagResult<()> {
326        self.diag.push_diag(diag)
327    }
328}
329
330impl GetSourceStrByHash for ResolveContext {
331    fn get_str_by_hash(&self, hash: u64) -> Option<&str> {
332        self.sources.get_str_by_hash(hash)
333    }
334
335    fn get_filename_by_hash(&self, hash: u64) -> Option<std::path::PathBuf> {
336        self.sources.get_filename_by_hash(hash)
337    }
338}
339
340impl Diag for ResolveContext {
341    fn fmt_diagnosis(&self, f: &mut dyn std::fmt::Write) -> std::fmt::Result {
342        self.diag.pretty_print(f, self)
343    }
344
345    fn warning_count(&self) -> u32 {
346        self.diag.error_count()
347    }
348
349    fn error_count(&self) -> u32 {
350        self.diag.error_count()
351    }
352
353    fn error_lines(&self) -> std::collections::HashSet<usize> {
354        self.diag.error_lines()
355    }
356
357    fn warning_lines(&self) -> std::collections::HashSet<usize> {
358        self.diag.warning_lines()
359    }
360}
361
362impl GetSourceByHash for ResolveContext {
363    fn get_by_hash(&self, hash: u64) -> ResolveResult<std::rc::Rc<SourceFile>> {
364        self.sources.get_by_hash(hash)
365    }
366}
367
368impl std::fmt::Debug for ResolveContext {
369    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370        writeln!(f, "Sources:\n")?;
371        write!(f, "{:?}", &self.sources)?;
372        writeln!(f, "\nSymbols:\n")?;
373        self.root.tree_print(f, TreeState::new_debug(1))?;
374        let err_count = self.diag.error_count();
375        if err_count == 0 {
376            writeln!(f, "No errors.")?;
377        } else {
378            writeln!(f, "\n{err_count} error(s):\n")?;
379            self.diag.pretty_print(f, &self.sources)?;
380        }
381        if let Some(unchecked) = &self.unchecked {
382            writeln!(f, "\nUnchecked:\n{unchecked}")?;
383        }
384        Ok(())
385    }
386}
387
388impl std::fmt::Display for ResolveContext {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390        if let Some(unchecked) = &self.unchecked {
391            writeln!(f, "Resolved & checked symbols:")?;
392            self.root.tree_print(f, TreeState::new_display())?;
393            if unchecked.is_empty() {
394                writeln!(f, "All symbols are referenced.\n{}", self.root)?;
395            } else {
396                writeln!(
397                    f,
398                    "Unreferenced symbols:\n{}\n",
399                    unchecked
400                        .iter()
401                        .filter(|symbol| !symbol.is_deleted())
402                        .map(|symbol| symbol.full_name().to_string())
403                        .collect::<Vec<_>>()
404                        .join(", ")
405                )?;
406            }
407        } else {
408            writeln!(f, "Resolved symbols:")?;
409            self.root.tree_print(f, TreeState::new_display())?;
410        }
411        if self.has_errors() {
412            writeln!(
413                f,
414                "{diag}{err} error(s) and {warn} warning(s) so far.",
415                err = self.error_count(),
416                warn = self.warning_count(),
417                diag = self.diagnosis()
418            )?;
419        } else {
420            writeln!(f, "No errors so far.")?;
421        }
422        Ok(())
423    }
424}