Repository: tweag/nickel
Pull request #551 (2022-01-06 17:40:25)

[Refactor] Break eval into submodules
Same as #547 but for `eval`. This PR:

- Create a folder `eval` for the eval module, instead of a one file module (that weighted around 1,7kLoC)
- Move the top-level modules `operation`, `merge` and `stack` as submodules of `eval`. Indeed, those only have to do with evaluation, and are only used within `eval`.
- Break eval further into smaller modules: `tests`, `callstack`, and `lazy`, the later containing stuff about thunks and thunks update.
- Move `should_update` as a method of `Thunk`, instead of a standalone one. It is used only on thunks.

Resulting `eval/mod.rs` is now around 800 LoC and new submodules around 200-250 LoC each. 


Patch:
diff --git a/src/error.rs b/src/error.rs
index f5cf8410..0fe51de4 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -9,7 +9,7 @@ use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle};
 use lalrpop_util::ErrorRecovery;
 
 use crate::{
-    eval::CallStack,
+    eval::callstack::CallStack,
     identifier::Ident,
     label,
     label::ty_path,
diff --git a/src/eval/callstack.rs b/src/eval/callstack.rs
new file mode 100644
index 00000000..3d2de11c
--- /dev/null
+++ b/src/eval/callstack.rs
@@ -0,0 +1,234 @@
+//! In a lazy language like Nickel, there are no well delimited stack frames due to how function
+//! application is evaluated. Additional information about the history of function calls is thus
+//! stored in a call stack solely for better error reporting.
+use super::IdentKind;
+use crate::{
+    identifier::Ident,
+    position::{RawSpan, TermPos},
+};
+use codespan::FileId;
+
+/// A call stack, saving the history of function calls.
+#[derive(PartialEq, Clone, Default, Debug)]
+pub struct CallStack(pub Vec<StackElem>);
+
+/// Basic description of a function call. Used for error reporting.
+pub struct CallDescr {
+    /// The name of the called function, if any.
+    pub head: Option<Ident>,
+    /// The position of the application.
+    pub span: RawSpan,
+}
+
+/// A call stack element.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum StackElem {
+    /// A function body was entered. The position is the position of the original application.
+    Fun(TermPos),
+    /// An application was evaluated.
+    App(TermPos),
+    /// A variable was entered.
+    Var {
+        kind: IdentKind,
+        id: Ident,
+        pos: TermPos,
+    },
+    /// A record field was entered.
+    Field {
+        id: Ident,
+        pos_record: TermPos,
+        pos_field: TermPos,
+        pos_access: TermPos,
+    },
+}
+
+impl CallStack {
+    pub fn new() -> Self {
+        CallStack(Vec::new())
+    }
+
+    /// Push a marker to indicate that a var was entered.
+    pub fn enter_var(&mut self, kind: IdentKind, id: Ident, pos: TermPos) {
+        self.0.push(StackElem::Var { kind, id, pos });
+    }
+
+    /// Push a marker to indicate that an application was entered.
+    pub fn enter_app(&mut self, pos: TermPos) {
+        // We ignore application without positions, which have been generated by the interpreter.
+        if pos.is_def() {
+            self.0.push(StackElem::App(pos));
+        }
+    }
+
+    /// Push a marker to indicate that during the evaluation an application, the function part was
+    /// finally evaluated to an expression of the form `fun x => body`, and that the body of this
+    /// function was entered.
+    pub fn enter_fun(&mut self, pos: TermPos) {
+        // We ignore application without positions, which have been generated by the interpreter.
+        if pos.is_def() {
+            self.0.push(StackElem::Fun(pos));
+        }
+    }
+
+    /// Push a marker to indicate that a record field was entered.
+    pub fn enter_field(
+        &mut self,
+        id: Ident,
+        pos_record: TermPos,
+        pos_field: TermPos,
+        pos_access: TermPos,
+    ) {
+        self.0.push(StackElem::Field {
+            id,
+            pos_record,
+            pos_field,
+            pos_access,
+        });
+    }
+
+    /// Process a raw callstack by aggregating elements belonging to the same call. Return a list
+    /// of call descriptions from the most nested/recent to the least nested/recent, together with
+    /// the last pending call, if any.
+    ///
+    /// Recall that when a call `f arg` is evaluated, the following events happen:
+    /// 1. `arg` is pushed on the evaluation stack.
+    /// 2. `f` is evaluated.
+    /// 3. Hopefully, the result of this evaluation is a function `Func(id, body)`. `arg` is popped
+    ///    from the stack, bound to `id` in the environment, and `body is entered`.
+    ///
+    /// For error reporting purpose, we want to be able to determine the chain of nested calls leading
+    /// to the current code path at any moment. To do so, the Nickel abstract machine maintains a
+    /// callstack via this basic mechanism:
+    /// 1. When an application is evaluated, push a marker with the position of the application on the callstack.
+    /// 2. When a function body is entered, push a marker with the position of the original application on the
+    ///    callstack.
+    /// 3. When a variable is evaluated, push a marker with its name and position on the callstack.
+    /// 4. When a record field is accessed, push a marker with its name and position on the
+    ///    callstack too.
+    ///
+    /// Both field and variable are useful to determine the name of a called function, when there
+    /// is one.  The resulting stack is not suited to be reported to the user for the following
+    /// reasons:
+    ///
+    /// 1. One call spans several items on the callstack. First the application is entered (pushing
+    ///    an `App`), then possibly variables or other application are evaluated until we
+    ///    eventually reach a function for the left hand side. Then body of this function is
+    ///    entered (pushing a `Fun`).
+    /// 2. Because of currying, multi-ary applications span several objects on the callstack.
+    ///    Typically, `(fun x y => x + y) arg1 arg2` spans two `App` and two `Fun` elements in the
+    ///    form `App1 App2 Fun2 Fun1`, where the position span of `App1` includes the position span
+    ///    of `App2`.  We want to group them as one call.
+    /// 3. The callstack includes calls to builtin contracts. These calls are inserted implicitly
+    ///    by the abstract machine and are not written explicitly by the user. Showing them is
+    ///    confusing and clutters the call chain, so we get rid of them too.
+    ///
+    /// This is the role of `group_by_calls`, which filter out unwanted elements and groups
+    /// callstack elements into atomic call elements represented by [`CallDescr`].
+    ///
+    /// The final call description list is reversed such that the most nested calls, which are
+    /// usually the most relevant to understand the error, are printed first.
+    ///
+    /// # Arguments
+    ///
+    /// - `contract_id`: the `FileId` of the source containing standard contracts, to filter their
+    ///   calls out.
+    pub fn group_by_calls(
+        self: &CallStack,
+        contract_id: FileId,
+    ) -> (Vec<CallDescr>, Option<CallDescr>) {
+        // We filter out calls and accesses made from within the builtin contracts, as well as
+        // generated variables introduced by program transformations.
+        let it = self.0.iter().filter(|elem| match elem {
+            StackElem::Var {id, ..} if id.is_generated() => false,
+            StackElem::Var{ pos: TermPos::Original(RawSpan { src_id, .. }), ..}
+            | StackElem::Var{pos: TermPos::Inherited(RawSpan { src_id, .. }), ..}
+            | StackElem::Fun(TermPos::Original(RawSpan { src_id, .. }))
+            | StackElem::Field {pos_access: TermPos::Original(RawSpan { src_id, .. }), ..}
+            | StackElem::Field {pos_access: TermPos::Inherited(RawSpan { src_id, .. }), ..}
+            | StackElem::App(TermPos::Original(RawSpan { src_id, .. }))
+            // We avoid applications (Fun/App) with inherited positions. Such calls include
+            // contracts applications which add confusing call items whose positions don't point to
+            // an actual call in the source.
+                if *src_id != contract_id =>
+            {
+                true
+            }
+            _ => false,
+        });
+
+        // We maintain a stack of active calls (whose head is being evaluated).  When encountering
+        // an identifier (variable or record field), we see if it could serve as a function name
+        // for the current active call. When a `Fun` is encountered, we check if this correspond to
+        // the current active call, and if it does, the call description is moved to a stack of
+        // processed calls.
+        //
+        // We also merge subcalls, in the sense that subcalls of larger calls are not considered
+        // separately. `app1` is a subcall of `app2` if the position of `app1` is included in the
+        // one of `app2` and the starting index is equal. We want `f a b c` to be reported as only
+        // one big call to `f` rather than three nested calls `f a`, `f a b`, and `f a b c`.
+        let mut pending: Vec<CallDescr> = Vec::new();
+        let mut entered: Vec<CallDescr> = Vec::new();
+
+        for elt in it {
+            match elt {
+                StackElem::Var { id, pos, .. }
+                | StackElem::Field {
+                    id,
+                    pos_access: pos,
+                    ..
+                } => {
+                    match pending.last_mut() {
+                        Some(CallDescr {
+                            head: ref mut head @ None,
+                            span: span_call,
+                        }) if pos.unwrap() <= *span_call => *head = Some(id.clone()),
+                        _ => (),
+                    };
+                }
+                StackElem::App(pos) => {
+                    let span = pos.unwrap();
+                    match pending.last() {
+                        Some(CallDescr {
+                            span: span_call, ..
+                        }) if span <= *span_call && span.start == span_call.start => (),
+                        _ => pending.push(CallDescr { head: None, span }),
+                    }
+                }
+                StackElem::Fun(pos) => {
+                    let span = pos.unwrap();
+                    if pending
+                        .last()
+                        .map(|cdescr| cdescr.span == span)
+                        .unwrap_or(false)
+                    {
+                        entered.push(pending.pop().unwrap());
+                    }
+                    // Otherwise, we are most probably entering a subcall () of the currently
+                    // active call (e.g. in an multi-ary application `f g h`, a subcall would be `f
+                    // g`). In any case, we do nothing.
+                }
+            }
+        }
+
+        entered.reverse();
+        (entered, pending.pop())
+    }
+
+    /// Return the length of the callstack. Wrapper for `callstack.0.len()`.
+    pub fn len(&self) -> usize {
+        self.0.len()
+    }
+
+    /// Truncate the callstack at a certain size. Used e.g. to quickly drop the elements introduced
+    /// during the strict evaluation of the operand of a primitive operator. Wrapper for
+    /// `callstack.0.truncate(len)`.
+    pub fn truncate(&mut self, len: usize) {
+        self.0.truncate(len)
+    }
+}
+
+impl From<CallStack> for Vec<StackElem> {
+    fn from(cs: CallStack) -> Self {
+        cs.0
+    }
+}
diff --git a/src/eval/lazy.rs b/src/eval/lazy.rs
new file mode 100644
index 00000000..df094d4e
--- /dev/null
+++ b/src/eval/lazy.rs
@@ -0,0 +1,256 @@
+//! Thunks and associated devices used to implement lazy evaluation.
+use super::{Closure, IdentKind};
+use std::cell::{Ref, RefCell, RefMut};
+use std::rc::{Rc, Weak};
+
+/// The state of a thunk.
+///
+/// When created, a thunk is flagged as suspended. When accessed for the first time, a corresponding
+/// [`ThunkUpdateFrame`](./struct.ThunkUpdateFrame.html) is pushed on the stack and the thunk is
+/// flagged as black-hole. This prevents direct infinite recursions, since if a thunk is
+/// re-accessed while still in a black-hole state, we are sure that the evaluation will loop, and
+/// we can thus error out before overflowing the stack or looping forever. Finally, once the
+/// content of a thunk has been evaluated, the thunk is updated with the new value and flagged as
+/// evaluated, so that future accesses won't even push an update frame on the stack.
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+pub enum ThunkState {
+    Blackholed,
+    Suspended,
+    Evaluated,
+}
+
+/// The mutable data stored inside a thunk.
+#[derive(Clone, Debug, PartialEq)]
+pub struct ThunkData {
+    inner: InnerThunkData,
+    state: ThunkState,
+}
+
+/// The part of [ThunkData] responsible for storing the closure itself. It can either be:
+/// - A standard thunk, that is destructively updated once and for all
+/// - A revertible thunk, that can be restored to its original expression. Used to implement
+/// recursive merging of records and overriding (see the [RFC
+/// overriding](https://github.com/tweag/nickel/pull/330))
+#[derive(Clone, Debug, PartialEq)]
+pub enum InnerThunkData {
+    Standard(Closure),
+    Revertible {
+        orig: Rc<Closure>,
+        cached: Rc<Closure>,
+    },
+}
+
+impl ThunkData {
+    /// Create new standard thunk data.
+    pub fn new(closure: Closure) -> Self {
+        ThunkData {
+            inner: InnerThunkData::Standard(closure),
+            state: ThunkState::Suspended,
+        }
+    }
+
+    /// Create new revertible thunk data.
+    pub fn new_rev(orig: Closure) -> Self {
+        let rc = Rc::new(orig);
+
+        ThunkData {
+            inner: InnerThunkData::Revertible {
+                orig: rc.clone(),
+                cached: rc,
+            },
+            state: ThunkState::Suspended,
+        }
+    }
+
+    /// Return a reference to the closure currently cached.
+    pub fn closure(&self) -> &Closure {
+        match self.inner {
+            InnerThunkData::Standard(ref closure) => closure,
+            InnerThunkData::Revertible { ref cached, .. } => cached,
+        }
+    }
+
+    /// Return a mutable reference to the closure currently cached.
+    pub fn closure_mut(&mut self) -> &mut Closure {
+        match self.inner {
+            InnerThunkData::Standard(ref mut closure) => closure,
+            InnerThunkData::Revertible { ref mut cached, .. } => Rc::make_mut(cached),
+        }
+    }
+
+    /// Consume the data and return the cached closure.
+    pub fn into_closure(self) -> Closure {
+        match self.inner {
+            InnerThunkData::Standard(closure) => closure,
+            InnerThunkData::Revertible { orig, cached } => {
+                std::mem::drop(orig);
+                Rc::try_unwrap(cached).unwrap_or_else(|rc| (*rc).clone())
+            }
+        }
+    }
+
+    /// Update the cached closure.
+    pub fn update(&mut self, new: Closure) {
+        match self.inner {
+            InnerThunkData::Standard(ref mut closure) => *closure = new,
+            InnerThunkData::Revertible { ref mut cached, .. } => *cached = Rc::new(new),
+        }
+
+        self.state = ThunkState::Evaluated;
+    }
+
+    /// Create fresh unevaluated thunk data from `self`, reverted to its original state before the
+    /// first update. For standard thunk data, the content is unchanged and the state is conserved:
+    /// in this case, `revert()` is the same as `clone()`.
+    pub fn revert(&self) -> Self {
+        match self.inner {
+            InnerThunkData::Standard(_) => self.clone(),
+            InnerThunkData::Revertible { ref orig, .. } => ThunkData {
+                inner: InnerThunkData::Revertible {
+                    orig: Rc::clone(orig),
+                    cached: Rc::clone(orig),
+                },
+                state: ThunkState::Suspended,
+            },
+        }
+    }
+}
+
+/// A thunk.
+///
+/// A thunk is a shared suspended computation. It is the primary device for the implementation of
+/// lazy evaluation.
+///
+/// For the implementation of recursive merging, some thunks need to be revertible, in the sense
+/// that we must be able to revert to the original expression before update. Those are called
+/// revertible thunks. Most expressions don't need revertible thunks as their evaluation will
+/// always give the same result, but some others, such as the ones containing recursive references
+/// inside a record may be invalidated by merging, and thus need to store the unaltered original
+/// expression. Those aspects are mainly handled in [InnerThunkData].
+#[derive(Clone, Debug, PartialEq)]
+pub struct Thunk {
+    data: Rc<RefCell<ThunkData>>,
+    ident_kind: IdentKind,
+}
+
+/// A black-holed thunk was accessed, which would lead to infinite recursion.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct BlackholedError;
+
+impl Thunk {
+    /// Create a new standard thunk.
+    pub fn new(closure: Closure, ident_kind: IdentKind) -> Self {
+        Thunk {
+            data: Rc::new(RefCell::new(ThunkData::new(closure))),
+            ident_kind,
+        }
+    }
+
+    /// Create a new revertible thunk.
+    pub fn new_rev(closure: Closure, ident_kind: IdentKind) -> Self {
+        Thunk {
+            data: Rc::new(RefCell::new(ThunkData::new_rev(closure))),
+            ident_kind,
+        }
+    }
+
+    pub fn state(&self) -> ThunkState {
+        self.data.borrow().state
+    }
+
+    /// Set the state to evaluated.
+    pub fn set_evaluated(&mut self) {
+        self.data.borrow_mut().state = ThunkState::Evaluated;
+    }
+
+    /// Generate an update frame from this thunk and set the state to `Blackholed`. Return an
+    /// error if the thunk was already black-holed.
+    pub fn mk_update_frame(&mut self) -> Result<ThunkUpdateFrame, BlackholedError> {
+        if self.data.borrow().state == ThunkState::Blackholed {
+            return Err(BlackholedError);
+        }
+
+        self.data.borrow_mut().state = ThunkState::Blackholed;
+
+        Ok(ThunkUpdateFrame {
+            data: Rc::downgrade(&self.data),
+            _ident_kind: self.ident_kind,
+        })
+    }
+
+    /// Immutably borrow the inner closure. Panic if there is another active mutable borrow.
+    pub fn borrow(&self) -> Ref<'_, Closure> {
+        Ref::map(self.data.borrow(), |data| data.closure())
+    }
+
+    /// Mutably borrow the inner closure. Panic if there is any other active borrow.
+    pub fn borrow_mut(&mut self) -> RefMut<'_, Closure> {
+        RefMut::map(self.data.borrow_mut(), |data| data.closure_mut())
+    }
+
+    /// Get an owned clone of the inner closure.
+    pub fn get_owned(&self) -> Closure {
+        self.data.borrow().closure().clone()
+    }
+
+    pub fn ident_kind(&self) -> IdentKind {
+        self.ident_kind
+    }
+
+    /// Consume the thunk and return an owned closure. Avoid cloning if this thunk is the only
+    /// reference to the inner closure.
+    pub fn into_closure(self) -> Closure {
+        match Rc::try_unwrap(self.data) {
+            Ok(inner) => inner.into_inner().into_closure(),
+            Err(rc) => rc.borrow().closure().clone(),
+        }
+    }
+
+    /// Create a fresh unevaluated thunk from `self`, reverted to its original state before the
+    /// first update. For a standard thunk, the content is unchanged and the state is conserved: in
+    /// this case, `revert()` is the same as `clone()`.
+    pub fn revert(&self) -> Self {
+        Thunk {
+            data: Rc::new(RefCell::new(self.data.borrow().revert())),
+            ident_kind: self.ident_kind,
+        }
+    }
+
+    /// Determine if a thunk is worth being put on the stack for future update.
+    ///
+    /// Typically, WHNFs and enriched values will not be evaluated to a simpler expression and are not
+    /// worth updating.
+    pub fn should_update(&self) -> bool {
+        let term = &self.borrow().body.term;
+        !term.is_whnf() && !term.is_metavalue()
+    }
+}
+
+/// A thunk update frame.
+///
+/// A thunk update frame is put on the stack whenever a variable is entered, such that once this
+/// variable is evaluated, the corresponding thunk can be updated. It is similar to a thunk but it
+/// holds a weak reference to the inner closure, to avoid unnecessarily keeping the underlying
+/// closure alive.
+#[derive(Clone, Debug)]
+pub struct ThunkUpdateFrame {
+    data: Weak<RefCell<ThunkData>>,
+    _ident_kind: IdentKind,
+}
+
+impl ThunkUpdateFrame {
+    /// Update the corresponding thunk with a closure. Set the state to `Evaluated`
+    ///
+    /// # Return
+    ///
+    /// - `true` if the thunk was successfully updated
+    /// - `false` if the corresponding closure has been dropped since
+    pub fn update(self, closure: Closure) -> bool {
+        if let Some(data) = Weak::upgrade(&self.data) {
+            data.borrow_mut().update(closure);
+            true
+        } else {
+            false
+        }
+    }
+}
diff --git a/src/merge.rs b/src/eval/merge.rs
similarity index 100%
rename from src/merge.rs
rename to src/eval/merge.rs
diff --git a/src/eval.rs b/src/eval/mod.rs
similarity index 53%
rename from src/eval.rs
rename to src/eval/mod.rs
index dfe53e08..f65bf14f 100644
--- a/src/eval.rs
+++ b/src/eval/mod.rs
@@ -1,4 +1,5 @@
 //! Evaluation of a Nickel term.
