Repository: tweag/nickel
Pull request #862 (2022-10-07 14:29:29)

Refactor/virtual machine now owns resolver +  repl and program have own virtual machine
`VirtualMachine` no longer depends on a lifetime parameter now and it owns its `import_resolver`field.
Moreover the `cache` field in both `ReplImpl` and `Program` have been replaced by an instance of `VirtualMachine` holding the cache. 
Now instead of creating a new `VirtualMachine` each time, the `vm` from a `ReplImpl` or a `Program` gets its `stack` and `call_stack` cleared and reused.


Patch:
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index d4c7dc1d..3e00a0d5 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -133,7 +133,7 @@ impl Default for EvalMode {
 }
 
 // The current state of the Nickel virtual machine.
-pub struct VirtualMachine<'a, R: ImportResolver> {
+pub struct VirtualMachine<R: ImportResolver> {
     // Behavior of evaluation with respect to metavalues.
     eval_mode: EvalMode,
     // The main stack, storing arguments, thunks and pending computations.
@@ -141,11 +141,11 @@ pub struct VirtualMachine<'a, R: ImportResolver> {
     // The call stack, for error reporting.
     call_stack: CallStack,
     // The interface used to fetch imports.
-    import_resolver: &'a mut R,
+    import_resolver: R,
 }
 
