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