random source

← 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.

ItemDefinedWorked exampleCall sites
rand fn11RND-00111 16 18 20 23 24 … (34 total)
rand_seed fn21RND-0018 22 25 26 27 29 … (30 total)
rand_indices fn28RND-00136 37 41 42 50 53 … (21 total)
RandStream struct55—65 72 79 100 26 65
seed_stream fn65RND-0037 14 24 29 34 37 … (29 total)
get fn79RND-002107 8 9 17 20 25 … (39 total)
indices fn100RND-00354 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");
}