Repository: tweag/nickel
Pull request #1623 (2023-09-21 19:11:34)

Convert symbols request away from the linearizer
I'm not completely sure about what should count as a symbol. Right now, let bindings and record fields create "symbols" but function arguments do not.

Returning all locally scoped symbols feels a little noisy to me right now. Maybe the best way to improve it would be to make a symbol hierarchy instead of a flat list. LSP supports this, but we aren't doing it yet.


Patch:
diff --git a/core/src/cache.rs b/core/src/cache.rs
index e59c9b1b..96e98c9a 100644
--- a/core/src/cache.rs
+++ b/core/src/cache.rs
@@ -283,7 +283,7 @@ impl From<SourcePath> for OsString {
             SourcePath::Std(StdlibModule::Std) => "<stdlib/std.ncl>".into(),
             SourcePath::Std(StdlibModule::Internals) => "<stdlib/internals.ncl>".into(),
             SourcePath::Query => "<query>".into(),
-            SourcePath::ReplInput(idx) => format!("<repl-input-{idx}").into(),
+            SourcePath::ReplInput(idx) => format!("<repl-input-{idx}>").into(),
             SourcePath::ReplTypecheck => "<repl-typecheck>".into(),
             SourcePath::ReplQuery => "<repl-query>".into(),
             SourcePath::Override(path) => format!("<override {}>", path.join(".")).into(),
diff --git a/core/src/error.rs b/core/src/error.rs
index 441292b5..6b14663c 100644
--- a/core/src/error.rs
+++ b/core/src/error.rs
@@ -7,7 +7,7 @@ use crate::cache::Cache;
 pub use codespan::{FileId, Files};
 pub use codespan_reporting::diagnostic::{Diagnostic, Label, LabelStyle};
 
-use codespan_reporting::term::termcolor::{ColorChoice, StandardStream};
+use codespan_reporting::term::termcolor::{ColorChoice, StandardStream, WriteColor};
 use lalrpop_util::ErrorRecovery;
 use malachite::num::conversion::traits::ToSci;
 
@@ -2287,23 +2287,27 @@ impl From<ColorOpt> for ColorChoice {
 /// infrastructure to point at specific locations and print snippets when needed.
 pub fn report<E: IntoDiagnostics<FileId>>(cache: &mut Cache, error: E, color_opt: ColorOpt) {
     let stdlib_ids = cache.get_all_stdlib_modules_file_id();
-    report_with(cache.files_mut(), stdlib_ids.as_ref(), error, color_opt)
+    report_with(
+        &mut StandardStream::stderr(color_opt.into()).lock(),
+        cache.files_mut(),
+        stdlib_ids.as_ref(),
+        error,
+    )
 }
 
 /// Report an error on `stderr`, provided a file database and a list of stdlib file ids.
-fn report_with<E: IntoDiagnostics<FileId>>(
+pub fn report_with<E: IntoDiagnostics<FileId>>(
+    writer: &mut dyn WriteColor,
     files: &mut Files<String>,
     stdlib_ids: Option<&Vec<FileId>>,
     error: E,
-    color_opt: ColorOpt,
 ) {
-    let writer = StandardStream::stderr(color_opt.into());
     let config = codespan_reporting::term::Config::default();
     let diagnostics = error.into_diagnostics(files, stdlib_ids);
 
     let result = diagnostics
         .iter()
-        .try_for_each(|d| codespan_reporting::term::emit(&mut writer.lock(), &config, files, d));
+        .try_for_each(|d| codespan_reporting::term::emit(writer, &config, files, d));
 
     match result {
         Ok(()) => (),
diff --git a/core/src/repl/mod.rs b/core/src/repl/mod.rs
index 996db949..2ec59661 100644
--- a/core/src/repl/mod.rs
+++ b/core/src/repl/mod.rs
@@ -43,6 +43,7 @@ pub mod simple_frontend;
 pub mod wasm_frontend;
 
 /// Result of the evaluation of an input.
+#[derive(Debug, Clone)]
 pub enum EvalResult {
     /// The input has been evaluated to a term.
     Evaluated(RichTerm),
diff --git a/doc/manual/contracts.md b/doc/manual/contracts.md
index 810864c5..6a901670 100644
--- a/doc/manual/contracts.md
+++ b/doc/manual/contracts.md
@@ -49,8 +49,8 @@ are performed:
 2
 
 > "a" | Number
-error: contract broken by a value.
-[..]
+error: contract broken by a value
+[...]
 ```
 
 Contracts corresponding to the basic types `Number`, `String`, `Bool` and `Dyn`
@@ -104,13 +104,24 @@ In `IsFoo`, we first test if the value is a string, and then if it is equal to
 appropriate error messages. Let us try:
 
 ```nickel #repl
+# hide-range{1-10}
+
+> let IsFoo = fun label value =>
+    if std.is_string value then
+      if value == "foo" then
+        value
+      else
+        std.contract.blame_with_message "not equal to \"foo\"" label
+    else
+      std.contract.blame_with_message "not a string" label
+
 > 1 | IsFoo
-error: contract broken by a value [not a string].
-[..]
+error: contract broken by a value: not a string
+[...]
 
 > "a" | IsFoo
-error: contract broken by a value [not equal to "foo"].
-[..]
+error: contract broken by a value: not equal to "foo"
+[...]
 
 > "foo" | IsFoo
 "foo"
@@ -315,7 +326,7 @@ If we export this example to JSON, we get:
 
 ```console
 $ nickel -f config.ncl export
-error: contract broken by a value
+error: contract broken by the value of `server_port`
    ┌─ example.ncl:26:7
    │
 16 │         server_port | Port,
@@ -370,10 +381,7 @@ or default values:
 > let config | Schema = {bar = 2}
 
 > std.serialize 'Json config
-"{
-  "bar": 2,
-  "foo": "foo"
-}"
+"{\n  \"bar\": 2,\n  \"foo\": \"foo\"\n}"
 
 # Don't parse this in tests hide-line
 > :query config foo
@@ -390,8 +398,8 @@ By default, record contracts are closed, meaning that additional fields are forb
 > let Contract = {foo | String}
 
 > {foo = "a", bar = 1} | Contract
-error: contract broken by a value [extra field `bar`].
-[..]
+error: contract broken by a value: extra field `bar`
+[...]
 ```
 
 If you want to allow additional fields, append `, ..` after the last field
@@ -401,7 +409,7 @@ definition to define an open contract:
 > let Contract = {foo | String, ..}
 
 > {foo = "a", bar = 1} | Contract
-{ bar = 1, foo = "a" }
+{ bar = 1, foo | String = "a", .. }
 ```
 
 #### Giving values to fields
@@ -419,26 +427,25 @@ example:
   }
 
 > std.serialize 'Json ({data = ""} | Secure)
-"{
-  "data": "",
-  "must_be_very_secure": true
-}"
+"{\n  \"data\": \"\",\n  \"must_be_very_secure\": true\n}"
 
 > {data = "", must_be_very_secure = false} | Secure
 error: non mergeable terms
-  ┌─ repl-input-15:1:35
+  ┌─ <repl-input-15>:1:36
   │
-1 │ {data = "", must_be_very_secure = false} | Secure
-  │                                   ^^^^^    ------ originally merged here
-  │                                   │
-  │                                   cannot merge this expression
+1 │  {data = "", must_be_very_secure = false} | Secure
+  │                                    ^^^^^    ------ originally merged here
+  │                                    │
+  │                                    cannot merge this expression
   │
-  ┌─ repl-input-13:2:32
+  ┌─ <repl-input-13>:2:34
   │
-2 │   must_be_very_secure | Bool = true,
-  │                                ^^^^ with this expression
+2 │     must_be_very_secure | Bool = true,
+  │                                  ^^^^ with this expression
   │
-  = Both values have the same merge priority but they can't be combined
+  = Both values have the same merge priority but they can't be combined.
+  = Primitive values (Number, String, and Bool) or arrays can be merged only if they are equal.
+  = Functions can never be merged.
 ```
 
 **Warning: `=` vs `|`**
@@ -465,11 +472,11 @@ be what you want:
   }
 
 > {sub_field.foo = "a", sub_field.bar = "b"} | ContractPipe
-error: contract broken by a value [extra field `bar`].
-[..]
+error: contract broken by the value of `sub_field`: extra field `bar`
+[...]
 
 > {sub_field.foo = "a", sub_field.bar = "b"} | ContractEq
-{ sub_field = { foo = <contract,value="a">, bar = "b"}}
+{ sub_field = { bar = "b", foo | String = "a", }, }
 ```
 
 There are other discrepancies, e.g. when applying the contract to a record with
@@ -497,12 +504,12 @@ contract to each element:
 
 > [1000, 10001, 2] | Array VeryBig
 error: contract broken by a value
-  ┌─ repl-input-22:1:15
+  ┌─ <repl-input-21>:1:16
   │
-1 │ [1000, 10001, 2] | Array VeryBig
-  │               ^          ------- expected array element type
-  │               │
-  │               applied to this expression
+1 │  [1000, 10001, 2] | Array VeryBig
+  │                ^          ------- expected array element type
+  │                │
+  │                applied to this expression
 ```
 
 #### Functions
@@ -546,13 +553,13 @@ order to make this caller/callee distinction:
 ```nickel #repl
 > let add_semi | String -> String = fun x => x ++ ";" in
   add_semi 1
-error: contract broken by the caller.
-[..]
+error: contract broken by the caller
+[...]
 
 > let wrong | String -> String = fun x => 0 in
   wrong "a"
-error: contract broken by a function.
-[..]
+error: contract broken by a function
+[...]
 ```
 
 ##### Higher-order functions
@@ -565,14 +572,18 @@ functions as parameters. Here is an example:
 > let apply_fun | (Number -> Number) -> Number = fun f => f 0 in
   apply_fun (fun x => "a")
 error: contract broken by the caller
-  ┌─ repl-input-25:1:28
+  ┌─ <repl-input-24>:1:29
+  │
+1 │  let apply_fun | (Number -> Number) -> Number = fun f => f 0 in
+  │                             ------ expected return type of a function provided by the caller
+2 │   apply_fun (fun x => "a")
+  │                       --- evaluated to this expression
   │
-1 │ let apply_fun | (Number -> Number) -> Number = fun f => f 0 in
-  │                            ------ expected return type of a function provided by the caller
-2 │ apply_fun (fun x => "a")
-  │                     --- evaluated to this expression
+  ┌─ <unknown> (generated by evaluation):1:1
   │
-[..]
+1 │ "a"
+  │ --- evaluated to this value
+[...]
 ```
 
 #### Dictionary
@@ -619,8 +630,8 @@ always failing contract `std.FailWith` to observe where evaluation takes place:
 * documentation: Some information
 
 > config.fail
-error: contract broken by a value: ooch
-[..]
+error: contract broken by the value of `fail`: ooch
+[...]
 ```
 
 See how the command `:query config.data` succeeds, although the field `fail`
@@ -718,38 +729,95 @@ value and continues with the second argument (here, our wrapped `value`).
 Let us see if we indeed preserved laziness:
 
 ```nickel #repl
+#hide-range{1-29}
+
+> let NumberBoolDict = fun label value =>
+    if std.is_record value then
+      let check_fields =
+        value
+        |> std.record.fields
+        |> std.array.fold_left
+          (
+            fun acc field_name =>
+              if std.string.is_match "^\\d+$" field_name then
+                acc # unused and always null through iteration
+              else
+                std.contract.blame_with_message "field name `%{field_name}` is not a number" label
+          )
+          null
+      in
+      value
+      |> std.record.map
+        (
+          fun name value =>
+            let label_with_msg =
+              std.contract.label.with_message "field `%{name}` is not a boolean" label
+            in
+            std.contract.apply Bool label_with_msg value
+        )
+      |> std.seq check_fields
+    else
+      std.contract.blame_with_message "not a record" label
+
 > let config | NumberBoolDict = {
     "1" | std.FailWith "ooch" = null, # same as our previous "fail"
     "0" | doc "Some information" = true,
   }
 
-# Don't parse this in tests hide-line
-> :query config "0"
-* documentation: Some information
+> config."0"
+true
 ```
 
 Yes! Our contract doesn't unduly cause the evaluation of the field `"1"`. Does
 it check anything, though?
 
 ```nickel #repl
+#hide-range{1-29}
+
+> let NumberBoolDict = fun label value =>
+    if std.is_record value then
+      let check_fields =
+        value
+        |> std.record.fields
+        |> std.array.fold_left
+          (
+            fun acc field_name =>
+              if std.string.is_match "^\\d+$" field_name then
+                acc # unused and always null through iteration
+              else
+                std.contract.blame_with_message "field name `%{field_name}` is not a number" label
+          )
+          null
+      in
+      value
+      |> std.record.map
+        (
+          fun name value =>
+            let label_with_msg =
+              std.contract.label.with_message "field `%{name}` is not a boolean" label
+            in
+            std.contract.apply Bool label_with_msg value
+        )
+      |> std.seq check_fields
+    else
+      std.contract.blame_with_message "not a record" label
+
 > let config | NumberBoolDict = {
     not_a_number = false,
     "0" | doc "Some information" = false,
   }
 
-# Don't parse this in tests hide-line
-> :q config."0"
-error: contract broken by a value [field name `not_a_number` is not a number].
-[..]
+> config."0"
+error: contract broken by a value: field name `not_a_number` is not a number
+[...]
 
 > let config | NumberBoolDict = {
     "0" | doc "Some information" = "not a boolean",
   }
 
-# Don't parse this in tests hide-line
-> :q config."0"
-error: contract broken by a value [field `0` is not a boolean].
-[..]
+> config."0"
+error: contract broken by a value: field `0` is not a boolean
+[...]
 ```
 
 It does!
diff --git a/doc/manual/correctness.md b/doc/manual/correctness.md
index f1f2aabc..f7b28d19 100644
--- a/doc/manual/correctness.md
+++ b/doc/manual/correctness.md
@@ -90,7 +90,7 @@ Type annotations are introduced with `:`. For example:
 
 > "not a Number" : Number
 error: incompatible types
-[..]
+[...]
 ```
 
 Contract annotations are introduced with `|`. For example:
@@ -99,8 +99,8 @@ Contract annotations are introduced with `|`. For example:
 > let GreaterThan = fun bound =>
     std.contract.from_predicate (fun val => val >= bound) in
   -1 | GreaterThan 10
-error: contract broken by value
-[..]
+error: contract broken by a value
+[...]
 ```
 
 Both type and contract annotations support the same syntax for properties on
@@ -120,15 +120,30 @@ Suppose we need a function to convert an array of key-value pairs into an array
 of keys and an array of values. Let's call it `split`:
 
 ```nickel #repl
+#hide-range{1-14}
+
+> let split = fun pairs =>
+    std.array.fold_right
+      (
+        fun pair acc =>
+          {
+            # problem: the correct expression to use is [pair.key]
+            keys = acc.keys @ [pair.key],
+            values = acc.values @ [pair.value],
+          }
+      )
+      { keys = [], values = [] }
+      pairs
+
 > split [{key = "foo", value = 1}, {key = "bar", value = 2}]
-{keys = ["foo", "bar"], values = [1, 2]}
+{ keys = [ "bar", "foo" ], values = [ 2, 1 ], }
 
 > split [
     {key = "firewall", value = true},
     {key = "grsec", value = false},
     {key = "iptables", value = true},
   ]
-{ keys: ["firewall", "grsec", "iptables"], values [true, false, true] }
+{ keys = [ "iptables", "grsec", "firewall" ], values = [ true, false, true ], }
 ```
 
 Here is the definition for `split`, but with a twist. On line 9 we accidentally
@@ -217,19 +232,18 @@ function contract for `split` has the following limitations:
   evaluating `config.ncl` reports the following error:
 
   ```text
-  error: type error
-    ┌─ /path/to/lib.ncl:6:27
+  error: dynamic type error
+    ┌─ lib.ncl:8:33
     │
-  6 │         keys = acc.keys @ pair.key,
-    │                           ^^^^^^^^ this expression has type String, but Array was expected
+  8 │               keys = acc.keys @ pair.key,
+    │                                 ^^^^^^^^ this expression has type String, but Array was expected
     │
-    ┌─ /path/to/config.ncl:2:41
+    ┌─ <repl-input-1:1:77
     │
-  2 │ split [{key = "foo", value = 1}, {key = "bar", value = 2}]
-    │                                         ----- evaluated to this
+  1 │ let {split} = import "error.ncl" in split [{key = "foo", value = 1}, {key = "bar", value = 2}]
+    │                                                                             ----- evaluated to this
     │
-    = @, 2nd operand
-
+    = (@) expects its 2nd argument to be a Array
   ```
 
   From the caller's perspective, this is not a particularly helpful error. For
@@ -265,17 +279,19 @@ now reports an error:
 
 ```text
 error: incompatible rows declaration
-   ┌─ /path/to/lib.ncl:10:7
+   ┌─ lib.ncl:13:9
    │
-10 │       pairs
-   │       ^^^^^ this expression
+13 │         pairs
+   │         ^^^^^ this expression
    │
-[..]
-error: While typing field `key`: incompatible types
- = The type of the expression was expected to be `Array _a`
- = The type of the expression was inferred to be `String`
- = These types are not compatible
+   = Expected an expression of a record type with the row `key: Array _a`
+   = Found an expression of a record type with the row `key: String`
+   = Could not match the two declarations of `key`
 
+error: while typing field `key`: incompatible types
+ = Expected an expression of type `Array _a`
+ = Found an expression of type `String`
+ = These types are not compatible
 ```
 
 The error says that the `key` field of the elements of `pairs` is a string, but
@@ -334,13 +350,13 @@ We get:
 
 ```text
 error: incompatible types
-  ┌─ repl-input-2:4:26
+  ┌─ config.ncl:4:26
   │
 4 │   opt_level : OptLevel = "A" ++ std.string.from_number level,
   │                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this expression
   │
-  = The type of the expression was expected to be `OptLevel` (a contract)
-  = The type of the expression was inferred to be `String`
+  = Expected an expression of type `OptLevel` (a contract)
+  = Found an expression of type `String`
   = Static types and contracts are not compatible
 ```
 
@@ -372,8 +388,8 @@ let level = 4 in
 This correctly reports an error, and even gives the computed offending value:
 
 ```text
-error: contract broken by a value
-  ┌─ repl-input-4:4:26
+error: contract broken by the value of `opt_level`
+  ┌─ config.ncl:4:26
   │
 4 │   opt_level | OptLevel = "A" ++ std.string.from_number level,
   │               --------   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ applied to this expression
@@ -384,7 +400,6 @@ error: contract broken by a value
   │
 1 │ "A4"
   │ ---- evaluated to this value
-  │
 ```
 
 ### Summary
diff --git a/doc/manual/merging.md b/doc/manual/merging.md
index adc6d37c..2b1e0fbd 100644
--- a/doc/manual/merging.md
+++ b/doc/manual/merging.md
@@ -327,17 +327,17 @@ other field doesn't have a definition**:
 
 ```nickel #repl
 > {foo = 1, bar | optional} & {bar | optional}
-{ foo = 1 }
+{ foo = 1, }
 
 > {foo = 1, bar | optional} & {bar}
 error: missing definition for `bar`
-  ┌─ repl-input-0:1:11
+  ┌─ <repl-input-1>:1:12
   │
-1 │ {foo = 1, bar | optional} & {bar}
-  │ ----------^^^--------------------
-  │ │         │
-  │ │         required here
-  │ in this record
+1 │  {foo = 1, bar | optional} & {bar}
+  │  ----------^^^--------------------
+  │  │         │
+  │  │         required here
+  │  in this record
 ```
 
 In the second example, `bar` isn't optional on the right-hand side of the merge.
@@ -351,6 +351,7 @@ definition error, etc.
 
 ```nickel #repl
 > let Contract = {foo = 1, bar | optional}
+
 > std.record.values Contract
 [ 1 ]
 
@@ -381,14 +382,18 @@ be meaningfully combined:
 ```nickel #repl
 > {foo = 1} & {foo = 2}
 error: non mergeable terms
-  ┌─ repl-input-1:1:8
+  ┌─ <repl-input-5>:1:9
   │
-1 │ {foo = 1} & {foo = 2}
-  │        ^           ^ with this expression
-  │        │
-  │        cannot merge this expression
+1 │  {foo = 1} & {foo = 2}
+  │  -------^-----------^-
+  │  │      │           │
+  │  │      │           with this expression
+  │  │      cannot merge this expression
+  │  originally merged here
   │
-  = Both values have the same merge priority but they can't be combined
+  = Both values have the same merge priority but they can't be combined.
+  = Primitive values (Number, String, and Bool) or arrays can be merged only if they are equal.
+  = Functions can never be merged.
 ```
 
 If the priorities differ, the value with the highest priority simply erases the
@@ -396,10 +401,10 @@ other:
 
 ```nickel #repl
 > {foo | priority 1 = 1} & {foo = 2}
-{ foo = 1 }
+{ foo | priority 1 = 1, }
 
 > {foo | priority -1 = 1} & {foo = 2}
-{ foo = 2 }
+{ foo = 2, }
 ```
 
 The priorities are ordered in the following way:
@@ -593,37 +598,32 @@ If we try to observe the intermediate result (`deep_seq` recursively forces the
 evaluation of its first argument and proceeds with evaluating the second
 argument), we do get a contract violation error:
 
-```nickel #no-check
-let FooContract = {
-  required_field1,
-  required_field2,
-}
-in
-let intermediate =
-  { foo | FooContract }
-  & { foo.required_field1 = "here" }
-in
-
-intermediate
-& { foo.required_field2 = "here" }
-|> std.deep_seq intermediate
-```
-
-Result:
-
-```text
+```nickel #repl
+> let FooContract = {
+    required_field1,
+    required_field2,
+  }
+  in
+  let intermediate =
+    { foo | FooContract }
+    & { foo.required_field1 = "here" }
+  in
+  
+  intermediate
+  & { foo.required_field2 = "here" }
+  |> std.deep_seq intermediate
 error: missing definition for `required_field2`
-     ┌─ /home/yago/Pro/Tweag/projects/nickel/nickel/doc/manual/foo.ncl:3:3
+     ┌─ <repl-input-8>:3:5
      │
-   3 │   required_field2,
-     │   ^^^^^^^^^^^^^^^ defined here
+   3 │     required_field2,
+     │     ^^^^^^^^^^^^^^^ required here
      ·
-   8 │   & { foo.required_field1 = "here" }
-     │           ^^^^^^^^^^^^^^^^^^^^^^^^ in this record
+   8 │     & { foo.required_field1 = "here" }
+     │             ------------------------ in this record
      │
-     ┌─ <stdlib/std.ncl>:2448:18
+     ┌─ <stdlib/std.ncl>:2868:18
      │
-2448 │     = fun x y => %deep_seq% x y,
+2868 │     = fun x y => %deep_seq% x y,
      │                  ------------ accessed here
 ```
 
@@ -666,13 +666,13 @@ let GreaterThan
 Because 80 would be less than 1024, this fails at evaluation:
 
 ```text
-error: contract broken by a value
-   ┌─ example.ncl:26:17
+error: contract broken by the value of `port`
+   ┌─ example.ncl:27:17
    │
-21 │     | GreaterThan 1024
+22 │     | GreaterThan 1024
    │       ---------------- expected type
    ·
-26 │   port | Port = 80,
+27 │   port | Port = 80,
    │                 ^^ applied to this expression
 ```
 
diff --git a/doc/manual/syntax.md b/doc/manual/syntax.md
index 4ea96477..0acf5a63 100644
--- a/doc/manual/syntax.md
+++ b/doc/manual/syntax.md
@@ -134,7 +134,8 @@ Here are some examples of string handling in Nickel:
 "Hello World"
 
 > let n = 5 in "The number %{n}."
-error: Type error
+error: dynamic type error
+[...]
 
 > let n = 5 in "The number %{std.string.from_number n}."
 "The number 5."
@@ -153,10 +154,7 @@ the output. For example:
         This line is even more indented.
     This line has no more indentation.
   "%
-"This line has no indentation.
-  This line is indented.
-    This line is even more indented.
-This line has no more indentation."
+"This line has no indentation.\n  This line is indented.\n    This line is even more indented.\nThis line has no more indentation."
 ```
 
 The only special sequence in a multiline string is the string interpolation:
@@ -166,8 +164,7 @@ The only special sequence in a multiline string is the string interpolation:
 "Multiline\\nString?"
 
 > m%"Multiline%{"\n"}String"%
-"Multiline
-String"
+"Multiline\nString"
 ```
 
 A multiline string can be opened and closed with multiple `%` signs, as long as
@@ -184,7 +181,7 @@ or `%{` sequence in a string without escaping:
 "Hello World"
 
 > let w = "World" in m%%"Hello %{w}"%%
-"Hello %{w}"
+"Hello \%{w}"
 
 > let w = "World" in m%%"Hello %%{w}"%%
 "Hello World"
@@ -205,13 +202,7 @@ interpolate a string with indentation and the result will be as expected:
       res.append(s)
     return res
   "%
-"def concat(str_array, log=false):
-  res = []
-  for s in str_array:
-    if log:
-      print("log:", s)
-    res.append(s)
-  return res"
+"def concat(str_array, log=false):\n  res = []\n  for s in str_array:\n    if log:\n      print(\"log:\", s)\n    res.append(s)\n  return res"
 ```
 
 Inside a multiline string, an interpolation sequence immediately preceded by a
@@ -306,9 +297,9 @@ The following examples show how symbolic strings are desugared:
 ```nickel #repl
 > mytag-s%"I'm %{"symbolic"} with %{"fragments"}"%
 {
-  tag = 'SymbolicString,
-  prefix = 'mytag
   fragments = [ "I'm ", "symbolic", " with ", "fragments" ],
+  prefix = 'mytag,
+  tag = 'SymbolicString,
 }
 
 > let terraform_computed_field = {
@@ -319,9 +310,15 @@ The following examples show how symbolic strings are desugared:
 
 > tf-s%"id: %{terraform_computed_field}, port: %{5}"%
 {
-  tag = 'SymbolicString
+  fragments =
+    [
+        "id: ",
+        { field = "id", resource = "foo", tag = 'TfComputed, },
+        ", port: ",
+        5
+      ],
   prefix = 'tf,
-  fragments = [ "id: ", { resource = "foo", field = "id", tag = 'TfComputed }, ", port: ", 5 ],
+  tag = 'SymbolicString,
 }
 ```
 
@@ -334,22 +331,17 @@ first argument, which is an enum tag among `'Json`, `'Toml` or `'Yaml`:
 
 ```nickel #repl
 > std.serialize 'Json {foo = 1}
-"{
-   \"foo\": 1
- }"
+"{\n  \"foo\": 1\n}"
 
 > std.serialize 'Toml {foo = 1}
-"foo = 1
-"
+"foo = 1\n"
 ```
 
 An enum tag `'foo` is serialized as the string `"foo"`:
 
 ```nickel #repl
 > std.serialize 'Json {foo = 'bar}
-"{
-  \"foo\": \"bar\"
-}"
+"{\n  \"foo\": \"bar\"\n}"
 ```
 
 While it's technically possible to just use strings in place of enum tags, using
@@ -436,7 +428,8 @@ Record fields can be accessed using the `.` operator :
 1
 
 > { a = 1 }.b
-error: Missing field
+error: missing field `b`
+[...]
 
 > { "1" = "one" }."1"
 "one"
@@ -447,13 +440,13 @@ separate fields by dots:
 
 ```nickel #repl
 > { a = { b = 1 } }
-{ a = { b = 1 } }
+{ a = { b = 1, }, }
 
 > { a.b = 1 }
-{ a = { b = 1 } }
+{ a = { b = 1, }, }
 
 > { a.b = 1, a.c = 2, b = 3}
-{ a = { b = 1, c = 2 }, b = 3 }
+{ a = { b = 1, c = 2, }, b = 3, }
 ```
 
 When fields are enclosed in double quotes (`"`), you can use string
@@ -461,7 +454,7 @@ interpolation to create or access fields:
 
 ```nickel #repl
 > let k = "a" in { "%{k}" = 1 }
-{ a = 1 }
+{ a = 1, }
 
 > let k = "a" in { a = 1 }."%{k}"
 1
@@ -487,7 +480,7 @@ Here are some valid conditional expressions in Nickel:
 "unequal"
 
 > ["1"] @ (if 42 == "42" then ["3"] else ["2"]) @ ["3"]
-["1", "2", "3"]
+[ "1", "2", "3" ]
 ```
 
 ### Let-In
@@ -518,7 +511,7 @@ true
 
 > let rec repeat = fun n x => if n <= 0 then [] else repeat (n - 1) x @ [x] in
     repeat 3 "foo"
-["foo", "foo", "foo"]
+[ "foo", "foo", "foo" ]
 ```
 
 ## Functions
@@ -574,7 +567,7 @@ left-associative, so `x |> f |> g` will be interpreted as `g (f x)`. For example
 
 ```nickel #repl
 > "Hello World" |> std.string.split " "
-["Hello", "World"]
+[ "Hello", "World" ]
 
 > "Hello World"
   |> std.string.split " "
@@ -622,14 +615,14 @@ Here are some examples of type annotations in Nickel:
 
 > 5 + "a" : _
 error: incompatible types
-[..]
+[...]
 
 > (1 + 1 : Number) + (('foo |> match { 'foo => 1, _ => 2 }) : Number)
 3
 
 > let x : Number = "a" in x
 error: incompatible types
-[..]
+[...]
 
 > let complex_argument : _ -> Number = fun {field1, field2, field3} => field1 in
     complex_argument {field1 = 5, field2 = null, field3 = false}
@@ -651,8 +644,8 @@ Here are some examples of contract annotations in Nickel:
 5
 
 > 5 | Bool
-error: contract broken by a value.
-[..]
+error: contract broken by a value
+[...]
 
 > let SmallNumber = std.contract.from_predicate (fun x => x < 5) in
   1 | SmallNumber
@@ -660,8 +653,8 @@ error: contract broken by a value.
 
 > let SmallNumber = std.contract.from_predicate (fun x => x < 5) in
   10 | SmallNumber
-error: contract broken by a value.
-[..]
+error: contract broken by a value
+[...]
 
 > let SmallNumber = std.contract.from_predicate (fun x => x < 5) in
   let NotTooSmallNumber = std.contract.from_predicate (fun x => x >= 2) in
@@ -713,15 +706,13 @@ constructor listed above). As soon as a term expression appears under a `forall`
 binder, the type variables aren't in scope anymore:
 
 ```nickel #repl
+# skip output check hide-line
 > forall a. a -> (a -> a) -> {_ : {foo : a}}
 <func>
 
 > forall a. a -> (a -> (fun x => a))
-error: unbound identifier
-  ┌─ repl-input-4:1:32
-  │
-1 │ forall a. a -> (a -> (fun x => a))
-  │                                ^ this identifier is unbound
+error: unbound identifier `a`
+[...]
 ```
 
 Here are some examples of more complicated types in Nickel:
@@ -731,7 +722,7 @@ Here are some examples of more complicated types in Nickel:
 5
 
 > {foo = [[1]]} : {foo : Array (Array Number)}
-{ foo = [ [ 1 ] ] }
+{ foo = [ [ 1 ] ], }
 
 > let select
     : forall a. {left: a, right: a} -> [| 'left, 'right |] -> a
@@ -745,20 +736,20 @@ Here are some examples of more complicated types in Nickel:
 true
 
 > let add_foo : forall a. {_: a} -> a -> {_: a} = fun dict value =>
-    record.insert "foo" value dict
+    std.record.insert "foo" value dict
   in
   add_foo {bar = 1} 5 : _
-{ bar = 1, foo = 5 }
+{ bar = 1, foo = 5, }
 
 > {foo = 1, bar = "string"} : {_ : Number}
 error: incompatible types
-  ┌─ repl-input-12:1:17
+  ┌─ <repl-input-77>:1:18
   │
-1 │ {foo = 1, bar = "string"} : {_ : Number}
-  │                 ^^^^^^^^ this expression
+1 │  {foo = 1, bar = "string"} : {_ : Number}
+  │                  ^^^^^^^^ this expression
   │
-  = The type of the expression was expected to be `Number`
-  = The type of the expression was inferred to be `String`
+  = Expected an expression of type `Number`
+  = Found an expression of type `String`
   = These types are not compatible
 ```
 
@@ -788,10 +779,10 @@ Here are some examples of record types in Nickel:
 
 ```nickel #repl
 > {foo = 1, bar = "foo" } : {foo : Number, bar: String}
-{ bar = "foo", foo = 1 }
+{ bar = "foo", foo = 1, }
 
 > {foo.bar = 1, baz = 2} : {foo: {bar : Number}, baz : Number}
-{ baz = 2, foo = { bar = 1 } }
+{ baz = 2, foo = { bar = 1, }, }
 ```
 
 Here, the right-hand side is missing a type annotation for `baz`, so it doesn't
@@ -801,23 +792,27 @@ qualify as a record type and is parsed as a record contract. This throws an
 ```nickel #repl
 > {foo = 1, bar = "foo" } : {foo : Number, bar : String, baz : Bool}
 error: type error: missing row `baz`
-  ┌─ <repl-input-0:1:1
-  │
-1 │ {foo = 1, bar = "foo" } : {foo : Number, bar : String, baz : Bool}
-  │ ^^^^^^^^^^^^^^^^^^^^^^^ this expression
-  │
-  = Expected an expression of type `{ foo : Number, bar : String, baz : Bool }`, which contains the field `baz`
-  = Found an expression of type `{ bar : String, foo : Number }`, which does not contain the field `baz`
+[...]
 ```
 
 If there's a metadata annotation apart from the type, the record cannot be
-parsed as a type. Consequently, it is considered a record contract and converted
-into an opaque type, yielding an error:
+parsed as a type. Consequently, Nickel tries to interpret it as a record
+contract which will most likely result in an error, because fields with a type
+annotation but no value are forbidden outside of types.
 
-```nickel #no-check
+```nickel #repl
 > {foo = 1, bar = "foo" } : {foo : Number, bar : String | optional}
-error: incompatible types
-[..]
+error: statically typed field without a definition
+  ┌─ <repl-input-81>:1:29
+  │
+1 │  {foo = 1, bar = "foo" } : {foo : Number, bar : String | optional}
+  │                             ^^^   ------ but it has a type annotation
+  │                             │
+  │                             this field doesn't have a definition
+  │
+  = A static type annotation must be attached to an expression but this field doesn't have a definition.
+  = Did you mean to use `|` instead of `:`, for example when defining a record contract?
+  = Typed fields without definitions are only allowed inside record types, but the enclosing record literal doesn't qualify as a record type. Please refer to the manual for the defining conditions of a record type.
 ```
 
 While in the following `MyDyn` isn't a proper type, the record literal `{foo :
@@ -827,7 +822,7 @@ parsed as such:
 ```nickel #repl
 > let MyDyn = fun label value => value in
     {foo = 1, bar | MyDyn = "foo"} : {foo : Number, bar : MyDyn}
-{ bar = "foo", foo = 1 }
+{ bar = "foo", foo = 1, }
 ```
 
 ## Metadata
@@ -844,7 +839,8 @@ Documentation can be attached with `| doc <string>`. For example:
 > let record = {
     value
       | doc "The number five"
-      | default = 5
+      | default
+      = 5
   }
 
 # Stop `core/tests/manual` from parsing this hide-line
@@ -862,8 +858,8 @@ Documentation can be attached with `| doc <string>`. For example:
           (Collins dictionary)
         "%
       = true,
-  }
-{ truth = true }
+  }.truth
+true
 ```
 
 Metadata can also set merge priorities using the following annotations:
@@ -881,26 +877,26 @@ Here are some examples using merge priorities in Nickel:
 ```nickel #repl
 > let Ais2ByDefault = { a | default = 2 } in
     {} | Ais2ByDefault
-{ a = 2 }
+{ a | default = 2, }
 
 > let Ais2ByDefault = { a | default = 2 } in
     { a = 1 } | Ais2ByDefault
-{ a = 1 }
+{ a = 1, }
 
 > { foo | default = 1, bar = foo + 1 }
-{ foo = 1, bar = 2 }
+{ bar = 2, foo | default = 1, }
 
 > {foo | default = 1, bar = foo + 1} & {foo = 2}
-{ foo = 2, bar = 3 }
+{ bar = 3, foo = 2, }
 
 > {foo | force = 1, bar = foo + 1} & {foo = 2}
-{ bar = 2, foo = 1 }
+{ bar = 2, foo | force = 1, }
 
 > {foo | priority 10 = 1} & {foo | priority 8 = 2} & {foo = 3}
-{ foo = 1 }
+{ foo | priority 10 = 1, }
 
 > {foo | priority -1 = 1} & {foo = 2}
-{ foo = 2 }
+{ foo = 2, }
 ```
 
 The `optional` annotation indicates that a field is not mandatory. It is usually
@@ -908,17 +904,19 @@ found in record contracts.
 
 ```nickel #repl
 > let Contract = {
-    foo | Num,
-    bar | Num
+    foo | Number,
+    bar | Number
         | optional,
   }
+
 > let value | Contract = {foo = 1}
+
 > value
-{ foo = 1 }
+{ foo | Number = 1, }
 
 > {bar = 1} | Contract
 error: missing definition for `foo`
-[..]
+[...]
 ```
 
 The `not_exported` annotation indicates that a field should be skipped when a
@@ -926,13 +924,12 @@ record is serialized. This includes the output of the `nickel export` command:
 
 ```nickel #repl
 > let value = { foo = 1, bar | not_exported = 2}
+
 > value
-{ foo = 1, bar = 2 }
+{ bar = 2, foo = 1, }
 
 > std.serialize 'Json value
-"{
-  "foo": 1
-}"
+"{\n  \"foo\": 1\n}"
 ```
 
 [nix-string-context]: https://shealevy.com/blog/2018/08/05/understanding-nixs-string-context/
diff --git a/doc/manual/types-vs-contracts.md b/doc/manual/types-vs-contracts.md
index 344165f1..b1c69f9b 100644
--- a/doc/manual/types-vs-contracts.md
+++ b/doc/manual/types-vs-contracts.md
@@ -48,13 +48,11 @@ What to do depends on the context:
     > let foo : Number =
         let addTwo = fun x => x + 2 in
         addTwo 4
-      in {}
 
     > let foo : Number =
         let ev : ((Number -> Number) -> Number) -> Number -> Number
           = fun f x => f (std.function.const x) in
         ev (fun f => f 0) 1
-      in {}
     ```
 
 ## Data (records and arrays)
diff --git a/doc/manual/typing.md b/doc/manual/typing.md
index 15fb533e..e1fe0021 100644
--- a/doc/manual/typing.md
+++ b/doc/manual/typing.md
@@ -45,8 +45,8 @@ example:
 configuration using `nickel export`, we get a reasonable error message:
 
 ```text
-error: type error
-  ┌─ repl-input-0:8:16
+error: dynamic type error
+  ┌─ <repl-input-0>:8:16
   │
 3 │   version = "0.1.1",
   │             ------- evaluated to this
@@ -54,7 +54,7 @@ error: type error
 8 │       "hello-%{version + 1}",
   │                ^^^^^^^ this expression has type String, but Number was expected
   │
-  = +, 1st argument
+  = (+) expects its 1st argument to be a Number
 ```
 
 While dynamic typing is fine for configuration code, the trouble begins once we
@@ -69,15 +69,15 @@ filter (fun x => if x % 2 == 0 then x else -1) [1,2,3,4,5,6]
 Result:
 
 ```text
-error: type error
-  ┌─ repl-input-3:2:40
+error: dynamic type error
+  ┌─ <repl-input-0>:2:40
   │
 2 │   std.array.fold_left (fun acc x => if pred x then acc @ [x] else acc) [] l in
   │                                        ^^^^^^ this expression has type Number, but Bool was expected
 3 │ filter (fun x => if x % 2 == 0 then x else -1) [1,2,3,4,5,6]
   │                                            -- evaluated to this
   │
-  = if
+  = the condition in an if expression must have type Bool
 ```
 
 This example illustrates how dynamic typing delays type errors, making them
@@ -130,13 +130,13 @@ Result:
 
 ```text
 error: incompatible types
-  ┌─ repl-input-6:3:18
+  ┌─ <repl-input-0>:3:18
   │
 3 │ filter (fun x => if x % 2 == 0 then x else -1) [1,2,3,4,5,6]) : Array Number
   │                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this expression
   │
-  = The type of the expression was expected to be `Bool`
-  = The type of the expression was inferred to be `Number`
+  = Expected an expression of type `Bool`
+  = Found an expression of type `Number`
   = These types are not compatible
 ```
 
@@ -311,23 +311,18 @@ annotation. If we try to add a call to `filter` on an array of strings, the
 typechecker surprisingly rejects our code:
 
 ```nickel #no-check
-(let filter = ... in
-let result = filter (fun x => x % 2 == 0) [1,2,3,4,5,6] in
-let dummy = filter (fun s => std.string.length s > 2) ["a","ab","abcd"] in
-result) : Array Number
-```
-
-Result:
-
-```text
+> (let filter = ... in
+  let result = filter (fun x => x % 2 == 0) [1,2,3,4,5,6] in
+  let dummy = filter (fun s => std.string.length s > 2) ["a","ab","abcd"] in
+  result) : Array Number
 error: incompatible types
-  ┌─ repl-input-1:5:48
+  ┌─ <repl-input-1:4:48
   │
-5 │ let dummy = filter (fun s => std.string.length s > 2) ["a","ab","abcd"] in
+4 │ let dummy = filter (fun s => std.string.length s > 2) ["a","ab","abcd"] in
   │                                                ^ this expression
   │
-  = The type of the expression was expected to be `String`
-  = The type of the expression was inferred to be `Number`
+  = Expected an expression of type `String`
+  = Found an expression of type `Number`
   = These types are not compatible
 ```
 
@@ -354,31 +349,27 @@ like Nickel.
 In a configuration language, you will often find yourself handling records of
 various kinds. In a simple type system, you can hit the following issue:
 
-```nickel #parse
-(
-  let add_total : { total : Number } -> { total : Number } -> Number
-    = fun r1 r2 => r1.total + r2.total
-    in
-  let r1 = { jan = 200, feb = 300, march = 10, total = jan + feb } in
-  let r2 = { aug = 50, sept = 20, total = aug + sept } in
-  let r3 = { may = 1300, june = 400, total = may + june } in
-  {
-    partial1 = add_total r1 r2,
-    partial2 = add_total r2 r3,
-  }
-) : { partial1 : Number, partial2 : Number }
-```
-
-```text
-error: type error: extra row `jan`
-  ┌─ src.ncl:9:25
+```nickel #repl
+> (
+    let add_total : { total : Number } -> { total : Number } -> Number
+      = fun r1 r2 => r1.total + r2.total
+      in
+    let r1 = { jan = 200, feb = 300, march = 10, total = jan + feb } in
+    let r2 = { aug = 50, sept = 20, total = aug + sept } in
+    let r3 = { may = 1300, june = 400, total = may + june } in
+    {
+      partial1 = add_total r1 r2,
+      partial2 = add_total r2 r3,
+    }
+  ) : { partial1 : Number, partial2 : Number }
+error: type error: extra row `march`
+  ┌─ <repl-input-0>:9:28
   │
-9 │     partial1 = add_total r1 r2,
-  │                         ^^ this expression
+9 │       partial1 = add_total r1 r2,
+  │                            ^^ this expression
   │
-  = The type of the expression was expected to be `{total: Number}`, which does not contain the field `jan`
-  = The type of the expression was inferred to be `{jan: Number, march: Number, feb: Number, total: Number}`, which contai
-ns the extra field `jan`
+  = Expected an expression of type `{ total : Number }`, which does not contain the field `march`
+  = Found an expression of type `{ total : Number, march : Number, feb : Number, jan : Number }`, which contains the extra field `march`
 ```
 
 The problem here is that for this code to run fine, the requirement of
@@ -393,25 +384,20 @@ similar to polymorphism, but instead of substituting a parameter for a single ty
 we can substitute a parameter for a whole sequence of field declarations, also
 referred to as rows:
 
-```nickel
-(
-  let add_total : forall a b. { total : Number; a } -> { total : Number; b } -> Number
-    = fun r1 r2 => r1.total + r2.total
-    in
-  let r1 = { jan = 200, feb = 300, march = 10, total = jan + feb } in
-  let r2 = { aug = 50, sept = 20, total = aug + sept } in
-  let r3 = { may = 1300, june = 400, total = may + june } in
-  {
-    partial1 = add_total r1 r2,
-    partial2 = add_total r2 r3,
-  }
-) : { partial1 : Number, partial2 : Number }
-```
-
-Result:
-
-```nickel
-{ partial1 = 570, partial2 = 1770 }
+```nickel #repl
+> (
+    let add_total : forall a b. { total : Number; a } -> { total : Number; b } -> Number
+      = fun r1 r2 => r1.total + r2.total
+      in
+    let r1 = { jan = 200, feb = 300, march = 10, total = jan + feb } in
+    let r2 = { aug = 50, sept = 20, total = aug + sept } in
+    let r3 = { may = 1300, june = 400, total = may + june } in
+    {
+      partial1 = add_total r1 r2,
+      partial2 = add_total r2 r3,
+    }
+  ) : { partial1 : Number, partial2 : Number }
+{ partial1 = 570, partial2 = 1770, }
 ```
 
 In the type of `add_total`, the part `{total: Number ; a}` expresses exactly what
@@ -476,42 +462,19 @@ Fortunately, Nickel does have a mechanism to prevent this from happening and to
 provide good error reporting in this situation. Let us see that by ourselves by
 calling to the statically typed `std.array.filter` from dynamically typed code:
 
-```nickel #parse
-std.array.filter (fun x => if x % 2 == 0 then x else null) [1,2,3,4,5,6]
-```
-
-Result:
-
-```text
-error: contract broken by the caller
-    ┌─ <stdlib/std.ncl>:333:25
+```nickel #repl
+> std.array.filter (fun x => if x % 2 == 0 then x else null) [1,2,3,4,5,6]
+error: contract broken by the caller of `filter`
+    ┌─ <stdlib/std.ncl>:349:25
     │
-333 │       : forall a. (a -> Bool) -> Array a -> Array a
+349 │       : forall a. (a -> Bool) -> Array a -> Array a
     │                         ---- expected return type of a function provided by the caller
     │
-    ┌─ repl-input-24:1:54
-    │
-  1 │ std.array.filter (fun x => if x % 2 == 0 then x else -1) [1,2,3,4,5,6]
-    │                                                      -- evaluated to this expression
+    ┌─ <repl-input-2>:1:55
     │
-    ┌─ <unknown> (generated by evaluation):1:1
-    │
-  1 │ -1
-    │ -- evaluated to this value
-    │
-    = This error may happen in the following situation:
-          1. A function `f` is bound by a contract: e.g. `(Number -> Number) -> Number`.
-          2. `f` takes another function `g` as an argument: e.g. `f = fun g => g 0`.
-          3. `f` is called by with an argument `g` that does not respect the contract: e.g. `f (fun x => false)`.
-    = Either change the contract accordingly, or call `f` with a function that returns a value of the right type.
-    = Note: this is an illustrative example. The actual error may involve deeper nested functions calls.
-
-note:
-  ┌─ repl-input-24:1:1
-  │
-1 │ std.array.filter (fun x => if x % 2 == 0 then x else -1) [1,2,3,4,5,6]
-  │ ---------------------------------------------------------------------- (1) calling filter
-
+  1 │  std.array.filter (fun x => if x % 2 == 0 then x else null) [1,2,3,4,5,6]
+    │                                                       ---- evaluated to this expression
+[...]
 ```
 
 We call `filter` from a dynamically typed location, but still get a spot-on
@@ -532,22 +495,17 @@ In the other direction, we face a different issue. Because dynamically typed
 code just get assigned the `Dyn` type most of the time, we can't use a
 dynamically typed value inside a statically typed block directly:
 
-```nickel #parse
-let x = 0 + 1 in
-(1 + x : Number)
-```
-
-Result:
-
-```text
-error: Incompatible types
-  ┌─ repl-input-6:1:6
+```nickel #repl
+> let x = 0 + 1 in
+  (1 + x : Number)
+error: incompatible types
+  ┌─ <repl-input-3>:2:8
   │
-1 │ (1 + x : Number)
-  │      ^ this expression
+2 │   (1 + x : Number)
+  │        ^ this expression
   │
-  = The type of the expression was expected to be `Number`
-  = The type of the expression was inferred to be `Dyn`
+  = Expected an expression of type `Number`
+  = Found an expression of type `Dyn`
   = These types are not compatible
 ```
 
@@ -583,21 +541,16 @@ code that you know is correct but wouldn't typecheck:
 The typechecker accepts the code above, while it rejects a fully statically
 typed version because of the type mismatch between the if branches:
 
-```nickel #parse
-(1 + (if true then 0 else "a")) : Number
-```
-
-Result:
-
-```text
-error: Incompatible types
-  ┌─ repl-input-46:1:27
+```nickel #repl
+> (1 + (if true then 0 else "a")) : Number
+error: incompatible types
+  ┌─ <repl-input-4>:1:28
   │
-1 │ (1 + (if true then 0 else "a")) : Number
-  │                           ^^^ this expression
+1 │  (1 + (if true then 0 else "a")) : Number
+  │                            ^^^ this expression
   │
-  = The type of the expression was expected to be `Number`
-  = The type of the expression was inferred to be `String`
+  = Expected an expression of type `Number`
+  = Found an expression of type `String`
   = These types are not compatible
 ```
 
@@ -658,15 +611,15 @@ But this program is unfortunately rejected by the typechecker:
 Result:
 
 ```text
-error: Incompatible types
-  ┌─ repl-input-0:7:2
+error: incompatible types
+  ┌─ <repl-input-0>:7:2
   │
-7 │ (10 : Port)
-  │  ^^ this expression
+7 │ (10 - 1 : Port)
+  │  ^^^^^^ this expression
   │
-  = The type of the expression was expected to be `Port`
-  = The type of the expression was inferred to be `Number`
-  = These types are not compatible
+  = Expected an expression of type `Port` (a contract)
+  = Found an expression of type `Number`
+  = Static types and contracts are not compatible
 ```
 
 It turns out statically ensuring that an arbitrary expression will eventually
diff --git a/lsp/lsp-harness/src/lib.rs b/lsp/lsp-harness/src/lib.rs
index e53f08ba..412314c2 100644
--- a/lsp/lsp-harness/src/lib.rs
+++ b/lsp/lsp-harness/src/lib.rs
@@ -4,8 +4,8 @@ mod output;
 pub use jsonrpc::Server;
 use log::error;
 use lsp_types::{
-    CompletionParams, DocumentFormattingParams, GotoDefinitionParams, HoverParams, ReferenceParams,
-    Url,
+    CompletionParams, DocumentFormattingParams, DocumentSymbolParams, GotoDefinitionParams,
+    HoverParams, ReferenceParams, Url,
 };
 pub use output::LspDebug;
 use serde::Deserialize;
@@ -35,6 +35,7 @@ pub enum Request {
     Completion(CompletionParams),
     Formatting(DocumentFormattingParams),
     Hover(HoverParams),
+    Symbols(DocumentSymbolParams),
 }
 
 #[derive(Deserialize, Debug, Default)]
diff --git a/lsp/lsp-harness/src/output.rs b/lsp/lsp-harness/src/output.rs
index bbd1fbc0..013cb358 100644
--- a/lsp/lsp-harness/src/output.rs
+++ b/lsp/lsp-harness/src/output.rs
@@ -4,7 +4,7 @@
 
 use std::io::Write;
 
-use lsp_types::GotoDefinitionResponse;
+use lsp_types::{DocumentSymbolResponse, GotoDefinitionResponse};
 
 pub trait LspDebug {
     fn debug(&self, w: impl Write) -> std::io::Result<()>;
@@ -157,3 +157,34 @@ impl LspDebug for lsp_types::MarkupContent {
         write!(w, "{}", self.value)
     }
 }
+
+impl LspDebug for lsp_types::SymbolInformation {
+    fn debug(&self, mut w: impl Write) -> std::io::Result<()> {
+        let name = &self.name;
+        let kind = self.kind;
+        write!(w, "{name} ({kind:?})@{}", self.location.debug_str())
+    }
+}
+
+impl LspDebug for lsp_types::DocumentSymbol {
+    fn debug(&self, mut w: impl Write) -> std::io::Result<()> {
+        let name = &self.name;
+        let kind = self.kind;
+        let detail = self.detail.clone().unwrap_or_default();
+        write!(
+            w,
+            "{name} ({kind:?} / {detail:?})@{} in {}",
+            self.selection_range.debug_str(),
+            self.range.debug_str()
+        )
+    }
+}
+
+impl LspDebug for DocumentSymbolResponse {
+    fn debug(&self, w: impl Write) -> std::io::Result<()> {
+        match self {
+            DocumentSymbolResponse::Flat(xs) => xs.debug(w),
+            DocumentSymbolResponse::Nested(xs) => xs.debug(w),
+        }
+    }
+}
diff --git a/lsp/nls/src/cache.rs b/lsp/nls/src/cache.rs
index 7176a091..be273c72 100644
--- a/lsp/nls/src/cache.rs
+++ b/lsp/nls/src/cache.rs
@@ -11,7 +11,7 @@ use nickel_lang_core::{
 };
 
 use crate::linearization::{building::Building, AnalysisHost, Environment, LinRegistry};
-use crate::linearization::{CombinedLinearizer, TypeCollector};
+use crate::linearization::{CollectedTypes, CombinedLinearizer, TypeCollector};
 
 pub trait CacheExt {
     fn typecheck_with_analysis(
@@ -78,7 +78,7 @@ impl CacheExt for Cache {
                 initial_ctxt.clone(),
                 self,
                 lin,
-                (building, HashMap::new()),
+                (building, CollectedTypes::default()),
             )
             .map_err(|err| vec![Error::TypecheckError(err)])?;
 
diff --git a/lsp/nls/src/linearization/mod.rs b/lsp/nls/src/linearization/mod.rs
index 6160e7a0..7a31f307 100644
--- a/lsp/nls/src/linearization/mod.rs
+++ b/lsp/nls/src/linearization/mod.rs
@@ -101,7 +101,7 @@ pub struct LinRegistry {
     pub position_lookups: HashMap<FileId, PositionLookup>,
     pub usage_lookups: HashMap<FileId, UsageLookup>,
     pub parent_lookups: HashMap<FileId, ParentLookup>,
-    pub type_lookups: HashMap<FileId, HashMap<RichTermPtr, Type>>,
+    pub type_lookups: HashMap<FileId, CollectedTypes<Type>>,
 }
 
 impl LinRegistry {
@@ -113,7 +113,7 @@ impl LinRegistry {
         &mut self,
         file_id: FileId,
         linearization: Completed,
-        type_lookups: HashMap<RichTermPtr, Type>,
+        type_lookups: CollectedTypes<Type>,
         term: &RichTerm,
     ) {
         self.map.insert(file_id, linearization);
@@ -154,7 +154,10 @@ impl LinRegistry {
 
     pub fn get_type(&self, rt: &RichTerm) -> Option<&Type> {
         let file = rt.pos.as_opt_ref()?.src_id;
-        self.type_lookups.get(&file)?.get(&RichTermPtr(rt.clone()))
+        self.type_lookups
+            .get(&file)?
+            .terms
+            .get(&RichTermPtr(rt.clone()))
     }
 
     pub fn get_parent_chain<'a>(&'a self, rt: &'a RichTerm) -> Option<ParentChainIter<'a>> {
@@ -788,9 +791,24 @@ pub struct TypeCollector {
     term_ids: Vec<RichTermPtr>,
 }
 
+#[derive(Clone, Debug)]
+pub struct CollectedTypes<Ty> {
+    pub terms: HashMap<RichTermPtr, Ty>,
+    pub idents: HashMap<LocIdent, Ty>,
+}
+
+impl<Ty> Default for CollectedTypes<Ty> {
+    fn default() -> Self {
+        Self {
+            terms: HashMap::new(),
+            idents: HashMap::new(),
+        }
+    }
+}
+
 impl Linearizer for TypeCollector {
-    type Building = HashMap<RichTermPtr, UnifType>;
-    type Completed = HashMap<RichTermPtr, Type>;
+    type Building = CollectedTypes<UnifType>;
+    type Completed = CollectedTypes<Type>;
     type CompletionExtra = Extra;
     type ItemId = usize;
 
@@ -804,7 +822,7 @@ impl Linearizer for TypeCollector {
 
     fn add_term(&mut self, lin: &mut Self::Building, rt: &RichTerm, ty: UnifType) -> Option<usize> {
         self.term_ids.push(RichTermPtr(rt.clone()));
-        lin.insert(RichTermPtr(rt.clone()), ty);
+        lin.terms.insert(RichTermPtr(rt.clone()), ty);
         Some(self.term_ids.len() - 1)
     }
 
@@ -827,16 +845,33 @@ impl Linearizer for TypeCollector {
             }
         };
 
-        lin.into_iter()
+        let terms = lin
+            .terms
+            .into_iter()
             .map(|(rt, uty)| (rt, transform_type(uty)))
-            .collect()
+            .collect();
+        let idents = lin
+            .idents
+            .into_iter()
+            .map(|(id, uty)| (id, transform_type(uty)))
+            .collect();
+        CollectedTypes { terms, idents }
     }
 
     fn retype(&mut self, lin: &mut Self::Building, item_id: Option<usize>, new_type: UnifType) {
         if let Some(id) = item_id {
-            lin.insert(self.term_ids[id].clone(), new_type);
+            lin.terms.insert(self.term_ids[id].clone(), new_type);
         }
     }
+
+    fn retype_ident(
+        &mut self,
+        lin: &mut Self::Building,
+        ident: &nickel_lang_core::identifier::LocIdent,
+        new_type: UnifType,
+    ) {
+        lin.idents.insert((*ident).into(), new_type);
+    }
 }
 
 pub struct CombinedLinearizer<T, U>(pub T, pub U);
diff --git a/lsp/nls/src/requests/symbols.rs b/lsp/nls/src/requests/symbols.rs
index 41a79395..7bbb5f59 100644
--- a/lsp/nls/src/requests/symbols.rs
+++ b/lsp/nls/src/requests/symbols.rs
@@ -1,15 +1,10 @@
-use crate::{
-    files::uri_to_path,
-    linearization::interface::TermKind,
-    term::RawSpanExt,
-    trace::{Enrich, Trace},
-};
 use lsp_server::{RequestId, Response, ResponseError};
 use lsp_types::{DocumentSymbol, DocumentSymbolParams, SymbolKind};
 use nickel_lang_core::cache::SourcePath;
-use serde_json::Value;
+use nickel_lang_core::typ::Type;
 
 use crate::server::Server;
+use crate::{files::uri_to_path, term::RawSpanExt};
 
 pub fn handle_document_symbols(
     params: DocumentSymbolParams,
@@ -17,47 +12,44 @@ pub fn handle_document_symbols(
     server: &mut Server,
 ) -> Result<(), ResponseError> {
     let path = uri_to_path(&params.text_document.uri)?;
-    let file_id = server.cache.id_of(&SourcePath::Path(path)).unwrap();
-
-    if let Some(completed) = server.lin_registry.map.get(&file_id) {
-        Trace::enrich(&id, completed);
-        let symbols = completed
-            .linearization
-            .iter()
-            .filter_map(|item| match &item.kind {
-                TermKind::Declaration { id: name, .. } => {
-                    // Although the position maybe shouldn't be `None`, opening the std library
-                    // source inside VSCode made the LSP panic on the previous `item.pos.unwrap()`.
-                    // Before investigating further, let's not make the VSCode extension panic in
-                    // the meantime.
-                    let (file_id, span) = item.pos.into_opt()?.to_range();
-
-                    let range =
-                        codespan_lsp::byte_span_to_range(server.cache.files(), file_id, span)
-                            .unwrap();
-
-                    // `deprecated` is a required field but causes a warning although we are not
-                    // using it
-                    #[allow(deprecated)]
-                    Some(DocumentSymbol {
-                        name: name.to_string(),
-                        detail: Some(format!("{}", item.ty)),
-                        kind: SymbolKind::Variable,
-                        tags: None,
-                        range,
-                        selection_range: range,
-                        children: None,
-                        deprecated: None,
-                    })
-                }
-                _ => None,
+    let file_id = server
+        .cache
+        .id_of(&SourcePath::Path(path))
+        .ok_or_else(|| crate::error::Error::FileNotFound(params.text_document.uri.clone()))?;
+
+    let usage_lookups = server.lin_registry.usage_lookups.get(&file_id).unwrap();
+
+    let type_lookups = server
+        .lin_registry
+        .type_lookups
+        .get(&file_id)
+        .ok_or_else(|| crate::error::Error::FileNotFound(params.text_document.uri.clone()))?;
+
+    let mut symbols = usage_lookups
+        .symbols()
+        .filter_map(|ident| {
+            let (file_id, span) = ident.pos.into_opt()?.to_range();
+            let range =
+                codespan_lsp::byte_span_to_range(server.cache.files(), file_id, span).ok()?;
+            let ty = type_lookups.idents.get(&ident);
+
+            #[allow(deprecated)] // because the `deprecated` field is... wait for it... deprecated.
+            Some(DocumentSymbol {
+                name: ident.ident.to_string(),
+                detail: ty.map(Type::to_string),
+                kind: SymbolKind::Variable,
+                tags: None,
+                range,
+                selection_range: range,
+                children: None,
+                deprecated: None,
             })
-            .collect::<Vec<_>>();
+        })
+        .collect::<Vec<_>>();
+
+    symbols.sort_by_key(|s| s.range.start);
 
-        server.reply(Response::new_ok(id, symbols));
-    } else {
-        server.reply(Response::new_ok(id, Value::Null));
-    }
+    server.reply(Response::new_ok(id, symbols));
 
     Ok(())
 }
diff --git a/lsp/nls/src/usage.rs b/lsp/nls/src/usage.rs
index 8819a289..3a918498 100644
--- a/lsp/nls/src/usage.rs
+++ b/lsp/nls/src/usage.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 
 use nickel_lang_core::{
     environment::Environment as GenericEnvironment,
@@ -87,6 +87,10 @@ pub struct UsageLookup {
     // environments) but its enough for us now.
     def_table: HashMap<RawSpan, Environment>,
     usage_table: HashMap<LocIdent, Vec<LocIdent>>,
+    // The list of all the symbols (and their locations) in the document.
+    //
+    // Currently, variables bound in `let` bindings and record fields count as symbols.
+    syms: HashSet<LocIdent>,
 }
 
 impl UsageLookup {
@@ -125,6 +129,15 @@ impl UsageLookup {
             .and_then(|span| self.def_table.get(span))
     }
 
+    /// Return the list of symbols in the document.
+    pub fn symbols(&self) -> impl Iterator<Item = LocIdent> + '_ {
+        self.syms.iter().cloned()
+    }
+
+    fn add_sym(&mut self, id: impl Into<LocIdent>) {
+        self.syms.insert(id.into());
+    }
+
     fn fill(&mut self, rt: &RichTerm, env: &Environment) {
         rt.traverse_ref(
             &mut |term: &RichTerm, env: &Environment| {
@@ -154,6 +167,7 @@ impl UsageLookup {
                     Term::Let(id, val, body, attrs) => {
                         let mut new_env = env.clone();
                         new_env.def(*id, Some(val.clone()), None);
+                        self.add_sym(*id);
 
                         self.fill(val, if attrs.rec { &new_env } else { env });
                         self.fill(body, &new_env);
@@ -164,6 +178,7 @@ impl UsageLookup {
                         let mut new_env = env.clone();
                         if let Some(id) = maybe_id {
                             new_env.def(*id, Some(val.clone()), None);
+                            self.add_sym(*id);
                         }
 
                         for m in &pat.matches {
@@ -173,7 +188,8 @@ impl UsageLookup {
                                     term: val.clone(),
                                     path,
                                 };
-                                new_env.def(id, Some(term), Some(field.metadata));
+                                new_env.def(id, Some(term.clone()), Some(field.metadata));
+                                self.add_sym(id);
                             }
                         }
                         TraverseControl::ContinueWithScope(new_env)
@@ -185,6 +201,7 @@ impl UsageLookup {
                         // all the fields in the environment and then recurse into their values.
                         for (id, field) in &data.fields {
                             new_env.def(*id, field.value.clone(), Some(field.metadata.clone()));
+                            self.add_sym(*id);
                         }
 
                         TraverseControl::ContinueWithScope(new_env)


Test patch:
diff --git a/core/tests/manual/main.rs b/core/tests/manual/main.rs
index c5cc85e1..766831c2 100644
--- a/core/tests/manual/main.rs
+++ b/core/tests/manual/main.rs
@@ -1,11 +1,18 @@
-use std::{fs::File, io::read_to_string};
+use std::{fs::File, io::read_to_string, iter::once};
 
+use codespan_reporting::term::termcolor::NoColor;
 use comrak::{
     arena_tree::{Node, NodeEdge},
     nodes::{Ast, AstNode, NodeCodeBlock, NodeValue},
     parse_document, ComrakOptions,
 };
+use nickel_lang_core::{
+    error,
+    eval::cache::CacheImpl,
+    repl::{EvalResult, Repl, ReplImpl},
+};
 use nickel_lang_utils::{project_root::project_root, test_program};
+use pretty_assertions::assert_str_eq;
 use test_generator::test_resources;
 use typed_arena::Arena;
 
@@ -28,10 +35,17 @@ enum CodeBlockType {
     /// > std.function.id 5
     /// 5
     /// ```
-    /// We interpret lines starting with `> ` directly after an empty line
-    /// and followed by indented lines (starting with at least one `' '`) as
-    /// a single Nickel program and try to parse them. For now, we don't check
-    /// evaluation. REPL inputs must be separated by an empty line.
+    /// We start a separate REPL session for each such block and interpret lines
+    /// starting with `> ` directly after an empty line and followed by indented
+    /// lines (starting with at least one `' '`) as a single REPL input. In
+    /// particular REPL inputs must be separated by an empty line.
+    ///
+    /// Any text following a REPL input directly, without containing an empty
+    /// line, is interpreted as an expected result. If it starts with `error:`,
+    /// we expect the evaluation to produce an error and match the error report
+    /// with the expected result. A final `[...]` means that the expected
+    /// result is merely a prefix of the error report. If it doesn't start with
+    /// `error:`, the evaluation is expected to succeed.
     Repl,
 }
 
@@ -53,14 +67,103 @@ impl CodeBlockType {
     }
 }
 
-fn check_repl_parts(content: String) {
+#[derive(Debug, Clone)]
+enum MessageExpectation {
+    Full(String),
+    Abridged(String),
+}
+
+#[derive(Debug, Clone)]
+enum ReplResult {
+    Value(String),
+    Error(MessageExpectation),
+    Empty,
+}
+
+fn extract_repl_piece(piece: impl AsRef<str>) -> (String, ReplResult) {
+    let lines: Vec<&str> = piece.as_ref().split_inclusive('\n').collect();
+    let split_index = lines
+        .iter()
+        .position(|l| !l.starts_with(' '))
+        .unwrap_or(lines.len());
+    let (program_lines, result_lines) = lines.split_at(split_index);
+
+    let result_string = result_lines.concat();
+    let result = if result_string.is_empty() {
+        ReplResult::Empty
+    } else if result_string.starts_with("error:") {
+        if let Some((result_string, _)) = result_string.rsplit_once("[...]") {
+            ReplResult::Error(MessageExpectation::Abridged(result_string.to_owned()))
+        } else {
+            ReplResult::Error(MessageExpectation::Full(result_string))
+        }
+    } else {
+        ReplResult::Value(result_string)
+    };
+
+    (program_lines.concat(), result)
+}
+
+/// Assert that two strings are equal except for possible trailing whitespace.
+/// The error reporting by `codespan` sometimes produces trailing whitespace
+/// which will be removed from the documentation markdown files.
+#[track_caller]
+fn assert_str_eq_approx(actual: impl AsRef<str>, expected: impl AsRef<str>) {
+    assert_str_eq!(
+        actual
+            .as_ref()
+            .lines()
+            .flat_map(|l| once(l.trim_end()).chain(once("\n")))
+            .collect::<String>()
+            .trim_end(),
+        expected.as_ref().trim_end()
+    );
+}
+
+#[track_caller]
+fn assert_prefix(actual: impl AsRef<str>, prefix: impl AsRef<str>) {
+    assert!(
+        actual.as_ref().starts_with(prefix.as_ref()),
+        "{} was expected to be a prefix of {}",
+        prefix.as_ref(),
+        actual.as_ref()
+    );
+}
+
+fn check_error_report(actual: impl AsRef<str>, expected: MessageExpectation) {
+    match expected {
+        MessageExpectation::Full(expected) => assert_str_eq_approx(actual, expected),
+        MessageExpectation::Abridged(prefix) => assert_prefix(actual, prefix),
+    }
+}
+
+fn check_repl(content: String) {
+    let mut repl = ReplImpl::<CacheImpl>::new(std::io::sink());
+    repl.load_stdlib().unwrap();
     for piece in content.split("\n\n") {
+        // We only process `piece`s starting with `>`. This way we can make the
+        // testing code ignore unknown REPL statements, e.g. `:query`.
         if let Some(piece) = piece.strip_prefix('>') {
-            let program = piece
-                .split_inclusive('\n')
-                .take_while(|l| l.starts_with(' '))
-                .collect();
-            check_parse_extended(program);
+            let (input, result) = extract_repl_piece(piece);
+
+            eprintln!(">{input}"); // Print the input to stderr to make tracking test failures easier
+            match (repl.eval_full(&input), result) {
+                (Ok(EvalResult::Evaluated(rt)), ReplResult::Value(expected)) => {
+                    assert_str_eq_approx(format!("{rt}"), expected)
+                }
+                (Ok(EvalResult::Bound(_)), ReplResult::Empty) => (),
+                (Err(e), ReplResult::Error(expected)) => {
+                    let mut error = NoColor::new(Vec::<u8>::new());
+                    let stdlib_ids = repl.cache_mut().get_all_stdlib_modules_file_id();
+                    let files = repl.cache_mut().files_mut();
+                    error::report_with(&mut error, files, stdlib_ids.as_ref(), e);
+
+                    check_error_report(String::from_utf8(error.into_inner()).unwrap(), expected);
+                }
+                (actual, expected) => {
+                    panic!("Evaluation produced {actual:?}, expected {expected:?}");
+                }
+            }
         }
     }
 }
@@ -70,11 +173,6 @@ fn check_eval(program: String) {
     test_program::eval(program).unwrap();
 }
 
-fn check_parse_extended(program: String) {
-    eprintln!("{program}"); // Print the program to stderr to make tracking test failures easier
-    test_program::parse_extended(&program).unwrap();
-}
-
 fn check_parse(program: String) {
     eprintln!("{program}"); // Print the program to stderr to make tracking test failures easier
     test_program::parse(&program).unwrap();
@@ -90,7 +188,7 @@ impl CodeBlock {
                 }
             }
             CodeBlockType::Parse => check_parse(self.content),
-            CodeBlockType::Repl => check_repl_parts(self.content),
+            CodeBlockType::Repl => check_repl(self.content),
         }
     }
 }
