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:
parent
3800271b1b
commit
1c9d74b407
6 changed files with 125 additions and 95 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue