microcad_lang/resolve/
resolve_context.rs

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