use core::ops::{Range, RangeInclusive};
const SPLITMIX_GAMMA: u64 = 0x9E37_79B9_7F4A_7C15;
const JUMP: [u64; 4] = [
0x180E_C6D3_3CFD_0ABA,
0xD5A6_1266_F0C9_392C,
0xA958_2618_E03F_C9AA,
0x39AB_DC45_29B1_661C,
];
const LONG_JUMP: [u64; 4] = [
0x76E1_5D3E_FEFD_CBBF,
0xC500_4E44_1C52_2FB3,
0x7771_0069_854E_E241,
0x3910_9BB0_2ACB_E635,
];
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Xoshiro256 {
state: [u64; 4],
}
impl Xoshiro256 {
#[inline]
pub fn seed_from_u64(seed: u64) -> Self {
let mut x = seed;
Self {
state: [
splitmix64(&mut x),
splitmix64(&mut x),
splitmix64(&mut x),
splitmix64(&mut x),
],
}
}
#[inline]
pub fn from_state(state: [u64; 4]) -> Option<Self> {
if state == [0; 4] {
None
} else {
Some(Self { state })
}
}
#[inline]
pub fn state(&self) -> [u64; 4] {
self.state
}
#[inline]
pub fn next_u64(&mut self) -> u64 {
let result = self.state[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
let t = self.state[1] << 17;
self.state[2] ^= self.state[0];
self.state[3] ^= self.state[1];
self.state[1] ^= self.state[2];
self.state[0] ^= self.state[3];
self.state[2] ^= t;
self.state[3] = self.state[3].rotate_left(45);
result
}
#[inline]
pub fn next_u32(&mut self) -> u32 {
(self.next_u64() >> 32) as u32
}
#[inline]
pub fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 * (1.0 / ((1u64 << 53) as f64))
}
pub fn fill_bytes(&mut self, buf: &mut [u8]) {
let mut chunks = buf.chunks_exact_mut(8);
for chunk in &mut chunks {
let bytes = self.next_u64().to_le_bytes();
chunk.copy_from_slice(&bytes);
}
let tail = chunks.into_remainder();
if !tail.is_empty() {
let bytes = self.next_u64().to_le_bytes();
tail.copy_from_slice(&bytes[..tail.len()]);
}
}
#[inline]
pub fn gen_range_u64(&mut self, range: Range<u64>) -> u64 {
let Range { start, end } = range;
assert!(start < end, "gen_range_u64: empty range {start}..{end}");
let span = end - start;
start + self.bounded_u64(span)
}
#[inline]
pub fn gen_range_inclusive_u64(&mut self, range: RangeInclusive<u64>) -> u64 {
let (start, end) = range.into_inner();
assert!(
start <= end,
"gen_range_inclusive_u64: empty range {start}..={end}"
);
if start == 0 && end == u64::MAX {
return self.next_u64();
}
let span = end - start + 1;
start + self.bounded_u64(span)
}
#[inline]
pub fn gen_range_u32(&mut self, range: Range<u32>) -> u32 {
let Range { start, end } = range;
assert!(start < end, "gen_range_u32: empty range {start}..{end}");
let span = (end - start) as u64;
(start as u64 + self.bounded_u64(span)) as u32
}
#[inline]
pub fn gen_range_inclusive_u32(&mut self, range: RangeInclusive<u32>) -> u32 {
let (start, end) = range.into_inner();
assert!(
start <= end,
"gen_range_inclusive_u32: empty range {start}..={end}"
);
let span = (end as u64) - (start as u64) + 1;
(start as u64 + self.bounded_u64(span)) as u32
}
#[inline]
pub fn gen_range_i64(&mut self, range: Range<i64>) -> i64 {
let Range { start, end } = range;
assert!(start < end, "gen_range_i64: empty range {start}..{end}");
let span = (end as i128 - start as i128) as u64;
let offset = self.bounded_u64(span);
((start as i128) + (offset as i128)) as i64
}
#[inline]
pub fn gen_range_inclusive_i64(&mut self, range: RangeInclusive<i64>) -> i64 {
let (start, end) = range.into_inner();
assert!(
start <= end,
"gen_range_inclusive_i64: empty range {start}..={end}"
);
if start == i64::MIN && end == i64::MAX {
return self.next_u64() as i64;
}
let span = ((end as i128) - (start as i128) + 1) as u64;
let offset = self.bounded_u64(span);
((start as i128) + (offset as i128)) as i64
}
#[inline]
pub fn gen_range_i32(&mut self, range: Range<i32>) -> i32 {
let Range { start, end } = range;
assert!(start < end, "gen_range_i32: empty range {start}..{end}");
let span = (end as i64 - start as i64) as u64;
let offset = self.bounded_u64(span);
((start as i64) + (offset as i64)) as i32
}
#[inline]
pub fn gen_range_inclusive_i32(&mut self, range: RangeInclusive<i32>) -> i32 {
let (start, end) = range.into_inner();
assert!(
start <= end,
"gen_range_inclusive_i32: empty range {start}..={end}"
);
let span = ((end as i64) - (start as i64) + 1) as u64;
let offset = self.bounded_u64(span);
((start as i64) + (offset as i64)) as i32
}
#[inline]
pub fn gen_range_f64(&mut self, range: Range<f64>) -> f64 {
let Range { start, end } = range;
assert!(
start.is_finite() && end.is_finite(),
"gen_range_f64: non-finite bounds {start}..{end}"
);
assert!(start < end, "gen_range_f64: empty range {start}..{end}");
let u = self.next_f64();
start + u * (end - start)
}
#[inline]
fn bounded_u64(&mut self, n: u64) -> u64 {
debug_assert!(n != 0, "bounded_u64 requires n > 0");
let mut x = self.next_u64();
let mut m: u128 = (x as u128).wrapping_mul(n as u128);
let mut l: u64 = m as u64;
if l < n {
let t: u64 = n.wrapping_neg() % n;
while l < t {
x = self.next_u64();
m = (x as u128).wrapping_mul(n as u128);
l = m as u64;
}
}
(m >> 64) as u64
}
pub fn jump(&mut self) {
self.apply_jump(&JUMP);
}
pub fn long_jump(&mut self) {
self.apply_jump(&LONG_JUMP);
}
fn apply_jump(&mut self, constants: &[u64; 4]) {
let mut s0 = 0u64;
let mut s1 = 0u64;
let mut s2 = 0u64;
let mut s3 = 0u64;
for &word in constants {
for b in 0..64 {
if (word & (1u64 << b)) != 0 {
s0 ^= self.state[0];
s1 ^= self.state[1];
s2 ^= self.state[2];
s3 ^= self.state[3];
}
let _ = self.next_u64();
}
}
self.state[0] = s0;
self.state[1] = s1;
self.state[2] = s2;
self.state[3] = s3;
}
}
#[inline]
fn splitmix64(x: &mut u64) -> u64 {
*x = x.wrapping_add(SPLITMIX_GAMMA);
let mut z = *x;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seed_produces_a_value() {
let mut rng = Xoshiro256::seed_from_u64(1);
let _ = rng.next_u64();
}
#[test]
fn different_seeds_produce_different_streams() {
let mut a = Xoshiro256::seed_from_u64(1);
let mut b = Xoshiro256::seed_from_u64(2);
assert_ne!(a.next_u64(), b.next_u64());
}
#[test]
fn same_seed_produces_same_stream() {
let mut a = Xoshiro256::seed_from_u64(42);
let mut b = Xoshiro256::seed_from_u64(42);
for _ in 0..256 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn zero_seed_does_not_yield_zero_state() {
let mut rng = Xoshiro256::seed_from_u64(0);
assert_ne!(rng.state(), [0; 4]);
assert_ne!(rng.next_u64(), 0);
}
#[test]
fn from_state_rejects_all_zero() {
assert!(Xoshiro256::from_state([0, 0, 0, 0]).is_none());
assert!(Xoshiro256::from_state([0, 0, 0, 1]).is_some());
}
#[test]
fn state_roundtrip_resumes_stream() {
let mut rng = Xoshiro256::seed_from_u64(99);
for _ in 0..17 {
let _ = rng.next_u64();
}
let snap = rng.state();
let mut resumed = Xoshiro256::from_state(snap).unwrap();
for _ in 0..32 {
assert_eq!(rng.next_u64(), resumed.next_u64());
}
}
#[test]
fn splitmix64_known_value_zero_counter() {
let mut x = 0u64;
assert_eq!(splitmix64(&mut x), 0xE220_A839_7B1D_CDAF);
}
#[test]
fn xoshiro_known_first_outputs_for_seed_zero() {
let mut rng = Xoshiro256::seed_from_u64(0);
let v0 = rng.next_u64();
let v1 = rng.next_u64();
let v2 = rng.next_u64();
let v3 = rng.next_u64();
assert_ne!(v0, 0);
assert_ne!(v0, v1);
assert_ne!(v1, v2);
assert_ne!(v2, v3);
let mut rng2 = Xoshiro256::seed_from_u64(0);
assert_eq!(rng2.next_u64(), v0);
assert_eq!(rng2.next_u64(), v1);
assert_eq!(rng2.next_u64(), v2);
assert_eq!(rng2.next_u64(), v3);
}
#[test]
fn next_u32_takes_high_bits() {
let mut a = Xoshiro256::seed_from_u64(7);
let mut b = Xoshiro256::seed_from_u64(7);
for _ in 0..64 {
let hi = (a.next_u64() >> 32) as u32;
assert_eq!(hi, b.next_u32());
}
}
#[test]
fn next_f64_bounds() {
let mut rng = Xoshiro256::seed_from_u64(1);
for _ in 0..10_000 {
let f = rng.next_f64();
assert!((0.0..1.0).contains(&f));
}
}
#[test]
fn fill_bytes_exact_chunks() {
let mut a = Xoshiro256::seed_from_u64(123);
let mut b = Xoshiro256::seed_from_u64(123);
let mut buf = [0u8; 64];
a.fill_bytes(&mut buf);
for chunk in buf.chunks_exact(8) {
let expected = b.next_u64().to_le_bytes();
assert_eq!(chunk, expected);
}
}
#[test]
fn fill_bytes_partial_tail() {
let mut rng = Xoshiro256::seed_from_u64(2026);
let mut buf = [0u8; 13];
rng.fill_bytes(&mut buf);
assert!(buf.iter().any(|&b| b != 0));
}
#[test]
fn jump_produces_different_state() {
let mut a = Xoshiro256::seed_from_u64(1);
let mut b = a.clone();
b.jump();
assert_ne!(a.state(), b.state());
assert_ne!(a.next_u64(), b.next_u64());
}
#[test]
fn long_jump_produces_different_state() {
let mut a = Xoshiro256::seed_from_u64(1);
let mut b = a.clone();
b.long_jump();
assert_ne!(a.state(), b.state());
assert_ne!(a.next_u64(), b.next_u64());
}
#[test]
fn jump_is_deterministic() {
let mut a = Xoshiro256::seed_from_u64(5);
let mut b = Xoshiro256::seed_from_u64(5);
a.jump();
b.jump();
assert_eq!(a.state(), b.state());
}
#[test]
fn gen_range_u64_bounds() {
let mut rng = Xoshiro256::seed_from_u64(1);
for _ in 0..10_000 {
let n = rng.gen_range_u64(100..200);
assert!((100..200).contains(&n));
}
}
#[test]
fn gen_range_u64_single_value_at_top() {
let mut rng = Xoshiro256::seed_from_u64(2);
for _ in 0..1000 {
assert_eq!(rng.gen_range_u64(7..8), 7);
}
}
#[test]
fn gen_range_inclusive_u64_die_roll() {
let mut rng = Xoshiro256::seed_from_u64(3);
let mut faces = [0u32; 6];
for _ in 0..10_000 {
let d = rng.gen_range_inclusive_u64(1..=6);
assert!((1..=6).contains(&d));
faces[(d - 1) as usize] += 1;
}
for (i, &c) in faces.iter().enumerate() {
assert!(c > 0, "face {} never appeared in 10000 rolls", i + 1);
}
}
#[test]
fn gen_range_inclusive_u64_single_value() {
let mut rng = Xoshiro256::seed_from_u64(4);
for _ in 0..1000 {
assert_eq!(rng.gen_range_inclusive_u64(42..=42), 42);
}
}
#[test]
fn gen_range_inclusive_u64_full_width_uses_raw_draw() {
let mut a = Xoshiro256::seed_from_u64(5);
let mut b = Xoshiro256::seed_from_u64(5);
for _ in 0..256 {
assert_eq!(a.gen_range_inclusive_u64(0..=u64::MAX), b.next_u64());
}
}
#[test]
fn gen_range_u32_bounds() {
let mut rng = Xoshiro256::seed_from_u64(6);
for _ in 0..10_000 {
let n = rng.gen_range_u32(0..256);
assert!(n < 256);
}
}
#[test]
fn gen_range_inclusive_u32_full_width() {
let mut rng = Xoshiro256::seed_from_u64(7);
for _ in 0..1000 {
let _ = rng.gen_range_inclusive_u32(0..=u32::MAX);
}
}
#[test]
fn gen_range_i64_negative_range() {
let mut rng = Xoshiro256::seed_from_u64(8);
for _ in 0..10_000 {
let n = rng.gen_range_i64(-100..-50);
assert!((-100..-50).contains(&n));
}
}
#[test]
fn gen_range_i64_mixed_sign_range() {
let mut rng = Xoshiro256::seed_from_u64(9);
let mut saw_neg = false;
let mut saw_pos = false;
for _ in 0..10_000 {
let n = rng.gen_range_i64(-100..100);
assert!((-100..100).contains(&n));
if n < 0 {
saw_neg = true;
}
if n >= 0 {
saw_pos = true;
}
}
assert!(saw_neg && saw_pos);
}
#[test]
fn gen_range_inclusive_i64_full_width_is_raw_draw() {
let mut a = Xoshiro256::seed_from_u64(10);
let mut b = Xoshiro256::seed_from_u64(10);
for _ in 0..256 {
let from_range = a.gen_range_inclusive_i64(i64::MIN..=i64::MAX);
let from_raw = b.next_u64() as i64;
assert_eq!(from_range, from_raw);
}
}
#[test]
fn gen_range_i32_bounds() {
let mut rng = Xoshiro256::seed_from_u64(11);
for _ in 0..10_000 {
let n = rng.gen_range_i32(-1000..1000);
assert!((-1000..1000).contains(&n));
}
}
#[test]
fn gen_range_inclusive_i32_full_width() {
let mut rng = Xoshiro256::seed_from_u64(12);
for _ in 0..1000 {
let _ = rng.gen_range_inclusive_i32(i32::MIN..=i32::MAX);
}
}
#[test]
fn gen_range_f64_bounds() {
let mut rng = Xoshiro256::seed_from_u64(13);
for _ in 0..10_000 {
let x = rng.gen_range_f64(-1.0..1.0);
assert!((-1.0..1.0).contains(&x));
}
}
#[test]
fn gen_range_f64_positive_range() {
let mut rng = Xoshiro256::seed_from_u64(14);
for _ in 0..10_000 {
let x = rng.gen_range_f64(10.0..20.0);
assert!((10.0..20.0).contains(&x));
}
}
#[test]
#[should_panic(expected = "empty range")]
fn gen_range_u64_panics_on_empty() {
let mut rng = Xoshiro256::seed_from_u64(1);
let _ = rng.gen_range_u64(10..10);
}
#[test]
#[should_panic(expected = "empty range")]
#[allow(clippy::reversed_empty_ranges)]
fn gen_range_u64_panics_on_reverse() {
let mut rng = Xoshiro256::seed_from_u64(1);
let _ = rng.gen_range_u64(10..5);
}
#[test]
#[should_panic(expected = "empty range")]
#[allow(clippy::reversed_empty_ranges)]
fn gen_range_inclusive_u64_panics_on_reverse() {
let mut rng = Xoshiro256::seed_from_u64(1);
let _ = rng.gen_range_inclusive_u64(10..=5);
}
#[test]
#[should_panic(expected = "empty range")]
#[allow(clippy::reversed_empty_ranges)]
fn gen_range_i64_panics_on_reverse() {
let mut rng = Xoshiro256::seed_from_u64(1);
let _ = rng.gen_range_i64(5..-5);
}
#[test]
#[should_panic(expected = "non-finite")]
fn gen_range_f64_panics_on_nan() {
let mut rng = Xoshiro256::seed_from_u64(1);
let _ = rng.gen_range_f64(f64::NAN..1.0);
}
#[test]
#[should_panic(expected = "non-finite")]
fn gen_range_f64_panics_on_infinity() {
let mut rng = Xoshiro256::seed_from_u64(1);
let _ = rng.gen_range_f64(0.0..f64::INFINITY);
}
#[test]
#[should_panic(expected = "empty range")]
fn gen_range_f64_panics_on_reverse() {
let mut rng = Xoshiro256::seed_from_u64(1);
let _ = rng.gen_range_f64(1.0..0.0);
}
#[test]
fn gen_range_is_deterministic_under_same_seed() {
let mut a = Xoshiro256::seed_from_u64(99);
let mut b = Xoshiro256::seed_from_u64(99);
for _ in 0..1000 {
assert_eq!(a.gen_range_u64(0..1000), b.gen_range_u64(0..1000));
}
}
#[test]
fn gen_range_uniformity_chi_squared() {
let mut rng = Xoshiro256::seed_from_u64(0xC0DE_F00D);
let mut counts = [0u32; 100];
for _ in 0..100_000 {
let v = rng.gen_range_u32(0..100);
counts[v as usize] += 1;
}
let expected = 100_000.0 / 100.0;
let chi: f64 = counts
.iter()
.map(|&c| {
let diff = c as f64 - expected;
diff * diff / expected
})
.sum();
assert!(
chi < 250.0,
"chi-squared {chi} too high — bounded-range output is biased"
);
}
#[test]
fn gen_range_die_roll_uniformity() {
let mut rng = Xoshiro256::seed_from_u64(0xD1CE_D011);
let mut faces = [0u32; 6];
for _ in 0..600_000 {
let d = rng.gen_range_inclusive_u32(1..=6);
faces[(d - 1) as usize] += 1;
}
let expected = 100_000.0;
let chi: f64 = faces
.iter()
.map(|&c| {
let diff = c as f64 - expected;
diff * diff / expected
})
.sum();
assert!(chi < 50.0, "d6 uniformity chi-squared {chi} too high");
}
}