use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use super::ProcessId;
pub const DEFAULT_SEED: u64 = 0x1234_5678_9ABC_DEF1;
pub fn splitmix64_next(state: &mut u64) -> u64 {
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
thread_local! {
static PROCESS_STATE: RefCell<HashMap<Option<ProcessId>, u64>> = RefCell::new(HashMap::new());
static INIT_STATE: RefCell<HashMap<u32, u64>> = RefCell::new(HashMap::new());
static INIT_ALIAS: RefCell<HashMap<ProcessId, u32>> = RefCell::new(HashMap::new());
static BASE_SEED: Cell<u64> = const { Cell::new(DEFAULT_SEED) };
}
pub fn base_seed() -> u64 {
BASE_SEED.with(Cell::get)
}
pub fn set_base_seed(n: u64) {
let mut state = DEFAULT_SEED ^ n;
BASE_SEED.with(|b| b.set(splitmix64_next(&mut state)));
}
pub fn reset() {
PROCESS_STATE.with(|s| s.borrow_mut().clear());
INIT_STATE.with(|s| s.borrow_mut().clear());
INIT_ALIAS.with(|s| s.borrow_mut().clear());
BASE_SEED.with(|b| b.set(DEFAULT_SEED));
}
pub fn with_process_state<R>(key: Option<ProcessId>, f: impl FnOnce(&mut u64) -> R) -> R {
if let Some(scope) = key.and_then(|id| INIT_ALIAS.with(|a| a.borrow().get(&id).copied())) {
return INIT_STATE.with(|s| f(s.borrow_mut().entry(scope).or_insert(base_seed())));
}
PROCESS_STATE.with(|s| f(s.borrow_mut().entry(key).or_insert(base_seed())))
}
pub fn preseed(id: ProcessId, seed: u64) {
clear_alias(id);
PROCESS_STATE.with(|s| {
s.borrow_mut().insert(Some(id), seed);
});
}
pub fn alias_init(id: ProcessId, scope: u32) {
INIT_ALIAS.with(|a| {
a.borrow_mut().insert(id, scope);
});
}
pub fn clear_alias(id: ProcessId) {
INIT_ALIAS.with(|a| {
a.borrow_mut().remove(&id);
});
}
pub fn draw_from_process(parent: ProcessId) -> u64 {
with_process_state(Some(parent), splitmix64_next)
}
pub fn draw_from_init(scope: u32) -> u64 {
INIT_STATE.with(|s| splitmix64_next(s.borrow_mut().entry(scope).or_insert(base_seed())))
}
pub fn state_from_srandom(seed: i32) -> u64 {
u64::from(seed as u32)
}
pub fn format_state(state: u64) -> String {
state.to_string()
}
pub fn parse_state(text: &[u8]) -> u64 {
std::str::from_utf8(text)
.ok()
.and_then(|t| t.parse::<u64>().ok())
.unwrap_or(DEFAULT_SEED)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unseeded_process_starts_at_the_default_seed() {
reset();
assert_eq!(with_process_state(Some(3), |s| *s), DEFAULT_SEED);
}
#[test]
fn a_base_seed_moves_every_unseeded_start_and_reset_restores_the_default() {
reset();
set_base_seed(1);
let one = with_process_state(Some(3), |s| *s);
assert_ne!(one, DEFAULT_SEED);
reset();
set_base_seed(2);
let two = with_process_state(Some(3), |s| *s);
assert_ne!(two, one, "different --seed, different start");
reset();
set_base_seed(1);
assert_eq!(
with_process_state(Some(3), |s| *s),
one,
"same --seed, same start"
);
assert_eq!(draw_from_init(9), {
let mut s = one;
splitmix64_next(&mut s)
});
reset();
assert_eq!(base_seed(), DEFAULT_SEED);
}
#[test]
fn preseed_replaces_a_stale_state_left_by_a_reused_process_id() {
reset();
with_process_state(Some(1), |s| *s = 5);
preseed(1, 99);
assert_eq!(with_process_state(Some(1), |s| *s), 99);
}
#[test]
fn an_aliased_process_draws_from_the_init_rng_until_preseeded() {
reset();
alias_init(5, 2);
let mut shadow = DEFAULT_SEED;
let first = splitmix64_next(&mut shadow);
assert_eq!(with_process_state(Some(5), splitmix64_next), first);
assert_eq!(
draw_from_init(2),
splitmix64_next(&mut shadow),
"the aliased draw advanced the scope's own RNG"
);
preseed(5, 77);
assert_eq!(with_process_state(Some(5), |s| *s), 77);
assert_eq!(draw_from_init(2), splitmix64_next(&mut shadow));
}
#[test]
fn a_fork_draw_advances_the_parent_by_exactly_one_step() {
reset();
let mut shadow = DEFAULT_SEED;
let first = splitmix64_next(&mut shadow);
let second = splitmix64_next(&mut shadow);
assert_eq!(draw_from_process(7), first);
assert_eq!(draw_from_process(7), second);
assert_eq!(with_process_state(Some(7), |s| *s), shadow);
}
#[test]
fn instance_scopes_have_independent_init_rngs() {
reset();
let a1 = draw_from_init(0);
let b1 = draw_from_init(1);
assert_eq!(a1, b1, "every init RNG starts at the same constant");
let a2 = draw_from_init(0);
assert_ne!(a1, a2, "drawing advances the scope's own RNG");
assert_eq!(draw_from_init(1), a2, "the other scope is untouched");
}
#[test]
fn randstate_strings_round_trip_and_garbage_resets_to_the_default() {
for state in [0, 1, DEFAULT_SEED, u64::MAX] {
assert_eq!(parse_state(format_state(state).as_bytes()), state);
}
assert_eq!(parse_state(b"not a state"), DEFAULT_SEED);
assert_eq!(parse_state(b""), DEFAULT_SEED);
assert_eq!(parse_state(&[0xff, 0xfe]), DEFAULT_SEED);
}
}