Skip to main content

dynomite/hashkit/
random.rs

1//! Pseudo-random integer source used by the `random` distribution.
2//!
3//! The C reference seeds glibc `srandom` with `time(NULL)` and dispatches
4//! requests to live servers using `random() % ncontinuum`. We replace
5//! that with a small, deterministic-when-seeded LCG so the engine does
6//! not depend on libc's BSD `random()` family.
7//!
8//! The default seed is drawn from `SystemTime::now()` (nanoseconds
9//! since the Unix epoch). PLAN.md Stage 3 originally suggested
10//! `clock_gettime(CLOCK_MONOTONIC)` for symmetry with the C `time(0)`
11//! seeding; the chosen `SystemTime` source achieves the same goal
12//! (per-process seed entropy) without requiring the `time` cargo
13//! feature on the `nix` workspace dependency. The choice is documented
14//! as a deviation in `docs/parity.md` and pinned by a regression
15//! test, so any future move to a monotonic source is an intentional
16//! change rather than a drift.
17
18use std::time::{SystemTime, UNIX_EPOCH};
19
20/// Linear congruential generator with the parameters used by glibc's
21/// non-secure `random()` family. Sufficient for ring dispatch (the only
22/// caller); not cryptographically strong.
23///
24/// # Examples
25///
26/// ```
27/// use dynomite::hashkit::PseudoRng;
28/// let mut rng = PseudoRng::from_seed(42);
29/// let value = rng.next_u32();
30/// // Same seed reproduces the same stream.
31/// let mut rng2 = PseudoRng::from_seed(42);
32/// assert_eq!(value, rng2.next_u32());
33/// ```
34#[derive(Clone, Debug)]
35pub struct PseudoRng {
36    state: u64,
37}
38
39impl PseudoRng {
40    /// Construct a generator seeded from the system clock.
41    ///
42    /// The seed mixes seconds and nanoseconds drawn from
43    /// [`SystemTime::now()`]. See the module-level docs for why we use
44    /// the system clock rather than a monotonic clock.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use dynomite::hashkit::PseudoRng;
50    /// let mut rng = PseudoRng::from_monotonic();
51    /// let _ = rng.next_u32();
52    /// ```
53    #[must_use]
54    pub fn from_monotonic() -> Self {
55        let seed = clock_seed();
56        Self::from_seed(seed)
57    }
58
59    /// Construct a generator from an explicit seed.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use dynomite::hashkit::PseudoRng;
65    /// let mut a = PseudoRng::from_seed(7);
66    /// let mut b = PseudoRng::from_seed(7);
67    /// assert_eq!(a.next_u32(), b.next_u32());
68    /// ```
69    #[must_use]
70    pub fn from_seed(seed: u64) -> Self {
71        // A zero seed in the LCG is a degenerate fixed point; nudge it.
72        let s = if seed == 0 {
73            0x9E37_79B9_7F4A_7C15
74        } else {
75            seed
76        };
77        Self { state: s }
78    }
79
80    /// Advance the generator and return the next 32-bit value.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use dynomite::hashkit::PseudoRng;
86    /// let mut rng = PseudoRng::from_seed(1);
87    /// let _: u32 = rng.next_u32();
88    /// ```
89    pub fn next_u32(&mut self) -> u32 {
90        // Knuth's MMIX LCG parameters; long-period and full 64-bit state.
91        self.state = self
92            .state
93            .wrapping_mul(6_364_136_223_846_793_005)
94            .wrapping_add(1_442_695_040_888_963_407);
95        // High bits of the LCG state have better statistical properties
96        // than the low bits, so return the upper half.
97        (self.state >> 32) as u32
98    }
99
100    /// Pick a uniform value in `[0, modulus)`. Returns `0` when modulus
101    /// is zero.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use dynomite::hashkit::PseudoRng;
107    /// let mut rng = PseudoRng::from_seed(7);
108    /// for _ in 0..16 {
109    ///     assert!(rng.next_index(13) < 13);
110    /// }
111    /// assert_eq!(rng.next_index(0), 0);
112    /// ```
113    pub fn next_index(&mut self, modulus: u32) -> u32 {
114        if modulus == 0 {
115            0
116        } else {
117            self.next_u32() % modulus
118        }
119    }
120}
121
122fn clock_seed() -> u64 {
123    SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| {
124        let secs = d.as_secs();
125        let nanos = u64::from(d.subsec_nanos());
126        secs.wrapping_mul(1_000_000_000).wrapping_add(nanos)
127    })
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn deterministic_for_same_seed() {
136        let mut a = PseudoRng::from_seed(123);
137        let mut b = PseudoRng::from_seed(123);
138        for _ in 0..32 {
139            assert_eq!(a.next_u32(), b.next_u32());
140        }
141    }
142
143    /// Pins the choice of LCG parameters and the high-bit return
144    /// strategy. If this test breaks, treat it as an intentional API
145    /// change and update `docs/parity.md`'s `PseudoRng` deviation.
146    #[test]
147    fn lcg_parameters_are_pinned() {
148        // Knuth MMIX constants applied once and the high 32 bits of
149        // the resulting state.
150        let mut rng = PseudoRng::from_seed(1);
151        let expected = ((1u64
152            .wrapping_mul(6_364_136_223_846_793_005)
153            .wrapping_add(1_442_695_040_888_963_407))
154            >> 32) as u32;
155        assert_eq!(rng.next_u32(), expected);
156    }
157
158    #[test]
159    fn distinct_seeds_diverge() {
160        let mut a = PseudoRng::from_seed(1);
161        let mut b = PseudoRng::from_seed(2);
162        let av: Vec<u32> = (0..16).map(|_| a.next_u32()).collect();
163        let bv: Vec<u32> = (0..16).map(|_| b.next_u32()).collect();
164        assert_ne!(av, bv);
165    }
166
167    #[test]
168    fn next_index_bounded() {
169        let mut rng = PseudoRng::from_seed(42);
170        for _ in 0..256 {
171            let i = rng.next_index(13);
172            assert!(i < 13);
173        }
174    }
175
176    #[test]
177    fn next_index_zero_modulus() {
178        let mut rng = PseudoRng::from_seed(42);
179        assert_eq!(rng.next_index(0), 0);
180    }
181
182    #[test]
183    fn monotonic_seed_produces_values() {
184        let mut rng = PseudoRng::from_monotonic();
185        let _ = rng.next_u32();
186    }
187}