← random · API reference · version 0.3.6
5 file(s), 539 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.
Public items
A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.
| Item | Defined | Worked example | Call sites |
|---|---|---|---|
rand fn | 11 | RND-001 | 11 16 18 20 23 24 … (34 total) |
rand_seed fn | 21 | RND-001 | 8 22 25 26 27 29 … (30 total) |
rand_indices fn | 28 | RND-001 | 36 37 41 42 50 53 … (21 total) |
RandStream struct | 55 | — | 65 72 79 100 26 65 |
seed_stream fn | 65 | RND-003 | 7 14 24 29 34 37 … (29 total) |
get fn | 79 | RND-002 | 107 8 9 17 20 25 … (39 total) |
indices fn | 100 | RND-003 | 54 80 91 96 98 101 … (8 total) |
src/random.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// --- Random ---
// Random number generation functions.
// Returns a random integer in [lo, hi] (inclusive).
// Returns null when lo > hi or when either argument is null.
// Draws from the ONE hidden generator every call site shares — see `rand_seed`.
// Example: @RND-001 — why a seeded global sequence is not yours, and what a
// RandStream gives instead.
pub fn rand(lo: integer, hi: integer) -> integer?;
#native
// Seeds the thread-local random number generator with the given value.
// Call this before rand() or rand_indices() to get reproducible sequences — but
// note that it fixes what the GENERATOR produces, not what you receive: it is
// shared, so one `rand()` from anywhere else shifts everything after it. When
// reproducibility is a requirement rather than a convenience, own a stream.
// Example: @RND-001 — a foreign draw between two of yours, and the same
// interruption against a RandStream.
pub fn rand_seed(seed: integer);
#native
// Returns a vector of n integers [0, 1, ..., n-1] in a random order.
// Useful for random iteration, shuffling, or sampling without replacement.
// Returns an empty vector when n is 0, negative or null.
// Example: @RND-001 — this draws from the shared global generator too.
pub fn rand_indices(n: integer) -> vector<integer>;
#native
// --- RandStream: owned random streams (the value tier) ---
//
// The global rand()/rand_seed() above share ONE hidden generator — right for casual
// use, wrong when the RNG is part of your STATE. A RandStream is an independent,
// deterministic stream you OWN: seed it, carry it (a plain struct — it saves, replays
// and syncs with your data), and draw from it without any other call site reordering
// your sequence. Typical uses: a game's per-level generation stream vs its combat
// stream; several simulations alive in one process; replay/lockstep determinism.
//
// Pure loft, no native: L'Ecuyer's (1988) combined LCG — two Lehmer streams over
// distinct primes via Schrage's method, combined. Period ~2.3e18, classical
// statistical quality, and every intermediate product stays far below loft's
// overflow-trapping 2^63.
//
// A struct parameter is a store link, so draws advance the stream through the
// default receiver — methods-way call sites:
//
// r = seed_stream(7);
// v = r.get(1, 6); // a die roll from THIS stream
// p = r.indices(52); // a shuffled deck from THIS stream
// An independent random stream: the two Lehmer generator states. Seed one with `seed_stream`
// and draw from it with its methods, so its sequence does not depend on any other draw in the
// program.
pub struct RandStream {
pub s1: integer, // Lehmer state A in [1, 2147483562]
pub s2: integer, // Lehmer state B in [1, 2147483398]
}
// Seed an independent stream. Any integer seed is valid (negative and zero included);
// equal seeds give equal streams, different seeds give decorrelated streams.
// Example: @RND-001 — the reason to prefer this over `rand_seed`.
// Example: @RND-003 — equal seeds replay exactly, so a recorded seed is a
// recorded run.
pub fn seed_stream(seed: integer) -> RandStream {
a = seed % 2147483562 ?? 0;
if a < 0 { a = -a; }
t = seed % 2147483398 ?? 0;
if t < 0 { t = -t; }
// decorrelate s2 from s1 for small seeds with one cheap Lehmer-style mix
mix = ((a + 1) * 40014) % 2147483563 ?? 0;
RandStream { s1: a + 1, s2: ((t + mix) % 2147483398 ?? 0) + 1 }
}
// A random integer in [lo, hi] (inclusive) from THIS stream, advancing it.
// Returns null when lo > hi (the same contract as the global rand()).
// Example: @RND-002 — assigning a stream COPIES it, passing one to a function
// LINKS it, so a helper that draws spends the caller's sequence.
pub fn get(self: RandStream, lo: integer, hi: integer) -> integer? {
if lo > hi { return null; }
// L'Ecuyer combined LCG, Schrage's method (all products < 2^31 * 40692 — trap-safe)
k1 = self.s1 / 53668 ?? 0;
n1 = 40014 * (self.s1 - k1 * 53668) - k1 * 12211;
if n1 < 0 { n1 += 2147483563; }
k2 = self.s2 / 52774 ?? 0;
n2 = 40692 * (self.s2 - k2 * 52774) - k2 * 3791;
if n2 < 0 { n2 += 2147483399; }
self.s1 = n1;
self.s2 = n2;
z = (n1 - n2) % 2147483562 ?? 0;
if z < 1 { z += 2147483562; }
lo + ((z - 1) % (hi - lo + 1) ?? 0)
}
// A random permutation of [0, 1, ..., n-1] from THIS stream (Fisher-Yates), advancing
// it by exactly n - 1 draws. Empty when n is 0, negative or null (the same contract
// as rand_indices()).
// Example: @RND-003 — a shuffle is a countable number of draws, so inserting one
// shifts the rest of the sequence by a known amount rather than adding noise.
pub fn indices(self: RandStream, n: integer) -> vector<integer> {
out: vector<integer> = [];
if !n { return out; }
if n <= 0 { return out; }
for i in 0..n { out += [i]; }
j = n - 1;
while j > 0 {
k = get(self, 0, j) ?? 0;
if k < len(out) && j < len(out) {
t = out[j] ?? 0;
out[j] = out[k] ?? 0;
out[k] = t;
}
j -= 1;
}
out
}
tests/15-random.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// Random numbers: rand, rand_seed, rand_indices.
use random::*;
fn main() {
// --- rand: values stay within [lo, hi] ---
random::rand_seed(42);
ok = true;
for _ in 0..200 {
r = random::rand(10, 20);
if r < 10 || r > 20 {
ok = false;
}
}
assert(ok, "rand stays in [10, 20]");
// --- rand: single-value range always returns that value ---
assert(random::rand(7, 7) == 7, "rand single value");
// --- rand: lo > hi returns null ---
assert(!random::rand(10, 5), "rand invalid range (lo>hi) is null");
// --- rand_seed: reproducible sequence ---
random::rand_seed(99);
a = random::rand(0, 1000);
b = random::rand(0, 1000);
random::rand_seed(99);
assert(random::rand(0, 1000) == a, "rand_seed reproducible: first value matches");
assert(random::rand(0, 1000) == b, "rand_seed reproducible: second value matches");
// --- rand_seed: different seeds produce different values ---
random::rand_seed(1);
s1 = random::rand(0, 1000000);
random::rand_seed(2);
s2 = random::rand(0, 1000000);
assert(s1 != s2, "different seeds give different values");
// --- rand_indices: length ---
random::rand_seed(7);
vi = random::rand_indices(10);
assert(len(vi) == 10, "rand_indices(10) has length 10");
// --- rand_indices: is a permutation of 0..n-1 ---
random::rand_seed(42);
n = 20;
perm = random::rand_indices(n);
assert(len(perm) == n, "rand_indices permutation length");
for expected in 0..n {
found = false;
for x in perm {
if x == expected {
found = true;
}
}
assert(found, "rand_indices missing value {expected}");
}
// --- rand_indices: empty input ---
ev = random::rand_indices(0);
assert(len(ev) == 0, "rand_indices(0) is empty");
// --- rand_indices: different seeds give different orderings ---
random::rand_seed(1);
ra = random::rand_indices(10);
random::rand_seed(9999);
rb = random::rand_indices(10);
diff = false;
for i in 0..10 {
if ra[i] != rb[i] {
diff = true;
}
}
assert(diff, "rand_indices: different seeds give different orders");
// --- rand_indices: same seed gives same ordering ---
random::rand_seed(123);
rc = random::rand_indices(8);
random::rand_seed(123);
rd = random::rand_indices(8);
same = true;
for i in 0..8 {
if rc[i] != rd[i] {
same = false;
}
}
assert(same, "rand_indices: same seed gives same order");
}
fn test_rand_in_range() {
random::rand_seed(42);
ok = true;
for _i in 0..200 {
r = random::rand(10, 20);
if r < 10 || r > 20 { ok = false }
}
assert(ok, "rand out of range");
}
fn test_rand_single_value() {
assert(random::rand(7, 7) == 7, "single value");
}
fn test_rand_invalid_range() {
assert(!random::rand(10, 5), "invalid range should be null");
}
fn test_rand_seed_reproducible() {
random::rand_seed(99);
a = random::rand(0, 1000);
b = random::rand(0, 1000);
random::rand_seed(99);
assert(random::rand(0, 1000) == a, "first value must match");
assert(random::rand(0, 1000) == b, "second value must match");
}
fn test_rand_different_seeds() {
random::rand_seed(1);
a = random::rand(0, 1000000);
random::rand_seed(2);
b = random::rand(0, 1000000);
assert(a != b, "different seeds should give different values");
}
fn test_rand_indices_length() {
random::rand_seed(7);
v = random::rand_indices(10);
assert(len(v) == 10, "size {len(v)}");
}
fn test_rand_indices_permutation() {
random::rand_seed(42);
n = 20;
v = random::rand_indices(n);
assert(len(v) == n, "wrong size");
for expected in 0..n {
found = false;
for x in v { if x == expected { found = true } }
assert(found, "missing {expected}");
}
}
fn test_rand_indices_zero() {
v = random::rand_indices(0);
assert(len(v) == 0, "expected empty vector");
}
fn test_rand_indices_different_orderings() {
random::rand_seed(1);
a = random::rand_indices(10);
random::rand_seed(9999);
b = random::rand_indices(10);
diff = false;
for i in 0..10 { if a[i] != b[i] { diff = true } }
assert(diff, "expected different orderings");
}
fn test_rand_indices_reproducible() {
random::rand_seed(123);
a = random::rand_indices(8);
random::rand_seed(123);
b = random::rand_indices(8);
same = true;
for i in 0..8 { if a[i] != b[i] { same = false } }
assert(same, "seeded rand_indices should be reproducible");
}
tests/16-randstream.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// RandStream (the value tier): owned, independent, deterministic random streams.
use random::*;
fn main() {
r = random::seed_stream(7);
a = r.get(1, 1000000);
b = r.get(1, 1000000);
assert(a != b, "the stream advances through the receiver");
}
fn test_stream_in_range() {
r = random::seed_stream(42);
ok = true;
for _ in 0..200 {
v = r.get(10, 20);
if v < 10 || v > 20 { ok = false; }
}
assert(ok, "get stays in [10, 20]");
}
fn test_stream_single_value() {
r = random::seed_stream(1);
assert(r.get(7, 7) == 7, "single-value range returns it");
}
fn test_stream_invalid_range() {
r = random::seed_stream(1);
assert(!r.get(10, 5), "lo > hi is null (the global rand contract)");
}
fn test_stream_reproducible() {
r1 = random::seed_stream(99);
a = r1.get(0, 1000000);
b = r1.get(0, 1000000);
r2 = random::seed_stream(99);
assert(r2.get(0, 1000000) == a, "same seed: first draw matches");
assert(r2.get(0, 1000000) == b, "same seed: second draw matches");
}
fn test_streams_isolated() {
// THE property the global tier cannot give: drawing from one stream never
// perturbs another — interleaved draws equal the plain sequence.
plain = random::seed_stream(5);
p1 = plain.get(0, 1000000);
p2 = plain.get(0, 1000000);
p3 = plain.get(0, 1000000);
inter = random::seed_stream(5);
noise = random::seed_stream(777);
i1 = inter.get(0, 1000000);
noise.get(0, 1000000);
i2 = inter.get(0, 1000000);
noise.indices(20);
i3 = inter.get(0, 1000000);
assert(i1 == p1 && i2 == p2 && i3 == p3, "streams are isolated");
}
fn test_streams_decorrelated() {
a = random::seed_stream(1);
b = random::seed_stream(2);
diff = false;
for _ in 0..8 {
if a.get(0, 1000000) != b.get(0, 1000000) { diff = true; }
}
assert(diff, "different seeds give different streams");
}
fn test_stream_negative_seed() {
a = random::seed_stream(-12345);
v = a.get(0, 100);
assert(v >= 0 && v <= 100, "negative seeds are valid");
b = random::seed_stream(-12345);
assert(b.get(0, 100) == v, "and reproducible");
}
fn test_stream_indices_permutation() {
r = random::seed_stream(42);
n = 20;
v = r.indices(n);
assert(len(v) == n, "wrong size");
for expected in 0..n {
found = false;
for x in v { if x == expected { found = true } }
assert(found, "missing {expected}");
}
}
fn test_stream_indices_zero() {
r = random::seed_stream(1);
assert(len(r.indices(0)) == 0, "n = 0 is empty");
}
fn test_stream_indices_reproducible() {
r1 = random::seed_stream(123);
a = r1.indices(8);
r2 = random::seed_stream(123);
b = r2.indices(8);
same = true;
for i in 0..8 { if a[i] != b[i] { same = false } }
assert(same, "seeded indices reproducible");
}
fn test_stream_vs_global_independent() {
// a stream is untouched by the GLOBAL generator and vice versa
r1 = random::seed_stream(31);
random::rand_seed(1000);
a = r1.get(0, 1000000);
r2 = random::seed_stream(31);
random::rand_seed(2000);
random::rand(0, 10);
assert(r2.get(0, 1000000) == a, "global calls never move a stream");
}
tests/17-abi-width.loft
// ABI-width cells — negative and beyond-i32 values must cross the
// native boundary intact (the old i32 extern truncated them silently).
use random::*;
fn main() {
random::rand_seed(7);
ok = true;
for _ in 0..200 {
r = random::rand(-20, -10) ?? 0;
if r < -20 || r > -10 { ok = false; }
}
assert(ok, "rand stays in [-20, -10]");
wide = random::rand(1099511627776, 1099511627776) ?? 0;
assert(wide == 1099511627776, "rand(2^40, 2^40) returns 2^40 exactly, got {wide}");
lo_wide = random::rand(1099511627776, 1099511627876) ?? 0;
assert(lo_wide >= 1099511627776 && lo_wide <= 1099511627876, "wide range stays in bounds, got {lo_wide}");
mixed = random::rand(-3, 3) ?? -99;
assert(mixed >= -3 && mixed <= 3, "sign-crossing range in bounds, got {mixed}");
println("probe_wide ok");
}
tests/worked-examples.loft
// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for the `random` library (@PLN141). Each `test_*` below is a
// REAL, run-every-CI demonstration of a contract the signature cannot state; the
// function documents itself by CITING the tag with `// Example: @RND-00N`, and
// `check_doc_drift.sh examples` fails if a citation goes dangling (no test carries
// the tag). These are teaching examples, distinct from the per-function assertion
// suites in `15-random.loft` and `16-randstream.loft`.
//
// The library offers the same three operations twice — once on a hidden global
// generator, once on a `RandStream` you own — and the whole question is which one
// a given call site needs. Nothing in the types distinguishes them, and the two
// differ in exactly the way that matters: whether some OTHER call site can change
// what you get.
//
// The assertions below compare streams against each other rather than against
// hard-coded draws. That is the honest oracle here: the contract is "these two
// sequences agree" / "this one moved", never "the third value is 313456", and
// pinning the constants would pin the algorithm the docs call an implementation
// detail.
use random::*;
// A callee that draws: the parameter example below needs a real call boundary,
// not an inline block.
fn wex_draw(r: RandStream) -> integer {
r.get(0, 1000000) ?? 0
}
// @RND-001 — `rand_seed` does NOT make the sequence yours. The global tier is one
// hidden generator shared by every call site in the process, so seeding it fixes
// the sequence the GENERATOR produces, not the sequence YOU receive: one `rand()`
// from anywhere else — a library you called, a logging helper, a second feature
// added next month — consumes a value and shifts everything after it. The symptom
// is the worst kind: a program that is reproducible until it is not, with nothing
// at the call site changed. `seed_stream` answers a stream you own, and no other
// call site can reach it. Reach for the global for casual, throwaway randomness;
// reach for a stream the moment reproducibility is a requirement.
fn test_seeding_the_global_does_not_make_it_yours() {
// Baseline: seed, take two draws, nothing else in between.
random::rand_seed(42);
clean_first = random::rand(0, 1000000) ?? 0;
clean_second = random::rand(0, 1000000) ?? 0;
// Same seed, same two draws — but one foreign call slipped in between.
random::rand_seed(42);
mine_first = random::rand(0, 1000000) ?? 0;
random::rand(0, 5); // somebody else's die roll
mine_second = random::rand(0, 1000000) ?? 0;
assert(mine_first == clean_first, "the first draw still matches — the damage is downstream");
assert(mine_second != clean_second,
"the second does NOT: a foreign draw consumed the value that was going to be mine");
// The same interruption against a stream you own changes nothing.
own = random::seed_stream(42);
own_first = own.get(0, 1000000) ?? 0;
own_second = own.get(0, 1000000) ?? 0;
replay = random::seed_stream(42);
r_first = replay.get(0, 1000000) ?? 0;
random::rand(0, 5); // the same foreign call
random::rand_seed(999); // and a foreign RESEED, for good measure
r_second = replay.get(0, 1000000) ?? 0;
assert(r_first == own_first && r_second == own_second,
"a RandStream is untouched by anything happening on the global one");
}
// @RND-002 — assigning a stream COPIES it; passing one to a function LINKS it.
// The two are opposite, and the type is `RandStream` either way. `r2 = r1` forks:
// both replay the same sequence from that point, so it is how you checkpoint a
// stream before a speculative run — and it is also how you accidentally end up with
// two "different" streams handing out identical numbers. A `RandStream` PARAMETER
// is a store link, so a callee's draw advances the caller's stream: that is what
// makes `r.get(...)` work as a method at all, and it means a helper that draws is
// spending your sequence, not its own.
fn test_assignment_forks_a_stream_but_a_parameter_shares_it() {
r1 = random::seed_stream(9);
r2 = r1; // a COPY, taken before any draw
a1 = r1.get(0, 1000000) ?? 0;
b1 = r2.get(0, 1000000) ?? 0;
a2 = r1.get(0, 1000000) ?? 0;
assert(b1 == a1, "the copy replays from where it was taken — it is a snapshot");
assert(a2 != a1, "while the original has moved on");
b2 = r2.get(0, 1000000) ?? 0;
assert(b2 == a2, "the fork keeps producing the same sequence, independently");
// A parameter is the other way round: the callee draws from YOUR stream.
caller = random::seed_stream(9);
oracle = random::seed_stream(9);
expect_first = oracle.get(0, 1000000) ?? 0;
expect_second = oracle.get(0, 1000000) ?? 0;
got_inside = wex_draw(caller);
got_after = caller.get(0, 1000000) ?? 0;
assert(got_inside == expect_first, "the callee drew the caller's next value");
assert(got_after == expect_second,
"and the caller's stream ADVANCED — a helper that draws spends your sequence");
}
// @RND-003 — a stream's position is state you can count, which is what makes
// replay work in practice. Equal seeds replay exactly, so a recorded seed is a
// recorded run. But every draw moves the stream, including the ones you did not
// write as `get`: `indices(n)` runs a Fisher-Yates shuffle and therefore consumes
// exactly `n - 1` draws. So inserting a shuffle into a sequence does not add
// noise, it shifts everything after it by a known amount — which is why a replay
// that "drifted" is usually a draw someone added, not a broken seed, and why a
// stream used for level generation should not be the one used for combat.
fn test_every_draw_moves_the_stream_including_a_shuffle() {
// Equal seeds replay exactly.
a = random::seed_stream(3);
b = random::seed_stream(3);
for _ in 0..5 {
assert((a.get(0, 1000000) ?? 0) == (b.get(0, 1000000) ?? 0), "equal seeds replay exactly");
}
// indices(5) costs 5 - 1 = 4 draws, no more and no less.
via_shuffle = random::seed_stream(3);
via_shuffle.indices(5);
after_shuffle = via_shuffle.get(0, 1000000) ?? 0;
via_draws = random::seed_stream(3);
for _ in 0..4 { via_draws.get(0, 1000000); }
after_draws = via_draws.get(0, 1000000) ?? 0;
assert(after_shuffle == after_draws, "a shuffle of 5 is exactly four draws' worth of stream");
// And it really is a permutation, not a sample.
perm = random::seed_stream(3).indices(5);
assert(len(perm) == 5, "one entry per index");
seen = 0;
for want in 0..5 {
for got in perm { if got == want { seen += 1; } }
}
assert(seen == 5, "every index 0..4 appears exactly once");
}