Repository: tweag/nickel
Pull request #765 (2022-07-06 15:15:05)

Benchmark array contracts
Add benchmarks in the context of #764 so we can evaluate results.


Patch:
diff --git a/.github/workflows/update-flake-lock.yml b/.github/workflows/update-flake-lock.yml
index 51aa7690..647d3610 100644
--- a/.github/workflows/update-flake-lock.yml
+++ b/.github/workflows/update-flake-lock.yml
@@ -16,4 +16,4 @@ jobs:
           extra_nix_config: |
             access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
       - name: Update flake.lock
-        uses: DeterminateSystems/update-flake-lock@v11
+        uses: DeterminateSystems/update-flake-lock@v10
diff --git a/benches/arrays.rs b/benches/arrays.rs
index 74c30ed3..06e36577 100644
--- a/benches/arrays.rs
+++ b/benches/arrays.rs
@@ -109,6 +109,15 @@ config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flam
         name = "sort normal",
         path = "arrays/sort",
         args = (20),
+    }, {
+        name = "sum normal 50",
+        path = "arrays/sum",
+        args = (50),
+    }, {
+        name = "sum deepseq 50",
+        path = "arrays/sum",
+        args = (50),
+        eval_mode = EvalMode::DeepSeq,
     }
 }
 criterion_main!(benches);
diff --git a/benches/arrays/primes.ncl b/benches/arrays/primes.ncl
new file mode 100644
index 00000000..dd932b77
--- /dev/null
+++ b/benches/arrays/primes.ncl
@@ -0,0 +1,37 @@
+let range
+  | doc "Generate an array of integers in the range [`start`, `end`)."
+  | Num -> Num -> Array Num
+  = fun start end =>
+    if end <= start then 
+      []
+    else
+      array.generate (fun x => x + start) (end - start) 
+in
+
+let is_prime 
+  | doc "Returns true if the argument is a prime number."
+  = fun x => x > 1 && array.all (fun d => x % d != 0) (range 2 (x - 1))
+in
+
+let Prime = contract.from_predicate is_prime in
+
+let primes 
+  | doc "Generate `max` primes using Sieve of Eratosthenes."
+  | Num -> Array Prime
+  = fun max =>
+    let limit = num.pow max (1 / 2) in # sqrt(max)
+    let drop_multiples = fun x xs => 
+      let to_drop = max
+        |> array.generate (fun y => (y + 2) * x)
+        |> array.filter (fun y => y <= max) in
+      array.filter (fun y => array.all ((!=) y) to_drop) xs in
+    let rec loop = fun x xs =>
+      if x > limit then
+        xs
+      else
+        loop (x + 1) (drop_multiples x xs) in
+    loop 2 (range 2 max) in
+
+{
+  run = primes
+}
diff --git a/benches/arrays/sum.ncl b/benches/arrays/sum.ncl
new file mode 100644
index 00000000..d5cc1ff2
--- /dev/null
+++ b/benches/arrays/sum.ncl
@@ -0,0 +1,8 @@
+let sum
+  | Array Num -> Num 
+  = fun xs =>
+    if array.length xs == 0 then 0
+    else array.head xs + sum (array.tail xs)
+in {
+  run = fun n => array.generate (fun x => x + 1) n |> sum
+}
diff --git a/doc/manual/contracts.md b/doc/manual/contracts.md
index 40a2069c..05bf10c7 100644
--- a/doc/manual/contracts.md
+++ b/doc/manual/contracts.md
@@ -1,6 +1,7 @@
 ---
 slug: contracts
 ---
