snarkvm_utilities/rand.rs
1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use core::convert::Infallible;
17use rand::{
18 Rng,
19 RngExt,
20 SeedableRng,
21 TryCryptoRng,
22 TryRng,
23 distr::{Distribution, StandardUniform},
24};
25use rand_xorshift::XorShiftRng;
26
27/// A trait for a uniform random number generator.
28pub trait Uniform: Sized {
29 /// Samples a random value from a uniform distribution.
30 fn rand<R: RngExt + ?Sized>(rng: &mut R) -> Self;
31}
32
33impl<T> Uniform for T
34where
35 StandardUniform: Distribution<T>,
36{
37 #[inline]
38 fn rand<R: RngExt + ?Sized>(rng: &mut R) -> Self {
39 rng.random()
40 }
41}
42
43// The rand 0.8 default implementation.
44pub trait UniformExt: Uniform {
45 /// Generates a random Option<Self> (50/50 chance of Some or None)
46 fn rand_option<R: Rng + ?Sized>(rng: &mut R) -> Option<Self> {
47 if rng.random() { Some(Self::rand(rng)) } else { None }
48 }
49}
50
51// Blanket implement it for anything that already implements Uniform
52impl<T: Uniform> UniformExt for T {}
53
54/// A fast RNG used **solely** for testing and benchmarking, **not** for any real world purposes.
55pub struct TestRng {
56 seed: u64,
57 rng: XorShiftRng,
58 calls: usize,
59}
60
61impl Default for TestRng {
62 fn default() -> Self {
63 // Obtain the initial seed using entropy provided by the OS.
64 let seed: u64 = rand::random();
65
66 // Use it as the basis for the underlying Rng.
67 Self::fixed(seed)
68 }
69}
70
71impl TestRng {
72 pub fn fixed(seed: u64) -> Self {
73 // Print the seed, so it's displayed if any of the tests using `test_rng` fails.
74 println!("\nInitializing 'TestRng' with seed '{seed}'\n");
75
76 // Use the seed to initialize a fast, non-cryptographic Rng.
77 Self::from_seed(seed)
78 }
79
80 // This is the preferred method to use once the main instance of TestRng had already
81 // been initialized in a test or benchmark and an auxiliary one is desired without
82 // spamming the stdout.
83 pub fn from_seed(seed: u64) -> Self {
84 Self { seed, rng: XorShiftRng::seed_from_u64(seed), calls: 0 }
85 }
86
87 /// Returns a randomly-sampled `String`, given the maximum size in bytes and an RNG.
88 ///
89 /// Some of the snarkVM internal tests involve the random generation of strings,
90 /// which are parsed and tested against the original ones. However, since the string parser
91 /// rejects certain characters, if those characters are randomly generated, the tests fail.
92 ///
93 /// To prevent these failures, as we randomly generate the characters,
94 /// we ensure that they are not among the ones rejected by the parser;
95 /// if they are, we adjust them to be allowed characters.
96 ///
97 /// Note that the randomness of the characters is strictly for **testing** purposes;
98 /// also note that the disallowed characters are a small fraction of the total set of characters,
99 /// and thus the adjustments rarely occur.
100 pub fn next_string(&mut self, max_bytes: u32, is_fixed_size: bool) -> String {
101 /// Adjust an unsafe character.
102 ///
103 /// As our parser rejects certain potentially unsafe characters (see `Sanitizer::parse_safe_char`),
104 /// we need to avoid generating them randomly. This function acts as an adjusting filter:
105 /// it changes an unsafe character to `'0'` (other choices are possible), and leaves other
106 /// characters unchanged.
107 fn adjust_unsafe_char(ch: char) -> char {
108 let code = ch as u32;
109 if code < 9
110 || code == 11
111 || code == 12
112 || (14..=31).contains(&code)
113 || code == 127
114 || (0x202a..=0x202e).contains(&code)
115 || (0x2066..=0x2069).contains(&code)
116 {
117 '0'
118 } else {
119 ch
120 }
121 }
122
123 /// Adjust a backslash and a double quote.
124 ///
125 /// Aside from the characters rejected through the function [adjust_unsafe_char],
126 /// the syntax of strings allows backslash and double quotes only in certain circumstances:
127 /// backslash is used to introduce an escape, and there are constraints on what can occur
128 /// after a backslash; double quotes is only used in escaped form just after a backslash.
129 ///
130 /// If we randomly sample characters, we may end up generating backslashes with
131 /// malformed escape syntax, or double quotes not preceded by backslash. Thus,
132 /// we also adjust backslashes and double quotes as we randomly sample characters.
133 ///
134 /// Note that, this way, we do not test the parsing of any escape sequences;
135 /// to do that, we would need to reify the possible elements of strings,
136 /// namely characters and escapes, and randomly generate such elements.
137 fn adjust_backslash_and_doublequote(ch: char) -> char {
138 if ch == '\\' || ch == '\"' { '0' } else { ch }
139 }
140
141 let range = match is_fixed_size {
142 true => 0..max_bytes,
143 false => 0..self.random_range(0..max_bytes),
144 };
145
146 range.map(|_| self.random::<char>()).map(adjust_unsafe_char).map(adjust_backslash_and_doublequote).collect()
147 }
148}
149
150impl TryRng for TestRng {
151 type Error = Infallible;
152
153 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
154 self.calls += 1;
155 Ok(self.rng.next_u32())
156 }
157
158 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
159 self.calls += 1;
160 Ok(self.rng.next_u64())
161 }
162
163 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
164 self.calls += 1;
165 self.rng.fill_bytes(dest);
166 Ok(())
167 }
168}
169
170impl TryCryptoRng for TestRng {}
171
172impl Drop for TestRng {
173 fn drop(&mut self) {
174 println!("Called TestRng with seed {} {} times", self.seed, self.calls);
175 }
176}
177
178/// This impl is Lemire's method with approximate zone. It reproduces rand 0.8's
179/// `rng.gen_range(low..=high)` for `usize` on 64-bit. It mustn't be modified
180/// to maintain backwards compatibility. It is a direct reimplementation of
181/// `sample_single_inclusive` for a concrete type (`usize`) an inlined widening multiply
182/// from https://github.com/rust-random/rand/blob/937320c/src/distributions/uniform.rs.
183pub fn gen_range_inclusive_legacy(low: usize, high: usize, rng: &mut impl Rng) -> usize {
184 debug_assert!(low <= high);
185
186 let range = high.wrapping_sub(low).wrapping_add(1);
187 // The range is 0..=usize::MAX.
188 if range == 0 {
189 return rng.random::<u64>() as usize;
190 }
191
192 // Approximate zone: conservative but avoids division.
193 let zone = (range << range.leading_zeros()).wrapping_sub(1);
194
195 loop {
196 let v = rng.next_u64() as usize;
197 // Widening multiply: v * range as u128, split into (hi, lo).
198 let wide = (v as u128) * (range as u128);
199 let hi = (wide >> 64) as usize;
200 let lo = wide as usize;
201 if lo <= zone {
202 return low.wrapping_add(hi);
203 }
204 }
205}
206
207/// This impl reproduces rand 0.8's `slice.choose_weighted(rng, weight_fn)` for u16 weights.
208/// It mustn't be modified to maintain backwards compatibility. It is a direct, "collapsed"
209/// reimplementation of `WeightedIndex::new(weights).sample(rng)` specifically for `u16` weights
210/// from https://github.com/rust-random/rand/blob/937320c/src/distributions/weighted_index.rs.
211pub fn choose_weighted_legacy<'a, T, R: Rng>(slice: &'a [T], weight_fn: impl Fn(&T) -> u16, rng: &mut R) -> &'a T {
212 // WeightedIndex::new.
213 let mut iter = slice.iter();
214 let first = iter.next().unwrap();
215 let mut total: u16 = weight_fn(first);
216 let mut cumulative: Vec<u16> = Vec::with_capacity(slice.len() - 1);
217 for item in iter {
218 cumulative.push(total);
219 total += weight_fn(item);
220 }
221 assert!(total > 0);
222
223 // Uniform::new(0u16, total) -> new_inclusive(0, total - 1)
224 // range as u16, then promoted to u32 for zone math.
225 let range = total as u32;
226 let ints_to_reject = (u32::MAX - range + 1) % range;
227 let zone = u32::MAX - ints_to_reject;
228
229 // Uniform::sample (exact Lemire, u32 sample space).
230 let chosen: u16 = loop {
231 let v = rng.next_u32();
232 let wide = (v as u64) * (range as u64);
233 let hi = (wide >> 32) as u32;
234 let lo = wide as u32;
235 if lo <= zone {
236 break hi as u16;
237 }
238 };
239
240 // Binary search (partition_point).
241 let idx = cumulative.partition_point(|w| *w <= chosen);
242 &slice[idx]
243}