Skip to content

Commit

Permalink
Move random number generation to the Random type
Browse files Browse the repository at this point in the history
This refactors std::random (now renamed to std::rand) to use a type
(Random) for random number generation, instead of using module methods.
Using a type makes it possible to customise the seed used for creating a
random number generator.

As part of this we also introduce the Shuffle type, used for randomly
sorting arrays, and used by Array.shuffle. Moving this logic into a
dedicated type means we can shuffle with fixed seeds, without having to
add more methods to the Array type.

This fixes https://gitlab.com/inko-lang/inko/-/issues/272.

Changelog: added
  • Loading branch information
yorickpeterse committed Sep 20, 2022
1 parent a0fe7a6 commit 6991a8d
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 116 deletions.
9 changes: 9 additions & 0 deletions bytecode/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,9 @@ pub enum BuiltinFunction {
ProcessStacktraceLength,
FloatToBits,
FloatFromBits,
RandomNew,
RandomFromInt,
RandomDrop,
}

impl BuiltinFunction {
Expand Down Expand Up @@ -1017,6 +1020,9 @@ impl BuiltinFunction {
BuiltinFunction::ProcessStacktraceLength => 140,
BuiltinFunction::FloatToBits => 141,
BuiltinFunction::FloatFromBits => 142,
BuiltinFunction::RandomNew => 143,
BuiltinFunction::RandomFromInt => 144,
BuiltinFunction::RandomDrop => 145,
}
}

Expand Down Expand Up @@ -1207,6 +1213,9 @@ impl BuiltinFunction {
}
BuiltinFunction::FloatToBits => "float_to_bits",
BuiltinFunction::FloatFromBits => "float_from_bits",
BuiltinFunction::RandomNew => "random_new",
BuiltinFunction::RandomFromInt => "random_from_int",
BuiltinFunction::RandomDrop => "random_drop",
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/type_check/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,9 @@ pub(crate) fn define_builtin_functions(state: &mut State) -> bool {
(BIF::ProcessStacktraceLength, int, never),
(BIF::FloatToBits, int, never),
(BIF::FloatFromBits, float, never),
(BIF::RandomNew, any, never),
(BIF::RandomFromInt, any, never),
(BIF::RandomDrop, nil, never),
];

// Regular VM instructions exposed directly to the standard library. These
Expand Down
30 changes: 5 additions & 25 deletions libstd/src/std/array.inko
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import std::hash::(Hash, Hasher)
import std::index::(bounds_check, Index, IndexMut, SetIndex)
import std::iter::(Enum, Iter)
import std::option::Option
import std::random::(int_range)
import std::rand::Shuffle

