Skip to content

Browser runtime: implement IO.sleep via JSPI (or Asyncify fallback) so animations don't freeze the tab #609

@aallan

Description

@aallan

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:

  1. The Vera-emitted WASM declares vera.sleep(ms: i64) as an imported function.
  2. The runtime imports it as a JSPI-suspending function: WebAssembly.promising wraps a JS function that returns a Promise resolved by setTimeout(resolve, ms).
  3. 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."

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttoolingIssue around tooling built for the language (e.g. package managers, IDE plug-ins)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions