Repository: tweag/nickel
Pull request #1132 (2023-02-20 13:37:08)

Non-exported record fields
This PR adds a `not_exported` field to record field metadata along with syntax to set it to true. The field is set to `false` by default and a metadata annotation in a record like
```nickel
{
  foo = 1,
  bar | not_exported = 2,
}
```
will set it to `true`. Upon merging it infects everything, i.e. when a field marked as `not_exported` is merged with any other field, the result will be `not_exported`.


Patch:
diff --git a/flake.lock b/flake.lock
index 46929e69..3afb20b5 100644
--- a/flake.lock
+++ b/flake.lock
@@ -10,11 +10,11 @@
         "rust-overlay": "rust-overlay"
       },
       "locked": {
-        "lastModified": 1673405853,
-        "narHash": "sha256-6Nq9DuOo+gE2I8z5UZaKuumykz2xxZ9JGYmUthOuwSA=",
+        "lastModified": 1676515499,
+        "narHash": "sha256-Qk9kVSAT/Iz2AwrtnOfCih6KB2Odu4mN8fBvRjiMdZ0=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "b13963c8c18026aa694acd98d14f66d24666f70b",
+        "rev": "cfb6efcfc706e21c84682171581168e591b265d9",
         "type": "github"
       },
       "original": {
@@ -26,11 +26,11 @@
     "flake-compat": {
       "flake": false,
       "locked": {
-        "lastModified": 1668681692,
-        "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
+        "lastModified": 1673956053,
+        "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
         "owner": "edolstra",
         "repo": "flake-compat",
-        "rev": "009399224d5e398d03b22badca40a37ac85412a1",
+        "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
         "type": "github"
       },
       "original": {
@@ -57,11 +57,11 @@
     },
     "flake-utils": {
       "locked": {
-        "lastModified": 1667395993,
-        "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
+        "lastModified": 1676283394,
+        "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+        "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
         "type": "github"
       },
       "original": {
@@ -72,11 +72,11 @@
     },
     "flake-utils_2": {
       "locked": {
-        "lastModified": 1667395993,
-        "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
+        "lastModified": 1676283394,
+        "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
+        "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
         "type": "github"
       },
       "original": {
@@ -108,11 +108,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1674211260,
-        "narHash": "sha256-xU6Rv9sgnwaWK7tgCPadV6HhI2Y/fl4lKxJoG2+m9qs=",
+        "lastModified": 1676569297,
+        "narHash": "sha256-2n4C4H3/U+3YbDrQB6xIw7AaLdFISCCFwOkcETAigqU=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "5ed481943351e9fd354aeb557679624224de38d5",
+        "rev": "ac1f5b72a9e95873d1de0233fddcb56f99884b37",
         "type": "github"
       },
       "original": {
@@ -150,11 +150,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1674122161,
-        "narHash": "sha256-9QM4rvgUSEwO8DWtJN9sR/afEqrH1s3b6ACsZT5wiAM=",
+        "lastModified": 1676513100,
+        "narHash": "sha256-MK39nQV86L2ag4TmcK5/+r1ULpzRLPbbfvWbPvIoYJE=",
         "owner": "cachix",
         "repo": "pre-commit-hooks.nix",
-        "rev": "53e766957b73298fa68b47478c48cbcc005cc18a",
+        "rev": "5f0cba88ac4d6dd8cad5c6f6f1540b3d6a21a798",
         "type": "github"
       },
       "original": {
@@ -184,11 +184,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1672712534,
-        "narHash": "sha256-8S0DdMPcbITnlOu0uA81mTo3hgX84wK8S9wS34HEFY4=",
+        "lastModified": 1676437770,
+        "narHash": "sha256-mhJye91Bn0jJIE7NnEywGty/U5qdELfsT8S+FBjTdG4=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "69fb7bf0a8c40e6c4c197fa1816773774c8ac59f",
+        "rev": "a619538647bd03e3ee1d7b947f7c11ff289b376e",
         "type": "github"
       },
       "original": {
@@ -207,11 +207,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1674267882,
-        "narHash": "sha256-53sIczqxA5BbrhgO6l54DSisDqHvQ3UUwbSqBryA/k0=",
+        "lastModified": 1676687290,
+        "narHash": "sha256-DP0CJ7qtUXf+mmMglJL1yANizzV1O4UfQ9NrKgy7O04=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "1fd6d280c132f4facad8cd023543fb10121e6487",
+        "rev": "bdccd5e973d45159f7d13f7c65a4271dc02cf6d4",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index 6c608754..4c61442e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -73,7 +73,7 @@
             cargoDeps = oldAttrs.cargoDeps.overrideAttrs (final.lib.const {
               # This `inherit src` is important, otherwise, the old `src` would be used here
               inherit src;
-              outputHash = "sha256-GwLeA6xLt7I+NzRaqjwVpt1pzRex1/snq30DPv4FR+g=";
+              outputHash = "sha256-n3Z/jdw4CZvqixnEQX0mS/Fs2ocskLM/s7nt+pd4hPA=";
             });
           });
       };
diff --git a/lsp/nls/src/cache.rs b/lsp/nls/src/cache.rs
index f57d9dc3..8a5f0d24 100644
--- a/lsp/nls/src/cache.rs
+++ b/lsp/nls/src/cache.rs
@@ -3,7 +3,7 @@ use std::{collections::HashMap, ffi::OsString, io};
 use codespan::FileId;
 use nickel_lang::{
     cache::{Cache, CacheError, CacheOp, CachedTerm, EntryState},
-    error::TypecheckError,
+    error::Error,
     typecheck::{self, linearization::Linearization},
 };
 
@@ -17,7 +17,7 @@ pub trait CacheExt {
         initial_ctxt: &typecheck::Context,
         initial_env: &Environment,
         lin_cache: &mut HashMap<FileId, Completed>,
-    ) -> Result<CacheOp<()>, CacheError<TypecheckError>>;
+    ) -> Result<CacheOp<()>, CacheError<Vec<Error>>>;
 }
 
 impl CacheExt for Cache {
@@ -39,12 +39,14 @@ impl CacheExt for Cache {
         initial_ctxt: &typecheck::Context,
         initial_env: &Environment,
         lin_cache: &mut HashMap<FileId, Completed>,
-    ) -> Result<CacheOp<()>, CacheError<TypecheckError>> {
+    ) -> Result<CacheOp<()>, CacheError<Vec<Error>>> {
         if !self.terms().contains_key(&file_id) {
             return Err(CacheError::NotParsed);
         }
 
-        if let Ok(CacheOp::Done(ids)) = self.resolve_imports(file_id) {
+        let mut import_errors = Vec::new();
+        if let Ok(CacheOp::Done((ids, errors))) = self.resolve_imports(file_id) {
+            import_errors = errors;
             for id in ids {
                 self.typecheck_with_analysis(id, initial_ctxt, initial_env, lin_cache)
                     .unwrap();
@@ -54,7 +56,7 @@ impl CacheExt for Cache {
         // After self.parse(), the cache must be populated
         let CachedTerm { term, state, .. } = self.terms().get(&file_id).unwrap();
 
-        if *state > EntryState::Typechecked && lin_cache.contains_key(&file_id) {
+        let result = if *state > EntryState::Typechecked && lin_cache.contains_key(&file_id) {
             Ok(CacheOp::Cached(()))
         } else if *state >= EntryState::Parsed {
             let host = AnalysisHost::new(file_id, initial_env.clone());
@@ -64,12 +66,20 @@ impl CacheExt for Cache {
                 cache: self,
             });
             let (_, linearized) =
-                typecheck::type_check_linearize(term, initial_ctxt.clone(), self, host, building)?;
+                typecheck::type_check_linearize(term, initial_ctxt.clone(), self, host, building)
+                    .map_err(|err| vec![Error::TypecheckError(err)])?;
             self.update_state(file_id, EntryState::Typechecked);
             lin_cache.insert(file_id, linearized);
             Ok(CacheOp::Done(()))
         } else {
             panic!()
+        };
+        if import_errors.is_empty() {
+            result
+        } else {
+            Err(CacheError::Error(
+                import_errors.into_iter().map(Error::ImportError).collect(),
+            ))
         }
     }
 }
diff --git a/lsp/nls/src/files.rs b/lsp/nls/src/files.rs
index 6a0c7a6c..b97672f0 100644
--- a/lsp/nls/src/files.rs
+++ b/lsp/nls/src/files.rs
@@ -80,7 +80,10 @@ fn typecheck(server: &mut Server, file_id: FileId) -> Result<CacheOp<()>, Vec<Di
             &mut server.lin_cache,
         )
         .map_err(|error| match error {
-            CacheError::Error(tc_error) => tc_error.to_diagnostic(server.cache.files_mut(), None),
+            CacheError::Error(tc_error) => tc_error
+                .into_iter()
+                .flat_map(|err| err.to_diagnostic(server.cache.files_mut(), None))
+                .collect(),
             CacheError::NotParsed => unreachable!(),
         })
 }
diff --git a/lsp/nls/src/linearization/building.rs b/lsp/nls/src/linearization/building.rs
index 52e7fbf6..879a6833 100644
--- a/lsp/nls/src/linearization/building.rs
+++ b/lsp/nls/src/linearization/building.rs
@@ -82,7 +82,7 @@ impl<'b> Building<'b> {
             .expect("Could not find parent")
         {
             TermKind::Record(_) | TermKind::Structure | TermKind::Usage(_) => unreachable!(),
-            TermKind::Declaration(_, ref mut usages, _)
+            TermKind::Declaration { ref mut usages, .. }
             | TermKind::RecordField { ref mut usages, .. } => usages.push(usage),
         };
     }
@@ -94,7 +94,10 @@ impl<'b> Building<'b> {
         value: ItemId,
     ) {
         let kind = self.get_item_kind_mut(current_file, declaration);
-        if let Some(TermKind::Declaration(_, _, value_state)) = kind {
+        if let Some(TermKind::Declaration {
+            value: value_state, ..
+        }) = kind
+        {
             *value_state = ValueState::Known(value)
         }
     }
@@ -162,10 +165,51 @@ impl<'b> Building<'b> {
                     .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)
+            TermKind::Declaration {
+                value: ValueState::Known(value),
+                path: Some(idents),
+                ..
+            } => {
+                let item = self.get_item_kind(current_file, *value)?;
+                let item = self.resolve_reference(current_file, item)?;
+
+                let mut ids = idents.clone();
+                let (mut prev_item, mut curr_item) = (item, item);
+                while let Some(id) = ids.pop() {
+                    match curr_item {
+                        TermKind::Record(ref fields) => {
+                            let item = fields.get(&id)?;
+                            let item_kind = self.get_item_kind(current_file, *item)?;
+                            match item_kind {
+                                TermKind::RecordField {
+                                    value: ValueState::Known(next_item),
+                                    ..
+                                } => {
+                                    prev_item = item_kind;
+                                    let item_kind = self.get_item_kind(current_file, *next_item)?;
+                                    curr_item = item_kind;
+                                    continue;
+                                }
+                                TermKind::RecordField {
+                                    value: ValueState::Unknown,
+                                    ..
+                                } => break,
+                                // the value from a record is always a record field
+                                _ => unreachable!(),
+                            }
+                        }
+                        _ => break,
+                    }
+                }
+                // return the `prev_item` because that was the record field we wanted to
+                // resolve, and `curr_item` points to the record field's value.
+                Some(prev_item)
             }
+            // if declaration is a let binding, resolve its value
+            TermKind::Declaration {
+                value: ValueState::Known(value),
+                ..
+            } => self.get_item_kind(current_file, *value),
 
             TermKind::Usage(UsageState::Resolved(pointed)) => {
                 let kind = self.get_item_kind(current_file, *pointed)?;
diff --git a/lsp/nls/src/linearization/completed.rs b/lsp/nls/src/linearization/completed.rs
index c5d3476b..3ec039bb 100644
--- a/lsp/nls/src/linearization/completed.rs
+++ b/lsp/nls/src/linearization/completed.rs
@@ -143,7 +143,10 @@ impl Completed {
             TermKind::Usage(UsageState::Resolved(declaration)) => self
                 .get_item(declaration, lin_cache)
                 .and_then(|decl| match decl.kind {
-                    TermKind::Declaration(_, _, ValueState::Known(value))
+                    TermKind::Declaration {
+                        value: ValueState::Known(value),
+                        ..
+                    }
                     | TermKind::RecordField {
                         value: ValueState::Known(value),
                         ..
@@ -151,9 +154,10 @@ impl Completed {
                     _ => None,
                 })
                 .unwrap_or(item),
-            TermKind::Declaration(_, _, ValueState::Known(value)) => {
-                self.get_item(value, lin_cache).unwrap_or(item)
-            }
+            TermKind::Declaration {
+                value: ValueState::Known(value),
+                ..
+            } => self.get_item(value, lin_cache).unwrap_or(item),
             _ => item,
         };
 
diff --git a/lsp/nls/src/linearization/interface.rs b/lsp/nls/src/linearization/interface.rs
index cf6d25a7..2c51860e 100644
--- a/lsp/nls/src/linearization/interface.rs
+++ b/lsp/nls/src/linearization/interface.rs
@@ -24,7 +24,18 @@ impl ResolutionState for Resolved {}
 /// Can be extended later to represent Contracts, Records, etc.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum TermKind {
-    Declaration(Ident, Vec<ItemId>, ValueState),
+    Declaration {
+        id: Ident,
+        usages: Vec<ItemId>,
+        value: ValueState,
+        // This is the path to a bound variable. If we have
+        // `let { a = {b = {c, ..}, ..}, ..} = ...`,  the `path` will
+        // be `Some([a, b, c])`, and `ident` will be `c`.
+        // If we have `let { a = {b = {c = somevar, ..}, ..}, ..} = ...`
+        // instead, the `path` remains the same, but the ident will be `somevar`
+        // If there is no pattern variable bound, the `path` is `None`
+        path: Option<Vec<Ident>>,
+    },
     Usage(UsageState),
     Record(HashMap<Ident, ItemId>),
     RecordField {
diff --git a/lsp/nls/src/linearization/mod.rs b/lsp/nls/src/linearization/mod.rs
index ea05cbae..bb1e7da1 100644
--- a/lsp/nls/src/linearization/mod.rs
+++ b/lsp/nls/src/linearization/mod.rs
@@ -68,7 +68,7 @@ pub struct AnalysisHost<'a> {
     /// gives the corresponding record field _term_ to the ident
     /// useable to construct a vale declaration.
     record_fields: Option<(ItemId, Vec<(ItemId, Ident)>)>,
-    let_binding: Option<ItemId>,
+    bindings: Option<Vec<ItemId>>,
     /// Accesses to nested records are recorded recursively.
     /// ```
     /// outer.middle.inner -> inner(middle(outer))
@@ -87,7 +87,7 @@ impl<'a> AnalysisHost<'a> {
             env,
             meta: Default::default(),
             record_fields: Default::default(),
-            let_binding: Default::default(),
+            bindings: Default::default(),
             access: Default::default(),
         }
     }
@@ -152,16 +152,18 @@ impl<'a> Linearizer for AnalysisHost<'a> {
                 }
             }
 
-            if let Some(declaration) = self.let_binding.take() {
+            if let Some(decls) = self.bindings.take() {
                 let offset = self.access.as_ref().map(|v| v.len()).unwrap_or(0);
-                lin.inform_declaration(
-                    self.file,
-                    declaration,
-                    ItemId {
-                        file_id: self.file,
-                        index: id_gen.get() + offset,
-                    },
-                );
+                for decl in decls {
+                    lin.inform_declaration(
+                        self.file,
+                        decl,
+                        ItemId {
+                            file_id: self.file,
+                            index: id_gen.get() + offset,
+                        },
+                    );
+                }
             }
         }
 
@@ -175,10 +177,11 @@ impl<'a> Linearizer for AnalysisHost<'a> {
         };
         match term {
             Term::LetPattern(ident, destruct, ..) | Term::FunPattern(ident, destruct, _) => {
+                let mut binding = None;
                 if let Some(ident) = ident {
                     let value_ptr = match term {
                         Term::LetPattern(..) => {
-                            self.let_binding = Some(id);
+                            binding = Some(id);
                             ValueState::Unknown
                         }
                         Term::FunPattern(..) => {
@@ -210,12 +213,18 @@ impl<'a> Linearizer for AnalysisHost<'a> {
                     };
                     self.env.insert(ident.to_owned(), id);
                     let kind = match term {
-                        Term::LetPattern(..) => {
-                            TermKind::Declaration(ident.to_owned(), Vec::new(), value_ptr)
-                        }
-                        Term::FunPattern(..) => {
-                            TermKind::Declaration(ident.to_owned(), Vec::new(), ValueState::Unknown)
-                        }
+                        Term::LetPattern(..) => TermKind::Declaration {
+                            id: ident.to_owned(),
+                            usages: Vec::new(),
+                            value: value_ptr,
+                            path: None,
+                        },
+                        Term::FunPattern(..) => TermKind::Declaration {
+                            id: ident.to_owned(),
+                            usages: Vec::new(),
+                            value: ValueState::Unknown,
+                            path: None,
+                        },
                         _ => unreachable!(),
                     };
                     lin.push(LinearizationItem {
@@ -227,33 +236,53 @@ impl<'a> Linearizer for AnalysisHost<'a> {
                         metadata: None,
                     });
                 }
-                for matched in destruct.to_owned().inner() {
-                    let (ident, field) = matched.as_binding();
 
+                let mut let_pattern_bindings = if let Some(id) = binding {
+                    vec![id]
+                } else {
+                    Vec::new()
+                };
+                for (path, bind_ident, field) in destruct
+                    .to_owned()
+                    .inner()
+                    .into_iter()
+                    .flat_map(|matched| matched.clone().as_flattened_bindings())
+                {
+                    // We cannot have an empty vector because a bound variable inside
+                    // a record pattern must have at least one item in it's path,
+                    // namely, the record field. i.e if we have: `let {a = value, ..} = ... in ...`
+                    // the path will be `vec![a]`, and this is the smallest possible case for a
+                    // record pattern which binds variables.
+                    let ident = path.first().unwrap();
                     let id = ItemId {
                         file_id: self.file,
                         index: id_gen.get_and_advance(),
                     };
-                    self.env.insert(ident, id);
+
+                    let_pattern_bindings.push(id);
+                    let new_ident = bind_ident.unwrap_or(*ident);
+                    self.env.insert(new_ident, id);
                     lin.push(LinearizationItem {
                         env: self.env.clone(),
                         id,
                         // TODO: get type from pattern
                         ty: UnifType::Concrete(TypeF::Dyn),
-                        pos: ident.pos,
-                        kind: TermKind::Declaration(
-                            ident.to_owned(),
-                            Vec::new(),
-                            ValueState::Known(id),
-                        ),
+                        pos: new_ident.pos,
+                        kind: TermKind::Declaration {
+                            id: new_ident.to_owned(),
+                            usages: Vec::new(),
+                            value: ValueState::Unknown,
+                            path: Some(path),
+                        },
                         metadata: Some(field.metadata),
                     });
                 }
+                self.bindings = Some(let_pattern_bindings);
             }
             Term::Let(ident, ..) | Term::Fun(ident, ..) => {
                 let value_ptr = match term {
                     Term::Let(..) => {
-                        self.let_binding = Some(id);
+                        self.bindings = Some(vec![id]);
                         ValueState::Unknown
                     }
                     Term::Fun(..) => {
@@ -286,10 +315,18 @@ impl<'a> Linearizer for AnalysisHost<'a> {
                     },
                 );
                 let kind = match term {
-                    Term::Let(..) => TermKind::Declaration(ident.to_owned(), Vec::new(), value_ptr),
-                    Term::Fun(..) => {
-                        TermKind::Declaration(ident.to_owned(), Vec::new(), ValueState::Unknown)
-                    }
+                    Term::Let(..) => TermKind::Declaration {
+                        id: ident.to_owned(),
+                        usages: Vec::new(),
+                        value: value_ptr,
+                        path: None,
+                    },
+                    Term::Fun(..) => TermKind::Declaration {
+                        id: ident.to_owned(),
+                        usages: Vec::new(),
+                        value: ValueState::Unknown,
+                        path: None,
+                    },
                     _ => unreachable!(),
                 };
                 lin.push(LinearizationItem {
@@ -316,17 +353,18 @@ impl<'a> Linearizer for AnalysisHost<'a> {
                 );
 
                 let key = ident.to_owned();
+                let pointed = self.env.get(&key).copied();
                 lin.push(LinearizationItem {
                     env: self.env.clone(),
                     id: root_id,
                     pos: ident.pos,
                     ty: UnifType::Concrete(TypeF::Dyn),
-                    kind: TermKind::Usage(UsageState::from(self.env.get(&key).copied())),
+                    kind: TermKind::Usage(UsageState::from(pointed)),
                     metadata: self.meta.take(),
                 });
 
-                if let Some(referenced) = self.env.get(&key) {
-                    lin.add_usage(self.file, *referenced, root_id)
+                if let Some(referenced) = pointed {
+                    lin.add_usage(self.file, referenced, root_id)
                 }
 
                 if let Some(chain) = self.access.take() {
@@ -567,7 +605,7 @@ impl<'a> Linearizer for AnalysisHost<'a> {
             record_fields: self.record_fields.as_mut().and_then(|(record, fields)| {
                 Some(*record).zip(fields.pop().map(|field| vec![field]))
             }),
-            let_binding: self.let_binding.take(),
+            bindings: self.bindings.take(),
             access: self.access.clone(),
         }
     }
@@ -584,7 +622,7 @@ impl<'a> Linearizer for AnalysisHost<'a> {
             // this new scope gets a cleared stated.
             meta: None,
             record_fields: None,
-            let_binding: None,
+            bindings: None,
             access: None,
         }
     }
diff --git a/lsp/nls/src/requests/completion.rs b/lsp/nls/src/requests/completion.rs
index a3d3509f..da44dd36 100644
--- a/lsp/nls/src/requests/completion.rs
+++ b/lsp/nls/src/requests/completion.rs
@@ -166,13 +166,24 @@ fn find_fields_from_term_kind(
                 }
             }
         }
+        TermKind::Declaration {
+            value: ValueState::Known(body_id),
+            path: Some(ref idents),
+            ..
+        } => {
+            for ident in idents {
+                path.push(ident.clone())
+            }
+            find_fields_from_term_kind(body_id, path, &info)
+        }
         TermKind::RecordField {
             value: ValueState::Known(new_id),
             ..
         }
-        | TermKind::Declaration(_, _, ValueState::Known(new_id)) => {
-            find_fields_from_term_kind(new_id, path, info)
-        }
+        | TermKind::Declaration {
+            value: ValueState::Known(new_id),
+            ..
+        } => find_fields_from_term_kind(new_id, path, info),
         TermKind::Usage(UsageState::Resolved(new_id)) => {
             find_fields_from_term_kind(new_id, path, info)
         }
@@ -197,7 +208,10 @@ fn find_fields_from_contract(
     match &item.metadata {
         Some(metadata) => find_fields_from_contracts(&metadata.annotation, path, info),
         None => match item.kind {
-            TermKind::Declaration(_, _, ValueState::Known(new_id))
+            TermKind::Declaration {
+                value: ValueState::Known(new_id),
+                ..
+            }
             | TermKind::RecordField {
                 value: ValueState::Known(new_id),
                 ..
@@ -440,7 +454,23 @@ fn collect_record_info(
                 // Get record fields from static type info
                 (_, Types(TypeF::Record(rrows))) => find_fields_from_rrows(&rrows, path, &info),
                 (
-                    TermKind::Declaration(_, _, ValueState::Known(body_id))
+                    TermKind::Declaration {
+                        value: ValueState::Known(body_id),
+                        path: Some(idents),
+                        ..
+                    },
+                    _,
+                ) => {
+                    for ident in idents {
+                        path.push(ident.clone())
+                    }
+                    find_fields_from_term_kind(*body_id, path, &info)
+                }
+                (
+                    TermKind::Declaration {
+                        value: ValueState::Known(body_id),
+                        ..
+                    }
                     | TermKind::RecordField {
                         value: ValueState::Known(body_id),
                         ..
@@ -599,7 +629,7 @@ fn get_completion_identifiers(
                     .get_in_scope(item, &server.lin_cache)
                     .iter()
                     .filter_map(|i| match i.kind {
-                        TermKind::Declaration(ident, _, _)
+                        TermKind::Declaration { id: ident, .. }
                         | TermKind::RecordField { ident, .. } => Some(IdentWithType {
                             ident,
                             meta: item.metadata.clone(),
@@ -903,11 +933,12 @@ mod tests {
 
         let a = make_lin_item(
             ItemId { file_id, index: 0 },
-            TermKind::Declaration(
-                Ident::from("a"),
-                vec![ItemId { file_id, index: 3 }],
-                ValueState::Known(ItemId { file_id, index: 1 }),
-            ),
+            TermKind::Declaration {
+                id: Ident::from("a"),
+                usages: vec![ItemId { file_id, index: 3 }],
+                value: ValueState::Known(ItemId { file_id, index: 1 }),
+                path: None,
+            },
             None,
         );
         let b = make_lin_item(
@@ -922,11 +953,12 @@ mod tests {
         let c = make_lin_item(ItemId { file_id, index: 2 }, TermKind::Structure, None);
         let d = make_lin_item(
             ItemId { file_id, index: 3 },
-            TermKind::Declaration(
-                Ident::from("d"),
-                Vec::new(),
-                ValueState::Known(ItemId { file_id, index: 4 }),
-            ),
+            TermKind::Declaration {
+                id: Ident::from("d"),
+                usages: Vec::new(),
+                value: ValueState::Known(ItemId { file_id, index: 4 }),
+                path: None,
+            },
             None,
         );
         let e = make_lin_item(
@@ -948,11 +980,12 @@ mod tests {
 
         let a = make_lin_item(
             ItemId { file_id, index: 0 },
-            TermKind::Declaration(
-                Ident::from("a"),
-                Vec::new(),
-                ValueState::Known(ItemId { file_id, index: 1 }),
-            ),
+            TermKind::Declaration {
+                id: Ident::from("a"),
+                usages: Vec::new(),
+                value: ValueState::Known(ItemId { file_id, index: 1 }),
+                path: None,
+            },
             None,
         );
         let b = make_lin_item(
@@ -968,29 +1001,32 @@ mod tests {
         let c = make_lin_item(ItemId { file_id, index: 2 }, TermKind::Structure, None);
         let d = make_lin_item(
             ItemId { file_id, index: 3 },
-            TermKind::Declaration(
-                Ident::from("d"),
-                Vec::new(),
-                ValueState::Known(ItemId { file_id, index: 13 }),
-            ),
+            TermKind::Declaration {
+                id: Ident::from("d"),
+                usages: Vec::new(),
+                value: ValueState::Known(ItemId { file_id, index: 13 }),
+                path: None,
+            },
             None,
         );
         let e = make_lin_item(
             ItemId { file_id, index: 4 },
-            TermKind::Declaration(
-                Ident::from("e"),
-                Vec::new(),
-                ValueState::Known(ItemId { file_id, index: 14 }),
-            ),
+            TermKind::Declaration {
+                id: Ident::from("e"),
+                usages: Vec::new(),
+                value: ValueState::Known(ItemId { file_id, index: 14 }),
+                path: None,
+            },
             None,
         );
         let f = make_lin_item(
             ItemId { file_id, index: 5 },
-            TermKind::Declaration(
-                Ident::from("f"),
-                Vec::new(),
-                ValueState::Known(ItemId { file_id, index: 15 }),
-            ),
+            TermKind::Declaration {
+                id: Ident::from("f"),
+                usages: Vec::new(),
+                value: ValueState::Known(ItemId { file_id, index: 15 }),
+                path: None,
+            },
             None,
         );
         let g = make_lin_item(
@@ -1034,11 +1070,12 @@ mod tests {
         let id = ItemId { file_id, index: 0 };
         let a = make_lin_item(
             id,
-            TermKind::Declaration(
-                Ident::from("a"),
-                Vec::new(),
-                ValueState::Known(ItemId { file_id, index: 1 }),
-            ),
+            TermKind::Declaration {
+                id: Ident::from("a"),
+                usages: Vec::new(),
+                value: ValueState::Known(ItemId { file_id, index: 1 }),
+                path: None,
+            },
             None,
         );
 
@@ -1049,6 +1086,7 @@ mod tests {
                 contracts: Vec::new(),
             },
             opt: false,
+            not_exported: false,
             priority: MergePriority::Neutral,
         };
 
diff --git a/lsp/nls/src/requests/goto.rs b/lsp/nls/src/requests/goto.rs
index 39ac483b..6f33456c 100644
--- a/lsp/nls/src/requests/goto.rs
+++ b/lsp/nls/src/requests/goto.rs
@@ -123,7 +123,7 @@ pub fn handle_to_usages(
     debug!("found referencing item: {:?}", item);
 
     let locations: Option<Vec<Location>> = match &item.kind {
-        TermKind::Declaration(_, usages, _) | TermKind::RecordField { usages, .. } => Some(
+        TermKind::Declaration { usages, .. } | TermKind::RecordField { usages, .. } => Some(
             usages
                 .iter()
                 .filter_map(|reference_id| {
diff --git a/lsp/nls/src/requests/symbols.rs b/lsp/nls/src/requests/symbols.rs
index c2291857..97539cdd 100644
--- a/lsp/nls/src/requests/symbols.rs
+++ b/lsp/nls/src/requests/symbols.rs
@@ -25,7 +25,7 @@ pub fn handle_document_symbols(
             .linearization
             .iter()
             .filter_map(|item| match &item.kind {
-                TermKind::Declaration(name, _, _) => {
+                TermKind::Declaration { id: name, .. } => {
                     // TODO: can `unwrap` fail here?
                     let (file_id, span) = item.pos.unwrap().to_range();
 
diff --git a/src/cache.rs b/src/cache.rs
index 96f95e49..578815c0 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -658,15 +658,21 @@ impl Cache {
     /// nothing if the imports of the entry have already been resolved. Require that the
     /// corresponding source has been parsed.
     /// If resolved imports contain imports themselves, resolve them recursively.
-    /// It returns a `Vec<FileId>`, which is a vector of all the imports that were transitively resolved.
+    /// It returns a two vectors which contain all the imports that were transitively resolved, and
+    /// all the errors it encountered while resolving imports in `file_id`, respectively. It only
+    ///  accumulates errors if the `Cache` is in error tolerant mode, otherwise it returns an `Err(..)`
+    /// containing  a `CacheError`
+    #[allow(clippy::type_complexity)]
     pub fn resolve_imports(
         &mut self,
         file_id: FileId,
-    ) -> Result<CacheOp<Vec<FileId>>, CacheError<ImportError>> {
+    ) -> Result<CacheOp<(Vec<FileId>, Vec<ImportError>)>, CacheError<ImportError>> {
         match self.entry_state(file_id) {
-            Some(state) if state >= EntryState::ImportsResolved => Ok(CacheOp::Cached(Vec::new())),
+            Some(state) if state >= EntryState::ImportsResolved => {
+                Ok(CacheOp::Cached((Vec::new(), Vec::new())))
+            }
             Some(state) if state >= EntryState::Parsed => {
-                let pending = if state < EntryState::ImportsResolving {
+                let (pending, errors) = if state < EntryState::ImportsResolving {
                     let CachedTerm {
                         term, parse_errs, ..
                     } = self.terms.get(&file_id).unwrap();
@@ -677,7 +683,21 @@ impl Cache {
                     // put it back when done, but to get a reference and clone it.
                     let term = term.clone();
                     let parse_errs = parse_errs.clone();
-                    let (term, pending) = import_resolution::resolve_imports(term, self)?;
+                    let (term, pending, errors) = match self.error_tolerance {
+                        ErrorTolerance::Tolerant => {
+                            let import_resolution::tolerant::ResolveResult {
+                                transformed_term,
+                                resolved_ids,
+                                import_errors,
+                            } = import_resolution::tolerant::resolve_imports(term, self);
+                            (transformed_term, resolved_ids, import_errors)
+                        }
+                        ErrorTolerance::Strict => {
+                            let (term, pending) =
+                                import_resolution::strict::resolve_imports(term, self)?;
+                            (term, pending, Vec::new())
+                        }
+                    };
 
                     self.terms.insert(
                         file_id,
@@ -688,28 +708,31 @@ impl Cache {
                         },
                     );
 
-                    pending
-                        .iter()
-                        .flat_map(|id| {
-                            if let Ok(CacheOp::Done(mut ps)) = self.resolve_imports(*id) {
-                                ps.push(*id);
-                                ps
-                            } else {
-                                Vec::new()
-                            }
-                        })
-                        .collect()
+                    (
+                        pending
+                            .iter()
+                            .flat_map(|id| {
+                                if let Ok(CacheOp::Done((mut ps, _))) = self.resolve_imports(*id) {
+                                    ps.push(*id);
+                                    ps
+                                } else {
+                                    Vec::new()
+                                }
+                            })
+                            .collect(),
+                        errors,
+                    )
                 } else {
                     let pending = self.imports.get(&file_id).cloned().unwrap_or_default();
 
                     for id in &pending {
                         self.resolve_imports(*id)?;
                     }
-                    pending.into_iter().collect()
+                    (pending.into_iter().collect(), Vec::new())
                 };
 
                 self.update_state(file_id, EntryState::ImportsResolved);
-                Ok(CacheOp::Done(pending))
+                Ok(CacheOp::Done((pending, errors)))
             }
             _ => Err(CacheError::NotParsed),
         }
@@ -779,7 +802,7 @@ impl Cache {
         if errs.no_errors() {
             return Err(Error::ParseErrors(errs));
         }
-        let (term, pending) = import_resolution::resolve_imports(term, self)?;
+        let (term, pending) = import_resolution::strict::resolve_imports(term, self)?;
         let wildcards = type_check(&term, initial_ctxt.clone(), self)?;
         let term = transform::transform(term, Some(&wildcards))
             .map_err(|err| Error::ParseErrors(err.into()))?;
diff --git a/src/destruct.rs b/src/destruct.rs
index 7f0aa73b..cbb0227b 100644
--- a/src/destruct.rs
+++ b/src/destruct.rs
@@ -130,4 +130,48 @@ impl Match {
             }
         }
     }
+
+    /// Returns info about each variable bound in a particular pattern.
+    /// It also tells the "path" to the bound variable; this is just the
+    /// record field names traversed to get to a pattern.
+    pub fn as_flattened_bindings(self) -> Vec<(Vec<Ident>, Option<Ident>, Field)> {
+        match self {
+            Match::Simple(id, field) => vec![(vec![id], None, field)],
+            Match::Assign(id, field, (bind_id, Destruct::Empty)) => {
+                vec![(vec![id], bind_id, field)]
+            }
+            Match::Assign(
+                id,
+                mut field,
+                (bind_id, ref destruct @ Destruct::Record { ref matches, .. }),
+            ) => {
+                let destruct = destruct.clone();
+                let mut label = destruct.label();
+                label.span = RawSpan::fuse(id.pos.unwrap(), label.span).unwrap();
+                field
+                    .metadata
+                    .annotation
+                    .contracts
+                    .push(destruct.into_contract_with_lbl(label));
+
+                let inner = matches
+                    .iter()
+                    .flat_map(|m| m.clone().as_flattened_bindings())
+                    .map(|(mut path, bind, field)| {
+                        path.push(id);
+                        (path, bind, field)
+                    });
+
+                // We only want to return the outer if we actually have a variable name
+                // bound to it.
+                let outer = if bind_id.is_some() {
+                    vec![(vec![id], bind_id, field)]
+                } else {
+                    Vec::new()
+                };
+
+                inner.chain(outer).collect()
+            }
+        }
+    }
 }
diff --git a/src/eval/merge.rs b/src/eval/merge.rs
index 3bf21520..db5a13df 100644
--- a/src/eval/merge.rs
+++ b/src/eval/merge.rs
@@ -432,6 +432,8 @@ fn merge_fields<'a, C: Cache, I: DoubleEndedIterator<Item = &'a Ident> + Clone>(
         // If one of the record requires this field, then it musn't be optional. The
         // resulting field is optional iff both are.
         opt: metadata1.opt && metadata2.opt,
+        // The resulting field will be suppressed from serialization if either of the fields to be merged is.
+        not_exported: metadata1.not_exported || metadata2.not_exported,
         priority,
     };
 
diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop
index 30c53afe..58238747 100644
--- a/src/parser/grammar.lalrpop
+++ b/src/parser/grammar.lalrpop
@@ -149,6 +149,10 @@ SimpleFieldAnnotAtom<TypeRule>: FieldMetadata = {
         opt: true,
         ..Default::default()
     },
+    "|" "not_exported" => FieldMetadata {
+        not_exported: true,
+        ..Default::default()
+    },
 }
 
 // A single field metadata annotation.
@@ -1009,6 +1013,7 @@ extern {
         "doc" => Token::Normal(NormalToken::Doc),
         "optional" => Token::Normal(NormalToken::Optional),
         "priority" => Token::Normal(NormalToken::Priority),
+        "not_exported" => Token::Normal(NormalToken::NotExported),
 
         "hash" => Token::Normal(NormalToken::OpHash),
         "serialize" => Token::Normal(NormalToken::Serialize),
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index c4fd2646..7b617811 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -263,6 +263,8 @@ pub enum NormalToken<'input> {
     Priority,
     #[token("force")]
     Force,
+    #[token("not_exported")]
+    NotExported,
 
     #[token("%hash%")]
     OpHash,
diff --git a/src/parser/uniterm.rs b/src/parser/uniterm.rs
index 9ad67120..1ced757c 100644
--- a/src/parser/uniterm.rs
+++ b/src/parser/uniterm.rs
@@ -189,6 +189,7 @@ impl UniRecord {
                                     contracts,
                                 },
                             opt: false,
+                            not_exported: false,
                             priority: MergePriority::Neutral,
                         },
                     // At this stage, this field should always be empty. It's a run-time thing, and
diff --git a/src/repl/mod.rs b/src/repl/mod.rs
index 8a3e0297..9c9cb430 100644
--- a/src/repl/mod.rs
+++ b/src/repl/mod.rs
@@ -134,7 +134,7 @@ impl<EC: EvalCache> ReplImpl<EC> {
             t: RichTerm,
         ) -> Result<RichTerm, Error> {
             let (t, pending) =
-                import_resolution::resolve_imports(t, repl_impl.vm.import_resolver_mut())?;
+                import_resolution::strict::resolve_imports(t, repl_impl.vm.import_resolver_mut())?;
             for id in &pending {
                 repl_impl
                     .vm
@@ -240,7 +240,7 @@ impl<EC: EvalCache> Repl for ReplImpl<EC> {
 
         let term = self.vm.import_resolver().get_owned(file_id).unwrap();
         let (term, pending) =
-            import_resolution::resolve_imports(term, self.vm.import_resolver_mut())?;
+            import_resolution::strict::resolve_imports(term, self.vm.import_resolver_mut())?;
         for id in &pending {
             self.vm.import_resolver_mut().resolve_imports(*id).unwrap();
         }
@@ -264,7 +264,7 @@ impl<EC: EvalCache> Repl for ReplImpl<EC> {
         // We ignore non fatal errors while type checking.
         let (term, _) = self.vm.import_resolver().parse_nocache(file_id)?;
         let (term, pending) =
-            import_resolution::resolve_imports(term, self.vm.import_resolver_mut())?;
+            import_resolution::strict::resolve_imports(term, self.vm.import_resolver_mut())?;
         for id in &pending {
             self.vm.import_resolver_mut().resolve_imports(*id).unwrap();
         }
diff --git a/src/serialize.rs b/src/serialize.rs
index e769def5..08261df8 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -103,7 +103,7 @@ where
     S: Serializer,
 {
     let mut entries = record
-        .iter_without_opts()
+        .iter_serializable()
         .collect::<Result<Vec<_>, _>>()
         .map_err(|missing_def_err| {
             Error::custom(format!(
@@ -195,7 +195,7 @@ pub fn validate(format: ExportFormat, t: &RichTerm) -> Result<(), SerializationE
             Null => Err(SerializationError::UnsupportedNull(format, t.clone())),
             Bool(_) | Num(_) | Str(_) | Enum(_) => Ok(()),
             Record(record) => {
-                record.iter_without_opts().try_for_each(|binding| {
+                record.iter_serializable().try_for_each(|binding| {
                     // unwrap(): terms must be fully evaluated before being validated for
                     // serialization. Otherwise, it's an internal error.
                     let (_, rt) = binding.unwrap_or_else(|err| panic!("encountered field without definition `{}` during pre-serialization validation", err.id));
@@ -426,6 +426,11 @@ mod tests {
             "{baz | default = {subfoo | default = !false} & {subbar | default = 1 - 1}}",
             json!({"baz": {"subfoo": true, "subbar": 0}})
         );
+
+        assert_json_eq!(
+            "{a = {b | default = {}} & {b.c | not_exported = false} & {b.d = true}}",
+            json!({"a": {"b": {"d": true}}})
+        );
     }
 
     #[test]
diff --git a/src/term/record.rs b/src/term/record.rs
index 0ab9e151..576d4eda 100644
--- a/src/term/record.rs
+++ b/src/term/record.rs
@@ -83,6 +83,8 @@ pub struct FieldMetadata {
     pub annotation: TypeAnnotation,
     /// If the field is optional.
     pub opt: bool,
+    /// If the field is serialized.
+    pub not_exported: bool,
     pub priority: MergePriority,
 }
 
@@ -133,6 +135,7 @@ impl FieldMetadata {
                 contracts: outer.annotation.contracts,
             },
             opt: outer.opt || inner.opt,
+            not_exported: outer.not_exported || inner.not_exported,
             priority,
         }
     }
@@ -359,22 +362,24 @@ impl RecordData {
             })
     }
 
-    /// Return an iterator over the fields' values, ignoring optional fields without
-    /// definition. Fields that aren't optional but yet don't have a definition are mapped to the
-    /// error `MissingFieldDefError`.
-    pub fn iter_without_opts(
+    /// Return an iterator over the fields' values, ignoring optional fields
+    /// without definition and fields marked as not_exported. Fields that
+    /// aren't optional but yet don't have a definition are mapped to the error
+    /// `MissingFieldDefError`.
+    pub fn iter_serializable(
         &self,
     ) -> impl Iterator<Item = Result<(&Ident, &RichTerm), MissingFieldDefError>> {
-        self.fields
-            .iter()
-            .filter_map(|(id, field)| match field.value {
-                Some(ref v) => Some(Ok((id, v))),
+        self.fields.iter().filter_map(|(id, field)| {
+            debug_assert!(field.pending_contracts.is_empty());
+            match field.value {
+                Some(ref v) if !field.metadata.not_exported => Some(Ok((id, v))),
                 None if !field.metadata.opt => Some(Err(MissingFieldDefError {
                     id: *id,
                     metadata: field.metadata.clone(),
                 })),
-                None => None,
-            })
+                _ => None,
+            }
+        })
     }
 
     /// Get the value of a field. Ignore optional fields without value: trying to get their value
diff --git a/src/transform/import_resolution.rs b/src/transform/import_resolution.rs
index 95ba1bfc..9f444822 100644
--- a/src/transform/import_resolution.rs
+++ b/src/transform/import_resolution.rs
@@ -3,7 +3,6 @@
 //! identifier directly.
 use super::ImportResolver;
 use crate::error::ImportError;
-use crate::term::{RichTerm, Term, Traverse, TraverseOrder};
 use codespan::FileId;
 use std::path::PathBuf;
 
@@ -14,67 +13,147 @@ struct ImportsResolutionState<'a, R> {
     resolver: &'a mut R,
     stack: &'a mut Vec<FileId>,
     parent: Option<PathBuf>,
+    unresolved: &'a mut Vec<ImportError>,
 }
 
-/// Perform imports resolution.
-///
-/// All resolved imports are stacked during the process. Once the term has been traversed,
-/// the elements of this stack are returned. The caller is responsible
-/// to recursively resolve imports of this stack and or to perform
-/// transformations on it.
-pub fn resolve_imports<R>(
-    rt: RichTerm,
-    resolver: &mut R,
-) -> Result<(RichTerm, Vec<FileId>), ImportError>
-where
-    R: ImportResolver,
-{
-    let mut stack = Vec::new();
+/// Performs import resolution, but return an error if any import terms cannot be resolved.
+pub mod strict {
+    use super::{tolerant::ResolveResult, ImportResolver};
+    use crate::error::ImportError;
+    use crate::term::RichTerm;
+    use codespan::FileId;
+    use std::path::PathBuf;
 
-    let source_file: Option<PathBuf> = rt.pos.as_opt_ref().map(|x| {
-        let path = resolver.get_path(x.src_id);
-        PathBuf::from(path)
-    });
+    /// Perform imports resolution.
+    ///
+    /// All resolved imports are stacked during the process. Once the term has been traversed,
+    /// the elements of this stack are returned. The caller is responsible
+    /// to recursively resolve imports of this stack and or to perform
+    /// transformations on it.
+    pub fn resolve_imports<R>(
+        rt: RichTerm,
+        resolver: &mut R,
+    ) -> Result<(RichTerm, Vec<FileId>), ImportError>
+    where
+        R: ImportResolver,
+    {
+        let ResolveResult {
+            transformed_term: term,
+            resolved_ids: resolved,
+            import_errors: errors,
+        } = super::tolerant::resolve_imports(rt, resolver);
+        match errors.first().cloned() {
+            Some(first) => Err(first),
+            None => Ok((term, resolved)),
+        }
+    }
 
-    let mut state = ImportsResolutionState {
-        resolver,
-        stack: &mut stack,
-        parent: source_file,
-    };
+    /// Resolve the import if the term is an unresolved import, or return the term unchanged. As
+    /// [`super::share_normal_form::transform_one`], this function is not recursive.
+    pub fn transform_one<R>(
+        rt: RichTerm,
+        resolver: &mut R,
+        parent: &Option<PathBuf>,
+    ) -> Result<RichTerm, ImportError>
+    where
+        R: ImportResolver,
+    {
+        let (rt, error) = super::tolerant::transform_one(rt, resolver, parent);
+        match error {
+            Some(err) => Err(err),
+            None => Ok(rt),
+        }
+    }
+}
 
-    // If an import is resolved, then stack it.
-    let transformed = rt.traverse(
-        &|rt: RichTerm, state: &mut ImportsResolutionState<R>| -> Result<RichTerm, ImportError> {
-            let rt = transform_one(rt, state.resolver, &state.parent)?;
+/// Performs import resolution that cannot fail: it accumulates all errors and returns in
+/// along side with a (partially) resolved term.
+pub mod tolerant {
+    use super::ImportResolver;
+    use super::ImportsResolutionState;
+    use crate::error::ImportError;
+    use crate::term::{RichTerm, Term, Traverse, TraverseOrder};
+    use codespan::FileId;
+    use std::path::PathBuf;
 
-            if let Term::ResolvedImport(file_id) = rt.term.as_ref() {
-                state.stack.push(*file_id);
-            }
-            Ok(rt)
-        },
-        &mut state,
-        TraverseOrder::BottomUp,
-    )?;
+    /// The result of an error tolerant import resolution transformation.
+    /// `transformed_term` is the main result of the transformation, which may still contain unresolved import terms.
+    /// `resolved_ids` indicates the imports that were resolved correctly.
+    /// `import_errors` indicates the imports that could not be resolved.
+    pub struct ResolveResult {
+        pub transformed_term: RichTerm,
+        pub resolved_ids: Vec<FileId>,
+        pub import_errors: Vec<ImportError>,
+    }
 
-    Ok((transformed, stack))
-}
+    /// Performs imports resolution for import terms independently, in an error tolerant way.
+    pub fn resolve_imports<R>(rt: RichTerm, resolver: &mut R) -> ResolveResult
+    where
+        R: ImportResolver,
+    {
+        let mut stack = Vec::new();
+        let mut import_errors = Vec::new();
+
+        let source_file: Option<PathBuf> = rt.pos.as_opt_ref().map(|x| {
+            let path = resolver.get_path(x.src_id);
+            PathBuf::from(path)
+        });
+
+        let mut state = ImportsResolutionState {
+            resolver,
+            stack: &mut stack,
+            parent: source_file,
+            unresolved: &mut import_errors,
+        };
+
+        // If an import is resolved, then stack it.
+        let transformed = rt
+            .traverse(
+                &|rt: RichTerm,
+                  state: &mut ImportsResolutionState<R>|
+                 -> Result<RichTerm, ImportError> {
+                    let (rt, err) = transform_one(rt, state.resolver, &state.parent);
+                    if let Some(err) = err {
+                        state.unresolved.push(err);
+                    }
+
+                    if let Term::ResolvedImport(file_id) = rt.term.as_ref() {
+                        state.stack.push(*file_id);
+                    }
+                    Ok(rt)
+                },
+                &mut state,
+                TraverseOrder::BottomUp,
+            )
+            // This will always succeed, becuase we always return `Ok(..)`, and
+            // we don't use the `?` operator
+            .unwrap();
+
+        ResolveResult {
+            transformed_term: transformed,
+            resolved_ids: stack,
+            import_errors,
+        }
+    }
 
-/// Resolve the import if the term is an unresolved import, or return the term unchanged. As
-/// [`super::share_normal_form::transform_one`], this function is not recursive.
-pub fn transform_one<R>(
-    rt: RichTerm,
-    resolver: &mut R,
-    parent: &Option<PathBuf>,
-) -> Result<RichTerm, ImportError>
-where
-    R: ImportResolver,
-{
-    let term = rt.as_ref();
-    match term {
-        Term::Import(path) => {
-            let (_, file_id) = resolver.resolve(path, parent.clone(), &rt.pos)?;
-            Ok(RichTerm::new(Term::ResolvedImport(file_id), rt.pos))
+    /// Try to resolve an import if the term is an unresolved import, and return
+    /// back the a potentially resolved richterm and an import error if it couldn't
+    /// be unresolved import could not be resolved.
+    pub fn transform_one<R>(
+        rt: RichTerm,
+        resolver: &mut R,
+        parent: &Option<PathBuf>,
+    ) -> (RichTerm, Option<ImportError>)
+    where
+        R: ImportResolver,
+    {
+        let term = rt.as_ref();
+        match term {
+            Term::Import(path) => match resolver.resolve(path, parent.clone(), &rt.pos) {
+                Ok((_, file_id)) => (RichTerm::new(Term::ResolvedImport(file_id), rt.pos), None),
+                Err(err) => (rt, Some(err)),
+            },
+            _ => (rt, None),
         }
-        _ => Ok(rt),
     }
 }
diff --git a/src/transform/mod.rs b/src/transform/mod.rs
index 850bbecb..1f05b326 100644
--- a/src/transform/mod.rs
+++ b/src/transform/mod.rs
@@ -19,7 +19,7 @@ pub mod substitute_wildcards;
 /// earlier, as it needs to be done before typechecking.
 ///
 /// Do not perform transformations on the imported files. If needed, either do it yourself using
-/// pending imports returned by [`resolve_imports`][import_resolution::resolve_imports] or use the
+/// pending imports returned by [`resolve_imports`][import_resolution::strict::resolve_imports] or use the
 /// [cache][crate::cache::Cache].
 pub fn transform(
     mut rt: RichTerm,
diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs
index d88472c7..dcba4d1f 100644
--- a/utilities/src/lib.rs
+++ b/utilities/src/lib.rs
@@ -151,7 +151,8 @@ pub fn bench_terms<'r>(rts: Vec<Bench<'r>>) -> Box<dyn Fn(&mut Criterion) + 'r>
                         let mut cache = cache.clone();
                         let id = cache.add_file(bench.path()).unwrap();
                         let (t, _) =
-                            import_resolution::resolve_imports(t.clone(), &mut cache).unwrap();
+                            import_resolution::strict::resolve_imports(t.clone(), &mut cache)
+                                .unwrap();
                         (cache, id, t)
                     },
                     |(mut c_local, id, t)| {
@@ -205,7 +206,7 @@ macro_rules! ncl_bench_group {
             use nickel_lang::{
                 cache::{Envs, Cache, ErrorTolerance, ImportResolver},
                 eval::{VirtualMachine, cache::Cache as EvalCache},
-                transform::import_resolution::resolve_imports,
+                transform::import_resolution::strict::resolve_imports,
             };
 
             let mut c: criterion::Criterion<_> = $config


Test patch:
diff --git a/src/eval/tests.rs b/src/eval/tests.rs
index 6ef6bacf..c9d30d9f 100644
--- a/src/eval/tests.rs
+++ b/src/eval/tests.rs
@@ -6,7 +6,7 @@ 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::transform::import_resolution::strict::resolve_imports;
 use crate::{mk_app, mk_fun};
 use codespan::Files;