From a5dd7faf3cb0084682560d3c07f928b62c8b88a1 Mon Sep 17 00:00:00 2001 From: Subhomoy Haldar Date: Wed, 3 Mar 2021 17:11:00 +0530 Subject: [PATCH] rand: add PRNG interface and unit-tests (#9083) --- vlib/io/util/util.v | 15 +++++----- vlib/rand/musl/musl_rng.v | 2 +- vlib/rand/rand.v | 51 +++++++++++++++++++++++++++++++-- vlib/rand/random_numbers_test.v | 49 +++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 12 deletions(-) diff --git a/vlib/io/util/util.v b/vlib/io/util/util.v index 60d4f4c175..e27be3f3ee 100644 --- a/vlib/io/util/util.v +++ b/vlib/io/util/util.v @@ -2,7 +2,6 @@ module util import os import rand -import rand.wyrand import rand.seed as rseed const ( @@ -25,9 +24,9 @@ pub fn temp_file(tfo TempFileOptions) ?(os.File, string) { ' could not create temporary file in "$d". Please ensure write permissions.') } d = d.trim_right(os.path_separator) - mut rng := rand.new_default(rand.PRNGConfigStruct{}) + mut rng := rand.new_default({}) prefix, suffix := prefix_and_suffix(tfo.pattern) or { return error(@FN + ' ' + err.msg) } - for retry := 0; retry < retries; retry++ { + for retry := 0; retry < util.retries; retry++ { path := os.join_path(d, prefix + random_number(mut rng) + suffix) mut mode := 'rw+' $if windows { @@ -42,7 +41,7 @@ pub fn temp_file(tfo TempFileOptions) ?(os.File, string) { } } return error(@FN + - ' could not create temporary file in "$d". Retry limit ($retries) exhausted. Please ensure write permissions.') + ' could not create temporary file in "$d". Retry limit ($util.retries) exhausted. Please ensure write permissions.') } pub struct TempDirOptions { @@ -61,9 +60,9 @@ pub fn temp_dir(tdo TempFileOptions) ?string { ' could not create temporary directory "$d". Please ensure write permissions.') } d = d.trim_right(os.path_separator) - mut rng := rand.new_default(rand.PRNGConfigStruct{}) + mut rng := rand.new_default({}) prefix, suffix := prefix_and_suffix(tdo.pattern) or { return error(@FN + ' ' + err.msg) } - for retry := 0; retry < retries; retry++ { + for retry := 0; retry < util.retries; retry++ { path := os.join_path(d, prefix + random_number(mut rng) + suffix) os.mkdir_all(path) or { rng.seed(rseed.time_seed_array(2)) @@ -78,11 +77,11 @@ pub fn temp_dir(tdo TempFileOptions) ?string { } } return error(@FN + - ' could not create temporary directory "$d". Retry limit ($retries) exhausted. Please ensure write permissions.') + ' could not create temporary directory "$d". Retry limit ($util.retries) exhausted. Please ensure write permissions.') } // * Utility functions -fn random_number(mut rng wyrand.WyRandRNG) string { +fn random_number(mut rng rand.PRNG) string { s := (u32(1e9) + (u32(os.getpid()) + rng.u32() % u32(1e9))).str() return s.substr(1, s.len) } diff --git a/vlib/rand/musl/musl_rng.v b/vlib/rand/musl/musl_rng.v index efc767cfc1..fdfb733fc3 100644 --- a/vlib/rand/musl/musl_rng.v +++ b/vlib/rand/musl/musl_rng.v @@ -106,7 +106,7 @@ pub fn (mut rng MuslRNG) u64n(max u64) u64 { // u32_in_range returns a pseudorandom 32-bit unsigned integer (`u32`) in range `[min, max)`. [inline] -pub fn (mut rng MuslRNG) u32_in_range(min u64, max u64) u64 { +pub fn (mut rng MuslRNG) u32_in_range(min u32, max u32) u32 { if max <= min { eprintln('max must be greater than min.') exit(1) diff --git a/vlib/rand/rand.v b/vlib/rand/rand.v index 2b81a8defc..b450853344 100644 --- a/vlib/rand/rand.v +++ b/vlib/rand/rand.v @@ -8,11 +8,42 @@ import rand.wyrand import time // PRNGConfigStruct is a configuration struct for creating a new instance of the default RNG. +// Note that the RNGs may have a different number of u32s required for seeding. The default +// generator WyRand used 64 bits, ie. 2 u32s so that is the default. In case your desired generator +// uses a different number of u32s, use the `seed.time_seed_array()` method with the correct +// number of u32s. pub struct PRNGConfigStruct { seed []u32 = seed.time_seed_array(2) } -__global ( default_rng &wyrand.WyRandRNG ) +// PRNG is a common interface for all PRNGs that can be used seamlessly with the rand +// modules's API. It defines all the methods that a PRNG (in the vlib or custom made) must +// implement in order to ensure that _all_ functions can be used with the generator. +pub interface PRNG { + seed(seed_data []u32) + u32() u32 + u64() u64 + u32n(max u32) u32 + u64n(max u64) u64 + u32_in_range(min u32, max u32) u32 + u64_in_range(min u64, max u64) u64 + int() int + i64() i64 + int31() int + int63() i64 + intn(max int) int + i64n(max i64) i64 + int_in_range(min int, max int) int + i64_in_range(min i64, max i64) i64 + f32() f32 + f64() f64 + f32n(max f32) f32 + f64n(max f64) f64 + f32_in_range(min f32, max f32) f32 + f64_in_range(min f64, max f64) f64 +} + +__global ( default_rng &PRNG ) // init initializes the default RNG. fn init() { @@ -20,13 +51,27 @@ fn init() { } // new_default returns a new instance of the default RNG. If the seed is not provided, the current time will be used to seed the instance. -pub fn new_default(config PRNGConfigStruct) &wyrand.WyRandRNG { +pub fn new_default(config PRNGConfigStruct) &PRNG { mut rng := &wyrand.WyRandRNG{} rng.seed(config.seed) return rng } -// seed sets the given array of `u32` values as the seed for the `default_rng`. +// get_current_rng returns the PRNG instance currently in use. If it is not changed, it will be an instance of wyrand.WyRandRNG. +pub fn get_current_rng() &PRNG { + return default_rng +} + +// set_rng changes the default RNG from wyrand.WyRandRNG (or whatever the last RNG was) to the one +// provided by the user. Note that this new RNG must be seeded manually with a constant seed or the +// `seed.time_seed_array()` method. Also, it is recommended to store the old RNG in a variable and +// should be restored if work with the custom RNG is complete. It is not necessary to restore if the +// program terminates soon afterwards. +pub fn set_rng(rng &PRNG) { + default_rng = rng +} + +// seed sets the given array of `u32` values as the seed for the `default_rng`. It is recommended to use pub fn seed(seed []u32) { default_rng.seed(seed) } diff --git a/vlib/rand/random_numbers_test.v b/vlib/rand/random_numbers_test.v index 7e8e1fe8c0..f09fa3c641 100644 --- a/vlib/rand/random_numbers_test.v +++ b/vlib/rand/random_numbers_test.v @@ -1,4 +1,7 @@ import rand +import rand.splitmix64 +import rand.musl +import rand.mt19937 const ( rnd_count = 40 @@ -268,3 +271,49 @@ fn test_rand_ascii() { assert rand.ascii(25) == output } } + +fn ensure_same_output(mut rng rand.PRNG) { + for _ in 0 .. 100 { + assert rand.int() == rng.int() + assert rand.intn(45) == rng.intn(45) + assert rand.u64() == rng.u64() + assert rand.f64() == rng.f64() + assert rand.u32n(25) == rng.u32n(25) + } +} + +fn test_new_global_rng() { + old := rand.get_current_rng() + + // MuslRNG + mut rng1a := musl.MuslRNG{} + mut rng1b := musl.MuslRNG{} + seed1 := [u32(1234)] + + rand.set_rng(rng1a) + rand.seed(seed1) + rng1b.seed(seed1) + ensure_same_output(mut rng1b) + + // SplitMix64RNG + mut rng2a := splitmix64.SplitMix64RNG{} + mut rng2b := splitmix64.SplitMix64RNG{} + seed2 := [u32(2325), 14] + + rand.set_rng(rng2a) + rand.seed(seed2) + rng2b.seed(seed2) + ensure_same_output(mut rng2b) + + // MT19937RNG + mut rng3a := mt19937.MT19937RNG{} + mut rng3b := mt19937.MT19937RNG{} + seed3 := [u32(0xcafe), 234] + + rand.set_rng(rng3a) + rand.seed(seed3) + rng3b.seed(seed3) + ensure_same_output(mut rng3b) + + rand.set_rng(old) +}