Chapter 23 10 min read

Random

Pseudo-random numbers backed by xorshift64\ — fast, with a full 64-bit period. It is not* cryptographically secure, but it is exactly what you want for shuffling, sampling, randomized tests, and reproducible simulations.

Seed deterministically with `Rng::with_seed` for runs you can replay, or from the monotonic clock with `Rng::from_clock` for "random enough".

Import

import stdlib::random::*;

Public surface at a glance

The module exposes exactly one type and eight methods — three constructors/ destructor and five draw helpers. Everything below is built on next_i64.

Item Signature Purpose
Rng pub struct Rng { state: i64 } The generator (one i64 of state).
with_seed pub fn with_seed(seed: i64) -> *Rng Construct from a known seed (reproducible).
from_clock pub fn from_clock() -> *Rng Construct from now_ns() ("random enough").
free pub fn free(self: *Rng) Release the generator.
next_i64 pub fn next_i64(self: *Rng) -> i64 Raw 64-bit value (wraps; may be negative).
next_int pub fn next_int(self: *Rng) -> i32 Non-negative i32 in [0, 2^31 - 1].
range pub fn range(self: *Rng, lo: i32, hi: i32) -> i32 Uniform i32 in [lo, hi).
next_f64 pub fn next_f64(self: *Rng) -> f64 Uniform f64 in [0.0, 1.0).
next_bool pub fn next_bool(self: *Rng) -> bool true with probability ~0.5.

The Rng generator

pub struct Rng { state: i64 }

Rng holds a single i64 of internal state. It is heap-allocated by its constructors and must be released with `free` — pair it with defer for leak-free use.

Construction

Function Signature Description
with_seed pub fn with_seed(seed: i64) -> *Rng Build an Rng from a known seed; same seed always yields the same stream.
from_clock pub fn from_clock() -> *Rng Build an Rng seeded from the monotonic clock (now_ns()).
free pub fn free(self: *Rng) Release the generator.

Rng::with_seed

pub fn with_seed(seed: i64) -> *Rng

Construct an Rng with a known starting state. The same seed always produces the same stream, making this the right choice for tests and reproducible simulations.

import stdlib::random::*;

fn main() -> i32 {
    let r: *Rng = Rng::with_seed(42);
    defer r.free();
    println!("first draw", r.next_int());   // same value on every run
    return 0;
}

Rng::from_clock

pub fn from_clock() -> *Rng

Construct an Rng seeded from the monotonic clock. Internally this is just Rng::with_seed(now_ns()).

import stdlib::random::*;

fn main() -> i32 {
    let r: *Rng = Rng::from_clock();
    defer r.free();
    for i in 0..5 { println!("d6", r.range(1, 7)); }   // five dice rolls
    return 0;
}

free

pub fn free(self: *Rng)

Frees the generator's heap allocation. Because the constructors hand back a raw *Rng, you are responsible for the lifetime — defer r.free(); right after construction is the idiomatic pattern.

Drawing values

Every draw advances the internal state in place. The full catalog:

Method Signature Range / result
next_i64 pub fn next_i64(self: *Rng) -> i64 Raw 64-bit value (wraps); full entropy.
next_int pub fn next_int(self: *Rng) -> i32 Non-negative i32 in [0, 2^31 - 1].
range pub fn range(self: *Rng, lo: i32, hi: i32) -> i32 Uniform i32 in [lo, hi).
next_f64 pub fn next_f64(self: *Rng) -> f64 Uniform f64 in [0.0, 1.0).
next_bool pub fn next_bool(self: *Rng) -> bool true with probability ~0.5.

All five together, from one seeded generator:

import stdlib::random::*;

fn main() -> i32 {
    let r: *Rng = Rng::with_seed(42);
    defer r.free();

    let raw: i64 = r.next_i64();      // full 64-bit, may be negative
    let n: i32 = r.next_int();        // 0 .. 2^31-1
    let d6: i32 = r.range(1, 7);      // 1..=6
    let p: f64 = r.next_f64();        // [0.0, 1.0)
    let flip: bool = r.next_bool();   // ~50/50

    println!("raw", raw);
    println!("int", n);
    println!("d6", d6);
    if flip { println!("heads"); } else { println!("tails"); }
    if p < 0.5 { println!("low half"); }
    return 0;
}

next_i64

pub fn next_i64(self: *Rng) -> i64

Next raw 64-bit value: one xorshift step followed by a multiply with an odd constant. The result wraps and may be negative. Use this when you want all 64 bits of entropy (hashing, jitter); for bounded integers prefer `range`. Every other draw helper is a thin wrapper over this.

let raw: i64 = r.next_i64();

next_int

pub fn next_int(self: *Rng) -> i32

