From eee41e3dd21a95eb9bbc423c0c7b6f096cd911a0 Mon Sep 17 00:00:00 2001 From: jneem Date: Thu, 26 Sep 2024 20:45:03 +0700 Subject: [PATCH] Adds a `nickel test` subcommand for testing examples in docs. (#2020) * Doctests * Add a custom_transform method * Make the vm private again * Update doc * Add some snapshot tests * Split out closurized and non-closurized variants of eval_record_spine * Fix doc link * Add some docs, but not sure where they should go * Make report_as_str honor the color setting * Separate the stdout and stderr outputs more intentionally * Commit the other snapshots --- Cargo.lock | 3 + cli/Cargo.toml | 6 +- cli/src/cli.rs | 9 +- cli/src/doctest.rs | 482 +++++++ cli/src/error.rs | 2 + cli/src/main.rs | 4 + .../inputs/doctest/fail_expected_error.ncl | 27 + .../inputs/doctest/fail_unexpected_error.ncl | 16 + .../inputs/doctest/fail_wrong_output.ncl | 31 + cli/tests/snapshot/inputs/doctest/pass.ncl | 50 + ..._eval_stderr_array_at_empty_array.ncl.snap | 4 +- ...eval_stderr_array_at_out_of_bound.ncl.snap | 4 +- ...derr_array_range_reversed_indices.ncl.snap | 4 +- ...rr_array_range_step_negative_step.ncl.snap | 4 +- ..._stderr_caller_contract_violation.ncl.snap | 4 +- ...__test_stderr_fail_expected_error.ncl.snap | 6 + ...test_stderr_fail_unexpected_error.ncl.snap | 6 + ...ot__test_stderr_fail_wrong_output.ncl.snap | 6 + .../snapshot__test_stderr_pass.ncl.snap | 5 + ...__test_stdout_fail_expected_error.ncl.snap | 21 + ...test_stdout_fail_unexpected_error.ncl.snap | 23 + ...ot__test_stdout_fail_wrong_output.ncl.snap | 75 ++ .../snapshot__test_stdout_pass.ncl.snap | 11 + core/src/cache.rs | 37 + core/src/error/mod.rs | 2 +- core/src/error/report.rs | 24 + core/src/program.rs | 150 ++- core/src/typecheck/mod.rs | 2 +- core/stdlib/std.ncl | 1145 +++++++++-------- doc/manual/cookbook.md | 195 +++ doc/manual/typing.md | 4 +- ..._tests__inputs__hover-stdlib-type.ncl.snap | 2 +- ...field_typed_block_regression_1574.ncl.snap | 4 +- 33 files changed, 1819 insertions(+), 549 deletions(-) create mode 100644 cli/src/doctest.rs create mode 100644 cli/tests/snapshot/inputs/doctest/fail_expected_error.ncl create mode 100644 cli/tests/snapshot/inputs/doctest/fail_unexpected_error.ncl create mode 100644 cli/tests/snapshot/inputs/doctest/fail_wrong_output.ncl create mode 100644 cli/tests/snapshot/inputs/doctest/pass.ncl create mode 100644 cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_expected_error.ncl.snap create mode 100644 cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_unexpected_error.ncl.snap create mode 100644 cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_wrong_output.ncl.snap create mode 100644 cli/tests/snapshot/snapshots/snapshot__test_stderr_pass.ncl.snap create mode 100644 cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_expected_error.ncl.snap create mode 100644 cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_unexpected_error.ncl.snap create mode 100644 cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_wrong_output.ncl.snap create mode 100644 cli/tests/snapshot/snapshots/snapshot__test_stdout_pass.ncl.snap diff --git a/Cargo.lock b/Cargo.lock index 2592cc09e0..6ed1258edd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1736,6 +1736,7 @@ version = "1.8.0" dependencies = [ "clap 4.5.2", "clap_complete", + "comrak", "directories", "git-version", "insta", @@ -1743,6 +1744,8 @@ dependencies = [ "metrics-util", "nickel-lang-core", "nickel-lang-utils", + "once_cell", + "regex", "serde", "serde_json", "tempfile", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d8623a41fa..be9449611f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -18,7 +18,7 @@ bench = false [features] default = ["repl", "doc", "format", "spanned-deser"] repl = ["nickel-lang-core/repl"] -doc = ["nickel-lang-core/doc"] +doc = ["nickel-lang-core/doc", "comrak"] format = ["nickel-lang-core/format", "dep:tempfile"] spanned-deser = ["nickel-lang-core/spanned-deser"] metrics = ["dep:metrics", "dep:metrics-util", "nickel-lang-core/metrics"] @@ -39,6 +39,10 @@ clap_complete = { workspace = true } metrics = { workspace = true, optional = true } metrics-util = { workspace = true, optional = true } +comrak = { workspace = true, optional = true } +once_cell.workspace = true +regex.workspace = true + [dev-dependencies] nickel-lang-utils.workspace = true test-generator.workspace = true diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 21237af48f..37339f2a40 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -13,7 +13,7 @@ use nickel_lang_core::error::report::ErrorFormat; use crate::repl::ReplCommand; #[cfg(feature = "doc")] -use crate::doc::DocCommand; +use crate::{doc::DocCommand, doctest::TestCommand}; #[cfg(feature = "format")] use crate::format::FormatCommand; @@ -63,7 +63,7 @@ pub struct GlobalOptions { /// Available subcommands. #[derive(clap::Subcommand, Debug)] pub enum Command { - /// Evaluate a Nickel program and pretty-print the result. + /// Evaluates a Nickel program and pretty-prints the result. Eval(EvalCommand), /// Converts the parsed representation (AST) back to Nickel source code and /// prints it. Used for debugging purpose @@ -72,7 +72,7 @@ pub enum Command { Export(ExportCommand), /// Prints the metadata attached to an attribute, given as a path Query(QueryCommand), - /// Typechecks the program but do not run it + /// Typechecks the program but does not run it Typecheck(TypecheckCommand), /// Starts a REPL session #[cfg(feature = "repl")] @@ -80,6 +80,9 @@ pub enum Command { /// Generates the documentation files for the specified nickel file #[cfg(feature = "doc")] Doc(DocCommand), + /// Tests the documentation examples in the specified nickel file + #[cfg(feature = "doc")] + Test(TestCommand), /// Format Nickel files #[cfg(feature = "format")] Format(FormatCommand), diff --git a/cli/src/doctest.rs b/cli/src/doctest.rs new file mode 100644 index 0000000000..0bde75a7a3 --- /dev/null +++ b/cli/src/doctest.rs @@ -0,0 +1,482 @@ +//! The `nickel test` command. +//! +//! Extracts tests from docstrings and evaluates them, printing out any failures. + +use std::{collections::HashMap, io::Write as _, path::PathBuf, rc::Rc}; + +use comrak::{arena_tree::NodeEdge, nodes::AstNode, Arena, ComrakOptions}; +use nickel_lang_core::{ + cache::{Cache, ImportResolver, InputFormat, SourcePath}, + error::{Error as CoreError, EvalError}, + eval::{cache::CacheImpl, Closure, Environment}, + identifier::{Ident, LocIdent}, + label::Label, + match_sharedterm, mk_app, mk_fun, + program::Program, + term::{ + make, record::RecordData, LabeledType, RichTerm, Term, Traverse as _, TraverseOrder, + TypeAnnotation, + }, + typ::{Type, TypeF}, +}; +use once_cell::sync::Lazy; +use regex::Regex; + +use crate::{ + cli::GlobalOptions, + customize::ExtractFieldOnly, + error::CliResult, + input::{InputOptions, Prepare}, +}; + +#[derive(clap::Parser, Debug)] +pub struct TestCommand { + #[command(flatten)] + pub input: InputOptions, +} + +/// The expected outcome of a test. +#[derive(Debug)] +enum Expected { + /// The test is expected to evaluate (without errors) to a specific value. + /// + /// The string here will be parsed into a nickel term, and then wrapped in a `std.contract.Equal` + /// contract to provide a nice error message. + Value(String), + /// The test is expected to raise an error, and the error message is expected to contain + /// this string as a substring. + Error(String), + /// The test is expected to evaluate without errors, but we don't care what it evaluates to. + None, +} + +impl Expected { + /// Parse out an expected outcome from a doctest. + /// + /// After ignoring whitespace-only trailing lines, we look for the last comment block in the doctest. + /// If that comment block has a line starting (modulo whitespace) with "=>", everything following + /// the "=>" is the expected value of the doctest. + /// + /// There are two special cases for tests that are expected to fail: + /// - if the "=>" line looks like "=> error: some text", the test is expected to exit with an error, + /// and the error message is supposed to contain "some text". + /// - if the "=>" line looks like "=> error", the test is expected to exit with an error (but we + /// don't care what the message is. + fn extract(doctest: &str) -> Self { + let mut lines: Vec<&str> = doctest.lines().collect(); + + // Throw away trailing empty lines. + let last_non_empty = lines + .iter() + .rposition(|line| !line.trim().is_empty()) + .unwrap_or(0); + lines.truncate(last_non_empty + 1); + + let mut expected = Vec::new(); + for line in lines.iter().rev() { + // If we encounter an uncommented line before we find a "=>", there's no expected value + // for this test. + let Some(commented) = line.trim_start().strip_prefix('#') else { + break; + }; + + if let Some(arrowed) = commented.trim_start().strip_prefix("=>") { + // We've found an expected value for the test. + if let Some(msg) = arrowed.trim_start().strip_prefix("error:") { + expected.push(msg.trim()); + expected.reverse(); + return Expected::Error(expected.join("\n")); + } else if arrowed.trim() == "error" { + return Expected::Error(String::new()); + } else { + expected.push(arrowed); + expected.reverse(); + return Expected::Value(expected.join("\n")); + } + } else { + expected.push(commented); + } + } + + Expected::None + } + + fn error(&self) -> Option { + match self { + Expected::Error(s) => Some(s.clone()), + _ => None, + } + } +} + +#[derive(Debug)] +struct DocTest { + input: String, + expected: Expected, +} + +struct TestEntry { + expected_error: Option, + field_name: LocIdent, + test_idx: usize, +} + +#[derive(Default)] +struct TestRegistry { + tests: HashMap, +} + +impl DocTest { + fn new(input: String) -> Self { + let expected = Expected::extract(&input); + DocTest { input, expected } + } +} + +struct Error { + /// The record path to the field whose doctest triggered this error. + path: Vec, + /// The field whose doctest triggered this error might have multiple tests in its + /// doc metadata. This is the index of the failing test. + idx: usize, + kind: ErrorKind, +} + +enum ErrorKind { + /// A doctest was expected to succeed, but it failed. + UnexpectedFailure { error: EvalError }, + /// A doctest was expected to fail, but instead it succeeded. + UnexpectedSuccess { result: RichTerm }, + /// A doctest failed with an unexpected message. + WrongTestFailure { message: String, expected: String }, +} + +// Go through the record spine, running tests one-by-one. +// +// `spine` is the already-evaluated record spine. It was previously transformed +// with [`doctest_transform`], so all the tests are present in the record spine. +// They've already been closurized with the correct environment. +fn run_tests( + path: &mut Vec, + prog: &mut Program, + errors: &mut Vec, + registry: &TestRegistry, + spine: &RichTerm, +) { + match spine.as_ref() { + Term::Record(data) | Term::RecRecord(data, ..) => { + for (id, field) in &data.fields { + if let Some(entry) = registry.tests.get(&id.ident()) { + let Some(val) = field.value.as_ref() else { + continue; + }; + + path.push(entry.field_name); + let path_display: Vec<_> = path.iter().map(|id| id.label()).collect(); + + print!("testing {}/{}...", path_display.join("."), entry.test_idx); + let _ = std::io::stdout().flush(); + + // Undo the test's lazy wrapper. + let result = prog.eval_closure(Closure { + body: mk_app!(val.clone(), Term::Null), + env: Environment::new(), + }); + + let err = match result { + Ok(v) => { + if entry.expected_error.is_some() { + Some(ErrorKind::UnexpectedSuccess { result: v }) + } else { + None + } + } + Err(e) => { + if let Some(expected) = &entry.expected_error { + let message = prog.report_as_str(e); + if !message.contains(expected) { + Some(ErrorKind::WrongTestFailure { + message, + expected: expected.clone(), + }) + } else { + None + } + } else { + Some(ErrorKind::UnexpectedFailure { error: e }) + } + } + }; + if let Some(err) = err { + println!("FAILED"); + errors.push(Error { + kind: err, + path: path.clone(), + idx: entry.test_idx, + }); + } else { + println!("ok"); + } + path.pop(); + } else if let Some(val) = field.value.as_ref() { + path.push(*id); + run_tests(path, prog, errors, registry, val); + path.pop(); + } + } + } + _ => {} + } +} + +impl TestCommand { + pub fn run(self, global: GlobalOptions) -> CliResult<()> { + let mut program = self.input.prepare(&global)?; + + let (spine, registry) = match self.prepare_tests(&mut program) { + Ok(x) => x, + Err(error) => return Err(crate::error::Error::Program { program, error }), + }; + + let mut path = Vec::new(); + let mut errors = Vec::new(); + run_tests(&mut path, &mut program, &mut errors, ®istry, &spine); + + let num_errors = errors.len(); + for e in errors { + let path_display: Vec<_> = e.path.iter().map(|id| id.label()).collect(); + let path_display = path_display.join("."); + match e.kind { + ErrorKind::UnexpectedSuccess { result } => { + println!( + "test {}/{} succeeded (evaluated to {result}), but it should have failed", + path_display, e.idx + ); + } + ErrorKind::WrongTestFailure { message, expected } => { + println!( + "test {}/{} failed, but the error didn't contain \"{expected}\". Actual error:\n{}", + path_display, e.idx, message, + ); + } + ErrorKind::UnexpectedFailure { error } => { + println!("test {}/{} failed", path_display, e.idx); + program.report_to_stdout( + error, + nickel_lang_core::error::report::ErrorFormat::Text, + ); + } + } + } + + if num_errors > 0 { + eprintln!("{num_errors} failures"); + Err(crate::error::Error::FailedTests) + } else { + Ok(()) + } + } + + fn prepare_tests( + self, + program: &mut Program, + ) -> Result<(RichTerm, TestRegistry), CoreError> { + let mut registry = TestRegistry::default(); + program.typecheck()?; + program + .custom_transform(|cache, rt| doctest_transform(cache, &mut registry, rt)) + .map_err(|e| e.unwrap_error("transforming doctest"))?; + Ok((program.eval_closurized_record_spine()?, registry)) + } +} + +/// Extract all the nickel code blocks from a single doc comment. +fn nickel_code_blocks<'a>(document: &'a AstNode<'a>) -> Vec { + use comrak::arena_tree::Node; + use comrak::nodes::{Ast, NodeCodeBlock, NodeValue}; + document + .traverse() + .flat_map(|ne| match ne { + // Question: can we extract enough location information so that + // we can munge the parsed AST to point into the doc comment? + NodeEdge::Start(Node { data, .. }) => match &*data.borrow() { + Ast { + value: NodeValue::CodeBlock(NodeCodeBlock { info, literal, .. }), + .. + } => info + .strip_prefix("nickel") + .map(|tag| match tag.trim() { + "ignore" => Vec::new(), + "multiline" => { + static BLANK_LINE: Lazy = + Lazy::new(|| Regex::new("\n\\s*\n").unwrap()); + BLANK_LINE + .split(literal) + .filter_map(|chunk| { + if !chunk.trim().is_empty() { + Some(DocTest::new(chunk.to_owned())) + } else { + None + } + }) + .collect() + } + _ => vec![DocTest::new(literal.to_owned())], + }) + .unwrap_or_default(), + _ => vec![], + }, + _ => vec![], + }) + .collect() +} + +// Transform a term by taking all its doctests and inserting them into the record next +// to the field that they're annotating. +// +// For example, +// { +// field | doc m%" +// ```nickel +// 1 + 1 +// ``` +// "% +// } +// becomes +// { +// field | doc m%" +// ```nickel +// 1 + 1 +// ``` +// "%, +// %0 = fun %1 => 1 + 1, +// } +// +// The idea is for the test to be evaluated in the same environment as the +// field that declares it. We wrap the test in a function so that it doesn't get +// evaluated too soon. +// +// The generated test field ids (i.e. `%0` in the example above) are collected +// in `registry` so that a later pass can go through and evaluate them. +// +// One disadvantage with this traversal approach is that any parse errors in +// the test will be encountered as soon as we explore the record spine. We might +// prefer to delay parsing the tests until it's time to evaluate them. +// The main advantage of this approach is that it makes it easy to have the test +// evaluated in the right environment. +fn doctest_transform( + cache: &mut Cache, + registry: &mut TestRegistry, + rt: RichTerm, +) -> Result { + // Get the path that of the current term, so we can pretend that test snippets + // came from the same path. This allows imports to work. + let path = rt + .pos + .as_opt_ref() + .and_then(|sp| cache.get_path(sp.src_id)) + .map(PathBuf::from); + + let source_path = match path { + Some(p) => SourcePath::Snippet(p), + None => SourcePath::Generated("test".to_owned()), + }; + + // Prepare a test snippet. Skips typechecking and transformations, because + // the returned term will get inserted into a bigger term that will be + // typechecked and transformed. + fn prepare( + cache: &mut Cache, + input: &str, + source_path: &SourcePath, + ) -> Result { + let src_id = cache.add_string(source_path.clone(), input.to_owned()); + cache.parse(src_id, InputFormat::Nickel)?; + // We could probably skip import resolution here also, but `Cache::get` insists + // that imports be resolved. + cache + .resolve_imports(src_id) + .map_err(|e| e.unwrap_error("test snippet"))?; + // unwrap: we just populated it + Ok(cache.get(src_id).unwrap()) + } + + let mut record_with_doctests = + |mut record_data: RecordData, dyn_fields, pos| -> Result<_, CoreError> { + let mut doc_fields: Vec<(Ident, RichTerm)> = Vec::new(); + for (id, field) in &record_data.fields { + if let Some(doc) = &field.metadata.doc { + let arena = Arena::new(); + let snippets = nickel_code_blocks(comrak::parse_document( + &arena, + doc, + &ComrakOptions::default(), + )); + + for (i, snippet) in snippets.iter().enumerate() { + let mut test_term = prepare(cache, &snippet.input, &source_path)?; + + if let Expected::Value(s) = &snippet.expected { + // Create the contract `std.contract.Equal ` and apply it to the + // test term. + let expected_term = prepare(cache, s, &source_path)?; + // unwrap: we just parsed it, so it will have a span + let expected_span = expected_term.pos.into_opt().unwrap(); + + let eq = make::static_access( + RichTerm::from(Term::Var("std".into())), + ["contract", "Equal"], + ); + let eq = mk_app!(eq, expected_term); + let eq_ty = Type::from(TypeF::Flat(eq)); + test_term = Term::Annotated( + TypeAnnotation { + typ: None, + contracts: vec![LabeledType { + typ: eq_ty.clone(), + label: Label { + typ: Rc::new(eq_ty), + span: expected_span, + ..Default::default() + }, + }], + }, + test_term, + ) + .into(); + } + + // Make the test term lazy, so that the tests don't automatically get evaluated + // just by evaluating the record spine. + let test_term = mk_fun!(LocIdent::fresh(), test_term); + let test_id = LocIdent::fresh().ident(); + let entry = TestEntry { + expected_error: snippet.expected.error(), + field_name: *id, + test_idx: i, + }; + registry.tests.insert(test_id, entry); + doc_fields.push((test_id, test_term)); + } + } + } + for (id, term) in doc_fields { + record_data.fields.insert(id.into(), term.into()); + } + Ok(RichTerm::from(Term::RecRecord(record_data, dyn_fields, None)).with_pos(pos)) + }; + + let mut traversal = |rt: RichTerm| -> Result { + let term = match_sharedterm!(match (rt.term) { + Term::RecRecord(record_data, dyn_fields, _deps) => { + record_with_doctests(record_data, dyn_fields, rt.pos)? + } + Term::Record(record_data) => { + record_with_doctests(record_data, Vec::new(), rt.pos)? + } + _ => rt, + }); + Ok(term) + }; + rt.traverse(&mut traversal, TraverseOrder::TopDown) +} diff --git a/cli/src/error.rs b/cli/src/error.rs index ea0fea0345..28de2308df 100644 --- a/cli/src/error.rs +++ b/cli/src/error.rs @@ -59,6 +59,7 @@ pub enum Error { /// /// Upon receiving this error, the caller should simply exit without proceeding with evaluation. CustomizeInfoPrinted, + FailedTests, } impl IntoDiagnostics for CliUsageError { @@ -258,6 +259,7 @@ impl Error { #[cfg(feature = "format")] Error::Format { error } => report_standalone("format error", Some(error.to_string())), Error::CliUsage { error, mut program } => program.report(error, format), + Error::FailedTests => report_standalone("tests failed", None), Error::CustomizeInfoPrinted => { // Nothing to do, the caller should simply exit. } diff --git a/cli/src/main.rs b/cli/src/main.rs index 18df73567d..387c32c0de 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,6 +2,8 @@ #[cfg(feature = "doc")] mod doc; +#[cfg(feature = "doc")] +mod doctest; #[cfg(feature = "format")] mod format; #[cfg(feature = "metrics")] @@ -48,6 +50,8 @@ fn main() -> ExitCode { #[cfg(feature = "doc")] Command::Doc(doc) => doc.run(opts.global), + #[cfg(feature = "doc")] + Command::Test(test) => test.run(opts.global), #[cfg(feature = "format")] Command::Format(format) => format.run(opts.global), diff --git a/cli/tests/snapshot/inputs/doctest/fail_expected_error.ncl b/cli/tests/snapshot/inputs/doctest/fail_expected_error.ncl new file mode 100644 index 0000000000..7eea6bef07 --- /dev/null +++ b/cli/tests/snapshot/inputs/doctest/fail_expected_error.ncl @@ -0,0 +1,27 @@ +# capture = 'all' +# command = ['test', '--color', 'never'] +{ + foo + | doc m%" + ```nickel + foo + # => error + ``` + + ```nickel + foo + "1" + # => error: wrong message + ``` + + There can be multiple tests in a single code block + + ```nickel multiline + foo + # => error + + 1 + # => error + ``` + "% + = 1, +} diff --git a/cli/tests/snapshot/inputs/doctest/fail_unexpected_error.ncl b/cli/tests/snapshot/inputs/doctest/fail_unexpected_error.ncl new file mode 100644 index 0000000000..20ca85c330 --- /dev/null +++ b/cli/tests/snapshot/inputs/doctest/fail_unexpected_error.ncl @@ -0,0 +1,16 @@ +# capture = 'all' +# command = ['test'] +{ + foo + | doc m%" + ```nickel + foo + "1" + ``` + + ```nickel + foo + "1" + # => 2 + ``` + "% + = 1, +} diff --git a/cli/tests/snapshot/inputs/doctest/fail_wrong_output.ncl b/cli/tests/snapshot/inputs/doctest/fail_wrong_output.ncl new file mode 100644 index 0000000000..cb299bcb80 --- /dev/null +++ b/cli/tests/snapshot/inputs/doctest/fail_wrong_output.ncl @@ -0,0 +1,31 @@ +# capture = 'all' +# command = ['test'] +{ + foo + | doc m%" + ```nickel + foo + # => 2 + ``` + + The output block can be split over multiple lines. + + ```nickel + foo + # => ( + # 2 + # ) + ``` + + There can be multiple tests in a code block. + + ```nickel multiline + foo + # => 2 + + foo + 1 + # => 3 + ``` + "% + = 1, +} diff --git a/cli/tests/snapshot/inputs/doctest/pass.ncl b/cli/tests/snapshot/inputs/doctest/pass.ncl new file mode 100644 index 0000000000..869c561671 --- /dev/null +++ b/cli/tests/snapshot/inputs/doctest/pass.ncl @@ -0,0 +1,50 @@ +# capture = 'all' +# command = ['test'] +{ + foo + | doc m%" + Here is some documentation with a test. + + ```nickel + foo + ``` + + The next test actually checks the value. + ```nickel + foo + # => 1 + ``` + + The trailing block can have multiple lines. + ```nickel + foo + # => ( + # 1 + # ) + ``` + + There can be multiple tests in a single code block + ```nickel multiline + foo + # => 1 + + 1 + # => 1 + ``` + "% + = 1, + + bar + | doc m%" + ```nickel + foo + "1" + # => error: this expression has type String, but Number was expected + ``` + + We can also test for an error without specifying the message. + ```nickel + foo + "1" + # => error + ``` + "% +} diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_at_empty_array.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_at_empty_array.ncl.snap index ab6e249b8b..65b7d36f63 100644 --- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_at_empty_array.ncl.snap +++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_at_empty_array.ncl.snap @@ -4,9 +4,9 @@ expression: err --- error: contract broken by the caller of `at` invalid array indexing - ┌─ :164:9 + ┌─ :165:9 │ -164 │ | std.contract.unstable.IndexedArrayFun 'Index +165 │ | std.contract.unstable.IndexedArrayFun 'Index │ -------------------------------------------- expected type │ ┌─ [INPUTS_PATH]/errors/array_at_empty_array.ncl:3:16 diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_at_out_of_bound.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_at_out_of_bound.ncl.snap index 1f42c95a48..454d46b5cf 100644 --- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_at_out_of_bound.ncl.snap +++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_at_out_of_bound.ncl.snap @@ -4,9 +4,9 @@ expression: err --- error: contract broken by the caller of `at` invalid array indexing - ┌─ :164:9 + ┌─ :165:9 │ -164 │ | std.contract.unstable.IndexedArrayFun 'Index +165 │ | std.contract.unstable.IndexedArrayFun 'Index │ -------------------------------------------- expected type │ ┌─ [INPUTS_PATH]/errors/array_at_out_of_bound.ncl:3:16 diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_reversed_indices.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_reversed_indices.ncl.snap index a06c9f1260..7408ee1e51 100644 --- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_reversed_indices.ncl.snap +++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_reversed_indices.ncl.snap @@ -4,9 +4,9 @@ expression: err --- error: contract broken by the caller of `range` invalid range - ┌─ :755:9 + ┌─ :769:9 │ -755 │ | std.contract.unstable.RangeFun Dyn +769 │ | std.contract.unstable.RangeFun Dyn │ ---------------------------------- expected type │ ┌─ [INPUTS_PATH]/errors/array_range_reversed_indices.ncl:3:19 diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_step_negative_step.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_step_negative_step.ncl.snap index a0750a16f1..ad589a24e2 100644 --- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_step_negative_step.ncl.snap +++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_array_range_step_negative_step.ncl.snap @@ -4,9 +4,9 @@ expression: err --- error: contract broken by the caller of `range_step` invalid range step - ┌─ :730:9 + ┌─ :744:9 │ -730 │ | std.contract.unstable.RangeFun (std.contract.unstable.RangeStep -> Dyn) +744 │ | std.contract.unstable.RangeFun (std.contract.unstable.RangeStep -> Dyn) │ ----------------------------------------------------------------------- expected type │ ┌─ [INPUTS_PATH]/errors/array_range_step_negative_step.ncl:3:27 diff --git a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_caller_contract_violation.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_caller_contract_violation.ncl.snap index e16b46d0ff..8a26cdc5dd 100644 --- a/cli/tests/snapshot/snapshots/snapshot__eval_stderr_caller_contract_violation.ncl.snap +++ b/cli/tests/snapshot/snapshots/snapshot__eval_stderr_caller_contract_violation.ncl.snap @@ -4,9 +4,9 @@ expression: err --- error: contract broken by the caller of `map` expected an array - ┌─ :148:33 + ┌─ :149:33 │ -148 │ : forall a b. (a -> b) -> Array a -> Array b +149 │ : forall a b. (a -> b) -> Array a -> Array b │ ------- expected type of the argument provided by the caller │ ┌─ [INPUTS_PATH]/errors/caller_contract_violation.ncl:3:31 diff --git a/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_expected_error.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_expected_error.ncl.snap new file mode 100644 index 0000000000..20fd1565e5 --- /dev/null +++ b/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_expected_error.ncl.snap @@ -0,0 +1,6 @@ +--- +source: cli/tests/snapshot/main.rs +expression: err +--- +4 failures +error: tests failed diff --git a/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_unexpected_error.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_unexpected_error.ncl.snap new file mode 100644 index 0000000000..51be7e1057 --- /dev/null +++ b/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_unexpected_error.ncl.snap @@ -0,0 +1,6 @@ +--- +source: cli/tests/snapshot/main.rs +expression: err +--- +2 failures +error: tests failed diff --git a/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_wrong_output.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_wrong_output.ncl.snap new file mode 100644 index 0000000000..20fd1565e5 --- /dev/null +++ b/cli/tests/snapshot/snapshots/snapshot__test_stderr_fail_wrong_output.ncl.snap @@ -0,0 +1,6 @@ +--- +source: cli/tests/snapshot/main.rs +expression: err +--- +4 failures +error: tests failed diff --git a/cli/tests/snapshot/snapshots/snapshot__test_stderr_pass.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__test_stderr_pass.ncl.snap new file mode 100644 index 0000000000..7e3d9f24f4 --- /dev/null +++ b/cli/tests/snapshot/snapshots/snapshot__test_stderr_pass.ncl.snap @@ -0,0 +1,5 @@ +--- +source: cli/tests/snapshot/main.rs +expression: err +--- + diff --git a/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_expected_error.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_expected_error.ncl.snap new file mode 100644 index 0000000000..c35cb20aa8 --- /dev/null +++ b/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_expected_error.ncl.snap @@ -0,0 +1,21 @@ +--- +source: cli/tests/snapshot/main.rs +expression: out +--- +testing foo/0...FAILED +testing foo/1...FAILED +testing foo/2...FAILED +testing foo/3...FAILED +test foo/0 succeeded (evaluated to 1), but it should have failed +test foo/1 failed, but the error didn't contain "wrong message". Actual error: +error: dynamic type error + ┌─ [INPUTS_PATH]/doctest/fail_expected_error.ncl:1:7 + │ +1 │ foo + "1" + │ ^^^ this expression has type String, but Number was expected + │ + = (+) expects its 2nd argument to be a Number + + +test foo/2 succeeded (evaluated to 1), but it should have failed +test foo/3 succeeded (evaluated to 1), but it should have failed diff --git a/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_unexpected_error.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_unexpected_error.ncl.snap new file mode 100644 index 0000000000..077f041d5d --- /dev/null +++ b/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_unexpected_error.ncl.snap @@ -0,0 +1,23 @@ +--- +source: cli/tests/snapshot/main.rs +expression: out +--- +testing foo/0...FAILED +testing foo/1...FAILED +test foo/0 failed +error: dynamic type error + ┌─ [INPUTS_PATH]/doctest/fail_unexpected_error.ncl:1:7 + │ +1 │ foo + "1" + │ ^^^ this expression has type String, but Number was expected + │ + = (+) expects its 2nd argument to be a Number + +test foo/1 failed +error: dynamic type error + ┌─ [INPUTS_PATH]/doctest/fail_unexpected_error.ncl:1:7 + │ +1 │ foo + "1" + │ ^^^ this expression has type String, but Number was expected + │ + = (+) expects its 2nd argument to be a Number diff --git a/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_wrong_output.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_wrong_output.ncl.snap new file mode 100644 index 0000000000..a25de89d34 --- /dev/null +++ b/cli/tests/snapshot/snapshots/snapshot__test_stdout_fail_wrong_output.ncl.snap @@ -0,0 +1,75 @@ +--- +source: cli/tests/snapshot/main.rs +expression: out +--- +testing foo/0...FAILED +testing foo/1...FAILED +testing foo/2...FAILED +testing foo/3...FAILED +test foo/0 failed +error: contract broken by a value + ┌─ (generated by evaluation):1:1 + │ + 1 │ std.contract.Equal 2 + │ -------------------- expected type + │ + ┌─ [INPUTS_PATH]/doctest/fail_wrong_output.ncl:1:1 + │ + 1 │ foo + │ ^^^ applied to this expression + │ + ┌─ [INPUTS_PATH]/doctest/fail_wrong_output.ncl:30:7 + │ +30 │ = 1, + │ - evaluated to this expression + +test foo/1 failed +error: contract broken by a value + ┌─ (generated by evaluation):1:1 + │ + 1 │ std.contract.Equal 2 + │ -------------------- expected type + │ + ┌─ [INPUTS_PATH]/doctest/fail_wrong_output.ncl:1:1 + │ + 1 │ foo + │ ^^^ applied to this expression + │ + ┌─ [INPUTS_PATH]/doctest/fail_wrong_output.ncl:30:7 + │ +30 │ = 1, + │ - evaluated to this expression + +test foo/2 failed +error: contract broken by a value + ┌─ (generated by evaluation):1:1 + │ + 1 │ std.contract.Equal 2 + │ -------------------- expected type + │ + ┌─ [INPUTS_PATH]/doctest/fail_wrong_output.ncl:1:1 + │ + 1 │ foo + │ ^^^ applied to this expression + │ + ┌─ [INPUTS_PATH]/doctest/fail_wrong_output.ncl:30:7 + │ +30 │ = 1, + │ - evaluated to this expression + +test foo/3 failed +error: contract broken by a value + ┌─ (generated by evaluation):1:1 + │ +1 │ std.contract.Equal 3 + │ -------------------- expected type + │ + ┌─ [INPUTS_PATH]/doctest/fail_wrong_output.ncl:1:1 + │ +1 │ foo + 1 + │ ^^^^^^^ applied to this expression + │ + ┌─ (generated by evaluation):1:1 + │ +1 │ 2 + │ - evaluated to this value diff --git a/cli/tests/snapshot/snapshots/snapshot__test_stdout_pass.ncl.snap b/cli/tests/snapshot/snapshots/snapshot__test_stdout_pass.ncl.snap new file mode 100644 index 0000000000..2cdfce2793 --- /dev/null +++ b/cli/tests/snapshot/snapshots/snapshot__test_stdout_pass.ncl.snap @@ -0,0 +1,11 @@ +--- +source: cli/tests/snapshot/main.rs +expression: out +--- +testing foo/0...ok +testing foo/1...ok +testing foo/2...ok +testing foo/3...ok +testing foo/4...ok +testing bar/0...ok +testing bar/1...ok diff --git a/core/src/cache.rs b/core/src/cache.rs index e259b685d5..b8afa37556 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -708,6 +708,43 @@ impl Cache { } } + /// Applies a custom transform to an input and its imports, leaving them + /// in the same state as before. Requires that the input has been parsed. + /// In order for the transform to apply to imports, they need to have been + /// resolved. + pub fn custom_transform( + &mut self, + file_id: FileId, + transform: &mut impl FnMut(&mut Cache, RichTerm) -> Result, + ) -> Result<(), CacheError> { + match self.entry_state(file_id) { + Some(state) if state >= EntryState::Parsed => { + if state < EntryState::Transforming { + let cached_term = self.terms.remove(&file_id).unwrap(); + let term = transform(self, cached_term.term)?; + self.terms.insert( + file_id, + TermEntry { + term, + state: EntryState::Transforming, + ..cached_term + }, + ); + + if let Some(imports) = self.imports.get(&file_id).cloned() { + for f in imports.into_iter() { + self.custom_transform(f, transform)?; + } + } + // TODO: We're setting the state back to whatever it was. + self.update_state(file_id, state); + } + Ok(()) + } + _ => Err(CacheError::NotParsed), + } + } + /// Apply program transformations to all the fields of a record. /// /// Used to transform stdlib modules and other records loaded in the environment, when using diff --git a/core/src/error/mod.rs b/core/src/error/mod.rs index faef695eb7..0d952e1eae 100644 --- a/core/src/error/mod.rs +++ b/core/src/error/mod.rs @@ -894,7 +894,7 @@ impl ParseError { } pub const INTERNAL_ERROR_MSG: &str = - "This error should not happen. This is likely a bug in the Nickel interpreter. Please consider\ + "This error should not happen. This is likely a bug in the Nickel interpreter. Please consider \ reporting it at https://github.com/tweag/nickel/issues with the above error message."; /// A trait for converting an error to a diagnostic. diff --git a/core/src/error/report.rs b/core/src/error/report.rs index 501f032bc8..0ad25fd648 100644 --- a/core/src/error/report.rs +++ b/core/src/error/report.rs @@ -78,6 +78,30 @@ pub fn report>( ) } +/// Pretty-print an error on stdout. +/// +/// # Arguments +/// +/// - `cache` is the file cache used during the evaluation, which is required by the reporting +/// infrastructure to point at specific locations and print snippets when needed. +pub fn report_to_stdout>( + cache: &mut Cache, + error: E, + format: ErrorFormat, + color_opt: ColorOpt, +) { + use std::io::{stdout, IsTerminal}; + + let stdlib_ids = cache.get_all_stdlib_modules_file_id(); + report_with( + &mut StandardStream::stdout(color_opt.for_terminal(stdout().is_terminal())).lock(), + cache.files_mut(), + stdlib_ids.as_ref(), + error, + format, + ) +} + /// Report an error on `stderr`, provided a file database and a list of stdlib file ids. pub fn report_with>( writer: &mut dyn WriteColor, diff --git a/core/src/program.rs b/core/src/program.rs index 63e93ad407..8c932bd9a6 100644 --- a/core/src/program.rs +++ b/core/src/program.rs @@ -22,8 +22,9 @@ //! Each such value is added to the initial environment before the evaluation of the program. use crate::{ cache::*, + closurize::Closurize as _, error::{ - report::{report, ColorOpt, ErrorFormat}, + report::{report, report_to_stdout, report_with, ColorOpt, ErrorFormat}, Error, EvalError, IOError, IntoDiagnostics, ParseError, }, eval::{cache::Cache as EvalCache, Closure, VirtualMachine}, @@ -31,18 +32,21 @@ use crate::{ label::Label, metrics::increment, term::{ - make as mk_term, make::builder, record::Field, BinaryOp, MergePriority, RichTerm, Term, + make::{self as mk_term, builder}, + record::Field, + BinaryOp, MergePriority, RichTerm, Term, }, }; +use clap::ColorChoice; use codespan::FileId; -use codespan_reporting::term::termcolor::Ansi; +use codespan_reporting::term::termcolor::{Ansi, NoColor, WriteColor}; use std::path::PathBuf; use std::{ ffi::OsString, fmt, - io::{self, Cursor, Read, Write}, + io::{self, Read, Write}, result::Result, }; @@ -389,6 +393,23 @@ impl Program { .clone()) } + /// Applies a custom transformation to the main term, assuming that it has been parsed but not + /// yet transformed. + /// + /// The term is left in whatever state it started in. + /// + /// This state-management isn't great, as it breaks the usual linear order of state changes. + /// In particular, there's no protection against double-applying the same transformation, and no + /// protection against applying it to a term that's in an unexpected state. + pub fn custom_transform(&mut self, mut transform: F) -> Result<(), CacheError> + where + F: FnMut(&mut Cache, RichTerm) -> Result, + { + self.vm + .import_resolver_mut() + .custom_transform(self.main_id, &mut transform) + } + /// Retrieve the parsed term, typecheck it, and generate a fresh initial environment. If /// `self.overrides` isn't empty, generate the required merge parts and return a merge /// expression including the overrides. Extract the field corresponding to `self.field`, if not @@ -456,6 +477,15 @@ impl Program { Ok(self.vm.eval_closure(prepared)?.body) } + /// Evaluate a closure using the same virtual machine (and import resolver) + /// as the main term. The closure should already have been prepared for + /// evaluation, with imports resolved and any necessary transformations + /// applied. + pub fn eval_closure(&mut self, closure: Closure) -> Result { + self.vm.reset(); + Ok(self.vm.eval_closure(closure)?.body) + } + /// Same as `eval`, but proceeds to a full evaluation. pub fn eval_full(&mut self) -> Result { let prepared = self.prepare_eval()?; @@ -533,6 +563,14 @@ impl Program { report(self.vm.import_resolver_mut(), error, format, self.color_opt) } + /// Wrapper for [`report_to_stdout`]. + pub fn report_to_stdout(&mut self, error: E, format: ErrorFormat) + where + E: IntoDiagnostics, + { + report_to_stdout(self.vm.import_resolver_mut(), error, format, self.color_opt) + } + /// Build an error report as a string and return it. pub fn report_as_str(&mut self, error: E) -> String where @@ -540,19 +578,27 @@ impl Program { { let cache = self.vm.import_resolver_mut(); let stdlib_ids = cache.get_all_stdlib_modules_file_id(); - let diagnostics = error.into_diagnostics(cache.files_mut(), stdlib_ids.as_ref()); - let mut buffer = Ansi::new(Cursor::new(Vec::new())); - let config = codespan_reporting::term::Config::default(); - // write to `buffer` - diagnostics - .iter() - .try_for_each(|d| { - codespan_reporting::term::emit(&mut buffer, &config, cache.files_mut(), d) - }) - // safe because writing to a cursor in memory - .unwrap(); - // unwrap(): emit() should only print valid utf8 to the the buffer - String::from_utf8(buffer.into_inner().into_inner()).unwrap() + + let mut buffer = Vec::new(); + let mut with_color; + let mut no_color; + let writer: &mut dyn WriteColor = if self.color_opt.0 == ColorChoice::Never { + no_color = NoColor::new(&mut buffer); + &mut no_color + } else { + with_color = Ansi::new(&mut buffer); + &mut with_color + }; + + report_with( + writer, + cache.files_mut(), + stdlib_ids.as_ref(), + error, + ErrorFormat::Text, + ); + // unwrap(): report_with() should only print valid utf8 to the the buffer + String::from_utf8(buffer).unwrap() } /// Evaluate a program into a record spine, a form suitable for extracting the general @@ -601,6 +647,33 @@ impl Program { /// [crate::error::EvalError::MissingFieldDef] errors are _ignored_: if this is encountered /// when evaluating a field, this field is just left as it is and the evaluation proceeds. pub fn eval_record_spine(&mut self) -> Result { + self.maybe_closurized_eval_record_spine(false) + } + + /// Evaluate a program into a record spine, while closurizing all the + /// non-record "leaves" in the spine. + /// + /// To understand the difference between this function and + /// [`Program::eval_record_spine`], consider a term like + /// + /// ```nickel + /// let foo = 1 in { bar = [foo] } + /// ``` + /// + /// `eval_record_spine` will evaluate this into a record containing the + /// field `bar`, and the value of that field will be a `Term::Array` + /// containing a `Term::Var("foo")`. In contrast, `eval_closurized` will + /// still evaluate the term into a record contining `bar`, but the value of + /// that field will be a `Term::Closure` containing that same `Term::Array`, + /// together with an `Environment` defining the variable "foo". In + /// particular, the closurized version is more useful if you intend to + /// further evaluate any record fields, while the non-closurized version is + /// more useful if you intend to do further static analysis. + pub fn eval_closurized_record_spine(&mut self) -> Result { + self.maybe_closurized_eval_record_spine(true) + } + + fn maybe_closurized_eval_record_spine(&mut self, closurize: bool) -> Result { use crate::eval::Environment; use crate::match_sharedterm; use crate::term::{record::RecordData, RuntimeContract}; @@ -613,12 +686,13 @@ impl Program { vm: &mut VirtualMachine, mut pending_contracts: Vec, current_env: Environment, + closurize: bool, ) -> Result, Error> { vm.reset(); for ctr in pending_contracts.iter_mut() { let rt = ctr.contract.clone(); - ctr.contract = do_eval(vm, rt, current_env.clone())?; + ctr.contract = do_eval(vm, rt, current_env.clone(), closurize)?; } Ok(pending_contracts) @@ -628,6 +702,7 @@ impl Program { vm: &mut VirtualMachine, term: RichTerm, env: Environment, + closurize: bool, ) -> Result { vm.reset(); let result = vm.eval_closure(Closure { @@ -660,12 +735,15 @@ impl Program { Field { value: field .value - .map(|value| do_eval(vm, value, result.env.clone())) + .map(|value| { + do_eval(vm, value, result.env.clone(), closurize) + }) .transpose()?, pending_contracts: eval_contracts( vm, field.pending_contracts, result.env.clone(), + closurize, )?, ..field }, @@ -678,11 +756,16 @@ impl Program { result.body.pos, )) } - _ => Ok(result.body), + _ => + if closurize { + Ok(result.body.closurize(&mut vm.cache, result.env)) + } else { + Ok(result.body) + }, }) } - do_eval(&mut self.vm, prepared.body, prepared.env) + do_eval(&mut self.vm, prepared.body, prepared.env, closurize) } /// Extract documentation from the program @@ -872,6 +955,31 @@ mod doc { } } } + + pub fn docstrings(&self) -> Vec<(Vec<&str>, &str)> { + fn collect<'a>( + slf: &'a ExtractedDocumentation, + path: &[&'a str], + acc: &mut Vec<(Vec<&'a str>, &'a str)>, + ) { + for (name, field) in &slf.fields { + let mut path = path.to_owned(); + path.push(name); + + if let Some(fields) = &field.fields { + collect(fields, &path, acc); + } + + if let Some(doc) = &field.documentation { + acc.push((path, doc)); + } + } + } + + let mut ret = Vec::new(); + collect(self, &[], &mut ret); + ret + } } /// Parses a string into markdown and increases any headers in the markdown by the specified diff --git a/core/src/typecheck/mod.rs b/core/src/typecheck/mod.rs index f8f1d06f46..5088f8e829 100644 --- a/core/src/typecheck/mod.rs +++ b/core/src/typecheck/mod.rs @@ -1372,7 +1372,7 @@ pub struct TypeTables { /// Typecheck a term. /// /// Return the inferred type in case of success. This is just a wrapper that calls -/// `type_check_linearize` with a blanket implementation for the visitor. +/// `type_check_with_visitor` with a blanket implementation for the visitor. /// /// Note that this function doesn't recursively typecheck imports (anymore), but just the current /// file. It however still needs the resolver to get the apparent type of imports. diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl index 4be3ef336a..aee41af5e8 100644 --- a/core/stdlib/std.ncl +++ b/core/stdlib/std.ncl @@ -52,11 +52,12 @@ # Examples - ```nickel - ([] | std.array.NonEmpty) => - error - ([ 1 ] | std.array.NonEmpty) => - [ 1 ] + ```nickel multiline + ([] | std.array.NonEmpty) + # => error: contract broken by a value + + ([ 1 ] | std.array.NonEmpty) + # => [ 1 ] ``` "% = @@ -79,8 +80,8 @@ # Examples ```nickel - std.array.first [ "this is the head", "this is not" ] => - "this is the head" + std.array.first [ "this is the head", "this is not" ] + # => "this is the head" ``` "% = fun array => %array/at% array 0, @@ -94,8 +95,8 @@ # Examples ```nickel - std.array.last [ "this is the head", "this is not" ] => - "this is not" + std.array.last [ "this is the head", "this is not" ] + # => "this is not" ``` "% = fun array => %array/at% array (%array/length% array - 1), @@ -109,8 +110,8 @@ # Examples ```nickel - std.array.drop_first [ 1, 2, 3 ] => - [ 2, 3 ] + std.array.drop_first [ 1, 2, 3 ] + # => [ 2, 3 ] ``` "% = fun array => %array/slice% 1 (%array/length% array) array, @@ -124,8 +125,8 @@ # Examples ```nickel - std.array.drop_last [ 1, 2, 3 ] => - [ 1, 2 ] + std.array.drop_last [ 1, 2, 3 ] + # => [ 1, 2 ] ``` "% = fun array => %array/slice% 0 (%array/length% array - 1) array, @@ -138,8 +139,8 @@ # Examples ```nickel - std.array.length [ "Hello,", " World!" ] => - 2 + std.array.length [ "Hello,", " World!" ] + # => 2 ``` "% = fun l => %array/length% l, @@ -153,8 +154,8 @@ # Examples ```nickel - std.array.map (fun x => x + 1) [ 1, 2, 3 ] => - [ 2, 3, 4 ] + std.array.map (fun x => x + 1) [ 1, 2, 3 ] + # => [ 2, 3, 4 ] ``` "% = fun f l => %array/map% l f, @@ -168,8 +169,8 @@ # Examples ```nickel - std.array.at 3 [ "zero", "one", "two", "three", "four" ] => - "three" + std.array.at 3 [ "zero", "one", "two", "three", "four" ] + # => "three" ``` "% = fun n l => %array/at% l n, @@ -184,12 +185,12 @@ # Examples - ```nickel - std.array.at_or 3 "default" [ "zero", "one", "two", "three" ] => - "three" + ```nickel multiline + std.array.at_or 3 "default" [ "zero", "one", "two", "three" ] + # => "three" - std.array.at_or 3 "default" [ "zero", "one" ] => - "default" + std.array.at_or 3 "default" [ "zero", "one" ] + # => "default" ``` "% = fun n default_value array => @@ -206,8 +207,8 @@ # Examples ```nickel - std.array.concat [ 1, 2, 3 ] [ 4, 5, 6 ] => - [ 1, 2, 3, 4, 5, 6 ] + std.array.concat [ 1, 2, 3 ] [ 4, 5, 6 ] + # => [ 1, 2, 3, 4, 5, 6 ] ``` "% = fun l1 l2 => l1 @ l2, @@ -236,9 +237,12 @@ ```nickel std.array.fold_right (-) 0 [1, 2, 3, 4] - => -2 + # => -2 + ``` + + ```nickel std.array.fold_left (-) 0 [1, 2, 3, 4] - => -10 + # => -10 ``` - If the folded function is associative, both `fold_right` and `fold_left` return the same result. In that case, **`fold_left` is @@ -272,9 +276,9 @@ # Examples ```nickel - fold_left (fun acc e => acc + e) 0 [ 1, 2, 3 ] => - (((0 + 1) + 2) 3) => - 6 + fold_left (fun acc e => acc + e) 0 [ 1, 2, 3 ] + # => (((0 + 1) + 2) + 3) + # => 6 ``` "% = fun f acc array => @@ -323,8 +327,8 @@ } in let even = fun x => x % 2 == 0 in - find_first even [1, 3, 4, 5, 2] => - 'Some 4 + find_first even [1, 3, 4, 5, 2] + # => 'Some 4 ``` "% = fun f acc array => @@ -366,9 +370,9 @@ # Examples ```nickel - std.array.fold_right (fun e acc => acc @ [e]) [] [ 1, 2, 3 ] => - ((([] @ [3]) @ [2]) @ [1]) => - [ 3, 2, 1 ] + std.array.fold_right (fun e acc => acc @ [e]) [] [ 1, 2, 3 ] + # => ((([] @ [3]) @ [2]) @ [1]) + # => [ 3, 2, 1 ] ``` "% = fun f fst l => @@ -389,8 +393,8 @@ # Examples ```nickel - std.array.prepend 1 [ 2, 3 ] => - [ 1, 2, 3 ] + std.array.prepend 1 [ 2, 3 ] + # => [ 1, 2, 3 ] ``` "% = fun x l => [x] @ l, @@ -403,8 +407,8 @@ # Examples ```nickel - std.array.append 3 [ 1, 2 ] => - [ 1, 2, 3 ] + std.array.append 3 [ 1, 2 ] + # => [ 1, 2, 3 ] ``` "% = fun x l => l @ [x], @@ -417,8 +421,8 @@ # Examples ```nickel - std.array.reverse [ 1, 2, 3 ] => - [ 3, 2, 1 ] + std.array.reverse [ 1, 2, 3 ] + # => [ 3, 2, 1 ] ``` "% = fun l => fold_left (fun acc e => [e] @ acc) [] l, @@ -431,8 +435,8 @@ # Examples ```nickel - std.array.filter (fun x => x <= 3) [ 4, 3, 2, 5, 1 ] => - [ 3, 2, 1 ] + std.array.filter (fun x => x <= 3) [ 4, 3, 2, 5, 1 ] + # => [ 3, 2, 1 ] ``` "% = fun pred l => fold_left (fun acc x => if pred x then acc @ [x] else acc) [] l, @@ -445,8 +449,8 @@ # Examples ```nickel - std.array.flatten [[1, 2], [3, 4]] => - [1, 2, 3, 4] + std.array.flatten [[1, 2], [3, 4]] + # => [1, 2, 3, 4] ``` "% = fun l => fold_right (fun l acc => l @ acc) [] l, @@ -459,11 +463,12 @@ # Examples - ```nickel - std.array.all (fun x => x < 3) [ 1, 2 ] => - true - std.array.all (fun x => x < 3) [ 1, 2, 3 ] => - false + ```nickel multiline + std.array.all (fun x => x < 3) [ 1, 2 ] + # => true + + std.array.all (fun x => x < 3) [ 1, 2, 3 ] + # => false ``` "% = fun pred l => fold_right (fun x acc => if pred x then acc else false) true l, @@ -476,11 +481,12 @@ # Examples - ```nickel - std.array.any (fun x => x < 3) [ 1, 2, 3, 4 ] => - true - std.array.any (fun x => x < 3) [ 5, 6, 7, 8 ] => - false + ```nickel multiline + std.array.any (fun x => x < 3) [ 1, 2, 3, 4 ] + # => true + + std.array.any (fun x => x < 3) [ 5, 6, 7, 8 ] + # => false ``` "% = fun pred l => fold_right (fun x acc => if pred x then true else acc) false l, @@ -505,8 +511,8 @@ # Examples ```nickel - std.array.elem 3 [ 1, 2, 3, 4, 5 ] => - true + std.array.elem 3 [ 1, 2, 3, 4, 5 ] + # => true ``` "% = fun elt => any (fun x => x == elt), @@ -521,8 +527,8 @@ # Examples ```nickel - std.array.partition (fun x => x < 5) [ 2, 4, 5, 3, 7, 8, 6 ] => - { right = [ 2, 4, 3 ], wrong = [ 5, 7, 8, 6 ] } + std.array.partition (fun x => x < 5) [ 2, 4, 5, 3, 7, 8, 6 ] + # => { right = [ 2, 4, 3 ], wrong = [ 5, 7, 8, 6 ] } ``` "% = fun pred l => @@ -545,8 +551,8 @@ # Examples ```nickel - std.array.generate (fun x => x * x) 4 => - [ 0, 1, 4, 9 ] + std.array.generate (fun x => x * x) 4 + # => [ 0, 1, 4, 9 ] ``` "% = fun f n => %array/generate% n f, @@ -559,11 +565,12 @@ # Examples - ```nickel - std.array.compare std.number.compare [3, 1, 4] [6, 2, 8] => - 'Lesser - std.array.compare std.string.compare ["hello", "world"] ["hello"] => - 'Greater + ```nickel multiline + std.array.compare std.number.compare [3, 1, 4] [6, 2, 8] + # => 'Lesser + + std.array.compare std.string.compare ["hello", "world"] ["hello"] + # => 'Greater ``` "% = @@ -595,7 +602,7 @@ else 'Greater) [ 4, 5, 1, 2 ] - => [ 1, 2, 4, 5 ] + # => [ 1, 2, 4, 5 ] ``` "% #TODO: maybe inline partition to avoid contract checks? @@ -619,7 +626,7 @@ ```nickel std.array.flat_map (fun x => [x, x]) [1, 2, 3] - => [1, 1, 2, 2, 3, 3] + # => [1, 1, 2, 2, 3, 3] ``` "% = fun f xs => map f xs |> flatten, @@ -631,13 +638,15 @@ # Examples - ```nickel + ```nickel multiline std.array.intersperse ", " [ "Hello", "wonderful", "world!" ] - => [ "Hello", ", ", "wonderful", ", ", "world!" ] + # => [ "Hello", ", ", "wonderful", ", ", "world!" ] + std.array.intersperse ", " [ "Hello" ] - => [ "Hello" ] + # => [ "Hello" ] + std.array.intersperse ", " [] - => [] + # => [] ``` "% = fun v array => @@ -663,13 +672,15 @@ # Examples - ```nickel + ```nickel multiline std.array.slice 1 3 [ 0, 1, 2, 3, 4, 5] - => [ 1, 2 ] + # => [ 1, 2 ] + std.array.slice 0 3 [ "Hello", "world", "!" ] - => [ "Hello", "world", "!" ] + # => [ "Hello", "world", "!" ] + std.array.slice 2 3 [ "Hello", "world", "!" ] - => [ "!" ] + # => [ "!" ] ``` "% = fun start end value => %array/slice% start end value, @@ -689,13 +700,15 @@ # Examples - ```nickel + ```nickel multiline std.array.split_at 2 [ 0, 1, 2, 3, 4, 5] - => { left = [ 0, 1 ], right = [ 2, 3, 4, 5 ] } + # => { left = [ 0, 1 ], right = [ 2, 3, 4, 5 ] } + std.array.split_at 0 [ "Hello", "world", "!" ] - => { left = [ ], right = [ "Hello", "world", "!" ] } + # => { left = [ ], right = [ "Hello", "world", "!" ] } + std.array.split_at 3 [ "Hello", "world", "!" ] - => { left = [ "Hello", "world", "!" ], right = [ ] } + # => { left = [ "Hello", "world", "!" ], right = [ ] } ``` "% = fun index value => @@ -716,11 +729,12 @@ # Examples - ```nickel + ```nickel multiline std.array.replicate 0 false - => [ ] + # => [ ] + std.array.replicate 5 "x" - => [ "x", "x", "x", "x", "x" ] + # => [ "x", "x", "x", "x", "x" ] ``` "% = fun n x => %array/generate% n (fun _i => x), @@ -742,7 +756,7 @@ ```nickel std.array.range_step (-1.5) 2 0.5 - => [ -1.5, -1, -0.5, 0, 0.5, 1, 1.5 ] + # => [ -1.5, -1, -0.5, 0, 0.5, 1, 1.5 ] ``` "% = fun start end step => @@ -769,7 +783,7 @@ ```nickel std.array.range 0 5 - => [ 0, 1, 2, 3, 4 ] + # => [ 0, 1, 2, 3, 4 ] ``` "% = fun start end => range_step start end 1, @@ -799,13 +813,14 @@ # Examples - ```nickel + ```nickel multiline std.array.reduce_left (@) [ [1, 2], [3], [4,5] ] - => (([1, 2] @ [3]) @ [4,5]) - => [ 1, 2, 4, 5 ] + # => (([1, 2] @ [3]) @ [4,5]) + # => [ 1, 2, 3, 4, 5 ] + std.array.reduce_left (-) [ 1, 2, 3, 4] - => ((1 - 2) - 3) - 4 - => -8 + # => ((1 - 2) - 3) - 4 + # => -8 ``` "% = fun f array => @@ -839,13 +854,14 @@ # Examples - ```nickel + ```nickel multiline std.array.reduce_right (@) [ [1, 2], [3], [4,5] ] - => [1, 2] @ ([3] @ [4,5]) - => [ 1, 2, 4, 5 ] + # => [1, 2] @ ([3] @ [4,5]) + # => [ 1, 2, 3, 4, 5 ] + std.array.reduce_right (-) [ 1, 2, 3, 4] - => 1 - (2 - (3 - 4)) - => -2 + # => 1 - (2 - (3 - 4)) + # => -2 ``` "% = fun f array => @@ -863,13 +879,15 @@ # Examples - ```nickel + ```nickel multiline std.array.zip_with (+) [1, 2, 3] [4, 5, 6] - => [5, 7, 9] + # => [5, 7, 9] + std.array.zip_with (*) [1, 2] [4, 5, 6] - => [4, 10] + # => [4, 10] + std.array.zip_with (-) [1, 2, 3] [4, 5] - => [-3, -3] + # => [-3, -3] ``` "% = fun f xs ys => @@ -887,8 +905,8 @@ # Examples ```nickel - std.array.map_with_index (fun i x => i + x + 1) [ 1, 2, 3 ] => - [ 2, 4, 6 ] + std.array.map_with_index (fun i x => i + x + 1) [ 1, 2, 3 ] + # => [ 2, 4, 6 ] ``` "% = fun f xs => @@ -910,11 +928,12 @@ # Example - ```nickel + ```nickel multiline 1 + 4 | std.contract.Equal 5 - => 5 + # => 5 + 4 | std.contract.Equal 5 - => error: contract broken by a value + # => error: contract broken by a value ``` "% = @@ -1021,11 +1040,13 @@ # Examples ```nickel - IsZero = fun label value => + let IsZero = fun label value => if value == 0 then value else std.contract.blame label + in + 0 | IsZero ``` "% = fun label => %blame% label, @@ -1106,7 +1127,7 @@ in null | Nullable Number - => null + # => null ``` "% = fun contract => %contract/custom% contract, @@ -1228,26 +1249,26 @@ # Examples - ```nickel + ```nickel ignore x | std.contract.Sequence [ Foo, Bar ] ``` is equivalent to - ```nickel + ```nickel ignore (x | Foo) | Bar ``` This is useful in positions where one cannot apply multiple contracts, such as an argument to another contract: - ```nickel + ```nickel ignore x | Array (std.contract.Sequence [ Foo, Bar ]) ``` Or stored in a variable: - ```nickel + ```nickel ignore let C = std.contract.Sequence [ Foo, Bar ] in x | C ``` @@ -1338,7 +1359,7 @@ in { foo = 4, hello = "world" } | FooIsEven - => { foo = 4, hello = "world" } + # => { foo = 4, hello = "world" } ``` "% = fun message label => %label/with_message% message label, @@ -1379,6 +1400,8 @@ in null | AlwaysFailWithNotes + + # => error ``` "% # the %label/with_notes% operator expects an array of strings which is @@ -1408,6 +1431,7 @@ in null | AlwaysFailWithNotes + # => error ``` "% = fun note label => %label/append_note% note label, @@ -1466,17 +1490,16 @@ applications. ```nickel - let FooOfNumber = fun Contract => + let FooOfNumber = std.contract.custom (fun label => match { - 'Foo arg => 'Foo (std.contract.apply Contract label value), + 'Foo arg => 'Ok ('Foo (std.contract.apply Number label arg)), _ => 'Error {}, } ) in - - { foo = 1 } | Nullable {foo | Number} + 'Foo 3 | FooOfNumber ``` # Diagnostics stack @@ -1507,6 +1530,7 @@ in null | ParentContract + # => error: contract broken by a value ``` This example will print two diagnostics: the main one, using the @@ -1684,7 +1708,7 @@ ```nickel let NotNumber = std.contract.not Number in "a" | NotNumber - => "a" + # => "a" ``` "% = fun Contract => @@ -1720,7 +1744,7 @@ That is, `some_function | DependentFun Foo Bar` performs the same checks as - ```nickel + ```nickel ignore fun arg => let arg_checked = (arg | Foo) in (some_function arg_checked | (Bar arg_checked)) @@ -1985,6 +2009,7 @@ let f | HasField Dyn = fun field record => record."%{field}" in (f "foo" { foo = 1, bar = 2 }) + (f "baz" { foo = 1, bar = 2 }) + # => error: missing field ``` In this example, the first call to `f` won't blame, but the second @@ -2020,13 +2045,15 @@ # Examples - ```nickel - ('foo | std.enum.Tag) => - 'foo - ('FooBar | std.enum.Tag) => - 'FooBar - ("tag" | std.enum.Tag) => - error + ```nickel multiline + ('foo | std.enum.Tag) + # => 'foo + + ('FooBar | std.enum.Tag) + # => 'FooBar + + ("tag" | std.enum.Tag) + # => error ``` "% = std.contract.from_predicate is_enum_tag, @@ -2037,13 +2064,15 @@ # Examples - ```nickel - ('Foo | std.enum.Enum) => - 'Foo - ('Bar 5 | std.enum.Enum) => - 'Bar 5 - ("tag" | std.enum.Enum) => - error + ```nickel multiline + ('Foo | std.enum.Enum) + # => 'Foo + + ('Bar 5 | std.enum.Enum) + # => 'Bar 5 + + ("tag" | std.enum.Enum) + # => error "% = %contract/custom% (fun _label value => @@ -2111,15 +2140,18 @@ # Examples - ```nickel + ```nickel multiline std.enum.is_enum_tag 'foo - => true + # => true + std.enum.is_enum_tag 'FooBar - => true + # => true + std.enum.is_enum_tag "tag" - => false + # => false + std.enum.is_enum_tag ('Foo "arg") - => false + # => false ``` "% = fun value => std.is_enum value && !(%enum/is_variant% value), @@ -2132,15 +2164,18 @@ # Examples - ```nickel + ```nickel multiline std.enum.is_enum_variant ('Foo "arg") - => true + # => true + std.enum.is_enum_variant ('Http {version = "1.1"}) - => true + # => true + std.enum.is_enum_variant 'foo - => false + # => false + std.enum.is_enum_variant [1, 2, 3] - => false + # => false ``` "% = fun value => %enum/is_variant% value, @@ -2156,11 +2191,12 @@ # Examples - ```nickel - std.enum.to_tag_and_arg ('Foo "arg") => - { tag = "Foo", arg = "arg" } + ```nickel multiline + std.enum.to_tag_and_arg ('Foo "arg") + # => { tag = "Foo", arg = "arg" } + std.enum.to_tag_and_arg 'http - => { tag = "http" } + # => { tag = "http" } ``` "% = fun enum_value => @@ -2184,11 +2220,12 @@ # Examples - ```nickel + ```nickel multiline std.enum.from_tag_and_arg { tag = "Foo", arg = "arg" } - => ('Foo "arg") + # => ('Foo "arg") + std.enum.from_tag_and_arg { tag = "http" } - => 'http + # => 'http ``` "% = fun enum_data => @@ -2205,11 +2242,12 @@ # Examples - ```nickel + ```nickel multiline std.enum.map ((+) 1) ('Foo 42) - => 'Foo 43 - std.enum.map f 'Bar - => 'Bar + # => 'Foo 43 + + std.enum.map (fun x => 1) 'Bar + # => 'Bar ``` "% = fun f enum_value => @@ -2230,11 +2268,12 @@ # Examples - ```nickel + ```nickel multiline std.function.id null - => null + # => null + (std.function.id (fun x => x + 1)) 0 - => 1 + # => 1 ``` "% = fun x => x, @@ -2251,8 +2290,8 @@ let f = std.function.compose (fun x => x + 1) (fun x => x / 2) in f 10 - => (10 / 2) + 1 - => 6 + # => (10 / 2) + 1 + # => 6 ``` "% = fun g f x => x |> f |> g, @@ -2266,7 +2305,7 @@ ```nickel std.function.flip (fun x y => "%{x} %{y}") "world!" "Hello," - => "Hello, world!" + # => "Hello, world!" ``` "%% = fun f x y => f y x, @@ -2282,7 +2321,7 @@ ```nickel let f = std.function.const 5 in f 7 - => 5 + # => 5 ``` "% = fun x y => x, @@ -2297,7 +2336,7 @@ ```nickel std.function.first 5 7 - => 5 + # => 5 ``` "% = fun x y => x, @@ -2311,7 +2350,7 @@ ```nickel std.function.second 5 7 - => 7 + # => 7 ``` "% = fun x y => y, @@ -2323,11 +2362,12 @@ # Examples - ```nickel + ```nickel multiline std.function.pipe 2 [ (+) 2, (+) 3 ] - => 7 + # => 7 + std.function.pipe 'World [ std.string.from, fun s => "Hello, %{s}!" ] - => "Hello, World!" + # => "Hello, World!" ``` "%% = fun x fs => std.array.fold_left (|>) x fs, @@ -2340,11 +2380,12 @@ # Examples - ```nickel - (1.5 | Integer) => - error - (42 | Integer) => - 42 + ```nickel multiline + (1.5 | Integer) + # => error + + (42 | Integer) + # => 42 ``` "% = @@ -2364,13 +2405,15 @@ # Examples - ```nickel - (42 | std.number.Nat) => - 42 - (0 | std.number.Nat) => - 0 - (-4 | std.number.Nat) => - error + ```nickel multiline + (42 | std.number.Nat) + # => 42 + + (0 | std.number.Nat) + # => 0 + + (-4 | std.number.Nat) + # => error ``` "% = @@ -2390,13 +2433,15 @@ # Examples - ```nickel - (42 | std.number.PosNat) => - 42 - (0 | std.number.PosNat) => - error - (-4 | std.number.PosNat) => - error + ```nickel multiline + (42 | std.number.PosNat) + # => 42 + + (0 | std.number.PosNat) + # => error + + (-4 | std.number.PosNat) + # => error ``` "% = @@ -2416,11 +2461,12 @@ # Examples - ```nickel - (1 | std.number.NonZero) => - 1 - (0.0 | std.number.NonZero) => - error + ```nickel multiline + (1 | std.number.NonZero) + # => 1 + + (0.0 | std.number.NonZero) + # => error ``` "% = @@ -2441,11 +2487,12 @@ # Examples - ```nickel - std.number.is_integer 42 => - true - std.number.is_integer 1.5 => - false + ```nickel multiline + std.number.is_integer 42 + # => true + + std.number.is_integer 1.5 + # => false ``` "% = fun x => x % 1 == 0, @@ -2457,8 +2504,8 @@ # Examples ```nickel - std.number.compare 1 2 => - 'Lesser + std.number.compare 1 2 + # => 'Lesser ``` "% = fun x y => @@ -2477,8 +2524,8 @@ # Examples ```nickel - std.number.min (-1337) 42 => - -1337 + std.number.min (-1337) 42 + # => -1337 ``` "% = fun x y => if x <= y then x else y, @@ -2491,8 +2538,8 @@ # Examples ```nickel - std.number.max (-1337) 42 => - 42 + std.number.max (-1337) 42 + # => 42 ``` "% = fun x y => if x >= y then x else y, @@ -2504,11 +2551,12 @@ # Examples - ```nickel - std.number.floor 42.5 => - 42 - std.number.floor (-42.5) => - -43 + ```nickel multiline + std.number.floor 42.5 + # => 42 + + std.number.floor (-42.5) + # => -43 ``` "% = fun x => @@ -2524,11 +2572,12 @@ # Examples - ```nickel - std.number.abs (-5) => - 5 - std.number.abs 42 => - 42 + ```nickel multiline + std.number.abs (-5) + # => 5 + + std.number.abs 42 + # => 42 ``` "% = fun x => if x < 0 then -x else x, @@ -2540,11 +2589,12 @@ # Examples - ```nickel - std.number.fract 13.37 => - 0.37 - std.number.fract 42 => - 0 + ```nickel multiline + std.number.fract 13.37 + # => 0.37 + + std.number.fract 42 + # => 0 ``` "% = fun x => x % 1, @@ -2556,11 +2606,12 @@ # Examples - ```nickel - std.number.truncate (-13.37) => - -13 - std.number.truncate 42.5 => - 42 + ```nickel multiline + std.number.truncate (-13.37) + # => -13 + + std.number.truncate 42.5 + # => 42 ``` "% = fun x => x - (x % 1), @@ -2573,8 +2624,8 @@ # Examples ```nickel - std.number.pow 2 8 => - 256 + std.number.pow 2 8 + # => 256 ``` # Precision @@ -2598,13 +2649,15 @@ `log x base` returns the logarithm of x with respect to the given base. # Examples - ```nickel - std.number.log 2 1024 => - 10 - std.number.log 100 10 => - 2 - std.number.log 10 std.number.e => - 2.302585 + ```nickel ignore + std.number.log 2 1024 + # => 10 + + std.number.log 100 10 + # => 2 + + std.number.log 10 std.number.e + # => 2.302585 ``` "% = fun x b => %number/log% x b, @@ -2616,9 +2669,9 @@ # Examples - ```nickel - std.number.cos (std.number.pi / 4) => - 0.70710678118 + ```nickel ignore + std.number.cos (std.number.pi / 4) + # => 0.70710678118 ``` "% = fun x => %number/cos% x, @@ -2630,9 +2683,9 @@ # Examples - ```nickel - std.number.sin (std.number.pi / 2) => - 1 + ```nickel ignore + std.number.sin (std.number.pi / 2) + # => 1 ``` "% = fun x => %number/sin% x, @@ -2644,9 +2697,9 @@ # Examples - ```nickel - std.number.tan (std.number.pi / 4) => - 1 + ```nickel ignore + std.number.tan (std.number.pi / 4) + # => 1 ``` "% = fun x => %number/tan% x, @@ -2658,11 +2711,12 @@ # Examples - ```nickel - std.number.arccos 0 => - 1.5707963 - std.number.arccos 1 => - 0 + ```nickel ignore + std.number.arccos 0 + # => 1.570796326794897 + + std.number.arccos 1 + # => 0 ``` "% = fun x => %number/arccos% x, @@ -2674,11 +2728,12 @@ # Examples - ```nickel - std.number.arcsin 0 => - 0 - std.number.arcsin 1 => - 1.5707963 + ```nickel ignore + std.number.arcsin 0 + # => 0 + + std.number.arcsin 1 + # => 1.570796326794897 ``` "% = fun x => %number/arcsin% x, @@ -2690,9 +2745,9 @@ # Examples - ```nickel - std.number.arctan 1 => - 0.78539816 + ```nickel ignore + std.number.arctan 1 + # => 0.7853981633974482 ``` "% = fun x => %number/arctan% x, @@ -2704,11 +2759,12 @@ # Examples - ```nickel - std.number.arctan2 1 0 => - 1.5707963 - std.number.arctan2 (-0.5) (-0.5) => - -0.7853981633974483 + ```nickel ignore + std.number.arctan2 1 0 + # => 1.570796326794897 + + std.number.arctan2 (-0.5) (-0.5) + # => -2.356194490192345 ``` "% = fun y x => %number/arctan2% y x, @@ -2721,8 +2777,8 @@ # Examples ```nickel - std.number.sqrt 4 => - 2 + std.number.sqrt 4 + # => 2 ``` "% = fun x => pow x (1 / 2), @@ -2751,11 +2807,12 @@ # Examples - ```nickel - std.record.map (fun s x => s) { hi = 2 } => - { hi = "hi" } - std.record.map (fun s x => x + 1) { hello = 1, world = 2 } => - { hello = 2, world = 3 } + ```nickel multiline + std.record.map (fun s x => s) { hi = 2 } + # => { hi = "hi" } + + std.record.map (fun s x => x + 1) { hello = 1, world = 2 } + # => { hello = 2, world = 3 } ``` "% = fun f r => %record/map% r f, @@ -2775,11 +2832,12 @@ # Examples - ```nickel - std.record.fields { one = 1, two = 2 } => - [ "one", "two" ] - std.record.fields { one = 1, two = 2, three_opt | optional } => - [ "one", "two" ] + ```nickel multiline + std.record.fields { one = 1, two = 2 } + # => [ "one", "two" ] + + std.record.fields { one = 1, two = 2, three_opt | optional } + # => [ "one", "two" ] ``` "% = fun r => %record/fields% r, @@ -2793,11 +2851,12 @@ # Examples - ```nickel - std.record.fields_with_opts { one = 1, two = 2 } => - [ "one", "two" ] - std.record.fields_with_opts { one = 1, two = 2, three_opt | optional } => - [ "one", "two", "three_opt" ] + ```nickel multiline + std.record.fields_with_opts { one = 1, two = 2 } + # => [ "one", "two" ] + + std.record.fields_with_opts { one = 1, two = 2, three_opt | optional } + # => [ "one", "two", "three_opt" ] ``` "% = fun r => %record/fields_with_opts% r, @@ -2810,8 +2869,8 @@ # Examples ```nickel - std.record.values { one = 1, world = "world" } => - [ 1, "world" ] + std.record.values { one = 1, world = "world" } + # => [ 1, "world" ] ``` "% = fun r => %record/values% r, @@ -2830,16 +2889,19 @@ # Examples - ```nickel + ```nickel multiline std.record.has_field "hello" { one = 1, two = 2 } - => false + # => false + std.record.has_field "one" { one = 1, two = 2 } - => true + # => true + std.record.has_field "two_opt" { one, two_opt | optional } - => false + # => false + ({ one = 1 } | {one, two_opt | optional }) |> std.record.has_field "two_opt" - => false + # => false ``` "% = fun field r => %record/has_field% field r, @@ -2854,16 +2916,19 @@ # Examples - ```nickel + ```nickel multiline std.record.has_field_with_opts "hello" { one = 1, two = 2 } - => false + # => false + std.record.has_field_with_opts "one" { one = 1, two = 2 } - => true + # => true + std.record.has_field_with_opts "two_opt" { one, two_opt | optional } - => true + # => true + ({ one = 1 } | {one, two_opt | optional }) |> std.record.has_field_with_opts "two_opt" - => true + # => true "% = fun field r => %record/has_field_with_opts% field r, @@ -2885,15 +2950,16 @@ # Examples - ```nickel - std.record.get "one" { one = 1, two = 2 } => - 1 + ```nickel multiline + std.record.get "one" { one = 1, two = 2 } + # => 1 + { one = 1, two = 2, string = "three"} |> std.record.to_array |> std.array.filter (fun { field, value } => std.is_number value) |> std.record.from_array |> std.record.get "two" - => 2 + # => 2 ``` "%% = fun field r => r."%{field}", @@ -2907,13 +2973,15 @@ # Examples - ```nickel + ```nickel multiline std.record.get_or "tree" 3 { one = 1, two = 2 } - => 3 + # => 3 + std.record.get_or "one" 11 { one = 1, two = 2 } - => 1 + # => 1 + std.record.get_or "value" "default" { tag = 'Hello, value } - => "default" + # => "default" ``` "%% = fun field default_value record => @@ -2946,17 +3014,20 @@ # Examples - ```nickel + ```nickel multiline std.record.insert "foo" 5 { bar = "bar" } - => { foo = 5, bar = "bar } + # => { foo = 5, bar = "bar" } + {} |> std.record.insert "file.txt" "data/text" |> std.record.insert "length" (10*1000) - => {"file.txt" = "data/text", "length" = 10000} + # => {"file.txt" = "data/text", "length" = 10000} + std.record.insert "already_there_opt" 0 {already_there_opt | optional} - => {already_there_optional = 0} + # => {already_there_opt = 0} + std.record.insert "already_there" 0 {already_there = 1} - => error + # => error ``` "%% = fun field content r => %record/insert% field r content, @@ -2977,17 +3048,20 @@ # Examples - ```nickel + ```nickel multiline std.record.insert_with_opts "foo" 5 { bar = "bar" } - => { foo = 5, bar = "bar } + # => { foo = 5, bar = "bar" } + {} |> std.record.insert_with_opts "file.txt" "data/text" |> std.record.insert_with_opts "length" (10*1000) - => {"file.txt" = "data/text", "length" = 10000} + # => {"file.txt" = "data/text", "length" = 10000} + std.record.insert_with_opts "already_there_optional" 0 {already_there | optional} - => error + # => { already_there_optional = 0 } + std.record.insert_with_opts "already_there" 0 {already_there = 1} - => error + # => error ``` "%% = fun field content r => %record/insert_with_opts% field r content, @@ -3014,13 +3088,15 @@ # Examples - ```nickel + ```nickel multiline std.record.remove "foo" { foo = "foo", bar = "bar" } - => { bar = "bar" } + # => { bar = "bar" } + std.record.remove "foo_opt" {foo_opt | optional} - => error + # => error + std.record.remove "foo" { bar = "bar" } - => error + # => error ``` "% = fun field r => %record/remove% field r, @@ -3040,13 +3116,15 @@ # Examples - ```nickel + ```nickel multiline std.record.remove_with_opts "foo" { foo = "foo", bar = "bar" } - => { bar = "bar" } + # => { bar = "bar" } + std.record.remove_with_opts "foo_opt" {foo_opt | optional} - => {} + # => {} + std.record.remove_with_opts "foo" { bar = "bar" } - => error + # => error ``` "% = fun field r => %record/remove_with_opts% field r, @@ -3066,13 +3144,15 @@ # Examples - ```nickel - std.record.update "foo" 5 { foo = "foo", bar = "bar" } => - { foo = 5, bar = "bar" } - std.record.update "foo" 5 { bar = "bar" } => - { foo = 5, bar = "bar" } - std.record.update "foo_opt" 5 {foo_opt | optional} => - {foo_opt = 5} + ```nickel multiline + std.record.update "foo" 5 { foo = "foo", bar = "bar" } + # => { foo = 5, bar = "bar" } + + std.record.update "foo" 5 { bar = "bar" } + # => { foo = 5, bar = "bar" } + + std.record.update "foo_opt" 5 {foo_opt | optional} + # => {foo_opt = 5} ``` # Overriding @@ -3081,11 +3161,12 @@ will only change the specified field and won't automatically update the other fields which depend on it: - ```nickel - { foo = bar + 1, bar | default = 0 } & { bar = 1 } => - { foo = 2, bar = 1 } - std.record.update "bar" 1 {foo = bar + 1, bar | default = 0 } => - { foo = 1, bar = 1 } + ```nickel multiline + { foo = bar + 1, bar | default = 0 } & { bar = 1 } + # => { foo = 2, bar = 1 } + + std.record.update "bar" 1 {foo = bar + 1, bar | default = 0 } + # => { foo = 1, bar = 1 } ``` "% = fun field content r => @@ -3105,11 +3186,12 @@ # Examples - ```nickel + ```nickel multiline std.record.map_values (fun x => x + 1) { hi = 2 } - => { hi = 3 } + # => { hi = 3 } + std.record.map_values (fun x => x + 1) { hello = 1, world = 2 } - => { hello = 2, world = 3 } + # => { hello = 2, world = 3 } ``` "% = fun f => map (fun _field => f), @@ -3123,10 +3205,7 @@ ```nickel std.record.to_array { hello = "world", foo = "bar" } - => [ - { field = "hello", value = "world" }, - { field = "foo", value = "bar" }, - ] + # => [ { field = "hello", value = "world" }, { field = "foo", value = "bar" } ] ``` "% = fun record => @@ -3147,7 +3226,7 @@ { field = "hello", value = "world" }, { field = "foo", value = "bar" } ] - => { hello = "world", foo = "bar" } + # => { hello = "world", foo = "bar" } ``` "% = fun bindings => @@ -3161,11 +3240,12 @@ # Examples - ```nickel + ```nickel multiline std.record.is_empty {} - => true + # => true + std.record.is_empty { foo = 1 } - => false + # => false ``` "% = (==) {}, @@ -3179,7 +3259,7 @@ ```nickel std.record.merge_all [ { foo = 1 }, { bar = 2 } ] - => { foo = 1, bar = 2 } + # => { foo = 1, bar = 2 } ``` "% = fun rs => (std.array.fold_left (&) {} (rs | Array Dyn)) | { _ : Dyn }, @@ -3195,7 +3275,7 @@ ```nickel std.record.filter (fun _name x => x % 2 == 0) { even = 2, odd = 3 } - => { even = 2 } + # => { even = 2 } ``` "% = fun f record => @@ -3212,11 +3292,12 @@ # Examples - ```nickel - std.record.apply_on "name" std.string.compare { name = "Alice", age = 27 } { name = "Bob", age = 23 } => - 'Lesser - std.record.apply_on "age" std.number.compare { name = "Alice", age = 27 } { name = "Bob", age = 23 } => - 'Greater + ```nickel multiline + std.record.apply_on "name" std.string.compare { name = "Alice", age = 27 } { name = "Bob", age = 23 } + # => 'Lesser + + std.record.apply_on "age" std.number.compare { name = "Alice", age = 27 } { name = "Bob", age = 23 } + # => 'Greater ``` "% = fun field f a b => f a."%{field}" b."%{field}", @@ -3244,13 +3325,15 @@ # Examples - ```nickel - ("true" | std.string.BoolLiteral) => - "true" - ("hello" | std.string.BoolLiteral) => - error - (true | std.string.BoolLiteral) => - error + ```nickel multiline + ("true" | std.string.BoolLiteral) + # => "true" + + ("hello" | std.string.BoolLiteral) + # => error + + (true | std.string.BoolLiteral) + # => error ``` "% = fun l s => @@ -3270,13 +3353,15 @@ # Examples - ```nickel - ("+1.2" | std.string.NumberLiteral) => - "+1.2" - ("5" | std.string.NumberLiteral) => - "5" - (42 | std.string.NumberLiteral) => - error + ```nickel multiline + ("+1.2" | std.string.NumberLiteral) + # => "+1.2" + + ("5" | std.string.NumberLiteral) + # => "5" + + (42 | std.string.NumberLiteral) + # => error ``` "% = @@ -3299,15 +3384,18 @@ # Examples - ```nickel + ```nickel multiline ("e" | std.string.Character) - => "e" + # => "e" + ("#" | std.string.Character) - => "#" + # => "#" + ("" | std.string.Character) - => error + # => error + (1 | std.string.Character) - => error + # => error ``` "% = @@ -3336,15 +3424,18 @@ # Examples - ```nickel + ```nickel multiline ('Foo | std.string.Stringable) - => 'Foo + # => 'Foo + (false | std.string.Stringable) - => false + # => false + ("bar" ++ "foo" | std.string.Stringable) - => "barfoo" + # => "barfoo" + ({foo = "baz"} | std.string.Stringable) - => error + # => error ``` "% = @@ -3369,13 +3460,15 @@ # Examples - ```nickel + ```nickel multiline ("" | std.string.NonEmpty) - => error + # => error + ("hi!" | std.string.NonEmpty) - => "hi!" + # => "hi!" + (42 | std.string.NonEmpty) - => error + # => error ``` "% = @@ -3396,13 +3489,15 @@ # Examples - ```nickel + ```nickel multiline std.string.join ", " [ "Hello", "World!" ] - => "Hello, World!" + # => "Hello, World!" + std.string.join ";" ["I'm alone"] - => "I'm alone" + # => "I'm alone" + std.string.join ", " [] - => "" + # => "" ``` "% = fun sep fragments => @@ -3427,11 +3522,12 @@ # Examples - ```nickel + ```nickel multiline std.string.split "," "1,2,3" - => [ "1", "2", "3" ] + # => [ "1", "2", "3" ] + std.string.split "." "1,2,3" - => [ "1,2,3" ] + # => [ "1,2,3" ] ``` "% = fun sep s => %string/split% s sep, @@ -3443,11 +3539,12 @@ # Examples - ```nickel + ```nickel multiline std.string.trim " hi " - => "hi" + # => "hi" + std.string.trim "1 2 3 " - => "1 2 3" + # => "1 2 3" ``` "% = fun s => %string/trim% s, @@ -3461,7 +3558,7 @@ ```nickel std.string.characters "Hello" - => [ "H", "e", "l", "l", "o" ] + # => [ "H", "e", "l", "l", "o" ] ``` "% = fun s => %string/chars% s, @@ -3474,13 +3571,15 @@ # Examples - ```nickel + ```nickel multiline std.string.uppercase "a" - => "A" + # => "A" + std.string.uppercase "æ" - => "Æ" + # => "Æ" + std.string.uppercase "hello.world" - => "HELLO.WORLD" + # => "HELLO.WORLD" ``` "% = fun s => %string/uppercase% s, @@ -3494,13 +3593,15 @@ # Examples - ```nickel + ```nickel multiline std.string.lowercase "A" - => "a" + # => "a" + std.string.lowercase "Æ" - => "æ" + # => "æ" + std.string.lowercase "HELLO.WORLD" - => "hello.world" + # => "hello.world" ``` "% = fun s => %string/lowercase% s, @@ -3515,13 +3616,15 @@ # Examples - ```nickel + ```nickel multiline std.string.contains "cde" "abcdef" - => true + # => true + std.string.contains "" "abcdef" - => true + # => true + std.string.contains "ghj" "abcdef" - => false + # => false ``` "% = fun subs s => %string/contains% s subs, @@ -3533,13 +3636,15 @@ # Examples - ```nickel + ```nickel multiline std.string.compare "abc" "def" - => 'Lesser + # => 'Lesser + std.string.compare "a" "a" - => 'Equal + # => 'Equal + std.string.compare "world" "hello" - => 'Greater + # => 'Greater ``` "% = fun a b => %string/compare% a b, @@ -3554,11 +3659,12 @@ # Examples - ```nickel + ```nickel multiline std.string.replace "cd" " " "abcdef" - => "ab ef" + # => "ab ef" + std.string.replace "" "A" "abcdef" - => "AaAbAcAdAeAfA" + # => "AaAbAcAdAeAfA" ``` "% = fun pattern replace s => @@ -3576,11 +3682,12 @@ # Examples - ```nickel + ```nickel multiline std.string.replace_regex "l+." "j" "Hello!" - => "Hej!" + # => "Hej!" + std.string.replace_regex "\\d+" "\"a\" is not" "This 37 is a number." - "This \"a\" is not a number." + # "This \"a\" is not a number." ``` "% = fun pattern replace s => @@ -3599,11 +3706,12 @@ # Examples - ```nickel + ```nickel multiline std.string.is_match "^\\d+$" "123" - => true + # => true + std.string.is_match "\\d{4}" "123" - => false + # => false ``` # Performance @@ -3623,7 +3731,7 @@ in ["0", "42", "0.5"] |> std.array.all is_number - => true + # => true ``` On the other hand, in the version below, the partial application of @@ -3635,7 +3743,7 @@ let is_number' = std.string.is_match "[0-9]*\\.?[0-9]+" in ["0", "42", "0.5"] |> std.array.all is_number' - => true + # => true ``` "% = fun regex => %string/is_match% regex, @@ -3657,11 +3765,12 @@ # Examples - ```nickel + ```nickel multiline std.string.find "^(\\d).*(\\d).*(\\d).*$" "5 apples, 6 pears and 0 grapes" - => { matched = "5 apples, 6 pears and 0 grapes", index = 0, groups = [ "5", "6", "0" ] } + # => { matched = "5 apples, 6 pears and 0 grapes", index = 0, groups = [ "5", "6", "0" ] } + std.string.find "3" "01234" - => { matched = "3", index = 3, groups = [ ] } + # => { matched = "3", index = 3, groups = [ ] } ``` # Performance @@ -3690,19 +3799,20 @@ # Examples - ```nickel + ```nickel multiline std.string.find_all "(\\d) (\\w+)" "5 apples, 6 pears and 0 grapes" - => [ - { groups = [ "5", "apples" ], index = 0, matched = "5 apples", }, - { groups = [ "6", "pears" ], index = 10, matched = "6 pears", }, - { groups = [ "0", "grapes" ], index = 22, matched = "0 grapes", } - ] + # => [ + # { groups = [ "5", "apples" ], index = 0, matched = "5 apples", }, + # { groups = [ "6", "pears" ], index = 10, matched = "6 pears", }, + # { groups = [ "0", "grapes" ], index = 22, matched = "0 grapes", } + # ] + std.string.find_all "2" "123 123 123" - => [ - { groups = [ ], index = 1, matched = "2", }, - { groups = [ ], index = 5, matched = "2", }, - { groups = [ ], index = 9, matched = "2", } - ] + # => [ + # { groups = [ ], index = 1, matched = "2", }, + # { groups = [ ], index = 5, matched = "2", }, + # { groups = [ ], index = 9, matched = "2", } + # ] ``` # Performance @@ -3729,15 +3839,18 @@ # Examples - ```nickel - std.string.length "" => - => 0 - std.string.length "hi" => - => 2 - std.string.length "四字熟語" => - => 4 - std.string.length "👨🏾‍❤️‍💋‍👨🏻" => - => 1 + ```nickel multiline + std.string.length "" + # => 0 + + std.string.length "hi" + # => 2 + + std.string.length "四字熟語" + # => 4 + + std.string.length "👨🏾‍❤️‍💋‍👨🏻" + # => 1 ``` "% = fun s => %string/length% s, @@ -3757,13 +3870,15 @@ # Examples - ```nickel - std.string.substring 3 5 "abcdef" => - "de" - std.string.substring 3 10 "abcdef" => - error - std.string.substring (-3) 4 "abcdef" => - error + ```nickel multiline + std.string.substring 3 5 "abcdef" + # => "de" + + std.string.substring 3 10 "abcdef" + # => error + + std.string.substring (-3) 4 "abcdef" + # => error ``` "% = fun start end s => %string/substr% s start end, @@ -3776,15 +3891,18 @@ # Examples - ```nickel + ```nickel multiline std.string.from 42 - => "42" + # => "42" + std.string.from 'Foo - => "Foo" + # => "Foo" + std.string.from null - => "null" + # => "null" + std.string.from {value = 0} - => error + # => error ``` "% = fun x => %to_string% x, @@ -3798,7 +3916,7 @@ ```nickel std.string.from_number 42 - => "42" + # => "42" ``` "% = from, @@ -3813,7 +3931,7 @@ ```nickel std.string.from_enum 'MyEnum - => "MyEnum" + # => "MyEnum" ``` "% = from, @@ -3827,7 +3945,7 @@ ```nickel std.string.from_bool true - => "true" + # => "true" ``` "% = from, @@ -3842,7 +3960,7 @@ ```nickel std.string.to_number "123" - => 123 + # => 123 ``` "% = fun s => %number/from_string% s, @@ -3855,11 +3973,12 @@ # Examples - ```nickel + ```nickel multiline std.string.to_bool "true" - => true + # => true + std.string.to_bool "false" - => false + # => false ``` "% # because of the contract on the argument, `s` can only be `"true"` or @@ -3873,11 +3992,12 @@ # Examples - ```nickel + ```nickel multiline std.string.to_enum "Hello" - => 'Hello + # => 'Hello + std.string.to_enum "hey,there!" - => '"hey,there!" + # => '"hey,there!" ``` "% = fun s => %enum/from_string% s, @@ -3890,12 +4010,12 @@ # Examples - ```nickel + ```nickel multiline 1 == 2 | std.test.Assert - => error: contract broken by a value + # => error: contract broken by a value 1 == 1 | std.test.Assert - => true + # => true ``` "% = @@ -3919,7 +4039,7 @@ ```nickel [ (1 == 2), (1 == 1), (1 == 3) ] |> std.test.assert_all - => error: contract broken by a value + # => error: contract broken by the caller ``` "% # We mostly rely on the contracts to do the work here. We just need to @@ -3934,11 +4054,12 @@ # Examples - ```nickel + ```nickel multiline std.is_number 1 - => true + # => true + std.is_number "Hello, World!" - => false + # => false ``` "% = fun x => %typeof% x == 'Number, @@ -3950,11 +4071,12 @@ # Examples - ```nickel + ```nickel multiline std.is_bool false - => true + # => true + std.is_bool 42 - => false + # => false ``` "% = fun x => %typeof% x == 'Bool, @@ -3966,11 +4088,12 @@ # Examples - ```nickel + ```nickel multiline std.is_string true - => false + # => false + std.is_string "Hello, World!" - => true + # => true ``` "% = fun x => %typeof% x == 'String, @@ -3982,11 +4105,12 @@ # Examples - ```nickel + ```nickel multiline std.is_enum true - => false - std.is_enum `false - => true + # => false + + std.is_enum 'false + # => true ``` "% = fun x => %typeof% x == 'Enum, @@ -3998,11 +4122,12 @@ # Examples - ```nickel + ```nickel multiline std.is_function (fun x => x) - => true + # => true + std.is_function 42 - => false + # => false ``` "% = fun x => %typeof% x == 'Function, @@ -4014,11 +4139,12 @@ # Examples - ```nickel + ```nickel multiline std.is_array [ 1, 2 ] - => true + # => true + std.is_array 42 - => false + # => false ``` "% = fun x => %typeof% x == 'Array, @@ -4030,11 +4156,12 @@ # Examples - ```nickel + ```nickel multiline std.is_record [ 1, 2 ] - => false + # => false + std.is_record { hello = "Hello", world = "World" } - => true + # => true ``` "% = fun x => %typeof% x == 'Record, @@ -4059,11 +4186,12 @@ # Examples - ```nickel + ```nickel multiline std.typeof [ 1, 2 ] - => 'Array + # => 'Array + std.typeof (fun x => x) - => 'Function + # => 'Function ``` "% = fun x => %typeof% x, @@ -4088,13 +4216,15 @@ # Examples - ```nickel + ```nickel multiline std.seq (42 / 0) 37 - => error + # => error + std.seq (42 / 2) 37 - => 37 + # => 37 + std.seq { too_far = 42 / 0 } 37 - => 37 + # => 37 ``` "% = fun x y => %seq% x y, @@ -4108,13 +4238,15 @@ # Examples - ```nickel + ```nickel multiline std.deep_seq (42 / 0) 37 - => error + # => error + std.deep_seq (42 / 2) 37 - => 37 + # => 37 + std.deep_seq [1+1, { not_too_far = [42 / 0] }] 37 - => error + # => error ``` "% = fun x y => %deep_seq% x y, @@ -4128,7 +4260,7 @@ ```nickel std.hash 'Md5 "hunter2" - => "2ab96390c7dbe3439de74d0c9b0b1767" + # => "2ab96390c7dbe3439de74d0c9b0b1767" ``` "% = fun type s => %hash% type s, @@ -4141,11 +4273,8 @@ # Examples ```nickel - serialize 'Json { hello = "Hello", world = "World" } => - "{ - \"hello\": \"Hello\", - \"world\": \"World\" - }" + serialize 'Json { hello = "Hello", world = "World" } + # => "{\n \"hello\": \"Hello\",\n \"world\": \"World\"\n}" ``` "% = fun format x => %serialize% format (%force% x), @@ -4159,7 +4288,7 @@ ```nickel deserialize 'Json "{ \"hello\": \"Hello\", \"world\": \"World\" }" - { hello = "Hello", world = "World" } + # => { hello = "Hello", world = "World" } ``` "% = fun format x => %deserialize% format x, @@ -4172,13 +4301,15 @@ # Examples - ```nickel - std.to_string 42 => - "42" - std.to_string 'Foo => - "Foo" - std.to_string null => - "null" + ```nickel multiline + std.to_string 42 + # => "42" + + std.to_string 'Foo + # => "Foo" + + std.to_string null + # => "null" ``` "% = fun x => %to_string% x, @@ -4193,8 +4324,8 @@ ```nickel std.trace "Hello, world!" true - std.trace: Hello, world! - => true + # std.trace: Hello, world! + # => true ``` "% = fun msg x => %trace% msg x, @@ -4207,7 +4338,7 @@ ```nickel 1 | std.FailWith "message" - => error: contract broken by a value: message + # => error: message ``` "% = fun msg => @@ -4223,7 +4354,7 @@ ```nickel std.fail_with "message" - => error: contract broken by a value: message + # => error: message ``` "% = fun msg => null | FailWith msg, diff --git a/doc/manual/cookbook.md b/doc/manual/cookbook.md index 83237dcf42..6c21c855cd 100644 --- a/doc/manual/cookbook.md +++ b/doc/manual/cookbook.md @@ -44,3 +44,198 @@ record level. It makes code more navigable and `query`-friendly, but at the expense of repetition and duplicated contract checks. It is also currently required for polymorphic functions because of [the following bug](https://github.com/tweag/nickel/issues/360). + +## Unit/documentation tests + +With the `nickel test` command, nickel supports using documentation examples as +tests. This command extracts markdown blocks in documentation metadata, and runs +them as nickel snippets. For example, if the file "main.ncl" contains + +````nickel +{ + foo + | Number + | doc m%" + This is my field named foo. + + ## Examples + + ```nickel + 1 + "2" + ``` + "% +} +```` + +then running `nickel test main.ncl` will fail with the error message + +```console +testing foo/0...FAILED +test foo/0 failed +error: dynamic type error + ┌─ [..]/test.ncl:1:7 + │ +1 │ 1 + "2" + │ ^^^ this expression has type String, but Number was expected + │ + = (+) expects its 2nd argument to be a Number + +1 failures +error: tests failed +``` + +Only code blocks with the "nickel" tag are tested. + +### Testing for expected output + +In order to check that a test evaluates to a given value, terminate its +code block with a comment like `# => `. This is equivalent +to applying a `std.contract.Equal ` contract, but might +look better in the rendered documentation. For example, running +`nickel test` on + +````nickel +{ + foo + | Number + | doc m%" + This is my field named foo. + + ## Examples + + ```nickel + 1 + 1 + # => 3 + ``` + "% +} +```` + +will output + +```console +testing foo/0...FAILED +test foo/0 failed +error: contract broken by a value + ┌─ (generated by evaluation):1:1 + │ +1 │ std.contract.Equal 3 + │ -------------------- expected type + │ + ┌─ /home/jneeman/tweag/nickel/test.ncl:1:3 + │ +1 │ 1 + 1 + │ ^^^^^ applied to this expression + │ + ┌─ (generated by evaluation):1:1 + │ +1 │ 2 + │ - evaluated to this value + +1 failures +error: tests failed +``` + +### Checking for errors + +Sometimes you want a test case to ensure that errors are raised when +appropriate. You can check for this by terminating a code block with +a comment like `# => error: `, and the test runner +will check that the example raises and error, and that the error message +contains "\" as a substring. To test for an error +without checking the error message, terminate a code block with `# => error`. + +For example, + +````nickel +{ + foo + | Number + | doc m%" + This is my field named foo. + + ## Examples + + ```nickel + 1 + "2" + # => error: has type String, but Number was expected + ``` + "% +} +```` + +### Ignoring tests + +To ignore a test while still keeping the code block tagged as nickel +source, add the "ignore" label, like + +````text +```nickel ignore +1 | String +``` +```` + +### Multiline tests + +If you have many short examples, you might prefer not to wrap each +one in a separate code block. In this case, you can use the "multiline" +label to create multiple blank-line-separated tests in a single code block. +For example, + +````nickel +{ + foo + | Number + | doc m%" + This is my field named foo. + + ## Examples + + ```nickel multiline + 1 + "2" + # => error: has type String, but Number was expected + + 1 + 1 + # => 2 + + 1 + 2 + # => 3 + ``` + "% +} +```` + +### The test expression's environment + +Each test expression will be evaluated in the same environment as the field it's +documenting: the name of the field will be in scope, along with the names of all +the sibling fields. + +For example, the following tests will succeed: + +````nickel +{ + bar + | Number + = 2, + + foo + | Number + | doc m%" + This is my field named foo. + + ## Examples + + ```nickel + foo + # => 1 + ``` + + ```nickel + foo + bar + # => 3 + ``` + "% + = 1, +} +```` diff --git a/doc/manual/typing.md b/doc/manual/typing.md index b42fbd6a82..977973e986 100644 --- a/doc/manual/typing.md +++ b/doc/manual/typing.md @@ -688,9 +688,9 @@ calling to the statically typed `std.array.filter` from dynamically typed code: ```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` - ┌─ :427:25 + ┌─ :431:25 │ -427 │ : forall a. (a -> Bool) -> Array a -> Array a +431 │ : forall a. (a -> Bool) -> Array a -> Array a │ ---- expected return type of a function provided by the caller │ ┌─ :1:55 diff --git a/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__hover-stdlib-type.ncl.snap b/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__hover-stdlib-type.ncl.snap index 5b7d5aadc9..c20eb1c500 100644 --- a/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__hover-stdlib-type.ncl.snap +++ b/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__hover-stdlib-type.ncl.snap @@ -11,7 +11,7 @@ String -> Number ```nickel std.string.to_number "123" - => 123 +# => 123 ```, ```nickel NumberLiteral -> Dyn ```, ```nickel diff --git a/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__hover_field_typed_block_regression_1574.ncl.snap b/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__hover_field_typed_block_regression_1574.ncl.snap index 2e9e52340c..710e6c9c52 100644 --- a/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__hover_field_typed_block_regression_1574.ncl.snap +++ b/lsp/nls/tests/snapshots/main__lsp__nls__tests__inputs__hover_field_typed_block_regression_1574.ncl.snap @@ -10,8 +10,8 @@ expression: output # Examples ```nickel -std.array.map (fun x => x + 1) [ 1, 2, 3 ] => - [ 2, 3, 4 ] +std.array.map (fun x => x + 1) [ 1, 2, 3 ] +# => [ 2, 3, 4 ] ```, ```nickel forall a b. (a -> b) -> Array a -> Array b ```]