diff --git a/lib/aiken/fuzz.ak b/lib/aiken/fuzz.ak index 7972f7f..eec30f3 100644 --- a/lib/aiken/fuzz.ak +++ b/lib/aiken/fuzz.ak @@ -1,5 +1,8 @@ use aiken/builtin +use aiken/bytearray +use aiken/hash.{blake2b_256} use aiken/list +use aiken/math // Internal @@ -95,11 +98,12 @@ pub fn bool() -> Fuzzer { } pub fn bytearray() -> Fuzzer { - fail + bytearray_between(0, 1024) } -pub fn bytearray_between(_min: Int, _max: Int) -> Fuzzer { - fail +pub fn bytearray_between(min: Int, max: Int) -> Fuzzer { + let bytes <- and_then(list_between(byte(), min, max)) + constant(list.foldl(bytes, #"", bytearray.concat)) } /// Generate a random integer value. It favors small values near zero, but generate across the whole range [-2^64; 2^64 - 1] @@ -113,33 +117,33 @@ pub fn int() -> Fuzzer { Some((Seeded { seed: builtin.blake2b_256(seed), choices }, choice)) } |> fn(return) { - if fst_choice < 128 { - return(fst_choice, builtin.cons_bytearray(fst_choice, choices)) - } else if fst_choice < 224 { - return( - fst_choice % 16, - builtin.cons_bytearray(fst_choice % 16, choices), - ) - } else if fst_choice < 236 { - let snd_choice = builtin.index_bytearray(seed, 1) - return( - -snd_choice, - builtin.cons_bytearray( - snd_choice, - builtin.cons_bytearray(fst_choice, choices), - ), - ) - } else { - let snd_choice = builtin.index_bytearray(seed, 1) - return( - u16(fst_choice, snd_choice), - builtin.cons_bytearray( - snd_choice, - builtin.cons_bytearray(fst_choice, choices), - ), - ) - } - } + if fst_choice < 128 { + return(fst_choice, builtin.cons_bytearray(fst_choice, choices)) + } else if fst_choice < 224 { + return( + fst_choice % 16, + builtin.cons_bytearray(fst_choice % 16, choices), + ) + } else if fst_choice < 236 { + let snd_choice = builtin.index_bytearray(seed, 1) + return( + -snd_choice, + builtin.cons_bytearray( + snd_choice, + builtin.cons_bytearray(fst_choice, choices), + ), + ) + } else { + let snd_choice = builtin.index_bytearray(seed, 1) + return( + u16(fst_choice, snd_choice), + builtin.cons_bytearray( + snd_choice, + builtin.cons_bytearray(fst_choice, choices), + ), + ) + } + } } Replayed { cursor, choices } -> @@ -166,6 +170,18 @@ pub fn int() -> Fuzzer { } } +pub fn positive_int() -> Fuzzer { + int() |> map(math.abs) |> map(fn(x) { x + 1 }) +} + +pub fn nonnegative_int() -> Fuzzer { + int() |> map(math.abs) +} + +pub fn negative_int() -> Fuzzer { + int() |> map(fn(x) { -math.abs(x + 1) }) +} + pub fn int_between(min: Int, max: Int) -> Fuzzer { if max < min { int_between(max, min) @@ -176,19 +192,117 @@ pub fn int_between(min: Int, max: Int) -> Fuzzer { let delta = ( max - min ) / 2 int() |> and_then( - fn(lo) { - int() - |> map(fn(hi) { mid - lo % ( delta + 1 ) + hi % ( delta + 1 ) }) - }, - ) + fn(lo) { + int() + |> map(fn(hi) { mid - lo % ( delta + 1 ) + hi % ( delta + 1 ) }) + }, + ) } } +pub fn uniform(bits: Int) -> Fuzzer { + // TODO: switch to Han-Hoshi for better uniform, and support min/max? + if bits == 0 { + constant(0) + } else { + let bit <- and_then(bool()) + let rest <- and_then(uniform(bits - 1)) + if bit { + constant(rest * 2 + 1) + } else { + constant(rest * 2) + } + } +} + +pub fn byte() -> Fuzzer { + let byte <- and_then(int_between(0, 255)) + constant(bytearray.push(#"", byte)) +} + +pub fn hash() -> Fuzzer { + let b <- and_then(byte()) + let hash = blake2b_256(b) + constant(bytearray.take(hash, 28)) +} + /// Generate a random list of elements from a given element. pub fn list(fuzzer: Fuzzer) -> Fuzzer> { list_between(fuzzer, 0, 32) } +pub fn nonempty_list(fuzzer: Fuzzer) -> Fuzzer> { + list_between(fuzzer, 1, 32) +} + +pub fn sorted( + fuzzer: Fuzzer>, + compare: fn(a, a) -> Ordering, +) -> Fuzzer> { + let ls <- and_then(fuzzer) + constant(list.sort(ls, compare)) +} + +pub fn list_with_elem(fuzzer: Fuzzer) -> Fuzzer<(List, a)> { + let xs <- and_then(nonempty_list(fuzzer)) + let x <- and_then(one_of(xs)) + constant((xs, x)) +} + +pub fn list_with_subset(fuzzer: Fuzzer) -> Fuzzer<(List, List)> { + let xs <- and_then(list(fuzzer)) + let bits <- and_then(list_between(bool(), 0, list.length(xs))) + let ys = + list.filter( + list.zip(xs, bits), + fn(pair) { + let (_, b) = pair + b + }, + ) + let (ys, _) = list.unzip(ys) + constant((xs, ys)) +} + +pub fn filter(fuzzer: Fuzzer, predicate: fn(a) -> Bool) -> Fuzzer { + do_filter(fuzzer, predicate, 100) +} + +fn do_filter( + fuzzer: Fuzzer, + predicate: fn(a) -> Bool, + max_tries: Int, +) -> Fuzzer { + if max_tries <= 0 { + fail + } else { + let x <- and_then(fuzzer) + if predicate(x) { + constant(x) + } else { + do_filter(fuzzer, predicate, max_tries - 1) + } + } +} + +pub fn distinct(fuzzer: Fuzzer>) -> Fuzzer> { + let xs <- and_then(fuzzer) + constant(dedup(xs, [])) +} + +fn dedup(xs: List, seen: List) -> List { + when xs is { + [] -> + [] + [x, ..xs] -> + if list.has(seen, x) { + dedup(xs, seen) + } else { + [x, ..dedup(xs, [x, ..seen])] + } + } +} + /// Generate a random list of elements with length within specified bounds. pub fn list_between(fuzzer: Fuzzer, min: Int, max: Int) -> Fuzzer> { if min > max { @@ -212,38 +326,38 @@ fn do_list_between(avg, fuzzer, min, max, length, xs) -> Fuzzer> { with_choice(min_rand) |> and_then(always(fuzzer, _)) |> and_then( - fn(x) { - do_list_between(avg, fuzzer, min, max, length + 1, [x, ..xs]) - }, - ) + fn(x) { + do_list_between(avg, fuzzer, min, max, length + 1, [x, ..xs]) + }, + ) } else if length >= max { with_choice(max_rand) |> map(fn(_) { xs }) } else { rand |> and_then( - fn(n) { - // This is the probability above but simplified to use only - // multiplications since division on-chain is expensive. - if n + n * avg <= max_rand * avg { - fuzzer - |> and_then( - fn(x) { - do_list_between( - avg, - fuzzer, - min, - max, - length + 1, - [x, ..xs], - ) - }, - ) - } else { - constant(xs) - } - }, - ) + fn(n) { + // This is the probability above but simplified to use only + // multiplications since division on-chain is expensive. + if n + n * avg <= max_rand * avg { + fuzzer + |> and_then( + fn(x) { + do_list_between( + avg, + fuzzer, + min, + max, + length + 1, + [x, ..xs], + ) + }, + ) + } else { + constant(xs) + } + }, + ) } } @@ -253,32 +367,55 @@ fn do_list_between(avg, fuzzer, min, max, length, xs) -> Fuzzer> { pub fn one_of(xs: List) -> Fuzzer { let len = list.length(xs) expect len > 0 - int_between(0, len - 1) + uniform(math.log(len, 2) + 1) |> map( - fn(ix: Int) { - expect Some(item) = list.at(xs, ix) - item - }, - ) + fn(ix: Int) { + expect Some(item) = list.at(xs, ix % len) + item + }, + ) } // Combining Fuzzers -pub fn either(_fuzz_a: Fuzzer, _fuzz_b: Fuzzer) -> Fuzzer { - fail +pub fn either(fuzz_a: Fuzzer, fuzz_b: Fuzzer) -> Fuzzer { + bool() + |> and_then( + fn(b) { + if b { + fuzz_a + } else { + fuzz_b + } + }, + ) +} + +pub fn ordered_pair( + fuzz_a: Fuzzer, + fuzz_b: Fuzzer, + compare: fn(a, a) -> Ordering, +) -> Fuzzer<(a, a)> { + let a <- and_then(fuzz_a) + let b <- and_then(fuzz_b) + if compare(a, b) == Greater { + constant((b, a)) + } else { + constant((a, b)) + } } pub fn option(fuzz_a: Fuzzer) -> Fuzzer> { bool() |> and_then( - fn(predicate) { - if predicate { - fuzz_a |> map(Some) - } else { - constant(None) - } - }, - ) + fn(predicate) { + if predicate { + fuzz_a |> map(Some) + } else { + constant(None) + } + }, + ) } /// Combine a [Fuzzer](https://aiken-lang.github.io/prelude/aiken.html#Fuzzer) with the result of a another one. @@ -608,6 +745,6 @@ pub fn map9( pub fn label(str: String) -> Void { str - |> builtin.append_string(@"", _) + |> builtin.append_string(@"\0", _) |> builtin.debug(Void) } diff --git a/lib/aiken/fuzz.test.ak b/lib/aiken/fuzz.test.ak index 9b858b4..973f07e 100644 --- a/lib/aiken/fuzz.test.ak +++ b/lib/aiken/fuzz.test.ak @@ -1,6 +1,12 @@ -use aiken/fuzz.{bool, int, label, list, list_between} +use aiken/fuzz.{ + bool, distinct, filter, int, int_between, label, ordered_pair, list, + list_between, list_with_elem, list_with_subset, negative_int, nonempty_list, + nonnegative_int, positive_int, sorted, uniform, +} +use aiken/int use aiken/list use aiken/math +use aiken/string test prop_int_distribution(n via int()) { label( @@ -48,3 +54,63 @@ test prop_list_int_shrink(xs via list_between(int(), 32, 32)) fail { let below_255 = xs |> list.filter(fn(n) { math.abs(n) <= 255 }) |> list.length below_255 >= 32 } + +fn is_even(x: Int) -> Bool { + x % 2 == 0 +} + +test prop_filter(x via filter(int(), is_even)) { + is_even(x) +} + +test prop_nonempty_list(xs via nonempty_list(int())) { + list.length(xs) > 0 +} + +test prop_test_ordered_pair(pair via ordered_pair(int(), int(), int.compare)) { + let (a, b) = pair + a <= b +} + +test prop_test_sorted(ls via sorted(list(int()), int.compare)) { + let sorted = list.sort(ls, int.compare) + ls == sorted +} + +test prop_positive(n via positive_int()) { + n > 0 +} + +test prop_nonnegative(n via nonnegative_int()) { + n >= 0 +} + +test prop_negative(n via negative_int()) { + n < 0 +} + +test prop_uniform(u via uniform(8)) { + // TODO: this reveals that uniform isn't so uniform, investigate why + label(string.from_int(u % 8)) + True +} + +test prop_list_with_elem(xs via list_with_elem(int())) { + let (xs, x) = xs + list.has(xs, x) +} + +test prop_list_with_subset(xs via list_with_subset(int())) { + let (xs, ys) = xs + list.all(ys, fn(y) { list.has(xs, y) }) +} + +test prop_distinct_list(xs via distinct(list(bool()))) { + list.length(xs) <= 2 +} + +test prop_distinct_list_2( + xs via distinct(list_between(int_between(0, 9), 0, 100)), +) { + list.length(xs) <= 10 +}