1+ // TODO: Document.
2+ :trait Random
3+ // Each random generator is expected to supply an implementation to generate
4+ // a pseudo-random 64-bit number, and other value types are derived from this.
5+ :fun ref u64 U64
6+
7+ // Unsigned integers are derived by bit-shifting down from the original U64.
8+ // However, some implementations may override these with different approaches.
9+ //
10+ // We intentionally cascade each method into the next, so that if for example,
11+ // the `u32` method is overridden with another approach, then all
12+ // smaller-width values will derive from `u32` instead of `u64`.
13+ //
14+ // In the normal case, with no overrides, we trust LLVM to inline and combine
15+ // the bit shift operations to remove unnecessary bit shift instructions.
16+ :fun ref u32 U32: @u64.bit_shr(32).u32
17+ :fun ref u16 U16: @u32.bit_shr(16).u16
18+ :fun ref u8 U8: @u16.bit_shr(8).u8
19+
20+ // Signed integers are derived by generating as unsigned, then converting.
21+ :fun ref i64 I64: @u64.i64
22+ :fun ref i32 I32: @u32.i32
23+ :fun ref i16 I16: @u16.i16
24+ :fun ref i8 I8: @u8.i8
25+
26+ // Floating-point numbers are similarly derived by converting unsigned ones.
27+ :fun ref f64 F64: F64.from_bits(@u64)
28+ :fun ref f32 F32: F32.from_bits(@u32)
29+
30+ // Boolean values are derived from the most significant bit of the value.
31+ :fun ref bool Bool: @u8.bit_and(0x80) != 0
32+
33+ :: Return a random 32-bit fraction - an F32 in the range [0, 1).
34+ ::
35+ :: The possible values will be uniformly spaced at one half-epsilon apart,
36+ :: because half-epsilon is the largest unit of least precision below 1.0.
37+ ::
38+ :: To generate in a range that includes 1 but excludes 0, see frac_32_nonzero.
39+ :fun ref frac_32 F32
40+ @u32.bit_shr(F32.exp_bit_width).f32 * F32.half_epsilon
41+
42+ :: Return a random 32-bit nonzero fraction - an F32 in the range (0, 1].
43+ ::
44+ :: The possible values will be uniformly spaced at one half-epsilon apart,
45+ :: because half-epsilon is the largest unit of least precision below 1.0.
46+ ::
47+ :: To generate in a range that includes 0 but excludes 1, see frac_32.
48+ :fun ref frac_32_nonzero F32
49+ @frac_32 + F32.half_epsilon
50+
51+ :: Return a random 64-bit fraction - an F64 in the range [0, 1).
52+ ::
53+ :: The possible values will be uniformly spaced at one half-epsilon apart,
54+ :: because half-epsilon is the largest unit of least precision below 1.0.
55+ ::
56+ :: To generate in a range that includes 1 but excludes 0, see frac_64_nonzero.
57+ :fun ref frac_64 F64
58+ @u64.bit_shr(F64.exp_bit_width).f64 * F64.half_epsilon
59+
60+ :: Return a random 64-bit nonzero fraction - an F64 in the range (0, 1].
61+ ::
62+ :: The possible values will be uniformly spaced at one half-epsilon apart,
63+ :: because half-epsilon is the largest unit of least precision below 1.0.
64+ ::
65+ :: To generate in a range that includes 0 but excludes 1, see frac_64.
66+ :fun ref frac_64_nonzero F64
67+ @frac_64 + F64.half_epsilon
68+
69+ :: Return a random U64 below the given limit - in the range [0, limit).
70+ ::
71+ :: The results will be slightly biased (some numbers have one more sample
72+ :: in the pool than others), but the bias is very slight if the limit is
73+ :: much smaller than the maximum representable U64 value, and the bias is
74+ :: eliminated entirely if the given limit is a power of 2.
75+ ::
76+ :: If you need fully unbiased results, use the `unbiased_u64_less_than`
77+ :: method instead, which discards all samples from the biased zone.
78+ :fun ref u64_less_than(limit U64) U64
79+ @u64.wide_multiply(limit).head
80+
81+ :: Return a random U32 below the given limit - in the range [0, limit).
82+ ::
83+ :: The results will be slightly biased (some numbers have one more sample
84+ :: in the pool than others), but the bias is very slight if the limit is
85+ :: much smaller than the maximum representable U32 value, and the bias is
86+ :: eliminated entirely if the given limit is a power of 2.
87+ ::
88+ :: If you need fully unbiased results, use the `unbiased_u32_less_than`
89+ :: method instead, which discards all samples from the biased zone.
90+ :fun ref u32_less_than(limit U32) U32
91+ @u32.wide_multiply(limit).head
92+
93+ :: Return an unbiased random U64 below the given limit, discarding the rare
94+ :: cases where we get a result from a biased zone of the output.
95+ ::
96+ :: This ensures that every integer in the range has an equal probability
97+ :: of occurring (at least, assuming a perfectly fair underlying generator).
98+ :: However, performance can degrade slightly due to discarding and retrying.
99+ ::
100+ :: To keep maximum performance at the cost of some possibility of bias,
101+ :: use the `u64_less_than` method instead, which never discards results.
102+ :fun ref unbiased_u64_less_than(limit U64) U64
103+ // See https://arxiv.org/pdf/1805.10941.pdf
104+ random = @
105+ product = random.u64.wide_multiply(limit)
106+ // Reject and try again if the tail happens to be in the zone of bias.
107+ // The loop will continue generating new numbers until we get a good sample.
108+ if (product.tail < limit) (
109+ threshold = limit.negate % limit
110+ while (product.tail < threshold) (
111+ product = random.u64.wide_multiply(limit)
112+ )
113+ )
114+ product.head
115+
116+ :: Return an unbiased random U32 below the given limit, discarding the rare
117+ :: cases where we get a result from a biased zone of the output.
118+ ::
119+ :: This ensures that every integer in the range has an equal probability
120+ :: of occurring (at least, assuming a perfectly fair underlying generator).
121+ :: However, performance can degrade slightly due to discarding and retrying.
122+ ::
123+ :: To keep maximum performance at the cost of some possibility of bias,
124+ :: use the `u32_less_than` method instead, which never discards results.
125+ :fun ref unbiased_u32_less_than(limit U32) U32
126+ // See https://arxiv.org/pdf/1805.10941.pdf
127+ random = @
128+ product = random.u32.wide_multiply(limit)
129+ // Reject and try again if the tail happens to be in the zone of bias.
130+ // The loop will continue generating new numbers until we get a good sample.
131+ if (product.tail < limit) (
132+ threshold = limit.negate % limit
133+ while (product.tail < threshold) (
134+ product = random.u32.wide_multiply(limit)
135+ )
136+ )
137+ product.head
138+
139+ :: Return a random USize below the given limit - in the range [0, limit).
140+ ::
141+ :: The results will be slightly biased (some numbers have one more sample
142+ :: in the pool than others), but the bias is very slight if the limit is
143+ :: much smaller than the maximum representable U32 value, and the bias is
144+ :: eliminated entirely if the given limit is a power of 2.
145+ ::
146+ :: If you need fully unbiased results, use the `unbiased_u32_less_than`
147+ :: method instead, which discards all samples from the biased zone.
148+ :fun ref usize_less_than(limit USize)
149+ if (USize.bit_width == 32) (
150+ @u32_less_than(limit.u32).usize
151+ |
152+ @u64_less_than(limit.u64).usize
153+ )
154+
155+ :: Return an unbiased random USize below the given limit, discarding the rare
156+ :: cases where we get a result from a biased zone of the output.
157+ ::
158+ :: This ensures that every integer in the range has an equal probability
159+ :: of occurring (at least, assuming a perfectly fair underlying generator).
160+ :: However, performance can degrade slightly due to discarding and retrying.
161+ ::
162+ :: To keep maximum performance at the cost of some possibility of bias,
163+ :: use the `uSize_less_than` method instead, which never discards results.
164+ :fun ref unbiased_usize_less_than(limit USize) USize
165+ if (USize.bit_width == 32) (
166+ @unbiased_u32_less_than(limit.u32).usize
167+ |
168+ @unbiased_u64_less_than(limit.u64).usize
169+ )
0 commit comments