Repository: tweag/nickel
Pull request #313 (2021-03-04 09:23:44)

Migrate failure tests
Partly address #161. Follow-up of #306.

- migrate integration tests expected to fail from `Program` (where they shouldn't really be) to the test-suite.
- add checks that the error is the expected one. A lot of these tests would just check that an error occurred. Some were useless because the error was actually a parse error as the tested expression had a typo, instead of an expected, say, blame error.
- doing so, remove deprecated metavalues syntax `Contract`, `Default`, etc. from the tests, and definitively remove this syntax from the parser.

Some tests were simplified or refactored a bit, but this is mostly a mechanical, semantics-preserving conversion, without questioning the relevance of the tests.


Patch:
diff --git a/src/cache.rs b/src/cache.rs
index 3bdbe11b..058b9f9a 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -739,7 +739,6 @@ pub fn timestamp(path: impl AsRef<OsStr>) -> io::Result<SystemTime> {
 }
 
 /// Provide mockup import resolvers for testing purpose.
-#[cfg(test)]
 pub mod resolvers {
     use super::*;
 
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 4781cefa..b8811c06 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -183,52 +183,6 @@ RecordOperationChain: RichTerm = {
 
 Atom: RichTerm = {
     "(" <Term> ")",
-    <l: @L> "Assume(" <ty: Types> "," <t: Term> ")" <r: @R> =>
-        RichTerm::from(Term::Assume(ty.clone(), mk_label(ty, src_id, l, r), t)),
-    <l: @L> "Contract(" <ty: Types> ")" <r: @R> =>
-        RichTerm::from(
-            Term::MetaValue(
-                MetaValue {
-                    doc: None,
-                    contract: Some((ty.clone(), mk_label(ty, src_id, l, r))),
-                    priority: Default::default(),
-                    value: None,
-                }
-            )
-        ),
-    "Default(" <t: Term> ")" =>
-        RichTerm::from(
-            Term::MetaValue(
-                MetaValue {
-                    doc: None,
-                    contract: None,
-                    priority: MergePriority::Default,
-                    value: Some(t),
-                }
-            )
-        ),
-    <l: @L> "ContractDefault(" <ty: Types> "," <t: Term> ")" <r: @R> =>
-        RichTerm::from(
-            Term::MetaValue(
-                MetaValue {
-                    doc: None,
-                    contract: Some((ty.clone(), mk_label(ty, src_id, l, r))),
-                    priority: MergePriority::Default,
-                    value: Some(t),
-                }
-            )
-        ),
-    "Docstring(" <s: Str> "," <t: Term> ")" =>
-        RichTerm::from(
-            Term::MetaValue(
-                MetaValue {
-                    doc: Some(s),
-                    contract: None,
-                    priority: Default::default(),
-                    value: Some(t)
-                }
-            )
-        ),
     "num literal" => RichTerm::from(Term::Num(<>)),
     "null" => RichTerm::from(Term::Null),
     Bool => RichTerm::from(Term::Bool(<>)),
@@ -651,12 +605,6 @@ extern {
 
         "tag" => Token::Normal(NormalToken::Tag),
         "applyContract" => Token::Normal(NormalToken::ApplyContract),
-        "Assume(" => Token::Normal(NormalToken::Assume),
-        "Promise(" => Token::Normal(NormalToken::Promise),
-        "Default(" => Token::Normal(NormalToken::Deflt),
-        "Contract(" => Token::Normal(NormalToken::Contract),
-        "ContractDefault(" => Token::Normal(NormalToken::ContractDeflt),
-        "Docstring(" => Token::Normal(NormalToken::Docstring),
         "isNum" => Token::Normal(NormalToken::IsNum),
         "isBool" => Token::Normal(NormalToken::IsBool),
         "isStr" => Token::Normal(NormalToken::IsStr),
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index 0cd8503f..45c1b5ff 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -143,19 +143,6 @@ pub enum NormalToken<'input> {
 
     #[token("%tag%")]
     Tag,
-    #[token("Assume(")]
-    Assume,
-    #[token("Promise(")]
-    Promise,
-    #[token("Default(")]
-    Deflt,
-    #[token("Contract(")]
-    Contract,
-    #[token("ContractDefault(")]
-    ContractDeflt,
-    #[token("Docstring(")]
-    Docstring,
-
     #[token("%isNum%")]
     IsNum,
     #[token("%isBool%")]
diff --git a/src/program.rs b/src/program.rs
index a7f72b4d..a93a0bfa 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -211,7 +211,6 @@ mod tests {
     use crate::error::EvalError;
     use crate::parser::{grammar, lexer};
     use crate::position::TermPos;
-    use assert_matches::assert_matches;
     use codespan::Files;
     use std::io::Cursor;
 
@@ -228,19 +227,7 @@ mod tests {
             .ok()
     }
 
-    fn eval_string(s: &str) -> Result<Term, Error> {
-        let src = Cursor::new(s);
-
-        let mut p = Program::new_from_source(src, "<test>").map_err(|io_err| {
-            Error::EvalError(EvalError::Other(
-                format!("IO error: {}", io_err),
-                TermPos::None,
-            ))
-        })?;
-        p.eval()
-    }
-
-    fn eval_string_full(s: &str) -> Result<Term, Error> {
+    fn eval_full(s: &str) -> Result<Term, Error> {
         let src = Cursor::new(s);
 
         let mut p = Program::new_from_source(src, "<test>").map_err(|io_err| {
@@ -252,299 +239,6 @@ mod tests {
         p.eval_full()
     }
 
-    #[test]
-    fn parse_error() {
-        let res = eval_string("let g  = funky x => x in g true");
-
-        if let Ok(_) = res {
-            panic!("This test should have returned Err()!");
-        }
-    }
-
-    #[test]
-    fn simple_type_check() {
-        let res = eval_string("let x = 5 in if %isNum% x then true else 1");
-
-        assert_eq!(Ok(Term::Bool(true)), res);
-    }
-
-    #[test]
-    fn promise_fail() {
-        let res = eval_string("let bool = fun l t => if isBool t then t else blame l in 5 : Bool");
-
-        if let Ok(_) = res {
-            panic!("This expression should return an error!");
-        }
-    }
-
-    #[test]
-    fn flat_contract_fail() {
-        let res = eval_string(
-            "let alwaysTrue = fun l t => let boolT = Assume(Bool, t) in
-    if boolT then boolT else blame l in
-Assume(#alwaysTrue, false)
-",
-        );
-        if let Ok(_) = res {
-            panic!("This expression should return an error!");
-        }
-    }
-
-    #[test]
-    fn id_fail() {
-        let res = eval_string(
-            "let id = Assume(forall a. a -> a, fun x => false) in
-            id false",
-        );
-        if let Ok(_) = res {
-            panic!("This expression should return an error!");
-        }
-    }
-
-    #[test]
-    fn enum_simple() {
-        eval_string("`far : <foo, bar>").unwrap_err();
-    }
-
-    #[test]
-    fn enum_complex() {
-        eval_string(
-            "let f : <foo, bar> -> Num =
-            fun x => switch { foo => 1, bar => 2, } x in
-            f `boo",
-        )
-        .unwrap_err();
-    }
-
-    #[test]
-    fn row_types() {
-        eval_string("Assume(< >, 123)").unwrap_err();
-    }
-
-    #[test]
-    fn records_accessing() {
-        eval_string("({ $(if false then \"foo\" else \"bar\") = false; bar = true; }).foo")
-            .unwrap_err();
-    }
-
-    #[test]
-    fn lists() {
-        eval_string("%elemAt% [1,2,3] (-1)").unwrap_err();
-        eval_string("%elemAt% [1,2,3] 4").unwrap_err();
-
-        assert_eq!(
-            eval_string("%head% [\"a\",\"b\",\"c\"]"),
-            Ok(Term::Str(String::from("a")))
-        );
-        eval_string("%head% []").unwrap_err();
-
-        assert_eq!(
-            eval_string("%length% (%tail% [true,false,1])"),
-            Ok(Term::Num(2.0))
-        );
-        eval_string("%tail% []").unwrap_err();
-    }
-
-    #[test]
-    #[should_panic]
-    fn merge_record_failure() {
-        eval_string("({a=1} & {a=2}).a").unwrap();
-    }
-
-    #[test]
-    #[should_panic]
-    fn enriched_terms_contract_default_fail() {
-        eval_string("ContractDefault(Num, true)").unwrap();
-    }
-
-    #[test]
-    fn merge_default() {
-        eval_string("({a=Default(1);} & {a=Default(2);}).a").unwrap_err();
-    }
-
-    #[test]
-    fn merge_contract() {
-        eval_string("let r = {a=2;} & {a=Contract(Bool)} in r.a").unwrap_err();
-    }
-
-    #[test]
-    fn merge_default_contract() {
-        eval_string("({a=2;} & {b=Contract(Num)} & {b=Default(true)}).b").unwrap_err();
-    }
-
-    fn make_composed_contract(value: &str) -> Result<Term, Error> {
-        let s = format!(
-            "let Y = fun f => (fun x => f (x x)) (fun x => f (x x)) in
-             let dec = fun x => x + (-1) in
-             let or = fun x => fun y => if x then x else y in
-             let isEven_ = Y (fun f =>
-                 (fun x =>
-                     if x == 0 then true
-                     else (
-                         if dec x == 0 then false
-                         else (f (dec (dec x)))
-                     )
-                 )
-             ) in
-             let isDivBy3_ = Y (fun f =>
-                 (fun x =>
-                     if x == 0 then true
-                     else (
-                         if or (dec (dec x) == 0) (dec x == 0) then false
-                         else (f (dec (dec (dec x))))
-                     )
-                 )
-             ) in
-             let toCtr = fun f => fun l => fun x => (
-               if (%isNum% x) then (
-                 if (f x) then x else %blame% l)
-               else %blame% l
-             ) in
-             let isEven = toCtr isEven_ in
-             let isDivBy3 = toCtr isDivBy3_ in
-             let composed = {{a=Contract(#isEven)}} & {{a=Contract(#isDivBy3)}} in
-             ({{a={}}} & composed).a",
-            value
-        );
-
-        eval_string(s.as_str())
-    }
-
-    #[test]
-    fn merge_compose_contract() {
-        make_composed_contract("1").unwrap_err();
-        make_composed_contract("14").unwrap_err();
-        make_composed_contract("27").unwrap_err();
-        make_composed_contract("10").unwrap_err();
-        make_composed_contract("35").unwrap_err();
-    }
-
-    #[test]
-    fn string_chunks() {
-        match eval_string(r##""bad type #{1 + 1}""##) {
-            Err(Error::EvalError(EvalError::TypeError(_, _, _, _))) => (),
-            _ => assert!(false),
-        };
-    }
-
-    #[test]
-    fn arithmetic_expr() {
-        eval_string("1 + 1 / (1 - 1)").unwrap_err();
-    }
-
-    #[test]
-    fn comparisons() {
-        eval_string("1 < 2 < 3").unwrap_err();
-        eval_string("1 < 2 > 3").unwrap_err();
-        eval_string("\"a\" < 2").unwrap_err();
-        eval_string("true <= []").unwrap_err();
-        eval_string("\"a\" > \"b\"").unwrap_err();
-        eval_string("\"a\" >= \"b\"").unwrap_err();
-    }
-
-    #[test]
-    fn boolean_op() {
-        eval_string("let throw = Assume(#fail, 0) in false || true && throw").unwrap_err();
-        eval_string("0 && true").unwrap_err();
-        eval_string("\"a\" || false").unwrap_err();
-    }
-
-    #[test]
-    fn records_contracts_simple() {
-        eval_string("Assume({}, {a=1})").unwrap_err();
-
-        eval_string("let x = Assume({a: Num, s: Str}, {a = 1; s = 2}) in %deepSeq% x x")
-            .unwrap_err();
-        eval_string("let x = Assume({a: Num, s: Str}, {a = \"a\"; s = \"b\"}) in %deepSeq% x x")
-            .unwrap_err();
-        eval_string("let x = Assume({a: Num, s: Str}, {a = 1}) in %deepSeq% x x").unwrap_err();
-        eval_string("let x = Assume({a: Num, s: Str}, {s = \"a\"}) in %deepSeq% x x").unwrap_err();
-        eval_string(
-            "let x = Assume({a: Num, s: Str}, {a = 1; s = \"a\"; extra = 1}) in %deepSeq% x x",
-        )
-        .unwrap_err();
-
-        eval_string(
-            "let x = Assume({a: Num, s: {foo: Bool} }, {a = 1; s = { foo = 2}}) in %deepSeq% x x",
-        )
-        .unwrap_err();
-        eval_string("let x = Assume({a: Num, s: {foo: Bool} }, {a = 1; s = { foo = true; extra = 1}}) in %deepSeq% x x").unwrap_err();
-        eval_string("let x = Assume({a: Num, s: {foo: Bool} }, {a = 1; s = { }}) in %deepSeq% x x")
-            .unwrap_err();
-    }
-
-    #[test]
-    fn records_contracts_poly() {
-        // TODO: this test should ultimately pass (i.e., the program should be rejected)
-        // but this is not yet the case.
-        // eval_string(&format!("{} {{foo = 2}}", extd)).unwrap_err();
-
-        let remv = "let f = Assume(forall a. {foo: Num | a} -> { | a}, fun x => x-$\"foo\") in f";
-        eval_string(&format!("{} {{}}", remv)).unwrap_err();
-
-        let bad_cst = "let f = Assume(forall a. { | a} -> { | a}, fun x => {a=1}) in f";
-        let bad_acc = "let f = Assume(forall a. { | a} -> { | a}, fun x => seq (x.a) x) in f";
-        let bad_extd =
-            "let f = Assume(forall a. { | a} -> {foo: Num | a}, fun x => x-$\"foo\" in f";
-        let bad_rmv =
-            "let f = Assume(forall a. {foo: Num | a} -> { | a}, fun x => x$[\"foo\"=1]) in f";
-
-        eval_string(&format!("{} {{}}", bad_cst)).unwrap_err();
-        eval_string(&format!("{} {{a=1}}", bad_acc)).unwrap_err();
-        eval_string(&format!("{} {{foo=1}}", bad_extd)).unwrap_err();
-        eval_string(&format!("{} {{}}", bad_rmv)).unwrap_err();
-        eval_string(
-            "
-            let f = Assume(
-                forall a. ((forall b. ({a: Num, b: Num |b} })
-                    -> ({ a: Num | b}))
-                    -> {a: Num | a}
-                    -> { | a}),
-                fun f rec => (f rec) -$ \"a\" -$ \"b\") in
-            f (fun x => x) {a = 1; b = bool; c = 3}",
-        )
-        .unwrap_err();
-    }
-
-    #[test]
-    fn lists_contracts() {
-        use crate::label::ty_path::Elem;
-
-        assert_matches!(
-            eval_string("%deepSeq% ([1, \"a\"] | List Num) 0"),
-            Err(Error::EvalError(EvalError::BlameError(..)))
-        );
-        assert_matches!(
-            eval_string("1 | List"),
-            Err(Error::EvalError(EvalError::BlameError(..)))
-        );
-        assert_matches!(
-            eval_string("(fun x => x) | List"),
-            Err(Error::EvalError(EvalError::BlameError(..)))
-        );
-
-        let res = eval_string("%deepSeq% ([{a = [1]}] | List {a: List Str}) false");
-        match &res {
-            Err(Error::EvalError(EvalError::BlameError(ref l, _))) => {
-                assert_matches!(l.path.as_slice(), [Elem::List, Elem::Field(id), Elem::List] if &id.to_string() == "a")
-            }
-            err => panic!("expected blame error, got {:?}", err),
-        }
-        // Check that reporting doesn't panic. Provide a dummy file database, as we won't report
-        // the error message but just check that it can be built.
-        let mut files = Files::new();
-        res.unwrap_err().to_diagnostic(&mut files, None);
-
-        let res = eval_string("(%elemAt% (({foo = [(fun x => \"a\")]} | {foo: List (forall a. a -> Num)}).foo) 0) false");
-        match &res {
-            Err(Error::EvalError(EvalError::BlameError(ref l, _))) => {
-                assert_matches!(l.path.as_slice(), [Elem::Field(id), Elem::List, Elem::Codomain] if &id.to_string() == "foo")
-            }
-            err => panic!("expected blame error, got {:?}", err),
-        }
-        res.unwrap_err().to_diagnostic(&mut files, None);
-    }
-
     #[test]
     fn evaluation_full() {
         use crate::mk_record;
@@ -557,22 +251,21 @@ Assume(#alwaysTrue, false)
             *tmp.term
         }
 
-        let t =
-            clean_pos(eval_string_full("[(1 + 1), (\"a\" ++ \"b\"), ([ 1, [1 + 2] ])]").unwrap());
+        let t = clean_pos(eval_full("[(1 + 1), (\"a\" ++ \"b\"), ([ 1, [1 + 2] ])]").unwrap());
         let mut expd = parse("[2, \"ab\", [1, [3]]]").unwrap();
+
         // String are parsed as StrChunks, but evaluated to Str, so we need to hack list a bit
         if let Term::List(ref mut data) = *expd.term {
             *data.get_mut(1).unwrap() = mk_term::string("ab");
         } else {
             panic!();
         }
+
         assert_eq!(t, *expd.term);
 
         let t = clean_pos(
-            eval_string_full(
-                "let x = 1 in let y = 1 + x in let z = { foo = {bar = { baz  = y } } } in z",
-            )
-            .unwrap(),
+            eval_full("let x = 1 in let y = 1 + x in let z = { foo = {bar = { baz  = y } } } in z")
+                .unwrap(),
         );
         // Records are parsed as RecRecords, so we need to build one by hand
         let expd = mk_record!((
@@ -585,26 +278,6 @@ Assume(#alwaysTrue, false)
         // Check that substitution do not replace bound variables. Before the fixing commit, this
         // example would go into an infinite loop, and stack overflow. If it does, this just means
         // that this test fails.
-        eval_string_full("{y = fun x => x; x = fun y => y}").unwrap();
-    }
-
-    #[test]
-    fn infinite_loops() {
-        assert_matches!(
-            eval_string("{x = x}.x"),
-            Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
-        );
-        assert_matches!(
-            eval_string("{x = y; y = z; z = x }.x"),
-            Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
-        );
-        assert_matches!(
-            eval_string("{x = y + z; y = z + x; z = 1}.x"),
-            Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
-        );
-        assert_matches!(
-            eval_string("{x = (fun a => a + y) 0; y = (fun a => a + x) 0}.x"),
-            Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
-        );
+        eval_full("{y = fun x => x; x = fun y => y}").unwrap();
     }
 }
diff --git a/src/serialize.rs b/src/serialize.rs
index 6b3f7310..c74176a2 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -352,17 +352,17 @@ mod tests {
     #[test]
     fn enriched_values() {
         assert_json_eq!(
-            "{a = Default(1); b = Docstring(\"doc\", 2+2); c = 3}",
+            "{a | default = 1; b | doc \"doc\" = 2+2; c = 3}",
             json!({"a": 1, "b": 4, "c": 3})
         );
 
         assert_json_eq!(
-            "{a = {b = Default({})} & {b = {c = Default(if true then `faux else `vrai)}}}",
+            "{a = {b | default = {}} & {b.c | default = (if true then `faux else `vrai)}}",
             json!({"a": {"b": {"c": "faux"}}})
         );
 
         assert_json_eq!(
-            "{baz = Default({subfoo = Default(!false)} & {subbar = Default(1 - 1)})}",
+            "{baz | default = {subfoo | default = !false} & {subbar | default = 1 - 1}}",
             json!({"baz": {"subfoo": true, "subbar": 0}})
         );
     }
diff --git a/src/term.rs b/src/term.rs
index 485a22d4..574e05f1 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -1047,7 +1047,6 @@ pub mod make {
         mk_fun!("x", var("x"))
     }
 
-    #[cfg(test)]
     pub fn import<S>(path: S) -> RichTerm
     where
         S: Into<OsString>,
diff --git a/src/typecheck.rs b/src/typecheck.rs
index e1bb34d3..17d1933d 100644
--- a/src/typecheck.rs
+++ b/src/typecheck.rs
@@ -1884,326 +1884,3 @@ pub fn get_root(table: &UnifTable, x: usize) -> TypeWrapper {
         Some(k @ TypeWrapper::Constant(_)) => k.clone(),
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::cache::resolvers::{DummyResolver, SimpleResolver};
-    use crate::error::ImportError;
-    use crate::label::Label;
-    use crate::parser::lexer;
-    use crate::term::make as mk_term;
-    use crate::transformations::transform;
-    use assert_matches::assert_matches;
-    use codespan::Files;
-
-    use crate::parser;
-
-    fn type_check_no_import(rt: &RichTerm) -> Result<Types, TypecheckError> {
-        type_check_in_env(rt, &Environment::new(), &mut DummyResolver {})
-    }
-
-    fn parse_and_typecheck(s: &str) -> Result<Types, TypecheckError> {
-        let id = Files::new().add("<test>", s);
-
-        match parser::grammar::TermParser::new().parse(id, lexer::Lexer::new(&s)) {
-            Ok(p) => type_check_no_import(&p),
-            Err(e) => panic!("Couldn't parse {}: {:?}", s, e),
-        }
-    }
-
-    #[test]
-    fn unbound_variable_always_throws() {
-        type_check_no_import(&mk_term::var("x")).unwrap_err();
-    }
-
-    #[test]
-    fn promise_simple_checks() {
-        type_check_no_import(
-            &Term::Promise(
-                Types(AbsType::Num()),
-                Label::dummy(),
-                Term::Bool(true).into(),
-            )
-            .into(),
-        )
-        .unwrap_err();
-
-        type_check_no_import(
-            &Term::Promise(
-                Types(AbsType::Num()),
-                Label::dummy(),
-                Term::Num(34.5).into(),
-            )
-            .into(),
-        )
-        .unwrap();
-        type_check_no_import(
-            &Term::Promise(
-                Types(AbsType::Bool()),
-                Label::dummy(),
-                Term::Num(34.5).into(),
-            )
-            .into(),
-        )
-        .unwrap_err();
-
-        type_check_no_import(
-            &Term::Promise(
-                Types(AbsType::Num()),
-                Label::dummy(),
-                Term::Assume(
-                    Types(AbsType::Bool()),
-                    Label::dummy(),
-                    Term::Num(34.).into(),
-                )
-                .into(),
-            )
-            .into(),
-        )
-        .unwrap_err();
-
-        parse_and_typecheck("\"hello\" : Num").unwrap_err();
-    }
-
-    #[test]
-    fn promise_complicated() {
-        // Inside Promises we typecheck strictly
-        parse_and_typecheck("let f : Bool -> Num = fun x => if x then x + 1 else 34 in f false")
-            .unwrap_err();
-
-        // not annotated let bindings type to Dyn
-        parse_and_typecheck(
-            "let id = fun x => x in
-            (id 4 : Num)",
-        )
-        .unwrap_err();
-
-        // But they are not polymorphic
-        parse_and_typecheck("(fun id => (id 4 : Num) + (id true : Bool)) (fun x => x)")
-            .unwrap_err();
-
-        // Only if they're named the same way
-        parse_and_typecheck("(fun x => x) : #(fun l t => t) -> #(fun l t => t)").unwrap_err();
-    }
-
-    #[test]
-    fn simple_forall() {
-        parse_and_typecheck(
-            "let f : forall a. (forall b. a -> b -> a) = fun x y => y in
-            f",
-        )
-        .unwrap_err();
-
-        parse_and_typecheck(
-            "((fun f => let g : forall b. b -> b = fun y => y in f g)
-               : ((forall a. a -> a) -> Num) -> Num)
-            (fun x => 3)",
-        )
-        .unwrap_err();
-
-        parse_and_typecheck(
-            "let g : Num -> Num = fun x => x in
-            let f : forall a. a -> a = fun x => g x in
-            f",
-        )
-        .unwrap_err();
-    }
-
-    #[test]
-    fn enum_simple() {
-        parse_and_typecheck("`blo : <bla>").unwrap_err();
-        parse_and_typecheck("(switch { bla => 3, } `blo) : Num").unwrap_err();
-        parse_and_typecheck("(switch { bla => 3, ble => true, } `bla) : Num").unwrap_err();
-    }
-
-    #[test]
-    fn enum_complex() {
-        parse_and_typecheck(
-            "(fun x => switch {bla => 1, ble => 2, bli => 4,} x) : <bla, ble> -> Num",
-        )
-        .unwrap_err();
-
-        // TODO typecheck this, I'm not sure how to do it with row variables
-        parse_and_typecheck(
-            "(fun x =>
-                (switch {bla => 3, bli => 2,} x) +
-                (switch {bla => 6, blo => 20,} x) ) `bla : Num",
-        )
-        .unwrap_err();
-
-        parse_and_typecheck(
-            "let f : forall r. <blo, ble, | r> -> Num =
-                fun x => (switch {blo => 1, ble => 2, bli => 3, } x) in
-            f",
-        )
-        .unwrap_err();
-
-        parse_and_typecheck(
-            "let f : forall r. (forall p. <blo, ble | r> -> <bla, bli | p>) =
-                fun x => (switch {blo => `bla, ble => `bli, _ => `blo, } x) in
-            f `bli",
-        )
-        .unwrap_err();
-    }
-
-    #[test]
-    fn static_record_simple() {
-        parse_and_typecheck("{bla = true} : {bla : Num}").unwrap_err();
-        parse_and_typecheck("{blo = 1} : {bla : Num}").unwrap_err();
-
-        parse_and_typecheck("{blo = 1}.blo : Bool").unwrap_err();
-
-        parse_and_typecheck(
-            "let f : forall a. (forall r. {bla : Bool, blo : a, ble : a | r} -> a) =
-                fun r => if r.bla then r.blo else r.ble
-            in
-            (f {bla = true; blo = 1; ble = true; blip = `blip}) : Num",
-        )
-        .unwrap_err();
-        parse_and_typecheck(
-            "let f : forall a. (forall r. {bla : Bool, blo : a, ble : a | r} -> a) =
-                fun r => if r.bla then (r.blo + 1) else r.ble
-            in
-            (f {bla = true; blo = 1; ble = 2; blip = `blip; }) : Num",
-        )
-        .unwrap_err();
-    }
-
-    #[test]
-    fn dynamic_record_simple() {
-        parse_and_typecheck(
-            "({ \"#{if true then \"foo\" else \"bar\"}\" = 2; \"foo\" = true; }.\"bla\") : Num",
-        )
-        .unwrap_err();
-    }
-
-    #[test]
-    fn simple_list() {
-        parse_and_typecheck("[1, 2, false] : List Num").unwrap_err();
-        parse_and_typecheck("[(1 : String), true, \"b\"] : List").unwrap_err();
-        parse_and_typecheck("[1, 2, \"3\"] : List Str").unwrap_err();
-    }
-
-    #[test]
-    fn lists_operations() {
-        parse_and_typecheck("(fun l => %head% l) : forall a b. (List a -> b)").unwrap_err();
-        parse_and_typecheck(
-            "(fun f l => %elemAt% (%map% l f) 0) : forall a. (forall b. (a -> b) -> List -> b)",
-        )
-        .unwrap_err();
-    }
-
-    #[test]
-    fn imports() {
-        let mut resolver = SimpleResolver::new();
-        resolver.add_source(String::from("good"), String::from("1 + 1 : Num"));
-        resolver.add_source(String::from("bad"), String::from("false : Num"));
-        resolver.add_source(
-            String::from("proxy"),
-            String::from("let x = import \"bad\" in x"),
-        );
-
-        fn mk_import<R>(import: &str, resolver: &mut R) -> Result<RichTerm, ImportError>
-        where
-            R: ImportResolver,
-        {
-            transform(
-                mk_term::let_in("x", mk_term::import(import), mk_term::var("x")),
-                resolver,
-            )
-        };
-
-        type_check_in_env(
-            &mk_import("good", &mut resolver).unwrap(),
-            &Environment::new(),
-            &mut resolver,
-        )
-        .unwrap();
-        type_check_in_env(
-            &mk_import("proxy", &mut resolver).unwrap(),
-            &Environment::new(),
-            &mut resolver,
-        )
-        .unwrap_err();
-    }
-
-    #[test]
-    fn recursive_records() {
-        parse_and_typecheck("{a : Num = true; b = a + 1} : {a : Num, b : Num}").unwrap_err();
-        parse_and_typecheck("{a = 1; b : Bool = a} : {a : Num, b : Bool}").unwrap_err();
-    }
-
-    #[test]
-    fn let_inference() {
-        parse_and_typecheck("(let x = 1 + 2 in let f = fun x => x ++ \"a\" in f x) : Num")
-            .unwrap_err();
-
-        // Fields in recursive records are treated in the type environment in the same way as let-bound expressions
-        parse_and_typecheck(
-            "{ f = fun x => if x == 0 then false else 1 + (f (x + (-1)))} : {f : Num -> Num}",
-        )
-        .unwrap_err();
-    }
-
-    /// Regression test following [#144](https://github.com/tweag/nickel/issues/144). Check that
-    /// polymorphic type variables appearing inside a row type are correctly constrained at
-    /// instantiation.
-    #[test]
-    fn polymorphic_row_constraints() {
-        // Assert that the result of evaluation is either directly a `RowConflict` error, or a
-        // `RowConflict` wrapped in an `ArrowTypeMismatch`.
-        fn assert_row_conflict(res: Result<Types, TypecheckError>) {
-            assert!(match res.unwrap_err() {
-                TypecheckError::RowConflict(_, _, _, _, _) => true,
-                TypecheckError::ArrowTypeMismatch(_, _, _, err_boxed, _) => {
-                    if let TypecheckError::RowConflict(_, _, _, _, _) = err_boxed.as_ref() {
-                        true
-                    } else {
-                        false
-                    }
-                }
-                _ => true,
-            })
-        }
-
-        let mut res = parse_and_typecheck(
-            "let extend = Assume(forall c. { | c} -> {a: Str | c}, 0) in
-           (let bad = extend {a = 1;} in 0) : Num",
-        );
-        assert_row_conflict(res);
-
-        res = parse_and_typecheck(
-            "let remove = Assume(forall c. {a: Str | c} -> { | c}, 0) in
-           (let bad = remove (remove {a = \"a\"}) in 0) : Num",
-        );
-        assert_row_conflict(res);
-    }
-
-    #[test]
-    fn dynamic_row_tail() {
-        // Currently, typechecking is conservative wrt the dynamic row type, meaning it can't to a
-        // less precise type with a dynamic tail.
-        parse_and_typecheck("{a = 1; b = 2} : {a: Num | Dyn}").unwrap_err();
-        parse_and_typecheck("{a = 1} : {a: Num | Dyn}").unwrap_err();
-        parse_and_typecheck("Assume({a: Num | Dyn}, {a = 1}) : {a: Num}").unwrap_err();
-        parse_and_typecheck("{a = 1} : {a: Num | Dyn}").unwrap_err();
-    }
-
-    #[test]
-    fn shallow_type_inference() {
-        assert_matches!(
-            parse_and_typecheck("let x = (1 + 1) in (x + 1 : Num)"),
-            Err(TypecheckError::TypeMismatch(..))
-        );
-    }
-
-    /// Regression test following, see [#297](https://github.com/tweag/nickel/pull/297). Check that
-    /// [apparent_type](../fn.apparent_type.html) doesn't silently convert list literals from `List
-    /// T` (for `T` a type or a type variable) to `List Dyn`.
-    #[test]
-    fn apparent_type_lists() {
-        parse_and_typecheck("{foo = [1]} : {foo : List Num}").unwrap();
-        parse_and_typecheck("(let y = [] in y) : forall a. List a").unwrap();
-    }
-}
diff --git a/src/types.rs b/src/types.rs
index 41f3ca9c..fb0e2171 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -409,7 +409,8 @@ mod test {
         use crate::term::MetaValue;
 
         // Wrap the type in a contract to have it accepted by the parser.
-        let wrapper = format!("Contract({})", s);
+        let wrapper = format!("null | {}", s);
+        println!("{}", wrapper);
         let id = Files::new().add("<test>", wrapper.clone());
 
         let rt = TermParser::new().parse(id, Lexer::new(&wrapper)).unwrap();


Test patch:
diff --git a/tests/basics_fail.rs b/tests/basics_fail.rs
new file mode 100644
index 00000000..ad33889f
--- /dev/null
+++ b/tests/basics_fail.rs
@@ -0,0 +1,65 @@
+use assert_matches::assert_matches;
+use nickel::error::{Error, EvalError};
+
+mod common;
+use common::eval;
+
+#[test]
+fn div_by_zero() {
+    assert_matches!(
+        eval("1 + 1 / (1 - 1)"),
+        Err(Error::EvalError(EvalError::Other(..)))
+    );
+}
+
+#[test]
+fn comparisons() {
+    assert_matches!(
+        eval("1 < 2 < 3"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+    assert_matches!(
+        eval("1 < 2 > 3"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+    assert_matches!(
+        eval("\"a\" < 2"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+    assert_matches!(
+        eval("true <= []"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+    assert_matches!(
+        eval("\"a\" > \"b\""),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+    assert_matches!(
+        eval("\"a\" >= \"b\""),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+}
+
+#[test]
+fn boolean_ops() {
+    assert_matches!(
+        eval("let throw | #fail = null in false || true && throw"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+    assert_matches!(
+        eval("0 && true"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+    assert_matches!(
+        eval("\"a\" || false"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+}
+
+#[test]
+fn string_chunks() {
+    assert_matches!(
+        eval(r##""bad type #{1 + 1}""##),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+}
diff --git a/tests/common.rs b/tests/common.rs
new file mode 100644
index 00000000..dc64c18e
--- /dev/null
+++ b/tests/common.rs
@@ -0,0 +1,9 @@
+use nickel::error::Error;
+use nickel::program::Program;
+use nickel::term::Term;
+use std::io::Cursor;
+
+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()
+}
diff --git a/tests/contracts_fail.rs b/tests/contracts_fail.rs
new file mode 100644
index 00000000..dd3e4084
--- /dev/null
+++ b/tests/contracts_fail.rs
@@ -0,0 +1,182 @@
+use assert_matches::assert_matches;
+use codespan::Files;
+use nickel::error::{Error, EvalError, ToDiagnostic};
+
+mod common;
+use common::eval;
+
+macro_rules! assert_raise_blame {
+    ($term:expr) => {{
+        assert_matches!(
+            eval($term),
+            Err(Error::EvalError(EvalError::BlameError(..)))
+        )
+    }};
+}
+
+#[test]
+fn flat_contract_fail() {
+    assert_raise_blame!(
+        "let alwaysTrue = fun l t => let boolT | Bool = t in
+        if boolT then boolT else %blame% l in
+        (false | #alwaysTrue)"
+    );
+}
+
+#[test]
+fn id_fail() {
+    assert_raise_blame!(
+        "let id | forall a. a -> a = fun x => false in
+            id false && false"
+    );
+}
+
+#[test]
+fn enum_simple() {
+    assert_raise_blame!("`far | <foo, bar>");
+    assert_raise_blame!("123 | <foo, bar>");
+    assert_raise_blame!("`foo | < >");
+}
+
+#[test]
+fn metavalue_contract_default_fail() {
+    assert_raise_blame!("true | default | Num");
+}
+
+#[test]
+fn merge_contract() {
+    assert_raise_blame!("let r = {a=2} & {a | Bool} in r.a");
+}
+
+#[test]
+fn merge_default_contract() {
+    assert_raise_blame!("({a=2} & {b | Num} & {b | default = true}).b");
+}
+
+#[test]
+fn merge_compose_contract() {
+    let mk_composed = |arg| {
+        format!(
+            "let Even = fun l x => if x % 2 == 0 then x else %blame% l in
+        let DivBy3 = fun l x => if x % 3 ==  0 then x else %blame% l in
+        let composed = {{a | #Even}} & {{a | #DivBy3}} in
+        (({{a = {};}}) & composed).a",
+            arg
+        )
+    };
+
+    assert_raise_blame!(mk_composed("1"));
+    assert_raise_blame!(mk_composed("14"));
+    assert_raise_blame!(mk_composed("27"));
+    assert_raise_blame!(mk_composed("10"));
+    assert_raise_blame!(mk_composed("35"));
+}
+
+#[test]
+fn records_contracts_simple() {
+    assert_raise_blame!("{a=1} | {}");
+
+    assert_raise_blame!("let x | {a: Num, s: Str} = {a = 1; s = 2} in %deepSeq% x x");
+    assert_raise_blame!("let x | {a: Num, s: Str} = {a = \"a\"; s = \"b\"} in %deepSeq% x x");
+    assert_raise_blame!("let x | {a: Num, s: Str} = {a = 1} in %deepSeq% x x");
+    assert_raise_blame!("let x | {a: Num, s: Str} = {s = \"a\"} in %deepSeq% x x");
+    assert_raise_blame!(
+        "let x | {a: Num, s: Str} = {a = 1; s = \"a\"; extra = 1} in %deepSeq% x x"
+    );
+
+    assert_raise_blame!(
+        "let x | {a: Num, s: {foo: Bool}} = {a = 1; s = {foo = 2}} in %deepSeq% x x"
+    );
+    assert_raise_blame!(
+        "let x | {a: Num, s: {foo: Bool}} = {a = 1; s = {foo = true; extra = 1}} in %deepSeq% x x"
+    );
+    assert_raise_blame!("let x | {a: Num, s: {foo: Bool}} = {a = 1; s = {}} in %deepSeq% x x");
+}
+
+#[test]
+fn records_contracts_poly() {
+    // TODO: this test should ultimately pass (i.e., the program should be rejected)
+    // but this is not yet the case.
+    // eval_string(&format!("{} {{foo = 2}}", extd)).unwrap_err();
+
+    let remv = "let f | forall a. {foo: Num | a} -> { | a} = fun x => x-$\"foo\" in f";
+    assert_raise_blame!(&format!("{} {{}}", remv));
+
+    let bad_cst = "let f | forall a. { | a} -> { | a} = fun x => {a=1} in f";
+    let bad_acc = "let f | forall a. { | a} -> { | a} = fun x => %seq% (x.a) x in f";
+    let bad_extd = "let f | forall a. { | a} -> {foo: Num | a} = fun x => x-$\"foo\" in f";
+    let bad_rmv = "let f | forall a. {foo: Num | a} -> { | a} = fun x => x$[\"foo\"=1] in f";
+
+    assert_raise_blame!(&format!("{} {{}}", bad_cst));
+    // 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)),
+        Err(Error::EvalError(EvalError::FieldMissing(..)))
+    );
+    assert_matches!(
+        eval(&format!("{} {{foo=1}}", bad_extd)),
+        Err(Error::EvalError(EvalError::FieldMissing(..)))
+    );
+    assert_raise_blame!(&format!("{} {{}}", bad_rmv));
+    assert_raise_blame!(
+        "let f |
+            forall a.
+                (forall b. {a: Num, b: Num | b} -> { a: Num | b})
+                -> {a: Num | a}
+                -> { | a}
+            = fun f rec => (f rec) -$ \"a\" -$ \"b\" in
+        f (fun x => x) {a = 1; b = true; c = 3}"
+    );
+}
+
+#[test]
+fn lists_contracts() {
+    use nickel::label::ty_path::Elem;
+
+    assert_matches!(
+        eval("%deepSeq% ([1, \"a\"] | List Num) 0"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+    assert_matches!(
+        eval("1 | List"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+    assert_matches!(
+        eval("(fun x => x) | List"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+
+    let res = eval("%deepSeq% ([{a = [1]}] | List {a: List Str}) false");
+    match &res {
+        Err(Error::EvalError(EvalError::BlameError(ref l, _))) => {
+            assert_matches!(l.path.as_slice(), [Elem::List, Elem::Field(id), Elem::List] if &id.to_string() == "a")
+        }
+        err => panic!("expected blame error, got {:?}", err),
+    }
+    // Check that reporting doesn't panic. Provide a dummy file database, as we won't report
+    // the error message but just check that it can be built.
+    let mut files = Files::new();
+    res.unwrap_err().to_diagnostic(&mut files, None);
+
+    let res = eval(
+        "(%elemAt% (({foo = [(fun x => \"a\")]} | {foo: List (forall a. a -> Num)}).foo) 0) false",
+    );
+    match &res {
+        Err(Error::EvalError(EvalError::BlameError(ref l, _))) => {
+            assert_matches!(l.path.as_slice(), [Elem::Field(id), Elem::List, Elem::Codomain] if &id.to_string() == "foo")
+        }
+        err => panic!("expected blame error, got {:?}", err),
+    }
+    res.unwrap_err().to_diagnostic(&mut files, None);
+}
+
+// #[test]
+// fn enum_complex() {
+//     eval(
+//         "let f : <foo, bar> -> Num =
+//             fun x => switch { foo => 1, bar => 2, } x in
+//             f `boo",
+//     )
+//     .unwrap_err();
+// }
diff --git a/tests/infinite_rec.rs b/tests/infinite_rec.rs
new file mode 100644
index 00000000..0216de3d
--- /dev/null
+++ b/tests/infinite_rec.rs
@@ -0,0 +1,25 @@
+use assert_matches::assert_matches;
+use nickel::error::{Error, EvalError};
+
+mod common;
+use common::eval;
+
+#[test]
+fn infinite_loops() {
+    assert_matches!(
+        eval("{x = x}.x"),
+        Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
+    );
+    assert_matches!(
+        eval("{x = y; y = z; z = x }.x"),
+        Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
+    );
+    assert_matches!(
+        eval("{x = y + z; y = z + x; z = 1}.x"),
+        Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
+    );
+    assert_matches!(
+        eval("{x = (fun a => a + y) 0; y = (fun a => a + x) 0}.x"),
+        Err(Error::EvalError(EvalError::InfiniteRecursion(..)))
+    );
+}
diff --git a/tests/pass/complete.ncl b/tests/pass/complete.ncl
index 9b546ea9..97d30048 100644
--- a/tests/pass/complete.ncl
+++ b/tests/pass/complete.ncl
@@ -1,6 +1,6 @@
 let AssertEq = fun val l x => val == x || %blame% l in
 
-let Y = Assume(((Num -> Num) -> Num -> Num) -> Num -> Num, fun f => (fun x => f (x x)) (fun x => f (x x))) in
+let Y | ((Num -> Num) -> Num -> Num) -> Num -> Num = fun f => (fun x => f (x x)) (fun x => f (x x)) in
   let dec : Num -> Num = fun x => x + (-1) in
   let or : Bool -> Bool -> Bool = fun x => fun y => if x then x else y in
   let fibo : Num -> Num = Y (fun fibo =>
diff --git a/tests/pass/contracts.ncl b/tests/pass/contracts.ncl
index 6467bbbb..53980e8f 100644
--- a/tests/pass/contracts.ncl
+++ b/tests/pass/contracts.ncl
@@ -1,10 +1,10 @@
 let Assert = fun l x => x || %blame% l in
 
 (let AlwaysTrue = fun l t =>
-  let boolT = Assume(Bool, t) in
+  let boolT | Bool = t in
   if boolT then boolT else %blame% l in
 let AlwaysFalse = fun l t =>
-  let boolT = Assume(Bool, t) in
+  let boolT | Bool = t in
   if boolT then %blame% l else boolT
 in let not | #AlwaysTrue -> #AlwaysFalse = fun b => if b then false else true in
 not true == false) &&
diff --git a/tests/pass/metavalues.ncl b/tests/pass/metavalues.ncl
index faf130e7..39a5b4fe 100644
--- a/tests/pass/metavalues.ncl
+++ b/tests/pass/metavalues.ncl
@@ -25,7 +25,7 @@ let Assert = fun l x => x || %blame% l in
 
 ({a=2} & {a | Num} & {a | default = 3} == {a = 2} | #Assert) &&
 ({a=2} & {b | Num} & {b | default = 3} == {a = 2; b = 3} | #Assert) &&
-(({a=Default(1)} & {b=Contract(Num)} & {a=Default(1)}).a
+(({a | default = 1} & {b | Num} & {a | default = 1}).a
   == 1
   | #Assert) &&
 
diff --git a/tests/pass/typechecking.ncl b/tests/pass/typechecking.ncl
index 3087263b..24fa3f8f 100644
--- a/tests/pass/typechecking.ncl
+++ b/tests/pass/typechecking.ncl
@@ -130,8 +130,8 @@ let typecheck = [
     : {f : Num -> Num},
 
   // polymorphic_row_constraints
-  let extend = Assume(forall c. { | c} -> {a: Str | c}, 0) in
-    let remove = Assume(forall c. {a: Str | c} -> { | c}, 0) in
+  let extend | forall c. { | c} -> {a: Str | c} = 0 in
+    let remove | forall c. {a: Str | c} -> { | c} = 0 in
     (let good = remove (extend {}) in 0) : Num,
 
   let r | {a: Num | Dyn} = {a = 1; b = 2} in (r.a : Num),
@@ -156,6 +156,11 @@ let typecheck = [
   let x = "a#{"some str inside"}" in (x ++ "a" : Str),
   let x = false in (x || true : Bool),
   let x = false in let y = x in let z = y in (z : Bool),
+  // Regression test following, see [#297](https://github.com/tweag/nickel/pull/297). Check that
+  // [apparent_type](../fn.apparent_type.html) doesn't silently convert list literals from `List
+  // T` (for `T` a type or a type variable) to `List Dyn`.
+  {foo = [1]} : {foo : List Num},
+  (let y = [] in y) : forall a. List a,
 ] in
 
 
diff --git a/tests/records_fail.rs b/tests/records_fail.rs
new file mode 100644
index 00000000..fc608f1b
--- /dev/null
+++ b/tests/records_fail.rs
@@ -0,0 +1,32 @@
+use assert_matches::assert_matches;
+use nickel::error::{Error, EvalError};
+
+mod common;
+use common::eval;
+
+#[test]
+fn records_access() {
+    assert_matches!(
+        eval("{foo = true}.bar"),
+        Err(Error::EvalError(EvalError::FieldMissing(..)))
+    );
+    assert_matches!(
+        eval("({ \"#{(if false then \"foo\" else \"bar\")}\" = false; bar = true; }).foo"),
+        Err(Error::EvalError(EvalError::Other(msg, ..))) if msg.starts_with("$[ .. ]"));
+}
+
+#[test]
+fn non_mergeable() {
+    assert_matches!(
+        eval("({a=1} & {a=2}).a"),
+        Err(Error::EvalError(EvalError::MergeIncompatibleArgs(..)))
+    );
+    assert_matches!(
+        eval("({a | default = false;} & {a | default = true}).a"),
+        Err(Error::EvalError(EvalError::MergeIncompatibleArgs(..)))
+    );
+    assert_matches!(
+        eval("({a.b = {}} & {a.b.c = []} & {a.b.c = {}}).a.b.c"),
+        Err(Error::EvalError(EvalError::MergeIncompatibleArgs(..)))
+    );
+}
diff --git a/tests/stdlib_lists_fail.rs b/tests/stdlib_lists_fail.rs
new file mode 100644
index 00000000..b337c84b
--- /dev/null
+++ b/tests/stdlib_lists_fail.rs
@@ -0,0 +1,76 @@
+use assert_matches::assert_matches;
+use nickel::error::{Error, EvalError};
+
+mod common;
+use common::eval;
+
+#[test]
+fn elem_at() {
+    assert_matches!(
+        eval("%elemAt% [] 0"),
+        Err(Error::EvalError(EvalError::Other(..)))
+    );
+    assert_matches!(
+        eval("%elemAt% [1,2,3] (-1)"),
+        Err(Error::EvalError(EvalError::Other(..)))
+    );
+    assert_matches!(
+        eval("%elemAt% [true, false, true] 3"),
+        Err(Error::EvalError(EvalError::Other(..)))
+    );
+    assert_matches!(
+        eval("%elemAt% {} 0"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+    assert_matches!(
+        eval("%elemAt% [1, 2, 3] 0.5"),
+        Err(Error::EvalError(EvalError::Other(..)))
+    );
+
+    assert_matches!(
+        eval("lists.elemAt {} 0"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+    assert_matches!(
+        eval("lists.elemAt [] \"a\""),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+}
+
+#[test]
+fn head_tail() {
+    assert_matches!(
+        eval("%head% []"),
+        Err(Error::EvalError(EvalError::Other(..)))
+    );
+    assert_matches!(
+        eval("%tail% []"),
+        Err(Error::EvalError(EvalError::Other(..)))
+    );
+    assert_matches!(
+        eval("%head% {}"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+    assert_matches!(
+        eval("%tail% {}"),
+        Err(Error::EvalError(EvalError::TypeError(..)))
+    );
+
+    // TODO: add non-empty contract to the input of lists.head and lists.tail
+    // assert_matches!(
+    //     eval("lists.head []"),
+    //     Err(Error::EvalError(EvalError::BlameError(..)))
+    // );
+    // assert_matches!(
+    //     eval("lists.tail []"),
+    //     Err(Error::EvalError(EvalError::BlameError(..)))
+    // );
+    assert_matches!(
+        eval("lists.head false"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+    assert_matches!(
+        eval("lists.tail 2"),
+        Err(Error::EvalError(EvalError::BlameError(..)))
+    );
+}
diff --git a/tests/typecheck_fail.rs b/tests/typecheck_fail.rs
new file mode 100644
index 00000000..01146aba
--- /dev/null
+++ b/tests/typecheck_fail.rs
@@ -0,0 +1,253 @@
+use assert_matches::assert_matches;
+use codespan::Files;
+use nickel::cache::{
+    resolvers::{DummyResolver, SimpleResolver},
+    ImportResolver,
+};
+use nickel::error::{ImportError, TypecheckError};
+use nickel::parser::{grammar, lexer};
+use nickel::term::make as mk_term;
+use nickel::term::RichTerm;
+use nickel::transformations::transform;
+use nickel::typecheck::{type_check_in_env, Environment};
+use nickel::types::Types;
+
+fn type_check(rt: &RichTerm) -> Result<Types, TypecheckError> {
+    type_check_in_env(rt, &Environment::new(), &mut DummyResolver {})
+}
+
+fn type_check_expr(s: impl std::string::ToString) -> Result<Types, TypecheckError> {
+    let s = s.to_string();
+    let id = Files::new().add("<test>", s.clone());
+    type_check(
+        &grammar::TermParser::new()
+            .parse(id, lexer::Lexer::new(&s))
+            .unwrap(),
+    )
+}
+
+macro_rules! assert_typecheck_fails {
+    ($term:expr) => {{
+        assert_matches!(type_check_expr($term), Err(..))
+    }};
+}
+
+#[test]
+fn unbound_variable_always_throws() {
+    assert_matches!(
+        type_check_expr("x"),
+        Err(TypecheckError::UnboundIdentifier(..))
+    )
+}
+
+#[test]
+fn promise_simple_checks() {
+    assert_typecheck_fails!("true : Num");
+    assert_typecheck_fails!("34.5 : Bool");
+    assert_typecheck_fails!("(34 | Bool) : Num");
+    assert_typecheck_fails!("\"hello\" : Num");
+}
+
+#[test]
+fn promise_complicated() {
+    // Inside Promises we typecheck strictly
+    assert_typecheck_fails!("let f : Bool -> Num = fun x => if x then x + 1 else 34 in f false");
+    // not annotated, non trivial let bindings type to Dyn
+    assert_typecheck_fails!("let id = fun x => x in (id 4 : Num)");
+    // no implicit polymorphism
+    assert_typecheck_fails!("(fun id => (id 4 : Num) + (id true : Bool)) (fun x => x)");
+    // contract equality (to be fair, the current implementation is full of issues: to be reworked)
+    assert_typecheck_fails!("(fun x => x) : #(fun l t => t) -> #(fun l t => t)");
+}
+
+#[test]
+fn simple_forall() {
+    assert_typecheck_fails!(
+        "let f : forall a. (forall b. a -> b -> a) = fun x y => y in
+         f"
+    );
+    assert_typecheck_fails!(
+        "((fun f =>
+            let g : forall b. b -> b = fun y => y in
+            f g)
+            : ((forall a. a -> a) -> Num) -> Num)
+            (fun x => 3)"
+    );
+    assert_typecheck_fails!(
+        "let g : Num -> Num = fun x => x in
+         let f : forall a. a -> a = fun x => g x in
+         f"
+    );
+}
+
+#[test]
+fn enum_simple() {
+    assert_typecheck_fails!("`foo : <bar>");
+    assert_typecheck_fails!("switch { foo => 3} `bar : Num");
+    assert_typecheck_fails!("switch { foo => 3, bar => true} `bar : Num");
+}
+
+#[test]
+fn enum_complex() {
+    assert_typecheck_fails!("fun x => switch {bla => 1, ble => 2, bli => 4} x : <bla, ble> -> Num");
+    // TODO typecheck this, I'm not sure how to do it with row variables
+    // LATER NOTE: this requires row subtyping, not easy
+    assert_typecheck_fails!(
+        "(fun x =>
+            (switch {bla => 3, bli => 2} x) +
+            (switch {bla => 6, blo => 20} x)) `bla : Num"
+    );
+    assert_typecheck_fails!(
+        "let f : forall r. <blo, ble | r> -> Num =
+            fun x => (switch {blo => 1, ble => 2, bli => 3} x) in
+        f"
+    );
+    assert_typecheck_fails!(
+        "let f : forall r. (forall p. <blo, ble | r> -> <bla, bli | p>) =
+            fun x => (switch {blo => `bla, ble => `bli, _ => `blo} x) in
+        f `bli"
+    );
+}
+
+#[test]
+fn static_record_simple() {
+    assert_typecheck_fails!("{bla = true} : {bla : Num}");
+    assert_typecheck_fails!("{blo = 1} : {bla : Num}");
+
+    assert_typecheck_fails!("{blo = 1}.blo : Bool");
+
+    assert_typecheck_fails!(
+        "let f : forall a. (forall r. {bla : Bool, blo : a, ble : a | r} -> a) =
+            fun r => if r.bla then r.blo else r.ble in
+         (f {bla = true; blo = 1; ble = true; blip = `blip} : Num)"
+    );
+    assert_typecheck_fails!(
+        "let f : forall a. (forall r. {bla : Bool, blo : a, ble : a | r} -> a) =
+            fun r => if r.bla then (r.blo + 1) else r.ble in
+         (f {bla = true; blo = 1; ble = 2; blip = `blip} : Num)"
+    );
+}
+
+#[test]
+fn dynamic_record_simple() {
+    assert_typecheck_fails!(
+        "({ \"#{if true then \"foo\" else \"bar\"}\" = 2; \"foo\" = true; }.\"bla\") : Num"
+    );
+}
+
+#[test]
+fn simple_list() {
+    assert_typecheck_fails!("[1, 2, false] : List Num");
+    assert_typecheck_fails!("[(1 : String), true, \"b\"] : List");
+    assert_typecheck_fails!("[1, 2, \"3\"] : List Str");
+}
+
+#[test]
+fn lists_operations() {
+    assert_typecheck_fails!("(fun l => %head% l) : forall a b. (List a -> b)");
+    assert_typecheck_fails!(
+        "(fun f l => %elemAt% (%map% l f) 0) : forall a. (forall b. (a -> b) -> List -> b)"
+    );
+}
+
+#[test]
+fn imports() {
+    let mut resolver = SimpleResolver::new();
+    resolver.add_source(String::from("good"), String::from("1 + 1 : Num"));
+    resolver.add_source(String::from("bad"), String::from("false : Num"));
+    resolver.add_source(
+        String::from("proxy"),
+        String::from("let x = import \"bad\" in x"),
+    );
+
+    fn mk_import<R>(import: &str, resolver: &mut R) -> Result<RichTerm, ImportError>
+    where
+        R: ImportResolver,
+    {
+        transform(
+            mk_term::let_in("x", mk_term::import(import), mk_term::var("x")),
+            resolver,
+        )
+    };
+
+    type_check_in_env(
+        &mk_import("good", &mut resolver).unwrap(),
+        &Environment::new(),
+        &mut resolver,
+    )
+    .unwrap();
+    type_check_in_env(
+        &mk_import("proxy", &mut resolver).unwrap(),
+        &Environment::new(),
+        &mut resolver,
+    )
+    .unwrap_err();
+}
+
+#[test]
+fn recursive_records() {
+    assert_typecheck_fails!("{a : Num = true; b = a + 1} : {a : Num, b : Num}");
+    assert_typecheck_fails!("{a = 1; b : Bool = a} : {a : Num, b : Bool}");
+}
+
+#[test]
+fn let_inference() {
+    assert_typecheck_fails!("(let x = 1 + 2 in let f = fun x => x ++ \"a\" in f x) : Num");
+
+    // Fields in recursive records are treated in the type environment in the same way as let-bound expressions
+    assert_typecheck_fails!(
+        "{ f = fun x => if x == 0 then false else 1 + (f (x + (-1)))} : {f : Num -> Num}"
+    );
+}
+
+/// Regression test following [#144](https://github.com/tweag/nickel/issues/144). Check that
+/// polymorphic type variables appearing inside a row type are correctly constrained at
+/// instantiation.
+#[test]
+fn polymorphic_row_constraints() {
+    // Assert that the result of evaluation is either directly a `RowConflict` error, or a
+    // `RowConflict` wrapped in an `ArrowTypeMismatch`.
+    fn assert_row_conflict(res: Result<Types, TypecheckError>) {
+        assert!(match res.unwrap_err() {
+            TypecheckError::RowConflict(_, _, _, _, _) => true,
+            TypecheckError::ArrowTypeMismatch(_, _, _, err_boxed, _) => {
+                if let TypecheckError::RowConflict(_, _, _, _, _) = err_boxed.as_ref() {
+                    true
+                } else {
+                    false
+                }
+            }
+            _ => true,
+        })
+    }
+
+    let mut res = type_check_expr(
+        "let extend | forall c. { | c} -> {a: Str | c} = null in
+           (let bad = extend {a = 1;} in 0) : Num",
+    );
+    assert_row_conflict(res);
+
+    res = type_check_expr(
+        "let remove | forall c. {a: Str | c} -> { | c} = nul in
+           (let bad = remove (remove {a = \"a\"}) in 0) : Num",
+    );
+    assert_row_conflict(res);
+}
+
+#[test]
+fn dynamic_row_tail() {
+    // Currently, typechecking is conservative wrt the dynamic row type, meaning it can't
+    // convert to a less precise type with a dynamic tail.
+    assert_typecheck_fails!("{a = 1; b = 2} : {a: Num | Dyn}");
+    assert_typecheck_fails!("{a = 1} : {a: Num | Dyn}");
+    assert_typecheck_fails!("({a = 1} | {a: Num | Dyn}) : {a: Num}");
+    assert_typecheck_fails!("{a = 1} : {a: Num | Dyn}");
+}
+
+#[test]
+fn shallow_type_inference() {
+    assert_matches!(
+        type_check_expr("let x = (1 + 1) in (x + 1 : Num)"),
+        Err(TypecheckError::TypeMismatch(..))
+    );
+}