diff --git a/Cargo.lock b/Cargo.lock index c3645ade..8efd76d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,7 +263,7 @@ dependencies = [ [[package]] name = "rand_xoshiro" -version = "0.8.0" +version = "0.8.1" dependencies = [ "postcard", "rand_core", diff --git a/rand_xoshiro/CHANGELOG.md b/rand_xoshiro/CHANGELOG.md index e537b409..a360b3c8 100644 --- a/rand_xoshiro/CHANGELOG.md +++ b/rand_xoshiro/CHANGELOG.md @@ -4,11 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.8.1] - 2026-05-16 +### Added +- `state()` method on every RNG type returning the internal state as a value + matching `SeedableRng::Seed`, allowing reproduction via `from_seed` without + the `serde` feature ([#110]). + ### Changed - Shrink the code size of the all-zero-seed fallback by replacing the per-RNG `SplitMix64::seed_from_u64(0)` call with a single shared constant. +[#110]: https://github.com/rust-random/rngs/pull/110 + ## [0.8.0] - 2026-02-01 ### Changes - Use Edition 2024 and MSRV 1.85 ([#73]) diff --git a/rand_xoshiro/Cargo.toml b/rand_xoshiro/Cargo.toml index 7a734b30..5cf6d2e2 100644 --- a/rand_xoshiro/Cargo.toml +++ b/rand_xoshiro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rand_xoshiro" -version = "0.8.0" +version = "0.8.1" authors = ["The Rand Project Developers"] license = "MIT OR Apache-2.0" readme = "README.md" diff --git a/rand_xoshiro/src/common.rs b/rand_xoshiro/src/common.rs index f7be5636..d92f6e6e 100644 --- a/rand_xoshiro/src/common.rs +++ b/rand_xoshiro/src/common.rs @@ -222,6 +222,89 @@ macro_rules! impl_xoshiro_large { }; } +/// Implement `state` for an RNG with two word-sized fields `s0` and `s1`. +macro_rules! impl_state_pair { + ($type:ident, $word:ty) => { + impl $type { + /// Return the internal state of the generator as little-endian + /// bytes that can be passed to [`SeedableRng::from_seed`] to + /// reconstruct an identical generator. + /// + /// The all-zero state is unreachable from any non-zero seed, so + /// the round-trip is exact for any generator obtained via the + /// usual constructors. (An all-zero input to `from_seed` is + /// remapped to `seed_from_u64(0)`.) + /// + /// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed + pub fn state(&self) -> [u8; 2 * core::mem::size_of::<$word>()] { + const N: usize = core::mem::size_of::<$word>(); + const { + assert!(core::mem::size_of::() == 2 * N); + } + let mut out = [0u8; 2 * N]; + out[..N].copy_from_slice(&self.s0.to_le_bytes()); + out[N..].copy_from_slice(&self.s1.to_le_bytes()); + out + } + } + }; +} + +/// Implement `state` for an RNG with an `s: [WORD; 4]` field. +macro_rules! impl_state_array_of_four { + ($type:ident, $word:ty) => { + impl $type { + /// Return the internal state of the generator as little-endian + /// bytes that can be passed to [`SeedableRng::from_seed`] to + /// reconstruct an identical generator. + /// + /// The all-zero state is unreachable from any non-zero seed, so + /// the round-trip is exact for any generator obtained via the + /// usual constructors. (An all-zero input to `from_seed` is + /// remapped to `seed_from_u64(0)`.) + /// + /// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed + pub fn state(&self) -> [u8; 4 * core::mem::size_of::<$word>()] { + const N: usize = core::mem::size_of::<$word>(); + const { + assert!(core::mem::size_of::() == 4 * N); + } + let mut out = [0u8; 4 * N]; + out[..N].copy_from_slice(&self.s[0].to_le_bytes()); + out[N..2 * N].copy_from_slice(&self.s[1].to_le_bytes()); + out[2 * N..3 * N].copy_from_slice(&self.s[2].to_le_bytes()); + out[3 * N..].copy_from_slice(&self.s[3].to_le_bytes()); + out + } + } + }; +} + +/// Implement `state` for a 512-bit RNG (`s: [u64; 8]`), returning a [`Seed512`]. +macro_rules! impl_state_seed512 { + ($type:ident) => { + impl $type { + /// Return the internal state of the generator as a [`Seed512`] + /// that can be passed to [`SeedableRng::from_seed`] to reconstruct + /// an identical generator. + /// + /// The all-zero state is unreachable from any non-zero seed, so + /// the round-trip is exact for any generator obtained via the + /// usual constructors. (An all-zero input to `from_seed` is + /// remapped to `seed_from_u64(0)`.) + /// + /// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed + pub fn state(&self) -> crate::Seed512 { + let mut out = [0u8; 64]; + for (i, word) in self.s.iter().enumerate() { + out[i * 8..(i + 1) * 8].copy_from_slice(&word.to_le_bytes()); + } + crate::Seed512(out) + } + } + }; +} + /// Fallback seed used when `from_seed` is called with an all-zero input. /// /// Equal to the first 64 bytes of `SplitMix64::seed_from_u64(0)`'s output. diff --git a/rand_xoshiro/src/splitmix64.rs b/rand_xoshiro/src/splitmix64.rs index da408430..ed70e4ea 100644 --- a/rand_xoshiro/src/splitmix64.rs +++ b/rand_xoshiro/src/splitmix64.rs @@ -60,6 +60,17 @@ impl TryRng for SplitMix64 { } } +impl SplitMix64 { + /// Return the internal state of the generator as little-endian bytes + /// that can be passed to [`SeedableRng::from_seed`] to reconstruct an + /// identical generator. + /// + /// [`SeedableRng::from_seed`]: rand_core::SeedableRng::from_seed + pub fn state(&self) -> [u8; 8] { + self.x.to_le_bytes() + } +} + impl SeedableRng for SplitMix64 { type Seed = [u8; 8]; @@ -142,6 +153,13 @@ mod tests { } } + #[test] + fn state_roundtrip() { + let rng = SplitMix64::seed_from_u64(42); + let clone = SplitMix64::from_seed(rng.state()); + assert_eq!(clone, rng); + } + #[test] fn next_u32() { let mut rng = SplitMix64::seed_from_u64(10); diff --git a/rand_xoshiro/src/xoroshiro128plus.rs b/rand_xoshiro/src/xoroshiro128plus.rs index 2e844d73..9e886a4b 100644 --- a/rand_xoshiro/src/xoroshiro128plus.rs +++ b/rand_xoshiro/src/xoroshiro128plus.rs @@ -80,6 +80,8 @@ impl TryRng for Xoroshiro128Plus { utils::fill_bytes_via_next_word(dest, || self.try_next_u64()) } } +impl_state_pair!(Xoroshiro128Plus, u64); + impl SeedableRng for Xoroshiro128Plus { type Seed = [u8; 16]; @@ -129,4 +131,11 @@ mod tests { let from_sm0 = Xoroshiro128Plus::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoroshiro128Plus::seed_from_u64(42); + let clone = Xoroshiro128Plus::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoroshiro128plusplus.rs b/rand_xoshiro/src/xoroshiro128plusplus.rs index 1496ede0..86f3e010 100644 --- a/rand_xoshiro/src/xoroshiro128plusplus.rs +++ b/rand_xoshiro/src/xoroshiro128plusplus.rs @@ -78,6 +78,8 @@ impl TryRng for Xoroshiro128PlusPlus { } } +impl_state_pair!(Xoroshiro128PlusPlus, u64); + impl SeedableRng for Xoroshiro128PlusPlus { type Seed = [u8; 16]; @@ -128,4 +130,11 @@ mod tests { let from_sm0 = Xoroshiro128PlusPlus::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoroshiro128PlusPlus::seed_from_u64(42); + let clone = Xoroshiro128PlusPlus::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoroshiro128starstar.rs b/rand_xoshiro/src/xoroshiro128starstar.rs index 199276e6..d22d98df 100644 --- a/rand_xoshiro/src/xoroshiro128starstar.rs +++ b/rand_xoshiro/src/xoroshiro128starstar.rs @@ -78,6 +78,8 @@ impl TryRng for Xoroshiro128StarStar { } } +impl_state_pair!(Xoroshiro128StarStar, u64); + impl SeedableRng for Xoroshiro128StarStar { type Seed = [u8; 16]; @@ -128,4 +130,11 @@ mod tests { let from_sm0 = Xoroshiro128StarStar::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoroshiro128StarStar::seed_from_u64(42); + let clone = Xoroshiro128StarStar::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoroshiro64star.rs b/rand_xoshiro/src/xoroshiro64star.rs index a3807576..991ef2ce 100644 --- a/rand_xoshiro/src/xoroshiro64star.rs +++ b/rand_xoshiro/src/xoroshiro64star.rs @@ -49,6 +49,8 @@ impl TryRng for Xoroshiro64Star { } } +impl_state_pair!(Xoroshiro64Star, u32); + impl SeedableRng for Xoroshiro64Star { type Seed = [u8; 8]; @@ -97,4 +99,11 @@ mod tests { let from_sm0 = Xoroshiro64Star::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoroshiro64Star::seed_from_u64(42); + let clone = Xoroshiro64Star::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoroshiro64starstar.rs b/rand_xoshiro/src/xoroshiro64starstar.rs index 6592b14c..0fe8ce66 100644 --- a/rand_xoshiro/src/xoroshiro64starstar.rs +++ b/rand_xoshiro/src/xoroshiro64starstar.rs @@ -48,6 +48,8 @@ impl TryRng for Xoroshiro64StarStar { } } +impl_state_pair!(Xoroshiro64StarStar, u32); + impl SeedableRng for Xoroshiro64StarStar { type Seed = [u8; 8]; @@ -96,4 +98,11 @@ mod tests { let from_sm0 = Xoroshiro64StarStar::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoroshiro64StarStar::seed_from_u64(42); + let clone = Xoroshiro64StarStar::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro128plus.rs b/rand_xoshiro/src/xoshiro128plus.rs index 7c03008a..bb8a9ccd 100644 --- a/rand_xoshiro/src/xoshiro128plus.rs +++ b/rand_xoshiro/src/xoshiro128plus.rs @@ -56,6 +56,8 @@ impl Xoshiro128Plus { } } +impl_state_array_of_four!(Xoshiro128Plus, u32); + impl SeedableRng for Xoshiro128Plus { type Seed = [u8; 16]; @@ -143,4 +145,11 @@ mod tests { let from_sm0 = Xoshiro128Plus::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro128Plus::seed_from_u64(42); + let clone = Xoshiro128Plus::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro128plusplus.rs b/rand_xoshiro/src/xoshiro128plusplus.rs index bcb95983..8ae6e25b 100644 --- a/rand_xoshiro/src/xoshiro128plusplus.rs +++ b/rand_xoshiro/src/xoshiro128plusplus.rs @@ -55,6 +55,8 @@ impl Xoshiro128PlusPlus { } } +impl_state_array_of_four!(Xoshiro128PlusPlus, u32); + impl SeedableRng for Xoshiro128PlusPlus { type Seed = [u8; 16]; @@ -145,4 +147,11 @@ mod tests { let from_sm0 = Xoshiro128PlusPlus::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro128PlusPlus::seed_from_u64(42); + let clone = Xoshiro128PlusPlus::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro128starstar.rs b/rand_xoshiro/src/xoshiro128starstar.rs index 9cd978aa..b2ce8269 100644 --- a/rand_xoshiro/src/xoshiro128starstar.rs +++ b/rand_xoshiro/src/xoshiro128starstar.rs @@ -55,6 +55,8 @@ impl Xoshiro128StarStar { } } +impl_state_array_of_four!(Xoshiro128StarStar, u32); + impl SeedableRng for Xoshiro128StarStar { type Seed = [u8; 16]; @@ -145,4 +147,11 @@ mod tests { let from_sm0 = Xoshiro128StarStar::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro128StarStar::seed_from_u64(42); + let clone = Xoshiro128StarStar::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro256plus.rs b/rand_xoshiro/src/xoshiro256plus.rs index 895bdbd6..e83f0f5a 100644 --- a/rand_xoshiro/src/xoshiro256plus.rs +++ b/rand_xoshiro/src/xoshiro256plus.rs @@ -74,6 +74,8 @@ impl Xoshiro256Plus { } } +impl_state_array_of_four!(Xoshiro256Plus, u64); + impl SeedableRng for Xoshiro256Plus { type Seed = [u8; 32]; @@ -150,4 +152,11 @@ mod tests { let from_sm0 = Xoshiro256Plus::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro256Plus::seed_from_u64(42); + let clone = Xoshiro256Plus::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro256plusplus.rs b/rand_xoshiro/src/xoshiro256plusplus.rs index e120a795..89c9e059 100644 --- a/rand_xoshiro/src/xoshiro256plusplus.rs +++ b/rand_xoshiro/src/xoshiro256plusplus.rs @@ -73,6 +73,8 @@ impl Xoshiro256PlusPlus { } } +impl_state_array_of_four!(Xoshiro256PlusPlus, u64); + impl SeedableRng for Xoshiro256PlusPlus { type Seed = [u8; 32]; @@ -149,4 +151,11 @@ mod tests { let from_sm0 = Xoshiro256PlusPlus::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro256PlusPlus::seed_from_u64(42); + let clone = Xoshiro256PlusPlus::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro256starstar.rs b/rand_xoshiro/src/xoshiro256starstar.rs index 236b1364..11a51cd5 100644 --- a/rand_xoshiro/src/xoshiro256starstar.rs +++ b/rand_xoshiro/src/xoshiro256starstar.rs @@ -73,6 +73,8 @@ impl Xoshiro256StarStar { } } +impl_state_array_of_four!(Xoshiro256StarStar, u64); + impl SeedableRng for Xoshiro256StarStar { type Seed = [u8; 32]; @@ -149,4 +151,11 @@ mod tests { let from_sm0 = Xoshiro256StarStar::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro256StarStar::seed_from_u64(42); + let clone = Xoshiro256StarStar::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro512plus.rs b/rand_xoshiro/src/xoshiro512plus.rs index e6499502..cbbe5887 100644 --- a/rand_xoshiro/src/xoshiro512plus.rs +++ b/rand_xoshiro/src/xoshiro512plus.rs @@ -84,6 +84,8 @@ impl Xoshiro512Plus { } } +impl_state_seed512!(Xoshiro512Plus); + impl SeedableRng for Xoshiro512Plus { type Seed = Seed512; @@ -162,4 +164,11 @@ mod tests { let from_sm0 = Xoshiro512Plus::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro512Plus::seed_from_u64(42); + let clone = Xoshiro512Plus::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro512plusplus.rs b/rand_xoshiro/src/xoshiro512plusplus.rs index f95991c9..5d3d8dc4 100644 --- a/rand_xoshiro/src/xoshiro512plusplus.rs +++ b/rand_xoshiro/src/xoshiro512plusplus.rs @@ -83,6 +83,8 @@ impl Xoshiro512PlusPlus { } } +impl_state_seed512!(Xoshiro512PlusPlus); + impl SeedableRng for Xoshiro512PlusPlus { type Seed = Seed512; @@ -160,4 +162,11 @@ mod tests { let from_sm0 = Xoshiro512PlusPlus::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro512PlusPlus::seed_from_u64(42); + let clone = Xoshiro512PlusPlus::from_seed(rng.state()); + assert_eq!(clone, rng); + } } diff --git a/rand_xoshiro/src/xoshiro512starstar.rs b/rand_xoshiro/src/xoshiro512starstar.rs index e808f6fa..b81c6411 100644 --- a/rand_xoshiro/src/xoshiro512starstar.rs +++ b/rand_xoshiro/src/xoshiro512starstar.rs @@ -83,6 +83,8 @@ impl Xoshiro512StarStar { } } +impl_state_seed512!(Xoshiro512StarStar); + impl SeedableRng for Xoshiro512StarStar { type Seed = Seed512; @@ -161,4 +163,11 @@ mod tests { let from_sm0 = Xoshiro512StarStar::seed_from_u64(0); assert_eq!(from_zero, from_sm0); } + + #[test] + fn state_roundtrip() { + let rng = Xoshiro512StarStar::seed_from_u64(42); + let clone = Xoshiro512StarStar::from_seed(rng.state()); + assert_eq!(clone, rng); + } }