From 6bcc20cb273b1fca66a3ed8139daeddb2becacdb Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 15 Jun 2026 11:18:26 -0400 Subject: [PATCH] =?UTF-8?q?repl:=20asm=20tier=20auto-pause=20uses=20replay?= =?UTF-8?q?=20mode=20=E2=80=94=20env=20reconstructs=20on=20resume?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asm tier has no in-eval poll site (the WAT interpreter doesn't read the SAB atomic), so SAB-pause + portal-snapshot don't apply. Until the asm interpreter grows its own poll, fall back to the same replay pattern the manual asm portal-save already uses: stash every successful prior input from the transcript on tab.autoPause.replayInputs. autoResumeTab now always reboots the tier first, then: * replayInputs set — re-eval each in order so env rebuilds, log a "; resumed asm tier — replayed N prior inputs" entry, then sendInput re-fires the active input. * blob set (c / python) — round-trip through portal-load! as before. * neither (no SAB, lost) — bare sendInput, env starts cold. Now switching away from a tab mid-asm-eval and switching back re-establishes every (define …) the user had before, then re-fires the loop. Same UX shape as the C/Python path. --- wasm/dist-repl/repl.js | 26 +++++++++++++++++++++++--- wasm/repl/repl.js | 26 +++++++++++++++++++++++--- www/repl/repl.js | 26 +++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/wasm/dist-repl/repl.js b/wasm/dist-repl/repl.js index 1ab7f7f..0c2de3c 100644 --- a/wasm/dist-repl/repl.js +++ b/wasm/dist-repl/repl.js @@ -264,7 +264,14 @@ async function autoPauseTab(tab) { } if (activeInput) { - tab.autoPause = { tier, blob, inputSrc: activeInput, savedAt: Date.now() }; + // Asm tier has no SAB poll site, so we can't snapshot env mid-eval. + // Fall back to replay mode: every successful prior input gets + // re-eval'd on resume to rebuild the env, then the active input + // is sent fresh. Same pattern the manual asm portal-save uses. + const replayInputs = (tier === "asm") + ? tab.transcript.filter((e) => e.kind !== "error" && e.input !== activeInput).map((e) => e.input) + : null; + tab.autoPause = { tier, blob, replayInputs, inputSrc: activeInput, savedAt: Date.now() }; } // Terminate the worker(s) so the heap is reclaimed while the tab @@ -284,8 +291,21 @@ async function autoResumeTab(tab) { tab.tier = ap.tier; tierSelectEl.value = ap.tier; - if (ap.blob) { - rebootTier(tab.id, ap.tier); + rebootTier(tab.id, ap.tier); + + if (ap.replayInputs && ap.replayInputs.length) { + // Asm tier replay path — re-eval every successful prior input + // in order so the env is reconstructed before sendInput fires + // the active one. Errors mid-replay are swallowed; restoring + // from a transcript that defined-on-error is the user's call. + for (const src of ap.replayInputs) { + try { await evalInTier(tab.id, ap.tier, src); } catch (e) { /* keep going */ } + } + tab.transcript.push({ + input: `; resumed asm tier — replayed ${ap.replayInputs.length} prior inputs`, + results: [{ tier: ap.tier, output: "#t" }], kind: "ok", + }); + } else if (ap.blob) { // Round-trip through the tier so portal-load! has the blob // to read. The bare 'init eval forces the tier to bootstrap // before we push the blob into MEMFS. diff --git a/wasm/repl/repl.js b/wasm/repl/repl.js index 1ab7f7f..0c2de3c 100644 --- a/wasm/repl/repl.js +++ b/wasm/repl/repl.js @@ -264,7 +264,14 @@ async function autoPauseTab(tab) { } if (activeInput) { - tab.autoPause = { tier, blob, inputSrc: activeInput, savedAt: Date.now() }; + // Asm tier has no SAB poll site, so we can't snapshot env mid-eval. + // Fall back to replay mode: every successful prior input gets + // re-eval'd on resume to rebuild the env, then the active input + // is sent fresh. Same pattern the manual asm portal-save uses. + const replayInputs = (tier === "asm") + ? tab.transcript.filter((e) => e.kind !== "error" && e.input !== activeInput).map((e) => e.input) + : null; + tab.autoPause = { tier, blob, replayInputs, inputSrc: activeInput, savedAt: Date.now() }; } // Terminate the worker(s) so the heap is reclaimed while the tab @@ -284,8 +291,21 @@ async function autoResumeTab(tab) { tab.tier = ap.tier; tierSelectEl.value = ap.tier; - if (ap.blob) { - rebootTier(tab.id, ap.tier); + rebootTier(tab.id, ap.tier); + + if (ap.replayInputs && ap.replayInputs.length) { + // Asm tier replay path — re-eval every successful prior input + // in order so the env is reconstructed before sendInput fires + // the active one. Errors mid-replay are swallowed; restoring + // from a transcript that defined-on-error is the user's call. + for (const src of ap.replayInputs) { + try { await evalInTier(tab.id, ap.tier, src); } catch (e) { /* keep going */ } + } + tab.transcript.push({ + input: `; resumed asm tier — replayed ${ap.replayInputs.length} prior inputs`, + results: [{ tier: ap.tier, output: "#t" }], kind: "ok", + }); + } else if (ap.blob) { // Round-trip through the tier so portal-load! has the blob // to read. The bare 'init eval forces the tier to bootstrap // before we push the blob into MEMFS. diff --git a/www/repl/repl.js b/www/repl/repl.js index 1ab7f7f..0c2de3c 100644 --- a/www/repl/repl.js +++ b/www/repl/repl.js @@ -264,7 +264,14 @@ async function autoPauseTab(tab) { } if (activeInput) { - tab.autoPause = { tier, blob, inputSrc: activeInput, savedAt: Date.now() }; + // Asm tier has no SAB poll site, so we can't snapshot env mid-eval. + // Fall back to replay mode: every successful prior input gets + // re-eval'd on resume to rebuild the env, then the active input + // is sent fresh. Same pattern the manual asm portal-save uses. + const replayInputs = (tier === "asm") + ? tab.transcript.filter((e) => e.kind !== "error" && e.input !== activeInput).map((e) => e.input) + : null; + tab.autoPause = { tier, blob, replayInputs, inputSrc: activeInput, savedAt: Date.now() }; } // Terminate the worker(s) so the heap is reclaimed while the tab @@ -284,8 +291,21 @@ async function autoResumeTab(tab) { tab.tier = ap.tier; tierSelectEl.value = ap.tier; - if (ap.blob) { - rebootTier(tab.id, ap.tier); + rebootTier(tab.id, ap.tier); + + if (ap.replayInputs && ap.replayInputs.length) { + // Asm tier replay path — re-eval every successful prior input + // in order so the env is reconstructed before sendInput fires + // the active one. Errors mid-replay are swallowed; restoring + // from a transcript that defined-on-error is the user's call. + for (const src of ap.replayInputs) { + try { await evalInTier(tab.id, ap.tier, src); } catch (e) { /* keep going */ } + } + tab.transcript.push({ + input: `; resumed asm tier — replayed ${ap.replayInputs.length} prior inputs`, + results: [{ tier: ap.tier, output: "#t" }], kind: "ok", + }); + } else if (ap.blob) { // Round-trip through the tier so portal-load! has the blob // to read. The bare 'init eval forces the tier to bootstrap // before we push the blob into MEMFS.