# An ordered, integer-indexed generic collection of values.
#
Expand Down Expand Up @@ -309,32 +309,12 @@ class builtin Array[T] {
#
# # Examples
#
# let a = [10, 20]
# let a = [10, 20]
#
# a.shuffle
#
# a # => [20, 10]
# a.shuffle
# a # => [20, 10]
fn pub mut shuffle {
# The algorithm used here is Sattolo's algorithm. Some more details on this
# are found here:
#
# - https://en.wikipedia.org/wiki/Fisher–Yates_shuffle#Sattolo's_algorithm
# - https://danluu.com/sattolo/
# - https://rosettacode.org/wiki/Sattolo_cycle
#
# Note that the types produced by `array_get()` and `array_set()` are `Any`.
# These values aren't dropped automatically, so there's no need to mark them
# as moved to prevent them from being dropped after the swap.
let mut swap = length - 1

while swap > 0 {
let swap_with = int_range(start: 0, stop: swap)
let swap_val = _INKO.array_get(self, swap)

_INKO.array_set(self, swap, _INKO.array_set(self, swap_with, swap_val))

swap -= 1
}
Shuffle.new.sort(self)
}

# Reverses `self` in-place
Expand Down
1 change: 0 additions & 1 deletion libstd/src/std/map.inko
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import std::hash::(Hash, Hasher)
import std::index::(Index, IndexMut, SetIndex)
import std::iter::Iter
import std::process::(panic)
import std::random

fn resize_threshold(size: Int) -> Int {
# This produces a threshold of 90%, without the need to allocate floats.
Expand Down
144 changes: 144 additions & 0 deletions libstd/src/std/rand.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Cryptographically secure random number generation.
import std::drop::Drop

# A cryptographically secure pseudo random number generator (CSPRNG).
#
# The algorithm used is unspecified but guaranteed to be cryptographically
# secure.
class pub Random {
# The internal/low-level random number generator.
let @rng: Any

# Returns a new `Random` using the given `Int` as its seed.
#
# `Random` instances created using this method **are not** suitable for
# cryptography, as a single `Int` doesn't produce enough entropy. For
# cryptography you _must_ use `Random.new` instead.
#
# # Examples
#
# import std::rand::Random
#
# Random.from_int(42)
fn pub static from_int(seed: Int) -> Self {
Self { @rng = _INKO.random_from_int(seed) }
}

# Returns a new `Random` seeded using a cryptographically secure seed.
#
# Seeding is performed by the runtime using a thread-local random number
# generator suitable for cryptography.
#
# # Examples
#
# import std::rand::Random
#
# Random.new
fn pub static new -> Self {
Self { @rng = _INKO.random_new }
}

# Returns a randomly generated `Int`.
#
# # Examples
#
# import std::rand::Random
#
# Random.new.int
fn pub mut int -> Int {
_INKO.random_int(@rng)
}

# Returns a randomly generated `Float`.
#
# # Examples
#
# import std::rand::Random
#
# Random.new.float
fn pub mut float -> Float {
_INKO.random_float(@rng)
}

# Returns a randomly generated `Int` in the given range.
#
# The returned value is in the range `start <= value < stop`. If
# `start >= stop` is true, this method returns `0`.
fn pub int_between(min: Int, max: Int) -> Int {
_INKO.random_int_range(@rng, min, max)
}

# Returns a randomly generated `Float` in the given range.
#
# The returned value is in the range `start <= value < stop`. If
# `start >= stop` is true, this method returns `0.0`.
fn pub float_between(min: Float, max: Float) -> Float {
_INKO.random_float_range(@rng, min, max)
}

# Returns a `ByteArray` containing randomly generated bytes.
#
# The returned `ByteArray` will contain exactly `size` bytes.
#
# # Panics
#
# This method might panic if no random bytes could be generated.
fn pub bytes(size: Int) -> ByteArray {
_INKO.random_bytes(@rng, size)
}
}

impl Drop for Random {
fn mut drop {
_INKO.random_drop(@rng)
}
}

# A type for sorting arrays in a random order.
class pub Shuffle {
let @rng: Random

# Returns a new `Shuffle` that sorts values in a random order.
fn pub static new -> Self {
Self { @rng = Random.new }
}

# Returns a new `Shuffle` that uses the given seed for sorting values.
fn pub static from_int(seed: Int) -> Self {
Self { @rng = Random.from_int(seed) }
}

# Sorts the values of the given `Array` in place such that they are in a
# random order.
#
# The algorithm used by this method is Sattolo's algorithm. Some more details
# on this are found here:
#
# - https://en.wikipedia.org/wiki/Fisher–Yates_shuffle#Sattolo's_algorithm
# - https://danluu.com/sattolo/
# - https://rosettacode.org/wiki/Sattolo_cycle
#
# # Examples
#
# import std::rand::Shuffle
#
# let a = [10, 20]
#
# Shuffle.new.sort(a)
# a # => [20, 10]
fn pub mut sort[T](array: mut Array[T]) {
# Note that the types produced by `array_get()` and `array_set()` are `Any`.
# These values aren't dropped automatically, so there's no need to mark them
# as moved to prevent them from being dropped after the swap.
let mut swap = array.length - 1

while swap > 0 {
let swap_with = @rng.int_between(min: 0, max: swap)
let swap_val = _INKO.array_get(array, swap)

_INKO.array_set(array, swap, _INKO.array_set(array, swap_with, swap_val))

swap -= 1
}
}
}
38 changes: 0 additions & 38 deletions libstd/src/std/random.inko

This file was deleted.

4 changes: 2 additions & 2 deletions libstd/test/main.inko
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import std::test_map
import std::test_nil
import std::test_option
import std::test_process
import std::test_random
import std::test_rand
import std::test_range
import std::test_set
import std::test_stdio
Expand Down Expand Up @@ -56,7 +56,7 @@ class async Main {
test_option.tests(tests)
test_path.tests(tests)
test_process.tests(tests)
test_random.tests(tests)
test_rand.tests(tests)
test_range.tests(tests)
test_set.tests(tests)
test_socket.tests(tests)
Expand Down
1 change: 1 addition & 0 deletions libstd/test/std/test_array.inko
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import helpers::(fmt, hash, Script)
import std::drop::(drop, Drop)
import std::rand::Random
import std::test::Tests

class Counter {
Expand Down
69 changes: 69 additions & 0 deletions libstd/test/std/test_rand.inko
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import helpers::(fmt, hash)
import std::rand::(Random, Shuffle)
import std::test::Tests

fn pub tests(t: mut Tests) {
t.test('Random.from_int') fn (t) {
let rng = Random.from_int(42)

# Since we have a fixed seed, we also have a fixed output.
t.equal(rng.int, -8733474309719776094)
t.equal(rng.float, 0.5427252099031439)
t.equal(rng.bytes(3), ByteArray.from_array([209, 52, 81]))
}

t.test('Random.int') fn (t) {
# This is just a smoke test to ensure the underlying code isn't outright
# wrong.
Random.new.int
}

t.test('Random.float') fn (t) {
# This is just a smoke test to ensure the underlying code isn't outright
# wrong.
Random.new.float
}

t.test('Random.int_between') fn (t) {
let rng = Random.new
let range1 = rng.int_between(min: 1, max: 5)
let range2 = rng.int_between(min: 1, max: 1)
let range3 = rng.int_between(min: 10, max: 1)

t.true(range1 >= 1 and range1 < 5)
t.equal(range2, 0)
t.equal(range3, 0)
}

t.test('Random.float_between') fn (t) {
let rng = Random.new
let range1 = rng.float_between(min: 1.0, max: 5.0)
let range2 = rng.float_between(min: 1.0, max: 1.0)
let range3 = rng.float_between(min: 10.0, max: 1.0)

t.true(range1 >= 1.0 and range1 < 5.0)
t.equal(range2, 0.0)
t.equal(range3, 0.0)
}

t.test('Random.bytes') fn (t) {
let rng = Random.new

t.equal(rng.bytes(3).length, 3)
}

t.test('Shuffle.sort') fn (t) {
let one = [10]
let two = [10, 20]
let three = [10, 20, 30]
let shuffle = Shuffle.from_int(42)

shuffle.sort(one)
shuffle.sort(two)
shuffle.sort(three)

t.equal(one, [10])
t.equal(two, [20, 10])
t.equal(three, [20, 30, 10])
}
}
Loading

0 comments on commit 6991a8d

Please sign in to comment.