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