wat iter-4: embedded Lisp prelude + append-only race output

WAT prelude (evaluated after primitive binding at init) adds:
  map, filter, fold-left, fold-right, for-each, any, every,
  count, find, sort (quicksort), vector-map, vector-for-each,
  vector-fill!, string-split, string-trim, string->list,
  random-state, assert-equal/true/false.

Higher-order ops are now Lisp-defined, not primitive bloat. Eval-time
parse + bind happens once per WASM instance startup.

Playground output: per fox, single append-only column instead of
3-up grid. Tiers still race in parallel workers; whichever finishes
first appears first in the output. Live ms counters move to the status
bar (python 312ms · c 47ms · asm 89ms).
This commit is contained in:
russell@unturf.com 2026-06-14 12:46:06 -04:00
parent 3800271b1b
commit 1c9d74b407
No known key found for this signature in database
6 changed files with 125 additions and 95 deletions

View file

@ -128,43 +128,24 @@ function cancelCurrentRun() {
}
}
// ─── Per-tier output block with live ms counter ────────────────────
function makeLiveBlock(tierName) {
// Append a finished tier-block to the output. Order = finish order, so
// the fastest tier appears first naturally.
function appendBlock(tierName, elapsedMs, text, kind) {
const block = document.createElement("div");
block.className = "tier-block";
const h = document.createElement("h3");
h.textContent = TIERS[tierName] || tierName;
const t = document.createElement("span");
t.className = "time";
t.textContent = " loading…";
if (kind === "cancelled") t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`;
else t.textContent = ` (${elapsedMs.toFixed(0)} ms)`;
h.appendChild(t);
block.appendChild(h);
const pre = document.createElement("pre");
pre.textContent = "";
if (kind === "error") pre.className = "err";
pre.textContent = text;
block.appendChild(pre);
outputEl.appendChild(block);
let raf = 0;
let start = 0;
function tickerLoop() {
const ms = (performance.now() - start) | 0;
t.textContent = ` ${ms} ms…`;
raf = requestAnimationFrame(tickerLoop);
}
return {
startTimer() { start = performance.now(); tickerLoop(); return start; },
stop(elapsedMs) {
if (raf) cancelAnimationFrame(raf);
t.textContent = ` (${elapsedMs.toFixed(0)} ms)`;
},
cancelled(elapsedMs) {
if (raf) cancelAnimationFrame(raf);
t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`;
},
setOutput(text) { pre.textContent = text; },
setError(msg) { pre.className = "err"; pre.textContent = msg; },
};
}
// ─── Run / cancel ──────────────────────────────────────────────────
@ -180,43 +161,57 @@ async function runAll() {
outputEl.innerHTML = "";
const tiers = selectedTiers();
const src = getEditorText();
// Each tier runs in its own worker — Promise.all so they race.
// Each tier in its own worker. Append-only output: as each tier finishes,
// we append its block — so the fastest tier shows up first.
const startTimes = {};
const tickStatus = () => {
const parts = [];
for (const t of tiers) {
if (startTimes[t] !== undefined) {
parts.push(`${t} ${((performance.now() - startTimes[t]) | 0)}ms`);
}
}
setStatus(parts.join(" · "), "busy");
};
let raf = requestAnimationFrame(function loop() {
tickStatus();
raf = requestAnimationFrame(loop);
});
const tierPromises = tiers.map((t) => {
const live = makeLiveBlock(t);
const startMark = live.startTimer();
startTimes[t] = performance.now();
return runOnTierInWorker(t, src, (loadingTier) => {
// Status reflects whichever tier most recently announced a load.
setStatus(`loading ${loadingTier}`, "busy");
})
.then((output) => {
const elapsed = performance.now() - startMark;
live.stop(elapsed);
live.setOutput(output);
const elapsed = performance.now() - startTimes[t];
delete startTimes[t];
appendBlock(t, elapsed, output, "ok");
return { tier: t, ok: true, elapsed };
})
.catch((e) => {
const elapsed = performance.now() - startMark;
const elapsed = performance.now() - startTimes[t];
delete startTimes[t];
if (e.message === "cancelled") {
live.cancelled(elapsed);
appendBlock(t, elapsed, "(cancelled)", "cancelled");
return { tier: t, cancelled: true, elapsed };
}
live.stop(elapsed);
live.setError(e.message || String(e));
appendBlock(t, elapsed, e.message || String(e), "error");
return { tier: t, error: e.message, elapsed };
});
});
try {
const results = await Promise.all(tierPromises);
cancelAnimationFrame(raf);
const cancelled = results.some((r) => r.cancelled);
const anyErr = results.some((r) => r.error);
if (cancelled) setStatus("cancelled", "warn");
else if (anyErr) setStatus("completed with errors", "err");
else {
// Winner of the race for ok styling.
const fastest = results.reduce((a, b) => (a.elapsed < b.elapsed ? a : b));
setStatus(`ok — ${fastest.tier} won in ${fastest.elapsed.toFixed(0)} ms`, "ok");
}
} catch (e) {
cancelAnimationFrame(raf);
setStatus(`fatal: ${e.message}`, "err");
} finally {
inFlight = false;

View file

@ -233,16 +233,9 @@ header code {
padding: 0.6rem 0.8rem;
font-size: 13px;
line-height: 1.35;
display: grid;
gap: 0.6rem;
align-content: start;
}
/* When 3 tier blocks render (All three mode), lay them out as 3 columns
* for race-style comparison. With fewer, they stack as a single column. */
#output:has(.tier-block:nth-child(3)) {
grid-template-columns: 1fr 1fr 1fr;
}
#output .tier-block { margin-bottom: 0; }
/* Append-only: results arrive in finish order — fastest tier shows up first. */
#output .tier-block { margin-bottom: 1rem; }
#output .tier-block:last-child { margin-bottom: 0; }
#output .tier-block h3 {
margin: 0 0 0.25rem;

View file

@ -68,6 +68,11 @@
(global $sym_case (mut i32) (i32.const 0))
(global $sym_do (mut i32) (i32.const 0))
;; Prelude — Lisp source evaluated after primitive binding to define
;; higher-order functions on top of the C-level primitives.
(global $prelude_ptr i32 (i32.const 0x50000))
(global $prelude_len (mut i32) (i32.const 0))
;; Constants (immediate value addresses)
(global $NIL i32 (i32.const 4))
(global $TRUE i32 (i32.const 8))
@ -2761,6 +2766,30 @@
(data (i32.const 0xE590) "hash-table-values")
(data (i32.const 0xE5A4) "hash-table->alist")
;; ── Embedded Lisp prelude. Evaluated at end of init. ─────────────
(data (i32.const 0x50000)
"(define (map f xs) (if (null? xs) (quote ()) (cons (f (car xs)) (map f (cdr xs)))))\n"
"(define (filter p xs) (cond ((null? xs) (quote ())) ((p (car xs)) (cons (car xs) (filter p (cdr xs)))) (else (filter p (cdr xs)))))\n"
"(define (fold-left f z xs) (if (null? xs) z (fold-left f (f z (car xs)) (cdr xs))))\n"
"(define (fold-right f z xs) (if (null? xs) z (f (car xs) (fold-right f z (cdr xs)))))\n"
"(define (for-each f xs) (cond ((null? xs) (void)) (else (f (car xs)) (for-each f (cdr xs)))))\n"
"(define (any p xs) (cond ((null? xs) #f) ((p (car xs)) #t) (else (any p (cdr xs)))))\n"
"(define (every p xs) (cond ((null? xs) #t) ((p (car xs)) (every p (cdr xs))) (else #f)))\n"
"(define (count p xs) (fold-left (lambda (acc x) (if (p x) (+ acc 1) acc)) 0 xs))\n"
"(define (find p xs) (cond ((null? xs) #f) ((p (car xs)) (car xs)) (else (find p (cdr xs)))))\n"
"(define (string-split s sep) (let loop ((i 0) (start 0) (acc (quote ()))) (cond ((>= i (string-length s)) (reverse (cons (substring s start i) acc))) ((char=? (string-ref s i) sep) (loop (+ i 1) (+ i 1) (cons (substring s start i) acc))) (else (loop (+ i 1) start acc)))))\n"
"(define (string-trim s) (let* ((len (string-length s)) (start (let loop ((i 0)) (cond ((>= i len) i) ((char-whitespace? (string-ref s i)) (loop (+ i 1))) (else i)))) (end (let loop ((i (- len 1))) (cond ((< i start) start) ((char-whitespace? (string-ref s i)) (loop (- i 1))) (else (+ i 1)))))) (substring s start end)))\n"
"(define (string->list s) (let loop ((i 0) (acc (quote ()))) (if (>= i (string-length s)) (reverse acc) (loop (+ i 1) (cons (string-ref s i) acc)))))\n"
"(define (random-state) (cons 12345 67890))\n"
"(define (vector-map f v) (let* ((n (vector-length v)) (r (make-vector n 0))) (let loop ((i 0)) (cond ((>= i n) r) (else (vector-set! r i (f (vector-ref v i))) (loop (+ i 1)))))))\n"
"(define (vector-for-each f v) (let ((n (vector-length v))) (let loop ((i 0)) (cond ((>= i n) (void)) (else (f (vector-ref v i)) (loop (+ i 1)))))))\n"
"(define (vector-fill! v x) (let ((n (vector-length v))) (let loop ((i 0)) (cond ((>= i n) (void)) (else (vector-set! v i x) (loop (+ i 1)))))))\n"
"(define (sort xs less?) (cond ((null? xs) (quote ())) ((null? (cdr xs)) xs) (else (let ((p (car xs)) (rest (cdr xs))) (append (sort (filter (lambda (x) (less? x p)) rest) less?) (cons p (sort (filter (lambda (x) (not (less? x p))) rest) less?)))))))\n"
"(define (assert-equal expected actual) (cond ((equal? expected actual) #t) (else (display \"FAIL expected=\") (display expected) (display \" actual=\") (display actual) (newline) #f)))\n"
"(define (assert-true v) (cond (v #t) (else (display \"FAIL expected truthy got=\") (display v) (newline) #f)))\n"
"(define (assert-false v) (cond ((not v) #t) (else (display \"FAIL expected #f got=\") (display v) (newline) #f)))\n"
)
(func $bind_prim (param $name_ptr i32) (param $name_len i32) (param $id i32)
(local $sym i32)
(local.set $sym (call $intern (local.get $name_ptr) (local.get $name_len)))
@ -2769,6 +2798,30 @@
(local.get $sym)
(call $make_primitive (local.get $id)))))
;; Length of a NUL-terminated byte sequence (used for prelude embed).
(func $strlen_nul (param $p i32) (result i32)
(local $i i32)
(local.set $i (i32.const 0))
(block $done
(loop $l
(br_if $done (i32.eqz (i32.load8_u (i32.add (local.get $p) (local.get $i)))))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $l)))
(local.get $i))
;; Evaluate the embedded prelude. Source bounds restored by next lumbda_eval.
(func $eval_prelude
(local $val i32)
(global.set $prelude_len (call $strlen_nul (global.get $prelude_ptr)))
(global.set $source_ptr (global.get $prelude_ptr))
(global.set $source_end (i32.add (global.get $prelude_ptr) (global.get $prelude_len)))
(block $done
(loop $loop
(call $skip_ws)
(br_if $done (i32.ge_u (global.get $source_ptr) (global.get $source_end)))
(local.set $val (call $eval (call $read) (global.get $NIL)))
(br $loop))))
(func $lumbda_init (export "lumbda_init")
(if (global.get $initialized) (then (return)))
(global.set $initialized (i32.const 1))
@ -2897,7 +2950,8 @@
(call $bind_prim (i32.const 0xE570) (i32.const 15) (i32.const 103)) ;; hash-table-size
(call $bind_prim (i32.const 0xE580) (i32.const 15) (i32.const 104)) ;; hash-table-keys
(call $bind_prim (i32.const 0xE590) (i32.const 17) (i32.const 105)) ;; hash-table-values
(call $bind_prim (i32.const 0xE5A4) (i32.const 17) (i32.const 106))) ;; hash-table->alist
(call $bind_prim (i32.const 0xE5A4) (i32.const 17) (i32.const 106)) ;; hash-table->alist
(call $eval_prelude))
;; ─── Public eval entry ─────────────────────────────────────────
;; JS writes UTF-8 source into 0x20000 and calls lumbda_eval(len).

