Skip to content

Commit db1fa4e

Browse files
authored
feat(stdlib): Add module for pseudo-random number generation (#921)
1 parent b7c0463 commit db1fa4e

File tree

6 files changed

+510
-3
lines changed

6 files changed

+510
-3
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Random from "random"
2+
3+
let rng1 = Random.make(0xf00L)
4+
let rng2 = Random.make(0xf00L)
5+
6+
assert Random.nextInt32(rng1) == 833494444l
7+
assert Random.nextInt32(rng2) == 833494444l
8+
assert Random.nextInt32(rng1) == -1746193004l
9+
assert Random.nextInt32(rng2) == -1746193004l
10+
11+
assert Random.nextInt32(rng1) == Random.nextInt32(rng2)
12+
assert Random.nextInt64(rng1) == Random.nextInt64(rng2)
13+
assert Random.nextInt64(rng1) == Random.nextInt64(rng2)
14+
15+
assert Random.nextInt64(rng1) == -2063148927841070477L
16+
assert Random.nextInt64(rng2) == -2063148927841070477L
17+
assert Random.nextInt64(rng1) != -2063148927841070477L
18+
19+
assert Random.nextInt32InRange(rng1, 5l, 20l) == 5l
20+
assert Random.nextInt32InRange(rng1, 5l, 20l) == 18l
21+
assert Random.nextInt32InRange(rng1, 5l, 20l) == 19l
22+
23+
assert Random.nextInt64InRange(rng1, 5L, 20L) == 18L
24+
assert Random.nextInt64InRange(rng1, 5L, 20L) == 10L
25+
assert Random.nextInt64InRange(rng1, 5L, 20L) == 11L
Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
import Random from "sys/random"
22

3+
// Just smoke tests, there's a miniscule chance these could fail
4+
5+
let r1 = Random.randomInt32()
6+
let r2 = Random.randomInt32()
7+
match ((r1, r2)) {
8+
(Ok(x), Ok(y)) => assert x != y,
9+
(Err(err), _) => throw err,
10+
(_, Err(err)) => throw err,
11+
}
12+
13+
let r1 = Random.randomInt64()
14+
let r2 = Random.randomInt64()
15+
16+
match ((r1, r2)) {
17+
(Ok(x), Ok(y)) => assert x != y,
18+
(Err(err), _) => throw err,
19+
(_, Err(err)) => throw err,
20+
}
21+
322
let r1 = Random.random()
423
let r2 = Random.random()
524

6-
// Just a smoke test, there's a miniscule chance this could fail
725
match ((r1, r2)) {
826
(Ok(x), Ok(y)) => assert x != y,
927
(Err(err), _) => throw err,
10-
(_, Err(err)) => throw err
28+
(_, Err(err)) => throw err,
1129
}

stdlib/random.gr

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/**
2+
* @module Random: Pseudo-random number generation.
3+
* @example import Random from "random"
4+
* @since v0.5.0
5+
*/
6+
import WasiRandom from "sys/random"
7+
import Result from "result"
8+
import Int32 from "int32"
9+
import Int64 from "int64"
10+
import WasmI32 from "runtime/unsafe/wasmi32"
11+
import WasmI64 from "runtime/unsafe/wasmi64"
12+
import Memory from "runtime/unsafe/memory"
13+
import DS from "runtime/dataStructures"
14+
15+
/**
16+
* @section Types: Type declarations included in the Random module.
17+
*/
18+
19+
record Random {
20+
seed: Int64,
21+
mut counter: Int64,
22+
mut initialized: Bool,
23+
}
24+
25+
/**
26+
* @section Values: Functions for working with pseudo-random number generators.
27+
*/
28+
29+
let incCounter = random => {
30+
random.counter = Int64.incr(random.counter)
31+
}
32+
33+
// https://arxiv.org/pdf/2004.06278v3.pdf
34+
@unsafe
35+
let squares = (ctr: Int64, key: Int64) => {
36+
// Implemented with @unsafe to boost efficiency
37+
// and have fine-grained control over overflow semantics
38+
let ctr = WasmI64.load(WasmI32.fromGrain(ctr), 8n)
39+
let key = WasmI64.load(WasmI32.fromGrain(key), 8n)
40+
let mut x = WasmI64.mul(ctr, key)
41+
let mut y = x
42+
let mut z = WasmI64.add(y, key)
43+
// round 1
44+
x = WasmI64.add(WasmI64.mul(x, x), y)
45+
x = WasmI64.or(WasmI64.shrU(x, 32N), WasmI64.shl(x, 32N))
46+
// round 2
47+
x = WasmI64.add(WasmI64.mul(x, x), z)
48+
x = WasmI64.or(WasmI64.shrU(x, 32N), WasmI64.shl(x, 32N))
49+
// round 3
50+
x = WasmI64.add(WasmI64.mul(x, x), y)
51+
x = WasmI64.or(WasmI64.shrU(x, 32N), WasmI64.shl(x, 32N))
52+
let ret = WasmI32.wrapI64(
53+
WasmI64.shrU(WasmI64.add(WasmI64.mul(x, x), z), 32N)
54+
)
55+
WasmI32.toGrain(DS.newInt32(ret)): Int32
56+
}
57+
58+
/**
59+
* Creates a new pseudo-random number generator with the given seed.
60+
*
61+
* @param seed: The seed for the pseudo-random number generator
62+
* @returns The pseudo-random number generator
63+
*
64+
* @since v0.5.0
65+
*/
66+
export let make = seed => {
67+
{ seed, counter: 0L, initialized: false }
68+
}
69+
70+
/**
71+
* Creates a new pseudo-random number generator with a random seed.
72+
*
73+
* @returns `Ok(generator)` of a pseudo-random number generator if successful or `Err(exception)` otherwise
74+
*
75+
* @since v0.5.0
76+
*/
77+
export let makeUnseeded = () => {
78+
// TODO: Should we just .expect this result for UX's sake?
79+
Result.map(seed => {
80+
{ seed, counter: 0L, initialized: false }
81+
}, WasiRandom.randomInt64())
82+
}
83+
84+
/**
85+
* [Internal note]
86+
* For low seed numbers, we sometimes need to churn through
87+
* some iterations to start getting interesting numbers. Taking
88+
* a cue from the API in https://pypi.org/project/squares-rng/ ,
89+
* we churn through until we generate an int with a MSB of 1.
90+
* Then, to avoid making all of the first generated numbers negative,
91+
* we do another increment at the end.
92+
*/
93+
let checkInitialized = (random: Random) => {
94+
if (!random.initialized) {
95+
while (Int32.gt(Int32.clz(squares(random.counter, random.seed)), 0l)) {
96+
incCounter(random)
97+
}
98+
// now that it's initialized, increment it again to make it a little more random
99+
incCounter(random)
100+
random.initialized = true
101+
}
102+
}
103+
104+
/**
105+
* Generates a random 32-bit integer from the given pseudo-random number generator.
106+
*
107+
* @param random: The pseudo-random number generator to use
108+
* @returns The randomly generated number
109+
*
110+
* @since v0.5.0
111+
*/
112+
export let nextInt32 = (random: Random) => {
113+
checkInitialized(random)
114+
let ret = squares(random.counter, random.seed)
115+
incCounter(random)
116+
ret
117+
}
118+
119+
/**
120+
* Generates a random 64-bit integer from the given pseudo-random number generator.
121+
*
122+
* @param random: The pseudo-random number generator to use
123+
* @returns The randomly generated number
124+
*
125+
* @since v0.5.0
126+
*/
127+
export let nextInt64 = (random: Random) => {
128+
checkInitialized(random)
129+
let ret1 = Int64.fromNumber(
130+
Int32.toNumber(squares(random.counter, random.seed))
131+
)
132+
incCounter(random)
133+
let ret2 = Int64.fromNumber(
134+
Int32.toNumber(squares(random.counter, random.seed))
135+
)
136+
incCounter(random)
137+
Int64.lor(Int64.shl(ret1, 32L), ret2)
138+
}
139+
140+
/**
141+
* Generates a random 32-bit integer from the given pseudo-random number generator
142+
* from a uniform distribution in the given range.
143+
*
144+
* @param random: The pseudo-random number generator to use
145+
* @param low: The lower bound of the range (inclusive)
146+
* @param high: The upper bound of the range (exclusive)
147+
* @returns The randomly generated number
148+
*
149+
* @since v0.5.0
150+
*/
151+
export let nextInt32InRange = (random: Random, low: Int32, high: Int32) => {
152+
// Algorithm source: https://www.pcg-random.org/posts/bounded-rands.html#bitmask-with-rejection-unbiased-apples-method
153+
let (+) = Int32.add
154+
let (-) = Int32.sub
155+
let (*) = Int32.mul
156+
let (/) = Int32.divU
157+
let (&) = Int32.land
158+
let (>) = Int32.gtU
159+
let range = high - low - 1l
160+
let mask = Int32.shrU(Int32.lnot(0l), Int32.clz(Int32.lor(range, 1l)))
161+
let mut x = nextInt32(random) & mask
162+
let mut iters = 0l
163+
while (x > range) {
164+
x = nextInt32(random) & mask
165+
iters += 1l
166+
}
167+
x + low
168+
}
169+
170+
/**
171+
* Generates a random 64-bit integer from the given pseudo-random number generator
172+
* from a uniform distribution in the given range.
173+
*
174+
* @param random: The pseudo-random number generator to use
175+
* @param low: The lower bound of the range (inclusive)
176+
* @param high: The upper bound of the range (exclusive)
177+
* @returns The randomly generated number
178+
*
179+
* @since v0.5.0
180+
*/
181+
export let nextInt64InRange = (random: Random, low: Int64, high: Int64) => {
182+
// Algorithm source: https://www.pcg-random.org/posts/bounded-rands.html#bitmask-with-rejection-unbiased-apples-method
183+
let (+) = Int64.add
184+
let (-) = Int64.sub
185+
let (*) = Int64.mul
186+
let (/) = Int64.divU
187+
let (&) = Int64.land
188+
let (>) = Int64.gtU
189+
let range = high - low - 1L
190+
let mask = Int64.shrU(Int64.lnot(0L), Int64.clz(Int64.lor(range, 1L)))
191+
let mut x = nextInt64(random) & mask
192+
while (x > range) {
193+
x = nextInt64(random) & mask
194+
}
195+
x + low
196+
}

0 commit comments

Comments
 (0)