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}