portal-rng: add (random-seed-from-os!) across all three tiers

Ticket 0002 — reads 8 bytes from /dev/urandom (little-endian u64) and
seeds xoshiro256**. Opt-in kernel entropy for stochastic runs; the
default stays deterministic (k=0 at startup), so ticket 0001's
portal-reproducibility contract is unchanged.

Real-world flow now one call away:
  Machine A: (random-seed-from-os!) + run simulation + portal-save
  Machine B: portal-resume — same stream, bit-for-bit

All three impls fail loud on /dev/urandom trouble (LispErr in Python
and C, stderr + exit(1) in asm) — no silent fallback to a weak seed.

Tests:
- tests/functional.lsp: 2 new shared asserts (entropic + replay)
- asm/test.sh: 2 new asm-local checks (149 total, was 147)
- make test-all green across Python (205), C (205), asm (149)

Whitepaper §7.5 gains one sentence noting the OS-seed path.
unmoad: zero new findings in added code.
This commit is contained in:
russell@unturf.com 2026-04-20 15:50:26 -04:00
parent e84c5eede3
commit 4960381c67
13 changed files with 306 additions and 81 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -199,15 +199,16 @@
.equ BI_RANDOMINT, 111
.equ BI_RANDOMSTATE, 112
.equ BI_RANDOMSTATESET, 113
.equ BI_RANDOMSEEDFROMOS, 114
.ifdef GC_NAIVE
.equ BI_GC_COLLECT, 114
.equ BI_GC_STATS, 115
.equ BI_WITH_ARENA, 116
.equ BI_ARENA_STATS, 117
.equ BI_ARENA_SET_MODE, 118
.equ BI_COUNT, 119
.equ BI_GC_COLLECT, 115
.equ BI_GC_STATS, 116
.equ BI_WITH_ARENA, 117
.equ BI_ARENA_STATS, 118
.equ BI_ARENA_SET_MODE, 119
.equ BI_COUNT, 120
.else
.equ BI_COUNT, 114
.equ BI_COUNT, 115
.endif
# ============================================================
@ -309,6 +310,7 @@ bn_randomseed: .byte 12; .ascii "random-seed!"
bn_randomint: .byte 10; .ascii "random-int"
bn_randomstate: .byte 12; .ascii "random-state"
bn_randomstateset: .byte 13; .ascii "random-state!"
bn_randomseedfromos: .byte 20; .ascii "random-seed-from-os!"
bn_integerp: .byte 8; .ascii "integer?"
bn_portalsave: .byte 11; .ascii "portal-save"
bn_portalresume:.byte 13; .ascii "portal-resume"
@ -404,6 +406,7 @@ bi_names:
.quad bn_tcpsendfile
.quad bn_isqrt
.quad bn_randomseed, bn_randomint, bn_randomstate, bn_randomstateset
.quad bn_randomseedfromos
.ifdef GC_NAIVE
.quad bn_gccollect, bn_gcstats, bn_witharena, bn_arenastats, bn_arenamode
.endif
@ -421,6 +424,9 @@ err_rng_bad_n: .ascii "Error: random-int: n must be positive\n"
.equ err_rng_bad_n_len, . - err_rng_bad_n
err_rng_short: .ascii "Error: random-state!: expected list of 8 integers\n"
.equ err_rng_short_len, . - err_rng_short
err_rng_urandom: .ascii "Error: random-seed-from-os!: /dev/urandom unavailable\n"
.equ err_rng_urandom_len, . - err_rng_urandom
s_dev_urandom: .asciz "/dev/urandom"
# Print strings
s_true: .ascii "#t"
@ -3397,6 +3403,8 @@ eval_list:
je bi_random_state
cmpq $BI_RANDOMSTATESET, %rax
je bi_random_state_bang
cmpq $BI_RANDOMSEEDFROMOS, %rax
je bi_random_seed_from_os
cmpq $BI_INTEGERP, %rax
je bi_integerp
cmpq $BI_PORTALSAVE, %rax
@ -4953,6 +4961,53 @@ bi_random_state_bang:
movq $VAL_VOID, %rax
RET_VAL
# bi_random_seed_from_os: (random-seed-from-os!) -> void
# Reads 8 bytes from /dev/urandom, interprets as little-endian u64,
# seeds xoshiro256**. Opt-in entropy for stochastic runs; determinism
# remains the default. See docs/tickets/0002-os-entropy-seed.md.
bi_random_seed_from_os:
# open("/dev/urandom", O_RDONLY, 0)
movq $SYS_OPEN, %rax
leaq s_dev_urandom(%rip), %rdi
movq $O_RDONLY, %rsi
xorq %rdx, %rdx
syscall
testq %rax, %rax
js .brsfo_fail
movq %rax, %rbx # fd
# read 8 bytes into stack slot
subq $8, %rsp
movq $SYS_READ, %rax
movq %rbx, %rdi
movq %rsp, %rsi
movq $8, %rdx
syscall
cmpq $8, %rax
jne .brsfo_short
movq (%rsp), %rdi # seed = little-endian u64
addq $8, %rsp
pushq %rbx # save fd across rng_seed call
call rng_seed
popq %rbx
# close(fd)
movq $SYS_CLOSE, %rax
movq %rbx, %rdi
syscall
movq $VAL_VOID, %rax
RET_VAL
.brsfo_short:
addq $8, %rsp
# fall through to fail with same error (urandom unavailable / short)
.brsfo_fail:
movq $SYS_WRITE, %rax
movq $2, %rdi
leaq err_rng_urandom(%rip), %rsi
movq $err_rng_urandom_len, %rdx
syscall
movq $SYS_EXIT, %rax
movq $1, %rdi
syscall
bi_vector:
# (vector e1 e2 ...) build from remaining args in %r12
# Count args

