lumbda/tests/portal-rng-cross-test.sh
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

146 lines
5.1 KiB
Bash
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# portal-rng-cross-test.sh — ticket 0001 deliverable (goal 4):
#
# "seed k, draw N values, save, clear, resume in any impl, draw M more —
# full stream matches a single-process Python baseline."
#
# This script proves that every (producer, consumer) pair across
# Python / C / asm reproduces the exact same N+M stream that a single
# Python process produces with no save/resume at all. That is the
# stronger claim than portal-cross-test.sh's producer-side self-
# consistency: here the truth is computed independently, once, upfront,
# by a Python run that never touches the portal. Every cell compares
# against the same independent baseline.
#
# Matrix: 3 producers × 3 consumers = 9 cells. K=42, N=5, M=5.
set -e
cd "$(dirname "$0")/.."
PY="python3 lumbda.py --fast"
C="./c/lumbda"
ASM="./asm/lumbda"
K=42 # seed
N=5 # draws before save
M=5 # draws after resume
RANGE=1000000 # random-int upper bound
# ─── Compute the independent single-process Python baseline (N+M draws) ───
BASELINE_LSP=$(mktemp --suffix=.lsp)
trap 'rm -f "$BASELINE_LSP" /tmp/rng-xstream.sexp' EXIT INT TERM
cat > "$BASELINE_LSP" <<EOF
(random-seed! $K)
;; for-each returns a non-printing value across Python/C/asm, so no
;; trailing token leaks into stdout. Iteration count is carried by the
;; length of the dummy list below (shell-expanded once).
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
'($(seq 1 $((N + M)) | tr '\n' ' ')))
EOF
# shellcheck disable=SC2086
BASELINE=$($PY "$BASELINE_LSP")
BASELINE_COUNT=$(printf "%s\n" "$BASELINE" | wc -l)
if [ "$BASELINE_COUNT" -ne $((N + M)) ]; then
echo "FAIL: baseline produced $BASELINE_COUNT draws, expected $((N + M))"
exit 1
fi
# Split baseline into first-N and next-M.
BASELINE_N=$(printf "%s\n" "$BASELINE" | head -n $N)
BASELINE_M=$(printf "%s\n" "$BASELINE" | tail -n $M)
echo "═══════════════════════════════════════════════════════"
echo "RNG portal cross-impl round-trip — independent Python baseline"
echo "Ticket: docs/tickets/0001-portal-rng.md (goal 4)"
echo " seed=$K, save after N=$N draws, resume for M=$M draws"
echo "═══════════════════════════════════════════════════════"
echo
echo "single-process Python baseline (N+M=$((N + M)) draws):"
printf "%s\n" "$BASELINE" | sed 's/^/ /'
echo
# ─── Producer script (generated): seed, draw N, emit them to stdout, save state ───
PROD_LSP=$(mktemp --suffix=.lsp)
cat > "$PROD_LSP" <<EOF
(random-seed! $K)
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
'($(seq 1 $N | tr '\n' ' ')))
(define snapshot (random-state))
(define port (open-output-file "/tmp/rng-xstream.sexp"))
(display "(random-state! '" port) (write snapshot port) (display ")\n" port)
(close-port port)
EOF
# ─── Consumer script (generated): load state, draw M, emit them to stdout ───
CONS_LSP=$(mktemp --suffix=.lsp)
cat > "$CONS_LSP" <<EOF
(load "/tmp/rng-xstream.sexp")
(for-each (lambda (_) (display (random-int $RANGE)) (newline))
'($(seq 1 $M | tr '\n' ' ')))
EOF
# shellcheck disable=SC2064
trap "rm -f '$BASELINE_LSP' '$PROD_LSP' '$CONS_LSP' /tmp/rng-xstream.sexp" EXIT INT TERM
PASS=0
FAIL=0
run_producer() {
local impl="$1"
case "$impl" in
Python) $PY "$PROD_LSP" ;;
C) $C "$PROD_LSP" ;;
Asm) $ASM < "$PROD_LSP" ;;
esac
}
run_consumer() {
local impl="$1"
case "$impl" in
Python) $PY "$CONS_LSP" ;;
C) $C "$CONS_LSP" ;;
Asm) $ASM < "$CONS_LSP" ;;
esac
}
verify_stream() {
local label="$1" expected="$2" actual="$3"
if [ "$expected" = "$actual" ]; then
PASS=$((PASS + 1))
printf " %-22s OK\n" "$label"
else
FAIL=$((FAIL + 1))
printf " %-22s FAIL\n" "$label"
echo " expected:"
printf "%s\n" "$expected" | sed 's/^/ /'
echo " actual:"
printf "%s\n" "$actual" | sed 's/^/ /'
fi
}
for P_NAME in Python C Asm; do
echo "── producer: $P_NAME ──"
rm -f /tmp/rng-xstream.sexp
PROD_OUT=$(run_producer "$P_NAME")
# Producer's first N must equal the Python baseline's first N.
verify_stream "$P_NAME: first $N draws" "$BASELINE_N" "$PROD_OUT"
for C_NAME in Python C Asm; do
CONS_OUT=$(run_consumer "$C_NAME")
# Consumer's M draws after resume must equal the Python baseline's last M.
verify_stream "$P_NAME -> $C_NAME (next $M)" "$BASELINE_M" "$CONS_OUT"
done
done
echo
echo "═══════════════════════════════════════════════════════"
echo "Cross-impl RNG round-trip vs Python baseline: $PASS passed, $FAIL failed"
echo "═══════════════════════════════════════════════════════"
if [ $FAIL -ne 0 ]; then
exit 1
fi