Skip to main content

microcad_lang/eval/
eval_context.rs

1// Copyright © 2024-2026 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    root: Symbol,
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            root: resolve_context.root,
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            .root
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.root.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!("{found} property '{name:?}'", found = crate::mark!(FOUND));
222                        return Ok(Symbol::new(
223                            SymbolDef::Constant(Visibility::Public, id.clone(), value),
224                            None,
225                        ));
226                    }
227                    Err(err) => return Err(err),
228                }
229            }
230        }
231        log::trace!(
232            "{not_found} Property '{name:?}'",
233            not_found = crate::mark!(NOT_FOUND)
234        );
235        Err(EvalError::NoPropertyId(name.clone()))
236    }
237
238    fn lookup_workbench(
239        &self,
240        name: &QualifiedName,
241        target: LookupTarget,
242    ) -> ResolveResult<Symbol> {
243        if let Some(workbench) = &self.stack.current_workbench_name() {
244            log::trace!(
245                "{lookup} for symbol '{name:?}' in current workbench '{workbench:?}'",
246                lookup = crate::mark!(LOOKUP)
247            );
248            self.deny_super(name)?;
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 = crate::mark!(FOUND),
254                    );
255                    Ok(symbol)
256                }
257                Err(err) => {
258                    log::trace!(
259                        "{not_found} symbol '{name:?}': {err}",
260                        not_found = crate::mark!(NOT_FOUND)
261                    );
262                    Err(err)
263                }
264            }
265        } else {
266            log::trace!(
267                "{not_found} No current workbench",
268                not_found = crate::mark!(NOT_FOUND)
269            );
270            Err(ResolveError::SymbolNotFound(name.clone()))
271        }
272    }
273
274    /// Check if current stack frame is code
275    fn is_code(&self) -> bool {
276        !matches!(self.stack.current_frame(), Some(StackFrame::Module(..)))
277    }
278
279    /// Check if current stack frame is a module
280    pub(crate) fn is_module(&self) -> bool {
281        matches!(
282            self.stack.current_frame(),
283            Some(StackFrame::Module(..) | StackFrame::Source(..))
284        )
285    }
286
287    fn lookup_within(&self, name: &QualifiedName, target: LookupTarget) -> ResolveResult<Symbol> {
288        self.root.lookup_within(
289            name,
290            &self.root.search(&self.stack.current_module_name(), false)?,
291            target,
292        )
293    }
294
295    /// Symbol table accessor.
296    pub fn root(&self) -> &Symbol {
297        &self.root
298    }
299}
300
301impl UseLocally for EvalContext {
302    fn use_symbol(&mut self, name: &QualifiedName, id: Option<Identifier>) -> EvalResult<Symbol> {
303        log::debug!("Using symbol {name:?}");
304
305        let symbol = self.lookup(name, LookupTarget::Any)?;
306        if self.is_code() {
307            self.stack.put_local(id, symbol.clone())?;
308            log::trace!("Local Stack:\n{:?}", self.stack);
309        }
310
311        Ok(symbol)
312    }
313
314    fn use_symbols_of(&mut self, name: &QualifiedName) -> EvalResult<Symbol> {
315        log::debug!("Using all symbols in {name:?}");
316
317        let symbol = self.lookup(name, LookupTarget::Any)?;
318        if symbol.is_empty() {
319            Err(EvalError::NoSymbolsToUse(symbol.full_name()))
320        } else {
321            if self.is_code() {
322                symbol.try_children(|(id, symbol)| {
323                    self.stack.put_local(Some(id.clone()), symbol.clone())
324                })?;
325                log::trace!("Local Stack:\n{:?}", self.stack);
326            }
327            Ok(symbol)
328        }
329    }
330}
331
332impl Locals for EvalContext {
333    fn set_local_value(&mut self, id: Identifier, value: Value) -> EvalResult<()> {
334        self.stack.set_local_value(id, value)
335    }
336
337    fn get_local_value(&self, id: &Identifier) -> EvalResult<Value> {
338        self.stack.get_local_value(id)
339    }
340
341    fn open(&mut self, frame: StackFrame) {
342        self.stack.open(frame);
343    }
344
345    fn close(&mut self) -> StackFrame {
346        self.stack.close()
347    }
348
349    fn fetch_symbol(&self, id: &Identifier) -> EvalResult<Symbol> {
350        self.stack.fetch_symbol(id)
351    }
352
353    fn get_model(&self) -> EvalResult<Model> {
354        self.stack.get_model()
355    }
356
357    fn current_name(&self) -> QualifiedName {
358        self.stack.current_name()
359    }
360}
361
362impl Default for EvalContext {
363    fn default() -> Self {
364        Self {
365            root: Default::default(),
366            sources: Default::default(),
367            stack: Default::default(),
368            output: Stdout::new(),
369            exporters: Default::default(),
370            importers: Default::default(),
371            diag: Default::default(),
372        }
373    }
374}
375
376impl Lookup<EvalError> for EvalContext {
377    fn lookup(&self, name: &QualifiedName, target: LookupTarget) -> EvalResult<Symbol> {
378        log::debug!("Lookup {target} '{name:?}' (at line {:?}):", name.src_ref());
379
380        log::trace!("- lookups -------------------------------------------------------");
381        // collect all symbols that can be found and remember origin
382        let results = [
383            ("local", { self.stack.lookup(name, target) }),
384            ("global", {
385                self.lookup_within(name, target).map_err(|err| err.into())
386            }),
387            ("property", { self.lookup_property(name) }),
388            ("workbench", {
389                self.lookup_workbench(name, target)
390                    .map_err(|err| err.into())
391            }),
392        ]
393        .into_iter();
394
395        log::trace!("- lookup results ------------------------------------------------");
396        let results = results.inspect(|(from, result)| log::trace!("{from}: {:?}", result));
397
398        // collect ok-results and ambiguity errors
399        let (found, mut ambiguities, mut errors) = results.fold(
400            (vec![], vec![], vec![]),
401            |(mut oks, mut ambiguities, mut errors), (origin, result)| {
402                match result {
403                    Ok(symbol) => oks.push((origin, symbol)),
404                    Err(EvalError::AmbiguousSymbol( ambiguous, others)) => {
405                        ambiguities.push((origin, EvalError::AmbiguousSymbol ( ambiguous, others )))
406                    }
407                    Err(
408                        // ignore all kinds of "not found" errors
409                        EvalError::SymbolNotFound(_)
410                        // for locals
411                        | EvalError::LocalNotFound(_)
412                        // for model property
413                        | EvalError::NoModelInWorkbench
414                        | EvalError::PropertyNotFound(_)
415                        | EvalError::NoPropertyId(_)
416                        // for symbol table
417                        | EvalError::ResolveError(ResolveError::SymbolNotFound(_))
418                        | EvalError::ResolveError(ResolveError::ExternalPathNotFound(_))
419                        | EvalError::ResolveError(ResolveError::SymbolIsPrivate(_))
420                        | EvalError::ResolveError(ResolveError::NulHash)
421                        | EvalError::ResolveError(ResolveError::WrongTarget),
422                    ) => (),
423                    Err(err) => errors.push((origin, err)),
424                }
425                (oks, ambiguities, errors)
426            },
427        );
428
429        // log any unexpected errors and return early
430        if !errors.is_empty() {
431            log::error!("Unexpected errors while lookup symbol '{name:?}':");
432            errors
433                .iter()
434                .for_each(|(origin, err)| log::error!("Lookup ({origin}) error: {err}"));
435
436            return Err(errors.remove(0).1);
437        }
438
439        // early emit any ambiguity error
440        if !ambiguities.is_empty() {
441            log::debug!(
442                "{ambiguous} Symbol '{name:?}':\n{}",
443                ambiguities
444                    .iter()
445                    .map(|(origin, err)| format!("{origin}: {err}"))
446                    .collect::<Vec<_>>()
447                    .join("\n"),
448                ambiguous = crate::mark!(AMBIGUOUS)
449            );
450            return Err(ambiguities.remove(0).1);
451        }
452
453        // filter by lookup target
454        let found: Vec<_> = found
455            .iter()
456            .filter(|(_, symbol)| target.matches(symbol))
457            .collect();
458
459        // check for ambiguity in what's left
460        match found.first() {
461            Some((origin, symbol)) => {
462                // check if all findings point to the same symbol
463                if found.iter().all(|(_, x)| x == symbol) {
464                    log::debug!(
465                        "{found} symbol '{name:?}' in {origin}",
466                        found = crate::mark!(FOUND!)
467                    );
468                    symbol.set_used();
469                    Ok(symbol.clone())
470                } else {
471                    let others: QualifiedNames =
472                        found.iter().map(|(_, symbol)| symbol.full_name()).collect();
473                    log::debug!(
474                        "{ambiguous} symbol '{name:?}' in {others:?}:\n{self:?}",
475                        ambiguous = crate::mark!(AMBIGUOUS),
476                    );
477                    Err(EvalError::AmbiguousSymbol(name.clone(), others))
478                }
479            }
480            None => {
481                log::debug!(
482                    "{not_found} Symbol '{name:?}'",
483                    not_found = crate::mark!(NOT_FOUND!)
484                );
485                Err(EvalError::SymbolNotFound(name.clone()))
486            }
487        }
488    }
489
490    fn ambiguity_error(ambiguous: QualifiedName, others: QualifiedNames) -> EvalError {
491        EvalError::AmbiguousSymbol(ambiguous, others)
492    }
493}
494
495impl Diag for EvalContext {
496    fn fmt_diagnosis(&self, f: &mut dyn std::fmt::Write) -> std::fmt::Result {
497        self.diag.pretty_print(f, self)
498    }
499
500    fn warning_count(&self) -> u32 {
501        self.diag.warning_count()
502    }
503
504    fn error_count(&self) -> u32 {
505        self.diag.error_count()
506    }
507
508    fn error_lines(&self) -> std::collections::HashSet<usize> {
509        self.diag.error_lines()
510    }
511
512    fn warning_lines(&self) -> std::collections::HashSet<usize> {
513        self.diag.warning_lines()
514    }
515}
516
517impl PushDiag for EvalContext {
518    fn push_diag(&mut self, diag: Diagnostic) -> DiagResult<()> {
519        let result = self.diag.push_diag(diag);
520        log::trace!("Error Context:\n{self:?}");
521        #[cfg(debug_assertions)]
522        if std::env::var("MICROCAD_ERROR_PANIC").is_ok() {
523            eprintln!("{}", self.diagnosis());
524            panic!("MICROCAD_ERROR_PANIC")
525        }
526        result
527    }
528}
529
530impl GetSourceByHash for EvalContext {
531    fn get_by_hash(&self, hash: u64) -> ResolveResult<Rc<SourceFile>> {
532        self.sources.get_by_hash(hash)
533    }
534}
535
536impl std::fmt::Debug for EvalContext {
537    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538        if let Ok(model) = self.get_model() {
539            write!(f, "\nModel:\n")?;
540            model.tree_print(f, TreeState::new_debug(4))?;
541        }
542        writeln!(f, "\nCurrent: {:?}", self.stack.current_name())?;
543        writeln!(f, "\nModule: {:?}", self.stack.current_module_name())?;
544        write!(f, "\nLocals Stack:\n{:?}", self.stack)?;
545        writeln!(f, "\nCall Stack:")?;
546        self.stack.pretty_print_call_trace(f, &self.sources)?;
547
548        writeln!(f, "\nSources:\n")?;
549        write!(f, "{:?}", &self.sources)?;
550
551        write!(f, "\nSymbol Table:\n")?;
552        self.root.tree_print(f, TreeState::new_debug(0))?;
553
554        match self.error_count() {
555            0 => write!(f, "No errors")?,
556            1 => write!(f, "1 error")?,
557            _ => write!(f, "{} errors", self.error_count())?,
558        };
559        match self.warning_count() {
560            0 => writeln!(
561                f,
562                ", no warnings{}",
563                if self.error_count() > 0 { ":" } else { "." }
564            )?,
565            1 => writeln!(f, ", 1 warning:")?,
566            _ => writeln!(f, ", {} warnings:", self.warning_count())?,
567        };
568        self.fmt_diagnosis(f)?;
569        Ok(())
570    }
571}
572
573impl ImporterRegistryAccess for EvalContext {
574    type Error = EvalError;
575
576    fn import(
577        &mut self,
578        arg_map: &Tuple,
579        search_paths: &[std::path::PathBuf],
580    ) -> Result<Value, Self::Error> {
581        match self.importers.import(arg_map, search_paths) {
582            Ok(value) => Ok(value),
583            Err(err) => {
584                self.error(arg_map, err)?;
585                Ok(Value::None)
586            }
587        }
588    }
589}
590
591impl ExporterAccess for EvalContext {
592    fn exporter_by_id(&self, id: &crate::Id) -> Result<Rc<dyn Exporter>, ExportError> {
593        self.exporters.exporter_by_id(id)
594    }
595
596    fn exporter_by_filename(
597        &self,
598        filename: &std::path::Path,
599    ) -> Result<Rc<dyn Exporter>, ExportError> {
600        self.exporters.exporter_by_filename(filename)
601    }
602}