-impl<'a, R: ImportResolver> VirtualMachine<'a, R> {
-    pub fn new(import_resolver: &'a mut R) -> Self {
+impl<R: ImportResolver> VirtualMachine<R> {
+    pub fn new(import_resolver: R) -> Self {
         VirtualMachine {
             import_resolver,
             eval_mode: Default::default(),
@@ -169,6 +169,14 @@ impl<'a, R: ImportResolver> VirtualMachine<'a, R> {
         self.eval_mode = new_mode;
     }
 
+    pub fn import_resolver(&self) -> &R {
+        &self.import_resolver
+    }
+
+    pub fn import_resolver_mut(&mut self) -> &mut R {
+        &mut self.import_resolver
+    }
+
     /// Evaluate a Nickel term. Wrapper around [eval_closure] that starts from an empty local
     /// environment and drops the final environment.
     pub fn eval(&mut self, t0: RichTerm, initial_env: &Environment) -> Result<RichTerm, EvalError> {
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index 0eb45c98..0fdeba9e 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -85,7 +85,7 @@ impl std::fmt::Debug for OperationCont {
     }
 }
 
-impl<'a, R: ImportResolver> VirtualMachine<'a, R> {
+impl<R: ImportResolver> VirtualMachine<R> {
     /// Process to the next step of the evaluation of an operation.
     ///
     /// Depending on the content of the stack, it either starts the evaluation of the first argument,
@@ -2726,8 +2726,7 @@ mod tests {
     #[test]
     fn ite_operation() {
         let cont = OperationCont::Op1(UnaryOp::Ite(), TermPos::None);
-        let mut resolver = DummyResolver {};
-        let mut vm = VirtualMachine::new(&mut resolver);
+        let mut vm = VirtualMachine::new(DummyResolver {});
 
         vm.stack.push_arg(
             Closure::atomic_closure(Term::Num(5.0).into()),
@@ -2772,8 +2771,7 @@ mod tests {
             body: Term::Num(7.0).into(),
             env: Environment::new(),
         };
-        let mut resolver = DummyResolver {};
-        let mut vm = VirtualMachine::new(&mut resolver);
+        let mut vm = VirtualMachine::new(DummyResolver {});
         vm.stack.push_op_cont(cont, 0, TermPos::None);
 
         clos = vm.continuate_operation(clos).unwrap();
@@ -2817,8 +2815,7 @@ mod tests {
             TermPos::None,
         );
 
-        let mut resolver = DummyResolver {};
-        let mut vm = VirtualMachine::new(&mut resolver);
+        let mut vm = VirtualMachine::new(DummyResolver {});
         let mut clos = Closure {
             body: Term::Num(6.0).into(),
             env: Environment::new(),
diff --git a/src/program.rs b/src/program.rs
index f09f6629..123e48bf 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -22,6 +22,7 @@
 //! Each such value is added to the initial environment before the evaluation of the program.
 use crate::cache::*;
 use crate::error::{Error, ToDiagnostic};
+use crate::eval::VirtualMachine;
 use crate::identifier::Ident;
 use crate::parser::lexer::Lexer;
 use crate::term::{RichTerm, Term};
@@ -39,8 +40,8 @@ use std::result::Result;
 pub struct Program {
     /// The id of the program source in the file database.
     main_id: FileId,
-    /// The cache holding the sources and parsed terms of the main source as well as imports.
-    cache: Cache,
+    /// The state of the Nickel virtual machine.
+    vm: VirtualMachine<Cache>,
 }
 
 impl Program {
@@ -52,8 +53,9 @@ impl Program {
     pub fn new_from_file(path: impl Into<OsString>) -> std::io::Result<Program> {
         let mut cache = Cache::new(ErrorTolerance::Strict);
         let main_id = cache.add_file(path)?;
+        let vm = VirtualMachine::new(cache);
 
-        Ok(Program { main_id, cache })
+        Ok(Program { main_id, vm })
     }
 
     /// Create a program by reading it from a generic source.
@@ -64,8 +66,9 @@ impl Program {
     {
         let mut cache = Cache::new(ErrorTolerance::Strict);
         let main_id = cache.add_source(source_name, source)?;
+        let vm = VirtualMachine::new(cache);
 
-        Ok(Program { main_id, cache })
+        Ok(Program { main_id, vm })
     }
 
     /// Retrieve the parsed term and typecheck it, and generate a fresh initial environment. Return
@@ -74,49 +77,56 @@ impl Program {
         let Envs {
             eval_env,
             type_ctxt,
-        } = self.cache.prepare_stdlib()?;
-        self.cache.prepare(self.main_id, &type_ctxt)?;
-        Ok((self.cache.get(self.main_id).unwrap(), eval_env))
+        } = self.vm.import_resolver_mut().prepare_stdlib()?;
+        self.vm
+            .import_resolver_mut()
+            .prepare(self.main_id, &type_ctxt)?;
+        Ok((
+            self.vm.import_resolver().get(self.main_id).unwrap(),
+            eval_env,
+        ))
     }
 
     /// Parse if necessary, typecheck and then evaluate the program.
     pub fn eval(&mut self) -> Result<RichTerm, Error> {
         let (t, initial_env) = self.prepare_eval()?;
-        let mut vm = eval::VirtualMachine::new(&mut self.cache);
-        vm.eval(t, &initial_env).map_err(|e| e.into())
+        self.vm.reset();
+        self.vm.eval(t, &initial_env).map_err(|e| e.into())
     }
 
     /// Same as `eval`, but proceeds to a full evaluation.
     pub fn eval_full(&mut self) -> Result<RichTerm, Error> {
         let (t, initial_env) = self.prepare_eval()?;
-        let mut vm = eval::VirtualMachine::new(&mut self.cache);
-        vm.eval_full(t, &initial_env).map_err(|e| e.into())
+        self.vm.reset();
+        self.vm.eval_full(t, &initial_env).map_err(|e| e.into())
     }
 
     /// Same as `eval_full`, but does not substitute all variables.
     pub fn eval_deep(&mut self) -> Result<RichTerm, Error> {
         let (t, initial_env) = self.prepare_eval()?;
-        let mut vm = eval::VirtualMachine::new(&mut self.cache);
-        vm.eval_deep(t, &initial_env).map_err(|e| e.into())
+        self.vm.reset();
+        self.vm.eval_deep(t, &initial_env).map_err(|e| e.into())
     }
 
     /// Wrapper for [`query`].
     pub fn query(&mut self, path: Option<String>) -> Result<Term, Error> {
-        let initial_env = self.cache.prepare_stdlib()?;
-        query(&mut self.cache, self.main_id, &initial_env, path)
+        let initial_env = self.vm.import_resolver_mut().prepare_stdlib()?;
+        query(&mut self.vm, self.main_id, &initial_env, path)
     }
 
     /// Load, parse, and typecheck the program and the standard library, if not already done.
     pub fn typecheck(&mut self) -> Result<(), Error> {
-        self.cache.parse(self.main_id)?;
-        self.cache.load_stdlib()?;
-        let initial_env = self.cache.mk_type_ctxt().expect("program::typecheck(): stdlib has been loaded but was not found in cache on mk_types_env()");
-        self.cache
+        self.vm.import_resolver_mut().parse(self.main_id)?;
+        self.vm.import_resolver_mut().load_stdlib()?;
+        let initial_env = self.vm.import_resolver().mk_type_ctxt().expect("program::typecheck(): stdlib has been loaded but was not found in cache on mk_types_env()");
+        self.vm
+            .import_resolver_mut()
             .resolve_imports(self.main_id)
             .map_err(|cache_err| {
                 cache_err.unwrap_error("program::typecheck(): expected source to be parsed")
             })?;
-        self.cache
+        self.vm
+            .import_resolver_mut()
             .typecheck(self.main_id, &initial_env)
             .map_err(|cache_err| {
                 cache_err.unwrap_error("program::typecheck(): expected source to be parsed")
@@ -129,18 +139,18 @@ impl Program {
     where
         E: ToDiagnostic<FileId>,
     {
-        report(&mut self.cache, error)
+        report(self.vm.import_resolver_mut(), error)
     }
 
     /// Create a markdown file with documentation for the specified program in `.nickel/doc/program_main_file_name.md`
     #[cfg(feature = "doc")]
     pub fn output_doc(&mut self, out: &mut dyn std::io::Write) -> Result<(), Error> {
-        doc::output_doc(&mut self.cache, self.main_id, out)
+        doc::output_doc(self.vm.import_resolver_mut(), self.main_id, out)
     }
 
     #[cfg(debug_assertions)]
     pub fn set_skip_stdlib(&mut self) {
-        self.cache.skip_stdlib = true;
+        self.vm.import_resolver_mut().skip_stdlib = true;
     }
 
     pub fn pprint_ast(
@@ -151,10 +161,10 @@ impl Program {
         use crate::pretty::*;
         use pretty::BoxAllocator;
 
-        let Program { ref main_id, cache } = self;
+        let Program { ref main_id, vm } = self;
         let allocator = BoxAllocator;
 
-        let rt = cache.parse_nocache(*main_id)?.0;
+        let rt = vm.import_resolver().parse_nocache(*main_id)?.0;
         let rt = if apply_transforms {
             crate::transform::transform(rt, None).unwrap()
         } else {
@@ -182,18 +192,19 @@ impl Program {
 //TODO: not sure where this should go. It seems to embed too much logic to be in `Cache`, but is
 //common to both `Program` and `Repl`. Leaving it here as a stand-alone function for now
 pub fn query(
-    cache: &mut Cache,
+    vm: &mut VirtualMachine<Cache>,
     file_id: FileId,
     initial_env: &Envs,
     path: Option<String>,
 ) -> Result<Term, Error> {
-    cache.prepare(file_id, &initial_env.type_ctxt)?;
+    vm.import_resolver_mut()
+        .prepare(file_id, &initial_env.type_ctxt)?;
     let t = if let Some(p) = path {
         // Parsing `y.path`. We `seq` it to force the evaluation of the underlying value,
         // which can be then showed to the user. The newline gives better messages in case of
         // errors.
         let source = format!("x.{}", p);
-        let query_file_id = cache.add_tmp("<query>", source.clone());
+        let query_file_id = vm.import_resolver_mut().add_tmp("<query>", source.clone());
         let new_term =
             parser::grammar::TermParser::new().parse_term(query_file_id, Lexer::new(&source))?;
 
@@ -202,15 +213,15 @@ pub fn query(
         eval::env_add(
             &mut env,
             Ident::from("x"),
-            cache.get_owned(file_id).unwrap(),
+            vm.import_resolver().get_owned(file_id).unwrap(),
             eval::Environment::new(),
         );
         eval::subst(new_term, &eval::Environment::new(), &env)
     } else {
-        cache.get_owned(file_id).unwrap()
+        vm.import_resolver().get_owned(file_id).unwrap()
     };
 
-    let mut vm = eval::VirtualMachine::new(cache);
+    vm.reset();
     Ok(vm.eval_meta(t, &initial_env.eval_env)?.into())
 }
 
diff --git a/src/repl/mod.rs b/src/repl/mod.rs
index 7f38c911..c48027a9 100644
--- a/src/repl/mod.rs
+++ b/src/repl/mod.rs
@@ -8,6 +8,7 @@
 //! formatting), etc.
 use crate::cache::{Cache, Envs, ErrorTolerance};
 use crate::error::{Error, EvalError, IOError, ParseError, ParseErrors, ReplError};
+use crate::eval::VirtualMachine;
 use crate::identifier::Ident;
 use crate::parser::{grammar, lexer, ExtendedTerm};
 use crate::term::{RichTerm, Term};
@@ -67,8 +68,6 @@ pub trait Repl {
 
 /// Standard implementation of the REPL backend.
 pub struct ReplImpl {
-    /// The underlying cache, storing input, loaded files and parsed terms.
-    cache: Cache,
     /// The parser, supporting toplevel let declaration.
     parser: grammar::ExtendedTermParser,
     /// The current environment (for evaluation and typing). Contain the initial environment with
@@ -77,35 +76,38 @@ pub struct ReplImpl {
     /// The initial typing context, without the toplevel declarations made inside the REPL. Used to
     /// typecheck imports in a fresh environment.
     initial_type_ctxt: typecheck::Context,
+    /// The state of the Nickel virtual machine, holding a cache of loaded files and parsed terms.
+    vm: VirtualMachine<Cache>,
 }
 
 impl ReplImpl {
     /// Create a new empty REPL.
     pub fn new() -> Self {
         ReplImpl {
-            cache: Cache::new(ErrorTolerance::Strict),
             parser: grammar::ExtendedTermParser::new(),
             env: Envs::new(),
             initial_type_ctxt: typecheck::Context::new(),
+            vm: VirtualMachine::new(Cache::new(ErrorTolerance::Strict)),
         }
     }
 
     /// Load and process the stdlib, and use it to populate the eval environment as well as the
     /// typing environment.
     pub fn load_stdlib(&mut self) -> Result<(), Error> {
-        self.env = self.cache.prepare_stdlib()?;
+        self.env = self.vm.import_resolver_mut().prepare_stdlib()?;
         self.initial_type_ctxt = self.env.type_ctxt.clone();
         Ok(())
     }
 
     fn eval_(&mut self, exp: &str, eval_full: bool) -> Result<EvalResult, Error> {
+        self.vm.reset();
         let eval_function = if eval_full {
             eval::VirtualMachine::eval_full
         } else {
             eval::VirtualMachine::eval
         };
 
-        let file_id = self.cache.add_string(
+        let file_id = self.vm.import_resolver_mut().add_string(
             format!("repl-input-{}", InputNameCounter::next()),
             String::from(exp),
         );
@@ -129,13 +131,21 @@ impl ReplImpl {
             id: Option<Ident>,
             t: RichTerm,
         ) -> Result<RichTerm, Error> {
-            let (t, pending) = import_resolution::resolve_imports(t, &mut repl_impl.cache)?;
+            let (t, pending) =
+                import_resolution::resolve_imports(t, repl_impl.vm.import_resolver_mut())?;
             for id in &pending {
-                repl_impl.cache.resolve_imports(*id).unwrap();
+                repl_impl
+                    .vm
+                    .import_resolver_mut()
+                    .resolve_imports(*id)
+                    .unwrap();
             }
 
-            let wildcards =
-                typecheck::type_check(&t, repl_impl.env.type_ctxt.clone(), &repl_impl.cache)?;
+            let wildcards = typecheck::type_check(
+                &t,
+                repl_impl.env.type_ctxt.clone(),
+                repl_impl.vm.import_resolver(),
+            )?;
 
             if let Some(id) = id {
                 typecheck::env_add(
@@ -143,7 +153,7 @@ impl ReplImpl {
                     id.clone(),
                     &t,
                     &repl_impl.env.type_ctxt.term_env,
-                    &repl_impl.cache,
+                    repl_impl.vm.import_resolver(),
                 );
                 repl_impl
                     .env
@@ -155,7 +165,8 @@ impl ReplImpl {
 
             for id in &pending {
                 repl_impl
-                    .cache
+                    .vm
+                    .import_resolver_mut()
                     .typecheck(*id, &repl_impl.initial_type_ctxt)
                     .map_err(|cache_err| {
                         cache_err.unwrap_error("repl::eval_(): expected imports to be parsed")
@@ -166,7 +177,8 @@ impl ReplImpl {
                 .map_err(|err| Error::ParseErrors(err.into()))?;
             for id in &pending {
                 repl_impl
-                    .cache
+                    .vm
+                    .import_resolver_mut()
                     .transform(*id)
                     .unwrap_or_else(|_| panic!("repl::eval_(): expected imports to be parsed"));
             }
@@ -177,12 +189,7 @@ impl ReplImpl {
         match term {
             ExtendedTerm::RichTerm(t) => {
                 let t = prepare(self, None, t)?;
-                Ok(eval_function(
-                    &mut eval::VirtualMachine::new(&mut self.cache),
-                    t,
-                    &self.env.eval_env,
-                )?
-                .into())
+                Ok(eval_function(&mut self.vm, t, &self.env.eval_env)?.into())
             }
             ExtendedTerm::ToplevelLet(id, t) => {
                 let t = prepare(self, Some(id.clone()), t)?;
@@ -205,11 +212,12 @@ impl Repl for ReplImpl {
 
     fn load(&mut self, path: impl AsRef<OsStr>) -> Result<RichTerm, Error> {
         let file_id = self
-            .cache
+            .vm
+            .import_resolver_mut()
             .add_file(OsString::from(path.as_ref()))
             .map_err(IOError::from)?;
-        self.cache.parse(file_id)?;
-        let RichTerm { term, pos } = self.cache.get_ref(file_id).unwrap();
+        self.vm.import_resolver_mut().parse(file_id)?;
+        let RichTerm { term, pos } = self.vm.import_resolver().get_ref(file_id).unwrap();
 
         // Check that the entry is a record, which is a precondition of transform_inner
         match term.as_ref() {
@@ -221,20 +229,24 @@ impl Repl for ReplImpl {
                 )))
             }
         };
-        self.cache.transform_inner(file_id).map_err(|err| {
-            err.unwrap_error("load(): expected term to be parsed before transformation")
-        })?;
-
-        let term = self.cache.get_owned(file_id).unwrap();
-        let (term, pending) = import_resolution::resolve_imports(term, &mut self.cache)?;
+        self.vm
+            .import_resolver_mut()
+            .transform_inner(file_id)
+            .map_err(|err| {
+                err.unwrap_error("load(): expected term to be parsed before transformation")
+            })?;
+
+        let term = self.vm.import_resolver().get_owned(file_id).unwrap();
+        let (term, pending) =
+            import_resolution::resolve_imports(term, self.vm.import_resolver_mut())?;
         for id in &pending {
-            self.cache.resolve_imports(*id).unwrap();
+            self.vm.import_resolver_mut().resolve_imports(*id).unwrap();
         }
         typecheck::env_add_term(
             &mut self.env.type_ctxt.type_env,
             &term,
             &self.env.type_ctxt.term_env,
-            &self.cache,
+            self.vm.import_resolver(),
         )
         .unwrap();
         eval::env_add_term(&mut self.env.eval_env, term.clone()).unwrap();
@@ -243,14 +255,19 @@ impl Repl for ReplImpl {
     }
 
     fn typecheck(&mut self, exp: &str) -> Result<Types, Error> {
-        let file_id = self.cache.add_tmp("<repl-typecheck>", String::from(exp));
+        let file_id = self
+            .vm
+            .import_resolver_mut()
+            .add_tmp("<repl-typecheck>", String::from(exp));
         // We ignore non fatal errors while type checking.
-        let (term, _) = self.cache.parse_nocache(file_id)?;
-        let (term, pending) = import_resolution::resolve_imports(term, &mut self.cache)?;
+        let (term, _) = self.vm.import_resolver().parse_nocache(file_id)?;
+        let (term, pending) =
+            import_resolution::resolve_imports(term, self.vm.import_resolver_mut())?;
         for id in &pending {
-            self.cache.resolve_imports(*id).unwrap();
+            self.vm.import_resolver_mut().resolve_imports(*id).unwrap();
         }
-        let wildcards = typecheck::type_check(&term, self.env.type_ctxt.clone(), &self.cache)?;
+        let wildcards =
+            typecheck::type_check(&term, self.env.type_ctxt.clone(), self.vm.import_resolver())?;
         // Substitute the wildcard types for their inferred types
         // We need to `traverse` the term, in case the type depends on inner terms that also contain wildcards
         let term = term
@@ -268,7 +285,7 @@ impl Repl for ReplImpl {
         Ok(typecheck::apparent_type(
             term.as_ref(),
             Some(&self.env.type_ctxt.type_env),
-            Some(&self.cache),
+            Some(self.vm.import_resolver()),
         )
         .into())
     }
@@ -276,12 +293,15 @@ impl Repl for ReplImpl {
     fn query(&mut self, exp: &str) -> Result<Term, Error> {
         use crate::program;
 
-        let file_id = self.cache.add_tmp("<repl-query>", String::from(exp));
-        program::query(&mut self.cache, file_id, &self.env, None)
+        let file_id = self
+            .vm
+            .import_resolver_mut()
+            .add_tmp("<repl-query>", String::from(exp));
+        program::query(&mut self.vm, file_id, &self.env, None)
     }
 
     fn cache_mut(&mut self) -> &mut Cache {
-        &mut self.cache
+        self.vm.import_resolver_mut()
     }
 }
 
diff --git a/src/serialize.rs b/src/serialize.rs
index 4fa5365f..c744091c 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -333,7 +333,7 @@ mod tests {
                     .unwrap();
 
             assert_eq!(
-                VirtualMachine::new(&mut DummyResolver {})
+                VirtualMachine::new(DummyResolver {})
                     .eval(
                         mk_term::op2(BinaryOp::Eq(), from_json, evaluated.clone()),
                         &Environment::new(),
@@ -342,7 +342,7 @@ mod tests {
                 Ok(Term::Bool(true))
             );
             assert_eq!(
-                VirtualMachine::new(&mut DummyResolver {})
+                VirtualMachine::new(DummyResolver {})
                     .eval(
                         mk_term::op2(BinaryOp::Eq(), from_yaml, evaluated.clone()),
                         &Environment::new(),
@@ -351,7 +351,7 @@ mod tests {
                 Ok(Term::Bool(true))
             );
             assert_eq!(
-                VirtualMachine::new(&mut DummyResolver {})
+                VirtualMachine::new(DummyResolver {})
                     .eval(
                         mk_term::op2(BinaryOp::Eq(), from_toml, evaluated),
                         &Environment::new(),
diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs
index 813c55f9..db73824b 100644
--- a/utilities/src/lib.rs
+++ b/utilities/src/lib.rs
@@ -148,9 +148,7 @@ pub fn bench_terms<'r>(rts: Vec<Bench<'r>>) -> Box<dyn Fn(&mut Criterion) + 'r>
                             c_local.typecheck(id, &type_ctxt).unwrap();
                         } else {
                             c_local.prepare(id, &type_ctxt).unwrap();
-                            VirtualMachine::new(&mut c_local)
-                                .eval(t, &eval_env)
-                                .unwrap();
+                            VirtualMachine::new(c_local).eval(t, &eval_env).unwrap();
                         }
                     },
                     criterion::BatchSize::LargeInput,
@@ -222,7 +220,7 @@ macro_rules! ncl_bench_group {
                                 c_local.typecheck(id, &type_ctxt).unwrap();
                             } else {
                                 c_local.prepare(id, &type_ctxt).unwrap();
-                                VirtualMachine::new(&mut c_local).eval(t, &eval_env).unwrap();
+                                VirtualMachine::new(c_local).eval(t, &eval_env).unwrap();
                             }
                         },
                         criterion::BatchSize::LargeInput,


Test patch:
diff --git a/src/eval/tests.rs b/src/eval/tests.rs
index ae224ee4..e50dee13 100644
--- a/src/eval/tests.rs
+++ b/src/eval/tests.rs
@@ -11,7 +11,7 @@ use codespan::Files;
 
 /// Evaluate a term without import support.
 fn eval_no_import(t: RichTerm) -> Result<Term, EvalError> {
-    VirtualMachine::new(&mut DummyResolver {})
+    VirtualMachine::new(DummyResolver {})
         .eval(t, &Environment::new())
         .map(Term::from)
 }
@@ -147,19 +147,22 @@ fn merge_incompatible_defaults() {
 
 #[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(
+    let mut vm = VirtualMachine::new(SimpleResolver::new());
+    vm.import_resolver_mut()
+        .add_source(String::from("two"), String::from("1 + 1"));
+    vm.import_resolver_mut()
+        .add_source(String::from("lib"), String::from("{f = true}"));
+    vm.import_resolver_mut()
+        .add_source(String::from("bad"), String::from("^$*/.23ab 0°@"));
+    vm.import_resolver_mut().add_source(
         String::from("nested"),
         String::from("let x = import \"two\" in x + 1"),
     );
-    resolver.add_source(
+    vm.import_resolver_mut().add_source(
         String::from("cycle"),
         String::from("let x = import \"cycle_b\" in {a = 1, b = x.a}"),
     );
-    resolver.add_source(
+    vm.import_resolver_mut().add_source(
         String::from("cycle_b"),
         String::from("let x = import \"cycle\" in {a = x.a}"),
     );
@@ -168,35 +171,35 @@ fn imports() {
         var: &str,
         import: &str,
         body: RichTerm,
-        resolver: &mut R,
+        vm: &mut VirtualMachine<R>,
     ) -> Result<RichTerm, ImportError>
     where
         R: ImportResolver,
     {
         resolve_imports(
             mk_term::let_in(var, mk_term::import(import), body),
-            resolver,
+            vm.import_resolver_mut(),
         )
         .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() {
+    match mk_import("x", "does_not_exist", mk_term::var("x"), &mut vm).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() {
+    match mk_import("x", "bad", mk_term::var("x"), &mut vm).unwrap_err() {
         ImportError::ParseErrors(_, _) => (),
         _ => assert!(false),
     };
 
     // let x = import "two" in x
-    let mk_import_two = mk_import("x", "two", mk_term::var("x"), &mut resolver).unwrap();
+    let mk_import_two = mk_import("x", "two", mk_term::var("x"), &mut vm).unwrap();
+    vm.reset();
     assert_eq!(
-        VirtualMachine::new(&mut resolver)
-            .eval(mk_import_two, &Environment::new(),)
+        vm.eval(mk_import_two, &Environment::new(),)
             .map(Term::from)
             .unwrap(),
         Term::Num(2.0)
@@ -207,11 +210,11 @@ fn imports() {
         "x",
         "lib",
         mk_term::op1(UnaryOp::StaticAccess(Ident::from("f")), mk_term::var("x")),
-        &mut resolver,
+        &mut vm,
     );
+    vm.reset();
     assert_eq!(
-        VirtualMachine::new(&mut resolver)
-            .eval(mk_import_lib.unwrap(), &Environment::new(),)
+        vm.eval(mk_import_lib.unwrap(), &Environment::new(),)
             .map(Term::from)
             .unwrap(),
         Term::Bool(true)
@@ -284,7 +287,6 @@ fn interpolation_nested() {
 #[test]
 fn initial_env() {
     let mut initial_env = Environment::new();
-    let mut resolver = DummyResolver {};
     initial_env.insert(
         Ident::from("g"),
         Thunk::new(
@@ -295,7 +297,7 @@ fn initial_env() {
 
     let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("x"));
     assert_eq!(
-        VirtualMachine::new(&mut resolver)
+        VirtualMachine::new(DummyResolver {})
             .eval(t, &initial_env)
             .map(Term::from),
         Ok(Term::Num(2.0))
@@ -303,7 +305,7 @@ fn initial_env() {
 
     let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("g"));
     assert_eq!(
-        VirtualMachine::new(&mut resolver)
+        VirtualMachine::new(DummyResolver {})
             .eval(t, &initial_env)
             .map(Term::from),
         Ok(Term::Num(1.0))
@@ -312,7 +314,7 @@ fn initial_env() {
     // Shadowing of the initial environment
     let t = mk_term::let_in("g", Term::Num(2.0), mk_term::var("g"));
     assert_eq!(
-        VirtualMachine::new(&mut resolver)
+        VirtualMachine::new(DummyResolver {})
             .eval(t, &initial_env)
             .map(Term::from),
         Ok(Term::Num(2.0))