Skip to main content

microcad_lang/eval/
eval_context.rs

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