use std::error::Error;
use std::fmt;
const GOLDEN_GAMMA: u64 = 0x9e37_79b9_7f4a_7c15;
fn splitmix64(counter: &mut u64) -> u64 {
*counter = counter.wrapping_add(GOLDEN_GAMMA);
let mut mixed = *counter;
mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
mixed ^ (mixed >> 31)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DrawError {
EmptyRange,
WiderThanStream,
}
impl fmt::Display for DrawError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::EmptyRange => f.write_str("a draw below zero has no value to return"),
Self::WiderThanStream => {
f.write_str("a draw over a length wider than 64 bits cannot be made exactly")
}
}
}
}
impl Error for DrawError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Seeded {
words: [u64; 4],
}
impl Seeded {
#[must_use]
pub fn from_seed(seed: u64) -> Self {
let mut counter = seed;
Self {
words: [
splitmix64(&mut counter),
splitmix64(&mut counter),
splitmix64(&mut counter),
splitmix64(&mut counter),
],
}
}
pub fn next_u64(&mut self) -> u64 {
let [s0, s1, s2, s3] = self.words;
let out = s1.wrapping_mul(5).rotate_left(7).wrapping_mul(9);
let t2 = s2 ^ s0;
let t3 = s3 ^ s1;
self.words = [s0 ^ t3, s1 ^ t2, t2 ^ (s1 << 17), t3.rotate_left(45)];
out
}
pub fn below(&mut self, bound: u64) -> Result<u64, DrawError> {
let Some(threshold) = bound.wrapping_neg().checked_rem(bound) else {
let refusal = Err(DrawError::EmptyRange);
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "seeded below: returning an error to the caller");
return refusal;
};
loop {
let product = u128::from(self.next_u64()).wrapping_mul(u128::from(bound));
let [
l0,
l1,
l2,
l3,
l4,
l5,
l6,
l7,
h0,
h1,
h2,
h3,
h4,
h5,
h6,
h7,
] = product.to_le_bytes();
if u64::from_le_bytes([l0, l1, l2, l3, l4, l5, l6, l7]) >= threshold {
return Ok(u64::from_le_bytes([h0, h1, h2, h3, h4, h5, h6, h7]));
}
}
}
pub fn index(&mut self, len: usize) -> Result<usize, DrawError> {
let Ok(bound) = u64::try_from(len) else {
let refusal = Err(DrawError::WiderThanStream);
#[cfg(feature = "trace")]
crate::trace::debug!(len, error = ?refusal.as_ref().err(), "seeded index: returning an error to the caller");
return refusal;
};
let drawn = self.below(bound)?;
usize::try_from(drawn).or(Err(DrawError::WiderThanStream))
}
}
#[cfg(test)]
mod tests {
use super::{DrawError, Seeded};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const VECTORS: [(u64, [u64; 4]); 3] = [
(
0,
[
0x99ec_5f36_cb75_f2b4,
0xbf6e_1f78_4956_452a,
0x1a5f_849d_4933_e6e0,
0x6aa5_94f1_262d_2d2c,
],
),
(
1,
[
0xb3f2_af6d_0fc7_10c5,
0x853b_5596_4736_4cea,
0x92f8_9756_082a_4514,
0x642e_1c7b_c266_a3a7,
],
),
(
u64::MAX,
[
0x8f55_20d5_2a7e_ad08,
0xc476_a018_caa1_802d,
0x81de_31c0_d260_469e,
0xbf65_8d7e_065f_3c2f,
],
),
];
#[test]
fn the_stream_matches_the_reference_vectors() {
for (seed, expected) in VECTORS {
let mut stream = Seeded::from_seed(seed);
let drawn = [
stream.next_u64(),
stream.next_u64(),
stream.next_u64(),
stream.next_u64(),
];
assert_eq!(drawn, expected, "seed {seed:#x} left the published stream");
}
}
#[test]
fn splitmix_fills_the_state_with_its_published_first_word() {
let mut counter = 0;
assert_eq!(super::splitmix64(&mut counter), 0xe220_a839_7b1d_cdaf);
}
#[test]
fn a_zero_bound_and_an_empty_collection_are_refused_without_drawing() {
let mut stream = Seeded::from_seed(3);
let before = stream.clone();
assert_eq!(stream.below(0), Err(DrawError::EmptyRange));
assert_eq!(stream.index(0), Err(DrawError::EmptyRange));
assert_eq!(stream, before, "a refused draw must not move the stream");
}
#[test]
fn a_bound_of_one_always_yields_zero_and_draws_one_word() {
let mut stream = Seeded::from_seed(11);
let mut twin = Seeded::from_seed(11);
for _ in 0..1_000 {
assert_eq!(stream.below(1), Ok(0));
twin.next_u64();
assert_eq!(stream, twin, "a bound of one never rejects");
}
}
#[test]
fn the_rejection_threshold_removes_the_bias_a_remainder_would_have() -> TestResult {
const BOUND: u64 = 0xaaaa_aaaa_aaaa_aaab;
const HALF: u64 = BOUND >> 1;
let mut stream = Seeded::from_seed(0x5eed_0344);
let mut control = Seeded::from_seed(0x5eed_0344);
let (mut lower, mut control_lower) = (0_u32, 0_u32);
for _ in 0..60_000 {
if stream.below(BOUND)? < HALF {
lower = lower.saturating_add(1);
}
let remainder = control
.next_u64()
.checked_rem(BOUND)
.ok_or("a non-zero bound has a remainder")?;
if remainder < HALF {
control_lower = control_lower.saturating_add(1);
}
}
assert!(
lower.abs_diff(30_000) < 1_000,
"the unbiased draw put {lower} of 60000 below the midpoint"
);
assert!(
control_lower.abs_diff(40_000) < 1_000,
"the remainder control lost its bias ({control_lower}), so the test measures nothing"
);
Ok(())
}
}