1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub struct SplitMix64 {
6 stream_seed: u64,
7 cursor: u64,
8}
9
10impl SplitMix64 {
11 #[must_use]
13 pub const fn new(seed: u64) -> Self {
14 Self {
15 stream_seed: seed,
16 cursor: seed,
17 }
18 }
19
20 #[must_use]
22 pub const fn stream_seed(self) -> u64 {
23 self.stream_seed
24 }
25
26 #[must_use]
28 pub const fn cursor(self) -> u64 {
29 self.cursor
30 }
31
32 pub fn next_u64(&mut self) -> u64 {
34 self.cursor = self.cursor.wrapping_add(0x9e37_79b9_7f4a_7c15);
35 let mut value = self.cursor;
36 value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
37 value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
38 value ^ (value >> 31)
39 }
40
41 #[must_use]
47 pub fn range_inclusive(&mut self, min: u32, max: u32) -> u32 {
48 assert!(min <= max, "range_inclusive requires min <= max");
49 let width = u64::from(max - min) + 1;
50 min + u32::try_from(self.next_u64() % width)
51 .expect("modulo result fits within the requested u32 range")
52 }
53}
54
55#[must_use]
63pub fn derive_stream_seed(base: u64, domain: u64, a: u32, b: u64) -> u64 {
64 let mix = |state: u64| SplitMix64::new(state).next_u64();
65 [domain, u64::from(a), b]
66 .into_iter()
67 .fold(mix(base), |state, component| mix(state ^ component))
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use wasm_bindgen_test::wasm_bindgen_test;
74
75 #[wasm_bindgen_test(unsupported = test)]
78 fn a_fresh_generator_exposes_its_seed_and_cursor() {
79 let mut rng = SplitMix64::new(42);
80 assert_eq!(rng.stream_seed(), 42);
81 assert_eq!(rng.cursor(), 42);
82 rng.next_u64();
83 assert_eq!(rng.cursor(), 42u64.wrapping_add(0x9e37_79b9_7f4a_7c15));
84 }
85
86 #[wasm_bindgen_test(unsupported = test)]
90 fn overlapping_components_do_not_cancel() {
91 for (left, right) in [
92 ((9, 3, 1 << 16, 0), (9, 3, 0, 1)),
93 ((9, 1, 5, 0), (9, 0, 5, 1 << 34)),
94 ] {
95 assert_ne!(
96 derive_stream_seed(left.0, left.1, left.2, left.3),
97 derive_stream_seed(right.0, right.1, right.2, right.3),
98 "{left:?} vs {right:?}"
99 );
100 }
101 }
102
103 #[wasm_bindgen_test(unsupported = test)]
104 fn splitmix64_sequence_is_frozen() {
105 let mut rng = SplitMix64::new(0);
106 assert_eq!(rng.next_u64(), 0xe220_a839_7b1d_cdaf);
107 assert_eq!(rng.next_u64(), 0x6e78_9e6a_a1b9_65f4);
108 assert_eq!(rng.next_u64(), 0x06c4_5d18_8009_454f);
109 }
110
111 #[wasm_bindgen_test(unsupported = test)]
112 fn inclusive_range_sampling_is_stable() {
113 let mut rng = SplitMix64::new(7);
114 assert_eq!(rng.range_inclusive(3, 7), 5);
115 assert_eq!(rng.range_inclusive(3, 7), 7);
116 assert_eq!(rng.range_inclusive(3, 7), 4);
117 }
118
119 #[wasm_bindgen_test(unsupported = test)]
120 fn derived_seed_is_frozen() {
121 assert_eq!(derive_stream_seed(17, 29, 41, 53), 0xfa0d_d366_259b_7519);
122 }
123}