Repository: tweag/nickel
Pull request #2051 (2024-09-24 12:41:29)

Use let-blocks when apprioriate in stdlib & examples
This PR updates to Topiary 0.5.1, which includes the support for formatting let-blocks (and the let-in formatting fix). Additionally, I made a pass on the examples and the stdlib to use let-blocks whenever it seemed appropriate, so that both remains idiomatic Nickel.

This is the last item on the checklist before being able to release Nickel 1.9

github-actions[bot]: <h2><a href="https://bencher.dev/perf/nickel/reports/88320476-303a-4111-a295-b63b441d6077?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel"><img src="https://bencher.dev/favicon.svg" width="24" height="24" alt="🐰" /> Bencher Report</a></h2><table><tr><td>Branch</td><td><a href="https://bencher.dev/perf/nickel/branches/2051-merge?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">2051/merge</a></td></tr><tr><td>Testbed</td><td><a href="https://bencher.dev/perf/nickel/testbeds/ubuntu-latest?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">ubuntu-latest</a></td></tr></table><blockquote><p><b>⚠️ WARNING:</b> The following Measure does not have a Threshold. Without a Threshold, no Alerts will ever be generated!</p><ul><li><a href="https://bencher.dev/perf/nickel/measures/latency?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">Latency</a></li></ul><p><a href="https://bencher.dev/console/projects/nickel/thresholds/add?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">Click here to create a new Threshold</a><br/>For more information, see <a href="https://bencher.dev/docs/explanation/thresholds/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">the Threshold documentation</a>.<br/>To only post results if a Threshold exists, set <a href="https://bencher.dev/docs/explanation/bencher-run/#--ci-only-thresholds?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">the <code lang="rust">--ci-only-thresholds</code> CLI flag</a>.</p></blockquote><details><summary>Click to view all benchmark results</summary><br/><table><thead><tr><th>Benchmark</th><th><a href="https://bencher.dev/perf/nickel/measures/latency?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">Latency</a></th><th>nanoseconds (ns)</th></tr></thead><tbody><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/fibonacci-10?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">fibonacci 10</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=2da43863-596c-490b-ba89-08757542db80&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3cfaf19e-c9c5-4341-95cd-703456d0fc9f&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1724590225000&end_time=1727182225000&report=88320476-303a-4111-a295-b63b441d6077&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>494,410.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/pidigits-100?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">pidigits 100</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=2da43863-596c-490b-ba89-08757542db80&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=5be25c57-d46f-4b48-b6f0-dee01b84220a&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1724590225000&end_time=1727182225000&report=88320476-303a-4111-a295-b63b441d6077&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>3,235,500.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/product-30?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">product 30</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=2da43863-596c-490b-ba89-08757542db80&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=2f5f7f6a-4e54-4542-aa09-da5671953fbb&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1724590225000&end_time=1727182225000&report=88320476-303a-4111-a295-b63b441d6077&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>809,710.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/scalar-10?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">scalar 10</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=2da43863-596c-490b-ba89-08757542db80&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=211bb66a-58f8-488b-97e4-7ea032870038&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1724590225000&end_time=1727182225000&report=88320476-303a-4111-a295-b63b441d6077&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>1,477,700.00</td></tr><tr><td><a href="https://bencher.dev/perf/nickel/benchmarks/sum-30?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">sum 30</a></td><td>📈 <a href="https://bencher.dev/perf/nickel?branches=2da43863-596c-490b-ba89-08757542db80&testbeds=66263ed0-9556-4385-9f30-26466ee26211&benchmarks=3f1710c5-ce75-4dfb-aadb-1435fed8a093&measures=2b5b9371-696f-422d-b1a5-262ce8157b6b&start_time=1724590225000&end_time=1727182225000&report=88320476-303a-4111-a295-b63b441d6077&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">view plot</a><br/>⚠️ NO THRESHOLD</td><td>807,800.00</td></tr></tbody></table></details><a href="https://bencher.dev/perf/nickel/reports/88320476-303a-4111-a295-b63b441d6077?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nickel">🐰 View full continuous benchmarking report in Bencher</a><div id="bencher.dev/projects/nickel/id/2051-merge/ubuntu-latest/rust_criterion"></div>
yannham: This is actually also block on https://github.com/tweag/topiary/pull/747 to be able to update to latest Topiary in the flake.
yannham: Now that we've got rid of Topiary as an input of the Nickel flake, we need Topiary to be compatible with tree-sitter 0.22 (used by the Nickel grammar) - which is the case, but this version hasn't been released yet. Waiting for a new version of Topiary to be released, so that we can finally format let-blocks.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 1efc85d3..aa0ad30f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -177,28 +177,6 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
 
