From 1e4bec10e5a0bfc0331818c7131fc274381a3e3a Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Wed, 27 Nov 2024 17:53:37 +0700 Subject: [PATCH 1/7] Do deep eval for doctests --- cli/src/doctest.rs | 2 +- .../inputs/doctest/fail_unexpected_error.ncl | 22 +++++++++++++------ ...test_stdout_fail_unexpected_error.ncl.snap | 13 +++++++++++ core/src/program.rs | 8 +++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/cli/src/doctest.rs b/cli/src/doctest.rs index 9427b67c20..fa8707873a 100644 --- a/cli/src/doctest.rs +++ b/cli/src/doctest.rs @@ -181,7 +181,7 @@ fn run_tests( let _ = std::io::stdout().flush(); // Undo the test's lazy wrapper. - let result = prog.eval_closure(Closure { + let result = prog.eval_deep_closure(Closure { body: mk_app!(val.clone(), Term::Null), env: Environment::new(), }); diff --git a/cli/tests/snapshot/inputs/doctest/fail_unexpected_error.ncl b/cli/tests/snapshot/inputs/doctest/fail_unexpected_error.ncl index 20ca85c330..3d26ad8ac6 100644 --- a/cli/tests/snapshot/inputs/doctest/fail_unexpected_error.ncl +++ b/cli/tests/snapshot/inputs/doctest/fail_unexpected_error.ncl @@ -3,14 +3,22 @@ { foo | doc m%" - ```nickel - foo + "1" - ``` + ```nickel + foo + "1" + ``` - ```nickel - foo + "1" - # => 2 - ``` + ```nickel + foo + "1" + # => 2 + ``` "% = 1, + + bar + | doc m%" + ```nickel + { foo = 1 } + # => { foo = 2 } + ``` + "% } 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 index 077f041d5d..74c4b48fc0 100644 --- 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 @@ -4,6 +4,7 @@ expression: out --- testing foo/0...FAILED testing foo/1...FAILED +testing bar/0...FAILED test foo/0 failed error: dynamic type error ┌─ [INPUTS_PATH]/doctest/fail_unexpected_error.ncl:1:7 @@ -21,3 +22,15 @@ error: dynamic type error │ ^^^ this expression has type String, but Number was expected │ = (+) expects its 2nd argument to be a Number + +test bar/0 failed +error: contract broken by a value + ┌─ (generated by evaluation):1:1 + │ +1 │ std.contract.Equal { foo = 2, } + │ ------------------------------- expected type + │ + ┌─ [INPUTS_PATH]/doctest/fail_unexpected_error.ncl:1:9 + │ +1 │ { foo = 1 } + │ ^ applied to this expression diff --git a/core/src/program.rs b/core/src/program.rs index f685c92ef9..2f364b5d5b 100644 --- a/core/src/program.rs +++ b/core/src/program.rs @@ -553,6 +553,14 @@ impl Program { Ok(self.vm.eval_deep_closure(prepared)?) } + /// Same as `eval_closure`, but does a full evaluation and does not substitute all variables. + /// + /// (Or, same as `eval_deep` but takes a closure.) + pub fn eval_deep_closure(&mut self, closure: Closure) -> Result { + self.vm.reset(); + self.vm.eval_deep_closure(closure) + } + /// Prepare for evaluation, then fetch the metadata of `self.field`, or list the fields of the /// whole program if `self.field` is empty. pub fn query(&mut self) -> Result { From 71e24b277e92ac3f8443fed7ea4c7bc3a7745c97 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Wed, 27 Nov 2024 18:08:35 +0700 Subject: [PATCH 2/7] Update snapshot --- .../snapshot__test_stderr_fail_unexpected_error.ncl.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 51be7e1057..dd6e76e152 100644 --- 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 @@ -2,5 +2,5 @@ source: cli/tests/snapshot/main.rs expression: err --- -2 failures +3 failures error: tests failed From 7622fe5105b65577dc88ed45d562d595170a4530 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Wed, 27 Nov 2024 18:29:08 +0700 Subject: [PATCH 3/7] Fix some stdlib tests --- core/stdlib/std.ncl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl index ed5c5af97e..f4f98230d7 100644 --- a/core/stdlib/std.ncl +++ b/core/stdlib/std.ncl @@ -3013,7 +3013,7 @@ # => [ "one", "two" ] std.record.fields_with_opts { one = 1, two = 2, three_opt | optional } - # => [ "one", "two", "three_opt" ] + # => [ "one", "three_opt", "two" ] ``` "% = fun r => %record/fields_with_opts% r, @@ -3363,7 +3363,7 @@ ```nickel std.record.to_array { hello = "world", foo = "bar" } - # => [ { field = "hello", value = "world" }, { field = "foo", value = "bar" } ] + # => [ { field = "foo", value = "bar" }, { field = "hello", value = "world" } ] ``` "% = fun record => From c70de8406741304be4a8405a76f83fd642db727f Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Fri, 22 Nov 2024 09:46:51 +0700 Subject: [PATCH 4/7] Add the package std module --- core/stdlib/std.ncl | 244 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl index f4f98230d7..e4f82f4d7f 100644 --- a/core/stdlib/std.ncl +++ b/core/stdlib/std.ncl @@ -2955,6 +2955,250 @@ = 2.7182818284590452354, }, + package = + let rec + # https://semver.org is kind enough to supply this "official" semver regex. + semver_re = m%"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"%, + # Just the major.minor.patch part, with minor and patch being optional. + partial_semver_re = m%"^(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?$"%, + # An exact version constraint. This one is required to have minor and patch versions, and it's allowed to have a prerelease. + semver_equals_req_re = m%"^=(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"%, + semver_req_re = "(%{partial_semver_re})|(%{semver_equals_req_re})", + in { + is_semver_req + : String -> Bool + | doc m%" + Returns true if a string is a valid version requirement in Nickel. + + See the `SemverReq` contract for more details. + "% + = std.string.is_match semver_req_re, + is_semver + : String -> Bool + | doc m%" + Returns true if a string is a valid semantic version. + + # Examples + + ```nickel multiline + std.package.is_semver "1.2.0-pre1" + # => true + + std.package.is_semver "1.foo" + # => false + ``` + "% + = std.string.is_match semver_re, + is_semver_prefix + : String -> Bool + | doc m%" + Returns true if a string is a valid semantic version prefix, + containing a major version and then optional minor and patch versions. + + # Examples + + ```nickel multiline + std.package.is_semver_prefix "1.2" + # => true + + std.package.is_semver_prefix "1.foo" + # => false + ``` + "% + = std.string.is_match partial_semver_re, + Semver + | doc m%" + A contract for semantic version ("semver") identifiers. + + # Examples + + ```nickel multiline + "1.2.0-pre1" | std.package.Semver + # => "1.2.0-pre1" + + "1.foo" | std.package.Semver + # => error: contract broken by a value + ``` + "% + = std.contract.from_predicate is_semver, + SemverPrefix + | doc m%" + A contract for semantic version ("semver") prefixes, + containing a major version and then optional minor and patch versions. + + # Examples + + ```nickel multiline + "1.2" | std.package.SemverPrefix + # => "1.2" + + "1.foo" | std.package.SemverPrefix + # => error: contract broken by a value + ``` + "% + = std.contract.from_predicate is_semver_prefix, + SemverReq + | doc m%" + A contract for semantic version ("semver") requirements. + + Nickel supports two kinds of requirements: semver-compatible + requirements and exact version requirements. Semver-compatible + requirements take the form "major.minor.patch", where minor and patch + are optional. Their semantics are: + + - "1.2.3" will match all versions having major version 1, minor version 2, + and patch version at least 3. + - "1.2" will match all versions having major version 1 and minor version + at least 2. + - "1" will match all versions having major version 1. + - a semver-compatible requirement will never match a prerelease version. + + Exact version requirements take the form "=major.minor.patch-pre", where + the prerelease tag is optional, but major, minor, and patch are all required. + + # Examples + + ```nickel multiline + "1.2" | SemverReq + # => "1.2" + + "=1.2" | SemverReq + # => error: contract broken by a value + + "1.2.0" | SemverReq + # => "1.2.0" + + "=1.2.0" | SemverReq + # => "=1.2.0" + + "1.2.0-pre1" | SemverReq + # => error: contract broken by a value + + "=1.2.0-pre1" | SemverReq + # => "=1.2.0-pre1" + ``` + "% + = std.contract.from_predicate is_semver_req, + # TODO: bikeshedding opportunity: which fields should be optional? + Manifest = { + name + | String + | doc m%" + The name of this package. + "%, + + version + | String + | Semver + | doc m%" + The version of this package. + + Any semantic version is accepted, but the build metadata field has no effect when matching versions. + "%, + + nickel_version + | String + | SemverPrefix + | doc m%" + The minimal nickel version required for this package. + "%, + + authors + | Array String + | doc m%" + The authors of this package. + "%, + + description + | String + | doc m%" + A description of this package. + "%, + + keywords + | Array String + | optional + | doc m%" + A list of keywords to help people find this package. + "%, + + # TODO: maybe restrict this to be a valid SPDX 2.3 license expression? + # Cargo allows anything here, but applies restrictions when trying to + # publish to crates.io. + license + | String + | optional + | doc m%" + The name of the license that this package is available under. + "%, + + dependencies + | { + _ : [| + 'Path String, + 'Git { + url + | String + | doc m%" + The url of a git repository. + + This supports local file paths, https urls like `https://example.com/example-repo`, + and ssh urls like `user@example.com:repo`. + "%, + ref + | [| 'Head, 'Branch String, 'Tag String, 'Commit String |] + | optional + | doc m%" + The git ref to fetch from the repository. + + If not provided, defaults to 'Head. + "%, + path + | String + | optional + | doc m%" + The path of the nickel package within the git repository. If omitted, the nickel package + is at the root of the git repository. + "%, + }, + 'Index { + package + | String + | doc m%" + The dependency's identifier within the nickel index, in the format "github//" + "%, + version + | String + | SemverReq + | doc m%" + The required version of the package. + + Nickel supports two kinds of requirements: semver-compatible + requirements and exact version requirements. Semver-compatible + requirements take the form "major.minor.patch", where minor and patch + are optional. Their semantics are: + + - "1.2.3" will match all versions having major version 1, minor version 2, + and patch version at least 3. + - "1.2" will match all versions having major version 1 and minor version + at least 2. + - "1" will match all versions having major version 1. + - a semver-compatible requirement will never match a prerelease version. + + Exact version requirements take the form "=major.minor.patch-pre", where + the prerelease tag is optional, but major, minor, and patch are all required. + "%, + }, + |] + } + | doc m%" + A dictionary of package dependencies, keyed by the name that this package uses to refer to them locally. + "% + | default + = {}, + }, + }, + record = { map : forall a b. (String -> a -> b) -> { _ : a } -> { _ : b } From adfa6cd8bdb5614a4d8bb49d338a2b6f74a7a15e Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Tue, 26 Nov 2024 10:23:44 +0700 Subject: [PATCH 5/7] Indent docs --- core/stdlib/std.ncl | 206 ++++++++++++++++++++++---------------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl index e4f82f4d7f..fb1ef79a76 100644 --- a/core/stdlib/std.ncl +++ b/core/stdlib/std.ncl @@ -2968,158 +2968,158 @@ is_semver_req : String -> Bool | doc m%" - Returns true if a string is a valid version requirement in Nickel. + Returns true if a string is a valid version requirement in Nickel. - See the `SemverReq` contract for more details. - "% + See the `SemverReq` contract for more details. + "% = std.string.is_match semver_req_re, is_semver : String -> Bool | doc m%" - Returns true if a string is a valid semantic version. + Returns true if a string is a valid semantic version. - # Examples + # Examples - ```nickel multiline - std.package.is_semver "1.2.0-pre1" - # => true + ```nickel multiline + std.package.is_semver "1.2.0-pre1" + # => true - std.package.is_semver "1.foo" - # => false - ``` - "% + std.package.is_semver "1.foo" + # => false + ``` + "% = std.string.is_match semver_re, is_semver_prefix : String -> Bool | doc m%" - Returns true if a string is a valid semantic version prefix, - containing a major version and then optional minor and patch versions. + Returns true if a string is a valid semantic version prefix, + containing a major version and then optional minor and patch versions. - # Examples + # Examples - ```nickel multiline - std.package.is_semver_prefix "1.2" - # => true + ```nickel multiline + std.package.is_semver_prefix "1.2" + # => true - std.package.is_semver_prefix "1.foo" - # => false - ``` - "% + std.package.is_semver_prefix "1.foo" + # => false + ``` + "% = std.string.is_match partial_semver_re, Semver | doc m%" - A contract for semantic version ("semver") identifiers. + A contract for semantic version ("semver") identifiers. - # Examples + # Examples - ```nickel multiline - "1.2.0-pre1" | std.package.Semver - # => "1.2.0-pre1" + ```nickel multiline + "1.2.0-pre1" | std.package.Semver + # => "1.2.0-pre1" - "1.foo" | std.package.Semver - # => error: contract broken by a value - ``` - "% + "1.foo" | std.package.Semver + # => error: contract broken by a value + ``` + "% = std.contract.from_predicate is_semver, SemverPrefix | doc m%" - A contract for semantic version ("semver") prefixes, - containing a major version and then optional minor and patch versions. + A contract for semantic version ("semver") prefixes, + containing a major version and then optional minor and patch versions. - # Examples + # Examples - ```nickel multiline - "1.2" | std.package.SemverPrefix - # => "1.2" + ```nickel multiline + "1.2" | std.package.SemverPrefix + # => "1.2" - "1.foo" | std.package.SemverPrefix - # => error: contract broken by a value - ``` - "% + "1.foo" | std.package.SemverPrefix + # => error: contract broken by a value + ``` + "% = std.contract.from_predicate is_semver_prefix, SemverReq | doc m%" - A contract for semantic version ("semver") requirements. + A contract for semantic version ("semver") requirements. - Nickel supports two kinds of requirements: semver-compatible - requirements and exact version requirements. Semver-compatible - requirements take the form "major.minor.patch", where minor and patch - are optional. Their semantics are: + Nickel supports two kinds of requirements: semver-compatible + requirements and exact version requirements. Semver-compatible + requirements take the form "major.minor.patch", where minor and patch + are optional. Their semantics are: - - "1.2.3" will match all versions having major version 1, minor version 2, - and patch version at least 3. - - "1.2" will match all versions having major version 1 and minor version - at least 2. - - "1" will match all versions having major version 1. - - a semver-compatible requirement will never match a prerelease version. + - "1.2.3" will match all versions having major version 1, minor version 2, + and patch version at least 3. + - "1.2" will match all versions having major version 1 and minor version + at least 2. + - "1" will match all versions having major version 1. + - a semver-compatible requirement will never match a prerelease version. - Exact version requirements take the form "=major.minor.patch-pre", where - the prerelease tag is optional, but major, minor, and patch are all required. + Exact version requirements take the form "=major.minor.patch-pre", where + the prerelease tag is optional, but major, minor, and patch are all required. - # Examples + # Examples - ```nickel multiline - "1.2" | SemverReq - # => "1.2" + ```nickel multiline + "1.2" | SemverReq + # => "1.2" - "=1.2" | SemverReq - # => error: contract broken by a value + "=1.2" | SemverReq + # => error: contract broken by a value - "1.2.0" | SemverReq - # => "1.2.0" + "1.2.0" | SemverReq + # => "1.2.0" - "=1.2.0" | SemverReq - # => "=1.2.0" + "=1.2.0" | SemverReq + # => "=1.2.0" - "1.2.0-pre1" | SemverReq - # => error: contract broken by a value + "1.2.0-pre1" | SemverReq + # => error: contract broken by a value - "=1.2.0-pre1" | SemverReq - # => "=1.2.0-pre1" - ``` - "% + "=1.2.0-pre1" | SemverReq + # => "=1.2.0-pre1" + ``` + "% = std.contract.from_predicate is_semver_req, # TODO: bikeshedding opportunity: which fields should be optional? Manifest = { name | String | doc m%" - The name of this package. + The name of this package. "%, version | String | Semver | doc m%" - The version of this package. + The version of this package. - Any semantic version is accepted, but the build metadata field has no effect when matching versions. + Any semantic version is accepted, but the build metadata field has no effect when matching versions. "%, nickel_version | String | SemverPrefix | doc m%" - The minimal nickel version required for this package. + The minimal nickel version required for this package. "%, authors | Array String | doc m%" - The authors of this package. + The authors of this package. "%, description | String | doc m%" - A description of this package. + A description of this package. "%, keywords | Array String | optional | doc m%" - A list of keywords to help people find this package. + A list of keywords to help people find this package. "%, # TODO: maybe restrict this to be a valid SPDX 2.3 license expression? @@ -3129,7 +3129,7 @@ | String | optional | doc m%" - The name of the license that this package is available under. + The name of the license that this package is available under. "%, dependencies @@ -3140,59 +3140,59 @@ url | String | doc m%" - The url of a git repository. + The url of a git repository. - This supports local file paths, https urls like `https://example.com/example-repo`, - and ssh urls like `user@example.com:repo`. + This supports local file paths, https urls like `https://example.com/example-repo`, + and ssh urls like `user@example.com:repo`. "%, ref | [| 'Head, 'Branch String, 'Tag String, 'Commit String |] | optional | doc m%" - The git ref to fetch from the repository. + The git ref to fetch from the repository. - If not provided, defaults to 'Head. + If not provided, defaults to 'Head. "%, path | String | optional | doc m%" - The path of the nickel package within the git repository. If omitted, the nickel package - is at the root of the git repository. + The path of the nickel package within the git repository. If omitted, the nickel package + is at the root of the git repository. "%, }, 'Index { package | String | doc m%" - The dependency's identifier within the nickel index, in the format "github//" + The dependency's identifier within the nickel index, in the format "github//" "%, version | String | SemverReq | doc m%" - The required version of the package. - - Nickel supports two kinds of requirements: semver-compatible - requirements and exact version requirements. Semver-compatible - requirements take the form "major.minor.patch", where minor and patch - are optional. Their semantics are: - - - "1.2.3" will match all versions having major version 1, minor version 2, - and patch version at least 3. - - "1.2" will match all versions having major version 1 and minor version - at least 2. - - "1" will match all versions having major version 1. - - a semver-compatible requirement will never match a prerelease version. - - Exact version requirements take the form "=major.minor.patch-pre", where - the prerelease tag is optional, but major, minor, and patch are all required. + The required version of the package. + + Nickel supports two kinds of requirements: semver-compatible + requirements and exact version requirements. Semver-compatible + requirements take the form "major.minor.patch", where minor and patch + are optional. Their semantics are: + + - "1.2.3" will match all versions having major version 1, minor version 2, + and patch version at least 3. + - "1.2" will match all versions having major version 1 and minor version + at least 2. + - "1" will match all versions having major version 1. + - a semver-compatible requirement will never match a prerelease version. + + Exact version requirements take the form "=major.minor.patch-pre", where + the prerelease tag is optional, but major, minor, and patch are all required. "%, }, |] } | doc m%" - A dictionary of package dependencies, keyed by the name that this package uses to refer to them locally. + A dictionary of package dependencies, keyed by the name that this package uses to refer to them locally. "% | default = {}, From 258741ed16afa746c08edd950b013ab7b2057f79 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Thu, 19 Dec 2024 17:49:23 +0700 Subject: [PATCH 6/7] WIP on structured semver stuff --- core/stdlib/std.ncl | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl index fb1ef79a76..2c15ab2ea8 100644 --- a/core/stdlib/std.ncl +++ b/core/stdlib/std.ncl @@ -2964,7 +2964,47 @@ # An exact version constraint. This one is required to have minor and patch versions, and it's allowed to have a prerelease. semver_equals_req_re = m%"^=(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"%, semver_req_re = "(%{partial_semver_re})|(%{semver_equals_req_re})", + Semver = { + major | Number, + minor | Number, + patch | Number, + pre + | String + | default + = "", + build + | String + | default + = "", + }, in { + NormalizeSemver + | doc m%" + ```nickel + "1.2.3-pre1+build" | NormalizeSemver + # => { major = 1, minor = 2, patch = 3, pre = "", build = "build" } + ``` + "% + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | Semver) + else if %typeof% value == 'String then + let matches = string.find semver_re value in + if matches.index == -1 then + 'Error { message = "invalid semver" } + else + let gs = matches.groups in + 'Ok { + major = gs |> array.at 0 |> string.to_number, + minor = gs |> array.at 1 |> string.to_number, + patch = gs |> array.at 2 |> string.to_number, + pre = gs |> array.at 3, + build = gs |> array.at 4 + } + else + 'Error { message = "expected a string or a record" } + ), is_semver_req : String -> Bool | doc m%" @@ -3096,7 +3136,7 @@ Any semantic version is accepted, but the build metadata field has no effect when matching versions. "%, - nickel_version + minimal_nickel_version | String | SemverPrefix | doc m%" From aa90380abab9a66e10a1ba1efa7ab7e327eee611 Mon Sep 17 00:00:00 2001 From: Joe Neeman Date: Tue, 24 Dec 2024 16:02:15 +0700 Subject: [PATCH 7/7] Structured semver parsing --- core/stdlib/std.ncl | 514 ++++++++++++++++++++++++++++---------------- 1 file changed, 325 insertions(+), 189 deletions(-) diff --git a/core/stdlib/std.ncl b/core/stdlib/std.ncl index 2c15ab2ea8..7e197ecfc6 100644 --- a/core/stdlib/std.ncl +++ b/core/stdlib/std.ncl @@ -2957,126 +2957,215 @@ package = let rec - # https://semver.org is kind enough to supply this "official" semver regex. - semver_re = m%"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"%, - # Just the major.minor.patch part, with minor and patch being optional. - partial_semver_re = m%"^(0|[1-9]\d*)(\.(0|[1-9]\d*))?(\.(0|[1-9]\d*))?$"%, - # An exact version constraint. This one is required to have minor and patch versions, and it's allowed to have a prerelease. - semver_equals_req_re = m%"^=(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"%, - semver_req_re = "(%{partial_semver_re})|(%{semver_equals_req_re})", - Semver = { - major | Number, - minor | Number, - patch | Number, - pre - | String - | default - = "", - build - | String - | default - = "", - }, - in { - NormalizeSemver - | doc m%" - ```nickel - "1.2.3-pre1+build" | NormalizeSemver - # => { major = 1, minor = 2, patch = 3, pre = "", build = "build" } - ``` - "% - = - %contract/custom% (fun _label value => - if %typeof% value == 'Record then - 'Ok (value | Semver) - else if %typeof% value == 'String then - let matches = string.find semver_re value in - if matches.index == -1 then - 'Error { message = "invalid semver" } - else - let gs = matches.groups in - 'Ok { - major = gs |> array.at 0 |> string.to_number, - minor = gs |> array.at 1 |> string.to_number, - patch = gs |> array.at 2 |> string.to_number, - pre = gs |> array.at 3, - build = gs |> array.at 4 - } - else - 'Error { message = "expected a string or a record" } - ), - is_semver_req - : String -> Bool - | doc m%" - Returns true if a string is a valid version requirement in Nickel. - - See the `SemverReq` contract for more details. - "% - = std.string.is_match semver_req_re, - is_semver - : String -> Bool - | doc m%" - Returns true if a string is a valid semantic version. - - # Examples - - ```nickel multiline - std.package.is_semver "1.2.0-pre1" - # => true + semver_re = m%"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"%, + # Just the major.minor.patch part, with minor and patch being optional. + partial_semver_re = m%"^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:\.(0|[1-9]\d*))?$"%, + # An exact version constraint. This one is required to have minor and patch versions, and it's allowed to have a prerelease. + semver_equals_req_re = m%"^=(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"%, + semver_req_re = "(%{partial_semver_re})|(%{semver_equals_req_re})", + in + { + structured = { + Semver + | doc m%" + A contract for semantic version ("semver") identifiers, structured as a record. - std.package.is_semver "1.foo" - # => false - ``` - "% - = std.string.is_match semver_re, - is_semver_prefix - : String -> Bool - | doc m%" - Returns true if a string is a valid semantic version prefix, - containing a major version and then optional minor and patch versions. + See also `std.package.Semver`, which can produce one of these by parsing a string. + "% + = { + major + | number.Nat + | doc "The major version number.", + minor + | number.Nat + | doc "The minor version number.", + patch + | number.Nat + | doc "The patch version.", + pre + | String + | doc m%" + The prerelease identifier. - # Examples + When comparing versions for compatibility, non-empty prerelease + strings only match one another if they are exactly equal. See + `std.package.SemverReq` for more details + "% + | optional, + build + | String + | doc m%" + The build metadata. - ```nickel multiline - std.package.is_semver_prefix "1.2" - # => true + This is completely ignored while comparing versions for compatibility. + "% + | optional, + }, + ExactSemverReq + | doc m%" + A contract for exact semantic version ("semver") requirements, structured as a record. - std.package.is_semver_prefix "1.foo" - # => false - ``` - "% - = std.string.is_match partial_semver_re, + This differs from `std.package.structured.Semver` in that it lacks a build metadata field. + "% + = { + major + | number.Nat + | doc "The major version number.", + minor + | number.Nat + | doc "The minor version number.", + patch + | number.Nat + | doc "The patch version.", + pre + | String + | doc "The prerelease identifier." + | optional, + }, + SemverPrefix + | doc m%" + A contract for semantic version ("semver") prefixes, structured as a record. + "% + = { + major + | number.Nat + | doc "The major version number.", + minor + | number.Nat + | doc "The minor version number." + | optional, + patch + | number.Nat + | doc "The patch version." + | optional, + }, + SemverReq = [| 'Compatible SemverPrefix, 'Exact ExactSemverReq |] + }, Semver | doc m%" A contract for semantic version ("semver") identifiers. + This is a normalizing contract, which accepts either a string to be parsed + or a record. If a string is provided, it will be parsed into a record (in + the `SemverRecord` format). + # Examples ```nickel multiline "1.2.0-pre1" | std.package.Semver - # => "1.2.0-pre1" + # => { major = 1, minor = 2, patch = 0, pre = "pre1", } + + { major = 1, minor = 2, patch = 0 } | std.package.Semver + # => { major = 1, minor = 2, patch = 0, } + + { major = 1, minor = 2 } | std.package.Semver + # => error: missing definition "1.foo" | std.package.Semver # => error: contract broken by a value ``` "% - = std.contract.from_predicate is_semver, + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | structured.Semver) + else if %typeof% value == 'String then + let matches = string.find semver_re value in + if matches.index == -1 then + 'Error { message = "invalid semver" } + else + let gs = matches.groups in + let + pre_ = gs |> array.at 3, + build_ = gs |> array.at 4, + in + 'Ok ( + ( + { + major = gs |> array.at 0 |> string.to_number, + minor = gs |> array.at 1 |> string.to_number, + patch = gs |> array.at 2 |> string.to_number, + } + & (if pre_ != "" then { pre = pre_ } else {}) + & (if build_ != "" then { build = build_ } else {}) + ) | structured.Semver + ) + else + 'Error { message = "expected a string or a record" } + ), SemverPrefix | doc m%" - A contract for semantic version ("semver") prefixes, - containing a major version and then optional minor and patch versions. + A contract for semantic version ("semver") prefixes. + + This prefix must contain a major version number. It may contain a minor + version and a patch version, but it must not contain a prerelease version + or build metadata. # Examples ```nickel multiline "1.2" | std.package.SemverPrefix - # => "1.2" + # => { major = 1, minor = 2 } + + { major = 1, minor = 2 } | std.package.SemverPrefix + # => { major = 1, minor = 2 } "1.foo" | std.package.SemverPrefix # => error: contract broken by a value ``` "% - = std.contract.from_predicate is_semver_prefix, + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | structured.SemverPrefix) + else if %typeof% value == 'String then + let matches = string.find partial_semver_re value in + if matches.index == -1 then + 'Error { message = "invalid semver" } + else + let gs = matches.groups in + let + minor_ = gs |> array.at 1, + patch_ = gs |> array.at 2, + in + 'Ok ( + ( + { major = gs |> array.at 0 |> string.to_number } + & (if minor_ == "" then {} else { minor = string.to_number minor_ }) + & (if patch_ == "" then {} else { patch = string.to_number patch_ }) + ) | structured.SemverPrefix + ) + else + 'Error { message = "expected a string or a record" } + ), + ExactSemverReq + | doc m%" + A contract for exact semantic version ("semver") requirements, structured as a record. + "% + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | structured.ExactSemverReq) + else if %typeof% value == 'String then + let matches = string.find semver_equals_req_re value in + if matches.index == -1 then + 'Error { message = "invalid semver" } + else + let gs = matches.groups in + let pre_ = array.at 3 gs in + 'Ok ( + ( + { + major = gs |> array.at 0 |> string.to_number, + minor = gs |> array.at 1 |> string.to_number, + patch = gs |> array.at 2 |> string.to_number, + } + & (if pre_ == "" then {} else { pre = pre_ }) + ) | structured.ExactSemverReq + ) + else + 'Error { message = "expected a string or a record" } + ), SemverReq | doc m%" A contract for semantic version ("semver") requirements. @@ -3100,143 +3189,190 @@ ```nickel multiline "1.2" | SemverReq - # => "1.2" + # => 'Compatible { major = 1, minor = 2 } "=1.2" | SemverReq # => error: contract broken by a value "1.2.0" | SemverReq - # => "1.2.0" + # => 'Compatible { major = 1, minor = 2, patch = 0 } "=1.2.0" | SemverReq - # => "=1.2.0" + # => 'Exact { major = 1, minor = 2, patch = 0, } "1.2.0-pre1" | SemverReq # => error: contract broken by a value "=1.2.0-pre1" | SemverReq - # => "=1.2.0-pre1" + # => 'Exact { major = 1, minor = 2, patch = 0, pre = "pre1", } ``` "% - = std.contract.from_predicate is_semver_req, - # TODO: bikeshedding opportunity: which fields should be optional? - Manifest = { - name - | String - | doc m%" + = + %contract/custom% (fun _label value => + if %typeof% value == 'Record then + 'Ok (value | structured.SemverReq) + else if %typeof% value == 'String then + if string.is_match semver_equals_req_re value then + 'Ok ('Exact (value | ExactSemverReq)) + else + 'Ok ('Compatible (value | SemverPrefix)) + else + 'Error { message = "expected a string or a record" } + ), + + GitDependency + | doc m%" + A contract identifying a dependency that can be fetched from a git repository. + "% + = { + url + | String + | doc m%" + The url of a git repository. + + This supports local file paths, https urls like `https://example.com/example-repo`, + and ssh urls like `user@example.com:repo`. + "%, + ref + | [| 'Head, 'Branch String, 'Tag String, 'Commit String |] + | optional + | doc m%" + The git ref to fetch from the repository. + + If not provided, defaults to 'Head. + "%, + path + | String + | optional + | doc m%" + The path of the nickel package within the git repository. If omitted, the nickel package + is at the root of the git repository. + "%, + }, + + IndexDependency + | doc m%" + A contract identifying a dependency that can be fetched from a package index. + "% + = { + package + | String + | doc m%" + The dependency's identifier within the package index, in the format "github//". + "%, + version + | SemverReq + | doc m%" + The required version of the package. + + Nickel supports two kinds of requirements: semver-compatible + requirements and exact version requirements. Semver-compatible + requirements take the form "major.minor.patch", where minor and patch + are optional. Their semantics are: + + - "1.2.3" will match all versions having major version 1, minor version 2, + and patch version at least 3. + - "1.2" will match all versions having major version 1 and minor version + at least 2. + - "1" will match all versions having major version 1. + - a semver-compatible requirement will never match a prerelease version. + + Exact version requirements take the form "=major.minor.patch-pre", where + the prerelease tag is optional, but major, minor, and patch are all required. + "%, + }, + + Manifest + | doc m%" + A contract for a Nickel package manifest. + + # Example + + ```nickel + { + name = "my-package", + version = "1.0.0", + minimal_nickel_version = "1.10", + authors = ["Me "], + description = "My great package", + + dependencies = { + my_local_dep = 'Path "../somewhere", + git_dep = 'Git { url = "https://example.com/repo", ref = 'Tag "v1.0" }, + index_dep = 'Index { package = "github/nickel-lang/example", version = "1.0" }, + }, + } | std.package.Manifest + ``` + "% + = { + name + | String + | doc m%" The name of this package. "%, - version - | String - | Semver - | doc m%" + version + | String + | Semver + | doc m%" The version of this package. Any semantic version is accepted, but the build metadata field has no effect when matching versions. "%, - minimal_nickel_version - | String - | SemverPrefix - | doc m%" + minimal_nickel_version + | String + | SemverPrefix + | doc m%" The minimal nickel version required for this package. "%, - authors - | Array String - | doc m%" + authors + | Array String + | doc m%" The authors of this package. "%, - description - | String - | doc m%" + description + | String + | optional + | doc m%" A description of this package. "%, - keywords - | Array String - | optional - | doc m%" + keywords + | Array String + | optional + | doc m%" A list of keywords to help people find this package. "%, - # TODO: maybe restrict this to be a valid SPDX 2.3 license expression? - # Cargo allows anything here, but applies restrictions when trying to - # publish to crates.io. - license - | String - | optional - | doc m%" + license + | String + | optional + | doc m%" The name of the license that this package is available under. + + This is a completely free-form string, but some tooling may impose + restrictions. For example, if you want to publish your package in + Nickel's global package registry, the license field needs to be a + valid SPDX license expression that allows redistribution. "%, - dependencies - | { - _ : [| - 'Path String, - 'Git { - url - | String - | doc m%" - The url of a git repository. - - This supports local file paths, https urls like `https://example.com/example-repo`, - and ssh urls like `user@example.com:repo`. - "%, - ref - | [| 'Head, 'Branch String, 'Tag String, 'Commit String |] - | optional - | doc m%" - The git ref to fetch from the repository. - - If not provided, defaults to 'Head. - "%, - path - | String - | optional - | doc m%" - The path of the nickel package within the git repository. If omitted, the nickel package - is at the root of the git repository. - "%, - }, - 'Index { - package - | String - | doc m%" - The dependency's identifier within the nickel index, in the format "github//" - "%, - version - | String - | SemverReq - | doc m%" - The required version of the package. - - Nickel supports two kinds of requirements: semver-compatible - requirements and exact version requirements. Semver-compatible - requirements take the form "major.minor.patch", where minor and patch - are optional. Their semantics are: - - - "1.2.3" will match all versions having major version 1, minor version 2, - and patch version at least 3. - - "1.2" will match all versions having major version 1 and minor version - at least 2. - - "1" will match all versions having major version 1. - - a semver-compatible requirement will never match a prerelease version. - - Exact version requirements take the form "=major.minor.patch-pre", where - the prerelease tag is optional, but major, minor, and patch are all required. - "%, - }, - |] - } - | doc m%" + dependencies + | { + _ : [| + 'Path String, + 'Git std.package.GitDependency, + 'Index std.package.IndexDependency, + |] + } + | doc m%" A dictionary of package dependencies, keyed by the name that this package uses to refer to them locally. "% - | default - = {}, - }, + | default + = {}, + }, }, record = {