Repository: tweag/nickel
Pull request #1615 (2023-09-20 13:53:45)

Run a REPL session for `nickel #repl` snippets
This is an enhancement of #1611 to improve testing REPL snippets. Instead of just making sure that REPL inputs parse correctly, we're now starting up a REPL session and run each command. This also enables us to compare the claimed output with what the REPL would actually produce.


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


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),
         }
     }
 }