-[[package]]
-name = "async-stream"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
-dependencies = [
- "async-stream-impl",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-stream-impl"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
-dependencies = [
- "proc-macro2 1.0.78",
- "quote 1.0.35",
- "syn 2.0.52",
-]
-
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -317,12 +295,6 @@ version = "1.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f"
 
-[[package]]
-name = "bytes"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
-
 [[package]]
 name = "cast"
 version = "0.3.0"
@@ -331,9 +303,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
 
 [[package]]
 name = "cc"
-version = "1.1.24"
+version = "1.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
+checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
 dependencies = [
  "shlex",
 ]
@@ -1454,7 +1426,7 @@ dependencies = [
  "petgraph",
  "pico-args",
  "regex",
- "regex-syntax 0.8.2",
+ "regex-syntax 0.8.5",
  "string_cache",
  "term",
  "tiny-keccak",
@@ -2478,25 +2450,25 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.10.6"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
 dependencies = [
  "aho-corasick",
  "memchr",
  "regex-automata",
- "regex-syntax 0.8.2",
+ "regex-syntax 0.8.5",
 ]
 
 [[package]]
 name = "regex-automata"
-version = "0.4.6"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
+checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax 0.8.2",
+ "regex-syntax 0.8.5",
 ]
 
 [[package]]
@@ -2507,9 +2479,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.2"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
 name = "rgb"
@@ -2946,7 +2918,7 @@ dependencies = [
  "once_cell",
  "onig",
  "plist",
- "regex-syntax 0.8.2",
+ "regex-syntax 0.8.5",
  "serde",
  "serde_derive",
  "serde_json",
@@ -3148,30 +3120,6 @@ dependencies = [
  "syn 2.0.52",
 ]
 
-[[package]]
-name = "tokio-stream"
-version = "0.1.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
-dependencies = [
- "futures-core",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
-name = "tokio-test"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89b3cbabd3ae862100094ae433e1def582cf86451b4e9bf83aa7ac1d8a7d719"
-dependencies = [
- "async-stream",
- "bytes",
- "futures-core",
- "tokio",
- "tokio-stream",
-]
-
 [[package]]
 name = "toml"
 version = "0.8.14"
@@ -3208,9 +3156,9 @@ dependencies = [
 
 [[package]]
 name = "topiary-core"
-version = "0.4.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b26541394bdbcd5a039deb917b2096a69c6604f733f0d41f534fc6975987040d"
+checksum = "0236570d34c4e60d88e6bce8446f6d6fd51f5e1e4bdea68cdcf0d1ff5c89776d"
 dependencies = [
  "futures",
  "itertools 0.11.0",
@@ -3220,22 +3168,21 @@ dependencies = [
  "serde",
  "serde_json",
  "tokio",
- "tokio-test",
  "topiary-tree-sitter-facade",
  "topiary-web-tree-sitter-sys",
 ]
 
 [[package]]
 name = "topiary-queries"
-version = "0.4.2"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "103bcbfd5c605f2ef94074ae4950473f9dcf159f8bb01959a13856f96a2ffc8c"
+checksum = "dea6b8d21ab29eeb34fca55e384ddb6507261019e075d78812de1a1465ac210c"
 
 [[package]]
 name = "topiary-tree-sitter-facade"
-version = "0.4.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea7c870d9a844bbba0f55be40eaa89624a9f627c155cd22eea70dd808bd71b3d"
+checksum = "2eb978cfd5d4686e9193eed02c6c3abe8aac98e9d4942aaaf83ea472fc9b32d6"
 dependencies = [
  "js-sys",
  "topiary-web-tree-sitter-sys",
@@ -3246,9 +3193,9 @@ dependencies = [
 
 [[package]]
 name = "topiary-web-tree-sitter-sys"
-version = "0.4.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc9e35caeeb7f07e97b71ee7ea204c89516e7cc490d1c2ae318eca3ff3c08d46"
+checksum = "7e6ec4ec3a2426af1ff74688cd19c841a1e852403665d96b455946cb491cb0b6"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -3258,9 +3205,9 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter"
-version = "0.20.10"
+version = "0.22.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d"
+checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca"
 dependencies = [
  "cc",
  "regex",
@@ -3268,9 +3215,9 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter-nickel"
-version = "0.2.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "449daa9ac0e2a18d7243eaf21d7299d6683ce6750ca225ebd557412d22d106cf"
+checksum = "5e8b3a80d772b47cef3145983c0f431f7af2c93088695c516d7a89e68216f854"
 dependencies = [
  "cc",
  "tree-sitter",
diff --git a/Cargo.toml b/Cargo.toml
index 73cb993b..0074a844 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -103,14 +103,9 @@ bumpalo = "3.16.0"
 metrics = "0.21"
 metrics-util = "0.15"
 
-topiary-core = "0.4.0"
-topiary-queries = { version = "=0.4.2", default-features = false, features = ["nickel"] }
-# This version should agree with the topiary-query version: Nickel queries
-# target a specific version of the Nickel grammar (though that dependency
-# doesn't appear explicitly in `topiary-queries`'s Cargo.toml file). For now you
-# might have to look at Topiary's commit messages, but in a near future, we'll
-# try to make this data more accessible, e.g. in the query file
-tree-sitter-nickel = "=0.2.0"
+topiary-core = "0.5.1"
+topiary-queries = { version = "0.5.1", default-features = false, features = ["nickel"] }
+tree-sitter-nickel = "0.3.0"
 tempfile = "3.5.0"
 
 [profile.dev.package.lalrpop]
diff --git a/core/benches/arrays/fold.ncl b/core/benches/arrays/fold.ncl
index 58ba6d9b..c91fad4d 100644
--- a/core/benches/arrays/fold.ncl
+++ b/core/benches/arrays/fold.ncl
@@ -1,8 +1,7 @@
-let letter | Number -> std.string.Character
-  = fun n =>
-    let letters = std.string.characters "abcdefghijklmnopqrstuvwxyz" in
-    std.array.at (n % 26) letters
-  in
+let letter | Number -> std.string.Character = fun n =>
+  let letters = std.string.characters "abcdefghijklmnopqrstuvwxyz" in
+  std.array.at (n % 26) letters
+in
 
 {
   right = {
diff --git a/core/benches/arrays/generate.ncl b/core/benches/arrays/generate.ncl
index bc7323a9..92b06e86 100644
--- a/core/benches/arrays/generate.ncl
+++ b/core/benches/arrays/generate.ncl
@@ -10,7 +10,8 @@ let g = fun n => n * 2 + 5 in
     run = fun n => generate n g,
   },
   checked = {
-    generate_with_contract | forall a. Number -> (Number -> a) -> Array a
+    generate_with_contract
+      | forall a. Number -> (Number -> a) -> Array a
       = fun n g =>
         if n == 0 then
           []
diff --git a/core/benches/arrays/primes.ncl b/core/benches/arrays/primes.ncl
index 7d06c7ff..aa563608 100644
--- a/core/benches/arrays/primes.ncl
+++ b/core/benches/arrays/primes.ncl
@@ -6,11 +6,12 @@ let range
       []
     else
       std.array.generate (fun x => x + start) (end - start)
-  in
+in
 
-let is_prime | doc "Returns true if the argument is a prime number."
+let is_prime
+  | doc "Returns true if the argument is a prime number."
   = fun x => x > 1 && std.array.all (fun d => x % d != 0) (range 2 (x - 1))
-  in
+in
 
 let Prime = std.contract.from_predicate is_prime in
 
@@ -34,7 +35,7 @@ let primes
         loop (x + 1) (drop_multiples x xs)
     in
     loop 2 (range 2 max)
-  in
+in
 
 {
   run = primes
diff --git a/core/benches/arrays/random.ncl b/core/benches/arrays/random.ncl
index 1b876e9b..d1a5b561 100644
--- a/core/benches/arrays/random.ncl
+++ b/core/benches/arrays/random.ncl
@@ -1,6 +1,7 @@
-let mgc | doc "A multiplicative congruential pseudorandom number generator (MCG)."
+let mgc
+  | doc "A multiplicative congruential pseudorandom number generator (MCG)."
   = fun params x => (params.a * x + params.c) % params.m
-  in
+in
 
 # NOTE: These parameters are from "Table 7: Good multipliers for MCGs with m = 2^32"
 # of the 2021 paper "Computationally Easy, Spectrally Good Multipliers for
diff --git a/core/benches/arrays/sum.ncl b/core/benches/arrays/sum.ncl
index 180ead32..8b1f805e 100644
--- a/core/benches/arrays/sum.ncl
+++ b/core/benches/arrays/sum.ncl
@@ -1,9 +1,8 @@
-let rec sum | Array Number -> Number
-  = match {
-    [] => 0,
-    [x, ..xs] => x + sum xs,
-  }
-  in
+let rec sum | Array Number -> Number = match {
+  [] => 0,
+  [x, ..xs] => x + sum xs,
+}
+in
 {
   run = fun n => std.array.generate (fun x => x + 1) n |> sum
 }
diff --git a/core/benches/mantis/jobs/mantis.ncl b/core/benches/mantis/jobs/mantis.ncl
index 5851d7ec..03b8b590 100644
--- a/core/benches/mantis/jobs/mantis.ncl
+++ b/core/benches/mantis/jobs/mantis.ncl
@@ -121,13 +121,12 @@ fun params =>
       & {
         reschedule = params.reschedule,
 
-        task.telegraf | Telegraf
-          = {
-            #infinte rec?
-            # #namespace = namespace,
-            name = "%{std.string.from_enum params.role}-${NOMAD_ALLOC_INDEX}",
-            prometheusPort = "metrics",
-          },
+        task.telegraf | Telegraf = {
+          #infinte rec?
+          # #namespace = namespace,
+          name = "%{std.string.from_enum params.role}-${NOMAD_ALLOC_INDEX}",
+          prometheusPort = "metrics",
+        },
 
         # Do we really need this?
         #task.mantis | tasks.#Mantis = {
diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl
index dc963457..8b9eb3d0 100644
--- a/core/stdlib/std.ncl
+++ b/core/stdlib/std.ncl
@@ -533,7 +533,7 @@
         "%
       = fun pred l =>
         let aux = fun acc x =>
-          if (pred x) then
+          if pred x then
             { right = acc.right @ [x], wrong = acc.wrong }
           else
             { right = acc.right, wrong = acc.wrong @ [x] }
@@ -654,8 +654,10 @@
         if length <= 1 then
           array
         else
-          let first = %array/at% array 0 in
-          let rest = %array/slice% 1 length array in
+          let
+            first = %array/at% array 0,
+            rest = %array/slice% 1 length array,
+          in
           [first] @ (flat_map (fun a => [v, a]) rest),
 
     slice
@@ -866,8 +868,10 @@
         "%
       = fun f array =>
         let last_index = %array/length% array - 1 in
-        let last = %array/at% array last_index in
-        let rest = %array/slice% 0 last_index array in
+        let
+          last = %array/at% array last_index,
+          rest = %array/slice% 0 last_index array,
+        in
         fold_right f last rest,
 
     zip_with
@@ -1850,50 +1854,49 @@
             let label_module = label in
             let attach_message = label_module.with_message "invalid array indexing" in
 
-            let ArrayIndexFirst =
-              %contract/custom% (fun label value =>
-                if %typeof% value == 'Number then
-                  let label =
-                    label
-                    |> attach_message
-                    |> label_module.append_note "Expected array index to be a positive integer, got %{%to_string% value} "
-                  in
-                  std.contract.check std.number.Nat label value
-                else
-                  'Ok value
-              )
-            in
-
-            let ArrayIndexSecond = fun type min_size =>
-              %contract/custom% (fun _label value =>
-                if %typeof% min_size == 'Number && %typeof% value == 'Array then
-                  let max_idx =
-                    type
-                    |> match {
-                      'Index => %array/length% value - 1,
-                      'Split => %array/length% value
-                    }
-                  in
-
-                  if min_size > max_idx then
-                    let index_as_str = %to_string% min_size in
-                    let max_as_str = %to_string% max_idx in
-                    let note =
-                      if %array/length% value == 0 then
-                        "Can't index into an empty array"
-                      else
-                        "Expected an array index between 0 and %{max_as_str} (included), got %{index_as_str}"
+            let
+              ArrayIndexFirst =
+                %contract/custom% (fun label value =>
+                  if %typeof% value == 'Number then
+                    let label =
+                      label
+                      |> attach_message
+                      |> label_module.append_note "Expected array index to be a positive integer, got %{%to_string% value} "
+                    in
+                    std.contract.check std.number.Nat label value
+                  else
+                    'Ok value
+                ),
+              ArrayIndexSecond = fun type min_size =>
+                %contract/custom% (fun _label value =>
+                  if %typeof% min_size == 'Number && %typeof% value == 'Array then
+                    let max_idx =
+                      type
+                      |> match {
+                        'Index => %array/length% value - 1,
+                        'Split => %array/length% value
+                      }
                     in
 
-                    'Error {
-                      message = "invalid array indexing",
-                      notes = [note],
-                    }
+                    if min_size > max_idx then
+                      let index_as_str = %to_string% min_size in
+                      let max_as_str = %to_string% max_idx in
+                      let note =
+                        if %array/length% value == 0 then
+                          "Can't index into an empty array"
+                        else
+                          "Expected an array index between 0 and %{max_as_str} (included), got %{index_as_str}"
+                      in
+
+                      'Error {
+                        message = "invalid array indexing",
+                        notes = [note],
+                      }
+                    else
+                      'Ok value
                   else
                     'Ok value
-                else
-                  'Ok value
-              )
+                ),
             in
 
             fun type =>
@@ -1923,59 +1926,56 @@
             let label_module = label in
             let attach_message = label_module.with_message "invalid array slice indexing" in
 
-            let SliceIndexFirst =
-              %contract/custom% (fun label value =>
-                if %typeof% value == 'Number then
-                  let label =
-                    label
-                    |> attach_message
-                    |> label_module.append_note "Expected the array slice start index to be a positive integer, got %{%to_string% value}"
-                  in
-                  std.contract.check std.number.Nat label value
-                else
-                  'Ok value
-              )
-            in
+            let
+              SliceIndexFirst =
+                %contract/custom% (fun label value =>
+                  if %typeof% value == 'Number then
+                    let label =
+                      label
+                      |> attach_message
+                      |> label_module.append_note "Expected the array slice start index to be a positive integer, got %{%to_string% value}"
+                    in
+                    std.contract.check std.number.Nat label value
+                  else
+                    'Ok value
+                ),
+              SliceIndexSecond = fun start =>
+                %contract/custom% (fun label value =>
+                  if %typeof% start == 'Number && %typeof% value == 'Number then
+                    if value < start then
+                      'Error {
+                        message = "invalid array slice indexing",
+                        notes = [
+                          "Expected the array slice indices to satisfy `start <= end`, but got %{%to_string% start} (start) and %{%to_string% value} (end)"
+                        ],
+                      }
+                    else
+                      let label =
+                        label
+                        |> attach_message
+                        |> label_module.append_note "Expected the array slice end index to be a positive integer, got %{%to_string% value}"
+                      in
+                      std.contract.check std.number.Nat label value
+                  else
+                    'Ok value
+                ),
+              ArraySliceArray = fun end_index =>
+                %contract/custom% (fun _label value =>
+                  if %typeof% end_index == 'Number
+                  && %typeof% value == 'Array
+                  && end_index > %array/length% value then
+                    let index_as_str = %to_string% end_index in
+                    let size_as_str = %to_string% (%array/length% value) in
 
-            let SliceIndexSecond = fun start =>
-              %contract/custom% (fun label value =>
-                if %typeof% start == 'Number && %typeof% value == 'Number then
-                  if value < start then
                     'Error {
                       message = "invalid array slice indexing",
                       notes = [
-                        "Expected the array slice indices to satisfy `start <= end`, but got %{%to_string% start} (start) and %{%to_string% value} (end)"
+                        "Expected the slice end index to be between 0 and %{size_as_str} (array's length), got %{index_as_str}"
                       ],
                     }
                   else
-                    let label =
-                      label
-                      |> attach_message
-                      |> label_module.append_note "Expected the array slice end index to be a positive integer, got %{%to_string% value}"
-                    in
-                    std.contract.check std.number.Nat label value
-                else
-                  'Ok value
-              )
-            in
-
-            let ArraySliceArray = fun end_index =>
-              %contract/custom% (fun _label value =>
-                if %typeof% end_index == 'Number
-                && %typeof% value == 'Array
-                && end_index > %array/length% value then
-                  let index_as_str = %to_string% end_index in
-                  let size_as_str = %to_string% (%array/length% value) in
-
-                  'Error {
-                    message = "invalid array slice indexing",
-                    notes = [
-                      "Expected the slice end index to be between 0 and %{size_as_str} (array's length), got %{index_as_str}"
-                    ],
-                  }
-                else
-                  'Ok value
-              )
+                    'Ok value
+                ),
             in
 
             DependentFun
@@ -2252,8 +2252,10 @@
         "%
       = fun f enum_value =>
         if %enum/is_variant% enum_value then
-          let tag = (%to_string% (%enum/get_tag% enum_value)) in
-          let mapped = f (%enum/get_arg% enum_value) in
+          let
+            tag = (%to_string% (%enum/get_tag% enum_value)),
+            mapped = f (%enum/get_arg% enum_value),
+          in
 
           %enum/make_variant% tag mapped
         else
@@ -3505,10 +3507,11 @@
         if length == 0 then
           ""
         else
-          let first = %array/at% fragments 0 in
-          let rest =
-            %array/slice% 1 length fragments
-            |> std.array.fold_left (fun acc s => acc ++ sep ++ s) ""
+          let
+            first = %array/at% fragments 0,
+            rest =
+              %array/slice% 1 length fragments
+              |> std.array.fold_left (fun acc s => acc ++ sep ++ s) "",
           in
           first ++ rest,
 
diff --git a/examples/arrays/arrays.ncl b/examples/arrays/arrays.ncl
index 4caba0e4..406d240a 100644
--- a/examples/arrays/arrays.ncl
+++ b/examples/arrays/arrays.ncl
@@ -2,25 +2,25 @@
 
 # Example array functions. This code is illustrative: prefer using the array
 # stdlib functions `std.array.map` and `std.array.fold_right` instead.
-let my_array_lib = {
-  map : forall a b. (a -> b) -> Array a -> Array b
+let rec
+  map
+    : forall a b. (a -> b) -> Array a -> Array b
     = fun f arr =>
       if arr == [] then
         []
       else
         let [head, ..tail] = arr in
         [f head] @ map f tail,
-
-  fold : forall a b. (a -> b -> b) -> b -> Array a -> b
+  fold
+    : forall a b. (a -> b -> b) -> b -> Array a -> b
     = fun f first arr =>
       if arr == [] then
         first
       else
         let [head, ..tail] = arr in
         f head (fold f first tail),
-}
 in
 # Compute `7!`
 [1, 2, 3, 4, 5, 6]
-|> my_array_lib.map ((+) 1)
-|> my_array_lib.fold (*) 1
+|> map ((+) 1)
+|> fold (*) 1
diff --git a/examples/config-gcc/config-gcc.ncl b/examples/config-gcc/config-gcc.ncl
index 874ec268..2ad76ec7 100644
--- a/examples/config-gcc/config-gcc.ncl
+++ b/examples/config-gcc/config-gcc.ncl
@@ -2,67 +2,62 @@
 
 # Validate and normalize gcc flags. They can be either a string `-Wextra` or
 # a structured value `{flag = "W", arg = "extra"}`. Arguments are not checked.
-let GccFlag =
-  let supported_flags = ["W", "c", "S", "e", "o"] in
-  let is_valid_flag
-    | doc "check if a string of length > 0 is a valid flag"
-    = fun string =>
-      std.array.elem (std.string.substring 0 1 string) supported_flags
+let
+  GccFlag =
+    let supported_flags = ["W", "c", "S", "e", "o"] in
+    let is_valid_flag
+      | doc "check if a string of length > 0 is a valid flag"
+      = fun string =>
+        std.array.elem (std.string.substring 0 1 string) supported_flags
     in
 
-  std.contract.custom (fun label =>
-    match {
-      value if std.is_string value && is_valid_flag value => 'Ok value,
-      { flag, arg } if std.array.elem flag supported_flags =>
-        # We normalize a record representation to a string
-        'Ok "%{flag}%{arg}",
-      value if std.is_string value =>
-        'Error { message = "unknown flag %{value}" },
-      { flag, arg = _ } =>
-        'Error { message = "unknown flag %{flag}" },
-      { .. } =>
-        'Error {
-          message = "bad record structure: missing field `flag` or `arg`",
-        },
-      _ => 'Error { message = "expected record or string" },
-    }
-  )
-in
-
-let Path =
-  let pattern = m%"^(.+)/([^/]+)$"% in
-  std.contract.from_validator (fun value =>
-    if std.is_string value then
-      if std.string.is_match pattern value then
-        'Ok
+    std.contract.custom (fun label =>
+      match {
+        value if std.is_string value && is_valid_flag value => 'Ok value,
+        { flag, arg } if std.array.elem flag supported_flags =>
+          # We normalize a record representation to a string
+          'Ok "%{flag}%{arg}",
+        value if std.is_string value =>
+          'Error { message = "unknown flag %{value}" },
+        { flag, arg = _ } =>
+          'Error { message = "unknown flag %{flag}" },
+        { .. } =>
+          'Error {
+            message = "bad record structure: missing field `flag` or `arg`",
+          },
+        _ => 'Error { message = "expected record or string" },
+      }
+    ),
+  Path =
+    let pattern = m%"^(.+)/([^/]+)$"% in
+    std.contract.from_validator (fun value =>
+      if std.is_string value then
+        if std.string.is_match pattern value then
+          'Ok
+        else
+          'Error { message = "invalid path" }
       else
-        'Error { message = "invalid path" }
-    else
-      'Error { message = "not a string" }
-  )
-in
-
-let SharedObjectFile =
-  std.contract.from_validator (fun value =>
-    if std.is_string value then
-      if std.string.is_match m%"\.so$"% value then
-        'Ok
+        'Error { message = "not a string" }
+    ),
+  SharedObjectFile =
+    std.contract.from_validator (fun value =>
+      if std.is_string value then
+        if std.string.is_match m%"\.so$"% value then
+          'Ok
+        else
+          'Error { message = "not an .so file" }
       else
-        'Error { message = "not an .so file" }
-    else
-      'Error { message = "not a string" }
-  )
-in
-
-let OptLevel =
-  std.contract.from_predicate (match {
-    0 or 1 or 2 => true,
-    _ => false,
-  }
-  )
+        'Error { message = "not a string" }
+    ),
+  OptLevel =
+    std.contract.from_predicate (match {
+      0 or 1 or 2 => true,
+      _ => false,
+    }
+    ),
 in
 
-let Contract = {
+let GccConf = {
   path_libc
     | doc "Path to libc."
     | Path
@@ -93,9 +88,7 @@ let Contract = {
 }
 in
 
-(
-  {
-    flags = ["Wextra", { flag = "o", arg = "stuff.o" }],
-    optimization_level = 2,
-  } | Contract
-)
+{
+  flags = ["Wextra", { flag = "o", arg = "stuff.o" }],
+  optimization_level = 2,
+} | GccConf
diff --git a/examples/fibonacci/fibonacci.ncl b/examples/fibonacci/fibonacci.ncl
index 26f666b7..c549f256 100644
--- a/examples/fibonacci/fibonacci.ncl
+++ b/examples/fibonacci/fibonacci.ncl
@@ -8,4 +8,5 @@ let rec fibonacci = match {
   n => fibonacci (n - 1) + fibonacci (n - 2),
 }
 in
+
 fibonacci 10
diff --git a/examples/imports/data_nickel_properties.ncl b/examples/imports/data_nickel_properties.ncl
index de8a14b6..360ff8a5 100644
--- a/examples/imports/data_nickel_properties.ncl
+++ b/examples/imports/data_nickel_properties.ncl
@@ -1,12 +1,15 @@
 # test = 'pass'
-let kelvin_to_celcius = fun kelvin => kelvin - 273.15 in
-let kelvin_to_fahrenheit = fun kelvin => (kelvin - 273.15) * 1.8000 + 32.00 in
+let
+  kelvin_to_celcius = fun kelvin => kelvin - 273.15,
+  kelvin_to_fahrenheit = fun kelvin => (kelvin - 273.15) * 1.8000 + 32.00,
+in
 
-let melting_celcius = std.string.from_number (kelvin_to_celcius 1728) in
-let melting_fahrenheit = std.string.from_number (kelvin_to_fahrenheit 1728) in
-
-let boiling_celcius = std.string.from_number (kelvin_to_celcius 3003) in
-let boiling_fahrenheit = std.string.from_number (kelvin_to_fahrenheit 3003) in
+let
+  melting_celcius = std.string.from_number (kelvin_to_celcius 1728),
+  melting_fahrenheit = std.string.from_number (kelvin_to_fahrenheit 1728),
+  boiling_celcius = std.string.from_number (kelvin_to_celcius 3003),
+  boiling_fahrenheit = std.string.from_number (kelvin_to_fahrenheit 3003),
+in
 
 {
   physical = {
diff --git a/examples/imports/imports.ncl b/examples/imports/imports.ncl
index 21e9a81a..1fdcd952 100644
--- a/examples/imports/imports.ncl
+++ b/examples/imports/imports.ncl
@@ -1,21 +1,21 @@
 # test = 'ignore'
 
 # Nickel can import plain yaml, or json files
-let _users = (import "data_users.yml") in
-let _groups = (import "data_groups.json") in
-
-# It even imports toml
-let _machines = (import "data_machines.toml") in
-
-# And of course other nickel files
-let _nickel_properties = (import "data_nickel_properties.ncl") in
+let
+  data_users = (import "data_users.yml"),
+  data_groups = (import "data_groups.json"),
+  # or even toml
+  data_machines = (import "data_machines.toml"),
+  # And of course other nickel files
+  data_nickel_properties = (import "data_nickel_properties.ncl"),
+in
 
 # This is the output object
 {
-  users = _users.users,
-  groups = _groups.groups,
-  machines = _machines.machines,
+  users = data_users.users,
+  groups = data_groups.groups,
+  machines = data_machines.machines,
   off_topic = {
-    nickel_properties = _nickel_properties
+    nickel_properties = data_nickel_properties,
   }
 }
diff --git a/examples/merge-priorities/main.ncl b/examples/merge-priorities/main.ncl
index 15e364a8..d2d7d7f8 100644
--- a/examples/merge-priorities/main.ncl
+++ b/examples/merge-priorities/main.ncl
@@ -2,13 +2,15 @@
 
 # Merge several blocks into one final configuration. In a real world case, one
 # would also want contracts to validate the shape of the data.
-let server = import "server.ncl" in
-let security = import "security.ncl" in
-# Disabling firewall in the final result
+let
+  server = import "server.ncl",
+  security = import "security.ncl",
+in
 server
 & security
 & {
-  # As opposed to the simple merge example, this would now fail
+  # As opposed to the simple merge example, uncommenting the next line would now
+  # fail
   # firewall.enabled = false
 
   firewall.open_ports | priority 10 = [80],
diff --git a/examples/merge/main.ncl b/examples/merge/main.ncl
index 7461c51f..4e2c0b2c 100644
--- a/examples/merge/main.ncl
+++ b/examples/merge/main.ncl
@@ -2,7 +2,9 @@
 
 # Merge several blocks into one final configuration. In a real world case, one
 # would also want contracts to validate the shape of the data.
-let server = import "server.ncl" in
-let security = import "security.ncl" in
+let
+  server = import "server.ncl",
+  security = import "security.ncl",
+in
 # Disabling firewall in the final result
 server & security & { firewall.enabled = false }
diff --git a/examples/record-contract/record-contract.ncl b/examples/record-contract/record-contract.ncl
index a78406cc..31e9f26a 100644
--- a/examples/record-contract/record-contract.ncl
+++ b/examples/record-contract/record-contract.ncl
@@ -8,7 +8,8 @@
 # This example is illustrative. If you actually want to use Nickel with
 # Kubernetes, consider using the auto-generated contracts from
 # https://github.com/tweag/nickel-kubernetes/ instead
-let Port | doc "A contract for a port number"
+let Port
+  | doc "A contract for a port number"
   =
     std.contract.from_predicate (fun value =>
       std.is_number value
@@ -16,14 +17,15 @@ let Port | doc "A contract for a port number"
       && (value >= 0)
       && (value <= 65535)
     )
-  in
+in
 
-let PortElt | doc "A contract for a port element of a Kubernetes configuration"
+let PortElt
+  | doc "A contract for a port element of a Kubernetes configuration"
   = {
     name | String,
     containerPort | Port,
   }
-  in
+in
 
 let Container = {
   name | String,
diff --git a/examples/simple-contracts/simple-contract-bool.ncl b/examples/simple-contracts/simple-contract-bool.ncl
index f739928a..876774ac 100644
--- a/examples/simple-contracts/simple-contract-bool.ncl
+++ b/examples/simple-contracts/simple-contract-bool.ncl
@@ -5,8 +5,10 @@ let EqualsTo = fun reference_value =>
   std.contract.from_predicate ((==) reference_value)
 in
 
-let AlwaysTrue = EqualsTo true in
-let AlwaysFalse = EqualsTo false in
+let
+  AlwaysTrue = EqualsTo true,
+  AlwaysFalse = EqualsTo false,
+in
 
 # This contract says: `not` requires its argument to be true, and in return
 # promise that the return value is false.
diff --git a/examples/simple-contracts/simple-contract-div.ncl b/examples/simple-contracts/simple-contract-div.ncl
index 3777d251..abbb8151 100644
--- a/examples/simple-contracts/simple-contract-div.ncl
+++ b/examples/simple-contracts/simple-contract-div.ncl
@@ -2,16 +2,15 @@
 
 # /!\ THIS EXAMPLE IS EXPECTED TO FAIL
 # Illustrates a basic contract violation.
-let Even =
-  std.contract.from_predicate (fun value =>
-    std.is_number value && value % 2 == 0
-  )
-in
-
-let DivBy3 =
-  std.contract.from_predicate (fun value =>
-    std.is_number value && value % 3 == 0
-  )
+let
+  Even =
+    std.contract.from_predicate (fun value =>
+      std.is_number value && value % 2 == 0
+    ),
+  DivBy3 =
+    std.contract.from_predicate (fun value =>
+      std.is_number value && value % 3 == 0
+    ),
 in
 
 # Will cause an error! 4 is not divisible by 3.


Test patch:
diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_reversed_indices.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_reversed_indices.ncl.snap
index 7408ee1e..0be30059 100644
--- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_reversed_indices.ncl.snap
+++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_reversed_indices.ncl.snap
@@ -4,9 +4,9 @@ expression: err
 ---
 error: contract broken by the caller of `range`
        invalid range
-    ┌─ <stdlib/std.ncl>:769:9
+    ┌─ <stdlib/std.ncl>:771:9
     │
-769 │       | std.contract.unstable.RangeFun Dyn
+771 │       | std.contract.unstable.RangeFun Dyn
     │         ---------------------------------- expected type
     │
     ┌─ [INPUTS_PATH]/errors/array_range_reversed_indices.ncl:3:19
diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_step_negative_step.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_step_negative_step.ncl.snap
index ad589a24..65c61f57 100644
--- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_step_negative_step.ncl.snap
+++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_step_negative_step.ncl.snap
@@ -4,9 +4,9 @@ expression: err
 ---
 error: contract broken by the caller of `range_step`
        invalid range step
-    ┌─ <stdlib/std.ncl>:744:9
+    ┌─ <stdlib/std.ncl>:746:9
     │
-744 │       | std.contract.unstable.RangeFun (std.contract.unstable.RangeStep -> Dyn)
+746 │       | std.contract.unstable.RangeFun (std.contract.unstable.RangeStep -> Dyn)
     │         ----------------------------------------------------------------------- expected type
     │
     ┌─ [INPUTS_PATH]/errors/array_range_step_negative_step.ncl:3:27