Tab switching during a long-running eval used to silently abandon
the calc — output stopped streaming, no snapshot, nothing to come
back to. Now setActiveTab pauses the outgoing tab's eval (and
optionally portal-saves the env), terminates the worker, and on
re-entry hydrates + re-fires the original input.
Pieces:
* serve-coop.py + make serve-repl — dev server that emits
Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp so SharedArrayBuffer
is constructable in the browser. Same headers production needs.
* C tier eval-loop pause poll — c/eval.c grows lumbda_check_pause(),
guarded by #ifdef LUMBDA_WASM. Called at the top of leval()'s
while(1); masked to every 1024th iteration so the polling cost
stays under noise floor. When the JS-library import
js_lumbda_pause_requested returns 1, lisp_error("paused")
longjmps out so module-global env survives intact for the
portal-snapshot that follows.
* SAB plumbing — main thread allocates new SharedArrayBuffer(4),
hands it through worker config → runner.setPauseFlag →
lumbda-c.loader.setPauseFlag → globalThis._lumbdaCPauseFlag.
Atomics.store / Atomics.load on index 0 is the signalling
channel. Falls back to null when COOP/COEP isn't isolated, in
which case pause degrades to a hard worker.terminate().
* autoPauseTab() — on setActiveTab away, snapshots the tier
(C tier with SAB) or hard-cancels (other tiers / no SAB),
stashes tab.autoPause = {tier, blob, inputSrc, savedAt},
terminates the workers so the heap is reclaimed.
* autoResumeTab() — on setActiveTab into a tab with autoPause,
reboots the tier, hydrates MEMFS, runs (portal-load! ...), then
re-fires the original input via sendInput so the eval restarts
from the saved state. Asm + Python paths re-run from scratch
until their poll sites land.
Also closes two UX papercuts from fox: chip ⇣ export icon bumped
from 0.85em muted to 1em green so it's actually discoverable; the
scope toggle now reads "scope: this tab" / "scope: all tabs" so the
button label describes the state rather than a target.
72 lines
2.7 KiB
JavaScript
72 lines
2.7 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, pauseFlag: 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);
|
|
}
|
|
}
|
|
|
|
// SharedArrayBuffer that the tier's eval loop polls for a pause
|
|
// signal. Main thread writes 1 to atomic index 0; the tier's polling
|
|
// hook (c/eval.c lumbda_check_pause) raises lisp_error("paused") at
|
|
// the next K-iteration boundary so module-global env is preserved
|
|
// and (portal-snapshot!) afterward captures everything that ran
|
|
// before the pause.
|
|
export function setPauseFlag(flag) {
|
|
config.pauseFlag = flag || null;
|
|
for (const t of Object.values(cache)) {
|
|
if (t && t.setPauseFlag) t.setPauseFlag(config.pauseFlag);
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (cache[name].setPauseFlag) cache[name].setPauseFlag(config.pauseFlag);
|
|
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;
|
|
}
|