diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 399db4d..e6d2b5e 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -31,9 +31,9 @@ jobs: uses: actions/configure-pages@v2 - name: 🧰 Install Aiken - uses: aiken-lang/setup-aiken@v1 + uses: aiken-lang/setup-aiken@v0.1.0 with: - version: v1.0.28-alpha + version: v1.0.18-alpha - name: 📝 Run fmt run: aiken fmt --check @@ -52,7 +52,7 @@ jobs: path: "docs/" deploy: - # if: ${{ startsWith(github.ref, 'refs/tags') }} + if: ${{ startsWith(github.ref, 'refs/tags') }} needs: build runs-on: ubuntu-latest environment: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bec225..bdc4355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,69 @@ # Changelog +## v2.0.0 - UNRELEASED + +### Added + +- New modules covering Conway-related features (i.e. governance) + - [`cardano/governance`](https://aiken-lang.github.io/stdlib/cardano/governance.html) + - [`cardano/governance/protocol_parameters`](https://aiken-lang.github.io/stdlib/cardano/governance/protocol_parameters.html) + +- New primitives in `aiken/crypto`: + - [`blake2b_224`](https://aiken-lang.github.io/stdlib/aiken/crypto.html#blake2b_224) + - [`keccak_256`](https://aiken-lang.github.io/stdlib/aiken/crypto.html#keccak_256) + +- New primitives in `aiken/math`: + - [`log2`](https://aiken-lang.github.io/stdlib/aiken/math.html#log2) + +- New primitives in `aiken/primitive/bytearray`: + - [`at`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#at) + - [`from_int_big_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#from_int_big_endian) + - [`from_int_little_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#from_int_little_endian) + - [`to_int_big_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#to_int_big_endian) + - [`to_int_little_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html#to_int_little_endian) + +- New primitives in `aiken/primitive/int`: + - [`from_bytearray_big_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/int.html#from_bytearray_big_endian) + - [`from_bytearray_little_endian`](https://aiken-lang.github.io/stdlib/aiken/primitive/int.html#from_bytearray_little_endian) + +- New primitives in `aiken/crypto`: + - [`verify_ecdsa_signature`](https://aiken-lang.github.io/stdlib/cardano/credential.html#verify_ecdsa_signature) + - [`verify_schnorr_signature`](https://aiken-lang.github.io/stdlib/cardano/credential.html#verify_schnorr_signature) + +### Changed + +- Few modules have been relocated and better organized: + - `aiken/hash` -> [`aiken/crypto`](https://aiken-lang.github.io/stdlib/aiken/crypto.html) + - **collections** + - `aiken/dict` -> [`aiken/collection/dict`](https://aiken-lang.github.io/stdlib/aiken/collection/dict.html) + - `aiken/list` -> [`aiken/collection/list`](https://aiken-lang.github.io/stdlib/aiken/collection/list.html) + - `aiken/pairs` -> [`aiken/collection/pairs`](https://aiken-lang.github.io/stdlib/aiken/collection/pairs.html) + - **primitive** + - `aiken/bytearray` -> [`aiken/primitive/bytearray`](https://aiken-lang.github.io/stdlib/aiken/primitive/bytearray.html) + - `aiken/int` -> [`aiken/primitive/int`](https://aiken-lang.github.io/stdlib/aiken/primitive/int.html) + - `aiken/string` -> [`aiken/primitive/string`](https://aiken-lang.github.io/stdlib/aiken/primitive/string.html) + - **cardano** + - `aiken/transaction` -> [`cardano/transaction`](https://aiken-lang.github.io/stdlib/cardano/transaction.html) + - `aiken/transaction/certificate` -> [`cardano/certificate`](https://aiken-lang.github.io/stdlib/cardano/certificate.html) + - `aiken/transaction/credential` -> [`cardano/address`](https://aiken-lang.github.io/stdlib/cardano/address.html) & `aiken/crypto` + - `aiken/transaction/value` -> [`cardano/assets`](https://aiken-lang.github.io/stdlib/cardano/assets.html) + +- The `Transaction` type from [`cardano/transaction`](https://aiken-lang.github.io/stdlib/cardano/transaction.html) (originally `aiken/transaction`) has been greatly reworked to match the new transaction format in Plutus V3. + +- The `ScriptContext` type has split from `cardano/transaction` (originally `aiken/transaction`) and moved into its own module [`cardano/script_context`](https://aiken-lang.github.io/stdlib/cardano/script_context.html) and adjusted to its new form as per Plutus V3. + +- The constructors of [`Credential`](https://aiken-lang.github.io/stdlib/cardano/address.html#credential) have been renamed from `VerificationKeyCredential` and `ScriptCredential` into `VerificationKey` and `Script` respectively. + +- The function `remove_all`, `remove_first` and `remove_last` from [`aiken/collection/pairs`](https://aiken-lang.github.io/stdlib/aiken/collection/pairs.html) (originally `aiken/pairs`) have been renamed to `delete_all`, `delete_first` and `delete_last` respectively. + +- The function `verify_signature` from [`aiken/crypto`](https://aiken-lang.github.io/stdlib/aiken/crypto.html) (originally `aiken/credential`) has been renamed to `verify_ed25519_signature`. + +### Removed + +- The module `aiken/time`. The `PosixTime` alias is no longer used anywhere. + +- `MintedValue` (from `aiken/transaction/value` originally) and its associated functions are no longer needed and, therefore, gone. + ## v1.9.0 - 2024-05-24 ### Added diff --git a/aiken.toml b/aiken.toml index 5d5dc77..d9881e5 100644 --- a/aiken.toml +++ b/aiken.toml @@ -1,5 +1,5 @@ name = "aiken-lang/stdlib" -version = "1.9.0" +version = "main" licences = ["Apache-2.0"] description = "The Aiken Standard Library" diff --git a/lib/aiken/cbor.ak b/lib/aiken/cbor.ak index e414edb..64de402 100644 --- a/lib/aiken/cbor.ak +++ b/lib/aiken/cbor.ak @@ -13,14 +13,14 @@ use aiken/builtin.{decode_utf8, serialise_data} /// useful for debugging. /// /// ```aiken -/// serialise(42) == #"182a" -/// serialise(#"a1b2") == #"42a1b2" -/// serialise([]) == #"80" -/// serialise((1, 2)) == #"9f0102ff" -/// serialise((1, #"ff", 3)) == #"9f0141ff03ff" -/// serialise([(1, #"ff")]) == #"a10141ff" -/// serialise(Some(42)) == #"d8799f182aff" -/// serialise(None) == #"d87a80" +/// cbor.serialise(42) == #"182a" +/// cbor.serialise(#"a1b2") == #"42a1b2" +/// cbor.serialise([]) == #"80" +/// cbor.serialise((1, 2)) == #"9f0102ff" +/// cbor.serialise((1, #"ff", 3)) == #"9f0141ff03ff" +/// cbor.serialise([(1, #"ff")]) == #"a10141ff" +/// cbor.serialise(Some(42)) == #"d8799f182aff" +/// cbor.serialise(None) == #"d87a80" /// ``` pub fn serialise(self: Data) -> ByteArray { serialise_data(self) @@ -73,15 +73,15 @@ test serialise_9() { /// a good idea in the Cardano world. /// /// ```aiken -/// diagnostic(42) == "42" -/// diagnostic(#"a1b2") == "h'A1B2'" -/// diagnostic([1, 2, 3]) == "[_ 1, 2, 3]" -/// diagnostic([]) == "[]" -/// diagnostic((1, 2)) == "[_ 1, 2]" -/// diagnostic((1, #"ff", 3)) == "[_ 1, h'FF', 3]" -/// diagnostic([(1, #"ff")]) == "{_ 1: h'FF' }" -/// diagnostic(Some(42)) == "121([_ 42])" -/// diagnostic(None) == "122([])" +/// cbor.diagnostic(42) == "42" +/// cbor.diagnostic(#"a1b2") == "h'A1B2'" +/// cbor.diagnostic([1, 2, 3]) == "[_ 1, 2, 3]" +/// cbor.diagnostic([]) == "[]" +/// cbor.diagnostic((1, 2)) == "[_ 1, 2]" +/// cbor.diagnostic((1, #"ff", 3)) == "[_ 1, h'FF', 3]" +/// cbor.diagnostic([(1, #"ff")]) == "{_ 1: h'FF' }" +/// cbor.diagnostic(Some(42)) == "121([_ 42])" +/// cbor.diagnostic(None) == "122([])" /// ``` pub fn diagnostic(self: Data) -> String { aiken.diagnostic(self, #"") diff --git a/lib/aiken/collection.ak b/lib/aiken/collection.ak new file mode 100644 index 0000000..39a8b23 --- /dev/null +++ b/lib/aiken/collection.ak @@ -0,0 +1,4 @@ +/// An positive integer, that materializes the position of an element in a +/// collection. +pub type Index = + Int diff --git a/lib/aiken/dict.ak b/lib/aiken/collection/dict.ak similarity index 96% rename from lib/aiken/dict.ak rename to lib/aiken/collection/dict.ak index d280443..e435bec 100644 --- a/lib/aiken/dict.ak +++ b/lib/aiken/collection/dict.ak @@ -1,15 +1,16 @@ //// A module for working with bytearray dictionaries. //// -//// ### Important //// -//// Dictionaries are **ordered sets** of key-value pairs, which thus -//// preserve some invariants. Specifically, each key is only present once in -//// the dictionary and all keys are stored in ascending lexicographic order. -//// -//// These invariants allow for more optimized functions to operate on `Dict`, -//// but as a trade-offs, prevent `Dict` from being serializable. To recover a `Dict` -//// from an unknown `Data`, you must first recover an `Pairs` and use -//// `dict.from_ascending_list`. +//// > [!IMPORTANT] +//// > +//// > Dictionaries are **ordered sets** of key-value pairs, which thus +//// > preserve some invariants. Specifically, each key is only present once in +//// > the dictionary and all keys are stored in ascending lexicographic order. +//// > +//// > These invariants allow for more optimized functions to operate on `Dict`, +//// > but as a trade-offs, prevent `Dict` from being serializable. To recover a `Dict` +//// > from an unknown `Data`, you must first recover an `Pairs` and use +//// > [`dict.from_ascending_list`](#from_ascending_list). use aiken/builtin @@ -30,6 +31,8 @@ pub opaque type Dict { inner: Pairs, } +// ## Constructing + /// Create a new empty Dict /// ```aiken /// dict.to_pairs(dict.new()) == [] @@ -50,147 +53,179 @@ fn fixture_1() { |> insert(bar, 14) } -/// Remove a key-value pair from the dictionary. If the key is not found, no changes are made. +/// Like ['from_pairs'](#from_pairs), but from an already sorted list by ascending +/// keys. This function fails (i.e. halt the program execution) if the list isn't +/// sorted. /// /// ```aiken +/// let pairs = [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// /// let result = -/// dict.new() -/// |> dict.insert(key: "a", value: 100) -/// |> dict.insert(key: "b", value: 200) -/// |> dict.delete(key: "a") +/// dict.from_ascending_pairs(pairs) /// |> dict.to_pairs() /// -/// result == [Pair("b", 200)] +/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] /// ``` -pub fn delete(self: Dict, key: ByteArray) -> Dict { - Dict { inner: do_delete(self.inner, key) } +/// +/// This is meant to be used to turn a list constructed off-chain into a `Dict` +/// which has taken care of maintaining interval invariants. This function still +/// performs a sanity check on all keys to avoid silly mistakes. It is, however, +/// considerably faster than ['from_pairs'](from_pairs) +pub fn from_ascending_pairs(xs: Pairs) -> Dict { + let Void = check_ascending_list(xs) + Dict { inner: xs } } -fn do_delete( - self: Pairs, - key k: ByteArray, -) -> Pairs { - when self is { - [] -> - [] - [Pair(k2, v2), ..rest] -> - if builtin.less_than_equals_bytearray(k, k2) { - if k == k2 { - rest - } else { - self - } +fn check_ascending_list(xs: Pairs) { + when xs is { + [] -> Void + [_] -> Void + [Pair(x0, _), Pair(x1, _) as e, ..rest] -> + if builtin.less_than_bytearray(x0, x1) { + check_ascending_list([e, ..rest]) } else { - [Pair(k2, v2), ..do_delete(rest, k)] + fail @"keys in associative list aren't in ascending order" } } } -test delete_1() { - delete(new(), foo) == new() -} - -test delete_2() { - let m = - new() - |> insert(foo, 14) - delete(m, foo) == new() -} - -test delete_3() { - let m = - new() - |> insert(foo, 14) - delete(m, bar) == m -} - -test delete_4() { - let m = - new() - |> insert(foo, 14) - |> insert(bar, 14) - !has_key(delete(m, foo), foo) +/// Like [`from_ascending_pairs`](#from_ascending_pairs) but fails if **any** +/// value doesn't satisfy the predicate. +/// +/// ```aiken +/// let pairs = [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// +/// dict.from_ascending_pairs_with(pairs, fn(x) { x <= 250 }) // fail +/// ``` +pub fn from_ascending_pairs_with( + xs: Pairs, + predicate: fn(value) -> Bool, +) -> Dict { + let Void = check_ascending_pairs_with(xs, predicate) + Dict { inner: xs } } -test delete_5() { - let m = - new() - |> insert(foo, 14) - |> insert(bar, 14) - has_key(delete(m, bar), foo) +fn check_ascending_pairs_with( + xs: Pairs, + predicate: fn(value) -> Bool, +) { + when xs is { + [] -> Void + [Pair(_, v)] -> + if predicate(v) { + Void + } else { + fail @"value doesn't satisfy predicate" + } + [Pair(x0, v0), Pair(x1, _) as e, ..rest] -> + if builtin.less_than_bytearray(x0, x1) { + if predicate(v0) { + check_ascending_pairs_with([e, ..rest], predicate) + } else { + fail @"value doesn't satisfy predicate" + } + } else { + fail @"keys in pairs aren't in ascending order" + } + } } -test delete_6() { - let m = - new() - |> insert("aaa", 1) - |> insert("bbb", 2) - |> insert("ccc", 3) - |> insert("ddd", 4) - |> insert("eee", 5) - |> insert("fff", 6) - |> insert("ggg", 7) - |> insert("hhh", 8) - |> insert("iii", 9) - |> insert("jjj", 10) +test bench_from_ascending_pairs() { + let dict = + from_ascending_pairs( + [ + Pair("aaaa", 1), + Pair("aaab", 9), + Pair("aaba", 5), + Pair("aabb", 13), + Pair("abaa", 2), + Pair("abab", 10), + Pair("abba", 6), + Pair("abbb", 14), + Pair("baaa", 3), + Pair("baab", 11), + Pair("baba", 7), + Pair("babb", 15), + Pair("bbaa", 4), + Pair("bbab", 12), + Pair("bbba", 8), + Pair("bbbb", 16), + ], + ) - delete(m, "bcd") == m + size(dict) == 16 } -/// Keep only the key-value pairs that pass the given predicate. +/// Construct a dictionary from a list of key-value pairs. Note that when a key is present +/// multiple times, the first occurrence prevails. /// /// ```aiken +/// let pairs = [Pair("a", 100), Pair("c", 300), Pair("b", 200)] +/// /// let result = -/// dict.new() -/// |> dict.insert(key: "a", value: 100) -/// |> dict.insert(key: "b", value: 200) -/// |> dict.insert(key: "c", value: 300) -/// |> dict.filter(fn(k, _v) { k != "a" }) +/// dict.from_pairs(pairs) /// |> dict.to_pairs() /// -/// result == [Pair("b", 200), Pair("c", 300)] +/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] /// ``` -pub fn filter( - self: Dict, - with: fn(ByteArray, value) -> Bool, -) -> Dict { - Dict { inner: do_filter(self.inner, with) } +pub fn from_pairs(self: Pairs) -> Dict { + Dict { inner: do_from_pairs(self) } } -fn do_filter( - self: Pairs, - with: fn(ByteArray, value) -> Bool, -) -> Pairs { - when self is { +fn do_from_pairs(xs: Pairs) -> Pairs { + when xs is { [] -> [] - [Pair(k, v), ..rest] -> - if with(k, v) { - [Pair(k, v), ..do_filter(rest, with)] - } else { - do_filter(rest, with) - } + [Pair(k, v), ..rest] -> do_insert(do_from_pairs(rest), k, v) } } -test filter_1() { - filter(new(), fn(_, _) { True }) == new() +test from_list_1() { + from_pairs([]) == new() } -test filter_2() { - let expected = - new() - |> insert(foo, 42) - filter(fixture_1(), fn(_, v) { v > 14 }) == expected +test from_list_2() { + from_pairs([Pair(foo, 42), Pair(bar, 14)]) == from_pairs( + [Pair(bar, 14), Pair(foo, 42)], + ) } -test filter_3() { - let expected = - new() - |> insert(bar, 14) - filter(fixture_1(), fn(k, _) { k == bar }) == expected +test from_list_3() { + from_pairs([Pair(foo, 42), Pair(bar, 14)]) == fixture_1() +} + +test from_list_4() { + from_pairs([Pair(foo, 42), Pair(bar, 14), Pair(foo, 1337)]) == fixture_1() +} + +test bench_from_pairs() { + let dict = + from_pairs( + [ + Pair("bbba", 8), + Pair("bbab", 12), + Pair("aabb", 13), + Pair("aaab", 9), + Pair("bbbb", 16), + Pair("aaaa", 1), + Pair("aaba", 5), + Pair("abab", 10), + Pair("baba", 7), + Pair("baab", 11), + Pair("abaa", 2), + Pair("baaa", 3), + Pair("bbaa", 4), + Pair("babb", 15), + Pair("abbb", 14), + Pair("abba", 6), + ], + ) + + size(dict) == 16 } +// ## Inspecting + /// Finds a value in the dictionary, and returns the first key found to have that value. /// /// ```aiken @@ -249,394 +284,417 @@ test find_4() { ) == Some(baz) } -/// Fold over the key-value pairs in a dictionary. The fold direction follows keys -/// in ascending order and is done from right-to-left. +/// Get a value in the dict by its key. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert(key: "a", value: 100) -/// |> dict.insert(key: "b", value: 200) -/// |> dict.insert(key: "c", value: 300) -/// |> dict.foldr(0, fn(_k, v, r) { v + r }) +/// |> dict.insert(key: "a", value: "Aiken") +/// |> dict.get(key: "a") /// -/// result == 600 +/// result == Some("Aiken") /// ``` -pub fn foldr( - self: Dict, - zero: result, - with: fn(ByteArray, value, result) -> result, -) -> result { - do_foldr(self.inner, zero, with) +pub fn get(self: Dict, key: ByteArray) -> Option { + do_get(self.inner, key) } -fn do_foldr( - self: Pairs, - zero: result, - with: fn(ByteArray, value, result) -> result, -) -> result { +fn do_get(self: Pairs, key k: ByteArray) -> Option { when self is { - [] -> zero - [Pair(k, v), ..rest] -> with(k, v, do_foldr(rest, zero, with)) + [] -> None + [Pair(k2, v), ..rest] -> + if builtin.less_than_equals_bytearray(k, k2) { + if k == k2 { + Some(v) + } else { + None + } + } else { + do_get(rest, k) + } } } -test foldr_1() { - foldr(new(), 14, fn(_, _, _) { 42 }) == 14 +test get_1() { + get(new(), foo) == None } -test foldr_2() { - foldr(fixture_1(), zero: 0, with: fn(_, v, total) { v + total }) == 56 +test get_2() { + let m = + new() + |> insert(foo, "Aiken") + |> insert(bar, "awesome") + get(m, key: foo) == Some("Aiken") } -/// Fold over the key-value pairs in a dictionary. The fold direction follows keys -/// in ascending order and is done from left-to-right. +test get_3() { + let m = + new() + |> insert(foo, "Aiken") + |> insert(bar, "awesome") + get(m, key: baz) == None +} + +test get_4() { + let m = + new() + |> insert("aaa", "1") + |> insert("bbb", "2") + |> insert("ccc", "3") + |> insert("ddd", "4") + |> insert("eee", "5") + |> insert("fff", "6") + |> insert("ggg", "7") + |> insert("hhh", "8") + |> insert("iii", "9") + |> insert("jjj", "10") + + get(m, "bcd") == None +} + +test get_5() { + let m = + new() + |> insert("aaa", "1") + |> insert("bbb", "2") + |> insert("ccc", "3") + |> insert("ddd", "4") + |> insert("eee", "5") + |> insert("fff", "6") + |> insert("ggg", "7") + |> insert("hhh", "8") + |> insert("iii", "9") + |> insert("jjj", "10") + + get(m, "kkk") == None +} + +/// Check if a key exists in the dictionary. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert(key: "a", value: 100) -/// |> dict.insert(key: "b", value: 200) -/// |> dict.insert(key: "c", value: 300) -/// |> dict.foldl(0, fn(_k, v, r) { v + r }) +/// |> dict.insert(key: "a", value: "Aiken") +/// |> dict.has_key("a") /// -/// result == 600 +/// result == True /// ``` -pub fn foldl( - self: Dict, - zero: result, - with: fn(ByteArray, value, result) -> result, -) -> result { - do_foldl(self.inner, zero, with) +pub fn has_key(self: Dict, key k: ByteArray) -> Bool { + do_has_key(self.inner, k) } -fn do_foldl( - self: Pairs, - zero: result, - with: fn(ByteArray, value, result) -> result, -) -> result { +fn do_has_key(self: Pairs, key k: ByteArray) -> Bool { when self is { - [] -> zero - [Pair(k, v), ..rest] -> do_foldl(rest, with(k, v, zero), with) + [] -> False + [Pair(k2, _), ..rest] -> + if builtin.less_than_equals_bytearray(k, k2) { + k == k2 + } else { + do_has_key(rest, k) + } } } -test fold_1() { - foldl(new(), 14, fn(_, _, _) { 42 }) == 14 +test has_key_1() { + !has_key(new(), foo) } -test fold_2() { - foldl(fixture_1(), zero: 0, with: fn(_, v, total) { v + total }) == 56 +test has_key_2() { + has_key( + new() + |> insert(foo, 14), + foo, + ) } -/// Construct a dictionary from a list of key-value pairs. Note that when a key is present -/// multiple times, the first occurrence prevails. -/// +test has_key_3() { + !has_key( + new() + |> insert(foo, 14), + bar, + ) +} + +test has_key_4() { + has_key( + new() + |> insert(foo, 14) + |> insert(bar, 42), + bar, + ) +} + +/// Efficiently checks whether a dictionary is empty. /// ```aiken -/// let pairs = [Pair("a", 100), Pair("c", 300), Pair("b", 200)] +/// dict.is_empty(dict.new()) == True +/// ``` +pub fn is_empty(self: Dict) -> Bool { + when self.inner is { + [] -> True + _ -> False + } +} + +test is_empty_1() { + is_empty(new()) +} + +/// Extract all the keys present in a given `Dict`. /// +/// ```aiken /// let result = -/// dict.from_pairs(pairs) -/// |> dict.to_pairs() +/// dict.new() +/// |> dict.insert("a", 14) +/// |> dict.insert("b", 42) +/// |> dict.insert("a", 1337) +/// |> dict.keys() /// -/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// result == ["a", "b"] /// ``` -pub fn from_pairs(self: Pairs) -> Dict { - Dict { inner: do_from_pairs(self) } +pub fn keys(self: Dict) -> List { + do_keys(self.inner) } -fn do_from_pairs(xs: Pairs) -> Pairs { - when xs is { +fn do_keys(self: Pairs) -> List { + when self is { [] -> [] - [Pair(k, v), ..rest] -> do_insert(do_from_pairs(rest), k, v) + [Pair(k, _), ..rest] -> + [k, ..do_keys(rest)] } } -test from_list_1() { - from_pairs([]) == new() -} - -test from_list_2() { - from_pairs([Pair(foo, 42), Pair(bar, 14)]) == from_pairs( - [Pair(bar, 14), Pair(foo, 42)], - ) -} - -test from_list_3() { - from_pairs([Pair(foo, 42), Pair(bar, 14)]) == fixture_1() -} - -test from_list_4() { - from_pairs([Pair(foo, 42), Pair(bar, 14), Pair(foo, 1337)]) == fixture_1() +test keys_1() { + keys(new()) == [] } -test bench_from_pairs() { - let dict = - from_pairs( - [ - Pair("bbba", 8), - Pair("bbab", 12), - Pair("aabb", 13), - Pair("aaab", 9), - Pair("bbbb", 16), - Pair("aaaa", 1), - Pair("aaba", 5), - Pair("abab", 10), - Pair("baba", 7), - Pair("baab", 11), - Pair("abaa", 2), - Pair("baaa", 3), - Pair("bbaa", 4), - Pair("babb", 15), - Pair("abbb", 14), - Pair("abba", 6), - ], - ) - - size(dict) == 16 +test keys_2() { + keys( + new() + |> insert(foo, 0) + |> insert(bar, 0), + ) == [bar, foo] } -/// Like ['from_list'](from_list), but from an already sorted list by ascending -/// keys. This function fails (i.e. halt the program execution) if the list isn't -/// sorted. +/// Return the number of key-value pairs in the dictionary. /// /// ```aiken -/// let pairs = [Pair("a", 100), Pair("b", 200), Pair("c", 300)] -/// /// let result = -/// dict.from_ascending_pairs(pairs) -/// |> dict.to_pairs() +/// dict.new() +/// |> dict.insert("a", 100) +/// |> dict.insert("b", 200) +/// |> dict.insert("c", 300) +/// |> dict.size() /// -/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// result == 3 /// ``` -/// -/// This is meant to be used to turn a list constructed off-chain into a `Dict` -/// which has taken care of maintaining interval invariants. This function still -/// performs a sanity check on all keys to avoid silly mistakes. It is, however, -/// considerably faster than ['from_list'](from_list) -pub fn from_ascending_pairs(xs: Pairs) -> Dict { - let Void = check_ascending_list(xs) - Dict { inner: xs } +pub fn size(self: Dict) -> Int { + do_size(self.inner) } -fn check_ascending_list(xs: Pairs) { - when xs is { - [] -> Void - [_] -> Void - [Pair(x0, _), Pair(x1, _) as e, ..rest] -> - if builtin.less_than_bytearray(x0, x1) { - check_ascending_list([e, ..rest]) - } else { - fail @"keys in associative list aren't in ascending order" - } +fn do_size(self: Pairs) -> Int { + when self is { + [] -> 0 + [_, ..rest] -> 1 + do_size(rest) } } -/// Like [`from_ascending_pairs`](#from_ascending_list) but fails if **any** -/// value doesn't satisfy the predicate. +test size_1() { + size(new()) == 0 +} + +test size_2() { + size( + new() + |> insert(foo, 14), + ) == 1 +} + +test size_3() { + size( + new() + |> insert(foo, 14) + |> insert(bar, 42), + ) == 2 +} + +/// Extract all the values present in a given `Dict`. /// /// ```aiken -/// let pairs = [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// let result = +/// dict.new() +/// |> dict.insert("a", 14) +/// |> dict.insert("b", 42) +/// |> dict.insert("c", 1337) +/// |> dict.values() /// -/// dict.from_ascending_pairs_with(pairs, fn(x) { x <= 250 }) // fail +/// result == [1337, 42] /// ``` -pub fn from_ascending_pairs_with( - xs: Pairs, - predicate: fn(value) -> Bool, -) -> Dict { - let Void = check_ascending_pairs_with(xs, predicate) - Dict { inner: xs } +pub fn values(self: Dict) -> List { + do_values(self.inner) } -fn check_ascending_pairs_with( - xs: Pairs, - predicate: fn(value) -> Bool, -) { - when xs is { - [] -> Void - [Pair(_, v)] -> - if predicate(v) { - Void - } else { - fail @"value doesn't satisfy predicate" - } - [Pair(x0, v0), Pair(x1, _) as e, ..rest] -> - if builtin.less_than_bytearray(x0, x1) { - if predicate(v0) { - check_ascending_pairs_with([e, ..rest], predicate) - } else { - fail @"value doesn't satisfy predicate" - } - } else { - fail @"keys in pairs aren't in ascending order" - } +fn do_values(self: Pairs) -> List { + when self is { + [] -> + [] + [Pair(_, v), ..rest] -> + [v, ..do_values(rest)] } } -test bench_from_ascending_pairs() { - let dict = - from_ascending_pairs( - [ - Pair("aaaa", 1), - Pair("aaab", 9), - Pair("aaba", 5), - Pair("aabb", 13), - Pair("abaa", 2), - Pair("abab", 10), - Pair("abba", 6), - Pair("abbb", 14), - Pair("baaa", 3), - Pair("baab", 11), - Pair("baba", 7), - Pair("babb", 15), - Pair("bbaa", 4), - Pair("bbab", 12), - Pair("bbba", 8), - Pair("bbbb", 16), - ], - ) +test values_1() { + values(new()) == [] +} - size(dict) == 16 +test values_2() { + values( + new() + |> insert(foo, 3) + |> insert(bar, 4), + ) == [4, 3] } -/// Get a value in the dict by its key. +// ## Modifying + +/// Remove a key-value pair from the dictionary. If the key is not found, no changes are made. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert(key: "a", value: "Aiken") -/// |> dict.get(key: "a") +/// |> dict.insert(key: "a", value: 100) +/// |> dict.insert(key: "b", value: 200) +/// |> dict.delete(key: "a") +/// |> dict.to_pairs() /// -/// result == Some("Aiken") +/// result == [Pair("b", 200)] /// ``` -pub fn get(self: Dict, key: ByteArray) -> Option { - do_get(self.inner, key) +pub fn delete(self: Dict, key: ByteArray) -> Dict { + Dict { inner: do_delete(self.inner, key) } } -fn do_get(self: Pairs, key k: ByteArray) -> Option { +fn do_delete( + self: Pairs, + key k: ByteArray, +) -> Pairs { when self is { - [] -> None - [Pair(k2, v), ..rest] -> + [] -> + [] + [Pair(k2, v2), ..rest] -> if builtin.less_than_equals_bytearray(k, k2) { if k == k2 { - Some(v) + rest } else { - None + self } } else { - do_get(rest, k) + [Pair(k2, v2), ..do_delete(rest, k)] } } } -test get_1() { - get(new(), foo) == None +test delete_1() { + delete(new(), foo) == new() } -test get_2() { +test delete_2() { let m = new() - |> insert(foo, "Aiken") - |> insert(bar, "awesome") - get(m, key: foo) == Some("Aiken") + |> insert(foo, 14) + delete(m, foo) == new() } -test get_3() { +test delete_3() { let m = new() - |> insert(foo, "Aiken") - |> insert(bar, "awesome") - get(m, key: baz) == None + |> insert(foo, 14) + delete(m, bar) == m } -test get_4() { +test delete_4() { let m = new() - |> insert("aaa", "1") - |> insert("bbb", "2") - |> insert("ccc", "3") - |> insert("ddd", "4") - |> insert("eee", "5") - |> insert("fff", "6") - |> insert("ggg", "7") - |> insert("hhh", "8") - |> insert("iii", "9") - |> insert("jjj", "10") + |> insert(foo, 14) + |> insert(bar, 14) + !has_key(delete(m, foo), foo) +} - get(m, "bcd") == None +test delete_5() { + let m = + new() + |> insert(foo, 14) + |> insert(bar, 14) + has_key(delete(m, bar), foo) } -test get_5() { +test delete_6() { let m = new() - |> insert("aaa", "1") - |> insert("bbb", "2") - |> insert("ccc", "3") - |> insert("ddd", "4") - |> insert("eee", "5") - |> insert("fff", "6") - |> insert("ggg", "7") - |> insert("hhh", "8") - |> insert("iii", "9") - |> insert("jjj", "10") + |> insert("aaa", 1) + |> insert("bbb", 2) + |> insert("ccc", 3) + |> insert("ddd", 4) + |> insert("eee", 5) + |> insert("fff", 6) + |> insert("ggg", 7) + |> insert("hhh", 8) + |> insert("iii", 9) + |> insert("jjj", 10) - get(m, "kkk") == None + delete(m, "bcd") == m } -/// Check if a key exists in the dictionary. +/// Keep only the key-value pairs that pass the given predicate. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert(key: "a", value: "Aiken") -/// |> dict.has_key("a") +/// |> dict.insert(key: "a", value: 100) +/// |> dict.insert(key: "b", value: 200) +/// |> dict.insert(key: "c", value: 300) +/// |> dict.filter(fn(k, _v) { k != "a" }) +/// |> dict.to_pairs() /// -/// result == True +/// result == [Pair("b", 200), Pair("c", 300)] /// ``` -pub fn has_key(self: Dict, key k: ByteArray) -> Bool { - do_has_key(self.inner, k) +pub fn filter( + self: Dict, + with: fn(ByteArray, value) -> Bool, +) -> Dict { + Dict { inner: do_filter(self.inner, with) } } -fn do_has_key(self: Pairs, key k: ByteArray) -> Bool { +fn do_filter( + self: Pairs, + with: fn(ByteArray, value) -> Bool, +) -> Pairs { when self is { - [] -> False - [Pair(k2, _), ..rest] -> - if builtin.less_than_equals_bytearray(k, k2) { - k == k2 + [] -> + [] + [Pair(k, v), ..rest] -> + if with(k, v) { + [Pair(k, v), ..do_filter(rest, with)] } else { - do_has_key(rest, k) - } - } -} - -test has_key_1() { - !has_key(new(), foo) + do_filter(rest, with) + } + } } -test has_key_2() { - has_key( - new() - |> insert(foo, 14), - foo, - ) +test filter_1() { + filter(new(), fn(_, _) { True }) == new() } -test has_key_3() { - !has_key( +test filter_2() { + let expected = new() - |> insert(foo, 14), - bar, - ) + |> insert(foo, 42) + filter(fixture_1(), fn(_, v) { v > 14 }) == expected } -test has_key_4() { - has_key( +test filter_3() { + let expected = new() - |> insert(foo, 14) - |> insert(bar, 42), - bar, - ) + |> insert(bar, 14) + filter(fixture_1(), fn(k, _) { k == bar }) == expected } /// Insert a value in the dictionary at a given key. If the key already exists, its value is **overridden**. If you need ways to combine keys together, use (`insert_with`)[#insert_with]. @@ -776,58 +834,6 @@ test insert_with_3() { result == [Pair("foo", 1)] } -/// Efficiently checks whether a dictionary is empty. -/// ```aiken -/// dict.is_empty(dict.new()) == True -/// ``` -pub fn is_empty(self: Dict) -> Bool { - when self.inner is { - [] -> True - _ -> False - } -} - -test is_empty_1() { - is_empty(new()) -} - -/// Extract all the keys present in a given `Dict`. -/// -/// ```aiken -/// let result = -/// dict.new() -/// |> dict.insert("a", 14) -/// |> dict.insert("b", 42) -/// |> dict.insert("a", 1337) -/// |> dict.keys() -/// -/// result == ["a", "b"] -/// ``` -pub fn keys(self: Dict) -> List { - do_keys(self.inner) -} - -fn do_keys(self: Pairs) -> List { - when self is { - [] -> - [] - [Pair(k, _), ..rest] -> - [k, ..do_keys(rest)] - } -} - -test keys_1() { - keys(new()) == [] -} - -test keys_2() { - keys( - new() - |> insert(foo, 0) - |> insert(bar, 0), - ) == [bar, foo] -} - /// Apply a function to all key-value pairs in a Dict. /// /// ```aiken @@ -871,71 +877,7 @@ test map_2() { get(result, foo) == Some(43) && size(result) == size(fixture_1()) } -/// Get the inner list holding the dictionary data. -/// -/// ```aiken -/// let result = -/// dict.new() -/// |> dict.insert("a", 100) -/// |> dict.insert("b", 200) -/// |> dict.insert("c", 300) -/// |> dict.to_pairs() -/// -/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] -/// ``` -pub fn to_pairs(self: Dict) -> Pairs { - self.inner -} - -test to_list_1() { - to_pairs(new()) == [] -} - -test to_list_2() { - to_pairs(fixture_1()) == [Pair(bar, 14), Pair(foo, 42)] -} - -/// Return the number of key-value pairs in the dictionary. -/// -/// ```aiken -/// let result = -/// dict.new() -/// |> dict.insert("a", 100) -/// |> dict.insert("b", 200) -/// |> dict.insert("c", 300) -/// |> dict.size() -/// -/// result == 3 -/// ``` -pub fn size(self: Dict) -> Int { - do_size(self.inner) -} - -fn do_size(self: Pairs) -> Int { - when self is { - [] -> 0 - [_, ..rest] -> 1 + do_size(rest) - } -} - -test size_1() { - size(new()) == 0 -} - -test size_2() { - size( - new() - |> insert(foo, 14), - ) == 1 -} - -test size_3() { - size( - new() - |> insert(foo, 14) - |> insert(bar, 42), - ) == 2 -} +// ## Combining /// Combine two dictionaries. If the same key exist in both the left and /// right dictionary, values from the left are preferred (i.e. left-biaised). @@ -1079,39 +1021,108 @@ test union_with_1() { result == from_pairs([Pair(foo, 1351), Pair(bar, 42)]) } -/// Extract all the values present in a given `Dict`. +// ## Transforming + +/// Fold over the key-value pairs in a dictionary. The fold direction follows keys +/// in ascending order and is done from left-to-right. /// /// ```aiken /// let result = /// dict.new() -/// |> dict.insert("a", 14) -/// |> dict.insert("b", 42) -/// |> dict.insert("c", 1337) -/// |> dict.values() +/// |> dict.insert(key: "a", value: 100) +/// |> dict.insert(key: "b", value: 200) +/// |> dict.insert(key: "c", value: 300) +/// |> dict.foldl(0, fn(_k, v, r) { v + r }) /// -/// result == [1337, 42] +/// result == 600 /// ``` -pub fn values(self: Dict) -> List { - do_values(self.inner) +pub fn foldl( + self: Dict, + zero: result, + with: fn(ByteArray, value, result) -> result, +) -> result { + do_foldl(self.inner, zero, with) } -fn do_values(self: Pairs) -> List { +fn do_foldl( + self: Pairs, + zero: result, + with: fn(ByteArray, value, result) -> result, +) -> result { when self is { - [] -> - [] - [Pair(_, v), ..rest] -> - [v, ..do_values(rest)] + [] -> zero + [Pair(k, v), ..rest] -> do_foldl(rest, with(k, v, zero), with) } } -test values_1() { - values(new()) == [] +test fold_1() { + foldl(new(), 14, fn(_, _, _) { 42 }) == 14 } -test values_2() { - values( - new() - |> insert(foo, 3) - |> insert(bar, 4), - ) == [4, 3] +test fold_2() { + foldl(fixture_1(), zero: 0, with: fn(_, v, total) { v + total }) == 56 +} + +/// Fold over the key-value pairs in a dictionary. The fold direction follows keys +/// in ascending order and is done from right-to-left. +/// +/// ```aiken +/// let result = +/// dict.new() +/// |> dict.insert(key: "a", value: 100) +/// |> dict.insert(key: "b", value: 200) +/// |> dict.insert(key: "c", value: 300) +/// |> dict.foldr(0, fn(_k, v, r) { v + r }) +/// +/// result == 600 +/// ``` +pub fn foldr( + self: Dict, + zero: result, + with: fn(ByteArray, value, result) -> result, +) -> result { + do_foldr(self.inner, zero, with) +} + +fn do_foldr( + self: Pairs, + zero: result, + with: fn(ByteArray, value, result) -> result, +) -> result { + when self is { + [] -> zero + [Pair(k, v), ..rest] -> with(k, v, do_foldr(rest, zero, with)) + } +} + +test foldr_1() { + foldr(new(), 14, fn(_, _, _) { 42 }) == 14 +} + +test foldr_2() { + foldr(fixture_1(), zero: 0, with: fn(_, v, total) { v + total }) == 56 +} + +/// Get the inner list holding the dictionary data. +/// +/// ```aiken +/// let result = +/// dict.new() +/// |> dict.insert("a", 100) +/// |> dict.insert("b", 200) +/// |> dict.insert("c", 300) +/// |> dict.to_pairs() +/// +/// result == [Pair("a", 100), Pair("b", 200), Pair("c", 300)] +/// ``` +pub fn to_pairs(self: Dict) -> Pairs { + self.inner +} + +test to_list_1() { + to_pairs(new()) == [] +} + +test to_list_2() { + to_pairs(fixture_1()) == [Pair(bar, 14), Pair(foo, 42)] } diff --git a/lib/aiken/list.ak b/lib/aiken/collection/list.ak similarity index 99% rename from lib/aiken/list.ak rename to lib/aiken/collection/list.ak index b8bb4b8..4fb1319 100644 --- a/lib/aiken/list.ak +++ b/lib/aiken/collection/list.ak @@ -1,6 +1,71 @@ use aiken/builtin -use aiken/bytearray -use aiken/int +use aiken/primitive/bytearray +use aiken/primitive/int + +// ## Constructing + +/// Add an element in front of the list. Sometimes useful when combined with +/// other functions. +/// +/// ```aiken +/// list.push([2, 3], 1) == [1, ..[2, 3]] == [1, 2, 3] +/// ``` +pub fn push(self: List, elem: a) -> List { + [elem, ..self] +} + +test push_1() { + push([], 0) == [0] +} + +test push_2() { + push([2, 3], 1) == [1, 2, 3] +} + +/// Construct a list of a integer from a given range. +/// +/// ```aiken +/// list.range(0, 3) == [0, 1, 2, 3] +/// list.range(-1, 1) == [-1, 0, 1] +/// ``` +pub fn range(from: Int, to: Int) -> List { + if from > to { + [] + } else { + [from, ..range(from + 1, to)] + } +} + +test range_1() { + range(0, 3) == [0, 1, 2, 3] +} + +test range_2() { + range(-1, 1) == [-1, 0, 1] +} + +/// Construct a list filled with n copies of a value. +/// +/// ```aiken +/// list.repeat("na", 3) == ["na", "na", "na"] +/// ``` +pub fn repeat(elem: a, n_times: Int) -> List { + if n_times <= 0 { + [] + } else { + [elem, ..repeat(elem, n_times - 1)] + } +} + +test repeat_1() { + repeat(42, 0) == [] +} + +test repeat_2() { + repeat(14, 3) == [14, 14, 14] +} + +// ## Inspecting /// Determine if all elements of the list satisfy the given predicate. /// @@ -59,6 +124,44 @@ test any_3() { any([], fn(n) { n == 42 }) == False } +/// Return Some(item) at the index or None if the index is out of range. The index is 0-based. +/// +/// ```aiken +/// list.at([1, 2, 3], 1) == Some(2) +/// list.at([1, 2, 3], 42) == None +/// ``` +pub fn at(self: List, index: Int) -> Option { + when self is { + [] -> None + [x, ..xs] -> + if index == 0 { + Some(x) + } else { + at(xs, index - 1) + } + } +} + +test at_1() { + at([1, 2, 3], -1) == None +} + +test at_2() { + at([], 0) == None +} + +test at_3() { + at([1, 2, 3], 3) == None +} + +test at_4() { + at([1], 0) == Some(1) +} + +test at_5() { + at([1, 2, 3], 2) == Some(3) +} + /// Count how many items in the list satisfy the given predicate. /// /// ```aiken @@ -97,134 +200,233 @@ test count_none() { count([1, 2, 3], fn(a) { a > 5 }) == 0 } -/// Return Some(item) at the index or None if the index is out of range. The index is 0-based. +/// Find the first element satisfying the given predicate, if any. /// /// ```aiken -/// list.at([1, 2, 3], 1) == Some(2) -/// list.at([1, 2, 3], 42) == None +/// list.find([1, 2, 3], fn(x) { x == 2 }) == Some(2) +/// list.find([4, 5, 6], fn(x) { x == 2 }) == None /// ``` -pub fn at(self: List, index: Int) -> Option { +pub fn find(self: List, predicate: fn(a) -> Bool) -> Option { when self is { [] -> None [x, ..xs] -> - if index == 0 { + if predicate(x) { Some(x) } else { - at(xs, index - 1) + find(xs, predicate) } } } -test at_1() { - at([1, 2, 3], -1) == None -} - -test at_2() { - at([], 0) == None -} - -test at_3() { - at([1, 2, 3], 3) == None +test find_1() { + find([1, 2, 3], fn(x) { x == 1 }) == Some(1) } -test at_4() { - at([1], 0) == Some(1) +test find_2() { + find([1, 2, 3], fn(x) { x > 42 }) == None } -test at_5() { - at([1, 2, 3], 2) == Some(3) +test find_3() { + find([], fn(_) { True }) == None } -/// Merge two lists together. +/// Figures out whether a list contain the given element. /// /// ```aiken -/// list.concat([], []) == [] -/// list.concat([], [1, 2, 3]) == [1, 2, 3] -/// list.concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6] +/// list.has([1, 2, 3], 2) == True +/// list.has([1, 2, 3], 14) == False +/// list.has([], 14) == False /// ``` -pub fn concat(left: List, right: List) -> List { - when left is { - [] -> right +pub fn has(self: List, elem: a) -> Bool { + when self is { + [] -> False [x, ..xs] -> - [x, ..concat(xs, right)] + if x == elem { + True + } else { + has(xs, elem) + } } } -test concat_1() { - concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6] +test has_1() { + has([1, 2, 3], 1) == True } -test concat_2() { - concat([1, 2, 3], []) == [1, 2, 3] +test has_2() { + has([1, 2, 3], 14) == False } -test concat_3() { - concat([], [1, 2, 3]) == [1, 2, 3] +test has_3() { + has([], 14) == False } -/// Remove the first occurrence of the given element from the list. +/// Get the first element of a list /// /// ```aiken -/// list.delete([1, 2, 3, 1], 1) == [2, 3, 1] -/// list.delete([1, 2, 3], 14) == [1, 2, 3] +/// list.head([1, 2, 3]) == Some(1) +/// list.head([]) == None /// ``` -pub fn delete(self: List, elem: a) -> List { +pub fn head(self: List) -> Option { when self is { - [] -> - [] - [x, ..xs] -> - if x == elem { - xs - } else { - [x, ..delete(xs, elem)] - } + [] -> None + _ -> Some(builtin.head_list(self)) } } -test delete_1() { - delete([], 42) == [] +test head_1() { + head([1, 2, 3]) == Some(1) } -test delete_2() { - delete([1, 2, 3, 1], 1) == [2, 3, 1] +test head_2() { + head([]) == None } -test delete_3() { - delete([1, 2, 3], 14) == [1, 2, 3] +/// Checks whether a list is empty. +/// +/// ```aiken +/// list.is_empty([]) == True +/// list.is_empty([1, 2, 3]) == False +/// ``` +pub fn is_empty(self: List) -> Bool { + when self is { + [] -> True + _ -> False + } } -test delete_4() { - delete([2], 2) == [] +test is_empty_1() { + is_empty([]) == True } -/// Remove the first occurrence of each element of the second list from the first one. +test is_empty_2() { + is_empty([1, 2, 3]) == False +} + +/// Gets the index of an element of a list, if any. Otherwise, returns None. /// +/// ```aiken +/// list.index_of([1, 5, 2], 2) == Some(2) +/// list.index_of([1, 7, 3], 4) == None +/// list.index_of([1, 0, 9, 6], 6) == 3 +/// list.index_of([], 6) == None /// ``` -/// list.difference(["h", "e", "l", "l", "o"], ["l", "e", "l"]) == ["h", "o"] -/// list.difference([1, 2, 3, 4, 5], [1, 1, 2]) == [3, 4, 5] -/// list.difference([1, 2, 3], []) == [1, 2, 3] -/// ``` -pub fn difference(self: List, with: List) -> List { - when with is { - [] -> self - [x, ..xs] -> difference(delete(self, x), xs) +pub fn index_of(self: List, elem: a) -> Option { + do_index_of(self, elem, 0) +} + +fn do_index_of(self: List, elem: a, i: Int) -> Option { + when self is { + [] -> None + [x, ..xs] -> + if x == elem { + Some(i) + } else { + do_index_of(xs, elem, i + 1) + } } } -test difference_1() { - difference(["h", "e", "l", "l", "o"], ["l", "e", "l"]) == ["h", "o"] +test index_of_1() { + index_of([1, 5, 2], 2) == Some(2) } -test difference_2() { - difference([1, 2, 3, 4, 5], [1, 1, 2]) == [3, 4, 5] +test index_of_2() { + index_of([1, 7, 3], 4) == None } -test difference_3() { - difference([1, 2, 3], []) == [1, 2, 3] +test index_of_3() { + index_of([1, 0, 9, 6], 6) == Some(3) } -test difference_4() { - difference([], [1, 2, 3]) == [] +test index_of_4() { + index_of([], 6) == None +} + +/// Get the last in the given list, if any. +/// +/// ```aiken +/// list.last([]) == None +/// list.last([1, 2, 3]) == Some(3) +/// ``` +pub fn last(self: List) -> Option { + when self is { + [] -> None + [x] -> Some(x) + [_, ..xs] -> last(xs) + } +} + +test last_1() { + last([]) == None +} + +test last_2() { + last([1]) == Some(1) +} + +test last_3() { + last([1, 2, 3, 4]) == Some(4) +} + +/// Get the number of elements in the given list. +/// +/// ```aiken +/// list.length([]) == 0 +/// list.length([1, 2, 3]) == 3 +/// ``` +pub fn length(self: List) -> Int { + when self is { + [] -> 0 + [_, ..xs] -> 1 + length(xs) + } +} + +test length_1() { + length([]) == 0 +} + +test length_2() { + length([1, 2, 3]) == 3 +} + +// ## Modifying + +// ### Extracting + +/// Remove the first occurrence of the given element from the list. +/// +/// ```aiken +/// list.delete([1, 2, 3, 1], 1) == [2, 3, 1] +/// list.delete([1, 2, 3], 14) == [1, 2, 3] +/// ``` +pub fn delete(self: List, elem: a) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + if x == elem { + xs + } else { + [x, ..delete(xs, elem)] + } + } +} + +test delete_1() { + delete([], 42) == [] +} + +test delete_2() { + delete([1, 2, 3, 1], 1) == [2, 3, 1] +} + +test delete_3() { + delete([1, 2, 3], 14) == [1, 2, 3] +} + +test delete_4() { + delete([2], 2) == [] } /// Drop the first `n` elements of a list. @@ -384,113 +586,6 @@ test filter_map_2() { ) == [3, 9, 15] } -/// Find the first element satisfying the given predicate, if any. -/// -/// ```aiken -/// list.find([1, 2, 3], fn(x) { x == 2 }) == Some(2) -/// list.find([4, 5, 6], fn(x) { x == 2 }) == None -/// ``` -pub fn find(self: List, predicate: fn(a) -> Bool) -> Option { - when self is { - [] -> None - [x, ..xs] -> - if predicate(x) { - Some(x) - } else { - find(xs, predicate) - } - } -} - -test find_1() { - find([1, 2, 3], fn(x) { x == 1 }) == Some(1) -} - -test find_2() { - find([1, 2, 3], fn(x) { x > 42 }) == None -} - -test find_3() { - find([], fn(_) { True }) == None -} - -/// Map elements of a list into a new list and flatten the result. -/// -/// ```aiken -/// list.flat_map([1, 2, 3], fn(a) { [a, 2*a] }) == [1, 2, 2, 4, 3, 6] -/// ``` -pub fn flat_map(self: List, with: fn(a) -> List) -> List { - foldr(self, [], fn(x, xs) { concat(with(x), xs) }) -} - -test flat_map_1() { - flat_map([], fn(a) { [a] }) == [] -} - -test flat_map_2() { - flat_map([1, 2, 3], fn(a) { [a, a] }) == [1, 1, 2, 2, 3, 3] -} - -/// Reduce a list from left to right. -/// -/// ```aiken -/// list.foldl([1, 2, 3], 0, fn(n, total) { n + total }) == 6 -/// list.foldl([1, 2, 3], [], fn(x, xs) { [x, ..xs] }) == [3, 2, 1] -/// ``` -pub fn foldl(self: List, zero: b, with: fn(a, b) -> b) -> b { - when self is { - [] -> zero - [x, ..xs] -> foldl(xs, with(x, zero), with) - } -} - -test foldl_1() { - foldl([], 0, fn(_, _) { 1 }) == 0 -} - -test foldl_2() { - foldl([1, 2, 3, 4, 5], 0, fn(n, total) { n + total }) == 15 -} - -test foldl_3() { - foldl([1, 2, 3, 4], [], fn(x, xs) { [x, ..xs] }) == [4, 3, 2, 1] -} - -/// Reduce a list from right to left. -/// -/// ```aiken -/// list.foldr([1, 2, 3], 0, fn(n, total) { n + total }) == 6 -/// list.foldr([1, 2, 3], [], fn(x, xs) { [x, ..xs] }) == [1, 2, 3] -/// ``` -pub fn foldr(self: List, zero: b, with: fn(a, b) -> b) -> b { - when self is { - [] -> zero - [x, ..xs] -> with(x, foldr(xs, zero, with)) - } -} - -test foldr_1() { - foldr([1, 2, 3, 4, 5], 0, fn(n, total) { n + total }) == 15 -} - -test foldr_2() { - foldr( - [1, 2, 3], - "", - fn(n, _str) { - if builtin.mod_integer(n, 2) == 0 { - "foo" - } else { - "bar" - } - }, - ) == "bar" -} - -test foldr_3() { - foldr([1, 2, 3, 4], [], fn(x, xs) { [x, ..xs] }) == [1, 2, 3, 4] -} - /// Return all elements except the last one. /// /// ```aiken @@ -526,469 +621,50 @@ test init_3() { init([1, 2, 3, 4]) == Some([1, 2, 3]) } -/// Figures out whether a list contain the given element. +/// Returns a tuple with all elements that satisfy the predicate at first +/// element, and the rest as second element. /// /// ```aiken -/// list.has([1, 2, 3], 2) == True -/// list.has([1, 2, 3], 14) == False -/// list.has([], 14) == False +/// list.partition([1, 2, 3, 4], fn(x) { x % 2 == 0 }) == ([2, 4], [1, 3]) /// ``` -pub fn has(self: List, elem: a) -> Bool { +pub fn partition(self: List, predicate: fn(a) -> Bool) -> (List, List) { when self is { - [] -> False - [x, ..xs] -> - if x == elem { - True + [] -> ([], []) + [x, ..xs] -> { + let (left, right) = partition(xs, predicate) + if predicate(x) { + ([x, ..left], right) } else { - has(xs, elem) + (left, [x, ..right]) } + } } } -test has_1() { - has([1, 2, 3], 1) == True +test partition_1() { + partition([], fn(x) { x > 2 }) == ([], []) } -test has_2() { - has([1, 2, 3], 14) == False +test partition_2() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + partition(xs, fn(x) { x > 5 }) == ([10, 9, 8, 7, 6], [5, 4, 3, 2, 1]) } -test has_3() { - has([], 14) == False +test partition_3() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + partition(xs, fn(x) { x == 42 }) == ([], xs) } -/// Gets the index of an element of a list, if any. Otherwise, returns None. -/// -/// ```aiken -/// list.index_of([1, 5, 2], 2) == Some(2) -/// list.index_of([1, 7, 3], 4) == None -/// list.index_of([1, 0, 9, 6], 6) == 3 -/// list.index_of([], 6) == None -/// ``` -pub fn index_of(self: List, elem: a) -> Option { - do_index_of(self, elem, 0) +test partition_4() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + partition(xs, fn(x) { x < 42 }) == (xs, []) } -fn do_index_of(self: List, elem: a, i: Int) -> Option { - when self is { - [] -> None - [x, ..xs] -> - if x == elem { - Some(i) - } else { - do_index_of(xs, elem, i + 1) - } - } -} - -test index_of_1() { - index_of([1, 5, 2], 2) == Some(2) -} - -test index_of_2() { - index_of([1, 7, 3], 4) == None -} - -test index_of_3() { - index_of([1, 0, 9, 6], 6) == Some(3) -} - -test index_of_4() { - index_of([], 6) == None -} - -/// Get the first element of a list -/// -/// ```aiken -/// list.head([1, 2, 3]) == Some(1) -/// list.head([]) == None -/// ``` -pub fn head(self: List) -> Option { - when self is { - [] -> None - _ -> Some(builtin.head_list(self)) - } -} - -test head_1() { - head([1, 2, 3]) == Some(1) -} - -test head_2() { - head([]) == None -} - -/// Like [`foldr`](#foldr), but also provides the position (0-based) of the elements when iterating. -/// -/// ```aiken -/// let group = fn(i, x, xs) { [(i, x), ..xs] } -/// list.indexed_foldr(["a", "b", "c"], [], group) == [ -/// (0, "a"), -/// (1, "b"), -/// (2, "c") -/// ] -/// ``` -pub fn indexed_foldr( - self: List, - zero: result, - with: fn(Int, a, result) -> result, -) -> result { - do_indexed_foldr(0, self, zero, with) -} - -fn do_indexed_foldr( - n: Int, - self: List, - zero: result, - with: fn(Int, a, result) -> result, -) -> result { - when self is { - [] -> zero - [x, ..xs] -> with(n, x, do_indexed_foldr(n + 1, xs, zero, with)) - } -} - -test indexed_foldr_1() { - indexed_foldr([], 0, fn(i, x, xs) { i + x + xs }) == 0 -} - -test indexed_foldr_2() { - let letters = - ["a", "b", "c"] - indexed_foldr(letters, [], fn(i, x, xs) { [(i, x), ..xs] }) == [ - (0, "a"), - (1, "b"), - (2, "c"), - ] -} - -/// List [`map`](#map) but provides the position (0-based) of the elements while iterating. -/// -/// ```aiken -/// list.indexed_map([1, 2, 3], fn(i, x) { i + x }) == [1, 3, 5] -/// ``` -pub fn indexed_map(self: List, with: fn(Int, a) -> result) -> List { - do_indexed_map(0, self, with) -} - -fn do_indexed_map( - n: Int, - self: List, - with: fn(Int, a) -> result, -) -> List { - when self is { - [] -> - [] - [x, ..xs] -> - [with(n, x), ..do_indexed_map(n + 1, xs, with)] - } -} - -test indexed_map_1() { - indexed_map([], fn(i, _n) { i }) == [] -} - -test indexed_map_2() { - indexed_map( - [4, 8, 13, 2], - fn(i, n) { - if n == 8 { - n - } else { - i - } - }, - ) == [0, 8, 2, 3] -} - -/// Checks whether a list is empty. -/// -/// ```aiken -/// list.is_empty([]) == True -/// list.is_empty([1, 2, 3]) == False -/// ``` -pub fn is_empty(self: List) -> Bool { - when self is { - [] -> True - _ -> False - } -} - -test is_empty_1() { - is_empty([]) == True -} - -test is_empty_2() { - is_empty([1, 2, 3]) == False -} - -/// Get the last in the given list, if any. -/// -/// ```aiken -/// list.last([]) == None -/// list.last([1, 2, 3]) == Some(3) -/// ``` -pub fn last(self: List) -> Option { - when self is { - [] -> None - [x] -> Some(x) - [_, ..xs] -> last(xs) - } -} - -test last_1() { - last([]) == None -} - -test last_2() { - last([1]) == Some(1) -} - -test last_3() { - last([1, 2, 3, 4]) == Some(4) -} - -/// Get the number of elements in the given list. -/// -/// ```aiken -/// list.length([]) == 0 -/// list.length([1, 2, 3]) == 3 -/// ``` -pub fn length(self: List) -> Int { - when self is { - [] -> 0 - [_, ..xs] -> 1 + length(xs) - } -} - -test length_1() { - length([]) == 0 -} - -test length_2() { - length([1, 2, 3]) == 3 -} - -/// Apply a function to each element of a list. -/// -/// ```aiken -/// list.map([1, 2, 3, 4], fn(n) { n + 1 }) == [2, 3, 4, 5] -/// ``` -pub fn map(self: List, with: fn(a) -> result) -> List { - when self is { - [] -> - [] - [x, ..xs] -> - [with(x), ..map(xs, with)] - } -} - -test map_1() { - map([], fn(n) { n + 1 }) == [] -} - -test map_2() { - map([1, 2, 3, 4], fn(n) { n + 1 }) == [2, 3, 4, 5] -} - -/// Apply a function of two arguments, combining elements from two lists. -/// -/// Note: if one list is longer, the extra elements are dropped. -/// -/// ```aiken -/// list.map2([1, 2, 3], [1, 2], fn(a, b) { a + b }) == [2, 4] -/// ``` -pub fn map2( - self: List, - bs: List, - with: fn(a, b) -> result, -) -> List { - when self is { - [] -> - [] - [x, ..xs] -> - when bs is { - [] -> - [] - [y, ..ys] -> - [with(x, y), ..map2(xs, ys, with)] - } - } -} - -test map2_1() { - map2([], [1, 2, 3], fn(a, b) { a + b }) == [] -} - -test map2_2() { - map2([1, 2, 3], [1, 2], fn(a, b) { a + b }) == [2, 4] -} - -test map2_3() { - map2([42], [1, 2, 3], fn(_a, b) { Some(b) }) == [Some(1)] -} - -/// Apply a function of three arguments, combining elements from three lists. -/// -/// Note: if one list is longer, the extra elements are dropped. -/// -/// ```aiken -/// list.map3([1, 2, 3], [1, 2], [1, 2, 3], fn(a, b, c) { a + b + c }) == [3, 6] -/// ``` -pub fn map3( - self: List, - bs: List, - cs: List, - with: fn(a, b, c) -> result, -) -> List { - when self is { - [] -> - [] - [x, ..xs] -> - when bs is { - [] -> - [] - [y, ..ys] -> - when cs is { - [] -> - [] - [z, ..zs] -> - [with(x, y, z), ..map3(xs, ys, zs, with)] - } - } - } -} - -test map3_1() { - map3([], [], [1, 2, 3], fn(a, b, c) { a + b + c }) == [] -} - -test map3_2() { - map3([1, 2, 3], [1, 2], [1, 2, 3], fn(a, b, c) { a + b + c }) == [3, 6] -} - -/// Add an element in front of the list. Sometimes useful when combined with -/// other functions. -/// -/// ```aiken -/// list.push([2, 3], 1) == [1, ..[2, 3]] == [1, 2, 3] -/// ``` -pub fn push(self: List, elem: a) -> List { - [elem, ..self] -} - -test push_1() { - push([], 0) == [0] -} - -test push_2() { - push([2, 3], 1) == [1, 2, 3] -} - -/// Construct a list of a integer from a given range. -/// -/// ```aiken -/// list.range(0, 3) == [0, 1, 2, 3] -/// list.range(-1, 1) == [-1, 0, 1] -/// ``` -pub fn range(from: Int, to: Int) -> List { - if from > to { - [] - } else { - [from, ..range(from + 1, to)] - } -} - -test range_1() { - range(0, 3) == [0, 1, 2, 3] -} - -test range_2() { - range(-1, 1) == [-1, 0, 1] -} - -/// Construct a list filled with n copies of a value. -/// -/// ```aiken -/// list.repeat("na", 3) == ["na", "na", "na"] -/// ``` -pub fn repeat(elem: a, n_times: Int) -> List { - if n_times <= 0 { - [] - } else { - [elem, ..repeat(elem, n_times - 1)] - } -} - -test repeat_1() { - repeat(42, 0) == [] -} - -test repeat_2() { - repeat(14, 3) == [14, 14, 14] -} - -/// Return the list with its elements in the reserve order. -/// -/// ```aiken -/// list.reverse([1, 2, 3]) == [3, 2, 1] -/// ``` -pub fn reverse(self: List) -> List { - foldl(self, [], fn(x, xs) { [x, ..xs] }) -} - -test reverse_1() { - reverse([]) == [] -} - -test reverse_2() { - reverse([1, 2, 3]) == [3, 2, 1] -} - -/// Returns a tuple with all elements that satisfy the predicate at first -/// element, and the rest as second element. -/// -/// ```aiken -/// list.partition([1, 2, 3, 4], fn(x) { x % 2 == 0 }) == ([2, 4], [1, 3]) -/// ``` -pub fn partition(self: List, predicate: fn(a) -> Bool) -> (List, List) { - when self is { - [] -> ([], []) - [x, ..xs] -> { - let (left, right) = partition(xs, predicate) - if predicate(x) { - ([x, ..left], right) - } else { - (left, [x, ..right]) - } - } - } -} - -test partition_1() { - partition([], fn(x) { x > 2 }) == ([], []) -} - -test partition_2() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - partition(xs, fn(x) { x > 5 }) == ([10, 9, 8, 7, 6], [5, 4, 3, 2, 1]) -} - -test partition_3() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - partition(xs, fn(x) { x == 42 }) == ([], xs) -} - -test partition_4() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - partition(xs, fn(x) { x < 42 }) == (xs, []) -} - -test partition_5() { - partition([1, 2, 3, 4], fn(x) { x % 2 == 0 }) == ([2, 4], [1, 3]) +test partition_5() { + partition([1, 2, 3, 4], fn(x) { x % 2 == 0 }) == ([2, 4], [1, 3]) } /// Extract a sublist from the given list using 0-based indexes. Negative @@ -999,7 +675,7 @@ test partition_5() { /// list.slice([1, 2, 3, 4, 5, 6], from: -2, to: -1) == [5, 6] /// list.slice([1, 2, 3, 4, 5, 6], from: 1, to: -1) == [2, 3, 4, 5, 6] /// ``` -pub fn slice(self: List, from: Int, to: Int) { +pub fn slice(self: List, from: Int, to: Int) -> List { let (i, l) = if from >= 0 { (from, None) @@ -1047,57 +723,6 @@ test slice_6() { slice([1, 2, 3, 4, 5, 6], from: -2, to: 1) == [] } -/// Sort a list in ascending order using the given comparison function. -/// -/// ```aiken -/// use aiken/int -/// -/// sort([3, 1, 4, 0, 2], int.compare) == [0, 1, 2, 3, 4] -/// sort([1, 2, 3], int.compare) == [1, 2, 3] -/// ``` -pub fn sort(self: List, compare: fn(a, a) -> Ordering) -> List { - when self is { - [] -> - [] - [x, ..xs] -> insert(sort(xs, compare), x, compare) - } -} - -fn insert(self: List, e: a, compare: fn(a, a) -> Ordering) -> List { - when self is { - [] -> - [e] - [x, ..xs] -> - if compare(e, x) == Less { - [e, ..self] - } else { - [x, ..insert(xs, e, compare)] - } - } -} - -test sort_1() { - let xs = - [6, 7, 5, 4, 1, 3, 9, 8, 0, 2] - sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -} - -test sort_2() { - let xs = - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -} - -test sort_3() { - let xs = - [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] - sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] -} - -test sort_4() { - sort([], int.compare) == [] -} - /// Cut a list in two, such that the first list contains the given number of / /// elements and the second list contains the rest. /// @@ -1195,58 +820,282 @@ pub fn take_while(self: List, predicate: fn(a) -> Bool) -> List { [] -> [] [x, ..xs] -> - if predicate(x) { - [x, ..take_while(xs, predicate)] - } else { - [] + if predicate(x) { + [x, ..take_while(xs, predicate)] + } else { + [] + } + } +} + +test take_while_1() { + take_while([], fn(x) { x > 2 }) == [] +} + +test take_while_2() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + take_while(xs, fn(x) { x > 5 }) == [10, 9, 8, 7, 6] +} + +test take_while_3() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + take_while(xs, fn(x) { x == 42 }) == [] +} + +test take_while_4() { + let xs = + [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] + take_while(xs, fn(x) { x < 42 }) == xs +} + +/// Removes duplicate elements from a list. +/// +/// ```aiken +/// list.unique([1, 2, 3, 1]) == [1, 2, 3] +/// ``` +pub fn unique(self: List) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + [x, ..unique(filter(xs, fn(y) { y != x }))] + } +} + +test unique_1() { + unique([]) == [] +} + +test unique_2() { + let xs = + [1, 2, 3, 1, 1, 3, 4, 1, 2, 3, 2, 4, 5, 6, 7, 8, 9, 10, 9] + unique(xs) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +} + +// ### Mapping + +/// Map elements of a list into a new list and flatten the result. +/// +/// ```aiken +/// list.flat_map([1, 2, 3], fn(a) { [a, 2*a] }) == [1, 2, 2, 4, 3, 6] +/// ``` +pub fn flat_map(self: List, with: fn(a) -> List) -> List { + foldr(self, [], fn(x, xs) { concat(with(x), xs) }) +} + +test flat_map_1() { + flat_map([], fn(a) { [a] }) == [] +} + +test flat_map_2() { + flat_map([1, 2, 3], fn(a) { [a, a] }) == [1, 1, 2, 2, 3, 3] +} + +/// List [`map`](#map) but provides the position (0-based) of the elements while iterating. +/// +/// ```aiken +/// list.indexed_map([1, 2, 3], fn(i, x) { i + x }) == [1, 3, 5] +/// ``` +pub fn indexed_map(self: List, with: fn(Int, a) -> result) -> List { + do_indexed_map(0, self, with) +} + +fn do_indexed_map( + n: Int, + self: List, + with: fn(Int, a) -> result, +) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + [with(n, x), ..do_indexed_map(n + 1, xs, with)] + } +} + +test indexed_map_1() { + indexed_map([], fn(i, _n) { i }) == [] +} + +test indexed_map_2() { + indexed_map( + [4, 8, 13, 2], + fn(i, n) { + if n == 8 { + n + } else { + i + } + }, + ) == [0, 8, 2, 3] +} + +/// Apply a function to each element of a list. +/// +/// ```aiken +/// list.map([1, 2, 3, 4], fn(n) { n + 1 }) == [2, 3, 4, 5] +/// ``` +pub fn map(self: List, with: fn(a) -> result) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + [with(x), ..map(xs, with)] + } +} + +test map_1() { + map([], fn(n) { n + 1 }) == [] +} + +test map_2() { + map([1, 2, 3, 4], fn(n) { n + 1 }) == [2, 3, 4, 5] +} + +/// Apply a function of two arguments, combining elements from two lists. +/// +/// Note: if one list is longer, the extra elements are dropped. +/// +/// ```aiken +/// list.map2([1, 2, 3], [1, 2], fn(a, b) { a + b }) == [2, 4] +/// ``` +pub fn map2( + self: List, + bs: List, + with: fn(a, b) -> result, +) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + when bs is { + [] -> + [] + [y, ..ys] -> + [with(x, y), ..map2(xs, ys, with)] + } + } +} + +test map2_1() { + map2([], [1, 2, 3], fn(a, b) { a + b }) == [] +} + +test map2_2() { + map2([1, 2, 3], [1, 2], fn(a, b) { a + b }) == [2, 4] +} + +test map2_3() { + map2([42], [1, 2, 3], fn(_a, b) { Some(b) }) == [Some(1)] +} + +/// Apply a function of three arguments, combining elements from three lists. +/// +/// Note: if one list is longer, the extra elements are dropped. +/// +/// ```aiken +/// list.map3([1, 2, 3], [1, 2], [1, 2, 3], fn(a, b, c) { a + b + c }) == [3, 6] +/// ``` +pub fn map3( + self: List, + bs: List, + cs: List, + with: fn(a, b, c) -> result, +) -> List { + when self is { + [] -> + [] + [x, ..xs] -> + when bs is { + [] -> + [] + [y, ..ys] -> + when cs is { + [] -> + [] + [z, ..zs] -> + [with(x, y, z), ..map3(xs, ys, zs, with)] + } } } } -test take_while_1() { - take_while([], fn(x) { x > 2 }) == [] +test map3_1() { + map3([], [], [1, 2, 3], fn(a, b, c) { a + b + c }) == [] } -test take_while_2() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - take_while(xs, fn(x) { x > 5 }) == [10, 9, 8, 7, 6] +test map3_2() { + map3([1, 2, 3], [1, 2], [1, 2, 3], fn(a, b, c) { a + b + c }) == [3, 6] } -test take_while_3() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - take_while(xs, fn(x) { x == 42 }) == [] +/// Return the list with its elements in the reserve order. +/// +/// ```aiken +/// list.reverse([1, 2, 3]) == [3, 2, 1] +/// ``` +pub fn reverse(self: List) -> List { + foldl(self, [], fn(x, xs) { [x, ..xs] }) } -test take_while_4() { - let xs = - [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] - take_while(xs, fn(x) { x < 42 }) == xs +test reverse_1() { + reverse([]) == [] } -/// Removes duplicate elements from a list. +test reverse_2() { + reverse([1, 2, 3]) == [3, 2, 1] +} + +/// Sort a list in ascending order using the given comparison function. /// /// ```aiken -/// list.unique([1, 2, 3, 1]) == [1, 2, 3] +/// use aiken/int +/// +/// sort([3, 1, 4, 0, 2], int.compare) == [0, 1, 2, 3, 4] +/// sort([1, 2, 3], int.compare) == [1, 2, 3] /// ``` -pub fn unique(self: List) -> List { +pub fn sort(self: List, compare: fn(a, a) -> Ordering) -> List { when self is { [] -> [] + [x, ..xs] -> insert(sort(xs, compare), x, compare) + } +} + +fn insert(self: List, e: a, compare: fn(a, a) -> Ordering) -> List { + when self is { + [] -> + [e] [x, ..xs] -> - [x, ..unique(filter(xs, fn(y) { y != x }))] + if compare(e, x) == Less { + [e, ..self] + } else { + [x, ..insert(xs, e, compare)] + } } } -test unique_1() { - unique([]) == [] +test sort_1() { + let xs = + [6, 7, 5, 4, 1, 3, 9, 8, 0, 2] + sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] } -test unique_2() { +test sort_2() { let xs = - [1, 2, 3, 1, 1, 3, 4, 1, 2, 3, 2, 4, 5, 6, 7, 8, 9, 10, 9] - unique(xs) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +} + +test sort_3() { + let xs = + [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + sort(xs, int.compare) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +} + +test sort_4() { + sort([], int.compare) == [] } /// Decompose a list of tuples into a tuple of lists. @@ -1272,6 +1121,65 @@ test unzip_2() { unzip([(1, "a"), (2, "b")]) == ([1, 2], ["a", "b"]) } +// ## Combining + +/// Merge two lists together. +/// +/// ```aiken +/// list.concat([], []) == [] +/// list.concat([], [1, 2, 3]) == [1, 2, 3] +/// list.concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6] +/// ``` +pub fn concat(left: List, right: List) -> List { + when left is { + [] -> right + [x, ..xs] -> + [x, ..concat(xs, right)] + } +} + +test concat_1() { + concat([1, 2, 3], [4, 5, 6]) == [1, 2, 3, 4, 5, 6] +} + +test concat_2() { + concat([1, 2, 3], []) == [1, 2, 3] +} + +test concat_3() { + concat([], [1, 2, 3]) == [1, 2, 3] +} + +/// Remove the first occurrence of each element of the second list from the first one. +/// +/// ``` +/// list.difference(["h", "e", "l", "l", "o"], ["l", "e", "l"]) == ["h", "o"] +/// list.difference([1, 2, 3, 4, 5], [1, 1, 2]) == [3, 4, 5] +/// list.difference([1, 2, 3], []) == [1, 2, 3] +/// ``` +pub fn difference(self: List, with: List) -> List { + when with is { + [] -> self + [x, ..xs] -> difference(delete(self, x), xs) + } +} + +test difference_1() { + difference(["h", "e", "l", "l", "o"], ["l", "e", "l"]) == ["h", "o"] +} + +test difference_2() { + difference([1, 2, 3, 4, 5], [1, 1, 2]) == [3, 4, 5] +} + +test difference_3() { + difference([1, 2, 3], []) == [1, 2, 3] +} + +test difference_4() { + difference([], [1, 2, 3]) == [] +} + /// Combine two lists together. /// /// Note: if one list is longer, the extra elements are dropped. @@ -1305,6 +1213,112 @@ test zip_3() { zip([1, 2], ["a", "b", "c"]) == [(1, "a"), (2, "b")] } +// ## Transforming + +/// Reduce a list from left to right. +/// +/// ```aiken +/// list.foldl([1, 2, 3], 0, fn(n, total) { n + total }) == 6 +/// list.foldl([1, 2, 3], [], fn(x, xs) { [x, ..xs] }) == [3, 2, 1] +/// ``` +pub fn foldl(self: List, zero: b, with: fn(a, b) -> b) -> b { + when self is { + [] -> zero + [x, ..xs] -> foldl(xs, with(x, zero), with) + } +} + +test foldl_1() { + foldl([], 0, fn(_, _) { 1 }) == 0 +} + +test foldl_2() { + foldl([1, 2, 3, 4, 5], 0, fn(n, total) { n + total }) == 15 +} + +test foldl_3() { + foldl([1, 2, 3, 4], [], fn(x, xs) { [x, ..xs] }) == [4, 3, 2, 1] +} + +/// Reduce a list from right to left. +/// +/// ```aiken +/// list.foldr([1, 2, 3], 0, fn(n, total) { n + total }) == 6 +/// list.foldr([1, 2, 3], [], fn(x, xs) { [x, ..xs] }) == [1, 2, 3] +/// ``` +pub fn foldr(self: List, zero: b, with: fn(a, b) -> b) -> b { + when self is { + [] -> zero + [x, ..xs] -> with(x, foldr(xs, zero, with)) + } +} + +test foldr_1() { + foldr([1, 2, 3, 4, 5], 0, fn(n, total) { n + total }) == 15 +} + +test foldr_2() { + foldr( + [1, 2, 3], + "", + fn(n, _str) { + if builtin.mod_integer(n, 2) == 0 { + "foo" + } else { + "bar" + } + }, + ) == "bar" +} + +test foldr_3() { + foldr([1, 2, 3, 4], [], fn(x, xs) { [x, ..xs] }) == [1, 2, 3, 4] +} + +/// Like [`foldr`](#foldr), but also provides the position (0-based) of the elements when iterating. +/// +/// ```aiken +/// let group = fn(i, x, xs) { [(i, x), ..xs] } +/// list.indexed_foldr(["a", "b", "c"], [], group) == [ +/// (0, "a"), +/// (1, "b"), +/// (2, "c") +/// ] +/// ``` +pub fn indexed_foldr( + self: List, + zero: result, + with: fn(Int, a, result) -> result, +) -> result { + do_indexed_foldr(0, self, zero, with) +} + +fn do_indexed_foldr( + n: Int, + self: List, + zero: result, + with: fn(Int, a, result) -> result, +) -> result { + when self is { + [] -> zero + [x, ..xs] -> with(n, x, do_indexed_foldr(n + 1, xs, zero, with)) + } +} + +test indexed_foldr_1() { + indexed_foldr([], 0, fn(i, x, xs) { i + x + xs }) == 0 +} + +test indexed_foldr_2() { + let letters = + ["a", "b", "c"] + indexed_foldr(letters, [], fn(i, x, xs) { [(i, x), ..xs] }) == [ + (0, "a"), + (1, "b"), + (2, "c"), + ] +} + /// Reduce a list from left to right using the accumulator as left operand. /// Said differently, this is [`foldl`](#foldl) with callback arguments swapped. /// diff --git a/lib/aiken/pairs.ak b/lib/aiken/collection/pairs.ak similarity index 82% rename from lib/aiken/pairs.ak rename to lib/aiken/collection/pairs.ak index dd67f56..f4ab58b 100644 --- a/lib/aiken/pairs.ak +++ b/lib/aiken/collection/pairs.ak @@ -4,142 +4,177 @@ //// that are specifically tailored to working with associative lists. Fundamentally, a `Pairs` is //// a type-alias to `List>`. //// -//// ### Important -//// -//// Unlike dictionnaries (a.k.a. `Dict`), associative lists make no assumption -//// about the ordering of elements within the list. As a result, lookup -//// functions do traverse the entire list when invoked. They are also not _sets_, -//// and thus allow for duplicate keys. This is reflected in the functions used -//// to interact with them. +//// > [!CAUTION] +//// > +//// > Unlike dictionnaries (a.k.a. [`Dict`](./dict.html#Dict), associative lists make no assumption +//// > about the ordering of elements within the list. As a result, lookup +//// > functions do traverse the entire list when invoked. They are also not _sets_, +//// > and thus allow for duplicate keys. This is reflected in the functions used +//// > to interact with them. -/// Remove a single key-value pair from the `Pairs`. If the key is not found, no changes are made. -/// Duplicate keys are not removed. Only the **first** key found is removed. +// ## Inspecting + +/// Get all values in the alist associated with a given key. /// /// ```aiken -/// pairs.remove_first([], "a") == [] -/// pairs.remove_first([Pair("a", 1)], "a") == [] -/// pairs.remove_first([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] -/// pairs.remove_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("b", 2), Pair("a", 3)] +/// pairs.get_all([], "a") == [] +/// pairs.get_all([Pair("a", 1)], "a") == [1] +/// pairs.get_all([Pair("a", 1), Pair("b", 2)], "a") == [1] +/// pairs.get_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [1, 3] /// ``` -pub fn remove_first(self: Pairs, key k: key) -> Pairs { +pub fn get_all(self: Pairs, key k: key) -> List { when self is { [] -> [] - [Pair(k2, v2), ..rest] -> + [Pair(k2, v), ..rest] -> if k == k2 { - rest + [v, ..get_all(rest, k)] } else { - [Pair(k2, v2), ..remove_first(rest, k)] + get_all(rest, k) } } } -test remove_first_1() { - remove_first([], "a") == [] +test get_all_1() { + get_all([], "a") == [] +} + +test get_all_2() { + get_all([Pair("a", 1)], "a") == [1] } -test remove_first_2() { - remove_first([Pair("a", 14)], "a") == [] +test get_all_3() { + get_all([Pair("a", 1), Pair("b", 2)], "a") == [1] } -test remove_first_3() { - let fixture = - [Pair("a", 14)] - remove_first(fixture, "b") == fixture +test get_all_4() { + get_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [1, 3] } -test remove_first_4() { - let fixture = - [Pair("a", 1), Pair("b", 2), Pair("a", 3)] - remove_first(fixture, "a") == [Pair("b", 2), Pair("a", 3)] +test get_all_5() { + get_all([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == [] } -/// Remove a single key-value pair from the Pairs. If the key is not found, no changes are made. -/// Duplicate keys are not removed. Only the **last** key found is removed. +/// Get the value in the alist by its key. +/// If multiple values with the same key exist, only the first one is returned. /// /// ```aiken -/// pairs.remove_last([], "a") == [] -/// pairs.remove_last([Pair("a", 1)], "a") == [] -/// pairs.remove_last([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] -/// pairs.remove_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("a", 1), Pair("b", 2)] +/// pairs.get_first([], "a") == None +/// pairs.get_first([Pair("a", 1)], "a") == Some(1) +/// pairs.get_first([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +/// pairs.get_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(1) /// ``` -pub fn remove_last(self: Pairs, key k: key) -> Pairs { +pub fn get_first(self: Pairs, key k: key) -> Option { when self is { - [] -> - [] - [Pair(k2, v2), ..rest] -> + [] -> None + [Pair(k2, v), ..rest] -> if k == k2 { - let tail = remove_last(rest, k) - if tail == rest { - rest - } else { - [Pair(k2, v2), ..tail] + Some(v) + } else { + get_first(rest, k) + } + } +} + +test get_first_1() { + get_first([], "a") == None +} + +test get_first_2() { + get_first([Pair("a", 1)], "a") == Some(1) +} + +test get_first_3() { + get_first([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +} + +test get_first_4() { + get_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(1) +} + +test get_first_5() { + get_first([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == None +} + +/// Get the value in the alist by its key. +/// If multiple values with the same key exist, only the last one is returned. +/// +/// ```aiken +/// pairs.get_last([], "a") == None +/// pairs.get_last([Pair("a", 1)], "a") == Some(1) +/// pairs.get_last([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +/// pairs.get_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(3) +/// ``` +pub fn get_last(self: Pairs, key k: key) -> Option { + when self is { + [] -> None + [Pair(k2, v), ..rest] -> + if k == k2 { + when get_last(rest, k) is { + None -> Some(v) + some -> some } } else { - [Pair(k2, v2), ..remove_last(rest, k)] + get_last(rest, k) } } } -test remove_last_1() { - remove_last([], "a") == [] +test get_last_1() { + get_last([], "a") == None } -test remove_last_2() { - remove_last([Pair("a", 14)], "a") == [] +test get_last_2() { + get_last([Pair("a", 1)], "a") == Some(1) } -test remove_last_3() { - let fixture = - [Pair("a", 14)] - remove_last(fixture, "b") == fixture +test get_last_3() { + get_last([Pair("a", 1), Pair("b", 2)], "a") == Some(1) } -test remove_last_4() { - let fixture = - [Pair("a", 1), Pair("b", 2), Pair("a", 3)] - remove_last(fixture, "a") == [Pair("a", 1), Pair("b", 2)] +test get_last_4() { + get_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(3) } -/// Remove all key-value pairs matching the key from the Pairs. If the key is not found, no changes are made. +test get_last_5() { + get_last([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == None +} + +/// Finds all keys in the alist associated with a given value. /// /// ```aiken -/// pairs.remove_all([], "a") == [] -/// pairs.remove_all([Pair("a", 1)], "a") == [] -/// pairs.remove_all([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] -/// pairs.remove_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("b", 2)] +/// pairs.find_all([], 1) == [] +/// pairs.find_all([Pair("a", 1)], 1) == ["a"] +/// pairs.find_all([Pair("a", 1), Pair("b", 2)], 1) == ["a"] +/// pairs.find_all([Pair("a", 1), Pair("b", 2), Pair("c", 1)], 1) == ["a", "c"] /// ``` -pub fn remove_all(self: Pairs, key k: key) -> Pairs { +pub fn find_all(self: Pairs, v: value) -> List { when self is { [] -> [] [Pair(k2, v2), ..rest] -> - if k == k2 { - remove_all(rest, k) + if v == v2 { + [k2, ..find_all(rest, v)] } else { - [Pair(k2, v2), ..remove_all(rest, k)] + find_all(rest, v) } } } -test remove_all_1() { - remove_all([], "a") == [] +test find_all_1() { + find_all([], "a") == [] } -test remove_all_2() { - remove_all([Pair("a", 14)], "a") == [] +test find_all_2() { + find_all([Pair("a", 14)], 14) == ["a"] } -test remove_all_3() { - let fixture = - [Pair("a", 14)] - remove_all(fixture, "b") == fixture +test find_all_3() { + find_all([Pair("a", 14)], 42) == [] } -test remove_all_4() { - let fixture = - [Pair("a", 1), Pair("b", 2), Pair("a", 3)] - remove_all(fixture, "a") == [Pair("b", 2)] +test find_all_4() { + find_all([Pair("a", 14), Pair("b", 42), Pair("c", 14)], 14) == ["a", "c"] } /// Finds the first key in the alist associated with a given value, if any. @@ -217,309 +252,234 @@ test find_last_4() { find_last([Pair("a", 14), Pair("b", 42), Pair("c", 14)], 14) == Some("c") } -/// Finds all keys in the alist associated with a given value. +/// Check if a key exists in the pairs. /// /// ```aiken -/// pairs.find_all([], 1) == [] -/// pairs.find_all([Pair("a", 1)], 1) == ["a"] -/// pairs.find_all([Pair("a", 1), Pair("b", 2)], 1) == ["a"] -/// pairs.find_all([Pair("a", 1), Pair("b", 2), Pair("c", 1)], 1) == ["a", "c"] +/// pairs.has_key([], "a") == False +/// pairs.has_key([Pair("a", 1)], "a") == True +/// pairs.has_key([Pair("a", 1), Pair("b", 2)], "a") == True +/// pairs.has_key([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == True /// ``` -pub fn find_all(self: Pairs, v: value) -> List { +pub fn has_key(self: Pairs, k: key) -> Bool { when self is { - [] -> - [] - [Pair(k2, v2), ..rest] -> - if v == v2 { - [k2, ..find_all(rest, v)] - } else { - find_all(rest, v) - } + [] -> False + // || is lazy so this is fine + [Pair(k2, _), ..rest] -> k == k2 || has_key(rest, k) } } -test find_all_1() { - find_all([], "a") == [] -} - -test find_all_2() { - find_all([Pair("a", 14)], 14) == ["a"] -} - -test find_all_3() { - find_all([Pair("a", 14)], 42) == [] -} - -test find_all_4() { - find_all([Pair("a", 14), Pair("b", 42), Pair("c", 14)], 14) == ["a", "c"] +test has_key_1() { + !has_key([], "a") } -/// Fold over the key-value pairs in a Pairs. The fold direction follows the -/// order of elements in the Pairs and is done from right-to-left. -/// -/// ```aiken -/// let fixture = [ -/// Pair(1, 100), -/// Pair(2, 200), -/// Pair(3, 300), -/// ] -/// -/// pairs.foldr(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 -/// ``` -pub fn foldr( - self: Pairs, - zero: result, - with: fn(key, value, result) -> result, -) -> result { - when self is { - [] -> zero - [Pair(k, v), ..rest] -> with(k, v, foldr(rest, zero, with)) - } +test has_key_2() { + has_key([Pair("a", 14)], "a") } -test foldr_1() { - foldr([], 14, fn(_, _, _) { 42 }) == 14 +test has_key_3() { + !has_key([Pair("a", 14)], "b") } -test foldr_2() { - foldr( - [Pair("a", 42), Pair("b", 14)], - zero: 0, - with: fn(_, v, total) { v + total }, - ) == 56 +test has_key_4() { + has_key([Pair("a", 14), Pair("b", 42)], "b") } -test foldr_3() { - let fixture = - [Pair(1, 100), Pair(2, 200), Pair(3, 300)] - - foldr(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 +test has_key_5() { + has_key([Pair("a", 14), Pair("b", 42), Pair("a", 42)], "a") } -/// Fold over the key-value pairs in a pairs. The fold direction follows keys -/// in ascending order and is done from left-to-right. +/// Extract all the keys present in a given `Pairs`. /// /// ```aiken -/// let fixture = [ -/// Pair(1, 100), -/// Pair(2, 200), -/// Pair(3, 300), -/// ] -/// -/// pairs.foldl(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 +/// pairs.keys([]) == [] +/// pairs.keys([Pair("a", 1)]) == ["a"] +/// pairs.keys([Pair("a", 1), Pair("b", 2)]) == ["a", "b"] +/// pairs.keys([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == ["a", "b", "a"] /// ``` -pub fn foldl( - self: Pairs, - zero: result, - with: fn(key, value, result) -> result, -) -> result { +pub fn keys(self: Pairs) -> List { when self is { - [] -> zero - [Pair(k, v), ..rest] -> foldl(rest, with(k, v, zero), with) + [] -> + [] + [Pair(k, _), ..rest] -> + [k, ..keys(rest)] } } -test foldl_1() { - foldl([], 14, fn(_, _, _) { 42 }) == 14 +test keys_1() { + keys([]) == [] } -test foldl_2() { - foldl( - [Pair("a", 42), Pair("b", 14)], - zero: 0, - with: fn(_, v, total) { v + total }, - ) == 56 +test keys_2() { + keys([Pair("a", 0)]) == ["a"] } -/// Get the value in the alist by its key. -/// If multiple values with the same key exist, only the first one is returned. +test keys_3() { + keys([Pair("a", 0), Pair("b", 0)]) == ["a", "b"] +} + +/// Extract all the values present in a given `Pairs`. /// /// ```aiken -/// pairs.get_first([], "a") == None -/// pairs.get_first([Pair("a", 1)], "a") == Some(1) -/// pairs.get_first([Pair("a", 1), Pair("b", 2)], "a") == Some(1) -/// pairs.get_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(1) +/// pairs.values([]) == [] +/// pairs.values([Pair("a", 1)]) == [1] +/// pairs.values([Pair("a", 1), Pair("b", 2)]) == [1, 2] +/// pairs.values([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == [1, 2, 3] /// ``` -pub fn get_first(self: Pairs, key k: key) -> Option { +pub fn values(self: Pairs) -> List { when self is { - [] -> None - [Pair(k2, v), ..rest] -> - if k == k2 { - Some(v) - } else { - get_first(rest, k) - } + [] -> + [] + [Pair(_, v), ..rest] -> + [v, ..values(rest)] } } -test get_first_1() { - get_first([], "a") == None +test values_1() { + values([]) == [] } -test get_first_2() { - get_first([Pair("a", 1)], "a") == Some(1) +test values_2() { + values([Pair("a", 1)]) == [1] } -test get_first_3() { - get_first([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +test values_3() { + values([Pair("a", 1), Pair("b", 2)]) == [1, 2] } -test get_first_4() { - get_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(1) +test values_4() { + values([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == [1, 2, 3] } -test get_first_5() { - get_first([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == None -} +// ## Modifying -/// Get the value in the alist by its key. -/// If multiple values with the same key exist, only the last one is returned. +/// Remove all key-value pairs matching the key from the Pairs. If the key is not found, no changes are made. /// /// ```aiken -/// pairs.get_last([], "a") == None -/// pairs.get_last([Pair("a", 1)], "a") == Some(1) -/// pairs.get_last([Pair("a", 1), Pair("b", 2)], "a") == Some(1) -/// pairs.get_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(3) +/// pairs.delete_all([], "a") == [] +/// pairs.delete_all([Pair("a", 1)], "a") == [] +/// pairs.delete_all([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] +/// pairs.delete_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("b", 2)] /// ``` -pub fn get_last(self: Pairs, key k: key) -> Option { +pub fn delete_all(self: Pairs, key k: key) -> Pairs { when self is { - [] -> None - [Pair(k2, v), ..rest] -> + [] -> + [] + [Pair(k2, v2), ..rest] -> if k == k2 { - when get_last(rest, k) is { - None -> Some(v) - some -> some - } + delete_all(rest, k) } else { - get_last(rest, k) + [Pair(k2, v2), ..delete_all(rest, k)] } } } -test get_last_1() { - get_last([], "a") == None +test delete_all_1() { + delete_all([], "a") == [] } -test get_last_2() { - get_last([Pair("a", 1)], "a") == Some(1) -} - -test get_last_3() { - get_last([Pair("a", 1), Pair("b", 2)], "a") == Some(1) +test delete_all_2() { + delete_all([Pair("a", 14)], "a") == [] } -test get_last_4() { - get_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == Some(3) +test delete_all_3() { + let fixture = + [Pair("a", 14)] + delete_all(fixture, "b") == fixture } -test get_last_5() { - get_last([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == None +test delete_all_4() { + let fixture = + [Pair("a", 1), Pair("b", 2), Pair("a", 3)] + delete_all(fixture, "a") == [Pair("b", 2)] } -/// Get all values in the alist associated with a given key. +/// Remove a single key-value pair from the `Pairs`. If the key is not found, no changes are made. +/// Duplicate keys are not deleted. Only the **first** key found is deleted. /// /// ```aiken -/// pairs.get_all([], "a") == [] -/// pairs.get_all([Pair("a", 1)], "a") == [1] -/// pairs.get_all([Pair("a", 1), Pair("b", 2)], "a") == [1] -/// pairs.get_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [1, 3] +/// pairs.delete_first([], "a") == [] +/// pairs.delete_first([Pair("a", 1)], "a") == [] +/// pairs.delete_first([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] +/// pairs.delete_first([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("b", 2), Pair("a", 3)] /// ``` -pub fn get_all(self: Pairs, key k: key) -> List { +pub fn delete_first(self: Pairs, key k: key) -> Pairs { when self is { [] -> [] - [Pair(k2, v), ..rest] -> + [Pair(k2, v2), ..rest] -> if k == k2 { - [v, ..get_all(rest, k)] + rest } else { - get_all(rest, k) + [Pair(k2, v2), ..delete_first(rest, k)] } } } -test get_all_1() { - get_all([], "a") == [] +test delete_first_1() { + delete_first([], "a") == [] } -test get_all_2() { - get_all([Pair("a", 1)], "a") == [1] +test delete_first_2() { + delete_first([Pair("a", 14)], "a") == [] } -test get_all_3() { - get_all([Pair("a", 1), Pair("b", 2)], "a") == [1] -} - -test get_all_4() { - get_all([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [1, 3] -} - -test get_all_5() { - get_all([Pair("a", 1), Pair("b", 2), Pair("c", 3)], "d") == [] -} - -/// Check if a key exists in the pairs. -/// -/// ```aiken -/// pairs.has_key([], "a") == False -/// pairs.has_key([Pair("a", 1)], "a") == True -/// pairs.has_key([Pair("a", 1), Pair("b", 2)], "a") == True -/// pairs.has_key([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == True -/// ``` -pub fn has_key(self: Pairs, k: key) -> Bool { - when self is { - [] -> False - // || is lazy so this is fine - [Pair(k2, _), ..rest] -> k == k2 || has_key(rest, k) - } -} - -test has_key_1() { - !has_key([], "a") -} - -test has_key_2() { - has_key([Pair("a", 14)], "a") -} - -test has_key_3() { - !has_key([Pair("a", 14)], "b") -} - -test has_key_4() { - has_key([Pair("a", 14), Pair("b", 42)], "b") +test delete_first_3() { + let fixture = + [Pair("a", 14)] + delete_first(fixture, "b") == fixture } -test has_key_5() { - has_key([Pair("a", 14), Pair("b", 42), Pair("a", 42)], "a") +test delete_first_4() { + let fixture = + [Pair("a", 1), Pair("b", 2), Pair("a", 3)] + delete_first(fixture, "a") == [Pair("b", 2), Pair("a", 3)] } -/// Extract all the keys present in a given `Pairs`. +/// Remove a single key-value pair from the Pairs. If the key is not found, no changes are made. +/// Duplicate keys are not deleted. Only the **last** key found is deleted. /// /// ```aiken -/// pairs.keys([]) == [] -/// pairs.keys([Pair("a", 1)]) == ["a"] -/// pairs.keys([Pair("a", 1), Pair("b", 2)]) == ["a", "b"] -/// pairs.keys([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == ["a", "b", "a"] +/// pairs.delete_last([], "a") == [] +/// pairs.delete_last([Pair("a", 1)], "a") == [] +/// pairs.delete_last([Pair("a", 1), Pair("b", 2)], "a") == [Pair("b", 2)] +/// pairs.delete_last([Pair("a", 1), Pair("b", 2), Pair("a", 3)], "a") == [Pair("a", 1), Pair("b", 2)] /// ``` -pub fn keys(self: Pairs) -> List { +pub fn delete_last(self: Pairs, key k: key) -> Pairs { when self is { [] -> [] - [Pair(k, _), ..rest] -> - [k, ..keys(rest)] + [Pair(k2, v2), ..rest] -> + if k == k2 { + let tail = delete_last(rest, k) + if tail == rest { + rest + } else { + [Pair(k2, v2), ..tail] + } + } else { + [Pair(k2, v2), ..delete_last(rest, k)] + } } } -test keys_1() { - keys([]) == [] +test delete_last_1() { + delete_last([], "a") == [] } -test keys_2() { - keys([Pair("a", 0)]) == ["a"] +test delete_last_2() { + delete_last([Pair("a", 14)], "a") == [] } -test keys_3() { - keys([Pair("a", 0), Pair("b", 0)]) == ["a", "b"] +test delete_last_3() { + let fixture = + [Pair("a", 14)] + delete_last(fixture, "b") == fixture +} + +test delete_last_4() { + let fixture = + [Pair("a", 1), Pair("b", 2), Pair("a", 3)] + delete_last(fixture, "a") == [Pair("a", 1), Pair("b", 2)] } /// Apply a function to all key-value pairs in a alist, replacing the values. @@ -555,35 +515,81 @@ test map_2() { map(fixture, with: fn(_, v) { v + 1 }) == [Pair("a", 2), Pair("b", 3)] } -/// Extract all the values present in a given `Pairs`. +// ## Transforming + +/// Fold over the key-value pairs in a pairs. The fold direction follows keys +/// in ascending order and is done from left-to-right. /// /// ```aiken -/// pairs.values([]) == [] -/// pairs.values([Pair("a", 1)]) == [1] -/// pairs.values([Pair("a", 1), Pair("b", 2)]) == [1, 2] -/// pairs.values([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == [1, 2, 3] +/// let fixture = [ +/// Pair(1, 100), +/// Pair(2, 200), +/// Pair(3, 300), +/// ] +/// +/// pairs.foldl(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 /// ``` -pub fn values(self: Pairs) -> List { +pub fn foldl( + self: Pairs, + zero: result, + with: fn(key, value, result) -> result, +) -> result { when self is { - [] -> - [] - [Pair(_, v), ..rest] -> - [v, ..values(rest)] + [] -> zero + [Pair(k, v), ..rest] -> foldl(rest, with(k, v, zero), with) } } -test values_1() { - values([]) == [] +test foldl_1() { + foldl([], 14, fn(_, _, _) { 42 }) == 14 } -test values_2() { - values([Pair("a", 1)]) == [1] +test foldl_2() { + foldl( + [Pair("a", 42), Pair("b", 14)], + zero: 0, + with: fn(_, v, total) { v + total }, + ) == 56 } -test values_3() { - values([Pair("a", 1), Pair("b", 2)]) == [1, 2] +/// Fold over the key-value pairs in a Pairs. The fold direction follows the +/// order of elements in the Pairs and is done from right-to-left. +/// +/// ```aiken +/// let fixture = [ +/// Pair(1, 100), +/// Pair(2, 200), +/// Pair(3, 300), +/// ] +/// +/// pairs.foldr(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 +/// ``` +pub fn foldr( + self: Pairs, + zero: result, + with: fn(key, value, result) -> result, +) -> result { + when self is { + [] -> zero + [Pair(k, v), ..rest] -> with(k, v, foldr(rest, zero, with)) + } } -test values_4() { - values([Pair("a", 1), Pair("b", 2), Pair("a", 3)]) == [1, 2, 3] +test foldr_1() { + foldr([], 14, fn(_, _, _) { 42 }) == 14 +} + +test foldr_2() { + foldr( + [Pair("a", 42), Pair("b", 14)], + zero: 0, + with: fn(_, v, total) { v + total }, + ) == 56 +} + +test foldr_3() { + let fixture = + [Pair(1, 100), Pair(2, 200), Pair(3, 300)] + + foldr(fixture, 0, fn(k, v, result) { k * v + result }) == 1400 } diff --git a/lib/aiken/crypto.ak b/lib/aiken/crypto.ak new file mode 100644 index 0000000..46a7dda --- /dev/null +++ b/lib/aiken/crypto.ak @@ -0,0 +1,147 @@ +use aiken/builtin + +pub type VerificationKey = + ByteArray + +pub type VerificationKeyHash = + Hash + +pub type Script = + ByteArray + +pub type ScriptHash = + Hash + +pub type Signature = + ByteArray + +pub type DataHash = + Hash + +/// A `Hash` is nothing more than a `ByteArray`, but it carries extra +/// information for readability. +/// +/// On-chain, any hash digest value is represented as a plain 'ByteArray'. +/// Though in practice, hashes come from different sources and have +/// different semantics. +/// +/// Hence, while this type-alias doesn't provide any strong type-guarantees, +/// it helps writing functions signatures with more meaningful types than mere +/// 'ByteArray'. +/// +/// Compare for example: +/// +/// ```aiken +/// pub type Credential { +/// VerificationKey(ByteArray) +/// Script(ByteArray) +/// } +/// ``` +/// +/// with +/// +/// ```aiken +/// pub type Credential { +/// VerificationKey(Hash) +/// Script(Hash) +/// } +/// ``` +/// +/// Both are strictly equivalent, but the second reads much better. +pub type Hash = + ByteArray + +// ## Hashing + +/// A blake2b-224 hash algorithm. +/// +/// Typically used for: +/// +/// - [`Credential`](../cardano/address.html#Credential) +/// - [`PolicyId`](../cardano/assets.html#PolicyId) +/// +/// Note: there's no function to calculate blake2b-224 hash digests on-chain. +pub opaque type Blake2b_224 { + Blake2b_224 +} + +/// Compute the blake2b-224 hash digest (28 bytes) of some data. +pub fn blake2b_224(bytes: ByteArray) -> Hash { + builtin.blake2b_224(bytes) +} + +/// A blake2b-256 hash algorithm. +/// +/// Typically used for: +/// +/// - [`TransactionId`](../cardano/transaction.html#TransactionId) +pub opaque type Blake2b_256 { + Blake2b_256 +} + +/// Compute the blake2b-256 hash digest (32 bytes) of some data. +pub fn blake2b_256(bytes: ByteArray) -> Hash { + builtin.blake2b_256(bytes) +} + +/// A Keccak-256 hash algorithm. +pub opaque type Keccak_256 { + Keccak_256 +} + +/// Compute the keccak-256 hash digest (32 bytes) of some data. +pub fn keccak_256(bytes: ByteArray) -> Hash { + builtin.keccak_256(bytes) +} + +/// A SHA2-256 hash algorithm. +pub opaque type Sha2_256 { + Sha2_256 +} + +/// Compute the sha2-256 hash digest (32 bytes) of some data. +pub fn sha2_256(bytes: ByteArray) -> Hash { + builtin.sha2_256(bytes) +} + +/// A SHA3-256 hash algorithm. +pub opaque type Sha3_256 { + Sha3_256 +} + +/// Compute the sha3-256 hash digest (32 bytes) of some data. +pub fn sha3_256(bytes: ByteArray) -> Hash { + builtin.sha3_256(bytes) +} + +// ## Verifying signatures + +/// Verify an ECDCA signature (over secp256k1) using the given verification key. +/// Returns `True` when the signature is valid. +pub fn verify_ecdsa_signature( + key: VerificationKey, + msg: ByteArray, + sig: Signature, +) -> Bool { + builtin.verify_ecdsa_secp256k1_signature(key, msg, sig) +} + +/// Verify an Ed25519 signature using the given verification key. +/// Returns `True` when the signature is valid. +pub fn verify_ed25519_signature( + key: VerificationKey, + msg: ByteArray, + sig: Signature, +) -> Bool { + builtin.verify_ed25519_signature(key, msg, sig) +} + +/// Verify a Schnorr signature (over secp256k1) using the given verification key. +/// Returns `True` when the signature is valid. +pub fn verify_schnorr_signature( + key: VerificationKey, + msg: ByteArray, + sig: Signature, +) -> Bool { + builtin.verify_schnorr_secp256k1_signature(key, msg, sig) +} diff --git a/lib/aiken/hash.ak b/lib/aiken/hash.ak deleted file mode 100644 index 4f86027..0000000 --- a/lib/aiken/hash.ak +++ /dev/null @@ -1,83 +0,0 @@ -//// This module defines `Hash`, a self-documenting type-alias with a -//// phantom-type for readability. -//// -//// On-chain, any hash digest value is represented as a plain 'ByteArray'. -//// Though in practice, hashes come from different sources and have -//// different semantics. -//// -//// Hence, while this type-alias doesn't provide any strong type-guarantees, -//// it helps writing functions signatures with more meaningful types than mere -//// 'ByteArray'. -//// -//// Compare for example: -//// -//// ```aiken -//// pub type Credential { -//// VerificationKeyCredential(ByteArray) -//// ScriptCredential(ByteArray) -//// } -//// ``` -//// -//// with -//// -//// ```aiken -//// pub type Credential { -//// VerificationKeyCredential(Hash) -//// ScriptCredential(Hash) -//// } -//// ``` -//// -//// Both are strictly equivalent, but the second reads much better. - -use aiken/builtin - -/// A `Hash` is nothing more than a `ByteArray`, but it carries extra -/// information for readability. -pub type Hash = - ByteArray - -/// A blake2b-224 hash algorithm. -/// -/// Typically used for: -/// -/// - [`Credential`](../aiken/transaction/credential.html#Credential) -/// - [`PolicyId`](../aiken/transaction/value.html#PolicyId) -/// -/// Note: there's no function to calculate blake2b-224 hash digests on-chain. -pub opaque type Blake2b_224 { - Blake2b_224 -} - -/// A blake2b-256 hash algorithm. -/// -/// Typically used for: -/// -/// - [`TransactionId`](../aiken/transaction.html#TransactionId) -pub opaque type Blake2b_256 { - Blake2b_256 -} - -/// Compute the blake2b-256 hash digest of some data. -pub fn blake2b_256(bytes: ByteArray) -> Hash { - builtin.blake2b_256(bytes) -} - -/// A SHA2-256 hash algorithm. -pub opaque type Sha2_256 { - Sha2_256 -} - -/// Compute the sha2-256 hash digest of some data. -pub fn sha2_256(bytes: ByteArray) -> Hash { - builtin.sha2_256(bytes) -} - -/// A SHA3-256 hash algorithm. -pub opaque type Sha3_256 { - Sha3_256 -} - -/// Compute the sha3-256 hash digest of some data. -pub fn sha3_256(bytes: ByteArray) -> Hash { - builtin.sha3_256(bytes) -} diff --git a/lib/aiken/int.ak b/lib/aiken/int.ak deleted file mode 100644 index fd602b6..0000000 --- a/lib/aiken/int.ak +++ /dev/null @@ -1,80 +0,0 @@ -use aiken/bytearray -use aiken/math -use aiken/option - -/// Compare two integers. -/// -/// ```aiken -/// int.compare(14, 42) == Less -/// int.compare(14, 14) == Equal -/// int.compare(42, 14) == Greater -/// ``` -pub fn compare(left: Int, right: Int) -> Ordering { - if left < right { - Less - } else if left > right { - Greater - } else { - Equal - } -} - -/// Parse an integer from a utf-8 encoded 'ByteArray', when possible. -/// -/// ```aiken -/// int.from_utf8("14") == Some(14) -/// int.from_utf8("-42") == Some(-42) -/// int.from_utf8("007") == Some(7) -/// int.from_utf8("foo") == None -/// int.from_utf8("1.0") == None -/// int.from_utf8("1-2") == None -/// ``` -pub fn from_utf8(bytes: ByteArray) -> Option { - bytes - |> bytearray.foldr( - Some((0, 0)), - fn(byte, st) { - when st is { - None -> None - Some((n, e)) -> - if byte < 48 || byte > 57 { - if byte == 45 { - Some((-n, 0)) - } else { - None - } - } else if n < 0 { - None - } else { - let digit = byte - 48 - Some((n + digit * math.pow(10, e), e + 1)) - } - } - }, - ) - |> option.map(fn(tuple) { tuple.1st }) -} - -test from_utf8_1() { - from_utf8("0017") == Some(17) -} - -test from_utf8_2() { - from_utf8("42") == Some(42) -} - -test from_utf8_3() { - from_utf8("1337") == Some(1337) -} - -test from_utf8_4() { - from_utf8("-14") == Some(-14) -} - -test from_utf8_5() { - from_utf8("foo") == None -} - -test from_utf8_6() { - from_utf8("1-2") == None -} diff --git a/lib/aiken/interval.ak b/lib/aiken/interval.ak index 323d5a9..db518e2 100644 --- a/lib/aiken/interval.ak +++ b/lib/aiken/interval.ak @@ -12,6 +12,8 @@ //// executed, but we can reason about the interval bounds to validate pieces of //// logic. +// TODO: Replace 'Int' with a generic 'a' once we have comparable traits. + /// A type to represent intervals of values. Interval are inhabited by a type /// `a` which is useful for non-infinite intervals that have a finite /// lower-bound and/or upper-bound. @@ -48,103 +50,182 @@ pub type IntervalBound { is_inclusive: Bool, } -/// Return the highest bound of the two. +/// A type of interval bound. Where finite, a value of type `a` must be +/// provided. `a` will typically be an `Int`, representing a number of seconds or +/// milliseconds. +pub type IntervalBoundType { + NegativeInfinity + Finite(a) + PositiveInfinity +} + +// ## Constructing + +/// Create an interval that includes all values greater than the given bound. i.e [lower_bound, +INF) /// /// ```aiken -/// let ib1 = IntervalBound { bound_type: Finite(0), is_inclusive: False } -/// let ib2 = IntervalBound { bound_type: Finite(1), is_inclusive: False } +/// interval.after(10) == Interval { +/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: True }, +/// upper_bound: IntervalBound { bound_type: PositiveInfinity, is_inclusive: True }, +/// } +/// ``` +pub fn after(lower_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: Finite(lower_bound), + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: PositiveInfinity, + is_inclusive: True, + }, + } +} + +/// Create an interval that includes all values after (and not including) the given bound. i.e (lower_bound, +INF) /// -/// interval.max(ib1, ib2) == ib2 +/// ```aiken +/// interval.entirely_after(10) == Interval { +/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, +/// upper_bound: IntervalBound { bound_type: PositiveInfinity, is_inclusive: True }, +/// } /// ``` -pub fn max( - left: IntervalBound, - right: IntervalBound, -) -> IntervalBound { - when compare_bound(left, right) is { - Less -> right - Equal -> left - Greater -> left +pub fn entirely_after(lower_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: Finite(lower_bound), + is_inclusive: False, + }, + upper_bound: IntervalBound { + bound_type: PositiveInfinity, + is_inclusive: True, + }, } } -/// Return the smallest bound of the two. +/// Create an interval that includes all values before (and including) the given bound. i.e (-INF, upper_bound] /// /// ```aiken -/// let ib1 = IntervalBound { bound_type: Finite(0), is_inclusive: False } -/// let ib2 = IntervalBound { bound_type: Finite(1), is_inclusive: False } +/// interval.before(100) == Interval { +/// lower_bound: IntervalBound { bound_type: NegativeInfinity, is_inclusive: True }, +/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: True }, +/// } +/// ``` +pub fn before(upper_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: NegativeInfinity, + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: Finite(upper_bound), + is_inclusive: True, + }, + } +} + +/// Create an interval that includes all values before (and not including) the given bound. i.e (-INF, upper_bound) /// -/// interval.min(ib1, ib2) == ib1 +/// ```aiken +/// interval.entirely_before(10) == Interval { +/// lower_bound: IntervalBound { bound_type: NegativeInfinity, is_inclusive: True }, +/// upper_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, +/// } /// ``` -pub fn min( - left: IntervalBound, - right: IntervalBound, -) -> IntervalBound { - when compare_bound(left, right) is { - Less -> left - Equal -> left - Greater -> right +pub fn entirely_before(upper_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: NegativeInfinity, + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: Finite(upper_bound), + is_inclusive: False, + }, } } -fn compare_bound( - left: IntervalBound, - right: IntervalBound, -) -> Ordering { - when compare_bound_type(left.bound_type, right.bound_type) is { - Less -> Less - Greater -> Greater - Equal -> - if left.is_inclusive == right.is_inclusive { - Equal - } else if left.is_inclusive { - Greater - } else { - Less - } +/// Create an interval that includes all values between two bounds, including the bounds. i.e. [lower_bound, upper_bound] +/// +/// ```aiken +/// interval.between(10, 100) == Interval { +/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: True }, +/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: True }, +/// } +/// ``` +pub fn between(lower_bound: a, upper_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: Finite(lower_bound), + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: Finite(upper_bound), + is_inclusive: True, + }, } } -/// A type of interval bound. Where finite, a value of type `a` must be -/// provided. `a` will typically be an `Int`, representing a number of seconds or -/// milliseconds. -pub type IntervalBoundType { - NegativeInfinity - Finite(a) - PositiveInfinity +/// Create an interval that includes all values between two bounds, excluding the bounds. i.e. (lower_bound, upper_bound) +/// +/// ```aiken +/// interval.entirely_between(10, 100) == Interval { +/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, +/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: False }, +/// } +/// ``` +pub fn entirely_between(lower_bound: a, upper_bound: a) -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: Finite(lower_bound), + is_inclusive: False, + }, + upper_bound: IntervalBound { + bound_type: Finite(upper_bound), + is_inclusive: False, + }, + } } -fn compare_bound_type( - left: IntervalBoundType, - right: IntervalBoundType, -) -> Ordering { - when left is { - NegativeInfinity -> - when right is { - NegativeInfinity -> Equal - _ -> Less - } - PositiveInfinity -> - when right is { - PositiveInfinity -> Equal - _ -> Greater - } - Finite(left) -> - when right is { - NegativeInfinity -> Greater - PositiveInfinity -> Less - Finite(right) -> - if left < right { - Less - } else if left == right { - Equal - } else { - Greater - } - } +/// Create an empty interval that contains no value. +/// +/// ```aiken +/// interval.contains(empty(), 0) == False +/// interval.contains(empty(), 1000) == False +/// ``` +pub fn empty() -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: PositiveInfinity, + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: NegativeInfinity, + is_inclusive: True, + }, } } -// TODO: Replace 'Int' with a generic 'a' once we have comparable traits. +/// Create an interval that contains every possible values. i.e. (-INF, +INF) +/// +/// ```aiken +/// interval.contains(everything(), 0) == True +/// interval.contains(everything(), 1000) == True +/// ``` +pub fn everything() -> Interval { + Interval { + lower_bound: IntervalBound { + bound_type: NegativeInfinity, + is_inclusive: True, + }, + upper_bound: IntervalBound { + bound_type: PositiveInfinity, + is_inclusive: True, + }, + } +} + +// ## Inspecting /// Checks whether an element is contained within the interval. /// @@ -152,11 +233,11 @@ fn compare_bound_type( /// let iv = /// Interval { /// lower_bound: IntervalBound { -/// bound_type: Finite(14), +/// bound_type: Finite(14), /// is_inclusive: True /// }, /// upper_bound: IntervalBound { -/// bound_type: Finite(42), +/// bound_type: Finite(42), /// is_inclusive: False /// }, /// } @@ -254,109 +335,51 @@ test contains_12() { !contains(iv, 14) } -/// Create an interval that contains every possible values. i.e. (-INF, +INF) -/// -/// ```aiken -/// interval.contains(everything(), 0) == True -/// interval.contains(everything(), 1000) == True -/// ``` -pub fn everything() -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: NegativeInfinity, - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: PositiveInfinity, - is_inclusive: True, - }, - } -} - -/// Create an empty interval that contains no value. -/// -/// ```aiken -/// interval.contains(empty(), 0) == False -/// interval.contains(empty(), 1000) == False -/// ``` -pub fn empty() -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: PositiveInfinity, - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: NegativeInfinity, - is_inclusive: True, - }, - } -} - -/// Create an interval that includes all values between two bounds, including the bounds. i.e. [lower_bound, upper_bound] +/// Tells whether an interval is empty; i.e. that is contains no value. /// /// ```aiken -/// interval.between(10, 100) == Interval { -/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: True }, -/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: True }, -/// } -/// ``` -pub fn between(lower_bound: a, upper_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: Finite(lower_bound), - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: Finite(upper_bound), - is_inclusive: True, - }, - } -} - -/// Create an interval that includes all values between two bounds, excluding the bounds. i.e. (lower_bound, upper_bound) +/// let iv1 = interval.empty() /// -/// ```aiken -/// interval.entirely_between(10, 100) == Interval { -/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, -/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: False }, -/// } -/// ``` -pub fn entirely_between(lower_bound: a, upper_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: Finite(lower_bound), - is_inclusive: False, - }, - upper_bound: IntervalBound { - bound_type: Finite(upper_bound), - is_inclusive: False, - }, - } -} - -/// Create an interval that includes all values greater than the given bound. i.e [lower_bound, +INF) +/// let iv2 = Interval { +/// lower_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, +/// upper_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, +/// } /// -/// ```aiken -/// interval.after(10) == Interval { -/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: True }, -/// upper_bound: IntervalBound { bound_type: PositiveInfinity, is_inclusive: True }, -/// } -/// ``` -pub fn after(lower_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: Finite(lower_bound), - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: PositiveInfinity, - is_inclusive: True, - }, +/// let iv3 = Interval { +/// lower_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, +/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: False }, +/// } +/// +/// interval.is_empty(iv1) == True +/// interval.is_empty(iv2) == True +/// interval.is_empty(iv3) == False +/// +/// // Note: Two empty intervals are not necessarily equal. +/// iv1 != iv2 +/// ``` +pub fn is_empty(self: Interval) -> Bool { + let ordering = + compare_bound_type(self.lower_bound.bound_type, self.upper_bound.bound_type) + + when ordering is { + Greater -> True + Equal -> !(self.lower_bound.is_inclusive && self.upper_bound.is_inclusive) + Less -> { + let is_open_interval = + !self.lower_bound.is_inclusive && !self.upper_bound.is_inclusive + if is_open_interval { + when (self.lower_bound.bound_type, self.upper_bound.bound_type) is { + (Finite(lower_bound), Finite(upper_bound)) -> + lower_bound + 1 == upper_bound + _ -> False + } + } else { + False + } + } } } -// TODO: Replace 'Int' with a generic 'a' once we have comparable traits. - /// Check whether the interval is entirely after the point "a" /// /// ```aiken @@ -413,8 +436,6 @@ test is_entirely_after_9() { !is_entirely_after(entirely_before(10), 5) } -// TODO: Replace 'Int' with a generic 'a' once we have comparable traits. - /// Check whether the interval is entirely before the point "a" /// /// ```aiken @@ -471,112 +492,46 @@ test is_entirely_before_9() { !is_entirely_before(entirely_after(10), 5) } -/// Create an interval that includes all values after (and not including) the given bound. i.e (lower_bound, +INF) -/// -/// ```aiken -/// interval.entirely_after(10) == Interval { -/// lower_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, -/// upper_bound: IntervalBound { bound_type: PositiveInfinity, is_inclusive: True }, -/// } -/// ``` -pub fn entirely_after(lower_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: Finite(lower_bound), - is_inclusive: False, - }, - upper_bound: IntervalBound { - bound_type: PositiveInfinity, - is_inclusive: True, - }, - } -} +// ## Combining -/// Create an interval that includes all values before (and including) the given bound. i.e (-INF, upper_bound] +/// Computes the smallest interval containing the two given intervals, if any /// /// ```aiken -/// interval.before(100) == Interval { -/// lower_bound: IntervalBound { bound_type: NegativeInfinity, is_inclusive: True }, -/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: True }, -/// } +/// let iv1 = between(0, 10) +/// let iv2 = between(2, 14) +/// hull(iv1, iv2) == between(0, 14) +/// +/// let iv1 = between(5, 10) +/// let iv2 = before(0) +/// hull(iv1, iv2) == before(10) +/// +/// let iv1 = entirely_after(0) +/// let iv2 = between(10, 42) +/// hull(iv1, iv2) = entirely_after(0) /// ``` -pub fn before(upper_bound: a) -> Interval { +pub fn hull(iv1: Interval, iv2: Interval) -> Interval { Interval { - lower_bound: IntervalBound { - bound_type: NegativeInfinity, - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: Finite(upper_bound), - is_inclusive: True, - }, + lower_bound: min(iv1.lower_bound, iv2.lower_bound), + upper_bound: max(iv1.upper_bound, iv2.upper_bound), } } -/// Create an interval that includes all values before (and not including) the given bound. i.e (-INF, upper_bound) -/// -/// ```aiken -/// interval.entirely_before(10) == Interval { -/// lower_bound: IntervalBound { bound_type: NegativeInfinity, is_inclusive: True }, -/// upper_bound: IntervalBound { bound_type: Finite(10), is_inclusive: False }, -/// } -/// ``` -pub fn entirely_before(upper_bound: a) -> Interval { - Interval { - lower_bound: IntervalBound { - bound_type: NegativeInfinity, - is_inclusive: True, - }, - upper_bound: IntervalBound { - bound_type: Finite(upper_bound), - is_inclusive: False, - }, - } +test hull_1() { + let iv1 = between(0, 10) + let iv2 = between(2, 14) + hull(iv1, iv2) == between(0, 14) } -/// Tells whether an interval is empty; i.e. that is contains no value. -/// -/// ```aiken -/// let iv1 = interval.empty() -/// -/// let iv2 = Interval { -/// lower_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, -/// upper_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, -/// } -/// -/// let iv3 = Interval { -/// lower_bound: IntervalBound { bound_type: Finite(0), is_inclusive: False }, -/// upper_bound: IntervalBound { bound_type: Finite(100), is_inclusive: False }, -/// } -/// -/// interval.is_empty(iv1) == True -/// interval.is_empty(iv2) == True -/// interval.is_empty(iv3) == False -/// -/// // Note: Two empty intervals are not necessarily equal. -/// iv1 != iv2 -/// ``` -pub fn is_empty(self: Interval) -> Bool { - let ordering = - compare_bound_type(self.lower_bound.bound_type, self.upper_bound.bound_type) +test hull_2() { + let iv1 = between(5, 10) + let iv2 = before(0) + hull(iv1, iv2) == before(10) +} - when ordering is { - Greater -> True - Equal -> !(self.lower_bound.is_inclusive && self.upper_bound.is_inclusive) - Less -> { - let is_open_interval = - !self.lower_bound.is_inclusive && !self.upper_bound.is_inclusive - if is_open_interval { - when (self.lower_bound.bound_type, self.upper_bound.bound_type) is { - (Finite(lower_bound), Finite(upper_bound)) -> - lower_bound + 1 == upper_bound - _ -> False - } - } else { - False - } - } - } +test hull_3() { + let iv1 = entirely_after(0) + let iv2 = between(10, 42) + hull(iv1, iv2) == entirely_after(0) } /// Computes the largest interval contains in the two given intervals, if any. @@ -639,42 +594,89 @@ test intersection_6() { intersection(iv1, iv2) == entirely_between(0, 10) } -/// Computes the smallest interval containing the two given intervals, if any +/// Return the highest bound of the two. /// /// ```aiken -/// let iv1 = between(0, 10) -/// let iv2 = between(2, 14) -/// hull(iv1, iv2) == between(0, 14) -/// -/// let iv1 = between(5, 10) -/// let iv2 = before(0) -/// hull(iv1, iv2) == before(10) +/// let ib1 = IntervalBound { bound_type: Finite(0), is_inclusive: False } +/// let ib2 = IntervalBound { bound_type: Finite(1), is_inclusive: False } /// -/// let iv1 = entirely_after(0) -/// let iv2 = between(10, 42) -/// hull(iv1, iv2) = entirely_after(0) +/// interval.max(ib1, ib2) == ib2 /// ``` -pub fn hull(iv1: Interval, iv2: Interval) -> Interval { - Interval { - lower_bound: min(iv1.lower_bound, iv2.lower_bound), - upper_bound: max(iv1.upper_bound, iv2.upper_bound), +pub fn max( + left: IntervalBound, + right: IntervalBound, +) -> IntervalBound { + when compare_bound(left, right) is { + Less -> right + Equal -> left + Greater -> left } } -test hull_1() { - let iv1 = between(0, 10) - let iv2 = between(2, 14) - hull(iv1, iv2) == between(0, 14) +/// Return the smallest bound of the two. +/// +/// ```aiken +/// let ib1 = IntervalBound { bound_type: Finite(0), is_inclusive: False } +/// let ib2 = IntervalBound { bound_type: Finite(1), is_inclusive: False } +/// +/// interval.min(ib1, ib2) == ib1 +/// ``` +pub fn min( + left: IntervalBound, + right: IntervalBound, +) -> IntervalBound { + when compare_bound(left, right) is { + Less -> left + Equal -> left + Greater -> right + } } -test hull_2() { - let iv1 = between(5, 10) - let iv2 = before(0) - hull(iv1, iv2) == before(10) +fn compare_bound( + left: IntervalBound, + right: IntervalBound, +) -> Ordering { + when compare_bound_type(left.bound_type, right.bound_type) is { + Less -> Less + Greater -> Greater + Equal -> + if left.is_inclusive == right.is_inclusive { + Equal + } else if left.is_inclusive { + Greater + } else { + Less + } + } } -test hull_3() { - let iv1 = entirely_after(0) - let iv2 = between(10, 42) - hull(iv1, iv2) == entirely_after(0) +fn compare_bound_type( + left: IntervalBoundType, + right: IntervalBoundType, +) -> Ordering { + when left is { + NegativeInfinity -> + when right is { + NegativeInfinity -> Equal + _ -> Less + } + PositiveInfinity -> + when right is { + PositiveInfinity -> Equal + _ -> Greater + } + Finite(left) -> + when right is { + NegativeInfinity -> Greater + PositiveInfinity -> Less + Finite(right) -> + if left < right { + Less + } else if left == right { + Equal + } else { + Greater + } + } + } } diff --git a/lib/aiken/math.ak b/lib/aiken/math.ak index 764152b..dd575e7 100644 --- a/lib/aiken/math.ak +++ b/lib/aiken/math.ak @@ -71,6 +71,152 @@ test clamp_3() { clamp(7, min: 10, max: 100) == 10 } +/// The greatest common divisor of two integers. +/// +/// ```aiken +/// math.gcd(42, 14) == 14 +/// math.gcd(14, 42) == 14 +/// math.gcd(0, 0) == 0 +/// ``` +pub fn gcd(x: Int, y: Int) -> Int { + abs(do_gcd(x, y)) +} + +fn do_gcd(x: Int, y: Int) -> Int { + when y is { + 0 -> x + _ -> do_gcd(y, x % y) + } +} + +test gcd_test1() { + gcd(10, 300) == 10 +} + +test gcd_test2() { + gcd(-10, 300) == 10 +} + +test gcd_test3() { + gcd(42, 14) == 14 +} + +/// Checks if an integer has a given integer square root x. +/// The check has constant time complexity $O(1)$. +/// +/// ```aiken +/// math.is_sqrt(0, 0) +/// math.is_sqrt(25, 5) +/// !math.is_sqrt(25, -5) +/// math.is_sqrt(44203, 210) +/// ``` +pub fn is_sqrt(self: Int, x: Int) -> Bool { + x * x <= self && ( x + 1 ) * ( x + 1 ) > self +} + +test is_sqrt1() { + is_sqrt(44203, 210) +} + +test is_sqrt2() { + is_sqrt(975461057789971041, 987654321) +} + +/// The logarithm in base `b` of an element using integer divisions. +/// +/// ```aiken +/// math.log(10, base: 2) == 3 +/// math.log(42, base: 2) == 5 +/// math.log(42, base: 3) == 3 +/// math.log(5, base: 0) == 0 +/// math.log(4, base: 4) == 1 +/// math.log(4, base: 42) == 0 +/// ``` +pub fn log(self: Int, base: Int) -> Int { + if base <= 0 { + 0 + } else if self == base { + 1 + } else if self < base { + 0 + } else { + 1 + log(self / base, base) + } +} + +test log_10_2() { + log(10, base: 2) == 3 +} + +test log_42_2() { + log(42, base: 2) == 5 +} + +test log_42_3() { + log(42, base: 3) == 3 +} + +test log_5_0() { + log(5, base: 0) == 0 +} + +test log_4_4() { + log(4, base: 4) == 1 +} + +test log_4_43() { + log(4, base: 43) == 0 +} + +/// The integer logarithm in base 2. Faster than [`log`](#log) in this particular case. +/// +/// ```aiken +/// math.log2(1) == 0 +/// math.log2(2) == 1 +/// math.log2(3) == 1 +/// math.log2(4) == 2 +/// math.log2(256) == 8 +/// math.log2(257) == 8 +/// math.log2(511) == 8 +/// math.log2(1025) == 10 +/// ``` +pub fn log2(x: Int) -> Int { + expect x > 0 + let s = builtin.integer_to_bytearray(True, 0, x) + let len = builtin.length_of_bytearray(s) + let b = builtin.index_bytearray(s, 0) + len * 8 - if b < 2 { + 8 + } else if b < 4 { + 7 + } else if b < 8 { + 6 + } else if b < 16 { + 5 + } else if b < 32 { + 4 + } else if b < 64 { + 3 + } else if b < 128 { + 2 + } else { + 1 + } +} + +test log2_matrix() { + and { + log2(1) == 0, + log2(2) == 1, + log2(3) == 1, + log2(4) == 2, + log2(256) == 8, + log2(257) == 8, + log2(511) == 8, + log2(1025) == 10, + } +} + /// Return the maximum of two integers. pub fn max(a: Int, b: Int) -> Int { if a > b { @@ -166,7 +312,7 @@ test pow_2_42() { } /// Calculates the power of 2 for a given exponent `e`. Much cheaper than -/// using `pow(2, _)` for small exponents (0 < e < 256). +/// using `pow(2, _)` for small exponents $0 < e < 256$. /// /// ```aiken /// math.pow2(-2) == 0 @@ -214,82 +360,6 @@ test pow2_256() { pow2(256) == 115792089237316195423570985008687907853269984665640564039457584007913129639936 } -/// The logarithm in base `b` of an element using integer divisions. -/// -/// ```aiken -/// math.log(10, base: 2) == 3 -/// math.log(42, base: 2) == 5 -/// math.log(42, base: 3) == 3 -/// math.log(5, base: 0) == 0 -/// math.log(4, base: 4) == 1 -/// math.log(4, base: 42) == 0 -/// ``` -pub fn log(self: Int, base: Int) -> Int { - if base <= 0 { - 0 - } else if self == base { - 1 - } else if self < base { - 0 - } else { - 1 + log(self / base, base) - } -} - -test log_10_2() { - log(10, base: 2) == 3 -} - -test log_42_2() { - log(42, base: 2) == 5 -} - -test log_42_3() { - log(42, base: 3) == 3 -} - -test log_5_0() { - log(5, base: 0) == 0 -} - -test log_4_4() { - log(4, base: 4) == 1 -} - -test log_4_43() { - log(4, base: 43) == 0 -} - -/// The greatest common divisor of two integers. -/// -/// ```aiken -/// math.gcd(42, 14) == 14 -/// math.gcd(14, 42) == 14 -/// math.gcd(0, 0) == 0 -/// ``` -pub fn gcd(x: Int, y: Int) -> Int { - abs(do_gcd(x, y)) -} - -fn do_gcd(x: Int, y: Int) -> Int { - when y is { - 0 -> x - _ -> do_gcd(y, x % y) - } -} - -test gcd_test1() { - gcd(10, 300) == 10 -} - -test gcd_test2() { - gcd(-10, 300) == 10 -} - -test gcd_test3() { - gcd(42, 14) == 14 -} - /// Calculates the square root of an integer using the [Babylonian /// method](https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method). This returns either the exact result or the smallest integer /// nearest to the square root. @@ -302,6 +372,9 @@ test gcd_test3() { /// math.sqrt(44203) == Some(210) /// math.sqrt(-42) == None /// ``` +/// +/// > [!TIP] +/// > This function can be quite expensive to perform on-chain. Prefer using [`is_sqrt`](#is_sqrt) whenever possible. pub fn sqrt(self: Int) -> Option { if self < 0 { None @@ -349,24 +422,3 @@ test sqrt5() { test sqrt6() { sqrt(-42) == None } - -/// Checks if an integer has a given integer square root x. -/// The check has constant time complexity (O(1)). -/// -/// ```aiken -/// math.is_sqrt(0, 0) -/// math.is_sqrt(25, 5) -/// ! math.is_sqrt(25, -5) -/// math.is_sqrt(44203, 210) -/// ``` -pub fn is_sqrt(self: Int, x: Int) -> Bool { - x * x <= self && ( x + 1 ) * ( x + 1 ) > self -} - -test is_sqrt1() { - is_sqrt(44203, 210) -} - -test is_sqrt2() { - is_sqrt(975461057789971041, 987654321) -} diff --git a/lib/aiken/math/rational.ak b/lib/aiken/math/rational.ak index d99ef20..bf163fb 100644 --- a/lib/aiken/math/rational.ak +++ b/lib/aiken/math/rational.ak @@ -1,16 +1,17 @@ -//// This module implements operations between rational numbers. Internally, rational aren't -//// automatically reduced as this is **only done on-demand**. +//// This module implements operations between rational numbers. //// -//// Thus, for example: -//// -//// ```aiken -//// rational.new(2, 3) != rational.new(4, 6) -//// ``` -//// -//// Comparing rational values should, therefore, only happen after reduction (see [reduce](#reduce)) or via the [compare](#compare) method. +//// > [!CAUTION] Internally, rational aren't automatically reduced as this is **only done on-demand**. +//// > +//// > Thus, for example: +//// > +//// > ```aiken +//// > rational.new(2, 3) != rational.new(4, 6) +//// > ``` +//// > +//// > Comparing rational values should, therefore, only happen after reduction (see [reduce](#reduce)) or via the [compare](#compare) method. use aiken/builtin -use aiken/list +use aiken/collection/list use aiken/math use aiken/option @@ -20,6 +21,27 @@ pub opaque type Rational { denominator: Int, } +// ## Constructing + +/// Create a new `Rational` from an `Int`. +/// +/// ```aiken +/// Some(rational.from_int(14)) == rational.new(14, 1) +/// Some(rational.from_int(-5)) == rational.new(-5, 1) +/// Some(rational.from_int(0)) == rational.new(0, 1) +/// ``` +pub fn from_int(numerator: Int) -> Rational { + Rational { numerator, denominator: 1 } +} + +test from_int_1() { + and { + (from_int(14) == ratio(14, 1))?, + (from_int(-5) == ratio(-5, 1))?, + (from_int(0) == ratio(0, 1))?, + } +} + /// An unsafe constructor for `Rational` values. Assumes that the following invariants are /// enforced: /// @@ -61,6 +83,40 @@ test new_1() { } } +/// A null `Rational`. +pub fn zero() -> Rational { + Rational { numerator: 0, denominator: 1 } +} + +test zero_1() { + zero() == ratio(0, 1) +} + +// ## Inspecting + +/// Get the denominator of a rational value. +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.denominator(x) == 3 +/// ``` +pub fn denominator(self: Rational) -> Int { + self.denominator +} + +test denominator_1() { + expect Some(x) = new(2, 3) + expect Some(y) = new(-2, 3) + expect Some(z) = new(2, -3) + expect Some(w) = new(-2, -3) + and { + (denominator(x) == 3)?, + (denominator(y) == 3)?, + (denominator(z) == 3)?, + (denominator(w) == 3)?, + } +} + /// Get the numerator of a rational value. /// /// ```aiken @@ -85,73 +141,128 @@ test numerator_1() { } } -/// Get the denominator of a rational value. +// ## Modifying + +/// Absolute value of a `Rational`. /// /// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.denominator(x) == 3 +/// expect Some(x) = rational.new(3, 2) +/// expect Some(y) = rational.new(-3, 2) +/// +/// rational.abs(x) == x +/// rational.abs(y) == x /// ``` -pub fn denominator(self: Rational) -> Int { - self.denominator +pub fn abs(self: Rational) -> Rational { + let Rational { numerator: a_n, denominator: a_d } = self + Rational { numerator: math.abs(a_n), denominator: a_d } } -test denominator_1() { - expect Some(x) = new(2, 3) - expect Some(y) = new(-2, 3) - expect Some(z) = new(2, -3) - expect Some(w) = new(-2, -3) +test abs_examples() { and { - (denominator(x) == 3)?, - (denominator(y) == 3)?, - (denominator(z) == 3)?, - (denominator(w) == 3)?, + (abs(ratio(5, 2)) == ratio(5, 2))?, + (abs(ratio(-5, 2)) == ratio(5, 2))?, + (abs(ratio(5, 2)) == abs(ratio(-5, 2)))?, } } -/// A null `Rational`. -pub fn zero() -> Rational { - Rational { numerator: 0, denominator: 1 } +/// Change the sign of a `Rational`. +/// +/// ```aiken +/// expect Some(x) = rational.new(3, 2) +/// expect Some(y) = rational.new(-3, 2) +/// +/// rational.negate(x) == y +/// rational.negate(y) == x +/// ``` +pub fn negate(a: Rational) -> Rational { + let Rational { numerator: a_n, denominator: a_d } = a + Rational { numerator: -a_n, denominator: a_d } } -test zero_1() { - zero() == ratio(0, 1) +test negate_1() { + and { + (negate(ratio(5, 2)) == ratio(-5, 2))?, + (negate(ratio(-5, 2)) == ratio(5, 2))?, + (negate(negate(ratio(5, 2))) == ratio(5, 2))?, + } } -/// Multiplication: the product of two rational values. +/// Reciprocal of a `Rational` number. That is, a new `Rational` where the +/// numerator and denominator have been swapped. +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 5) +/// rational.reciprocal(x) == rational.new(5, 2) +/// +/// let y = rational.zero() +/// rational.reciprocal(y) == None +/// ``` +pub fn reciprocal(self: Rational) -> Option { + let Rational { numerator: a_n, denominator: a_d } = self + if a_n < 0 { + Some(Rational { numerator: -a_d, denominator: -a_n }) + } else if a_n > 0 { + Some(Rational { numerator: a_d, denominator: a_n }) + } else { + None + } +} + +test reciprocal_1() { + and { + (reciprocal(ratio(5, 2)) == new(2, 5))?, + (reciprocal(ratio(-5, 2)) == new(-2, 5))?, + (reciprocal(ratio(0, 2)) == None)?, + (reciprocal(ratio(2, 3)) == new(3, 2))?, + (reciprocal(ratio(-2, 3)) == new(-3, 2))?, + } +} + +/// Reduce a rational to its irreducible form. This operation makes the +/// numerator and denominator coprime. +/// +/// ```aiken +/// expect Some(x) = rational.new(80, 200) +/// Some(rational.reduce(x)) == rational.new(2, 5) +/// ``` +pub fn reduce(self: Rational) -> Rational { + let Rational { numerator: a_n, denominator: a_d } = self + let d = math.gcd(a_n, a_d) + Rational { numerator: a_n / d, denominator: a_d / d } +} + +test reduce_1() { + and { + (reduce(ratio(80, 200)) == ratio(2, 5))?, + (reduce(ratio(-5, 1)) == ratio(-5, 1))?, + (reduce(ratio(0, 3)) == ratio(0, 1))?, + } +} + +// ## Combining + +// ### Arithmetic operations + +/// Addition: sum of two rational values /// /// ```aiken /// expect Some(x) = rational.new(2, 3) /// expect Some(y) = rational.new(3, 4) /// -/// Some(rational.mul(x, y)) == rational.new(6, 12) +/// Some(rational.add(x, y)) == rational.new(17, 12) /// ``` -pub fn mul(left: Rational, right: Rational) -> Rational { +pub fn add(left: Rational, right: Rational) -> Rational { let Rational { numerator: a_n, denominator: a_d } = left let Rational { numerator: b_n, denominator: b_d } = right - Rational { numerator: a_n * b_n, denominator: a_d * b_d } -} - -test mul_1() { - mul(ratio(2, 3), ratio(3, 4)) == ratio(6, 12) + Rational { numerator: a_n * b_d + b_n * a_d, denominator: a_d * b_d } } -test mul_2() { - mul(ratio(-2, 3), ratio(-3, 4)) == ratio(6, 12) +test add_1() { + add(ratio(2, 3), ratio(3, 4)) == ratio(17, 12) } -test mul_3() { - let result = - ratio(2, 5) - |> mul(ratio(1, 8)) - |> mul(ratio(3, 10)) - |> mul(ratio(21, 100)) - |> mul(ratio(3, 5)) - |> mul(ratio(2, 8)) - |> mul(ratio(4, 10)) - |> mul(ratio(22, 100)) - |> reduce - - result == ratio(2079, 50000000) +test add_2() { + add(ratio(-2, 3), ratio(3, 4)) == ratio(1, 12) } /// Division: quotient of two rational values. Returns `None` when the second @@ -175,26 +286,41 @@ test div_2() { div(ratio(2, 3), ratio(-3, 4)) == new(-8, 9) } -/// Addition: sum of two rational values +/// Multiplication: the product of two rational values. /// /// ```aiken /// expect Some(x) = rational.new(2, 3) /// expect Some(y) = rational.new(3, 4) /// -/// Some(rational.add(x, y)) == rational.new(17, 12) +/// Some(rational.mul(x, y)) == rational.new(6, 12) /// ``` -pub fn add(left: Rational, right: Rational) -> Rational { +pub fn mul(left: Rational, right: Rational) -> Rational { let Rational { numerator: a_n, denominator: a_d } = left let Rational { numerator: b_n, denominator: b_d } = right - Rational { numerator: a_n * b_d + b_n * a_d, denominator: a_d * b_d } + Rational { numerator: a_n * b_n, denominator: a_d * b_d } } -test add_1() { - add(ratio(2, 3), ratio(3, 4)) == ratio(17, 12) +test mul_1() { + mul(ratio(2, 3), ratio(3, 4)) == ratio(6, 12) } -test add_2() { - add(ratio(-2, 3), ratio(3, 4)) == ratio(1, 12) +test mul_2() { + mul(ratio(-2, 3), ratio(-3, 4)) == ratio(6, 12) +} + +test mul_3() { + let result = + ratio(2, 5) + |> mul(ratio(1, 8)) + |> mul(ratio(3, 10)) + |> mul(ratio(21, 100)) + |> mul(ratio(3, 5)) + |> mul(ratio(2, 8)) + |> mul(ratio(4, 10)) + |> mul(ratio(22, 100)) + |> reduce + + result == ratio(2079, 50000000) } /// Subtraction: difference of two rational values @@ -214,375 +340,17 @@ pub fn sub(left: Rational, right: Rational) -> Rational { test sub_1() { sub(ratio(2, 3), ratio(3, 4)) == ratio(-1, 12) } - -test sub_2() { - sub(ratio(2, 3), ratio(-3, 4)) == ratio(17, 12) -} - -test sub_3() { - sub(ratio(-2, 3), ratio(3, 4)) == ratio(-17, 12) -} - -/// Create a new `Rational` from an `Int`. -/// -/// ```aiken -/// Some(rational.from_int(14)) == rational.new(14, 1) -/// Some(rational.from_int(-5)) == rational.new(-5, 1) -/// Some(rational.from_int(0)) == rational.new(0, 1) -/// ``` -pub fn from_int(numerator: Int) -> Rational { - Rational { numerator, denominator: 1 } -} - -test from_int_1() { - and { - (from_int(14) == ratio(14, 1))?, - (from_int(-5) == ratio(-5, 1))?, - (from_int(0) == ratio(0, 1))?, - } -} - -/// Returns the nearest `Int` between zero and a given `Rational`. -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.truncate(x) == 0 -/// -/// expect Some(y) = rational.new(44, 14) -/// rational.truncate(y) == 3 -/// -/// expect Some(z) = rational.new(-14, 3) -/// rational.truncate(z) == -4 -/// ``` -pub fn truncate(self: Rational) -> Int { - let Rational { numerator: a_n, denominator: a_d } = self - builtin.quotient_integer(a_n, a_d) -} - -test truncate_1() { - and { - (truncate(ratio(5, 2)) == 2)?, - (truncate(ratio(5, 3)) == 1)?, - (truncate(ratio(5, 4)) == 1)?, - (truncate(ratio(5, 5)) == 1)?, - (truncate(ratio(5, 6)) == 0)?, - (truncate(ratio(8, 3)) == 2)?, - (truncate(ratio(-14, 3)) == -4)?, - } -} - -/// Returns the greatest `Int` no greater than a given `Rational` -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.floor(x) == 0 -/// -/// expect Some(y) = rational.new(44, 14) -/// rational.floor(y) == 3 -/// -/// expect Some(z) = rational.new(-14, 3) -/// rational.floor(z) == -5 -/// ``` -pub fn floor(self: Rational) -> Int { - let Rational { numerator: a_n, denominator: a_d } = self - a_n / a_d -} - -test floor_1() { - and { - (floor(ratio(5, 2)) == 2)?, - (floor(ratio(5, 3)) == 1)?, - (floor(ratio(5, 4)) == 1)?, - (floor(ratio(5, 5)) == 1)?, - (floor(ratio(5, 6)) == 0)?, - (floor(ratio(8, 3)) == 2)?, - (floor(ratio(-14, 3)) == -5)?, - } -} - -/// Returns the smallest `Int` not less than a given `Rational` -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.ceil(x) == 1 -/// -/// expect Some(y) = rational.new(44, 14) -/// rational.ceil(y) == 4 -/// -/// expect Some(z) = rational.new(-14, 3) -/// rational.ceil(z) == -4 -/// ``` -pub fn ceil(self: Rational) -> Int { - let Rational { numerator, denominator } = self - if builtin.remainder_integer(numerator, denominator) > 0 { - builtin.quotient_integer(numerator, denominator) + 1 - } else { - builtin.quotient_integer(numerator, denominator) - } -} - -test ceil_1() { - and { - (ceil(ratio(13, 5)) == 3)?, - (ceil(ratio(15, 5)) == 3)?, - (ceil(ratio(16, 5)) == 4)?, - (ceil(ratio(-3, 5)) == 0)?, - (ceil(ratio(-5, 5)) == -1)?, - (ceil(ratio(-14, 3)) == -4)?, - (ceil(ratio(-14, 6)) == -2)?, - (ceil(ratio(44, 14)) == 4)?, - } -} - -/// Returns the proper fraction of a given `Rational` `r`. That is, a 2-tuple of -/// an `Int` and `Rational` (n, f) such that: -/// -/// - `r = n + f`; -/// - `n` and `f` have the same sign as `r`; -/// - `f` has an absolute value less than 1. -pub fn proper_fraction(self: Rational) -> (Int, Rational) { - let Rational { numerator, denominator } = self - ( - builtin.quotient_integer(numerator, denominator), - Rational { - numerator: builtin.remainder_integer(numerator, denominator), - denominator, - }, - ) -} - -test proper_fraction_1() { - let r = ratio(10, 7) - let (n, f) = proper_fraction(r) - and { - (n == 1)?, - (f == ratio(3, 7))?, - (r == add(from_int(n), f))?, - } -} - -test proper_fraction_2() { - let r = ratio(-10, 7) - let (n, f) = proper_fraction(r) - and { - (n == -1)?, - (f == ratio(-3, 7))?, - (r == add(from_int(n), f))?, - } -} - -test proper_fraction_3() { - let r = ratio(4, 2) - let (n, f) = proper_fraction(r) - and { - (n == 2)?, - (f == ratio(0, 2))?, - (r == add(from_int(n), f))?, - } -} - -/// Round the argument to the nearest whole number. If the argument is -/// equidistant between two values, the greater value is returned (it -/// rounds half towards positive infinity). -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 3) -/// rational.round(x) == 1 -/// -/// expect Some(y) = rational.new(3, 2) -/// rational.round(y) == 2 -/// -/// expect Some(z) = rational.new(-3, 2) -/// rational.round(z) == -1 -/// ``` -/// -/// ⚠️ This behaves differently than _Haskell_. If you're coming from -/// `PlutusTx`, beware that in Haskell, rounding on equidistant values depends on the -/// whole number being odd or even. If you need this behaviour, use [`round_even`](#round_even). -pub fn round(self: Rational) -> Int { - let (n, f) = proper_fraction(self) - - let is_negative = f.numerator < 0 - - when compare(abs(f), ratio(1, 2)) is { - Less -> n - Equal -> - if is_negative { - n - } else { - n + 1 - } - Greater -> - if is_negative { - n - 1 - } else { - n + 1 - } - } -} - -test round_1() { - and { - (round(ratio(10, 7)) == 1)?, - (round(ratio(11, 7)) == 2)?, - (round(ratio(3, 2)) == 2)?, - (round(ratio(5, 2)) == 3)?, - (round(ratio(-3, 2)) == -1)?, - (round(ratio(-2, 3)) == -1)?, - (round(ratio(-10, 7)) == -1)?, - (round(ratio(4, 2)) == 2)?, - } -} - -/// Round the argument to the nearest whole number. If the argument is -/// equidistant between two values, it returns the value that is even (it -/// rounds half to even, also known as 'banker's rounding'). -/// -/// ```aiken -/// expect Some(w) = rational.new(2, 3) -/// rational.round_even(w) == 1 -/// -/// expect Some(x) = rational.new(3, 2) -/// rational.round_even(x) == 2 -/// -/// expect Some(y) = rational.new(5, 2) -/// rational.round_even(y) == 2 -/// -/// expect Some(y) = rational.new(-3, 2) -/// rational.round_even(y) == -2 -/// ``` -pub fn round_even(self: Rational) -> Int { - let (n, f) = proper_fraction(self) - - let m = - when compare(f, ratio(0, 1)) is { - Less -> -1 - _ -> 1 - } - - let is_even = n % 2 == 0 - - when compare(abs(f), ratio(1, 2)) is { - Less -> n - Equal -> - if is_even { - n - } else { - n + m - } - Greater -> n + m - } -} - -test round_even_1() { - and { - (round_even(ratio(10, 7)) == 1)?, - (round_even(ratio(11, 7)) == 2)?, - (round_even(ratio(3, 2)) == 2)?, - (round_even(ratio(5, 2)) == 2)?, - (round_even(ratio(-3, 2)) == -2)?, - (round_even(ratio(-2, 3)) == -1)?, - (round_even(ratio(-10, 7)) == -1)?, - (round_even(ratio(4, 2)) == 2)?, - } -} - -/// Change the sign of a `Rational`. -/// -/// ```aiken -/// expect Some(x) = rational.new(3, 2) -/// expect Some(y) = rational.new(-3, 2) -/// -/// rational.negate(x) == y -/// rational.negate(y) == x -/// ``` -pub fn negate(a: Rational) -> Rational { - let Rational { numerator: a_n, denominator: a_d } = a - Rational { numerator: -a_n, denominator: a_d } -} - -test negate_1() { - and { - (negate(ratio(5, 2)) == ratio(-5, 2))?, - (negate(ratio(-5, 2)) == ratio(5, 2))?, - (negate(negate(ratio(5, 2))) == ratio(5, 2))?, - } -} - -/// Absolute value of a `Rational`. -/// -/// ```aiken -/// expect Some(x) = rational.new(3, 2) -/// expect Some(y) = rational.new(-3, 2) -/// -/// rational.abs(x) == x -/// rational.abs(y) == x -/// ``` -pub fn abs(self: Rational) -> Rational { - let Rational { numerator: a_n, denominator: a_d } = self - Rational { numerator: math.abs(a_n), denominator: a_d } -} - -test abs_examples() { - and { - (abs(ratio(5, 2)) == ratio(5, 2))?, - (abs(ratio(-5, 2)) == ratio(5, 2))?, - (abs(ratio(5, 2)) == abs(ratio(-5, 2)))?, - } -} - -/// Reciprocal of a `Rational` number. That is, a new `Rational` where the -/// numerator and denominator have been swapped. -/// -/// ```aiken -/// expect Some(x) = rational.new(2, 5) -/// rational.reciprocal(x) == rational.new(5, 2) -/// -/// let y = rational.zero() -/// rational.reciprocal(y) == None -/// ``` -pub fn reciprocal(self: Rational) -> Option { - let Rational { numerator: a_n, denominator: a_d } = self - if a_n < 0 { - Some(Rational { numerator: -a_d, denominator: -a_n }) - } else if a_n > 0 { - Some(Rational { numerator: a_d, denominator: a_n }) - } else { - None - } -} - -test reciprocal_1() { - and { - (reciprocal(ratio(5, 2)) == new(2, 5))?, - (reciprocal(ratio(-5, 2)) == new(-2, 5))?, - (reciprocal(ratio(0, 2)) == None)?, - (reciprocal(ratio(2, 3)) == new(3, 2))?, - (reciprocal(ratio(-2, 3)) == new(-3, 2))?, - } -} - -/// Reduce a rational to its irreducible form. This operation makes the -/// numerator and denominator coprime. -/// -/// ```aiken -/// expect Some(x) = rational.new(80, 200) -/// Some(rational.reduce(x)) == rational.new(2, 5) -/// ``` -pub fn reduce(self: Rational) -> Rational { - let Rational { numerator: a_n, denominator: a_d } = self - let d = math.gcd(a_n, a_d) - Rational { numerator: a_n / d, denominator: a_d / d } + +test sub_2() { + sub(ratio(2, 3), ratio(-3, 4)) == ratio(17, 12) } -test reduce_1() { - and { - (reduce(ratio(80, 200)) == ratio(2, 5))?, - (reduce(ratio(-5, 1)) == ratio(-5, 1))?, - (reduce(ratio(0, 3)) == ratio(0, 1))?, - } +test sub_3() { + sub(ratio(-2, 3), ratio(3, 4)) == ratio(-17, 12) } +// ### Ordering + /// Compare two rationals for an ordering. This is safe to use even for /// non-reduced rationals. /// @@ -709,6 +477,8 @@ test compare_with_lt() { lt(x, y)? && !lt(y, x)? && !lt(x, x)? } +// ### Means + /// Calculate the arithmetic mean between two `Rational` values. /// /// ```aiken @@ -805,3 +575,250 @@ test geometric_mean5() { expect Some(yi) = reciprocal(y) geometric_mean(x, yi) == new(258, 9398) } + +// ## Transforming + +/// Returns the smallest `Int` not less than a given `Rational` +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.ceil(x) == 1 +/// +/// expect Some(y) = rational.new(44, 14) +/// rational.ceil(y) == 4 +/// +/// expect Some(z) = rational.new(-14, 3) +/// rational.ceil(z) == -4 +/// ``` +pub fn ceil(self: Rational) -> Int { + let Rational { numerator, denominator } = self + if builtin.remainder_integer(numerator, denominator) > 0 { + builtin.quotient_integer(numerator, denominator) + 1 + } else { + builtin.quotient_integer(numerator, denominator) + } +} + +test ceil_1() { + and { + (ceil(ratio(13, 5)) == 3)?, + (ceil(ratio(15, 5)) == 3)?, + (ceil(ratio(16, 5)) == 4)?, + (ceil(ratio(-3, 5)) == 0)?, + (ceil(ratio(-5, 5)) == -1)?, + (ceil(ratio(-14, 3)) == -4)?, + (ceil(ratio(-14, 6)) == -2)?, + (ceil(ratio(44, 14)) == 4)?, + } +} + +/// Returns the greatest `Int` no greater than a given `Rational` +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.floor(x) == 0 +/// +/// expect Some(y) = rational.new(44, 14) +/// rational.floor(y) == 3 +/// +/// expect Some(z) = rational.new(-14, 3) +/// rational.floor(z) == -5 +/// ``` +pub fn floor(self: Rational) -> Int { + let Rational { numerator: a_n, denominator: a_d } = self + a_n / a_d +} + +test floor_1() { + and { + (floor(ratio(5, 2)) == 2)?, + (floor(ratio(5, 3)) == 1)?, + (floor(ratio(5, 4)) == 1)?, + (floor(ratio(5, 5)) == 1)?, + (floor(ratio(5, 6)) == 0)?, + (floor(ratio(8, 3)) == 2)?, + (floor(ratio(-14, 3)) == -5)?, + } +} + +/// Returns the proper fraction of a given `Rational` `r`. That is, a 2-tuple of +/// an `Int` and `Rational` (n, f) such that: +/// +/// - `r = n + f`; +/// - `n` and `f` have the same sign as `r`; +/// - `f` has an absolute value less than 1. +pub fn proper_fraction(self: Rational) -> (Int, Rational) { + let Rational { numerator, denominator } = self + ( + builtin.quotient_integer(numerator, denominator), + Rational { + numerator: builtin.remainder_integer(numerator, denominator), + denominator, + }, + ) +} + +test proper_fraction_1() { + let r = ratio(10, 7) + let (n, f) = proper_fraction(r) + and { + (n == 1)?, + (f == ratio(3, 7))?, + (r == add(from_int(n), f))?, + } +} + +test proper_fraction_2() { + let r = ratio(-10, 7) + let (n, f) = proper_fraction(r) + and { + (n == -1)?, + (f == ratio(-3, 7))?, + (r == add(from_int(n), f))?, + } +} + +test proper_fraction_3() { + let r = ratio(4, 2) + let (n, f) = proper_fraction(r) + and { + (n == 2)?, + (f == ratio(0, 2))?, + (r == add(from_int(n), f))?, + } +} + +/// Round the argument to the nearest whole number. If the argument is +/// equidistant between two values, the greater value is returned (it +/// rounds half towards positive infinity). +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.round(x) == 1 +/// +/// expect Some(y) = rational.new(3, 2) +/// rational.round(y) == 2 +/// +/// expect Some(z) = rational.new(-3, 2) +/// rational.round(z) == -1 +/// ``` +/// +/// > [!CAUTION] +/// > This behaves differently than _Haskell_. If you're coming from `PlutusTx`, beware that in Haskell, rounding on equidistant values depends on the whole number being odd or even. +/// > If you need this behaviour, use [`round_even`](#round_even). +pub fn round(self: Rational) -> Int { + let (n, f) = proper_fraction(self) + + let is_negative = f.numerator < 0 + + when compare(abs(f), ratio(1, 2)) is { + Less -> n + Equal -> + if is_negative { + n + } else { + n + 1 + } + Greater -> + if is_negative { + n - 1 + } else { + n + 1 + } + } +} + +test round_1() { + and { + (round(ratio(10, 7)) == 1)?, + (round(ratio(11, 7)) == 2)?, + (round(ratio(3, 2)) == 2)?, + (round(ratio(5, 2)) == 3)?, + (round(ratio(-3, 2)) == -1)?, + (round(ratio(-2, 3)) == -1)?, + (round(ratio(-10, 7)) == -1)?, + (round(ratio(4, 2)) == 2)?, + } +} + +/// Round the argument to the nearest whole number. If the argument is +/// equidistant between two values, it returns the value that is even (it +/// rounds half to even, also known as 'banker's rounding'). +/// +/// ```aiken +/// expect Some(w) = rational.new(2, 3) +/// rational.round_even(w) == 1 +/// +/// expect Some(x) = rational.new(3, 2) +/// rational.round_even(x) == 2 +/// +/// expect Some(y) = rational.new(5, 2) +/// rational.round_even(y) == 2 +/// +/// expect Some(y) = rational.new(-3, 2) +/// rational.round_even(y) == -2 +/// ``` +pub fn round_even(self: Rational) -> Int { + let (n, f) = proper_fraction(self) + + let m = + when compare(f, ratio(0, 1)) is { + Less -> -1 + _ -> 1 + } + + let is_even = n % 2 == 0 + + when compare(abs(f), ratio(1, 2)) is { + Less -> n + Equal -> + if is_even { + n + } else { + n + m + } + Greater -> n + m + } +} + +test round_even_1() { + and { + (round_even(ratio(10, 7)) == 1)?, + (round_even(ratio(11, 7)) == 2)?, + (round_even(ratio(3, 2)) == 2)?, + (round_even(ratio(5, 2)) == 2)?, + (round_even(ratio(-3, 2)) == -2)?, + (round_even(ratio(-2, 3)) == -1)?, + (round_even(ratio(-10, 7)) == -1)?, + (round_even(ratio(4, 2)) == 2)?, + } +} + +/// Returns the nearest `Int` between zero and a given `Rational`. +/// +/// ```aiken +/// expect Some(x) = rational.new(2, 3) +/// rational.truncate(x) == 0 +/// +/// expect Some(y) = rational.new(44, 14) +/// rational.truncate(y) == 3 +/// +/// expect Some(z) = rational.new(-14, 3) +/// rational.truncate(z) == -4 +/// ``` +pub fn truncate(self: Rational) -> Int { + let Rational { numerator: a_n, denominator: a_d } = self + builtin.quotient_integer(a_n, a_d) +} + +test truncate_1() { + and { + (truncate(ratio(5, 2)) == 2)?, + (truncate(ratio(5, 3)) == 1)?, + (truncate(ratio(5, 4)) == 1)?, + (truncate(ratio(5, 5)) == 1)?, + (truncate(ratio(5, 6)) == 0)?, + (truncate(ratio(8, 3)) == 2)?, + (truncate(ratio(-14, 3)) == -4)?, + } +} diff --git a/lib/aiken/option.ak b/lib/aiken/option.ak index 8ad35fd..cf5ef7d 100644 --- a/lib/aiken/option.ak +++ b/lib/aiken/option.ak @@ -1,3 +1,90 @@ +//// A type to capture optional results; useful for handling errors. +//// +//// Note that the `Option` type and its constructors are readily available in Aiken. They are part of the [Prelude](https://aiken-lang.github.io/prelude/aiken.html#Option) module imported by default in every module. + +// ## Inspecting + +/// Asserts whether an option is `None`. +pub fn is_none(self: Option) -> Bool { + when self is { + Some(_) -> False + _ -> True + } +} + +test is_none_1() { + is_none(Some(0)) == False +} + +test is_none_2() { + is_none(None) == True +} + +/// Asserts whether an option is `Some`, irrespective of the value it contains. +pub fn is_some(self: Option) -> Bool { + when self is { + Some(_) -> True + _ -> False + } +} + +test is_some_1() { + is_some(Some(0)) == True +} + +test is_some_2() { + is_some(None) == False +} + +// ## Combining + +/// Chain together many computations that may fail. +/// +/// ```aiken +/// self +/// |> dict.get(policy_id) +/// |> option.and_then(dict.get(_, asset_name)) +/// |> option.or_else(0) +/// ``` +pub fn and_then( + self: Option, + then: fn(a) -> Option, +) -> Option { + when self is { + None -> None + Some(a) -> then(a) + } +} + +fn try_decrement(n: Int) -> Option { + if n > 0 { + Some(n - 1) + } else { + None + } +} + +test and_then_1() { + let result = + None + |> and_then(try_decrement) + result == None +} + +test and_then_2() { + let result = + Some(14) + |> and_then(try_decrement) + result == Some(13) +} + +test and_then_3() { + let result = + Some(0) + |> and_then(try_decrement) + result == None +} + /// Picks the first element which is not None. If there's no such element, return None. /// /// ```aiken @@ -29,47 +116,51 @@ test choice_3() { Some(1) == choice([None, Some(1)]) } -/// Provide a default value, turning an optional value into a normal value. +/// Converts from `Option>` to `Option`. /// /// ```aiken -/// option.or_else(None, "aiken") == "aiken" -/// option.or_else(Some(42), 14) == 42 +/// option.flatten(Some(Some(42))) == Some(42) +/// option.flatten(Some(None)) == None +/// option.flatten(None) == None /// ``` -pub fn or_else(self: Option, default: a) -> a { - when self is { - None -> default - Some(a) -> a +/// +/// Flattening only removes one level of nesting at a time: +/// +/// ```aiken +/// flatten(Some(Some(Some(42)))) == Some(Some(42)) +/// Some(Some(Some(42))) |> flatten |> flatten == Some(42) +/// ``` +pub fn flatten(opt: Option>) -> Option { + when opt is { + Some(inner) -> inner + None -> None } } -test or_else_1() { - or_else(None, "aiken") == "aiken" +test flatten_1() { + let x: Option> = Some(Some(6)) + Some(6) == flatten(x) } -test or_else_2() { - or_else(Some(42), 14) == 42 +test flatten_2() { + let x: Option> = Some(None) + None == flatten(x) } -/// Like [`or_else`](#or_else) but allows returning an `Option`. -/// This is effectively mapping the error branch. -/// -/// ```aiken -/// option.or_try(None, fn(_) { Some("aiken") }) == Some("aiken") -/// option.or_try(Some(42), fn(_) { Some(14) }) == Some(42) -/// ``` -pub fn or_try(self: Option, compute_default: fn() -> Option) -> Option { - when self is { - None -> compute_default() - _ -> self - } +test flatten_3() { + let x: Option> = None + None == flatten(x) } -test or_try_1() { - or_try(None, fn() { Some("aiken") }) == Some("aiken") -} +test flatten_4() { + let x: Option>> = Some(Some(Some(6))) -test or_try_2() { - or_try(Some(42), fn() { fail }) == Some(42) + let result = + x + |> flatten + |> flatten + + Some(6) == result } /// Apply a function to the inner value of an [`Option`](#option) @@ -174,128 +265,48 @@ test map3_3() { map3(Some(14), Some(42), Some(1337), fn(a, b, c) { c - a + b }) == Some(1365) } -/// Chain together many computations that may fail. +/// Like [`or_else`](#or_else) but allows returning an `Option`. +/// This is effectively mapping the error branch. /// /// ```aiken -/// self -/// |> dict.get(policy_id) -/// |> option.and_then(dict.get(_, asset_name)) -/// |> option.or_else(0) +/// option.or_try(None, fn(_) { Some("aiken") }) == Some("aiken") +/// option.or_try(Some(42), fn(_) { Some(14) }) == Some(42) +/// option.or_try(None, fn (_) { fail }) => 💥 /// ``` -pub fn and_then( - self: Option, - then: fn(a) -> Option, -) -> Option { +pub fn or_try(self: Option, compute_default: fn() -> Option) -> Option { when self is { - None -> None - Some(a) -> then(a) - } -} - -fn try_decrement(n: Int) -> Option { - if n > 0 { - Some(n - 1) - } else { - None + None -> compute_default() + _ -> self } } -test and_then_1() { - let result = - None - |> and_then(try_decrement) - result == None +test or_try_1() { + or_try(None, fn() { Some("aiken") }) == Some("aiken") } -test and_then_2() { - let result = - Some(14) - |> and_then(try_decrement) - result == Some(13) +test or_try_2() { + or_try(Some(42), fn() { fail }) == Some(42) } -test and_then_3() { - let result = - Some(0) - |> and_then(try_decrement) - result == None -} +// ## Transforming -/// Converts from `Option>` to `Option`. -/// -/// ```aiken -/// option.flatten(Some(Some(42))) == Some(42) -/// option.flatten(Some(None)) == None -/// option.flatten(None) == None -/// ``` -/// -/// Flattening only removes one level of nesting at a time: +/// Provide a default value, turning an optional value into a normal value. /// /// ```aiken -/// flatten(Some(Some(Some(42)))) == Some(Some(42)) -/// Some(Some(Some(42))) |> flatten |> flatten == Some(42) +/// option.or_else(None, "aiken") == "aiken" +/// option.or_else(Some(42), 14) == 42 /// ``` -pub fn flatten(opt: Option>) -> Option { - when opt is { - Some(inner) -> inner - None -> None - } -} - -test flatten_1() { - let x: Option> = Some(Some(6)) - Some(6) == flatten(x) -} - -test flatten_2() { - let x: Option> = Some(None) - None == flatten(x) -} - -test flatten_3() { - let x: Option> = None - None == flatten(x) -} - -test flatten_4() { - let x: Option>> = Some(Some(Some(6))) - - let result = - x - |> flatten - |> flatten - - Some(6) == result -} - -/// Asserts whether an option is `Some`, irrespective of the value it contains. -pub fn is_some(self: Option) -> Bool { - when self is { - Some(_) -> True - _ -> False - } -} - -test is_some_1() { - is_some(Some(0)) == True -} - -test is_some_2() { - is_some(None) == False -} - -/// Asserts whether an option is `None`. -pub fn is_none(self: Option) -> Bool { +pub fn or_else(self: Option, default: a) -> a { when self is { - Some(_) -> False - _ -> True + None -> default + Some(a) -> a } } -test is_none_1() { - is_none(Some(0)) == False +test or_else_1() { + or_else(None, "aiken") == "aiken" } -test is_none_2() { - is_none(None) == True +test or_else_2() { + or_else(Some(42), 14) == 42 } diff --git a/lib/aiken/bytearray.ak b/lib/aiken/primitive/bytearray.ak similarity index 72% rename from lib/aiken/bytearray.ak rename to lib/aiken/primitive/bytearray.ak index 982a2fc..181be84 100644 --- a/lib/aiken/bytearray.ak +++ b/lib/aiken/primitive/bytearray.ak @@ -2,48 +2,288 @@ use aiken/builtin use aiken/math use aiken/option -/// Compare two bytearrays lexicographically. +pub type Byte = + Int + +// ## Constructing + +/// Encode an integer value as a Big-Endian (most-significant bytes first) `ByteArray`. +/// The size is the expected size in number of bytes. +/// +/// > [!IMPORTANT] +/// > This function halts the program if the value cannot fit in the given size. When the +/// > size is _too large_, the array is left-padded with zeroes. /// /// ```aiken -/// bytearray.compare(#"00", #"FF") == Less -/// bytearray.compare(#"42", #"42") == Equal -/// bytearray.compare(#"FF", #"00") == Greater +/// bytearray.from_int_big_endian(1_000_000, 3) == #"0f4240" +/// bytearray.from_int_big_endian(1_000_000, 5) == #"00000f4240" +/// bytearray.from_int_big_endian(0, 8) == #"0000000000000000" +/// bytearray.from_int_big_endian(1_000_000, 1) => 💥 /// ``` -pub fn compare(left: ByteArray, right: ByteArray) -> Ordering { - if builtin.less_than_bytearray(left, right) { - Less - } else if builtin.equals_bytearray(left, right) { - Equal +pub fn from_int_big_endian(self: Int, size: Int) -> ByteArray { + builtin.integer_to_bytearray(True, size, self) +} + +test from_int_big_endian_1() { + from_int_big_endian(1_000_000, 3) == #"0f4240" +} + +test from_int_big_endian_2() { + from_int_big_endian(1_000_000, 5) == #"00000f4240" +} + +test from_int_big_endian_3() { + from_int_big_endian(0, 8) == #"0000000000000000" +} + +test from_int_big_endian_4() fail { + from_int_big_endian(1_000_000, 1) == #"40" +} + +/// Encode an integer value as a Little-Endian (least-significant bytes first) `ByteArray`. +/// The size is the expected size in number of bytes. +/// +/// > [!IMPORTANT] +/// > This function halts the program if the value cannot fit in the given size. When the +/// > size is _too large_, the array is right-padded with zeroes. +/// +/// ```aiken +/// bytearray.from_int_big_endian(1_000_000, 3) == #"0f4240" +/// bytearray.from_int_big_endian(1_000_000, 5) == #"00000f4240" +/// bytearray.from_int_big_endian(0, 8) == #"0000000000000000" +/// bytearray.from_int_big_endian(1_000_000, 1) => 💥 +/// ``` +pub fn from_int_little_endian(self: Int, size: Int) -> ByteArray { + builtin.integer_to_bytearray(False, size, self) +} + +test from_int_little_endian_1() { + from_int_little_endian(1_000_000, 3) == #"40420f" +} + +test from_int_little_endian_2() { + from_int_little_endian(1_000_000, 5) == #"40420f0000" +} + +test from_int_little_endian_3() { + from_int_little_endian(0, 8) == #"0000000000000000" +} + +test from_int_little_endian_4() fail { + from_int_little_endian(1_000_000, 1) == #"40" +} + +/// Convert a `String` into a `ByteArray`. +/// +/// ```aiken +/// bytearray.from_string(@"ABC") == #"414243" +/// ``` +pub fn from_string(str: String) -> ByteArray { + builtin.encode_utf8(str) +} + +test from_string_1() { + from_string(@"") == "" +} + +test from_string_2() { + from_string(@"ABC") == #"414243" +} + +/// Add a byte element in front of a `ByteArray`. When the given byte is +/// greater than 255, it wraps-around. **PlutusV2 behavior** So 256 is mapped to 0, 257 to 1, and so +/// forth. +/// In PlutusV3 this will error instead of wrapping around. +/// +/// ```aiken +/// bytearray.push(#"", 0) == #"00" +/// bytearray.push(#"0203", 1) == #"010203" +/// bytearray.push(#"0203", 257) == #"010203" +/// ``` +pub fn push(self: ByteArray, byte: Byte) -> ByteArray { + builtin.cons_bytearray(byte, self) +} + +test push_1() { + push(#[], 0) == #[0] +} + +test push_2() { + push(#[2, 3], 1) == #[1, 2, 3] +} + +test push_3() { + let x = 257 + push(#[2, 3], x) == #[1, 2, 3] +} + +// ## Inspecting + +/// Get the `Byte` at the given index, or crash. +/// +/// > [!WARNING] +/// > This functions halts the program if there's no byte at the given index. +pub fn at(self: ByteArray, index: Int) -> Byte { + builtin.index_bytearray(self, index) +} + +/// Search the start and end positions of a sub-array in a `ByteArray`. +/// +/// ```aiken +/// bytearray.index_of("Hello, World!", "World") == Some((7, 11)) +/// bytearray.index_of("Hello, World!", "foo") == None +/// bytearray.index_of("Hello, World!", "!") == Some((12, 12)) +/// bytearray.index_of("Hello, World!", "o") == Some((4, 4)) +/// bytearray.index_of("Hello, World!", "Hello, World!") == Some((0, 12)) +/// ``` +pub fn index_of(self: ByteArray, bytes: ByteArray) -> Option<(Int, Int)> { + let offset = length(bytes) + + do_index_of(self, bytes, 0, offset, length(self)) + |> option.map(fn(ix) { (ix, ix + offset - 1) }) +} + +fn do_index_of( + self: ByteArray, + bytes: ByteArray, + cursor: Int, + offset: Int, + size: Int, +) -> Option { + if cursor + offset > size { + None } else { - Greater + if builtin.slice_bytearray(cursor, offset, self) == bytes { + Some(cursor) + } else { + do_index_of(self, bytes, cursor + 1, offset, size) + } } } -/// Combine two `ByteArray` together. +test index_of_1() { + index_of("Hello, World!", "World") == Some((7, 11)) +} + +test index_of_2() { + index_of("Hello, World!", "foo") == None +} + +test index_of_3() { + index_of("Hello, World!", "!") == Some((12, 12)) +} + +test index_of_4() { + index_of("Hello, World!", "o") == Some((4, 4)) +} + +test index_of_5() { + index_of("Hello, World!", "Hello, World!") == Some((0, 12)) +} + +/// Returns `True` when the given `ByteArray` is empty. /// /// ```aiken -/// bytearray.concat(left: #[1, 2, 3], right: #[4, 5, 6]) == #[1, 2, 3, 4, 5, 6] +/// bytearray.is_empty(#"") == True +/// bytearray.is_empty(#"00ff") == False /// ``` -pub fn concat(left: ByteArray, right: ByteArray) -> ByteArray { - builtin.append_bytearray(left, right) +pub fn is_empty(self: ByteArray) -> Bool { + builtin.length_of_bytearray(self) == 0 } -test concat_1() { - concat(#"", #"") == #"" +test is_empty_1() { + is_empty(#"") == True } -test concat_2() { - concat(#"", #"01") == #"01" +test is_empty_2() { + is_empty(#"01") == False } -test concat_3() { - concat(#"0102", #"") == #"0102" +/// Returns the number of bytes in a `ByteArray`. +/// +/// ```aiken +/// bytearray.length(#[1, 2, 3]) == 3 +/// ``` +pub fn length(self: ByteArray) -> Int { + builtin.length_of_bytearray(self) } -test concat_4() { - concat(#"0102", #"0304") == #"01020304" +test length_1() { + length(#"") == 0 +} + +test length_2() { + length(#"010203") == 3 +} + +/// Checks whether a bit (Most-Significant-Bit first) is set in the given 'ByteArray'. +/// +/// For example, consider the following bytearray: `#"8b765f"`. It can also be written as the +/// following bits sequence: +/// +/// `8` | `b` | `7` | `6` | `5` | `f` +/// --- | --- | --- | --- | --- | --- +/// `1000` | `1011` | `0111` | `0110` | `0101` | `1111` +/// +/// And thus, we have: +/// +/// ```aiken +/// test_bit(#"8b765f", 0) == True +/// test_bit(#"8b765f", 1) == False +/// test_bit(#"8b765f", 2) == False +/// test_bit(#"8b765f", 3) == False +/// test_bit(#"8b765f", 7) == True +/// test_bit(#"8b765f", 8) == False +/// test_bit(#"8b765f", 20) == True +/// test_bit(#"8b765f", 21) == True +/// test_bit(#"8b765f", 22) == True +/// test_bit(#"8b765f", 23) == True +/// ``` +pub fn test_bit(self: ByteArray, ix: Int) -> Bool { + builtin.less_than_equals_bytearray( + #[128], + builtin.cons_bytearray( + builtin.index_bytearray(self, ix / 8) * math.pow2(ix % 8), + "", + ), + ) +} + +test test_bit_0() { + test_bit(#"8b765f", 0) } +test test_bit_1() { + !test_bit(#"8b765f", 1) +} + +test test_bit_2() { + !test_bit(#"8b765f", 2) +} + +test test_bit_3() { + !test_bit(#"8b765f", 3) +} + +test test_bit_7() { + test_bit(#"8b765f", 7) +} + +test test_bit_8() { + !test_bit(#"8b765f", 8) +} + +test test_bit_20_21_22_23() { + and { + test_bit(#"8b765f", 20), + test_bit(#"8b765f", 21), + test_bit(#"8b765f", 22), + test_bit(#"8b765f", 23), + } +} + +// ## Modifying + /// Returns the suffix of a `ByteArray` after `n` elements. /// /// ```aiken @@ -68,10 +308,116 @@ test drop_3() { drop(x, 1) == #"" } -test drop_4() { - let x = #"" - drop(x, 2) == #"" -} +test drop_4() { + let x = #"" + drop(x, 2) == #"" +} + +/// Extract a `ByteArray` as a slice of another `ByteArray`. +/// +/// Indexes are 0-based and inclusive. +/// +/// ```aiken +/// bytearray.slice(#[0, 1, 2, 3, 4, 5, 6], start: 1, end: 3) == #[1, 2, 3] +/// ``` +pub fn slice(self: ByteArray, start: Int, end: Int) -> ByteArray { + builtin.slice_bytearray(start, end - start + 1, self) +} + +test slice_1() { + slice(#"", 1, 2) == #"" +} + +test slice_2() { + slice(#"010203", 1, 2) == #"0203" +} + +test slice_3() { + slice(#"010203", 0, 42) == #"010203" +} + +test slice_4() { + slice(#[0, 1, 2, 3, 4], 0, 3) == #[0, 1, 2, 3] +} + +test slice_5() { + slice(#[0, 1, 2, 3, 4], 1, 2) == #[1, 2] +} + +/// Returns the n-length prefix of a `ByteArray`. +/// +/// ```aiken +/// bytearray.take(#[1, 2, 3], n: 2) == #[1, 2] +/// ``` +pub fn take(self: ByteArray, n: Int) -> ByteArray { + builtin.slice_bytearray(0, n, self) +} + +test take_1() { + let x = #"01020304050607" + take(x, 2) == #"0102" +} + +test take_2() { + let x = #"01020304050607" + take(x, 0) == #"" +} + +test take_3() { + let x = #"01" + take(x, 1) == x +} + +test take_4() { + let x = #"010203" + take(x, 0) == #"" +} + +// ## Combining + +/// Combine two `ByteArray` together. +/// +/// ```aiken +/// bytearray.concat(left: #[1, 2, 3], right: #[4, 5, 6]) == #[1, 2, 3, 4, 5, 6] +/// ``` +pub fn concat(left: ByteArray, right: ByteArray) -> ByteArray { + builtin.append_bytearray(left, right) +} + +test concat_1() { + concat(#"", #"") == #"" +} + +test concat_2() { + concat(#"", #"01") == #"01" +} + +test concat_3() { + concat(#"0102", #"") == #"0102" +} + +test concat_4() { + concat(#"0102", #"0304") == #"01020304" +} + +/// Compare two bytearrays lexicographically. +/// +/// ```aiken +/// bytearray.compare(#"00", #"FF") == Less +/// bytearray.compare(#"42", #"42") == Equal +/// bytearray.compare(#"FF", #"00") == Greater +/// ``` +pub fn compare(left: ByteArray, right: ByteArray) -> Ordering { + if builtin.less_than_bytearray(left, right) { + Less + } else if builtin.equals_bytearray(left, right) { + Equal + } else { + Greater + } +} + +// ## Transforming /// Left-fold over bytes of a [`ByteArray`](https://aiken-lang.github.io/prelude/aiken.html#ByteArray). Note that every byte given to the callback function is comprised between 0 and 255. /// @@ -171,139 +517,6 @@ test foldr_3() { foldr(#[1, 2, 3, 4, 5], #"", flip(push)) == #[1, 2, 3, 4, 5] } -/// Search the start and end positions of a sub-array in a `ByteArray`. -/// -/// ```aiken -/// bytearray.index_of("Hello, World!", "World") == Some((7, 11)) -/// bytearray.index_of("Hello, World!", "foo") == None -/// bytearray.index_of("Hello, World!", "!") == Some((12, 12)) -/// bytearray.index_of("Hello, World!", "o") == Some((4, 4)) -/// bytearray.index_of("Hello, World!", "Hello, World!") == Some((0, 12)) -/// ``` -pub fn index_of(self: ByteArray, bytes: ByteArray) -> Option<(Int, Int)> { - let offset = length(bytes) - - do_index_of(self, bytes, 0, offset, length(self)) - |> option.map(fn(ix) { (ix, ix + offset - 1) }) -} - -fn do_index_of( - self: ByteArray, - bytes: ByteArray, - cursor: Int, - offset: Int, - size: Int, -) -> Option { - if cursor + offset > size { - None - } else { - if builtin.slice_bytearray(cursor, offset, self) == bytes { - Some(cursor) - } else { - do_index_of(self, bytes, cursor + 1, offset, size) - } - } -} - -test index_of_1() { - index_of("Hello, World!", "World") == Some((7, 11)) -} - -test index_of_2() { - index_of("Hello, World!", "foo") == None -} - -test index_of_3() { - index_of("Hello, World!", "!") == Some((12, 12)) -} - -test index_of_4() { - index_of("Hello, World!", "o") == Some((4, 4)) -} - -test index_of_5() { - index_of("Hello, World!", "Hello, World!") == Some((0, 12)) -} - -/// Returns the number of bytes in a `ByteArray`. -/// -/// ```aiken -/// bytearray.length(#[1, 2, 3]) == 3 -/// ``` -pub fn length(self: ByteArray) -> Int { - builtin.length_of_bytearray(self) -} - -test length_1() { - length(#"") == 0 -} - -test length_2() { - length(#"010203") == 3 -} - -/// Returns `True` when the given `ByteArray` is empty. -/// -/// ```aiken -/// bytearray.is_empty(#"") == True -/// bytearray.is_empty(#"00ff") == False -/// ``` -pub fn is_empty(self: ByteArray) -> Bool { - builtin.length_of_bytearray(self) == 0 -} - -test is_empty_1() { - is_empty(#"") == True -} - -test is_empty_2() { - is_empty(#"01") == False -} - -/// Convert a `String` into a `ByteArray`. -/// -/// ```aiken -/// bytearray.from_string(@"ABC") == #"414243" -/// ``` -pub fn from_string(str: String) -> ByteArray { - builtin.encode_utf8(str) -} - -test from_string_1() { - from_string(@"") == "" -} - -test from_string_2() { - from_string(@"ABC") == #"414243" -} - -/// Add a byte element in front of a `ByteArray`. When the given byte is -/// greater than 255, it wraps-around. **PlutusV2 behavior** So 256 is mapped to 0, 257 to 1, and so -/// forth. -/// In PlutusV3 this will error instead of wrapping around. -/// -/// ```aiken -/// bytearray.push(#"", 0) == #"00" -/// bytearray.push(#"0203", 1) == #"010203" -/// bytearray.push(#"0203", 257) == #"010203" -/// ``` -pub fn push(self: ByteArray, byte: Int) -> ByteArray { - builtin.cons_bytearray(byte, self) -} - -test push_1() { - push(#[], 0) == #[0] -} - -test push_2() { - push(#[2, 3], 1) == #[1, 2, 3] -} - -test push_3() { - let x = 257 - push(#[2, 3], x) == #[1, 2, 3] -} - /// Reduce bytes in a ByteArray from left to right using the accumulator as left operand. /// Said differently, this is [`foldl`](#foldl) with callback arguments swapped. /// @@ -326,76 +539,62 @@ test reduce_2() { reduce(#[1, 2, 3], #[], push) == #[3, 2, 1] } -/// Extract a `ByteArray` as a slice of another `ByteArray`. -/// -/// Indexes are 0-based and inclusive. +/// Interpret a Big-Endian (most-significant bytes first) `ByteArray` as an `Int`. /// /// ```aiken -/// bytearray.slice(#[0, 1, 2, 3, 4, 5, 6], start: 1, end: 3) == #[1, 2, 3] +/// bytearray.to_int_big_endian(#"0f4240") == 1_000_000 +/// bytearray.to_int_big_endian(#"00000f4240") == 1_000_000 +/// bytearray.to_int_big_endian(#"0000000000000000") == 0 /// ``` -pub fn slice(self: ByteArray, start: Int, end: Int) -> ByteArray { - builtin.slice_bytearray(start, end - start + 1, self) -} - -test slice_1() { - slice(#"", 1, 2) == #"" -} - -test slice_2() { - slice(#"010203", 1, 2) == #"0203" +pub fn to_int_big_endian(self: ByteArray) -> Int { + builtin.bytearray_to_integer(True, self) } -test slice_3() { - slice(#"010203", 0, 42) == #"010203" +test to_int_big_endian_1() { + to_int_big_endian(#"0f4240") == 1_000_000 } -test slice_4() { - slice(#[0, 1, 2, 3, 4], 0, 3) == #[0, 1, 2, 3] +test to_int_big_endian_2() { + to_int_big_endian(#"00000f4240") == 1_000_000 } -test slice_5() { - slice(#[0, 1, 2, 3, 4], 1, 2) == #[1, 2] +test to_int_big_endian_3() { + to_int_big_endian(#"0000000000000000") == 0 } -/// Returns the n-length prefix of a `ByteArray`. +/// Interpret a Little-Endian (least-significant bytes first) `ByteArray` as an `Int`. /// /// ```aiken -/// bytearray.take(#[1, 2, 3], n: 2) == #[1, 2] +/// bytearray.to_int_big_endian(#"40420f") == 1_000_000 +/// bytearray.to_int_big_endian(#"40420f0000") == 1_000_000 +/// bytearray.to_int_big_endian(#"0000000000000000") == 0 /// ``` -pub fn take(self: ByteArray, n: Int) -> ByteArray { - builtin.slice_bytearray(0, n, self) -} - -test take_1() { - let x = #"01020304050607" - take(x, 2) == #"0102" +pub fn to_int_little_endian(self: ByteArray) -> Int { + builtin.bytearray_to_integer(False, self) } -test take_2() { - let x = #"01020304050607" - take(x, 0) == #"" +test to_int_little_endian_1() { + to_int_little_endian(#"40420f") == 1_000_000 } -test take_3() { - let x = #"01" - take(x, 1) == x +test to_int_little_endian_2() { + to_int_little_endian(#"40420f0000") == 1_000_000 } -test take_4() { - let x = #"010203" - take(x, 0) == #"" +test to_int_little_endian_3() { + to_int_little_endian(#"0000000000000000") == 0 } /// Convert a `ByteArray` into a `String`. /// -///
⚠️
WARNING
| This functions fails if the underlying `ByteArray` isn't UTF-8-encoded.
In particular, you cannot convert arbitrary hash digests using this function.
For converting arbitrary `ByteArray`s, use [bytearray.to_hex](#to_hex). -/// --- | --- -/// +/// > [!WARNING] +/// > This functions fails if the underlying `ByteArray` isn't UTF-8-encoded. In particular, you cannot convert arbitrary hash digests using this function. +/// > +/// > For converting arbitrary `ByteArray`s, use [bytearray.to_hex](#to_hex). /// /// ```aiken /// bytearray.to_string(#"414243") == "ABC" -/// -/// bytearray.to_string(some_hash) -> fail +/// bytearray.to_string(some_hash) => 💥 /// ``` pub fn to_string(self: ByteArray) -> String { builtin.decode_utf8(self) @@ -412,8 +611,6 @@ test to_string_2() { /// Encode a `ByteArray` as a hexidecimal `String`. /// /// ```aiken -/// use aiken/bytearray -/// /// bytearray.to_hex("Hello world!") == @"48656c6c6f20776f726c6421" /// ``` pub fn to_hex(self: ByteArray) -> String { @@ -429,69 +626,3 @@ test to_hex_1() { test to_hex_2() { to_hex("The quick brown fox jumps over the lazy dog") == @"54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67" } - -/// Checks whether a bit (Most-Significant-Bit first) is set in the given 'ByteArray'. -/// -/// For example, consider the following bytearray: `#"8b765f"`. It can also be written as the -/// following bits sequence: -/// -/// `8` | `b` | `7` | `6` | `5` | `f` -/// --- | --- | --- | --- | --- | --- -/// `1000` | `1011` | `0111` | `0110` | `0101` | `1111` -/// -/// And thus, we have: -/// -/// ```aiken -/// test_bit(#"8b765f", 0) == True -/// test_bit(#"8b765f", 1) == False -/// test_bit(#"8b765f", 2) == False -/// test_bit(#"8b765f", 3) == False -/// test_bit(#"8b765f", 7) == True -/// test_bit(#"8b765f", 8) == False -/// test_bit(#"8b765f", 20) == True -/// test_bit(#"8b765f", 21) == True -/// test_bit(#"8b765f", 22) == True -/// test_bit(#"8b765f", 23) == True -/// ``` -pub fn test_bit(self: ByteArray, ix: Int) -> Bool { - builtin.less_than_equals_bytearray( - #[128], - builtin.cons_bytearray( - builtin.index_bytearray(self, ix / 8) * math.pow2(ix % 8), - "", - ), - ) -} - -test test_bit_0() { - test_bit(#"8b765f", 0) -} - -test test_bit_1() { - !test_bit(#"8b765f", 1) -} - -test test_bit_2() { - !test_bit(#"8b765f", 2) -} - -test test_bit_3() { - !test_bit(#"8b765f", 3) -} - -test test_bit_7() { - test_bit(#"8b765f", 7) -} - -test test_bit_8() { - !test_bit(#"8b765f", 8) -} - -test test_bit_20_21_22_23() { - and { - test_bit(#"8b765f", 20), - test_bit(#"8b765f", 21), - test_bit(#"8b765f", 22), - test_bit(#"8b765f", 23), - } -} diff --git a/lib/aiken/primitive/int.ak b/lib/aiken/primitive/int.ak new file mode 100644 index 0000000..217749e --- /dev/null +++ b/lib/aiken/primitive/int.ak @@ -0,0 +1,156 @@ +use aiken/builtin.{bytearray_to_integer, decode_utf8} +use aiken/math +use aiken/option +use aiken/primitive/bytearray + +// ## Combining + +/// Compare two integers. +/// +/// ```aiken +/// int.compare(14, 42) == Less +/// int.compare(14, 14) == Equal +/// int.compare(42, 14) == Greater +/// ``` +pub fn compare(left: Int, right: Int) -> Ordering { + if left < right { + Less + } else if left > right { + Greater + } else { + Equal + } +} + +// ## Transforming + +/// Interpret a Big-Endian (most-significant bytes first) `ByteArray` as an `Int`. +/// +/// ```aiken +/// int.from_bytearray_big_endian(#"0f4240") == 1_000_000 +/// int.from_bytearray_big_endian(#"00000f4240") == 1_000_000 +/// int.from_bytearray_big_endian(#"0000000000000000") == 0 +/// ``` +pub fn from_bytearray_big_endian(self: ByteArray) -> Int { + bytearray_to_integer(True, self) +} + +test from_bytearray_big_endian_1() { + from_bytearray_big_endian(#"0f4240") == 1_000_000 +} + +test from_bytearray_big_endian_2() { + from_bytearray_big_endian(#"00000f4240") == 1_000_000 +} + +test from_bytearray_big_endian_3() { + from_bytearray_big_endian(#"0000000000000000") == 0 +} + +/// Interpret a Little-Endian (least-significant bytes first) `ByteArray` as an `Int`. +/// +/// ```aiken +/// int.from_bytearray_big_endian(#"40420f") == 1_000_000 +/// int.from_bytearray_big_endian(#"40420f0000") == 1_000_000 +/// int.from_bytearray_big_endian(#"0000000000000000") == 0 +/// ``` +pub fn from_bytearray_little_endian(self: ByteArray) -> Int { + bytearray_to_integer(False, self) +} + +test from_bytearray_little_endian_1() { + from_bytearray_little_endian(#"40420f") == 1_000_000 +} + +test from_bytearray_little_endian_2() { + from_bytearray_little_endian(#"40420f0000") == 1_000_000 +} + +test from_bytearray_little_endian_3() { + from_bytearray_little_endian(#"0000000000000000") == 0 +} + +/// Parse an integer from a utf-8 encoded `ByteArray`, when possible. +/// +/// ```aiken +/// int.from_utf8("14") == Some(14) +/// int.from_utf8("-42") == Some(-42) +/// int.from_utf8("007") == Some(7) +/// int.from_utf8("foo") == None +/// int.from_utf8("1.0") == None +/// int.from_utf8("1-2") == None +/// ``` +pub fn from_utf8(bytes: ByteArray) -> Option { + bytes + |> bytearray.foldr( + Some((0, 0)), + fn(byte, st) { + when st is { + None -> None + Some((n, e)) -> + if byte < 48 || byte > 57 { + if byte == 45 { + Some((-n, 0)) + } else { + None + } + } else if n < 0 { + None + } else { + let digit = byte - 48 + Some((n + digit * math.pow(10, e), e + 1)) + } + } + }, + ) + |> option.map(fn(tuple) { tuple.1st }) +} + +test from_utf8_1() { + from_utf8("0017") == Some(17) +} + +test from_utf8_2() { + from_utf8("42") == Some(42) +} + +test from_utf8_3() { + from_utf8("1337") == Some(1337) +} + +test from_utf8_4() { + from_utf8("-14") == Some(-14) +} + +test from_utf8_5() { + from_utf8("foo") == None +} + +test from_utf8_6() { + from_utf8("1-2") == None +} + +/// Convert an `Int` to its `String` representation. +/// +/// ```aiken +/// int.to_string(42) == @"42" +/// ``` +pub fn to_string(n: Int) -> String { + diagnostic(n, "") |> decode_utf8 +} + +test to_string_1() { + to_string(0) == @"0" +} + +test to_string_2() { + to_string(5) == @"5" +} + +test to_string_3() { + to_string(42) == @"42" +} + +test to_string_4() { + to_string(200) == @"200" +} diff --git a/lib/aiken/string.ak b/lib/aiken/primitive/string.ak similarity index 86% rename from lib/aiken/string.ak rename to lib/aiken/primitive/string.ak index b9eee08..35fa556 100644 --- a/lib/aiken/string.ak +++ b/lib/aiken/primitive/string.ak @@ -1,39 +1,19 @@ use aiken/builtin.{ append_bytearray, append_string, decode_utf8, encode_utf8, length_of_bytearray, } -use aiken/cbor -/// Combine two `String` together. -/// -/// ```aiken -/// string.concat(left: @"Hello", right: @", World!") == @"Hello, World!" -/// ``` -pub fn concat(left: String, right: String) -> String { - append_string(left, right) -} - -test concat_1() { - concat(@"", @"") == @"" -} - -test concat_2() { - concat(@"", @"foo") == concat(@"foo", @"") -} - -test concat_3() { - concat(left: @"Hello", right: @", World!") == @"Hello, World!" -} +// ## Constructing /// Convert a `ByteArray` into a `String` /// -///
⚠️
WARNING
| This functions fails if the underlying `ByteArray` isn't UTF-8-encoded.
In particular, you cannot convert arbitrary hash digests using this function.
For converting arbitrary `ByteArray`s, use [bytearray.to_hex](/stdlib/aiken/bytearray.html#to_hex). -/// --- | --- +/// > [!WARNING] +/// > This functions fails if the underlying `ByteArray` isn't UTF-8-encoded. In particular, you cannot convert arbitrary hash digests using this function. +/// > +/// > For converting arbitrary `ByteArray`s, use [bytearray.to_hex](./bytearray.html#to_hex). /// /// ```aiken /// string.from_bytearray("foo") == @"foo" -/// /// string.from_bytearray(#"666f6f") == @"foo" -/// /// string.from_bytearray(some_hash) -> fail /// ``` pub fn from_bytearray(bytes: ByteArray) -> String { @@ -58,7 +38,7 @@ test from_bytearray_3() { /// string.from_int(42) == @"42" /// ``` pub fn from_int(n: Int) -> String { - cbor.diagnostic(n) + diagnostic(n, "") |> decode_utf8 } test from_int_1() { @@ -77,6 +57,29 @@ test from_int_4() { from_int(200) == @"200" } +// ## Combining + +/// Combine two `String` together. +/// +/// ```aiken +/// string.concat(left: @"Hello", right: @", World!") == @"Hello, World!" +/// ``` +pub fn concat(left: String, right: String) -> String { + append_string(left, right) +} + +test concat_1() { + concat(@"", @"") == @"" +} + +test concat_2() { + concat(@"", @"foo") == concat(@"foo", @"") +} + +test concat_3() { + concat(left: @"Hello", right: @", World!") == @"Hello, World!" +} + /// Join a list of strings, separated by a given _delimiter_. /// /// ```aiken @@ -112,6 +115,8 @@ test join_2() { join([@"a", @"b", @"c"], @",") == @"a,b,c" } +// ## Transforming + /// Convert a `String` into a `ByteArray` /// /// ```aiken diff --git a/lib/aiken/time.ak b/lib/aiken/time.ak index f181815..e69de29 100644 --- a/lib/aiken/time.ak +++ b/lib/aiken/time.ak @@ -1,3 +0,0 @@ -/// A number of milliseconds since 00:00:00 UTC on 1 January 1970. -pub type PosixTime = - Int diff --git a/lib/aiken/transaction/certificate.ak b/lib/aiken/transaction/certificate.ak deleted file mode 100644 index a5d9c8e..0000000 --- a/lib/aiken/transaction/certificate.ak +++ /dev/null @@ -1,15 +0,0 @@ -use aiken/hash.{Blake2b_224, Hash} -use aiken/transaction/credential.{PoolId, StakeCredential, VerificationKey} - -/// An on-chain certificate attesting of some operation. Publishing -/// certificates / triggers different kind of rules; most of the time, -/// they require signatures from / specific keys. -pub type Certificate { - CredentialRegistration { delegator: StakeCredential } - CredentialDeregistration { delegator: StakeCredential } - CredentialDelegation { delegator: StakeCredential, delegatee: PoolId } - PoolRegistration { pool_id: PoolId, vrf: Hash } - PoolDeregistration { pool_id: PoolId, epoch: Int } - Governance - TreasuryMovement -} diff --git a/lib/aiken/transaction/credential.ak b/lib/cardano/address.ak similarity index 76% rename from lib/aiken/transaction/credential.ak rename to lib/cardano/address.ak index fa6327d..ceef947 100644 --- a/lib/aiken/transaction/credential.ak +++ b/lib/cardano/address.ak @@ -1,15 +1,18 @@ -use aiken/builtin -use aiken/hash.{Blake2b_224, Hash} +use aiken/crypto.{ + Blake2b_224, Hash, Script, ScriptHash, VerificationKey, VerificationKeyHash, +} /// A general structure for representing an on-chain `Credential`. /// /// Credentials are always one of two kinds: a direct public/private key /// pair, or a script (native or Plutus). pub type Credential { - VerificationKeyCredential(Hash) - ScriptCredential(Hash) + VerificationKey(VerificationKeyHash) + Script(ScriptHash) } +// ## Constructing + /// A Cardano `Address` typically holding one or two credential references. /// /// Note that legacy bootstrap addresses (a.k.a. 'Byron addresses') are @@ -21,20 +24,14 @@ pub type Address { stake_credential: Option, } -/// Smart-constructor for an [Address](#Address) from a [verification key](#VerificationKey) hash. The resulting address has no delegation rights whatsoever. -pub fn from_verification_key(vk: Hash) -> Address { - Address { - payment_credential: VerificationKeyCredential(vk), - stake_credential: None, - } -} - /// Smart-constructor for an [Address](#Address) from a [script](#Script) hash. The address has no delegation rights whatsoever. pub fn from_script(script: Hash) -> Address { - Address { - payment_credential: ScriptCredential(script), - stake_credential: None, - } + Address { payment_credential: Script(script), stake_credential: None } +} + +/// Smart-constructor for an [Address](#Address) from a [verification key](#VerificationKey) hash. The resulting address has no delegation rights whatsoever. +pub fn from_verification_key(vk: Hash) -> Address { + Address { payment_credential: VerificationKey(vk), stake_credential: None } } /// Set (or reset) the delegation part of an [Address](#Address) using a [verification key](#VerificationKey) hash. This is useful when combined with [`from_verification_key`](#from_verification_key) and/or [`from_script`](#from_script). @@ -44,7 +41,7 @@ pub fn with_delegation_key( ) -> Address { Address { payment_credential: self.payment_credential, - stake_credential: Some(Inline(VerificationKeyCredential(vk))), + stake_credential: Some(Inline(VerificationKey(vk))), } } @@ -55,7 +52,7 @@ pub fn with_delegation_script( ) -> Address { Address { payment_credential: self.payment_credential, - stake_credential: Some(Inline(ScriptCredential(script))), + stake_credential: Some(Inline(Script(script))), } } @@ -69,25 +66,6 @@ pub type Referenced
{ Pointer { slot_number: Int, transaction_index: Int, certificate_index: Int } } -pub type VerificationKey = - ByteArray - -pub type Script = - ByteArray - -pub type Signature = - ByteArray - -/// Verify an Ed25519 signature using the given verification key. -/// Returns `True` when the signature is valid. -pub fn verify_signature( - key: VerificationKey, - msg: ByteArray, - sig: Signature, -) -> Bool { - builtin.verify_ed25519_signature(key, msg, sig) -} - /// A `StakeCredential` represents the delegation and rewards withdrawal conditions /// associated with some stake address / account. /// @@ -106,7 +84,3 @@ pub type StakeCredential = /// pub type PaymentCredential = Credential - -/// A unique stake pool identifier, as a hash of its owner verification key. -pub type PoolId = - Hash diff --git a/lib/aiken/transaction/value.ak b/lib/cardano/assets.ak similarity index 71% rename from lib/aiken/transaction/value.ak rename to lib/cardano/assets.ak index eac2203..1181e1d 100644 --- a/lib/aiken/transaction/value.ak +++ b/lib/cardano/assets.ak @@ -1,8 +1,11 @@ -use aiken/dict.{Dict, from_ascending_pairs_with} -use aiken/hash.{Blake2b_224, Hash} -use aiken/list +use aiken/collection/dict.{Dict, from_ascending_pairs_with} +use aiken/collection/list +use aiken/crypto.{Blake2b_224, Hash, Script} use aiken/option -use aiken/transaction/credential.{Script} + +/// Lovelace is now a type wrapper for Int. +pub type Lovelace = + Int /// A type-alias for a `PolicyId`. A `PolicyId` is always 28-byte long pub type PolicyId = @@ -12,7 +15,7 @@ pub type PolicyId = /// possible to mint Ada!). /// /// By convention, it is an empty `ByteArray`. -pub const ada_policy_id = #"" +pub const ada_policy_id = "" /// A type-alias for 'AssetName`, which are free-form byte-arrays between /// 0 and 32 bytes. @@ -23,25 +26,18 @@ pub type AssetName = /// possible to mint Ada!). /// /// By convention, it is an empty `ByteArray`. -pub const ada_asset_name = #"" +pub const ada_asset_name = "" /// A multi-asset output `Value`. Contains tokens indexed by [PolicyId](#PolicyId) and [AssetName](#AssetName). /// -/// This type maintain some invariants by construction; in particular, a `Value` will never contain a +/// > [!IMPORTANT] +/// > This type maintain some invariants by construction; in particular, a `Value` will never contain a /// zero quantity of a particular token. pub opaque type Value { inner: Dict>, } -/// Construct an empty `Value` with nothing in it. -pub fn zero() -> Value { - Value { inner: dict.new() } -} - -/// Check is a `Value` is zero. That is, it has no assets and holds no Ada/Lovelace. -pub fn is_zero(self: Value) -> Bool { - self == zero() -} +// ## Constructing /// Construct a `Value` from an asset identifier (i.e. `PolicyId` + `AssetName`) /// and a given quantity. @@ -63,136 +59,169 @@ pub fn from_asset( } } -/// Construct a `Value` from a lovelace quantity. +/// Promote an arbitrary list of assets into a `Value`. This function fails +/// (i.e. halt the program execution) if: /// -/// Friendly reminder: 1 Ada = 1.000.000 Lovelace -pub fn from_lovelace(quantity: Int) -> Value { - from_asset(ada_policy_id, ada_asset_name, quantity) +/// - there's any duplicate amongst `PolicyId`; +/// - there's any duplicate amongst `AssetName`; +/// - the `AssetName` aren't sorted in ascending lexicographic order; or +/// - any asset quantity is null. +/// +/// This function is meant to turn arbitrary user-defined `Data` into safe `Value`, +/// while checking for internal invariants. +pub fn from_asset_list(xs: Pairs>) -> Value { + xs + |> list.foldr( + dict.new(), + fn(inner, acc) { + expect Pair(p, [_, ..] as x) = inner + x + |> from_ascending_pairs_with(fn(v) { v != 0 }) + |> dict.insert_with( + acc, + p, + _, + fn(_, _, _) { + fail @"Duplicate policy in the asset list." + }, + ) + }, + ) + |> Value } -/// Get a `Value` excluding Ada. -pub fn without_lovelace(self: Value) -> Value { - dict.delete(self.inner, ada_policy_id) - |> Value +test from_asset_list_1() { + let v = from_asset_list([]) + v == zero() } -test without_lovelace_1() { - let v = from_lovelace(1000000) - without_lovelace(v) == zero() +test from_asset_list_2() fail { + let v = from_asset_list([Pair(#"33", [])]) + v == zero() } -test without_lovelace_2() { - let v = from_lovelace(1000000) - let v2 = from_lovelace(50000000) - without_lovelace(v) == without_lovelace(v2) +test from_asset_list_3() fail { + let v = from_asset_list([Pair(#"33", [Pair(#"", 0)])]) + v != zero() } -test without_lovelace_3() { - let v = - from_asset(#"010203", #"040506", 100) - |> add(ada_policy_id, ada_asset_name, 100000000) - let v2 = from_asset(#"010203", #"040506", 100) - without_lovelace(v) == without_lovelace(v2) && without_lovelace(v) == v2 +test from_asset_list_4() { + let v = from_asset_list([Pair(#"33", [Pair(#"", 1)])]) + flatten(v) == [(#"33", #"", 1)] } -/// Negates quantities of all tokens (including Ada) in that `Value`. -/// -/// ``` -/// v1 -/// |> value.negate -/// |> value.merge(v1) -/// |> value.is_zero -/// // True -/// ``` -pub fn negate(self: Value) -> Value { - dict.map(self.inner, fn(_, a) { dict.map(a, fn(_, q) { 0 - q }) }) - |> Value +test from_asset_list_5() { + let v = from_asset_list([Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)])]) + flatten(v) == [(#"33", #"", 1), (#"33", #"33", 1)] } -/// Combine two `Value` together. -pub fn merge(left v0: Value, right v1: Value) -> Value { - Value( - dict.union_with( - v0.inner, - v1.inner, - fn(_, a0, a1) { - let result = - dict.union_with( - a0, - a1, - fn(_, q0, q1) { - let q = q0 + q1 - if q == 0 { - None - } else { - Some(q) - } - }, - ) - if dict.is_empty(result) { - None - } else { - Some(result) - } - }, - ), - ) +test from_asset_list_6() fail { + let v = + from_asset_list( + [ + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + ], + ) + v != zero() } -test merge_1() { - let v1 = from_lovelace(1) - let v2 = from_lovelace(-1) - merge(v1, v2) == zero() +test from_asset_list_7() fail { + let v = + from_asset_list( + [ + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + Pair(#"34", [Pair(#"", 1), Pair(#"", 1)]), + ], + ) + v != zero() } -test merge_2() { - let v1 = from_asset(#"00", #"", 1) - let v2 = from_asset(#"01", #"", 2) - let v3 = from_asset(#"02", #"", 3) +test from_asset_list_8() { let v = - from_lovelace(42) - |> merge(v3) - |> merge(v1) - |> merge(v2) + from_asset_list( + [ + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + Pair(#"34", [Pair(#"31", 1)]), + Pair(#"35", [Pair(#"", 1)]), + ], + ) + flatten(v) == [ + (#"33", #"", 1), + (#"33", #"33", 1), + (#"34", #"31", 1), + (#"35", #"", 1), + ] +} +test from_asset_list_9() { + let v = + from_asset_list( + [ + Pair(#"35", [Pair(#"", 1)]), + Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), + Pair(#"34", [Pair(#"31", 1)]), + ], + ) flatten(v) == [ - (#"", #"", 42), - (#"00", #"", 1), - (#"01", #"", 2), - (#"02", #"", 3), + (#"33", #"", 1), + (#"33", #"33", 1), + (#"34", #"31", 1), + (#"35", #"", 1), ] } -test merge_3() { - let v1 = from_asset(#"00", #"", 1) - let v2 = from_asset(#"00", #"", -1) - let v3 = from_asset(#"01", #"", 1) +/// Construct a `Value` from a lovelace quantity. +/// +/// Friendly reminder: 1 Ada = 1.000.000 Lovelace +pub fn from_lovelace(quantity: Int) -> Value { + from_asset(ada_policy_id, ada_asset_name, quantity) +} - let v = - zero() - |> merge(v1) - |> merge(v2) - |> merge(v3) +/// Construct an empty `Value` with nothing in it. +pub fn zero() -> Value { + Value { inner: dict.new() } +} - flatten(v) == [(#"01", #"", 1)] +// ## Inspecting + +/// Check is a `Value` is zero. That is, it has no assets and holds no Ada/Lovelace. +pub fn is_zero(self: Value) -> Bool { + self == zero() } -test merge_4() { - let v1 = from_asset(#"00", #"", 1) - let v2 = from_asset(#"00", #"", -1) +/// A specialized version of `quantity_of` for the Ada currency. +pub fn lovelace_of(self: Value) -> Int { + quantity_of(self, ada_policy_id, ada_asset_name) +} - merge(v1, v2) == zero() +/// A list of all token policies in that Value with non-zero tokens. +pub fn policies(self: Value) -> List { + dict.keys(self.inner) } -test merge_5() { - let v = - zero() - |> add(#"acab", #"beef", 0) +/// Extract the quantity of a given asset. +pub fn quantity_of( + self: Value, + policy_id: PolicyId, + asset_name: AssetName, +) -> Int { + self.inner + |> dict.get(policy_id) + |> option.and_then(dict.get(_, asset_name)) + |> option.or_else(0) +} - merge(zero(), v) == zero() +/// Get all tokens associated with a given policy. +pub fn tokens(self: Value, policy_id: PolicyId) -> Dict { + self.inner + |> dict.get(policy_id) + |> option.or_else(dict.new()) } -/// Add a (positive or negative) quantity of a single token to a value. +// ## Combining + +/// Add a (positive or negative) quantity of a single token to a assets. /// This is more efficient than [`merge`](#merge) for a single asset. pub fn add( self: Value, @@ -276,36 +305,131 @@ test add_5() { v == zero() } -/// Extract the quantity of a given asset. -pub fn quantity_of( - self: Value, - policy_id: PolicyId, - asset_name: AssetName, -) -> Int { - self.inner - |> dict.get(policy_id) - |> option.and_then(dict.get(_, asset_name)) - |> option.or_else(0) -} - -/// A specialized version of `quantity_of` for the Ada currency. -pub fn lovelace_of(self: Value) -> Int { - quantity_of(self, ada_policy_id, ada_asset_name) -} - -/// Get all tokens associated with a given policy. -pub fn tokens(self: Value, policy_id: PolicyId) -> Dict { - self.inner - |> dict.get(policy_id) - |> option.or_else(dict.new()) -} - -/// A list of all token policies in that Value with non-zero tokens. -pub fn policies(self: Value) -> List { - dict.keys(self.inner) +/// Combine two `Value` together. +pub fn merge(left v0: Value, right v1: Value) -> Value { + Value( + dict.union_with( + v0.inner, + v1.inner, + fn(_, a0, a1) { + let result = + dict.union_with( + a0, + a1, + fn(_, q0, q1) { + let q = q0 + q1 + if q == 0 { + None + } else { + Some(q) + } + }, + ) + if dict.is_empty(result) { + None + } else { + Some(result) + } + }, + ), + ) +} + +test merge_1() { + let v1 = from_lovelace(1) + let v2 = from_lovelace(-1) + merge(v1, v2) == zero() } -/// Flatten a value as list of 3-tuple (PolicyId, AssetName, Quantity). +test merge_2() { + let v1 = from_asset(#"00", #"", 1) + let v2 = from_asset(#"01", #"", 2) + let v3 = from_asset(#"02", #"", 3) + let v = + from_lovelace(42) + |> merge(v3) + |> merge(v1) + |> merge(v2) + + flatten(v) == [ + (#"", #"", 42), + (#"00", #"", 1), + (#"01", #"", 2), + (#"02", #"", 3), + ] +} + +test merge_3() { + let v1 = from_asset(#"00", #"", 1) + let v2 = from_asset(#"00", #"", -1) + let v3 = from_asset(#"01", #"", 1) + + let v = + zero() + |> merge(v1) + |> merge(v2) + |> merge(v3) + + flatten(v) == [(#"01", #"", 1)] +} + +test merge_4() { + let v1 = from_asset(#"00", #"", 1) + let v2 = from_asset(#"00", #"", -1) + + merge(v1, v2) == zero() +} + +test merge_5() { + let v = + zero() + |> add(#"acab", #"beef", 0) + + merge(zero(), v) == zero() +} + +/// Negates quantities of all tokens (including Ada) in that `Value`. +/// +/// ``` +/// v1 +/// |> assets.negate +/// |> assets.merge(v1) +/// |> assets.is_zero +/// // True +/// ``` +pub fn negate(self: Value) -> Value { + dict.map(self.inner, fn(_, a) { dict.map(a, fn(_, q) { 0 - q }) }) + |> Value +} + +/// Get a `Value` excluding Ada. +pub fn without_lovelace(self: Value) -> Value { + dict.delete(self.inner, ada_policy_id) + |> Value +} + +test without_lovelace_1() { + let v = from_lovelace(1000000) + without_lovelace(v) == zero() +} + +test without_lovelace_2() { + let v = from_lovelace(1000000) + let v2 = from_lovelace(50000000) + without_lovelace(v) == without_lovelace(v2) +} + +test without_lovelace_3() { + let v = + from_asset(#"010203", #"040506", 100) + |> add(ada_policy_id, ada_asset_name, 100000000) + let v2 = from_asset(#"010203", #"040506", 100) + without_lovelace(v) == without_lovelace(v2) && without_lovelace(v) == v2 +} + +// ## Transforming + +/// Flatten a `Value` as list of 3-tuple `(PolicyId, AssetName, Quantity)`. /// /// Handy to manipulate values as uniform lists. pub fn flatten(self: Value) -> List<(PolicyId, AssetName, Int)> { @@ -324,7 +448,7 @@ pub fn flatten(self: Value) -> List<(PolicyId, AssetName, Int)> { ) } -/// Flatten a value as a list of results, possibly discarding some along the way. +/// Flatten a `Value` as a list of results, possibly discarding some along the way. /// /// When the transform function returns `None`, the result is discarded altogether. pub fn flatten_with( @@ -376,17 +500,17 @@ test flatten_with_2() { /// Reduce a value into a single result /// /// ``` -/// value.zero() -/// |> value.add("a", "1", 10) -/// |> value.add("b", "2", 20) -/// |> value.reduce(v, 0, fn(_, _, quantity, acc) { acc + quantity }) +/// assets.zero() +/// |> assets.add("a", "1", 10) +/// |> assets.add("b", "2", 20) +/// |> assets.reduce(v, 0, fn(_, _, quantity, acc) { acc + quantity }) /// // 30 /// ``` pub fn reduce( self: Value, - start: acc, - with: fn(PolicyId, AssetName, Int, acc) -> acc, -) -> acc { + start: result, + with: fn(PolicyId, AssetName, Int, result) -> result, +) -> result { dict.foldr( self.inner, start, @@ -426,261 +550,7 @@ test reduce_3() { result == 1 } -/// Promote an arbitrary list of assets into a `Value`. This function fails -/// (i.e. halt the program execution) if: -/// -/// - there's any duplicate amongst `PolicyId`; -/// - there's any duplicate amongst `AssetName`; -/// - the `AssetName` aren't sorted in ascending lexicographic order; or -/// - any asset quantity is null. -/// -/// This function is meant to turn arbitrary user-defined `Data` into safe `Value`, -/// while checking for internal invariants. -pub fn from_asset_list(xs: Pairs>) -> Value { - xs - |> list.foldr( - dict.new(), - fn(inner, acc) { - expect Pair(p, [_, ..] as x) = inner - x - |> from_ascending_pairs_with(fn(v) { v != 0 }) - |> dict.insert_with( - acc, - p, - _, - fn(_, _, _) { - fail @"Duplicate policy in the asset list." - }, - ) - }, - ) - |> Value -} - -test from_asset_list_1() { - let v = from_asset_list([]) - v == zero() -} - -test from_asset_list_2() fail { - let v = from_asset_list([Pair(#"33", [])]) - v == zero() -} - -test from_asset_list_3() fail { - let v = from_asset_list([Pair(#"33", [Pair(#"", 0)])]) - v != zero() -} - -test from_asset_list_4() { - let v = from_asset_list([Pair(#"33", [Pair(#"", 1)])]) - flatten(v) == [(#"33", #"", 1)] -} - -test from_asset_list_5() { - let v = from_asset_list([Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)])]) - flatten(v) == [(#"33", #"", 1), (#"33", #"33", 1)] -} - -test from_asset_list_6() fail { - let v = - from_asset_list( - [ - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - ], - ) - v != zero() -} - -test from_asset_list_7() fail { - let v = - from_asset_list( - [ - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - Pair(#"34", [Pair(#"", 1), Pair(#"", 1)]), - ], - ) - v != zero() -} - -test from_asset_list_8() { - let v = - from_asset_list( - [ - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - Pair(#"34", [Pair(#"31", 1)]), - Pair(#"35", [Pair(#"", 1)]), - ], - ) - flatten(v) == [ - (#"33", #"", 1), - (#"33", #"33", 1), - (#"34", #"31", 1), - (#"35", #"", 1), - ] -} - -test from_asset_list_9() { - let v = - from_asset_list( - [ - Pair(#"35", [Pair(#"", 1)]), - Pair(#"33", [Pair(#"", 1), Pair(#"33", 1)]), - Pair(#"34", [Pair(#"31", 1)]), - ], - ) - flatten(v) == [ - (#"33", #"", 1), - (#"33", #"33", 1), - (#"34", #"31", 1), - (#"35", #"", 1), - ] -} - /// Convert the value into a dictionary of dictionaries. pub fn to_dict(self: Value) -> Dict> { self.inner } - -/// A multi-asset value that can be found when minting transaction. It always holds -/// a null quantity of _Ada_. Note that because of historical reasons, this is slightly -/// different from `Value` found in transaction outputs. -/// -/// Note that you're never expected to construct a `MintedValue` yourself. If you need to -/// manipulate multi-asset values, use [Value](#Value) -/// -/// See also [`from_minted_value`](#from_minted_value). -pub opaque type MintedValue { - inner: Dict>, -} - -/// Convert minted value into a dictionary of dictionaries. -pub fn minted_to_dict(self: MintedValue) -> Dict> { - self.inner -} - -/// Convert a [`MintedValue`](#MintedValue) into a [`Value`](#Value). -pub fn from_minted_value(self: MintedValue) -> Value { - self.inner |> dict.delete(ada_policy_id) |> Value -} - -test from_minted_value_1() { - flatten(from_minted_value(from_internal_list([]))) == [] -} - -test from_minted_value_2() { - flatten(from_minted_value(from_internal_list([("p0", "a0", 1)]))) == [ - ("p0", "a0", 1), - ] -} - -test from_minted_value_3() { - let assets = - [("p0", "a0", 1), ("p1", "a0", 1), ("p0", "a0", 1), ("p1", "a1", 1)] - - let result = - [("p0", "a0", 2), ("p1", "a0", 1), ("p1", "a1", 1)] - - flatten(from_minted_value(from_internal_list(assets))) == result -} - -test from_minted_value_4() { - let assets = - [ - ("", "", 0), - ("p0", "a0", 1), - ("p1", "a0", 1), - ("p0", "a0", 1), - ("p1", "a1", 1), - ] - - let result = - [("p0", "a0", 2), ("p1", "a0", 1), ("p1", "a1", 1)] - - flatten(from_minted_value(from_internal_list(assets))) == result -} - -test from_minted_value_5() { - let assets = - [ - ("p0", "a0", 1), - ("p0", "a1", 1), - ("p1", "a0", 1), - ("p1", "a1", 1), - ("p1", "a2", 1), - ("p2", "a0", 1), - ("p2", "a1", 1), - ("p3", "a0", 1), - ("p3", "a1", 1), - ("p3", "a2", 1), - ("p3", "a3", 1), - ("p3", "a4", 1), - ("p3", "a5", 1), - ("p3", "a6", 1), - ("p3", "a7", 1), - ] - - flatten(from_minted_value(from_internal_list(assets))) == assets -} - -/// Convert a [`Value`](#Value) into a [`MintedValue`](#MintedValue). -pub fn to_minted_value(self: Value) -> MintedValue { - self.inner - |> dict.insert(ada_policy_id, dict.insert(dict.new(), ada_asset_name, 0)) - |> MintedValue -} - -test to_minted_value_1() { - let minted_value = to_minted_value(zero()) - ( minted_value.inner |> dict.to_pairs |> list.length ) == 1 -} - -test to_minted_value_2() { - let minted_value = to_minted_value(from_lovelace(42)) - ( - minted_value.inner - |> dict.get(ada_policy_id) - |> option.and_then(dict.get(_, ada_asset_name)) - ) == Some(0) -} - -/// Convert a list of tokens into a `MintedValue`. -/// -/// NOTE: Not exposed because we do not want people to construct `MintedValue`. Only -/// get them from the script context. -fn from_internal_list(xs: List<(PolicyId, AssetName, Int)>) -> MintedValue { - list.foldr( - xs, - MintedValue(dict.new()), - fn(elem, st) { - let (policy_id, asset_name, quantity) = elem - unchecked_add(st, policy_id, asset_name, quantity) - }, - ) -} - -fn unchecked_add( - self: MintedValue, - policy_id: PolicyId, - asset_name: AssetName, - quantity: Int, -) -> MintedValue { - MintedValue( - dict.insert_with( - self.inner, - policy_id, - dict.from_ascending_pairs([Pair(asset_name, quantity)]), - fn(_, left, _right) { - Some( - dict.insert_with( - left, - asset_name, - quantity, - fn(_k, ql, qr) { Some(ql + qr) }, - ), - ) - }, - ), - ) -} diff --git a/lib/cardano/certificate.ak b/lib/cardano/certificate.ak new file mode 100644 index 0000000..c8ee231 --- /dev/null +++ b/lib/cardano/certificate.ak @@ -0,0 +1,93 @@ +use aiken/crypto.{Blake2b_224, Hash, VerificationKey, VerificationKeyHash} +use cardano/address.{Credential} +use cardano/assets.{Lovelace} + +pub type StakePoolId = + Hash + +/// An on-chain certificate attesting of some operation. Publishing +/// certificates / triggers different kind of rules; most of the time, +/// they require signatures from / specific keys. +pub type Certificate { + /// Register a stake credential with an optional deposit amount. + /// The deposit is always present when using the new registration certificate + /// format available since the Conway era. + RegisterCredential { + credential: Credential, + /// > [!INFO] + /// > The `deposit` ought to be an `Option`, but due to unfortunate + /// > circumstances it will always be instantiated to `None` even when set in + /// > the host transaction. This is what the `Never` type captures here. + deposit: Never, + } + /// Un-Register a stake credential with an optional refund amount + /// The deposit is always present when using the new de-registration certificate + /// format available since the Conway era. + UnregisterCredential { + credential: Credential, + /// > [!INFO] + /// > The `refund` ought to be an `Option`, but due to unfortunate + /// > circumstances it will always be instantiated to `None` even when set in + /// > the host transaction. This is what the `Never` type captures here. + refund: Never, + } + /// Delegate stake to a [Delegate](#Delegate). + DelegateCredential { credential: Credential, delegate: Delegate } + /// Register and delegate staking credential to a Delegatee in one certificate. + RegisterAndDelegateCredential { + credential: Credential, + delegate: Delegate, + deposit: Lovelace, + } + /// Register a delegate representative (a.k.a DRep). The deposit is explicit and + /// is refunded when the delegate steps down (unregister). + RegisterDelegateRepresentative { + delegate_representative: Credential, + deposit: Lovelace, + } + /// Update a delegate representative (a.k.a DRep). The certificate also contains + /// metadata which aren't visible on-chain. + UpdateDelegateRepresentative { delegate_representative: Credential } + /// UnRegister a delegate representative, and refund back its past deposit. + UnregisterDelegateRepresentative { + delegate_representative: Credential, + refund: Lovelace, + } + /// Register a new stake pool + RegisterStakePool { + /// The hash digest of the stake pool's cold (public) key + stake_pool: StakePoolId, + /// The hash digest of the stake pool's VRF (public) key + vrf: VerificationKeyHash, + } + /// Retire a stake pool. 'at_epoch' indicates in which the retirement will take place + RetireStakePool { stake_pool: StakePoolId, at_epoch: Int } + /// Authorize a Hot credential for a specific Committee member's cold credential + AuthorizeConstitutionalCommitteeProxy { + constitutional_committee_member: Credential, + proxy: Credential, + } + /// Step down from the constitutional committee as a member. + RetireFromConstitutionalCommittee { + constitutional_committee_member: Credential, + } +} + +/// A type of stake delegation that can be either block-production, vote or +/// both. Note that delegation types aren't cancelling one another, so it is +/// possible to delegate block production in one transaction, and delegate vote +/// in another. This second delegation **does NOT** invalidate the first one. +pub type Delegate { + DelegateBlockProduction { stake_pool: StakePoolId } + DelegateVote { delegate_representative: DelegateRepresentative } + DelegateBoth { + stake_pool: StakePoolId, + delegate_representative: DelegateRepresentative, + } +} + +pub type DelegateRepresentative { + Registered(Credential) + AlwaysAbstain + AlwaysNoConfidence +} diff --git a/lib/cardano/governance.ak b/lib/cardano/governance.ak new file mode 100644 index 0000000..3ec9680 --- /dev/null +++ b/lib/cardano/governance.ak @@ -0,0 +1,109 @@ +use aiken/collection.{Index} +use aiken/crypto.{Blake2b_256, Hash, ScriptHash, VerificationKeyHash} +use aiken/math/rational.{Rational} +use cardano/address.{Credential} +use cardano/assets.{Lovelace} +use cardano/governance/protocol_parameters.{ProtocolParametersUpdate} + +pub type ProposalProcedure { + deposit: Lovelace, + return_address: Credential, + governance_action: GovernanceAction, +} + +pub type GovernanceAction { + ProtocolParameters { + /// The last governance action of type 'ProtocolParameters'. They must all + /// form a chain. + ancestor: Option, + /// The new proposed protocol parameters. Only values set to `Some` are relevant. + new_parameters: ProtocolParametersUpdate, + /// The optional guardrails script defined in the constitution. The script + /// is executed by the ledger in addition to the hard-coded ledger rules. + /// + /// It must pass for the new protocol parameters to be deemed valid. + guardrails: Option, + } + HardFork { + /// The last governance action of type `HardFork`. They must all + /// form a chain. + ancestor: Option, + /// The new proposed version. Few rules apply to proposing new versions: + /// + /// - The `major` component, if incremented, must be exactly one more than the current. + /// - The `minor` component, if incremented, must be exactly one more than the current. + /// - If the `major` component is incremented, `minor` must be set to `0`. + /// - Neither `minor` nor `major` can be decremented. + new_version: ProtocolVersion, + } + TreasuryWithdrawal { + /// A collection of beneficiaries, which can be plain verification key + /// hashes or script hashes (e.g. DAO). + beneficiaries: Pairs, + /// The optional guardrails script defined in the constitution. The script + /// is executed by the ledger in addition to the hard-coded ledger rules. + /// + /// It must pass for the withdrawals to be authorized. + guardrails: Option, + } + NoConfidence { + /// The last governance action of type `NoConfidence` or + /// `ConstitutionalCommittee`. They must all / form a chain. + ancestor: Option, + } + ConstitutionalCommittee { + /// The last governance action of type `NoConfidence` or + /// `ConstitutionalCommittee`. They must all / form a chain. + ancestor: Option, + /// Constitutional members to be removed. + evicted_members: List, + /// Constitutional members to be added. + added_members: Pairs, + /// The new quorum value, as a ratio of a numerator and a denominator. The + /// quorum specifies the threshold of 'Yes' votes necessary for the + /// constitutional committee to accept a proposal procedure. + quorum: Rational, + } + NewConstitution { + /// The last governance action of type `Constitution` or + /// `ConstitutionalCommittee`. They must all / form a chain. + ancestor: Option, + /// The new proposed constitution. + constitution: Constitution, + } + NicePoll +} + +pub type Vote { + No + Yes + Abstain +} + +pub type TransactionId = + Hash + +pub type GovernanceActionId { + transaction: TransactionId, + proposal_procedure: Index, +} + +pub type ProtocolVersion { + major: Int, + minor: Int, +} + +pub type Constitution { + guardrails: Option, +} + +/// An epoch number after which constitutional committee member +/// mandate expires. +pub type Mandate = + Int + +pub type Voter { + ConstitutionalCommitteeMember(Credential) + DelegateRepresentative(Credential) + StakePool(VerificationKeyHash) +} diff --git a/lib/cardano/governance/protocol_parameters.ak b/lib/cardano/governance/protocol_parameters.ak new file mode 100644 index 0000000..27e4dbc --- /dev/null +++ b/lib/cardano/governance/protocol_parameters.ak @@ -0,0 +1,367 @@ +use aiken/math/rational.{Rational} +use cardano/assets.{Lovelace} + +pub opaque type ProtocolParametersUpdate { + inner: Pairs, +} + +pub type ScriptExecutionPrices { + memory: Rational, + cpu: Rational, +} + +pub type ExecutionUnits { + memory: Int, + cpu: Int, +} + +pub type StakePoolOperatorVotingThresholds { + motion_of_no_confidence: Rational, + constitutional_committee: ConstitutionalCommitteeThresholds, + hard_fork: Rational, + protocol_parameters: ProtocolParametersThresholds< + Rational, + Void, + Void, + Void, + Void, + >, +} + +pub type DelegateRepresentativeVotingThresholds { + motion_of_no_confidence: Rational, + constitutional_committee: ConstitutionalCommitteeThresholds, + constitution: Rational, + hard_fork: Rational, + protocol_parameters: ProtocolParametersThresholds< + Void, + Rational, + Rational, + Rational, + Rational, + >, + treasury_withdrawal: Rational, +} + +pub type ProtocolParametersThresholds< + security, + network, + economic, + technical, + governance, +> { + security_group: security, + network_group: network, + economic_group: economic, + technical_group: technical, + governance_group: governance, +} + +pub type ConstitutionalCommitteeThresholds { + default: Rational, + under_no_confidence: Rational, +} + +/// The linear coefficient that intervenes in the transaction fee calculation. +/// It is multiplied by the size of the transaction in bytes to obtain a Lovelace value. +pub fn min_fee_coefficient(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 0, into_int) +} + +/// The constant factor that intervenes in the transaction fee calculation. It is +/// a flat cost of lovelace that is added to every fee calculation. +pub fn min_fee_constant(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 1, into_int) +} + +/// The maximum size of a serialized block body, expressed in bytes. +pub fn max_block_body_size(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 2, into_int) +} + +/// The maximum size of a serialized transaction (body + witnesses), expressed in bytes. +pub fn max_transaction_size(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 3, into_int) +} + +/// The maximum size of a serialized block header, expressed in bytes. +pub fn max_block_header_size(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 4, into_int) +} + +/// The required deposit amount when registering stake credentials, expressed in Lovelace. +pub fn stake_credential_deposit( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 5, into_int) +} + +/// The required deposit amount when registering a stake pool, expressed in Lovelace. +pub fn stake_pool_deposit(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 6, into_int) +} + +/// The maximum number of epoch in the future allowed for a stake pool retirement to be scheduled. +pub fn stake_pool_retirement_horizon( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 7, into_int) +} + +/// The desired/optimal number of fully saturated stake pools in the system. Also known as the _'k-parameter'_. +pub fn desired_number_of_stake_pools( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 8, into_int) +} + +/// A parameter controlling the influence of an pool owner's pledge on the rewards. Also known as _'a0'_. +pub fn stake_pool_pledge_influence( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 9, into_rational) +} + +/// The monetary expansion parameter, controlling the fraction of Ada put in circulation on every epoch through the incentivies model. Also known as _'ρ'_. +pub fn monetary_expansion(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 10, into_rational) +} + +/// The parameter controlling what fraction (%) of available rewards is sent to the treasury on every epoch. Also known as _'τ'_. +pub fn treasury_expansion(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 11, into_rational) +} + +/// Minimum authorized constant cost that stake pools can declare when registering, expressed in Lovelace. +pub fn min_stake_pool_cost(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 16, into_int) +} + +/// The linear coefficient that intervenes in the calculation of the minimum Ada value that any UTxO must hold. It is expressed in Lovelace per Byte, and is also known as the 'coins per utxo byte' parameter. +pub fn min_utxo_deposit_coefficient( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 17, into_int) +} + +/// The costs associated with the various operations of the Plutus Virtual Machine, which can be different for each Plutus version. +pub fn cost_models(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 18, identity) +} + +/// The price, in Lovelace per unit, of the execution units corresponding to cpu and memory usage of on-chain scripts. +pub fn script_execution_prices( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 19, into_script_execution_prices) +} + +/// The maximum execution units allowed for a single transaction. +pub fn max_transaction_execution_units( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 20, into_execution_units) +} + +/// The maximum execution units allowed for a single block. +pub fn max_block_execution_units( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 21, into_execution_units) +} + +/// The maximum size of a serialized value in a transaction output. This effectively limits +/// the maximum kinds of assets that can be sent in a single output. It is expressed in bytes. +pub fn max_value_size(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 22, into_int) +} + +/// The scaling factor applied to the transaction cost for defining the minimum collateral +/// amount. It is expressed in percent points (so 100 = 100%). +pub fn collateral_percentage(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 23, into_int) +} + +/// The maximum number of collateral inputs allowed in the transaction. +pub fn max_collateral_inputs(self: ProtocolParametersUpdate) -> Option { + get_protocol_param(self.inner, 24, into_int) +} + +/// The various governance voting thresholds pertaining to stake pool operators. +pub fn stake_pool_operator_voting_thresholds( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 25, into_spo_voting_thresholds) +} + +/// The various governance voting thresholds pertaining to delegate representatives +/// (a.k.a DReps). +pub fn delegate_representative_voting_thresholds( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 26, into_drep_voting_thresholds) +} + +/// The minimum number of members in the constitutional committee. Any updates of the committee +/// must leave at least this number of members. +pub fn min_constitutional_committee_size( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 27, into_int) +} + +/// The maximum length of a constitutional committee member, expressed in number of epochs. +pub fn max_constitutional_committee_mandate( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 28, into_int) +} + +/// The lifetime of any governance proposal. An action that hasn't been approved beyond that +/// period is considered inactive and discarded. It is expressed in number of epochs. +pub fn governance_proposal_lifetime( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 29, into_int) +} + +/// The required deposit amount for governance proposal procedures, expressed in Lovelace. +pub fn governance_proposal_deposit( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 30, into_int) +} + +/// The required deposit amount when registering as a delegate representative, expressed in +/// Lovelace. +pub fn delegate_representative_deposit( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 31, into_int) +} + +/// The maximum number of epochs that a delegate representative can stay inactive (i.e. no +/// voting) without becoming _inactive_ and removed from thresholds calculations. +pub fn delegate_representative_max_idle_time( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 32, into_int) +} + +/// The base tier fee coefficient for reference scripts. Reference scripts gets increasingly +/// more expensives every ~24KB, the base coefficient is a multiplicating factor which grows +/// exponentially with each tier. +pub fn reference_scripts_tier_fee_initial_factor( + self: ProtocolParametersUpdate, +) -> Option { + get_protocol_param(self.inner, 33, into_rational) +} + +// Internals ------------------------------------------------------------------- + +type ProtocolParametersIndex = + Int + +fn get_protocol_param( + self: Pairs, + ix: ProtocolParametersIndex, + into: fn(Data) -> a, +) -> Option { + when self is { + [] -> None + [Pair(jx, param), ..tail] -> + if ix == jx { + Some(into(param)) + } else { + get_protocol_param(tail, ix, into) + } + } +} + +fn into_int(param: Data) -> Int { + expect param: Int = param + param +} + +fn into_rational(param: Data) -> Rational { + expect [numerator, denominator]: List = param + expect Some(r) = rational.new(numerator, denominator) + r +} + +fn into_execution_units(param: Data) -> ExecutionUnits { + expect [memory, cpu]: List = param + ExecutionUnits { memory, cpu } +} + +fn into_script_execution_prices(param: Data) -> ScriptExecutionPrices { + expect [memory, cpu]: List = param + let memory = into_rational(memory) + let cpu = into_rational(cpu) + ScriptExecutionPrices { memory, cpu } +} + +fn into_spo_voting_thresholds(param: Data) -> StakePoolOperatorVotingThresholds { + expect [ + motion_of_no_confidence, + constitutional_committee, + constitutional_committee_under_no_confidence, + hard_fork, + protocol_parameters_security_group, + ]: List = param + + StakePoolOperatorVotingThresholds { + motion_of_no_confidence: into_rational(motion_of_no_confidence), + constitutional_committee: ConstitutionalCommitteeThresholds { + default: into_rational(constitutional_committee), + under_no_confidence: into_rational( + constitutional_committee_under_no_confidence, + ), + }, + hard_fork: into_rational(hard_fork), + protocol_parameters: ProtocolParametersThresholds { + security_group: into_rational(protocol_parameters_security_group), + network_group: Void, + economic_group: Void, + technical_group: Void, + governance_group: Void, + }, + } +} + +fn into_drep_voting_thresholds( + param: Data, +) -> DelegateRepresentativeVotingThresholds { + expect [ + motion_of_no_confidence, + constitutional_committee, + constitutional_committee_under_no_confidence, + constitution, + hard_fork, + protocol_parameters_network_group, + protocol_parameters_economic_group, + protocol_parameters_technical_group, + protocol_parameters_governance_group, + treasury_withdrawal, + ]: List = param + + DelegateRepresentativeVotingThresholds { + motion_of_no_confidence: into_rational(motion_of_no_confidence), + constitutional_committee: ConstitutionalCommitteeThresholds { + default: into_rational(constitutional_committee), + under_no_confidence: into_rational( + constitutional_committee_under_no_confidence, + ), + }, + constitution: into_rational(constitution), + hard_fork: into_rational(hard_fork), + protocol_parameters: ProtocolParametersThresholds { + security_group: Void, + network_group: into_rational(protocol_parameters_network_group), + economic_group: into_rational(protocol_parameters_economic_group), + technical_group: into_rational(protocol_parameters_technical_group), + governance_group: into_rational(protocol_parameters_governance_group), + }, + treasury_withdrawal: into_rational(treasury_withdrawal), + } +} diff --git a/lib/cardano/script_context.ak b/lib/cardano/script_context.ak new file mode 100644 index 0000000..ff73836 --- /dev/null +++ b/lib/cardano/script_context.ak @@ -0,0 +1,62 @@ +//// This module contains utilities for manually dealing with [`ScriptContext`](#ScriptContext). This is only ever useful for writing custom `else` handlers in validators. +//// +//// > [!NOTE] +//// > Unless you know what you're doing, you should prefer using named handlers: +//// > +//// > - `mint` +//// > - `spend` +//// > - `withdraw` +//// > - `publish` +//// > - `vote` +//// > - `propose` + +use aiken/collection.{Index} +use cardano/address.{Credential} +use cardano/assets.{PolicyId} +use cardano/certificate.{Certificate} +use cardano/governance.{ProposalProcedure, Voter} +use cardano/transaction.{OutputReference, Redeemer, Transaction} + +/// A context given to a script by the Cardano ledger when being executed. +/// +/// The context contains information about the entire transaction that contains +/// the script. The transaction may also contain other scripts; to distinguish +/// between multiple scripts, the [`ScriptContext`](#ScriptContext) contains a +/// [`ScriptInfo`](#ScriptInfo) which indicates which script (or, for what +/// purpose) the transaction is being executed. +pub type ScriptContext { + transaction: Transaction, + redeemer: Redeemer, + info: ScriptInfo, +} + +/// Characterizes the script information. The main (and only) difference with [`ScriptPurpose`](./transaction.html#ScriptPurpose) resides in the `Spending` variant which here contains a second field `datum: Option`. +pub type ScriptInfo { + /// For scripts executed as minting/burning policies, to insert + /// or remove assets from circulation. It's parameterized by the identifier + /// of the associated policy. + Minting(PolicyId) + /// For scripts that are used as payment credentials for addresses in + /// transaction outputs. They govern the rule by which the output they + /// reference can be spent. + Spending { output: OutputReference, datum: Option } + /// For scripts that validate reward withdrawals from a reward account. + /// + /// The argument identifies the target reward account. + Withdrawing(Credential) + /// Needed when delegating to a pool using stake credentials defined as a + /// custom script. This purpose is also triggered when de-registering such + /// stake credentials. + /// + /// The Int is a 0-based index of the given `Certificate` in `certificates`. + Publishing { at: Index, certificate: Certificate } + /// Voting for a type of voter using a governance action id to vote + /// yes / no / abstain inside a transaction. + /// + /// The voter is who is doing the governance action. + Voting(Voter) + /// Used to propose a governance action. + /// + /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. + Proposing { at: Index, proposal_procedure: ProposalProcedure } +} diff --git a/lib/aiken/transaction.ak b/lib/cardano/transaction.ak similarity index 57% rename from lib/aiken/transaction.ak rename to lib/cardano/transaction.ak index eb1572e..7cb4b70 100644 --- a/lib/aiken/transaction.ak +++ b/lib/cardano/transaction.ak @@ -1,30 +1,21 @@ use aiken/builtin -use aiken/dict.{Dict} -use aiken/hash.{Blake2b_224, Blake2b_256, Hash, blake2b_256} +use aiken/collection.{Index} +use aiken/collection/dict.{Dict} +use aiken/collection/list +use aiken/crypto.{ + Blake2b_256, DataHash, Hash, ScriptHash, VerificationKeyHash, blake2b_256, +} use aiken/interval.{Interval} -use aiken/list use aiken/option -use aiken/time.{PosixTime} -use aiken/transaction/certificate.{Certificate} -use aiken/transaction/credential.{ - Address, Script, ScriptCredential, StakeCredential, VerificationKey, - VerificationKeyCredential, -} -use aiken/transaction/value.{MintedValue, PolicyId, Value} +use cardano/address.{Address, Credential, Script, VerificationKey} +use cardano/assets.{Lovelace, PolicyId, Value} +use cardano/certificate.{Certificate} +use cardano/governance.{GovernanceActionId, ProposalProcedure, Vote, Voter} -/// A context given to a script by the Cardano ledger when being executed. -/// -/// The context contains information about the entire transaction that contains -/// the script. The transaction may also contain other scripts; to distinguish -/// between multiple scripts, the `ScriptContext` also contains a `purpose` -/// which indicates which script (or, for what purpose) of the transaction is -/// being executed. -pub type ScriptContext { - transaction: Transaction, - purpose: ScriptPurpose, -} +pub type TransactionId = + Hash -/// Characterizes the kind of script being executed. +/// Characterizes the script purpose. pub type ScriptPurpose { /// For scripts executed as minting/burning policies, to insert /// or remove assets from circulation. It's parameterized by the identifier @@ -37,79 +28,58 @@ pub type ScriptPurpose { /// For scripts that validate reward withdrawals from a reward account. /// /// The argument identifies the target reward account. - WithdrawFrom(StakeCredential) + Withdraw(Credential) /// Needed when delegating to a pool using stake credentials defined as a - /// Plutus script. This purpose is also triggered when de-registering such + /// custom script. This purpose is also triggered when de-registering such /// stake credentials. /// - /// It embeds the certificate that's being validated. - Publish(Certificate) + /// The Int is a 0-based index of the given `Certificate` in `certificates`. + Publishing { at: Index, certificate: Certificate } + /// Voting for a type of voter using a governance action id to vote + /// yes / no / abstain inside a transaction. + /// + /// The voter is who is doing the governance action. + Vote(Voter) + /// Used to propose a governance action. + /// + /// A 0-based index of the given `ProposalProcedure` in `proposal_procedures`. + Proposing { at: Index, proposal_procedure: ProposalProcedure } } -/// A Cardano `Transaction`, as seen by Plutus scripts. +/// A Cardano `Transaction`, as seen by on-chain scripts. /// /// Note that this is a representation of a transaction, and not the 1:1 /// translation of the transaction as seen by the ledger. In particular, -/// Plutus scripts can't see inputs locked by bootstrap addresses, outputs +/// on-chain scripts can't see inputs locked by bootstrap addresses, outputs /// to bootstrap addresses or just transaction metadata. pub type Transaction { inputs: List, reference_inputs: List, outputs: List, - fee: Value, - mint: MintedValue, + fee: Lovelace, + mint: Value, certificates: List, - withdrawals: Pairs, + /// > [!IMPORTANT] + /// > Withdrawals are ordered by ascending [Credential](./credential.html#Credential). Yet, note that [`Script`](./credential.html#Credential) credentials are treated as **lower values** than [`VerificationKey`](./credential.html#Credential) credentials. + withdrawals: Pairs, validity_range: ValidityRange, - extra_signatories: List>, + extra_signatories: List, + /// > [!IMPORTANT] + /// > Redeemers are ordered by ascending [ScriptPurpose](./transaction.html#ScriptPurpose). redeemers: Pairs, - datums: Dict, Data>, + datums: Dict, id: TransactionId, + /// > [!IMPORTANT] + /// > Votes are ordered by ascending [Voter](./governance.html#Voter) and [GovernanceActionId](./governance.html#GovernanceActionId).
First constructor variants in a type are treated as lower indices; except for [Credential](./credential.html#Credential) where [`Script`](./credential.html#Credential) credentials are treated as **lower values** than [`VerificationKey`](./credential.html#Credential) credentials. + votes: Pairs>, + proposal_procedures: List, + current_treasury_amount: Option, + treasury_donation: Option, } -/// A placeholder / empty `Transaction` to serve as a base in a transaction -/// builder. This is particularly useful for constructing test transactions. -/// -/// Every field is empty or null, and we have in particular: -/// -/// ```aiken -/// use aiken/transaction -/// -/// transaction.placeholder().id == TransactionId { -/// hash: #"0000000000000000000000000000000000000000000000000000000000000000", -/// } -/// -/// transaction.placeholder().validity_range == interval.everything() -/// ``` -pub fn placeholder() -> Transaction { - Transaction { - inputs: [], - reference_inputs: [], - outputs: [], - fee: value.zero(), - mint: value.zero() |> value.to_minted_value(), - certificates: [], - withdrawals: [], - validity_range: interval.everything(), - extra_signatories: [], - redeemers: [], - datums: dict.new(), - id: TransactionId { - hash: #"0000000000000000000000000000000000000000000000000000000000000000", - }, - } -} - -/// An interval of POSIX time, measured in number milliseconds since 1970-01-01T00:00:00Z. +/// An interval of POSIX time, measured in **number of milliseconds** since 1970-01-01T00:00:00Z. pub type ValidityRange = - Interval - -/// A unique transaction identifier, as the hash of a transaction body. Note that the transaction id -/// isn't a direct hash of the `Transaction` as visible on-chain. Rather, they correspond to hash -/// digests of transaction body as they are serialized on the network. -pub type TransactionId { - hash: Hash, -} + Interval /// An `Input` made of an output reference and, the resolved value associated with that output. pub type Input { @@ -121,7 +91,7 @@ pub type Input { /// corresponds to the position in the output list of the transaction (identified by its id) /// that produced that output pub type OutputReference { - transaction_id: TransactionId, + transaction_id: Hash, output_index: Int, } @@ -130,14 +100,14 @@ pub type Output { address: Address, value: Value, datum: Datum, - reference_script: Option>, + reference_script: Option, } /// An output `Datum`. pub type Datum { NoDatum /// A datum referenced by its hash digest. - DatumHash(Hash) + DatumHash(DataHash) /// A datum completely inlined in the output. InlineDatum(Data) } @@ -148,18 +118,17 @@ pub type Datum { pub type Redeemer = Data +// ## Querying + /// Find an input by its [`OutputReference`](#OutputReference). This is typically used in /// combination with the `Spend` [`ScriptPurpose`](#ScriptPurpose) to find a script's own /// input. /// /// ```aiken /// validator { -/// fn(datum, redeemer, ctx: ScriptContext) { -/// expect Spend(my_output_reference) = -/// ctx.purpose -/// +/// spend(datum, redeemer, my_output_reference, self) { /// expect Some(input) = -/// ctx.transaction.inputs +/// self.inputs /// |> transaction.find_input(my_output_reference) /// } /// } @@ -177,8 +146,8 @@ pub fn find_input( /// witnesses. pub fn find_datum( outputs: List, - datums: Dict, Data>, - datum_hash: Hash, + datums: Dict, + datum_hash: DataHash, ) -> Option { datums |> dict.get(datum_hash) @@ -191,7 +160,7 @@ pub fn find_datum( InlineDatum(data) -> if blake2b_256(builtin.serialise_data(data)) == datum_hash{ - + Some(data) } else { None @@ -209,16 +178,51 @@ pub fn find_datum( /// contracts running over multiple transactions. pub fn find_script_outputs( outputs: List, - script_hash: Hash, + script_hash: ScriptHash, ) -> List { outputs |> list.filter( fn(output) { when output.address.payment_credential is { - ScriptCredential(addr_script_hash) -> - script_hash == addr_script_hash - VerificationKeyCredential(_) -> False + Script(addr_script_hash) -> script_hash == addr_script_hash + VerificationKey(_) -> False } }, ) } + +// ## Testing + +/// A placeholder / empty `Transaction` to serve as a base in a transaction +/// builder. This is particularly useful for constructing test transactions. +/// +/// Every field is empty or null, and we have in particular: +/// +/// ```aiken +/// use aiken/interval +/// +/// transaction.placeholder().id == +/// #"0000000000000000000000000000000000000000000000000000000000000000" +/// +/// transaction.placeholder().validity_range == interval.everything() +/// ``` +pub fn placeholder() -> Transaction { + Transaction { + inputs: [], + reference_inputs: [], + outputs: [], + fee: 0, + mint: assets.zero(), + certificates: [], + withdrawals: [], + validity_range: interval.everything(), + extra_signatories: [], + redeemers: [], + datums: dict.new(), + id: #"0000000000000000000000000000000000000000000000000000000000000000", + votes: [], + proposal_procedures: [], + current_treasury_amount: None, + treasury_donation: None, + } +}