Skip to main content

vrd/
xoshiro.rs

1// Copyright © 2023-2026 vrd. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Xoshiro256++ pseudo-random number generator.
5//!
6//! Small-state (32 bytes) generator with a period of 2^256 - 1, designed
7//! by David Blackman and Sebastiano Vigna. See
8//! <https://prng.di.unimi.it/xoshiro256plusplus.c>.
9//!
10//! Seeds are routed through SplitMix64 before priming the state, as
11//! recommended by the original authors, so low-entropy seeds (e.g.
12//! `[0u8; 32]` or `[1u8; 32]`) still yield well-distributed initial state.
13
14use core::convert::Infallible;
15use rand::rand_core::{SeedableRng, TryRng};
16
17/// Xoshiro256++ generator state.
18///
19/// # Examples
20///
21/// ```
22/// use vrd::xoshiro::Xoshiro256PlusPlus;
23///
24/// let rng = Xoshiro256PlusPlus::from_u64_seed(42);
25/// ```
26#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
27#[cfg_attr(
28    feature = "serde",
29    derive(serde::Serialize, serde::Deserialize)
30)]
31pub struct Xoshiro256PlusPlus {
32    /// 256-bit state, four 64-bit words. Combined via XOR /
33    /// shift / rotate to produce each output. Never observed
34    /// directly except via `jump()` derivatives.
35    state: [u64; 4],
36}
37
38/// SplitMix64 - a fast, well-distributed 64-bit mixer used to whiten seed
39/// material before priming the main generator. Constants from
40/// <https://prng.di.unimi.it/splitmix64.c>.
41#[inline]
42fn splitmix64(state: &mut u64) -> u64 {
43    *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
44    let mut z = *state;
45    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
46    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
47    z ^ (z >> 31)
48}
49
50impl Xoshiro256PlusPlus {
51    /// Builds an instance from a 32-byte seed, whitened via SplitMix64.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use vrd::xoshiro::Xoshiro256PlusPlus;
57    ///
58    /// let seed = [0u8; 32];
59    /// let mut rng = Xoshiro256PlusPlus::from_seed(seed);
60    /// assert_ne!(rng.next_u64(), 0);
61    /// ```
62    pub fn from_seed(seed: [u8; 32]) -> Self {
63        let mut sm = u64::from_le_bytes([
64            seed[0], seed[1], seed[2], seed[3], seed[4], seed[5],
65            seed[6], seed[7],
66        ]) ^ u64::from_le_bytes([
67            seed[8], seed[9], seed[10], seed[11], seed[12], seed[13],
68            seed[14], seed[15],
69        ]) ^ u64::from_le_bytes([
70            seed[16], seed[17], seed[18], seed[19], seed[20], seed[21],
71            seed[22], seed[23],
72        ]) ^ u64::from_le_bytes([
73            seed[24], seed[25], seed[26], seed[27], seed[28], seed[29],
74            seed[30], seed[31],
75        ]);
76        if sm == 0 {
77            sm = 0xBAD1_DEAA_5BD1_CAFE;
78        }
79        let s0 = splitmix64(&mut sm);
80        let s1 = splitmix64(&mut sm);
81        let s2 = splitmix64(&mut sm);
82        let s3 = splitmix64(&mut sm);
83        Self {
84            state: [s0, s1, s2, s3],
85        }
86    }
87
88    /// Convenience constructor from a single `u64` seed.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use vrd::xoshiro::Xoshiro256PlusPlus;
94    ///
95    /// let mut rng = Xoshiro256PlusPlus::from_u64_seed(12345);
96    /// assert_ne!(rng.next_u64(), 0);
97    /// ```
98    pub fn from_u64_seed(mut seed: u64) -> Self {
99        if seed == 0 {
100            seed = 0xBAD1_DEAA_5BD1_CAFE;
101        }
102        let s0 = splitmix64(&mut seed);
103        let s1 = splitmix64(&mut seed);
104        let s2 = splitmix64(&mut seed);
105        let s3 = splitmix64(&mut seed);
106        Self {
107            state: [s0, s1, s2, s3],
108        }
109    }
110
111    /// Generates the next random `u64`.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use vrd::xoshiro::Xoshiro256PlusPlus;
117    ///
118    /// let mut rng = Xoshiro256PlusPlus::from_u64_seed(42);
119    /// let n = rng.next_u64();
120    /// ```
121    #[inline]
122    pub fn next_u64(&mut self) -> u64 {
123        let res = self.state[0]
124            .wrapping_add(self.state[3])
125            .rotate_left(23)
126            .wrapping_add(self.state[0]);
127
128        let t = self.state[1] << 17;
129
130        self.state[2] ^= self.state[0];
131        self.state[3] ^= self.state[1];
132        self.state[1] ^= self.state[2];
133        self.state[0] ^= self.state[3];
134
135        self.state[2] ^= t;
136        self.state[3] = self.state[3].rotate_left(45);
137
138        res
139    }
140
141    /// Generates the next random `u32` by taking the high 32 bits of the
142    /// next `u64`.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use vrd::xoshiro::Xoshiro256PlusPlus;
148    ///
149    /// let mut rng = Xoshiro256PlusPlus::from_u64_seed(42);
150    /// let n = rng.next_u32();
151    /// ```
152    #[inline]
153    pub fn next_u32(&mut self) -> u32 {
154        (self.next_u64() >> 32) as u32
155    }
156
157    /// Fills `dest` with random bytes.
158    ///
159    /// With the `simd` feature, large buffers go through a SIMD-batched
160    /// path that holds K independent Xoshiro256++ lanes in vector
161    /// registers (K = 2 on AArch64 NEON, K = 4 on x86_64 AVX2). Same
162    /// seed produces a **different** byte stream under `simd` vs.
163    /// scalar - see [`crate::xoshiro_simd`] for the contract.
164    ///
165    /// # Examples
166    ///
167    /// ```
168    /// use vrd::xoshiro::Xoshiro256PlusPlus;
169    ///
170    /// let mut rng = Xoshiro256PlusPlus::from_u64_seed(42);
171    /// let mut buf = [0u8; 16];
172    /// rng.fill_bytes(&mut buf);
173    /// ```
174    pub fn fill_bytes(&mut self, dest: &mut [u8]) {
175        #[cfg(feature = "simd")]
176        crate::xoshiro_simd::fill_bytes(self, dest);
177        #[cfg(not(feature = "simd"))]
178        self.fill_bytes_scalar(dest);
179    }
180
181    /// Scalar `fill_bytes` - always available; the SIMD path falls
182    /// back to this for the trailing bytes that don't fill a full
183    /// register.
184    pub(crate) fn fill_bytes_scalar(&mut self, dest: &mut [u8]) {
185        let mut i = 0;
186        while i + 8 <= dest.len() {
187            let bytes = self.next_u64().to_le_bytes();
188            dest[i..i + 8].copy_from_slice(&bytes);
189            i += 8;
190        }
191        if i < dest.len() {
192            let bytes = self.next_u64().to_le_bytes();
193            let remaining = dest.len() - i;
194            dest[i..].copy_from_slice(&bytes[..remaining]);
195        }
196    }
197
198    /// Returns a copy of the current 4-word state. Used by the SIMD
199    /// fill path (feature `simd`) to seed independent lanes via
200    /// [`Self::jump`].
201    #[doc(hidden)]
202    #[cfg(feature = "simd")]
203    pub(crate) fn state_snapshot(&self) -> [u64; 4] {
204        self.state
205    }
206
207    /// Overwrites the 4-word state. Pair with [`Self::state_snapshot`]
208    /// to roll the SIMD lanes back into the scalar generator.
209    #[doc(hidden)]
210    #[cfg(feature = "simd")]
211    pub(crate) fn set_state(&mut self, state: [u64; 4]) {
212        self.state = state;
213    }
214
215    /// Advances the state by 2^128 calls to [`Self::next_u64`].
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// use vrd::xoshiro::Xoshiro256PlusPlus;
221    ///
222    /// let mut rng = Xoshiro256PlusPlus::from_u64_seed(42);
223    /// rng.jump();
224    /// ```
225    pub fn jump(&mut self) {
226        const JUMP: [u64; 4] = [
227            0x180E_C6D3_3CFD_0ABA,
228            0xD5A6_1266_F0C9_392C,
229            0xA958_2618_E03F_C9AA,
230            0x39AB_DC45_29B1_661C,
231        ];
232
233        let mut s0 = 0u64;
234        let mut s1 = 0u64;
235        let mut s2 = 0u64;
236        let mut s3 = 0u64;
237
238        for &jump in &JUMP {
239            for b in 0..64 {
240                if (jump & (1u64 << b)) != 0 {
241                    s0 ^= self.state[0];
242                    s1 ^= self.state[1];
243                    s2 ^= self.state[2];
244                    s3 ^= self.state[3];
245                }
246                let _ = self.next_u64();
247            }
248        }
249
250        self.state = [s0, s1, s2, s3];
251    }
252}
253
254impl TryRng for Xoshiro256PlusPlus {
255    type Error = Infallible;
256
257    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
258        Ok(self.next_u32())
259    }
260
261    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
262        Ok(self.next_u64())
263    }
264
265    fn try_fill_bytes(
266        &mut self,
267        dest: &mut [u8],
268    ) -> Result<(), Self::Error> {
269        self.fill_bytes(dest);
270        Ok(())
271    }
272}
273
274impl SeedableRng for Xoshiro256PlusPlus {
275    type Seed = [u8; 32];
276
277    fn from_seed(seed: Self::Seed) -> Self {
278        Self::from_seed(seed)
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn produces_nonzero_from_zero_seed() {
288        let mut rng = Xoshiro256PlusPlus::from_seed([0u8; 32]);
289        // SplitMix64 whitening must yield non-zero state.
290        assert_ne!(rng.next_u64(), 0);
291    }
292
293    #[test]
294    fn produces_nonzero_from_one_seed() {
295        let mut rng = Xoshiro256PlusPlus::from_seed([1u8; 32]);
296        assert_ne!(rng.next_u64(), 0);
297    }
298
299    #[test]
300    fn deterministic_for_same_seed() {
301        let seed = [42u8; 32];
302        let mut a = Xoshiro256PlusPlus::from_seed(seed);
303        let mut b = Xoshiro256PlusPlus::from_seed(seed);
304        for _ in 0..16 {
305            assert_eq!(a.next_u64(), b.next_u64());
306        }
307    }
308
309    #[test]
310    fn jump_diverges_from_baseline() {
311        let seed = [123u8; 32];
312        let mut a = Xoshiro256PlusPlus::from_seed(seed);
313        let mut b = Xoshiro256PlusPlus::from_seed(seed);
314        a.jump();
315        assert_ne!(a.next_u64(), b.next_u64());
316    }
317
318    #[test]
319    fn fill_bytes_handles_unaligned_lengths() {
320        let mut rng = Xoshiro256PlusPlus::from_seed([7u8; 32]);
321        let mut buf = [0u8; 17]; // not a multiple of 8
322        rng.fill_bytes(&mut buf);
323        // At least one byte should be non-zero; the probability of all
324        // zeros is 2^-136, so this is effectively a structural check.
325        assert!(buf.iter().any(|&b| b != 0));
326    }
327
328    #[test]
329    fn next_u32_uses_high_bits() {
330        let rng = Xoshiro256PlusPlus::from_seed([99u8; 32]);
331        let high = (rng.clone().next_u64() >> 32) as u32;
332        let mut copy = rng;
333        assert_eq!(copy.next_u32(), high);
334    }
335}
336
337#[cfg(test)]
338mod additional_xoshiro_tests {
339    use super::*;
340
341    #[test]
342    fn test_xoshiro_from_u64_seed() {
343        let mut rng = Xoshiro256PlusPlus::from_u64_seed(123);
344        let mut rng2 = Xoshiro256PlusPlus::from_u64_seed(123);
345        assert_eq!(rng.next_u64(), rng2.next_u64());
346    }
347}
348
349#[cfg(test)]
350mod xoshiro_coverage {
351    use super::*;
352    #[cfg(feature = "alloc")]
353    use alloc::format;
354    #[cfg(all(not(feature = "alloc"), feature = "std"))]
355    use std::format;
356
357    #[test]
358    fn test_xoshiro_fill_bytes_large() {
359        let mut rng = Xoshiro256PlusPlus::from_u64_seed(42);
360        let mut buf = [0u8; 100];
361        rng.fill_bytes(&mut buf);
362        assert!(buf.iter().any(|&x| x != 0));
363    }
364
365    #[test]
366    fn test_xoshiro_next_u32() {
367        let mut rng = Xoshiro256PlusPlus::from_u64_seed(42);
368        let _ = rng.next_u32();
369    }
370
371    #[test]
372    fn test_xoshiro_debug_display() {
373        let rng = Xoshiro256PlusPlus::from_u64_seed(42);
374        let s = format!("{:?}", rng);
375        assert!(s.contains("Xoshiro256PlusPlus"));
376    }
377}
378
379#[cfg(test)]
380mod xoshiro_final_coverage {
381    use super::*;
382
383    #[test]
384    fn test_xoshiro_try_next() {
385        let mut rng = Xoshiro256PlusPlus::from_u64_seed(42);
386        assert!(rng.try_next_u32().is_ok());
387        assert!(rng.try_next_u64().is_ok());
388    }
389
390    #[test]
391    fn test_xoshiro_seedable_rng() {
392        let seed = [1u8; 32];
393        let mut rng =
394            <Xoshiro256PlusPlus as SeedableRng>::from_seed(seed);
395        assert_ne!(rng.next_u64(), 0);
396    }
397}
398
399#[cfg(test)]
400mod xoshiro_zero_test {
401    use super::*;
402
403    #[test]
404    fn test_xoshiro_all_zero_seed() {
405        let seed = [0u8; 32];
406        let mut rng = Xoshiro256PlusPlus::from_seed(seed);
407        assert_ne!(rng.next_u64(), 0);
408    }
409}
410
411#[cfg(test)]
412mod xoshiro_edge_cases {
413    use super::*;
414
415    #[test]
416    fn test_xoshiro_from_u64_seed_zero() {
417        let mut rng = Xoshiro256PlusPlus::from_u64_seed(0);
418        assert_ne!(rng.next_u64(), 0);
419    }
420}