Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Requires [wasi-sdk](https://github.com/WebAssembly/wasi-sdk) and [wasmtime](http
make # build build/bin/io_static (WASM binary)
make test # build build/bin/test_iterative_eval
make check # run both test suites with wasmtime
make component # build build/bin/io_component.wasm (WASI 0.2 component, wasm32-wasip2)
make check-component # run the Io test suite against the component
make clean # remove build artifacts
make regenerate # regenerate IoVMInit.c from .io files
```
Expand Down
26 changes: 25 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
# make regenerate Regenerate IoVMInit.c from .io files
# make browser Build browser/io_browser.wasm (reactor module)
# make serve Serve browser REPL on localhost:8000
# make component Build io_component.wasm (WASI 0.2 component, wasm32-wasip2)
# make check-component Run the Io test suite against the component

WASI_SDK ?= $(HOME)/wasi-sdk
BUILD := build
Expand Down Expand Up @@ -55,7 +57,7 @@ IO_SOURCES := $(wildcard libs/iovm/io/*.io)

# --- Targets ---

.PHONY: all test check clean regenerate browser serve check-browser
.PHONY: all test check clean regenerate browser serve check-browser component check-component

all: $(BINDIR)/io_static

Expand Down Expand Up @@ -94,6 +96,28 @@ $(OBJDIR)/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<

# --- Component (WASI 0.2, wasm32-wasip2) ---
#
# Same sources compiled for the wasm32-wasip2 triple, linked through
# wasm-component-ld into a WebAssembly component speaking the WASI 0.2
# interfaces. Objects live in a separate tree because the triple differs.

P2_OBJDIR := $(BUILD)/obj-p2
P2_CFLAGS := --target=wasm32-wasip2 $(CFLAGS)
P2_OBJS := $(patsubst %.c,$(P2_OBJDIR)/%.o,$(ALL_SRCS))

component: $(BINDIR)/io_component.wasm

$(BINDIR)/io_component.wasm: $(P2_OBJDIR)/tools/source/main.o $(P2_OBJS) | $(BINDIR)
$(CC) $(P2_CFLAGS) -o $@ $^ $(LDFLAGS)

$(P2_OBJDIR)/%.o: %.c
@mkdir -p $(dir $@)
$(CC) $(P2_CFLAGS) -c -o $@ $<

check-component: $(BINDIR)/io_component.wasm
wasmtime --dir=. --dir=/tmp $(BINDIR)/io_component.wasm libs/iovm/tests/correctness/run.io

# --- Browser (reactor module) ---

BROWSER_DIR := browser
Expand Down
95 changes: 95 additions & 0 deletions agents/WASI_ASYNC_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# WASI 0.3 Async Integration Plan

Status: **prep work landed, 0.3 integration blocked on toolchain** (June 2026)

Reference: [WASI 0.3 announcement](https://bytecodealliance.org/articles/WASI-0.3)

## What WASI 0.3 changes

WASI 0.3 makes async native to the component model. The canonical ABI gains
three first-class constructs:

- `future<T>` — replaces the `pollable` resource
- `stream<T>` — replaces `input-stream` / `output-stream`
- async functions — replaces the 0.2 `start-foo`/`finish-foo`/`subscribe` pattern

Scheduling is completion-based (io_uring/IOCP style): the host runtime owns a
single shared event loop and drives task scheduling across components. The
design explicitly accommodates stackless coroutine runtimes — which is what
this VM is.

## What has landed (works today, WASI 0.1/0.2)

1. **WASI 0.2 component build** (`make component`, `make check-component`).
`build/bin/io_component.wasm` is a real component (layer-1 binary) built
via wasi-sdk's `wasm32-wasip2` target and `wasm-component-ld`. The full
correctness suite passes against it under wasmtime 42.

2. **Scheduler timer queue** (`Scheduler addTimerAt/addTimer/removeTimerFor/
wakeExpiredTimers/idleUntilNextTimer` in `libs/iovm/io/Exception.io`).
`Object wait` parks the current coroutine on the timer queue instead of
busy-yielding. Expired timers re-enter the run queue on every
`yield`/`pause`. When nothing is runnable but timers are pending, the VM
blocks in **one** host wait until the nearest deadline
(`Scheduler idleUntilNextTimer` → `System sleep`).

That single idle point is the WASI 0.3 seam: under 0.3 it awaits a host
future instead of sleeping.

3. **Dead-ancestor walk in the eval loop** (`IoState_iterative.c`, empty-frame
handler). When a coroutine finishes, the eval loop resumes its
`parentCoroutine`. Timer wakeups let a coroutine outlive the coroutine
that started it, so the walk now skips dead ancestors (no saved frames)
to find the nearest resumable one. Without this, a finishing coroutine
whose starter already finished hit the `nestedEvalDepth > 0` early-return
and stranded every parked coroutine.

## VM contracts to preserve (learned the hard way)

- **Spurious wakeups are normal.** A finished child coroutine resumes its
parent directly, bypassing the scheduler. Anything that parks a coroutine
must use condition-variable semantics: loop, re-check the condition
(deadline, future resolved, …), re-park if unsatisfied. `Object wait` does
this; a future `awaitHostFuture` must too.
- **Timer entries can go stale.** A coroutine woken by something other than
its timer must remove its entry (`Scheduler removeTimerFor`), or
`wakeExpiredTimers` will later re-enqueue a coroutine that is already
running or dead — and `resume` on a dead coroutine restarts its body.
- **Coroutine bodies resolve chain heads against `runTarget`.** `coroDoLater`
sets `runTarget := self`, so method locals are NOT visible to chain-head
messages (argument evaluation does see them, which makes failures look
intermittent). Use `coroDo`/`coroFor` (runTarget = sender) for bodies that
capture locals. Misuse shows up as a swallowed Importer "does not respond
to" exception that silently kills the coroutine.

## The 0.3 integration, when unblocked

Target shape: a `HostFuture` primitive owned by C, with the scheduler extended
from "timers only" to "timers + host futures".

1. `Scheduler idleUntilNextTimer` generalizes to `Scheduler idle`: collect the
nearest timer deadline and all pending host futures, and make one blocking
host call (`waitable-set.wait` in the 0.3 canonical ABI) instead of
`System sleep`.
2. `File read`/`write` and any socket primitive lower onto `stream<T>`; the
calling coroutine parks on the paired completion future and re-enters the
run queue when the host completes it. No Asyncify, no transform: parking
is the same heap-frame operation as a coroutine switch.
3. `@`/`@@` actor futures (`Actor.io`) optionally back `Future setResult` with
host-future completion so an actor awaiting host I/O consumes no VM
scheduling at all.
4. Export the VM's eval entry as an async component function so embedders can
call Io code without blocking their event loop (service chaining).

## Upgrade triggers (re-check before starting)

- **wasmtime ≥ 46 installed** (0.3.0 interfaces, async on by default).
Local machine has 42.0.1 — `wasmtime --version`.
- **wasi-sdk / wit-bindgen C support for 0.3 async** (`future<T>`/`stream<T>`
lowering and `waitable-set` intrinsics from C). At the time of writing,
guest toolchain support was rolling out for Rust/Go/JS/Python first;
wasi-sdk 25 has no 0.3 target.
- **jco 0.3 support** if the browser target should share the same model.

When both land: regenerate nothing — start from `Scheduler idle` (step 1) and
keep `System sleep` as the fallback for hosts without 0.3.
Binary file added docs/Book/images/Introduction_old.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_originals/Appendix.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_originals/Concurrency.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_originals/Control Flow.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_originals/Introduction.v1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_originals/Introduction.v2.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_originals/Objects.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_originals/Primitives.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_originals/Syntax.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_unused/Cover.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_unused/Grid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/Book/images/_unused/Header.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/Technical Notes/WASM/Browser Target/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/Technical Notes/WASM/DOM Interop/index.html

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion docs/Technical Notes/WASM/_index.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@
"title": "About",
"body": "<p>Historically the Io VM was a native C binary with a per-platform build matrix (macOS, Linux, Windows, BSD), platform-specific coroutine assembly, and a native addon model that compiled C extensions against the host toolchain. That model works, but every new platform multiplies the work: new assembly for coroutines, new build recipes, new binary artifacts, new ways for addons to break.</p><p>Compiling the VM to <strong>WebAssembly</strong> collapses that matrix to a single portable module. The same <code>io_static.wasm</code> runs under <a href=\"https://wasmtime.dev\">wasmtime</a>, Node.js, and directly in the browser, with no platform-specific code paths in the VM itself.</p><h3>Why it matters</h3><ul><li><strong>One binary, every host</strong> &mdash; a single WASM module replaces the per-OS and per-architecture build matrix. No cross-compilation toolchains, no CI jobs for each target, no separate releases. If your environment has a WASM runtime, it can run Io.</li><li><strong>Runs in the browser</strong> &mdash; the same VM that runs on the command line loads as a script tag. Io programs get direct access to the DOM, <code>fetch</code>, Web Audio, WebGL &mdash; any capability the host page exposes &mdash; without a separate &ldquo;web Io&rdquo; fork.</li><li><strong>Bidirectional Io&harr;JavaScript bridge</strong> &mdash; the old native-addon model is replaced by a symmetric bridge: Io can call any JavaScript function and receive JS objects as Io values; JavaScript can call Io methods and pass JS values as arguments. One mechanism replaces what used to require a per-library C addon.</li><li><strong>Access to the JavaScript ecosystem</strong> &mdash; through the bridge, Io programs can reach the roughly two million packages on npm and every Web API the browser exposes. The classic Io distribution shipped a few dozen hand-written addons covering networking, databases, graphics, crypto, and serialization; the JS ecosystem already covers all of those, plus machine learning, 3D rendering, audio synthesis, protocol implementations, cloud SDKs, and much more &mdash; without anyone writing a line of binding code. Io inherits decades of JavaScript library work as a side effect of the port.</li><li><strong>Embeddable by design</strong> &mdash; a WASM module is an embeddable artifact. Native apps can host Io through wasmtime or wasmer; server-side JS can host it through Node; the browser hosts it directly. Embedding no longer means linking C libraries and matching ABIs.</li><li><strong>Sandboxed by default</strong> &mdash; WASM modules only see the capabilities their host grants. File-system and network access flow through WASI or host-supplied JS, not raw syscalls, so an Io program can&rsquo;t silently reach parts of the system the host didn&rsquo;t intend to expose.</li><li><strong>Forces a cleaner core</strong> &mdash; the WASM target doesn&rsquo;t expose the native C stack, which ruled out the old ucontext/setjmp coroutine implementations and motivated the stackless evaluator. The discipline that came with the port left the VM smaller, more portable, and easier to reason about.</li></ul><p>Trade-offs are real: the WASM target is early-access, JIT throughput depends on the host runtime, and some classic native addons (notably anything linking C libraries) don&rsquo;t carry over &mdash; their roles are now filled by JavaScript libraries reached through the bridge.</p>"
},
{
"type": "ContentText",
"title": "WASI 0.3",
"body": "<p>In 2026 the Bytecode Alliance shipped <a href=\"https://bytecodealliance.org/articles/WASI-0.3\">WASI 0.3</a>, whose headline change is that <strong>async is now native to WebAssembly components</strong>. The component model's canonical ABI gains three first-class constructs &mdash; <code>stream&lt;T&gt;</code>, <code>future&lt;T&gt;</code>, and async functions &mdash; and the host runtime takes over scheduling with a single shared, completion-based event loop (in the style of <code>io_uring</code> and IOCP) instead of each component polling readiness through <code>pollable</code> handles. Wasmtime 46 ships these interfaces with async enabled by default, and <code>jco</code> is bringing the same model to JavaScript hosts.</p><h3>Why this fits Io unusually well</h3><ul><li><strong>Blocking I/O stops blocking the VM</strong> &mdash; today's build targets WASI preview1, where every read and write is synchronous. A WASM module has a single thread of execution, so one coroutine waiting on I/O stalls every other actor in the VM. Under WASI 0.3 the host event loop owns the wait: a coroutine that performs I/O can be parked on a <code>future&lt;T&gt;</code> while the scheduler runs other coroutines, then resumed when the host completes the operation.</li><li><strong>The stackless evaluator already paid the entry fee</strong> &mdash; most language runtimes need the Asyncify transform (which inflates code size and slows execution) or compiler-level async/await to suspend mid-call inside WASM. Io's evaluator keeps all execution state in heap-allocated frames, so suspending on a host future is the same operation as an ordinary coroutine switch &mdash; no binary transform, no annotations. WASI 0.3 was explicitly designed to accommodate both stackful and stackless coroutine runtimes; Io is in the second camp by construction.</li><li><strong>Actors map directly onto host futures</strong> &mdash; Io's <code>@</code> (futureSend) and <code>@@</code> (asyncSend) already give programs a future-based concurrency surface. A WASI 0.3 <code>future&lt;T&gt;</code> is the host-level version of the same idea, so an Io future awaiting a network response could be backed one-to-one by a host future and consume no VM scheduling at all until completion.</li><li><strong>Streams replace the polling dance</strong> &mdash; WASI 0.2 I/O required a three-step <code>start</code>/<code>finish</code>/<code>subscribe</code> pattern over pollables. 0.3 collapses that into <code>stream&lt;T&gt;</code> values paired with completion futures that can finally distinguish &ldquo;stream closed&rdquo; from &ldquo;stream failed.&rdquo; Io's <code>File</code> and any future socket primitives map onto these cleanly.</li><li><strong>The VM as a component</strong> &mdash; packaging <code>io_static.wasm</code> as a WebAssembly component gives it typed, language-neutral interfaces. Components compose in-process (&ldquo;service chaining&rdquo;), so an Io component could sit in a pipeline next to components written in Rust or Go with nanosecond rather than millisecond call overhead &mdash; a substantial upgrade to the embedding story above.</li></ul><p>The migration path is incremental rather than architectural, and the first two steps have landed. The VM now also builds as a real <strong>WASI 0.2 component</strong> (<code>make component</code>, via wasi-sdk's <code>wasm32-wasip2</code> target) that passes the full correctness suite under wasmtime. And the scheduler gained a <strong>timer queue</strong>: <code>wait</code> parks the calling coroutine instead of busy-spinning, timed coroutines run concurrently, and when nothing is runnable the VM blocks in a single host wait until the nearest deadline &mdash; the one idle point where a WASI 0.3 <code>future&lt;T&gt;</code> will be awaited instead. The remaining step &mdash; backing that idle point and the I/O primitives with 0.3 futures and streams &mdash; is gated on host and C-toolchain support (Wasmtime 46 and <code>future&lt;T&gt;</code>/<code>stream&lt;T&gt;</code> lowering from C), a change the Bytecode Alliance describes as &ldquo;entirely mechanical&rdquo; on the interface side.</p>"
},
{
"type": "ContentCards",
"columns": 2,
"items": ["Browser Target", "DOM Interop"]
"items": [
"Browser Target",
"DOM Interop"
]
}
]
}
Loading
Loading