Repository: tweag/nickel
Pull request #984 (2022-12-08 14:50:28)

Reorganize term module and submodules
Preliminary refactoring before proceeding with the implementation of RFC005. Initially term was a flat module with everything defined inside. But with changes like array slices or record sealing, the `array` and `record` submodule were created. However, it felt like a lot of things still in `term` could be pushed into those modules as well, such as `ArrayAttrs` or `RecordDeps`.

This PR put things purely array-related or record-related to the corresponding submodule of `term`. As the file is quite big, we've also split it into smaller files, using a directory hierarchy for the `term` module.


Patch:
diff --git a/benches/arrays.rs b/benches/arrays.rs
index 896d90f5..32e36ee5 100644
--- a/benches/arrays.rs
+++ b/benches/arrays.rs
@@ -1,7 +1,10 @@
 use std::rc::Rc;
 
 use criterion::{criterion_main, Criterion};
-use nickel_lang::term::{array::Array, ArrayAttrs, RichTerm, Term};
+use nickel_lang::term::{
+    array::{Array, ArrayAttrs},
+    RichTerm, Term,
+};
 use nickel_lang_utilities::{ncl_bench_group, EvalMode};
 use pprof::criterion::{Output, PProfProfiler};
 use pretty::{BoxAllocator, DocBuilder, Pretty};
diff --git a/src/destruct.rs b/src/destruct.rs
index 3c847ef1..00c050a1 100644
--- a/src/destruct.rs
+++ b/src/destruct.rs
@@ -5,7 +5,7 @@ use crate::identifier::Ident;
 use crate::label::Label;
 use crate::position::RawSpan;
 use crate::term::record::RecordData;
-use crate::term::{Contract, MetaValue, RecordAttrs, RichTerm, Term};
+use crate::term::{record::RecordAttrs, Contract, MetaValue, RichTerm, Term};
 use crate::types::{TypeF, Types};
 
 /// A match field in a `Destruct` pattern.
diff --git a/src/eval/lazy.rs b/src/eval/lazy.rs
index a65053a1..a7896253 100644
--- a/src/eval/lazy.rs
+++ b/src/eval/lazy.rs
@@ -2,7 +2,7 @@
 use super::{cache::CacheIndex, Closure, Environment, IdentKind};
 use crate::{
     identifier::Ident,
-    term::{FieldDeps, RichTerm, Term},
+    term::{record::FieldDeps, RichTerm, Term},
 };
 use std::cell::{Ref, RefCell, RefMut};
 use std::rc::{Rc, Weak};
