Skip to main content

microcad_lang/resolve/
resolve_context.rs

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