View file

@ -128,43 +128,24 @@ function cancelCurrentRun() {
}
}
// ─── Per-tier output block with live ms counter ────────────────────
function makeLiveBlock(tierName) {
// Append a finished tier-block to the output. Order = finish order, so
// the fastest tier appears first naturally.
function appendBlock(tierName, elapsedMs, text, kind) {
const block = document.createElement("div");
block.className = "tier-block";
const h = document.createElement("h3");
h.textContent = TIERS[tierName] || tierName;
const t = document.createElement("span");
t.className = "time";
t.textContent = " loading…";
if (kind === "cancelled") t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`;
else t.textContent = ` (${elapsedMs.toFixed(0)} ms)`;
h.appendChild(t);
block.appendChild(h);
const pre = document.createElement("pre");
pre.textContent = "";
if (kind === "error") pre.className = "err";
pre.textContent = text;
block.appendChild(pre);
outputEl.appendChild(block);
let raf = 0;
let start = 0;
function tickerLoop() {
const ms = (performance.now() - start) | 0;
t.textContent = ` ${ms} ms…`;
raf = requestAnimationFrame(tickerLoop);
}
return {
startTimer() { start = performance.now(); tickerLoop(); return start; },
stop(elapsedMs) {
if (raf) cancelAnimationFrame(raf);
t.textContent = ` (${elapsedMs.toFixed(0)} ms)`;
},
cancelled(elapsedMs) {
if (raf) cancelAnimationFrame(raf);
t.textContent = ` (cancelled @ ${elapsedMs.toFixed(0)} ms)`;
},
setOutput(text) { pre.textContent = text; },
setError(msg) { pre.className = "err"; pre.textContent = msg; },
};
}
// ─── Run / cancel ──────────────────────────────────────────────────
@ -180,43 +161,57 @@ async function runAll() {
outputEl.innerHTML = "";
const tiers = selectedTiers();
const src = getEditorText();
// Each tier runs in its own worker — Promise.all so they race.
// Each tier in its own worker. Append-only output: as each tier finishes,
// we append its block — so the fastest tier shows up first.
const startTimes = {};
const tickStatus = () => {
const parts = [];
for (const t of tiers) {
if (startTimes[t] !== undefined) {
parts.push(`${t} ${((performance.now() - startTimes[t]) | 0)}ms`);
}
}
setStatus(parts.join(" · "), "busy");
};
let raf = requestAnimationFrame(function loop() {
tickStatus();
raf = requestAnimationFrame(loop);
});
const tierPromises = tiers.map((t) => {
const live = makeLiveBlock(t);
const startMark = live.startTimer();
startTimes[t] = performance.now();
return runOnTierInWorker(t, src, (loadingTier) => {
// Status reflects whichever tier most recently announced a load.
setStatus(`loading ${loadingTier}`, "busy");
})
.then((output) => {
const elapsed = performance.now() - startMark;
live.stop(elapsed);
live.setOutput(output);
const elapsed = performance.now() - startTimes[t];
delete startTimes[t];
appendBlock(t, elapsed, output, "ok");
return { tier: t, ok: true, elapsed };
})
.catch((e) => {
const elapsed = performance.now() - startMark;
const elapsed = performance.now() - startTimes[t];
delete startTimes[t];
if (e.message === "cancelled") {
live.cancelled(elapsed);
appendBlock(t, elapsed, "(cancelled)", "cancelled");
return { tier: t, cancelled: true, elapsed };
}
live.stop(elapsed);
live.setError(e.message || String(e));
appendBlock(t, elapsed, e.message || String(e), "error");
return { tier: t, error: e.message, elapsed };
});
});
try {
const results = await Promise.all(tierPromises);
cancelAnimationFrame(raf);
const cancelled = results.some((r) => r.cancelled);
const anyErr = results.some((r) => r.error);
if (cancelled) setStatus("cancelled", "warn");
else if (anyErr) setStatus("completed with errors", "err");
else {
// Winner of the race for ok styling.
const fastest = results.reduce((a, b) => (a.elapsed < b.elapsed ? a : b));
setStatus(`ok — ${fastest.tier} won in ${fastest.elapsed.toFixed(0)} ms`, "ok");
}
} catch (e) {
cancelAnimationFrame(raf);
setStatus(`fatal: ${e.message}`, "err");
} finally {
inFlight = false;

Binary file not shown.

View file

@ -233,16 +233,9 @@ header code {
padding: 0.6rem 0.8rem;
font-size: 13px;
line-height: 1.35;
display: grid;
gap: 0.6rem;
align-content: start;
}
/* When 3 tier blocks render (All three mode), lay them out as 3 columns
* for race-style comparison. With fewer, they stack as a single column. */
#output:has(.tier-block:nth-child(3)) {
grid-template-columns: 1fr 1fr 1fr;
}
#output .tier-block { margin-bottom: 0; }
/* Append-only: results arrive in finish order — fastest tier shows up first. */
#output .tier-block { margin-bottom: 1rem; }
#output .tier-block:last-child { margin-bottom: 0; }
#output .tier-block h3 {
margin: 0 0 0.25rem;