Skip to content

rand_xoshiro: Add state() for serde-free state export#110

Open
vks wants to merge 1 commit intomasterfrom
export-xoshiro-state
Open

rand_xoshiro: Add state() for serde-free state export#110
vks wants to merge 1 commit intomasterfrom
export-xoshiro-state

Conversation

@vks
Copy link
Copy Markdown
Contributor

@vks vks commented Apr 26, 2026

Disclaimer: This PR is mostly Claude generated.

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.

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.
@vks vks force-pushed the export-xoshiro-state branch from 956d473 to 76737e8 Compare April 26, 2026 19:50
@vks vks changed the title rand_xoshiro: Add state() for serde-free state export (#109) rand_xoshiro: Add state() for serde-free state export Apr 26, 2026
Copy link
Copy Markdown
Member

@dhardy dhardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few comments on style, otherwise this looks fine.

You might as well bump the version number to prepare the a patch release (unless you'd prefer to make a new PR for that).

for _ in 0..10 {
rng.next_u64();
}
let mut clone = SplitMix64::from_seed(rng.state());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the generator impls Eq we should do assert_eq!(clone, rng).

Comment on lines +256 to +258
pub fn state(&self) -> [u8; $bytes] {
let mut out = [0u8; $bytes];
let n = core::mem::size_of::<$word>();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to use core::mem::size_of::<$word>() * 2 instead of $bytes here.

Use const N: usize = core::mem::size_of::<u64>() if you want a const binding inside the fn.


/// Implement `state` for an RNG with a `s: [WORD; N]` field.
macro_rules! impl_state_array {
($type:ident, $word:ty, $bytes:literal) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more intuitive to pass $N (size in words) instead of $bytes? Unimportant.

Actually, since N == 4 in all uses, it would be better to rename this impl_state_array_of_four and unroll the loop below.

Comment on lines +281 to +282
pub fn state(&self) -> [u8; $bytes] {
let mut out = [0u8; $bytes];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add:

    const {
        assert!(core::mem::size_of::<Self>, $bytes);
    }

Comment on lines +225 to +227
/// Implement `state`, returning the internal state of a single-word RNG as
/// little-endian bytes that round-trip through [`SeedableRng::from_seed`].
macro_rules! impl_state_scalar {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used once so should be inlined.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add state export for rand_xoshiro

2 participants