+//!
 //! The implementation of the Nickel abstract machine which evaluates a term. Note that this
 //! machine is not currently formalized somewhere and is just a convenient name to designate the
 //! current implementation.
@@ -92,487 +93,19 @@ use crate::{
     error::EvalError,
     identifier::Ident,
     mk_app,
-    operation::{continuate_operation, OperationCont},
-    position::RawSpan,
-    position::TermPos,
-    stack::Stack,
     term::{make as mk_term, BinaryOp, BindingType, MetaValue, RichTerm, StrChunk, Term, UnaryOp},
 };
-use codespan::FileId;
-use std::cell::{Ref, RefCell, RefMut};
-use std::rc::{Rc, Weak};
-
-/// The state of a thunk.
-///
-/// When created, a thunk is flagged as suspended. When accessed for the first time, a corresponding
-/// [`ThunkUpdateFrame`](./struct.ThunkUpdateFrame.html) is pushed on the stack and the thunk is
-/// flagged as black-hole. This prevents direct infinite recursions, since if a thunk is
-/// re-accessed while still in a black-hole state, we are sure that the evaluation will loop, and
-/// we can thus error out before overflowing the stack or looping forever. Finally, once the
-/// content of a thunk has been evaluated, the thunk is updated with the new value and flagged as
-/// evaluated, so that future accesses won't even push an update frame on the stack.
-#[derive(Copy, Clone, Eq, PartialEq, Debug)]
-pub enum ThunkState {
-    Blackholed,
-    Suspended,
-    Evaluated,
-}
-
-/// The mutable data stored inside a thunk.
-#[derive(Clone, Debug, PartialEq)]
-pub struct ThunkData {
-    inner: InnerThunkData,
-    state: ThunkState,
-}
-
-/// The part of [ThunkData] responsible for storing the closure itself. It can either be:
-/// - A standard thunk, that is destructively updated once and for all
-/// - A revertible thunk, that can be restored to its original expression. Used to implement
-/// recursive merging of records and overriding (see the [RFC
-/// overriding](https://github.com/tweag/nickel/pull/330))
-#[derive(Clone, Debug, PartialEq)]
-pub enum InnerThunkData {
-    Standard(Closure),
-    Revertible {
-        orig: Rc<Closure>,
-        cached: Rc<Closure>,
-    },
-}
-
-impl ThunkData {
-    /// Create new standard thunk data.
-    pub fn new(closure: Closure) -> Self {
-        ThunkData {
-            inner: InnerThunkData::Standard(closure),
-            state: ThunkState::Suspended,
-        }
-    }
-
-    /// Create new revertible thunk data.
-    pub fn new_rev(orig: Closure) -> Self {
-        let rc = Rc::new(orig);
-
-        ThunkData {
-            inner: InnerThunkData::Revertible {
-                orig: rc.clone(),
-                cached: rc,
-            },
-            state: ThunkState::Suspended,
-        }
-    }
-
-    /// Return a reference to the closure currently cached.
-    pub fn closure(&self) -> &Closure {
-        match self.inner {
-            InnerThunkData::Standard(ref closure) => closure,
-            InnerThunkData::Revertible { ref cached, .. } => cached,
-        }
-    }
-
-    /// Return a mutable reference to the closure currently cached.
-    pub fn closure_mut(&mut self) -> &mut Closure {
-        match self.inner {
-            InnerThunkData::Standard(ref mut closure) => closure,
-            InnerThunkData::Revertible { ref mut cached, .. } => Rc::make_mut(cached),
-        }
-    }
-
-    /// Consume the data and return the cached closure.
-    pub fn into_closure(self) -> Closure {
-        match self.inner {
-            InnerThunkData::Standard(closure) => closure,
-            InnerThunkData::Revertible { orig, cached } => {
-                std::mem::drop(orig);
-                Rc::try_unwrap(cached).unwrap_or_else(|rc| (*rc).clone())
-            }
-        }
-    }
-
-    /// Update the cached closure.
-    pub fn update(&mut self, new: Closure) {
-        match self.inner {
-            InnerThunkData::Standard(ref mut closure) => *closure = new,
-            InnerThunkData::Revertible { ref mut cached, .. } => *cached = Rc::new(new),
-        }
-
-        self.state = ThunkState::Evaluated;
-    }
-
-    /// Create fresh unevaluated thunk data from `self`, reverted to its original state before the
-    /// first update. For standard thunk data, the content is unchanged and the state is conserved:
-    /// in this case, `revert()` is the same as `clone()`.
-    pub fn revert(&self) -> Self {
-        match self.inner {
-            InnerThunkData::Standard(_) => self.clone(),
-            InnerThunkData::Revertible { ref orig, .. } => ThunkData {
-                inner: InnerThunkData::Revertible {
-                    orig: Rc::clone(orig),
-                    cached: Rc::clone(orig),
-                },
-                state: ThunkState::Suspended,
-            },
-        }
-    }
-}
-
-/// A thunk.
-///
-/// A thunk is a shared suspended computation. It is the primary device for the implementation of
-/// lazy evaluation.
-///
-/// For the implementation of recursive merging, some thunks need to be revertible, in the sense
-/// that we must be able to revert to the original expression before update. Those are called
-/// revertible thunks. Most expressions don't need revertible thunks as their evaluation will
-/// always give the same result, but some others, such as the ones containing recursive references
-/// inside a record may be invalidated by merging, and thus need to store the unaltered original
-/// expression. Those aspects are mainly handled in [InnerThunkData].
-#[derive(Clone, Debug, PartialEq)]
-pub struct Thunk {
-    data: Rc<RefCell<ThunkData>>,
-    ident_kind: IdentKind,
-}
-
-/// A black-holed thunk was accessed, which would lead to infinite recursion.
-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
-pub struct BlackholedError;
-
-impl Thunk {
-    /// Create a new standard thunk.
-    pub fn new(closure: Closure, ident_kind: IdentKind) -> Self {
-        Thunk {
-            data: Rc::new(RefCell::new(ThunkData::new(closure))),
-            ident_kind,
-        }
-    }
-
-    /// Create a new revertible thunk.
-    pub fn new_rev(closure: Closure, ident_kind: IdentKind) -> Self {
-        Thunk {
-            data: Rc::new(RefCell::new(ThunkData::new_rev(closure))),
-            ident_kind,
-        }
-    }
-
-    pub fn state(&self) -> ThunkState {
-        self.data.borrow().state
-    }
-
-    /// Set the state to evaluated.
-    pub fn set_evaluated(&mut self) {
-        self.data.borrow_mut().state = ThunkState::Evaluated;
-    }
-
-    /// Generate an update frame from this thunk and set the state to `Blackholed`. Return an
-    /// error if the thunk was already black-holed.
-    pub fn mk_update_frame(&mut self) -> Result<ThunkUpdateFrame, BlackholedError> {
-        if self.data.borrow().state == ThunkState::Blackholed {
-            return Err(BlackholedError);
-        }
-
-        self.data.borrow_mut().state = ThunkState::Blackholed;
-
-        Ok(ThunkUpdateFrame {
-            data: Rc::downgrade(&self.data),
-            ident_kind: self.ident_kind,
-        })
-    }
-
-    /// Immutably borrow the inner closure. Panic if there is another active mutable borrow.
-    pub fn borrow(&self) -> Ref<'_, Closure> {
-        Ref::map(self.data.borrow(), |data| data.closure())
-    }
-
-    /// Mutably borrow the inner closure. Panic if there is any other active borrow.
-    pub fn borrow_mut(&mut self) -> RefMut<'_, Closure> {
-        RefMut::map(self.data.borrow_mut(), |data| data.closure_mut())
-    }
-
-    /// Get an owned clone of the inner closure.
-    pub fn get_owned(&self) -> Closure {
-        self.data.borrow().closure().clone()
-    }
-
-    pub fn ident_kind(&self) -> IdentKind {
-        self.ident_kind
-    }
-
-    /// Consume the thunk and return an owned closure. Avoid cloning if this thunk is the only
-    /// reference to the inner closure.
-    pub fn into_closure(self) -> Closure {
-        match Rc::try_unwrap(self.data) {
-            Ok(inner) => inner.into_inner().into_closure(),
-            Err(rc) => rc.borrow().closure().clone(),
-        }
-    }
-
-    /// Create a fresh unevaluated thunk from `self`, reverted to its original state before the
-    /// first update. For a standard thunk, the content is unchanged and the state is conserved: in
-    /// this case, `revert()` is the same as `clone()`.
-    pub fn revert(&self) -> Self {
-        Thunk {
-            data: Rc::new(RefCell::new(self.data.borrow().revert())),
-            ident_kind: self.ident_kind,
-        }
-    }
-}
-
-/// A thunk update frame.
-///
-/// A thunk update frame is put on the stack whenever a variable is entered, such that once this
-/// variable is evaluated, the corresponding thunk can be updated. It is similar to a thunk but it
-/// holds a weak reference to the inner closure, to avoid unnecessarily keeping the underlying
-/// closure alive.
-#[derive(Clone, Debug)]
-pub struct ThunkUpdateFrame {
-    data: Weak<RefCell<ThunkData>>,
-    ident_kind: IdentKind,
-}
-
-impl ThunkUpdateFrame {
-    /// Update the corresponding thunk with a closure. Set the state to `Evaluated`
-    ///
-    /// # Return
-    ///
-    /// - `true` if the thunk was successfully updated
-    /// - `false` if the corresponding closure has been dropped since
-    pub fn update(self, closure: Closure) -> bool {
-        if let Some(data) = Weak::upgrade(&self.data) {
-            data.borrow_mut().update(closure);
-            true
-        } else {
-            false
-        }
-    }
-}
-
-/// A call stack, saving the history of function calls.
-///
-/// In a lazy language like Nickel, there are no well delimited stack frames due to how function
-/// application is evaluated. Additional information about the history of function calls is thus
-/// stored in the call stack solely for better error reporting.
-#[derive(PartialEq, Clone, Default, Debug)]
-pub struct CallStack(pub Vec<StackElem>);
-
-/// Basic description of a function call. Used for error reporting.
-pub struct CallDescr {
-    /// The name of the called function, if any.
-    pub head: Option<Ident>,
-    /// The position of the application.
-    pub span: RawSpan,
-}
-
-/// A call stack element.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub enum StackElem {
-    /// A function body was entered. The position is the position of the original application.
-    Fun(TermPos),
-    /// An application was evaluated.
-    App(TermPos),
-    /// A variable was entered.
-    Var {
-        kind: IdentKind,
-        id: Ident,
-        pos: TermPos,
-    },
-    /// A record field was entered.
-    Field {
-        id: Ident,
-        pos_record: TermPos,
-        pos_field: TermPos,
-        pos_access: TermPos,
-    },
-}
-
-impl CallStack {
-    pub fn new() -> Self {
-        CallStack(Vec::new())
-    }
-
-    /// Push a marker to indicate that a var was entered.
-    pub fn enter_var(&mut self, kind: IdentKind, id: Ident, pos: TermPos) {
-        self.0.push(StackElem::Var { kind, id, pos });
-    }
-
-    /// Push a marker to indicate that an application was entered.
-    pub fn enter_app(&mut self, pos: TermPos) {
-        // We ignore application without positions, which have been generated by the interpreter.
-        if pos.is_def() {
-            self.0.push(StackElem::App(pos));
-        }
-    }
 
-    /// Push a marker to indicate that during the evaluation an application, the function part was
-    /// finally evaluated to an expression of the form `fun x => body`, and that the body of this
-    /// function was entered.
-    pub fn enter_fun(&mut self, pos: TermPos) {
-        // We ignore application without positions, which have been generated by the interpreter.
-        if pos.is_def() {
-            self.0.push(StackElem::Fun(pos));
-        }
-    }
+pub mod callstack;
+pub mod lazy;
+pub mod merge;
+pub mod operation;
+pub mod stack;
 
-    /// Push a marker to indicate that a record field was entered.
-    pub fn enter_field(
-        &mut self,
-        id: Ident,
-        pos_record: TermPos,
-        pos_field: TermPos,
-        pos_access: TermPos,
-    ) {
-        self.0.push(StackElem::Field {
-            id,
-            pos_record,
-            pos_field,
-            pos_access,
-        });
-    }
-
-    /// Process a raw callstack by aggregating elements belonging to the same call. Return a list
-    /// of call descriptions from the most nested/recent to the least nested/recent, together with
-    /// the last pending call, if any.
-    ///
-    /// Recall that when a call `f arg` is evaluated, the following events happen:
-    /// 1. `arg` is pushed on the evaluation stack.
-    /// 2. `f` is evaluated.
-    /// 3. Hopefully, the result of this evaluation is a function `Func(id, body)`. `arg` is popped
-    ///    from the stack, bound to `id` in the environment, and `body is entered`.
-    ///
-    /// For error reporting purpose, we want to be able to determine the chain of nested calls leading
-    /// to the current code path at any moment. To do so, the Nickel abstract machine maintains a
-    /// callstack via this basic mechanism:
-    /// 1. When an application is evaluated, push a marker with the position of the application on the callstack.
-    /// 2. When a function body is entered, push a marker with the position of the original application on the
-    ///    callstack.
-    /// 3. When a variable is evaluated, push a marker with its name and position on the callstack.
-    /// 4. When a record field is accessed, push a marker with its name and position on the
-    ///    callstack too.
-    ///
-    /// Both field and variable are useful to determine the name of a called function, when there
-    /// is one.  The resulting stack is not suited to be reported to the user for the following
-    /// reasons:
-    ///
-    /// 1. One call spans several items on the callstack. First the application is entered (pushing
-    ///    an `App`), then possibly variables or other application are evaluated until we
-    ///    eventually reach a function for the left hand side. Then body of this function is
-    ///    entered (pushing a `Fun`).
-    /// 2. Because of currying, multi-ary applications span several objects on the callstack.
-    ///    Typically, `(fun x y => x + y) arg1 arg2` spans two `App` and two `Fun` elements in the
-    ///    form `App1 App2 Fun2 Fun1`, where the position span of `App1` includes the position span
-    ///    of `App2`.  We want to group them as one call.
-    /// 3. The callstack includes calls to builtin contracts. These calls are inserted implicitly
-    ///    by the abstract machine and are not written explicitly by the user. Showing them is
-    ///    confusing and clutters the call chain, so we get rid of them too.
-    ///
-    /// This is the role of `group_by_calls`, which filter out unwanted elements and groups
-    /// callstack elements into atomic call elements represented by [`CallDescr`].
-    ///
-    /// The final call description list is reversed such that the most nested calls, which are
-    /// usually the most relevant to understand the error, are printed first.
-    ///
-    /// # Arguments
-    ///
-    /// - `contract_id`: the `FileId` of the source containing standard contracts, to filter their
-    ///   calls out.
-    pub fn group_by_calls(
-        self: &CallStack,
-        contract_id: FileId,
-    ) -> (Vec<CallDescr>, Option<CallDescr>) {
-        // We filter out calls and accesses made from within the builtin contracts, as well as
-        // generated variables introduced by program transformations.
-        let it = self.0.iter().filter(|elem| match elem {
-            StackElem::Var {id, ..} if id.is_generated() => false,
-            StackElem::Var{ pos: TermPos::Original(RawSpan { src_id, .. }), ..}
-            | StackElem::Var{pos: TermPos::Inherited(RawSpan { src_id, .. }), ..}
-            | StackElem::Fun(TermPos::Original(RawSpan { src_id, .. }))
-            | StackElem::Field {pos_access: TermPos::Original(RawSpan { src_id, .. }), ..}
-            | StackElem::Field {pos_access: TermPos::Inherited(RawSpan { src_id, .. }), ..}
-            | StackElem::App(TermPos::Original(RawSpan { src_id, .. }))
-            // We avoid applications (Fun/App) with inherited positions. Such calls include
-            // contracts applications which add confusing call items whose positions don't point to
-            // an actual call in the source.
-                if *src_id != contract_id =>
-            {
-                true
-            }
-            _ => false,
-        });
-
-        // We maintain a stack of active calls (whose head is being evaluated).  When encountering
-        // an identifier (variable or record field), we see if it could serve as a function name
-        // for the current active call. When a `Fun` is encountered, we check if this correspond to
-        // the current active call, and if it does, the call description is moved to a stack of
-        // processed calls.
-        //
-        // We also merge subcalls, in the sense that subcalls of larger calls are not considered
-        // separately. `app1` is a subcall of `app2` if the position of `app1` is included in the
-        // one of `app2` and the starting index is equal. We want `f a b c` to be reported as only
-        // one big call to `f` rather than three nested calls `f a`, `f a b`, and `f a b c`.
-        let mut pending: Vec<CallDescr> = Vec::new();
-        let mut entered: Vec<CallDescr> = Vec::new();
-
-        for elt in it {
-            match elt {
-                StackElem::Var { id, pos, .. }
-                | StackElem::Field {
-                    id,
-                    pos_access: pos,
-                    ..
-                } => {
-                    match pending.last_mut() {
-                        Some(CallDescr {
-                            head: ref mut head @ None,
-                            span: span_call,
-                        }) if pos.unwrap() <= *span_call => *head = Some(id.clone()),
-                        _ => (),
-                    };
-                }
-                StackElem::App(pos) => {
-                    let span = pos.unwrap();
-                    match pending.last() {
-                        Some(CallDescr {
-                            span: span_call, ..
-                        }) if span <= *span_call && span.start == span_call.start => (),
-                        _ => pending.push(CallDescr { head: None, span }),
-                    }
-                }
-                StackElem::Fun(pos) => {
-                    let span = pos.unwrap();
-                    if pending
-                        .last()
-                        .map(|cdescr| cdescr.span == span)
-                        .unwrap_or(false)
-                    {
-                        entered.push(pending.pop().unwrap());
-                    }
-                    // Otherwise, we are most probably entering a subcall () of the currently
-                    // active call (e.g. in an multi-ary application `f g h`, a subcall would be `f
-                    // g`). In any case, we do nothing.
-                }
-            }
-        }
-
-        entered.reverse();
-        (entered, pending.pop())
-    }
-
-    /// Return the length of the callstack. Wrapper for `callstack.0.len()`.
-    pub fn len(&self) -> usize {
-        self.0.len()
-    }
-
-    /// Truncate the callstack at a certain size. Used e.g. to quickly drop the elements introduced
-    /// during the strict evaluation of the operand of a primitive operator. Wrapper for
-    /// `callstack.0.truncate(len)`.
-    pub fn truncate(&mut self, len: usize) {
-        self.0.truncate(len)
-    }
-}
-
-impl From<CallStack> for Vec<StackElem> {
-    fn from(cs: CallStack) -> Self {
-        cs.0
-    }
-}
+use callstack::*;
+use lazy::*;
+use operation::{continuate_operation, OperationCont};
+use stack::Stack;
 
 /// Kind of an identifier.
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -635,14 +168,6 @@ pub fn env_add(env: &mut Environment, id: Ident, rt: RichTerm, local_env: Enviro
     env.insert(id, Thunk::new(closure, IdentKind::Let));
 }
 
