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        self.symbol_table
93            .unused_private()
94            .iter()
95            .try_for_each(|symbol| {
96                self.warning(
97                    &symbol.src_ref(),
98                    EvalError::UnusedGlobalSymbol(match self.sources.get_code(&symbol) {
99                        Ok(id) => id,
100                        Err(_) => symbol.id().to_string(),
101                    }),
102                )
103            })?;
104
105        if model.has_no_output() {
106            // TODO Check if we can simply return Some(model) even if there is no output.
107            Ok(None)
108        } else {
109            Ok(Some(model))
110        }
111    }
112
113    /// Run the closure `f` within the given `stack_frame`.
114    pub(super) fn scope<T>(
115        &mut self,
116        stack_frame: StackFrame,
117        f: impl FnOnce(&mut EvalContext) -> T,
118    ) -> T {
119        self.open(stack_frame);
120        let result = f(self);
121        let mut unused: Vec<_> = if let Some(frame) = &self.stack.current_frame() {
122            if let Some(locals) = frame.locals() {
123                locals
124                    .iter()
125                    .filter(|(_, symbol)| !symbol.is_used())
126                    .filter(|(id, _)| !id.ignore())
127                    .filter(|(_, symbol)| !symbol.src_ref().is_none())
128                    .map(|(id, _)| id.clone())
129                    .collect()
130            } else {
131                vec![]
132            }
133        } else {
134            vec![]
135        };
136        unused.sort();
137
138        unused
139            .iter()
140            .try_for_each(|id| self.warning(id, EvalError::UnusedLocal(id.clone())))
141            .expect("diag error");
142
143        self.close();
144        result
145    }
146
147    /// All registered exporters.
148    pub fn exporters(&self) -> &ExporterRegistry {
149        &self.exporters
150    }
151
152    /// Return search paths of this context.
153    pub fn search_paths(&self) -> &Vec<std::path::PathBuf> {
154        self.sources.search_paths()
155    }
156
157    /// Get property from current model.
158    pub(super) fn get_property(&self, id: &Identifier) -> EvalResult<Value> {
159        match self.get_model() {
160            Ok(model) => {
161                if let Some(value) = model.get_property(id) {
162                    Ok(value.clone())
163                } else {
164                    Err(EvalError::PropertyNotFound(id.clone()))
165                }
166            }
167            Err(err) => Err(err),
168        }
169    }
170
171    /// Initialize a property.
172    ///
173    /// Returns error if there is no model or the property has been initialized before.
174    pub(super) fn init_property(&self, id: Identifier, value: Value) -> EvalResult<()> {
175        match self.get_model() {
176            Ok(model) => {
177                if let Some(previous_value) = model.borrow_mut().set_property(id.clone(), value) {
178                    if !previous_value.is_invalid() {
179                        return Err(EvalError::ValueAlreadyDefined(
180                            id.clone(),
181                            previous_value.to_string(),
182                            id.src_ref(),
183                        ));
184                    }
185                }
186                Ok(())
187            }
188            Err(err) => Err(err),
189        }
190    }
191
192    /// Return if the current frame is an init frame.
193    pub(super) fn is_init(&mut self) -> bool {
194        matches!(self.stack.current_frame(), Some(StackFrame::Init(_)))
195    }
196
197    pub(super) fn is_within_function(&self) -> bool {
198        self.stack.is_within_function()
199    }
200
201    /// Lookup a property by qualified name.
202    fn lookup_property(&self, name: &QualifiedName) -> EvalResult<Symbol> {
203        log::trace!(
204            "{lookup} for property {name:?}",
205            lookup = crate::mark!(LOOKUP)
206        );
207        self.symbol_table.deny_super(name)?;
208
209        if self.stack.current_workbench_name().is_some() {
210            if let Some(id) = name.single_identifier() {
211                match self.get_property(id) {
212                    Ok(value) => {
213                        log::trace!(
214                            "{found} property '{name:?}'",
215                            found = crate::mark!(FOUND_INTERIM)
216                        );
217                        return Ok(Symbol::new(
218                            SymbolDef::Constant(Visibility::Public, id.clone(), value),
219                            None,
220                        ));
221                    }
222                    Err(err) => return Err(err),
223                }
224            }
225        }
226        log::trace!(
227            "{not_found} Property '{name:?}'",
228            not_found = crate::mark!(NOT_FOUND_INTERIM)
229        );
230        Err(EvalError::NoPropertyId(name.clone()))
231    }
232
233    fn lookup_workbench(
234        &self,
235        name: &QualifiedName,
236        target: LookupTarget,
237    ) -> ResolveResult<Symbol> {
238        if let Some(workbench) = &self.stack.current_workbench_name() {
239            log::trace!(
240                "{lookup} for symbol '{name:?}' in current workbench '{workbench:?}'",
241                lookup = crate::mark!(LOOKUP)
242            );
243            self.deny_super(name)?;
244            match self
245                .symbol_table
246                .lookup_within_name(name, workbench, target)
247            {
248                Ok(symbol) => {
249                    log::trace!(
250                        "{found} symbol in current module: {symbol:?}",
251                        found = crate::mark!(FOUND_INTERIM),
252                    );
253                    Ok(symbol)
254                }
255                Err(err) => {
256                    log::trace!(
257                        "{not_found} symbol '{name:?}': {err}",
258                        not_found = crate::mark!(NOT_FOUND_INTERIM)
259                    );
260                    Err(err)
261                }
262            }
263        } else {
264            log::trace!(
265                "{not_found} No current workbench",
266                not_found = crate::mark!(NOT_FOUND_INTERIM)
267            );
268            Err(ResolveError::SymbolNotFound(name.clone()))
269        }
270    }
271
272    /// Check if current stack frame is code
273    fn is_code(&self) -> bool {
274        !matches!(self.stack.current_frame(), Some(StackFrame::Module(..)))
275    }
276
277    /// Check if current stack frame is a module
278    pub(crate) fn is_module(&self) -> bool {
279        matches!(
280            self.stack.current_frame(),
281            Some(StackFrame::Module(..) | StackFrame::Source(..))
282        )
283    }
284
285    fn lookup_within(&self, name: &QualifiedName, target: LookupTarget) -> ResolveResult<Symbol> {
286        self.symbol_table.lookup_within(
287            name,
288            &self
289                .symbol_table
290                .search(&self.stack.current_module_name(), false)?,
291            target,
292        )
293    }
294
295    /// Symbol table accessor.
296    pub fn symbol_table(&self) -> &SymbolTable {
297        &self.symbol_table
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            symbol_table: 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{:?}", self.symbol_table)?;
552        match self.error_count() {
553            0 => write!(f, "No errors")?,
554            1 => write!(f, "1 error")?,
555            _ => write!(f, "{} errors", self.error_count())?,
556        };
557        match self.warning_count() {
558            0 => writeln!(
559                f,
560                ", no warnings{}",
561                if self.error_count() > 0 { ":" } else { "." }
562            )?,
563            1 => writeln!(f, ", 1 warning:")?,
564            _ => writeln!(f, ", {} warnings:", self.warning_count())?,
565        };
566        self.fmt_diagnosis(f)?;
567        Ok(())
568    }
569}
570
571impl ImporterRegistryAccess for EvalContext {
572    type Error = EvalError;
573
574    fn import(
575        &mut self,
576        arg_map: &Tuple,
577        search_paths: &[std::path::PathBuf],
578    ) -> Result<Value, Self::Error> {
579        match self.importers.import(arg_map, search_paths) {
580            Ok(value) => Ok(value),
581            Err(err) => {
582                self.error(arg_map, err)?;
583                Ok(Value::None)
584            }
585        }
586    }
587}
588
589impl ExporterAccess for EvalContext {
590    fn exporter_by_id(&self, id: &crate::Id) -> Result<Rc<dyn Exporter>, ExportError> {
591        self.exporters.exporter_by_id(id)
592    }
593
594    fn exporter_by_filename(
595        &self,
596        filename: &std::path::Path,
597    ) -> Result<Rc<dyn Exporter>, ExportError> {
598        self.exporters.exporter_by_filename(filename)
599    }
600}