yo_common/rng.rs
1//! A random number generator you can write down.
2//!
3//! Two parts of the engine need one and they want the same thing from it. The
4//! crash harness is worthless if a failure cannot be reproduced: a trial that
5//! fails prints its seed, and running that seed again has to produce the same
6//! trial, byte for byte, on any machine and any target. `SPOP` and
7//! `SRANDMEMBER` want the same property for the same reason, because a test
8//! that cannot say which member comes back can only assert that something did.
9//! Between them that rules out anything seeded from the clock, anything that
10//! consults the operating system, and anything whose output depends on the
11//! width of a pointer.
12//!
13//! So this is `splitmix64`, which is nine lines and has no state beyond a
14//! `u64`. It is not cryptographic and does not need to be. Neither caller is
15//! keeping a secret, and an adversary who can predict which member `SPOP`
16//! returns is welcome to, the same way they are on a real server: Redis draws
17//! from `random()` seeded from the clock and the pid.
18//!
19//! It lives here rather than in either crate that uses it because the second
20//! caller would otherwise have copied it, and two copies of a generator is two
21//! chances for one of them to get a constant wrong.
22
23/// A seeded stream of numbers, reproducible everywhere.
24#[derive(Debug, Clone)]
25pub struct Rng {
26 state: u64,
27}
28
29impl Rng {
30 /// A stream from a seed.
31 #[must_use]
32 pub const fn new(seed: u64) -> Rng {
33 Rng { state: seed }
34 }
35
36 /// The seed a stream would need to be at this point again.
37 ///
38 /// Every trial takes one of these and prints it on failure, so a hundred
39 /// thousand trial run that fails on trial 74,113 hands back a number that
40 /// reproduces trial 74,113 on its own in a millisecond.
41 #[must_use]
42 pub const fn state(&self) -> u64 {
43 self.state
44 }
45
46 /// The next number.
47 pub const fn next_u64(&mut self) -> u64 {
48 self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
49 let mut z = self.state;
50 z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
51 z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
52 z ^ (z >> 31)
53 }
54
55 /// A number below `n`, or 0 when `n` is 0.
56 ///
57 /// Biased, by about one part in 2^64 divided by `n`. Everything this picks
58 /// is smaller than a few thousand, so the bias is not measurable and a
59 /// rejection loop would only add a way for the harness to hang.
60 pub const fn below(&mut self, n: usize) -> usize {
61 if n == 0 {
62 return 0;
63 }
64 (self.next_u64() % n as u64) as usize
65 }
66
67 /// A number in `lo..=hi`.
68 pub const fn between(&mut self, lo: usize, hi: usize) -> usize {
69 if hi <= lo {
70 return lo;
71 }
72 lo + self.below(hi - lo + 1)
73 }
74
75 /// True with probability `num` in `den`.
76 pub const fn chance(&mut self, num: u32, den: u32) -> bool {
77 self.below(den as usize) < num as usize
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn the_same_seed_is_the_same_stream() {
87 let mut a = Rng::new(12345);
88 let mut b = Rng::new(12345);
89 for _ in 0..1000 {
90 assert_eq!(a.next_u64(), b.next_u64());
91 }
92 }
93
94 #[test]
95 fn different_seeds_are_different_streams() {
96 let mut a = Rng::new(1);
97 let mut b = Rng::new(2);
98 let same = (0..1000).filter(|_| a.next_u64() == b.next_u64()).count();
99 assert_eq!(same, 0);
100 }
101
102 #[test]
103 fn the_stream_does_not_settle_on_one_value() {
104 // A generator with a bad constant can reach a fixed point and sit there,
105 // and every trial after that is the same trial. That failure is silent
106 // and it makes the whole run worthless, so it is worth one assertion.
107 //
108 // Ten thousand draws is instant natively and about fifty seconds under
109 // Miri, which is the slowest thing in this crate for the least reason:
110 // a generator that has settled repeats on the second draw, not the ten
111 // thousandth. Miri takes five hundred, which is still two orders of
112 // magnitude more than it takes to notice.
113 const N: usize = if cfg!(miri) { 500 } else { 10_000 };
114 let mut r = Rng::new(0);
115 let mut seen = std::collections::HashSet::new();
116 for _ in 0..N {
117 seen.insert(r.next_u64());
118 }
119 assert_eq!(seen.len(), N, "the stream repeats itself");
120 }
121
122 #[test]
123 fn below_stays_below() {
124 let mut r = Rng::new(99);
125 for n in 1..50usize {
126 for _ in 0..200 {
127 assert!(r.below(n) < n);
128 }
129 }
130 assert_eq!(r.below(0), 0, "no division by zero");
131 }
132
133 #[test]
134 fn below_reaches_both_ends() {
135 let mut r = Rng::new(7);
136 let mut lo = false;
137 let mut hi = false;
138 for _ in 0..1000 {
139 match r.below(8) {
140 0 => lo = true,
141 7 => hi = true,
142 _ => {}
143 }
144 }
145 assert!(
146 lo && hi,
147 "a generator that never picks an end is not uniform"
148 );
149 }
150
151 #[test]
152 fn between_covers_its_range_inclusive() {
153 let mut r = Rng::new(4);
154 let mut seen = [false; 5];
155 for _ in 0..1000 {
156 let v = r.between(2, 6);
157 assert!((2..=6).contains(&v));
158 seen[v - 2] = true;
159 }
160 assert!(seen.iter().all(|&s| s));
161 assert_eq!(r.between(5, 5), 5);
162 assert_eq!(r.between(9, 3), 9, "a backwards range is its own low end");
163 }
164
165 #[test]
166 fn chance_is_roughly_the_odds_it_says() {
167 let mut r = Rng::new(31);
168 let hits = (0..10_000).filter(|_| r.chance(1, 4)).count();
169 assert!((2200..2800).contains(&hits), "got {hits} in 10000");
170 }
171}