Repository: tweag/nickel
Pull request #1038 (2023-01-11 16:36:06)

DumbCache experiments: Fixed `Thunk` leakage


Patch:
diff --git a/flake.lock b/flake.lock
index d6522b41..30d00d79 100644
--- a/flake.lock
+++ b/flake.lock
@@ -10,11 +10,11 @@
         "rust-overlay": "rust-overlay"
       },
       "locked": {
-        "lastModified": 1673056065,
-        "narHash": "sha256-a68tMDTDqdAauxq377ALl4Uwm6oh9MeoY2WbTYRWZoo=",
+        "lastModified": 1673405853,
+        "narHash": "sha256-6Nq9DuOo+gE2I8z5UZaKuumykz2xxZ9JGYmUthOuwSA=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "0144134311767fcee80213321f079a8ffa0b9cc1",
+        "rev": "b13963c8c18026aa694acd98d14f66d24666f70b",
         "type": "github"
       },
       "original": {
@@ -108,11 +108,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1672953546,
-        "narHash": "sha256-oz757DnJ1ITvwyTovuwG3l9cX6j9j6/DH9eH+cXFJmc=",
+        "lastModified": 1673540789,
+        "narHash": "sha256-xqnxBOK3qctIeUVxecydrEDbEXjsvHCPGPbvsl63M/U=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "a518c77148585023ff56022f09c4b2c418a51ef5",
+        "rev": "0f213d0fee84280d8c3a97f7469b988d6fe5fcdf",
         "type": "github"
       },
       "original": {
@@ -150,11 +150,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1672912243,
-        "narHash": "sha256-QnQeKUjco2kO9J4rBqIBPp5XcOMblIMnmyhpjeaJBYc=",
+        "lastModified": 1673627351,
+        "narHash": "sha256-oppRxEg/7ICcG67ErBvu1UlXt3su6zMcNoQmKaHPs5I=",
         "owner": "cachix",
         "repo": "pre-commit-hooks.nix",
-        "rev": "a4548c09eac4afb592ab2614f4a150120b29584c",
+        "rev": "496e4505c2ddf5f205242eae8064d7d89cd976c0",
         "type": "github"
       },
       "original": {
@@ -207,11 +207,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1673058265,
-        "narHash": "sha256-FFigGHIO9BQeIIKjH5dcpB+ey5CSgfy47wHPGeOhCps=",
+        "lastModified": 1673662873,
+        "narHash": "sha256-/YOtiDKPUXKKpIhsAds11llfC42ScGW27bbHnNZebco=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "802ff3314663ec7114f29a6e8b200dfc892023f8",
+        "rev": "90163bbbadce526f8b248a5fe545b06c59597108",
         "type": "github"
       },
       "original": {
diff --git a/lsp/nls/src/linearization/building.rs b/lsp/nls/src/linearization/building.rs
index 9652fa74..b6ec6761 100644
--- a/lsp/nls/src/linearization/building.rs
+++ b/lsp/nls/src/linearization/building.rs
@@ -32,10 +32,42 @@ impl<'b> Building<'b> {
         self.linearization.push(item)
     }
 
-    fn get_item_kind_mut(&mut self, file: FileId, id: ItemId) -> Option<&mut TermKind> {
-        if file == id.file_id {
+    fn get_item(&self, id: ItemId) -> Option<&LinearizationItem<Unresolved>> {
+        self.linearization.get(id.index)
+    }
+
+    fn get_item_mut(&mut self, id: ItemId) -> Option<&mut LinearizationItem<Unresolved>> {
+        self.linearization.get_mut(id.index)
+    }
+
+    fn get_item_kind_with_id(
+        &self,
+        current_file: FileId,
+        id: ItemId,
+    ) -> Option<(&ItemId, &TermKind)> {
+        if current_file == id.file_id {
             // This usage references an item in the file we're currently linearizing
-            let item = self.linearization.get_mut(id.index)?;
+            let item = self.get_item(id)?;
+            Some((&item.id, &item.kind))
+        } else {
+            // This usage references an item in another file (that has already been linearized)
+            let item = self
+                .lin_cache
+                .get(&id.file_id)?
+                .get_item(id, self.lin_cache)?;
+            Some((&item.id, &item.kind))
+        }
+    }
+
+    fn get_item_kind(&self, current_file: FileId, id: ItemId) -> Option<&TermKind> {
+        let (_, kind) = self.get_item_kind_with_id(current_file, id)?;
+        Some(kind)
+    }
+
+    fn get_item_kind_mut(&mut self, current_file: FileId, id: ItemId) -> Option<&mut TermKind> {
+        if current_file == id.file_id {
+            // This usage references an item in the file we're currently linearizing
+            let item = self.get_item_mut(id)?;
             Some(&mut item.kind)
         } else {
             // This usage references an item in another file (that has already been linearized)
@@ -44,9 +76,9 @@ impl<'b> Building<'b> {
         }
     }
 
-    pub(super) fn add_usage(&mut self, file: FileId, decl: ItemId, usage: ItemId) {
+    pub(super) fn add_usage(&mut self, current_file: FileId, decl: ItemId, usage: ItemId) {
         match self
-            .get_item_kind_mut(file, decl)
+            .get_item_kind_mut(current_file, decl)
             .expect("Could not find parent")
         {
             TermKind::Record(_) | TermKind::Structure | TermKind::Usage(_) => unreachable!(),
@@ -55,8 +87,13 @@ impl<'b> Building<'b> {
         };
     }
 
-    pub(super) fn inform_declaration(&mut self, file: FileId, declaration: ItemId, value: ItemId) {
-        let kind = self.get_item_kind_mut(file, declaration);
+    pub(super) fn inform_declaration(
+        &mut self,
+        current_file: FileId,
+        declaration: ItemId,
+        value: ItemId,
+    ) {
+        let kind = self.get_item_kind_mut(current_file, declaration);
         if let Some(TermKind::Declaration(_, _, value_state)) = kind {
             *value_state = ValueState::Known(value)
         }
@@ -64,14 +101,14 @@ impl<'b> Building<'b> {
 
     pub(super) fn register_fields(
         &mut self,
+        current_file: FileId,
         record_fields: &HashMap<Ident, RichTerm>,
         record: ItemId,
         env: &mut Environment,
-        file: FileId,
     ) {
         for (ident, value) in record_fields.iter() {
             let id = ItemId {
-                file_id: file,
+                file_id: current_file,
                 index: self.id_gen().get_and_advance(),
             };
             self.push(LinearizationItem {
@@ -96,20 +133,19 @@ impl<'b> Building<'b> {
             });
             let key = *ident;
             env.insert(key, id);
-            self.add_record_field(record, (*ident, id))
+            self.add_record_field(current_file, record, (*ident, id))
         }
     }
 
     pub(super) fn add_record_field(
         &mut self,
+        current_file: FileId,
         record: ItemId,
         (field_ident, reference_id): (Ident, ItemId),
     ) {
         match self
-            .linearization
-            .get_mut(record.index)
+            .get_item_kind_mut(current_file, record)
             .expect("Could not find record")
-            .kind
         {
             TermKind::Record(ref mut fields) => {
                 fields.insert(field_ident, reference_id);
@@ -120,40 +156,39 @@ impl<'b> Building<'b> {
 
     pub(super) fn resolve_reference<'a>(
         &'a self,
-        item: &'a LinearizationItem<UnifType>,
-    ) -> Option<&'a LinearizationItem<UnifType>> {
-        // if item is a usage, resolve the usage first
-        match item.kind {
-            TermKind::Usage(UsageState::Resolved(pointed)) => self.linearization.get(pointed.index),
-            _ => Some(item),
-        }
-        // load referenced value, either from record field or declaration
-        .and_then(|item_pointer| {
-            match &item_pointer.kind {
-                // if declaration is a record field, resolve its value
-                TermKind::RecordField { value, .. } => {
-                    debug!("parent referenced a record field {:?}", value);
-                    value
-                        // retrieve record
-                        .as_option()
-                        .and_then(|value_index| self.linearization.get(value_index.index))
-                }
-                // if declaration is a let binding resolve its value
-                TermKind::Declaration(_, _, ValueState::Known(value)) => {
-                    self.linearization.get(value.index)
-                }
+        current_file: FileId,
+        item: &'a TermKind,
+    ) -> Option<&'a TermKind> {
+        match item {
+            // if declaration is a record field, resolve its value
+            TermKind::RecordField { value, .. } => {
+                debug!("parent referenced a record field {:?}", value);
+                value
+                    // retrieve record
+                    .as_option()
+                    .and_then(|value_index| self.get_item_kind(current_file, value_index))
+            }
+            // if declaration is a let binding resolve its value
+            TermKind::Declaration(_, _, ValueState::Known(value)) => {
+                self.get_item_kind(current_file, *value)
+            }
 
-                // if something else was referenced, stop.
-                _ => Some(item_pointer),
+            TermKind::Usage(UsageState::Resolved(pointed)) => {
+                let kind = self.get_item_kind(current_file, *pointed)?;
+                self.resolve_reference(current_file, kind)
             }
-        })
+            // if something else was referenced, stop.
+            _ => Some(item),
+        }
     }
 
+    /// [`resolve_record_references`] tries to resolve the references passed to it, and
+    /// returns the ids of the items it couldn't resolve.
     pub(super) fn resolve_record_references(
         &mut self,
-        file: FileId,
+        current_file: FileId,
         mut defers: Vec<(ItemId, ItemId, Ident)>,
-    ) {
+    ) -> Vec<(ItemId, ItemId, Ident)> {
         let mut unresolved: Vec<(ItemId, ItemId, Ident)> = Vec::new();
 
         while let Some(deferred) = defers.pop() {
@@ -165,27 +200,19 @@ impl<'b> Building<'b> {
             let (child_item, parent_accessor_id, child_ident) = &deferred;
             // resolve the value referenced by the parent accessor element
             // get the parent accessor, and read its resolved reference
-            let parent_referenced = self.linearization.get(parent_accessor_id.index);
-
-            if let Some(LinearizationItem {
-                kind: TermKind::Usage(UsageState::Deferred { .. }),
-                ..
-            }) = parent_referenced
-            {
+            let parent_referenced = self.get_item_kind(current_file, *parent_accessor_id);
+            if let Some(TermKind::Usage(UsageState::Deferred { .. })) = parent_referenced {
                 debug!("parent references deferred usage");
                 unresolved.push(deferred);
                 continue;
             }
 
             // load the parent referenced declaration (i.e.: a declaration or record field term)
-            let parent_declaration = parent_referenced
-                .and_then(|parent_usage_value| self.resolve_reference(parent_usage_value));
+            let parent_declaration = parent_referenced.and_then(|parent_usage_value| {
+                self.resolve_reference(current_file, parent_usage_value)
+            });
 
-            if let Some(LinearizationItem {
-                kind: TermKind::Usage(UsageState::Deferred { .. }),
-                ..
-            }) = parent_declaration
-            {
+            if let Some(TermKind::Usage(UsageState::Deferred { .. })) = parent_declaration {
                 debug!("parent references deferred usage");
                 unresolved.push(deferred);
                 continue;
@@ -193,48 +220,50 @@ impl<'b> Building<'b> {
 
             let referenced_declaration = parent_declaration
                 // resolve indirection by following the usage
-                .and_then(|parent_declaration| self.resolve_reference(parent_declaration))
+                .and_then(|parent_declaration| {
+                    self.resolve_reference(current_file, parent_declaration)
+                })
                 // get record field
-                .and_then(|parent_declaration| match &parent_declaration.kind {
+                .and_then(|parent_declaration| match &parent_declaration {
                     TermKind::Record(fields) => {
                         fields.get(child_ident).and_then(|child_declaration_id| {
-                            self.linearization.get(child_declaration_id.index)
+                            self.get_item_kind_with_id(current_file, *child_declaration_id)
                         })
                     }
                     _ => None,
                 });
 
             let referenced_declaration =
-                referenced_declaration.and_then(|referenced| match &referenced.kind {
+                referenced_declaration.and_then(|(id, referenced)| match &referenced {
                     TermKind::Usage(UsageState::Resolved(pointed)) => {
-                        self.linearization.get(pointed.index)
+                        self.get_item_kind_with_id(current_file, *pointed)
                     }
                     TermKind::RecordField { value, .. } => value
                         // retrieve record
                         .as_option()
-                        .and_then(|value_index| self.linearization.get(value_index.index))
+                        .and_then(|value_index| self.get_item_kind(current_file, value_index))
                         // retrieve field
-                        .and_then(|record| match &record.kind {
+                        .and_then(|record| match &record {
                             TermKind::Record(fields) => {
                                 debug!(
                                     "parent referenced a nested record indirectly`: {:?}",
                                     fields
                                 );
                                 fields.get(child_ident).and_then(|accessor_id| {
-                                    self.linearization.get(accessor_id.index)
+                                    self.get_item_kind_with_id(current_file, *accessor_id)
                                 })
                             }
                             TermKind::Usage(UsageState::Resolved(pointed)) => {
-                                self.linearization.get(pointed.index)
+                                self.get_item_kind_with_id(current_file, *pointed)
                             }
                             _ => None,
                         })
-                        .or(Some(referenced)),
+                        .or(Some((id, referenced))),
 
-                    _ => Some(referenced),
+                    _ => Some((id, referenced)),
                 });
 
-            let referenced_id = referenced_declaration.map(|reference| reference.id);
+            let referenced_id = referenced_declaration.map(|(id, _)| *id);
 
             debug!(
                 "Associating child {} to value {:?}",
@@ -242,19 +271,20 @@ impl<'b> Building<'b> {
             );
 
             {
-                let child: &mut LinearizationItem<UnifType> =
-                    self.linearization.get_mut(child_item.index).unwrap();
-                child.kind = TermKind::Usage(UsageState::from(referenced_id));
+                let child = self.get_item_kind_mut(current_file, *child_item).unwrap();
+                *child = TermKind::Usage(UsageState::from(referenced_id));
             }
 
             if let Some(referenced_id) = referenced_id {
-                self.add_usage(file, referenced_id, *child_item);
+                self.add_usage(current_file, referenced_id, *child_item);
             }
+        }
 
-            if defers.is_empty() && !unresolved.is_empty() {
-                debug!("unresolved references: {:?}", unresolved);
-                defers = mem::take(&mut unresolved);
-            }
+        if defers.is_empty() && !unresolved.is_empty() {
+            defers = mem::take(&mut unresolved);
+            defers
+        } else {
+            Vec::new()
         }
     }
 
diff --git a/lsp/nls/src/linearization/mod.rs b/lsp/nls/src/linearization/mod.rs
index 3fcacaf4..dbf2d28b 100644
--- a/lsp/nls/src/linearization/mod.rs
+++ b/lsp/nls/src/linearization/mod.rs
@@ -345,6 +345,7 @@ impl<'a> Linearizer for AnalysisHost<'a> {
                             file_id: self.file,
                             index: id_gen.get_and_advance(),
                         };
+
                         lin.push(LinearizationItem {
                             env: self.env.clone(),
                             id,
@@ -372,7 +373,7 @@ impl<'a> Linearizer for AnalysisHost<'a> {
                     meta: self.meta.take(),
                 });
 
-                lin.register_fields(&record.fields, id, &mut self.env, self.file);
+                lin.register_fields(self.file, &record.fields, id, &mut self.env);
                 let mut field_names = record.fields.keys().cloned().collect::<Vec<_>>();
                 field_names.sort_unstable();
 
@@ -486,7 +487,7 @@ impl<'a> Linearizer for AnalysisHost<'a> {
         debug!("linearizing");
 
         // TODO: Storing defers while linearizing?
-        let defers: Vec<(ItemId, ItemId, Ident)> = lin
+        let mut defers: Vec<(ItemId, ItemId, Ident)> = lin
             .linearization
             .iter()
             .filter_map(|item| match &item.kind {
@@ -497,7 +498,9 @@ impl<'a> Linearizer for AnalysisHost<'a> {
             })
             .collect();
 
-        lin.resolve_record_references(self.file, defers);
+        defers.reverse();
+        let unresolved = lin.resolve_record_references(self.file, defers);
+        debug!("unresolved references: {:?}", unresolved);
 
         let Building {
             mut linearization, ..
diff --git a/lsp/nls/src/requests/completion.rs b/lsp/nls/src/requests/completion.rs
index d3e149a8..09b9b8f2 100644
--- a/lsp/nls/src/requests/completion.rs
+++ b/lsp/nls/src/requests/completion.rs
@@ -267,15 +267,26 @@ fn find_fields_from_term(term: &RichTerm, path: &mut Vec<Ident>) -> Vec<IdentWit
 }
 
 lazy_static! {
-    // unwrap is safe here because we know this is a correct regex.
-    // This regexp must be the reverse of the regexp in the lexer (nickel_lang::parser::lexer)
-    // to correctly parse identifiers.
-    static ref RE: regex::Regex = regex::Regex::new(r"[_a-zA-Z0-9-']*[a-zA-Z]_?").unwrap();
+    // unwraps are safe here because we know these are correct regexes.
+    // This regexp must be the same as the regex for identifiers in the lexer (nickel_lang::parser::lexer)
+    static ref RE_IDENTIFIER: regex::Regex = regex::Regex::new(r"_?[a-zA-Z][_a-zA-Z0-9-']*").unwrap();
     static ref RE_SPACE: regex::Regex = regex::Regex::new(r"\s+").unwrap();
 }
 
 /// Get the string chunks that make up an identifier path.
 fn get_identifier_path(text: &str) -> Option<Vec<String>> {
+    /// Remove quotes from a record fields name (if any) and return a tuple
+    /// of a String (the fields name) with the quotes removed and a bool
+    /// indicating if the fields name actually contained quotes.
+    fn remove_quotes(name: &str) -> (String, bool) {
+        let mut chars = name.chars();
+        if let (Some('"'), Some('"')) = (chars.next(), chars.last()) {
+            (String::from(&name[1..name.len() - 1]), true)
+        } else {
+            (String::from(name), false)
+        }
+    }
+
     // skip any `.` at the end of the text
     let text = {
         let mut text = String::from(text);
@@ -287,16 +298,21 @@ fn get_identifier_path(text: &str) -> Option<Vec<String>> {
 
     let result: Vec<_> = RE_SPACE.split(text.as_ref()).map(String::from).collect();
     let path = result.iter().rev().next().cloned()?;
-    // Remove quotes from a record name
-    fn remove_quotes(name: &str) -> String {
-        let mut chars = name.chars();
-        if let (Some('"'), Some('"')) = (chars.next(), chars.last()) {
-            String::from(&name[1..name.len() - 1])
-        } else {
-            String::from(name)
-        }
-    }
-    Some(path.split('.').map(remove_quotes).collect())
+
+    let path = path
+        .split('.')
+        .map(|str| {
+            let (str, had_quotes) = remove_quotes(str);
+            if had_quotes {
+                // Nickel identifiers cannot contain unicode characters
+                Some(str)
+            } else {
+                RE_IDENTIFIER.find(&str).map(|m| String::from(m.as_str()))
+            }
+        })
+        .collect::<Option<Vec<_>>>()?;
+
+    Some(path)
 }
 
 /// Get the identifiers before `.<text>` for record completion.
@@ -305,7 +321,7 @@ fn get_identifiers_before_field(text: &str) -> Option<Vec<String>> {
     let text: String = text
         .chars()
         .rev()
-        .skip_while(|c| RE.is_match(c.to_string().as_ref()))
+        .skip_while(|c| RE_IDENTIFIER.is_match(c.to_string().as_ref()))
         .collect::<String>()
         .chars()
         .rev()
@@ -531,52 +547,68 @@ mod tests {
             (
                 "Simple let binding with nested record index",
                 "let person = {} in \n    a.b.c.d",
-                vec!["a", "b", "c", "d"],
+                Some(vec!["a", "b", "c", "d"]),
             ),
-            ("Simple record index", "   ab.dec", vec!["ab", "dec"]),
+            ("Simple record index", "   ab.dec", Some(vec!["ab", "dec"])),
             (
                 "Multiple let bindings with nested record index ",
                 r##"let name = { sdf.clue.add.bar = 10 } in
                 let other = name in
                 let another = other in
             name.sdf.clue.add.bar"##,
-                vec!["name", "sdf", "clue", "add", "bar"],
+                Some(vec!["name", "sdf", "clue", "add", "bar"]),
             ),
             (
                 "Incomplete record with nested record index",
                 r##"{
                     foo = let bar = {a.b.c.d = 10} in bar.a.b.c.d"##,
-                vec!["bar", "a", "b", "c", "d"],
+                Some(vec!["bar", "a", "b", "c", "d"]),
+            ),
+            (
+                "Simple record Index",
+                "name.class",
+                Some(vec!["name", "class"]),
             ),
-            ("Simple record Index", "name.class", vec!["name", "class"]),
             (
                 "Simple record Index ending with a dot",
                 "name.class.",
-                vec!["name", "class"],
+                Some(vec!["name", "class"]),
             ),
-            ("Single record variable", "number", vec!["number"]),
+            ("Single record variable", "number", Some(vec!["number"])),
             (
                 "Single record variable ending with a dot",
                 "number.",
-                vec!["number"],
+                Some(vec!["number"]),
             ),
             (
                 "Record binding with unicode string names for fields",
                 r##"let x = {"fo京o" = {bar = 42}} in x."fo京o".foo"##,
-                vec!["x", "fo京o", "foo"],
+                Some(vec!["x", "fo京o", "foo"]),
             ),
             (
                 "Typed record binding with nested record indexing",
                 r##"let me : _ = { name = "foo", time = 1800, a.b.c.d = 10 } in
                     me.ca.cb"##,
-                vec!["me", "ca", "cb"],
+                Some(vec!["me", "ca", "cb"]),
+            ),
+            ("Single quote", "\"", None),
+            (
+                "Nested Parenthesis prefix",
+                "(alpha.beta.",
+                Some(vec!["alpha", "beta"]),
+            ),
+            ("Parenthesis prefix", "(single", Some(vec!["single"])),
+            (
+                "Nested curly brace prefix",
+                "{first.second",
+                Some(vec!["first", "second"]),
             ),
-            ("Single quote", "\"", vec!["\""]),
+            ("Curly brace prefix", "{double.", Some(vec!["double"])),
         ];
         for (case_name, input, expected) in tests {
             let actual = get_identifier_path(input);
             let expected: Option<Vec<_>> =
-                Some(expected.iter().cloned().map(String::from).collect());
+                expected.map(|path| path.iter().map(|s| String::from(*s)).collect());
             assert_eq!(actual, expected, "test failed: {}", case_name)
         }
     }
diff --git a/lsp/nls/src/requests/goto.rs b/lsp/nls/src/requests/goto.rs
index fb266f4c..337b00f9 100644
--- a/lsp/nls/src/requests/goto.rs
+++ b/lsp/nls/src/requests/goto.rs
@@ -44,20 +44,22 @@ pub fn handle_to_definition(
 
     Trace::enrich(&id, linearization);
 
-    let item = linearization.item_at(&locator);
-
-    if item.is_none() {
+    let Some(item) = linearization.item_at(&locator) else {
         server.reply(Response::new_ok(id, Value::Null));
-        return Ok(());
-    }
-
-    let item = item.unwrap();
+        return Ok(())
+    };
 
     debug!("found referencing item: {:?}", item);
 
     let location = match item.kind {
         TermKind::Usage(UsageState::Resolved(usage_id)) => {
             let definition = linearization.get_item(usage_id, &server.lin_cache).unwrap();
+            if server.cache.is_stdlib_module(definition.id.file_id) {
+                // The standard library files are embeded in the executable,
+                // so we can't possibly go to their definition on disk.
+                server.reply(Response::new_ok(id, Value::Null));
+                return Ok(());
+            }
             let RawSpan {
                 start: ByteIndex(start),
                 end: ByteIndex(end),
@@ -93,7 +95,14 @@ pub fn handle_to_usages(
 ) -> Result<(), ResponseError> {
     let file_id = server
         .cache
-        .id_of(params.text_document_position.text_document.uri.as_str())
+        .id_of(
+            params
+                .text_document_position
+                .text_document
+                .uri
+                .to_file_path()
+                .unwrap(),
+        )
         .unwrap();
 
     let start = position_to_byte_index(
@@ -128,7 +137,7 @@ pub fn handle_to_usages(
                         .pos
                         .as_opt_ref()
                         .map(|RawSpan { start, end, src_id }| Location {
-                            uri: Url::parse(&server.cache.name(*src_id).to_string_lossy()).unwrap(),
+                            uri: Url::from_file_path(server.cache.name(*src_id)).unwrap(),
                             range: Range::from_codespan(
                                 src_id,
                                 &((*start).into()..(*end).into()),
diff --git a/src/cache.rs b/src/cache.rs
index f86420a3..ffbd60c3 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -850,6 +850,14 @@ impl Cache {
         self.terms.get(&file_id).map(|CachedTerm { term, .. }| term)
     }
 
+    /// Returns true if a particular file id represents a Nickel standard library file, false otherwise.
+    pub fn is_stdlib_module(&self, file: FileId) -> bool {
+        let Some(table) = &self.stdlib_ids else {
+            return false
+        };
+        table.values().any(|stdlib_file| *stdlib_file == file)
+    }
+
     /// Retrieve the FileId for a given standard libray module.
     pub fn get_submodule_file_id(&self, module: StdlibModule) -> Option<FileId> {
         let file = self.stdlib_ids.as_ref()?.get(&module).copied()?;
diff --git a/src/error.rs b/src/error.rs
index 50b3af5d..629c033a 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -46,7 +46,14 @@ pub enum Error {
 #[derive(Debug, Clone, PartialEq)]
 pub enum EvalError {
     /// A blame occurred: a contract have been broken somewhere.
-    BlameError(label::Label, CallStack),
+    BlameError {
+        /// The argument failing the contract. If the argument has been forced by the contract, `evaluated_arg` provides the final value.
+        evaluated_arg: Option<RichTerm>,
+        /// The label of the corresponding contract.
+        label: label::Label,
+        /// The callstack when the blame error was raised.
+        call_stack: CallStack,
+    },
     /// A field required by a record contract is missing a definition.
     MissingFieldDef(Option<label::Label>, CallStack),
     /// Mismatch between the expected type and the actual type of an expression.
@@ -100,6 +107,7 @@ pub enum EvalError {
     /// A polymorphic record contract was broken somewhere.
     IllegalPolymorphicTailAccess {
         action: IllegalPolymorphicTailAction,
+        evaluated_arg: Option<RichTerm>,
         label: label::Label,
         call_stack: CallStack,
     },
@@ -760,28 +768,37 @@ impl ToDiagnostic<FileId> for EvalError {
         contract_id: Option<FileId>,
     ) -> Vec<Diagnostic<FileId>> {
         match self {
-            EvalError::BlameError(l, call_stack) => {
+            EvalError::BlameError {
+                evaluated_arg,
+                label,
+                call_stack,
+            } => {
                 let mut msg = String::new();
 
                 // Writing in a string should not raise an error, hence the fearless `unwrap()`
-                write!(&mut msg, "{}", blame_error::title(l)).unwrap();
+                write!(&mut msg, "{}", blame_error::title(label)).unwrap();
 
-                if !l.tag.is_empty() {
-                    write!(&mut msg, ": {}", &escape(&l.tag)).unwrap();
+                if !label.tag.is_empty() {
+                    write!(&mut msg, ": {}", &escape(&label.tag)).unwrap();
                 }
 
-                let (path_label, notes) = blame_error::report_ty_path(l, files);
-                let labels =
-                    blame_error::build_diagnostic_labels(l, path_label, files, contract_id);
+                let (path_label, notes) = blame_error::report_ty_path(label, files);
+                let labels = blame_error::build_diagnostic_labels(
+                    evaluated_arg.clone(),
+                    label,
+                    path_label,
+                    files,
+                    contract_id,
+                );
 
                 let mut diagnostics = vec![Diagnostic::error()
                     .with_message(msg)
                     .with_labels(labels)
                     .with_notes(notes)];
 
-                diagnostics.push(blame_error::note(l));
+                diagnostics.push(blame_error::note(label));
 
-                if ty_path::has_no_dom(&l.path) {
+                if ty_path::has_no_dom(&label.path) {
                 } else if let Some(id) = contract_id {
                     diagnostics.extend_with_call_stack(id, call_stack);
                 }
@@ -1037,6 +1054,7 @@ impl ToDiagnostic<FileId> for EvalError {
             EvalError::IllegalPolymorphicTailAccess {
                 action,
                 label: l,
+                evaluated_arg,
                 call_stack,
             } => {
                 let mut msg = String::new();
@@ -1048,8 +1066,13 @@ impl ToDiagnostic<FileId> for EvalError {
                 }
 
                 let (path_label, notes) = blame_error::report_ty_path(l, files);
-                let labels =
-                    blame_error::build_diagnostic_labels(l, path_label, files, contract_id);
+                let labels = blame_error::build_diagnostic_labels(
+                    evaluated_arg.clone(),
+                    l,
+                    path_label,
+                    files,
+                    contract_id,
+                );
 
                 let mut diagnostics = vec![Diagnostic::error()
                     .with_message(msg)
@@ -1081,6 +1104,7 @@ mod blame_error {
             ty_path::{self, PathSpan},
         },
         position::TermPos,
+        term::RichTerm,
     };
 
     use super::{primary, secondary, secondary_term};
@@ -1102,6 +1126,7 @@ mod blame_error {
 
     /// Constructs the diagnostic labels used when raising a blame error.
     pub fn build_diagnostic_labels(
+        evaluated_arg: Option<RichTerm>,
         blame_label: &label::Label,
         path_label: Label<FileId>,
         files: &mut Files<String>,
@@ -1126,16 +1151,20 @@ mod blame_error {
         // If we have a reference to the thunk that was being tested, we can try to show
         // more information about the final, evaluated value that is responsible for the
         // blame.
-        if let Some(ref thunk) = blame_label.arg_idx {
-            let mut val = thunk.get_owned().body;
-
-            match (val.pos, blame_label.arg_pos.as_opt_ref(), contract_id) {
+        if let Some(mut evaluated_arg) = evaluated_arg {
+            match (
+                evaluated_arg.pos,
+                blame_label.arg_pos.as_opt_ref(),
+                contract_id,
+            ) {
                 // Avoid showing a position inside builtin contracts, it's rarely
                 // informative.
                 (TermPos::Original(val_pos), _, Some(c_id)) if val_pos.src_id == c_id => {
-                    val.pos = TermPos::None;
-                    labels
-                        .push(secondary_term(&val, files).with_message("evaluated to this value"));
+                    evaluated_arg.pos = TermPos::None;
+                    labels.push(
+                        secondary_term(&evaluated_arg, files)
+                            .with_message("evaluated to this value"),
+                    );
                 }
                 // Do not show the same thing twice: if arg_pos and val_pos are the same,
                 // the first label "applied to this value" is sufficient.
@@ -1147,21 +1176,25 @@ mod blame_error {
                 // print the actual value than referring to the same position as
                 // before.
                 (TermPos::Inherited(ref val_pos), Some(arg_pos), _) if val_pos == arg_pos => {
-                    val.pos = TermPos::None;
-                    labels
-                        .push(secondary_term(&val, files).with_message("evaluated to this value"));
+                    evaluated_arg.pos = TermPos::None;
+                    labels.push(
+                        secondary_term(&evaluated_arg, files)
+                            .with_message("evaluated to this value"),
+                    );
                 }
                 // Finally, if the parameter reduced to a value which originates from a
                 // different expression, show both the expression and the value.
                 (TermPos::Inherited(ref val_pos), ..) => {
                     labels.push(secondary(val_pos).with_message("evaluated to this expression"));
-                    val.pos = TermPos::None;
-                    labels
-                        .push(secondary_term(&val, files).with_message("evaluated to this value"));
-                }
-                (TermPos::None, ..) => {
-                    labels.push(secondary_term(&val, files).with_message("evaluated to this value"))
+                    evaluated_arg.pos = TermPos::None;
+                    labels.push(
+                        secondary_term(&evaluated_arg, files)
+                            .with_message("evaluated to this value"),
+                    );
                 }
+                (TermPos::None, ..) => labels.push(
+                    secondary_term(&evaluated_arg, files).with_message("evaluated to this value"),
+                ),
             }
         }
 
diff --git a/src/eval/cache.rs b/src/eval/cache.rs
index b7c2cf1b..f87fe3bd 100644
--- a/src/eval/cache.rs
+++ b/src/eval/cache.rs
@@ -1,10 +1,11 @@
-use std::fmt::Debug;
-
 use super::{
     lazy::{BlackholedError, Thunk, ThunkState, ThunkUpdateFrame},
-    IdentKind,
+    Closure, Environment, IdentKind,
+};
+use crate::{
+    identifier::Ident,
+    term::{record::FieldDeps, BindingType, RichTerm},
 };
-use crate::{eval::Closure, identifier::Ident, term::BindingType};
 
 pub trait Cache: Clone {
     type UpdateIndex; // Temporary: we won't really need that once an alternative caching mechanism gets implemented
@@ -19,13 +20,25 @@ pub trait Cache: Clone {
     fn get_then<T, F: FnOnce(&Closure) -> T>(&self, idx: CacheIndex, f: F) -> T;
     fn update(&mut self, clos: Closure, idx: Self::UpdateIndex);
     fn new() -> Self;
-    fn reset_index_state(&self, idx: &mut Self::UpdateIndex);
+    fn reset_index_state(&mut self, idx: &mut Self::UpdateIndex);
     fn map_at_index<F: FnMut(&Closure) -> Closure>(&mut self, idx: &CacheIndex, f: F)
         -> CacheIndex;
-    // TODO: Needs a better name
     fn build_cached(&mut self, idx: &mut CacheIndex, rec_env: &[(Ident, CacheIndex)]);
+    fn ident_kind(&self, idx: &CacheIndex) -> IdentKind;
+    fn saturate<'a, I: DoubleEndedIterator<Item = &'a Ident> + Clone>(
+        &mut self,
+        idx: CacheIndex,
+        env: &mut Environment,
+        fields: I,
+    ) -> RichTerm;
+    fn revert(&mut self, idx: &CacheIndex) -> CacheIndex;
+    fn deps(&self, idx: &CacheIndex) -> Option<FieldDeps>;
+    fn make_update_index(&self, idx: &mut CacheIndex)
+        -> Result<Self::UpdateIndex, BlackholedError>;
 }
 
+//pub type CacheIndex = usize;
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct CBNCache {}
 
@@ -79,7 +92,7 @@ impl Cache for CBNCache {
         CBNCache {}
     }
 
-    fn reset_index_state(&self, idx: &mut Self::UpdateIndex) {
+    fn reset_index_state(&mut self, idx: &mut Self::UpdateIndex) {
         idx.reset_state();
     }
 
@@ -94,4 +107,32 @@ impl Cache for CBNCache {
     fn build_cached(&mut self, idx: &mut CacheIndex, rec_env: &[(Ident, CacheIndex)]) {
         idx.build_cached(rec_env)
     }
+
+    fn ident_kind(&self, idx: &CacheIndex) -> IdentKind {
+        idx.ident_kind()
+    }
+
+    fn saturate<'a, I: DoubleEndedIterator<Item = &'a Ident> + Clone>(
+        &mut self,
+        idx: CacheIndex,
+        env: &mut Environment,
+        fields: I,
+    ) -> RichTerm {
+        idx.saturate(env, fields)
+    }
+
+    fn deps(&self, idx: &CacheIndex) -> Option<FieldDeps> {
+        Some(idx.deps())
+    }
+
+    fn revert(&mut self, idx: &CacheIndex) -> CacheIndex {
+        idx.revert()
+    }
+
+    fn make_update_index(
+        &self,
+        idx: &mut CacheIndex,
+    ) -> Result<Self::UpdateIndex, BlackholedError> {
+        idx.mk_update_frame()
+    }
 }
diff --git a/src/eval/merge.rs b/src/eval/merge.rs
index a7aef200..287dedb2 100644
--- a/src/eval/merge.rs
+++ b/src/eval/merge.rs
@@ -381,6 +381,7 @@ pub fn merge<C: Cache>(
             if let Some(record::SealedTail { label, .. }) = r1.sealed_tail.or(r2.sealed_tail) {
                 return Err(EvalError::IllegalPolymorphicTailAccess {
                     action: IllegalPolymorphicTailAction::Merge,
+                    evaluated_arg: label.get_evaluated_arg(cache),
                     label,
                     call_stack: std::mem::take(call_stack),
                 });
@@ -398,7 +399,11 @@ pub fn merge<C: Cache>(
                         left.keys().map(|field| format!("`{}`", field)).collect();
                     let plural = if fields.len() == 1 { "" } else { "s" };
                     lbl.tag = format!("extra field{} {}", plural, fields.join(","));
-                    return Err(EvalError::BlameError(lbl, CallStack::new()));
+                    return Err(EvalError::BlameError {
+                        evaluated_arg: lbl.get_evaluated_arg(cache),
+                        label: lbl,
+                        call_stack: CallStack::new(),
+                    });
                 }
                 _ => (),
             };
@@ -423,18 +428,26 @@ pub fn merge<C: Cache>(
             // [crate::eval::lazy::Thunk::saturate].
             m.extend(
                 left.into_iter()
-                    .map(|(field, t)| (field, revert_closurize(t, &mut env, &env1))),
+                    .map(|(field, t)| (field, revert_closurize(cache, t, &mut env, &env1))),
             );
             m.extend(
                 right
                     .into_iter()
-                    .map(|(field, t)| (field, revert_closurize(t, &mut env, &env2))),
+                    .map(|(field, t)| (field, revert_closurize(cache, t, &mut env, &env2))),
             );
 
             for (field, (t1, t2)) in center.into_iter() {
                 m.insert(
                     field,
-                    fields_merge_closurize(&mut env, t1, &env1, t2, &env2, field_names.iter())?,
+                    fields_merge_closurize(
+                        cache,
+                        &mut env,
+                        t1,
+                        &env1,
+                        t2,
+                        &env2,
+                        field_names.iter(),
+                    )?,
                 );
             }
 
@@ -462,9 +475,11 @@ pub fn merge<C: Cache>(
         }
         (t1_, t2_) => match (mode, &t2_) {
             // We want to merge a non-record term with a record contract
-            (MergeMode::Contract(label), Term::Record(..)) => {
-                Err(EvalError::BlameError(label, call_stack.clone()))
-            }
+            (MergeMode::Contract(label), Term::Record(..)) => Err(EvalError::BlameError {
+                evaluated_arg: label.get_evaluated_arg(cache),
+                label,
+                call_stack: call_stack.clone(),
+            }),
             // The following cases are either errors or not yet implemented
             _ => Err(EvalError::MergeIncompatibleArgs(
                 RichTerm {
@@ -529,30 +544,34 @@ fn merge_doc(doc1: Option<String>, doc2: Option<String>) -> Option<String> {
 /// If the expression is not a variable referring to a thunk (this can happen e.g. for numeric
 /// constants), we just return the term as it is, which falls into the zero dependencies special
 /// case.
-fn saturate<'a, I: DoubleEndedIterator<Item = &'a Ident> + Clone>(
+fn saturate<'a, I: DoubleEndedIterator<Item = &'a Ident> + Clone, C: Cache>(
+    cache: &mut C,
     rt: RichTerm,
     env: &mut Environment,
     local_env: &Environment,
     fields: I,
 ) -> Result<RichTerm, EvalError> {
     if let Term::Var(var_id) = &*rt.term {
-        let thunk = local_env
+        let idx = local_env
             .get(var_id)
             .cloned()
             .ok_or(EvalError::UnboundIdentifier(*var_id, rt.pos))?;
 
-        Ok(thunk.saturate(env, fields).with_pos(rt.pos))
+        Ok(cache.saturate(idx, env, fields).with_pos(rt.pos))
     } else {
         Ok(rt)
     }
 }
 
 /// Return the dependencies of a field when represented as a `RichTerm`.
-fn field_deps(rt: &RichTerm, local_env: &Environment) -> Result<FieldDeps, EvalError> {
+fn field_deps<C: Cache>(
+    cache: &C,
+    rt: &RichTerm,
+    local_env: &Environment,
+) -> Result<FieldDeps, EvalError> {
     if let Term::Var(var_id) = &*rt.term {
-        local_env
-            .get(var_id)
-            .map(Thunk::deps)
+        cache
+            .deps(local_env.get(var_id).unwrap())
             .ok_or(EvalError::UnboundIdentifier(*var_id, rt.pos))
     } else {
         Ok(FieldDeps::empty())
@@ -568,7 +587,8 @@ fn field_deps(rt: &RichTerm, local_env: &Environment) -> Result<FieldDeps, EvalE
 ///
 /// The fields are saturated (see [saturate]) to properly propagate recursive dependencies down to
 /// `t1` and `t2` in the final, merged record.
-fn fields_merge_closurize<'a, I: DoubleEndedIterator<Item = &'a Ident> + Clone>(
+fn fields_merge_closurize<'a, I: DoubleEndedIterator<Item = &'a Ident> + Clone, C: Cache>(
+    cache: &mut C,
     env: &mut Environment,
     t1: RichTerm,
     env1: &Environment,
@@ -578,11 +598,11 @@ fn fields_merge_closurize<'a, I: DoubleEndedIterator<Item = &'a Ident> + Clone>(
 ) -> Result<RichTerm, EvalError> {
     let mut local_env = Environment::new();
 
-    let combined_deps = field_deps(&t1, env1)?.union(field_deps(&t2, env2)?);
+    let combined_deps = field_deps(cache, &t1, env1)?.union(field_deps(cache, &t2, env2)?);
     let body = RichTerm::from(Term::Op2(
         BinaryOp::Merge(),
-        saturate(t1, &mut local_env, env1, fields.clone())?,
-        saturate(t2, &mut local_env, env2, fields)?,
+        saturate(cache, t1, &mut local_env, env1, fields.clone())?,
+        saturate(cache, t2, &mut local_env, env2, fields)?,
     ));
 
     // We closurize the final result in a thunk with appropriate dependencies
@@ -595,7 +615,11 @@ fn fields_merge_closurize<'a, I: DoubleEndedIterator<Item = &'a Ident> + Clone>(
     // new_rev takes care of not creating a revertible thunk if the dependencies are empty.
     env.insert(
         fresh_var,
-        Thunk::new_rev(closure, IdentKind::Record, combined_deps),
+        cache.add(
+            closure,
+            IdentKind::Record,
+            BindingType::Revertible(combined_deps),
+        ),
     );
 
     Ok(RichTerm::from(Term::Var(fresh_var)))
@@ -623,10 +647,15 @@ fn merge_closurize<C: Cache>(
 }
 
 /// Revert the thunk inside the provided field (if any), and closurize the result inside `env`.
-fn revert_closurize(rt: RichTerm, env: &mut Environment, local_env: &Environment) -> RichTerm {
+fn revert_closurize<C: Cache>(
+    cache: &mut C,
+    rt: RichTerm,
+    env: &mut Environment,
+    local_env: &Environment,
+) -> RichTerm {
     if let Term::Var(id) = rt.as_ref() {
-        // This create a fresh variable which is bound to a reverted copy of the original thunk
-        let reverted = local_env.get(id).unwrap().revert();
+        // This creates a fresh variable which is bound to a reverted copy of the original thunk
+        let reverted = cache.revert(local_env.get(id).unwrap());
         let fresh_id = Ident::fresh();
         env.insert(fresh_id, reverted);
         RichTerm::new(Term::Var(fresh_id), rt.pos)
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 48f28300..092b5ae3 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -112,7 +112,6 @@ pub mod stack;
 
 use callstack::*;
 use codespan::FileId;
-use lazy::*;
 use operation::OperationCont;
 use stack::Stack;
 
@@ -322,10 +321,11 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                         }
                         None | Some(..) => {
                             // This operation should not be allowed to evaluate a sealed term
-                            return Err(EvalError::BlameError(
-                                lbl.clone(),
-                                self.call_stack.clone(),
-                            ));
+                            return Err(EvalError::BlameError {
+                                evaluated_arg: lbl.get_evaluated_arg(&self.cache),
+                                label: lbl.clone(),
+                                call_stack: self.call_stack.clone(),
+                            });
                         }
                     }
                 }
@@ -340,13 +340,14 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                     match self.cache.get_update_index(&mut idx) {
                         Ok(Some(idx_upd)) => self.stack.push_update_index(idx_upd),
                         Ok(None) => {}
-                        Err(BlackholedError) => {
+                        Err(_blackholed_error) => {
                             return Err(EvalError::InfiniteRecursion(self.call_stack.clone(), pos))
                         }
                     }
 
-                    self.call_stack.enter_var(idx.ident_kind(), *x, pos);
-                    self.cache.get(idx)
+                    self.call_stack
+                        .enter_var(self.cache.ident_kind(&idx), *x, pos);
+                    self.cache.get(idx.clone())
                 }
                 Term::App(t1, t2) => {
                     self.call_stack.enter_app(pos);
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index 7bcaeb6d..8a7d050d 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -276,10 +276,11 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
             }
             UnaryOp::Blame() => match_sharedterm! { t, with {
                     Term::Lbl(label) => Err(
-                        EvalError::BlameError(
+                        EvalError::BlameError {
+                            evaluated_arg: label.get_evaluated_arg(&self.cache),
                             label,
-                            std::mem::take(&mut self.call_stack),
-                        )),
+                            call_stack: std::mem::take(&mut self.call_stack),
+                        }),
                 } else
                     Err(EvalError::TypeError(
                         String::from("Label"),
@@ -639,6 +640,7 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                             if let Some(record::SealedTail { label, .. }) = record.sealed_tail {
                                 return Err(EvalError::IllegalPolymorphicTailAccess {
                                     action: IllegalPolymorphicTailAction::Map,
+                                    evaluated_arg: label.get_evaluated_arg(&self.cache),
                                     label,
                                     call_stack: std::mem::take(&mut self.call_stack),
                                 })
@@ -2784,8 +2786,10 @@ impl<R: ImportResolver, C: Cache> VirtualMachine<R, C> {
                         .clone()
                         .sealed_tail
                         .and_then(|t| t.unseal(s).cloned())
-                        .ok_or_else(|| {
-                            EvalError::BlameError(l.clone(), std::mem::take(&mut self.call_stack))
+                        .ok_or_else(|| EvalError::BlameError {
+                            evaluated_arg: l.get_evaluated_arg(&self.cache),
+                            label: l.clone(),
+                            call_stack: std::mem::take(&mut self.call_stack),
                         })
                         .map(|t| Closure { body: t, env: env3 }),
                     (Term::SealingKey(..), _, _) => Err(EvalError::TypeError(
diff --git a/src/eval/stack.rs b/src/eval/stack.rs
index c3a11596..d449602e 100644
--- a/src/eval/stack.rs
+++ b/src/eval/stack.rs
@@ -352,7 +352,7 @@ impl<C: Cache> std::fmt::Debug for Stack<C> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::eval::{cache::CBNCache, IdentKind, Thunk};
+    use crate::eval::{cache::CBNCache, IdentKind};
     use crate::term::{Term, UnaryOp};
     use assert_matches::assert_matches;
 
@@ -382,9 +382,10 @@ mod tests {
         Marker::Arg(some_closure(), TermPos::None)
     }
 
-    fn some_thunk_marker() -> Marker<EC> {
-        let mut thunk = Thunk::new(some_closure(), IdentKind::Let);
-        Marker::UpdateIndex(thunk.mk_update_frame().unwrap())
+    fn some_thunk_marker(eval_cache: &mut EC) -> Marker<EC> {
+        let mut idx = eval_cache.add(some_closure(), IdentKind::Let, BindingType::Normal);
+        let uidx = eval_cache.make_update_index(&mut idx).unwrap();
+        Marker::UpdateIndex(uidx)
     }
 
     fn some_cont_marker() -> Marker<EC> {
@@ -393,8 +394,9 @@ mod tests {
 
     #[test]
     fn marker_differentiates() {
+        let mut eval_cache = EC::new();
         assert!(some_arg_marker().is_arg());
-        assert!(some_thunk_marker().is_idx());
+        assert!(some_thunk_marker(&mut eval_cache).is_idx());
         assert!(some_cont_marker().is_cont());
     }
 
@@ -408,7 +410,7 @@ mod tests {
         assert_eq!(2, s.count_args());
         assert_eq!(
             some_closure(),
-            s.pop_arg(&CBNCache {}).expect("Already checked").0
+            s.pop_arg(&EC::new()).expect("Already checked").0
         );
         assert_eq!(1, s.count_args());
     }
@@ -418,11 +420,12 @@ mod tests {
         let mut s = Stack::new();
         assert_eq!(0, s.count_thunks());
 
-        let mut thunk = Thunk::new(some_closure(), IdentKind::Let);
-        s.push_update_index(thunk.mk_update_frame().unwrap());
-        // TODO: This is not generic
-        thunk = Thunk::new(some_closure(), IdentKind::Let);
-        s.push_update_index(thunk.mk_update_frame().unwrap());
+        let mut eval_cache = EC::new();
+
+        let mut idx = eval_cache.add(some_closure(), IdentKind::Let, BindingType::Normal);
+        s.push_update_index(eval_cache.make_update_index(&mut idx).unwrap());
+        idx = eval_cache.add(some_closure(), IdentKind::Let, BindingType::Normal);
+        s.push_update_index(eval_cache.make_update_index(&mut idx).unwrap());
 
         assert_eq!(2, s.count_thunks());
         s.pop_update_index().expect("Already checked");
@@ -431,12 +434,13 @@ mod tests {
 
     #[test]
     fn thunk_blackhole() {
-        let mut thunk = Thunk::new(some_closure(), IdentKind::Let);
-        let thunk_upd = thunk.mk_update_frame();
-        assert_matches!(thunk_upd, Ok(..));
-        assert_matches!(thunk.mk_update_frame(), Err(..));
-        thunk_upd.unwrap().update(some_closure());
-        assert_matches!(thunk.mk_update_frame(), Ok(..));
+        let mut eval_cache = CBNCache::new();
+        let mut idx = eval_cache.add(some_closure(), IdentKind::Let, BindingType::Normal);
+        let idx_upd = eval_cache.make_update_index(&mut idx);
+        assert_matches!(idx_upd, Ok(..));
+        assert_matches!(eval_cache.make_update_index(&mut idx), Err(..));
+        eval_cache.update(some_closure(), idx_upd.unwrap());
+        assert_matches!(eval_cache.make_update_index(&mut idx), Ok(..));
     }
 
     #[test]
@@ -457,7 +461,7 @@ mod tests {
     #[test]
     fn pushing_and_poping_strictness_markers() {
         let mut s = Stack::new();
-        let cache = CBNCache {};
+        let cache = CBNCache::new();
         assert_eq!(0, s.count_args());
 
         s.push_strictness(EvalMode::UnwrapMeta);
diff --git a/src/label.rs b/src/label.rs
index 75259948..020706cd 100644
--- a/src/label.rs
+++ b/src/label.rs
@@ -4,9 +4,13 @@
 //! information about the context of a contract failure.
 use std::rc::Rc;
 
-use crate::eval::cache::CacheIndex;
-use crate::position::{RawSpan, TermPos};
-use crate::types::{TypeF, Types};
+use crate::{
+    eval::cache::{Cache as EvalCache, CacheIndex},
+    position::{RawSpan, TermPos},
+    term::RichTerm,
+    types::{TypeF, Types},
+};
+
 use codespan::Files;
 
 pub mod ty_path {
@@ -356,6 +360,10 @@ impl Label {
             path: Vec::new(),
         }
     }
+
+    pub fn get_evaluated_arg<EC: EvalCache>(&self, cache: &EC) -> Option<RichTerm> {
+        self.arg_idx.clone().map(|idx| cache.get(idx).body)
+    }
 }
 
 impl Default for Label {
diff --git a/src/serialize.rs b/src/serialize.rs
index ac03fc80..fd4df47c 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -113,7 +113,7 @@ where
         .iter()
         // Filtering out optional fields without a definition. All variable should have been
         // substituted at this point, so we pass an empty environment.
-        .filter(|(_, t)| !is_empty_optional(&CBNCache {}, t, &eval::Environment::new()))
+        .filter(|(_, t)| !is_empty_optional(&CBNCache::new(), t, &eval::Environment::new()))
         .collect();
     entries.sort_by_key(|(k, _)| *k);
 
@@ -277,7 +277,7 @@ mod tests {
     use super::*;
     use crate::cache::resolvers::DummyResolver;
     use crate::error::{Error, EvalError};
-    use crate::eval::{cache::CBNCache, Environment, VirtualMachine};
+    use crate::eval::{Environment, VirtualMachine};
     use crate::position::TermPos;
     use crate::program::Program;
     use crate::term::{make as mk_term, BinaryOp};


Test patch:
diff --git a/src/eval/tests.rs b/src/eval/tests.rs
index 62d2d3ff..fde3379e 100644
--- a/src/eval/tests.rs
+++ b/src/eval/tests.rs
@@ -1,7 +1,7 @@
+use super::cache::CBNCache;
 use super::*;
 use crate::cache::resolvers::{DummyResolver, SimpleResolver};
 use crate::error::ImportError;
-use crate::eval::cache::CBNCache;
 use crate::label::Label;
 use crate::parser::{grammar, lexer};
 use crate::term::make as mk_term;
@@ -43,11 +43,14 @@ fn identity_over_values() {
 
 #[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())))
+    let l = Label::dummy();
+    if let Err(EvalError::BlameError {
+        evaluated_arg: _,
+        label,
+        call_stack: _,
+    }) = eval_no_import(mk_term::op1(UnaryOp::Blame(), Term::Lbl(l.clone())))
     {
-        assert_eq!(l, label);
+        assert_eq!(label, l);
     } else {
         panic!("This evaluation should've returned a BlameError!");
     }
diff --git a/tests/integration/basics_fail.rs b/tests/integration/basics_fail.rs
index 7b4d06ec..4fd56165 100644
--- a/tests/integration/basics_fail.rs
+++ b/tests/integration/basics_fail.rs
@@ -43,7 +43,7 @@ fn comparisons() {
 fn boolean_ops() {
     assert_matches!(
         eval("let throw | (fun l _v => %blame% l) = null in false || true && throw"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
     assert_matches!(
         eval("0 && true"),
diff --git a/tests/integration/contracts_fail.rs b/tests/integration/contracts_fail.rs
index 3a5106f7..ee9acafc 100644
--- a/tests/integration/contracts_fail.rs
+++ b/tests/integration/contracts_fail.rs
@@ -8,13 +8,13 @@ macro_rules! assert_raise_blame {
     ($term:expr) => {{
         assert_matches!(
             eval($term),
-            Err(Error::EvalError(EvalError::BlameError(..)))
+            Err(Error::EvalError(EvalError::BlameError {..}))
         )
     }};
     ($term:expr, $($arg:tt)+) => {{
         assert_matches!(
             eval($term),
-            Err(Error::EvalError(EvalError::BlameError(..))),
+            Err(Error::EvalError(EvalError::BlameError {..})),
             $($arg)+
         )
     }}
@@ -235,21 +235,25 @@ fn lists_contracts() {
 
     assert_matches!(
         eval("%force% ([1, \"a\"] | Array Num) 0"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
     assert_matches!(
         eval("1 | Array Dyn"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
     assert_matches!(
         eval("(fun x => x) | Array Dyn"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
 
     let res = eval("%force% ([{a = [1]}] | Array {a: Array Str}) false");
     match &res {
-        Err(Error::EvalError(EvalError::BlameError(ref l, _))) => {
-            assert_matches!(l.path.as_slice(), [Elem::Array, Elem::Field(id), Elem::Array] if &id.to_string() == "a")
+        Err(Error::EvalError(EvalError::BlameError {
+            evaluated_arg: _,
+            ref label,
+            call_stack: _,
+        })) => {
+            assert_matches!(label.path.as_slice(), [Elem::Array, Elem::Field(id), Elem::Array] if &id.to_string() == "a")
         }
         err => panic!("expected blame error, got {:?}", err),
     }
@@ -262,8 +266,12 @@ fn lists_contracts() {
         "(%elem_at% (({foo = [(fun x => \"a\")]} | {foo: Array (forall a. a -> Num)}).foo) 0) false",
     );
     match &res {
-        Err(Error::EvalError(EvalError::BlameError(ref l, _))) => {
-            assert_matches!(l.path.as_slice(), [Elem::Field(id), Elem::Array, Elem::Codomain] if &id.to_string() == "foo")
+        Err(Error::EvalError(EvalError::BlameError {
+            evaluated_arg: _,
+            ref label,
+            call_stack: _,
+        })) => {
+            assert_matches!(label.path.as_slice(), [Elem::Field(id), Elem::Array, Elem::Codomain] if &id.to_string() == "foo")
         }
         err => panic!("expected blame error, got {:?}", err),
     }
@@ -293,7 +301,7 @@ fn type_path_with_aliases() {
     fn assert_blame_dont_panic(expr: &str) {
         let mut p = program_from_expr(expr);
         let result = p.eval();
-        assert_matches!(result, Err(Error::EvalError(EvalError::BlameError(..))));
+        assert_matches!(result, Err(Error::EvalError(EvalError::BlameError { .. })));
         // Check that the diagnostic is correctly produced
         p.report(result.unwrap_err());
     }
diff --git a/tests/integration/destructuring.rs b/tests/integration/destructuring.rs
index deaa3ff5..5f870f53 100644
--- a/tests/integration/destructuring.rs
+++ b/tests/integration/destructuring.rs
@@ -64,7 +64,7 @@ fn assign_fail() {
 fn closed_fail() {
     assert_matches!(
         eval_file("destructuring/closed_fail.ncl"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
 }
 
@@ -80,7 +80,7 @@ fn rest_fail() {
 fn typecontract_fail() {
     assert_matches!(
         eval_file("destructuring/typecontract_fail.ncl"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
 }
 
diff --git a/tests/integration/examples.rs b/tests/integration/examples.rs
index 8b311dca..97585726 100644
--- a/tests/integration/examples.rs
+++ b/tests/integration/examples.rs
@@ -19,7 +19,7 @@ fn eval_file(file: &str) {
             let mut p = TestProgram::new_from_file(path).expect("could not load file as a program");
             assert_matches!(
                 p.eval_deep(),
-                Err(Error::EvalError(EvalError::BlameError(..)))
+                Err(Error::EvalError(EvalError::BlameError { .. }))
             );
         }
         ExampleAnnotation::Pass => {
diff --git a/tests/integration/imports.rs b/tests/integration/imports.rs
index 3ede4d1a..21bec40f 100644
--- a/tests/integration/imports.rs
+++ b/tests/integration/imports.rs
@@ -53,7 +53,7 @@ fn contract_fail() {
     .unwrap();
     assert_matches!(
         prog.eval(),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
 }
 
diff --git a/tests/integration/stdlib_arrays_fail.rs b/tests/integration/stdlib_arrays_fail.rs
index 4c6e6726..089c2ce3 100644
--- a/tests/integration/stdlib_arrays_fail.rs
+++ b/tests/integration/stdlib_arrays_fail.rs
@@ -28,11 +28,11 @@ fn elem_at() {
 
     assert_matches!(
         eval("array.elem_at 0 {} 0"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
     assert_matches!(
         eval("array.elem_at \"a\" []"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
 }
 
@@ -58,18 +58,18 @@ fn head_tail() {
     // TODO: add non-empty contract to the input of array.head and array.tail
     // assert_matches!(
     //     eval("array.head []"),
-    //     Err(Error::EvalError(EvalError::BlameError(..)))
+    //     Err(Error::EvalError(EvalError::BlameError {..}))
     // );
     // assert_matches!(
     //     eval("array.tail []"),
-    //     Err(Error::EvalError(EvalError::BlameError(..)))
+    //     Err(Error::EvalError(EvalError::BlameError {..}))
     // );
     assert_matches!(
         eval("array.head false"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
     assert_matches!(
         eval("array.tail 2"),
-        Err(Error::EvalError(EvalError::BlameError(..)))
+        Err(Error::EvalError(EvalError::BlameError { .. }))
     );
 }