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