diff --git a/lsp/nls/test2.ncl b/lsp/nls/test2.ncl
new file mode 100644
index 00000000..aea6f0da
--- /dev/null
+++ b/lsp/nls/test2.ncl
@@ -0,0 +1,2 @@
+let x : { _ : { foo : Number | default = 1 } } = {} in
+x.PATH.foo
diff --git a/lsp/nls/tests/inputs/symbols-basic.ncl b/lsp/nls/tests/inputs/symbols-basic.ncl
new file mode 100644
index 00000000..fba1a615
--- /dev/null
+++ b/lsp/nls/tests/inputs/symbols-basic.ncl
@@ -0,0 +1,15 @@
+### /syms.ncl
+let foo = 3 in
+let func = fun x => 1 in
+{
+  name = "value",
+  other_name = {
+    inner_name = let inner_binding = 3 in "value",
+  },
+  type_checked_block = {
+    inner_name = { name = "value" },
+  } : _,
+}
+### [[request]]
+### type = "Symbols"
+### textDocument.uri = "file:///syms.ncl"
diff --git a/lsp/nls/tests/main.rs b/lsp/nls/tests/main.rs
index 9e86aee2..3537b957 100644
--- a/lsp/nls/tests/main.rs
+++ b/lsp/nls/tests/main.rs
@@ -2,7 +2,8 @@ use std::collections::{hash_map::Entry, HashMap};
 
 use assert_cmd::cargo::CommandCargoExt;
 use lsp_types::request::{
-    Completion, Formatting, GotoDefinition, HoverRequest, References, Request as LspRequest,
+    Completion, DocumentSymbolRequest, Formatting, GotoDefinition, HoverRequest, References,
+    Request as LspRequest,
 };
 use nickel_lang_utils::project_root::project_root;
 use test_generator::test_resources;
