Adds a portal-bar to the REPL between tabbar and transcript: a save
button + chip strip showing all saved checkpoints for the active tab.
Click a chip to restore, click × to delete.
Per-tier strategy:
* c, python — call the tier's (portal-snapshot! NAME), then read the
JSON blob out of MEMFS (Emscripten/Pyodide FS) and stash it in the
encrypted vault entry. Restore reverses: hydrate MEMFS, then
(portal-load! NAME) merges the bindings into the live env.
* asm — no portal serializer in the WAT tier yet (would need a
Cheney-aware walk). Falls back to transcript replay: save snapshots
every successful prior input, restore reboots the tier and re-evals
them in order.
Plumbing:
* Worker bridge: new portal-save / portal-load message kinds wire
MEMFS reads/writes to the main thread.
* runner.js exposes portalSave / portalLoad — null when a tier
hasn't implemented portals (asm stays grey).
* C tier: replace EM_JS with extern + --js-library for js_lumbda_bend_call
(EM_JS-generated declaration was unreachable from wasmImports at
instantiate time, browsers threw "import object field ... not a
Function"). FS added to EXPORTED_RUNTIME_METHODS so JS can reach
pyodide.FS / Module.FS for MEMFS I/O.
Smoke-tested all three tiers headlessly: save → chip render → restore
round-trips clean on c / python / asm, zero console errors.
58 lines
2.1 KiB
JavaScript
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;
|
|
}
|