Skip to main content

nickel_lang_core/eval/
callstack.rs

1//! In a lazy language like Nickel, there are no well delimited stack frames due to how function
2//! application is evaluated. Additional information about the history of function calls is thus
3//! stored in a call stack solely for better error reporting.
4use crate::{
5    files::Files,
6    identifier::LocIdent,
7    position::{PosIdx, PosTable, RawSpan, TermPos},
8};
9
10/// A call stack, saving the history of function calls.
11///
12/// # Size
13///
14/// The callstack length is assumed to be smaller at most `u32::MAX`, which allows to store indices
15/// in a more compact way on the eval stack. Any method leading to a push that reaches this size
16/// will currently panic.
17#[derive(PartialEq, Eq, Clone, Default, Debug)]
18pub struct CallStack(pub Vec<StackElem>);
19
20/// Basic description of a function call. Used for error reporting.
21pub struct CallDescr {
22    /// The name of the called function, if any.
23    pub head: Option<LocIdent>,
24    /// The position of the application.
25    pub span: RawSpan,
26}
27
28/// A call stack element.
29#[derive(Debug, Clone, Eq, PartialEq)]
30pub enum StackElem {
31    /// A function body was entered. The position is the position of the original application.
32    Fun(PosIdx),
33    /// An application was evaluated.
34    App(PosIdx),
35    /// A variable was entered.
36    Var { id: LocIdent, pos: PosIdx },
37    /// A record field was entered.
38    Field {
39        id: LocIdent,
40        pos_record: PosIdx,
41        pos_field: PosIdx,
42        pos_access: PosIdx,
43    },
44}
45
46impl CallStack {
47    pub fn new() -> Self {
48        CallStack(Vec::new())
49    }
50
51    /// Pushes an element to the stack, or panic if the size of the call stack is [u32::MAX].
52    fn push(&mut self, elem: StackElem) {
53        if self.0.len() == u32::MAX as usize {
54            panic!("max call stack size reached");
55        }
56
57        self.0.push(elem);
58    }
59
60    /// Push a marker to indicate that a var was entered.
61    pub fn enter_var(&mut self, id: LocIdent, pos: PosIdx) {
62        self.push(StackElem::Var { id, pos });
63    }
64
65    /// Push a marker to indicate that an application was entered.
66    pub fn enter_app(&mut self, pos_table: &PosTable, pos: PosIdx) {
67        // We ignore application without positions, which have been generated by the interpreter.
68        if pos_table.get(pos).is_def() {
69            self.push(StackElem::App(pos));
70        }
71    }
72
73    /// Push a marker to indicate that during the evaluation an application, the function part was
74    /// finally evaluated to an expression of the form `fun x => body`, and that the body of this
75    /// function was entered.
76    pub fn enter_fun(&mut self, pos_table: &PosTable, pos: PosIdx) {
77        // We ignore application without positions, which have been generated by the interpreter.
78        if pos_table.get(pos).is_def() {
79            self.push(StackElem::Fun(pos));
80        }
81    }
82
83    /// Push a marker to indicate that a record field was entered.
84    pub fn enter_field(
85        &mut self,
86        id: LocIdent,
87        pos_record: PosIdx,
88        pos_field: PosIdx,
89        pos_access: PosIdx,
90    ) {
91        self.push(StackElem::Field {
92            id,
93            pos_record,
94            pos_field,
95            pos_access,
96        });
97    }
98
99    /// Process a raw callstack by aggregating elements belonging to the same call. Return a list of
100    /// call descriptions from the most nested/recent to the least nested/recent, together with the
101    /// last pending call, if any.
102    ///
103    /// Recall that when a call `f arg` is evaluated, the following events happen:
104    /// 1. `arg` is pushed on the evaluation stack.
105    /// 2. `f` is evaluated.
106    /// 3. Hopefully, the result of this evaluation is a function `Func(id, body)`. `arg` is popped
107    ///    from the stack, bound to `id` in the environment, and `body is entered`.
108    ///
109    /// For error reporting purpose, we want to be able to determine the chain of nested calls
110    /// leading to the current code path at any moment. To do so, the Nickel abstract machine
111    /// maintains a callstack via this basic mechanism:
112    /// 1. When an application is evaluated, push a marker with the position of the application on
113    ///    the callstack.
114    /// 2. When a function body is entered, push a marker with the position of the original
115    ///    application on the callstack.
116    /// 3. When a variable is evaluated, push a marker with its name and position on the callstack.
117    /// 4. When a record field is accessed, push a marker with its name and position on the
118    ///    callstack too.
119    ///
120    /// Both field and variable are useful to determine the name of a called function, when there is
121    /// one.  The resulting stack is not suited to be reported to the user for the following
122    /// reasons:
123    ///
124    /// 1. One call spans several items on the callstack. First the application is entered (pushing
125    ///    an `App`), then possibly variables or other application are evaluated until we eventually
126    ///    reach a function for the left hand side. Then body of this function is entered (pushing a
127    ///    `Fun`).
128    /// 2. Because of currying, multi-ary applications span several objects on the callstack.
129    ///    Typically, `(fun x y => x + y) arg1 arg2` spans two `App` and two `Fun` elements in the
130    ///    form `App1 App2 Fun2 Fun1`, where the position span of `App1` includes the position span
131    ///    of `App2`.  We want to group them as one call.
132    /// 3. The callstack includes calls to builtin contracts. These calls are inserted implicitly by
133    ///    the abstract machine and are not written explicitly by the user. Showing them is
134    ///    confusing and clutters the call chain, so we get rid of them too.
135    ///
136    /// This is the role of `group_by_calls`, which filter out unwanted elements and groups
137    /// callstack elements into atomic call elements represented by [`CallDescr`].
138    ///
139    /// The final call description list is reversed such that the most nested calls, which are
140    /// usually the most relevant to understand the error, are printed first.
141    ///
142    /// # Arguments
143    ///
144    /// - `stdlib_ids`: the `FileId`s of the sources containing standard contracts, to filter their
145    ///   calls out.
146    pub fn group_by_calls(
147        self: &CallStack,
148        pos_table: &PosTable,
149        files: &Files,
150    ) -> (Vec<CallDescr>, Option<CallDescr>) {
151        // We filter out calls and accesses made from within the builtin contracts, as well as
152        // generated variables introduced by program transformations.
153        let it = self.0.iter().filter(|elem| {
154            // We avoid applications (Fun/App) with inherited positions. Such calls include
155            // contracts applications which add confusing call items whose positions don't point to
156            // an actual call in the source.
157            let src_id = match elem {
158                StackElem::Var { id, .. } if id.is_generated() => None,
159                StackElem::Var { pos, .. }
160                | StackElem::Field {
161                    pos_access: pos, ..
162                } => pos_table.get(*pos).into_opt().map(|span| span.src_id),
163                StackElem::Fun(pos) | StackElem::App(pos) => {
164                    if let TermPos::Original(RawSpan { src_id, .. }) = pos_table.get(*pos) {
165                        Some(src_id)
166                    } else {
167                        None
168                    }
169                }
170            };
171
172            src_id.is_some_and(|src_id| !files.is_stdlib(src_id))
173        });
174
175        // We maintain a stack of active calls (whose head is being evaluated).  When encountering
176        // an identifier (variable or record field), we see if it could serve as a function name
177        // for the current active call. When a `Fun` is encountered, we check if this correspond to
178        // the current active call, and if it does, the call description is moved to a stack of
179        // processed calls.
180        //
181        // We also merge subcalls, in the sense that subcalls of larger calls are not considered
182        // separately. `app1` is a subcall of `app2` if the position of `app1` is included in the
183        // one of `app2` and the starting index is equal. We want `f a b c` to be reported as only
184        // one big call to `f` rather than three nested calls `f a`, `f a b`, and `f a b c`.
185        let mut pending: Vec<CallDescr> = Vec::new();
186        let mut entered: Vec<CallDescr> = Vec::new();
187
188        for elt in it {
189            match elt {
190                StackElem::Var { id, pos, .. }
191                | StackElem::Field {
192                    id,
193                    pos_access: pos,
194                    ..
195                } => {
196                    let span = pos_table.get(*pos).unwrap();
197
198                    match pending.last_mut() {
199                        Some(CallDescr {
200                            head: head @ None,
201                            span: span_call,
202                        }) if span <= *span_call => *head = Some(*id),
203                        _ => (),
204                    };
205                }
206                StackElem::App(pos) => {
207                    let span = pos_table.get(*pos).unwrap();
208
209                    match pending.last() {
210                        Some(CallDescr {
211                            span: span_call, ..
212                        }) if span <= *span_call && span.start == span_call.start => (),
213                        _ => pending.push(CallDescr { head: None, span }),
214                    }
215                }
216                StackElem::Fun(pos) => {
217                    let span = pos_table.get(*pos).unwrap();
218
219                    if pending
220                        .last()
221                        .map(|cdescr| cdescr.span == span)
222                        .unwrap_or(false)
223                    {
224                        entered.push(pending.pop().unwrap());
225                    }
226                    // Otherwise, we are most probably entering a subcall () of the currently
227                    // active call (e.g. in an multi-ary application `f g h`, a subcall would be `f
228                    // g`). In any case, we do nothing.
229                }
230            }
231        }
232
233        entered.reverse();
234        (entered, pending.pop())
235    }
236
237    /// Return the length of the callstack. Wrapper for `callstack.0.len()`.
238    pub fn len(&self) -> u32 {
239        // safe conversion: we maintain the invariant that the length of the stack is at most
240        // u32::MAX
241        self.0.len() as u32
242    }
243
244    /// Return whether the callstack is empty.
245    pub fn is_empty(&self) -> bool {
246        self.0.is_empty()
247    }
248
249    /// Truncate the callstack at a certain size. Used e.g. to quickly drop the elements introduced
250    /// during the strict evaluation of the operand of a primitive operator. Wrapper for
251    /// `callstack.0.truncate(len)`.
252    pub fn truncate(&mut self, len: usize) {
253        self.0.truncate(len)
254    }
255}
256
257impl From<CallStack> for Vec<StackElem> {
258    fn from(cs: CallStack) -> Self {
259        cs.0
260    }
261}