Repository: tweag/nickel
Pull request #924 (2022-11-18 12:28:44)

Expand documentation of revertible thunks
First step toward fixing #908. After pairing with @vkleen, we realized that some underlying assumptions and the overall mental model of what are revertible thunks and what they are used for wasn't very clear. This PR tries to fill this gap.

A subsequent PR will bring the conclusion of this documentation to its logical conclusion, by piggy-packing on functions and function application instead of re-implementing that in an ad-hoc way inside `eval::fixpoint` as much as possible.


Patch:
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 504df4ae..da68c560 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -50,4 +50,4 @@ jobs:
 
     - name: Run all checks
       run: |
-        nix flake check --print-build-logs
+        nix flake check
diff --git a/Cargo.lock b/Cargo.lock
index ea7d343c..53a2bea3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1197,7 +1197,6 @@ dependencies = [
  "rustyline",
  "rustyline-derive",
  "serde",
- "serde-wasm-bindgen",
  "serde_json",
  "serde_repr",
  "serde_yaml",
@@ -1869,17 +1868,6 @@ dependencies = [
  "serde_derive",
 ]
 
-[[package]]
-name = "serde-wasm-bindgen"
-version = "0.4.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
-dependencies = [
- "js-sys",
- "serde",
- "wasm-bindgen",
-]
-
 [[package]]
 name = "serde_cbor"
 version = "0.11.2"
diff --git a/Cargo.toml b/Cargo.toml
index 2c875bb6..8ccdfc20 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -54,7 +54,6 @@ rustyline-derive = { version = "0.6.0", optional = true }
 
 # The `wasm-bindgen` version is pinned (`=`) because it must be the same as `wasm-bindgen-cli` in `flake.nix`
 wasm-bindgen = { version = "=0.2.83", optional = true, features = ["serde-serialize"] }
-serde-wasm-bindgen = "0.4.5"
 js-sys = { version = "0.3", optional = true }
 serde_repr = { version = "0.1", optional = true }
 pretty = "0.11.3"
diff --git a/flake.nix b/flake.nix
index 74ca4dc8..6ae93575 100644
--- a/flake.nix
+++ b/flake.nix
@@ -138,98 +138,51 @@
           '';
         };
 
-      pre-commit-builder =
-        { rust ? mkRust { }
-        , nickel ? buildNickel { inherit rust; }
-        , isHermetic
-        }: pre-commit-hooks.lib.${system}.run {
-          src = self;
-          hooks = {
-            nixpkgs-fmt = {
-              enable = true;
-              excludes = [
-                "lsp/client-extension/default.nix"
-                "lsp/client-extension/node-env.nix"
-                "lsp/client-extension/node-packages.nix"
-              ];
-            };
+      buildNickel =
+        { channel ? "stable"
+        , isDevShell ? false
+        , target ? pkgs.rust.toRustTarget pkgs.stdenv.hostPlatform
+        ,
+        }:
+        let
+          rustProfile =
+            if isDevShell then "default"
+            else "minimal";
 
-            rustfmt = {
-              enable = true;
-              entry = pkgs.lib.mkForce "${rust}/bin/cargo-fmt fmt -- --check --color always";
-            };
+          rust = mkRust { inherit rustProfile channel target; };
 
-            markdownlint = {
-              enable = true;
-              excludes = [
-                "notes/(.+)\\.md$"
-                "^RELEASES\\.md$"
-              ];
-            };
-
-            # The default `clippy` pre-commit-hook is too standard, hence we create our own.
-            # Compared to the default `clippy` hook, we customize the following:
-            # - custom Clippy options
-            # - in case of hermetic run (mostly used by CI), we reuse the Nixified Rust dependencies from `nickel`
-            custom-clippy =
-              let
-                allow = [
-                  "clippy::new-without-default"
-                  "clippy::match_like_matches_macro"
+          pre-commit = pre-commit-hooks.lib.${system}.run {
+            src = self;
+            hooks = {
+              nixpkgs-fmt = {
+                enable = true;
+                excludes = [
+                  "lsp/client-extension/default.nix"
+                  "lsp/client-extension/node-env.nix"
+                  "lsp/client-extension/node-packages.nix"
                 ];
-
-                allowOpts = builtins.concatStringsSep " "
-                  (builtins.map (lint: "--allow \"${lint}\"") allow);
-
-                # Clippy requires access to the crate registry because many lints require dependency information.
-                # Similarly to `buildNickel`, we support 2 modes:
-                # - when run as a shell hook, we want to be fast, so we let Cargo do incremental compilation, and download dependencies as needed.
-                #   This is mainly used by developers, who care about speed at the expense of hermeticity.
-                # - when run as a flake check, we want to be fully reproducible, so we Nixify Cargo dependency resolution.
-                #   This is mainly used by CI, where the extra time is not such a big deal.
-                offlineOpts = pkgs.lib.optionalString isHermetic "--frozen --offline";
-
-                # When running in hermetic mode, all Rust dependencies must be already present.
-                # Hence we can't just call `cargo` directly, as it would try to download dependencies.
-                # Hence we do the following:
-                # - `source ${nickel.inputDerivation}` to set the environment variables `buildInputs` and `stdenv` just like in a Nickel build.
-                #   The `buildInputs` will contain the path to Nixified Rust dependencies (a.k.a. `cargoHome`), as well as other build inputs, like `Security` for Darwin.
-                #   ⚠️ This will also completely unset `PATH`, and *update* various environment variables used in downstream scripts, like `TMPDIR`.
-                # - `source $stdenv/setup` does a lot of Nix magic, including loading in the `PATH` everything in the `buildInputs` environment variable.
-                #
-                # 💡 The other commands are needed for Darwin builds to undo some changes of `source ${nickel.inputDerivation}`.
-                #    For Linux, these extra commands are unnecessary (because their value is "updated" from `/build` to `/build`), though harmless.
-                nickelBuildInputsSetup = pkgs.lib.optionalString isHermetic ''
-                  nix_build_top=$NIX_BUILD_TOP
-                  source ${nickel.inputDerivation}
-                  NIX_BUILD_TOP=$nix_build_top
-                  TEMP=$nix_build_top
-                  TEMPDIR=$nix_build_top
-                  TMP=$nix_build_top
-                  TMPDIR=$nix_build_top
-                  source $stdenv/setup
-                '';
-              in
-              {
+              };
+              rustfmt = {
                 enable = true;
-                files = "\\.rs$";
-                pass_filenames = false;
-                entry =
-                  "${pkgs.writeShellScript "clippy-hook" ''
-                    ${nickelBuildInputsSetup}
-                    cargo clippy --workspace ${offlineOpts} -- --no-deps --deny warnings ${allowOpts}
-                  ''}";
+                entry = pkgs.lib.mkForce "${rust}/bin/cargo-fmt fmt -- --check --color always";
+              };
+              markdownlint = {
+                enable = true;
+                excludes = [
+                  "notes/(.+)\\.md$"
+                  "^RELEASES\\.md$"
+                ];
               };
+            };
           };
-        };
 
-      buildNickel =
-        { rust ? mkRust { }, withCargoHome ? true }:
+        in
         pkgs.stdenv.mkDerivation {
           name = "nickel-${version}";
 
           buildInputs =
             [ rust ]
+            ++ missingSysPkgs
             # cargoHome is used for a fully, hermetic, nixified build. This is
             # what we want for e.g. nix-build. However, when hacking on Nickel,
             # we rather provide the necessary tooling but let people use the
@@ -240,10 +193,9 @@
             # - we get incremental build and a build order of magnitudes
             #   faster
             # - etc.
-            ++ pkgs.lib.optional withCargoHome cargoHome
-            ++ missingSysPkgs;
+            ++ (if isDevShell then [ pkgs.rust-analyzer ] else [ cargoHome ]);
 
-          src = self;
+          src = if isDevShell then null else self;
 
           buildPhase = ''
             cargo build --workspace --exclude nickel-repl --release --frozen --offline
@@ -253,7 +205,9 @@
 
           checkPhase = ''
             cargo test --release --frozen --offline
-          '';
+          '' + (pkgs.lib.optionalString (channel == "stable") ''
+            cargo fmt --all -- --check
+          '');
 
           installPhase = ''
             mkdir -p $out
@@ -262,24 +216,26 @@
             rm $out/.crates.toml
           '';
 
-        };
-
-      makeDevShell = { rust }: pkgs.mkShell {
-        inputsFrom = [ (buildNickel { inherit rust; withCargoHome = false; }) ];
-        buildInputs = [ pkgs.rust-analyzer ];
+          shellHook = pre-commit.shellHook + ''
+            echo "=== Nickel development shell ==="
+            echo "Info: Git hooks can be installed using \`pre-commit install\`"
+          '';
 
-        shellHook = (pre-commit-builder { inherit rust; isHermetic = false; }).shellHook + ''
-          echo "=== Nickel development shell ==="
-          echo "Info: Git hooks can be installed using \`pre-commit install\`"
-        '';
+          passthru = { inherit rust pre-commit; };
 
-        RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
-      };
+          RUST_SRC_PATH = "${rust}/lib/rustlib/src/rust/library";
+        };
 
       buildNickelWasm =
-        { rust ? mkRust { target = "wasm32-unknown-unknown"; }
+        { channel ? "stable"
         , optimize ? true
         }:
+        let
+          rust = mkRust {
+            inherit channel;
+            target = "wasm32-unknown-unknown";
+          };
+        in
         pkgs.stdenv.mkDerivation {
           name = "nickel-wasm-${version}";
 
@@ -368,7 +324,7 @@
       packages = {
         default = packages.build;
         build = buildNickel { };
-        buildWasm = buildNickelWasm { };
+        buildWasm = buildNickelWasm { optimize = true; };
         dockerImage = buildDocker packages.build; # TODO: docker image should be a passthru
         inherit vscodeExtension;
         inherit userManual;
@@ -380,18 +336,18 @@
       } // (forEachRustChannel
         (channel: {
           name = channel;
-          value = makeDevShell { rust = mkRust { inherit channel; rustProfile = "default"; }; };
+          value = buildNickel { inherit channel; isDevShell = true; };
         }
         ));
 
       checks = {
         # wasm-opt can take long: eschew optimizations in checks
-        wasm = buildNickelWasm { optimize = false; };
-        pre-commit = pre-commit-builder { isHermetic = true; };
+        wasm = buildNickelWasm { channel = "stable"; optimize = false; };
+        pre-commit = packages.default.pre-commit;
       } // (forEachRustChannel (channel:
         {
           name = "nickel-against-${channel}-rust-channel";
-          value = buildNickel { rust = mkRust { inherit channel; }; };
+          value = buildNickel { inherit channel; };
         }
       ));
     }
diff --git a/lsp/nls/src/files.rs b/lsp/nls/src/files.rs
index 18aec38c..b53d9902 100644
--- a/lsp/nls/src/files.rs
+++ b/lsp/nls/src/files.rs
@@ -97,7 +97,8 @@ fn parse_and_typecheck(server: &mut Server, uri: Url, file_id: FileId) -> Result
 
     let diagnostics = diagnostics
         .into_iter()
-        .flat_map(|d| lsp_types::Diagnostic::from_codespan(d, server.cache.files_mut()))
+        .map(|d| lsp_types::Diagnostic::from_codespan(d, server.cache.files_mut()))
+        .flatten()
         .collect();
 
     server.notify(lsp_server::Notification::new(
diff --git a/lsp/nls/src/linearization/building.rs b/lsp/nls/src/linearization/building.rs
index 42b728d4..54ea0be2 100644
--- a/lsp/nls/src/linearization/building.rs
+++ b/lsp/nls/src/linearization/building.rs
@@ -46,12 +46,12 @@ impl Building {
     }
 
     pub(super) fn inform_declaration(&mut self, declaration: ID, value: ID) {
-        if let Some(LinearizationItem {
-            kind: TermKind::Declaration(_, _, value_state),
-            ..
-        }) = self.linearization.get_mut(declaration)
-        {
-            *value_state = ValueState::Known(value)
+        match self.linearization.get_mut(declaration) {
+            Some(LinearizationItem {
+                kind: TermKind::Declaration(_, _, value_state),
+                ..
+            }) => *value_state = ValueState::Known(value),
+            _ => (),
         }
     }
 
@@ -71,7 +71,7 @@ impl Building {
                 ty: UnifType::Concrete(TypeF::Dyn),
                 kind: TermKind::RecordField {
                     record,
-                    ident: *ident,
+                    ident: ident.clone(),
                     usages: Vec::new(),
                     value: ValueState::Unknown,
                 },
@@ -83,8 +83,8 @@ impl Building {
                     _ => None,
                 },
             });
-            env.insert(*ident, id);
-            self.add_record_field(record, (*ident, id))
+            env.insert(ident.clone(), id);
+            self.add_record_field(record, (ident.clone(), id))
         }
     }
 
@@ -157,7 +157,7 @@ impl Building {
             }) = parent_referenced
             {
                 debug!("parent references deferred usage");
-                unresolved.push(deferred);
+                unresolved.push(deferred.clone());
                 continue;
             }
 
@@ -171,7 +171,7 @@ impl Building {
             }) = parent_declaration
             {
                 debug!("parent references deferred usage");
-                unresolved.push(deferred);
+                unresolved.push(deferred.clone());
                 continue;
             }
 
diff --git a/lsp/nls/src/linearization/interface.rs b/lsp/nls/src/linearization/interface.rs
index ed4212d2..aa7a2604 100644
--- a/lsp/nls/src/linearization/interface.rs
+++ b/lsp/nls/src/linearization/interface.rs
@@ -22,7 +22,7 @@ impl ResolutionState for Resolved {}
 /// 3. Records, listing their fields
 /// 4. wildcard (Structure) for any other kind of term.
 /// Can be extended later to represent Contracts, Records, etc.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum TermKind {
     Declaration(Ident, Vec<ID>, ValueState),
     Usage(UsageState),
