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        "../tests/test_files/update_files/sub/sub_0.µcad",
314        "../tests/test_files/update_files/sub/sub.µcad",
315    )
316    .expect("test error");
317
318    let root =
319        SourceFile::load("../tests/test_files/update_files/sub/top.µcad").expect("test error");
320    let mut context = ResolveContext::test_create(root, ResolveMode::Checked).expect("test error");
321
322    eprintln!("{context:?}");
323
324    std::fs::copy(
325        "../tests/test_files/update_files/sub/sub_1.µcad",
326        "../tests/test_files/update_files/sub/sub.µcad",
327    )
328    .expect("test error");
329
330    context
331        .reload_files(&["../tests/test_files/update_files/sub/sub.µcad"])
332        .expect("test error");
333
334    eprintln!("{context:?}");
335
336    let mut context = EvalContext::new(
337        context,
338        Stdout::new(),
339        Default::default(),
340        Default::default(),
341    );
342    context.eval().expect("test error");
343    assert!(!context.has_errors());
344}
345
346#[test]
347fn test_update_top_mod() {
348    use crate::eval::*;
349
350    std::fs::copy(
351        "../tests/test_files/update_files/top/top_0.µcad",
352        "../tests/test_files/update_files/top/top.µcad",
353    )
354    .expect("test error");
355
356    let root =
357        SourceFile::load("../tests/test_files/update_files/top/top.µcad").expect("test error");
358    let mut context = ResolveContext::test_create(root, ResolveMode::Checked).expect("test error");
359
360    eprintln!("{context:?}");
361
362    std::fs::copy(
363        "../tests/test_files/update_files/top/top_1.µcad",
364        "../tests/test_files/update_files/top/top.µcad",
365    )
366    .expect("test error");
367
368    context
369        .reload_files(&["../tests/test_files/update_files/top/top.µcad"])
370        .expect("test error");
371
372    eprintln!("{context:?}");
373
374    let mut context = EvalContext::new(
375        context,
376        Stdout::new(),
377        Default::default(),
378        Default::default(),
379    );
380    context.eval().expect("test error");
381    assert!(!context.has_errors());
382}
383
384impl WriteToFile for ResolveContext {}
385
386impl PushDiag for ResolveContext {
387    fn push_diag(&mut self, diag: Diagnostic) -> DiagResult<()> {
388        self.diag.push_diag(diag)
389    }
390}
391
392impl Diag for ResolveContext {
393    fn fmt_diagnosis(&self, f: &mut dyn std::fmt::Write) -> std::fmt::Result {
394        self.diag.pretty_print(f, self)
395    }
396
397    fn warning_count(&self) -> u32 {
398        self.diag.error_count()
399    }
400
401    fn error_count(&self) -> u32 {
402        self.diag.error_count()
403    }
404
405    fn error_lines(&self) -> std::collections::HashSet<usize> {
406        self.diag.error_lines()
407    }
408
409    fn warning_lines(&self) -> std::collections::HashSet<usize> {
410        self.diag.warning_lines()
411    }
412}
413
414impl GetSourceByHash for ResolveContext {
415    fn get_by_hash(&self, hash: u64) -> ResolveResult<std::rc::Rc<SourceFile>> {
416        self.sources.get_by_hash(hash)
417    }
418}
419
420impl std::fmt::Debug for ResolveContext {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        writeln!(f, "Sources:\n")?;
423        write!(f, "{:?}", &self.sources)?;
424        writeln!(f, "\nSymbols:\n")?;
425        write!(f, "{:?}", &self.symbol_table)?;
426        let err_count = self.diag.error_count();
427        if err_count == 0 {
428            writeln!(f, "No errors.")?;
429        } else {
430            writeln!(f, "\n{err_count} error(s):\n")?;
431            self.diag.pretty_print(f, &self.sources)?;
432        }
433        if let Some(unchecked) = &self.unchecked {
434            writeln!(f, "\nUnchecked:\n{unchecked}")?;
435        }
436        Ok(())
437    }
438}
439
440impl std::fmt::Display for ResolveContext {
441    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442        if let Some(unchecked) = &self.unchecked {
443            writeln!(f, "Resolved & checked symbols:\n{}", self.symbol_table)?;
444            if unchecked.is_empty() {
445                writeln!(f, "All symbols are referenced.\n{}", self.symbol_table)?;
446            } else {
447                writeln!(
448                    f,
449                    "Unreferenced symbols:\n{}\n",
450                    unchecked
451                        .iter()
452                        .filter(|symbol| !symbol.is_deleted())
453                        .map(|symbol| symbol.full_name().to_string())
454                        .collect::<Vec<_>>()
455                        .join(", ")
456                )?;
457            }
458        } else {
459            writeln!(f, "Resolved symbols:\n{}", self.symbol_table)?;
460        }
461        if self.has_errors() {
462            writeln!(
463                f,
464                "{err} error(s) and {warn} warning(s) so far:\n{diag}",
465                err = self.error_count(),
466                warn = self.warning_count(),
467                diag = self.diagnosis()
468            )?;
469        } else {
470            writeln!(f, "No errors so far.")?;
471        }
472        Ok(())
473    }
474}