ya_rand/lib.rs
1/*!
2# YA-Rand: Yet Another Rand
3
4Provides simple and fast pseudo/crypto random number generation.
5
6## But why?
7
8Because `rand` is very cool and powerful, but kind of an enormous fucking pain in the ass
9to use, and it's far too large and involved for someone who just a needs to flip a coin once
10every 7 minutes. But if you're doing some crazy black magic computational sorcery, it almost
11certainly has something you can use to complete your spell.
12
13Other crates, like `fastrand`, `tinyrand`, or `oorandom`, fall somewhere between "I'm not sure I trust the
14backing RNG" to "this API is literally just `rand` but less powerful". Meaning their state size (how many
15internal bits they hold) is too small for comfort, or they lean into having a properly idiomatic Rust API
16instead of being straightforward to use. I wanted something easy, but also fast and statistically robust.
17
18So here we are.
19
20## Usage
21
22Glob import the contents of the library and use [`new_rng`] to create new RNGs wherever
23you need them. Then call whatever method you require on those instances. All methods available
24are directly accessible through any generator instance via the dot operator, and are named
25in a way that should make it easy to quickly identify what you need. See below for a few examples.
26
27If you need cryptographic security, enable the **secure** library feature and use
28[`new_rng_secure`] instead.
29
30"How do I access the thread-local RNG?" There isn't one, and unless Rust improves the performance and
31ergonomics of the TLS implementation, there probably won't ever be. Create a local instance when and
32where you need one and use it while you need it. If you need an RNG to stick around for awhile, passing
33it between functions or storing it in structs is a perfectly valid solution.
34
35```
36use ya_rand::*;
37
38// **Correct** instantiation is very easy.
39// This seeds the RNG using operating system entropy,
40// so you never have to worry about the quality of the
41// initial state of RNG instances.
42let mut rng = new_rng();
43
44// Generate a random number with a given upper bound.
45let max: u64 = 420;
46let val = rng.bound(max);
47assert!(val < max);
48
49// Generate a random number in a given range.
50let min: i64 = -69;
51let max: i64 = 69;
52let val = rng.range(min, max);
53assert!(min <= val && val < max);
54
55// Generate a random floating point value.
56let val = rng.f64();
57assert!(0.0 <= val && val < 1.0);
58
59// Generate a random ascii digit: '0'..='9' as a char.
60let digit = rng.ascii_digit();
61assert!(digit.is_ascii_digit());
62```
63
64## Features
65
66* **std** -
67 Enabled by default, but can be disabled for compatibility with `no_std` environments.
68 Enables normal/exponential distributions and error type conversions for getrandom.
69 Also enables generation of random `String`'s when enabled alongside the **secure** feature.
70* **inline** -
71 Marks each [`YARandGenerator::u64`] implementation with #\[inline\]. Should generally increase
72 runtime performance at the cost of binary size and maybe compile time. You'll have
73 to test your specific use case to determine how much this feature will impact you.
74* **secure** -
75 Enables infrastructure for cryptographically secure random number generation via the
76 [`chacha20`] crate. Moderately increases compile time and binary size.
77
78## Details
79
80This crate uses the [xoshiro] family of pseudo-random number generators. These generators are
81very fast, of [very high statistical quality], and small. They aren't cryptograpically secure,
82but most users don't need their RNG to be secure, they just need it to be random and fast. The default
83generator is xoshiro256++, which should provide a large enough period for most users. The xoshiro512++
84generator is also provided in case you need a longer period.
85
86[xoshiro]: https://prng.di.unimi.it/
87[very high statistical quality]: https://vigna.di.unimi.it/ftp/papers/ScrambledLinear.pdf
88
89All generators output a distinct `u64` value on each call, and the various methods used for transforming
90those outputs into more usable forms are all high-quality and well-understood. Placing an upper bound
91on these values uses [Lemire's method]. Doing this inclusively or within a given range are both
92applications of this same method with simple intermediary steps to alter the bound and apply shifts
93when needed. This approach is unbiased and quite fast, but for very large bounds performance might degrade
94slightly, since the algorithm may need to sample the underlying RNG more times to get an unbiased result.
95If your bound happens to be a power of 2, always use [`YARandGenerator::bits`], since it's nothing more
96than a bitshift of the `u64` provided by the RNG, and will always be as fast as possible.
97
98Floating point values (besides the normal and exponential distributions) are uniformally distributed,
99with all the possible outputs being equidistant within the given interval. They are **not** maximally dense,
100if that's something you need you'll have to generate those values yourself. This approach is very fast, and
101endorsed by both [Lemire] and [Vigna] (the author of the RNGs used in this crate). The normal distribution
102implementation uses the [Marsaglia polar method], returning pairs of independently sampled `f64` values.
103Exponential variates are generated using [this approach].
104
105[Lemire's method]: https://arxiv.org/abs/1805.10941
106[Lemire]: https://lemire.me/blog/2017/02/28/how-many-floating-point-numbers-are-in-the-interval-01/
107[Vigna]: https://prng.di.unimi.it/#remarks
108[Marsaglia polar method]: https://en.wikipedia.org/wiki/Marsaglia_polar_method
109[this approach]: https://en.wikipedia.org/wiki/Exponential_distribution#Random_variate_generation
110
111## Security
112
113If you're in the market for secure random number generation, you'll want to enable the **secure**
114feature, which provides [`SecureRng`] and the [`SecureYARandGenerator`] trait. It functions identically to
115the other provided RNGs, but with added functionality that isn't safe to use on pseudo RNGs. The current
116implementation uses ChaCha with 8 rounds via the [`chacha20`] crate. In the future I'd like to look into
117doing a custom implementation of ChaCha, but no timeline on that. Why only 8 rounds? Because people who are
118very passionate about cryptography are convinced that's enough, and I have zero reason to doubt them, nor
119any capacity to prove them wrong. See the top of page 14 of the [`Too Much Crypto`] paper.
120
121The security promises made to the user are identical to those made by ChaCha as an algorithm. It is up
122to you to determine if those guarantees meet the demands of your use case.
123
124[`Too Much Crypto`]: https://eprint.iacr.org/2019/1492
125
126## Safety
127
128Generators are seeded using entropy from the underlying OS, and have the *potential* to fail during creation.
129But in practice this is extraordinarily unlikely, and isn't something the end-user should ever worry about.
130Modern Windows versions (10 and newer) have a crypto subsystem that will never fail during runtime, and
131the error branch should be optimized out.
132
133In the pursuit of consistent performance and no runtime failures, there are no checks performed during
134runtime in release mode. This means that there are a few areas where the end-user is able to receive garbage
135after providing garbage. It is expected of the user to provide reasonable values where there is an input to
136be given: values shouldn't be on the verge of overflow and ranges should always have a max larger than their
137min. There is very little unsafe used, and it's all easily determined to have no ill side-effects.
138*/
139
140#![no_std]
141
142#[cfg(feature = "std")]
143extern crate std;
144
145mod rng;
146mod util;
147mod xoshiro256pp;
148mod xoshiro512pp;
149
150pub use rng::{SeedableYARandGenerator, YARandGenerator};
151pub use xoshiro256pp::Xoshiro256pp;
152pub use xoshiro512pp::Xoshiro512pp;
153
154/// The recommended generator for all non-cryptographic purposes.
155pub type ShiroRng = Xoshiro256pp;
156
157/// The recommended way to create new PRNG instances.
158///
159/// Identical to calling [`ShiroRng::new`] or [`Xoshiro256pp::new`].
160#[inline]
161pub fn new_rng() -> ShiroRng {
162 ShiroRng::new()
163}
164
165#[cfg(feature = "secure")]
166mod secure;
167#[cfg(feature = "secure")]
168pub use {rng::SecureYARandGenerator, secure::SecureRng};
169
170/// The recommended way to create new CRNG instances.
171///
172/// Identical to calling [`SecureRng::new`].
173#[cfg(feature = "secure")]
174#[inline]
175pub fn new_rng_secure() -> SecureRng {
176 SecureRng::new()
177}
178
179#[cfg(all(feature = "secure", feature = "std"))]
180mod encoding;
181#[cfg(all(feature = "secure", feature = "std"))]
182pub mod ya_rand_encoding {
183 pub use super::encoding::*;
184}
185
186#[cfg(test)]
187mod test {
188 #[cfg(not(feature = "std"))]
189 compile_error!("tests can only be run when the `std` feature is enabled");
190
191 use super::*;
192 use std::collections::HashSet;
193
194 const CAP: usize = 100;
195 const ITERATIONS: u64 = 1 << 12;
196 const ITERATIONS_LONG: u64 = 1 << 24;
197
198 #[test]
199 pub fn ascii_alphabetic() {
200 let mut rng = new_rng();
201 let mut vals = HashSet::with_capacity(CAP);
202 for _ in 0..ITERATIONS {
203 let result = rng.ascii_alphabetic();
204 assert!(result.is_ascii_alphabetic());
205 vals.insert(result);
206 }
207 assert!(vals.len() == 52);
208 }
209
210 #[test]
211 pub fn ascii_uppercase() {
212 let mut rng = new_rng();
213 let mut vals = HashSet::with_capacity(CAP);
214 for _ in 0..ITERATIONS {
215 let result = rng.ascii_uppercase();
216 assert!(result.is_ascii_uppercase());
217 vals.insert(result);
218 }
219 assert!(vals.len() == 26);
220 }
221
222 #[test]
223 pub fn ascii_lowercase() {
224 let mut rng = new_rng();
225 let mut vals = HashSet::with_capacity(CAP);
226 for _ in 0..ITERATIONS {
227 let result = rng.ascii_lowercase();
228 assert!(result.is_ascii_lowercase());
229 vals.insert(result);
230 }
231 assert!(vals.len() == 26);
232 }
233
234 #[test]
235 pub fn ascii_alphanumeric() {
236 let mut rng = new_rng();
237 let mut vals = HashSet::with_capacity(CAP);
238 for _ in 0..ITERATIONS {
239 let result = rng.ascii_alphanumeric();
240 assert!(result.is_ascii_alphanumeric());
241 vals.insert(result);
242 }
243 assert!(vals.len() == 62);
244 }
245
246 #[test]
247 pub fn ascii_digit() {
248 let mut rng = new_rng();
249 let mut vals = HashSet::with_capacity(CAP);
250 for _ in 0..ITERATIONS {
251 let result = rng.ascii_digit();
252 assert!(result.is_ascii_digit());
253 vals.insert(result);
254 }
255 assert!(vals.len() == 10);
256 }
257
258 #[test]
259 fn wide_mul() {
260 const SHIFT: u32 = 48;
261 const EXPECTED_HIGH: u64 = 1 << ((SHIFT * 2) - u64::BITS);
262 const EXPECTED_LOW: u64 = 0;
263 let x = 1 << SHIFT;
264 let y = x;
265 // 2^48 * 2^48 = 2^96
266 let (high, low) = util::wide_mul(x, y);
267 assert!(high == EXPECTED_HIGH);
268 assert!(low == EXPECTED_LOW);
269 }
270
271 #[test]
272 fn f64() {
273 let mut rng = new_rng();
274 for _ in 0..ITERATIONS_LONG {
275 let val = rng.f64();
276 assert!(0.0 <= val && val < 1.0);
277 }
278 }
279
280 #[test]
281 fn f32() {
282 let mut rng = new_rng();
283 for _ in 0..ITERATIONS_LONG {
284 let val = rng.f32();
285 assert!(0.0 <= val && val < 1.0);
286 }
287 }
288
289 #[test]
290 fn f64_nonzero() {
291 let mut rng = new_rng();
292 for _ in 0..ITERATIONS_LONG {
293 let val = rng.f64_nonzero();
294 assert!(0.0 < val && val <= 1.0);
295 }
296 }
297
298 #[test]
299 fn f32_nonzero() {
300 let mut rng = new_rng();
301 for _ in 0..ITERATIONS_LONG {
302 let val = rng.f32_nonzero();
303 assert!(0.0 < val && val <= 1.0);
304 }
305 }
306
307 #[test]
308 fn f64_wide() {
309 let mut rng = new_rng();
310 for _ in 0..ITERATIONS_LONG {
311 let val = rng.f64_wide();
312 assert!(val.abs() < 1.0);
313 }
314 }
315
316 #[test]
317 fn f32_wide() {
318 let mut rng = new_rng();
319 for _ in 0..ITERATIONS_LONG {
320 let val = rng.f32_wide();
321 assert!(val.abs() < 1.0);
322 }
323 }
324}