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