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}