Repository: tweag/nickel
Pull request #673 (2022-03-29 13:07:30)

Benches/program mutualize
Use the same eval environment for all subbenches.
Also implement a new macro to have a clean description of benches


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 037bd287..5bb2a0b7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -66,9 +66,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.56"
+version = "1.0.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27"
+checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
 
 [[package]]
 name = "arrayvec"
@@ -107,15 +107,15 @@ dependencies = [
 
 [[package]]
 name = "autocfg"
-version = "1.1.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
 
 [[package]]
 name = "backtrace"
-version = "0.3.64"
+version = "0.3.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f"
+checksum = "321629d8ba6513061f26707241fa9bc89524ff1cd7a915a97ef0c62c666ce1b6"
 dependencies = [
  "addr2line",
  "cc",
@@ -176,15 +176,15 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.9.1"
+version = "3.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
+checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
 
 [[package]]
 name = "bytemuck"
-version = "1.8.0"
+version = "1.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e851ca7c24871e7336801608a4797d7376545b6928a10d32d75685687141ead"
+checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f"
 
 [[package]]
 name = "cast"
@@ -197,9 +197,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.73"
+version = "1.0.70"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
 
 [[package]]
 name = "cfg-if"
@@ -209,11 +209,11 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "clap"
-version = "2.34.0"
+version = "2.33.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
 dependencies = [
- "ansi_term 0.12.1",
+ "ansi_term 0.11.0",
  "atty",
  "bitflags",
  "strsim",
@@ -293,7 +293,7 @@ dependencies = [
  "oorandom",
  "plotters",
  "rayon",
- "regex 1.5.5",
+ "regex 1.5.4",
  "serde",
  "serde_cbor",
  "serde_derive",
@@ -328,9 +328,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.2"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa"
+checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4"
 dependencies = [
  "cfg-if",
  "crossbeam-utils",
@@ -349,9 +349,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.7"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9"
+checksum = "4ec02e091aa634e2c3ada4a392989e7c3116673ef0ac5b72232439094d73b7fd"
 dependencies = [
  "cfg-if",
  "crossbeam-utils",
@@ -362,9 +362,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-queue"
-version = "0.3.4"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dd435b205a4842da59efd07628f921c096bc1cc0a156835b4fa0bcb9a19bcce"
+checksum = "9b10ddc024425c88c2ad148c1b0fd53f4c6d38db9697c9f1588381212fa657c9"
 dependencies = [
  "cfg-if",
  "crossbeam-utils",
@@ -372,9 +372,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.7"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6"
+checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
 dependencies = [
  "cfg-if",
  "lazy_static",
@@ -419,7 +419,7 @@ checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
 dependencies = [
  "bstr",
  "csv-core",
- "itoa 0.4.8",
+ "itoa",
  "ryu",
  "serde",
 ]
@@ -435,9 +435,9 @@ dependencies = [
 
 [[package]]
 name = "debugid"
-version = "0.7.3"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6ee87af31d84ef885378aebca32be3d682b0e0dc119d5b4860a2c5bb5046730"
+checksum = "f91cf5a8c2f2097e2a32627123508635d47ce10563d999ec1a95addf08b502ba"
 dependencies = [
  "uuid",
 ]
@@ -517,6 +517,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "dtoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
+
 [[package]]
 name = "either"
 version = "1.6.1"
@@ -541,19 +547,10 @@ dependencies = [
  "atty",
  "humantime",
  "log",
- "regex 1.5.5",
+ "regex 1.5.4",
  "termcolor",
 ]
 
-[[package]]
-name = "fastrand"
-version = "1.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
-dependencies = [
- "instant",
-]
-
 [[package]]
 name = "fixedbitset"
 version = "0.2.0"
@@ -588,9 +585,9 @@ dependencies = [
 
 [[package]]
 name = "generic-array"
-version = "0.14.5"
+version = "0.14.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
 dependencies = [
  "typenum",
  "version_check",
@@ -598,9 +595,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.5"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
 dependencies = [
  "cfg-if",
  "libc",
@@ -662,9 +659,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "1.8.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
 dependencies = [
  "autocfg",
  "hashbrown",
@@ -672,14 +669,14 @@ dependencies = [
 
 [[package]]
 name = "inferno"
-version = "0.10.12"
+version = "0.10.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de3886428c6400486522cf44b8626e7b94ad794c14390290f2a274dcf728a58f"
+checksum = "0fe3ba15e9cce341de198353aebd07d8009d211fb5556920bd4bdd3bb79fafd7"
 dependencies = [
  "ahash",
  "atty",
  "indexmap",
- "itoa 1.0.1",
+ "itoa",
  "lazy_static",
  "log",
  "num-format",
@@ -690,18 +687,18 @@ dependencies = [
 
 [[package]]
 name = "instant"
-version = "0.1.12"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
 dependencies = [
  "cfg-if",
 ]
 
 [[package]]
 name = "itertools"
-version = "0.10.3"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
+checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf"
 dependencies = [
  "either",
 ]
@@ -712,12 +709,6 @@ version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
 
-[[package]]
-name = "itoa"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
-
 [[package]]
 name = "js-sys"
 version = "0.3.51"
@@ -729,9 +720,9 @@ dependencies = [
 
 [[package]]
 name = "lalrpop"
-version = "0.19.7"
+version = "0.19.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "852b75a095da6b69da8c5557731c3afd06525d4f655a4fc1c799e2ec8bc4dce4"
+checksum = "b15174f1c529af5bf1283c3bc0058266b483a67156f79589fab2a25e23cf8988"
 dependencies = [
  "ascii-canvas",
  "atty",
@@ -742,7 +733,7 @@ dependencies = [
  "lalrpop-util",
  "petgraph",
  "pico-args",
- "regex 1.5.5",
+ "regex 1.5.4",
  "regex-syntax 0.6.25",
  "string_cache",
  "term",
@@ -752,11 +743,11 @@ dependencies = [
 
 [[package]]
 name = "lalrpop-util"
-version = "0.19.7"
+version = "0.19.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6d265705249fe209280676d8f68887859fa42e1d34f342fc05bd47726a5e188"
+checksum = "d3e58cce361efcc90ba8a0a5f982c741ff86b603495bb15a998412e957dcd278"
 dependencies = [
- "regex 1.5.5",
+ "regex 1.5.4",
 ]
 
 [[package]]
@@ -767,9 +758,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.119"
+version = "0.2.101"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4"
+checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21"
 
 [[package]]
 name = "linked-hash-map"
@@ -779,9 +770,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
 
 [[package]]
 name = "lock_api"
-version = "0.4.6"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
+checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
 dependencies = [
  "scopeguard",
 ]
@@ -868,28 +859,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
 
 [[package]]
-name = "memmap2"
-version = "0.5.3"
+name = "memmap"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f"
+checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b"
 dependencies = [
  "libc",
+ "winapi",
 ]
 
 [[package]]
 name = "memoffset"
-version = "0.6.5"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
 dependencies = [
  "autocfg",
 ]
 
 [[package]]
 name = "minimad"
-version = "0.9.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd37b2e65fbd459544194d8f52ed84027e031684335a062c708774c09d172b0b"
+checksum = "8957f240ecb82a4e699bcf4db189fe8a7f5aa68b9e6d5abf829c62a9ee4630ed"
 dependencies = [
  "once_cell",
 ]
@@ -906,9 +898,9 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "0.7.14"
+version = "0.7.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
 dependencies = [
  "libc",
  "log",
@@ -968,7 +960,7 @@ dependencies = [
 ]
 
 [[package]]
-name = "nickel-lang-lsp"
+name = "nickel-lang-nls"
 version = "0.1.0"
 dependencies = [
  "anyhow",
@@ -1034,9 +1026,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
 
 [[package]]
 name = "ntapi"
-version = "0.3.7"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
 dependencies = [
  "winapi",
 ]
@@ -1048,7 +1040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465"
 dependencies = [
  "arrayvec",
- "itoa 0.4.8",
+ "itoa",
 ]
 
 [[package]]
@@ -1081,9 +1073,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.10.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
 
 [[package]]
 name = "oorandom"
@@ -1140,9 +1132,9 @@ dependencies = [
 
 [[package]]
 name = "phf_shared"
-version = "0.10.0"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
 dependencies = [
  "siphasher",
 ]
@@ -1200,6 +1192,12 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
+
 [[package]]
 name = "precomputed-hash"
 version = "0.1.1"
@@ -1242,9 +1240,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.36"
+version = "1.0.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
+checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
 dependencies = [
  "unicode-xid",
 ]
@@ -1260,13 +1258,53 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.15"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
 dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
+dependencies = [
+ "rand_core",
+]
+
 [[package]]
 name = "rayon"
 version = "1.5.1"
@@ -1294,9 +1332,9 @@ dependencies = [
 
 [[package]]
 name = "redox_syscall"
-version = "0.2.11"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c"
+checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
 dependencies = [
  "bitflags",
 ]
@@ -1326,9 +1364,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.5.5"
+version = "1.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
 dependencies = [
  "aho-corasick 0.7.18",
  "memchr",
@@ -1367,9 +1405,9 @@ dependencies = [
 
 [[package]]
 name = "rgb"
-version = "0.8.32"
+version = "0.8.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e74fdc210d8f24a7dbfedc13b04ba5764f5232754ccebfdf5fff1bad791ccbc6"
+checksum = "9a374af9a0e5fdcdd98c1c7b64f05004f9ea2555b6c75f211daa81268a3c50f1"
 dependencies = [
  "bytemuck",
 ]
@@ -1391,9 +1429,9 @@ dependencies = [
 
 [[package]]
 name = "rustversion"
-version = "1.0.6"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
+checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
 
 [[package]]
 name = "rustyline"
@@ -1428,9 +1466,9 @@ dependencies = [
 
 [[package]]
 name = "ryu"
-version = "1.0.9"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
 
 [[package]]
 name = "same-file"
@@ -1449,15 +1487,15 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
 name = "semver"
-version = "1.0.6"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d"
+checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
 
 [[package]]
 name = "serde"
-version = "1.0.136"
+version = "1.0.130"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
 dependencies = [
  "serde_derive",
 ]
@@ -1474,9 +1512,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.136"
+version = "1.0.130"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1485,11 +1523,11 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.79"
+version = "1.0.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
+checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950"
 dependencies = [
- "itoa 1.0.1",
+ "itoa",
  "ryu",
  "serde",
 ]
@@ -1507,12 +1545,12 @@ dependencies = [
 
 [[package]]
 name = "serde_yaml"
-version = "0.8.23"
+version = "0.8.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0"
+checksum = "ad104641f3c958dab30eb3010e834c2622d1f3f4c530fef1dee20ad9485f3c09"
 dependencies = [
+ "dtoa",
  "indexmap",
- "ryu",
  "serde",
  "yaml-rust",
 ]
@@ -1532,9 +1570,9 @@ dependencies = [
 
 [[package]]
 name = "sha2"
-version = "0.9.9"
+version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+checksum = "9204c41a1597a8c5af23c82d1c921cb01ec0a4c59e07a9c7306062829a3903f3"
 dependencies = [
  "block-buffer",
  "cfg-if",
@@ -1545,9 +1583,9 @@ dependencies = [
 
 [[package]]
 name = "signal-hook"
-version = "0.3.13"
+version = "0.3.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
+checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
 dependencies = [
  "libc",
  "signal-hook-registry",
@@ -1581,15 +1619,15 @@ checksum = "4bb57743b52ea059937169c0061d70298fe2df1d2c988b44caae79dd979d9b49"
 
 [[package]]
 name = "siphasher"
-version = "0.3.10"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+checksum = "729a25c17d72b06c68cb47955d44fda88ad2d3e7d77e025663fdd69b93dd71a1"
 
 [[package]]
 name = "smallvec"
-version = "1.8.0"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
 
 [[package]]
 name = "stable_deref_trait"
@@ -1605,13 +1643,12 @@ checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb"
 
 [[package]]
 name = "string_cache"
-version = "0.8.3"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26"
+checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a"
 dependencies = [
  "lazy_static",
  "new_debug_unreachable",
- "parking_lot",
  "phf_shared",
  "precomputed-hash",
 ]
@@ -1624,9 +1661,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
 
 [[package]]
 name = "structopt"
-version = "0.3.26"
+version = "0.3.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
+checksum = "bf9d950ef167e25e0bdb073cf1d68e9ad2795ac826f2f3f59647817cf23c0bfa"
 dependencies = [
  "clap",
  "lazy_static",
@@ -1635,9 +1672,9 @@ dependencies = [
 
 [[package]]
 name = "structopt-derive"
-version = "0.4.18"
+version = "0.4.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
+checksum = "134d838a2c9943ac3125cf6df165eda53493451b719f3255b2a26b85f772d0ba"
 dependencies = [
  "heck",
  "proc-macro-error",
@@ -1648,21 +1685,21 @@ dependencies = [
 
 [[package]]
 name = "symbolic-common"
-version = "8.6.1"
+version = "8.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52ca6f4079d985e79702d1cce708bdd03ac570e220bcf87105d86f5a8ebb26be"
+checksum = "cfc8618f0f31ed048f8e66aa2caecedfbdbbca962ff9ad87107ba4171de0742b"
 dependencies = [
  "debugid",
- "memmap2",
+ "memmap",
  "stable_deref_trait",
  "uuid",
 ]
 
 [[package]]
 name = "symbolic-demangle"
-version = "8.6.1"
+version = "8.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7bd7f5075d8bbe9b00eaa9cb47e788fe9405f3306f7497b617b62e5f65ec619a"
+checksum = "8be790f170c892899802aa1d382b7b5b65baf692b1357864c74e92bbbbdabfbe"
 dependencies = [
  "cpp_demangle",
  "rustc-demangle",
@@ -1671,9 +1708,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.86"
+version = "1.0.75"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
+checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1682,13 +1719,13 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.3.0"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
 dependencies = [
  "cfg-if",
- "fastrand",
  "libc",
+ "rand",
  "redox_syscall",
  "remove_dir_all",
  "winapi",
@@ -1707,18 +1744,18 @@ dependencies = [
 
 [[package]]
 name = "termcolor"
-version = "1.1.3"
+version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
 dependencies = [
  "winapi-util",
 ]
 
 [[package]]
 name = "termimad"
-version = "0.16.4"
+version = "0.16.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d88221817ef21964ccccf7d8ccc35fed3bae3066a177d1fcda72d694a016a47d"
+checksum = "5cae919a0f56a978584712d3fbb21d60c9952fad28c872634590d5a201c79860"
 dependencies = [
  "crossbeam",
  "crossterm",
@@ -1738,18 +1775,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.30"
+version = "1.0.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+checksum = "283d5230e63df9608ac7d9691adc1dfb6e701225436eb64d0b9a7f0a5a04f6ec"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.30"
+version = "1.0.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+checksum = "fa3884228611f5cd3608e2d409bf7dce832e4eb3135e3f11addbd7e41bd68e71"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1786,9 +1823,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.5.1"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
+checksum = "5241dd6f21443a3606b432718b166d3cedc962fd4b8bea54a8bc7f514ebda986"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -1810,9 +1847,9 @@ dependencies = [
 
 [[package]]
 name = "typenum"
-version = "1.15.0"
+version = "1.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
 
 [[package]]
 name = "ucd-util"
@@ -1822,9 +1859,9 @@ checksum = "c85f514e095d348c279b1e5cd76795082cf15bd59b93207832abe0b1d8fed236"
 
 [[package]]
 name = "unicode-bidi"
-version = "0.3.7"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
+checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
 
 [[package]]
 name = "unicode-normalization"
@@ -1837,15 +1874,15 @@ dependencies = [
 
 [[package]]
 name = "unicode-segmentation"
-version = "1.9.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
 
 [[package]]
 name = "unicode-width"
-version = "0.1.9"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
 
 [[package]]
 name = "unicode-xid"
@@ -1892,9 +1929,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 
 [[package]]
 name = "version_check"
-version = "0.9.4"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
 
 [[package]]
 name = "void"
diff --git a/Cargo.toml b/Cargo.toml
index 3fcd3180..130e0841 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -65,8 +65,7 @@ nickel-lang-utilities = {path = "utilities", version = "0.1.0"}
 [workspace]
 members = [
     ".",
-    "lsp/nls",
-    "utilities",
+    "lsp/nls"
 ]
 
 # Enable this to use flamegraphs
diff --git a/README.md b/README.md
index cfd33689..0df0a9c3 100644
--- a/README.md
+++ b/README.md
@@ -174,7 +174,7 @@ to be used in production. The next steps we plan to work on are:
 - Nix integration: being able to seamlessly use Nickel to write shells, packages
   and NixOS modules.
 - Custom merge functions and priorities (second part of the
-  [overriding proposal](https://github.com/tweag/nickel/blob/9fd6e436c0db8f101d4eb26cf97c4993357a7c38/rfcs/001-overriding.md))
+  [overriding proposal](https://github.com/tweag/nickel/blob/9fd6e436c0db8f101d4eb26cf97c4993357a7c38/rfcs/001-overriding.md)
 - Performance improvements
 
 ## Related projects and inspirations
diff --git a/benches/arrays.rs b/benches/arrays.rs
index ec1dcaee..d488f048 100644
--- a/benches/arrays.rs
+++ b/benches/arrays.rs
@@ -1,238 +1,107 @@
-use criterion::{criterion_group, criterion_main, Criterion};
-use nickel_lang_utilities::{bench, EvalMode};
+use criterion::{criterion_main, Criterion};
+use nickel_lang_utilities::{ncl_bench_group, EvalMode};
 use pprof::criterion::{Output, PProfProfiler};
 
-fn fold_strings(c: &mut Criterion) {
-    bench(
-        "foldr strings 50",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("right.strings"),
-        50,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn fold_strings_deep(c: &mut Criterion) {
-    bench(
-        "foldr deepseq strings 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("right.strings"),
-        30,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn fold_nums(c: &mut Criterion) {
-    bench(
-        "foldr nums 50",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("right.nums"),
-        50,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn fold_nums_deep(c: &mut Criterion) {
-    bench(
-        "foldr deepseq nums 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("right.nums"),
-        30,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn fold_arrays(c: &mut Criterion) {
-    bench(
-        "foldr arrays 50",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("right.arrays"),
-        50,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn fold_arrays_deep(c: &mut Criterion) {
-    bench(
-        "foldr deepseq arrays 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("right.arrays"),
-        30,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn foldl_strings(c: &mut Criterion) {
-    bench(
-        "foldl strings 50",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("left.strings"),
-        50,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn foldl_strings_deep(c: &mut Criterion) {
-    bench(
-        "foldl deepseq strings 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("left.strings"),
-        30,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn foldl_nums(c: &mut Criterion) {
-    bench(
-        "foldl nums 50",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("left.nums"),
-        50,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn foldl_nums_deep(c: &mut Criterion) {
-    bench(
-        "foldl deepseq nums 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("left.nums"),
-        30,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn foldl_arrays(c: &mut Criterion) {
-    bench(
-        "foldl arrays 50",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("left.arrays"),
-        50,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn foldl_arrays_deep(c: &mut Criterion) {
-    bench(
-        "foldl deepseq arrays 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/fold",
-        Some("left.arrays"),
-        30,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn generate_normal(c: &mut Criterion) {
-    bench(
-        "generate normal 50",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/generate",
-        None,
-        50,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn generate_deepseq(c: &mut Criterion) {
-    bench(
-        "generate deepseq 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/generate",
-        None,
-        30,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn map_normal(c: &mut Criterion) {
-    bench(
-        "map normal 50",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/map",
-        None,
-        50,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn map_deepseq(c: &mut Criterion) {
-    bench(
-        "map deepseq 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/map",
-        None,
-        30,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn pipe_normal(c: &mut Criterion) {
-    bench(
-        "pipe normal 20",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/pipe",
-        None,
-        20,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn pipe_deepseq(c: &mut Criterion) {
-    bench(
-        "pipe deepseq 20",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/pipe",
-        None,
-        20,
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn sort_normal(c: &mut Criterion) {
-    bench(
-        "sort normal",
-        env!("CARGO_MANIFEST_DIR"),
-        "arrays/sort",
-        None,
-        20,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-criterion_group! {
-    name = benches;
-    config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
-    targets = fold_strings, fold_strings_deep, fold_nums, fold_nums_deep, fold_arrays, fold_arrays_deep, foldl_strings, foldl_strings_deep, foldl_nums, foldl_nums_deep, foldl_arrays, foldl_arrays_deep, generate_normal, generate_deepseq, map_normal, map_deepseq, pipe_normal, pipe_deepseq, sort_normal
+ncl_bench_group! {
+name = benches;
+config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
+{
+        name = "foldr strings 50",
+        path = "arrays/fold",
+        subtest = "right.strings",
+        args = (50),
+    }, {
+        name = "foldr deepseq strings 30",
+        path = "arrays/fold",
+        subtest = "right.strings",
+        args = (30),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "foldr nums 50",
+        path = "arrays/fold",
+        subtest = "right.nums",
+        args = (50),
+    }, {
+        name = "foldr deepseq nums 30",
+        path = "arrays/fold",
+        subtest = "right.nums",
+        args = (30),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "foldr arrays 50",
+        path = "arrays/fold",
+        subtest = "right.arrays",
+        args = (50),
+    }, {
+        name = "foldr deepseq arrays 30",
+        path = "arrays/fold",
+        subtest = "right.arrays",
+        args = (30),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "foldl strings 50",
+        path = "arrays/fold",
+        subtest = "left.strings",
+        args = (50),
+    }, {
+        name = "foldl deepseq strings 30",
+        path = "arrays/fold",
+        subtest = "left.strings",
+        args = (30),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "foldl nums 50",
+        path = "arrays/fold",
+        subtest = "left.nums",
+        args = (50),
+    }, {
+        name = "foldl deepseq nums 30",
+        path = "arrays/fold",
+        subtest = "left.nums",
+        args = (30),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "foldl arrays 50",
+        path = "arrays/fold",
+        subtest = "left.arrays",
+        args = (50),
+    }, {
+        name = "foldl deepseq arrays 30",
+        path = "arrays/fold",
+        subtest = "left.arrays",
+        args = (30),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "generate normal 50",
+        path = "arrays/generate",
+        args = (50),
+    }, {
+        name = "generate deepseq 30",
+        path = "arrays/generate",
+        args = (30),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "map normal 50",
+        path = "arrays/map",
+        args = (50),
+    }, {
+        name = "map deepseq 30",
+        path = "arrays/map",
+        args = (30),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "pipe normal 20",
+        path = "arrays/pipe",
+        args = (20),
+    }, {
+        name = "pipe deepseq 20",
+        path = "arrays/pipe",
+        args = (20),
+        eval_mode = EvalMode::DeepSeq,
+    }, {
+        name = "sort normal",
+        path = "arrays/sort",
+        args = (20),
+    }
 }
 criterion_main!(benches);
diff --git a/benches/functions.rs b/benches/functions.rs
index 00cd5077..ceb2aca7 100644
--- a/benches/functions.rs
+++ b/benches/functions.rs
@@ -1,25 +1,14 @@
-use criterion::{criterion_group, criterion_main, Criterion};
-use nickel_lang::term::Term;
-use nickel_lang_utilities::{bench_expect, EvalMode};
+use criterion::{criterion_main, Criterion};
+use nickel_lang_utilities::ncl_bench_group;
 use pprof::criterion::{Output, PProfProfiler};
 
-fn church(c: &mut Criterion) {
-    let expect = |term| matches!(term, Term::Bool(true));
-    bench_expect(
-        "church 3",
-        env!("CARGO_MANIFEST_DIR"),
-        "functions/church",
-        None,
-        3,
-        EvalMode::Normal,
-        expect,
-        c,
-    );
-}
-
-criterion_group! {
+ncl_bench_group! {
     name = benches;
     config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
-    targets = church
+    {
+        name = "church 3",
+        path = "functions/church",
+        args = (3),
+    }
 }
 criterion_main!(benches);
diff --git a/benches/numeric.rs b/benches/numeric.rs
index 5e7bc31f..15b4f695 100644
--- a/benches/numeric.rs
+++ b/benches/numeric.rs
@@ -1,70 +1,32 @@
-use criterion::{criterion_group, criterion_main, Criterion};
-use nickel_lang_utilities::{bench, EvalMode};
+use criterion::{criterion_main, Criterion};
+use nickel_lang_utilities::ncl_bench_group;
 use pprof::criterion::{Output, PProfProfiler};
 
-fn fibonacci(c: &mut Criterion) {
-    bench(
-        "fibonacci 10",
-        env!("CARGO_MANIFEST_DIR"),
-        "numeric/fibonacci",
-        None,
-        10,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn pidigits(c: &mut Criterion) {
-    bench(
-        "pidigits 100",
-        env!("CARGO_MANIFEST_DIR"),
-        "numeric/pidigits",
-        None,
-        100,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn sum(c: &mut Criterion) {
-    bench(
-        "sum 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "numeric/reduce",
-        Some("sum"),
-        30,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn product(c: &mut Criterion) {
-    bench(
-        "product 30",
-        env!("CARGO_MANIFEST_DIR"),
-        "numeric/reduce",
-        Some("product"),
-        30,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-fn scalar(c: &mut Criterion) {
-    bench(
-        "scalar 10",
-        env!("CARGO_MANIFEST_DIR"),
-        "numeric/scalar",
-        None,
-        10,
-        EvalMode::Normal,
-        c,
-    );
-}
-
-criterion_group! {
+ncl_bench_group! {
     name = benches;
     config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
-    targets = fibonacci, pidigits, sum, product, scalar
+    {
+        name = "fibonacci 10",
+        path = "numeric/fibonacci",
+        args = (10),
+    }, {
+        name = "pidigits 100",
+        path = "numeric/pidigits",
+        args = (100),
+    }, {
+        name = "sum 30",
+        path = "numeric/reduce",
+        subtest = "sum",
+        args = (30),
+    }, {
+        name = "product 30",
+        path = "numeric/reduce",
+        subtest = "product",
+        args = (30),
+    }, {
+        name = "scalar 10",
+        path = "numeric/scalar",
+        args = (10),
+    }
 }
 criterion_main!(benches);
diff --git a/benches/records.rs b/benches/records.rs
index f5c9da3b..e73261dc 100644
--- a/benches/records.rs
+++ b/benches/records.rs
@@ -1,34 +1,20 @@
-use criterion::{criterion_group, criterion_main, Criterion};
-use nickel_lang_utilities::{bench_args, EvalMode};
+use criterion::{criterion_main, Criterion};
+use nickel_lang_utilities::{ncl_bench_group, EvalMode};
 use pprof::criterion::{Output, PProfProfiler};
 
-fn count_letters(c: &mut Criterion) {
-    bench_args(
-        "countLetters",
-        env!("CARGO_MANIFEST_DIR"),
-        "records/countLetters",
-        None,
-        vec![String::from(include_str!("lorem.txt"))],
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-fn merge(c: &mut Criterion) {
-    bench_args(
-        "merge",
-        env!("CARGO_MANIFEST_DIR"),
-        "records/merge",
-        None,
-        vec![String::from("500"), String::from("50")],
-        EvalMode::DeepSeq,
-        c,
-    );
-}
-
-criterion_group! {
+ncl_bench_group! {
     name = benches;
     config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
-    targets = count_letters, merge
+    {
+            name = "countLetters",
+            path = "records/countLetters",
+            args = (include_str!("lorem.txt")),
+            eval_mode = EvalMode::DeepSeq,
+        }, {
+            name = "merge",
+            path = "records/merge",
+            args = (500, 50),
+            eval_mode = EvalMode::DeepSeq,
+    }
 }
 criterion_main!(benches);
diff --git a/benches/serialization.rs b/benches/serialization.rs
index d25cd43b..ab0e99c5 100644
--- a/benches/serialization.rs
+++ b/benches/serialization.rs
@@ -1,25 +1,14 @@
-use criterion::{criterion_group, criterion_main, Criterion};
-use nickel_lang::program::Program;
+use criterion::{criterion_main, Criterion};
+use nickel_lang_utilities::ncl_bench_group;
 use pprof::criterion::{Output, PProfProfiler};
-use std::io::Cursor;
-use std::path::PathBuf;
 
-fn round_trip(c: &mut Criterion) {
-    let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
-    path.push("benches/serialization/input.json");
-
-    let content = format!(
-        "builtin.serialize `Json (import \"{}\")",
-        path.to_string_lossy(),
-    );
-
-    let mut p = Program::new_from_source(Cursor::new(content), "serialize_round_trip").unwrap();
-    c.bench_function("round_trip", |b| b.iter(|| p.eval().unwrap()));
-}
-
-criterion_group! {
+ncl_bench_group! {
     name = benches;
     config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
-    targets = round_trip
+    {
+        name = "round_trip",
+        path = "serialization/main",
+        subtest = "input",
+    }
 }
 criterion_main!(benches);
diff --git a/benches/serialization/main.ncl b/benches/serialization/main.ncl
new file mode 100644
index 00000000..da4d24a5
--- /dev/null
+++ b/benches/serialization/main.ncl
@@ -0,0 +1,6 @@
+
+{
+    input.run = (
+        builtin.serialize `Json (import "input.json")
+    )
+}
diff --git a/doc/manual/correctness.md b/doc/manual/correctness.md
index 2d1de0f3..57a1beea 100644
--- a/doc/manual/correctness.md
+++ b/doc/manual/correctness.md
@@ -356,4 +356,8 @@ You'll find a in-depth description of the type system and how to use it in the
 cheat-sheet about when to use static typing or contracts, go to
 [Type versus contracts: when to?](./types-vs-contracts.md).
 
-[^1]: This statement is to be nuanced. Using the Nickel LSP server in your editor, for example, does perform checks ahead of time, while you are typing program. You can also perform typechecking separately before distributing a configuration using `nickel typecheck`. However, a source program is not guaranteed to have been checked in any way before execution.
+[^1]: This statement is to be nuanced. Using the Nickel LSP server in your
+  editor, for example, does perform checks ahead of time, while you are typing
+  program. You can also perform typechecking separately before distributing a
+  configuration using `nickel typecheck`. However, a source program is not
+  guaranteed to have been checked in any way before execution.
diff --git a/doc/manual/introduction.md b/doc/manual/introduction.md
index 96d30bd7..a362228f 100644
--- a/doc/manual/introduction.md
+++ b/doc/manual/introduction.md
@@ -81,7 +81,7 @@ to be used in production. The next steps we plan to work on are:
 - Nix integration: being able to seamlessly use Nickel to write shells, packages
   and NixOS modules.
 - Custom merge functions and priorities (second part of the
-  [overriding proposal](https://github.com/tweag/nickel/blob/9fd6e436c0db8f101d4eb26cf97c4993357a7c38/rfcs/001-overriding.md))
+  [overriding proposal](https://github.com/tweag/nickel/blob/9fd6e436c0db8f101d4eb26cf97c4993357a7c38/rfcs/001-overriding.md)
 - Performance improvements
 
 ## Content
diff --git a/doc/manual/merging.md b/doc/manual/merging.md
index a6d8455d..c594728f 100644
--- a/doc/manual/merging.md
+++ b/doc/manual/merging.md
@@ -86,20 +86,20 @@ In other terms, `left & right` is the union of `left` and `right`.
 You can split a configuration into subdomains:
 
 ```nickel
-# file: server.ncl
+// file: server.ncl
 {
     host_name = "example",
     host = "example.org",
     ip_addr = "0.0.0.0",
 }
 
-# file: firewall.ncl
+// file: firewall.ncl
 {
     enable_firewall = true,
     open_ports = [23, 80, 443],
 }
 
-# file: network.ncl
+// file: network.ncl
 let server = import "server.ncl" in
 let firewall = import "firewall.ncl" in
 server & firewall
@@ -122,7 +122,7 @@ This gives:
 Given a configuration, you can use merge to add new fields:
 
 ```nickel
-# file: safe-network.ncl
+// file: safe-network.ncl
 let base = import "network.ncl" in
 base & {use_iptables = true}
 ```
@@ -213,19 +213,19 @@ v1 & v2 = v1    if (type_of(v1) is Num, Bool, Str, Enum or v1 == null)
 ### Example
 
 ```nickel
-# file: udp.ncl
+// file: udp.ncl
 {
-    # same as firewall = {open_ports = {udp = [...]}},
+    // same as firewall = {open_ports = {udp = [...]}},
     firewall.open_ports.udp = [12345,12346],
 }
 
-# file: tcp.ncl
+// file: tcp.ncl
 {
-    # same as firewall = {open_ports = {tcp = [...]}},
+    // same as firewall = {open_ports = {tcp = [...]}},
     firewall.open_ports.tcp = [23, 80, 443],
 }
 
-# firewall.ncl
+// firewall.ncl
 let udp = import "udp.ncl" in
 let tcp = import "tcp.ncl" in
 udp & tcp
@@ -449,7 +449,7 @@ during merging. For example, querying `foo` by using the command `nickel -f
 config.ncl query foo` on:
 
 ```nickel
-# config.ncl
+// config.ncl
 {
   foo | doc "Some documentation"
       | = {}
@@ -516,8 +516,8 @@ let security = {
         @ (if firewall.open_proto.ftp then [21] else [])
         @ (if firewall.open_proto.http then [80] else [])
         @ (if firewall.open_proto.https then [443] else []),
-} in # => security.firewall.open_ports = [21, 80, 443]
-security & {firewall.open_proto.ftp = false} # => firewall.open_ports = [80, 443]
+} in // => security.firewall.open_ports = [21, 80, 443]
+security & {firewall.open_proto.ftp = false} // => firewall.open_ports = [80, 443]
 ```
 
 Here, `security.firewall.open_ports` is `[21, 80, 443]`. But in the returned
diff --git a/doc/manual/syntax.md b/doc/manual/syntax.md
index 8e747f1c..437f6505 100644
--- a/doc/manual/syntax.md
+++ b/doc/manual/syntax.md
@@ -38,7 +38,7 @@ There are a some predefined operators for working with numbers :
 
 > **Remark about the `-` operator:**
 > Since `-` can be used inside an identifier, the subtraction operators **needs** to be surrounded by spaces:
-> write `a - b`, not `a-b`. `1-2` works as expected, because `1` and `2` are no identifiers.
+> write `1 - 1`, not `1-1`.
 
 Numbers can be compared using the following operators :
 | Operator | Description      | Example   |
@@ -263,8 +263,7 @@ Records are key-value storage, or in Nickel terms, field-value storage. They are
 Field-value elements are noted as `field = value`.
 The fields are strings, but can be written without quotes `"` if they respect identifiers syntax. Values can be of any type.
 Elements inside a record are unordered.
-Two records can be _merged_ together using the operator `&`. The reader can find
-more information about merging in the [section on merging](./merging.md).
+Two records can be _merged_ together using the operator `&`. The reader can find more information about merging in the relevant documentation.
 
 Examples:
 ```nickel
diff --git a/flake.lock b/flake.lock
index 651d0a77..3c302daf 100644
--- a/flake.lock
+++ b/flake.lock
@@ -32,11 +32,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1647297614,
-        "narHash": "sha256-ulGq3W5XsrBMU/u5k9d4oPy65pQTkunR4HKKtTq0RwY=",
+        "lastModified": 1646254136,
+        "narHash": "sha256-8nQx02tTzgYO21BP/dy5BCRopE8OwE8Drsw98j+Qoaw=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "73ad5f9e147c0d2a2061f1d4bd91e05078dc0b58",
+        "rev": "3e072546ea98db00c2364b81491b893673267827",
         "type": "github"
       },
       "original": {
@@ -87,11 +87,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1647656634,
-        "narHash": "sha256-f3VQSat3L2ObJm7DDr72HFCa9e7WAjKPqW70o2Bb7pg=",
+        "lastModified": 1646447076,
+        "narHash": "sha256-iGA3OueLeVPjyFM6LjDK1wIIci1DwsLh7CUCRc1qRbA=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "a21c163919cd90dd67bcd03345fac5441e53cccc",
+        "rev": "6923045c7b80aba6c81fe3eca7824a20c8724c0f",
         "type": "github"
       },
       "original": {
diff --git a/lsp/README.md b/lsp/README.md
index 7ff82547..ee667b3b 100644
--- a/lsp/README.md
+++ b/lsp/README.md
@@ -58,20 +58,16 @@ nix-env -f . -i
 
 ### Using Cargo
 
-If you already have a working [`cargo`](https://doc.rust-lang.org/cargo/)
-installation, you can make `nls` available globally without Nix:
+If you already have a working [`cargo`](https://doc.rust-lang.org/cargo/) installation, you can make `nls` available
+globally (it will be built and stored inside the repository) without
+Nix:
 
 ```
-cargo install nickel-lang-lsp
+git clone https://github.com/tweag/nickel.git
+cd nickel/lsp/nls
+cargo install --path .
 ```
 
-**WARNING**: the 0.1.0 version of the NLS crate
-([nickel-lang-lsp](https://crates.io/crates/nickel-lang-lsp)) doesn't
-correctly define the name of the binary as `nls`. If you can't find `nls` after
-a successful cargo installation, try to run `nickel-lang-lsp --version`. If this
-command is available, you'll have to substitute `nls` for `nickel-lang-lsp` in
-the instructions that follow.
-
 ## Interfacing with editors
 
 Once the `nls` binary is available, you can proceed with the configuration of
diff --git a/lsp/nls/Cargo.toml b/lsp/nls/Cargo.toml
index 607dfb5c..0def0403 100644
--- a/lsp/nls/Cargo.toml
+++ b/lsp/nls/Cargo.toml
@@ -1,18 +1,12 @@
 [package]
-name = "nickel-lang-lsp"
+name = "nickel-lang-nls"
 version = "0.1.0"
-authors = ["Nickel team"]
-license = "MIT"
+authors = ["Nicl team"]
+license = "MIT OR Apache-2.0"
 readme = "README.md"
-description = "NLS: A language server for the Nickel configuration language."
-repository = "https://github.com/tweag/nickel"
-keywords = ["nickel", "configuration", "language", "lsp"]
+description = "Programmable configuration files."
 edition = "2018"
 
-[[bin]]
-name = "nls"
-path = "src/main.rs"
-
 [build-dependencies]
 lalrpop = "0.19.6"
 
diff --git a/lsp/nls/LICENSE b/lsp/nls/LICENSE
deleted file mode 100644
index 4232985d..00000000
--- a/lsp/nls/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) Tweag Holding and its affiliates.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/lsp/nls/README.md b/lsp/nls/README.md
deleted file mode 100644
index 34b113b9..00000000
--- a/lsp/nls/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Nickel Language Server
-
-The Nickel Language Server (NLS) is a [language
-server](https://en.wikipedia.org/wiki/Language_Server_Protocol) for the
-[Nickel](https://www.nickel-lang.org/) programming language. NLS offers error
-messages, type hints, and auto-completion right in your favorite LSP-enabled
-editor.
-
-NLS is a stand-alone binary. Once built, you must then configure you code editor
-to use it for Nickel source files. This document covers building NLS and using
-it in VSCode and (Neo)Vim.
diff --git a/src/cache.rs b/src/cache.rs
index 6db78262..2235228b 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -55,7 +55,8 @@ impl InputFormat {
 /// - The term cache, holding parsed terms indexed by `FileId`s.
 ///
 /// Terms possibly undergo typechecking and program transformation. The state of each entry (that
-/// is, the operations that have been performed on this term) is stored in an [EntryState].
+/// is, the operations that have been performed on this term) is stored in an
+/// [`EntryState`](./enum.EntryState.html).
 #[derive(Debug, Clone)]
 pub struct Cache {
     /// The content of the program sources plus imports.
@@ -80,8 +81,8 @@ pub struct GlobalEnv {
     /// The eval environment.
     pub eval_env: eval::Environment,
     /// The typing environment, counterpart of the eval environment for typechecking. Entries are
-    /// [crate::typecheck::TypeWrapper] for the ease of interacting with the typechecker, but there
-    /// are not any unification variable in it.
+    /// [`TypeWrapper`](../typecheck/enum.TypeWrapper.html) for the ease of interacting with the
+    /// typechecker, but there are not any unification variable in it.
     pub type_env: typecheck::Environment,
 }
 
@@ -190,7 +191,7 @@ impl<E> CacheError<E> {
 /// Return status indicating if an import has been resolved from a file (first encounter), or was
 /// retrieved from the cache.
 ///
-/// See [ImportResolver::resolve].
+/// See [`resolve`](./trait.ImportResolver.html#tymethod.resolve).
 #[derive(Debug, PartialEq)]
 pub enum ResolvedTerm {
     FromFile {
@@ -222,8 +223,8 @@ impl Cache {
             .map(|_| self.files.add(path, buffer))
     }
 
-    /// Same as [Self::add_file], but assume that the path is already normalized, and take the
-    /// timestamp as a parameter.
+    /// Same as [`add_file`](#method.add_file), but assume that the path is already normalized,
+    /// and take the timestamp as a parameter.
     fn add_file_(
         &mut self,
         path: impl Into<OsString>,
@@ -254,8 +255,8 @@ impl Cache {
         self.add_file_(normalized, timestamp)
     }
 
-    /// Same as [get_or_add_file], but assume that the path is already normalized, and take the
-    /// timestamp as a parameter.
+    /// Same as [`get_or_add_file`](#method.get_or_add_file), but assume that the path is already
+    /// normalized, and take the timestamp as a parameter.
     fn get_or_add_file_(
         &mut self,
         path: impl Into<OsString>,
@@ -481,10 +482,10 @@ impl Cache {
     /// Apply program transformations to all the fields of a record.
     ///
     /// Used to transform stdlib modules and other records loaded in the environment, when using
-    /// e.g. the `load` command of the REPL. If one just uses [Self::transform], the share normal
-    /// form transformation would add let bindings to a record entry `{ ... }`, turning it into
-    /// `let %0 = ... in ... in { ... }`. But stdlib entries are required to be syntactically
-    /// records.
+    /// e.g. the `load` command of the REPL. If one just uses [`transform`](#method.transform), the
+    /// share normal form transformation would add let bindings to a record entry `{ ... }`,
+    /// turning it into `let %0 = ... in ... in { ... }`. But stdlib entries are required to be
+    /// syntactically records.
     ///
     /// Note that this requirement may be relaxed in the future by e.g. evaluating stdlib entries
     /// before adding their fields to the global environment.
@@ -661,9 +662,8 @@ impl Cache {
         Ok(result)
     }
 
-    /// Same as [Self::prepare], but do not use nor populate the cache. Used for inputs which are
-    /// known to not be reused.
-    ///
+    /// Same as [`prepare`](#method.prepare), but do not use nor populate the cache. Used for
+    /// inputs which are known to not be reused.
     /// In this case, the caller has to process the imports themselves as needed:
     /// - typechecking
     /// - resolve imports performed inside these imports.
@@ -690,8 +690,9 @@ impl Cache {
 
     /// Retrieve the id of a source given a name.
     ///
-    /// Note that files added via [Self::add_file] are indexed by their full normalized path (cf
-    /// [normalize_path]). When querying file, rather use [Self::id_of_file].
+    /// Note that files added via [`add_file`](#method.add_file) are indexed by their full
+    /// normalized path (cf [`normalize_path`](./fn.normalize_path.html)). When querying file,
+    /// rather use [`id_of_file`](#method.id_of_file).
     pub fn id_of(&self, name: impl AsRef<OsStr>) -> Option<FileId> {
         self.file_ids.get(name.as_ref()).map(|entry| entry.id)
     }
@@ -708,8 +709,8 @@ impl Cache {
         Ok(self.id_of_file_(normalized, timestamp))
     }
 
-    /// Retrieve the id of a file given a path. Same as [Self::id_of_file], but assume that the
-    /// given path is already normalized and take the timestamp as a parameter.
+    /// Retrieve the id of a file given a path. Same as [`id_of_file`](#method.id_of_file), but assume
+    /// that the given path is already normalized and take the timestamp as a parameter.
     fn id_of_file_(&self, path: impl AsRef<OsStr>, timestamp: SystemTime) -> Option<FileId> {
         self.file_ids
             .get(path.as_ref())
@@ -726,7 +727,7 @@ impl Cache {
     }
 
     /// Get a mutable reference to the underlying files. Required by
-    /// [crate::error::ToDiagnostic::to_diagnostic].
+    /// [`to_diagnostic`](../error/trait.ToDiagnostic.html#tymethod.to_diagnostic).
     pub fn files_mut(&mut self) -> &mut Files<String> {
         &mut self.files
     }
@@ -913,8 +914,9 @@ pub trait ImportResolver {
     /// Indeed, at import resolution phase, the term of an import encountered for the first time is
     /// queued to be processed (e.g. having its own imports resolved). The path is needed to
     /// resolve nested imports relatively to this parent. Only after this processing the term is
-    /// inserted back in the cache. On the other hand, if it has been resolved before, it is
-    /// already transformed in the cache and do not need further processing.
+    /// inserted back in the cache via [`insert`](#method.insert). On the other hand, if it has
+    /// been resolved before, it is already transformed in the cache and do not need further
+    /// processing.
     fn resolve(
         &mut self,
         path: &OsStr,
diff --git a/src/environment.rs b/src/environment.rs
index 5d9ed035..59f834c6 100644
--- a/src/environment.rs
+++ b/src/environment.rs
@@ -183,7 +183,7 @@ impl<'a, K: 'a + Hash + Eq, V: 'a + PartialEq> Iterator for EnvLayerIter<'a, K,
 
 /// An iterator over all the elements inside the `Environment`, from the oldest layer to the current one.
 ///
-/// Created by the [`Environment::iter_elems`] method.
+/// Created by the [`iter_elems`] method on [`Environment`].
 ///
 pub struct EnvElemIter<'a, K: 'a + Hash + Eq, V: 'a + PartialEq> {
     env: Vec<NonNull<HashMap<K, V>>>,
diff --git a/src/error.rs b/src/error.rs
index 4d2265ee..6c728fd8 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -163,11 +163,11 @@ pub enum TypecheckError {
     /// expression is inferred to be `{ field: Type | t}`, then `t` must not be unified later with
     /// a type including a different declaration for field, such as `field: Type2`.
     ///
-    /// A [constraint][crate::typecheck::RowConstr] is added accordingly, and if this constraint is
-    /// violated (that is if `t` does end up being unified with a type of the form
+    /// A [constraint](../typecheck/type.RowConstr.html) is added accordingly, and if this
+    /// constraint is violated (that is if `t` does end up being unified with a type of the form
     /// `{ .., field: Type2, .. }`), `RowConflict` is raised.  We do not have access to the
-    /// original `field: Type` declaration, as opposed to `RowKindMismatch`, which corresponds to
-    /// the direct failure to unify `{ .. , x: T1, .. }` and `{ .., x: T2, .. }`.
+    /// original `field: Type` declaration, as opposed to `RowKindMismatch`, which corresponds to the
+    /// direct failure to unify `{ .. , x: T1, .. }` and `{ .., x: T2, .. }`.
     RowConflict(
         Ident,
         /* the second type assignment which violates the constraint */ Option<Types>,
@@ -188,7 +188,7 @@ pub enum TypecheckError {
     /// let id_mono = fun x => x in let _ign = id_mono true in id_mono 0 : Num
     /// ```
     ///
-    /// This specific error stores additionally the [type path][crate::label::ty_path] that
+    /// This specific error stores additionally the [type path](../label/ty_path/index.html) that
     /// identifies the subtype where unification failed and the corresponding error.
     ArrowTypeMismatch(
         /* the expected arrow type */ Types,
@@ -542,17 +542,17 @@ pub trait ToDiagnostic<FileId> {
     /// # Arguments
     ///
     /// - `files`: to know why it takes a mutable reference to `Files<String>`, see
-    ///   `label_alt`.
+    ///   [`label_alt`](fn.label_alt.html).
     /// - `contract_id` is required to format the callstack when reporting blame errors. For some
-    ///   errors (such as [`ParseError`])), contracts may not have been loaded yet, hence the
-    ///   optional. See also [`crate::eval::callstack::CallStack::group_by_calls`].
+    ///   errors (such as [`ParseError`](./enum.ParseError.html)), contracts may not have been loaded
+    ///   yet, hence the optional. See also [`process_callstack`](fn.process_callstack.html).
     ///
     /// # Return
     ///
     /// Return a list of diagnostics. Most errors generate only one, but showing the callstack
     /// ordered requires to sidestep a limitation of codespan. The current solution is to generate
-    /// one diagnostic per callstack element. See issue
-    /// [#285](https://github.com/brendanzab/codespan/issues/285).
+    /// one diagnostic per callstack element. See [this
+    /// issue](https://github.com/brendanzab/codespan/issues/285).
     fn to_diagnostic(
         &self,
         files: &mut Files<String>,
@@ -643,7 +643,7 @@ fn label_alt(
 /// Create a secondary label from an optional span, or fallback to annotating the alternative snippet
 /// `alt_term` if the span is `None`.
 ///
-/// See [`label_alt`].
+/// See [`label_alt`](fn.label_alt.html).
 fn primary_alt(
     span_opt: Option<RawSpan>,
     alt_term: String,
@@ -655,7 +655,7 @@ fn primary_alt(
 /// Create a primary label from a term, or fallback to annotating the shallow representation of this term
 /// if its span is `None`.
 ///
-/// See [`label_alt`].
+/// See [`label_alt`](fn.label_alt.html).
 fn primary_term(term: &RichTerm, files: &mut Files<String>) -> Label<FileId> {
     primary_alt(term.pos.into_opt(), term.as_ref().shallow_repr(), files)
 }
@@ -663,7 +663,7 @@ fn primary_term(term: &RichTerm, files: &mut Files<String>) -> Label<FileId> {
 /// Create a secondary label from an optional span, or fallback to annotating the alternative snippet
 /// `alt_term` if the span is `None`.
 ///
-/// See [`label_alt`].
+/// See [`label_alt`](fn.label_alt.html).
 fn secondary_alt(span_opt: TermPos, alt_term: String, files: &mut Files<String>) -> Label<FileId> {
     label_alt(span_opt.into_opt(), alt_term, LabelStyle::Secondary, files)
 }
@@ -671,13 +671,14 @@ fn secondary_alt(span_opt: TermPos, alt_term: String, files: &mut Files<String>)
 /// Create a secondary label from a term, or fallback to annotating the shallow representation of this term
 /// if its span is `None`.
 ///
-/// See [`label_alt`].
+/// See [`label_alt`](fn.label_alt.html).
 fn secondary_term(term: &RichTerm, files: &mut Files<String>) -> Label<FileId> {
     secondary_alt(term.pos, term.as_ref().shallow_repr(), files)
 }
 
-/// Generate a codespan label that describes the [type path][crate::label::TyPath] of a (Nickel)
-/// label, and notes to hint at the situation that may have caused the corresponding error.
+/// Generate a codespan label that describes the [type path](../label/enum.TyPath.html) of a
+/// (Nickel) label, and notes to hint at the situation that may have caused the corresponding
+/// error.
 fn report_ty_path(l: &label::Label, files: &mut Files<String>) -> (Label<FileId>, Vec<String>) {
     let end_note = String::from("Note: this is an illustrative example. The actual error may involve deeper nested functions calls.");
 
@@ -951,13 +952,13 @@ impl ToDiagnostic<FileId> for EvalError {
                 // (either variable or field) as the name of the missing field.
                 let mut field: Option<String> = None;
                 let mut pos_record = TermPos::None;
-                let mut pos_access = TermPos::None;
+                let mut pos_access: Option<TermPos> = None;
 
                 for elt in callstack.as_ref().iter().rev() {
                     match elt {
                         StackElem::Var { id, pos, .. } if !id.is_generated() && field.is_none() => {
                             field = Some(id.to_string());
-                            pos_access = *pos;
+                            pos_access = Some(*pos);
                         }
                         StackElem::Field {
                             id,
@@ -965,8 +966,8 @@ impl ToDiagnostic<FileId> for EvalError {
                             pos_access: pos_acc,
                             ..
                         } => {
-                            field = Some(id.to_string());
-                            pos_access = *pos_acc;
+                            field.get_or_insert(id.to_string());
+                            pos_access.get_or_insert(*pos_acc);
                             pos_record = *pos_rec;
                             break;
                         }
@@ -980,7 +981,7 @@ impl ToDiagnostic<FileId> for EvalError {
                     labels.push(primary(&span).with_message("in this record"));
                 }
 
-                if let Some(span) = pos_access.into_opt() {
+                if let Some(span) = pos_access.map(TermPos::into_opt).flatten() {
                     labels.push(secondary(&span).with_message("accessed here"));
                 }
 
diff --git a/src/eval/fixpoint.rs b/src/eval/fixpoint.rs
index 1cb85fc7..9cfb0ab2 100644
--- a/src/eval/fixpoint.rs
+++ b/src/eval/fixpoint.rs
@@ -37,7 +37,7 @@ pub fn rec_env<'a, I: Iterator<Item = (&'a Ident, &'a RichTerm)>>(
 /// For each field, retrieve the set set of dependencies from the corresponding thunk in the
 /// environment, and only add those dependencies to the environment. This avoid retaining
 /// reference-counted pointers to unused data. If no dependencies are available, conservatively add
-/// all the recursive environment. See [`crate::transform::free_vars`].
+/// all the recursive environment. See [`transform::free_var`].
 pub fn patch_field(
     rt: &RichTerm,
     rec_env: &Vec<(Ident, Thunk)>,
diff --git a/src/eval/lazy.rs b/src/eval/lazy.rs
index 942a30c8..e4ef43f4 100644
--- a/src/eval/lazy.rs
+++ b/src/eval/lazy.rs
@@ -7,15 +7,13 @@ use std::rc::{Rc, Weak};
 
 /// The state of a thunk.
 ///
-/// When created, a thunk is flagged as suspended. When accessed for the first time, a
-/// corresponding [ThunkUpdateFrame] is pushed on the stack and the thunk is flagged as
-/// black-hole.
-///
-/// This prevents direct infinite recursions, since if a thunk is re-accessed while still in a
-/// black-hole state, we are sure that the evaluation will loop, and we can thus error out before
-/// overflowing the stack or looping forever. Finally, once the content of a thunk has been
-/// evaluated, the thunk is updated with the new value and flagged as evaluated, so that future
-/// accesses won't even push an update frame on the stack.
+/// When created, a thunk is flagged as suspended. When accessed for the first time, a corresponding
+/// [`ThunkUpdateFrame`](./struct.ThunkUpdateFrame.html) is pushed on the stack and the thunk is
+/// flagged as black-hole. This prevents direct infinite recursions, since if a thunk is
+/// re-accessed while still in a black-hole state, we are sure that the evaluation will loop, and
+/// we can thus error out before overflowing the stack or looping forever. Finally, once the
+/// content of a thunk has been evaluated, the thunk is updated with the new value and flagged as
+/// evaluated, so that future accesses won't even push an update frame on the stack.
 #[derive(Copy, Clone, Eq, PartialEq, Debug)]
 pub enum ThunkState {
     Blackholed,
@@ -35,7 +33,7 @@ pub struct ThunkData {
 /// - 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
+///   stores the set of recusive fields it depends on. See the [`transform::free_vars`] for more
 ///   details.
 #[derive(Clone, Debug, PartialEq)]
 pub enum InnerThunkData {
@@ -126,7 +124,7 @@ impl ThunkData {
         }
     }
 
-    /// Return the potential field dependencies stored in a revertible thunk. See [`crate::transform::free_vars`]
+    /// Return the potential field dependencies stored in a revertible thunk. See [`transform::free_vars`]
     pub fn deps(&self) -> ThunkDeps {
         match self.inner {
             InnerThunkData::Standard(_) => ThunkDeps::Empty,
@@ -248,7 +246,7 @@ impl Thunk {
     }
 
     /// Return a clone of the potential field dependencies stored in a revertible thunk. See
-    /// [`crate::transform::free_vars`].
+    /// [`transform::free_vars`].
     pub fn deps(&self) -> ThunkDeps {
         self.data.borrow().deps().clone()
     }
diff --git a/src/eval/merge.rs b/src/eval/merge.rs
index 06ff540e..7bc5262b 100644
--- a/src/eval/merge.rs
+++ b/src/eval/merge.rs
@@ -82,8 +82,8 @@ impl Default for MergeMode {
 ///
 /// # Mode
 ///
-/// In [`MergeMode::Contract`] mode, `t1` must be the value and `t2` must be the contract. It is
-/// important as `merge` is not commutative in this mode.
+/// In `Contract` mode (see [`MergingMode`]()), `t1` must be the value and `t2` must be the
+/// contract. It is important as `merge` is not commutative in this mode.
 pub fn merge(
     t1: RichTerm,
     mut env1: Environment,
@@ -119,10 +119,6 @@ pub fn merge(
 
     match (t1.into_owned(), t2.into_owned()) {
         // Merge is idempotent on basic terms
-        (Term::Null, Term::Null) => Ok(Closure::atomic_closure(RichTerm::new(
-            Term::Null,
-            pos_op.into_inherited(),
-        ))),
         (Term::Bool(b1), Term::Bool(b2)) => {
             if b1 == b2 {
                 Ok(Closure::atomic_closure(RichTerm::new(
@@ -223,9 +219,6 @@ pub fn merge(
                 ))
             }
         }
-        (Term::Array(arr1), Term::Array(arr2)) if arr1.is_empty() && arr2.is_empty() => Ok(
-            Closure::atomic_closure(RichTerm::new(Term::Array(arr1), pos_op.into_inherited())),
-        ),
         (Term::MetaValue(meta1), Term::MetaValue(meta2)) => {
             // For now, we blindly closurize things and copy environments in this section. A
             // careful analysis would make it possible to spare a few closurize operations and more
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 1a392861..3a272a13 100644
--- a/src/eval/mod.rs
+++ b/src/eval/mod.rs
@@ -8,31 +8,31 @@
 //! The abstract machine is a stack machine composed of the following elements:
 //! - The term being currently evaluated
 //! - The main stack, storing arguments, thunks and pending computations
-//! - A pair of [environment][Environment], mapping identifiers to [closures][Closure]:
+//! - A pair of [environments](type.Environment.html), mapping identifiers to [closures](type.Closure.html):
 //!     * The global environment contains builtin functions accessible from anywhere, and alive
 //!     during the whole evaluation
 //!     * The local environment contains the variables in scope of the current term and is subject
 //!     to garbage collection (currently reference counting based)
-//! - A [callstack], mainly for error reporting purpose
+//! - A [callstack](type.CallStack.html), mainly for error reporting purpose
 //!
 //! Depending on the shape of the current term, the following actions are preformed:
 //!
 //! ## Core calculus
 //! - **Var(id)**: the term bound to `id` in the environment is fetched, and an update thunk is
-//!   pushed on the stack to indicate that once this term has been evaluated, the content of the
-//!   variable must be updated
+//! pushed on the stack to indicate that once this term has been evaluated, the content of the
+//! variable must be updated
 //! - **App(func, arg)**: a closure containing the argument and the current environment is pushed
-//!   on the stack, and the applied term `func` is evaluated
+//! on the stack, and the applied term `func` is evaluated
 //! - **Let(id, term, body)**: `term` is bound to `id` in the environment, and the machine proceeds with the evaluation of the body
 //! - **Fun(id, body)**: Try to pop an argument from the stack. If there is some, we bound it to
-//!   `id` in the environment, and proceed with the body of the function. Otherwise, we are done: the
-//!   end result is an unapplied function
+//! `id` in the environment, and proceed with the body of the function. Otherwise, we are done: the
+//! end result is an unapplied function
 //! - **Thunk on stack**: If the evaluation of the current term is done, and there is one (or
-//!   several) thunk on the stack, this means we have to perform an update. Consecutive thunks are
-//!   popped from the stack and are updated to point to the current evaluated term.
+//! several) thunk on the stack, this means we have to perform an update. Consecutive thunks are
+//! popped from the stack and are updated to point to the current evaluated term.
 //! - **Import**: Import must have been resolved before the evaluation starts. An unresolved import
-//!   causes an [`crate::error::EvalError::InternalError`]. A resolved import, identified by a
-//!   `FileId`, is retrieved from the import resolver and evaluation proceeds.
+//! causes an [`InternalError`](../error/enum.EvalError.html#variant.InternalError). A resolved
+//! import, identified by a `FileId`, is retrieved from the import resolver and evaluation proceeds.
 //!
 //! ## Contracts
 //!
@@ -54,30 +54,30 @@
 //! `op`, the second argument `second` and the current environment, and proceed with the evaluation
 //! of `first`
 //! - **OpFirst on stack**: if the evaluation of the current term is done and there is an `OpFirst`
-//!   marker on the stack, then:
+//! marker on the stack, then:
 //!     1. Extract the saved operator, the second argument and the environment `env2` from the marker
 //!     2. Push an `OpSecond` marker, saving the operator and the evaluated form of the first
 //!        argument with its environment
 //!     3. Proceed with the evaluation of the second argument in environment `env2`
 //! - **OpSecond on stack**: once the second term is evaluated, we can get back the operator and
-//!   the first term evaluated, and forward all both arguments evaluated and their respective
-//!   environment to the specific implementation of the operator (located in [operation], or in
-//!   [merge] for `merge`).
+//! the first term evaluated, and forward all both arguments evaluated and their respective
+//! environment to the specific implementation of the operator (located in
+//! [operation](../operation/index.html), or in [merge](../merge/index.html) for `merge`).
 //!
 //! ## Enriched values
 //!
 //! The evaluation of enriched values is controlled by the parameter `enriched_strict`. If it is
 //! set to true (which is usually the case), the machine tries to extract a simple value from it:
 //!  - **Contract**: raise an error. This usually means that an access to a field was attempted,
-//!    and that this field had a contract to satisfy, but it was never defined.
+//!  and that this field had a contract to satisfy, but it was never defined.
 //!  - **Default(value)**: an access to a field which has a default value. Proceed with the
-//!    evaluation of this value
+//!  evaluation of this value
 //!  - **ContractDefault(type, label, value)**: same as above, but the field also has an attached
-//!    contract.  Proceed with the evaluation of `Assume(type, label, value)` to ensure that the
-//!    default value satisfies this contract.
+//!  contract.  Proceed with the evaluation of `Assume(type, label, value)` to ensure that the
+//!  default value satisfies this contract.
 //!
-//! If `enriched_strict` is set to false, as it is when evaluating `merge`, the machine does not
-//! evaluate enriched values further, and consider the term evaluated.
+//!  If `enriched_strict` is set to false, as it is when evaluating `merge`, the machine does not
+//!  evaluate enriched values further, and consider the term evaluated.
 //!
 //! # Garbage collection
 //!
@@ -177,8 +177,8 @@ pub fn env_add(env: &mut Environment, id: Ident, rt: RichTerm, local_env: Enviro
     env.insert(id, Thunk::new(closure, IdentKind::Let));
 }
 
-/// Evaluate a Nickel term. Wrapper around [eval_closure] that starts from an empty local
-/// environment and drops the final environment.
+/// Evaluate a Nickel term. Wrapper around [eval_closure](fn.eval_closure.html) that starts from an
+/// empty local environment and drops the final environment.
 pub fn eval<R>(
     t0: RichTerm,
     global_env: &Environment,
@@ -272,7 +272,8 @@ where
 ///
 /// Implement the evaluation of the core language, which includes application, thunk update,
 /// evaluation of the arguments of operations, and a few others. The specific implementations of
-/// primitive operations is delegated to the modules [operation] and [merge].
+/// primitive operations is delegated to the modules [operation](../operation/index.html) and
+/// [merge](../merge/index.html).
 ///
 /// # Arguments
 ///
diff --git a/src/eval/operation.rs b/src/eval/operation.rs
index 09d49171..75cb4e86 100644
--- a/src/eval/operation.rs
+++ b/src/eval/operation.rs
@@ -1,11 +1,11 @@
 //! Implementation of primitive operations.
 //!
 //! Define functions which perform the evaluation of primitive operators. The machinery required
-//! for the strict evaluation of the operands is mainly handled by [crate::eval], and marginally in
-//! [`continuate_operation`].
-//!
-//! On the other hand, the functions `process_unary_operation` and `process_binary_operation`
-//! receive evaluated operands and implement the actual semantics of operators.
+//! for the strict evaluation of the operands is mainly handled by [`eval`](../eval/index.html),
+//! and marginally in [`continuate_operation`](fn.continuate_operation.html). On the other hand,
+//! the functions [`process_unary_operation`](fn.process_unary_operation.html) and
+//! [`process_binary_operation`](fn.process_binary_operation.html) receive evaluated operands and
+//! implement the actual semantics of operators.
 use super::{
     callstack, merge,
     merge::{merge, MergeMode},
@@ -40,7 +40,7 @@ generate_counter!(FreshVariableCounter, usize);
 /// first element of this non-empty list is encoded as the two first parameters of `Eqs`, while the
 /// last vector parameter is the (potentially empty) tail.
 ///
-/// See [`eq`].
+/// See [`eq`](./fn.eq.html).
 enum EqResult {
     Bool(bool),
     Eqs(RichTerm, RichTerm, Vec<(Closure, Closure)>),
diff --git a/src/label.rs b/src/label.rs
index 3462b5d5..efc788ba 100644
--- a/src/label.rs
+++ b/src/label.rs
@@ -70,7 +70,8 @@ pub mod ty_path {
     /// Return the position span encoded by a type path in the string representation of the
     /// corresponding type.
     ///
-    /// Used in the error reporting of blame errors (see `crate::error::report_ty_path`).
+    /// Used in the error reporting of blame errors (see
+    /// [report_ty_path](../error/fn.report_ty_path.html)).
     ///
     /// # Example
     ///
@@ -197,7 +198,7 @@ pub mod ty_path {
 ///
 /// A label is associated to a contract check (an assume, a promise or a contract as an enriched
 /// value) and contains information to report to the user when a contract fails and a blame occurs.
-/// It includes in particular a [type path][ty_path::Path] and a **polarity**.
+/// It includes in particular a [type path](enum.TyPath.html) and a **polarity**.
 ///
 /// # Polarity
 ///
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index 704d6f29..fe8c1332 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -418,7 +418,7 @@ pub struct Lexer<'input> {
     pub stack: Vec<ModeElt>,
     /// A token that has been buffered and must be returned at the next call to `next()`. This is
     /// made necessary by an issue of Logos (<https://github.com/maciejhirsz/logos/issues/200>). See
-    /// [`MultiStringToken::QuotesCandidateInterpolation`].
+    /// `MultiStringToken::QuotesCandidateInterpolation`.
     pub buffer: Option<(Token<'input>, Range<usize>)>,
 }
 
@@ -520,35 +520,6 @@ impl<'input> Lexer<'input> {
             _ => panic!("lexer::leave_normal"),
         }
     }
-
-    /// Split a candidate interpolation token into a string literal and an interpolation token. Put
-    /// the interpolation token in the buffer to be popped later, and return the literal as the
-    /// next token.
-    ///
-    /// # Precondition
-    ///
-    /// - this function requires `s.len() >= self.count`, or will panic.
-    fn split_candidate_interp(
-        &mut self,
-        s: &'input str,
-        span: Range<usize>,
-    ) -> (Option<Token<'input>>, Range<usize>) {
-        let split_at = s.len() - self.count + 1;
-        let next_token = Token::MultiStr(MultiStringToken::Interpolation);
-        let next_span = Range {
-            start: span.start + split_at,
-            end: span.end,
-        };
-        self.buffer = Some((next_token, next_span));
-
-        let token = Some(Token::MultiStr(MultiStringToken::Literal(&s[0..split_at])));
-        let span = Range {
-            start: span.start,
-            end: span.start + split_at,
-        };
-
-        (token, span)
-    }
 }
 
 impl<'input> Iterator for Lexer<'input> {
@@ -593,22 +564,11 @@ impl<'input> Iterator for Lexer<'input> {
             }
             // If we encounter a `CandidateInterp` token with the right number of characters, this is
             // an interpolation sequence.
-            //
-            // Note that the number of characters may be greater than `count`: in `m"%
-            // %%%{foo} "%m`, the lexer will process `%%%{` as a candidate interpolation with 4
-            // characters, while `count` is 1. In that case, we must emit an `%%` literal and put
-            // an interpolation token in the buffer.
             Some(MultiStr(MultiStringToken::CandidateInterpolation(s)))
-                if s.len() >= (self.count - 1) =>
+                if s.len() == (self.count - 1) =>
             {
-                if s.len() == (self.count - 1) {
-                    token = Some(MultiStr(MultiStringToken::Interpolation));
-                    self.enter_normal();
-                } else {
-                    let (token_fst, span_fst) = self.split_candidate_interp(s, span);
-                    token = token_fst;
-                    span = span_fst;
-                }
+                token = Some(MultiStr(MultiStringToken::Interpolation));
+                self.enter_normal();
             }
             // We never lex something as a `MultiStringToken::Interpolation` directly, but rather
             // generate it in this very function from other tokens. However, such a token could
@@ -627,9 +587,19 @@ impl<'input> Iterator for Lexer<'input> {
             Some(MultiStr(MultiStringToken::QuotesCandidateInterpolation(s)))
                 if s.len() >= self.count =>
             {
-                let (token_fst, span_fst) = self.split_candidate_interp(s, span);
-                token = token_fst;
-                span = span_fst;
+                let split_at = s.len() - self.count + 1;
+                let next_token = MultiStr(MultiStringToken::Interpolation);
+                let next_span = Range {
+                    start: span.start + split_at,
+                    end: span.end,
+                };
+                self.buffer = Some((next_token, next_span));
+
+                token = Some(MultiStr(MultiStringToken::Literal(&s[0..split_at])));
+                span = Range {
+                    start: span.start,
+                    end: span.start + split_at,
+                };
             }
             // Otherwise, it is just part of the string, so we transform the token into a
             // `FalseInterpolation` one
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 5f311556..d01afdf0 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -18,13 +18,6 @@ pub mod utils;
 mod tests;
 
 /// Either a term or a toplevel let declaration.
-/// Used exclusively in the REPL to allow the defining of variables without having to specify `in`.
-/// For instance:
-/// ```text
-/// nickel>let foo = 1
-/// nickel>foo
-/// 1
-/// ```
 pub enum ExtendedTerm {
     RichTerm(RichTerm),
     ToplevelLet(Ident, RichTerm),
diff --git a/src/parser/uniterm.rs b/src/parser/uniterm.rs
index e09d9951..a2e6d5c6 100644
--- a/src/parser/uniterm.rs
+++ b/src/parser/uniterm.rs
@@ -342,7 +342,7 @@ impl TryFrom<UniRecord> for Types {
 /// we have to wait until we eventually convert the unirecord to a term (in which case we fix all the
 /// top-level annotations) or a type (in which case we do nothing: the enclosing type will trigger
 /// the fix once it's fully constructed). Fixing a unirecord prior to a conversion to a term is
-/// done by [`fix_field_types`].
+/// done by [`fix_fields_types`].
 pub fn fix_type_vars(ty: &mut Types) {
     fn fix_type_vars_aux(ty: &mut Types, mut bound_vars: Cow<HashSet<Ident>>) {
         match ty.0 {
diff --git a/src/parser/utils.rs b/src/parser/utils.rs
index 21d09f57..aa7c28ff 100644
--- a/src/parser/utils.rs
+++ b/src/parser/utils.rs
@@ -360,9 +360,10 @@ pub fn min_indent(chunks: &[StrChunk<RichTerm>]) -> usize {
 
 /// Strip the common indentation prefix from a multi-line string.
 ///
-/// Determine the minimum indentation level of a multi-line string via [`min_indent`], and strip an
-/// equal number of whitespace characters (` ` or `\t`) from the beginning of each line. If the
-/// last line is empty or consist only of whitespace characters, it is filtered out.
+/// Determine the minimum indentation level of a multi-line string via
+/// [`min_indent`](./fn.min_indent.html), and strip an equal number of whitespace characters (` `
+/// or `\t`) from the beginning of each line. If the last line is empty or consist only of
+/// whitespace characters, it is filtered out.
 ///
 /// The indentation of interpolated expressions in a multi-line string follow the rules:
 /// - if an interpolated expression is alone on a line with whitespaces, its indentation -- minus
@@ -523,7 +524,7 @@ pub fn strip_indent(mut chunks: Vec<StrChunk<RichTerm>>) -> Vec<StrChunk<RichTer
 }
 
 /// Strip the indentation of a documentation metavalue. Wrap it as a literal string chunk and call
-/// [`strip_indent`].
+/// [`strip_indent`](./fn.strip_indent.html).
 pub fn strip_indent_doc(doc: String) -> String {
     let chunk = vec![StrChunk::Literal(doc)];
     strip_indent(chunk)
diff --git a/src/program.rs b/src/program.rs
index 82071f15..692cd7c1 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -16,10 +16,11 @@
 //! ```
 //!
 //! These .ncl file are not actually distributed as files, instead they are embedded, as plain
-//! text, in the Nickel executable. The embedding is done by way of the [crate::stdlib], which
-//! exposes the standard library files as strings. The embedded strings are then parsed by the
-//! functions in [`crate::cache`] (see [`crate::cache::Cache::mk_eval_env`]).
-//! Each such value is added to the global environment before the evaluation of the program.
+//! text, in the Nickel executable. The embedding is done by way of the [stdlib
+//! module](../stdlib/index.html), which exposes the standard library files as strings. The
+//! embedded strings are then parsed by the functions in this module (see
+//! [`mk_global_env`](./struct.Program.html#method.mk_global_env)).  Each such value is added to
+//! the global environment before the evaluation of the program.
 use crate::cache::*;
 use crate::error::{Error, ToDiagnostic};
 use crate::identifier::Ident;
@@ -94,7 +95,7 @@ impl Program {
         eval::eval_deep(t, &global_env, &mut self.cache).map_err(|e| e.into())
     }
 
-    /// Wrapper for [`query`].
+    /// Wrapper for [`query`](./fn.query.html).
     pub fn query(&mut self, path: Option<String>) -> Result<Term, Error> {
         let global_env = self.cache.prepare_stdlib()?;
         query(&mut self.cache, self.main_id, &global_env, path)
@@ -118,7 +119,7 @@ impl Program {
         Ok(())
     }
 
-    /// Wrapper for [`report`].
+    /// Wrapper for [`report`](./fn.report.html).
     pub fn report<E>(&mut self, error: E)
     where
         E: ToDiagnostic<FileId>,
@@ -182,8 +183,8 @@ pub fn query(
 
 /// Pretty-print an error.
 ///
-/// This function is located here in `Program` because errors need a reference to `files` in order
-/// to produce a diagnostic (see `crate::error::label_alt`).
+/// This function is located here in `Program` because errors need a reference to `files` in
+/// order to produce a diagnostic (see [`label_alt`](../error/fn.label_alt.html)).
 //TODO: not sure where this should go. It seems to embed too much logic to be in `Cache`, but is
 //common to both `Program` and `Repl`. Leaving it here as a stand-alone function for now
 pub fn report<E>(cache: &mut Cache, error: E)
diff --git a/src/repl/mod.rs b/src/repl/mod.rs
index 12d2aa6e..210ca64c 100644
--- a/src/repl/mod.rs
+++ b/src/repl/mod.rs
@@ -57,7 +57,7 @@ pub trait Repl {
     fn eval_full(&mut self, exp: &str) -> Result<EvalResult, Error>;
     /// Load the content of a file in the environment. Return the loaded record.
     fn load(&mut self, path: impl AsRef<OsStr>) -> Result<RichTerm, Error>;
-    /// Typecheck an expression and return its [apparent type][crate::typecheck::ApparentType].
+    /// Typecheck an expression and return its [apparent type](../typecheck/fn.apparent_type.html).
     fn typecheck(&mut self, exp: &str) -> Result<Types, Error>;
     /// Query the metadata of an expression.
     fn query(&mut self, exp: &str) -> Result<Term, Error>;
diff --git a/src/repl/query_print.rs b/src/repl/query_print.rs
index cc6b5b6a..3d0c777d 100644
--- a/src/repl/query_print.rs
+++ b/src/repl/query_print.rs
@@ -152,9 +152,10 @@ impl Default for Attributes {
 }
 
 /// Print the result of a metadata query, which is a "weakly" evaluated term (see
-/// [`crate::eval::eval_meta`] and [`crate::program::query`]).
+/// [`eval_meta`](../../eval/fn.eval_meta.html) and [`query`](../../program/fn.query.html)).
 ///
-/// Wrapper around `write_query_result_` that selects an adapated query printer at compile time.
+/// Wrapper around [`write_query_result_`](./fn.write_query_result_) that selects an adapated
+/// query printer at compile time.
 pub fn write_query_result(
     out: &mut impl Write,
     term: &Term,
@@ -170,7 +171,7 @@ pub fn write_query_result(
 }
 
 /// Print the result of a metadata query, which is a "weakly" evaluated term (see
-/// [`crate::eval::eval_meta`] and [`crate::program::query`]).
+/// [`eval_meta`](../../eval/fn.eval_meta.html) and [`query`](../../program/fn.query.html)).
 fn write_query_result_<R: QueryPrinter>(
     out: &mut impl Write,
     term: &Term,
diff --git a/src/term.rs b/src/term.rs
index 39446fce..00bc6243 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -14,8 +14,8 @@
 //!
 //! Enriched values are special terms used to represent metadata about record fields: types or
 //! contracts, default values, documentation, etc. They bring such usually external object down to
-//! the term level, and together with [crate::eval::merge], they allow for flexible and modular
-//! definitions of contracts, record and metadata all together.
+//! the term level, and together with [merge](../merge/index.html), they allow for flexible and
+//! modular definitions of contracts, record and metadata all together.
 use crate::destruct::Destruct;
 use crate::identifier::Ident;
 use crate::label::Label;
@@ -33,7 +33,7 @@ use std::rc::Rc;
 /// The AST of a Nickel expression.
 ///
 /// Parsed terms also need to store their position in the source for error reporting.  This is why
-/// this type is nested with [`RichTerm`].
+/// this type is nested with [`RichTerm`](type.RichTerm.html).
 ///
 #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
 #[serde(untagged)]
@@ -165,12 +165,12 @@ impl From<MetaValue> for Term {
 
 /// Type of let-binding. This only affects run-time behavior. Revertible bindings introduce
 /// revertible thunks at evaluation, which are devices used for the implementation of recursive
-/// records merging. See the [`crate::eval::merge`] and [`crate::eval`] modules for more details.
+/// records merging. See the [`merge`] and [`eval`] modules for more details.
 #[derive(Debug, Eq, PartialEq, Clone)]
 pub enum BindingType {
     Normal,
     /// In the revertible case, we also store an optional set of dependencies. See
-    /// [`crate::transform::free_vars`] for more details.
+    /// [`transform::free_vars`] for more details.
     Revertible(FieldDeps),
 }
 
@@ -710,7 +710,7 @@ pub enum UnaryOp {
     Pol(),
     /// Go to the domain in the type path of a label.
     ///
-    /// If the argument is a label with a [type path][crate::label::TyPath) representing some
+    /// If the argument is a label with a [type path](../label/enum.TyPath.html) representing some
     /// subtype of the type of the original contract, as in:
     ///
     /// ```text
@@ -736,7 +736,7 @@ pub enum UnaryOp {
     /// See `GoDom`.
     GoArray(),
 
-    /// Wrap a term with a type tag (see [`Term::Wrapped`]).
+    /// Wrap a term with a type tag (see `Wrapped` in [`Term`](enum.Term.html)).
     Wrap(),
 
     /// Force the evaluation of its argument and proceed with the second.
@@ -830,7 +830,7 @@ pub enum BinaryOp {
     Assume(),
     /// Unwrap a tagged term.
     ///
-    /// See [`UnaryOp::Wrap`].
+    /// See `Wrap` in [`UnaryOp`](enum.UnaryOp.html).
     Unwrap(),
     /// Go to a specific field in the type path of a label.
     ///
@@ -855,7 +855,7 @@ pub enum BinaryOp {
     ArrayConcat(),
     /// Access the n-th element of an array.
     ArrayElemAt(),
-    /// The merge operator (see [crate::eval::merge]).
+    /// The merge operator (see the [merge module](../merge/index.html)).
     Merge(),
 
     /// Hash a string.
@@ -891,12 +891,15 @@ impl BinaryOp {
 pub enum NAryOp {
     /// Replace a substring by another one in a string.
     StrReplace(),
-    /// Same as [`NAryOp::StrReplace`], but the pattern is interpreted as a regular expression.
+    /// Same as [`StrReplace()`], but the pattern is interpreted as a regular expression.
+    ///
+    /// [`StrReplace()`]: NAryOp::StrReplace
     StrReplaceRegex(),
     /// Return a substring of an original string.
     StrSubstr(),
-    /// The merge operator in contract mode (see [crate::eval::merge]). The arguments are in order
-    /// the contract's label, the value to check, and the contract as a record.
+    /// The merge operator in contract mode (see the [merge module](../merge/index.html)). The
+    /// arguments are in order the contract's label, the value to check, and the contract as a
+    /// record.
     MergeContract(),
 }
 
@@ -932,7 +935,7 @@ pub enum TraverseOrder {
     BottomUp,
 }
 
-/// Wrap [Term] with positional information.
+/// Wrap [terms](type.Term.html) with positional information.
 #[derive(Debug, PartialEq, Clone)]
 pub struct RichTerm {
     pub term: SharedTerm,
diff --git a/src/transform/import_resolution.rs b/src/transform/import_resolution.rs
index 06184a97..f79a8505 100644
--- a/src/transform/import_resolution.rs
+++ b/src/transform/import_resolution.rs
@@ -62,7 +62,8 @@ where
 }
 
 /// Resolve the import if the term is an unresolved import, or return the term unchanged. As
-/// [`super::share_normal_form::transform_one`], this function is not recursive.
+/// [`share_normal_form::transform_one`](../share_normal_form/fn.transform_one.html), this function
+/// is not recursive.
 pub fn transform_one<R>(
     rt: RichTerm,
     resolver: &mut R,
diff --git a/src/transform/mod.rs b/src/transform/mod.rs
index bc3c3fad..acbf2895 100644
--- a/src/transform/mod.rs
+++ b/src/transform/mod.rs
@@ -21,8 +21,8 @@ pub mod share_normal_form;
 /// earlier, as it needs to be done before typechecking.
 ///
 /// Do not perform transformations on the imported files. If needed, either do it yourself using
-/// pending imports returned by [`resolve_imports`][import_resolution::resolve_imports] or use the
-/// [cache][crate::cache::Cache].
+/// pending imports returned by [`resolve_imports`](../fn.resolve_imports.html) or use the
+/// [`Cache`](../../cache/struct.Cache.html)
 pub fn transform(mut rt: RichTerm) -> Result<RichTerm, UnboundTypeVariableError> {
     free_vars::transform(&mut rt);
     transform_no_free_vars(rt)
@@ -63,8 +63,8 @@ pub fn fresh_var() -> Ident {
 
 /// Structures which can be packed together with their environment as a closure.
 ///
-/// The typical implementer is [`crate::term::RichTerm`], but structures containing
-/// terms can also be closurizable, such as the contract in a [`crate::types::Types`].
+/// The typical implementer is [`RichTerm`](../term/enum.RichTerm.html), but structures containing
+/// terms can also be closurizable, such as the contract in a [`Types`](../types/typ.Types.html).
 /// In this case, the inner term is closurized.
 pub trait Closurizable {
     /// Pack a closurizable together with its environment `with_env` as a closure in the main
diff --git a/src/transform/share_normal_form.rs b/src/transform/share_normal_form.rs
index c1876872..61bf8784 100644
--- a/src/transform/share_normal_form.rs
+++ b/src/transform/share_normal_form.rs
@@ -82,7 +82,7 @@ pub fn transform_one(rt: RichTerm) -> RichTerm {
                 // the sare normal form transformation ensure the following post-condition: the
                 // fields of recursive records contain either a constant or a *generated* variable,
                 // but never a user-supplied variable directly (the former starts with a special
-                // marker). See comments inside [`crate::RichTerm::closurize`] for more details.
+                // marker). See comments inside [`RichTerm::closurize`] for more details.
                 let mut bindings = Vec::with_capacity(map.len());
 
                 fn mk_binding_type(field_deps: Option<HashSet<Ident>>) -> BindingType {
diff --git a/src/typecheck/error.rs b/src/typecheck/error.rs
index 792a4bb9..05136420 100644
--- a/src/typecheck/error.rs
+++ b/src/typecheck/error.rs
@@ -30,7 +30,7 @@ pub enum RowUnifError {
     /// typechecking algorithm. We let it as an error for now, but it could be removed in the
     /// future.
     IllformedRow(TypeWrapper),
-    /// A [row constraint][super::RowConstr] was violated.
+    /// A [row constraint](./type.RowConstr.html) was violated.
     UnsatConstr(Ident, Option<TypeWrapper>),
     /// Tried to unify a type constant with another different type.
     WithConst(usize, TypeWrapper),
@@ -44,9 +44,9 @@ impl RowUnifError {
     /// Convert a row unification error to a unification error.
     ///
     /// There is a hierarchy between error types, from the most local/specific to the most high-level:
-    /// - [`RowUnifError`]
-    /// - [`UnifError`]
-    /// - [`crate::error::TypecheckError`]
+    /// - [`RowUnifError`](./enum.RowUnifError.html)
+    /// - [`UnifError`](./enum.UnifError.html)
+    /// - [`TypecheckError`](../errors/enum.TypecheckError.html)
     ///
     /// Each level usually adds information (such as types or positions) and group different
     /// specific errors into most general ones.
@@ -91,7 +91,7 @@ pub enum UnifError {
     /// A row was ill-formed.
     IllformedRow(TypeWrapper),
     /// Tried to unify a unification variable with a row type violating the [row
-    /// constraints][super::RowConstr] of the variable.
+    /// constraints](./type.RowConstr.html) of the variable.
     RowConflict(Ident, Option<TypeWrapper>, TypeWrapper, TypeWrapper),
     /// Tried to unify a type constant with another different type.
     WithConst(usize, TypeWrapper),
@@ -112,8 +112,8 @@ pub enum UnifError {
 impl UnifError {
     /// Convert a unification error to a typechecking error.
     ///
-    /// Wrapper that calls [`Self::into_typecheck_err_`] with an empty [name
-    /// registry][reporting::NameReg].
+    /// Wrapper that calls [`to_typecheck_err_`](./fn.to_typecheck_err_.html) with an empty [name
+    /// registry](./reporting/struct.NameReg.html).
     pub fn into_typecheck_err(self, state: &State, pos_opt: TermPos) -> TypecheckError {
         self.into_typecheck_err_(state, &mut reporting::NameReg::new(), pos_opt)
     }
@@ -121,9 +121,9 @@ impl UnifError {
     /// Convert a unification error to a typechecking error.
     ///
     /// There is a hierarchy between error types, from the most local/specific to the most high-level:
-    /// - [`RowUnifError`]
-    /// - [`UnifError`]
-    /// - [`crate::error::TypecheckError`]
+    /// - [`RowUnifError`](./enum.RowUnifError.html)
+    /// - [`UnifError`](./enum.UnifError.html)
+    /// - [`TypecheckError`](../errors/enum.TypecheckError.html)
     ///
     /// Each level usually adds information (such as types or positions) and group different
     /// specific errors into most general ones.
@@ -132,7 +132,7 @@ impl UnifError {
     ///
     /// - `state`: the state of unification. Used to access the unification table, and the original
     /// names of of unification variable or type constant.
-    /// - `names`: a [name registry][reporting::NameReg], structure used to assign
+    /// - `names`: a [name registry](./reporting/struct.NameReg.html), structure used to assign
     /// unique a humain-readable names to unification variables and type constants.
     /// - `pos_opt`: the position span of the expression that failed to typecheck.
     pub fn into_typecheck_err_(
diff --git a/src/typecheck/linearization.rs b/src/typecheck/linearization.rs
index 3b563bce..db0f4aab 100644
--- a/src/typecheck/linearization.rs
+++ b/src/typecheck/linearization.rs
@@ -13,13 +13,14 @@
 //! # Components
 //!
 //! - [Linearizer]: Implements functionality to record terms (integrated into typechecking)
-//!                 into a temporary [Linearizer::Building] structure and linearize them into a
-//!                 [Linearizer::Completed] Linearization.
+//!                 into a temporary [Building] structure and linearize them into a
+//!                 [Completed] Linearization.
 //!                 Additionally handles registration in different scopes.
 //! - [Linearization]: Linearization in a given state.
 //!                    The state holds context while building or the finalized linearization
 //! - [StubHost]: The purpose of this is to do nothing. It serves as an implementation used
 //!               outside the LSP context meaning to cause as little runtime impact as possible.
+//! - [LinearizationItem]: Abstract information for each term.
 
 use std::marker::PhantomData;
 use std::ops::{Deref, DerefMut};
@@ -68,6 +69,14 @@ pub trait LinearizationState {}
 impl LinearizationState for () {}
 impl LinearizationState for Uninit {}
 
+/// Possible resolution states of a LinearizationItem.
+///
+/// [LinearizationItem]s are initialized as [Unresolved] and are
+/// being resolved when completing the linearization
+///
+/// As implementors are used as typestate items of the [LinearizationItem]
+/// to determine the type of the Item.
+
 /// The linearizer trait is what is refered to during typechecking.
 /// It is the interface to recording terms (while tracking their scope)
 /// and finalizing a linearization using generically defined external information
@@ -105,7 +114,7 @@ pub trait Linearizer {
     ) {
     }
 
-    /// Defines how to turn a [Self::Building] Linearization of the tracked type into
+    /// Defines how to turn a [Building] Linearization of the tracked type into
     /// a [Self::Completed] linearization.
     /// By default creates an entirely empty [Self::Completed] object
     fn complete(
diff --git a/src/typecheck/mod.rs b/src/typecheck/mod.rs
index 5ad80466..189ec29f 100644
--- a/src/typecheck/mod.rs
+++ b/src/typecheck/mod.rs
@@ -68,8 +68,8 @@ pub type Environment = GenericEnvironment<Ident, TypeWrapper>;
 /// A structure holding the two typing environments, the global and the local.
 ///
 /// The global typing environment is constructed from the global term environment (see
-/// [`crate::eval::eval`]) which holds the Nickel builtin functions. It is a read-only shared
-/// environment used to retrieve the type of such functions.
+/// [`eval`](../eval/fn.eval.html)) which holds the Nickel builtin functions. It is a read-only
+/// shared environment used to retrieve the type of such functions.
 #[derive(Debug, PartialEq, Clone)]
 pub struct Envs<'a> {
     global: &'a Environment,
@@ -90,7 +90,8 @@ impl<'a> Envs<'a> {
         }
     }
 
-    /// Similar to [`Envs::from_global`], but use another `Envs` to provide the global environment.
+    /// Similar to [`from_global`](./struct.Envs.html#method.from_global), but
+    /// use another `Envs` to provide the global environment.
     pub fn from_envs(envs: &'a Envs) -> Self {
         Envs {
             global: envs.global,
@@ -179,8 +180,8 @@ pub struct State<'a> {
 
 /// Typecheck a term.
 ///
-/// Return the inferred type in case of success. This is just a wrapper that calls `type_check_`
-/// with a fresh unification variable as goal.
+/// Return the inferred type in case of success. This is just a wrapper that calls
+/// [`type_check_`](fn.type_check_.html) with a fresh unification variable as goal.
 ///
 /// Note that this function doesn't recursively typecheck imports (anymore), but just the current
 /// file. It however still needs the resolver to get the apparent type of imports.
@@ -222,16 +223,16 @@ where
     Ok((result, lin))
 }
 
-/// Typecheck a term using the given global typing environment. Same as [`type_check`], but it
-/// directly takes a global typing environment, instead of building one from a term environment as
-/// `type_check` does.
+/// Typecheck a term using the given global typing environment. Same as
+/// [`type_check`](./fun.type_check.html), but it directly takes a global typing environment,
+/// instead of building one from a term environment as `type_check` does.
 ///
 /// This function is used to typecheck an import in a clean environment, when we don't have access
 /// to the original term environment anymore, and hence cannot call `type_check` directly, but we
 /// already have built a global typing environment.
 ///
-/// Return the inferred type in case of success. This is just a wrapper that calls `type_check_`
-/// with a fresh unification variable as goal.
+/// Return the inferred type in case of success. This is just a wrapper that calls
+/// [`type_check_`](fn.type_check_.html) with a fresh unification variable as goal.
 pub fn type_check_in_env(
     t: &RichTerm,
     global: &Environment,
@@ -261,7 +262,7 @@ pub fn type_check_in_env(
 ///
 /// # Arguments
 ///
-/// - `state`: the unification state (see [`State`]).
+/// - `state`: the unification state (see [`State`](struct.State.html)).
 /// - `env`: the typing environment, mapping free variable to types.
 /// - `lin`: The current building linearization of building state `S`
 /// - `linearizer`: A linearizer that can modify the linearization
@@ -710,8 +711,8 @@ fn type_check_<L: Linearizer>(
 /// Determine the type of a let-bound expression, or more generally of any binding (e.g. fields)
 /// that may be stored in a typing environment at some point.
 ///
-/// Call [`apparent_type`] to see if the binding is annotated. If it is, return this type as a
-/// [`TypeWrapper`]. Otherwise:
+/// Call [`apparent_type`](./fn.apparent_type.html) to see if the binding is annotated. If
+/// it is, return this type as a [`TypeWrapper`](./enum.TypeWrapper.html). Otherwise:
 ///     * in non strict mode, we won't (and possibly can't) infer the type of `bound_exp`: just
 ///       return `Dyn`.
 ///     * in strict mode, we will typecheck `bound_exp`: return a new unification variable to be
@@ -733,7 +734,7 @@ fn binding_type(
     }
 }
 
-/// Different kinds of apparent types (see [`apparent_type`]).
+/// Different kinds of apparent types (see [`apparent_type`](fn.apparent_type.html)).
 ///
 /// Indicate the nature of an apparent type. In particular, when in strict mode, the typechecker
 /// throws away approximations as it can do better and infer the actual type of an expression by
@@ -947,7 +948,7 @@ impl From<Types> for TypeWrapper {
 }
 
 /// Look for a binding in a row, or add a new one if it is not present and if allowed by [row
-/// constraints][RowConstr].
+/// constraints](type.RowConstr.html).
 ///
 /// The row may be given as a concrete type or as a unification variable.
 ///
@@ -1150,8 +1151,8 @@ pub fn unify_(
     }
 }
 
-/// Try to unify two row types. Return an [`RowUnifError::IllformedRow`] error if one of the given
-/// type is not a row type.
+/// Try to unify two row types. Return an [`IllformedRow`](./enum.RowUnifError.html#variant.IllformedRow) error if one of the given type
+/// is not a row type.
 pub fn unify_rows(
     state: &mut State,
     t1: AbsType<Box<TypeWrapper>>,
@@ -1228,7 +1229,7 @@ fn to_type(table: &UnifTable, ty: TypeWrapper) -> Types {
 
 /// Type of the parameter controlling instantiation of foralls.
 ///
-/// See [`instantiate_foralls`].
+/// See [`instantiate_foralls`](./fn.instantiate_foralls.html).
 #[derive(Copy, Clone, Debug, PartialEq)]
 enum ForallInst {
     Constant,
@@ -1339,7 +1340,7 @@ pub type RowConstr = HashMap<usize, HashSet<Ident>>;
 
 /// Add a row constraint on a type.
 ///
-/// See [`RowConstr`].
+/// See [`RowConstr`](type.RowConstr.html).
 fn constraint(state: &mut State, x: TypeWrapper, id: Ident) -> Result<(), RowUnifError> {
     match x {
         TypeWrapper::Ptr(p) => match state.table.root(p) {
diff --git a/src/typecheck/reporting.rs b/src/typecheck/reporting.rs
index 27cd032d..c21f850a 100644
--- a/src/typecheck/reporting.rs
+++ b/src/typecheck/reporting.rs
@@ -25,14 +25,16 @@ impl NameReg {
 
 /// Create a fresh name candidate for a type variable or a type constant.
 ///
-/// Used by [`to_type_report`] and subfunctions `var_to_type` and `cst_to_type` when converting a
-/// type wrapper to a human-readable representation.
+/// Used by [`to_type_report`](./fn.to_type_report.html) and subfunctions
+/// [`var_to_type`](./fn.var_to_type) and [`cst_to_type`](./fn.cst_to_type) when converting a type
+/// wrapper to a human-readable representation.
 ///
 /// To select a candidate, first check in `names` if the variable or the constant corresponds to a
 /// type variable written by the user. If it is, return the name of the variable. Otherwise, use
 /// the given counter to generate a new single letter.
 ///
-/// Generated name is clearly not necessarily unique. This is handled by [`select_uniq`].
+/// Generated name is clearly not necessarily unique. This is handled by
+/// [`select_uniq`](./fn.select_uniq.html).
 fn mk_name(names: &HashMap<usize, Ident>, counter: &mut usize, id: usize) -> String {
     match names.get(&id) {
         // First check if that constant or variable was introduced by a forall. If it was, try
@@ -52,8 +54,9 @@ fn mk_name(names: &HashMap<usize, Ident>, counter: &mut usize, id: usize) -> Str
 /// Select a name distinct from all the others, starting from a candidate name for a type
 /// variable or a type constant.
 ///
-/// If the name is already taken, it just iterates by adding a numeric suffix `1`, `2`, .., and so
-/// on until a free name is found. See `var_to_type` and `cst_to_type`.
+/// If the name is already taken, it just iterates by adding a numeric suffix `1`, `2`, .., and
+/// so on until a free name is found. See [`var_to_type`](./fn.var_to_type.html) and
+/// [`cst_to_type`](./fn.cst_to_type.html).
 fn select_uniq(name_reg: &mut NameReg, mut name: String, id: usize) -> Ident {
     // To avoid clashing with already picked names, we add a numeric suffix to the picked
     // letter.
@@ -100,9 +103,9 @@ fn cst_to_type(names: &HashMap<usize, Ident>, name_reg: &mut NameReg, c: usize)
 
 /// Extract a concrete type corresponding to a type wrapper for error reporting.
 ///
-/// Similar to [`to_type`], excepted that free unification variables and type constants are
-/// replaced by type variables which names are determined by the `var_to_type` and `cst_to_type`.
-///
+/// Similar to [`to_type`](./fn.to_type.html), excepted that free unification variables and
+/// type constants are replaced by type variables which names are determined by the
+/// [`var_to_type`](./fn.var_to_type.html) and [`cst_to_type`](./fn.cst_tot_type.html).
 /// Distinguishing occurrences of unification variables and type constants is more informative
 /// than having `Dyn` everywhere.
 pub fn to_type(
diff --git a/src/types.rs b/src/types.rs
index c09d16dc..2c8953d3 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -72,7 +72,7 @@ pub enum AbsType<Ty> {
     Str(),
     /// A symbol.
     ///
-    /// See [`crate::term::Term::Wrapped`].
+    /// See `Wrapped` in [term](../term/enum.Term.html).
     Sym(),
     /// A type created from a user-defined contract.
     Flat(RichTerm),
@@ -190,10 +190,11 @@ impl Types {
     /// # Arguments
     ///
     /// - `h` is an environment mapping type variables to contracts. Type variables are introduced
-    ///   locally when opening a `forall`.
+    /// locally when opening a `forall`.
     /// - `pol` is the current polarity, which is toggled when generating a contract for the argument
-    ///   of an arrow type (see [`crate::label::Label`]).
-    /// - `sy` is a counter used to generate fresh symbols for `forall` contracts (see [`crate::term::Wrapped`]).
+    /// of an arrow type (see [`Label`](../label/struct.label.html)).
+    /// - `sy` is a counter used to generate fresh symbols for `forall` contracts (see `Wrapped` in
+    /// [terms](../term/enum.Term.html).
     fn subcontract(
         &self,
         mut h: HashMap<Ident, (RichTerm, RichTerm)>,
diff --git a/utilities/src/lib.rs b/utilities/src/lib.rs
index aa52320a..463186a9 100644
--- a/utilities/src/lib.rs
+++ b/utilities/src/lib.rs
@@ -2,10 +2,13 @@
 use codespan::Files;
 use criterion::Criterion;
 use nickel_lang::{
+    cache::Cache,
     error::{Error, ParseError},
+    eval,
     parser::{grammar, lexer},
     program::Program,
     term::{RichTerm, Term},
+    transform::import_resolution,
 };
 
 use std::{io::Cursor, path::PathBuf};
@@ -36,108 +39,171 @@ pub enum EvalMode {
     DeepSeq,
 }
 
-pub fn bench(
-    name: &str,
-    base_dir: &str,
-    subpath: &str,
-    subtest: Option<&str>,
-    iteration: u32,
-    eval_mode: EvalMode,
-    c: &mut Criterion,
-) {
-    bench_args(
-        name,
-        base_dir,
-        subpath,
-        subtest,
-        vec![iteration.to_string()],
-        eval_mode,
-        c,
-    )
+pub struct Bench<'b> {
+    pub name: &'b str,
+    pub base_dir: &'b str,
+    pub subpath: &'b str,
+    pub subtest: Option<&'b str>,
+    pub args: Vec<String>,
+    pub eval_mode: EvalMode,
 }
 
-pub fn bench_expect<F>(
-    name: &str,
-    base_dir: &str,
-    subpath: &str,
-    subtest: Option<&str>,
-    iteration: u32,
-    eval_mode: EvalMode,
-    pred: F,
-    c: &mut Criterion,
-) where
-    F: Fn(Term) -> bool,
-{
-    bench_args_expect(
-        name,
-        base_dir,
-        subpath,
-        subtest,
-        vec![iteration.to_string()],
-        eval_mode,
-        pred,
-        c,
-    )
+impl<'b> Bench<'b> {
+    pub fn bench(
+        name: &'b str,
+        base_dir: &'b str,
+        subpath: &'b str,
+        subtest: Option<&'b str>,
+        iteration: u32,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self::bench_args(
+            name,
+            base_dir,
+            subpath,
+            subtest,
+            vec![iteration.to_string()],
+            eval_mode,
+        )
+    }
+
+    pub fn bench_args(
+        name: &'b str,
+        base_dir: &'b str,
+        subpath: &'b str,
+        subtest: Option<&'b str>,
+        args: Vec<String>,
+        eval_mode: EvalMode,
+    ) -> Self {
+        Self {
+            name,
+            base_dir,
+            subpath,
+            subtest,
+            args,
+            eval_mode,
+        }
+    }
+
+    pub fn term(&self) -> RichTerm {
+        let path = self.path();
+
+        let field_path = self.subtest.map(|s| format!(".{}", s)).unwrap_or_default();
+        let content = format!(
+            "(import \"{}\"){}.run {}",
+            path.to_string_lossy(),
+            field_path,
+            self.args
+                .iter()
+                .map(|s| format!("({})", s))
+                .collect::<Vec<String>>()
+                .join(" ")
+        );
+
+        let content = if self.eval_mode == EvalMode::DeepSeq {
+            format!("%deep_seq% ({}) true", content)
+        } else {
+            content
+        };
+        parse(&content).unwrap()
+    }
+
+    pub fn path(&self) -> PathBuf {
+        let mut path = PathBuf::from(self.base_dir);
+        path.push(format!("benches/{}.ncl", self.subpath));
+        path
+    }
 }
 
-pub fn bench_args(
-    name: &str,
-    base_dir: &str,
-    subpath: &str,
-    subtest: Option<&str>,
-    args: Vec<String>,
-    eval_mode: EvalMode,
-    c: &mut Criterion,
-) {
-    bench_args_expect(
-        name,
-        base_dir,
-        subpath,
-        subtest,
-        args,
-        eval_mode,
-        |_| true,
-        c,
-    )
+pub fn bench_terms<'r>(rts: Vec<Bench<'r>>) -> Box<dyn Fn(&mut Criterion) + 'r> {
+    use nickel_lang::cache::GlobalEnv;
+
+    let mut cache = Cache::new();
+    let GlobalEnv { eval_env, type_env } = cache.prepare_stdlib().unwrap();
+    Box::new(move |c: &mut Criterion| {
+        rts.iter().for_each(|bench| {
+            let t = bench.term();
+            c.bench_function(bench.name, |b| {
+                b.iter_batched(
+                    || {
+                        let mut cache = cache.clone();
+                        let id = cache.add_file(bench.path()).unwrap();
+                        let (t, _) =
+                            import_resolution::resolve_imports(t.clone(), &mut cache).unwrap();
+                        (cache, id, t)
+                    },
+                    |(mut c_local, id, t)| {
+                        c_local.prepare(id, &type_env).unwrap();
+                        eval::eval(t, &eval_env, &mut c_local).unwrap()
+                    },
+                    criterion::BatchSize::LargeInput,
+                )
+            });
+        })
+    })
 }
 
-pub fn bench_args_expect<F>(
-    name: &str,
-    base_dir: &str,
-    subpath: &str,
-    subtest: Option<&str>,
-    args: Vec<String>,
-    eval_mode: EvalMode,
-    pred: F,
-    c: &mut Criterion,
-) where
-    F: Fn(Term) -> bool,
-{
-    let mut path = PathBuf::from(base_dir);
-    path.push(format!("benches/{}.ncl", subpath));
-
-    let field_path = subtest.map(|s| format!(".{}", s)).unwrap_or_default();
-    let content = format!(
-        "(import \"{}\"){}.run {}",
-        path.to_string_lossy(),
-        field_path,
-        args.into_iter()
-            .map(|s| format!("({})", s))
-            .collect::<Vec<String>>()
-            .join(" ")
-    );
-
-    let content = if eval_mode == EvalMode::DeepSeq {
-        format!("%deep_seq% ({}) true", content)
-    } else {
-        content
-    };
-
-    c.bench_function(name, |b| {
-        b.iter_batched_ref(
-            || Program::new_from_source(Cursor::new(content.clone()), name).unwrap(),
-            |p| assert!(pred(p.eval().map(Term::from).unwrap())),
-            criterion::BatchSize::LargeInput,
-        )
-    });
+#[macro_export]
+macro_rules! ncl_bench {
+    {
+        name = $name:literal,
+        $( base_dir = $base_dir:literal, )?
+        path = $subpath:literal,
+        $( subtest = $subtest:literal, )?
+        $( args = ( $( $arg:expr ),* ),)?
+        $( eval_mode = $eval_mode:path,)?
+    } => {
+        $crate::Bench {
+            $( base_dir: $base_dir,)?
+                $( subtest: Some($subtest),)?
+                $( args: vec![ $( $arg.to_string() ),* ],)?
+                $( eval_mode: $eval_mode,)?
+                ..$crate::Bench {
+                    name: $name,
+                    subpath: $subpath,
+                    base_dir: env!("CARGO_MANIFEST_DIR"),
+                    subtest: None,
+                    args: vec![],
+                    eval_mode: $crate::EvalMode::Normal,
+                }
+        }
+    }
+}
+
+#[macro_export]
+macro_rules! ncl_bench_group {
+    (name = $group_name:ident; config = $config:expr; $($b:tt),+ $(,)*) => {
+        pub fn $group_name() {
+            use nickel_lang::{
+                cache::{GlobalEnv, Cache, ImportResolver},
+                eval::eval,
+                transform::import_resolution::resolve_imports,
+            };
+
+            let mut c: criterion::Criterion<_> = $config
+                .configure_from_args();
+            let mut cache = Cache::new();
+            let GlobalEnv{eval_env, type_env} = cache.prepare_stdlib().unwrap();
+            $(
+                let bench = $crate::ncl_bench!$b;
+                let t = bench.term();
+                c.bench_function(bench.name, |b| {
+                    b.iter_batched(
+                        || {
+                            let mut cache = cache.clone();
+                            let id = cache.add_file(bench.path()).unwrap();
+                            let (t, _) =
+                                resolve_imports(t.clone(), &mut cache).unwrap();
+                            (cache, id, t)
+                        },
+                        |(mut c_local, id, t)| {
+                            c_local.prepare(id, &type_env).unwrap();
+                            eval(t, &eval_env, &mut c_local).unwrap()
+                        },
+                        criterion::BatchSize::LargeInput,
+                        )
+                });
+             )+
+        }
+    }
 }


Test patch:
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 1f63e4fc..03ae08f2 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -355,6 +355,10 @@ fn multiline_str_escape() {
         parse_without_pos(r##"m%"%Hel%%lo%%%"%m"##),
         mk_single_chunk("%Hel%%lo%%%"),
     );
+    assert_eq!(
+        parse_without_pos(r##"m%"%Hel%%{lo%%%{"%m"##),
+        mk_single_chunk("%Hel%%{lo%%%{"),
+    );
 }
 
 #[test]
diff --git a/tests/pass/strings.ncl b/tests/pass/strings.ncl
index a1e21762..7599785a 100644
--- a/tests/pass/strings.ncl
+++ b/tests/pass/strings.ncl
@@ -20,9 +20,5 @@ let Assert = fun l x => x || %blame% l in
   let s = "Hello" in m%%""%%{s}" World"%%m == "\"Hello\" World",
   let s = "Hello" in m%%""%%%{s}" World"%%m == "\"%Hello\" World",
   m%%""%%s"%%m == "\"%%s",
-
-  # regression test for issue #659 (https://github.com/tweag/nickel/issues/659)
-  let b = "x" in m%"a%%{b}c"%m == "a%xc",
-  m%"%Hel%%{"1"}lo%%%{"2"}"%m == "%Hel%1lo%%2",
 ]
 |> array.foldl (fun x y => (x | Assert) && y) true