Skip to main content

microcad_lang/eval/
eval_context.rs

1// Copyright © 2024-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use microcad_core::hash::HashSet;
5use microcad_lang_base::{
6    Diag, DiagHandler, DiagResult, Diagnostic, FormatTree, GetSourceLocInfoByHash, HashId, Output,
7    PushDiag, SourceLocInfo, SrcReferrer, TreeDisplay, TreeState,
8};
9
10use crate::{
11    builtin::*,
12    eval::*,
13    lower::{SingleIdentifier, ir},
14    model::*,
15    symbol::SymbolDef,
16};
17
18/// *Context* for *evaluation* of a resolved µcad file.
19///
20/// The context is used to store the current state of the evaluation.
21pub struct EvalContext {
22    /// Symbol table
23    pub root: Symbol,
24    /// Source cache
25    sources: Sources,
26    /// Stack of currently opened scopes with symbols while evaluation.
27    pub(super) stack: Stack,
28    /// Output channel for [__builtin::print].
29    output: Box<dyn Output>,
30    /// Exporter registry.
31    exporters: ExporterRegistry,
32    /// Importer registry.
33    importers: ImporterRegistry,
34    /// Diagnostics handler.
35    pub diag: DiagHandler,
36}
37
38impl EvalContext {
39    /// Create a new context from a resolved symbol table.
40    pub fn new(
41        resolve_context: ResolveContext,
42        output: Box<dyn Output>,
43        exporters: ExporterRegistry,
44        importers: ImporterRegistry,
45    ) -> Self {
46        log::debug!("Creating evaluation context");
47
48        Self {
49            root: resolve_context.root,
50            sources: resolve_context.sources,
51            diag: resolve_context.diag,
52            output,
53            exporters,
54            importers,
55            stack: Stack::default(),
56        }
57    }
58
59    /// Current symbol, panics if there no current symbol.
60    pub(crate) fn current_symbol(&self) -> Option<Symbol> {
61        self.stack.current_symbol()
62    }
63
64    /// Create a new context from a source file.
65    pub fn from_source(
66        root: std::rc::Rc<ir::Source>,
67        builtin: Option<Symbol>,
68        search_paths: Vec<std::path::PathBuf>,
69        output: Box<dyn Output>,
70        exporters: ExporterRegistry,
71        importers: ImporterRegistry,
72    ) -> EvalResult<Self> {
73        Ok(Self::new(
74            ResolveContext::create(root, search_paths, builtin, DiagHandler::default())?,
75            output,
76            exporters,
77            importers,
78        ))
79    }
80
81    /// Access captured output.
82    pub fn output(&self) -> Option<String> {
83        self.output.output()
84    }
85
86    /// Print for `__builtin::print`.
87    pub fn print(&mut self, what: String) {
88        self.output.print(what).expect("could not write to output");
89    }
90
91    /// Evaluate context into a value.
92    pub fn eval(&mut self) -> EvalResult<Model> {
93        if self.diag.error_count() > 0 {
94            log::error!("Aborting evaluation because of prior resolve errors!");
95            return Err(EvalError::ResolveFailed.into());
96        }
97        let model: Model = self.sources.root().eval(self)?;
98        log::trace!("Post-evaluation context:\n{self:?}");
99        log::trace!("Evaluated Model:\n{}", FormatTree(&model));
100
101        let unused = self
102            .root
103            .unused_private()
104            .iter()
105            .map(|symbol| {
106                (
107                    match self.sources.get_code(&symbol) {
108                        Ok(id) => id,
109                        Err(_) => symbol.id().to_string(),
110                    },
111                    symbol.src_ref(),
112                )
113            })
114            // intermediate hasp storage to avoid duplicates
115            .collect::<indexmap::IndexMap<_, _>>();
116
117        unused.into_iter().try_for_each(|(id, src_ref)| {
118            self.warning(&src_ref, EvalError::UnusedGlobalSymbol(id))
119        })?;
120
121       
122       Ok(model)
123    }
124
125    /// Run the closure `f` within the given `stack_frame`.
126    pub(super) fn scope<T>(
127        &mut self,
128        stack_frame: StackFrame,
129        f: impl FnOnce(&mut EvalContext) -> T,
130    ) -> T {
131        self.open(stack_frame);
132        let result = f(self);
133        let mut unused: Vec<_> = if let Some(frame) = &self.stack.current_frame() {
134            if let Some(locals) = frame.locals() {
135                locals
136                    .iter()
137                    .filter(|(_, symbol)| !symbol.is_used())
138                    .filter(|(id, _)| !id.ignore())
139                    .filter(|(_, symbol)| !symbol.src_ref().is_none())
140                    .map(|(id, _)| id.clone())
141                    .collect()
142            } else {
143                vec![]
144            }
145        } else {
146            vec![]
147        };
148        unused.sort();
149
150        unused
151            .iter()
152            .try_for_each(|id| self.warning(id, EvalError::UnusedLocal(id.clone())))
153            .expect("diag error");
154
155        self.close();
156        result
157    }
158
159    /// All registered exporters.
160    pub fn exporters(&self) -> &ExporterRegistry {
161        &self.exporters
162    }
163
164    /// Return search paths of this context.
165    pub fn search_paths(&self) -> &Vec<std::path::PathBuf> {
166        self.sources.search_paths()
167    }
168
169    /// Get property from current model.
170    pub(super) fn get_property(&self, id: &Identifier) -> EvalResult<Value> {
171        match self.get_model() {
172            Ok(model) => {
173                if let Some(value) = model.get_property(id) {
174                    Ok(value.clone())
175                } else {
176                    Err(EvalError::PropertyNotFound(id.clone()).into())
177                }
178            }
179            Err(err) => Err(err),
180        }
181    }
182
183    /// Initialize a property.
184    ///
185    /// Returns error if there is no model or the property has been initialized before.
186    pub(super) fn init_property(&self, id: Identifier, value: Value) -> EvalResult<()> {
187        match self.get_model() {
188            Ok(model) => {
189                if let Some(previous_value) = model.borrow_mut().set_property(id.clone(), value) {
190                    if !previous_value.is_invalid() {
191                        return Err(EvalError::ValueAlreadyDefined {
192                            location: id.src_ref(),
193                            name: id.clone(),
194                            value: previous_value.to_string(),
195                            previous_location: id.src_ref(),
196                        }
197                        .into());
198                    }
199                }
200                Ok(())
201            }
202            Err(err) => Err(err),
203        }
204    }
205
206    /// Return if the current frame is an init frame.
207    pub(super) fn is_init(&mut self) -> bool {
208        matches!(self.stack.current_frame(), Some(StackFrame::Init(_)))
209    }
210
211    /// Lookup a property by qualified name.
212    fn lookup_property(&self, name: &ir::QualifiedName) -> EvalResult<Symbol> {
213        log::trace!(
214            "{lookup} for property {name:?}",
215            lookup = microcad_lang_base::mark!(LOOKUP)
216        );
217
218        if self.stack.current_call_name().is_some() {
219            if let Some(id) = name.single_identifier() {
220                match self.get_property(id) {
221                    Ok(value) => {
222                        log::trace!(
223                            "{found} property '{name:?}'",
224                            found = microcad_lang_base::mark!(FOUND)
225                        );
226                        return Ok(Symbol::new(SymbolDef::Value(id.clone(), value), None));
227                    }
228                    Err(err) => return Err(err),
229                }
230            }
231        }
232        log::trace!(
233            "{not_found} Property '{name:?}'",
234            not_found = microcad_lang_base::mark!(NOT_FOUND)
235        );
236        Err(EvalError::NoPropertyId(name.clone()).into())
237    }
238
239    fn lookup_workbench(
240        &self,
241        name: &ir::QualifiedName,
242        target: LookupTarget,
243    ) -> ResolveResult<Symbol> {
244        if let Some(workbench) = &self.stack.current_call_name() {
245            log::trace!(
246                "{lookup} for symbol '{name:?}' in current workbench '{workbench:?}'",
247                lookup = microcad_lang_base::mark!(LOOKUP)
248            );
249            match self.root.lookup_within_name(name, workbench, target) {
250                Ok(symbol) => {
251                    log::trace!(
252                        "{found} symbol in current module: {symbol:?}",
253                        found = microcad_lang_base::mark!(FOUND),
254                    );
255                    Ok(symbol)
256                }
257                Err(err) => {
258                    log::trace!(
259                        "{not_found} symbol '{name:?}': {err}",
260                        not_found = microcad_lang_base::mark!(NOT_FOUND)
261                    );
262                    Err(err)
263                }
264            }
265        } else {
266            log::trace!(
267                "{not_found} No current workbench",
268                not_found = microcad_lang_base::mark!(NOT_FOUND)
269            );
270            Err(ResolveError::SymbolNotFound(name.clone()))
271        }
272    }
273
274    fn lookup_within(
275        &self,
276        name: &ir::QualifiedName,
277        target: LookupTarget,
278    ) -> ResolveResult<Symbol> {
279        self.root.lookup_within(
280            name,
281            &self.root.search(&self.stack.current_module_name(), false)?,
282            target,
283        )
284    }
285}
286
287impl Locals for EvalContext {
288    fn set_local_value(&mut self, id: Identifier, value: Value) -> EvalResult<()> {
289        self.stack.set_local_value(id, value)
290    }
291
292    fn get_local_value(&self, id: &Identifier) -> EvalResult<Value> {
293        self.stack.get_local_value(id)
294    }
295
296    fn open(&mut self, frame: StackFrame) {
297        self.stack.open(frame);
298    }
299
300    fn close(&mut self) -> StackFrame {
301        self.stack.close()
302    }
303
304    fn fetch_symbol(&self, id: &Identifier) -> EvalResult<Symbol> {
305        self.stack.fetch_symbol(id)
306    }
307
308    fn get_model(&self) -> EvalResult<Model> {
309        self.stack.get_model()
310    }
311
312    fn current_name(&self) -> ir::QualifiedName {
313        self.stack.current_name()
314    }
315}
316
317impl Lookup<Box<EvalError>> for EvalContext {
318    fn lookup(&self, name: &ir::QualifiedName, target: LookupTarget) -> EvalResult<Symbol> {
319        log::debug!("Lookup {target} '{name:?}' (at line {:?}):", name.src_ref());
320
321        log::trace!("- lookups -------------------------------------------------------");
322        // collect all symbols that can be found and remember origin
323        let results = [
324            ("local", { self.stack.lookup(name, target) }),
325            ("global", {
326                self.lookup_within(name, target).map_err(|err| err.into())
327            }),
328            ("property", { self.lookup_property(name) }),
329            ("workbench", {
330                self.lookup_workbench(name, target)
331                    .map_err(|err| err.into())
332            }),
333        ]
334        .into_iter();
335
336        log::trace!("- lookup results ------------------------------------------------");
337        let results = results.inspect(|(from, result)| log::trace!("{from}: {:?}", result));
338
339        // collect ok-results and ambiguity errors
340        let (found, mut ambiguities, mut errors) = results.fold(
341            (vec![], vec![], vec![]),
342            |(mut oks, mut ambiguities, mut errors), (origin, result)| {
343                match result {
344                    Ok(symbol) => oks.push((origin, symbol)),
345                    Err(err) => match *err {
346                        EvalError::AmbiguousSymbol(ambiguous, others) => {
347                            ambiguities.push((origin, EvalError::AmbiguousSymbol ( ambiguous, others )));
348                        }                
349                        // ignore all kinds of "not found" errors
350                        EvalError::SymbolNotFound(_)
351                        // for locals
352                        | EvalError::LocalNotFound(_)
353                        // for model property
354                        | EvalError::NoModelInWorkbench
355                        | EvalError::PropertyNotFound(_)
356                        | EvalError::NoPropertyId(_)
357                        // for symbol table
358                        | EvalError::ResolveError(ResolveError::SymbolNotFound(_))
359                        | EvalError::ResolveError(ResolveError::SymbolIsPrivate(_))
360                        | EvalError::ResolveError(ResolveError::NulHash)
361                        | EvalError::ResolveError(ResolveError::WrongTarget) => {},
362                        err => errors.push((origin, err)),
363                    }
364                }
365                (oks, ambiguities, errors)
366            },
367        );
368
369        // log any unexpected errors and return early
370        if !errors.is_empty() {
371            log::error!("Unexpected errors while lookup symbol '{name:?}':");
372            errors
373                .iter()
374                .for_each(|(origin, err)| log::error!("Lookup ({origin}) error: {err}"));
375
376            return Err(errors.remove(0).1.into());
377        }
378
379        // early emit any ambiguity error
380        if !ambiguities.is_empty() {
381            log::debug!(
382                "{ambiguous} Symbol '{name:?}':\n{}",
383                ambiguities
384                    .iter()
385                    .map(|(origin, err)| format!("{origin}: {err}"))
386                    .collect::<Vec<_>>()
387                    .join("\n"),
388                ambiguous = microcad_lang_base::mark!(AMBIGUOUS)
389            );
390            return Err(ambiguities.remove(0).1.into());
391        }
392
393        // filter by lookup target
394        let found: Vec<_> = found
395            .iter()
396            .filter(|(_, symbol)| target.matches(symbol))
397            .collect();
398
399        // check for ambiguity in what's left
400        match found.first() {
401            Some((origin, symbol)) => {
402                // check if all findings point to the same symbol
403                if found.iter().all(|(_, x)| x == symbol) {
404                    log::debug!(
405                        "{found} symbol '{name:?}' in {origin}",
406                        found = microcad_lang_base::mark!(FOUND!)
407                    );
408                    symbol.set_used();
409                    Ok(symbol.clone())
410                } else {
411                    let others: ir::QualifiedNames =
412                        found.iter().map(|(_, symbol)| symbol.full_name()).collect();
413                    log::debug!(
414                        "{ambiguous} symbol '{name:?}' in {others:?}:\n{self:?}",
415                        ambiguous = microcad_lang_base::mark!(AMBIGUOUS),
416                    );
417                    Err(EvalError::AmbiguousSymbol(name.clone(), others).into())
418                }
419            }
420            None => {
421                log::debug!(
422                    "{not_found} Symbol '{name:?}'",
423                    not_found = microcad_lang_base::mark!(NOT_FOUND!)
424                );
425                Err(EvalError::SymbolNotFound(name.clone()).into())
426            }
427        }
428    }
429
430    fn ambiguity_error(ambiguous: ir::QualifiedName, others: ir::QualifiedNames) -> Box<EvalError> {
431        EvalError::AmbiguousSymbol(ambiguous, others).into()
432    }
433}
434
435impl microcad_lang_base::Diag for EvalContext {
436    fn fmt_diagnosis(&self, f: &mut dyn std::fmt::Write) -> std::fmt::Result {
437        self.diag.pretty_print(f, self)
438    }
439
440    fn warning_count(&self) -> u32 {
441        self.diag.warning_count()
442    }
443
444    fn error_count(&self) -> u32 {
445        self.diag.error_count()
446    }
447
448    fn error_lines(&self) -> HashSet<u32> {
449        self.diag.error_lines()
450    }
451
452    fn warning_lines(&self) -> HashSet<u32> {
453        self.diag.warning_lines()
454    }
455}
456
457impl PushDiag for EvalContext {
458    fn push_diag(&mut self, diag: Diagnostic) -> DiagResult<()> {
459        self.diag.push_diag(diag)
460    }
461}
462
463impl GetSourceByHash for EvalContext {
464    fn get_by_hash(&self, hash: u64) -> ResolveResult<std::rc::Rc<ir::Source>> {
465        self.sources.get_by_hash(hash)
466    }
467}
468
469impl GetSourceLocInfoByHash for EvalContext {
470    fn get_source_loc_info_by_hash(&'_ self, hash: HashId) -> Option<SourceLocInfo<'_>> {
471        self.sources.get_source_loc_info_by_hash(hash)
472    }
473}
474
475impl std::fmt::Debug for EvalContext {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477        if let Ok(model) = self.get_model() {
478            write!(f, "\nModel:\n")?;
479            model.tree_print(f, TreeState::new_debug(4))?;
480        }
481        writeln!(f, "\nCurrent: {:?}", self.stack.current_name())?;
482        writeln!(f, "\nModule: {:?}", self.stack.current_module_name())?;
483        write!(f, "\nLocals Stack:\n{:?}", self.stack)?;
484        writeln!(f, "\nCall Stack:")?;
485        self.stack.pretty_print_call_trace(f, &self.sources)?;
486
487        writeln!(f, "\nSources:\n")?;
488        write!(f, "{:?}", &self.sources)?;
489
490        write!(f, "\nSymbol Table:\n")?;
491        self.root.tree_print(f, TreeState::new_debug(0))?;
492
493        match self.error_count() {
494            0 => write!(f, "No errors")?,
495            1 => write!(f, "1 error")?,
496            _ => write!(f, "{} errors", self.error_count())?,
497        };
498        match self.warning_count() {
499            0 => writeln!(
500                f,
501                ", no warnings{}",
502                if self.error_count() > 0 { ":" } else { "." }
503            )?,
504            1 => writeln!(f, ", 1 warning:")?,
505            _ => writeln!(f, ", {} warnings:", self.warning_count())?,
506        };
507        self.fmt_diagnosis(f)?;
508        Ok(())
509    }
510}
511
512impl ImporterRegistryAccess for EvalContext {
513    type Error = Box<EvalError>;
514
515    fn import(
516        &mut self,
517        arg_map: &Tuple,
518        search_paths: &[std::path::PathBuf],
519    ) -> Result<Value, Self::Error> {
520        match self.importers.import(arg_map, search_paths) {
521            Ok(value) => Ok(value),
522            Err(err) => {
523                self.error(arg_map, err)?;
524                Ok(Value::None)
525            }
526        }
527    }
528}
529
530impl ExporterAccess for EvalContext {
531    fn exporter_by_id(&self, id: &crate::Id) -> Result<std::rc::Rc<dyn Exporter>, ExportError> {
532        self.exporters.exporter_by_id(id)
533    }
534
535    fn exporter_by_filename(
536        &self,
537        filename: &std::path::Path,
538    ) -> Result<std::rc::Rc<dyn Exporter>, ExportError> {
539        self.exporters.exporter_by_filename(filename)
540    }
541}