diff --git a/src/eval/merge.rs b/src/eval/merge.rs
index 58b7f94b..a7aef200 100644
--- a/src/eval/merge.rs
+++ b/src/eval/merge.rs
@@ -57,8 +57,9 @@ use crate::label::Label;
 use crate::position::TermPos;
 use crate::term::record::{self, RecordData};
 use crate::term::{
-    make as mk_term, BinaryOp, Contract, FieldDeps, MetaValue, RecordAttrs, RichTerm, SharedTerm,
-    Term,
+    make as mk_term,
+    record::{FieldDeps, RecordAttrs},
+    BinaryOp, Contract, MetaValue, RichTerm, SharedTerm, Term,
 };
 use crate::transform::Closurizable;
 use std::collections::HashMap;
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index b3db913f..48f28300 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -96,7 +96,7 @@ use crate::{
     identifier::Ident,
     match_sharedterm,
     term::{
-        make as mk_term, record::RecordData, ArrayAttrs, BinaryOp, BindingType, LetAttrs,
+        array::ArrayAttrs, make as mk_term, record::RecordData, BinaryOp, BindingType, LetAttrs,
         MetaValue, PendingContract, RichTerm, SharedTerm, StrChunk, Term, UnaryOp,
     },
     transform::Closurizable,
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index 60b79d97..9a94870b 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -11,14 +11,9 @@ use super::{
     merge::{merge, MergeMode},
     subst, Closure, Environment, ImportResolver, VirtualMachine,
 };
-use crate::term::{record::RecordData, MergePriority};
-use crate::{
-    error::IllegalPolymorphicTailAction,
-    term::{record, MetaValue},
-};
 
 use crate::{
-    error::EvalError,
+    error::{EvalError, IllegalPolymorphicTailAction},
     eval,
     eval::Cache,
     identifier::Ident,
@@ -29,8 +24,11 @@ use crate::{
     serialize::ExportFormat,
     stdlib::internals,
     term::{
-        array::Array, make as mk_term, ArrayAttrs, BinaryOp, NAryOp, PendingContract, RichTerm,
-        SharedTerm, StrChunk, Term, UnaryOp,
+        array::{Array, ArrayAttrs},
+        make as mk_term,
+        record::{self, RecordData},
+        BinaryOp, MergePriority, MetaValue, NAryOp, PendingContract, RichTerm, SharedTerm,
+        StrChunk, Term, UnaryOp,
     },
     transform::{apply_contracts::apply_contracts, Closurizable},
 };
diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop
index 16d0a177..7fcf877e 100644
--- a/src/parser/grammar.lalrpop
+++ b/src/parser/grammar.lalrpop
@@ -59,7 +59,7 @@ use crate::{
     destruct::{Match, LastMatch, Destruct},
     term::{
         BinaryOp, RichTerm, Term, UnaryOp, StrChunk, MetaValue,
-        MergePriority, Contract, NAryOp, RecordAttrs, SharedTerm,
+        MergePriority, Contract, NAryOp, record::RecordAttrs, SharedTerm,
         NumeralPriority, array::Array, make as mk_term,
     },
     types::{
diff --git a/src/parser/uniterm.rs b/src/parser/uniterm.rs
index 57b6b641..96b61578 100644
--- a/src/parser/uniterm.rs
+++ b/src/parser/uniterm.rs
@@ -6,7 +6,7 @@ use utils::{build_record, elaborate_field_path, FieldPath, FieldPathElem};
 use crate::{
     environment::Environment,
     position::{RawSpan, TermPos},
-    term::{Contract, MergePriority, MetaValue, RecordAttrs, RichTerm, SharedTerm, Term},
+    term::{record::RecordAttrs, Contract, MergePriority, MetaValue, RichTerm, SharedTerm, Term},
     types::{
         EnumRows, EnumRowsIteratorItem, RecordRow, RecordRows, RecordRowsF, TypeF, Types,
         UnboundTypeVariableError, VarKind,
diff --git a/src/parser/utils.rs b/src/parser/utils.rs
index 52ff87b7..46fc628f 100644
--- a/src/parser/utils.rs
+++ b/src/parser/utils.rs
@@ -17,8 +17,8 @@ use crate::{
     mk_app, mk_fun,
     position::{RawSpan, TermPos},
     term::{
-        make as mk_term, record::RecordData, BinaryOp, Contract, MetaValue, RecordAttrs, RichTerm,
-        StrChunk, Term, UnaryOp,
+        make as mk_term, record::RecordAttrs, record::RecordData, BinaryOp, Contract, MetaValue,
+        RichTerm, StrChunk, Term, UnaryOp,
     },
     types::{TypeF, Types},
 };
diff --git a/src/program.rs b/src/program.rs
index 0474faa4..d9c78999 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -367,7 +367,7 @@ mod tests {
     use crate::error::EvalError;
     use crate::eval::cache::CBNCache;
     use crate::position::TermPos;
-    use crate::term::ArrayAttrs;
+    use crate::term::array::ArrayAttrs;
     use assert_matches::assert_matches;
     use std::io::Cursor;
 
diff --git a/src/serialize.rs b/src/serialize.rs
index a2c2f138..ac03fc80 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -6,7 +6,11 @@ use crate::{
         cache::{CBNCache, Cache},
         is_empty_optional,
     },
-    term::{array::Array, record::RecordData, ArrayAttrs, MetaValue, RichTerm, Term},
+    term::{
+        array::{Array, ArrayAttrs},
+        record::RecordData,
+        MetaValue, RichTerm, Term,
+    },
 };
 
 use serde::{
diff --git a/src/term/array.rs b/src/term/array.rs
new file mode 100644
index 00000000..62ed77e9
--- /dev/null
+++ b/src/term/array.rs
@@ -0,0 +1,202 @@
+use std::mem::transmute;
+use std::mem::ManuallyDrop;
+
+use super::*;
+
+#[derive(Debug, Default, PartialEq, Clone)]
+pub struct ArrayAttrs {
+    /// A `closurized` array verifies the following conditions:
+    ///   - Each element is a generated variable with a unique name (although the same
+    ///     variable can occur in several places, it should always refer to the same thunk anyway).
+    ///   - The environment of the array's closure only contains those generated variables.
+    pub closurized: bool,
+    /// List of lazily-applied contracts.
+    /// These are only observed when data enters or leaves the array.
+    pub pending_contracts: Vec<PendingContract>,
+}
+
+impl ArrayAttrs {
+    /// Create an `ArrayAttrs`.
+    /// By default, the `closurized` flag is set to `false`,
+    /// and `pending_contracts` is empty.
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    /// Set the `closurized` flag to `true`.
+    pub fn closurized(mut self) -> Self {
+        self.closurized = true;
+        self
+    }
+
+    /// Drop the pending contracts.
+    pub fn contracts_cleared(mut self) -> Self {
+        self.pending_contracts.clear();
+        self
+    }
+
+    /// Extend contracts from an iterator of `PendingContract`.
+    /// De-duplicate equal contracts. Note that current contract
+    /// equality testing is very limited, but this may change in the
+    /// future
+    pub fn with_extra_contracts<I>(mut self, iter: I) -> Self
+    where
+        I: IntoIterator<Item = PendingContract>,
+    {
+        for ctr in iter {
+            if !self.pending_contracts.contains(&ctr) {
+                self.pending_contracts.push(ctr)
+            }
+        }
+
+        self
+    }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Array {
+    inner: Rc<[RichTerm]>,
+    start: usize,
+    end: usize,
+}
+
+impl Array {
+    /// Creates a Nickel array from reference-counted slice.
+    pub fn new(inner: Rc<[RichTerm]>) -> Self {
+        let start = 0;
+        let end = inner.len();
+
+        Self { inner, start, end }
+    }
+
+    /// Returns the effective length of the array.
+    pub fn len(&self) -> usize {
+        self.end - self.start
+    }
+
+    /// Returns `true` if the array is empty.
+    pub fn is_empty(&self) -> bool {
+        self.end == self.start
+    }
+
+    /// Returns a reference to the term at the given index.
+    pub fn get(&self, idx: usize) -> Option<&RichTerm> {
+        self.inner.get(self.start + idx)
+    }
+
+    /// Discards the first `diff` terms of the array.
+    pub fn advance_by(mut self, diff: usize) -> Self {
+        self.start += usize::min(diff, self.len());
+        self
+    }
+
+    /// Makes a mutable slice into the given `Array`.
+    pub fn make_mut(&mut self) -> &mut [RichTerm] {
+        // NOTE: trying to use `Rc::make_mut` will result in the following compiler error:
+        // > the trait `Clone` is not implemented for `[term::RichTerm]`
+        // This seems to be an edge-case in the standard library.
+        // As a workaround, if `get_mut` fails we recollect the array into a new `Rc` by cloning all terms.
+        // Thus we make sure that the second call to `get_mut` won't fail.
+
+        // This condition is the same as `!Rc::is_unique(&mut self.inner)`, but that function
+        // is not public.
+        if Rc::strong_count(&self.inner) != 1 || Rc::weak_count(&self.inner) != 0 {
+            self.inner = self.iter().cloned().collect::<Rc<[_]>>();
+        }
+
+        Rc::get_mut(&mut self.inner).expect("non-unique Rc after deep-cloning Array")
+    }
+
+    /// Returns an iterator of references over the array.
+    pub fn iter(&self) -> std::slice::Iter<'_, RichTerm> {
+        self.as_ref().iter()
+    }
+}
+
+impl Default for Array {
+    fn default() -> Self {
+        Self::new(Rc::new([]))
+    }
+}
+
+impl FromIterator<RichTerm> for Array {
+    fn from_iter<T: IntoIterator<Item = RichTerm>>(iter: T) -> Self {
+        let inner = iter.into_iter().collect::<Rc<[_]>>();
+        let start = 0;
+        let end = inner.len();
+
+        Self { inner, start, end }
+    }
+}
+
+impl AsRef<[RichTerm]> for Array {
+    fn as_ref(&self) -> &[RichTerm] {
+        &self.inner[self.start..self.end]
+    }
+}
+
+impl IntoIterator for Array {
+    type Item = RichTerm;
+
+    type IntoIter = IntoIter;
+
+    fn into_iter(self) -> Self::IntoIter {
+        // SAFETY: If there are no other strong or weak references
+        // to the inner slice, then we can:
+        // - Drop the elements outside our inner view,
+        // - Move out the elements inside out inner view.
+        // Otherwise, we clone everything.
+
+        unsafe {
+            let mut inner: Rc<[ManuallyDrop<RichTerm>]> = transmute(self.inner);
+            let idx = self.start;
+            let end = self.end;
+
+            if let Some(slice) = Rc::get_mut(&mut inner) {
+                for term in &mut slice[..idx] {
+                    ManuallyDrop::drop(term)
+                }
+                for term in &mut slice[end..] {
+                    ManuallyDrop::drop(term)
+                }
+            }
+
+            IntoIter { inner, idx, end }
+        }
+    }
+}
+
+pub struct IntoIter {
+    inner: Rc<[ManuallyDrop<RichTerm>]>,
+    idx: usize,
+    end: usize,
+}
+
+impl Iterator for IntoIter {
+    type Item = RichTerm;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.idx == self.end {
+            None
+        } else {
+            let term = match Rc::get_mut(&mut self.inner) {
+                None => self.inner[..self.end]
+                    .get(self.idx)
+                    .cloned()
+                    .map(ManuallyDrop::into_inner),
+                Some(slice) => slice[..self.end]
+                    .get_mut(self.idx)
+                    .map(|t| unsafe { ManuallyDrop::take(t) }),
+            };
+
+            self.idx += 1;
+            term
+        }
+    }
+}
+
+impl ExactSizeIterator for IntoIter {
+    fn len(&self) -> usize {
+        self.end - self.idx
+    }
+}
diff --git a/src/term.rs b/src/term/mod.rs
similarity index 83%
rename from src/term.rs
rename to src/term/mod.rs
index d0331100..b51d858f 100644
--- a/src/term.rs
+++ b/src/term/mod.rs
@@ -16,7 +16,11 @@
 //! contracts, default values, documentation, etc. They bring such usually external object down to
 //! the term level, and together with [crate::eval::merge], they allow for flexible and modular
 //! definitions of contracts, record and metadata all together.
-use array::Array;
+
+pub mod array;
+pub mod record;
+
+use array::{Array, ArrayAttrs};
 
 use crate::{
     destruct::Destruct,
@@ -35,14 +39,14 @@ use serde::{Deserialize, Serialize};
 
 use std::{
     cmp::{Ordering, PartialOrd},
-    collections::{HashMap, HashSet},
+    collections::HashMap,
     ffi::OsString,
     fmt,
     ops::Deref,
     rc::Rc,
 };
 
-use self::record::RecordData;
+use record::{FieldDeps, RecordData, RecordDeps};
 
 /// The AST of a Nickel expression.
 ///
@@ -214,317 +218,6 @@ impl PendingContract {
     }
 }
 
-pub mod array {
-    use std::mem::transmute;
-    use std::mem::ManuallyDrop;
-
-    use super::*;
-
-    #[derive(Debug, Clone, PartialEq)]
-    pub struct Array {
-        inner: Rc<[RichTerm]>,
-        start: usize,
-        end: usize,
-    }
-
-    impl Array {
-        /// Creates a Nickel array from reference-counted slice.
-        pub fn new(inner: Rc<[RichTerm]>) -> Self {
-            let start = 0;
-            let end = inner.len();
-
-            Self { inner, start, end }
-        }
-
-        /// Returns the effective length of the array.
-        pub fn len(&self) -> usize {
-            self.end - self.start
-        }
-
-        /// Returns `true` if the array is empty.
-        pub fn is_empty(&self) -> bool {
-            self.end == self.start
-        }
-
-        /// Returns a reference to the term at the given index.
-        pub fn get(&self, idx: usize) -> Option<&RichTerm> {
-            self.inner.get(self.start + idx)
-        }
-
-        /// Discards the first `diff` terms of the array.
-        pub fn advance_by(mut self, diff: usize) -> Self {
-            self.start += usize::min(diff, self.len());
-            self
-        }
-
-        /// Makes a mutable slice into the given `Array`.
-        pub fn make_mut(&mut self) -> &mut [RichTerm] {
-            // NOTE: trying to use `Rc::make_mut` will result in the following compiler error:
-            // > the trait `Clone` is not implemented for `[term::RichTerm]`
-            // This seems to be an edge-case in the standard library.
-            // As a workaround, if `get_mut` fails we recollect the array into a new `Rc` by cloning all terms.
-            // Thus we make sure that the second call to `get_mut` won't fail.
-
-            // This condition is the same as `!Rc::is_unique(&mut self.inner)`, but that function
-            // is not public.
-            if Rc::strong_count(&self.inner) != 1 || Rc::weak_count(&self.inner) != 0 {
-                self.inner = self.iter().cloned().collect::<Rc<[_]>>();
-            }
-
-            Rc::get_mut(&mut self.inner).expect("non-unique Rc after deep-cloning Array")
-        }
-
-        /// Returns an iterator of references over the array.
-        pub fn iter(&self) -> std::slice::Iter<'_, RichTerm> {
-            self.as_ref().iter()
-        }
-    }
-
-    impl Default for Array {
-        fn default() -> Self {
-            Self::new(Rc::new([]))
-        }
-    }
-
-    impl FromIterator<RichTerm> for Array {
-        fn from_iter<T: IntoIterator<Item = RichTerm>>(iter: T) -> Self {
-            let inner = iter.into_iter().collect::<Rc<[_]>>();
-            let start = 0;
-            let end = inner.len();
-
-            Self { inner, start, end }
-        }
-    }
-
-    impl AsRef<[RichTerm]> for Array {
-        fn as_ref(&self) -> &[RichTerm] {
-            &self.inner[self.start..self.end]
-        }
-    }
-
-    impl IntoIterator for Array {
-        type Item = RichTerm;
-
-        type IntoIter = IntoIter;
-
-        fn into_iter(self) -> Self::IntoIter {
-            // SAFETY: If there are no other strong or weak references
-            // to the inner slice, then we can:
-            // - Drop the elements outside our inner view,
-            // - Move out the elements inside out inner view.
-            // Otherwise, we clone everything.
-
-            unsafe {
-                let mut inner: Rc<[ManuallyDrop<RichTerm>]> = transmute(self.inner);
-                let idx = self.start;
-                let end = self.end;
-
-                if let Some(slice) = Rc::get_mut(&mut inner) {
-                    for term in &mut slice[..idx] {
-                        ManuallyDrop::drop(term)
-                    }
-                    for term in &mut slice[end..] {
-                        ManuallyDrop::drop(term)
-                    }
-                }
-
-                IntoIter { inner, idx, end }
-            }
-        }
-    }
-
-    pub struct IntoIter {
-        inner: Rc<[ManuallyDrop<RichTerm>]>,
-        idx: usize,
-        end: usize,
-    }
-
-    impl Iterator for IntoIter {
-        type Item = RichTerm;
-
-        fn next(&mut self) -> Option<Self::Item> {
-            if self.idx == self.end {
-                None
-            } else {
-                let term = match Rc::get_mut(&mut self.inner) {
-                    None => self.inner[..self.end]
-                        .get(self.idx)
-                        .cloned()
-                        .map(ManuallyDrop::into_inner),
-                    Some(slice) => slice[..self.end]
-                        .get_mut(self.idx)
-                        .map(|t| unsafe { ManuallyDrop::take(t) }),
-                };
-
-                self.idx += 1;
-                term
-            }
-        }
-    }
-
-    impl ExactSizeIterator for IntoIter {
-        fn len(&self) -> usize {
-            self.end - self.idx
-        }
-    }
-}
-
-#[derive(Debug, Default, PartialEq, Clone)]
-pub struct ArrayAttrs {
-    /// A `closurized` array verifies the following conditions:
-    ///   - Each element is a generated variable with a unique name (although the same
-    ///     variable can occur in several places, it should always refer to the same thunk anyway).
-    ///   - The environment of the array's closure only contains those generated variables.
-    pub closurized: bool,
-    /// List of lazily-applied contracts.
-    /// These are only observed when data enters or leaves the array.
-    pub pending_contracts: Vec<PendingContract>,
-}
-
-impl ArrayAttrs {
-    /// Create an `ArrayAttrs`.
-    /// By default, the `closurized` flag is set to `false`,
-    /// and `pending_contracts` is empty.
-    pub fn new() -> Self {
-        Default::default()
-    }
-
-    /// Set the `closurized` flag to `true`.
-    pub fn closurized(mut self) -> Self {
-        self.closurized = true;
-        self
-    }
-
-    /// Drop the pending contracts.
-    pub fn contracts_cleared(mut self) -> Self {
-        self.pending_contracts.clear();
-        self
-    }
-
-    /// Extend contracts from an iterator of `PendingContract`.
-    /// De-duplicate equal contracts. Note that current contract
-    /// equality testing is very limited, but this may change in the
-    /// future
-    pub fn with_extra_contracts<I>(mut self, iter: I) -> Self
-    where
-        I: IntoIterator<Item = PendingContract>,
-    {
-        for ctr in iter {
-            if !self.pending_contracts.contains(&ctr) {
-                self.pending_contracts.push(ctr)
-            }
-        }
-
-        self
-    }
-}
-
-pub mod record {
-    use super::{RecordAttrs, RichTerm, SealingKey};
-    use crate::{identifier::Ident, label::Label};
-    use std::collections::HashMap;
-
-    /// The base structure of a Nickel record.
-    ///
-    /// Used to group together fields common to both the [super::Term::Record] and
-    /// [super::Term::RecRecord] terms.
-    #[derive(Clone, Debug, Default, PartialEq)]
-    pub struct RecordData {
-        /// Fields whose names are known statically.
-        pub fields: HashMap<Ident, RichTerm>,
-        /// Attributes which may be applied to a record.
-        pub attrs: RecordAttrs,
-        /// The hidden part of a record under a polymorphic contract.
-        pub sealed_tail: Option<SealedTail>,
-    }
-
-    impl RecordData {
-        pub fn new(
-            fields: HashMap<Ident, RichTerm>,
-            attrs: RecordAttrs,
-            sealed_tail: Option<SealedTail>,
-        ) -> Self {
-            RecordData {
-                fields,
-                attrs,
-                sealed_tail,
-            }
-        }
-
-        /// A record with no fields and the default set of attributes.
-        pub fn empty() -> Self {
-            Default::default()
-        }
-
-        /// A record with the provided fields & the default set of attributes.
-        pub fn with_fields(fields: HashMap<Ident, RichTerm>) -> Self {
-            let attrs = Default::default();
-            let sealed_tail = Default::default();
-            RecordData {
-                fields,
-                attrs,
-                sealed_tail,
-            }
-        }
-
-        /// Returns the record resulting from applying the provided function
-        /// to each field.
-        ///
-        /// Note that `f` is taken as `mut` in order to allow it to mutate
-        /// external state while iterating.
-        pub fn map_fields<F>(self, mut f: F) -> Self
-        where
-            F: FnMut(Ident, RichTerm) -> RichTerm,
-        {
-            let fields = self
-                .fields
-                .into_iter()
-                .map(|(id, t)| (id, f(id, t)))
-                .collect();
-            RecordData { fields, ..self }
-        }
-    }
-
-    /// The sealed tail of a Nickel record under a polymorphic contract.
-    ///
-    /// Note that access to the enclosed term must only be allowed when a matching sealing key is
-    /// provided. If this is not enforced it will lead to parametricity violations.
-    #[derive(Clone, Debug, PartialEq)]
-    pub struct SealedTail {
-        /// The key with which the tail is sealed.
-        sealing_key: SealingKey,
-        /// The label to which blame will be attributed if code tries to
-        /// interact with the sealed tail in any way.
-        pub label: Label,
-        /// The term which is sealed.
-        term: RichTerm,
-    }
-
-    impl SealedTail {
-        pub fn new(sealing_key: SealingKey, label: Label, term: RichTerm) -> Self {
-            Self {
-                sealing_key,
-                label,
-                term,
-            }
-        }
-
-        /// Returns the sealed term if the key matches, otherwise returns None.
-        pub fn unseal(&self, key: &SealingKey) -> Option<&RichTerm> {
-            if key == &self.sealing_key {
-                Some(&self.term)
-            } else {
-                None
-            }
-        }
-    }
-}
-
-#[derive(Debug, Default, Eq, PartialEq, Copy, Clone)]
-pub struct RecordAttrs {
-    pub open: bool,
-}
-
 /// The attributes of a let binding.
 #[derive(Debug, Default, Eq, PartialEq, Clone)]
 pub struct LetAttrs {
@@ -534,69 +227,6 @@ pub struct LetAttrs {
     pub rec: bool,
 }
 
-impl RecordAttrs {
-    pub fn merge(attrs1: RecordAttrs, attrs2: RecordAttrs) -> RecordAttrs {
-        RecordAttrs {
-            open: attrs1.open || attrs2.open,
-        }
-    }
-}
-
-/// Dependencies of a field or a thunk over the other recursive fields of a recursive record.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum FieldDeps {
-    /// The set of dependencies is fixed and has been computed. When attached to a thunk, an empty
-    /// set of dependency means that the thunk isn't revertible, but standard.
-    Known(Rc<HashSet<Ident>>),
-    /// The thunk is revertible, but the set of dependencies hasn't been computed. In that case,
-    /// the interpreter should be conservative and assume that any recursive references can appear
-    /// in the content of the corresponding thunk.
-    Unknown,
-}
-
-impl FieldDeps {
-    /// Compute the union of two thunk dependencies. [`FieldDeps::Unknown`] can be see as the top
-    /// element, meaning that if one of the two set of dependencies is [`FieldDeps::Unknown`], so
-    /// is the result.
-    pub fn union(self, other: Self) -> Self {
-        match (self, other) {
-            // If one of the field has unknown dependencies (understand: may depend on all the other
-            // fields), then the resulting fields has unknown dependencies as well
-            (FieldDeps::Unknown, _) | (_, FieldDeps::Unknown) => FieldDeps::Unknown,
-            (FieldDeps::Known(deps1), FieldDeps::Known(deps2)) => {
-                let union: HashSet<Ident> = deps1.union(&*deps2).cloned().collect();
-                FieldDeps::Known(Rc::new(union))
-            }
-        }
-    }
-
-    /// Return an empty set of dependencies
-    pub fn empty() -> Self {
-        FieldDeps::Known(Rc::new(HashSet::new()))
-    }
-
-    /// Return `true` if the dependencies are known and are empty, or `false` otherwise.
-    pub fn is_empty(&self) -> bool {
-        matches!(self, FieldDeps::Known(deps) if deps.is_empty())
-    }
-}
-
-impl From<HashSet<Ident>> for FieldDeps {
-    fn from(set: HashSet<Ident>) -> Self {
-        FieldDeps::Known(Rc::new(set))
-    }
-}
-
-/// Store field interdependencies in a recursive record. Map each static and dynamic field to the
-/// set of recursive fields that syntactically appears in their definition as free variables.
-#[derive(Debug, Default, Eq, PartialEq, Clone)]
-pub struct RecordDeps {
-    /// Must have exactly the same keys as the static fields map of the recursive record.
-    pub stat_fields: HashMap<Ident, FieldDeps>,
-    /// Must have exactly the same length as the dynamic fields list of the recursive record.
-    pub dyn_fields: Vec<FieldDeps>,
-}
-
 /// A wrapper around f64 which makes `NaN` not representable. As opposed to floats, it is `Eq` and
 /// `Ord`.
 #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
diff --git a/src/term/record.rs b/src/term/record.rs
new file mode 100644
index 00000000..27a84c09
--- /dev/null
+++ b/src/term/record.rs
@@ -0,0 +1,169 @@
+use super::{RichTerm, SealingKey};
+use crate::{identifier::Ident, label::Label};
+use std::{
+    collections::{HashMap, HashSet},
+    rc::Rc,
+};
+
+#[derive(Debug, Default, Eq, PartialEq, Copy, Clone)]
+pub struct RecordAttrs {
+    pub open: bool,
+}
+
+impl RecordAttrs {
+    pub fn merge(attrs1: RecordAttrs, attrs2: RecordAttrs) -> RecordAttrs {
+        RecordAttrs {
+            open: attrs1.open || attrs2.open,
+        }
+    }
+}
+
+/// Dependencies of a field or a thunk over the other recursive fields of a recursive record.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum FieldDeps {
+    /// The set of dependencies is fixed and has been computed. When attached to a thunk, an empty
+    /// set of dependency means that the thunk isn't revertible, but standard.
+    Known(Rc<HashSet<Ident>>),
+    /// The thunk is revertible, but the set of dependencies hasn't been computed. In that case,
+    /// the interpreter should be conservative and assume that any recursive references can appear
+    /// in the content of the corresponding thunk.
+    Unknown,
+}
+
+impl FieldDeps {
+    /// Compute the union of two thunk dependencies. [`FieldDeps::Unknown`] can be see as the top
+    /// element, meaning that if one of the two set of dependencies is [`FieldDeps::Unknown`], so
+    /// is the result.
+    pub fn union(self, other: Self) -> Self {
+        match (self, other) {
+            // If one of the field has unknown dependencies (understand: may depend on all the other
+            // fields), then the resulting fields has unknown dependencies as well
+            (FieldDeps::Unknown, _) | (_, FieldDeps::Unknown) => FieldDeps::Unknown,
+            (FieldDeps::Known(deps1), FieldDeps::Known(deps2)) => {
+                let union: HashSet<Ident> = deps1.union(&*deps2).cloned().collect();
+                FieldDeps::Known(Rc::new(union))
+            }
+        }
+    }
+
+    /// Return an empty set of dependencies
+    pub fn empty() -> Self {
+        FieldDeps::Known(Rc::new(HashSet::new()))
+    }
+
+    /// Return `true` if the dependencies are known and are empty, or `false` otherwise.
+    pub fn is_empty(&self) -> bool {
+        matches!(self, FieldDeps::Known(deps) if deps.is_empty())
+    }
+}
+
+impl From<HashSet<Ident>> for FieldDeps {
+    fn from(set: HashSet<Ident>) -> Self {
+        FieldDeps::Known(Rc::new(set))
+    }
+}
+
+/// Store field interdependencies in a recursive record. Map each static and dynamic field to the
+/// set of recursive fields that syntactically appears in their definition as free variables.
+#[derive(Debug, Default, Eq, PartialEq, Clone)]
+pub struct RecordDeps {
+    /// Must have exactly the same keys as the static fields map of the recursive record.
+    pub stat_fields: HashMap<Ident, FieldDeps>,
+    /// Must have exactly the same length as the dynamic fields list of the recursive record.
+    pub dyn_fields: Vec<FieldDeps>,
+}
+
+/// The base structure of a Nickel record.
+///
+/// Used to group together fields common to both the [super::Term::Record] and
+/// [super::Term::RecRecord] terms.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct RecordData {
+    /// Fields whose names are known statically.
+    pub fields: HashMap<Ident, RichTerm>,
+    /// Attributes which may be applied to a record.
+    pub attrs: RecordAttrs,
+    /// The hidden part of a record under a polymorphic contract.
+    pub sealed_tail: Option<SealedTail>,
+}
+
+impl RecordData {
+    pub fn new(
+        fields: HashMap<Ident, RichTerm>,
+        attrs: RecordAttrs,
+        sealed_tail: Option<SealedTail>,
+    ) -> Self {
+        RecordData {
+            fields,
+            attrs,
+            sealed_tail,
+        }
+    }
+
+    /// A record with no fields and the default set of attributes.
+    pub fn empty() -> Self {
+        Default::default()
+    }
+
+    /// A record with the provided fields & the default set of attributes.
+    pub fn with_fields(fields: HashMap<Ident, RichTerm>) -> Self {
+        let attrs = Default::default();
+        let sealed_tail = Default::default();
+        RecordData {
+            fields,
+            attrs,
+            sealed_tail,
+        }
+    }
+
+    /// Returns the record resulting from applying the provided function
+    /// to each field.
+    ///
+    /// Note that `f` is taken as `mut` in order to allow it to mutate
+    /// external state while iterating.
+    pub fn map_fields<F>(self, mut f: F) -> Self
+    where
+        F: FnMut(Ident, RichTerm) -> RichTerm,
+    {
+        let fields = self
+            .fields
+            .into_iter()
+            .map(|(id, t)| (id, f(id, t)))
+            .collect();
+        RecordData { fields, ..self }
+    }
+}
+
+/// The sealed tail of a Nickel record under a polymorphic contract.
+///
+/// Note that access to the enclosed term must only be allowed when a matching sealing key is
+/// provided. If this is not enforced it will lead to parametricity violations.
+#[derive(Clone, Debug, PartialEq)]
+pub struct SealedTail {
+    /// The key with which the tail is sealed.
+    sealing_key: SealingKey,
+    /// The label to which blame will be attributed if code tries to
+    /// interact with the sealed tail in any way.
+    pub label: Label,
+    /// The term which is sealed.
+    term: RichTerm,
+}
+
+impl SealedTail {
+    pub fn new(sealing_key: SealingKey, label: Label, term: RichTerm) -> Self {
+        Self {
+            sealing_key,
+            label,
+            term,
+        }
+    }
+
+    /// Returns the sealed term if the key matches, otherwise returns None.
+    pub fn unseal(&self, key: &SealingKey) -> Option<&RichTerm> {
+        if key == &self.sealing_key {
+            Some(&self.term)
+        } else {
+            None
+        }
+    }
+}
diff --git a/src/transform/free_vars.rs b/src/transform/free_vars.rs
index 51587281..774d4c07 100644
--- a/src/transform/free_vars.rs
+++ b/src/transform/free_vars.rs
@@ -6,7 +6,10 @@
 use crate::{
     destruct::{Destruct, Match},
     identifier::Ident,
-    term::{FieldDeps, RecordDeps, RichTerm, SharedTerm, StrChunk, Term},
+    term::{
+        record::{FieldDeps, RecordDeps},
+        RichTerm, SharedTerm, StrChunk, Term,
+    },
     types::{RecordRowF, RecordRows, RecordRowsF, TypeF, Types},
 };
 
diff --git a/src/transform/share_normal_form.rs b/src/transform/share_normal_form.rs
index a14ff803..c679fd81 100644
--- a/src/transform/share_normal_form.rs
+++ b/src/transform/share_normal_form.rs
@@ -31,7 +31,7 @@ use crate::{
     identifier::Ident,
     match_sharedterm,
     position::TermPos,
-    term::{BindingType, FieldDeps, LetAttrs, RichTerm, Term},
+    term::{record::FieldDeps, BindingType, LetAttrs, RichTerm, Term},
 };
 
 /// Transform the top-level term of an AST to a share normal form, if it can.


Test patch:
diff --git a/tests/integration/free_vars.rs b/tests/integration/free_vars.rs
index a80e9f6e..55b1b929 100644
--- a/tests/integration/free_vars.rs
+++ b/tests/integration/free_vars.rs
@@ -1,4 +1,4 @@
-use nickel_lang::term::FieldDeps;
+use nickel_lang::term::record::FieldDeps;
 use nickel_lang::{identifier::Ident, term::Term, transform::free_vars};
 
 use std::collections::{HashMap, HashSet};