lumbda/docs/tickets/0001-portal-rng.md
russell@unturf.com d2866f486d portal-rng: 9-cell cross-impl stream matches independent Python baseline
Closes tickets 0001 (portal-rng) and 0002 (os-entropy-seed).

Ticket 0001 goal 4 called for proof that seeding with k, drawing N,
saving, clearing, resuming in any impl, and drawing M more produces a
full stream matching a single-process Python baseline bit-for-bit.
Prior tests/portal-cross-test.sh exercised producer-consumer agreement
but used a producer-side self-computed baseline; it did not compare
against an independent Python run that never saves or resumes.

tests/portal-rng-cross-test.sh computes a single-process Python baseline
once (seed=42, N+M=10 draws, no portal), then runs all 9 producer x
consumer cells (Python, C, asm each side) and checks that producer's
first N plus consumer's M equals the independent baseline. All 12
assertions pass.

Wired into make test-all. Ticket status updated to resolved on both
0001 and 0002 with dated one-line resolution notes.
2026-04-23 20:19:16 -04:00

5.5 KiB
Raw Permalink Blame History

0001 — Portal preserves RNG state across processes

Status: resolved Reporter: Zoe (via fox) Implementer: blackops Opened: 2026-04-20 Resolved: 2026-04-24 — xoshiro256** shipped in Python, C, asm; portal round-trips RNG state; tests/portal-rng-cross-test.sh proves all 9 producer×consumer cells reproduce the independent single-process Python baseline bit-for-bit. Wired into make test-all.

Problem

Our portal serializes environment bindings and full continuations. It does not capture any random-number-generator state — because no RNG builtin exists in any of our three impls today. A simulation that draws randoms, saves mid-run, and resumes in another process will diverge from a single-process baseline the moment it calls (random).

Zoe flagged this as a research contribution: "does portal tech keep the random seed so we can transfer random entropy between processes when continuing a simulation?" Answer today: no. Answer after this ticket: yes, bit-for-bit identical across Python ↔ C ↔ asm.

Goals

  1. Pick one deterministic, portable PRNG and bind it in all three impls.
  2. Expose a minimal builtin API: seed, draw float, draw bounded int, introspect/set state.
  3. Extend portal-v1 so save/resume round-trips RNG state.
  4. Prove cross-impl reproducibility with a test: seed k, draw N values, save, clear, resume in any impl, draw M more — full stream matches a single-process Python baseline.

Non-goals

  • Statistical quality beyond xoshiro256**'s documented properties.
  • Thread-local RNG (one global stream per process; matches current __thread portal checkpoint discipline in C).
  • Cryptographic strength.
  • Rejection-sampling uniformity for (random-int n) — plain modulo, bias acceptable for n << 2^64.

Algorithm — xoshiro256**

State: four uint64_t words s[0..3].

rotl(x, k) = (x << k) | (x >> (64 - k))

next():
    result = rotl(s[1] * 5, 7) * 9
    t      = s[1] << 17
    s[2]  ^= s[0]
    s[3]  ^= s[1]
    s[1]  ^= s[2]
    s[0]  ^= s[3]
    s[2]  ^= t
    s[3]   = rotl(s[3], 45)
    return result

Seeding uses splitmix64 from a single 64-bit seed:

splitmix64(&z):
    z += 0x9e3779b97f4a7c15
    z  = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9
    z  = (z ^ (z >> 27)) * 0x94d049bb133111eb
    return z ^ (z >> 31)

seed(k):
    z = k
    for i in 0..3: s[i] = splitmix64(&z)

Chosen because:

  • Public domain reference (Blackman & Vigna, 2018).
  • Identical bit output on any machine — no libc, no FPU, no rand().
  • Four u64 words → same wire format across Python, C, asm.
  • Small code footprint — asm port is ~40 instructions.

Builtin API

builtin args returns
(random-seed! k) int k void — reseeds from k via splitmix64
(random) float in [0.0, 1.0) — top 53 bits / 2^53
(random-int n) positive int n int in [0, n) — raw modulo
(random-state) list of 8 ints: (w0_lo w0_hi w1_lo w1_hi w2_lo w2_hi w3_lo w3_hi) each in [0, 2^32)
(random-state! s) list of 8 ints void — restores state from the list

Rationale for the 8-int representation: each 64-bit word split into low and high 32-bit halves. Fits Lumbda's 48-bit C int and 61-bit asm int without widening. Portable over the S-expression portal too.

Default seed at interpreter startup: 0 (all impls). Deterministic from process start means tests don't need to seed explicitly unless they want a specific stream.

Portal format changes

JSON (Python + C) — lumbda-portal-v1

Add optional top-level key "rng" to the existing lumbda-portal-v1 envelope. No magic bump required — absent field means "assume seed 0".

{
  "format": "lumbda-portal-v1",
  "env": {...},
  "continuation": ...,
  "rng": {
    "algo": "xoshiro256**",
    "state": [w0_lo, w0_hi, w1_lo, w1_hi, w2_lo, w2_hi, w3_lo, w3_hi]
  }
}

Asm binary — LUMBDAB2

Bump magic from LUMBDAB1 to LUMBDAB2. Extend PORTAL_HDR_SIZE from 48 to 80 bytes: append 32 bytes of raw RNG state (4 × u64, little-endian, native layout).

Old LUMBDAB1 files rejected during resume. (Portal is not yet externally released, so no migration burden.)

Asm text (GC build) — ;; lumbda-portal v1

Append one comment line after the header:

;; lumbda-portal v1
;; rng xoshiro256** w0_lo w0_hi w1_lo w1_hi w2_lo w2_hi w3_lo w3_hi
...

Parser recognizes the ;; rng prefix and calls random-state! internally.

Test plan

  1. Unit — seed 42, first 5 draws match a hardcoded Python baseline bit-for-bit. Asserted in all three impls.
  2. Functionaltests/functional.lsp gets determinism tests.
  3. Portal round-triptests/portal-rng-save.lsp seeds, draws 50, saves, exits. tests/portal-rng-load.lsp resumes, draws 50 more, compares against full 100-draw baseline.
  4. Cross-impl — wire into tests/portal-cross-test.sh. Python saves, C resumes, next draws match. All 9 producer×consumer cells.
  5. MOAD scan~/git/unmoad.com/unmoad on all changed files.

Deliverables

  • docs/tickets/0001-portal-rng.md (this ticket)
  • Python: lumbda.py — xoshiro256** + 5 builtins + portal hooks
  • C: c/builtins.c + c/portal.c + c/lumbda.h — same
  • Asm: asm/lumbda.s — same, plus LUMBDAB2 header
  • Shared tests: tests/functional.lsp determinism block
  • Cross tests: tests/portal-rng-save.lsp, tests/portal-rng-load.lsp
  • tests/portal-cross-test.sh — new RNG cells
  • Asm test: asm/test.sh determinism checks
  • make test-all green
  • unmoad clean on all changed files