@@ -36,7 +36,7 @@ pub enum TermKind {
     Structure,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum ValueState {
     Unknown,
     Known(ID),
@@ -52,7 +52,7 @@ impl ValueState {
 }
 /// Some usages cannot be fully resolved in a first pass (i.e. recursive record fields)
 /// In these cases we defer the resolution to a second pass during linearization
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum UsageState {
     Unbound,
     Resolved(ID),
diff --git a/lsp/nls/src/linearization/mod.rs b/lsp/nls/src/linearization/mod.rs
index b7a28a27..7bf8bcc3 100644
--- a/lsp/nls/src/linearization/mod.rs
+++ b/lsp/nls/src/linearization/mod.rs
@@ -120,7 +120,7 @@ impl Linearizer for AnalysisHost {
                     pos
                 });
 
-                if let Some(field) = lin.linearization.get_mut(record + offset) {
+                for field in lin.linearization.get_mut(record + offset).into_iter() {
                     debug!("{:?}", field.kind);
                     let usage_offset = if matches!(term, Term::Var(_)) {
                         debug!(
@@ -168,7 +168,7 @@ impl Linearizer for AnalysisHost {
                                 id: id_gen.get_and_advance(),
 
                                 ty: ty.clone(),
-                                pos: ident.pos,
+                                pos: ident.pos.clone(),
                                 kind: TermKind::Structure,
                                 meta: self.meta.take(),
                             });
@@ -227,7 +227,7 @@ impl Linearizer for AnalysisHost {
                             id: id_gen.get_and_advance(),
 
                             ty: ty.clone(),
-                            pos: ident.pos,
+                            pos: ident.pos.clone(),
                             kind: TermKind::Structure,
                             meta: self.meta.take(),
                         });
@@ -357,7 +357,7 @@ impl Linearizer for AnalysisHost {
             .iter()
             .filter_map(|item| match &item.kind {
                 TermKind::Usage(UsageState::Deferred { parent, child }) => {
-                    Some((item.id, *parent, *child))
+                    Some((item.id, *parent, child.clone()))
                 }
                 _ => None,
             })
@@ -387,7 +387,7 @@ impl Linearizer for AnalysisHost {
                 id_mapping.insert(*id, index);
             });
 
-        fn transform_wildcard(wildcars: &[Types], t: Types) -> Types {
+        fn transform_wildcard(wildcars: &Vec<Types>, t: Types) -> Types {
             match t {
                 Types(TypeF::Wildcard(i)) => wildcars.get(i).unwrap_or(&t).clone(),
                 _ => t,
diff --git a/lsp/nls/src/requests/completion.rs b/lsp/nls/src/requests/completion.rs
index 23f33dd4..3da92cb9 100644
--- a/lsp/nls/src/requests/completion.rs
+++ b/lsp/nls/src/requests/completion.rs
@@ -44,7 +44,7 @@ fn find_fields_from_term_kind(
             } else {
                 let name = path.pop()?;
                 let new_id = fields.get(&name)?;
-                find_fields_from_term_kind(linearization, *new_id, path)
+                find_fields_from_term_kind(&linearization, *new_id, path)
             }
         }
         TermKind::RecordField {
@@ -72,7 +72,7 @@ fn find_fields_from_contract(
         None => match item.kind {
             TermKind::Declaration(_, _, ValueState::Known(new_id))
             | TermKind::Usage(UsageState::Resolved(new_id)) => {
-                find_fields_from_contract(linearization, new_id, path)
+                find_fields_from_contract(&linearization, new_id, path)
             }
             _ => None,
         },
@@ -87,7 +87,7 @@ fn find_fields_from_meta_value(meta_value: &MetaValue, path: &mut Vec<Ident>) ->
         .iter()
         .chain(meta_value.types.iter())
         .flat_map(|contract| match &contract.types {
-            Types(TypeF::Record(row)) => find_fields_from_type(row, path),
+            Types(TypeF::Record(row)) => find_fields_from_type(&row, path),
             Types(TypeF::Flat(term)) => find_fields_from_term(term, path).unwrap_or_default(),
             _ => Vec::new(),
         })
@@ -115,7 +115,7 @@ fn find_fields_from_type(rrows: &RecordRows, path: &mut Vec<Ident>) -> Vec<Ident
         rrows
             .iter()
             .filter_map(|item| match item {
-                RecordRowsIteratorItem::Row(row) => Some(row.id),
+                RecordRowsIteratorItem::Row(row) => Some(row.id.clone()),
                 _ => None,
             })
             .collect::<Vec<_>>()
@@ -172,7 +172,7 @@ fn get_identifier_path(text: &str) -> Option<Vec<String>> {
             String::from(name)
         }
     }
-    Some(path.split('.').map(remove_quotes).collect())
+    Some(path.split(".").map(remove_quotes).collect())
 }
 
 /// Get the identifiers before `.<text>` for record completion.
@@ -187,7 +187,7 @@ fn get_identifiers_before_field(text: &str) -> Option<Vec<String>> {
         .rev()
         .collect();
 
-    get_identifier_path(text.as_str())
+    get_identifier_path(&text.as_str())
 }
 
 fn remove_duplicates(items: &Vec<CompletionItem>) -> Vec<CompletionItem> {
@@ -219,11 +219,11 @@ fn collect_record_info(
                     Some((find_fields_from_type(&rrows, path), item.ty.clone()))
                 }
                 (TermKind::Declaration(_, _, ValueState::Known(body_id)), _) if id == item.id => {
-                    match find_fields_from_contract(linearization, *body_id, path) {
+                    match find_fields_from_contract(&linearization, *body_id, path) {
                         // Get record fields from contract metadata
                         Some(fields) => Some((fields, item.ty.clone())),
                         // Get record fields from lexical scoping
-                        None => find_fields_from_term_kind(linearization, id, path)
+                        None => find_fields_from_term_kind(&linearization, id, path)
                             .map(|idents| (idents, item.ty.clone())),
                     }
                 }
@@ -280,7 +280,7 @@ fn get_completion_identifiers(
                     .iter()
                     .filter_map(|i| match i.kind {
                         TermKind::Declaration(ref ident, _, _) => {
-                            Some((vec![*ident], i.ty.clone()))
+                            Some((vec![ident.clone()], i.ty.clone()))
                         }
                         _ => None,
                     })
@@ -339,10 +339,12 @@ pub fn handle_completion(
         Some(item) => {
             debug!("found closest item: {:?}", item);
 
-            let trigger = params
-                .context
-                .as_ref()
-                .and_then(|context| context.trigger_character.as_deref());
+            let trigger = params.context.as_ref().and_then(|context| {
+                context
+                    .trigger_character
+                    .as_ref()
+                    .map(|trigger| trigger.as_str())
+            });
 
             let in_scope =
                 get_completion_identifiers(&text[..start], trigger, linearization, item)?;
diff --git a/lsp/nls/src/requests/goto.rs b/lsp/nls/src/requests/goto.rs
index 96ac6c7f..401f08f5 100644
--- a/lsp/nls/src/requests/goto.rs
+++ b/lsp/nls/src/requests/goto.rs
@@ -45,7 +45,7 @@ pub fn handle_to_definition(
 
     let item = linearization.item_at(&locator);
 
-    if item.is_none() {
+    if item == None {
         server.reply(Response::new_ok(id, Value::Null));
         return Ok(());
     }
@@ -57,18 +57,19 @@ pub fn handle_to_definition(
     let location = match item.kind {
         TermKind::Usage(UsageState::Resolved(usage_id)) => {
             let definition = linearization.get_item(usage_id).unwrap();
-            let RawSpan {
-                start: ByteIndex(start),
-                end: ByteIndex(end),
-                src_id,
-            } = definition.pos.unwrap();
-            let location = Location {
-                uri: Url::parse(&server.cache.name(src_id).to_string_lossy()).unwrap(),
-                range: Range::from_codespan(
-                    &src_id,
-                    &(start as usize..end as usize),
-                    server.cache.files(),
-                ),
+            let location = match definition.pos.unwrap() {
+                RawSpan {
+                    start: ByteIndex(start),
+                    end: ByteIndex(end),
+                    src_id,
+                } => Location {
+                    uri: Url::parse(&server.cache.name(src_id).to_string_lossy()).unwrap(),
+                    range: Range::from_codespan(
+                        &src_id,
+                        &(start as usize..end as usize),
+                        server.cache.files(),
+                    ),
+                },
             };
             Some(location)
         }
@@ -107,7 +108,7 @@ pub fn handle_to_usages(
 
     let item = linearization.item_at(&locator);
 
-    if item.is_none() {
+    if item == None {
         server.reply(Response::new_ok(id, Value::Null));
         return Ok(());
     }
diff --git a/lsp/nls/src/requests/hover.rs b/lsp/nls/src/requests/hover.rs
index 7425a176..b27fa63b 100644
--- a/lsp/nls/src/requests/hover.rs
+++ b/lsp/nls/src/requests/hover.rs
@@ -44,7 +44,7 @@ pub fn handle(
 
     Trace::enrich(&id, linearization);
 
-    if item.is_none() {
+    if item == None {
         server.reply(Response::new_ok(id, Value::Null));
         return Ok(());
     }
diff --git a/lsp/nls/src/server.rs b/lsp/nls/src/server.rs
index 49305b61..cf3f5bb8 100644
--- a/lsp/nls/src/server.rs
+++ b/lsp/nls/src/server.rs
@@ -25,7 +25,7 @@ use crate::{
     trace::Trace,
 };
 
-pub const DOT_COMPL_TRIGGER: &str = ".";
+pub const DOT_COMPL_TRIGGER: &'static str = ".";
 
 pub struct Server {
     pub connection: Connection,
diff --git a/src/cache.rs b/src/cache.rs
index a509734d..bc8650fc 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -202,7 +202,7 @@ impl<E> CacheError<E> {
 /// retrieved from the cache.
 ///
 /// See [ImportResolver::resolve].
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq)]
 pub enum ResolvedTerm {
     FromFile {
         path: PathBuf, /* the loaded path */
@@ -917,7 +917,7 @@ impl Cache {
     /// Generate the initial typing context from the list of `file_ids` corresponding to the
     /// standard library parts.
     pub fn mk_type_ctxt(&self) -> Result<typecheck::Context, CacheError<Void>> {
-        let stdlib_terms_vec: Vec<RichTerm> =
+        let stdlib_terms_vec =
             self.stdlib_ids
                 .as_ref()
                 .map_or(Err(CacheError::NotParsed), |ids| {
@@ -1030,7 +1030,7 @@ impl ImportResolver for Cache {
         };
 
         self.parse_multi(file_id, format)
-            .map_err(|err| ImportError::ParseErrors(err, *pos))?;
+            .map_err(|err| ImportError::ParseErrors(err.into(), *pos))?;
 
         Ok((ResolvedTerm::FromFile { path: path_buf }, file_id))
     }
diff --git a/src/deserialize.rs b/src/deserialize.rs
index 88051126..af372688 100644
--- a/src/deserialize.rs
+++ b/src/deserialize.rs
@@ -47,7 +47,7 @@ macro_rules! deserialize_number_round {
 }
 
 /// An error occurred during deserialization to Rust.
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum RustDeserializationError {
     InvalidType { expected: String, occurred: String },
     MissingValue,
diff --git a/src/environment.rs b/src/environment.rs
index 008f7baf..49e15fc4 100644
--- a/src/environment.rs
+++ b/src/environment.rs
@@ -101,7 +101,7 @@ impl<K: Hash + Eq, V: PartialEq> Environment<K, V> {
         let mut env: Vec<NonNull<HashMap<K, V>>> = self
             .iter_layers()
             // SAFETY: all NonNull::new_unchecked comes from pointers created from Rc, so cannot be null
-            .map(|hmap| unsafe { NonNull::new_unchecked(Rc::as_ptr(hmap) as *mut _) })
+            .map(|hmap| unsafe { NonNull::new_unchecked(Rc::as_ptr(&hmap) as *mut _) })
             .collect();
         // SAFETY: by design, env cannot be empty, and coming from an Rc, it is well aligned and initialized
         let current_map = unsafe { env.pop().unwrap().as_ref() }.iter();
@@ -297,7 +297,7 @@ mod tests {
         env_base.insert(1, 'a');
         assert_eq!(env_base.iter_layers().count(), 1);
         assert_eq!(env_base.iter_layers().next().unwrap().get(&1), Some(&'a'));
-        assert_eq!(env_base.iter_layers().nth(1), None);
+        assert_eq!(env_base.iter_layers().skip(1).next(), None);
         let _ = env_base.clone();
         assert_eq!(env_base.iter_layers().count(), 1);
         env_base.insert(2, 'b');
@@ -336,8 +336,8 @@ mod tests {
         env2.insert(1, 'y');
         assert_eq!(env_base.iter_elems().count(), 3);
         assert_eq!(env2.iter_elems().count(), 3);
-        assert_eq!(env_base.iter_elems().nth(2), Some((&1, &'z')));
-        assert_eq!(env2.iter_elems().nth(2), Some((&1, &'y')));
+        assert_eq!(env_base.iter_elems().skip(2).next(), Some((&1, &'z')));
+        assert_eq!(env2.iter_elems().skip(2).next(), Some((&1, &'y')));
 
         let mut iter_elems_base = env_base.iter_elems();
         let _ = iter_elems_base.next();
diff --git a/src/error.rs b/src/error.rs
index d12592d0..38eef58a 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -208,7 +208,7 @@ pub enum TypecheckError {
     ),
 }
 
-#[derive(Debug, PartialEq, Eq, Clone, Default)]
+#[derive(Debug, PartialEq, Clone, Default)]
 pub struct ParseErrors {
     pub errors: Vec<ParseError>,
 }
@@ -273,7 +273,7 @@ impl ToDiagnostic<FileId> for ParseErrors {
 }
 
 /// An error occurring during parsing.
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum ParseError {
     /// Unexpected end of file.
     UnexpectedEOF(FileId, /* tokens expected by the parser */ Vec<String>),
@@ -320,7 +320,7 @@ pub enum ParseError {
 }
 
 /// An error occurring during the resolution of an import.
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum ImportError {
     /// An IO error occurred during an import.
     IOError(
@@ -348,11 +348,11 @@ pub enum SerializationError {
 }
 
 /// A general I/O error, occurring when reading a source file or writing an export.
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub struct IOError(pub String);
 
 /// An error occurring during an REPL session.
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum ReplError {
     UnknownCommand(String),
     MissingArg {
@@ -1269,7 +1269,7 @@ impl ToDiagnostic<FileId> for TypecheckError {
             TypecheckError::UnboundIdentifier(ident, pos_opt) =>
             // Use the same diagnostic as `EvalError::UnboundIdentifier` for consistency.
                 {
-                    EvalError::UnboundIdentifier(*ident, *pos_opt)
+                    EvalError::UnboundIdentifier(ident.clone(), *pos_opt)
                         .to_diagnostic(files, contract_id)
                 }
             TypecheckError::MissingRow(ident, expd, actual, span_opt) =>
@@ -1365,7 +1365,7 @@ impl ToDiagnostic<FileId> for TypecheckError {
                 // nested field (e.g. `pkg.subpkg1.meta.url`) and only show once the row mismatch
                 // error followed by the underlying error.
                 let mut err = (*err_).clone();
-                let mut path = vec![*ident];
+                let mut path = vec![ident.clone()];
 
                 while let TypecheckError::RowMismatch(id_next, _, _, next, _) = *err {
                     path.push(id_next);
diff --git a/src/eval/callstack.rs b/src/eval/callstack.rs
index ed98561a..3d2de11c 100644
--- a/src/eval/callstack.rs
+++ b/src/eval/callstack.rs
@@ -9,7 +9,7 @@ use crate::{
 use codespan::FileId;
 
 /// A call stack, saving the history of function calls.
-#[derive(PartialEq, Eq, Clone, Default, Debug)]
+#[derive(PartialEq, Clone, Default, Debug)]
 pub struct CallStack(pub Vec<StackElem>);
 
 /// Basic description of a function call. Used for error reporting.
@@ -181,7 +181,7 @@ impl CallStack {
                         Some(CallDescr {
                             head: ref mut head @ None,
                             span: span_call,
-                        }) if pos.unwrap() <= *span_call => *head = Some(*id),
+                        }) if pos.unwrap() <= *span_call => *head = Some(id.clone()),
                         _ => (),
                     };
                 }
@@ -219,11 +219,6 @@ impl CallStack {
         self.0.len()
     }
 
-    /// Return whether the callstack is empty.
-    pub fn is_empty(&self) -> bool {
-        self.0.is_empty()
-    }
-
     /// Truncate the callstack at a certain size. Used e.g. to quickly drop the elements introduced
     /// during the strict evaluation of the operand of a primitive operator. Wrapper for
     /// `callstack.0.truncate(len)`.
diff --git a/src/eval/fixpoint.rs b/src/eval/fixpoint.rs
index e5ad8530..843407ef 100644
--- a/src/eval/fixpoint.rs
+++ b/src/eval/fixpoint.rs
@@ -15,8 +15,8 @@ pub fn rec_env<'a, I: Iterator<Item = (&'a Ident, &'a RichTerm)>>(
                 let thunk = env
                     .get(var_id)
                     .cloned()
-                    .ok_or(EvalError::UnboundIdentifier(*var_id, rt.pos))?;
-                Ok((*id, thunk))
+                    .ok_or_else(|| EvalError::UnboundIdentifier(var_id.clone(), rt.pos))?;
+                Ok((id.clone(), thunk))
             }
             _ => {
                 // If we are in this branch, `rt` must be a constant after the share normal form
@@ -26,7 +26,7 @@ pub fn rec_env<'a, I: Iterator<Item = (&'a Ident, &'a RichTerm)>>(
                     body: rt.clone(),
                     env: Environment::new(),
                 };
-                Ok((*id, Thunk::new(closure, IdentKind::Let)))
+                Ok((id.clone(), Thunk::new(closure, IdentKind::Let)))
             }
         })
         .collect()
@@ -48,7 +48,7 @@ pub fn patch_field(
         let mut thunk = env
             .get(var_id)
             .cloned()
-            .ok_or(EvalError::UnboundIdentifier(*var_id, rt.pos))?;
+            .ok_or_else(|| EvalError::UnboundIdentifier(var_id.clone(), rt.pos))?;
 
         let deps = thunk.deps();
 
diff --git a/src/eval/lazy.rs b/src/eval/lazy.rs
index 9b0cebf0..f1501fbb 100644
--- a/src/eval/lazy.rs
+++ b/src/eval/lazy.rs
@@ -34,9 +34,72 @@ pub struct ThunkData {
 /// - A standard thunk, that is destructively updated once and for all
 /// - A revertible thunk, that can be restored to its original expression. Used to implement
 ///   recursive merging of records and overriding (see the
-///   [RFC overriding](https://github.com/tweag/nickel/pull/330)). A revertible thunks optionally
-///   stores the set of recusive fields it depends on. See the [`crate::transform::free_vars`] for more
-///   details.
+///   [RFC overriding](https://github.com/tweag/nickel/pull/330)). A revertible thunk optionally
+///   stores the set of recursive fields it depends on (see [`crate::transform::free_vars`]).
+///
+/// # Revertible thunks
+///
+/// Recursive records are better understood as being functions. For example, the recursive record:
+///
+/// ```nickel
+/// {
+///   foo = bar + baz + 1,
+///   bar = 2,
+///   baz = 1,
+/// }
+/// ```
+///
+/// is really a builtin representation of `fun self => {foo = self.bar + self.baz + 1, bar = 2, baz
+/// = 1}`. The interpreter computes a fixpoint when evaluating a `RecRecord` away to a `Record`,
+/// equivalent to `let rec fixpoint = repr fixpoint in fixpoint`. We use a different vocabulary and
+/// representation, but this is what we essentially do. We're just hiding the details from the
+/// user and pretend recursive records are data that can be used as a normal record, because it's
+/// both simple and natural (for the user).
+///
+/// A naive representation of recursive records could be to always keep recursive records in their
+/// functional representation, and re-compute the fixpoint at each record operation, such as field
+/// access. While correct, this would be wasteful, because the result of the fixpoint computations
+/// is actually the same throughout most record operations (put differently, `self` doesn't
+/// change). But we can't just store the computed `fixpoint` in a normal thunk and forget
+/// about the original function, because upon merge, and more specifically upon recursive
+/// overriding, we do need to compute a new fixpoint.
+///
+/// Revertible thunks are a **memoization device** for the `fun self => ...` functional
+/// representation of a recursive record. First, note that there are several different but
+/// equivalent ways of encoding the example above as a function:
+///
+/// ```nickel
+/// fun self => { foo = self.bar + self.baz + 1, bar = 2, baz = 1 }
+///
+/// # pushing the function down the fields
+/// {foo = fun self => self.bar + self.baz + 1, bar = fun self => 2, baz = fun self => 1}
+///
+/// # removing unused arguments
+/// {foo = fun self => self.bar + self.baz + 1, bar = 2, baz = 1}
+///
+/// # splitting the self argument
+/// {foo = fun bar baz => bar + baz + 1, bar = 2, bar 1}
+/// ```
+///
+/// All those representations are isomorphic, in that, modulo basic free variable analysis, we can
+/// get from one to another mechanically. Pushing the functions down the fields is better because
+/// it reveals more static information about the record (such as the list of fields) without having
+/// to provide any argument first. Then, splitting `self` and removing the function altogether when
+/// the field doesn't depend on other fields is more precise with respect to dependency tracking
+/// (this has an important positive memory impact, see [ThunkDeps]).
+///
+/// In practice, Nickel uses the last representation. Fields that don't have any free variable
+/// intersecting with the name of the other fields are allocated as normal thunks that will only be
+/// evaluated at most once. Fields with actual dependencies are stored in revertible thunks, where
+/// the `orig` field stores the body of the function (in our example the expression `bar + baz +
+/// 1`), and the cached version is the application to `self` computed when we evaluated the
+/// recursive record to a normal record. The function arguments (that is, the intersection of the
+/// free variables of the expression with the set of fields) are stored inside `deps`. `deps`
+/// represent the other fields a specific field is depending on, and are guaranteed to appear free
+/// inside `orig`.
+///
+/// In fact, unless there is an unbound identifier, `deps` should be exactly the set of free
+/// variables of `orig`.
 #[derive(Clone, Debug, PartialEq)]
 pub enum InnerThunkData {
     Standard(Closure),
diff --git a/src/eval/merge.rs b/src/eval/merge.rs
index 61878d36..af564a7d 100644
--- a/src/eval/merge.rs
+++ b/src/eval/merge.rs
@@ -321,10 +321,6 @@ pub fn merge(
                 .into_iter()
                 .map(|ctr| ctr.closurize(&mut env, env1.clone()))
                 .collect();
-            // Clippy is wrong to complain about the useless `collect` here:
-            // It is necessary to release the mutable borrow on `env`
-            // See https://github.com/rust-lang/rust-clippy/issues/7526
-            #[allow(clippy::needless_collect)]
             let contracts2: Vec<Contract> = contracts2
                 .into_iter()
                 .map(|ctr| ctr.closurize(&mut env, env2.clone()))
@@ -384,11 +380,7 @@ pub fn merge(
             let m1_values: Vec<_> = r1.fields.values().cloned().collect();
             let m2_values: Vec<_> = r2.fields.values().cloned().collect();
 
-            let hashmap::SplitResult {
-                left,
-                center,
-                right,
-            } = hashmap::split(r1.fields, r2.fields);
+            let (left, center, right) = hashmap::split(r1.fields, r2.fields);
 
             match mode {
                 MergeMode::Contract(mut lbl) if !r2.attrs.open && !left.is_empty() => {
@@ -535,16 +527,13 @@ fn rev_thunks<'a, I: Iterator<Item = &'a mut RichTerm>>(map: I, env: &mut Enviro
 pub mod hashmap {
     use std::collections::HashMap;
 
-    pub struct SplitResult<K, V1, V2> {
-        pub left: HashMap<K, V1>,
-        pub center: HashMap<K, (V1, V2)>,
-        pub right: HashMap<K, V2>,
-    }
-
     /// Split two hashmaps m1 and m2 in three parts (left,center,right), where left holds bindings
     /// `(key,value)` where key is not in `m2.keys()`, right is the dual (keys of m2 that are not
     /// in m1), and center holds bindings for keys that are both in m1 and m2.
-    pub fn split<K, V1, V2>(m1: HashMap<K, V1>, m2: HashMap<K, V2>) -> SplitResult<K, V1, V2>
+    pub fn split<K, V1, V2>(
+        m1: HashMap<K, V1>,
+        m2: HashMap<K, V2>,
+    ) -> (HashMap<K, V1>, HashMap<K, (V1, V2)>, HashMap<K, V2>)
     where
         K: std::hash::Hash + Eq,
     {
@@ -560,11 +549,7 @@ pub mod hashmap {
             }
         }
 
-        SplitResult {
-            left,
-            center,
-            right,
-        }
+        (left, center, right)
     }
 
     #[cfg(test)]
@@ -577,11 +562,7 @@ pub mod hashmap {
             let m2 = HashMap::<isize, isize>::new();
 
             m1.insert(1, 1);
-            let SplitResult {
-                mut left,
-                center,
-                right,
-            } = split(m1, m2);
+            let (mut left, center, right) = split(m1, m2);
 
             if left.remove(&1) == Some(1)
                 && left.is_empty()
@@ -600,11 +581,7 @@ pub mod hashmap {
             let mut m2 = HashMap::new();
 
             m2.insert(1, 1);
-            let SplitResult {
-                left,
-                center,
-                mut right,
-            } = split(m1, m2);
+            let (left, center, mut right) = split(m1, m2);
 
             if right.remove(&1) == Some(1)
                 && right.is_empty()
@@ -626,11 +603,7 @@ pub mod hashmap {
 
             m1.insert(1, 1);
             m2.insert(1, 2);
-            let SplitResult {
-                left,
-                mut center,
-                right,
-            } = split(m1, m2);
+            let (left, mut center, right) = split(m1, m2);
 
             if center.remove(&1) == Some((1, 2))
                 && center.is_empty()
@@ -654,11 +627,7 @@ pub mod hashmap {
             m1.insert(2, 1);
             m2.insert(1, -1);
             m2.insert(3, -1);
-            let SplitResult {
-                mut left,
-                mut center,
-                mut right,
-            } = split(m1, m2);
+            let (mut left, mut center, mut right) = split(m1, m2);
 
             if left.remove(&2) == Some(1)
                 && center.remove(&1) == Some((1, -1))
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 5b5ce842..c5286b7c 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -316,7 +316,7 @@ impl<R: ImportResolver> VirtualMachine<R> {
                         .get(x)
                         .or_else(|| initial_env.get(x))
                         .cloned()
-                        .ok_or(EvalError::UnboundIdentifier(*x, pos))?;
+                        .ok_or_else(|| EvalError::UnboundIdentifier(x.clone(), pos))?;
                     std::mem::drop(env); // thunk may be a 1RC pointer
 
                     if thunk.state() != ThunkState::Evaluated {
@@ -336,7 +336,8 @@ impl<R: ImportResolver> VirtualMachine<R> {
                             thunk.set_evaluated();
                         }
                     }
-                    self.call_stack.enter_var(thunk.ident_kind(), *x, pos);
+                    self.call_stack
+                        .enter_var(thunk.ident_kind(), x.clone(), pos);
                     thunk.into_closure()
                 }
                 Term::App(t1, t2) => {
@@ -371,10 +372,10 @@ impl<R: ImportResolver> VirtualMachine<R> {
                     // Patch the environment with the (x <- closure) binding
                     if *rec {
                         let thunk_ = thunk.clone();
-                        thunk.borrow_mut().env.insert(*x, thunk_);
+                        thunk.borrow_mut().env.insert(x.clone(), thunk_);
                     }
 
-                    env.insert(*x, thunk);
+                    env.insert(x.clone(), thunk);
                     Closure {
                         body: t.clone(),
                         env,
@@ -668,13 +669,13 @@ impl<R: ImportResolver> VirtualMachine<R> {
                 Term::Fun(x, t) => {
                     if let Some((thunk, pos_app)) = self.stack.pop_arg_as_thunk() {
                         self.call_stack.enter_fun(pos_app);
-                        env.insert(*x, thunk);
+                        env.insert(x.clone(), thunk);
                         Closure {
                             body: t.clone(),
                             env,
                         }
                     } else {
-                        return Ok((RichTerm::new(Term::Fun(*x, t.clone()), pos), env));
+                        return Ok((RichTerm::new(Term::Fun(x.clone(), t.clone()), pos), env));
                     }
                 }
                 // Otherwise, this is either an ill-formed application, or we are done
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index dbe2435c..9e852ebb 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -500,7 +500,7 @@ impl<R: ImportResolver> VirtualMachine<R> {
                             .into_iter()
                             // Ignore optional fields without definitions.
                             .filter_map(|(_, t)| {
-                                (!is_empty_optional(&t, &env)).then_some(t)
+                                (!is_empty_optional(&t, &env)).then(|| t)
                             })
                             .collect();
                         Ok(Closure {
@@ -621,10 +621,10 @@ impl<R: ImportResolver> VirtualMachine<R> {
                             // As for `ArrayMap` (see above), we closurize the content of fields
                             let record = record.map_fields_without_optionals(&env, |id, t| {
                                 let pos = t.pos.into_inherited();
-
-                                mk_app!(f_as_var.clone(), mk_term::string(id.label()), t)
+                                let map_app = mk_app!(f_as_var.clone(), mk_term::string(id.label()), t)
                                     .closurize(&mut shared_env, env.clone())
-                                    .with_pos(pos)
+                                    .with_pos(pos);
+                                map_app
                             });
 
                             Ok(Closure {
@@ -1024,7 +1024,7 @@ impl<R: ImportResolver> VirtualMachine<R> {
 
                     let param = Ident::fresh();
                     let matcher = Term::Fun(
-                        param,
+                        param.clone(),
                         RichTerm::new(
                             Term::Op1(
                                 UnaryOp::StrIsMatchCompiled(re.into()),
@@ -1051,7 +1051,7 @@ impl<R: ImportResolver> VirtualMachine<R> {
 
                     let param = Ident::fresh();
                     let matcher = Term::Fun(
-                        param,
+                        param.clone(),
                         RichTerm::new(
                             Term::Op1(
                                 UnaryOp::StrMatchCompiled(re.into()),
@@ -1147,14 +1147,14 @@ impl<R: ImportResolver> VirtualMachine<R> {
 
                             let record = record.map_fields_without_optionals(&env, |id, t| {
                                 let stack_elem = Some(callstack::StackElem::Field {
-                                    id,
+                                    id: id.clone(),
                                     pos_record: pos,
                                     pos_field: t.pos,
                                     pos_access: pos_op,
                                 });
-
-                                mk_term::op1(UnaryOp::Force(stack_elem), t)
-                                    .closurize(&mut shared_env, env.clone())
+                                let forced = mk_term::op1(UnaryOp::Force(stack_elem), t)
+                                    .closurize(&mut shared_env, env.clone());
+                                forced
                             });
 
                             let terms = record.fields.clone().into_values();
@@ -2009,7 +2009,7 @@ impl<R: ImportResolver> VirtualMachine<R> {
 
                                 let mut env = env1.clone();
                                 // TODO: Is there a cheaper way to "merge" two environements?
-                                env.extend(env2.iter_elems().map(|(k, v)| (*k, v.clone())));
+                                env.extend(env2.iter_elems().map(|(k, v)| (k.clone(), v.clone())));
 
                                 // We have two sets of contracts from the LHS and RHS arrays.
                                 // - Common contracts between the two sides can be put into
@@ -2614,7 +2614,7 @@ impl PushPriority {
     fn push_into_record(&self, record: RecordData, env: &Environment, pos: TermPos) -> Closure {
         let mut new_env = Environment::new();
 
-        let record = record.map_fields_without_optionals(env, |_, rt| {
+        let record = record.map_fields_without_optionals(&env, |_, rt| {
             // There is a subtlety with respect to overriding here. Take:
             //
             // ```nickel
@@ -2664,7 +2664,7 @@ impl PushPriority {
             };
 
             let fresh_id = Ident::fresh();
-            new_env.insert(fresh_id, thunk);
+            new_env.insert(fresh_id.clone(), thunk);
             RichTerm::new(Term::Var(fresh_id), pos)
         });
 
@@ -2813,15 +2813,11 @@ fn eq(env: &mut Environment, c1: Closure, c2: Closure) -> EqResult {
         (Term::SealingKey(s1), Term::SealingKey(s2)) => EqResult::Bool(s1 == s2),
         (Term::Enum(id1), Term::Enum(id2)) => EqResult::Bool(id1 == id2),
         (Term::Record(r1), Term::Record(r2)) => {
-            let merge::hashmap::SplitResult {
-                left,
-                center,
-                right,
-            } = merge::hashmap::split(r1.fields, r2.fields);
+            let (left, center, right) = merge::hashmap::split(r1.fields, r2.fields);
 
             // As for other record operations, we ignore optional fields without a definition.
-            if !left.values().all(|rt| is_empty_optional(rt, &env1))
-                || !right.values().all(|rt| is_empty_optional(rt, &env2))
+            if !left.values().all(|rt| is_empty_optional(&rt, &env1))
+                || !right.values().all(|rt| is_empty_optional(&rt, &env2))
             {
                 EqResult::Bool(false)
             } else if center.is_empty() {
diff --git a/src/identifier.rs b/src/identifier.rs
index 68687368..1f7e5792 100644
--- a/src/identifier.rs
+++ b/src/identifier.rs
@@ -89,12 +89,6 @@ where
     }
 }
 
-// False-positive Clippy error: if we apply this suggestion,
-// we end up with an implementation of `From<Ident> for String`.
-// Then setting `F = Ident` in the implementation above gives
-// `From<Ident> for Ident` which is incoherent with the
-// blanket implementation of `From<T> for T`.
-#[allow(clippy::from_over_into)]
 impl Into<String> for Ident {
     fn into(self) -> String {
         self.into_label()
diff --git a/src/label.rs b/src/label.rs
index 85f9dd19..b30ee72f 100644
--- a/src/label.rs
+++ b/src/label.rs
@@ -48,7 +48,7 @@ pub mod ty_path {
     };
 
     /// An element of a path type.
-    #[derive(Debug, Clone, PartialEq, Eq)]
+    #[derive(Debug, Clone, PartialEq)]
     pub enum Elem {
         Domain,
         Codomain,
diff --git a/src/parser/error.rs b/src/parser/error.rs
index e858788b..ab79be26 100644
--- a/src/parser/error.rs
+++ b/src/parser/error.rs
@@ -1,6 +1,6 @@
 use crate::{identifier::Ident, position::RawSpan};
 
-#[derive(Clone, PartialEq, Eq, Debug)]
+#[derive(Clone, PartialEq, Debug)]
 pub enum LexicalError {
     /// A closing brace '}' does not match an opening brace '{'.
     UnmatchedCloseBrace(usize),
@@ -12,7 +12,7 @@ pub enum LexicalError {
     Generic(usize, usize),
 }
 
-#[derive(Clone, PartialEq, Eq, Debug)]
+#[derive(Clone, PartialEq, Debug)]
 pub enum ParseError {
     /// A specific lexical error
     Lexical(LexicalError),
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index 2d02231e..b518def8 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -322,7 +322,7 @@ pub const KEYWORDS: &[&str] = &[
 ];
 
 /// The tokens in string mode.
-#[derive(Logos, Debug, PartialEq, Eq, Clone)]
+#[derive(Logos, Debug, PartialEq, Clone)]
 pub enum StringToken<'input> {
     #[error]
     Error,
@@ -344,7 +344,7 @@ pub enum StringToken<'input> {
 }
 
 /// The tokens in multiline string mode.
-#[derive(Logos, Debug, PartialEq, Eq, Clone)]
+#[derive(Logos, Debug, PartialEq, Clone)]
 pub enum MultiStringToken<'input> {
     #[error]
     Error,
diff --git a/src/parser/uniterm.rs b/src/parser/uniterm.rs
index 68d96d85..f1ee4e1c 100644
--- a/src/parser/uniterm.rs
+++ b/src/parser/uniterm.rs
@@ -390,7 +390,7 @@ impl VarKindCell {
 
     /// Return the current var_kind.
     pub fn var_kind(&self) -> VarKind {
-        self.0.borrow().var_kind
+        (*self.0.borrow()).var_kind
     }
 }
 
@@ -473,8 +473,8 @@ impl FixTypeVars for Types {
             | TypeF::Flat(_)
             | TypeF::Wildcard(_) => (),
             TypeF::Arrow(ref mut s, ref mut t) => {
-                (*s).fix_type_vars_env(bound_vars.clone());
-                (*t).fix_type_vars_env(bound_vars);
+                (&mut *s).fix_type_vars_env(bound_vars.clone());
+                (&mut *t).fix_type_vars_env(bound_vars);
             }
             TypeF::Var(ref mut id) => {
                 if let Some(cell) = bound_vars.get(id) {
@@ -498,14 +498,14 @@ impl FixTypeVars for Types {
                 // We span a new VarKindCell and put it in the environment. The recursive calls to
                 // fix_type_vars will fill this cell with the correct kind, which we get afterwards
                 // to set the right value for `var_kind`.
-                bound_vars.insert(*var, VarKindCell::new());
-                (*body).fix_type_vars_env(bound_vars.clone());
+                bound_vars.insert(var.clone(), VarKindCell::new());
+                (&mut *body).fix_type_vars_env(bound_vars.clone());
                 // unwrap(): we just inseted a value for `var` above, and environment can never
                 // delete values.
                 *var_kind = bound_vars.get(var).unwrap().var_kind();
             }
             TypeF::Dict(ref mut ty) | TypeF::Array(ref mut ty) => {
-                (*ty).fix_type_vars_env(bound_vars)
+                (&mut *ty).fix_type_vars_env(bound_vars)
             }
             TypeF::Enum(ref mut erows) => erows.fix_type_vars_env(bound_vars),
             TypeF::Record(ref mut rrows) => rrows.fix_type_vars_env(bound_vars),
diff --git a/src/pretty.rs b/src/pretty.rs
index a67f0cbe..8beeff23 100644
--- a/src/pretty.rs
+++ b/src/pretty.rs
@@ -402,7 +402,8 @@ where
                 .text("let")
                 .append(allocator.space())
                 .append(
-                    (*opt_id)
+                    opt_id
+                        .clone()
                         .map(|id| {
                             allocator.as_string(id).append(if dst.is_empty() {
                                 allocator.nil()
diff --git a/src/repl/mod.rs b/src/repl/mod.rs
index 325c690b..c48027a9 100644
--- a/src/repl/mod.rs
+++ b/src/repl/mod.rs
@@ -150,7 +150,7 @@ impl ReplImpl {
             if let Some(id) = id {
                 typecheck::env_add(
                     &mut repl_impl.env.type_ctxt.type_env,
-                    id,
+                    id.clone(),
                     &t,
                     &repl_impl.env.type_ctxt.term_env,
                     repl_impl.vm.import_resolver(),
@@ -192,9 +192,9 @@ impl ReplImpl {
                 Ok(eval_function(&mut self.vm, t, &self.env.eval_env)?.into())
             }
             ExtendedTerm::ToplevelLet(id, t) => {
-                let t = prepare(self, Some(id), t)?;
+                let t = prepare(self, Some(id.clone()), t)?;
                 let local_env = self.env.eval_env.clone();
-                eval::env_add(&mut self.env.eval_env, id, t, local_env);
+                eval::env_add(&mut self.env.eval_env, id.clone(), t, local_env);
                 Ok(EvalResult::Bound(id))
             }
         }
@@ -272,7 +272,7 @@ impl Repl for ReplImpl {
         // We need to `traverse` the term, in case the type depends on inner terms that also contain wildcards
         let term = term
             .traverse(
-                &|rt: RichTerm, _| -> Result<RichTerm, std::convert::Infallible> {
+                &mut |rt: RichTerm, _| -> Result<RichTerm, std::convert::Infallible> {
                     Ok(transform::substitute_wildcards::transform_one(
                         rt, &wildcards,
                     ))
diff --git a/src/repl/simple_frontend.rs b/src/repl/simple_frontend.rs
index bf3781cf..ac2857fe 100644
--- a/src/repl/simple_frontend.rs
+++ b/src/repl/simple_frontend.rs
@@ -99,7 +99,7 @@ pub fn serialize<R: Repl>(
 ) -> Result<InputResult, InputError> {
     repl.eval_full(input)
         .and_then(|eval_res| match eval_res {
-            EvalResult::Evaluated(t) => serialize::to_string(format, &t)
+            EvalResult::Evaluated(t) => serialize::to_string(format, &t.into())
                 .map(InputResult::Success)
                 .map_err(Error::from),
             EvalResult::Bound(_) => Ok(InputResult::Success(String::new())),
diff --git a/src/repl/wasm_frontend.rs b/src/repl/wasm_frontend.rs
index 05e41999..5dc3a9b6 100644
--- a/src/repl/wasm_frontend.rs
+++ b/src/repl/wasm_frontend.rs
@@ -162,7 +162,7 @@ impl WasmInitResult {
     /// Make a `WasmInitResult` result from an `InputError`.
     fn error(mut state: ReplState, error: InputError) -> Self {
         WasmInitResult {
-            msg: err_to_string(state.0.cache_mut(), &error),
+            msg: err_to_string(&mut state.0.cache_mut(), &error),
             tag: WasmResultTag::Error,
             state,
         }
@@ -209,13 +209,13 @@ impl WasmInputResult {
         WasmInputResult {
             msg,
             tag: WasmResultTag::Error,
-            errors: serde_wasm_bindgen::to_value(&errors).unwrap(),
+            errors: JsValue::from_serde(&errors).unwrap(),
         }
     }
 
     /// Generate a serializable empty list.
     fn empty_errors() -> JsValue {
-        serde_wasm_bindgen::to_value(&Vec::<WasmErrorDiagnostic>::new()).unwrap()
+        JsValue::from_serde(&Vec::<WasmErrorDiagnostic>::new()).unwrap()
     }
 }
 
@@ -271,14 +271,14 @@ impl TryInto<ExportFormat> for WasmExportFormat {
 }
 
 /// Render error diagnostics as a string.
-pub fn diags_to_string(cache: &mut Cache, diags: &[Diagnostic<FileId>]) -> String {
+pub fn diags_to_string(cache: &mut Cache, diags: &Vec<Diagnostic<FileId>>) -> String {
     let mut buffer = Ansi::new(Cursor::new(Vec::new()));
     let config = codespan_reporting::term::Config::default();
 
     diags
         .iter()
         .try_for_each(|d| {
-            codespan_reporting::term::emit(&mut buffer, &config, cache.files_mut(), d)
+            codespan_reporting::term::emit(&mut buffer, &config, cache.files_mut(), &d)
         })
         .unwrap();
 
diff --git a/src/serialize.rs b/src/serialize.rs
index f8fd2b66..4cb0d114 100644
--- a/src/serialize.rs
+++ b/src/serialize.rs
@@ -225,7 +225,7 @@ where
             .map_err(|err| SerializationError::Other(err.to_string())),
         ExportFormat::Yaml => serde_yaml::to_writer(writer, &rt)
             .map_err(|err| SerializationError::Other(err.to_string())),
-        ExportFormat::Toml => toml::Value::try_from(rt)
+        ExportFormat::Toml => toml::Value::try_from(&rt)
             .map_err(|err| SerializationError::Other(err.to_string()))
             .and_then(|v| {
                 write!(writer, "{}", v).map_err(|err| SerializationError::Other(err.to_string()))
@@ -249,7 +249,7 @@ pub fn to_string(format: ExportFormat, rt: &RichTerm) -> Result<String, Serializ
         ExportFormat::Yaml => {
             serde_yaml::to_string(&rt).map_err(|err| SerializationError::Other(err.to_string()))
         }
-        ExportFormat::Toml => toml::Value::try_from(rt)
+        ExportFormat::Toml => toml::Value::try_from(&rt)
             .map(|v| format!("{}", v))
             .map_err(|err| SerializationError::Other(err.to_string())),
         ExportFormat::Raw => match rt.as_ref() {
diff --git a/src/term.rs b/src/term.rs
index fe768fda..179a0672 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -266,7 +266,7 @@ pub mod array {
 
             // This condition is the same as `!Rc::is_unique(&mut self.inner)`, but that function
             // is not public.
-            if Rc::strong_count(&self.inner) != 1 || Rc::weak_count(&self.inner) != 0 {
+            if Rc::strong_count(&mut self.inner) != 1 || Rc::weak_count(&mut self.inner) != 0 {
                 self.inner = self.iter().cloned().collect::<Rc<[_]>>();
             }
 
@@ -516,11 +516,6 @@ pub struct PriorityIsNaN;
 
 // The following impl are ok because `NumeralPriority(NaN)` can't be constructed.
 impl Eq for NumeralPriority {}
-
-// We can't derive `Ord` because there is an `f64` inside
-// but it is actually an `Ord` because `NaN` is forbidden.
-// See `TryFrom` smart constructor.
-#[allow(clippy::derive_ord_xor_partial_ord)]
 impl Ord for NumeralPriority {
     fn cmp(&self, other: &Self) -> Ordering {
         // Ok: NaN is forbidden
@@ -720,7 +715,7 @@ impl MetaValue {
 
 /// A chunk of a string with interpolated expressions inside. Same as `Either<String,
 /// RichTerm>` but with explicit constructor names.
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum StrChunk<E> {
     /// A string literal.
     Literal(String),
@@ -796,7 +791,7 @@ impl Term {
             OpN(_, ref mut terms) => terms.iter_mut().for_each(|t| {
                 func(t);
             }),
-            Array(ref mut terms, _) => terms.make_mut().iter_mut().for_each(func),
+            Array(ref mut terms, _) => terms.make_mut().iter_mut().for_each(|t| func(t)),
             StrChunks(chunks) => chunks.iter_mut().for_each(|chunk| match chunk {
                 StrChunk::Literal(_) => (),
                 StrChunk::Expr(e, _) => func(e),
@@ -1315,7 +1310,7 @@ impl UnaryOp {
 }
 
 /// Primitive binary operators
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum BinaryOp {
     /// Addition of numerals.
     Plus(),
@@ -1419,7 +1414,7 @@ impl BinaryOp {
 
 /// Primitive n-ary operators. Unary and binary operator make up for most of operators and are
 /// hence special cased. `NAryOp` handles strict operations of arity greater than 2.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq)]
 pub enum NAryOp {
     /// Replace a substring by another one in a string.
     StrReplace(),
@@ -1563,7 +1558,7 @@ impl RichTerm {
                 let cases_res: Result<HashMap<Ident, RichTerm>, E> = cases
                     .into_iter()
                     // For the conversion to work, note that we need a Result<(Ident,RichTerm), E>
-                    .map(|(id, t)| t.traverse(f, state, order).map(|t_ok| (id, t_ok)))
+                    .map(|(id, t)| t.traverse(f, state, order).map(|t_ok| (id.clone(), t_ok)))
                     .collect();
 
                 let default = default.map(|t| t.traverse(f, state, order)).transpose()?;
@@ -1612,7 +1607,7 @@ impl RichTerm {
                 let fields_res: Result<HashMap<Ident, RichTerm>, E> = record.fields
                     .into_iter()
                     // For the conversion to work, note that we need a Result<(Ident,RichTerm), E>
-                    .map(|(id, t)| t.traverse(f, state, order).map(|t_ok| (id, t_ok)))
+                    .map(|(id, t)| t.traverse(f, state, order).map(|t_ok| (id.clone(), t_ok)))
                     .collect();
                 RichTerm::new(Term::Record(RecordData::new(fields_res?, record.attrs)), pos)
             },
@@ -1678,7 +1673,7 @@ impl RichTerm {
                     .into_iter()
                     .map(|ctr| {
                         let Contract {types, label} = ctr;
-                        types.traverse(&f_on_type, state, order).map(|types| Contract { types, label })
+                        types.traverse(&mut f_on_type, state, order).map(|types| Contract { types, label })
                     })
                     .collect();
                 let contracts = contracts?;
@@ -1687,7 +1682,7 @@ impl RichTerm {
                     .types
                     .map(|ctr| {
                         let Contract {types, label} = ctr;
-                        types.traverse(&f_on_type, state, order).map(|types| Contract { types, label })
+                        types.traverse(&mut f_on_type, state, order).map(|types| Contract { types, label })
                     })
                     .transpose()?;
 
@@ -1909,13 +1904,13 @@ pub mod make {
     macro_rules! mk_array {
         ( $( $terms:expr ),* ; $attrs:expr ) => {
             {
-                let ts = $crate::term::array::Array::new(std::rc::Rc::new([$( $crate::term::RichTerm::from($terms) ),*]));
+                let ts = $crate::term::array::Array::new(std::rc::Rc::new([$( crate::term::RichTerm::from($terms) ),*]));
                 $crate::term::RichTerm::from(Term::Array(ts, $attrs))
             }
         };
         ( $( $terms:expr ),* ) => {
             {
-                let ts = $crate::term::array::Array::new(std::rc::Rc::new([$( $crate::term::RichTerm::from($terms) ),*]));
+                let ts = $crate::term::array::Array::new(std::rc::Rc::new([$( crate::term::RichTerm::from($terms) ),*]));
                 $crate::term::RichTerm::from(Term::Array(ts, ArrayAttrs::default()))
             }
         };
diff --git a/src/transform/desugar_destructuring.rs b/src/transform/desugar_destructuring.rs
index e20e7d0b..b70f5d9d 100644
--- a/src/transform/desugar_destructuring.rs
+++ b/src/transform/desugar_destructuring.rs
@@ -60,7 +60,7 @@ pub fn desugar_fun(rt: RichTerm) -> RichTerm {
             let t_pos = t_.pos;
             RichTerm::new(
                 Term::Fun(
-                    x,
+                    x.clone(),
                     RichTerm::new(Term::LetPattern(None, pat, Term::Var(x).into(), t_), t_pos /* TODO: should we use rt.pos? */),
                 ),
                 rt.pos,
@@ -112,9 +112,9 @@ pub fn desugar(rt: RichTerm) -> RichTerm {
                 let x = x.unwrap_or_else(Ident::fresh);
                 RichTerm::new(
                     Term::Let(
-                        x,
+                        x.clone(),
                         t_,
-                        destruct_term(x, &pat, bind_open_field(x, &pat, body)),
+                        destruct_term(x.clone(), &pat, bind_open_field(x, &pat, body)),
                         Default::default(),
                     ),
                     pos,
@@ -136,7 +136,7 @@ fn bind_open_field(x: Ident, pat: &Destruct, body: RichTerm) -> RichTerm {
             open: true,
             rest: Some(x),
             ..
-        } => (matches, *x),
+        } => (matches, x.clone()),
         Destruct::Record {
             matches,
             open: true,
@@ -172,15 +172,20 @@ fn destruct_term(x: Ident, pat: &Destruct, body: RichTerm) -> RichTerm {
         Destruct::Record { matches, .. } => matches.iter().fold(body, move |t, m| match m {
             Match::Simple(id, _) => RichTerm::new(
                 Term::Let(
-                    *id,
-                    op1(StaticAccess(*id), Term::Var(x)),
+                    id.clone(),
+                    op1(StaticAccess(id.clone()), Term::Var(x.clone())),
                     t,
                     Default::default(),
                 ),
                 pos,
             ),
             Match::Assign(f, _, (id, pat)) => desugar(RichTerm::new(
-                Term::LetPattern(*id, pat.clone(), op1(StaticAccess(*f), Term::Var(x)), t),
+                Term::LetPattern(
+                    id.clone(),
+                    pat.clone(),
+                    op1(StaticAccess(f.clone()), Term::Var(x.clone())),
+                    t,
+                ),
                 pos,
             )),
         }),
diff --git a/src/transform/free_vars.rs b/src/transform/free_vars.rs
index 99ad50c1..0e68fb13 100644
--- a/src/transform/free_vars.rs
+++ b/src/transform/free_vars.rs
@@ -27,7 +27,7 @@ impl CollectFreeVars for RichTerm {
     fn collect_free_vars(&mut self, free_vars: &mut HashSet<Ident>) {
         match SharedTerm::make_mut(&mut self.term) {
             Term::Var(id) => {
-                free_vars.insert(*id);
+                free_vars.insert(id.clone());
             }
             Term::ParseError(_)
             | Term::Null
@@ -122,7 +122,9 @@ impl CollectFreeVars for RichTerm {
                     fresh.clear();
 
                     t.collect_free_vars(&mut fresh);
-                    new_deps.stat_fields.insert(*id, &fresh & &rec_fields);
+                    new_deps
+                        .stat_fields
+                        .insert(id.clone(), &fresh & &rec_fields);
 
                     free_vars.extend(&fresh - &rec_fields);
                 }
diff --git a/src/transform/import_resolution.rs b/src/transform/import_resolution.rs
index 4e2cf984..06184a97 100644
--- a/src/transform/import_resolution.rs
+++ b/src/transform/import_resolution.rs
@@ -44,7 +44,9 @@ where
 
     // If an import is resolved, then stack it.
     let transformed = rt.traverse(
-        &|rt: RichTerm, state: &mut ImportsResolutionState<R>| -> Result<RichTerm, ImportError> {
+        &mut |rt: RichTerm,
+              state: &mut ImportsResolutionState<R>|
+         -> Result<RichTerm, ImportError> {
             let rt = transform_one(rt, state.resolver, &state.parent)?;
 
             if let Term::ResolvedImport(file_id) = rt.term.as_ref() {
diff --git a/src/transform/mod.rs b/src/transform/mod.rs
index 3bd0c987..328d1304 100644
--- a/src/transform/mod.rs
+++ b/src/transform/mod.rs
@@ -35,7 +35,7 @@ pub fn transform_no_free_vars(
     wildcards: Option<&Wildcards>,
 ) -> Result<RichTerm, UnboundTypeVariableError> {
     let rt = rt.traverse(
-        &|mut rt: RichTerm, _| -> Result<RichTerm, UnboundTypeVariableError> {
+        &mut |mut rt: RichTerm, _| -> Result<RichTerm, UnboundTypeVariableError> {
             // Start by substituting any wildcard with its inferred type
             if let Some(wildcards) = wildcards {
                 rt = substitute_wildcards::transform_one(rt, wildcards);
@@ -52,7 +52,7 @@ pub fn transform_no_free_vars(
 
     Ok(rt
         .traverse(
-            &|rt: RichTerm, _| -> Result<RichTerm, ()> {
+            &mut |rt: RichTerm, _| -> Result<RichTerm, ()> {
                 let rt = share_normal_form::transform_one(rt);
                 Ok(rt)
             },
@@ -141,7 +141,7 @@ impl Closurizable for RichTerm {
             }
         };
 
-        env.insert(var, thunk);
+        env.insert(var.clone(), thunk);
         RichTerm::new(Term::Var(var), pos.into_inherited())
     }
 }
diff --git a/src/transform/share_normal_form.rs b/src/transform/share_normal_form.rs
index 06c6e62e..df94b92e 100644
--- a/src/transform/share_normal_form.rs
+++ b/src/transform/share_normal_form.rs
@@ -55,7 +55,7 @@ pub fn transform_one(rt: RichTerm) -> RichTerm {
                     if should_share(&t.term) {
                         let fresh_var = Ident::fresh();
                         let pos_t = t.pos;
-                        bindings.push((fresh_var, t, BindingType::Normal));
+                        bindings.push((fresh_var.clone(), t, BindingType::Normal));
                         RichTerm::new(Term::Var(fresh_var), pos_t)
                     } else {
                         t
@@ -88,7 +88,7 @@ pub fn transform_one(rt: RichTerm) -> RichTerm {
                     // rather that the dependencies haven't been computed. In the latter
                     // case, we must be conservative and assume the field is potentially
                     // recursive.
-                    let is_non_rec = field_deps.as_ref().map(|deps| deps.is_empty()).unwrap_or(false);
+                    let is_non_rec = (&field_deps).as_ref().map(|deps| deps.is_empty()).unwrap_or(false);
                     if is_non_rec {
                         BindingType::Normal
                     } else {
@@ -103,9 +103,9 @@ pub fn transform_one(rt: RichTerm) -> RichTerm {
                         let fresh_var = Ident::fresh();
                         let pos_t = t.pos;
                         let field_deps = deps.as_ref().and_then(|deps| deps.stat_fields.get(&id)).cloned();
-                        let is_non_rec = field_deps.as_ref().map(|deps| deps.is_empty()).unwrap_or(false);
+                        let is_non_rec = (&field_deps).as_ref().map(|deps| deps.is_empty()).unwrap_or(false);
                         let btype = mk_binding_type(field_deps);
-                        bindings.push((fresh_var, t, btype));
+                        bindings.push((fresh_var.clone(), t, btype));
 
                         RichTerm::new(Term::Var(fresh_var), pos_t)
                     } else {
@@ -122,7 +122,7 @@ pub fn transform_one(rt: RichTerm) -> RichTerm {
                             let pos_t = t.pos;
                             let field_deps = deps.as_ref().and_then(|deps| deps.dyn_fields.get(index)).cloned();
                             let btype = mk_binding_type(field_deps);
-                            bindings.push((fresh_var, t, btype));
+                            bindings.push((fresh_var.clone(), t, btype));
                             (id_t, RichTerm::new(Term::Var(fresh_var), pos_t))
                         } else {
                             (id_t, t)
@@ -141,7 +141,7 @@ pub fn transform_one(rt: RichTerm) -> RichTerm {
                         if should_share(&t.term) {
                             let fresh_var = Ident::fresh();
                             let pos_t = t.pos;
-                            bindings.push((fresh_var, t, BindingType::Normal));
+                            bindings.push((fresh_var.clone(), t, BindingType::Normal));
                             RichTerm::new(Term::Var(fresh_var), pos_t)
                         } else {
                             t
@@ -156,7 +156,7 @@ pub fn transform_one(rt: RichTerm) -> RichTerm {
                     let fresh_var = Ident::fresh();
                     let t = meta.value.take().unwrap();
                     meta.value
-                        .replace(RichTerm::new(Term::Var(fresh_var), t.pos));
+                        .replace(RichTerm::new(Term::Var(fresh_var.clone()), t.pos));
                     let inner = RichTerm::new(Term::MetaValue(meta), pos);
                     let attrs = LetAttrs {
                         binding_type: BindingType::Normal,
diff --git a/src/transform/substitute_wildcards.rs b/src/transform/substitute_wildcards.rs
index db4fb805..7f1c671b 100644
--- a/src/transform/substitute_wildcards.rs
+++ b/src/transform/substitute_wildcards.rs
@@ -51,7 +51,7 @@ fn get_wildcard_type(wildcards: &Wildcards, id: usize) -> Types {
 /// Recursively substitutes wildcards for their inferred type inside a given type.
 fn substitute_wildcards_recursively(ty: Types, wildcards: &Wildcards) -> Types {
     ty.traverse::<_, _, Infallible>(
-        &|ty, _| {
+        &mut |ty, _| {
             if let Types(TypeF::Wildcard(id)) = ty {
                 Ok(get_wildcard_type(wildcards, id))
             } else {
diff --git a/src/typecheck/eq.rs b/src/typecheck/eq.rs
index 076634b8..5fb4e536 100644
--- a/src/typecheck/eq.rs
+++ b/src/typecheck/eq.rs
@@ -114,7 +114,7 @@ impl From<eval::Environment> for SimpleTermEnvironment {
             .map(|(id, thunk)| {
                 let borrowed = thunk.borrow();
                 (
-                    *id,
+                    id.clone(),
                     (
                         borrowed.body.clone(),
                         SimpleTermEnvironment::from(borrowed.env.clone()),
@@ -386,7 +386,7 @@ fn rows_as_set(erows: &UnifEnumRows) -> Option<HashSet<Ident>> {
     let set: Option<HashSet<_>> = erows
         .iter()
         .map(|item| match item {
-            UnifEnumRowsIteratorItem::Row(id) => Some(*id),
+            UnifEnumRowsIteratorItem::Row(id) => Some(id.clone()),
             _ => None,
         })
         .collect();
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index ce1cbb43..a001e7a7 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -202,7 +202,7 @@ impl From<EnumRows> for UnifEnumRows {
 
 impl UnifEnumRows {
     /// Return an iterator producing immutable references to individual rows.
-    pub fn iter(&self) -> EnumRowsIterator<UnifEnumRows> {
+    pub fn iter<'a>(&'a self) -> EnumRowsIterator<'a, UnifEnumRows> {
         EnumRowsIterator { erows: Some(self) }
     }
 }
@@ -221,7 +221,9 @@ impl<E: TermEnvironment + Clone> GenericUnifRecordRows<E> {
 }
 
 impl<E: TermEnvironment> GenericUnifRecordRows<E> {
-    pub(super) fn iter(&self) -> RecordRowsIterator<GenericUnifType<E>, GenericUnifRecordRows<E>> {
+    pub(super) fn iter<'a>(
+        &'a self,
+    ) -> RecordRowsIterator<'a, GenericUnifType<E>, GenericUnifRecordRows<E>> {
         RecordRowsIterator {
             rrows: Some(self),
             ty: std::marker::PhantomData,
@@ -338,11 +340,11 @@ impl<E: TermEnvironment + Clone> GenericUnifType<E> {
     /// equality involving contracts.
     pub fn from_type(ty: Types, env: &E) -> Self {
         match ty.0 {
-            TypeF::Flat(t) => GenericUnifType::Contract(t, env.clone()),
+            TypeF::Flat(t) => GenericUnifType::Contract(t.clone(), env.clone()),
             ty => GenericUnifType::Concrete(ty.map(
                 |ty_| Box::new(GenericUnifType::from_type(*ty_, env)),
                 |rrows| GenericUnifRecordRows::from_record_rows(rrows, env),
-                UnifEnumRows::from,
+                |erows| UnifEnumRows::from(erows),
             )),
         }
     }
@@ -506,12 +508,12 @@ impl<'a, E: TermEnvironment> Iterator
                 }
                 RecordRowsF::TailVar(id) => {
                     self.rrows = None;
-                    Some(GenericUnifRecordRowsIteratorItem::TailVar(id))
+                    Some(GenericUnifRecordRowsIteratorItem::TailVar(&id))
                 }
                 RecordRowsF::Extend { row, tail } => {
-                    self.rrows = Some(tail);
+                    self.rrows = Some(&*tail);
                     Some(GenericUnifRecordRowsIteratorItem::Row(RecordRowF {
-                        id: row.id,
+                        id: row.id.clone(),
                         types: row.types.as_ref(),
                     }))
                 }
@@ -548,11 +550,11 @@ impl<'a> Iterator for EnumRowsIterator<'a, UnifEnumRows> {
                 }
                 EnumRowsF::TailVar(id) => {
                     self.erows = None;
-                    Some(UnifEnumRowsIteratorItem::TailVar(id))
+                    Some(UnifEnumRowsIteratorItem::TailVar(&id))
                 }
                 EnumRowsF::Extend { row, tail } => {
-                    self.erows = Some(tail);
-                    Some(UnifEnumRowsIteratorItem::Row(row))
+                    self.erows = Some(&*tail);
+                    Some(UnifEnumRowsIteratorItem::Row(&row))
                 }
             },
             UnifEnumRows::UnifVar(var_id) => {
@@ -593,13 +595,16 @@ pub enum EnvBuildError {
 }
 
 /// Populate the initial typing environment from a `Vec` of parsed files.
-pub fn mk_initial_ctxt(initial_env: &[RichTerm]) -> Result<Context, EnvBuildError> {
+pub fn mk_initial_ctxt(initial_env: &Vec<RichTerm>) -> Result<Context, EnvBuildError> {
     // Collect the bindings for each module, clone them and flatten the result to a single list.
     let bindings = initial_env
         .iter()
         .map(|rt| {
             if let Term::RecRecord(record, ..) = rt.as_ref() {
-                Ok(record.fields.iter().map(|(id, rt)| (*id, rt.clone())))
+                Ok(record
+                    .fields
+                    .iter()
+                    .map(|(id, rt)| (id.clone(), rt.clone())))
             } else {
                 Err(EnvBuildError::NotARecord(rt.clone()))
             }
@@ -614,7 +619,7 @@ pub fn mk_initial_ctxt(initial_env: &[RichTerm]) -> Result<Context, EnvBuildErro
         .collect();
 
     let type_env = bindings
-        .map(|(id, rt)| (id, infer_record_type(rt.as_ref(), &term_env)))
+        .map(|(id, rt)| (id.clone(), infer_record_type(rt.as_ref(), &term_env)))
         .collect();
 
     Ok(Context { type_env, term_env })
@@ -638,7 +643,7 @@ pub fn env_add_term(
                     apparent_type(t.as_ref(), Some(env), Some(resolver)),
                     term_env,
                 );
-                env.insert(*id, uty);
+                env.insert(id.clone(), uty);
             }
 
             Ok(())
@@ -789,7 +794,7 @@ fn walk<L: Linearizer>(
         | Term::ResolvedImport(_) => Ok(()),
         Term::Var(x) => ctxt.type_env
             .get(x)
-            .ok_or(TypecheckError::UnboundIdentifier(*x, *pos))
+            .ok_or_else(|| TypecheckError::UnboundIdentifier(x.clone(), *pos))
             .map(|_| ()),
         Term::StrChunks(chunks) => {
             chunks
@@ -805,12 +810,12 @@ fn walk<L: Linearizer>(
         }
         Term::Fun(id, t) => {
             // The parameter of an un-annotated function is assigned the type `Dyn`.
-            ctxt.type_env.insert(*id, mk_typewrapper::dynamic());
+            ctxt.type_env.insert(id.clone(), mk_typewrapper::dynamic());
             walk(state, ctxt, lin, linearizer, t)
         }
         Term::FunPattern(id, pat, t) => {
             if let Some(id) = id {
-                ctxt.type_env.insert(*id, binding_type(state, t.as_ref(), &ctxt, false));
+                ctxt.type_env.insert(id.clone(), binding_type(state, t.as_ref(), &ctxt, false));
             }
 
             inject_pat_vars(pat, &mut ctxt.type_env);
@@ -831,17 +836,17 @@ fn walk<L: Linearizer>(
             // allocate all the term environments inside an arena, local to each statically typed
             // block, and use bare references to represent cycles. Then everything would be cleaned
             // at the end of the block.
-            ctxt.term_env.0.insert(*x, (re.clone(), ctxt.term_env.clone()));
+            ctxt.term_env.0.insert(x.clone(), (re.clone(), ctxt.term_env.clone()));
 
             if attrs.rec {
-                ctxt.type_env.insert(*x, ty_let.clone());
+                ctxt.type_env.insert(x.clone(), ty_let.clone());
             }
 
             linearizer.retype_ident(lin, x, ty_let.clone());
             walk(state, ctxt.clone(), lin, linearizer.scope(), re)?;
 
             if !attrs.rec {
-                ctxt.type_env.insert(*x, ty_let);
+                ctxt.type_env.insert(x.clone(), ty_let);
             }
 
             walk(state, ctxt, lin, linearizer, rt)
@@ -852,7 +857,7 @@ fn walk<L: Linearizer>(
 
             if let Some(x) = x {
                 linearizer.retype_ident(lin, x, ty_let.clone());
-                ctxt.type_env.insert(*x, ty_let);
+                ctxt.type_env.insert(x.clone(), ty_let);
             }
 
             inject_pat_vars(pat, &mut ctxt.type_env);
@@ -878,7 +883,7 @@ fn walk<L: Linearizer>(
                     &ctxt,
                     false,
                 );
-                ctxt.type_env.insert(*id, binding_type.clone());
+                ctxt.type_env.insert(id.clone(), binding_type.clone());
                 linearizer.retype_ident(lin, id, binding_type);
             }
 
@@ -1002,13 +1007,13 @@ fn walk_rrows<L: Linearizer>(
 fn inject_pat_vars(pat: &Destruct, env: &mut Environment) {
     if let Destruct::Record { matches, rest, .. } = pat {
         if let Some(id) = rest {
-            env.insert(*id, UnifType::Concrete(TypeF::Dyn));
+            env.insert(id.clone(), UnifType::Concrete(TypeF::Dyn));
         }
         matches.iter().for_each(|m| match m {
-            Match::Simple(id, ..) => env.insert(*id, UnifType::Concrete(TypeF::Dyn)),
+            Match::Simple(id, ..) => env.insert(id.clone(), UnifType::Concrete(TypeF::Dyn)),
             Match::Assign(id, _, (bind_id, pat)) => {
                 let id = bind_id.as_ref().unwrap_or(id);
-                env.insert(*id, UnifType::Concrete(TypeF::Dyn));
+                env.insert(id.clone(), UnifType::Concrete(TypeF::Dyn));
                 if !pat.is_empty() {
                     inject_pat_vars(pat, env);
                 }
@@ -1082,7 +1087,7 @@ fn type_check_<L: Linearizer>(
 
             unify(state, &ctxt, ty, arr).map_err(|err| err.into_typecheck_err(state, rt.pos))?;
 
-            ctxt.type_env.insert(*x, src);
+            ctxt.type_env.insert(x.clone(), src);
             type_check_(state, ctxt, lin, linearizer, t, trg)
         }
         Term::FunPattern(x, pat, t) => {
@@ -1093,7 +1098,7 @@ fn type_check_<L: Linearizer>(
             let arr = mk_tyw_arrow!(src.clone(), trg.clone());
             if let Some(x) = x {
                 linearizer.retype_ident(lin, x, src.clone());
-                ctxt.type_env.insert(*x, src);
+                ctxt.type_env.insert(x.clone(), src);
             }
             inject_pat_vars(pat, &mut ctxt.type_env);
             unify(state, &ctxt, ty, arr).map_err(|err| err.into_typecheck_err(state, rt.pos))?;
@@ -1130,10 +1135,10 @@ fn type_check_<L: Linearizer>(
             // `Let` case in `walk`.
             ctxt.term_env
                 .0
-                .insert(*x, (re.clone(), ctxt.term_env.clone()));
+                .insert(x.clone(), (re.clone(), ctxt.term_env.clone()));
 
             if attrs.rec {
-                ctxt.type_env.insert(*x, ty_let.clone());
+                ctxt.type_env.insert(x.clone(), ty_let.clone());
             }
 
             linearizer.retype_ident(lin, x, ty_let.clone());
@@ -1147,7 +1152,7 @@ fn type_check_<L: Linearizer>(
             )?;
 
             if !attrs.rec {
-                ctxt.type_env.insert(*x, ty_let);
+                ctxt.type_env.insert(x.clone(), ty_let);
             }
             type_check_(state, ctxt, lin, linearizer, rt, ty)
         }
@@ -1164,7 +1169,7 @@ fn type_check_<L: Linearizer>(
 
             if let Some(x) = x {
                 linearizer.retype_ident(lin, x, ty_let.clone());
-                ctxt.type_env.insert(*x, ty_let);
+                ctxt.type_env.insert(x.clone(), ty_let);
             }
             inject_pat_vars(pat, &mut ctxt.type_env);
             type_check_(state, ctxt, lin, linearizer, rt, ty)
@@ -1200,7 +1205,7 @@ fn type_check_<L: Linearizer>(
                 None => cases.iter().try_fold(
                     EnumRowsF::Empty.into(),
                     |acc, x| -> Result<UnifEnumRows, TypecheckError> {
-                        Ok(mk_tyw_enum_row!(*x.0; acc))
+                        Ok(mk_tyw_enum_row!(x.0.clone(); acc))
                     },
                 )?,
             };
@@ -1213,7 +1218,7 @@ fn type_check_<L: Linearizer>(
                 .type_env
                 .get(x)
                 .cloned()
-                .ok_or(TypecheckError::UnboundIdentifier(*x, *pos))?;
+                .ok_or_else(|| TypecheckError::UnboundIdentifier(x.clone(), *pos))?;
 
             let instantiated = instantiate_foralls(state, x_ty, ForallInst::Ptr);
             unify(state, &ctxt, ty, instantiated)
@@ -1221,7 +1226,7 @@ fn type_check_<L: Linearizer>(
         }
         Term::Enum(id) => {
             let row = state.table.fresh_erows_uvar();
-            unify(state, &ctxt, ty, mk_tyw_enum!(*id; row))
+            unify(state, &ctxt, ty, mk_tyw_enum!(id.clone(); row))
                 .map_err(|err| err.into_typecheck_err(state, rt.pos))
         }
         // If some fields are defined dynamically, the only potential type that works is `{_ : a}`
@@ -1230,7 +1235,7 @@ fn type_check_<L: Linearizer>(
             let ty_dict = state.table.fresh_type_uvar();
 
             for id in record.fields.keys() {
-                ctxt.type_env.insert(*id, ty_dict.clone());
+                ctxt.type_env.insert(id.clone(), ty_dict.clone());
                 linearizer.retype_ident(lin, id, ty_dict.clone())
             }
 
@@ -1260,7 +1265,7 @@ fn type_check_<L: Linearizer>(
             if let Term::RecRecord(..) = t.as_ref() {
                 for (id, rt) in &record.fields {
                     let uty = binding_type(state, rt.as_ref(), &ctxt, true);
-                    ctxt.type_env.insert(*id, uty.clone());
+                    ctxt.type_env.insert(id.clone(), uty.clone());
                     linearizer.retype_ident(lin, id, uty);
                 }
             }
@@ -1304,7 +1309,7 @@ fn type_check_<L: Linearizer>(
                             ty.clone(),
                         )?;
 
-                        Ok(mk_tyw_row!((*id, ty); acc))
+                        Ok(mk_tyw_row!((id.clone(), ty); acc))
                     },
                 )?;
 
@@ -1608,7 +1613,7 @@ pub fn infer_record_type(t: &Term, term_env: &SimpleTermEnvironment) -> UnifType
                 RecordRowsF::Empty,
                 |r, (id, rt)| RecordRowsF::Extend {
                     row: UnifRecordRow {
-                        id: *id,
+                        id: id.clone(),
                         types: Box::new(infer_record_type(rt.term.as_ref(), term_env)),
                     },
                     tail: Box::new(r.into()),
@@ -1627,7 +1632,7 @@ fn has_wildcards(ty: &Types) -> bool {
     let mut has_wildcard = false;
     ty.clone()
         .traverse::<_, _, std::convert::Infallible>(
-            &|ty, has_wildcard| {
+            &mut |ty, has_wildcard| {
                 if ty.0.is_wildcard() {
                     *has_wildcard = true;
                 }
@@ -1663,7 +1668,7 @@ fn rrows_add(
     match rrows {
         UnifRecordRows::Concrete(erows) => match erows {
             RecordRowsF::Empty | RecordRowsF::TailDyn | RecordRowsF::TailVar(_) => {
-                Err(RowUnifError::MissingRow(*id))
+                Err(RowUnifError::MissingRow(id.clone()))
             }
             RecordRowsF::Extend { row, tail } => {
                 if *id == row.id {
@@ -1687,24 +1692,24 @@ fn rrows_add(
                 .map(|set| set.contains(id))
                 .unwrap_or(false)
             {
-                return Err(RowUnifError::UnsatConstr(*id, Some(*ty)));
+                return Err(RowUnifError::UnsatConstr(id.clone(), Some(*ty)));
             }
             let tail_var_id = state.table.fresh_rrows_var_id();
             let new_tail = UnifRecordRows::Concrete(RecordRowsF::Extend {
                 row: RecordRowF {
-                    id: *id,
+                    id: id.clone(),
                     types: ty.clone(),
                 },
-                tail: Box::new(UnifRecordRows::UnifVar(tail_var_id)),
+                tail: Box::new(UnifRecordRows::UnifVar(tail_var_id.clone())),
             });
 
-            new_tail.constrain_fresh_rrows_var(state, tail_var_id);
+            new_tail.constrain_fresh_rrows_var(state, tail_var_id.clone());
 
             state.table.assign_rrows(uvar, new_tail);
 
             Ok((ty, UnifRecordRows::UnifVar(tail_var_id)))
         }
-        UnifRecordRows::Constant(_) => Err(RowUnifError::MissingRow(*id)),
+        UnifRecordRows::Constant(_) => Err(RowUnifError::MissingRow(id.clone())),
     }
 }
 
@@ -1728,7 +1733,7 @@ fn erows_add(
 
     match uerows {
         UnifEnumRows::Concrete(erows) => match erows {
-            EnumRowsF::Empty | EnumRowsF::TailVar(_) => Err(RowUnifError::MissingRow(*id)),
+            EnumRowsF::Empty | EnumRowsF::TailVar(_) => Err(RowUnifError::MissingRow(id.clone())),
             EnumRowsF::Extend { row, tail } => {
                 if *id == row {
                     Ok(*tail)
@@ -1748,21 +1753,21 @@ fn erows_add(
                 .map(|set| set.contains(id))
                 .unwrap_or(false)
             {
-                return Err(RowUnifError::UnsatConstr(*id, None));
+                return Err(RowUnifError::UnsatConstr(id.clone(), None));
             }
             let tail_var_id = state.table.fresh_erows_var_id();
             let new_tail = UnifEnumRows::Concrete(EnumRowsF::Extend {
-                row: *id,
-                tail: Box::new(UnifEnumRows::UnifVar(tail_var_id)),
+                row: id.clone(),
+                tail: Box::new(UnifEnumRows::UnifVar(tail_var_id.clone())),
             });
 
-            new_tail.constrain_fresh_erows_var(state, tail_var_id);
+            new_tail.constrain_fresh_erows_var(state, tail_var_id.clone());
 
             state.table.assign_erows(uvar, new_tail);
 
             Ok(UnifEnumRows::UnifVar(tail_var_id))
         }
-        UnifEnumRows::Constant(_) => Err(RowUnifError::MissingRow(*id)),
+        UnifEnumRows::Constant(_) => Err(RowUnifError::MissingRow(id.clone())),
     }
 }
 
@@ -1809,7 +1814,7 @@ pub fn unify(
             }
             (TypeF::Flat(s), TypeF::Flat(t)) => Err(UnifError::IncomparableFlatTypes(s, t)),
             (TypeF::Enum(erows1), TypeF::Enum(erows2)) => {
-                unify_erows(state, erows1.clone(), erows2.clone()).map_err(|err| {
+                unify_erows(state, ctxt, erows1.clone(), erows2.clone()).map_err(|err| {
                     err.into_unif_err(mk_tyw_enum!(; erows1), mk_tyw_enum!(; erows2))
                 })
             }
@@ -1818,7 +1823,7 @@ pub fn unify(
                     err.into_unif_err(mk_tyw_record!(; rrows1), mk_tyw_record!(; rrows2))
                 })
             }
-            (TypeF::Dict(t1), TypeF::Dict(t2)) => unify(state, ctxt, *t1, *t2),
+            (TypeF::Dict(t1), TypeF::Dict(t2)) => unify(state, &ctxt, *t1, *t2),
             (
                 TypeF::Forall {
                     var: var1,
@@ -1856,7 +1861,7 @@ pub fn unify(
                     }
                 };
 
-                unify(state, ctxt, substd1, substd2)
+                unify(state, &ctxt, substd1, substd2)
             }
             (TypeF::Var(ident), _) | (_, TypeF::Var(ident)) => {
                 Err(UnifError::UnboundTypeVariable(ident))
@@ -1945,7 +1950,7 @@ pub fn unify_rrows(
                     let (ty2, t2_tail) =
                         rrows_add(state, &id, types.clone(), UnifRecordRows::Concrete(r2))?;
                     unify(state, ctxt, *types, *ty2)
-                        .map_err(|err| RowUnifError::RowMismatch(id, Box::new(err)))?;
+                        .map_err(|err| RowUnifError::RowMismatch(id.clone(), Box::new(err)))?;
                     unify_rrows(state, ctxt, *tail, t2_tail)
                 }
             }
@@ -1972,6 +1977,7 @@ pub fn unify_rrows(
 /// Try to unify two enum row types.
 pub fn unify_erows(
     state: &mut State,
+    ctxt: &Context,
     uerows1: UnifEnumRows,
     uerows2: UnifEnumRows,
 ) -> Result<(), RowUnifError> {
@@ -1993,7 +1999,7 @@ pub fn unify_erows(
                 }
                 (EnumRowsF::Extend { row: id, tail }, erows2 @ EnumRowsF::Extend { .. }) => {
                     let t2_tail = erows_add(state, &id, UnifEnumRows::Concrete(erows2))?;
-                    unify_erows(state, *tail, t2_tail)
+                    unify_erows(state, ctxt, *tail, t2_tail)
                 }
             }
         }
@@ -2056,7 +2062,7 @@ fn instantiate_foralls(state: &mut State, mut ty: UnifType, inst: ForallInst) ->
                     ForallInst::Constant => UnifType::Constant(fresh_uid),
                     ForallInst::Ptr => UnifType::UnifVar(fresh_uid),
                 };
-                state.names.insert(fresh_uid, var);
+                state.names.insert(fresh_uid, var.clone());
                 ty = body.subst_type(&var, &uvar);
             }
             VarKind::RecordRows => {
@@ -2065,7 +2071,7 @@ fn instantiate_foralls(state: &mut State, mut ty: UnifType, inst: ForallInst) ->
                     ForallInst::Constant => UnifRecordRows::Constant(fresh_uid),
                     ForallInst::Ptr => UnifRecordRows::UnifVar(fresh_uid),
                 };
-                state.names.insert(fresh_uid, var);
+                state.names.insert(fresh_uid, var.clone());
                 ty = body.subst_rrows(&var, &uvar);
 
                 if inst == ForallInst::Ptr {
@@ -2078,7 +2084,7 @@ fn instantiate_foralls(state: &mut State, mut ty: UnifType, inst: ForallInst) ->
                     ForallInst::Constant => UnifEnumRows::Constant(fresh_uid),
                     ForallInst::Ptr => UnifEnumRows::UnifVar(fresh_uid),
                 };
-                state.names.insert(fresh_uid, var);
+                state.names.insert(fresh_uid, var.clone());
                 ty = body.subst_erows(&var, &uvar);
 
                 if inst == ForallInst::Ptr {
@@ -2358,9 +2364,9 @@ impl ConstrainFreshRRowsVar for UnifRecordRows {
                 UnifRecordRows::Concrete(ty) => match ty {
                     RecordRowsF::Empty | RecordRowsF::TailDyn | RecordRowsF::TailVar(_) => (),
                     RecordRowsF::Extend { row, tail } => {
-                        constr.insert(row.id);
+                        constr.insert(row.id.clone());
                         row.types.constrain_fresh_rrows_var(state, var_id);
-                        constrain_var(state, constr, tail, var_id);
+                        constrain_var(state, constr, &*tail, var_id);
                     }
                 },
                 UnifRecordRows::Constant(_) => (),
@@ -2441,8 +2447,8 @@ impl ConstrainFreshERowsVar for UnifEnumRows {
                 UnifEnumRows::Concrete(ty) => match ty {
                     EnumRowsF::Empty | EnumRowsF::TailVar(_) => (),
                     EnumRowsF::Extend { row, tail } => {
-                        constr.insert(*row);
-                        constrain_var(state, constr, tail, var_id);
+                        constr.insert(row.clone());
+                        constrain_var(state, constr, &*tail, var_id);
                     }
                 },
                 UnifEnumRows::Constant(_) => (),
@@ -2480,7 +2486,7 @@ pub fn constr_unify_rrows(
                 if p_constr.contains(&row.id) =>
             {
                 Err(RowUnifError::UnsatConstr(
-                    row.id,
+                    row.id.clone(),
                     Some(UnifType::Concrete(TypeF::Record(rrows.clone()))),
                 ))
             }
@@ -2514,9 +2520,9 @@ pub fn constr_unify_erows(
 ) -> Result<(), RowUnifError> {
     if let Some(p_constr) = constr.remove(&var_id) {
         match erows {
-            UnifEnumRows::Concrete(EnumRowsF::Extend { row, .. }) if p_constr.contains(row) => {
+            UnifEnumRows::Concrete(EnumRowsF::Extend { row, .. }) if p_constr.contains(&row) => {
                 Err(RowUnifError::UnsatConstr(
-                    *row,
+                    row.clone(),
                     Some(UnifType::Concrete(TypeF::Enum(erows.clone()))),
                 ))
             }
diff --git a/src/typecheck/operation.rs b/src/typecheck/operation.rs
index 9f48699f..9de33e64 100644
--- a/src/typecheck/operation.rs
+++ b/src/typecheck/operation.rs
@@ -48,7 +48,7 @@ pub fn get_uop_type(
             let row = UnifEnumRows::UnifVar(row_var_id);
 
             let domain = mk_tyw_enum!(; row.clone());
-            let codomain = mk_tyw_enum!(*id; row);
+            let codomain = mk_tyw_enum!(id.clone(); row);
 
             // The codomain is the only type which can impose a constraint on the fresh row
             // unification variable, namely that it can't contain `id`.
@@ -66,7 +66,7 @@ pub fn get_uop_type(
             let rows = state.table.fresh_rrows_uvar();
             let res = state.table.fresh_type_uvar();
 
-            (mk_tyw_record!((*id, res.clone()); rows), res)
+            (mk_tyw_record!((id.clone(), res.clone()); rows), res)
         }
         // forall a b. Array a -> (a -> b) -> Array b
         UnaryOp::ArrayMap() => {
diff --git a/src/typecheck/reporting.rs b/src/typecheck/reporting.rs
index da1a9835..252a4a4c 100644
--- a/src/typecheck/reporting.rs
+++ b/src/typecheck/reporting.rs
@@ -68,7 +68,7 @@ fn select_uniq(name_reg: &mut NameReg, mut name: String, id: usize) -> Ident {
     }
 
     let ident = Ident::from(name);
-    name_reg.reg.insert(id, ident);
+    name_reg.reg.insert(id, ident.clone());
     ident
 }
 
diff --git a/src/types.rs b/src/types.rs
index a62e10c3..c39cde56 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -70,7 +70,7 @@ use std::{
 ///
 /// As other types with the `F` suffix, this type is parametrized by one or more recursive
 /// unfoldings (here, `Ty` for `TypeF`). See [`TypeF`] for more details.
-#[derive(Clone, PartialEq, Eq, Debug)]
+#[derive(Clone, PartialEq, Debug)]
 pub struct RecordRowF<Ty> {
     pub id: Ident,
     pub types: Ty,
@@ -96,7 +96,7 @@ pub type EnumRow = EnumRowF;
 ///   wrapper around an instantiation of `TypeF`.
 /// - `RRows` is the recursive unfolding of record rows (the tail of this row sequence). In
 ///   practice, a wrapper around an instantiation of `RecordRowsF`.
-#[derive(Clone, PartialEq, Eq, Debug)]
+#[derive(Clone, PartialEq, Debug)]
 pub enum RecordRowsF<Ty, RRows> {
     Empty,
     Extend { row: RecordRowF<Ty>, tail: RRows },
@@ -113,7 +113,7 @@ pub enum RecordRowsF<Ty, RRows> {
 ///
 /// - `ERows` is the recursive unfolding of enum rows (the tail of this row sequence). In practice,
 ///   a wrapper around `EnumRowsF`.
-#[derive(Clone, PartialEq, Eq, Debug)]
+#[derive(Clone, PartialEq, Debug)]
 pub enum EnumRowsF<ERows> {
     Empty,
     Extend { row: EnumRowF, tail: ERows },
@@ -623,7 +623,7 @@ impl<'a> Iterator for RecordRowsIterator<'a, Types, RecordRows> {
             RecordRowsF::Extend { ref row, ref tail } => {
                 self.rrows = Some(tail);
                 Some(RecordRowsIteratorItem::Row(RecordRowF {
-                    id: row.id,
+                    id: row.id.clone(),
                     types: row.types.as_ref(),
                 }))
             }
@@ -651,7 +651,7 @@ impl<'a> Iterator for EnumRowsIterator<'a, EnumRows> {
             }
             EnumRowsF::TailVar(ref id) => {
                 self.erows = None;
-                Some(EnumRowsIteratorItem::TailVar(id))
+                Some(EnumRowsIteratorItem::TailVar(&id))
             }
             EnumRowsF::Extend { ref row, ref tail } => {
                 self.erows = Some(tail);
@@ -669,7 +669,9 @@ fn get_var_contract(
     id: &Ident,
     pol: bool,
 ) -> Result<RichTerm, UnboundTypeVariableError> {
-    let (pos, neg) = vars.get(id).ok_or(UnboundTypeVariableError(*id))?;
+    let (pos, neg) = vars
+        .get(id)
+        .ok_or_else(|| UnboundTypeVariableError(id.clone()))?;
     if pol {
         Ok(pos.clone())
     } else {
@@ -689,7 +691,7 @@ impl EnumRows {
         for row in self.iter() {
             match row {
                 EnumRowsIteratorItem::Row(id) => {
-                    cases.insert(*id, mk_term::var(value_arg));
+                    cases.insert(id.clone(), mk_term::var(value_arg.clone()));
                 }
                 EnumRowsIteratorItem::TailVar(_) => {
                     has_tail = true;
@@ -701,7 +703,7 @@ impl EnumRows {
         // If the enum type has a tail, the tail must be a universally quantified variable,
         // and this means that the tag can be anything.
         let case_body = if has_tail {
-            mk_term::var(value_arg)
+            mk_term::var(value_arg.clone())
         }
         // Otherwise, we build a switch with all the tags as cases, which just returns the
         // original argument, and a default case that blames.
@@ -720,9 +722,12 @@ impl EnumRows {
         // ```
         else {
             RichTerm::from(Term::Switch(
-                mk_term::var(value_arg),
+                mk_term::var(value_arg.clone()),
                 cases,
-                Some(mk_app!(contract::enum_fail(), mk_term::var(label_arg))),
+                Some(mk_app!(
+                    contract::enum_fail(),
+                    mk_term::var(label_arg.clone())
+                )),
             ))
         };
         let case = mk_fun!(label_arg, value_arg, case_body);
@@ -730,7 +735,7 @@ impl EnumRows {
         Ok(mk_app!(contract::enums(), case))
     }
 
-    pub fn iter(&self) -> EnumRowsIterator<EnumRows> {
+    pub fn iter<'a>(&'a self) -> EnumRowsIterator<'a, EnumRows> {
         EnumRowsIterator { erows: Some(self) }
     }
 }
@@ -802,7 +807,7 @@ impl RecordRows {
         }
     }
 
-    pub fn iter(&self) -> RecordRowsIterator<Types, RecordRows> {
+    pub fn iter<'a>(&'a self) -> RecordRowsIterator<'a, Types, RecordRows> {
         RecordRowsIterator {
             rrows: Some(self),
             ty: std::marker::PhantomData,
@@ -866,7 +871,7 @@ impl Types {
                     Term::Bool(pol)
                 );
 
-                h.insert(*var, (inst_var, inst_tail));
+                h.insert(var.clone(), (inst_var, inst_tail));
                 *sy += 1;
                 body.subcontract(h, pol, sy)?
             }
diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs
index 4d26d783..db73824b 100644
--- a/utilities/src/lib.rs
+++ b/utilities/src/lib.rs
@@ -27,7 +27,7 @@ pub fn parse(s: &str) -> Result<RichTerm, ParseError> {
     let id = Files::new().add("<test>", String::from(s));
 
     grammar::TermParser::new()
-        .parse_term(id, lexer::Lexer::new(s))
+        .parse_term(id, lexer::Lexer::new(&s))
         .map_err(|errs| errs.errors.first().unwrap().clone())
 }
 
@@ -59,7 +59,7 @@ pub struct Bench<'b> {
 }
 
 impl<'b> Bench<'b> {
-    pub fn new(
+    pub fn bench(
         name: &'b str,
         base_dir: &'b str,
         subpath: &'b str,


Test patch:
diff --git a/src/eval/tests.rs b/src/eval/tests.rs
index 5566eeb4..e50dee13 100644
--- a/src/eval/tests.rs
+++ b/src/eval/tests.rs
@@ -20,7 +20,7 @@ fn parse(s: &str) -> Option<RichTerm> {
     let id = Files::new().add("<test>", String::from(s));
 
     grammar::TermParser::new()
-        .parse_term(id, lexer::Lexer::new(s))
+        .parse_term(id, lexer::Lexer::new(&s))
         .map(RichTerm::without_pos)
         .map_err(|err| println!("{:?}", err))
         .ok()
@@ -35,7 +35,7 @@ fn identity_over_values() {
     assert_eq!(Ok(boolean.clone()), eval_no_import(boolean.into()));
 
     let lambda = mk_fun!("x", mk_app!(mk_term::var("x"), mk_term::var("x")));
-    assert_eq!(Ok(lambda.as_ref().clone()), eval_no_import(lambda));
+    assert_eq!(Ok(lambda.as_ref().clone()), eval_no_import(lambda.into()));
 }
 
 #[test]
@@ -225,11 +225,14 @@ fn imports() {
 fn interpolation_simple() {
     let mut chunks = vec![
         StrChunk::Literal(String::from("Hello")),
-        StrChunk::expr(mk_term::op2(
-            BinaryOp::StrConcat(),
-            mk_term::string(", "),
-            mk_term::string("World!"),
-        )),
+        StrChunk::expr(
+            mk_term::op2(
+                BinaryOp::StrConcat(),
+                mk_term::string(", "),
+                mk_term::string("World!"),
+            )
+            .into(),
+        ),
         StrChunk::Literal(String::from(" How")),
         StrChunk::expr(mk_term::if_then_else(
             Term::Bool(true),
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 3e005c62..12e6b65e 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -13,7 +13,7 @@ fn parse(s: &str) -> Result<RichTerm, ParseError> {
     let id = Files::new().add("<test>", String::from(s));
 
     super::grammar::TermParser::new()
-        .parse_term(id, Lexer::new(s))
+        .parse_term(id, Lexer::new(&s))
         .map_err(|errs| errs.errors.first().unwrap().clone())
 }
 
diff --git a/tests/free_vars.rs b/tests/free_vars.rs
index e9e347c9..0d13db1d 100644
--- a/tests/free_vars.rs
+++ b/tests/free_vars.rs
@@ -10,7 +10,7 @@ use nickel_lang_utilities::parse;
 // }
 
 fn free_vars_eq(free_vars: &HashSet<Ident>, expected: Vec<&str>) -> bool {
-    let expected_set: HashSet<Ident> = expected.into_iter().map(Ident::from).collect();
+    let expected_set: HashSet<Ident> = expected.into_iter().map(|s| Ident::from(s)).collect();
     *free_vars == expected_set
 }