microcad_lang/eval/
eval_context.rs

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