rand

tape
import rand from "rand";

rand makes pseudo-random numbers from a seed: for games, simulations, sampling, shuffling and test data. A generator is a small struct you own; the same seed gives the same numbers on every platform, in the VM and natively, so a run can be repeated exactly.

tape
var g: rand.Xoshiro256 = rand.xoshiro256(2026);
let die: i64 = g.range_i64(1, 7);      // 1 to 6
let x: f64 = g.next_f64();             // 0.0 <= x < 1.0

These generators are not for security: anyone who sees a few outputs can predict the rest. Don’t use them for passwords, tokens or keys.

The module is pure: it computes, nothing else. It works in t1 and t2.

Generators

There are two, with the same methods. Pick Xoshiro256 unless you have a reason not to.

Xoshiro256Pcg32
Algorithmxoshiro256**PCG-XSH-RR 64/32
Native output64 bits32 bits
State32 bytes16 bytes
Make onerand.xoshiro256(seed)rand.pcg32(seed), rand.pcg32_stream(seed, stream)

xoshiro256

tape
pub fn xoshiro256(seed: u64) -> Xoshiro256

A generator started from seed. Any seed is fine, 0 included: the state is spread out from it with SplitMix64.

pcg32 and pcg32_stream

tape
pub fn pcg32(seed: u64) -> Pcg32
pub fn pcg32_stream(seed: u64, stream: u64) -> Pcg32

A PCG32 generator. pcg32_stream also picks one of 2^63 streams: two generators with the same seed on different streams give unrelated numbers, handy for giving each part of a program its own sequence. The results match the reference pcg32_srandom_r(seed, stream) from pcg-random.org.

Methods

Both generators have these, as g.next_u64() and so on.

next_u64, next_u32

tape
pub fn Xoshiro256.next_u64(*self) -> u64
pub fn Xoshiro256.next_u32(*self) -> u32

The next 64 or 32 random bits.

range_i64

tape
pub fn Xoshiro256.range_i64(*self, lo: i64, hi: i64) -> i64

A random integer from lo up to but not including hi, every value equally likely. % on a random number would favour small values; this doesn’t (it uses Lemire’s method). Any range works, negative bounds and spans past 2^63 included. Traps with rand: empty range when lo >= hi.

tape
let card: i64 = g.range_i64(0, 52);      // 0 to 51
let delta: i64 = g.range_i64(-3, 4);     // -3 to 3

next_f64

tape
pub fn Xoshiro256.next_f64(*self) -> f64

A random float in [0, 1): never 1.0. It has 53 random bits, the full precision of an f64 in that range. For a float in [a, b), use a + (b - a) * g.next_f64().

Shuffle and choice

tape
pub @tape fn gen_shuffle(T: type)

Shuffling and picking from a slice of T. gen_shuffle(T) emits, into your module:

tape
pub fn shuffle_$T(rng: *rand.Xoshiro256, s: []T)
pub fn choice_$T(rng: *rand.Xoshiro256, s: []const T) -> ?T
pub fn shuffle_pcg32_$T(rng: *rand.Pcg32, s: []T)
pub fn choice_pcg32_$T(rng: *rand.Pcg32, s: []const T) -> ?T

shuffle puts the elements in a random order, every order equally likely (Fisher–Yates). Strings and structs holding them are moved, never copied. choice returns a random element, or null when the slice is empty.

The ones for i64 already exist: rand.shuffle_i64, rand.choice_i64, rand.shuffle_pcg32_i64, rand.choice_pcg32_i64.

tape
import rand from "rand";

pub struct Card { name: string; rank: i64; }

@tape { rand.gen_shuffle(Card); }

var g: rand.Xoshiro256 = rand.xoshiro256(7);
shuffle_Card(&g, deck[0..52]);

var nums: [5]i64 = [1, 2, 3, 4, 5];
let pick: ?i64 = rand.choice_i64(&g, nums[0..5]);

The struct must be pub, because the generated functions are.

Seeding

A fixed seed repeats the same run, which is what tests and replays want. For numbers that differ on every run, seed from the OS’s random source with os.random_bytes (rand itself stays pure):

tape
import os from "os";
import bits from "bits";

var seed: [8]u8;
os.random_bytes(seed[0..8]) or { return 1; };
var g: rand.Xoshiro256 = rand.xoshiro256(bits.load_u64_le(seed[0..8], 0));

Example

stdlib/examples/rand.tape rolls dice, shuffles a deck and takes a random walk:

tape
var g: rand.Xoshiro256 = rand.xoshiro256(2026);
var counts: [6]i64;
for (i in 0..600) {
    let d: i64 = g.range_i64(1, 7);
    counts[d - 1] = counts[d - 1] + 1;
}
var deck: [10]i64;
for (i in 0..10) { deck[i] = i + 1; }
rand.shuffle_i64(&g, deck[0..10]);
io.println("drawn: {rand.choice_i64(&g, deck[0..10])!}");
plaintext
600 rolls: 88 112 105 103 100 92
shuffled: 9 10 5 7 3 6 4 1 2 8
drawn: 1
walk after 1000 steps: 7.4763310258561
pcg32 words: 3096658103 2014397322

Last modified: