Skip to content

Commit f27ce32

Browse files
committed
Add Random library code.
1 parent c95c334 commit f27ce32

6 files changed

Lines changed: 281 additions & 3 deletions

File tree

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright 2018 Joe Eli McIlvain
1+
Copyright 2021 Joe Eli McIlvain
22

33
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
44

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
A base repository for Savi language libraries, with common CI actions configured.
1+
# Random
22

3-
See the [Guide](https://github.com/savi-lang/base-standard-library/wiki/Guide) for details on how it works and how to use it for your own libraries.
3+
A standard interface for the varying random number generators in the Savi standard library.

manifest.savi

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
:manifest lib Random
2+
:sources "src/*.savi"
3+
4+
:manifest bin "spec"
5+
:copies Random
6+
:sources "spec/*.savi"
7+
8+
:dependency DeterministicRandom v0
9+
// TODO: :from "github:savi-lang/DeterministicRandom"
10+
:depends on Random
11+
12+
:dependency Spec v0
13+
:from "github:savi-lang/Spec"
14+
:depends on Map
15+
16+
:transitive dependency Map v0
17+
:from "github:savi-lang/Map"

spec/Main.savi

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:actor Main
2+
:new (env)
3+
Spec.Process.run(env, [
4+
Spec.Run(Random.Spec).new(env)
5+
])

spec/Random.Spec.savi

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// A fake random implementation that always returns the same 64-bit value,
2+
// so that the variety of different derived functions can be easily tested.
3+
:class _FakeRandom64
4+
:is Random
5+
:fun ref u64 U64: 0x0123456789abcdef
6+
7+
// A fake random implementation that always returns the same 64-bit value,
8+
// but overrides the 32-bit function to return a differently derived value,
9+
// to test that smaller values will prefer deriving from the 32-bit value.
10+
:class _FakeRandom6432
11+
:is Random
12+
:fun ref u64 U64: 0x0123456789abcdef
13+
:fun ref u32 U32: 0xf7e6d5c4
14+
15+
:class Random.Spec
16+
:is Spec
17+
:const describes: "Random"
18+
19+
:it "derives other values from the high side of the 64-bit value"
20+
random = _FakeRandom64.new
21+
assert: random.u64 == 0x0123456789abcdef
22+
assert: random.u32 == 0x01234567
23+
assert: random.u16 == 0x0123
24+
assert: random.u8 == 0x01
25+
assert: random.i64 == 0x0123456789abcdef
26+
assert: random.i32 == 0x01234567
27+
assert: random.i16 == 0x0123
28+
assert: random.i8 == 0x01
29+
assert: random.f64 == F64.from_bits(0x0123456789abcdef)
30+
assert: random.f32 == F32.from_bits(0x01234567)
31+
assert: random.bool == False
32+
33+
:it "derives smaller values from the high side of the 32-bit value if present"
34+
random = _FakeRandom6432.new
35+
assert: random.u64 == 0x0123456789abcdef
36+
assert: random.u32 == 0xf7e6d5c4
37+
assert: random.u16 == 0xf7e6
38+
assert: random.u8 == 0xf7
39+
assert: random.i64 == 0x0123456789abcdef
40+
assert: random.i32 == 0xf7e6d5c4
41+
assert: random.i16 == 0xf7e6
42+
assert: random.i8 == 0xf7
43+
assert: random.f64 == F64.from_bits(0x0123456789abcdef)
44+
assert: random.f32 == F32.from_bits(0xf7e6d5c4)
45+
assert: random.bool == True
46+
47+
:it "generates 64-bit fractional values between 0 and 1, averaging around 0.5"
48+
random = DeterministicRandom.Xoroshiro128.new_128(1, 2)
49+
count USize = 0x1_0000
50+
total F64 = 0
51+
count.times -> (total += random.frac_64)
52+
assert: (total / count.f64) == 0.49999695846019471
53+
54+
:it "generates 32-bit fractional values between 0 and 1, averaging around 0.5"
55+
random = DeterministicRandom.Xoroshiro128.new_128(1, 2)
56+
count USize = 0x1_0000
57+
total F32 = 0
58+
count.times -> (total += random.frac_32)
59+
assert: (total / count.f32) == 0.49999812245368958
60+
61+
:it "generates U64s below a given limit 52, averaging around 25.5"
62+
random = DeterministicRandom.Xoroshiro128.new_128(1, 2)
63+
count USize = 0x10_0000
64+
total U64 = 0
65+
count.times -> (total += random.u64_less_than(52))
66+
assert: (total.f64 / count.f64) == 25.514418601989746
67+
68+
:it "generates U32s below a given limit 52, averaging around 25.5"
69+
random = DeterministicRandom.Xoroshiro128.new_128(1, 2)
70+
count USize = 0x10_0000
71+
total U32 = 0
72+
count.times -> (total += random.u32_less_than(52))
73+
assert: (total.f32 / count.f32) == 25.514419555664062
74+
75+
:it "generates unbiased U64s below a given limit 52, discarding bias"
76+
random = DeterministicRandom.Xoroshiro128.new_128(1, 2)
77+
count USize = 0x10_0000
78+
total U64 = 0
79+
count.times -> (total += random.unbiased_u64_less_than(52))
80+
assert: (total.f64 / count.f64) == 25.514418601989746
81+
82+
:it "generates unbiased U32s below a given limit 52, discarding bias"
83+
random = DeterministicRandom.Xoroshiro128.new_128(1, 2)
84+
count USize = 0x10_0000
85+
total U32 = 0
86+
count.times -> (total += random.unbiased_u32_less_than(52))
87+
assert: (total.f32 / count.f32) == 25.514451980590820

src/Random.savi

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// TODO: Document.
2+
:trait Random
3+
// Each random generator is expected to supply an implementation to generate
4+
// a pseudo-random 64-bit number, and other value types are derived from this.
5+
:fun ref u64 U64
6+
7+
// Unsigned integers are derived by bit-shifting down from the original U64.
8+
// However, some implementations may override these with different approaches.
9+
//
10+
// We intentionally cascade each method into the next, so that if for example,
11+
// the `u32` method is overridden with another approach, then all
12+
// smaller-width values will derive from `u32` instead of `u64`.
13+
//
14+
// In the normal case, with no overrides, we trust LLVM to inline and combine
15+
// the bit shift operations to remove unnecessary bit shift instructions.
16+
:fun ref u32 U32: @u64.bit_shr(32).u32
17+
:fun ref u16 U16: @u32.bit_shr(16).u16
18+
:fun ref u8 U8: @u16.bit_shr(8).u8
19+
20+
// Signed integers are derived by generating as unsigned, then converting.
21+
:fun ref i64 I64: @u64.i64
22+
:fun ref i32 I32: @u32.i32
23+
:fun ref i16 I16: @u16.i16
24+
:fun ref i8 I8: @u8.i8
25+
26+
// Floating-point numbers are similarly derived by converting unsigned ones.
27+
:fun ref f64 F64: F64.from_bits(@u64)
28+
:fun ref f32 F32: F32.from_bits(@u32)
29+
30+
// Boolean values are derived from the most significant bit of the value.
31+
:fun ref bool Bool: @u8.bit_and(0x80) != 0
32+
33+
:: Return a random 32-bit fraction - an F32 in the range [0, 1).
34+
::
35+
:: The possible values will be uniformly spaced at one half-epsilon apart,
36+
:: because half-epsilon is the largest unit of least precision below 1.0.
37+
::
38+
:: To generate in a range that includes 1 but excludes 0, see frac_32_nonzero.
39+
:fun ref frac_32 F32
40+
@u32.bit_shr(F32.exp_bit_width).f32 * F32.half_epsilon
41+
42+
:: Return a random 32-bit nonzero fraction - an F32 in the range (0, 1].
43+
::
44+
:: The possible values will be uniformly spaced at one half-epsilon apart,
45+
:: because half-epsilon is the largest unit of least precision below 1.0.
46+
::
47+
:: To generate in a range that includes 0 but excludes 1, see frac_32.
48+
:fun ref frac_32_nonzero F32
49+
@frac_32 + F32.half_epsilon
50+
51+
:: Return a random 64-bit fraction - an F64 in the range [0, 1).
52+
::
53+
:: The possible values will be uniformly spaced at one half-epsilon apart,
54+
:: because half-epsilon is the largest unit of least precision below 1.0.
55+
::
56+
:: To generate in a range that includes 1 but excludes 0, see frac_64_nonzero.
57+
:fun ref frac_64 F64
58+
@u64.bit_shr(F64.exp_bit_width).f64 * F64.half_epsilon
59+
60+
:: Return a random 64-bit nonzero fraction - an F64 in the range (0, 1].
61+
::
62+
:: The possible values will be uniformly spaced at one half-epsilon apart,
63+
:: because half-epsilon is the largest unit of least precision below 1.0.
64+
::
65+
:: To generate in a range that includes 0 but excludes 1, see frac_64.
66+
:fun ref frac_64_nonzero F64
67+
@frac_64 + F64.half_epsilon
68+
69+
:: Return a random U64 below the given limit - in the range [0, limit).
70+
::
71+
:: The results will be slightly biased (some numbers have one more sample
72+
:: in the pool than others), but the bias is very slight if the limit is
73+
:: much smaller than the maximum representable U64 value, and the bias is
74+
:: eliminated entirely if the given limit is a power of 2.
75+
::
76+
:: If you need fully unbiased results, use the `unbiased_u64_less_than`
77+
:: method instead, which discards all samples from the biased zone.
78+
:fun ref u64_less_than(limit U64) U64
79+
@u64.wide_multiply(limit).head
80+
81+
:: Return a random U32 below the given limit - in the range [0, limit).
82+
::
83+
:: The results will be slightly biased (some numbers have one more sample
84+
:: in the pool than others), but the bias is very slight if the limit is
85+
:: much smaller than the maximum representable U32 value, and the bias is
86+
:: eliminated entirely if the given limit is a power of 2.
87+
::
88+
:: If you need fully unbiased results, use the `unbiased_u32_less_than`
89+
:: method instead, which discards all samples from the biased zone.
90+
:fun ref u32_less_than(limit U32) U32
91+
@u32.wide_multiply(limit).head
92+
93+
:: Return an unbiased random U64 below the given limit, discarding the rare
94+
:: cases where we get a result from a biased zone of the output.
95+
::
96+
:: This ensures that every integer in the range has an equal probability
97+
:: of occurring (at least, assuming a perfectly fair underlying generator).
98+
:: However, performance can degrade slightly due to discarding and retrying.
99+
::
100+
:: To keep maximum performance at the cost of some possibility of bias,
101+
:: use the `u64_less_than` method instead, which never discards results.
102+
:fun ref unbiased_u64_less_than(limit U64) U64
103+
// See https://arxiv.org/pdf/1805.10941.pdf
104+
random = @
105+
product = random.u64.wide_multiply(limit)
106+
// Reject and try again if the tail happens to be in the zone of bias.
107+
// The loop will continue generating new numbers until we get a good sample.
108+
if (product.tail < limit) (
109+
threshold = limit.negate % limit
110+
while (product.tail < threshold) (
111+
product = random.u64.wide_multiply(limit)
112+
)
113+
)
114+
product.head
115+
116+
:: Return an unbiased random U32 below the given limit, discarding the rare
117+
:: cases where we get a result from a biased zone of the output.
118+
::
119+
:: This ensures that every integer in the range has an equal probability
120+
:: of occurring (at least, assuming a perfectly fair underlying generator).
121+
:: However, performance can degrade slightly due to discarding and retrying.
122+
::
123+
:: To keep maximum performance at the cost of some possibility of bias,
124+
:: use the `u32_less_than` method instead, which never discards results.
125+
:fun ref unbiased_u32_less_than(limit U32) U32
126+
// See https://arxiv.org/pdf/1805.10941.pdf
127+
random = @
128+
product = random.u32.wide_multiply(limit)
129+
// Reject and try again if the tail happens to be in the zone of bias.
130+
// The loop will continue generating new numbers until we get a good sample.
131+
if (product.tail < limit) (
132+
threshold = limit.negate % limit
133+
while (product.tail < threshold) (
134+
product = random.u32.wide_multiply(limit)
135+
)
136+
)
137+
product.head
138+
139+
:: Return a random USize below the given limit - in the range [0, limit).
140+
::
141+
:: The results will be slightly biased (some numbers have one more sample
142+
:: in the pool than others), but the bias is very slight if the limit is
143+
:: much smaller than the maximum representable U32 value, and the bias is
144+
:: eliminated entirely if the given limit is a power of 2.
145+
::
146+
:: If you need fully unbiased results, use the `unbiased_u32_less_than`
147+
:: method instead, which discards all samples from the biased zone.
148+
:fun ref usize_less_than(limit USize)
149+
if (USize.bit_width == 32) (
150+
@u32_less_than(limit.u32).usize
151+
|
152+
@u64_less_than(limit.u64).usize
153+
)
154+
155+
:: Return an unbiased random USize below the given limit, discarding the rare
156+
:: cases where we get a result from a biased zone of the output.
157+
::
158+
:: This ensures that every integer in the range has an equal probability
159+
:: of occurring (at least, assuming a perfectly fair underlying generator).
160+
:: However, performance can degrade slightly due to discarding and retrying.
161+
::
162+
:: To keep maximum performance at the cost of some possibility of bias,
163+
:: use the `uSize_less_than` method instead, which never discards results.
164+
:fun ref unbiased_usize_less_than(limit USize) USize
165+
if (USize.bit_width == 32) (
166+
@unbiased_u32_less_than(limit.u32).usize
167+
|
168+
@unbiased_u64_less_than(limit.u64).usize
169+
)

0 commit comments

Comments
 (0)