Repository: tweag/nickel
Pull request #473 (2021-11-23 09:06:18)

Destructuring
relate to #81 
Full implementation of the record destructuring as described in #81 . The list destructuring is not implemented yet. 
Typechecking is not performed at this time. will be done in future PR


Patch:
diff --git a/src/destruct.rs b/src/destruct.rs
new file mode 100644
index 00000000..03054cc4
--- /dev/null
+++ b/src/destruct.rs
@@ -0,0 +1,107 @@
+//! In this module, you have the main structures used in the destructuring feature of nickel.
+//! Also, there are implementation managing the generation of a contract from a pattern.
+
+use crate::identifier::Ident;
+use crate::label::Label;
+use crate::term::{Contract, MetaValue, RecordAttrs, RichTerm, Term};
+use crate::types::{AbsType, Types};
+
+/// A match field in a `Destruct` pattern.
+/// every field can contain a `MetaValue` either simply because they are annotated either because
+/// they are of the form `a ? "something"` (default value).
+#[derive(Debug, PartialEq, Clone)]
+pub enum Match {
+    /// `{..., a=b, ...}` will bind the field a of the record to variable a. Here, a is the first
+    /// field of this variant and b the optional one. The last field can actualy be a nested
+    /// destruct pattern.
+    Assign(Ident, MetaValue, (Option<Ident>, Destruct)),
+    /// Simple binding. the `Ident` is bind to a variable with the same name.
+    Simple(Ident, MetaValue),
+}
+
+/// Last match field of a `Destruct`.
+#[derive(Debug, PartialEq, Clone)]
+pub enum LastMatch {
+    /// The last field is a normal match. In this case the pattern is "closed" so every record
+    /// fields should be matched.
+    Match(Match),
+    /// The pattern is "open" `, ..}`. Optionaly you can bind a record containing the remaining
+    /// fields to an `Identifier` using the syntax `, ..y}`.
+    Ellipsis(Option<Ident>),
+}
+
+/// A destructuring pattern without the `x @` part.
+#[derive(Debug, PartialEq, Clone)]
+pub enum Destruct {
+    /// A record pattern, the onlyone implemented for now.
+    Record(Vec<Match>, bool, Option<Ident>),
+    /// A list destructuring. Not implemented.
+    List(Vec<Match>),
+    /// An empty destructuring. In this case, the pattern is a clasical `let var = something in
+    /// body` form.
+    Empty,
+}
+
+impl Destruct {
+    /// generate the metavalue containing the contract representing this pattern.
+    pub fn as_contract(self) -> MetaValue {
+        let open = self.is_open();
+        MetaValue {
+            contracts: vec![Contract {
+                types: Types(AbsType::Flat(
+                    Term::Record(
+                        self.inner()
+                            .into_iter()
+                            .map(|m| m.as_meta_field())
+                            .collect(),
+                        RecordAttrs { open },
+                    )
+                    .into(),
+                )),
+                label: Label::dummy(),
+            }],
+            ..Default::default()
+        }
+    }
+
+    /// Get the inner vector of `Matches` of the pattern. If `Empty` return a empty vector.
+    pub fn inner(self) -> Vec<Match> {
+        match self {
+            Destruct::Record(i, _, _) | Destruct::List(i) => i,
+            Destruct::Empty => vec![],
+        }
+    }
+
+    /// Is this pattern open? Does it finish with `, ..}` form?
+    pub fn is_open(&self) -> bool {
+        match self {
+            Destruct::Record(_, o, _) => *o,
+            _ => false,
+        }
+    }
+
+    /// check if the pattern is empty.
+    pub fn is_empty(&self) -> bool {
+        match self {
+            Destruct::Empty => true,
+            _ => false,
+        }
+    }
+}
+
+impl Match {
+    /// Convert the `Match` to a metavalue. It's used to generate the record contract representing
+    /// a record pattern destructuring.
+    pub fn as_meta_field(self) -> (Ident, RichTerm) {
+        match self {
+            Match::Assign(id, m, (_, Destruct::Empty)) | Match::Simple(id, m) => {
+                (id, Term::MetaValue(m).into())
+            }
+            Match::Assign(id, m, (_, d @ Destruct::Record(..))) => (
+                id,
+                Term::MetaValue(MetaValue::flatten(m, d.as_contract())).into(),
+            ),
+            Match::Assign(_id, _m, (_, _d @ Destruct::List(..))) => unimplemented!(),
+        }
+    }
+}
diff --git a/src/eval.rs b/src/eval.rs
index 13565a37..51c8233f 100644
--- a/src/eval.rs
+++ b/src/eval.rs
@@ -945,6 +945,7 @@ pub fn subst(rt: RichTerm, global_env: &Environment, env: &Environment) -> RichT
 
                 RichTerm::new(Term::Let(id, t1, t2), pos)
             }
