Skip to main content

patterns/
chance.rs

1use patterns_macros::{because, source};
2
3pub struct SplitMixSearch;
4source!(
5    SplitMixSearch,
6    "the avalanche multiplier search by Steele, Lea and Flood"
7);
8
9pub struct WeylSequence;
10source!(
11    WeylSequence,
12    "the equidistribution result an odd addend relies on"
13);
14
15const GOLDEN_GAMMA: u64 = 0x9E3779B97F4A7C15;
16because!(
17    GOLDEN_GAMMA,
18    WeylSequence,
19    "the odd golden ratio addend at this word width, which gives the sequence full period"
20);
21
22const MIX_A: u64 = 0xBF58476D1CE4E5B9;
23because!(
24    MIX_A,
25    SplitMixSearch,
26    "the first multiplier it found"
27);
28
29const MIX_B: u64 = 0x94D049BB133111EB;
30because!(
31    MIX_B,
32    SplitMixSearch,
33    "the second multiplier it found"
34);
35
36const SHIFT_HIGH: u32 = 30;
37because!(
38    SHIFT_HIGH,
39    SplitMixSearch,
40    "the first xor shift distance it found, which moves high bits down before the multiply"
41);
42
43const SHIFT_MID: u32 = 27;
44because!(
45    SHIFT_MID,
46    SplitMixSearch,
47    "the second xor shift distance, tuned against avalanche bias"
48);
49
50const SHIFT_LOW: u32 = 31;
51because!(
52    SHIFT_LOW,
53    SplitMixSearch,
54    "the final xor shift distance it found, which spreads the top bit across the word"
55);
56
57#[derive(Clone, Copy, PartialEq, Eq, Debug)]
58pub struct Seed(pub u64);
59
60pub fn next(seed: Seed) -> (u64, Seed) {
61    let moved = seed.0.wrapping_add(GOLDEN_GAMMA);
62    let a = (moved ^ (moved >> SHIFT_HIGH)).wrapping_mul(MIX_A);
63    let b = (a ^ (a >> SHIFT_MID)).wrapping_mul(MIX_B);
64    (b ^ (b >> SHIFT_LOW), Seed(moved))
65}
66
67pub fn below(seed: Seed, bound: u64) -> (u64, Seed) {
68    let (value, moved) = next(seed);
69    let scaled = (value as u128 * bound as u128) >> u64::BITS;
70    (scaled as u64, moved)
71}
72
73pub fn pick<T: Clone>(seed: Seed, items: &[T]) -> (Option<T>, Seed) {
74    if items.is_empty() {
75        return (None, seed);
76    }
77    let (at, moved) = below(seed, items.len() as u64);
78    (Some(items[at as usize].clone()), moved)
79}
80because!(next, "the next value from the generator, exposed so a caller advances the sequence without reaching into its state");
81because!(below, "a value under a bound taken from the high bits of the word by one multiply, which never divides, so a zero bound yields zero rather than a panic; a bound that does not divide the word leaves the same slight bias a remainder would, and that is not removed here");
82because!(pick, "choosing an element by index from a slice, so a caller never computes an index itself");