View file

@ -62,6 +62,8 @@ check "random-int-2" "(begin (random-seed! 42) (random-int 1000000) (random-i
check "random-reseed" "(begin (random-seed! 42) (random-int 1000000) (random-seed! 42) (random-int 1000000))" "558742"
check "random-state-len" "(begin (random-seed! 42) (length (random-state)))" "8"
check "random-state-restore" "(begin (random-seed! 42) (define s (random-state)) (random-int 1000000) (random-state! s) (random-int 1000000))" "558742"
check "random-os-entropic" "(begin (random-seed-from-os!) (define a (random-state)) (random-seed-from-os!) (not (equal? a (random-state))))" "#t"
check "random-os-replay" "(begin (random-seed-from-os!) (define s (random-state)) (define v (random-int 1000000)) (random-state! s) (= v (random-int 1000000)))" "#t"
check "odd?" "(odd? 3)" "#t"
check "odd?-even" "(odd? 4)" "#f"
check "even?" "(even? 4)" "#t"

View file

@ -5,6 +5,7 @@
* Serializes environment bindings and full continuations.
*/
#include "lumbda.h"
#include <fcntl.h>
/* ═══════════════════════════════════════════════════════════════════════════
* Portal checkpoint thread-local signal for mid-execution save
@ -801,10 +802,26 @@ static Value builtin_random_state_bang(Value *args, int nargs, Env *env) {
return VAL_VOID;
}
/* Opt-in kernel entropy seed. See docs/tickets/0002-os-entropy-seed.md. */
static Value builtin_random_seed_from_os(Value *args, int nargs, Env *env) {
(void)args; (void)env;
if (nargs != 0) lisp_error("random-seed-from-os!: expected 0 args");
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) lisp_error("random-seed-from-os!: cannot open /dev/urandom");
uint64_t k = 0;
ssize_t n = read(fd, &k, sizeof k);
close(fd);
if (n != (ssize_t)sizeof k)
lisp_error("random-seed-from-os!: short read from /dev/urandom");
rng_seed(k);
return VAL_VOID;
}
void register_portal_builtins(Env *env) {
env_define(env, intern("portal-checkpoint!"), VAL_BUILTIN(builtin_portal_checkpoint));
env_define(env, intern("portal-save!"), VAL_BUILTIN(builtin_portal_checkpoint));
env_define(env, intern("random-seed!"), VAL_BUILTIN(builtin_random_seed_bang));
env_define(env, intern("random-seed-from-os!"), VAL_BUILTIN(builtin_random_seed_from_os));
env_define(env, intern("random"), VAL_BUILTIN(builtin_random));
env_define(env, intern("random-int"), VAL_BUILTIN(builtin_random_int));
env_define(env, intern("random-state"), VAL_BUILTIN(builtin_random_state));

View file

@ -0,0 +1,110 @@
# 0002 — `(random-seed-from-os!)` pulls kernel entropy
**Status:** open
**Reporter:** fox (response to 0001 follow-up)
**Implementer:** blackops
**Opened:** 2026-04-20
## Problem
Ticket 0001 gave us xoshiro256\*\* with deterministic startup seed `k = 0`
and explicit `(random-seed! k)`. Two processes running the same program
produce the same stream. That is the point — it is what lets a portal
round-trip reproduce state across machines.
But determinism has a dual cost: a program that wants a *different*
stream per run must seed itself from outside. Without a kernel-entropy
hook, users have to hand-roll one by opening `/dev/urandom` and folding
bytes into an integer. That is defect-prone (wrong endianness, short
read not handled, FD left open) and duplicates in every caller.
fox's note: *"a random seed from os on at least one side is very
important"* — because the common real-world flow is:
1. Machine A seeds from kernel entropy (`random-seed-from-os!`).
2. Machine A runs a stochastic simulation for a while.
3. Machine A portal-saves — state captured, including xoshiro256\*\* state.
4. Machine B portal-resumes — continues the *same* stream bit-for-bit.
Step 1 is missing today. This ticket adds it.
## Goals
1. One builtin, all three impls: `(random-seed-from-os!)`.
2. Reads 8 bytes from `/dev/urandom`, interprets as little-endian `u64`,
calls existing `rng_seed`. No new PRNG, just a different seed source.
3. Returns void; user captures `(random-state)` if they want to replay.
4. Fail-loud on I/O error (missing `/dev/urandom`, short read) — we do
not silently fall back to a weak seed.
## Non-goals
- Blocking `/dev/random` (entropy-exhaustion semantics). `urandom` is the
right default; `man 4 urandom` confirms it is cryptographically strong
after boot.
- `getrandom(2)` syscall. Portable to older kernels matters more than
saving one open/read/close.
- Any silent use of OS entropy on startup. The existing `k = 0` default
stays; determinism remains the default, kernel entropy is opt-in.
- `/dev/urandom` emulation on non-Linux. Lumbda targets Linux x86_64;
other platforms earn their own ticket.
## API
```
(random-seed-from-os!) → void
```
Reads exactly 8 bytes from `/dev/urandom`, little-endian, passes the
resulting `u64` to the same `rng_seed` path as `(random-seed!)`. After
return, `(random-state)` reflects the new state, and the portal system
captures it unchanged.
Errors (fail-loud):
- `/dev/urandom` cannot be opened → error + exit(1) in asm; `LispErr`
in Python + C.
- Short read (fewer than 8 bytes) → same treatment.
## Implementation
Python (~3 lines):
```python
def _rng_seed_from_os():
with open('/dev/urandom', 'rb') as f:
b = f.read(8)
if len(b) != 8: raise LispErr('random-seed-from-os!: short read')
_rng_seed(int.from_bytes(b, 'little', signed=False))
```
C (~10 lines): `open` / `read` / `close` on `/dev/urandom`, then call
existing `rng_seed(uint64_t)`.
Asm (~30 lines): `SYS_OPEN`, `SYS_READ`, `SYS_CLOSE` with an 8-byte
stack slot, then `call rng_seed`. New data:
`s_dev_urandom: .asciz "/dev/urandom"`. Error message follows the
existing `err_rng_*` pattern.
## Test plan
1. **Unit**`(random-seed-from-os!)` runs without error in each tier.
2. **Entropic** — two consecutive `(random-seed-from-os!)` calls in
the *same* process produce different streams (with overwhelming
probability). Asserted via `(not (equal? state-a state-b))`.
3. **Determinism preserved**`(random-seed-from-os!)` + capture via
`(random-state)` + reseed with `(random-state! saved)` produces the
original stream. This proves the OS seed plugs cleanly into the
portal path from 0001.
## Deliverables
- [ ] `docs/tickets/0002-os-entropy-seed.md` (this ticket)
- [ ] Python: `lumbda.py` — helper + builtin binding
- [ ] C: `c/portal.c` — helper + builtin + `register_portal_builtins`
- [ ] Asm: `asm/lumbda.s` — new `BI_RANDOMSEEDFROMOS`, dispatch,
`bi_random_seed_from_os`, data strings
- [ ] `tests/functional.lsp` — 2 shared assertions
- [ ] `asm/test.sh` — 2 asm-local checks
- [ ] Whitepaper §7.5 — one sentence noting the OS-seed option
- [ ] `make test-all` green
- [ ] `unmoad` clean on changed files

View file

@ -2026,6 +2026,17 @@ def _rng_state_from_halves(halves):
_rng_state[i] = (hi << 32) | lo
def _rng_seed_from_os():
"""Read 8 bytes from /dev/urandom and seed xoshiro256**. Opt-in entropy
for stochastic runs; determinism remains the default (seed=0 at startup).
See docs/tickets/0002-os-entropy-seed.md."""
with open('/dev/urandom', 'rb') as f:
b = f.read(8)
if len(b) != 8:
raise LispErr('random-seed-from-os!: short read from /dev/urandom')
_rng_seed(int.from_bytes(b, 'little', signed=False))
# Default seed = 0 at module load so (random) without (random-seed!) is
# deterministic and non-zero. All three impls agree on this startup state.
_rng_seed(0)
@ -3550,6 +3561,7 @@ def make_global_env():
# ── Random (xoshiro256**) — portal-serialized across all three impls ────
d(S('random-seed!'), lambda a, _: _rng_seed(int(_num(a[0]))) or VOID)
d(S('random-seed-from-os!'), lambda a, _: _rng_seed_from_os() or VOID)
d(S('random'), lambda a, _: _rng_random_float())
d(S('random-int'), lambda a, _: _rng_random_int(int(_num(a[0]))))
d(S('random-state'), lambda a, _: _P(_rng_state_to_halves()))

View file

@ -82,6 +82,20 @@
(random-state! saved-state)
(assert-equal "random-state-restore" (random-int 1000000) 558742)
;; random-seed-from-os! pulls kernel entropy; two calls differ w/ overwhelming
;; probability. Ticket: docs/tickets/0002-os-entropy-seed.md.
(random-seed-from-os!)
(define os-state-a (random-state))
(random-seed-from-os!)
(define os-state-b (random-state))
(assert-true "random-seed-from-os-entropic" (not (equal? os-state-a os-state-b)))
;; Capture via (random-state) still works after OS seed — ties 0002 to 0001.
(random-seed-from-os!)
(define os-snapshot (random-state))
(define os-next1 (random-int 1000000))
(random-state! os-snapshot)
(assert-equal "random-seed-from-os-replay" (random-int 1000000) os-next1)
;;; ═══════════════════════════════════════════════════════════════
;;; Comparison
;;; ═══════════════════════════════════════════════════════════════

View file

@ -1833,6 +1833,7 @@ bi_portal_resume:
<li><p><strong>S-expression portal:</strong> one line — <span class="docutils literal"><span class="pre">(random-state!</span> '(w0_lo w0_hi …))</span> — which every tier already evaluates. No format change. The language really is the wire format.</p></li>
</ul>
<p>Builtins shared across all three: <span class="docutils literal"><span class="pre">(random-seed!</span> k)</span>, <span class="docutils literal"><span class="pre">(random-int</span> n)</span>, <span class="docutils literal"><span class="pre">(random-state)</span></span>, <span class="docutils literal"><span class="pre">(random-state!</span> lst)</span>. Python and C also export <span class="docutils literal">(random)</span> returning a float in <span class="docutils literal">[0, 1)</span>; the asm tier has no floats, so this one builtin is omitted by construction. All three tiers seed with <span class="docutils literal">k = 0</span> at process startup, so a program that never calls <span class="docutils literal"><span class="pre">random-seed!</span></span> still gets a non-zero, deterministic stream.</p>
<p>A fifth shared builtin — <span class="docutils literal"><span class="pre">(random-seed-from-os!)</span></span> — reads 8 bytes from <span class="docutils literal">/dev/urandom</span> and seeds xoshiro256** with them, so the typical real-world flow <em>(Machine A seeds from kernel entropy → runs simulation → portal-saves → Machine B resumes the same stream bit-for-bit)</em> is one call away. Determinism stays the default; kernel entropy is opt-in and fails loud if <span class="docutils literal">/dev/urandom</span> is unavailable. Ticket: <span class="docutils literal"><span class="pre">docs/tickets/0002-os-entropy-seed.md</span></span>.</p>
<p>Verification: <span class="docutils literal"><span class="pre">tests/portal-cross-test.sh</span></span> runs a 3×3 producer × consumer matrix where each tier writes a portal file capturing the RNG state, every other tier reads it, draws the next 5 values, and the values must match bit-for-bit against a baseline the producer also wrote into the file. All 9 cells green. The baseline for seed=42 is <span class="docutils literal">558742 543102 559009 124193 317476 …</span> — identical in Python, C, and asm, identical after any portal round-trip through any format.</p>
<p><strong>Ticket:</strong> <span class="docutils literal"><span class="pre">docs/tickets/0001-portal-rng.md</span></span>. Contributed by <strong>Zoë Trout</strong>, whose question — <em>does the portal tech keep the random list seed which will allow transferring random entropy between processes when continuing a simulation?</em> — surfaced a gap we had not noticed. The earlier claim <em>portal = portable machine state</em> carried a silent asterisk: <em>deterministic code only</em>. That asterisk is now gone.</p>
</section>

View file

@ -622,7 +622,7 @@ endobj
endobj
73 0 obj
<<
/Author () /CreationDate (D:20260420112032-04'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260420112032-04'00') /Producer (ReportLab PDF Library - \(opensource\))
/Author () /CreationDate (D:20260420155007-04'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260420155007-04'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (lumbda.) /Trapped /False
>>
endobj
@ -805,12 +805,12 @@ endobj
endobj
108 0 obj
<<
/Dest [ 42 0 R /XYZ 57.02362 765.0236 0 ] /Next 109 0 R /Parent 101 0 R /Prev 107 0 R /Title (7.6 Cross-Process Benchmarks)
/Dest [ 42 0 R /XYZ 57.02362 723.0236 0 ] /Next 109 0 R /Parent 101 0 R /Prev 107 0 R /Title (7.6 Cross-Process Benchmarks)
>>
endobj
109 0 obj
<<
/Dest [ 42 0 R /XYZ 57.02362 443.8236 0 ] /Next 110 0 R /Parent 101 0 R /Prev 108 0 R /Title (7.7 Mismatch Cases: Graceful Degradation)
/Dest [ 42 0 R /XYZ 57.02362 401.8236 0 ] /Next 110 0 R /Parent 101 0 R /Prev 108 0 R /Title (7.7 Mismatch Cases: Graceful Degradation)
>>
endobj
110 0 obj
@ -7147,7 +7147,7 @@ endstream
endobj
156 0 obj
<<
/Length 8366
/Length 8989
>>
stream
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
@ -7278,13 +7278,19 @@ Q
q
1 0 0 1 57.02362 179.8236 cm
q
BT 1 0 0 1 0 50 Tm 1.829104 Tw 12 TL /F1 10 Tf 0 0 0 rg (A fifth shared builtin \227 ) Tj /F5 10 Tf (\(random-seed-from-os!\)) Tj /F1 10 Tf ( \227 reads 8 bytes from ) Tj /F5 10 Tf (/dev/urandom) Tj /F1 10 Tf ( and seeds) Tj T* 0 Tw 2.747223 Tw (xoshiro256** with them, so the typical real-world flow ) Tj /F4 10 Tf (\(Machine A seeds from kernel entropy ) Tj /F6 10 Tf 12 TL (\256) Tj /F4 10 Tf 12 TL ( runs) Tj T* 0 Tw .313223 Tw (simulation ) Tj /F6 10 Tf 12 TL (\256) Tj /F4 10 Tf 12 TL ( portal-saves ) Tj /F6 10 Tf 12 TL (\256) Tj /F4 10 Tf 12 TL ( Machine B resumes the same stream bit-for-bit\)) Tj /F1 10 Tf ( is one call away. Determinism) Tj T* 0 Tw 4.219168 Tw (stays the default; kernel entropy is opt-in and fails loud if ) Tj /F5 10 Tf (/dev/urandom) Tj /F1 10 Tf ( is unavailable. Ticket:) Tj T* 0 Tw /F5 10 Tf (docs/tickets/0002-os-entropy-seed.md) Tj /F1 10 Tf (.) Tj T* ET
Q
Q
q
1 0 0 1 57.02362 113.8236 cm
q
BT 1 0 0 1 0 50 Tm 1.928941 Tw 12 TL /F1 10 Tf 0 0 0 rg (Verification: ) Tj /F5 10 Tf (tests/portal-cross-test.sh) Tj /F1 10 Tf ( runs a 3\3273 producer \327 consumer matrix where each tier) Tj T* 0 Tw .217417 Tw (writes a portal file capturing the RNG state, every other tier reads it, draws the next 5 values, and the values) Tj T* 0 Tw .365464 Tw (must match bit-for-bit against a baseline the producer also wrote into the file. All 9 cells green. The baseline) Tj T* 0 Tw .232397 Tw (for seed=42 is ) Tj /F5 10 Tf (558742) Tj ( ) Tj (543102) Tj ( ) Tj (559009) Tj ( ) Tj (124193) Tj ( ) Tj (317476) Tj ( ) Tj (\205) Tj /F1 10 Tf ( \227 identical in Python, C, and asm, identical) Tj T* 0 Tw (after any portal round-trip through any format.) Tj T* ET
Q
Q
q
1 0 0 1 57.02362 125.8236 cm
1 0 0 1 57.02362 83.82362 cm
q
BT 1 0 0 1 0 38 Tm 1.177835 Tw 12 TL /F3 10 Tf 0 0 0 rg (Ticket:) Tj /F1 10 Tf ( ) Tj /F5 10 Tf (docs/tickets/0001-portal-rng.md) Tj /F1 10 Tf (. Contributed by ) Tj /F3 10 Tf (Zo\353 Trout) Tj /F1 10 Tf (, whose question \227 ) Tj /F4 10 Tf (does the) Tj T* 0 Tw .328556 Tw (portal tech keep the random list seed which will allow transferring random entropy between processes when) Tj T* 0 Tw .598726 Tw (continuing a simulation?) Tj /F1 10 Tf ( \227 surfaced a gap we had not noticed. The earlier claim ) Tj /F4 10 Tf (portal = portable machine) Tj T* 0 Tw (state) Tj /F1 10 Tf ( carried a silent asterisk: ) Tj /F4 10 Tf (deterministic code only) Tj /F1 10 Tf (. That asterisk is now gone.) Tj T* ET
BT 1 0 0 1 0 14 Tm 1.177835 Tw 12 TL /F3 10 Tf 0 0 0 rg (Ticket:) Tj /F1 10 Tf ( ) Tj /F5 10 Tf (docs/tickets/0001-portal-rng.md) Tj /F1 10 Tf (. Contributed by ) Tj /F3 10 Tf (Zo\353 Trout) Tj /F1 10 Tf (, whose question \227 ) Tj /F4 10 Tf (does the) Tj T* 0 Tw .328556 Tw (portal tech keep the random list seed which will allow transferring random entropy between processes when) Tj T* 0 Tw ET
Q
Q
@ -7292,27 +7298,33 @@ endstream
endobj
157 0 obj
<<
/Length 9926
/Length 10316
>>
stream
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 57.02362 751.8236 cm
1 0 0 1 57.02362 741.0236 cm
q
BT 1 0 0 1 0 14 Tm .598726 Tw 12 TL /F4 10 Tf 0 0 0 rg (continuing a simulation?) Tj /F1 10 Tf ( \227 surfaced a gap we had not noticed. The earlier claim ) Tj /F4 10 Tf (portal = portable machine) Tj T* 0 Tw (state) Tj /F1 10 Tf ( carried a silent asterisk: ) Tj /F4 10 Tf (deterministic code only) Tj /F1 10 Tf (. That asterisk is now gone.) Tj T* ET
Q
Q
q
1 0 0 1 57.02362 709.8236 cm
q
BT 1 0 0 1 0 2.2 Tm 13.2 TL /F2 11 Tf .133333 .133333 .133333 rg (7.6 Cross-Process Benchmarks) Tj T* ET
Q
Q
q
1 0 0 1 57.02362 707.8236 cm
1 0 0 1 57.02362 665.8236 cm
q
BT 1 0 0 1 0 26 Tm 2.703647 Tw 12 TL /F1 10 Tf 0 0 0 rg (Producer process A saves state to a file; consumer process B starts fresh, loads the file, continues.) Tj T* 0 Tw 10.67759 Tw (Wall-clock time for both processes end-to-end, 50 iterations, same-laptop. ) Tj /F3 10 Tf (Reproduce:) Tj /F1 10 Tf T* 0 Tw /F5 10 Tf (make) Tj ( ) Tj (bench-portal) Tj /F1 10 Tf ( \(source: ) Tj /F5 10 Tf (tests/portal-benchmark.sh) Tj /F1 10 Tf (\).) Tj T* ET
Q
Q
q
1 0 0 1 57.02362 701.8236 cm
1 0 0 1 57.02362 659.8236 cm
Q
q
1 0 0 1 57.02362 503.8236 cm
1 0 0 1 57.02362 461.8236 cm
q
1 1 1 rg
n 0 198 481.2283 -18 re f*
@ -7518,32 +7530,32 @@ Q
Q
Q
q
1 0 0 1 57.02362 503.8236 cm
1 0 0 1 57.02362 461.8236 cm
Q
q
1 0 0 1 57.02362 461.8236 cm
1 0 0 1 57.02362 419.8236 cm
q
BT 1 0 0 1 0 26 Tm .698334 Tw 12 TL /F1 10 Tf 0 0 0 rg (The asm) Tj /F6 10 Tf 12 TL (\256) Tj /F1 10 Tf 12 TL (asm cross-process is ~160\327 faster than Python) Tj /F6 10 Tf 12 TL (\256) Tj /F1 10 Tf 12 TL (Python. The binary & S-expression portals are) Tj T* 0 Tw .812256 Tw (within 10% of each other on this workload \227 the bottleneck is process startup, not serialization. For larger) Tj T* 0 Tw (heaps the binary format pulls further ahead; for portability, S-expression always wins.) Tj T* ET
Q
Q
q
1 0 0 1 57.02362 430.6236 cm
1 0 0 1 57.02362 388.6236 cm
q
BT 1 0 0 1 0 2.2 Tm 13.2 TL /F2 11 Tf .133333 .133333 .133333 rg (7.7 Mismatch Cases: Graceful Degradation) Tj T* ET
Q
Q
q
1 0 0 1 57.02362 410.6236 cm
1 0 0 1 57.02362 368.6236 cm
q
0 0 0 rg
BT 1 0 0 1 0 2 Tm /F1 10 Tf 12 TL (A usable persistence layer fails well. What happens when the consumer meets unexpected input?) Tj T* ET
Q
Q
q
1 0 0 1 57.02362 404.6236 cm
1 0 0 1 57.02362 362.6236 cm
Q
q
1 0 0 1 57.02362 254.6236 cm
1 0 0 1 57.02362 212.6236 cm
q
1 1 1 rg
n 0 150 481.2283 -18 re f*
@ -7776,10 +7788,10 @@ Q
Q
Q
q
1 0 0 1 57.02362 254.6236 cm
1 0 0 1 57.02362 212.6236 cm
Q
q
1 0 0 1 57.02362 188.6236 cm
1 0 0 1 57.02362 146.6236 cm
q
BT 1 0 0 1 0 50 Tm 1.476556 Tw 12 TL /F1 10 Tf 0 0 0 rg (All defects surfaced & fixed during benchmark development: an asm segfault on ) Tj /F5 10 Tf (\(define) Tj ( ) Tj (x\)) Tj /F1 10 Tf ( without a) Tj T* 0 Tw 1.838453 Tw (value \(now binds to ) Tj /F5 10 Tf (VOID) Tj /F1 10 Tf (\), an asm portal-resume that accepted short headers \(now verifies ) Tj /F5 10 Tf (sys_read) Tj /F1 10 Tf T* 0 Tw 1.204272 Tw (returned a full 48 bytes & sanity-checks heap metadata\), a Python ) Tj /F5 10 Tf (file) Tj ( ) Tj (not) Tj ( ) Tj (found) Tj /F1 10 Tf ( error reporting the) Tj T* 0 Tw 1.304862 Tw (outer script path instead of the inner missing file \(now uses ) Tj /F5 10 Tf (FileNotFoundError.filename) Tj /F1 10 Tf (\). Graceful) Tj T* 0 Tw (degradation is not free; it is tested.) Tj T* ET
Q
@ -12724,63 +12736,63 @@ xref
0002525313 00000 n
0002536448 00000 n
0002543919 00000 n
0002552338 00000 n
0002562317 00000 n
0002567970 00000 n
0002578510 00000 n
0002588031 00000 n
0002597336 00000 n
0002599378 00000 n
0002612868 00000 n
0002628356 00000 n
0002638138 00000 n
0002647160 00000 n
0002654073 00000 n
0002663708 00000 n
0002671841 00000 n
0002679929 00000 n
0002687227 00000 n
0002689430 00000 n
0002691714 00000 n
0002692140 00000 n
0002692175 00000 n
0002692210 00000 n
0002692245 00000 n
0002692280 00000 n
0002692315 00000 n
0002692350 00000 n
0002692385 00000 n
0002692420 00000 n
0002692455 00000 n
0002692491 00000 n
0002692527 00000 n
0002692563 00000 n
0002692599 00000 n
0002692635 00000 n
0002692671 00000 n
0002692707 00000 n
0002692743 00000 n
0002692779 00000 n
0002692815 00000 n
0002692851 00000 n
0002692887 00000 n
0002692923 00000 n
0002692959 00000 n
0002692995 00000 n
0002693031 00000 n
0002693067 00000 n
0002693103 00000 n
0002693139 00000 n
0002693175 00000 n
0002693211 00000 n
0002693247 00000 n
0002693283 00000 n
0002693319 00000 n
0002693355 00000 n
0002552961 00000 n
0002563331 00000 n
0002568984 00000 n
0002579524 00000 n
0002589045 00000 n
0002598350 00000 n
0002600392 00000 n
0002613882 00000 n
0002629370 00000 n
0002639152 00000 n
0002648174 00000 n
0002655087 00000 n
0002664722 00000 n
0002672855 00000 n
0002680943 00000 n
0002688241 00000 n
0002690444 00000 n
0002692728 00000 n
0002693154 00000 n
0002693189 00000 n
0002693224 00000 n
0002693259 00000 n
0002693294 00000 n
0002693329 00000 n
0002693364 00000 n
0002693399 00000 n
0002693434 00000 n
0002693469 00000 n
0002693505 00000 n
0002693541 00000 n
0002693577 00000 n
0002693613 00000 n
0002693649 00000 n
0002693685 00000 n
0002693721 00000 n
0002693757 00000 n
0002693793 00000 n
0002693829 00000 n
0002693865 00000 n
0002693901 00000 n
0002693937 00000 n
0002693973 00000 n
0002694009 00000 n
0002694045 00000 n
0002694081 00000 n
0002694117 00000 n
0002694153 00000 n
0002694189 00000 n
0002694225 00000 n
0002694261 00000 n
0002694297 00000 n
0002694333 00000 n
0002694369 00000 n
trailer
<<
/ID
[<0317565ca3c256fb6011c65bb1ae607c><0317565ca3c256fb6011c65bb1ae607c>]
[<fcb62d2a2e02cd4b2f0741a9f988a5e3><fcb62d2a2e02cd4b2f0741a9f988a5e3>]
% ReportLab generated PDF document -- digest (opensource)
/Info 73 0 R
@ -12788,5 +12800,5 @@ trailer
/Size 210
>>
startxref
2693391
2694405
%%EOF

View file

@ -805,6 +805,8 @@ Lumbda addresses this across all three tiers with one shared PRNG: **xoshiro256\
Builtins shared across all three: ``(random-seed! k)``, ``(random-int n)``, ``(random-state)``, ``(random-state! lst)``. Python and C also export ``(random)`` returning a float in ``[0, 1)``; the asm tier has no floats, so this one builtin is omitted by construction. All three tiers seed with ``k = 0`` at process startup, so a program that never calls ``random-seed!`` still gets a non-zero, deterministic stream.
A fifth shared builtin — ``(random-seed-from-os!)`` — reads 8 bytes from ``/dev/urandom`` and seeds xoshiro256\*\* with them, so the typical real-world flow *(Machine A seeds from kernel entropy → runs simulation → portal-saves → Machine B resumes the same stream bit-for-bit)* is one call away. Determinism stays the default; kernel entropy is opt-in and fails loud if ``/dev/urandom`` is unavailable. Ticket: ``docs/tickets/0002-os-entropy-seed.md``.
Verification: ``tests/portal-cross-test.sh`` runs a 3×3 producer × consumer matrix where each tier writes a portal file capturing the RNG state, every other tier reads it, draws the next 5 values, and the values must match bit-for-bit against a baseline the producer also wrote into the file. All 9 cells green. The baseline for seed=42 is ``558742 543102 559009 124193 317476 …`` — identical in Python, C, and asm, identical after any portal round-trip through any format.
**Ticket:** ``docs/tickets/0001-portal-rng.md``. Contributed by **Zoë Trout**, whose question — *does the portal tech keep the random list seed which will allow transferring random entropy between processes when continuing a simulation?* — surfaced a gap we had not noticed. The earlier claim *portal = portable machine state* carried a silent asterisk: *deterministic code only*. That asterisk is now gone.