lumbda/www/playground/runner.js
russell@unturf.com ffdfc1df43
playground: stream tier output line-by-line during eval
Previously every (display ...) call buffered into the tier's outBuf /
sys.stdout / WAT output-buffer and only landed in the playground panel
after evalLisp returned. During the 18-second cuda-secp256k1-bench
dispatch the panel stayed blank, then everything (probe + telemetry +
heavy result) appeared at once. Fox: 'demo waits for the full program
to return before emitting instead of line by line as it finishes'.

Each tier's print sink now ALSO calls a per-eval onChunk callback
that the worker forwards to the main thread as a {kind:"chunk"}
postMessage. The main thread spawns an in-progress tier-block on
the first chunk and appends each subsequent chunk to its <pre>,
auto-scrolling. On done/error the timing header finalizes in place;
the block looks identical to a non-streamed appendBlock at the end.

C tier (Emscripten): the Module.print + Module.printErr callbacks
that already fired per-line now invoke currentOnChunk in addition
to buffering. evalLisp accepts an onChunk arg and threads it.

Python tier (pyodide): _lumbda_eval swaps sys.stdout for a
_StreamingStdout subclass of io.StringIO whose write() also calls
into globalThis._lumbdaPyEmitChunk on the JS side. The final
auto-printed value also emits.

Asm tier (WAT): output stays buffered until evalLisp returns —
streaming there needs a new wasm import (out_line) and a wasm
rebuild. Caller-side fallback: app.js's appendBlock path still
runs for asm so its block appears at the end as before.

worker.mjs forwards each chunk via postMessage; app.js's
runOnTierInWorker handles 'chunk' messages by lazily creating
a startLiveBlock on first chunk, appending text on each, and
finalizing the timing in finalize() once done arrives.
2026-06-14 20:25:47 -04:00

58 lines
2.1 KiB
JavaScript

// wasm/app/runner.js
// Tier runner — wraps the three loaders. Exposes a per-tier API so the
// SPA can iterate, time, and render a live elapsed-ms counter between
// the eval start and finish.
import { createPythonTier } from "./python/lumbda-py.js";
import { createCTier } from "./c/lumbda-c.loader.js";
import { createAsmTier } from "./asm/lumbda-asm.loader.js";
const cache = {};
const config = { bendUrl: null };
export function setBendUrl(url) {
config.bendUrl = url || null;
// Propagate to already-loaded tiers.
for (const t of Object.values(cache)) {
if (t && t.setBendUrl) t.setBendUrl(config.bendUrl);
}
}
export async function getTier(name, onLoad) {
if (cache[name]) return cache[name];
if (onLoad) onLoad(name);
if (name === "python") cache[name] = await createPythonTier();
else if (name === "c") cache[name] = await createCTier();
else if (name === "asm") cache[name] = await createAsmTier();
else throw new Error(`unknown tier: ${name}`);
if (cache[name].setBendUrl) cache[name].setBendUrl(config.bendUrl);
return cache[name];
}
export async function evalOnTier(name, src, onLoad, onChunk) {
const tier = await getTier(name, onLoad);
return tier.evalLisp(src, onChunk);
}
export function heapStats(name) {
const tier = cache[name];
if (!tier || !tier.heapStats) return null;
return tier.heapStats();
}
// Portal — REPL save/resume bridge. portalSave reads a snapshot blob
// from the tier (MEMFS on the C tier, similar bridges on others as
// they land). Returns null when the tier hasn't implemented portals
// yet (asm-wat today) so the caller can surface a friendly message
// instead of crashing. portalLoad is the symmetric write-in path.
export function portalSave(name, checkpointName) {
const tier = cache[name];
if (!tier || !tier.portalSave) return null;
return tier.portalSave(checkpointName);
}
export function portalLoad(name, checkpointName, blob) {
const tier = cache[name];
if (!tier || !tier.portalLoad) return false;
tier.portalLoad(checkpointName, blob);
return true;
}