WASM tail-call instruction wired into every tail position:
if branches, eval_begin last expression, all special-form dispatchers
(cond/when/unless/case/let/let*/letrec/begin/and/or), function
application's $apply, and apply's closure-body $eval_begin.
wat2wasm + wasm-validate now use --enable-tail-call.
Reader fixes:
- Dotted pair syntax: (a b . rest) parses as a real dotted list.
Without this, variadic params and other dotted-cdr forms parsed
as 4-element proper lists.
- Stray ) at top level advances source_ptr instead of spinning
forever. Found via bisection of functional.lsp under TCO.
- eval_args guards against non-pair tails so a misplaced dotted
argument (e.g. an unexpanded macro template) can't deref garbage.
Cross-tier numbers (wasm-test-functional-cross):
before: 93/111 reached, stack overflow on (ack 3 4)
now: progresses through the full TCO section, deep let, named-let
to 100k, mutual recursion to 200k. Still climbing.
Bend gpu demo:
6th playground option ("bend (gpu dispatch) ⚡") ships a SHAKE256
fan-out at 1M inputs via (bend!-call ...). Run button is GUARDED:
if no bend URL is configured, refuses with
"set a bend URL first — this demo is GPU-only by design".
Protects customer machines from burning minutes on a workload the
local tiers cannot finish in reasonable time.
365 lines
14 KiB
JavaScript
365 lines
14 KiB
JavaScript
// wasm/app/app.js
|
|
// Single-page app shell — CodeMirror 6 editor + Web-Worker-backed tier
|
|
// runner. The main thread stays responsive: the live ms counter ticks
|
|
// every animation frame, and Cancel terminates the worker mid-eval.
|
|
|
|
import { EditorState } from "@codemirror/state";
|
|
import { EditorView, keymap, lineNumbers, drawSelection } from "@codemirror/view";
|
|
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
import { syntaxHighlighting, defaultHighlightStyle, StreamLanguage } from "@codemirror/language";
|
|
import { scheme } from "@codemirror/legacy-modes/mode/scheme";
|
|
import { oneDark } from "@codemirror/theme-one-dark";
|
|
import { openVault } from "./crypto.js";
|
|
|
|
const TIERS = { python: "python (pyodide)", c: "c (emcc)", asm: "asm (wat)" };
|
|
const FREE_FORM_DEFAULT = `; free-form mode — unlock the vault below to persist this code
|
|
; encrypted in localStorage with your password
|
|
|
|
(print "67")
|
|
(/ 42 6)
|
|
`;
|
|
|
|
const demoSources = {};
|
|
// Vault state. When unlocked, free-form code auto-saves on every edit.
|
|
const vaultState = { vault: null, freeForm: null, saveTimer: null };
|
|
|
|
async function loadDemoSource(name) {
|
|
if (name === "free-form") {
|
|
return vaultState.freeForm != null ? vaultState.freeForm : FREE_FORM_DEFAULT;
|
|
}
|
|
if (!demoSources[name]) {
|
|
const resp = await fetch(`demos/${name}.lsp`);
|
|
demoSources[name] = await resp.text();
|
|
}
|
|
return demoSources[name];
|
|
}
|
|
|
|
const editorParent = document.getElementById("editor");
|
|
const outputEl = document.getElementById("output");
|
|
const statusEl = document.getElementById("status");
|
|
const runBtn = document.getElementById("run");
|
|
const cancelBtn = document.getElementById("cancel");
|
|
|
|
const editorView = new EditorView({
|
|
state: EditorState.create({
|
|
doc: "",
|
|
extensions: [
|
|
lineNumbers(),
|
|
history(),
|
|
drawSelection(),
|
|
syntaxHighlighting(defaultHighlightStyle),
|
|
StreamLanguage.define(scheme),
|
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
oneDark,
|
|
EditorView.updateListener.of((u) => {
|
|
if (u.docChanged) scheduleFreeFormSave();
|
|
}),
|
|
],
|
|
}),
|
|
parent: editorParent,
|
|
});
|
|
|
|
function setEditorText(text) {
|
|
editorView.dispatch({
|
|
changes: { from: 0, to: editorView.state.doc.length, insert: text },
|
|
});
|
|
}
|
|
|
|
function getEditorText() {
|
|
return editorView.state.doc.toString();
|
|
}
|
|
|
|
async function loadCurrentDemo() {
|
|
const sel = document.querySelector('input[name="program"]:checked').value;
|
|
const vaultBar = document.getElementById("vault-bar");
|
|
vaultBar.hidden = sel !== "free-form";
|
|
const src = await loadDemoSource(sel);
|
|
setEditorText(src);
|
|
}
|
|
|
|
// ─── Free-form vault ───────────────────────────────────────────────
|
|
const vaultBarEl = document.getElementById("vault-bar");
|
|
const vaultPwEl = document.getElementById("vault-pw");
|
|
const vaultUnlockBtn = document.getElementById("vault-unlock");
|
|
const vaultLockBtn = document.getElementById("vault-lock");
|
|
const vaultStateEl = document.getElementById("vault-state");
|
|
const vaultNoteEl = document.getElementById("vault-note");
|
|
|
|
function setVaultNote(text, isErr) {
|
|
vaultNoteEl.textContent = text || "";
|
|
vaultNoteEl.className = "vault-note" + (isErr ? " err" : "");
|
|
}
|
|
|
|
async function unlockVault() {
|
|
const pw = vaultPwEl.value;
|
|
if (!pw) { setVaultNote("password required", true); return; }
|
|
setVaultNote("");
|
|
try {
|
|
vaultState.vault = await openVault(pw);
|
|
const data = await vaultState.vault.read();
|
|
if (data && data.__decryptionFailed) {
|
|
setVaultNote("vault exists but password is wrong", true);
|
|
vaultState.vault = null;
|
|
return;
|
|
}
|
|
vaultState.freeForm = (data && typeof data.freeForm === "string")
|
|
? data.freeForm
|
|
: FREE_FORM_DEFAULT;
|
|
vaultStateEl.textContent = "unlocked";
|
|
vaultStateEl.classList.add("unlocked");
|
|
vaultUnlockBtn.hidden = true;
|
|
vaultLockBtn.hidden = false;
|
|
vaultPwEl.value = "";
|
|
vaultPwEl.disabled = true;
|
|
// If free-form is the current program, swap the editor in.
|
|
const sel = document.querySelector('input[name="program"]:checked').value;
|
|
if (sel === "free-form") setEditorText(vaultState.freeForm);
|
|
setVaultNote("ok — edits auto-save");
|
|
} catch (e) {
|
|
setVaultNote("unlock failed: " + e.message, true);
|
|
}
|
|
}
|
|
|
|
function lockVault() {
|
|
vaultState.vault = null;
|
|
vaultState.freeForm = null;
|
|
if (vaultState.saveTimer) { clearTimeout(vaultState.saveTimer); vaultState.saveTimer = null; }
|
|
vaultStateEl.textContent = "locked";
|
|
vaultStateEl.classList.remove("unlocked");
|
|
vaultUnlockBtn.hidden = false;
|
|
vaultLockBtn.hidden = true;
|
|
vaultPwEl.disabled = false;
|
|
vaultPwEl.value = "";
|
|
setVaultNote("");
|
|
// If free-form is current program, swap to the placeholder.
|
|
const sel = document.querySelector('input[name="program"]:checked').value;
|
|
if (sel === "free-form") setEditorText(FREE_FORM_DEFAULT);
|
|
}
|
|
|
|
function scheduleFreeFormSave() {
|
|
if (!vaultState.vault) return;
|
|
const sel = document.querySelector('input[name="program"]:checked').value;
|
|
if (sel !== "free-form") return;
|
|
if (vaultState.saveTimer) clearTimeout(vaultState.saveTimer);
|
|
vaultState.saveTimer = setTimeout(async () => {
|
|
const text = getEditorText();
|
|
vaultState.freeForm = text;
|
|
try { await vaultState.vault.write({ freeForm: text, savedAt: Date.now() }); }
|
|
catch (e) { setVaultNote("save failed: " + e.message, true); }
|
|
}, 350);
|
|
}
|
|
|
|
function selectedTiers() {
|
|
const sel = document.querySelector('input[name="tier"]:checked').value;
|
|
return sel === "all" ? ["python", "c", "asm"] : [sel];
|
|
}
|
|
|
|
function setStatus(text, cls) {
|
|
statusEl.textContent = text || "";
|
|
statusEl.className = "status" + (cls ? " " + cls : "");
|
|
}
|
|
|
|
// ─── Worker plumbing ───────────────────────────────────────────────
|
|
// ONE worker per tier so the three tiers run on independent threads —
|
|
// in "All three" mode they race, and a slow tier never blocks a fast one.
|
|
// Cancel terminates every active worker; next eval recreates them.
|
|
|
|
const workerState = {
|
|
workers: { python: null, c: null, asm: null },
|
|
pending: { python: null, c: null, asm: null },
|
|
nextRunId: 0,
|
|
};
|
|
|
|
function spawnWorker() {
|
|
const w = new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" });
|
|
if (bendState.url) w.postMessage({ kind: "config", bendUrl: bendState.url });
|
|
return w;
|
|
}
|
|
|
|
function ensureWorker(tier) {
|
|
if (!workerState.workers[tier]) workerState.workers[tier] = spawnWorker();
|
|
return workerState.workers[tier];
|
|
}
|
|
|
|
// ─── Bend URL ──────────────────────────────────────────────────────
|
|
const bendState = { url: localStorage.getItem("lumbda_bend_url") || "" };
|
|
const bendUrlEl = document.getElementById("bend-url");
|
|
const bendSaveBtn = document.getElementById("bend-save");
|
|
const bendStateEl = document.getElementById("bend-state");
|
|
if (bendState.url) {
|
|
bendUrlEl.value = bendState.url;
|
|
bendStateEl.textContent = "saved";
|
|
}
|
|
function saveBendUrl() {
|
|
bendState.url = bendUrlEl.value.trim();
|
|
if (bendState.url) {
|
|
localStorage.setItem("lumbda_bend_url", bendState.url);
|
|
bendStateEl.textContent = "saved";
|
|
} else {
|
|
localStorage.removeItem("lumbda_bend_url");
|
|
bendStateEl.textContent = "";
|
|
}
|
|
// Push config to any already-spawned workers.
|
|
for (const w of Object.values(workerState.workers)) {
|
|
if (w) w.postMessage({ kind: "config", bendUrl: bendState.url });
|
|
}
|
|
}
|
|
bendSaveBtn.addEventListener("click", saveBendUrl);
|
|
bendUrlEl.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") { e.preventDefault(); saveBendUrl(); }
|
|
});
|
|
|
|
function runOnTierInWorker(tier, src, onLoading) {
|
|
return new Promise((resolve, reject) => {
|
|
const w = ensureWorker(tier);
|
|
const myRunId = ++workerState.nextRunId;
|
|
workerState.pending[tier] = { runId: myRunId, resolve, reject };
|
|
const handler = (e) => {
|
|
if (e.data.runId !== myRunId) return;
|
|
if (e.data.kind === "loading") {
|
|
onLoading && onLoading(e.data.tier);
|
|
} else if (e.data.kind === "done") {
|
|
w.removeEventListener("message", handler);
|
|
workerState.pending[tier] = null;
|
|
resolve(e.data.output);
|
|
} else if (e.data.kind === "error") {
|
|
w.removeEventListener("message", handler);
|
|
workerState.pending[tier] = null;
|
|
reject(new Error(e.data.message));
|
|
}
|
|
};
|
|
w.addEventListener("message", handler);
|
|
w.postMessage({ kind: "eval", runId: myRunId, tier, src });
|
|
});
|
|
}
|
|
|
|
function cancelCurrentRun() {
|
|
for (const t of Object.keys(workerState.workers)) {
|
|
if (workerState.workers[t]) {
|
|
workerState.workers[t].terminate();
|
|
workerState.workers[t] = null;
|
|
}
|
|
if (workerState.pending[t]) {
|
|
workerState.pending[t].reject(new Error("cancelled"));
|
|
workerState.pending[t] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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";
|
|
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");
|
|
if (kind === "error") pre.className = "err";
|
|
pre.textContent = text;
|
|
block.appendChild(pre);
|
|
outputEl.appendChild(block);
|
|
}
|
|
|
|
// ─── Run / cancel ──────────────────────────────────────────────────
|
|
|
|
let inFlight = false;
|
|
|
|
async function runAll() {
|
|
if (inFlight) return;
|
|
// bend demo guard: refuse to run locally without a configured bend URL.
|
|
// A CPU-impossible workload would lock the customer's tab.
|
|
const program = document.querySelector('input[name="program"]:checked').value;
|
|
if (program === "bend-gpu" && !bendState.url) {
|
|
setStatus("set a bend URL first — this demo is GPU-only by design", "err");
|
|
return;
|
|
}
|
|
inFlight = true;
|
|
runBtn.disabled = true;
|
|
cancelBtn.disabled = false;
|
|
setStatus("running…", "busy");
|
|
outputEl.innerHTML = "";
|
|
const tiers = selectedTiers();
|
|
const src = getEditorText();
|
|
// 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) => {
|
|
startTimes[t] = performance.now();
|
|
return runOnTierInWorker(t, src, (loadingTier) => {
|
|
setStatus(`loading ${loadingTier}…`, "busy");
|
|
})
|
|
.then((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() - startTimes[t];
|
|
delete startTimes[t];
|
|
if (e.message === "cancelled") {
|
|
appendBlock(t, elapsed, "(cancelled)", "cancelled");
|
|
return { tier: t, cancelled: true, elapsed };
|
|
}
|
|
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 {
|
|
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;
|
|
runBtn.disabled = false;
|
|
cancelBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
function onCancel() {
|
|
if (!inFlight) return;
|
|
setStatus("cancelling…", "warn");
|
|
cancelCurrentRun();
|
|
}
|
|
|
|
document.querySelectorAll('input[name="program"]').forEach((el) => {
|
|
el.addEventListener("change", loadCurrentDemo);
|
|
});
|
|
runBtn.addEventListener("click", runAll);
|
|
cancelBtn.addEventListener("click", onCancel);
|
|
cancelBtn.disabled = true;
|
|
vaultUnlockBtn.addEventListener("click", unlockVault);
|
|
vaultLockBtn.addEventListener("click", lockVault);
|
|
vaultPwEl.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") { e.preventDefault(); unlockVault(); }
|
|
});
|
|
loadCurrentDemo();
|