diff --git a/lib/bolero-engine/Cargo.toml b/lib/bolero-engine/Cargo.toml index 96f1ad3..2e2dcc5 100644 --- a/lib/bolero-engine/Cargo.toml +++ b/lib/bolero-engine/Cargo.toml @@ -12,7 +12,7 @@ readme = "../../README.md" rust-version = "1.57.0" [features] -rng = ["rand", "bolero-generator/alloc"] +rng = ["rand", "rand_xoshiro", "bolero-generator/alloc"] [dependencies] anyhow = "1" @@ -20,6 +20,7 @@ bolero-generator = { version = "0.10", path = "../bolero-generator", default-fea lazy_static = "1" pretty-hex = "0.3" rand = { version = "0.8", optional = true } +rand_xoshiro = { version = "0.6", optional = true } [target.'cfg(not(kani))'.dependencies] backtrace = { version = "0.3", default-features = false, features = ["std"] } @@ -27,3 +28,4 @@ backtrace = { version = "0.3", default-features = false, features = ["std"] } [dev-dependencies] bolero-generator = { path = "../bolero-generator", features = ["std"] } rand = "^0.8" +rand_xoshiro = "0.6" diff --git a/lib/bolero-engine/src/rng.rs b/lib/bolero-engine/src/rng.rs index d440004..97a92c4 100644 --- a/lib/bolero-engine/src/rng.rs +++ b/lib/bolero-engine/src/rng.rs @@ -1,8 +1,10 @@ use crate::{driver, panic, ByteSliceTestInput, Engine, TargetLocation, Test}; use core::{fmt::Debug, time::Duration}; -use rand::{rngs::StdRng, Rng, RngCore, SeedableRng}; +use rand::{Rng, RngCore, SeedableRng}; use std::time::Instant; +pub use rand_xoshiro::Xoshiro256PlusPlus as Recommended; + #[derive(Clone, Copy, Debug)] pub struct Options { pub test_time: Option, @@ -170,7 +172,7 @@ where } struct RngState { - rng: StdRng, + rng: Recommended, max_len: usize, options: driver::Options, buffer: Vec, @@ -179,7 +181,7 @@ struct RngState { impl RngState { fn new(seed: u64, max_len: usize, options: driver::Options) -> Self { Self { - rng: StdRng::seed_from_u64(seed), + rng: SeedableRng::seed_from_u64(seed), max_len, options, buffer: vec![], diff --git a/lib/bolero-generator/src/uniform.rs b/lib/bolero-generator/src/uniform.rs index caf97bb..983c538 100644 --- a/lib/bolero-generator/src/uniform.rs +++ b/lib/bolero-generator/src/uniform.rs @@ -3,6 +3,7 @@ use core::ops::{Bound, RangeBounds}; pub trait Uniform: Sized + PartialEq + Eq + PartialOrd + Ord { fn sample(fill: &mut F, min: Bound<&Self>, max: Bound<&Self>) -> Option; + fn sample_unbound(fill: &mut F) -> Option; } pub trait FillBytes { @@ -19,8 +20,15 @@ pub trait FillBytes { } macro_rules! uniform_int { - ($ty:ident, $unsigned:ident $(, $smaller:ident)?) => { + ($ty:ident, $unsigned:ident $(, $smaller:ident)*) => { impl Uniform for $ty { + #[inline(always)] + fn sample_unbound(fill: &mut F) -> Option<$ty> { + let mut bytes = [0u8; core::mem::size_of::<$ty>()]; + fill.fill_bytes(&mut bytes)?; + return Some(<$ty>::from_le_bytes(bytes)); + } + #[inline] fn sample(fill: &mut F, min: Bound<&$ty>, max: Bound<&$ty>) -> Option<$ty> { match (min, max) { @@ -45,19 +53,11 @@ macro_rules! uniform_int { | (Bound::Unbounded, Bound::Included(&$ty::MAX)) | (Bound::Included(&$ty::MIN), Bound::Unbounded) | (Bound::Included(&$ty::MIN), Bound::Included(&$ty::MAX)) => { - let mut bytes = [0u8; core::mem::size_of::<$ty>()]; - fill.fill_bytes(&mut bytes)?; - return Some(<$ty>::from_le_bytes(bytes)); + return Self::sample_unbound(fill); } _ => {} } - // if we're in direct mode, just sample a value and check if it's within the provided range - if fill.mode() == DriverMode::Direct { - return Self::sample(fill, Bound::Unbounded, Bound::Unbounded) - .filter(|value| (min, max).contains(value)); - } - let lower = match min { Bound::Included(&v) => v, Bound::Excluded(v) => v.saturating_add(1), @@ -90,16 +90,15 @@ macro_rules! uniform_int { return Some(value); } - })? + })* - let value: $unsigned = Uniform::sample(fill, Bound::Unbounded, Bound::Unbounded)?; + let value: $unsigned = Uniform::sample_unbound(fill)?; if cfg!(test) { assert!(range_inclusive < $unsigned::MAX, "range inclusive should always be less than the max value"); } let range_exclusive = range_inclusive.wrapping_add(1); - // TODO make this less biased - let value = value % range_exclusive; + let value = value.scale(range_exclusive); let value = value as $ty; let value = lower.wrapping_add(value); @@ -118,23 +117,77 @@ uniform_int!(u8, u8); uniform_int!(i8, u8); uniform_int!(u16, u16, u8); uniform_int!(i16, u16, u8); -uniform_int!(u32, u32, u16); -uniform_int!(i32, u32, u16); -uniform_int!(u64, u64, u32); -uniform_int!(i64, u64, u32); -uniform_int!(u128, u128, u64); -uniform_int!(i128, u128, u64); -uniform_int!(usize, usize, u64); -uniform_int!(isize, usize, u64); +uniform_int!(u32, u32, u8, u16); +uniform_int!(i32, u32, u8, u16); +uniform_int!(u64, u64, u8, u16, u32); +uniform_int!(i64, u64, u8, u16, u32); +uniform_int!(usize, usize, u8, u16, u32); +uniform_int!(isize, usize, u8, u16, u32); +uniform_int!(u128, u128, u8, u16, u32, u64); +uniform_int!(i128, u128, u8, u16, u32, u64); + +trait Scaled: Sized { + fn scale(self, range: Self) -> Self; +} + +macro_rules! scaled { + ($s:ty, $upper:ty) => { + impl Scaled for $s { + #[inline(always)] + fn scale(self, range: Self) -> Self { + // similar approach to Lemire random sampling + // see https://lemire.me/blog/2019/06/06/nearly-divisionless-random-integer-generation-on-various-systems/ + let m = self as $upper * range as $upper; + (m >> Self::BITS) as Self + } + } + }; +} + +scaled!(u8, u16); +scaled!(u16, u32); +scaled!(u32, u64); +scaled!(u64, u128); +scaled!(usize, u128); + +impl Scaled for u128 { + #[inline(always)] + fn scale(self, range: Self) -> Self { + // adapted from mulddi3 https://github.com/llvm/llvm-project/blob/6a3982f8b7e37987659706cb3e6427c54c9bc7ce/compiler-rt/lib/builtins/multi3.c#L19 + const BITS_IN_DWORD_2: u32 = 64; + const LOWER_MASK: u128 = u128::MAX >> BITS_IN_DWORD_2; + + let a = self; + let b = range; + + let mut low = (a & LOWER_MASK) * (b & LOWER_MASK); + let mut t = low >> BITS_IN_DWORD_2; + low &= LOWER_MASK; + t += (a >> BITS_IN_DWORD_2) * (b & LOWER_MASK); + low += (t & LOWER_MASK) << BITS_IN_DWORD_2; + let mut high = t >> BITS_IN_DWORD_2; + t = low >> BITS_IN_DWORD_2; + low &= LOWER_MASK; + t += (b >> BITS_IN_DWORD_2) * (a & LOWER_MASK); + low += (t & LOWER_MASK) << BITS_IN_DWORD_2; + high += t >> BITS_IN_DWORD_2; + high += (a >> BITS_IN_DWORD_2) * (b >> BITS_IN_DWORD_2); + + // discard the low bits + let _ = low; + + high + } +} impl Uniform for char { + #[inline(always)] + fn sample_unbound(fill: &mut F) -> Option { + Self::sample(fill, Bound::Unbounded, Bound::Unbounded) + } + #[inline] fn sample(fill: &mut F, min: Bound<&Self>, max: Bound<&Self>) -> Option { - if fill.mode() == DriverMode::Direct { - let value = u32::sample(fill, Bound::Unbounded, Bound::Unbounded)?; - return char::from_u32(value); - } - const START: u32 = 0xD800; const LEN: u32 = 0xE000 - START; @@ -174,6 +227,15 @@ impl Uniform for char { #[cfg(test)] mod tests { use super::*; + use core::fmt; + + #[test] + fn scaled_u128_test() { + assert_eq!(0u128.scale(3), 0); + assert_eq!(u128::MAX.scale(3), 2); + assert_eq!((u128::MAX - 1).scale(3), 2); + assert_eq!((u128::MAX / 2).scale(3), 1); + } #[derive(Clone, Copy, Debug)] struct Byte { @@ -210,7 +272,7 @@ mod tests { } } - #[derive(Clone, Copy, Debug, PartialEq)] + #[derive(Clone, Copy, PartialEq)] struct Seen([bool; 256], core::marker::PhantomData); impl Default for Seen { @@ -219,6 +281,19 @@ mod tests { } } + impl fmt::Debug for Seen { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_list() + .entries( + self.0 + .iter() + .enumerate() + .filter_map(|(idx, seen)| if *seen { Some(idx) } else { None }), + ) + .finish() + } + } + impl Seen { fn insert(&mut self, v: T) { self.0[v.index()] = true; @@ -226,11 +301,15 @@ mod tests { } trait SeenValue: Copy + Uniform + core::fmt::Debug { + const ENTRIES: usize; + fn index(self) -> usize; fn fill_expected(min: Bound, max: Bound, seen: &mut Seen); } impl SeenValue for u8 { + const ENTRIES: usize = 256; + fn index(self) -> usize { self as _ } @@ -245,6 +324,8 @@ mod tests { } impl SeenValue for i8 { + const ENTRIES: usize = 256; + fn index(self) -> usize { (self as isize + -(i8::MIN as isize)).try_into().unwrap() } diff --git a/lib/bolero/src/test/input.rs b/lib/bolero/src/test/input.rs index 0f89008..d3c4da7 100644 --- a/lib/bolero/src/test/input.rs +++ b/lib/bolero/src/test/input.rs @@ -1,8 +1,8 @@ #![cfg_attr(fuzzing_random, allow(dead_code))] -use bolero_engine::RngInput; +use bolero_engine::{rng::Recommended as Rng, RngInput}; use bolero_generator::{driver, TypeGenerator}; -use rand::{rngs::StdRng, SeedableRng}; +use rand::SeedableRng; use std::{io::Read, path::PathBuf}; pub enum TestInput { @@ -42,8 +42,8 @@ impl RngTest { &self, buffer: &'a mut Vec, options: &'a driver::Options, - ) -> RngInput<'a, StdRng> { - RngInput::new(StdRng::seed_from_u64(self.seed), buffer, options) + ) -> RngInput<'a, Rng> { + RngInput::new(Rng::seed_from_u64(self.seed), buffer, options) } pub fn buffered_input<'a>( @@ -51,7 +51,7 @@ impl RngTest { buffer: &'a mut Vec, options: &'a driver::Options, ) -> RngBufferedInput<'a> { - let rng = StdRng::seed_from_u64(self.seed); + let rng = Rng::seed_from_u64(self.seed); let driver = RngBufferedDriver { rng, buffer }; let driver = driver::Rng::new(driver, options); RngBufferedInput { @@ -62,7 +62,7 @@ impl RngTest { } pub struct RngBufferedDriver<'a> { - rng: StdRng, + rng: Rng, buffer: &'a mut Vec, } diff --git a/lib/bolero/src/test/mod.rs b/lib/bolero/src/test/mod.rs index 5de70bc..c09c96c 100644 --- a/lib/bolero/src/test/mod.rs +++ b/lib/bolero/src/test/mod.rs @@ -84,6 +84,7 @@ impl TestEngine { let iterations = self.rng_cfg.iterations_or_default(); let max_len = self.rng_cfg.max_len_or_default(); let seed = self.rng_cfg.seed_or_rand(); + // use StdRng for high entropy seeds let mut seed_rng = StdRng::seed_from_u64(seed); (0..iterations)