+
 # Contracts in Nickel
 
 (For the motivation behind contracts and a high-level overview of contracts and
@@ -233,7 +234,7 @@ let MyConfig = {
   connection = {
     server_port =
       if host == "localhost" then
-        "8080"
+        8080
       else
         80,
     host = "localhost",
@@ -377,11 +378,11 @@ error: Non mergeable terms
 **Warning: `=` vs `|`**
 
 It may be tempting to use `=` instead of  `|` to attach a record contract to a
-field. That is, writing `Contract = {foo = {bar | Str}}` instead of
-`Contract = {foo | {bar | Str}}`. When applying this contract, the merging
-operator will apply the `Str` contract to the field `foo` of the checked value.
-At first sight, `=` also fits the bill. However, there are a number of subtle
-but potentially surprising differences.
+field. That is, writing `Contract = {foo = {bar | Str}}` instead of `Contract =
+{foo | {bar | Str}}`. When applying this contract, the merging operator will
+apply the `Str` contract to the field `foo` of the checked value. At first
+sight, `=` also fits the bill. However, there are a number of subtle but
+potentially surprising differences.
 
 One concerns open contracts. Merging never requires the presence of specific
 fields: thus, the contract `{bar | Str}` attached to `foo` will actually behave
@@ -566,11 +567,11 @@ contracts combinators are lazy too.
 
 Imagine we want to write a contract similar to `{_ : Bool}`, that is a
 dictionary of booleans, but we also want keys to be number literals (although
-represented as strings). A valid value could look like
-`{"1": true, "2": false, "10": true}`. If we used boolean predicates as the
-default for contracts, it would be impossible to make it lazy: as soon as your
-contract is called, you would need to produce a `true` or `false` answer, and
-checking that fields are all `Bool` would force their evaluation.
+represented as strings). A valid value could look like `{"1": true, "2": false,
+"10": true}`. If we used boolean predicates as the default for contracts, it
+would be impossible to make it lazy: as soon as your contract is called, you
+would need to produce a `true` or `false` answer, and checking that fields are
+all `Bool` would force their evaluation.
 
 What we can do is to not perform all the checks right away, but **return a new
 value, which is wrapping the original value with delayed checks inside**. This
diff --git a/doc/manual/correctness.md b/doc/manual/correctness.md
index faffd158..6c01cb52 100644
--- a/doc/manual/correctness.md
+++ b/doc/manual/correctness.md
@@ -279,7 +279,7 @@ If we write:
 ```nickel
 let level = 1 in
 {
-  opt_level : OptLevel = "0" ++ string.from_num level,
+  opt_level : OptLevel = "0" ++ srings.from_num level,
 }
 ```
 
@@ -287,10 +287,10 @@ We get:
 
 ```
 error: incompatible types
-  ┌─ repl-input-0:3:26
+  ┌─ repl-input-3:3:27
   │
-3 │   opt_level : OptLevel = "0" ++ string.from_num level,
-  │                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this expression
+3 │   opt_level : OptLevel = "0" ++ (if level == 1 then "1" else "2"),
+  │                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this expression
   │
   = The type of the expression was expected to be `OptLevel`
   = The type of the expression was inferred to be `Str`
diff --git a/doc/manual/types-vs-contracts.md b/doc/manual/types-vs-contracts.md
index 138b0726..68cd211e 100644
--- a/doc/manual/types-vs-contracts.md
+++ b/doc/manual/types-vs-contracts.md
@@ -1,5 +1,5 @@
 ---
-slug: types-vs-contracts
+slug: type-vs-contract
 ---
 
 # Type vs contract: when to?
@@ -83,7 +83,7 @@ let Schema = {
 ## Computation (compound expressions)
 
 Some expressions are neither immediate data nor functions. Take for example the
-function application `array.map (fun s => "http://%{s}/index") servers`.
+function application `array.map (fun s => "http://#{s}/index") servers`.
 Usually, you should do **nothing**.
 
 - *Inside configuration: nothing*. The function or operator you are using should
diff --git a/doc/manual/typing.md b/doc/manual/typing.md
index 5f334dce..017bb699 100644
--- a/doc/manual/typing.md
+++ b/doc/manual/typing.md
@@ -356,7 +356,7 @@ we can substitute a parameter for a whole sequence of field declarations, also
 referred to as rows:
 
 ```nickel
-(let addTotal: forall a b. {total: Num ; a} -> {total: Num ; b} -> Num
+(let addTotal: forall a b. {total: Num | a} -> {total: Num | b} -> Num
   = fun r1 r2 => r1.total + r2.total in
 let r1 = {jan = 200, feb = 300, march = 10, total = jan + feb} in
 let r2 = {aug = 50, sept = 20, total = aug + sept} in
@@ -372,18 +372,18 @@ Result:
 {partial1 = 570, partial2 = 1770}
 ```
 
-In the type of `addTotal`, the part `{total: Num ; a}` expresses exactly what we
+In the type of `addTotal`, the part `{total: Num | a}` expresses exactly what we
 wanted: the argument must have a field `total: Num`, but the *tail* (the rest of
 the record type) is polymorphic, and `a` may be substituted for arbitrary fields
 (such as `jan: Num, feb: Num`). We used two different generic parameters `a` and
 `b`, to express that the tails of the arguments may differ.  If we used `a` in
-both places, as in `forall a. {total: Num ; a} -> {total: Num ; a} -> Num`, we
+both places, as in `forall a. {total: Num | a} -> {total: Num | a} -> Num`, we
 could still write `addTotal {total = 1, foo = 1} {total = 2, foo = 2}` but not
 `addTotal {total = 1, foo = 1} {total = 2, bar = 2}`. Using distinct parameters
 `a` and `b` gives us maximum flexibility.
 
 What comes before the tail may include several fields, is in e.g. `forall a.
-{total: Num, subtotal: Num ; a} -> Num`.
+{total: Num, subtotal: Num | a} -> Num`.
 
 <!-- Note that row polymorphism also works with enums, with the same intuition of a -->
 <!-- tail that can be substituted for something else. For example: -->
@@ -595,12 +595,6 @@ typechecker on, a contract annotation switches it back off.
 
 ## Using contracts as types
 
-**Warning**: as of version 0.2.x, using contracts as types won't work in the
-majority of cases (typechecking will fail with a "can't compare contract types"
-error). This is temporary and will likely be fixed in the upcoming 0.3.x release. For more
-details, see issues [#701](https://github.com/tweag/nickel/issues/701) and
-[#724](https://github.com/tweag/nickel/issues/724).
-
 <!-- TODO: find a good name for this section. Will need rework after merging
 types and contracts syntax -->
 
diff --git a/flake.lock b/flake.lock
index a3e05ff5..a622b8f8 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
   "nodes": {
     "flake-utils": {
       "locked": {
-        "lastModified": 1656065134,
-        "narHash": "sha256-oc6E6ByIw3oJaIyc67maaFcnjYOz1mMcOtHxbEf9NwQ=",
+        "lastModified": 1652776076,
+        "narHash": "sha256-gzTw/v1vj4dOVbpBSJX4J0DwUR6LIyXo7/SuuTJp1kM=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "bee6a7250dd1b01844a2de7e02e4df7d8a0a206c",
+        "rev": "04c1b180862888302ddfb2e3ad9eaa63afc60cf8",
         "type": "github"
       },
       "original": {
@@ -32,11 +32,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1655983783,
-        "narHash": "sha256-0h1FzkYWei24IdKNpCX93onkF/FMiXQG8SdEbTc0r8A=",
+        "lastModified": 1652885393,
+        "narHash": "sha256-YIgvvlk4iQ1Hi7KD9o5gsojc+ApB+jiH1d5stK8uXiw=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "6141b8932a5cf376fe18fcd368cecd9ad946cb68",
+        "rev": "48037fd90426e44e4bf03e6479e88a11453b9b66",
         "type": "github"
       },
       "original": {
@@ -55,11 +55,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1656169028,
-        "narHash": "sha256-y9DRauokIeVHM7d29lwT8A+0YoGUBXV3H0VErxQeA8s=",
+        "lastModified": 1652714503,
+        "narHash": "sha256-qQKVEfDe5FqvGgkZtg5Pc491foeiDPIOeycHMqnPDps=",
         "owner": "cachix",
         "repo": "pre-commit-hooks.nix",
-        "rev": "db3bd555d3a3ceab208bed48f983ccaa6a71a25e",
+        "rev": "521a524771a8e93caddaa0ac1d67d03766a8b0b3",
         "type": "github"
       },
       "original": {
@@ -87,11 +87,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1656125323,
-        "narHash": "sha256-Ckez1NOVyPLOEI2zbSok6oDCm/xJxX/pietAWukEwoo=",
+        "lastModified": 1653118478,
+        "narHash": "sha256-K9fG0VT/Wi/QJpXJd6d1QrWL5ulAhy1S67n5OGBwbcE=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "59a3a304c987dc65d2a33de0a77a1ae6ae777cd4",
+        "rev": "3bc2619665745f5e6f2efc3d0664edad4f62201b",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index f7442169..db4bcfd6 100644
--- a/flake.nix
+++ b/flake.nix
@@ -286,21 +286,7 @@
         src = ./doc/manual;
         installPhase = ''
           mkdir -p $out
-          cp -r ./ $out
-        '';
-      };
-
-      stdlibDoc = pkgs.stdenv.mkDerivation {
-        name = "nickel-stdlib-doc-${version}";
-        src = ./stdlib;
-        installPhase = ''
-          mkdir -p $out
-          for file in *
-          do
-            module=$(basename $file .ncl)
-            ${self.defaultPackage."${system}"}/bin/nickel doc -f "$module.ncl" \
-              --output "$out/$module.md"
-          done
+          cp -r ./ "$out"
         '';
       };
 
@@ -313,7 +299,6 @@
         dockerImage = buildDocker packages.build; # TODO: docker image should be a passthru
         inherit vscodeExtension;
         inherit userManual;
-        inherit stdlibDoc;
       };
 
       devShell = devShells.stable;
diff --git a/src/bin/nickel.rs b/src/bin/nickel.rs
index 5ce4c1c3..c6d7fec4 100644
--- a/src/bin/nickel.rs
+++ b/src/bin/nickel.rs
@@ -6,15 +6,11 @@ use nickel_lang::repl::query_print;
 use nickel_lang::repl::rustyline_frontend;
 use nickel_lang::term::{RichTerm, Term};
 use nickel_lang::{serialize, serialize::ExportFormat};
-use std::path::{Path, PathBuf};
-use std::{
-    fs::{self, File},
-    process,
-};
+use std::path::PathBuf;
+use std::{fs, process};
 // use std::ffi::OsStr;
 use directories::BaseDirs;
 use structopt::StructOpt;
-
 /// Command-line options and subcommands.
 #[derive(StructOpt, Debug)]
 /// The interpreter of the Nickel language.
@@ -75,13 +71,7 @@ enum Command {
     },
     /// Generates the documentation files for the specified nickel file
     #[cfg(feature = "doc")]
-    Doc {
-        /// Specify the path of the generated documentation file. Default to
-        /// `~/.nickel/doc/<input-file>.md` for input `<input-file>.ncl`, or to
-        /// `~/.nickel/doc/out.md` if the input is read from stdin.
-        #[structopt(short = "o", long, parse(from_os_str))]
-        output: Option<PathBuf>,
-    },
+    Doc {},
 }
 
 fn main() {
@@ -106,7 +96,6 @@ fn main() {
     } else {
         let mut program = opts
             .file
-            .clone()
             .map(Program::new_from_file)
             .unwrap_or_else(Program::new_from_stdin)
             .unwrap_or_else(|err| {
@@ -120,7 +109,7 @@ fn main() {
         }
 
         let result = match opts.command {
-            Some(Command::PprintAst { transform }) => program.pprint_ast(
+            Some(Command::PprintAst { transform }) => program.expand(
                 &mut std::io::BufWriter::new(Box::new(std::io::stdout())),
                 transform,
             ),
@@ -153,51 +142,7 @@ fn main() {
             Some(Command::Typecheck) => program.typecheck(),
             Some(Command::Repl { .. }) => unreachable!(),
             #[cfg(feature = "doc")]
-            Some(Command::Doc { ref output }) => output
-                .as_ref()
-                .map(|output| {
-                    fs::File::create(output.clone()).map_err(|e| {
-                        Error::IOError(IOError(format!(
-                            "when opening or creating output file `{}`: {}",
-                            output.to_string_lossy(),
-                            e
-                        )))
-                    })
-                })
-                .unwrap_or_else(|| {
-                    let docpath = Path::new(".nickel/doc/");
-                    fs::create_dir_all(docpath).map_err(|e| {
-                        Error::IOError(IOError(format!(
-                            "when creating output path `{}`: {}",
-                            docpath.to_string_lossy(),
-                            e
-                        )))
-                    })?;
-                    let mut markdown_file = docpath.to_path_buf();
-
-                    let mut has_file_name = false;
-
-                    if let Some(path) = opts.file {
-                        if let Some(file_stem) = path.file_stem() {
-                            markdown_file.push(file_stem);
-                            has_file_name = true;
-                        }
-                    }
-
-                    if !has_file_name {
-                        markdown_file.push("out");
-                    }
-
-                    markdown_file.set_extension("md");
-                    File::create(markdown_file.clone().into_os_string()).map_err(|e| {
-                        Error::IOError(IOError(format!(
-                            "when opening or creating output file `{}`: {}",
-                            markdown_file.to_string_lossy(),
-                            e
-                        )))
-                    })
-                })
-                .and_then(|mut out| program.output_doc(&mut out)),
+            Some(Command::Doc { .. }) => program.output_doc(),
             None => program
                 .eval_full()
                 .map(|t| println!("{}", Term::from(t).deep_repr())),
diff --git a/src/destruct.rs b/src/destruct.rs
index eebe808b..b99e083f 100644
--- a/src/destruct.rs
+++ b/src/destruct.rs
@@ -25,7 +25,7 @@ pub enum Match {
 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(Box<Match>),
+    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>),
@@ -50,12 +50,12 @@ pub enum Destruct {
 
 impl Destruct {
     /// generate the metavalue containing the contract representing this pattern.
-    pub fn into_contract(self) -> MetaValue {
+    pub fn as_contract(self) -> MetaValue {
         let label = self.label();
-        self.into_contract_with_lbl(label)
+        self.as_contract_with_lbl(label)
     }
 
-    fn into_contract_with_lbl(self, label: Label) -> MetaValue {
+    fn as_contract_with_lbl(self, label: Label) -> MetaValue {
         let open = self.is_open();
         MetaValue {
             contracts: vec![Contract {
@@ -125,7 +125,7 @@ impl Match {
                 let label = Label { span, ..label };
                 (
                     id,
-                    Term::MetaValue(MetaValue::flatten(m, d.into_contract_with_lbl(label))).into(),
+                    Term::MetaValue(MetaValue::flatten(m, d.as_contract_with_lbl(label))).into(),
                 )
             }
             Match::Assign(_id, _m, (_, _d @ Destruct::Array { .. })) => unimplemented!(),
diff --git a/src/error.rs b/src/error.rs
index 23f6616c..642ec386 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -197,16 +197,6 @@ pub enum TypecheckError {
         /* the error on the subtype unification */ Box<TypecheckError>,
         TermPos,
     ),
-    /// This is a temporary error due to the current limitation of checking type equality between
-    /// contracts. Checking if the unification of two contracts is valid is non-trivial even in
-    /// simple cases (see [#724](https://github.com/tweag/nickel/issues/724)). Currently, the
-    /// typechecker simply bails out of any attempt of contract unification, represented by this
-    /// error.
-    IncomparableFlatTypes(
-        RichTerm, /* the expected flat type */
-        RichTerm, /* the inferred flat type */
-        TermPos,
-    ),
 }
 
 #[derive(Debug, PartialEq, Clone, Default)]
@@ -235,8 +225,8 @@ impl ParseErrors {
         ParseErrors { errors: Vec::new() }
     }
 
-    pub fn from_recoverable(
-        errs: Vec<ErrorRecovery<usize, Token<'_>, parser::error::ParseError>>,
+    pub fn from_recoverable<'a>(
+        errs: Vec<ErrorRecovery<usize, Token<'a>, parser::error::ParseError>>,
         file_id: FileId,
     ) -> Self {
         ParseErrors {
@@ -268,7 +258,8 @@ impl ToDiagnostic<FileId> for ParseErrors {
     ) -> Vec<Diagnostic<FileId>> {
         self.errors
             .iter()
-            .flat_map(|e| e.to_diagnostic(files, contract_id))
+            .map(|e| e.to_diagnostic(files, contract_id))
+            .flatten()
             .collect()
     }
 }
@@ -808,7 +799,8 @@ impl ToDiagnostic<FileId> for Error {
             Error::ParseErrors(errs) => errs
                 .errors
                 .iter()
-                .flat_map(|e| e.to_diagnostic(files, contract_id))
+                .map(|e| e.to_diagnostic(files, contract_id))
+                .flatten()
                 .collect(),
             Error::TypecheckError(err) => err.to_diagnostic(files, contract_id),
             Error::EvalError(err) => err.to_diagnostic(files, contract_id),
@@ -917,7 +909,7 @@ impl ToDiagnostic<FileId> for EvalError {
                     .with_labels(labels)
                     .with_notes(notes)];
 
-                diagnostics.push(blame_label_note(l));
+                diagnostics.push(blame_label_note(&l));
 
                 if ty_path::is_only_codom(&l.path) {
                 } else if let Some(id) = contract_id {
@@ -1001,7 +993,7 @@ impl ToDiagnostic<FileId> for EvalError {
                 let mut diags = vec![Diagnostic::error()
                     .with_message(format!(
                         "missing definition for `{}`",
-                        field.unwrap_or_else(|| String::from("?"))
+                        field.unwrap_or(String::from("?"))
                     ))
                     .with_labels(labels)
                     .with_notes(vec![])];
@@ -1224,7 +1216,7 @@ impl ToDiagnostic<FileId> for ParseError {
                 ))
                 .with_labels(vec![primary(span)]),
             ParseError::InvalidUniRecord(illegal_span, tail_span, span) => Diagnostic::error()
-                .with_message("invalid record literal")
+                .with_message(format!("invalid record literal"))
                 .with_labels(vec![
                     primary(span),
                     secondary(illegal_span).with_message("can't use this record construct"),
@@ -1235,7 +1227,7 @@ impl ToDiagnostic<FileId> for ParseError {
                     String::from("Value assignements, such as `<field> = <expr>`, metadata, etc. are forbidden."),
                 ]),
             ParseError::RecursiveLetPattern(span) => Diagnostic::error()
-                .with_message("recursive destructuring is not supported")
+                .with_message(format!("recursive destructuring is not supported"))
                 .with_labels(vec![
                     primary(span),
                 ])
@@ -1457,23 +1449,6 @@ impl ToDiagnostic<FileId> for TypecheckError {
 
                 diags
             }
-            TypecheckError::IncomparableFlatTypes(expd, actual, span_opt) => {
-                vec![Diagnostic::error()
-                    .with_message("can't compare contract types")
-                    .with_labels(mk_expr_label(span_opt))
-                    .with_notes(vec![
-                        format!("The type of the expression was expected to be `{}`", expd.as_ref().shallow_repr()),
-                        format!("The type of the expression was inferred to be `{}`", actual.as_ref().shallow_repr()),
-                        String::from("Nickel can't compare contracts during typechecking"),
-                    ]),
-                    Diagnostic::note()
-                    .with_notes(vec![
-                        String::from("Due to a temporary limitation, contracts don't mix well with static types (see https://github.com/tweag/nickel/issues/724). This error may happen when using a contract as a type annotation or when calling to a function whose type contain contracts."),
-                        String::from("As a temporary fix, please annotate the offending expression with its expected type using the pipe operator `|`. This disables static typing for the given expression."),
-                        String::from("For example: if `foo` has type `MyContract -> Num`, rewrite `foo value + 1` as `(foo value | Num) + 1`."),
-                    ])
-                ]
-            }
         }
     }
 }
@@ -1499,7 +1474,8 @@ impl ToDiagnostic<FileId> for ImportError {
                 let mut diagnostic: Vec<Diagnostic<FileId>> = error
                     .errors
                     .iter()
-                    .flat_map(|e| e.to_diagnostic(files, contract_id))
+                    .map(|e| e.to_diagnostic(files, contract_id))
+                    .flatten()
                     .collect();
 
                 if let Some(span) = span_opt.as_opt_ref() {
diff --git a/src/eval/fixpoint.rs b/src/eval/fixpoint.rs
index c4ac8244..1cb85fc7 100644
--- a/src/eval/fixpoint.rs
+++ b/src/eval/fixpoint.rs
@@ -40,7 +40,7 @@ pub fn rec_env<'a, I: Iterator<Item = (&'a Ident, &'a RichTerm)>>(
 /// all the recursive environment. See [`crate::transform::free_vars`].
 pub fn patch_field(
     rt: &RichTerm,
-    rec_env: &[(Ident, Thunk)],
+    rec_env: &Vec<(Ident, Thunk)>,
     env: &Environment,
 ) -> Result<(), EvalError> {
     if let Term::Var(var_id) = &*rt.term {
@@ -55,7 +55,7 @@ pub fn patch_field(
                 .borrow_mut()
                 .env
                 .extend(rec_env.iter().filter(|(id, _)| deps.contains(id)).cloned()),
-            ThunkDeps::Unknown => thunk.borrow_mut().env.extend(rec_env.iter().cloned()),
+            ThunkDeps::Unknown => thunk.borrow_mut().env.extend(rec_env.clone()),
             ThunkDeps::Empty => (),
         };
     }
diff --git a/src/eval/lazy.rs b/src/eval/lazy.rs
index 1eb900f5..942a30c8 100644
--- a/src/eval/lazy.rs
+++ b/src/eval/lazy.rs
@@ -107,27 +107,22 @@ impl ThunkData {
         self.state = ThunkState::Evaluated;
     }
 
-    /// Create a freshly unevaluated thunk (minus `ident_type`) from a thunk, reverted to its
-    /// original state before the first update.
-    ///
-    /// For standard thunk data, the content is unchanged and shared with the original thunk: in
-    /// this case, `revert()` is the same as cloning the original thunk.
-    ///
-    /// For revertible thunk data, the result is independent from the original one: any update to
-    /// one of the thunks doesn't affect the other.
-    pub fn revert(thunk: &Rc<RefCell<ThunkData>>) -> Rc<RefCell<ThunkData>> {
-        match thunk.borrow().inner {
-            InnerThunkData::Standard(_) => Rc::clone(thunk),
+    /// Create fresh unevaluated thunk data from `self`, reverted to its original state before the
+    /// first update. For standard thunk data, the content is unchanged and the state is conserved:
+    /// in this case, `revert()` is the same as `clone()`.
+    pub fn revert(&self) -> Self {
+        match self.inner {
+            InnerThunkData::Standard(_) => self.clone(),
             InnerThunkData::Revertible {
                 ref orig, ref deps, ..
-            } => Rc::new(RefCell::new(ThunkData {
+            } => ThunkData {
                 inner: InnerThunkData::Revertible {
                     orig: Rc::clone(orig),
                     cached: Rc::clone(orig),
                     deps: deps.clone(),
                 },
                 state: ThunkState::Suspended,
-            })),
+            },
         }
     }
 
@@ -238,7 +233,7 @@ impl Thunk {
     /// this case, `revert()` is the same as `clone()`.
     pub fn revert(&self) -> Self {
         Thunk {
-            data: ThunkData::revert(&self.data),
+            data: Rc::new(RefCell::new(self.data.borrow().revert())),
             ident_kind: self.ident_kind,
         }
     }
@@ -255,7 +250,7 @@ impl Thunk {
     /// Return a clone of the potential field dependencies stored in a revertible thunk. See
     /// [`crate::transform::free_vars`].
     pub fn deps(&self) -> ThunkDeps {
-        self.data.borrow().deps()
+        self.data.borrow().deps().clone()
     }
 }
 
diff --git a/src/eval/merge.rs b/src/eval/merge.rs
index ab22f079..5c511fde 100644
--- a/src/eval/merge.rs
+++ b/src/eval/merge.rs
@@ -503,7 +503,7 @@ fn rev_thunks<'a, I: Iterator<Item = &'a mut RichTerm>>(map: I, env: &mut Enviro
     for rt in map {
         if let Term::Var(id) = rt.as_ref() {
             // This create a fresh variable which is bound to a reverted copy of the original thunk
-            let reverted = env.get(id).unwrap().revert();
+            let reverted = env.get(&id).unwrap().revert();
             let fresh_id = fresh_var();
             env.insert(fresh_id.clone(), reverted);
             *(SharedTerm::make_mut(&mut rt.term)) = Term::Var(fresh_id);
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index fbbc49b3..a9a0f8f7 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -253,14 +253,17 @@ where
 {
     let (mut rt, env) = eval_closure(Closure::atomic_closure(t), global_env, resolver, false)?;
 
-    if let Term::MetaValue(ref mut meta) = *SharedTerm::make_mut(&mut rt.term) {
-        if let Some(t) = meta.value.take() {
-            let (evaluated, env) =
-                eval_closure(Closure { body: t, env }, global_env, resolver, true)?;
-            let substituted = subst(evaluated, global_env, &env);
-
-            meta.value = Some(substituted);
+    match *SharedTerm::make_mut(&mut rt.term) {
+        Term::MetaValue(ref mut meta) => {
+            if let Some(t) = meta.value.take() {
+                let (evaluated, env) =
+                    eval_closure(Closure { body: t, env }, global_env, resolver, true)?;
+                let substituted = subst(evaluated, global_env, &env);
+
+                meta.value = Some(substituted);
+            }
         }
+        _ => (),
     };
 
     Ok(rt)
@@ -525,7 +528,7 @@ where
 
                 //TODO: We should probably avoid cloning the `ts` hashmap, using `match_sharedterm`
                 //instead of `match` in the main eval loop, if possible
-                let static_part = RichTerm::new(Term::Record(ts.clone(), *attrs), pos);
+                let static_part = RichTerm::new(Term::Record(ts.clone(), attrs.clone()), pos);
 
                 // Transform the static part `{stat1 = val1, ..., statn = valn}` and the dynamic
                 // part `{exp1 = dyn_val1, ..., expm = dyn_valm}` to a sequence of extensions
@@ -533,7 +536,7 @@ where
                 // The `dyn_val` are given access to the recursive environment, but the recursive
                 // environment only contains the static fields, and not the dynamic fields.
                 let extended = dyn_fields
-                    .iter()
+                    .into_iter()
                     .try_fold::<_, _, Result<RichTerm, EvalError>>(
                         static_part,
                         |acc, (id_t, t)| {
@@ -616,7 +619,7 @@ where
             Term::Array(terms, attrs) if !attrs.closurized => {
                 let mut local_env = Environment::new();
                 let closurized_array = terms
-                    .iter()
+                    .into_iter()
                     .map(|t| t.clone().closurize(&mut local_env, env.clone()))
                     .collect();
                 Closure {
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index 783dc983..c3dfe168 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -764,7 +764,7 @@ fn process_unary_operation(
                     let indent_str: String = std::iter::once('\n')
                         .chain((0..indent).map(|_| ' '))
                         .collect();
-                    s.replace('\n', &indent_str)
+                    s.replace("\n", &indent_str)
                 } else {
                     s.clone()
                 };
@@ -932,21 +932,19 @@ fn process_unary_operation(
             }
         }
         UnaryOp::ToStr() => {
-            let result = match_sharedterm! {t, with {
+            let result = match &*t {
                 Term::Num(n) => Ok(Term::Str(n.to_string())),
-                Term::Str(s) => Ok(Term::Str(s)),
+                Term::Str(s) => Ok(Term::Str(s.clone())),
                 Term::Bool(b) => Ok(Term::Str(b.to_string())),
                 Term::Enum(id) => Ok(Term::Str(id.to_string())),
-                Term::Null => Ok(Term::Str(String::from("null"))),
-            } else {
-                Err(EvalError::Other(
+                t => Err(EvalError::Other(
                     format!(
                         "strFrom: can't convert the argument of type {} to string",
                         t.type_of().unwrap()
                     ),
                     pos,
-                ))
-            }}?;
+                )),
+            }?;
             Ok(Closure::atomic_closure(RichTerm::new(result, pos_op_inh)))
         }
         UnaryOp::NumFromStr() => {
@@ -1059,9 +1057,10 @@ fn process_unary_operation(
                     let groups: Vec<RichTerm> = capt
                         .iter()
                         .skip(1)
-                        .filter_map(|s_opt| {
+                        .map(|s_opt| {
                             s_opt.map(|s| RichTerm::from(Term::Str(String::from(s.as_str()))))
                         })
+                        .flatten()
                         .collect();
 
                     mk_record!(
@@ -1855,7 +1854,7 @@ fn process_binary_operation(
                             ts.extend(ts1.into_iter());
                             ts.extend(ts2.into_iter());
 
-                            let mut env = env1;
+                            let mut env = env1.clone();
                             // TODO: Is there a cheaper way to "merge" two environements?
                             env.extend(env2.iter_elems().map(|(k, v)| (k.clone(), v.clone())));
 
@@ -2327,7 +2326,7 @@ fn process_nary_operation(
                         )
                     }
                 } else {
-                    Err(EvalError::InternalError(format!("The MergeContract() operator was expecting a first argument of type Label, got {}", t1.type_of().unwrap_or_else(|| String::from("<unevaluated>"))), pos_op))
+                    Err(EvalError::InternalError(format!("The MergeContract() operator was expecting a first argument of type Label, got {}", t1.type_of().unwrap_or(String::from("<unevaluated>"))), pos_op))
                 }
             }
         }
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index f3c0c55c..da72053f 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -369,7 +369,7 @@ FieldPathElem: FieldPathElem = {
 
 // Last field of a pattern
 LastMatch: LastMatch = {
-    Match => LastMatch::Match(Box::new(<>)),
+    Match => LastMatch::Match(<>),
     ".." <Ident?> => LastMatch::Ellipsis(<>),
 };
 
@@ -385,7 +385,7 @@ Destruct: Destruct = {
     <start: @L> "{" <mut matches: (<Match> ",")*> <last:LastMatch?> "}" <end: @R> => {
         let (open, rest) = match last {
 	    Some(LastMatch::Match(m)) => {
-	        matches.push(*m);
+	        matches.push(m);
 	        (false,None)
 	    },
 	    Some(LastMatch::Ellipsis(rest)) => (true, rest),
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index d9617000..7483a48e 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -7,7 +7,6 @@ use lalrpop_util::lalrpop_mod;
 lalrpop_mod!(
     #[allow(clippy::all)]
     #[allow(unused_parens)]
-    #[allow(unused_imports)]
     pub grammar);
 
 pub mod error;
diff --git a/src/parser/uniterm.rs b/src/parser/uniterm.rs
index e0d1bd9c..7e5c4f0a 100644
--- a/src/parser/uniterm.rs
+++ b/src/parser/uniterm.rs
@@ -164,7 +164,7 @@ impl UniRecord {
     pub fn into_type_strict(self) -> Result<Types, InvalidRecordTypeError> {
         // An open record (with an ellipsis `..` at the end) can't be translated to a record type.
         // `pos_ellipsis` should be set iff `attrs.open` is true.
-        debug_assert!((self.pos_ellipsis == TermPos::None) != self.attrs.open);
+        debug_assert!((self.pos_ellipsis == TermPos::None) == !self.attrs.open);
 
         if let Some(raw_span) = self.pos_ellipsis.into_opt() {
             return Err(InvalidRecordTypeError(TermPos::Original(raw_span)));
@@ -397,13 +397,16 @@ pub fn fix_type_vars(ty: &mut Types) {
 /// Fix the type variables of types appearing as annotations of record fields. See
 /// [`fix_type_vars`].
 pub fn fix_field_types(rt: &mut RichTerm) {
-    if let Term::MetaValue(ref mut m) = SharedTerm::make_mut(&mut rt.term) {
-        if let Some(Contract { ref mut types, .. }) = m.types {
-            fix_type_vars(types);
-        }
+    match SharedTerm::make_mut(&mut rt.term) {
+        Term::MetaValue(ref mut m) => {
+            if let Some(Contract { ref mut types, .. }) = m.types {
+                fix_type_vars(types);
+            }
 
-        for ctr in m.contracts.iter_mut() {
-            fix_type_vars(&mut ctr.types);
+            for ctr in m.contracts.iter_mut() {
+                fix_type_vars(&mut ctr.types);
+            }
         }
+        _ => (),
     }
 }
diff --git a/src/parser/utils.rs b/src/parser/utils.rs
index 08a8506c..3f172bd3 100644
--- a/src/parser/utils.rs
+++ b/src/parser/utils.rs
@@ -283,7 +283,7 @@ fn merge_field(rterm1: RichTerm, rterm2: RichTerm) -> Option<RichTerm> {
             Some(RichTerm::from(Term::MetaValue(new_meta)))
         }
         (Some(meta), None) | (None, Some(meta)) => {
-            let mut new_meta = meta;
+            let mut new_meta = meta.clone();
             new_meta.value = new_value;
             Some(RichTerm::from(Term::MetaValue(new_meta)))
         }
@@ -329,7 +329,7 @@ pub fn mk_let(
     t2: RichTerm,
     span: RawSpan,
 ) -> Result<RichTerm, ParseError> {
-    let result = match pat {
+    let result = match pat.into() {
         d @ (Destruct::Record { .. } | Destruct::Array { .. }) => {
             if rec {
                 return Err(ParseError::RecursiveLetPattern(span));
diff --git a/src/pretty.rs b/src/pretty.rs
index 0afe0e6a..8707a9d1 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -5,34 +5,28 @@ pub use pretty::{DocAllocator, DocBuilder, Pretty};
 use regex::Regex;
 use std::collections::HashMap;
 
-/// Helper to find the min number of `%` sign needed to interpolate a string containing this chunk.
+/// helper to find the min number of `%` sign needed to interpolate
+/// a string containing this chunk.
 fn min_interpolate_sign(text: &str) -> usize {
     let reg = Regex::new(r#"([%]+\{)|("[%]+m)"#).unwrap();
-    reg.find_iter(text)
-        .map(|m| {
-            let d = m.end() - m.start();
-            // We iterate over all sequences `%+{` and `"%+m`, which could clash with the interpolation
-            // syntax, and return the maximum number of `%` insead each sequence.
-            //
-            // For the case of a closing delimiter `"%m`, we could actually be slightly smarter as we
-            // don't necessarily need more `%`, but just a different number of `%`. For example, if the
-            // string contains only one `"%%m`, then single `%` delimiters like `m%"` and `"%m` would
-            // be fine. But picking the maximum
-            //
-            // TODO: Improve the `"%*m` case if necessary.
-            if m.as_str().ends_with('{') {
-                d
-            } else {
-                d - 1
-            }
-        })
-        .max()
-        .unwrap_or(1)
+    reg.find_iter(text).fold(0, |nb, m| {
+        let d = m.end() - m.start();
+        // if the match end with `{` the nb of `%` is equal to the size of the match.
+        // This because a `%*{` match len is nb of `%` + 1. It's corect because we are
+        // looking for the minimum number of `%` to perform interpolation on this string
+        // Finaly, if the match ends with `m` we return match len - 1 because of the
+        // extra `"`. In this case, we could improve, because interpolation actualy
+        // need no more than a different number of `%` than the `"%*m` sequence.
+        // But this way is valid anyway and easyer to describe algorithmicaly.
+        // TODO: Improve the `"%*m` case if necessary.
+        let d = if m.as_str().ends_with("{") { d } else { d - 1 };
+        nb.max(d)
+    })
 }
 
-fn sorted_map<K: Ord, V>(m: &'_ HashMap<K, V>) -> Vec<(&'_ K, &'_ V)> {
+fn sorted_map<'a, K: Ord, V>(m: &'a HashMap<K, V>) -> Vec<(&'a K, &'a V)> {
     let mut ret: Vec<(&K, &V)> = m.iter().collect();
-    ret.sort_by_key(|(k, _)| *k);
+    ret.sort_by(|x, y| x.0.cmp(&y.0));
     ret
 }
 
@@ -43,24 +37,24 @@ where
     Self::Doc: Clone,
     A: Clone,
 {
-    fn quote_if_needed(&'a self, id: &crate::identifier::Ident) -> DocBuilder<'a, Self, A> {
+    fn quote_if_needed(self: &'a Self, id: &crate::identifier::Ident) -> DocBuilder<'a, Self, A> {
         let reg = Regex::new("^_?[a-zA-Z][_a-zA-Z0-9-]*$").unwrap();
-        if reg.is_match(id.as_ref()) {
+        if reg.is_match(&id.to_string()) {
             self.as_string(id)
         } else {
             self.as_string(id).double_quotes()
         }
     }
 
-    fn escaped_string(&'a self, s: &str) -> DocBuilder<'a, Self, A> {
+    fn escaped_string(self: &'a Self, s: &str) -> DocBuilder<'a, Self, A> {
         let s = s
-            .replace('\\', "\\\\")
+            .replace("\\", "\\\\")
             .replace("%{", "\\%{")
-            .replace('\"', "\\\"");
+            .replace("\"", "\\\"");
         self.text(s)
     }
 
-    fn metadata(&'a self, mv: &MetaValue, with_doc: bool) -> DocBuilder<'a, Self, A> {
+    fn metadata(self: &'a Self, mv: &MetaValue, with_doc: bool) -> DocBuilder<'a, Self, A> {
         if let Some(types) = &mv.types {
             self.text(":")
                 .append(self.space())
@@ -80,14 +74,14 @@ where
                         .append(
                             self.hardline()
                                 .append(self.intersperse(
-                                    doc.lines().map(|d| self.escaped_string(d)),
+                                    doc.lines().map(|d| self.escaped_string(&d)),
                                     self.hardline().clone(),
                                 ))
                                 .double_quotes(),
                         )
                         .append(self.line())
                 })
-                .unwrap_or_else(|| self.nil())
+                .unwrap_or(self.nil())
         } else {
             self.nil()
         })
@@ -194,8 +188,9 @@ where
         match self {
             Destruct::Record {
                 matches,
-                open,
-                rest,
+                // TODO: manage `..}` and `..x}` ending patterns
+                open: _,
+                rest: _,
                 ..
             } => allocator
                 .intersperse(
@@ -203,44 +198,11 @@ where
                         destruct::Match::Simple(id, meta) => allocator
                             .as_string(id)
                             .append(allocator.space())
-                            .append(match meta.clone() {
-                                MetaValue {
-                                    types,
-                                    contracts,
-                                    priority: crate::term::MergePriority::Default,
-                                    value: Some(value),
-                                    ..
-                                } => allocator
-                                    .text("?")
-                                    .append(allocator.space())
-                                    .append(allocator.atom(&value))
-                                    .append(allocator.metadata(
-                                        &MetaValue {
-                                            types,
-                                            contracts,
-                                            ..Default::default()
-                                        },
-                                        false,
-                                    )),
-                                m => allocator.metadata(&m, false),
-                            }),
+                            .append(allocator.metadata(meta, false)),
                         _ => unimplemented!(),
                     }),
                     allocator.text(",").append(allocator.space()),
                 )
-                .append(if *open {
-                    allocator
-                        .text(",")
-                        .append(allocator.space())
-                        .append(allocator.text(".."))
-                        .append(if let Some(rest) = rest {
-                            allocator.as_string(rest)
-                        } else {
-                            allocator.nil()
-                        })
-                } else {
-                    allocator.nil()
-                })
                 .braces(),
             Destruct::Empty => allocator.nil(),
             _ => unimplemented!(),
@@ -276,8 +238,8 @@ where
                         }, // be sure we have at least 1 `%` sign when an interpolation is present
                     )
                     .max()
-                    .unwrap_or(1);
-                let interp: String = "%".repeat(nb_perc);
+                    .unwrap();
+                let interp: String = std::iter::repeat("%").take(nb_perc).collect();
                 allocator
                     .intersperse(
                         chunks.iter().rev().map(|c| match c {
@@ -333,16 +295,15 @@ where
             FunPattern(..) => {
                 let mut params = vec![];
                 let mut rt = &self;
-                while let FunPattern(id, dst, t) = rt.as_ref() {
-                    params.push(if let Some(id) = id {
-                        allocator.as_string(id).append(if !dst.is_empty() {
-                            allocator.text("@").append(dst.pretty(allocator))
+                while let FunPattern(id, _dst, t) = rt.as_ref() {
+                    params.push(
+                        if let Some(id) = id {
+                            allocator.as_string(id)
                         } else {
                             allocator.nil()
-                        })
-                    } else {
-                        dst.pretty(allocator)
-                    });
+                        }
+                        .append(allocator.nil()),
+                    );
                     rt = t;
                 }
                 allocator
@@ -403,7 +364,7 @@ where
                                     .append(allocator.space())
                             })
                         })
-                        .unwrap_or_else(|| allocator.nil()),
+                        .unwrap_or(allocator.nil()),
                 )
                 .append(dst.pretty(allocator))
                 .append(if let MetaValue(ref mv) = rt.as_ref() {
@@ -468,7 +429,7 @@ where
                             .append(allocator.space())
                             .append(if let MetaValue(mv) = rt.as_ref() {
                                 allocator
-                                    .metadata(mv, true)
+                                    .metadata(&mv, true)
                                     .append(allocator.space())
                                     .append(
                                         mv.value
@@ -479,7 +440,7 @@ where
                                                     .append(allocator.space())
                                                     .append(v.pretty(allocator))
                                             })
-                                            .unwrap_or_else(|| allocator.nil()),
+                                            .unwrap_or(allocator.nil()),
                                     )
                             } else {
                                 allocator
@@ -517,7 +478,7 @@ where
                                     .append(allocator.space())
                                     .append(if let MetaValue(mv) = rt.as_ref() {
                                         allocator
-                                            .metadata(mv, true)
+                                            .metadata(&mv, true)
                                             .append(allocator.space())
                                             .append(
                                                 mv.value
@@ -528,7 +489,7 @@ where
                                                             .append(allocator.space())
                                                             .append(v.pretty(allocator))
                                                     })
-                                                    .unwrap_or_else(|| allocator.nil()),
+                                                    .unwrap_or(allocator.nil()),
                                             )
                                     } else {
                                         allocator
@@ -544,7 +505,7 @@ where
                                     .append(allocator.space())
                                     .append(if let MetaValue(mv) = rt.as_ref() {
                                         allocator
-                                            .metadata(mv, true)
+                                            .metadata(&mv, true)
                                             .append(allocator.space())
                                             .append(
                                                 mv.value
@@ -554,7 +515,7 @@ where
                                                             .text("=")
                                                             .append(v.pretty(allocator))
                                                     })
-                                                    .unwrap_or_else(|| allocator.nil()),
+                                                    .unwrap_or(allocator.nil()),
                                             )
                                     } else {
                                         allocator
@@ -602,7 +563,7 @@ where
                                 .append(allocator.space())
                                 .append(allocator.text("=>"))
                                 .append(allocator.space())
-                                .append(d.pretty(allocator))
+                                .append(d.to_owned().pretty(allocator))
                         }))
                         .nest(2)
                         .append(allocator.line_())
diff --git a/src/program.rs b/src/program.rs
index d0181656..652736ae 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -128,8 +128,8 @@ impl Program {
 
     /// Create a markdown file with documentation for the specified program in `.nickel/doc/program_main_file_name.md`
     #[cfg(feature = "doc")]
-    pub fn output_doc(&mut self, out: &mut dyn std::io::Write) -> Result<(), Error> {
-        doc::output_doc(&mut self.cache, self.main_id, out)
+    pub fn output_doc(&mut self) -> Result<(), Error> {
+        doc::output_doc(&mut self.cache, self.main_id)
     }
 
     #[cfg(debug_assertions)]
@@ -137,7 +137,7 @@ impl Program {
         self.cache.skip_stdlib = true;
     }
 
-    pub fn pprint_ast(
+    pub fn expand(
         &mut self,
         out: &mut std::io::BufWriter<Box<dyn std::io::Write>>,
         apply_transforms: bool,
@@ -244,28 +244,43 @@ mod doc {
     use comrak::arena_tree::NodeEdge;
     use comrak::nodes::{Ast, AstNode, NodeCode, NodeHeading, NodeValue};
     use comrak::{format_commonmark, parse_document, Arena, ComrakOptions};
-    use std::io::Write;
+    use std::fs::{self, File};
+    use std::path::Path;
 
     /// Create a markdown file with documentation for the specified FileId.
-    pub fn output_doc(
-        cache: &mut Cache,
-        file_id: FileId,
-        out: &mut dyn Write,
-    ) -> Result<(), Error> {
+    pub fn output_doc(cache: &mut Cache, file_id: FileId) -> Result<(), Error> {
         cache.parse(file_id)?;
-        // unwrap(): at this point the term was correctly parsed and should exist in cache
-        let term = cache.get_ref(file_id).unwrap();
-        let document = AstNode::from(NodeValue::Document);
 
-        // Our nodes in the Markdown document are owned by this arena
-        let arena = Arena::new();
+        for (file_id, term) in cache.terms() {
+            let document = AstNode::from(NodeValue::Document);
+
+            // Our nodes in the Markdown document are owned by this arena
+            let arena = Arena::new();
+
+            // The default ComrakOptions disables all extensions (essentially reducing to CommonMark)
+            let options = ComrakOptions::default();
 
-        // The default ComrakOptions disables all extensions (essentially reducing to CommonMark)
-        let options = ComrakOptions::default();
+            // Populate the document with the documentation
+            to_markdown(&term.term, 0, &arena, &document, &options)?;
 
-        to_markdown(term, 0, &arena, &document, &options)?;
-        format_commonmark(&document, &options, out)
-            .map_err(|e| Error::IOError(IOError(e.to_string())))?;
+            // Create markdown file and write resulting markdown to it
+            let file_path: &Path = cache.files().name(*file_id).as_ref();
+            let file_name = file_path.file_name().ok_or_else(|| {
+                Error::IOError(IOError(format!(
+                    "Could not get file name of: {}",
+                    file_path.display()
+                )))
+            })?;
+            let docpath: &Path = Path::new(".nickel/doc/");
+            fs::create_dir_all(docpath).map_err(|e| Error::IOError(IOError(e.to_string())))?;
+            let mut markdown_file = docpath.to_path_buf();
+            markdown_file.push(file_name);
+            markdown_file.set_extension("md");
+            let mut file = File::create(markdown_file.clone().into_os_string())
+                .map_err(|e| Error::IOError(IOError(e.to_string())))?;
+            format_commonmark(&document, &options, &mut file)
+                .map_err(|e| Error::IOError(IOError(e.to_string())))?;
+        }
 
         Ok(())
     }
@@ -281,14 +296,10 @@ mod doc {
     ) -> Result<(), Error> {
         match rt.term.as_ref() {
             Term::MetaValue(MetaValue { doc: Some(md), .. }) => {
-                document.append(parse_documentation(header_level, arena, md, options))
+                document.append(&mut parse_documentation(header_level, arena, &md, options))
             }
-            Term::Record(map, _) | Term::RecRecord(map, _, _, _) => {
-                // Sorting fields for a determinstic output
-                let mut entries: Vec<(_, _)> = map.iter().collect();
-                entries.sort_by_key(|(k, _)| *k);
-
-                for (ident, rt) in entries {
+            Term::Record(hm, _) | Term::RecRecord(hm, _, _, _) => {
+                for (ident, rt) in hm {
                     let header = mk_header(&ident.label, header_level + 1, arena);
                     document.append(header);
                     to_markdown(rt, header_level + 1, arena, document, options)?;
@@ -326,12 +337,12 @@ mod doc {
                 setext,
             });
         }
-        ast
+        return ast;
     }
 
     /// Creates a codespan header of the provided string with the provided header level.
     fn mk_header<'a>(
-        ident: &str,
+        ident: &String,
         header_level: u32,
         arena: &'a Arena<AstNode<'a>>,
     ) -> &'a AstNode<'a> {
@@ -342,7 +353,7 @@ mod doc {
 
         let code = arena.alloc(AstNode::from(NodeValue::Code(NodeCode {
             num_backticks: 1,
-            literal: ident.bytes().collect(),
+            literal: ident.clone().into_bytes(),
         })));
 
         res.append(code);
diff --git a/src/repl/command.rs b/src/repl/command.rs
index 192329ad..65ccac53 100644
--- a/src/repl/command.rs
+++ b/src/repl/command.rs
@@ -92,7 +92,7 @@ impl FromStr for Command {
     type Err = ReplError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let cmd_end = s.find(' ').unwrap_or(s.len());
+        let cmd_end = s.find(' ').unwrap_or_else(|| s.len());
         let cmd_str: String = s.chars().take(cmd_end).collect();
         let cmd: CommandType = cmd_str
             .parse()
diff --git a/src/repl/mod.rs b/src/repl/mod.rs
index e9e02d62..47b28d49 100644
--- a/src/repl/mod.rs
+++ b/src/repl/mod.rs
@@ -138,7 +138,12 @@ impl ReplImpl {
                 typecheck::type_check_in_env(&t, &repl_impl.env.type_env, &repl_impl.cache)?;
 
             if let Some(id) = id {
-                typecheck::Envs::env_add(&mut repl_impl.env.type_env, id, &t, &repl_impl.cache);
+                typecheck::Envs::env_add(
+                    &mut repl_impl.env.type_env,
+                    id.clone(),
+                    &t,
+                    &repl_impl.cache,
+                );
             }
 
             for id in &pending {
@@ -326,7 +331,7 @@ impl InputParser {
 
         match result {
             Ok((t, e)) if e.no_errors() => InputStatus::Complete(t),
-            Ok((_, e)) if e.errors.iter().all(partial) => InputStatus::Partial,
+            Ok((_, e)) if e.errors.iter().all(|e| partial(e)) => InputStatus::Partial,
             Ok((_, e)) => InputStatus::Failed(e),
             Err(e) if partial(&e) => InputStatus::Partial,
             Err(err) => InputStatus::Failed(err.into()),
diff --git a/src/serialize.rs b/src/serialize.rs
index d1c30e23..063a1e34 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -102,7 +102,7 @@ where
     S: Serializer,
 {
     let mut entries: Vec<(_, _)> = map.iter().collect();
-    entries.sort_by_key(|(k, _)| *k);
+    entries.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
 
     let mut map_ser = serializer.serialize_map(Some(entries.len()))?;
     for (id, t) in entries.iter() {
diff --git a/src/term.rs b/src/term.rs
index c93860f6..96ce7bbb 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -313,9 +313,10 @@ impl MetaValue {
             // If both have type annotations, the result will have the outer one as a type annotation.
             // However we still need to enforce the corresponding contract to preserve the operational
             // semantics. Thus, the inner type annotation is derelicted to a contract.
-            if let Some(ctr) = inner.types.take() {
-                contracts.push(ctr)
-            }
+            match inner.types.take() {
+                Some(ctr) => contracts.push(ctr),
+                _ => (),
+            };
         }
 
         contracts.extend(inner.contracts.into_iter());
@@ -391,10 +392,9 @@ impl Term {
             MetaValue(ref mut meta) => {
                 meta.contracts
                     .iter_mut()
-                    .for_each(|Contract { types, .. }| {
-                        if let AbsType::Flat(ref mut rt) = types.0 {
-                            func(rt)
-                        }
+                    .for_each(|Contract { types, .. }| match types.0 {
+                        AbsType::Flat(ref mut rt) => func(rt),
+                        _ => (),
                     });
                 meta.value.iter_mut().for_each(func);
             }
@@ -674,10 +674,6 @@ impl SharedTerm {
     pub fn make_mut(this: &mut Self) -> &mut Term {
         Rc::make_mut(&mut this.shared)
     }
-
-    pub fn ptr_eq(this: &SharedTerm, that: &SharedTerm) -> bool {
-        Rc::ptr_eq(&this.shared, &that.shared)
-    }
 }
 
 impl AsRef<Term> for SharedTerm {
@@ -1065,7 +1061,8 @@ impl RichTerm {
     pub fn without_pos(mut self) -> Self {
         fn clean_pos(rt: &mut RichTerm) {
             rt.pos = TermPos::None;
-            SharedTerm::make_mut(&mut rt.term).apply_to_rich_terms(clean_pos);
+            SharedTerm::make_mut(&mut rt.term)
+                .apply_to_rich_terms(|rt: &mut RichTerm| clean_pos(rt));
         }
 
         clean_pos(&mut self);
diff --git a/src/transform/desugar_destructuring.rs b/src/transform/desugar_destructuring.rs
index d8d6a61a..4b767f4e 100644
--- a/src/transform/desugar_destructuring.rs
+++ b/src/transform/desugar_destructuring.rs
@@ -84,7 +84,7 @@ pub fn desugar_with_contract(rt: RichTerm) -> RichTerm {
         with {
             Term::LetPattern(x, pat, t_, body) => {
                 let pos = body.pos;
-                let meta = pat.clone().into_contract();
+                let meta = pat.clone().as_contract();
                 let t_ = {
                     let t_pos = t_.pos;
                     RichTerm::new(
@@ -152,7 +152,7 @@ fn bind_open_field(x: Ident, pat: &Destruct, body: RichTerm) -> RichTerm {
         _ => panic!("A closed pattern can not have a rest binding"),
     };
     Term::Let(
-        var,
+        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)
diff --git a/src/transform/free_vars.rs b/src/transform/free_vars.rs
index e2accf72..99bd9d94 100644
--- a/src/transform/free_vars.rs
+++ b/src/transform/free_vars.rs
@@ -150,8 +150,9 @@ fn collect_free_vars(rt: &mut RichTerm, free_vars: &mut HashSet<Ident>) {
         }
         Term::StrChunks(chunks) => {
             for chunk in chunks {
-                if let StrChunk::Expr(t, _) = chunk {
-                    collect_free_vars(t, free_vars)
+                match chunk {
+                    StrChunk::Expr(t, _) => collect_free_vars(t, free_vars),
+                    _ => (),
                 }
             }
         }
diff --git a/src/transform/mod.rs b/src/transform/mod.rs
index a11e3f81..0e5e90ef 100644
--- a/src/transform/mod.rs
+++ b/src/transform/mod.rs
@@ -137,12 +137,10 @@ impl Closurizable for RichTerm {
         let pos = self.pos;
 
         let thunk = match self.as_ref() {
-            Term::Var(id) if id.is_generated() => with_env.get(id).unwrap_or_else(|| {
-                panic!(
+            Term::Var(id) if id.is_generated() => with_env.get(&id).expect(&format!(
                 "Internal error(closurize) : generated identifier {} not found in the environment",
                 id
-            )
-            }),
+            )),
             _ => {
                 let closure = Closure {
                     body: self,
diff --git a/src/typecheck/eq.rs b/src/typecheck/eq.rs
deleted file mode 100644
index 31beb589..00000000
--- a/src/typecheck/eq.rs
+++ /dev/null
@@ -1,356 +0,0 @@
-//! Computation of type equality for contracts (flat types).
-//!
-//! Determine if two contracts are equal as opaque types. Used to decide if two contract should
-//! unify.
-//!
-//! ## Aliases
-//!
-//! One basic case we want to handle is aliases, which come in handy for parametrized contracts. We
-//! want to equate `Alias` with e.g. `Foo "bar" "baz"` if `Alias` has been defined as
-//! `let Alias = Foo "bar" "baz" in ...`.
-//!
-//! We also want to equate different aliases with the same definition: `Alias type_eq Alias'` if
-//! `let Alias' = Foo "bar" "baz" in ...`, or `let Alias' = Alias in ...`.
-//!
-//! ## Recursion
-//!
-//! We must refrain from following all variables links blindly, as there could be cycles in the
-//! graph leading to an infinite loop:
-//!
-//! ```nickel
-//! {
-//!   Foo = Bar,
-//!   Bar = Foo,
-//! }
-//! ```
-//!
-//! Because we just follows variables, and don't apply functions, we can detect cycles while
-//! walking the graph. Still, as it is potentially performed many times during typechecking, type
-//! equality ought to stay reasonably cheap. We choose to just set an arbitrary limit (the gas) on
-//! the number of variable links that the type equality may follow. Doing so, we don't have to
-//! worry about loops anymore.
-//!
-//! ## Equality on terms
-//!
-//! The terms inside a type may be arbitrarily complex. Primops applications, `switch`, and the
-//! like are quite unlikely to appear inside an annotation (they surely appear inside contract
-//! definitions). We don't want to compare functions syntactically either. The spirit of this
-//! implementation is to equate aliases or simple constructs that may appear inlined inside an
-//! annotation (applications, records, primitive constants and arrays, mostly) in a structural way.
-//! We first test for physical equality (both as an optimization and to detect two variables
-//! pointing to the same contract definition in the AST). If the comparison fails, we do a simple
-//! structural recursion, unfolding simple forms and following variables with a limited number of
-//! times. For anything more complex, we return false.
-
-use super::*;
-
-/// The maximal number of variable links we want to unfold before abandoning the check. It should
-/// stay low, but has been fixed arbitrarily: feel fee to increase reasonably if it turns out
-/// legitimate type equalities between simple contracts are unduly rejected in practice.
-pub const MAX_GAS: u8 = 8;
-
-#[derive(PartialEq, Clone)]
-pub struct TermEnvironment<'a>(
-    pub crate::environment::Environment<Ident, (&'a RichTerm, TermEnvironment<'a>)>,
-);
-
-/// State threaded through the type equality computation.
-#[derive(Copy, Clone, Default)]
-struct State {
-    /// Used to generate temporary rigid type variables for substituting type variables when
-    /// comparing foralls. Those ids never escape the type equality computations and are used
-    /// solely as rigid type variables: this is why they don't need proper allocation in the
-    /// unification table or to care about those ids clashing with the one generated by the
-    /// typechecker. Generated type constants simply needs to be unique for the duration of the
-    /// type equality computation.
-    var_uid: usize,
-    /// The current gas remaining for variable substitutions. Once it reaches zero and we encounter
-    /// a variable, we abort the computation and return false.
-    gas: u8,
-}
-
-impl State {
-    /// Create a fresh unique rigid type variable.
-    pub fn fresh_cst(&mut self) -> TypeWrapper {
-        let result = self.var_uid;
-        self.var_uid += 1;
-        TypeWrapper::Constant(result)
-    }
-
-    /// Try to consume one unit of gas for a variable substitution. Return true in case of success,
-    /// or false if the gas was already at zero.
-    pub fn use_gas(&mut self) -> bool {
-        if self.gas == 0 {
-            false
-        } else {
-            self.gas -= 1;
-            true
-        }
-    }
-}
-
-/// Compute the equality between two flat types (contracts).
-///
-/// # Parameters
-///
-/// - `env`: an environment mapping variables to their definition (the second placeholder in a
-///   `let _ = _ in _`)
-pub fn type_eq(t1: &RichTerm, t2: &RichTerm, env: &TermEnvironment) -> bool {
-    contract_eq_bounded(&mut State::default(), t1, env, t2, env)
-}
-
-/// Decide type equality on contracts in their respective environment and given the remaining gas
-/// `gas`.
-fn contract_eq_bounded(
-    state: &mut State,
-    t1: &RichTerm,
-    env1: &TermEnvironment,
-    t2: &RichTerm,
-    env2: &TermEnvironment,
-) -> bool {
-    use Term::*;
-
-    // Test for physical equality as both an optimization and a way to cheaply equate complex
-    // contracts that happen to point to the same definition (while the purposely limited
-    // structural checks below may reject the equality)
-    if crate::term::SharedTerm::ptr_eq(&t1.term, &t2.term) {
-        return true;
-    }
-
-    match (t1.as_ref(), t2.as_ref()) {
-        (Null, Null) => true,
-        (Bool(b1), Bool(b2)) => b1 == b2,
-        (Num(n1), Num(n2)) => n1 == n2,
-        (Str(s1), Str(s2)) => s1 == s2,
-        (Enum(id1), Enum(id2)) => id1 == id2,
-        (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
-        // interpolated expression), as static string may be currently parsed as such. We return
-        // false for anything more complex.
-        (StrChunks(scs1), StrChunks(scs2)) => {
-            scs1.len() == scs2.len()
-                && scs1
-                    .iter()
-                    .zip(scs2.iter())
-                    .all(|(chunk1, chunk2)| match (chunk1, chunk2) {
-                        (StrChunk::Literal(s1), StrChunk::Literal(s2)) => s1 == s2,
-                        _ => false,
-                    })
-        }
-        (App(head1, arg1), App(head2, arg2)) => {
-            contract_eq_bounded(state, head1, env1, head2, env2)
-                && contract_eq_bounded(state, arg1, env1, arg2, env2)
-        }
-        // All variables must be bound at this stage. This is checked by the typechecker when
-        // walking annotations. However, we may assume that `env` is a local environment (that it
-        // doesn't include the stdlib). In that case, free variables (unbound) may be deemed equal
-        // if they have the same identifier: whatever global environment the term will be put in,
-        // free variables are not redefined locally and will be bound to the same value in any case.
-        (Var(id1), Var(id2)) => match (env1.0.get(id1), env2.0.get(id2)) {
-            (None, None) => id1 == id2,
-            (Some((t1, env1)), Some((t2, env2))) => {
-                // We may end up using one more gas unit if gas was exactly 1. That is not very
-                // important, and it's simpler to just ignore this case. We still return false
-                // if gas was already at zero.
-                let had_gas = state.use_gas();
-                state.use_gas();
-                had_gas && contract_eq_bounded(state, t1, &env1, t2, &env2)
-            }
-            _ => false,
-        },
-        (Var(id), _) => {
-            state.use_gas()
-                && env1
-                    .0
-                    .get(id)
-                    .map(|(t1, env1)| contract_eq_bounded(state, t1, &env1, t2, env2))
-                    .unwrap_or(false)
-        }
-        (_, Var(id)) => {
-            state.use_gas()
-                && env2
-                    .0
-                    .get(id)
-                    .map(|(t2, env2)| contract_eq_bounded(state, t1, env1, t2, &env2))
-                    .unwrap_or(false)
-        }
-        (Record(m1, attrs1), Record(m2, attrs2)) => {
-            map_eq(contract_eq_bounded, state, m1, env1, m2, env2) && attrs1 == attrs2
-        }
-        (RecRecord(m1, dyn_fields1, attrs1, _), RecRecord(m2, dyn_fields2, attrs2, _)) =>
-        // We only compare records which field structure is statically known (i.e. without
-        // dynamic fields).
-        {
-            dyn_fields1.is_empty()
-                && dyn_fields2.is_empty()
-                && map_eq(contract_eq_bounded, state, m1, env1, m2, env2)
-                && attrs1 == attrs2
-        }
-        (Array(ts1, attrs1), Array(ts2, attrs2)) => {
-            ts1.len() == ts2.len()
-                && ts1
-                    .iter()
-                    .zip(ts2.iter())
-                    .all(|(t1, t2)| contract_eq_bounded(state, t1, env1, t2, env2))
-                && attrs1 == attrs2
-        }
-        // We must compare the inner values as well as the corresponding contracts or type
-        // annotations. Documentation and merge priority shouldn't impact the result on the
-        // other hand.
-        (MetaValue(m1), MetaValue(m2)) => {
-            let value_eq = match (&m1.value, &m2.value) {
-                (None, None) => true,
-                (Some(v1), Some(v2)) => contract_eq_bounded(state, v1, env1, v2, env2),
-                _ => false,
-            };
-
-            // We use the same logic as in the typechecker: the type associated to an annotated
-            // value is either the type annotation, or the first contract annotation.
-            let ty1 = m1.types.as_ref().or_else(|| m1.contracts.first());
-            let ty2 = m2.types.as_ref().or_else(|| m2.contracts.first());
-
-            let ty_eq = match (ty1, ty2) {
-                (None, None) => true,
-                (Some(ctr1), Some(ctr2)) => type_eq_bounded(
-                    state,
-                    &TypeWrapper::from(ctr1.types.clone()),
-                    env1,
-                    &TypeWrapper::from(ctr2.types.clone()),
-                    env2,
-                ),
-                _ => false,
-            };
-
-            value_eq && ty_eq
-        }
-        // We dont treat imports and parse errors
-        _ => false,
-    }
-}
-
-/// Compute the equality between two hashmaps holding either types or terms.
-fn map_eq<V, F>(
-    mut f: F,
-    state: &mut State,
-    map1: &HashMap<Ident, V>,
-    env1: &TermEnvironment,
-    map2: &HashMap<Ident, V>,
-    env2: &TermEnvironment,
-) -> bool
-where
-    F: FnMut(&mut State, &V, &TermEnvironment, &V, &TermEnvironment) -> bool,
-{
-    map1.len() == map2.len()
-        && map1.iter().all(|(id, v1)| {
-            map2.get(id)
-                .map(|v2| f(state, v1, env1, v2, env2))
-                .unwrap_or(false)
-        })
-}
-
-/// Convert rows (represented as linked list for the needs of unification) to a hashmap to perform
-/// the type equality comparison.
-///
-/// Require the rows to be closed (i.e. the last element must be RowEmpty), otherwise `None` is
-/// returned. `None` is returned as well if a type encountered is not a row type, a if it is a enum
-/// row.
-fn rows_as_map(ty1: &TypeWrapper) -> Option<HashMap<Ident, &TypeWrapper>> {
-    let mut map = HashMap::new();
-    let mut curr = ty1;
-
-    loop {
-        match curr {
-            TypeWrapper::Concrete(AbsType::RowEmpty()) => return Some(map),
-            TypeWrapper::Concrete(AbsType::RowExtend(id, Some(ty), tail)) => {
-                map.insert(id.clone(), ty);
-                curr = tail.as_ref();
-            }
-            _ => return None,
-        }
-    }
-}
-
-/// Perform the type equality comparison on types. Structurally recurse into type constructors and test
-/// that subtypes or subterms (contracts) are equals.
-///
-/// Currently, this function morally operates on `Types` rather than `TypeWrapper`s as it is called
-/// by `contract_eq_bounded` on type annotations. But we need to substitute variables to correctly
-/// compare `foralls`, hence it accepts more general `TypeWrapper`s. However, we expect to never
-/// meet unification variables (we treat them for completeness and to be future proof), and that
-/// all the rigid type variables encountered have been introduced by `type_eq_bounded` itself. This
-/// is why we don't need unique identifiers that are distinct from the one used during
-/// typechecking, and we can just start from `0`.
-fn type_eq_bounded(
-    state: &mut State,
-    ty1: &TypeWrapper,
-    env1: &TermEnvironment,
-    ty2: &TypeWrapper,
-    env2: &TermEnvironment,
-) -> bool {
-    match (ty1, ty2) {
-        (TypeWrapper::Concrete(s1), TypeWrapper::Concrete(s2)) => match (s1, s2) {
-            (AbsType::Wildcard(id1), AbsType::Wildcard(id2)) => id1 == id2,
-            (AbsType::Dyn(), AbsType::Dyn())
-            | (AbsType::Num(), AbsType::Num())
-            | (AbsType::Bool(), AbsType::Bool())
-            | (AbsType::Sym(), AbsType::Sym())
-            | (AbsType::Str(), AbsType::Str()) => true,
-            (AbsType::Enum(tyw1), AbsType::Enum(tyw2))
-            | (AbsType::DynRecord(tyw1), AbsType::DynRecord(tyw2))
-            | (AbsType::Array(tyw1), AbsType::Array(tyw2)) => {
-                type_eq_bounded(state, tyw1, env1, tyw2, env2)
-            }
-            (AbsType::Arrow(s1, t1), AbsType::Arrow(s2, t2)) => {
-                type_eq_bounded(state, s1, env1, s2, env2)
-                    && type_eq_bounded(state, t1, env1, t2, env2)
-            }
-            (AbsType::StaticRecord(tyw1), AbsType::StaticRecord(tyw2)) => {
-                fn type_eq_bounded_wrapper(
-                    state: &mut State,
-                    tyw1: &&TypeWrapper,
-                    env1: &TermEnvironment,
-                    tyw2: &&TypeWrapper,
-                    env2: &TermEnvironment,
-                ) -> bool {
-                    type_eq_bounded(state, *tyw1, env1, *tyw2, env2)
-                }
-
-                let map1 = rows_as_map(tyw1);
-                let map2 = rows_as_map(tyw2);
-
-                map1.zip(map2)
-                    .map(|(m1, m2)| map_eq(type_eq_bounded_wrapper, state, &m1, env1, &m2, env2))
-                    .unwrap_or(false)
-            }
-            (AbsType::Flat(t1), AbsType::Flat(t2)) => {
-                contract_eq_bounded(state, t1, env1, t2, env2)
-            }
-            (AbsType::Forall(i1, tyw1), AbsType::Forall(i2, tyw2)) => {
-                let constant_type: TypeWrapper = state.fresh_cst();
-
-                type_eq_bounded(
-                    state,
-                    &tyw1.clone().subst(i1.clone(), constant_type.clone()),
-                    env1,
-                    &tyw2.clone().subst(i2.clone(), constant_type),
-                    env2,
-                )
-            }
-            // We can't compare type variables without knowing what they are instantiated to, and
-            // all type variables should have been substituted at this point, so we bail out.
-            _ => false,
-        },
-        (TypeWrapper::Ptr(p1), TypeWrapper::Ptr(p2)) => {
-            debug_assert!(
-                false,
-                "we shouldn't come across unification variables during type equality computation"
-            );
-            p1 == p2
-        }
-        (TypeWrapper::Constant(i1), TypeWrapper::Constant(i2)) => i1 == i2,
-        _ => false,
-    }
-}
diff --git a/src/typecheck/error.rs b/src/typecheck/error.rs
index 160792ac..792a4bb9 100644
--- a/src/typecheck/error.rs
+++ b/src/typecheck/error.rs
@@ -97,7 +97,7 @@ pub enum UnifError {
     WithConst(usize, TypeWrapper),
     /// A flat type, which is an opaque type corresponding to custom contracts, contained a Nickel
     /// term different from a variable. Only a variables is a legal inner term of a flat type.
-    IncomparableFlatTypes(RichTerm, RichTerm),
+    IllformedFlatType(RichTerm),
     /// A generic type was ill-formed. Currently, this happens if a `StatRecord` or `Enum` type
     /// does not contain a row type.
     IllformedType(TypeWrapper),
@@ -173,8 +173,8 @@ impl UnifError {
                 reporting::to_type(state.table, state.names, names, ty),
                 pos_opt,
             ),
-            UnifError::IncomparableFlatTypes(rt1, rt2) => {
-                TypecheckError::IncomparableFlatTypes(rt1, rt2, pos_opt)
+            UnifError::IllformedFlatType(rt) => {
+                TypecheckError::IllformedType(Types(AbsType::Flat(rt)))
             }
             UnifError::IllformedType(tyw) => TypecheckError::IllformedType(reporting::to_type(
                 state.table,
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 360e3610..a37fe306 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -64,7 +64,6 @@ pub mod operation;
 pub mod reporting;
 #[macro_use]
 pub mod mk_typewrapper;
-pub mod eq;
 
 use error::*;
 use operation::{get_bop_type, get_nop_type, get_uop_type};
@@ -426,7 +425,7 @@ fn walk<L: Linearizer>(
                 ..
                 } => {
                     let tyw2 = TypeWrapper::from(ty2.clone());
-                    let instantiated = instantiate_foralls(state, tyw2, ForallInst::Constant);
+                    let instantiated = instantiate_foralls(state, tyw2.clone(), ForallInst::Constant);
                     type_check_(state, envs, lin, linearizer, t, instantiated)
                 }
                 MetaValue {value: Some(t), .. } =>  walk(state, envs, lin, linearizer, t),
@@ -488,7 +487,7 @@ fn inject_pat_vars(pat: &Destruct, envs: &mut Envs) {
                 let id = bind_id.as_ref().unwrap_or(id);
                 envs.insert(id.clone(), TypeWrapper::Concrete(AbsType::Dyn()));
                 if !pat.is_empty() {
-                    inject_pat_vars(pat, envs);
+                    inject_pat_vars(&pat, envs);
                 }
             }
         });
@@ -1259,7 +1258,15 @@ pub fn unify(state: &mut State, mut t1: TypeWrapper, mut t2: TypeWrapper) -> Res
                     )
                 })
             }
-            (AbsType::Flat(s), AbsType::Flat(t)) => Err(UnifError::IncomparableFlatTypes(s, t)),
+            (AbsType::Flat(s), AbsType::Flat(t)) => match (s.as_ref(), t.as_ref()) {
+                (Term::Var(vs), Term::Var(ts)) if vs == ts => Ok(()),
+                (Term::Var(_), Term::Var(_)) => Err(UnifError::TypeMismatch(
+                    TypeWrapper::Concrete(AbsType::Flat(s)),
+                    TypeWrapper::Concrete(AbsType::Flat(t)),
+                )),
+                (Term::Var(_), _) => Err(UnifError::IllformedFlatType(t)),
+                _ => Err(UnifError::IllformedFlatType(s)),
+            },
             (r1, r2) if r1.is_row_type() && r2.is_row_type() => {
                 unify_rows(state, r1.clone(), r2.clone()).map_err(|err| {
                     err.into_unif_err(TypeWrapper::Concrete(r1), TypeWrapper::Concrete(r2))
@@ -1685,6 +1692,6 @@ fn get_wildcard_var(
 fn wildcard_vars_to_type(wildcard_vars: Vec<TypeWrapper>, table: &UnifTable) -> Wildcards {
     wildcard_vars
         .into_iter()
-        .map(|var| to_type(table, var))
+        .map(|var| to_type(&table, var))
         .collect()
 }
diff --git a/stdlib/builtin.ncl b/stdlib/builtin.ncl
index d151a8ed..f90cb7d5 100644
--- a/stdlib/builtin.ncl
+++ b/stdlib/builtin.ncl
@@ -112,21 +112,6 @@
       "%m
     = fun x => %typeof% x == `Str,
 
-    is_enum : Dyn -> Bool
-    | doc m%"
-      Checks if the given value is an enum tag.
-
-      For example:
-      ```nickel
-        is_enum true =>
-          false
-        is_enum `false =>
-          true
-      ```
-      "%m
-
-    = fun x => %typeof% x == `Enum,
-
     is_fun : Dyn -> Bool
     | doc m%"
       Checks if the given value is a function.
@@ -253,22 +238,5 @@
       ```
       "%m
     = fun format x => %deserialize% format x,
-
-    to_str | string.Stringable -> Str
-    | doc m%"
-      Converts a stringable value to a string representation. Same as
-      `string.from`.
-
-      For example:
-      ```nickel
-      from 42 =>
-        "42"
-      from `Foo =>
-        "Foo"
-      from null =>
-        "null"
-      "%m
-    = fun x => %to_str% x,
-
   }
 }
diff --git a/stdlib/string.ncl b/stdlib/string.ncl
index 9ef115ef..1d83d957 100644
--- a/stdlib/string.ncl
+++ b/stdlib/string.ncl
@@ -76,53 +76,6 @@
       else
         %blame% (%tag% "not a string" l),
 
-    EnumTag
-    | doc m%"
-      Contract to enforce the value is an enum tag.
-
-      For example:
-      ```nickel
-        (`foo | EnumTag) =>
-          `foo
-        (`FooBar | EnumTag) =>
-          `FooBar
-        ("tag" | EnumTag) =>
-          error
-      ```
-      "%m
-    = contract.from_predicate builtin.is_enum,
-
-    Stringable
-    | doc m%"
-      Contract to enforce the value is convertible to a string via
-      `builtin.to_string` or `string.from`. Accepted values are:
-
-      - numbers
-      - booleans
-      - strings
-      - enum tags
-      - null
-
-      For example:
-      ```nickel
-        (`Foo | Stringable) =>
-          `Foo
-        (false | Stringable) =>
-          false
-        ("bar" ++ "foo" | Stringable) =>
-          "barfoo"
-        ({foo = "baz"} | Stringable) =>
-          error
-      ```
-      "%m
-    = contract.from_predicate (fun value =>
-      let type = builtin.typeof value in
-      value == null
-      || type == `Num
-      || type == `Bool
-      || type == `Str
-      || type == `Enum),
-
     NonEmpty
     | doc m%"
       Contract to enforce the value is a non-empty string.
@@ -399,36 +352,17 @@
       "%m
     = fun start end s => %str_substr% s start end,
 
-    from | Stringable -> Str
-    | doc m%"
-      Converts a correct value to a string representation. Same as
-      `builtin.to_str`.
-
-      For example:
-      ```nickel
-      from 42 =>
-        "42"
-      from `Foo =>
-        "Foo"
-      from null =>
-        "null"
-      "%m
-    = fun x => %to_str% x,
-
     from_num | Num -> Str
     | doc m%"
       Converts a number to its string representation.
 
-      For example:
-      ```nickel
       from_num 42 =>
         "42"
-      ```
       "%m
-    = from,
+    = fun n => %to_str% n,
 
     # from_enum | < | Dyn> -> Str = fun tag => %to_str% tag,
-    from_enum | EnumTag -> Str
+    from_enum | Dyn -> Str
     | doc m%"
       Converts an enum variant to its string representation.
 
@@ -438,7 +372,7 @@
         "MyEnum"
       ```
       "%m
-    = from,
+    = fun tag => %to_str% tag,
 
     from_bool | Bool -> Str
     | doc m%"
@@ -450,7 +384,7 @@
           "true"
       ```
       "%m
-    = from,
+    = fun b => %to_str% b,
 
     to_num | NumLiteral -> Num
     | doc m%"
@@ -481,7 +415,7 @@
     = fun s => s == "true",
 
     # to_enum | Str -> < | Dyn> = fun s => %enum_from_str% s,
-    to_enum | Str -> EnumTag
+    to_enum | Str -> Dyn
     | doc m%"
       Converts any string that represents an enum variant to that enum variant.
 


Test patch:
diff --git a/tests/pass/typechecking.ncl b/tests/pass/typechecking.ncl
index 3adf43b3..7c325a85 100644
--- a/tests/pass/typechecking.ncl
+++ b/tests/pass/typechecking.ncl
@@ -13,14 +13,9 @@ let typecheck = [
   # functions
   (fun x => if x then x + 1 else 34) false,
   let id : Num -> Num = fun x => x in (id 4 : Num),
-
   # contracts are opaque types
-  # TODO: restore the following tests once type equality for contracts is
-  # properly implemented (see https://github.com/tweag/nickel/issues/701 and
-  # https://github.com/tweag/nickel/issues/724)
-
-  # (let AlwaysTrue = fun l t => if t then t else %blame% l in
-  #  ((fun x => x) : AlwaysTrue -> AlwaysTrue)),
+  (let AlwaysTrue = fun l t => if t then t else %blame% l in
+  ((fun x => x) : AlwaysTrue -> AlwaysTrue)),
 
   # simple_polymorphism
   let f : forall a. a -> a = fun x => x in