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        }
180
181        if resolved {
182            log::info!("Resolve OK ({passes_needed} passes).");
183        } else {
184            log::info!("Resolve failed after {passes_needed} passes.");
185        }
186        log::debug!("Resolved symbol table:\n{self:?}");
187
188        self.mode = ResolveMode::Resolved;
189
190        Ok(())
191    }
192
193    fn has_links(&self) -> bool {
194        self.symbol_table
195            .symbols()
196            .iter()
197            .filter(|symbol| !symbol.is_deleted())
198            .any(|symbol| symbol.has_links())
199    }
200
201    /// check names in all symbols
202    pub fn check(&mut self) -> ResolveResult<()> {
203        log::trace!("Checking symbol table");
204        self.mode = ResolveMode::Failed;
205
206        let exclude_ids = self.symbol_table.search_target_mode_ids()?;
207        log::trace!("Excluding target mode ids: {exclude_ids}");
208
209        if let Err(err) = self
210            .symbol_table
211            .symbols()
212            .iter_mut()
213            .try_for_each(|symbol| symbol.check(self, &exclude_ids))
214        {
215            self.error(&crate::src_ref::SrcRef::default(), err)?;
216        } else if !self.has_errors() {
217            self.mode = ResolveMode::Checked;
218        }
219
220        log::info!("Symbol table OK!");
221
222        let unchecked = self.symbol_table.unchecked();
223        log::trace!(
224            "Symbols never used in ANY code:\n{}",
225            unchecked
226                .iter()
227                .map(|symbol| format!("{symbol:?}"))
228                .collect::<Vec<_>>()
229                .join("\n")
230        );
231        self.unchecked = Some(unchecked);
232
233        Ok(())
234    }
235
236    /// Load file into source cache and symbolize it into a symbol.
237    pub fn symbolize_file(
238        &mut self,
239        visibility: Visibility,
240        parent_path: impl AsRef<std::path::Path>,
241        id: &Identifier,
242    ) -> ResolveResult<Symbol> {
243        self.sources
244            .load_file(parent_path, id)?
245            .symbolize(visibility, self)
246    }
247
248    /// Create a symbol out of all sources (without resolving them)
249    /// Return `true` if context has been resolved (or checked as well)
250    pub fn is_checked(&self) -> bool {
251        self.mode >= ResolveMode::Checked
252    }
253}
254
255impl WriteToFile for ResolveContext {}
256
257impl PushDiag for ResolveContext {
258    fn push_diag(&mut self, diag: Diagnostic) -> DiagResult<()> {
259        self.diag.push_diag(diag)
260    }
261}
262
263impl Diag for ResolveContext {
264    fn fmt_diagnosis(&self, f: &mut dyn std::fmt::Write) -> std::fmt::Result {
265        self.diag.pretty_print(f, self)
266    }
267
268    fn warning_count(&self) -> u32 {
269        self.diag.error_count()
270    }
271
272    fn error_count(&self) -> u32 {
273        self.diag.error_count()
274    }
275
276    fn error_lines(&self) -> std::collections::HashSet<usize> {
277        self.diag.error_lines()
278    }
279
280    fn warning_lines(&self) -> std::collections::HashSet<usize> {
281        self.diag.warning_lines()
282    }
283}
284
285impl GetSourceByHash for ResolveContext {
286    fn get_by_hash(&self, hash: u64) -> ResolveResult<std::rc::Rc<SourceFile>> {
287        self.sources.get_by_hash(hash)
288    }
289}
290
291impl std::fmt::Debug for ResolveContext {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        writeln!(f, "Sources:\n")?;
294        write!(f, "{:?}", &self.sources)?;
295        writeln!(f, "\nSymbols:\n")?;
296        write!(f, "{:?}", &self.symbol_table)?;
297        let err_count = self.diag.error_count();
298        if err_count == 0 {
299            writeln!(f, "No errors.")?;
300        } else {
301            writeln!(f, "\n{err_count} error(s):\n")?;
302            self.diag.pretty_print(f, &self.sources)?;
303        }
304        if let Some(unchecked) = &self.unchecked {
305            writeln!(f, "\nUnchecked:\n{unchecked}")?;
306        }
307        Ok(())
308    }
309}
310
311impl std::fmt::Display for ResolveContext {
312    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        if let Some(unchecked) = &self.unchecked {
314            writeln!(f, "Resolved & checked symbols:\n{}", self.symbol_table)?;
315            if unchecked.is_empty() {
316                writeln!(f, "All symbols are referenced.\n{}", self.symbol_table)?;
317            } else {
318                writeln!(
319                    f,
320                    "Unreferenced symbols:\n{}\n",
321                    unchecked
322                        .iter()
323                        .filter(|symbol| !symbol.is_deleted())
324                        .map(|symbol| symbol.full_name().to_string())
325                        .collect::<Vec<_>>()
326                        .join(", ")
327                )?;
328            }
329        } else {
330            writeln!(f, "Resolved symbols:\n{}", self.symbol_table)?;
331        }
332        if self.has_errors() {
333            writeln!(
334                f,
335                "{err} error(s) and {warn} warning(s) so far:\n{diag}",
336                err = self.error_count(),
337                warn = self.warning_count(),
338                diag = self.diagnosis()
339            )?;
340        } else {
341            writeln!(f, "No errors so far.")?;
342        }
343        Ok(())
344    }
345}