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).
235 lines
8.4 KiB
JavaScript
235 lines
8.4 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";
|
|
|
|
const TIERS = { python: "python (pyodide)", c: "c (emcc)", asm: "asm (wat)" };
|
|
|
|
const demoSources = {};
|
|
|
|
async function loadDemoSource(name) {
|
|
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.theme({ "&": { height: "100%" } }),
|
|
],
|
|
}),
|
|
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 src = await loadDemoSource(sel);
|
|
setEditorText(src);
|
|
}
|
|
|
|
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() {
|
|
return new Worker(new URL("./worker.mjs", import.meta.url), { type: "module" });
|
|
}
|
|
|
|
function ensureWorker(tier) {
|
|
if (!workerState.workers[tier]) workerState.workers[tier] = spawnWorker();
|
|
return workerState.workers[tier];
|
|
}
|
|
|
|
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;
|
|
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;
|
|
loadCurrentDemo();
|