From 9349d0e4b02f58550f7ff3c5041c621f917ef175 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 1 Sep 2024 19:25:30 +0200 Subject: [PATCH] Classify doc exports in sections. --- CHANGELOG.md | 6 + README.md | 2 +- aiken.lock | 6 +- aiken.toml | 8 +- lib/aiken/fuzz.ak | 449 +++++++++++++++++++++++----------------------- plutus.json | 14 -- 6 files changed, 243 insertions(+), 242 deletions(-) delete mode 100644 plutus.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e1b3382..f1223f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.0.0 - 2024-09-01 + +### Changed + +- Integrated with `aiken==1.1.0` and `aiken-lang/stdlib==v2` + ## v1.0.0 - 2024-07-26 ### Added diff --git a/README.md b/README.md index 8dda258..a1bcfa5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It provides many useful primitives for writing and composing arbitrary generator ## Installation ``` -aiken add aiken-lang/fuzz --version v1 +aiken add aiken-lang/fuzz --version v2 ``` ## Getting started diff --git a/aiken.lock b/aiken.lock index 9cbf0a5..242397d 100644 --- a/aiken.lock +++ b/aiken.lock @@ -3,14 +3,14 @@ [[requirements]] name = "aiken-lang/stdlib" -version = "main" +version = "v2" source = "github" [[packages]] name = "aiken-lang/stdlib" -version = "main" +version = "v2" requirements = [] source = "github" [etags] -"aiken-lang/stdlib@main" = [{ secs_since_epoch = 1724927247, nanos_since_epoch = 594472000 }, "21da5761ffd088c964cb038888826da8a6ea1d8c26f6f4e8a1dc6e97a64fe3f7"] +"aiken-lang/stdlib@v2" = [{ secs_since_epoch = 1725211062, nanos_since_epoch = 948577000 }, "d79382d2b6ecb3aee9b0755c31d8a5bbafe88a7b3706d7fb8a52fd4d05818501"] diff --git a/aiken.toml b/aiken.toml index b3a1db6..81e4b84 100644 --- a/aiken.toml +++ b/aiken.toml @@ -1,5 +1,7 @@ name = "aiken-lang/fuzz" -version = "2.0.0" +version = "main" +compiler = "v1.1.0" +plutus = "v3" license = "Apache-2.0" description = "A library for writing Aiken's fuzzers." @@ -10,5 +12,7 @@ platform = "github" [[dependencies]] name = "aiken-lang/stdlib" -version = "main" +version = "v2" source = "github" + +[config] diff --git a/lib/aiken/fuzz.ak b/lib/aiken/fuzz.ak index e28c262..01ad2dc 100644 --- a/lib/aiken/fuzz.ak +++ b/lib/aiken/fuzz.ak @@ -3,12 +3,8 @@ use aiken/collection/list use aiken/math use aiken/option -// Primitives - -/// Create a constant [Fuzzer](https://aiken-lang.github.io/prelude/aiken.html#Fuzzer) from an arbitrary value. -pub fn constant(a: a) -> Fuzzer { - fn(s0) { Some((s0, a)) } -} +// ## Constructing +// ### Primitives /// Generate a random [`Bool`](https://aiken-lang.github.io/prelude/aiken.html#Bool) value from a /// uniform distribution. @@ -22,6 +18,16 @@ pub fn bool() -> Fuzzer { rand |> map(fn(n) { n % 2 == 0 }) } +/// Create a constant [Fuzzer](https://aiken-lang.github.io/prelude/aiken.html#Fuzzer) from an arbitrary value. +pub fn constant(a: a) -> Fuzzer { + fn(s0) { Some((s0, a)) } +} + +/// Generate a byte uniformly across the range `[0; 255]`. +pub fn byte() -> Fuzzer { + rand +} + /// Generate a random [`ByteArray`](https://aiken-lang.github.io/prelude/aiken.html#ByteArray) of 32 bytes. pub fn bytearray() -> Fuzzer { let lsb <- and_then(rand) @@ -32,6 +38,20 @@ pub fn bytearray() -> Fuzzer { |> builtin.blake2b_256 } +/// Generate a random [`ByteArray`](https://aiken-lang.github.io/prelude/aiken.html#ByteArray) of +/// size within a lower and upper bounds. +pub fn bytearray_between(min: Int, max: Int) -> Fuzzer { + if max < min { + bytearray_between(max, min) + } else if max <= 0 { + constant("") + } else { + expect min >= 0 + let size <- and_then(int_between(min, max)) + bytearray_fixed_inner(size) + } +} + /// Generate a random [`ByteArray`](https://aiken-lang.github.io/prelude/aiken.html#ByteArray) of `len` bytes. pub fn bytearray_fixed(len: Int) -> Fuzzer { expect len >= 0 @@ -50,20 +70,6 @@ fn bytearray_fixed_inner(len: Int) -> Fuzzer { } } -/// Generate a random [`ByteArray`](https://aiken-lang.github.io/prelude/aiken.html#ByteArray) of -/// size within a lower and upper bounds. -pub fn bytearray_between(min: Int, max: Int) -> Fuzzer { - if max < min { - bytearray_between(max, min) - } else if max <= 0 { - constant("") - } else { - expect min >= 0 - let size <- and_then(int_between(min, max)) - bytearray_fixed_inner(size) - } -} - const int_bucket_small: Int = 128 const int_bucket_zero: Int = 132 @@ -152,26 +158,6 @@ pub fn int() -> Fuzzer { } } -/// Generate a random integer which is at least `min`. -pub fn int_at_least(min: Int) -> Fuzzer { - let abs = math.abs(min) - if abs <= max_rand { - int_between(min, max_rand) - } else { - int_between(min, min + 5 * abs) - } -} - -/// Generate a random integer which is at most `max`. -pub fn int_at_most(max: Int) -> Fuzzer { - let abs = math.abs(max) - if abs <= max_rand { - int_between(-max_rand, max) - } else { - int_between(max - 5 * abs, max) - } -} - /// Generate integers values uniformly between a lower and upper bounds (both inclusive). pub fn int_between(min: Int, max: Int) -> Fuzzer { if min > max { @@ -193,106 +179,28 @@ pub fn int_between(min: Int, max: Int) -> Fuzzer { } } -/// Generate a byte uniformly across the range `[0; 255]`. -pub fn byte() -> Fuzzer { - rand -} - -/// Generate a random list of **unique** elements (a.k.a. a set) from a given fuzzer. -/// The list contains *at most `20`* elements, and has a higher probability of -/// generating smaller lists. -/// -/// **Important:** The specified fuzzer must have a high enough entropy to -/// yield enough unique values to fill the set with the required size! -/// -/// For sets of a specific length, use [`set_between`](#set_between). -pub fn set(fuzzer: Fuzzer) -> Fuzzer> { - set_between(fuzzer, 0, 20) -} - -/// Generate a random set of elements from a given fuzzer, with at least `min` elements. -pub fn set_at_least(fuzzer: Fuzzer, min: Int) -> Fuzzer> { - set_between(fuzzer, min, min + 20) -} - -/// Generate a random set of elements from a given fuzzer, with at most `max` elements. -pub fn set_at_most(fuzzer: Fuzzer, max: Int) -> Fuzzer> { - set_between(fuzzer, 0, max) -} - -/// Generate a random set and pick an element from that set. Return both. -pub fn set_with_elem(fuzzer: Fuzzer) -> Fuzzer<(List, a)> { - let xs <- and_then(set_at_least(fuzzer, 1)) - let x <- map(one_of(xs)) - (xs, x) -} - -/// Take a random subset from an existing set. -pub fn subset(xs: List) -> Fuzzer> { - sublist(xs) -} - -/// Generate a random list of **unique** elements (a.k.a a set) with length -/// within specified bounds. The resulting set contains *at least `min`* -/// elements and *at most `max`* elements, with a higher probability of -/// generating smaller sets. -/// -/// More specifically, there's approximately 1/n chance of generating n -/// elements within the range. For example, the distribution when generating a -/// set between 0 and 10 elements resemble the following: -/// -/// **Important:** The specified fuzzer must have a high enough entropy to -/// yield enough unique values to fill the set with the required size! -/// -/// ``` -/// 22.7% 0 elements ████████ -/// 19.7% 1 element ███████ -/// 13.5% 2 elements █████ -/// 9.5% 3 elements ███ -/// 6.3% 4 elements ██ -/// 5.6% 5 elements ██ -/// 5.6% 6 elements ██ -/// 4.0% 7 elements █ -/// 3.1% 8 elements █ -/// 2.0% 9 elements █ -/// 8.0% 10 elements ███ -/// ``` -pub fn set_between(fuzzer: Fuzzer, min: Int, max: Int) -> Fuzzer> { - if min > max { - set_between(fuzzer, max, min) - } else if max <= 0 { - constant([]) +/// Generate a random integer which is at least `min`. +pub fn int_at_least(min: Int) -> Fuzzer { + let abs = math.abs(min) + if abs <= max_rand { + int_between(min, max_rand) } else { - do_list_between( - max - min, - if max == min { - -1 - } else { - log2(max - min) - }, - nub(100, fuzzer, _), - min, - max, - 0, - [], - ) + int_between(min, min + 5 * abs) } } -/// Construct a fuzzer that returns values not present in a given list. -fn nub(n: Int, fuzzer: Fuzzer, st: List) -> Fuzzer { - if n <= 0 { - fail @"gave up trying to find unique values: the fuzzer did not yield any *new* value after many tries!" +/// Generate a random integer which is at most `max`. +pub fn int_at_most(max: Int) -> Fuzzer { + let abs = math.abs(max) + if abs <= max_rand { + int_between(-max_rand, max) } else { - let a <- and_then(fuzzer) - if list.has(st, a) { - nub(n - 1, fuzzer, st) - } else { - constant(a) - } + int_between(max - 5 * abs, max) } } +// ### Data-structures + /// Generate a random list of elements from a given fuzzer. The list contains /// *at most `20`* elements, and has a higher probability of generating smaller lists. /// @@ -301,40 +209,6 @@ pub fn list(fuzzer: Fuzzer) -> Fuzzer> { list_between(fuzzer, 0, 20) } -/// Generate a random list of elements from a given fuzzer, with at least `min` elements. -pub fn list_at_least(fuzzer: Fuzzer, min: Int) -> Fuzzer> { - list_between(fuzzer, min, min + 20) -} - -/// Generate a random list of elements from a given fuzzer, with at most `max` elements. -pub fn list_at_most(fuzzer: Fuzzer, max: Int) -> Fuzzer> { - list_between(fuzzer, 0, max) -} - -/// Generate a random list and pick an element from that list. Return both. -pub fn list_with_elem(fuzzer: Fuzzer) -> Fuzzer<(List, a)> { - let xs <- and_then(list_at_least(fuzzer, 1)) - let x <- map(one_of(xs)) - (xs, x) -} - -/// Take a random sublist from an existing list. -pub fn sublist(xs: List) -> Fuzzer> { - let threshold <- and_then(rand) - when xs is { - [] -> constant([]) - [head, ..tail] -> { - let n <- and_then(rand) - let rest <- map(sublist(tail)) - if n < threshold { - [head, ..rest] - } else { - rest - } - } - } -} - /// Generate a random list of elements with length within specified bounds. The /// resulting list contains *at least `min`* elements and *at most `max`* /// elements, with a higher probability of generating smaller lists. @@ -433,81 +307,115 @@ fn do_list_between(p, q, fuzzer, min, max, length, xs) -> Fuzzer> { } } -/// Pick a value from a (non-empty!) list with equal probability. -/// -/// ```aiken -/// let any_bool = fuzz.one_of([True, False]) -/// ``` -pub fn one_of(xs: List) -> Fuzzer { - let len = list.length(xs) - expect len > 0 - int_between(0, len - 1) - |> map( - fn(ix: Int) { - expect Some(item) = list.at(xs, ix) - item - }, - ) +/// Generate a random list of elements from a given fuzzer, with at least `min` elements. +pub fn list_at_least(fuzzer: Fuzzer, min: Int) -> Fuzzer> { + list_between(fuzzer, min, min + 20) +} + +/// Generate a random list of elements from a given fuzzer, with at most `max` elements. +pub fn list_at_most(fuzzer: Fuzzer, max: Int) -> Fuzzer> { + list_between(fuzzer, 0, max) } -// Combining Fuzzers +/// Generate a random list and pick an element from that list. Return both. +pub fn list_with_elem(fuzzer: Fuzzer) -> Fuzzer<(List, a)> { + let xs <- and_then(list_at_least(fuzzer, 1)) + let x <- map(one_of(xs)) + (xs, x) +} -/// Choose either of two fuzzers with an equal probability. -pub fn either(left: Fuzzer, right: Fuzzer) -> Fuzzer { - let pick_left <- and_then(bool()) - if pick_left { - left +/// Generate a random list of **unique** elements (a.k.a. a set) from a given fuzzer. +/// The list contains *at most `20`* elements, and has a higher probability of +/// generating smaller lists. +/// +/// **Important:** The specified fuzzer must have a high enough entropy to +/// yield enough unique values to fill the set with the required size! +/// +/// For sets of a specific length, use [`set_between`](#set_between). +pub fn set(fuzzer: Fuzzer) -> Fuzzer> { + set_between(fuzzer, 0, 20) +} + +/// Generate a random list of **unique** elements (a.k.a a set) with length +/// within specified bounds. The resulting set contains *at least `min`* +/// elements and *at most `max`* elements, with a higher probability of +/// generating smaller sets. +/// +/// More specifically, there's approximately 1/n chance of generating n +/// elements within the range. For example, the distribution when generating a +/// set between 0 and 10 elements resemble the following: +/// +/// **Important:** The specified fuzzer must have a high enough entropy to +/// yield enough unique values to fill the set with the required size! +/// +/// ``` +/// 22.7% 0 elements ████████ +/// 19.7% 1 element ███████ +/// 13.5% 2 elements █████ +/// 9.5% 3 elements ███ +/// 6.3% 4 elements ██ +/// 5.6% 5 elements ██ +/// 5.6% 6 elements ██ +/// 4.0% 7 elements █ +/// 3.1% 8 elements █ +/// 2.0% 9 elements █ +/// 8.0% 10 elements ███ +/// ``` +pub fn set_between(fuzzer: Fuzzer, min: Int, max: Int) -> Fuzzer> { + if min > max { + set_between(fuzzer, max, min) + } else if max <= 0 { + constant([]) } else { - right + do_list_between( + max - min, + if max == min { + -1 + } else { + log2(max - min) + }, + nub(100, fuzzer, _), + min, + max, + 0, + [], + ) } } -/// Combine two fuzzers into a 2-tuple. -pub fn both(left: Fuzzer, right: Fuzzer) -> Fuzzer<(a, b)> { - map2(left, right, fn(l, r) { (l, r) }) +/// Generate a random set of elements from a given fuzzer, with at least `min` elements. +pub fn set_at_least(fuzzer: Fuzzer, min: Int) -> Fuzzer> { + set_between(fuzzer, min, min + 20) } -/// Choose between `None` or `Some` value with an equal probability. -pub fn option(fuzz_a: Fuzzer) -> Fuzzer> { - bool() - |> and_then( - fn(predicate) { - if predicate { - fuzz_a |> map(Some) - } else { - constant(None) - } - }, - ) +/// Generate a random set of elements from a given fuzzer, with at most `max` elements. +pub fn set_at_most(fuzzer: Fuzzer, max: Int) -> Fuzzer> { + set_between(fuzzer, 0, max) } -/// Generate a value that satisfies a given predicate. Beware that this -/// function may heavily impact the performances of your fuzzers. In general, you -/// should prefer constraining the fuzzers beforehand than trying to discard -/// values after the fact! -/// -/// Use with caution. -pub fn such_that(fuzzer: Fuzzer, predicate: fn(a) -> Bool) -> Fuzzer { - do_such_that(fuzzer, predicate, 100) +/// Generate a random set and pick an element from that set. Return both. +pub fn set_with_elem(fuzzer: Fuzzer) -> Fuzzer<(List, a)> { + let xs <- and_then(set_at_least(fuzzer, 1)) + let x <- map(one_of(xs)) + (xs, x) } -fn do_such_that( - fuzzer: Fuzzer, - predicate: fn(a) -> Bool, - max_tries: Int, -) -> Fuzzer { - if max_tries <= 0 { - fail @"such_that: couldn't satisfy the predicate after 100 attempts." +/// Construct a fuzzer that returns values not present in a given list. +fn nub(n: Int, fuzzer: Fuzzer, st: List) -> Fuzzer { + if n <= 0 { + fail @"gave up trying to find unique values: the fuzzer did not yield any *new* value after many tries!" } else { - let x <- and_then(fuzzer) - if predicate(x) { - constant(x) + let a <- and_then(fuzzer) + if list.has(st, a) { + nub(n - 1, fuzzer, st) } else { - do_such_that(fuzzer, predicate, max_tries - 1) + constant(a) } } } +// ## Combining + /// Combine a [Fuzzer](https://aiken-lang.github.io/prelude/aiken.html#Fuzzer) with the result of a another one. /// This function works great with [backpassing](https://aiken-lang.org/language-tour/functions#backpassing-). /// @@ -530,6 +438,21 @@ pub fn and_then(fuzz_a: Fuzzer, f: fn(a) -> Fuzzer) -> Fuzzer { } } +/// Combine two fuzzers into a 2-tuple. +pub fn both(left: Fuzzer, right: Fuzzer) -> Fuzzer<(a, b)> { + map2(left, right, fn(l, r) { (l, r) }) +} + +/// Choose either of two fuzzers with an equal probability. +pub fn either(left: Fuzzer, right: Fuzzer) -> Fuzzer { + let pick_left <- and_then(bool()) + if pick_left { + left + } else { + right + } +} + /// Transform the result of a [Fuzzer](https://aiken-lang.github.io/prelude/aiken.html#Fuzzer) using a function. /// This function works great with [backpassing](https://aiken-lang.org/language-tour/functions#backpassing-). /// @@ -853,6 +776,88 @@ pub fn map9( } } +/// Pick a value from a (non-empty!) list with equal probability. +/// +/// ```aiken +/// let any_bool = fuzz.one_of([True, False]) +/// ``` +pub fn one_of(xs: List) -> Fuzzer { + let len = list.length(xs) + expect len > 0 + int_between(0, len - 1) + |> map( + fn(ix: Int) { + expect Some(item) = list.at(xs, ix) + item + }, + ) +} + +/// Choose between `None` or `Some` value with an equal probability. +pub fn option(fuzz_a: Fuzzer) -> Fuzzer> { + bool() + |> and_then( + fn(predicate) { + if predicate { + fuzz_a |> map(Some) + } else { + constant(None) + } + }, + ) +} + +/// Take a random sublist from an existing list. +pub fn sublist(xs: List) -> Fuzzer> { + let threshold <- and_then(rand) + when xs is { + [] -> constant([]) + [head, ..tail] -> { + let n <- and_then(rand) + let rest <- map(sublist(tail)) + if n < threshold { + [head, ..rest] + } else { + rest + } + } + } +} + +/// Take a random subset from an existing set. +pub fn subset(xs: List) -> Fuzzer> { + sublist(xs) +} + +/// Generate a value that satisfies a given predicate. Beware that this +/// function may heavily impact the performances of your fuzzers. In general, you +/// should prefer constraining the fuzzers beforehand than trying to discard +/// values after the fact! +/// +/// Use with caution. +pub fn such_that(fuzzer: Fuzzer, predicate: fn(a) -> Bool) -> Fuzzer { + do_such_that(fuzzer, predicate, 100) +} + +fn do_such_that( + fuzzer: Fuzzer, + predicate: fn(a) -> Bool, + max_tries: Int, +) -> Fuzzer { + if max_tries <= 0 { + fail @"such_that: couldn't satisfy the predicate after 100 attempts." + } else { + let x <- and_then(fuzzer) + if predicate(x) { + constant(x) + } else { + do_such_that(fuzzer, predicate, max_tries - 1) + } + } +} + +// ## Inspecting + /// Label a test case. Labels are collected at the end of a property run and a /// distribution amongst all labels is computed and shown. This is particularly /// useful to assert that specific scenarios are met or to test your fuzzers. diff --git a/plutus.json b/plutus.json deleted file mode 100644 index cd9b1dc..0000000 --- a/plutus.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "preamble": { - "title": "waalge/fuzz", - "description": "Fuzzing in Aiken", - "version": "0.0.0", - "plutusVersion": "v2", - "compiler": { - "name": "Aiken", - "version": "v1.0.24-alpha+b6b57c7" - }, - "license": "Apache-2.0" - }, - "validators": [] -} \ No newline at end of file