Skip to content

Commit be9aa09

Browse files
authored
Merge pull request #110 from rust-random/export-xoshiro-state
rand_xoshiro: Add `state()` for serde-free state export
2 parents 228241b + a9ca2ac commit be9aa09

19 files changed

Lines changed: 237 additions & 3 deletions

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rand_xoshiro/CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ 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]
7+
## [0.8.1] - 2026-05-16
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 ([#110]).
12+
813
### Changed
914
- Shrink the code size of the all-zero-seed fallback by replacing the per-RNG
1015
`SplitMix64::seed_from_u64(0)` call with a single shared constant.
1116

17+
[#110]: https://github.com/rust-random/rngs/pull/110
18+
1219
## [0.8.0] - 2026-02-01
1320
### Changes
1421
- Use Edition 2024 and MSRV 1.85 ([#73])

rand_xoshiro/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rand_xoshiro"
3-
version = "0.8.0"
3+
version = "0.8.1"
44
authors = ["The Rand Project Developers"]
55
license = "MIT OR Apache-2.0"
66
readme = "README.md"

rand_xoshiro/src/common.rs

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

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

rand_xoshiro/src/splitmix64.rs

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

63+
impl SplitMix64 {
64+
/// Return the internal state of the generator as little-endian bytes
65+
/// that can be passed to [`SeedableRng::from_seed`] to reconstruct an
66+
/// identical generator.
67+
///
68+
/// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed
69+
pub fn state(&self) -> [u8; 8] {
70+
self.x.to_le_bytes()
71+
}
72+
}
73+
6374
impl SeedableRng for SplitMix64 {
6475
type Seed = [u8; 8];
6576

@@ -142,6 +153,13 @@ mod tests {
142153
}
143154
}
144155

156+
#[test]
157+
fn state_roundtrip() {
158+
let rng = SplitMix64::seed_from_u64(42);
159+
let clone = SplitMix64::from_seed(rng.state());
160+
assert_eq!(clone, rng);
161+
}
162+
145163
#[test]
146164
fn next_u32() {
147165
let mut rng = SplitMix64::seed_from_u64(10);

rand_xoshiro/src/xoroshiro128plus.rs

Lines changed: 9 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);
84+
8385
impl SeedableRng for Xoroshiro128Plus {
8486
type Seed = [u8; 16];
8587

@@ -129,4 +131,11 @@ mod tests {
129131
let from_sm0 = Xoroshiro128Plus::seed_from_u64(0);
130132
assert_eq!(from_zero, from_sm0);
131133
}
134+
135+
#[test]
136+
fn state_roundtrip() {
137+
let rng = Xoroshiro128Plus::seed_from_u64(42);
138+
let clone = Xoroshiro128Plus::from_seed(rng.state());
139+
assert_eq!(clone, rng);
140+
}
132141
}

rand_xoshiro/src/xoroshiro128plusplus.rs

Lines changed: 9 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);
82+
8183
impl SeedableRng for Xoroshiro128PlusPlus {
8284
type Seed = [u8; 16];
8385

@@ -128,4 +130,11 @@ mod tests {
128130
let from_sm0 = Xoroshiro128PlusPlus::seed_from_u64(0);
129131
assert_eq!(from_zero, from_sm0);
130132
}
133+
134+
#[test]
135+
fn state_roundtrip() {
136+
let rng = Xoroshiro128PlusPlus::seed_from_u64(42);
137+
let clone = Xoroshiro128PlusPlus::from_seed(rng.state());
138+
assert_eq!(clone, rng);
139+
}
131140
}

rand_xoshiro/src/xoroshiro128starstar.rs

Lines changed: 9 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);
82+
8183
impl SeedableRng for Xoroshiro128StarStar {
8284
type Seed = [u8; 16];
8385

@@ -128,4 +130,11 @@ mod tests {
128130
let from_sm0 = Xoroshiro128StarStar::seed_from_u64(0);
129131
assert_eq!(from_zero, from_sm0);
130132
}
133+
134+
#[test]
135+
fn state_roundtrip() {
136+
let rng = Xoroshiro128StarStar::seed_from_u64(42);
137+
let clone = Xoroshiro128StarStar::from_seed(rng.state());
138+
assert_eq!(clone, rng);
139+
}
131140
}

rand_xoshiro/src/xoroshiro64star.rs

Lines changed: 9 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);
53+
5254
impl SeedableRng for Xoroshiro64Star {
5355
type Seed = [u8; 8];
5456

@@ -97,4 +99,11 @@ mod tests {
9799
let from_sm0 = Xoroshiro64Star::seed_from_u64(0);
98100
assert_eq!(from_zero, from_sm0);
99101
}
102+
103+
#[test]
104+
fn state_roundtrip() {
105+
let rng = Xoroshiro64Star::seed_from_u64(42);
106+
let clone = Xoroshiro64Star::from_seed(rng.state());
107+
assert_eq!(clone, rng);
108+
}
100109
}

rand_xoshiro/src/xoroshiro64starstar.rs

Lines changed: 9 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);
52+
5153
impl SeedableRng for Xoroshiro64StarStar {
5254
type Seed = [u8; 8];
5355

@@ -96,4 +98,11 @@ mod tests {
9698
let from_sm0 = Xoroshiro64StarStar::seed_from_u64(0);
9799
assert_eq!(from_zero, from_sm0);
98100
}
101+
102+
#[test]
103+
fn state_roundtrip() {
104+
let rng = Xoroshiro64StarStar::seed_from_u64(42);
105+
let clone = Xoroshiro64StarStar::from_seed(rng.state());
106+
assert_eq!(clone, rng);
107+
}
99108
}

0 commit comments

Comments
 (0)