diff --git a/lib/aiken/fuzz.ak b/lib/aiken/fuzz.ak index 054bf28..e284606 100644 --- a/lib/aiken/fuzz.ak +++ b/lib/aiken/fuzz.ak @@ -1,6 +1,7 @@ use aiken/builtin use aiken/collection/list use aiken/math +use aiken/math/rational.{Rational, new, reduce} use aiken/option // ## Constructing @@ -242,6 +243,137 @@ pub fn int_at_most(max: Int) -> Fuzzer { } } +/// Generates a random rational value within the range `[-255, 16383]`, +/// following the specified distribution: +/// [-255,-101] 0.1% +/// [-100,-1] 23.3% +/// [0, 100] 68.8% +/// [101,255] 4.9% +/// >255 3.0% +pub fn rational() -> Fuzzer { + map( + both(int(), int_at_least(1)), + fn((numerator, denominator)) { + expect Some(fraction) = new(numerator, denominator) + fraction + }, + ) +} + +/// Generates rational values between a lower and upper bound (both inclusive), with the following distribution: +/// [-255, -101] 31.1% +/// [-100, -1] 19.2% +/// [0, 100] 19.6% +/// [101, 255] 30.1% +/// +/// The upper and lower bounds must be between -255 and 255. +pub fn rational_between( + lower_bound: Rational, + upper_bound: Rational, +) -> Fuzzer { + expect correct_bounds(lower_bound, upper_bound) + + if lower_bound == upper_bound { + lower_bound |> rational.reduce |> constant + } else if rational.compare(lower_bound, upper_bound) == Greater { + rational_between(upper_bound, lower_bound) + } else { + let denominator <- + and_then( + int_at_least( + math.max( + rational.denominator(lower_bound), + rational.denominator(upper_bound), + ) + 1, + ), + ) + + let min_numerator = + binary_search_min_numerator( + lower_bound, + denominator, + -255 * denominator, + 255 * denominator, + ) + let max_numerator = + binary_search_max_numerator( + upper_bound, + denominator, + -255 * denominator, + 255 * denominator, + ) + map( + int_between(min_numerator, max_numerator), + fn(numerator) { + expect Some(fraction) = new(numerator, denominator) + reduce(fraction) + }, + ) + } +} + +/// Generates a random rational value that is at least `optional_min`. +/// The lower bound must be between -255 and 255. +pub fn rational_at_least(lower_bound: Rational) -> Fuzzer { + expect Some(upper_bound) = new(255, 1) + rational_between(lower_bound, upper_bound) +} + +/// Generates a random rational value that is at most `opt_max`. +/// The upper bound must be between -255 and 255. +pub fn rational_at_most(upper_bound: Rational) { + expect Some(lower_bound) = new(-255, 1) + rational_between(lower_bound, upper_bound) +} + +fn correct_bounds(min_fraction: Rational, max_fraction: Rational) { + expect Some(lower_bound) = new(-256, 1) + expect Some(upper_bound) = new(256, 1) + + and { + rational.compare(min_fraction, lower_bound) == Greater, + rational.compare(min_fraction, upper_bound) == Less, + rational.compare(max_fraction, lower_bound) == Greater, + rational.compare(max_fraction, upper_bound) == Less, + } +} + +fn binary_search_min_numerator( + min_fraction: Rational, + denominator: Int, + low: Int, + high: Int, +) -> Int { + let mid_point = ( low + high ) / 2 + expect Some(mid_fraction) = new(mid_point, denominator) + + if low > high { + low + } else if rational.compare(mid_fraction, min_fraction) == Less { + binary_search_min_numerator(min_fraction, denominator, mid_point + 1, high) + } else { + binary_search_min_numerator(min_fraction, denominator, low, mid_point - 1) + } +} + +fn binary_search_max_numerator( + max_fraction: Rational, + denominator: Int, + low: Int, + high: Int, +) -> Int { + let mid_point = ( low + high ) / 2 + expect Some(mid_fraction) = new(mid_point, denominator) + + if low > high { + high + } else if rational.compare(mid_fraction, max_fraction) == Greater { + binary_search_max_numerator(max_fraction, denominator, low, mid_point - 1) + } else { + binary_search_max_numerator(max_fraction, denominator, mid_point + 1, high) + } +} + // ### Data-structures /// Generate a random list of elements from a given fuzzer. The list contains diff --git a/lib/aiken/fuzz.test.ak b/lib/aiken/fuzz.test.ak index 2fb11c7..939dfa8 100644 --- a/lib/aiken/fuzz.test.ak +++ b/lib/aiken/fuzz.test.ak @@ -3,10 +3,12 @@ use aiken/fuzz.{ and_then, bool, byte, bytearray, constant, either3, either4, either5, either6, either7, either8, either9, int, int_between, label, list_between, list_with_elem, map, map2, map3, map4, map5, map6, map7, map8, map9, one_of, - set, set_between, sublist, such_that, tuple, tuple3, tuple4, tuple5, tuple6, - tuple7, tuple8, tuple9, + rational, rational_at_least, rational_at_most, rational_between, set, + set_between, sublist, such_that, tuple, tuple3, tuple4, tuple5, tuple6, tuple7, + tuple8, tuple9, } use aiken/math +use aiken/math/rational.{Rational} use aiken/primitive/bytearray use aiken/primitive/string @@ -20,7 +22,7 @@ test prop_int_distribution(n via int()) { @"0" } else if n < 256 { @"]0; 255]" - } else if n < 16383 { + } else if n <= 16383 { @"[256; 16383]" } else { fail @"n > 16383" @@ -207,6 +209,92 @@ test prop_set_between_distribution(n via set_between(int_between(0, 50), 3, 13)) True } +test rational_distribution(fraction via rational()) { + fraction_distribution(fraction) +} + +fn expect_rational(numerator: Int, denominator: Int) -> Rational { + expect Some(r) = rational.new(numerator, denominator) + r +} + +test prop_fraction_between_bounds( + fraction via rational_between( + expect_rational(-255, 1), + expect_rational(255, 1), + ), +) { + fraction_distribution(fraction) + and { + { + let ord = rational.compare(fraction, expect_rational(-255, 1)) + ord == Greater || ord == Equal + }, + { + let ord = rational.compare(fraction, expect_rational(255, 1)) + ord == Less || ord == Equal + }, + } +} + +test prop_at_least_for_positive_fractions( + fraction via rational_at_least(expect_rational(1, 1)), +) { + fraction_distribution(fraction) + + and { + { + let ord = rational.compare(fraction, expect_rational(1, 1)) + ord == Greater || ord == Equal + }, + { + let ord = rational.compare(fraction, expect_rational(255, 1)) + ord == Less || ord == Equal + }, + } +} + +test prop_at_most_for_negative_fraction( + fraction via rational_at_most(expect_rational(0, 1)), +) { + fraction_distribution(fraction) + + and { + { + let ord = rational.compare(fraction, expect_rational(-255, 1)) + ord == Greater || ord == Equal + }, + { + let ord = rational.compare(fraction, expect_rational(0, 1)) + ord == Less || ord == Equal + }, + } +} + +fn fraction_distribution(fraction: Rational) { + expect Some(bound_1) = rational.new(-255, 1) + expect Some(bound_2) = rational.new(-100, 1) + expect Some(bound_3) = rational.new(0, 1) + expect Some(bound_4) = rational.new(100, 1) + expect Some(bound_5) = rational.new(256, 1) + + label( + if rational.compare(fraction, bound_1) == Less { + fail + } else if rational.compare(fraction, bound_2) == Less { + @"[-255,-101]" + } else if rational.compare(fraction, bound_3) == Less { + @"[-100,-1]" + } else if rational.compare(fraction, bound_4) == Less { + @"[0, 100]" + } else if rational.compare(fraction, bound_5) == Less { + @"[101,255]" + } else { + @">255" + }, + ) +} + // This property simply illustrate a case where the `set` // fuzzer would fail and not loop forever after not being // able to satisfy the demand (not enough entropy in the