Skip to main content

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}