Next non-negative i32. The value is masked to 31 bits so the result is always positive after the i64 -> i32 cast — keeping all 32 bits would let the high bit make it negative. Range: [0, 2^31 - 1].

let n: i32 = r.next_int();        // 0 .. 2^31-1

range

pub fn range(self: *Rng, lo: i32, hi: i32) -> i32

Uniform integer in the half-open interval [lo, hi). Returns lo when the range is empty (hi <= lo), so it never traps on a degenerate span.

let dice: i32 = r.range(1, 7);    // d6  -> 1..=6
let pct:  i32 = r.range(0, 100);  // 0..=99
let same: i32 = r.range(5, 5);    // empty range -> returns 5

next_f64

pub fn next_f64(self: *Rng) -> f64

Uniform float in [0.0, 1.0). Built from the upper 53 bits of a raw draw so the result fits exactly in an f64 mantissa with no boundary rounding error. Ideal for probability thresholds and Bernoulli trials.

let p: f64 = r.next_f64();
if p < 0.1 { /* runs ~10% of the time */ }

next_bool

pub fn next_bool(self: *Rng) -> bool

Coin flip — returns true with probability ~0.5 (it tests the low bit of a raw draw).

if r.next_bool() { println!("heads"); } else { println!("tails"); }

Reproducibility

Two generators built from the same seed produce identical sequences, which is what makes seeded runs replayable — record the seed, and you can replay the exact run later.

import stdlib::random::*;

fn main() -> i32 {
    let a: *Rng = Rng::with_seed(12345);
    defer a.free();
    let b: *Rng = Rng::with_seed(12345);
    defer b.free();

    let mut matched: bool = true;
    for i in 0..10 {
        if a.next_i64() != b.next_i64() { matched = false; }
    }
    if matched { println!("identical streams"); }
    return 0;
}

Distribution sanity check — a Bernoulli trial built on next_f64:

import stdlib::random::*;

fn main() -> i32 {
    // Count ~250 hits out of 1000 trials at p = 0.25.
    let r: *Rng = Rng::with_seed(0);   // 0 is rewritten to a non-zero state
    defer r.free();
    let mut hits: i32 = 0;
    for i in 0..1000 {
        if r.next_f64() < 0.25 { hits = hits + 1; }
    }
    println!("approx 250", hits);
    return 0;
}

Recipes

This module ships only seeding and the five draw helpers; the common collection operations are short to build yourself.

Picking a random element (choice)

Index a vector with range(0, len):

import stdlib::random::*;

fn pick(r: *Rng, v: *Vector<i32>) -> i32 {
    let i: i32 = r.range(0, v.len() as i32);
    return v.get(i as usize);
}

fn main() -> i32 {
    let nums: *Vector<i32> = Vector::new();
    defer nums.free();
    push_all!(nums, 10, 20, 30, 40, 50);

    let r: *Rng = Rng::with_seed(99);
    defer r.free();
    for i in 0..3 { println!("pick", pick(r, nums)); }
    return 0;
}

Shuffling (Fisher–Yates)

Walk from the last index down, swapping each element with a random earlier (or equal) one. Note the inclusive upper bound r.range(0, i + 1).

import stdlib::random::*;

fn shuffle(r: *Rng, v: *Vector<i32>) {
    let n: i32 = v.len() as i32;
    let mut i: i32 = n - 1;
    while i > 0 {
        let j: i32 = r.range(0, i + 1);   // 0..=i
        let tmp: i32 = v.get(i as usize);
        v.set(i as usize, v.get(j as usize));
        v.set(j as usize, tmp);
        i = i - 1;
    }
}

fn main() -> i32 {
    let deck: *Vector<i32> = Vector::new();
    defer deck.free();
    for k in 0..8 { deck.push(k); }

    let r: *Rng = Rng::with_seed(2024);
    defer r.free();
    shuffle(r, deck);

    for k in 0..deck.len() { println!("card", deck.get(k)); }
    return 0;
}

Random bytes

There is no bytes helper; draw each byte with range(0, 256):

import stdlib::random::*;

fn fill_bytes(r: *Rng, out: *Vector<i32>, count: i32) {
    let mut i: i32 = 0;
    while i < count {
        out.push(r.range(0, 256));   // a byte 0..=255
        i = i + 1;
    }
}

fn main() -> i32 {
    let buf: *Vector<i32> = Vector::new();
    defer buf.free();

    let r: *Rng = Rng::with_seed(7);
    defer r.free();
    fill_bytes(r, buf, 16);

    println!("len", buf.len());
    println!("first", buf.get(0));
    return 0;
}

See also

  • `time`now_ns() is what from_clock seeds from.
  • The vector reference — Vector::new, push, get, set, len used in
  • the recipes above.