Skip to main content

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}