diff --git a/core/src/cache.rs b/core/src/cache.rs index e59c9b1b10..96e98c9a62 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -283,7 +283,7 @@ impl From for OsString { SourcePath::Std(StdlibModule::Std) => "".into(), SourcePath::Std(StdlibModule::Internals) => "".into(), SourcePath::Query => "".into(), - SourcePath::ReplInput(idx) => format!(" format!("").into(), SourcePath::ReplTypecheck => "".into(), SourcePath::ReplQuery => "".into(), SourcePath::Override(path) => format!("", path.join(".")).into(), diff --git a/core/src/error.rs b/core/src/error.rs index 441292b582..6b14663c9e 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 for ColorChoice { /// infrastructure to point at specific locations and print snippets when needed. pub fn report>(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>( +pub fn report_with>( + writer: &mut dyn WriteColor, files: &mut Files, stdlib_ids: Option<&Vec>, 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 996db9494b..2ec5966101 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/core/tests/manual/main.rs b/core/tests/manual/main.rs index c5cc85e18c..766831c23a 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) -> (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, expected: impl AsRef) { + assert_str_eq!( + actual + .as_ref() + .lines() + .flat_map(|l| once(l.trim_end()).chain(once("\n"))) + .collect::() + .trim_end(), + expected.as_ref().trim_end() + ); +} + +#[track_caller] +fn assert_prefix(actual: impl AsRef, prefix: impl AsRef) { + 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, 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::::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::::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/doc/manual/contracts.md b/doc/manual/contracts.md index 810864c5d9..6a90167083 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 + ┌─ :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 + ┌─ :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 = , 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 + ┌─ :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 + ┌─ :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 + ┌─ (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 f1f2aabcd2..f7b28d1902 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 + ┌─ {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 + ┌─ :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 + ┌─ :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 + ┌─ :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 │ - ┌─ :2448:18 + ┌─ :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 4ea964779b..0acf5a630c 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}} > 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 + ┌─ :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` - ┌─ {foo = 1, bar = "foo" } : {foo : Number, bar : String | optional} -error: incompatible types -[..] +error: statically typed field without a definition + ┌─ :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 `. 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 `. 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 344165f185..b1c69f9b35 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 15fb533e25..e1fe0021d7 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 + ┌─ :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 + ┌─ :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 + ┌─ :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 + ┌─ 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` + ┌─ :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 - ┌─ :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` + ┌─ :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 + ┌─ :1:55 │ - ┌─ (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 + ┌─ :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 + ┌─ :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 + ┌─ :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