Summary
IO.sleep in the browser runtime is currently a busy-wait that pegs the main thread for the entire duration. This makes any animation or simulation that uses IO.sleep(N) between frames freeze the tab — the canonical terminal-style render-sleep-step loop is fundamentally incompatible with a browser. The proposed fix doesn't require a language change: implement IO.sleep against the WebAssembly JavaScript Promise Integration (JSPI) proposal, with Asyncify as the fallback for older browsers.
Discovery context
An agent writing a browser variant of Conway's Life on current main reported this as the core obstacle to "write once, run anywhere": they had to fork their working terminal program (life.vera) into a browser-shaped variant (life_web.vera) with packed-string emission and a JavaScript canvas driver, purely because the terminal version's IO.sleep(100) between frames couldn't yield to the browser event loop.
Their framing — quoted because it's exactly right — was that this is "two things masquerading as one: timing and rendering. IO.sleep busy-waits because WASM can't yield to the JS event loop from synchronous code. ANSI escapes don't render because the browser runtime treats IO.print as text-to-DOM rather than text-to-terminal. Both meant life.vera couldn't run unchanged — I had to write a different program that computed the same thing differently."
This issue covers the timing half. The rendering half is tracked separately as a sibling architectural issue: ANSI subset interpretation in the browser runtime.
Proposed approach
Primary: implement IO.sleep in vera/browser/runtime.mjs using JSPI:
- The Vera-emitted WASM declares
vera.sleep(ms: i64) as an imported function.
- The runtime imports it as a JSPI-suspending function:
WebAssembly.promising wraps a JS function that returns a Promise resolved by setTimeout(resolve, ms).
- Vera-side code calling
IO.sleep(100) then suspends the WASM call, yields to the event loop, and resumes after 100ms — without blocking the main thread.
Fallback: if JSPI's browser support is too thin (it shipped in Chrome 137 and Firefox 134; Safari has no current implementation), provide an Asyncify-built variant of the same module that achieves the same suspension via stack-saving transformation. Asyncify has a runtime cost (~50% slowdown for code that uses the suspension paths), but it's a known-working drop-in.
The runtime should pick the right implementation at init time based on feature detection.
Why this is high-leverage
- It closes the timing half of the terminal-vs-browser seam without language changes. The same
life.vera source — IO.sleep between frames, recursive run_loop — would Just Work in both targets. No life_web.vera fork, no JS driver.
- Pairs naturally with ANSI interpretation (sibling issue): with both fixes, a typical terminal Vera program (animation via
IO.sleep + screen control via ANSI escapes) renders identically on both targets.
- No bundle-size cost in the JSPI path — it's a thin runtime change. Asyncify adds bytes but only when JSPI is unavailable.
Acceptance
- A program calling
IO.sleep(100) in a recursive loop renders animated output in the browser without freezing the tab.
- The same source (
life.vera) runs on both vera run (terminal) and vera compile --target browser (browser) with identical output.
- A test in
tests/test_browser.py verifies that IO.sleep actually yields rather than busy-waiting (e.g., by measuring that other event-loop callbacks fire during a 500ms sleep).
Related
- #608 — the framing issue ("terminal-vs-browser IO seam")
- #603 — runtime helper exports (separate gap, also browser-target)
- #604 — prelude combinators skipped on browser target
- The agent's full design memo is posted as a comment on the umbrella issue: #608 design memo. Key paragraph: "JSPI-driven IO.sleep plus a minimal ANSI interpreter would do most of the heavy lifting on their own."
Summary
IO.sleepin the browser runtime is currently a busy-wait that pegs the main thread for the entire duration. This makes any animation or simulation that usesIO.sleep(N)between frames freeze the tab — the canonical terminal-style render-sleep-step loop is fundamentally incompatible with a browser. The proposed fix doesn't require a language change: implementIO.sleepagainst the WebAssembly JavaScript Promise Integration (JSPI) proposal, with Asyncify as the fallback for older browsers.Discovery context
An agent writing a browser variant of Conway's Life on current main reported this as the core obstacle to "write once, run anywhere": they had to fork their working terminal program (
life.vera) into a browser-shaped variant (life_web.vera) with packed-string emission and a JavaScript canvas driver, purely because the terminal version'sIO.sleep(100)between frames couldn't yield to the browser event loop.Their framing — quoted because it's exactly right — was that this is "two things masquerading as one: timing and rendering.
IO.sleepbusy-waits because WASM can't yield to the JS event loop from synchronous code. ANSI escapes don't render because the browser runtime treatsIO.printas text-to-DOM rather than text-to-terminal. Both meantlife.veracouldn't run unchanged — I had to write a different program that computed the same thing differently."This issue covers the timing half. The rendering half is tracked separately as a sibling architectural issue: ANSI subset interpretation in the browser runtime.
Proposed approach
Primary: implement
IO.sleepinvera/browser/runtime.mjsusing JSPI:vera.sleep(ms: i64)as an imported function.WebAssembly.promisingwraps a JS function that returns aPromiseresolved bysetTimeout(resolve, ms).IO.sleep(100)then suspends the WASM call, yields to the event loop, and resumes after 100ms — without blocking the main thread.Fallback: if JSPI's browser support is too thin (it shipped in Chrome 137 and Firefox 134; Safari has no current implementation), provide an Asyncify-built variant of the same module that achieves the same suspension via stack-saving transformation. Asyncify has a runtime cost (~50% slowdown for code that uses the suspension paths), but it's a known-working drop-in.
The runtime should pick the right implementation at
inittime based on feature detection.Why this is high-leverage
life.verasource —IO.sleepbetween frames, recursiverun_loop— would Just Work in both targets. Nolife_web.verafork, no JS driver.IO.sleep+ screen control via ANSI escapes) renders identically on both targets.Acceptance
IO.sleep(100)in a recursive loop renders animated output in the browser without freezing the tab.life.vera) runs on bothvera run(terminal) andvera compile --target browser(browser) with identical output.tests/test_browser.pyverifies thatIO.sleepactually yields rather than busy-waiting (e.g., by measuring that other event-loop callbacks fire during a 500ms sleep).Related