@@ -40,6 +41,7 @@ impl TestHarness {
             Request::Formatting(f) => self.request::<Formatting>(f),
             Request::Hover(h) => self.request::<HoverRequest>(h),
             Request::References(r) => self.request::<References>(r),
+            Request::Symbols(s) => self.request::<DocumentSymbolRequest>(s),
         }
     }
 
diff --git a/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__symbols-basic.ncl.snap b/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__symbols-basic.ncl.snap
new file mode 100644
index 00000000..6bb0416a
--- /dev/null
+++ b/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__symbols-basic.ncl.snap
@@ -0,0 +1,6 @@
+---
+source: lsp/nls/tests/main.rs
+expression: output
+---
+[foo (Variable / "Number")@0:4-0:7 in 0:4-0:7, func (Variable / "Dyn")@1:4-1:8 in 1:4-1:8, name (Variable / "String")@3:2-3:6 in 3:2-3:6, other_name (Variable / "Dyn")@4:2-4:12 in 4:2-4:12, inner_name (Variable / "Dyn")@5:4-5:14 in 5:4-5:14, inner_binding (Variable / "Number")@5:21-5:34 in 5:21-5:34, type_checked_block (Variable / "Dyn")@7:2-7:20 in 7:2-7:20, inner_name (Variable / "{ name : String }")@8:4-8:14 in 8:4-8:14, name (Variable / "String")@8:19-8:23 in 8:19-8:23]
+