-/// Determine if a thunk is worth being put on the stack for future update.
-///
-/// Typically, WHNFs and enriched values will not be evaluated to a simpler expression and are not
-/// worth updating.
-fn should_update(t: &Term) -> bool {
-    !t.is_whnf() && !t.is_metavalue()
-}
-
 /// Evaluate a Nickel term. Wrapper around [eval_closure](fn.eval_closure.html) that starts from an
 /// empty local environment and drops the final environment.
 pub fn eval<R>(
@@ -790,7 +315,7 @@ where
                 std::mem::drop(env); // thunk may be a 1RC pointer
 
                 if thunk.state() != ThunkState::Evaluated {
-                    if should_update(&thunk.borrow().body.term) {
+                    if thunk.should_update() {
                         match thunk.mk_update_frame() {
                             Ok(thunk_upd) => stack.push_thunk(thunk_upd),
                             Err(BlackholedError) => {
@@ -1350,362 +875,4 @@ pub fn subst(rt: RichTerm, global_env: &Environment, env: &Environment) -> RichT
 }
 
 #[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::cache::resolvers::{DummyResolver, SimpleResolver};
-    use crate::error::ImportError;
-    use crate::label::Label;
-    use crate::parser::{grammar, lexer};
-    use crate::term::make as mk_term;
-    use crate::term::{BinaryOp, StrChunk, UnaryOp};
-    use crate::transform::import_resolution::resolve_imports;
-    use crate::{mk_app, mk_fun};
-    use codespan::Files;
-
-    /// Evaluate a term without import support.
-    fn eval_no_import(t: RichTerm) -> Result<Term, EvalError> {
-        eval(t, &Environment::new(), &mut DummyResolver {}).map(Term::from)
-    }
-
-    fn parse(s: &str) -> Option<RichTerm> {
-        let id = Files::new().add("<test>", String::from(s));
-
-        grammar::TermParser::new()
-            .parse_term(id, lexer::Lexer::new(&s))
-            .map(RichTerm::without_pos)
-            .map_err(|err| println!("{:?}", err))
-            .ok()
-    }
-
-    #[test]
-    fn identity_over_values() {
-        let num = Term::Num(45.3);
-        assert_eq!(Ok(num.clone()), eval_no_import(num.into()));
-
-        let boolean = Term::Bool(true);
-        assert_eq!(Ok(boolean.clone()), eval_no_import(boolean.into()));
-
-        let lambda = mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x")));
-        assert_eq!(Ok(lambda.as_ref().clone()), eval_no_import(lambda.into()));
-    }
-
-    #[test]
-    fn blame_panics() {
-        let label = Label::dummy();
-        if let Err(EvalError::BlameError(l, ..)) =
-            eval_no_import(mk_term::op1(UnaryOp::Blame(), Term::Lbl(label.clone())))
-        {
-            assert_eq!(l, label);
-        } else {
-            panic!("This evaluation should've returned a BlameError!");
-        }
-    }
-
-    #[test]
-    #[should_panic]
-    fn lone_var_panics() {
-        eval_no_import(mk_term::var("unbound")).unwrap();
-    }
-
-    #[test]
-    fn only_fun_are_applicable() {
-        eval_no_import(mk_app!(Term::Bool(true), Term::Num(45.))).unwrap_err();
-    }
-
-    #[test]
-    fn simple_app() {
-        let t = mk_app!(mk_term::id(), Term::Num(5.0));
-        assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
-    }
-
-    #[test]
-    fn simple_let() {
-        let t = mk_term::let_in("x", Term::Num(5.0), mk_term::var("x"));
-        assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
-    }
-
-    #[test]
-    fn simple_ite() {
-        let t = mk_term::if_then_else(Term::Bool(true), Term::Num(5.0), Term::Bool(false));
-        assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
-    }
-
-    #[test]
-    fn simple_plus() {
-        let t = mk_term::op2(BinaryOp::Plus(), Term::Num(5.0), Term::Num(7.5));
-        assert_eq!(Ok(Term::Num(12.5)), eval_no_import(t));
-    }
-
-    #[test]
-    fn asking_for_various_types() {
-        let num = mk_term::op1(UnaryOp::IsNum(), Term::Num(45.3));
-        assert_eq!(Ok(Term::Bool(true)), eval_no_import(num));
-
-        let boolean = mk_term::op1(UnaryOp::IsBool(), Term::Bool(true));
-        assert_eq!(Ok(Term::Bool(true)), eval_no_import(boolean));
-
-        let lambda = mk_term::op1(
-            UnaryOp::IsFun(),
-            mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x"))),
-        );
-        assert_eq!(Ok(Term::Bool(true)), eval_no_import(lambda));
-    }
-
-    fn mk_default(t: RichTerm) -> Term {
-        use crate::term::MergePriority;
-
-        let mut meta = MetaValue::from(t);
-        meta.priority = MergePriority::Default;
-        Term::MetaValue(meta)
-    }
-
-    fn mk_docstring<S>(t: RichTerm, s: S) -> Term
-    where
-        S: Into<String>,
-    {
-        let mut meta = MetaValue::from(t);
-        meta.doc.replace(s.into());
-        Term::MetaValue(meta)
-    }
-
-    #[test]
-    fn enriched_terms_unwrapping() {
-        let t = mk_default(mk_default(mk_docstring(Term::Bool(false).into(), "a").into()).into())
-            .into();
-        assert_eq!(Ok(Term::Bool(false)), eval_no_import(t));
-    }
-
-    #[test]
-    fn merge_enriched_default() {
-        let t = mk_term::op2(
-            BinaryOp::Merge(),
-            Term::Num(1.0),
-            mk_default(Term::Num(2.0).into()),
-        );
-        assert_eq!(Ok(Term::Num(1.0)), eval_no_import(t));
-    }
-
-    #[test]
-    fn merge_incompatible_defaults() {
-        let t = mk_term::op2(
-            BinaryOp::Merge(),
-            mk_default(Term::Num(1.0).into()),
-            mk_default(Term::Num(2.0).into()),
-        );
-
-        eval_no_import(t).unwrap_err();
-    }
-
-    #[test]
-    fn imports() {
-        let mut resolver = SimpleResolver::new();
-        resolver.add_source(String::from("two"), String::from("1 + 1"));
-        resolver.add_source(String::from("lib"), String::from("{f = true}"));
-        resolver.add_source(String::from("bad"), String::from("^$*/.23ab 0°@"));
-        resolver.add_source(
-            String::from("nested"),
-            String::from("let x = import \"two\" in x + 1"),
-        );
-        resolver.add_source(
-            String::from("cycle"),
-            String::from("let x = import \"cycle_b\" in {a = 1, b = x.a}"),
-        );
-        resolver.add_source(
-            String::from("cycle_b"),
-            String::from("let x = import \"cycle\" in {a = x.a}"),
-        );
-
-        fn mk_import<R>(
-            var: &str,
-            import: &str,
-            body: RichTerm,
-            resolver: &mut R,
-        ) -> Result<RichTerm, ImportError>
-        where
-            R: ImportResolver,
-        {
-            resolve_imports(
-                mk_term::let_in(var, mk_term::import(import), body),
-                resolver,
-            )
-            .map(|(t, _)| t)
-        }
-
-        // let x = import "does_not_exist" in x
-        match mk_import("x", "does_not_exist", mk_term::var("x"), &mut resolver).unwrap_err() {
-            ImportError::IOError(_, _, _) => (),
-            _ => assert!(false),
-        };
-
-        // let x = import "bad" in x
-        match mk_import("x", "bad", mk_term::var("x"), &mut resolver).unwrap_err() {
-            ImportError::ParseErrors(_, _) => (),
-            _ => assert!(false),
-        };
-
-        // let x = import "two" in x
-        assert_eq!(
-            eval(
-                mk_import("x", "two", mk_term::var("x"), &mut resolver).unwrap(),
-                &Environment::new(),
-                &mut resolver
-            )
-            .map(Term::from)
-            .unwrap(),
-            Term::Num(2.0)
-        );
-
-        // let x = import "lib" in x.f
-        assert_eq!(
-            eval(
-                mk_import(
-                    "x",
-                    "lib",
-                    mk_term::op1(UnaryOp::StaticAccess(Ident::from("f")), mk_term::var("x")),
-                    &mut resolver,
-                )
-                .unwrap(),
-                &Environment::new(),
-                &mut resolver
-            )
-            .map(Term::from)
-            .unwrap(),
-            Term::Bool(true)
-        );
-    }
-
-    #[test]
-    fn interpolation_simple() {
-        let mut chunks = vec![
-            StrChunk::Literal(String::from("Hello")),
-            StrChunk::expr(
-                mk_term::op2(
-                    BinaryOp::StrConcat(),
-                    mk_term::string(", "),
-                    mk_term::string("World!"),
-                )
-                .into(),
-            ),
-            StrChunk::Literal(String::from(" How")),
-            StrChunk::expr(mk_term::if_then_else(
-                Term::Bool(true),
-                mk_term::string(" are"),
-                mk_term::string(" is"),
-            )),
-            StrChunk::Literal(String::from(" you?")),
-        ];
-        chunks.reverse();
-
-        let t: RichTerm = Term::StrChunks(chunks).into();
-        assert_eq!(
-            eval_no_import(t),
-            Ok(Term::Str(String::from("Hello, World! How are you?")))
-        );
-    }
-
-    #[test]
-    fn interpolation_nested() {
-        let mut inner_chunks = vec![
-            StrChunk::Literal(String::from(" How")),
-            StrChunk::expr(
-                Term::Op2(
-                    BinaryOp::StrConcat(),
-                    mk_term::string(" ar"),
-                    mk_term::string("e"),
-                )
-                .into(),
-            ),
-            StrChunk::expr(mk_term::if_then_else(
-                Term::Bool(true),
-                mk_term::string(" you"),
-                mk_term::string(" me"),
-            )),
-        ];
-        inner_chunks.reverse();
-
-        let mut chunks = vec![
-            StrChunk::Literal(String::from("Hello, World!")),
-            StrChunk::expr(Term::StrChunks(inner_chunks).into()),
-            StrChunk::Literal(String::from("?")),
-        ];
-        chunks.reverse();
-
-        let t: RichTerm = Term::StrChunks(chunks).into();
-        assert_eq!(
-            eval_no_import(t),
-            Ok(Term::Str(String::from("Hello, World! How are you?")))
-        );
-    }
-
-    #[test]
-    fn global_env() {
-        let mut global_env = Environment::new();
-        let mut resolver = DummyResolver {};
-        global_env.insert(
-            Ident::from("g"),
-            Thunk::new(
-                Closure::atomic_closure(Term::Num(1.0).into()),
-                IdentKind::Let,
-            ),
-        );
-
-        let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("x"));
-        assert_eq!(
-            eval(t, &global_env, &mut resolver).map(Term::from),
-            Ok(Term::Num(2.0))
-        );
-
-        let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("g"));
-        assert_eq!(
-            eval(t, &global_env, &mut resolver).map(Term::from),
-            Ok(Term::Num(1.0))
-        );
-
-        // Shadowing of global environment
-        let t = mk_term::let_in("g", Term::Num(2.0), mk_term::var("g"));
-        assert_eq!(
-            eval(t, &global_env, &mut resolver).map(Term::from),
-            Ok(Term::Num(2.0))
-        );
-    }
-
-    fn mk_env(bindings: Vec<(&str, RichTerm)>) -> Environment {
-        bindings
-            .into_iter()
-            .map(|(id, t)| {
-                (
-                    id.into(),
-                    Thunk::new(Closure::atomic_closure(t), IdentKind::Let),
-                )
-            })
-            .collect()
-    }
-
-    #[test]
-    fn substitution() {
-        let global_env = mk_env(vec![
-            ("glob1", Term::Num(1.0).into()),
-            ("glob2", parse("\"Glob2\"").unwrap()),
-            ("glob3", Term::Bool(false).into()),
-        ]);
-        let env = mk_env(vec![
-            ("loc1", Term::Bool(true).into()),
-            ("loc2", parse("if glob3 then glob1 else glob2").unwrap()),
-        ]);
-
-        let t = parse("let x = 1 in if loc1 then 1 + loc2 else glob3").unwrap();
-        assert_eq!(
-            subst(t, &global_env, &env),
-            parse("let x = 1 in if true then 1 + (if false then 1 else \"Glob2\") else false")
-                .unwrap()
-        );
-
-        let t =
-            parse("switch {`x => [1, glob1], `y => loc2, `z => {id = true, other = glob3}} loc1")
-                .unwrap();
-        assert_eq!(
-            subst(t, &global_env, &env),
-            parse("switch {`x => [1, 1], `y => (if false then 1 else \"Glob2\"), `z => {id = true, other = false}} true").unwrap()
-        );
-    }
-}
+mod tests;
diff --git a/src/operation.rs b/src/eval/operation.rs
similarity index 99%
rename from src/operation.rs
rename to src/eval/operation.rs
index 4629a06c..811aee5c 100644
--- a/src/operation.rs
+++ b/src/eval/operation.rs
@@ -6,15 +6,15 @@
 //! the functions [`process_unary_operation`](fn.process_unary_operation.html) and
 //! [`process_binary_operation`](fn.process_binary_operation.html) receive evaluated operands and
 //! implement the actual semantics of operators.
+use super::merge;
+use super::merge::{merge, MergeMode};
+use super::stack::Stack;
 use crate::error::EvalError;
 use crate::eval::{subst, CallStack, Closure, Environment};
 use crate::identifier::Ident;
 use crate::label::ty_path;
-use crate::merge;
-use crate::merge::{merge, MergeMode};
 use crate::mk_record;
 use crate::position::TermPos;
-use crate::stack::Stack;
 use crate::term::make as mk_term;
 use crate::term::{BinaryOp, NAryOp, RichTerm, StrChunk, Term, UnaryOp};
 use crate::transform::Closurizable;
diff --git a/src/stack.rs b/src/eval/stack.rs
similarity index 99%
rename from src/stack.rs
rename to src/eval/stack.rs
index e5f7d085..bb7588cb 100644
--- a/src/stack.rs
+++ b/src/eval/stack.rs
@@ -1,8 +1,8 @@
 //! Define the main evaluation stack of the Nickel abstract machine and related operations.
 //!
 //! See [eval](../eval/index.html).
+use super::operation::OperationCont;
 use crate::eval::{Closure, Environment, IdentKind, Thunk, ThunkUpdateFrame};
-use crate::operation::OperationCont;
 use crate::position::TermPos;
 use crate::term::{RichTerm, StrChunk};
 
diff --git a/src/label.rs b/src/label.rs
index e42ce1e2..700c0742 100644
--- a/src/label.rs
+++ b/src/label.rs
@@ -2,7 +2,7 @@
 //!
 //! A label is a value holding metadata relative to contract checking. It gives the user useful
 //! information about the context of a contract failure.
-use crate::eval::Thunk;
+use crate::eval::lazy::Thunk;
 use crate::position::{RawSpan, TermPos};
 use crate::types::{AbsType, Types};
 use codespan::Files;
diff --git a/src/lib.rs b/src/lib.rs
index 2d863b8e..b1a081fc 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -5,14 +5,11 @@ pub mod error;
 pub mod eval;
 pub mod identifier;
 pub mod label;
-pub mod merge;
-pub mod operation;
 pub mod parser;
 pub mod position;
 pub mod program;
 pub mod repl;
 pub mod serialize;
-pub mod stack;
 pub mod stdlib;
 pub mod term;
 pub mod transform;
diff --git a/src/term.rs b/src/term.rs
index 31f54efb..4d91ff4d 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -265,15 +265,13 @@ impl MetaValue {
             value: _,
         } = outer;
 
-        if types.is_some() {
-            // If both have type annotations, the result will have the outer one as a type annotation.
-            // However we still need to enforce the corresponding contract to preserve the operational
-            // semantics. Thus, the inner type annotation is derelicted to a contract.
-            match inner.types.take() {
-                Some(ctr) => contracts.push(ctr),
-                _ => (),
-            };
-        }
+        // If both have type annotations, the result will have the outer one as a type annotation.
+        // However we still need to enforce the corresponding contract to preserve the operational
+        // semantics. Thus, the inner type annotation is derelicted to a contract.
+        match inner.types.take() {
+            Some(ctr) if types.is_some() => contracts.push(ctr),
+            _ => (),
+        };
 
         contracts.extend(inner.contracts.into_iter());
 
@@ -1327,21 +1325,3 @@ pub mod make {
         Term::Import(path.into()).into()
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    /// Regression test for issue [#548](https://github.com/tweag/nickel/issues/548)
-    #[test]
-    fn metavalue_flatten() {
-        let mut inner = MetaValue::new();
-        inner.types = Some(Contract {
-            types: Types(AbsType::Num()),
-            label: Label::dummy(),
-        });
-        let outer = MetaValue::new();
-        let res = MetaValue::flatten(outer, inner);
-        assert_ne!(res.types, None);
-    }
-}
diff --git a/src/transform/mod.rs b/src/transform/mod.rs
index 0b96ae76..cb4fd827 100644
--- a/src/transform/mod.rs
+++ b/src/transform/mod.rs
@@ -1,5 +1,5 @@
 use crate::cache::ImportResolver;
-use crate::eval::{Closure, Environment, IdentKind, Thunk};
+use crate::eval::{lazy::Thunk, Closure, Environment, IdentKind};
 use crate::identifier::Ident;
 use crate::term::{Contract, RichTerm, Term, TraverseMethod};
 use crate::types::{AbsType, Types};


Test patch:
diff --git a/src/eval/tests.rs b/src/eval/tests.rs
new file mode 100644
index 00000000..b0338b3f
--- /dev/null
+++ b/src/eval/tests.rs
@@ -0,0 +1,355 @@
+use super::*;
+use crate::cache::resolvers::{DummyResolver, SimpleResolver};
+use crate::error::ImportError;
+use crate::label::Label;
+use crate::parser::{grammar, lexer};
+use crate::term::make as mk_term;
+use crate::term::{BinaryOp, StrChunk, UnaryOp};
+use crate::transform::import_resolution::resolve_imports;
+use crate::{mk_app, mk_fun};
+use codespan::Files;
+
+/// Evaluate a term without import support.
+fn eval_no_import(t: RichTerm) -> Result<Term, EvalError> {
+    eval(t, &Environment::new(), &mut DummyResolver {}).map(Term::from)
+}
+
+fn parse(s: &str) -> Option<RichTerm> {
+    let id = Files::new().add("<test>", String::from(s));
+
+    grammar::TermParser::new()
+        .parse_term(id, lexer::Lexer::new(&s))
+        .map(RichTerm::without_pos)
+        .map_err(|err| println!("{:?}", err))
+        .ok()
+}
+
+#[test]
+fn identity_over_values() {
+    let num = Term::Num(45.3);
+    assert_eq!(Ok(num.clone()), eval_no_import(num.into()));
+
+    let boolean = Term::Bool(true);
+    assert_eq!(Ok(boolean.clone()), eval_no_import(boolean.into()));
+
+    let lambda = mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x")));
+    assert_eq!(Ok(lambda.as_ref().clone()), eval_no_import(lambda.into()));
+}
+
+#[test]
+fn blame_panics() {
+    let label = Label::dummy();
+    if let Err(EvalError::BlameError(l, ..)) =
+        eval_no_import(mk_term::op1(UnaryOp::Blame(), Term::Lbl(label.clone())))
+    {
+        assert_eq!(l, label);
+    } else {
+        panic!("This evaluation should've returned a BlameError!");
+    }
+}
+
+#[test]
+#[should_panic]
+fn lone_var_panics() {
+    eval_no_import(mk_term::var("unbound")).unwrap();
+}
+
+#[test]
+fn only_fun_are_applicable() {
+    eval_no_import(mk_app!(Term::Bool(true), Term::Num(45.))).unwrap_err();
+}
+
+#[test]
+fn simple_app() {
+    let t = mk_app!(mk_term::id(), Term::Num(5.0));
+    assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
+}
+
+#[test]
+fn simple_let() {
+    let t = mk_term::let_in("x", Term::Num(5.0), mk_term::var("x"));
+    assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
+}
+
+#[test]
+fn simple_ite() {
+    let t = mk_term::if_then_else(Term::Bool(true), Term::Num(5.0), Term::Bool(false));
+    assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t));
+}
+
+#[test]
+fn simple_plus() {
+    let t = mk_term::op2(BinaryOp::Plus(), Term::Num(5.0), Term::Num(7.5));
+    assert_eq!(Ok(Term::Num(12.5)), eval_no_import(t));
+}
+
+#[test]
+fn asking_for_various_types() {
+    let num = mk_term::op1(UnaryOp::IsNum(), Term::Num(45.3));
+    assert_eq!(Ok(Term::Bool(true)), eval_no_import(num));
+
+    let boolean = mk_term::op1(UnaryOp::IsBool(), Term::Bool(true));
+    assert_eq!(Ok(Term::Bool(true)), eval_no_import(boolean));
+
+    let lambda = mk_term::op1(
+        UnaryOp::IsFun(),
+        mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x"))),
+    );
+    assert_eq!(Ok(Term::Bool(true)), eval_no_import(lambda));
+}
+
+fn mk_default(t: RichTerm) -> Term {
+    use crate::term::MergePriority;
+
+    let mut meta = MetaValue::from(t);
+    meta.priority = MergePriority::Default;
+    Term::MetaValue(meta)
+}
+
+fn mk_docstring<S>(t: RichTerm, s: S) -> Term
+where
+    S: Into<String>,
+{
+    let mut meta = MetaValue::from(t);
+    meta.doc.replace(s.into());
+    Term::MetaValue(meta)
+}
+
+#[test]
+fn enriched_terms_unwrapping() {
+    let t =
+        mk_default(mk_default(mk_docstring(Term::Bool(false).into(), "a").into()).into()).into();
+    assert_eq!(Ok(Term::Bool(false)), eval_no_import(t));
+}
+
+#[test]
+fn merge_enriched_default() {
+    let t = mk_term::op2(
+        BinaryOp::Merge(),
+        Term::Num(1.0),
+        mk_default(Term::Num(2.0).into()),
+    );
+    assert_eq!(Ok(Term::Num(1.0)), eval_no_import(t));
+}
+
+#[test]
+fn merge_incompatible_defaults() {
+    let t = mk_term::op2(
+        BinaryOp::Merge(),
+        mk_default(Term::Num(1.0).into()),
+        mk_default(Term::Num(2.0).into()),
+    );
+
+    eval_no_import(t).unwrap_err();
+}
+
+#[test]
+fn imports() {
+    let mut resolver = SimpleResolver::new();
+    resolver.add_source(String::from("two"), String::from("1 + 1"));
+    resolver.add_source(String::from("lib"), String::from("{f = true}"));
+    resolver.add_source(String::from("bad"), String::from("^$*/.23ab 0°@"));
+    resolver.add_source(
+        String::from("nested"),
+        String::from("let x = import \"two\" in x + 1"),
+    );
+    resolver.add_source(
+        String::from("cycle"),
+        String::from("let x = import \"cycle_b\" in {a = 1, b = x.a}"),
+    );
+    resolver.add_source(
+        String::from("cycle_b"),
+        String::from("let x = import \"cycle\" in {a = x.a}"),
+    );
+
+    fn mk_import<R>(
+        var: &str,
+        import: &str,
+        body: RichTerm,
+        resolver: &mut R,
+    ) -> Result<RichTerm, ImportError>
+    where
+        R: ImportResolver,
+    {
+        resolve_imports(
+            mk_term::let_in(var, mk_term::import(import), body),
+            resolver,
+        )
+        .map(|(t, _)| t)
+    }
+
+    // let x = import "does_not_exist" in x
+    match mk_import("x", "does_not_exist", mk_term::var("x"), &mut resolver).unwrap_err() {
+        ImportError::IOError(_, _, _) => (),
+        _ => assert!(false),
+    };
+
+    // let x = import "bad" in x
+    match mk_import("x", "bad", mk_term::var("x"), &mut resolver).unwrap_err() {
+        ImportError::ParseErrors(_, _) => (),
+        _ => assert!(false),
+    };
+
+    // let x = import "two" in x
+    assert_eq!(
+        eval(
+            mk_import("x", "two", mk_term::var("x"), &mut resolver).unwrap(),
+            &Environment::new(),
+            &mut resolver
+        )
+        .map(Term::from)
+        .unwrap(),
+        Term::Num(2.0)
+    );
+
+    // let x = import "lib" in x.f
+    assert_eq!(
+        eval(
+            mk_import(
+                "x",
+                "lib",
+                mk_term::op1(UnaryOp::StaticAccess(Ident::from("f")), mk_term::var("x")),
+                &mut resolver,
+            )
+            .unwrap(),
+            &Environment::new(),
+            &mut resolver
+        )
+        .map(Term::from)
+        .unwrap(),
+        Term::Bool(true)
+    );
+}
+
+#[test]
+fn interpolation_simple() {
+    let mut chunks = vec![
+        StrChunk::Literal(String::from("Hello")),
+        StrChunk::expr(
+            mk_term::op2(
+                BinaryOp::StrConcat(),
+                mk_term::string(", "),
+                mk_term::string("World!"),
+            )
+            .into(),
+        ),
+        StrChunk::Literal(String::from(" How")),
+        StrChunk::expr(mk_term::if_then_else(
+            Term::Bool(true),
+            mk_term::string(" are"),
+            mk_term::string(" is"),
+        )),
+        StrChunk::Literal(String::from(" you?")),
+    ];
+    chunks.reverse();
+
+    let t: RichTerm = Term::StrChunks(chunks).into();
+    assert_eq!(
+        eval_no_import(t),
+        Ok(Term::Str(String::from("Hello, World! How are you?")))
+    );
+}
+
+#[test]
+fn interpolation_nested() {
+    let mut inner_chunks = vec![
+        StrChunk::Literal(String::from(" How")),
+        StrChunk::expr(
+            Term::Op2(
+                BinaryOp::StrConcat(),
+                mk_term::string(" ar"),
+                mk_term::string("e"),
+            )
+            .into(),
+        ),
+        StrChunk::expr(mk_term::if_then_else(
+            Term::Bool(true),
+            mk_term::string(" you"),
+            mk_term::string(" me"),
+        )),
+    ];
+    inner_chunks.reverse();
+
+    let mut chunks = vec![
+        StrChunk::Literal(String::from("Hello, World!")),
+        StrChunk::expr(Term::StrChunks(inner_chunks).into()),
+        StrChunk::Literal(String::from("?")),
+    ];
+    chunks.reverse();
+
+    let t: RichTerm = Term::StrChunks(chunks).into();
+    assert_eq!(
+        eval_no_import(t),
+        Ok(Term::Str(String::from("Hello, World! How are you?")))
+    );
+}
+
+#[test]
+fn global_env() {
+    let mut global_env = Environment::new();
+    let mut resolver = DummyResolver {};
+    global_env.insert(
+        Ident::from("g"),
+        Thunk::new(
+            Closure::atomic_closure(Term::Num(1.0).into()),
+            IdentKind::Let,
+        ),
+    );
+
+    let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("x"));
+    assert_eq!(
+        eval(t, &global_env, &mut resolver).map(Term::from),
+        Ok(Term::Num(2.0))
+    );
+
+    let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("g"));
+    assert_eq!(
+        eval(t, &global_env, &mut resolver).map(Term::from),
+        Ok(Term::Num(1.0))
+    );
+
+    // Shadowing of global environment
+    let t = mk_term::let_in("g", Term::Num(2.0), mk_term::var("g"));
+    assert_eq!(
+        eval(t, &global_env, &mut resolver).map(Term::from),
+        Ok(Term::Num(2.0))
+    );
+}
+
+fn mk_env(bindings: Vec<(&str, RichTerm)>) -> Environment {
+    bindings
+        .into_iter()
+        .map(|(id, t)| {
+            (
+                id.into(),
+                Thunk::new(Closure::atomic_closure(t), IdentKind::Let),
+            )
+        })
+        .collect()
+}
+
+#[test]
+fn substitution() {
+    let global_env = mk_env(vec![
+        ("glob1", Term::Num(1.0).into()),
+        ("glob2", parse("\"Glob2\"").unwrap()),
+        ("glob3", Term::Bool(false).into()),
+    ]);
+    let env = mk_env(vec![
+        ("loc1", Term::Bool(true).into()),
+        ("loc2", parse("if glob3 then glob1 else glob2").unwrap()),
+    ]);
+
+    let t = parse("let x = 1 in if loc1 then 1 + loc2 else glob3").unwrap();
+    assert_eq!(
+        subst(t, &global_env, &env),
+        parse("let x = 1 in if true then 1 + (if false then 1 else \"Glob2\") else false").unwrap()
+    );
+
+    let t = parse("switch {`x => [1, glob1], `y => loc2, `z => {id = true, other = glob3}} loc1")
+        .unwrap();
+    assert_eq!(
+        subst(t, &global_env, &env),
+        parse("switch {`x => [1, 1], `y => (if false then 1 else \"Glob2\"), `z => {id = true, other = false}} true").unwrap()
+    );
+}