microcad_lang/eval/symbols/
stack.rs

1// Copyright © 2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use crate::{eval::*, model::*, resolve::*};
5
6/// A stack with a list of stack frames.
7///
8/// [`StackFrame`]s can have the following different types:
9/// - source file (bottom of stack)
10/// - modules ( e.g. `mod my_lib { ... }`)
11/// - init calls (e.g. `std::geo2d::Circle(radius = 1m)`)
12/// - function calls (e.g. `std::print("µcad")`)
13/// - bodies (e.g. `{ ... }`)
14#[derive(Default)]
15pub struct Stack(Vec<StackFrame>);
16
17impl Stack {
18    /// Put (or overwrite any existing) *symbol* into the current stack frame.
19    /// - `id`: *identifier* of the symbol to add/set. The *symbol's* internal *identifier* is used when `None`.
20    pub fn put_local(&mut self, id: Option<Identifier>, symbol: Symbol) -> EvalResult<()> {
21        let id = if let Some(id) = id { id } else { symbol.id() };
22        let name = symbol.full_name();
23        for (pos, frame) in self.0.iter_mut().rev().enumerate() {
24            match frame {
25                StackFrame::Source(_, locals)
26                | StackFrame::Workbench(_, _, locals)
27                | StackFrame::Init(locals)
28                | StackFrame::Body(locals)
29                | StackFrame::Module(_, locals)
30                | StackFrame::Function(locals) => {
31                    let op = if locals.insert(id.clone(), symbol).is_some() {
32                        "Added"
33                    } else {
34                        "Set"
35                    };
36                    if name.is_qualified() {
37                        log::debug!("{op} {name} as {id:?} to local stack");
38                    } else {
39                        log::debug!("{op} {id:?} to local stack");
40                    }
41
42                    log::trace!("Local Stack:\n{self}");
43                    return Ok(());
44                }
45                StackFrame::Call {
46                    symbol: _,
47                    args: _,
48                    src_ref: _,
49                } => {
50                    // RULE: top call frame is transparent on stack
51                    if pos > 0 {
52                        return Err(EvalError::WrongStackFrame(id, "call"));
53                    }
54                }
55            }
56        }
57        Err(EvalError::LocalStackEmpty(id))
58    }
59
60    fn current_workbench_id(&self) -> Option<&Identifier> {
61        self.0.iter().rev().find_map(|frame| {
62            if let StackFrame::Workbench(_, id, _) = frame {
63                Some(id)
64            } else {
65                None
66            }
67        })
68    }
69
70    /// Get name of current module.
71    pub fn current_module_name(&self) -> QualifiedName {
72        if self.0.is_empty() {
73            QualifiedName::default()
74        } else {
75            let mut module_name = QualifiedName::default();
76            for (n, frame) in self.0.iter().rev().enumerate() {
77                match frame {
78                    StackFrame::Source(id, ..) | StackFrame::Module(id, ..) => {
79                        module_name.insert(0, id.clone());
80                    }
81                    StackFrame::Call { symbol, .. } => {
82                        if n > 0 {
83                            // log::trace!("CALL: {}, {}", symbol.full_name(), module_name);
84                            module_name =
85                                symbol.full_name().remove_last().with_prefix(&module_name);
86                            break;
87                        }
88                    }
89                    _ => (),
90                }
91            }
92
93            // log::trace!("current_module_name: {module_name:?}");
94            module_name
95        }
96    }
97
98    /// Get name of current workbench.
99    pub fn current_workbench_name(&self) -> Option<QualifiedName> {
100        if let Some(id) = self.current_workbench_id() {
101            let name = QualifiedName::new(vec![id.clone()], id.src_ref());
102            Some(name.with_prefix(&self.current_module_name()))
103        } else {
104            None
105        }
106    }
107
108    /// Return the current *stack frame* if there is any.
109    pub fn current_frame(&self) -> Option<&StackFrame> {
110        self.0.last()
111    }
112
113    /// Pretty print call trace.
114    pub fn pretty_print_call_trace(
115        &self,
116        f: &mut dyn std::fmt::Write,
117        source_by_hash: &impl super::GetSourceByHash,
118    ) -> std::fmt::Result {
119        let mut none: bool = true;
120        for (idx, frame) in self
121            .0
122            .iter()
123            .filter(|frame| {
124                matches!(
125                    frame,
126                    StackFrame::Call {
127                        symbol: _,
128                        args: _,
129                        src_ref: _
130                    }
131                )
132            })
133            .enumerate()
134        {
135            none = false;
136            frame.print_stack(f, source_by_hash, idx)?;
137        }
138        if none {
139            writeln!(f, crate::invalid!(STACK))?
140        }
141        Ok(())
142    }
143
144    pub(crate) fn current_symbol(&self) -> Option<Symbol> {
145        self.0.iter().rev().find_map(|frame| frame.symbol())
146    }
147}
148
149impl Locals for Stack {
150    fn open(&mut self, frame: StackFrame) {
151        if let Some(id) = frame.id() {
152            log::trace!("Opening {} stack frame '{id}'", frame.kind_str());
153        } else {
154            log::trace!("Opening {} stack frame", frame.kind_str());
155        }
156        self.0.push(frame);
157    }
158
159    fn close(&mut self) {
160        if let Some(frame) = self.0.pop() {
161            log::trace!("Closing {} stack frame", frame.kind_str());
162        }
163    }
164
165    fn set_local_value(&mut self, id: Identifier, value: Value) -> EvalResult<()> {
166        self.put_local(
167            Some(id.clone()),
168            Symbol::new(
169                SymbolDefinition::Constant(Visibility::Private, id, value),
170                None,
171            ),
172        )
173    }
174
175    fn get_local_value(&self, id: &Identifier) -> EvalResult<Value> {
176        match self.fetch(id) {
177            Ok(symbol) => match &symbol.borrow().def {
178                SymbolDefinition::Constant(.., value) | SymbolDefinition::Argument(.., value) => {
179                    Ok(value.clone())
180                }
181                _ => Err(EvalError::LocalNotFound(id.clone())),
182            },
183            Err(_) => Err(EvalError::LocalNotFound(id.clone())),
184        }
185    }
186
187    fn get_model(&self) -> EvalResult<Model> {
188        match self
189            .0
190            .iter()
191            .rev()
192            .find(|frame| matches!(frame, StackFrame::Workbench(_, _, _)))
193        {
194            Some(StackFrame::Workbench(model, _, _)) => Ok(model.clone()),
195            _ => Err(EvalError::NoModelInWorkbench),
196        }
197    }
198
199    fn fetch(&self, id: &Identifier) -> EvalResult<Symbol> {
200        // search from inner scope to root scope to shadow outside locals
201        for (n, frame) in self.0.iter().rev().enumerate() {
202            match frame {
203                StackFrame::Source(_, locals)
204                | StackFrame::Body(locals)
205                | StackFrame::Workbench(_, _, locals)
206                | StackFrame::Init(locals)
207                | StackFrame::Function(locals) => {
208                    if let Some(local) = locals.get(id) {
209                        log::trace!("fetched {id:?} from locals");
210                        return Ok(local.clone());
211                    }
212                }
213                // stop stack lookup at calls
214                StackFrame::Module(_, _) => {
215                    log::trace!("stop at call frame");
216                    break;
217                }
218                // skip any of these
219                StackFrame::Call {
220                    symbol: _,
221                    args: _,
222                    src_ref: _,
223                } => {
224                    if n > 0 {
225                        break;
226                    }
227                }
228            }
229        }
230        Err(EvalError::LocalNotFound(id.clone()))
231    }
232
233    /// Get name of current workbench or module (might be empty).
234    fn current_name(&self) -> QualifiedName {
235        if let Some(id) = self.current_workbench_id() {
236            let name = QualifiedName::new(vec![id.clone()], id.src_ref());
237            name.with_prefix(&self.current_module_name())
238        } else {
239            self.current_module_name()
240        }
241    }
242}
243
244impl std::fmt::Display for Stack {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        if self.0.is_empty() {
247            writeln!(f, crate::invalid!(STACK))
248        } else {
249            for (n, locals) in self.0.iter().enumerate() {
250                locals.print_locals(f, n, 0)?;
251            }
252            Ok(())
253        }
254    }
255}
256
257#[test]
258#[allow(clippy::unwrap_used)]
259fn local_stack() {
260    let mut stack = Stack::default();
261
262    let make_int = |id, value| {
263        Symbol::new(
264            SymbolDefinition::Constant(Visibility::Private, id, Value::Integer(value)),
265            None,
266        )
267    };
268
269    let fetch_int = |stack: &Stack, id: &str| -> Option<i64> {
270        match stack.fetch(&id.into()) {
271            Ok(node) => match &node.borrow().def {
272                SymbolDefinition::Constant(.., Value::Integer(value)) => Some(*value),
273                _ => todo!("error"),
274            },
275            _ => None,
276        }
277    };
278
279    let root_name = "test".into();
280    let root_id = QualifiedName::from_id(root_name);
281    stack.open(StackFrame::Source("test".into(), SymbolMap::default()));
282    assert!(stack.current_module_name() == root_id);
283
284    assert!(stack.put_local(None, make_int("a".into(), 1)).is_ok());
285
286    println!("{stack}");
287
288    assert!(fetch_int(&stack, "a").unwrap() == 1);
289    assert!(fetch_int(&stack, "b").is_none());
290    assert!(fetch_int(&stack, "c").is_none());
291
292    stack.open(StackFrame::Body(SymbolMap::default()));
293    assert!(stack.current_module_name() == root_id);
294
295    assert!(fetch_int(&stack, "a").unwrap() == 1);
296    assert!(fetch_int(&stack, "b").is_none());
297    assert!(fetch_int(&stack, "c").is_none());
298
299    assert!(stack.put_local(None, make_int("b".into(), 2)).is_ok());
300
301    assert!(fetch_int(&stack, "a").unwrap() == 1);
302    assert!(fetch_int(&stack, "b").unwrap() == 2);
303    assert!(fetch_int(&stack, "c").is_none());
304
305    // test alias
306    assert!(stack
307        .put_local(Some("x".into()), make_int("x".into(), 3))
308        .is_ok());
309
310    assert!(fetch_int(&stack, "a").unwrap() == 1);
311    assert!(fetch_int(&stack, "b").unwrap() == 2);
312    assert!(fetch_int(&stack, "x").unwrap() == 3);
313
314    stack.close();
315    assert!(stack.current_module_name() == root_id);
316
317    assert!(fetch_int(&stack, "a").unwrap() == 1);
318    assert!(fetch_int(&stack, "b").is_none());
319    assert!(fetch_int(&stack, "c").is_none());
320
321    stack.close();
322    assert!(stack.current_module_name().is_empty());
323}