Skip to content

Commit 956d473

Browse files
committed
rand_xoshiro: Add state() for serde-free state export (#109)
Add a `state()` method to all 15 RNG types in `rand_xoshiro`, returning the internal state as a value matching the type's `SeedableRng::Seed`. This lets callers persist and reload generator state without enabling the `serde` feature: `Self::from_seed(rng.state())` reconstructs an identical generator. Return type per RNG matches its `Seed`: | RNG | `state()` | | --------------------------------------------------------- | ----------- | | `SplitMix64`, `Xoroshiro64Star`, `Xoroshiro64StarStar` | `[u8; 8]` | | `Xoroshiro128*`, `Xoshiro128*` | `[u8; 16]` | | `Xoshiro256*` | `[u8; 32]` | | `Xoshiro512*` | `Seed512` | The all-zero state is unreachable from any non-zero seed for these algorithms, so the round-trip is exact for any generator obtained via the usual constructors. (`from_seed` remaps an all-zero input to `seed_from_u64(0)`; this is documented on each `state()`.) Implementation lives in four small macros in `common.rs` (`impl_state_scalar!`, `impl_state_pair!`, `impl_state_array!`, `impl_state_seed512!`); each RNG file gets one macro invocation and one `state_roundtrip` test. Fixes #109, #64.
1 parent cc285e9 commit 956d473

17 files changed

Lines changed: 312 additions & 0 deletions

rand_xoshiro/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
### Added
9+
- `state()` method on every RNG type returning the internal state as a value
10+
matching `SeedableRng::Seed`, allowing reproduction via `from_seed` without
11+
the `serde` feature ([#109], [#64]).
12+
13+
[[#109]: https://github.com/rust-random/rngs/issues/109
14+
[[#64]: https://github.com/rust-random/rngs/issues/64
15+
716
## [0.8.0] - 2026-02-01
817
### Changes
918
- Use Edition 2024 and MSRV 1.85 ([#73])

rand_xoshiro/src/common.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,99 @@ macro_rules! impl_xoshiro_large {
222222
};
223223
}
224224

225+
/// Implement `state`, returning the internal state of a single-word RNG as
226+
/// little-endian bytes that round-trip through [`SeedableRng::from_seed`].
227+
macro_rules! impl_state_scalar {
228+
($type:ident) => {
229+
impl $type {
230+
/// Return the internal state of the generator as little-endian
231+
/// bytes that can be passed to [`SeedableRng::from_seed`] to
232+
/// reconstruct an identical generator.
233+
///
234+
/// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed
235+
pub fn state(&self) -> [u8; 8] {
236+
self.x.to_le_bytes()
237+
}
238+
}
239+
};
240+
}
241+
242+
/// Implement `state` for an RNG with two word-sized fields `s0` and `s1`.
243+
macro_rules! impl_state_pair {
244+
($type:ident, $word:ty, $bytes:literal) => {
245+
impl $type {
246+
/// Return the internal state of the generator as little-endian
247+
/// bytes that can be passed to [`SeedableRng::from_seed`] to
248+
/// reconstruct an identical generator.
249+
///
250+
/// The all-zero state is unreachable from any non-zero seed, so
251+
/// the round-trip is exact for any generator obtained via the
252+
/// usual constructors. (An all-zero input to `from_seed` is
253+
/// remapped to `seed_from_u64(0)`.)
254+
///
255+
/// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed
256+
pub fn state(&self) -> [u8; $bytes] {
257+
let mut out = [0u8; $bytes];
258+
let n = core::mem::size_of::<$word>();
259+
out[..n].copy_from_slice(&self.s0.to_le_bytes());
260+
out[n..].copy_from_slice(&self.s1.to_le_bytes());
261+
out
262+
}
263+
}
264+
};
265+
}
266+
267+
/// Implement `state` for an RNG with a `s: [WORD; N]` field.
268+
macro_rules! impl_state_array {
269+
($type:ident, $word:ty, $bytes:literal) => {
270+
impl $type {
271+
/// Return the internal state of the generator as little-endian
272+
/// bytes that can be passed to [`SeedableRng::from_seed`] to
273+
/// reconstruct an identical generator.
274+
///
275+
/// The all-zero state is unreachable from any non-zero seed, so
276+
/// the round-trip is exact for any generator obtained via the
277+
/// usual constructors. (An all-zero input to `from_seed` is
278+
/// remapped to `seed_from_u64(0)`.)
279+
///
280+
/// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed
281+
pub fn state(&self) -> [u8; $bytes] {
282+
let mut out = [0u8; $bytes];
283+
let n = core::mem::size_of::<$word>();
284+
for (i, word) in self.s.iter().enumerate() {
285+
out[i * n..(i + 1) * n].copy_from_slice(&word.to_le_bytes());
286+
}
287+
out
288+
}
289+
}
290+
};
291+
}
292+
293+
/// Implement `state` for a 512-bit RNG (`s: [u64; 8]`), returning a [`Seed512`].
294+
macro_rules! impl_state_seed512 {
295+
($type:ident) => {
296+
impl $type {
297+
/// Return the internal state of the generator as a [`Seed512`]
298+
/// that can be passed to [`SeedableRng::from_seed`] to reconstruct
299+
/// an identical generator.
300+
///
301+
/// The all-zero state is unreachable from any non-zero seed, so
302+
/// the round-trip is exact for any generator obtained via the
303+
/// usual constructors. (An all-zero input to `from_seed` is
304+
/// remapped to `seed_from_u64(0)`.)
305+
///
306+
/// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed
307+
pub fn state(&self) -> crate::Seed512 {
308+
let mut out = [0u8; 64];
309+
for (i, word) in self.s.iter().enumerate() {
310+
out[i * 8..(i + 1) * 8].copy_from_slice(&word.to_le_bytes());
311+
}
312+
crate::Seed512(out)
313+
}
314+
}
315+
};
316+
}
317+
225318
/// Map an all-zero seed to a different one.
226319
macro_rules! deal_with_zero_seed {
227320
($seed:expr, $Self:ident, $bytes:expr) => {

rand_xoshiro/src/splitmix64.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ impl TryRng for SplitMix64 {
6060
}
6161
}
6262

63+
impl_state_scalar!(SplitMix64);
64+
6365
impl SeedableRng for SplitMix64 {
6466
type Seed = [u8; 8];
6567

@@ -142,6 +144,18 @@ mod tests {
142144
}
143145
}
144146

147+
#[test]
148+
fn state_roundtrip() {
149+
let mut rng = SplitMix64::seed_from_u64(42);
150+
for _ in 0..10 {
151+
rng.next_u64();
152+
}
153+
let mut clone = SplitMix64::from_seed(rng.state());
154+
for _ in 0..10 {
155+
assert_eq!(rng.next_u64(), clone.next_u64());
156+
}
157+
}
158+
145159
#[test]
146160
fn next_u32() {
147161
let mut rng = SplitMix64::seed_from_u64(10);

rand_xoshiro/src/xoroshiro128plus.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ impl TryRng for Xoroshiro128Plus {
8080
utils::fill_bytes_via_next_word(dest, || self.try_next_u64())
8181
}
8282
}
83+
impl_state_pair!(Xoroshiro128Plus, u64, 16);
84+
8385
impl SeedableRng for Xoroshiro128Plus {
8486
type Seed = [u8; 16];
8587

@@ -123,4 +125,16 @@ mod tests {
123125
assert_eq!(rng.next_u64(), e);
124126
}
125127
}
128+
129+
#[test]
130+
fn state_roundtrip() {
131+
let mut rng = Xoroshiro128Plus::seed_from_u64(42);
132+
for _ in 0..10 {
133+
rng.next_u64();
134+
}
135+
let mut clone = Xoroshiro128Plus::from_seed(rng.state());
136+
for _ in 0..10 {
137+
assert_eq!(rng.next_u64(), clone.next_u64());
138+
}
139+
}
126140
}

rand_xoshiro/src/xoroshiro128plusplus.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ impl TryRng for Xoroshiro128PlusPlus {
7878
}
7979
}
8080

81+
impl_state_pair!(Xoroshiro128PlusPlus, u64, 16);
82+
8183
impl SeedableRng for Xoroshiro128PlusPlus {
8284
type Seed = [u8; 16];
8385

@@ -122,4 +124,16 @@ mod tests {
122124
assert_eq!(rng.next_u64(), e);
123125
}
124126
}
127+
128+
#[test]
129+
fn state_roundtrip() {
130+
let mut rng = Xoroshiro128PlusPlus::seed_from_u64(42);
131+
for _ in 0..10 {
132+
rng.next_u64();
133+
}
134+
let mut clone = Xoroshiro128PlusPlus::from_seed(rng.state());
135+
for _ in 0..10 {
136+
assert_eq!(rng.next_u64(), clone.next_u64());
137+
}
138+
}
125139
}

rand_xoshiro/src/xoroshiro128starstar.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ impl TryRng for Xoroshiro128StarStar {
7878
}
7979
}
8080

81+
impl_state_pair!(Xoroshiro128StarStar, u64, 16);
82+
8183
impl SeedableRng for Xoroshiro128StarStar {
8284
type Seed = [u8; 16];
8385

@@ -122,4 +124,16 @@ mod tests {
122124
assert_eq!(rng.next_u64(), e);
123125
}
124126
}
127+
128+
#[test]
129+
fn state_roundtrip() {
130+
let mut rng = Xoroshiro128StarStar::seed_from_u64(42);
131+
for _ in 0..10 {
132+
rng.next_u64();
133+
}
134+
let mut clone = Xoroshiro128StarStar::from_seed(rng.state());
135+
for _ in 0..10 {
136+
assert_eq!(rng.next_u64(), clone.next_u64());
137+
}
138+
}
125139
}

rand_xoshiro/src/xoroshiro64star.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ impl TryRng for Xoroshiro64Star {
4949
}
5050
}
5151

52+
impl_state_pair!(Xoroshiro64Star, u32, 8);
53+
5254
impl SeedableRng for Xoroshiro64Star {
5355
type Seed = [u8; 8];
5456

@@ -91,4 +93,16 @@ mod tests {
9193
let mut rng = Xoroshiro64Star::seed_from_u64(0);
9294
assert_ne!(rng.next_u64(), 0);
9395
}
96+
97+
#[test]
98+
fn state_roundtrip() {
99+
let mut rng = Xoroshiro64Star::seed_from_u64(42);
100+
for _ in 0..10 {
101+
rng.next_u32();
102+
}
103+
let mut clone = Xoroshiro64Star::from_seed(rng.state());
104+
for _ in 0..10 {
105+
assert_eq!(rng.next_u32(), clone.next_u32());
106+
}
107+
}
94108
}

rand_xoshiro/src/xoroshiro64starstar.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ impl TryRng for Xoroshiro64StarStar {
4848
}
4949
}
5050

51+
impl_state_pair!(Xoroshiro64StarStar, u32, 8);
52+
5153
impl SeedableRng for Xoroshiro64StarStar {
5254
type Seed = [u8; 8];
5355

@@ -90,4 +92,16 @@ mod tests {
9092
let mut rng = Xoroshiro64StarStar::seed_from_u64(0);
9193
assert_ne!(rng.next_u64(), 0);
9294
}
95+
96+
#[test]
97+
fn state_roundtrip() {
98+
let mut rng = Xoroshiro64StarStar::seed_from_u64(42);
99+
for _ in 0..10 {
100+
rng.next_u32();
101+
}
102+
let mut clone = Xoroshiro64StarStar::from_seed(rng.state());
103+
for _ in 0..10 {
104+
assert_eq!(rng.next_u32(), clone.next_u32());
105+
}
106+
}
93107
}

rand_xoshiro/src/xoshiro128plus.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ impl Xoshiro128Plus {
5656
}
5757
}
5858

59+
impl_state_array!(Xoshiro128Plus, u32, 16);
60+
5961
impl SeedableRng for Xoshiro128Plus {
6062
type Seed = [u8; 16];
6163

@@ -137,4 +139,16 @@ mod tests {
137139
assert_eq!(rng.s[2], 966769569);
138140
assert_eq!(rng.s[3], 3193880526);
139141
}
142+
143+
#[test]
144+
fn state_roundtrip() {
145+
let mut rng = Xoshiro128Plus::seed_from_u64(42);
146+
for _ in 0..10 {
147+
rng.next_u32();
148+
}
149+
let mut clone = Xoshiro128Plus::from_seed(rng.state());
150+
for _ in 0..10 {
151+
assert_eq!(rng.next_u32(), clone.next_u32());
152+
}
153+
}
140154
}

rand_xoshiro/src/xoshiro128plusplus.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ impl Xoshiro128PlusPlus {
5555
}
5656
}
5757

58+
impl_state_array!(Xoshiro128PlusPlus, u32, 16);
59+
5860
impl SeedableRng for Xoshiro128PlusPlus {
5961
type Seed = [u8; 16];
6062

@@ -139,4 +141,16 @@ mod tests {
139141
assert_eq!(rng.s[2], 966769569);
140142
assert_eq!(rng.s[3], 3193880526);
141143
}
144+
145+
#[test]
146+
fn state_roundtrip() {
147+
let mut rng = Xoshiro128PlusPlus::seed_from_u64(42);
148+
for _ in 0..10 {
149+
rng.next_u32();
150+
}
151+
let mut clone = Xoshiro128PlusPlus::from_seed(rng.state());
152+
for _ in 0..10 {
153+
assert_eq!(rng.next_u32(), clone.next_u32());
154+
}
155+
}
142156
}

0 commit comments

Comments
 (0)