Repository: tweag/nickel
Pull request #790 (2022-08-03 18:10:04)

Initial implementation of Deserializer to export Nickel terms to Rust.
Ad discussed in Discussed in https://github.com/tweag/nickel/discussions/786, I've approached a first implementation of a Deserializer to convert Nickel terms into Rust plain types.


Patch:
diff --git a/src/deserialize.rs b/src/deserialize.rs
new file mode 100644
index 00000000..591b743b
--- /dev/null
+++ b/src/deserialize.rs
@@ -0,0 +1,708 @@
+//! Deserialization of an evaluated program to plain Rust types.
+
+use std::collections::HashMap;
+
+use serde::de::{
+    Deserialize, DeserializeSeed, EnumAccess, IntoDeserializer, MapAccess, SeqAccess,
+    VariantAccess, Visitor,
+};
+
+use crate::identifier::Ident;
+use crate::term::{MetaValue, RichTerm, Term};
+
+macro_rules! deserialize_number {
+    ($method:ident, $type:tt, $visit:ident) => {
+        fn $method<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+        where
+            V: Visitor<'de>,
+        {
+            match unwrap_term(self)? {
+                Term::Num(n) => visitor.$visit(n as $type),
+                other => Err(RustDeserializationError::InvalidType {
+                    expected: "Num".to_string(),
+                    occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+                }),
+            }
+        }
+    };
+}
+
+macro_rules! deserialize_number_round {
+    ($method:ident, $type:tt, $visit:ident) => {
+        fn $method<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+        where
+            V: Visitor<'de>,
+        {
+            match unwrap_term(self)? {
+                Term::Num(n) => visitor.$visit(n.round() as $type),
+                other => Err(RustDeserializationError::InvalidType {
+                    expected: "Num".to_string(),
+                    occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+                }),
+            }
+        }
+    };
+}
+
+/// An error occurred during deserialization to Rust.
+#[derive(Debug, PartialEq, Clone)]
+pub enum RustDeserializationError {
+    InvalidType { expected: String, occurred: String },
+    MissingValue,
+    EmptyMetaValue,
+    UnimplementedType { occurred: String },
+    InvalidRecordLength(usize),
+    InvalidArrayLength(usize),
+    Other(String),
+}
+
+impl<'de> serde::Deserializer<'de> for RichTerm {
+    type Error = RustDeserializationError;
+
+    /// Catch-all deserialization
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Null => visitor.visit_unit(),
+            Term::Bool(v) => visitor.visit_bool(v),
+            Term::Num(v) => visitor.visit_f64(v),
+            Term::Str(v) => visitor.visit_string(v),
+            Term::Enum(v) => visitor.visit_enum(EnumDeserializer {
+                variant: v.label,
+                rich_term: None,
+            }),
+            Term::Record(v, _) => visit_record(v, visitor),
+            Term::Array(v, _) => visit_array(v, visitor),
+            Term::MetaValue(_) => visitor.visit_unit(),
+            other => Err(RustDeserializationError::UnimplementedType {
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+        }
+    }
+
+    deserialize_number_round!(deserialize_i8, i8, visit_i8);
+    deserialize_number_round!(deserialize_i16, i16, visit_i16);
+    deserialize_number_round!(deserialize_i32, i32, visit_i32);
+    deserialize_number_round!(deserialize_i64, i64, visit_i64);
+    deserialize_number_round!(deserialize_i128, i128, visit_i128);
+    deserialize_number_round!(deserialize_u8, u8, visit_u8);
+    deserialize_number_round!(deserialize_u16, u16, visit_u16);
+    deserialize_number_round!(deserialize_u32, u32, visit_u32);
+    deserialize_number_round!(deserialize_u64, u64, visit_u64);
+    deserialize_number_round!(deserialize_u128, u128, visit_u128);
+    deserialize_number!(deserialize_f32, f32, visit_f32);
+    deserialize_number!(deserialize_f64, f64, visit_f64);
+
+    /// Deserialize nullable field.
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Null => visitor.visit_none(),
+            some => visitor.visit_some(RichTerm::from(some)),
+        }
+    }
+
+    /// deserialize `RichTerm::Enum` tags or `RichTerm::Record`s with a single item.
+    fn deserialize_enum<V>(
+        self,
+        _name: &str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        let (variant, rich_term) = match unwrap_term(self)? {
+            Term::Enum(ident) => (ident.label, None),
+            Term::Record(v, _) => {
+                let mut iter = v.into_iter();
+                let (variant, value) = match iter.next() {
+                    Some(v) => v,
+                    None => {
+                        return Err(RustDeserializationError::InvalidType {
+                            expected: "Record with single key".to_string(),
+                            occurred: "Record without keys".to_string(),
+                        });
+                    }
+                };
+                if iter.next().is_some() {
+                    return Err(RustDeserializationError::InvalidType {
+                        expected: "Record with single key".to_string(),
+                        occurred: "Record with multiple keys".to_string(),
+                    });
+                }
+                (variant.label, Some(value))
+            }
+            other => {
+                return Err(RustDeserializationError::InvalidType {
+                    expected: "Enum or Record".to_string(),
+                    occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+                });
+            }
+        };
+
+        visitor.visit_enum(EnumDeserializer { variant, rich_term })
+    }
+
+    /// Deserialize pass-through tuples/structs.
+    fn deserialize_newtype_struct<V>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        visitor.visit_newtype_struct(self)
+    }
+
+    /// Deserialize `RichTerm::Bool`
+    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Bool(v) => visitor.visit_bool(v),
+            other => Err(RustDeserializationError::InvalidType {
+                expected: "Bool".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+        }
+    }
+
+    /// Deserialize `RichTerm::Str` as char
+    fn deserialize_char<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_string(visitor)
+    }
+
+    /// Deserialize `RichTerm::Str` as str
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_string(visitor)
+    }
+
+    /// Deserialize `RichTerm::Str` as String
+    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Str(v) => visitor.visit_string(v),
+            other => Err(RustDeserializationError::InvalidType {
+                expected: "Str".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+        }
+    }
+
+    /// Deserialize `RichTerm::Str` as String or `RichTerm::Array` as array,
+    fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_byte_buf(visitor)
+    }
+
+    /// Deserialize `RichTerm::Str` as String or `RichTerm::Array` as array,
+    fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Str(v) => visitor.visit_string(v),
+            Term::Array(v, _) => visit_array(v, visitor),
+            other => Err(RustDeserializationError::InvalidType {
+                expected: "Str or Array".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+        }
+    }
+
+    /// Deserialize `RichTerm::Null` as `()`.
+    fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Null => visitor.visit_unit(),
+            other => Err(RustDeserializationError::InvalidType {
+                expected: "Null".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+        }
+    }
+
+    /// Deserialize `RichTerm::Null` as `()`.
+    fn deserialize_unit_struct<V>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_unit(visitor)
+    }
+
+    /// Deserialize `RichTerm::Array` as `Vec<T>`.
+    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Array(v, _) => visit_array(v, visitor),
+            other => Err(RustDeserializationError::InvalidType {
+                expected: "Array".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+        }
+    }
+
+    /// Deserialize `RichTerm::Array` as `Vec<T>`.
+    fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_seq(visitor)
+    }
+
+    /// Deserialize `RichTerm::Array` as `Vec<T>`.
+    fn deserialize_tuple_struct<V>(
+        self,
+        _name: &'static str,
+        _len: usize,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_seq(visitor)
+    }
+
+    /// Deserialize `RichTerm::Record` as `HashMap<K, V>`.
+    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Record(v, _) => visit_record(v, visitor),
+            other => Err(RustDeserializationError::InvalidType {
+                expected: "Record".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+        }
+    }
+
+    /// Deserialize `RichTerm::Record` as `struct`.
+    fn deserialize_struct<V>(
+        self,
+        _name: &'static str,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match unwrap_term(self)? {
+            Term::Array(v, _) => visit_array(v, visitor),
+            Term::Record(v, _) => visit_record(v, visitor),
+            other => Err(RustDeserializationError::InvalidType {
+                expected: "Record".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+        }
+    }
+
+    /// Deserialize `Ident` as `String`.
+    fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        self.deserialize_string(visitor)
+    }
+
+    fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        drop(self);
+        visitor.visit_unit()
+    }
+}
+
+struct ArrayDeserializer {
+    iter: std::vec::IntoIter<RichTerm>,
+}
+
+impl ArrayDeserializer {
+    fn new(vec: Vec<RichTerm>) -> Self {
+        ArrayDeserializer {
+            iter: vec.into_iter(),
+        }
+    }
+}
+
+impl<'de> SeqAccess<'de> for ArrayDeserializer {
+    type Error = RustDeserializationError;
+
+    fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
+    where
+        T: DeserializeSeed<'de>,
+    {
+        match self.iter.next() {
+            Some(value) => seed.deserialize(value).map(Some),
+            None => Ok(None),
+        }
+    }
+
+    fn size_hint(&self) -> Option<usize> {
+        match self.iter.size_hint() {
+            (lower, Some(upper)) if lower == upper => Some(upper),
+            _ => None,
+        }
+    }
+}
+
+fn unwrap_term(mut rich_term: RichTerm) -> Result<Term, RustDeserializationError> {
+    loop {
+        rich_term = match Term::from(rich_term) {
+            Term::MetaValue(MetaValue { value, .. }) => {
+                if let Some(rich_term) = value {
+                    rich_term
+                } else {
+                    break Err(RustDeserializationError::EmptyMetaValue);
+                }
+            }
+            other => break Ok(other),
+        }
+    }
+}
+
+fn visit_array<'de, V>(
+    array: Vec<RichTerm>,
+    visitor: V,
+) -> Result<V::Value, RustDeserializationError>
+where
+    V: Visitor<'de>,
+{
+    let len = array.len();
+    let mut deserializer = ArrayDeserializer::new(array);
+    let seq = visitor.visit_seq(&mut deserializer)?;
+    let remaining = deserializer.iter.len();
+    if remaining == 0 {
+        Ok(seq)
+    } else {
+        Err(RustDeserializationError::InvalidArrayLength(len))
+    }
+}
+
+struct RecordDeserializer {
+    iter: <HashMap<Ident, RichTerm> as IntoIterator>::IntoIter,
+    rich_term: Option<RichTerm>,
+}
+
+impl RecordDeserializer {
+    fn new(map: HashMap<Ident, RichTerm>) -> Self {
+        RecordDeserializer {
+            iter: map.into_iter(),
+            rich_term: None,
+        }
+    }
+}
+
+impl<'de> MapAccess<'de> for RecordDeserializer {
+    type Error = RustDeserializationError;
+
+    fn next_key_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
+    where
+        T: DeserializeSeed<'de>,
+    {
+        match self.iter.next() {
+            Some((key, value)) => {
+                self.rich_term = Some(value);
+                seed.deserialize(key.label.into_deserializer()).map(Some)
+            }
+            None => Ok(None),
+        }
+    }
+
+    fn next_value_seed<T>(&mut self, seed: T) -> Result<T::Value, Self::Error>
+    where
+        T: DeserializeSeed<'de>,
+    {
+        match self.rich_term.take() {
+            Some(value) => seed.deserialize(value),
+            _ => Err(RustDeserializationError::MissingValue),
+        }
+    }
+
+    fn size_hint(&self) -> Option<usize> {
+        match self.iter.size_hint() {
+            (lower, Some(upper)) if lower == upper => Some(upper),
+            _ => None,
+        }
+    }
+}
+
+fn visit_record<'de, V>(
+    record: HashMap<Ident, RichTerm>,
+    visitor: V,
+) -> Result<V::Value, RustDeserializationError>
+where
+    V: Visitor<'de>,
+{
+    let len = record.len();
+    let mut deserializer = RecordDeserializer::new(record);
+    let map = visitor.visit_map(&mut deserializer)?;
+    let remaining = deserializer.iter.len();
+    if remaining == 0 {
+        Ok(map)
+    } else {
+        Err(RustDeserializationError::InvalidRecordLength(len))
+    }
+}
+
+struct VariantDeserializer {
+    rich_term: Option<RichTerm>,
+}
+
+impl<'de> VariantAccess<'de> for VariantDeserializer {
+    type Error = RustDeserializationError;
+
+    fn unit_variant(self) -> Result<(), Self::Error> {
+        match self.rich_term {
+            Some(value) => Deserialize::deserialize(value),
+            None => Ok(()),
+        }
+    }
+
+    fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
+    where
+        T: DeserializeSeed<'de>,
+    {
+        match self.rich_term {
+            Some(value) => seed.deserialize(value),
+            None => Err(RustDeserializationError::MissingValue),
+        }
+    }
+
+    fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match self.rich_term.map(unwrap_term) {
+            Some(Ok(Term::Array(v, _))) => {
+                if v.is_empty() {
+                    visitor.visit_unit()
+                } else {
+                    visit_array(v, visitor)
+                }
+            }
+            Some(Ok(other)) => Err(RustDeserializationError::InvalidType {
+                expected: "Array variant".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+            Some(Err(err)) => Err(err),
+            None => Err(RustDeserializationError::MissingValue),
+        }
+    }
+
+    fn struct_variant<V>(
+        self,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: Visitor<'de>,
+    {
+        match self.rich_term.map(unwrap_term) {
+            Some(Ok(Term::Record(v, _))) => visit_record(v, visitor),
+            Some(Ok(other)) => Err(RustDeserializationError::InvalidType {
+                expected: "Array variant".to_string(),
+                occurred: other.type_of().unwrap_or_else(|| "Other".to_string()),
+            }),
+            Some(Err(err)) => Err(err),
+            None => Err(RustDeserializationError::MissingValue),
+        }
+    }
+}
+
+struct EnumDeserializer {
+    variant: String,
+    rich_term: Option<RichTerm>,
+}
+
+impl<'de> EnumAccess<'de> for EnumDeserializer {
+    type Error = RustDeserializationError;
+    type Variant = VariantDeserializer;
+
+    fn variant_seed<V>(self, seed: V) -> Result<(V::Value, VariantDeserializer), Self::Error>
+    where
+        V: DeserializeSeed<'de>,
+    {
+        let variant = self.variant.into_deserializer();
+        let visitor = VariantDeserializer {
+            rich_term: self.rich_term,
+        };
+        seed.deserialize(variant).map(|v| (v, visitor))
+    }
+}
+
+impl std::fmt::Display for RustDeserializationError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        match self {
+            RustDeserializationError::InvalidType {
+                ref expected,
+                ref occurred,
+            } => write!(f, "invalid type: {occurred}, expected: {expected}"),
+            RustDeserializationError::MissingValue => write!(f, "missing value"),
+            RustDeserializationError::EmptyMetaValue => write!(f, "empty Metavalue"),
+            RustDeserializationError::InvalidRecordLength(len) => {
+                write!(f, "invalid record length, expected {len}")
+            }
+            RustDeserializationError::InvalidArrayLength(len) => {
+                write!(f, "invalid array length, expected {len}")
+            }
+            RustDeserializationError::UnimplementedType { ref occurred } => {
+                write!(f, "unimplemented conversion from type: {occurred}")
+            }
+            RustDeserializationError::Other(ref err) => write!(f, "{err}"),
+        }
+    }
+}
+
+impl std::error::Error for RustDeserializationError {}
+
+impl serde::de::Error for RustDeserializationError {
+    fn custom<T>(msg: T) -> Self
+    where
+        T: std::fmt::Display,
+    {
+        RustDeserializationError::Other(msg.to_string())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::io::Cursor;
+
+    use serde::Deserialize;
+
+    use super::RustDeserializationError;
+    use crate::program::Program;
+
+    #[test]
+    fn rust_deserialize_struct_with_fields() {
+        #[derive(Debug, PartialEq, Deserialize)]
+        #[serde(rename_all = "lowercase")]
+        enum E {
+            Foo,
+            Bar,
+        }
+
+        #[derive(Debug, PartialEq, Deserialize)]
+        #[serde(rename_all = "lowercase")]
+        enum H {
+            Foo(u16),
+            Bar(String),
+        }
+
+        #[derive(Debug, PartialEq, Deserialize)]
+        struct A {
+            a: f64,
+            b: String,
+            c: (),
+            d: bool,
+            e: E,
+            f: Option<bool>,
+            g: i16,
+            h: H,
+        }
+
+        assert_eq!(
+            A::deserialize(
+                Program::new_from_source(
+                    Cursor::new(
+                        br#"{ a = 10, b = "test string", c = null, d = true, e = `foo, f = null, g = -10, h = { bar = "some other string" } }"#.to_vec()
+                    ),
+                    "source"
+                )
+                .expect("program should't fail")
+                .eval_full()
+                .expect("evaluation should't fail")
+            )
+            .expect("deserialization should't fail"),
+            A {
+                a: 10.0,
+                b: "test string".to_string(),
+                c: (),
+                d: true,
+                e: E::Foo,
+                f: None,
+                g: -10,
+                h: H::Bar("some other string".to_string())
+            }
+        )
+    }
+
+    #[test]
+    fn rust_deserialize_array_of_numbers() {
+        assert_eq!(
+            Vec::<f64>::deserialize(
+                Program::new_from_source(Cursor::new(br#"[1, 2, 3, 4]"#.to_vec()), "source")
+                    .expect("program should't fail")
+                    .eval_full()
+                    .expect("evaluation should't fail")
+            )
+            .expect("deserialization should't fail"),
+            vec![1.0, 2.0, 3.0, 4.0]
+        )
+    }
+
+    #[test]
+    fn rust_deserialize_fail_non_data() {
+        #[derive(Debug, PartialEq, Deserialize)]
+        struct A;
+
+        assert_eq!(
+            A::deserialize(
+                Program::new_from_source(Cursor::new(br#"fun a b => a + b"#.to_vec()), "source")
+                    .expect("program should't fail")
+                    .eval_full()
+                    .expect("evaluation should't fail")
+            ),
+            Err(RustDeserializationError::InvalidType {
+                expected: "Null".to_string(),
+                occurred: "Fun".to_string()
+            })
+        )
+    }
+
+    #[test]
+    fn rust_deserialize_ignore_metavalue() {
+        #[derive(Debug, PartialEq, Deserialize)]
+        struct A {
+            a: f64,
+        }
+
+        assert_eq!(
+            A::deserialize(
+                Program::new_from_source(Cursor::new(br#"{ a = (10 | Num) }"#.to_vec()), "source")
+                    .expect("program should't fail")
+                    .eval_full()
+                    .expect("evaluation should't fail")
+            )
+            .expect("deserialization should't fail"),
+            A { a: 10.0 }
+        )
+    }
+}
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index b0b37375..fbbc49b3 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -704,7 +704,7 @@ pub fn subst(rt: RichTerm, global_env: &Environment, env: &Environment) -> RichT
         // loop. Although avoidable, this requires some care and is not currently needed.
         | v @ Term::Fun(..)
         | v @ Term::Lbl(_)
-        | v @ Term::SealingKey(_)
+        | v @ Term::Sym(_)
         | v @ Term::Enum(_)
         | v @ Term::Import(_)
         | v @ Term::ResolvedImport(_) => RichTerm::new(v, pos),
@@ -757,10 +757,10 @@ pub fn subst(rt: RichTerm, global_env: &Environment, env: &Environment) -> RichT
 
             RichTerm::new(Term::OpN(op, ts), pos)
         }
-        Term::Sealed(i, t) => {
+        Term::Wrapped(i, t) => {
             let t = subst(t, global_env, env);
 
-            RichTerm::new(Term::Sealed(i, t), pos)
+            RichTerm::new(Term::Wrapped(i, t), pos)
         }
         Term::Record(map, attrs) => {
             let map = map
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index 8fd7544b..a5100d0b 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -434,10 +434,10 @@ fn process_unary_operation(
                 ))
             }
         },
-        UnaryOp::Seal() => {
-            if let Term::SealingKey(s) = &*t {
+        UnaryOp::Wrap() => {
+            if let Term::Sym(s) = &*t {
                 Ok(Closure::atomic_closure(
-                    mk_fun!("x", Term::Sealed(*s, mk_term::var("x"))).with_pos(pos_op_inh),
+                    mk_fun!("x", Term::Wrapped(*s, mk_term::var("x"))).with_pos(pos_op_inh),
                 ))
             } else {
                 Err(EvalError::TypeError(
@@ -1410,12 +1410,12 @@ fn process_binary_operation(
                 ))
             }
         }
-        BinaryOp::Unseal() => {
-            if let Term::SealingKey(s1) = &*t1 {
+        BinaryOp::Unwrap() => {
+            if let Term::Sym(s1) = &*t1 {
                 // Return a function that either behaves like the identity or
                 // const unwrapped_term
 
-                Ok(if let Term::Sealed(s2, t) = t2.into_owned() {
+                Ok(if let Term::Wrapped(s2, t) = t2.into_owned() {
                     if *s1 == s2 {
                         Closure {
                             body: mk_fun!("-invld", t),
@@ -2398,7 +2398,7 @@ fn eq(env: &mut Environment, c1: Closure, c2: Closure) -> EqResult {
         (Term::Num(n1), Term::Num(n2)) => EqResult::Bool(n1 == n2),
         (Term::Str(s1), Term::Str(s2)) => EqResult::Bool(s1 == s2),
         (Term::Lbl(l1), Term::Lbl(l2)) => EqResult::Bool(l1 == l2),
-        (Term::SealingKey(s1), Term::SealingKey(s2)) => EqResult::Bool(s1 == s2),
+        (Term::Sym(s1), Term::Sym(s2)) => EqResult::Bool(s1 == s2),
         (Term::Enum(id1), Term::Enum(id2)) => EqResult::Bool(id1 == id2),
         (Term::Record(m1, _), Term::Record(m2, _)) => {
             let (left, center, right) = merge::hashmap::split(m1, m2);
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 835236c9..bee710ff 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -532,7 +532,7 @@ UOp: UnaryOp = {
     "go_dom" => UnaryOp::GoDom(),
     "go_codom" => UnaryOp::GoCodom(),
     "go_array" => UnaryOp::GoArray(),
-    "seal" => UnaryOp::Seal(),
+    "wrap" => UnaryOp::Wrap(),
     "embed" <Ident> => UnaryOp::Embed(<>),
     "map"  => UnaryOp::ArrayMap(),
     "generate" => UnaryOp::ArrayGen(),
@@ -722,7 +722,7 @@ InfixExpr: UniTerm = {
 
 BOpPre: BinaryOp = {
     "assume" => BinaryOp::Assume(),
-    "unseal" => BinaryOp::Unseal(),
+    "unwrap" => BinaryOp::Unwrap(),
     "go_field" => BinaryOp::GoField(),
     "has_field" => BinaryOp::HasField(),
     "elem_at" => BinaryOp::ArrayElemAt(),
@@ -859,8 +859,8 @@ extern {
         "go_codom" => Token::Normal(NormalToken::GoCodom),
         "go_array" => Token::Normal(NormalToken::GoArray),
         "go_field" => Token::Normal(NormalToken::GoField),
-        "seal" => Token::Normal(NormalToken::Seal),
-        "unseal" => Token::Normal(NormalToken::Unseal),
+        "wrap" => Token::Normal(NormalToken::Wrap),
+        "unwrap" => Token::Normal(NormalToken::Unwrap),
         "embed" => Token::Normal(NormalToken::Embed),
         "record_map" => Token::Normal(NormalToken::RecordMap),
         "record_insert" => Token::Normal(NormalToken::RecordInsert),
diff --git a/src/lib.rs b/src/lib.rs
index e402cf60..c7769aca 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,5 @@
 pub mod cache;
+pub mod deserialize;
 pub mod destruct;
 pub mod environment;
 pub mod error;
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index 0002b877..5ef17e70 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -167,10 +167,10 @@ pub enum NormalToken<'input> {
     #[token("%go_array%")]
     GoArray,
 
-    #[token("%seal%")]
-    Seal,
-    #[token("%unseal%")]
-    Unseal,
+    #[token("%wrap%")]
+    Wrap,
+    #[token("%unwrap%")]
+    Unwrap,
     #[token("%embed%")]
     Embed,
     #[token("%record_map%")]
diff --git a/src/pretty.rs b/src/pretty.rs
index 7063de95..fc8f29bf 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -671,12 +671,12 @@ where
                 )
                 .group(),
 
-            SealingKey(sym) => allocator
-                .text(format!("#<sealing key: {}>", sym))
+            Sym(sym) => allocator
+                .text(format!("# <symbol: {}>", sym))
                 .append(allocator.hardline()),
 
             // TODO
-            Sealed(_i, _rt) => allocator.text("#<sealed>").append(allocator.hardline()),
+            Wrapped(_i, _rt) => allocator.text("# <wraped>").append(allocator.hardline()),
 
             MetaValue(mv) => mv.to_owned().pretty(allocator),
             Import(f) => allocator
@@ -685,7 +685,7 @@ where
                 .append(allocator.as_string(f.to_string_lossy()).double_quotes()),
             ResolvedImport(id) => allocator.text(format!("import <file_id: {:?}>", id)),
             ParseError => allocator
-                .text("#<PARSE ERROR!>")
+                .text("# <PARCE ERROR!>")
                 .append(allocator.hardline()),
         }
     }
diff --git a/src/term.rs b/src/term.rs
index 627ec420..c93860f6 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -119,15 +119,15 @@ pub enum Term {
     #[serde(skip)]
     OpN(NAryOp, Vec<RichTerm>),
 
-    /// A key locking a sealed term.
+    /// A symbol.
     ///
-    /// A unique key corresponding to a type variable. See [`Term::Wrapped`] below.
+    /// A unique tag corresponding to a type variable. See `Wrapped` below.
     #[serde(skip)]
-    SealingKey(SealingKey),
+    Sym(i32),
 
-    /// A sealed term.
+    /// A wrapped term.
     ///
-    /// Sealed terms are introduced by contracts on polymorphic types. Take the following example:
+    /// Wrapped terms are introduced by contracts on polymorphic types. Take the following example:
     ///
     /// ```text
     /// let f = Assume(forall a. forall b. a -> b -> a, fun x y => y) in
@@ -138,13 +138,13 @@ pub enum Term {
     /// - Assign a unique identifier to each type variable: say `a => 1`, `b => 2`
     /// - For each cast on a negative occurrence of a type variable `a` or `b` (corresponding to an
     /// argument position), tag the argument with the associated identifier. In our example, `f
-    /// true "a"` will push `Sealed(1, true)` then `Sealed(2, "a")` on the stack.
+    /// true "a"` will push `Wrapped(1, true)` then `Wrapped(2, "a")` on the stack.
     /// - For each cast on a positive occurrence of a type variable, this contract check that the
-    /// term is of the form `Sealed(id, term)` where `id` corresponds to the identifier of the
-    /// type variable. In our example, the last cast to `a` finds `Sealed(2, "a")`, while it
-    /// expected `Sealed(1, _)`, hence it raises a positive blame.
+    /// term is of the form `Wrapped(id, term)` where `id` corresponds to the identifier of the
+    /// type variable. In our example, the last cast to `a` finds `Wrapped(2, "a")`, while it
+    /// expected `Wrapped(1, _)`, hence it raises a positive blame.
     #[serde(skip)]
-    Sealed(SealingKey, RichTerm),
+    Wrapped(i32, RichTerm),
 
     #[serde(serialize_with = "crate::serialize::serialize_meta_value")]
     #[serde(skip_deserializing)]
@@ -160,8 +160,6 @@ pub enum Term {
     ParseError,
 }
 
-pub type SealingKey = i32;
-
 impl From<MetaValue> for Term {
     fn from(m: MetaValue) -> Self {
         Term::MetaValue(m)
@@ -382,12 +380,12 @@ impl Term {
                     func(t2);
                 });
             }
-            Bool(_) | Num(_) | Str(_) | Lbl(_) | Var(_) | SealingKey(_) | Enum(_) | Import(_)
+            Bool(_) | Num(_) | Str(_) | Lbl(_) | Var(_) | Sym(_) | Enum(_) | Import(_)
             | ResolvedImport(_) => {}
             Fun(_, ref mut t)
             | FunPattern(_, _, ref mut t)
             | Op1(_, ref mut t)
-            | Sealed(_, ref mut t) => {
+            | Wrapped(_, ref mut t) => {
                 func(t);
             }
             MetaValue(ref mut meta) => {
@@ -434,8 +432,8 @@ impl Term {
             Term::Enum(_) => Some("Enum"),
             Term::Record(..) | Term::RecRecord(..) => Some("Record"),
             Term::Array(..) => Some("Array"),
-            Term::SealingKey(_) => Some("SealingKey"),
-            Term::Sealed(_, _) => Some("Sealed"),
+            Term::Sym(_) => Some("Sym"),
+            Term::Wrapped(_, _) => Some("Wrapped"),
             Term::MetaValue(_) => Some("Metavalue"),
             Term::Let(..)
             | Term::LetPattern(..)
@@ -486,8 +484,8 @@ impl Term {
             }
             Term::Record(..) | Term::RecRecord(..) => String::from("{ ... }"),
             Term::Array(..) => String::from("[ ... ]"),
-            Term::SealingKey(_) => String::from("<sealing key>"),
-            Term::Sealed(_, _) => String::from("<sealed>"),
+            Term::Sym(_) => String::from("<sym>"),
+            Term::Wrapped(_, _) => String::from("<wrapped>"),
             Term::MetaValue(ref meta) => {
                 let mut content = String::new();
 
@@ -564,7 +562,7 @@ impl Term {
             | Term::Enum(_)
             | Term::Record(..)
             | Term::Array(..)
-            | Term::SealingKey(_) => true,
+            | Term::Sym(_) => true,
             Term::Let(..)
             | Term::LetPattern(..)
             | Term::FunPattern(..)
@@ -574,7 +572,7 @@ impl Term {
             | Term::Op1(_, _)
             | Term::Op2(_, _, _)
             | Term::OpN(..)
-            | Term::Sealed(_, _)
+            | Term::Wrapped(_, _)
             | Term::MetaValue(_)
             | Term::Import(_)
             | Term::ResolvedImport(_)
@@ -601,7 +599,7 @@ impl Term {
             | Term::Str(_)
             | Term::Lbl(_)
             | Term::Enum(_)
-            | Term::SealingKey(_) => true,
+            | Term::Sym(_) => true,
             Term::Let(..)
             | Term::LetPattern(..)
             | Term::Record(..)
@@ -614,7 +612,7 @@ impl Term {
             | Term::Op1(_, _)
             | Term::Op2(_, _, _)
             | Term::OpN(..)
-            | Term::Sealed(_, _)
+            | Term::Wrapped(_, _)
             | Term::MetaValue(_)
             | Term::Import(_)
             | Term::ResolvedImport(_)
@@ -639,7 +637,7 @@ impl Term {
             | Term::Array(..)
             | Term::Var(..)
             | Term::Op1(..)
-            | Term::SealingKey(..) => true,
+            | Term::Sym(..) => true,
             Term::Let(..)
             | Term::Switch(..)
             | Term::LetPattern(..)
@@ -648,7 +646,7 @@ impl Term {
             | Term::App(..)
             | Term::Op2(..)
             | Term::OpN(..)
-            | Term::Sealed(..)
+            | Term::Wrapped(..)
             | Term::MetaValue(..)
             | Term::Import(..)
             | Term::ResolvedImport(..)
@@ -786,8 +784,8 @@ pub enum UnaryOp {
     /// See `GoDom`.
     GoArray(),
 
-    /// Seal a term with a sealing key (see [`Term::Sealed`]).
-    Seal(),
+    /// Wrap a term with a type tag (see [`Term::Wrapped`]).
+    Wrap(),
 
     /// Force the evaluation of its argument and proceed with the second.
     Seq(),
@@ -935,10 +933,10 @@ pub enum BinaryOp {
     /// operation with its argument. Finally, this operator marks the location of the contract
     /// argument for better error reporting.
     Assume(),
-    /// Unseal a sealed term.
+    /// Unwrap a tagged term.
     ///
-    /// See [`UnaryOp::Seal`].
-    Unseal(),
+    /// See [`UnaryOp::Wrap`].
+    Unwrap(),
     /// Go to a specific field in the type path of a label.
     ///
     /// See `GoDom`.
@@ -1179,10 +1177,10 @@ impl RichTerm {
                     pos,
                 )
             },
-            Term::Sealed(i, t) => {
+            Term::Wrapped(i, t) => {
                 let t = t.traverse(f, state, order)?;
                 RichTerm::new(
-                    Term::Sealed(i, t),
+                    Term::Wrapped(i, t),
                     pos,
                 )
             },
diff --git a/src/transform/free_vars.rs b/src/transform/free_vars.rs
index 9ad53263..e2accf72 100644
--- a/src/transform/free_vars.rs
+++ b/src/transform/free_vars.rs
@@ -30,7 +30,7 @@ fn collect_free_vars(rt: &mut RichTerm, free_vars: &mut HashSet<Ident>) {
         | Term::Num(_)
         | Term::Str(_)
         | Term::Lbl(_)
-        | Term::SealingKey(_)
+        | Term::Sym(_)
         | Term::Enum(_)
         | Term::Import(_)
         | Term::ResolvedImport(_) => (),
@@ -99,7 +99,7 @@ fn collect_free_vars(rt: &mut RichTerm, free_vars: &mut HashSet<Ident>) {
                 collect_free_vars(t, free_vars);
             }
         }
-        Term::Sealed(_, t) => collect_free_vars(t, free_vars),
+        Term::Wrapped(_, t) => collect_free_vars(t, free_vars),
         Term::Record(map, _) => {
             for t in map.values_mut() {
                 collect_free_vars(t, free_vars);
diff --git a/src/transform/share_normal_form.rs b/src/transform/share_normal_form.rs
index ae2c8cfa..c3c935ad 100644
--- a/src/transform/share_normal_form.rs
+++ b/src/transform/share_normal_form.rs
@@ -187,7 +187,7 @@ fn should_share(t: &Term) -> bool {
         | Term::Num(_)
         | Term::Str(_)
         | Term::Lbl(_)
-        | Term::SealingKey(_)
+        | Term::Sym(_)
         | Term::Var(_)
         | Term::Enum(_)
         | Term::Fun(_, _) => false,
diff --git a/src/typecheck/eq.rs b/src/typecheck/eq.rs
index f59ca1f1..833e5d11 100644
--- a/src/typecheck/eq.rs
+++ b/src/typecheck/eq.rs
@@ -123,8 +123,8 @@ fn contract_eq_bounded(
         (Num(n1), Num(n2)) => n1 == n2,
         (Str(s1), Str(s2)) => s1 == s2,
         (Enum(id1), Enum(id2)) => id1 == id2,
-        (SealingKey(s1), SealingKey(s2)) => s1 == s2,
-        (Sealed(key1, inner1), Sealed(key2, inner2)) => {
+        (Sym(s1), Sym(s2)) => s1 == s2,
+        (Wrapped(key1, inner1), Wrapped(key2, inner2)) => {
             key1 == key2 && contract_eq_bounded(state, inner1, env1, inner2, env2)
         }
         // We only compare string chunks when they represent a plain string (they don't contain any
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 59903039..07a4eb65 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -292,7 +292,7 @@ fn walk<L: Linearizer>(
         | Term::Str(_)
         | Term::Lbl(_)
         | Term::Enum(_)
-        | Term::SealingKey(_)
+        | Term::Sym(_)
         // This function doesn't recursively typecheck imports: this is the responsibility of the
         // caller.
         | Term::Import(_)
@@ -437,7 +437,7 @@ fn walk<L: Linearizer>(
                 _ => Ok(()),
             }
         }
-        Term::Sealed(_, t) => walk(state, envs, lin, linearizer, t),
+        Term::Wrapped(_, t) => walk(state, envs, lin, linearizer, t),
    }
 }
 
@@ -870,9 +870,9 @@ fn type_check_<L: Linearizer>(
                     .map_err(|err| err.into_typecheck_err(state, rt.pos)),
             }
         }
-        Term::SealingKey(_) => unify(state, ty, mk_typewrapper::sym())
+        Term::Sym(_) => unify(state, ty, mk_typewrapper::sym())
             .map_err(|err| err.into_typecheck_err(state, rt.pos)),
-        Term::Sealed(_, t) => type_check_(state, envs, lin, linearizer, t, ty),
+        Term::Wrapped(_, t) => type_check_(state, envs, lin, linearizer, t, ty),
         Term::Import(_) => unify(state, ty, mk_typewrapper::dynamic())
             .map_err(|err| err.into_typecheck_err(state, rt.pos)),
         // We use the apparent type of the import for checking. This function doesn't recursively
@@ -1017,7 +1017,7 @@ pub fn apparent_type(
         }
         Term::Num(_) => ApparentType::Inferred(Types(AbsType::Num())),
         Term::Bool(_) => ApparentType::Inferred(Types(AbsType::Bool())),
-        Term::SealingKey(_) => ApparentType::Inferred(Types(AbsType::Sym())),
+        Term::Sym(_) => ApparentType::Inferred(Types(AbsType::Sym())),
         Term::Str(_) | Term::StrChunks(_) => ApparentType::Inferred(Types(AbsType::Str())),
         Term::Array(..) => {
             ApparentType::Approximated(Types(AbsType::Array(Box::new(Types(AbsType::Dyn())))))
diff --git a/src/typecheck/operation.rs b/src/typecheck/operation.rs
index d51ebcb4..17919ff8 100644
--- a/src/typecheck/operation.rs
+++ b/src/typecheck/operation.rs
@@ -56,7 +56,7 @@ pub fn get_uop_type(
             (mk_typewrapper::dynamic(), mk_typewrapper::dynamic())
         }
         // Sym -> Dyn -> Dyn
-        UnaryOp::Seal() => (
+        UnaryOp::Wrap() => (
             mk_typewrapper::sym(),
             mk_tyw_arrow!(AbsType::Dyn(), AbsType::Dyn()),
         ),
@@ -228,7 +228,7 @@ pub fn get_bop_type(
             mk_tyw_arrow!(mk_typewrapper::dynamic(), mk_typewrapper::dynamic()),
         ),
         // Sym -> Dyn -> Dyn -> Dyn
-        BinaryOp::Unseal() => (
+        BinaryOp::Unwrap() => (
             mk_typewrapper::sym(),
             mk_typewrapper::dynamic(),
             mk_tyw_arrow!(AbsType::Dyn(), AbsType::Dyn()),
diff --git a/src/types.rs b/src/types.rs
index c804124e..85e61a5d 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -241,17 +241,9 @@ impl Types {
             AbsType::Flat(ref t) => t.clone(),
             AbsType::Var(ref id) => get_var(&h, id, true)?,
             AbsType::Forall(ref i, ref t) => {
-                let inst_var = mk_app!(
-                    contract::forall_var(),
-                    Term::SealingKey(*sy),
-                    Term::Bool(pol)
-                );
-
-                let inst_tail = mk_app!(
-                    contract::forall_tail(),
-                    Term::SealingKey(*sy),
-                    Term::Bool(pol)
-                );
+                let inst_var = mk_app!(contract::forall_var(), Term::Sym(*sy), Term::Bool(pol));
+
+                let inst_tail = mk_app!(contract::forall_tail(), Term::Sym(*sy), Term::Bool(pol));
 
                 h.insert(i.clone(), (inst_var, inst_tail));
                 *sy += 1;
diff --git a/stdlib/contract.ncl b/stdlib/contract.ncl
index f1f5409d..14835f18 100644
--- a/stdlib/contract.ncl
+++ b/stdlib/contract.ncl
@@ -23,9 +23,9 @@
   "$forall_var" = fun sy pol l t =>
       let lPol = %polarity% l in
       if pol == lPol then
-          %unseal% sy t (%blame% l)
+          %unwrap% sy t (%blame% l)
       else
-          %seal% sy t,
+          %wrap% sy t,
 
   "$row_extend" = fun contr case l t =>
       if (case t) then
@@ -57,20 +57,20 @@
           %blame% (%tag% "missing field `%{field}`" l),
 
   "$forall_tail" = fun sy pol acc l t =>
-      let magic_fld = "_%sealed" in
+      let magic_fld = "_%wrapped" in
       if pol == (%polarity% l) then
           if %has_field% magic_fld t then
               let rest = %record_remove% magic_fld t in
               if rest == {} then
                   let fail = %blame% (%tag% "polymorphic tail mismatch" l) in
-                  let inner = %unseal% sy (t."%{magic_fld}") fail in
+                  let inner = %unwrap% sy (t."%{magic_fld}") fail in
                   acc & inner
               else
                   %blame% (%tag% "extra field `%{%head% (%fields% rest)}`" l)
           else
               %blame% (%tag% "missing polymorphic part" l)
       else
-          %record_insert% magic_fld acc (%seal% sy t),
+          %record_insert% magic_fld acc (%wrap% sy t),
 
   "$dyn_tail" = fun acc l t => acc & t,
 


Test patch:
diff --git a/tests/contracts_fail.rs b/tests/contracts_fail.rs
index ea200921..511e0e4b 100644
--- a/tests/contracts_fail.rs
+++ b/tests/contracts_fail.rs
@@ -110,7 +110,7 @@ fn records_contracts_poly() {
         "let f | forall a. {foo: Num ; a} -> { ; a} = fun x => %record_insert% \"foo\" x 1 in f";
 
     assert_raise_blame!(&format!("{} {{}}", bad_cst));
-    // The polymorphic part is protected by hiding it. Thus, trying to access the protected field
+    // The polymorphic parts is protected by hiding it. Thus, trying to access the protected field
     // gives a field missing error, instead of a blame error.
     assert_matches!(
         eval(&format!("{} {{a=1}}", bad_acc)),