+            p @ Term::LetPattern(..) => panic!("Pattern {:?} has not been transformed before evaluation", p),
             Term::App(t1, t2) => {
                 let t1 = subst_(t1, global_env, env, Cow::Borrowed(bound.as_ref()));
                 let t2 = subst_(t2, global_env, env, bound);
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 8e623295..a350e83a 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -13,6 +13,7 @@ use super::lexer::{Token, NormalToken, StringToken, MultiStringToken};
 
 use crate::{mk_app, mk_opn};
 use crate::identifier::Ident;
+use crate::destruct::{Match, LastMatch, Destruct};
 use crate::parser::error::ParseError;
 use crate::term::{BinaryOp, RichTerm, Term, UnaryOp, StrChunk, MetaValue,
     MergePriority, Contract, NAryOp, RecordAttrs};
@@ -56,6 +57,12 @@ MetaAnnotAtom: MetaValue = {
     },
 };
 
+DefaultAnnot: MetaValue = "?" <t: Term> => MetaValue {
+    priority: MergePriority::Default,
+    value: Some(t),
+    ..Default::default()
+};
+
 MetaAnnot: MetaValue = <anns: MetaAnnotAtom+> => anns.into_iter().fold(MetaValue::new(), MetaValue::flatten);
 
 Annot: MetaValue = {
@@ -73,7 +80,7 @@ Annot: MetaValue = {
 pub Term: RichTerm = WithPos<RootTerm>;
 
 pub ExtendedTerm: ExtendedTerm = {
-    "let" <id:Ident> <meta: Annot?> "=" <t: Term> => {
+    "let" <id: Ident> <meta: Annot?> "=" <t: Term> => {
         let t = if let Some(mut meta) = meta {
             let pos = t.pos;
             meta.value = Some(t);
@@ -82,14 +89,13 @@ pub ExtendedTerm: ExtendedTerm = {
         else {
             t
         };
-
         ExtendedTerm::ToplevelLet(id, t)
     },
     Term => ExtendedTerm::RichTerm(<>),
 }
 
 RootTerm: RichTerm = {
-    "let" <id:Ident> <meta: Annot?> "=" <t1: Term> "in"
+    "let" <pat:Pattern> <meta: Annot?> "=" <t1: Term> "in"
         <t2: Term> => {
         let t1 = if let Some(mut meta) = meta {
             let pos = t1.pos;
@@ -100,11 +106,11 @@ RootTerm: RichTerm = {
             t1
         };
 
-        mk_term::let_in(id, t1, t2)
+        mk_term::let_pat(pat.0, pat.1, t1, t2)
     },
-    <l: @L> "fun" <ps:Pattern+> "=>" <t: Term> <r: @R> => {
+    <l: @L> "fun" <ids:Ident+> "=>" <t: Term> <r: @R> => {
         let pos = mk_pos(src_id, l, r);
-        ps.into_iter().rev().fold(t, |t, p| RichTerm {
+        ids.into_iter().rev().fold(t, |t, p| RichTerm {
             term: Box::new(Term::Fun(p, t)),
             pos,
         })
@@ -252,8 +258,48 @@ FieldPathElem: FieldPathElem = {
     <WithPos<StrChunks>> => FieldPathElem::Expr(<>),
 };
 
-Pattern: Ident = {
-    Ident,
+#[inline]
+Pattern: (Option<Ident>,Destruct) = {
+    <id:(<Ident> "@")?> <pat:Destruct> => (id,pat),
+    Ident => (Some(<>),Destruct::Empty),
+};
+
+Destruct: Destruct = {
+    "{" <mut matches: (<Match> ",")*> <last:LastMatch?> "}" => {
+        let (open, rst) = match last {
+	    Some(LastMatch::Match(m)) => {
+	        matches.push(m);
+	        (false,None)
+	    },
+	    Some(LastMatch::Ellipsis(rst)) => (true, rst),
+	    _ => (false, None),
+	};
+	Destruct::Record(matches, open, rst)
+    },
+};
+
+Match: Match = {
+    <left:Ident> <anns: MetaAnnot?> <default: DefaultAnnot?> "=" <right: Pattern> => {
+	let meta = match (default, anns) {
+	    (Some(d), Some(m)) => MetaValue::flatten(d,m),
+	    (Some(m),_) | (_,Some(m)) => m,
+  	    _ => MetaValue::new(),
+	};
+	Match::Assign(left, meta, right)
+    },
+    <id:Ident> <anns: MetaAnnot?> <default: DefaultAnnot?> => {
+	let meta = match (default, anns) {
+	    (Some(d), Some(m)) => MetaValue::flatten(d,m),
+	    (Some(m),_) | (_,Some(m)) => m,
+  	    _ => MetaValue::new(),
+	};
+	Match::Simple(id, meta)
+    },
+};
+
+LastMatch: LastMatch = {
+    Match => LastMatch::Match(<>),
+    ".."<Ident?> => LastMatch::Ellipsis(<>),
 };
 
 Ident: Ident = <l:@L> <i: "identifier"> <r:@R> => Ident(i.to_string(), mk_pos(src_id, l, r));
@@ -558,6 +604,7 @@ extern {
         "true" => Token::Normal(NormalToken::True),
         "false" => Token::Normal(NormalToken::False),
 
+        "?" => Token::Normal(NormalToken::QuestionMark),
         "," => Token::Normal(NormalToken::Comma),
         ":" => Token::Normal(NormalToken::Colon),
         "$" => Token::Normal(NormalToken::Dollar),
diff --git a/src/label.rs b/src/label.rs
index 8733267f..e42ce1e2 100644
--- a/src/label.rs
+++ b/src/label.rs
@@ -261,7 +261,7 @@ impl Label {
             },
             arg_thunk: None,
             arg_pos: TermPos::None,
-            polarity: false,
+            polarity: true,
             path: Vec::new(),
         }
     }
diff --git a/src/lib.rs b/src/lib.rs
index 99ce8c76..08376bcd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,5 @@
 pub mod cache;
+pub mod destruct;
 pub mod environment;
 pub mod error;
 pub mod eval;
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index 7f043564..d1d2b1dd 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -78,6 +78,8 @@ pub enum NormalToken<'input> {
     #[token("false")]
     False,
 
+    #[token("?")]
+    QuestionMark,
     #[token(",")]
     Comma,
     #[token(":")]
diff --git a/src/term.rs b/src/term.rs
index c69fadbb..589fb446 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -16,6 +16,7 @@
 //! contracts, default values, documentation, etc. They bring such usually external object down to
 //! the term level, and together with [merge](../merge/index.html), they allow for flexible and
 //! modular definitions of contracts, record and metadata all together.
+use crate::destruct::Destruct;
 use crate::identifier::Ident;
 use crate::label::Label;
 use crate::position::TermPos;
@@ -63,6 +64,8 @@ pub enum Term {
     /// A let binding.
     #[serde(skip)]
     Let(Ident, RichTerm, RichTerm),
+    #[serde(skip)]
+    LetPattern(Option<Ident>, Destruct, RichTerm, RichTerm),
     /// An application.
     #[serde(skip)]
     App(RichTerm, RichTerm),
@@ -204,6 +207,12 @@ impl From<RichTerm> for MetaValue {
     }
 }
 
+impl Default for MetaValue {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 impl MetaValue {
     pub fn new() -> Self {
         MetaValue {
@@ -325,6 +334,7 @@ impl Term {
                 meta.value.iter_mut().for_each(func);
             }
             Let(_, ref mut t1, ref mut t2)
+            | LetPattern(_, _, ref mut t1, ref mut t2)
             | App(ref mut t1, ref mut t2)
             | Op2(_, ref mut t1, ref mut t2) => {
                 func(t1);
@@ -361,6 +371,7 @@ impl Term {
             Term::Wrapped(_, _) => Some("Wrapped"),
             Term::MetaValue(_) => Some("Metavalue"),
             Term::Let(_, _, _)
+            | Term::LetPattern(_, _, _, _)
             | Term::App(_, _)
             | Term::Var(_)
             | Term::Switch(..)
@@ -436,6 +447,7 @@ impl Term {
             Term::Var(id) => id.to_string(),
             Term::ParseError => String::from("<parse error>"),
             Term::Let(_, _, _)
+            | Term::LetPattern(_, _, _, _)
             | Term::App(_, _)
             | Term::Switch(..)
             | Term::Op1(_, _)
@@ -487,6 +499,7 @@ impl Term {
             | Term::List(_)
             | Term::Sym(_) => true,
             Term::Let(_, _, _)
+            | Term::LetPattern(_, _, _, _)
             | Term::App(_, _)
             | Term::Var(_)
             | Term::Switch(..)
@@ -522,6 +535,7 @@ impl Term {
             | Term::Enum(_)
             | Term::Sym(_) => true,
             Term::Let(_, _, _)
+            | Term::LetPattern(_, _, _, _)
             | Term::Record(..)
             | Term::List(_)
             | Term::Fun(_, _)
@@ -912,6 +926,14 @@ impl RichTerm {
                     pos,
                 }
             }
+            Term::LetPattern(id, pat, t1, t2) => {
+                let t1 = t1.traverse(f, state, method)?;
+                let t2 = t2.traverse(f, state, method)?;
+                RichTerm {
+                    term: Box::new(Term::LetPattern(id, pat, t1, t2)),
+                    pos,
+                }
+            }
             Term::App(t1, t2) => {
                 let t1 = t1.traverse(f, state, method)?;
                 let t2 = t2.traverse(f, state, method)?;
@@ -1203,6 +1225,27 @@ pub mod make {
         Term::Let(id.into(), t1.into(), t2.into()).into()
     }
 
+    pub fn let_pat<I, D, T1, T2>(id: Option<I>, pat: D, t1: T1, t2: T2) -> RichTerm
+    where
+        T1: Into<RichTerm>,
+        T2: Into<RichTerm>,
+        D: Into<Destruct>,
+        I: Into<Ident>,
+    {
+        match pat.into() {
+            d @ (Destruct::Record(..) | Destruct::List(_)) => {
+                Term::LetPattern(id.map(|i| i.into()), d, t1.into(), t2.into()).into()
+            }
+            Destruct::Empty => {
+                if let Some(id) = id {
+                    let_in(id, t1, t2)
+                } else {
+                    Term::Null.into()
+                }
+            }
+        }
+    }
+
     #[cfg(test)]
     pub fn if_then_else<T1, T2, T3>(cond: T1, t1: T2, t2: T3) -> RichTerm
     where
diff --git a/src/transformations.rs b/src/transformations.rs
index 1d270c1b..9b5418ff 100644
--- a/src/transformations.rs
+++ b/src/transformations.rs
@@ -10,6 +10,130 @@ use std::path::PathBuf;
 
 generate_counter!(FreshVarCounter, usize);
 
+/// # Desugarise destructuring
+///
+/// Replace a let-binding with destructuring by a classical let-binding.
+/// It will first destruct the pattern and create a new var for each field of the patternn.
+/// After that, it will construct a new Record/List from the extracted fields.
+///
+/// ## Example:
+///
+/// Taking the let pattern:
+/// ```text
+/// let x @ {a, b=d, ..} = {a=1,b=2,c="ignored"} in ...
+/// ```
+/// after transformation we will have:
+/// ```text
+/// let x = {a=1,b=2,c="ignored"} in
+/// let a = x.a in
+/// let d = x.b in
+/// ...
+/// ```
+pub mod desugar_destructuring {
+    use super::{Ident, RichTerm, Term};
+    use crate::destruct::{Destruct, Match};
+    use crate::term::make::{op1, op2};
+    use crate::term::MetaValue;
+    use crate::term::{BinaryOp::DynRemove, UnaryOp::StaticAccess};
+
+    /// Wrap the desugarized term in a meta value containing the "Record contract" needed to check
+    /// the pattern exaustivity and also fill the default values (`?` operator) if not presents in the record.
+    /// This function should be, in the general case, considered as the entry point of this
+    /// transformation.
+    pub fn desugar_with_contract(rt: RichTerm) -> RichTerm {
+        if let Term::LetPattern(x, pat, t_, body) = *rt.term {
+            let pos = body.pos;
+            let meta = pat.clone().as_contract();
+            let t_ = {
+                let t_pos = t_.pos;
+                RichTerm::new(
+                    Term::MetaValue(MetaValue {
+                        value: Some(t_),
+                        ..meta
+                    }),
+                    t_pos,
+                )
+            };
+            desugar(RichTerm::new(Term::LetPattern(x, pat, t_, body), pos))
+        } else {
+            rt
+        }
+    }
+
+    /// Main transformation function to desuggar let patterns.
+    /// WARNING: In a real usage case, you will want to generate also the matavalue associated to
+    /// this pattern destructuring. Do not concider this function as the entry point of the
+    /// transformation. For that, use `desugar_with_contract`.
+    pub fn desugar(rt: RichTerm) -> RichTerm {
+        if let Term::LetPattern(x, pat, t_, body) = *rt.term {
+            let pos = body.pos;
+            let x = x.unwrap_or_else(super::fresh_var);
+            RichTerm::new(
+                Term::Let(
+                    x.clone(),
+                    t_,
+                    destruct_term(x.clone(), &pat, bind_open_field(x, &pat, body)),
+                ),
+                pos,
+            )
+        } else {
+            rt
+        }
+    }
+
+    /// Wrap `body` in a let construct binding the open part of the pattern to the required value.
+    /// Having `let {a,..y} = {a=1, b=2, c=3} in <BODY>` will bind `y` to {b=2,c=3} in `BODY`.
+    /// Here, the x, is the identifier pointing to the full record. If having `val @ {...} = ... in
+    /// ...` the param x should be `Ident("val")` but if we have a `@` binding less form, you will
+    /// probably generate a fresh variable.
+    fn bind_open_field(x: Ident, pat: &Destruct, body: RichTerm) -> RichTerm {
+        let (matches, var) = match pat {
+            Destruct::Record(matches, true, Some(x)) => (matches, x.clone()),
+            Destruct::Record(matches, true, None) => (matches, super::fresh_var()),
+            Destruct::Record(_, false, None) | Destruct::Empty => return body,
+            _ => panic!("A closed pattern can not have a rest binding"),
+        };
+        Term::Let(
+            var.clone(),
+            matches.iter().fold(Term::Var(x).into(), |x, m| match m {
+                Match::Simple(i, _) | Match::Assign(i, _, _) => {
+                    op2(DynRemove(), Term::Str(i.to_string()), x)
+                }
+            }),
+            body,
+        )
+        .into()
+    }
+
+    /// Core of the destructuring. Bind all the variables of the pattern except the "open" (`..y`)
+    /// part. For that, see `bind_open_field`.
+    fn destruct_term(x: Ident, pat: &Destruct, body: RichTerm) -> RichTerm {
+        let pos = body.pos;
+        match pat {
+            Destruct::Record(matches, ..) => matches.iter().fold(body, move |t, m| match m {
+                Match::Simple(id, _) => RichTerm::new(
+                    Term::Let(
+                        id.clone(),
+                        op1(StaticAccess(id.clone()), Term::Var(x.clone())),
+                        t,
+                    ),
+                    pos,
+                ),
+                Match::Assign(f, _, (id, pat)) => desugar(RichTerm::new(
+                    Term::LetPattern(
+                        id.clone(),
+                        pat.clone(),
+                        op1(StaticAccess(f.clone()), Term::Var(x.clone())),
+                        t,
+                    ),
+                    pos,
+                )),
+            }),
+            _ => body,
+        }
+    }
+}
+
 /// Share normal form.
 ///
 /// Replace the subexpressions of WHNFs that are not functions by thunks, such that they can be
@@ -269,6 +393,8 @@ pub fn transform(rt: RichTerm) -> RichTerm {
     let rt = rt
         .traverse(
             &mut |rt: RichTerm, _| -> Result<RichTerm, ()> {
+                // before anything, we have to desugar the syntax
+                let rt = desugar_destructuring::desugar_with_contract(rt);
                 // We need to do contract generation before wrapping stuff in variables
                 let rt = apply_contracts::transform_one(rt);
                 Ok(rt)
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 1a49fa69..ee9220f6 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -639,6 +639,50 @@ fn type_check_<S, E>(
             envs.insert(x.clone(), ty_let);
             type_check_(state, envs, lin, linearizer, strict, rt, ty)
         }
+        Term::LetPattern(x, pat, re, rt) => {
+            use crate::destruct::*;
+            fn inject_pat_vars(pat: &Destruct, envs: &mut Envs) {
+                match pat {
+                    Destruct::Record(matches, _, rst) => {
+                        if let Some(id) = rst {
+                            envs.insert(id.clone(), TypeWrapper::Concrete(AbsType::Dyn()));
+                        }
+                        matches.iter().for_each(|m| match m {
+                            Match::Simple(id, ..) => {
+                                envs.insert(id.clone(), TypeWrapper::Concrete(AbsType::Dyn()))
+                            }
+                            Match::Assign(_, _, (bind_id, pat)) => {
+                                if let Some(id) = bind_id {
+                                    envs.insert(id.clone(), TypeWrapper::Concrete(AbsType::Dyn()));
+                                }
+                                if !pat.is_empty() {
+                                    inject_pat_vars(pat, envs);
+                                }
+                            }
+                        });
+                    }
+                    _ => (),
+                }
+            }
+            let ty_let = binding_type(re.as_ref(), &envs, state.table, strict);
+            type_check_(
+                state,
+                envs.clone(),
+                lin,
+                linearizer.scope(ScopeId::Left),
+                strict,
+                re,
+                ty_let.clone(),
+            )?;
+
+            // TODO typecheck the interior of the patern
+            if let Some(x) = x {
+                envs.insert(x.clone(), ty_let);
+                inject_pat_vars(pat, &mut envs);
+            }
+            //type_check_(state, envs, strict, rt, ty)
+            Ok(())
+        }
         Term::App(e, t) => {
             let src = TypeWrapper::Ptr(new_var(state.table));
             let arr = mk_tyw_arrow!(src.clone(), ty);


Test patch:
diff --git a/tests/common.rs b/tests/common.rs
index dc64c18e..6b67d04e 100644
--- a/tests/common.rs
+++ b/tests/common.rs
@@ -7,3 +7,9 @@ pub fn eval(s: impl std::string::ToString) -> Result<Term, Error> {
     let mut p = Program::new_from_source(Cursor::new(s.to_string()), "test").unwrap();
     p.eval()
 }
+
+pub fn eval_file(f: &str) -> Result<Term, Error> {
+    let mut p =
+        Program::new_from_file(format!("{}/tests/{}", env!("CARGO_MANIFEST_DIR"), f)).unwrap();
+    p.eval()
+}
diff --git a/tests/destructuring.rs b/tests/destructuring.rs
new file mode 100644
index 00000000..c839de58
--- /dev/null
+++ b/tests/destructuring.rs
@@ -0,0 +1,87 @@
+use assert_matches::assert_matches;
+use nickel::error::{Error, EvalError, TypecheckError};
+use nickel::term::Term;
+
+#[path = "./common.rs"]
+mod common;
+use common::eval_file;
+
+#[test]
+fn simple() {
+    assert_eq!(eval_file("destructuring/simple.ncl"), Ok(Term::Bool(true)));
+}
+
+#[test]
+fn assign() {
+    assert_eq!(eval_file("destructuring/assign.ncl"), Ok(Term::Bool(true)));
+}
+
+#[test]
+fn atbind() {
+    assert_eq!(eval_file("destructuring/atbind.ncl"), Ok(Term::Bool(true)));
+}
+
+#[test]
+fn open() {
+    assert_eq!(eval_file("destructuring/open.ncl"), Ok(Term::Bool(true)));
+}
+
+#[test]
+fn nested() {
+    assert_eq!(eval_file("destructuring/nested.ncl"), Ok(Term::Bool(true)));
+}
+
+#[test]
+fn rest() {
+    assert_eq!(eval_file("destructuring/rest.ncl"), Ok(Term::Bool(true)));
+}
+
+#[test]
+fn default() {
+    assert_eq!(eval_file("destructuring/default.ncl"), Ok(Term::Bool(true)));
+}
+
+#[test]
+fn typecontract() {
+    assert_eq!(
+        eval_file("destructuring/typecontract.ncl"),
+        Ok(Term::Bool(true))
+    );
+}
+
+#[test]
+fn mixed() {
+    assert_eq!(eval_file("destructuring/mixed.ncl"), Ok(Term::Bool(true)));
+}
+
+#[test]
+fn assign_fail() {
+    assert_matches!(
+        eval_file("destructuring/assign_fail.ncl"),
+        Err(Error::EvalError(EvalError::UnboundIdentifier(..)))
+    );
+}
+
+#[test]
+fn closed_fail() {
+    assert_matches!(
+        eval_file("destructuring/closed_fail.ncl"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+}
+
+#[test]
+fn rest_fail() {
+    assert_matches!(
+        eval_file("destructuring/rest_fail.ncl"),
+        Err(Error::EvalError(EvalError::FieldMissing(..)))
+    );
+}
+
+#[test]
+fn typecontract_fail() {
+    assert_matches!(
+        eval_file("destructuring/typecontract_fail.ncl"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+}
diff --git a/tests/destructuring/assign.ncl b/tests/destructuring/assign.ncl
new file mode 100644
index 00000000..8a3e3454
--- /dev/null
+++ b/tests/destructuring/assign.ncl
@@ -0,0 +1,3 @@
+
+let {a=x, b=y} = {a=1, b=2} in
+x + y == 3
diff --git a/tests/destructuring/assign_fail.ncl b/tests/destructuring/assign_fail.ncl
new file mode 100644
index 00000000..0d44692b
--- /dev/null
+++ b/tests/destructuring/assign_fail.ncl
@@ -0,0 +1,3 @@
+
+let {a=x, b=y} = {a=1, b=2} in
+a + b == 3
diff --git a/tests/destructuring/atbind.ncl b/tests/destructuring/atbind.ncl
new file mode 100644
index 00000000..67d61b9a
--- /dev/null
+++ b/tests/destructuring/atbind.ncl
@@ -0,0 +1,3 @@
+
+let x @ {..} = {a=1, b=2} in
+x.a + x.b == 3
diff --git a/tests/destructuring/closed_fail.ncl b/tests/destructuring/closed_fail.ncl
new file mode 100644
index 00000000..0912d960
--- /dev/null
+++ b/tests/destructuring/closed_fail.ncl
@@ -0,0 +1,3 @@
+
+let {a} = {a=1, b=2} in
+a == 1
diff --git a/tests/destructuring/default.ncl b/tests/destructuring/default.ncl
new file mode 100644
index 00000000..81f78aba
--- /dev/null
+++ b/tests/destructuring/default.ncl
@@ -0,0 +1,3 @@
+
+let {a ? 1, b} = {b=2} in
+a + b == 3
diff --git a/tests/destructuring/mixed.ncl b/tests/destructuring/mixed.ncl
new file mode 100644
index 00000000..eeed102c
--- /dev/null
+++ b/tests/destructuring/mixed.ncl
@@ -0,0 +1,4 @@
+
+let baz = 10 in
+let x @ {foo ? 10, b = xb, d ? baz = bar, c = xc @ {ca, ..}, ..y} = {a = 2, b = 3, c = {ca = 22, cb = 0}} in
+y.a + x.b - xb + x.c.ca - xc.ca - ca + foo + bar == 0
diff --git a/tests/destructuring/nested.ncl b/tests/destructuring/nested.ncl
new file mode 100644
index 00000000..c9adaef2
--- /dev/null
+++ b/tests/destructuring/nested.ncl
@@ -0,0 +1,3 @@
+
+let {a={b}} = {a={b=1}} in
+b == 1
diff --git a/tests/destructuring/open.ncl b/tests/destructuring/open.ncl
new file mode 100644
index 00000000..62900b87
--- /dev/null
+++ b/tests/destructuring/open.ncl
@@ -0,0 +1,3 @@
+
+let {a, ..} = {a=1, b=2} in
+a == 1
diff --git a/tests/destructuring/rest.ncl b/tests/destructuring/rest.ncl
new file mode 100644
index 00000000..58d73634
--- /dev/null
+++ b/tests/destructuring/rest.ncl
@@ -0,0 +1,3 @@
+
+let x @ {..y} = {a=1, b=2} in
+x == y
diff --git a/tests/destructuring/rest_fail.ncl b/tests/destructuring/rest_fail.ncl
new file mode 100644
index 00000000..28904d5c
--- /dev/null
+++ b/tests/destructuring/rest_fail.ncl
@@ -0,0 +1,3 @@
+
+let {a, ..y} = {a=1, b=2} in
+y.a
diff --git a/tests/destructuring/simple.ncl b/tests/destructuring/simple.ncl
new file mode 100644
index 00000000..a34001ec
--- /dev/null
+++ b/tests/destructuring/simple.ncl
@@ -0,0 +1,3 @@
+
+let {a, b} = {a=1, b=2} in
+a + b == 3
diff --git a/tests/destructuring/typecontract.ncl b/tests/destructuring/typecontract.ncl
new file mode 100644
index 00000000..1127d2d2
--- /dev/null
+++ b/tests/destructuring/typecontract.ncl
@@ -0,0 +1,3 @@
+
+let {a | Num, b | Num} = {a=1, b=2} in
+a + b == 3
diff --git a/tests/destructuring/typecontract_fail.ncl b/tests/destructuring/typecontract_fail.ncl
new file mode 100644
index 00000000..26ed3f9e
--- /dev/null
+++ b/tests/destructuring/typecontract_fail.ncl
@@ -0,0 +1,3 @@
+
+let {a | Str} = {a=1} in
+a