Status: Phase 1 of 0.18 "Span". This document is the contract. Anything not on this page is out of scope until the contract is updated and a decision block in
decisions/architecture.avsays otherwise.
--target wasip2 produces a WebAssembly Component (.component.wasm plus a sibling .wit) that imports WASI 0.2 worlds directly — no preview-1 adapter, no compatibility bridge. Aver effects lower to WIT/WASI imports; Aver values stay private inside the core module; WIT/canonical ABI is the only thing the host sees. The component is intended for WASI 0.2 Component Model hosts such as wasmtime, Spin, NGINX Unit, wasmCloud, and Fermyon Cloud. Exact support depends on the world and interfaces used by the generated WIT — Component Model alone is not sufficient; the host also has to provide the specific interfaces the world declares.
This is not a general "export Aver as a WIT library" feature. In 0.18 the only public export shape is wasi:cli/run (the entry function the wasi:cli/command world requires). Arbitrary Aver functions are not exported as WIT interfaces. The component is something a host runs, not a typed library other components link against.
| Target | Job | Hosts |
|---|---|---|
--target wasm-gc |
Portable core wasm with engine GC + tail calls. Self-contained binary, host wires aver/* imports. |
Browsers (Chrome 119+, Firefox 120+, Safari 18.2+), Cloudflare Workers (via --preset cloudflare --handler <fn>), Node 22+, Deno, Bun, embedded wasmtime |
--target wasip2 |
WASI 0.2 component whose public import/export surface is described by WIT. The wasm-gc emitter produces core imports/exports in canonical-ABI-compatible shapes; component-type metadata declares which WIT world they correspond to; ComponentEncoder builds the actual component boundary from the two. |
wasmtime, Spin 3.x, NGINX Unit, wasmCloud, Fermyon Cloud — any host that takes a .component.wasm AND provides the world's interfaces |
Cloudflare Workers and browsers do not run components natively; they stay on --target wasm-gc. --preset cloudflare is a wasm-gc preset and stays that way.
The preview-1 component adapter is the right tool for migrating existing preview-1 wasm modules into the Component Model — it preserves their original ABI and translates calls at the boundary. Aver does not need that. Aver effects are typed and declared in source (! [Console.print, Time.unixMs]); there is no preview-1 ABI to preserve. Routing through the adapter would just replicate one compatibility shim with another. Direct WIT lowering is the natural shape for a language whose effects already declare the host capabilities the component imports.
Aver effect call site
└─► Aver-side glue
Aver value (GC string / list / record / variant / option / result)
is marshalled into the canonical ABI's core boundary representation:
ptr+len, retptr, handle (i32), tag+payload, …
core wasm import / export
└─► plain core wasm types and signatures
i32 / i64 / f32 / f64 / refs as applicable
canonical-ABI-compatible shapes
component-type metadata
└─► describes which WIT world / interface those core signatures
correspond to. Embedded as a `component-type:<world>` custom
section in the core module via `wit-encoder`.
ComponentEncoder
└─► builds the actual component boundary from core module + metadata.
Produces `.component.wasm`. The host sees the WIT view; it never
sees Aver's runtime object layout.
The wasm-gc emitter does not implement the Component Model boundary. It emits core imports/exports in canonical-ABI-compatible shapes and inserts Aver-side glue at effect call sites. Component-type metadata says what those core signatures mean in WIT. wit_component::ComponentEncoder does the actual lifting/lowering at component build time.
Eight properties every --target wasip2 build must satisfy:
- Imports are declared effects only. Every WIT import in the component is justified by at least one declared Aver effect (
! [...]), and every declared effect either lowers to one or more WIT imports in the selected world or is rejected at compile time. A single Aver call may translate into several WIT calls in the generated glue (e.g.,Console.printcache + stream write); a single WIT interface may serve many Aver effects (e.g.,wasi:io/streamsfor both stdout writes and stdin reads). No silent capability creep, no host hooks beyond what the source asks for. - Exports are the handler shape only. A program with a
mainfunction exportswasi:cli/run; a program compiled with--world wasi:http/proxy(Phase 3 / 0.19) exportswasi:http/incoming-handler. No internal Aver functions, types, or runtime helpers leak out as public exports. - All public ABI goes through WIT. Anything that crosses the component boundary uses canonical WIT types: strings, lists, records, variants, results. No Aver-specific encoding.
- No Aver values cross the boundary. Per-instantiation
Map<K, V>,List<T>,Vector<T>,Option<T>,Result<T, E>, tuples, records, and variants stay inside the user core module. The canonical ABI for engine-GC types is still pre-proposal upstream; we do not encode anything that would break when it lands. - Generated WIT is emitted next to the artifact.
aver compile --target wasip2 -o outproducesout/<name>.component.wasmandout/<name>.wit. The WIT is human-readable and is the source of truth for what the component imports and exports — no hidden surface in custom sections. - Component validates with
wasm-tools.wasm-tools validate --features component-model out/<name>.component.wasmexits zero on every artifactaver compile --target wasip2produces. Bench scenarios and example programs are gated on this in CI. - WASI resources stay implementation-internal. Stdout / stderr
output-streamhandles, filesystem descriptors, pollables, and similar resource handles may be cached and reused inside the per-effect glue. They are not exposed as Aver-level values. There is noResource<T>/Handle<T>/Stream<T>type on the Aver surface in 0.18. Adding one is a deliberate language decision for 0.19+, not a side effect of WIT lowering. - Filesystem access is preopen-scoped.
Disk.*paths resolve only against WASI preopened directories. Absolute paths and paths that escape preopens returnResult.Err("path not preopened")(a dynamic host capability gap, distinct from compile-time rejects). The Aver source-levelDiskAPI stays unchanged; the wasip2 lowering enforces the WASI capability model at the boundary.
Compiles the source to a wasi:cli/command component, instantiates it via embedded wasmtime, and runs the wasi:cli/run export:
- Effects are recorded at the Aver call level, above the WIT import boundary. Recordings are interchangeable with VM, wasm-gc, and self-host traces (same
recording.jsonshape since 0.16.1). - Diagnostics are Aver-shaped. Wasmtime trap messages translate through the same path that
aver run --wasm-gcuses today; users see Aver source spans, not core-wasm offsets. - No build cache. Compile is fast enough that adding a cache layer is not worth the cache-invalidation contract.
--record <dir>/--replay <recording.json>are not yet wired for--wasip2and the flags are rejected at CLI time. Recording requires a separate plumbing pass against the canonical-ABI WASI imports; until that lands, useaver run --wasm-gc --record(recordings are interchangeable across backends). The earlier sentence about effects being recorded "at the Aver call level" describes the cross-backend recording shape, not what--wasip2itself accepts.
External hosts: wasmtime run for command components is the canonical path; wasmtime serve is the canonical local runner for the HTTP/proxy world.
The component the wasm-gc backend emits uses the WebAssembly wasm-gc + tail-call proposals. WASI 0.2 itself is stable and supported across hosts, but those two engine proposals are still opt-in on most runtimes — pick a host that ships them enabled (or enables them via flag). Verified against the eight tests in tests/wasip2_http_server_stress.rs (echo / large body / routing / method dispatch / headers / JSON / concurrent / sequential).
| Host | Status | Notes |
|---|---|---|
wasmtime serve 43.x |
✅ works | Pass -W gc=y -W tail-call=y. The address binds via --addr=ip:port (e.g. --addr=127.0.0.1:8080). Bound port surfaces on stderr as Serving HTTP on http://...:N/ — useful for --addr=:0 ephemeral binds in test harnesses. |
Embedded wasmtime via wasmtime-wasi-http |
✅ works | Enable Config::wasm_gc(true) + Config::wasm_tail_call(true) on the engine, plumb a wasmtime_wasi_http::WasiHttpCtx. Same engine as wasmtime serve under the hood. |
jco serve (Bytecode Alliance) on Node ≥ 22 |
✅ works | npx @bytecodealliance/jco serve component.wasm --host 127.0.0.1 --port N. Transpiles the component to JS + core wasm modules and runs them on V8, which has wasm-gc + tail-call enabled since 22.0. Different engine entirely from wasmtime, so a passing run here confirms the component's portability across engine implementations — not just wasmtime variants. Node 20 rejects with Unknown type code 0x4e, enable with --experimental-wasm-gc; the flag can't be set via NODE_OPTIONS, so use Node 22+ rather than working around it. |
| Spin 3.5.x | ❌ rejected at load | Bundled wasmtime does not enable the wasm-gc proposal (rec group usage requires 'gc' proposal to be enabled). No user-facing flag to override; the runtime-config TOML has no [wasmtime] table. Tracks Spin upstream — once their bundled wasmtime turns the proposal on (or exposes a flag), the same .component.wasm will run unchanged. |
NGINX Unit 1.34.x (wasm-wasi-component) |
❌ rejected at load | Same root cause as Spin — Unit's wasm_wasi_component.unit.so module embeds wasmtime with the wasm-gc proposal off; tested via the unit:wasm Docker image. Identical error: rec group usage requires 'gc' proposal to be enabled. Lands automatically once Unit upgrades its bundled wasmtime build. |
| WasmEdge 0.16.x | ❌ component model experimental | --enable-component exists but the validator is still under construction; rejects our component with Alias export: Export index 0 exceeds available component instance index 0 before the wasm-gc / tail-call question even comes up. Re-test once their component-model validator stabilises. |
| Wasmer 7.x | ❌ no component model | error: ... encoded as a component but the WebAssembly component model feature is not enabled. Component support is not on Wasmer's near-term roadmap. |
wasmCloud wash 2.x |
wash dev is a mesh-deployment daemon expecting a full wasmCloud project + manifest, not a standalone serve. No quick equivalent of wasmtime serve component.wasm. A wasmCloud project around our component would presumably work (their host is wasmtime-based with GC enabled in recent versions), but takes a project scaffold to verify — out of scope for this round. |
|
| Fermyon Cloud, Fastly Compute | Cloud-only deployments — would need an account + push. Spec-compatible against wasi:http/proxy; whether each enables wasm-gc + tail-call depends on their bundled runtime build. |
The component itself is portable: the only host requirement is "WASI 0.2 wasi:http/proxy host with wasm-gc + tail-call proposals on". Future Aver work to widen host coverage waits on host updates, not codegen changes.
Tcp.* programs compile to the same wasi:cli/command world as the other CLI effects, but the runtime additionally needs wasi-sockets imports enabled. With wasmtime run:
wasmtime run \
-W gc=y -W tail-call=y \
-S inherit-network=y \
-S allow-ip-name-lookup=y \
-S tcp=y \
component.wasm
| Flag | Why |
|---|---|
-W gc=y -W tail-call=y |
Engine proposals — same requirement as the HTTP/proxy world. |
-S inherit-network=y |
Grants the guest access to the host's network stack (otherwise every wasi-sockets call returns the default-deny error). |
-S allow-ip-name-lookup=y |
Enables wasi:sockets/ip-name-lookup. Required even for IP-literal hosts like "127.0.0.1" — without it resolve-addresses rejects every input. |
-S tcp=y |
Enables wasi:sockets/tcp. Without it create-tcp-socket traps before the connect can start. |
-S udp=y is intentionally not needed: Aver's Tcp.* does not touch wasi:sockets/udp. Embedded wasmtime hosts get the same capability via WasiCtxBuilder::inherit_network() + allow_ip_name_lookup(true) + socket_addr_check(...).
Produces:
out/
<name>.component.wasm -- the component
<name>.wit -- generated WIT, human-readable
Flags:
--world <world>— which WIT world the component targets. Two values:wasi:cli/command(default — long-running process exportingwasi:cli/run.run) andwasi:http/proxy(HTTP server, exportingwasi:http/incoming-handler.handle; shipped in 0.19). The proxy world pairs with--handler <fn>(same flag the wasm-gc + Cloudflare path uses) — names the user fn with signatureFn(HttpRequest) -> HttpResponsethat becomes the proxy handler. The compile path is purely flag-driven;main's body can stay portable (HttpServer.listen(port, handler)runs the same source underaver runon the VM, lowers to a no-op when wasip2 proxy codegen takes over). Programs whose effects do not fit the chosen world fail at compile time withtarget-effect-unsupportedpointing at the offending call.--optimize {size,speed}— rejected on--target wasip2. Upstreamwasm-optdoes not yet handle wasm-gc + Component Model bytes cleanly, so the flag is refused at the CLI rather than silently dropped. Use--target wasm-gcif you need post-pass size/speed optimization; we will wire it for wasip2 once the toolchain catches up.
The compiler does not shell out. WIT emission goes through wit-encoder; component-type metadata is encoded via wit-component::metadata and embedded as a custom section in the core module; the actual component wrap goes through wit_component::ComponentEncoder. Single binary, no toolchain to install on the user's machine.
Aver effects lower directly to WASI 0.2 imports. The mapping is fixed per effect; a single Aver call at the source level may translate into one or several WIT calls in the generated glue (e.g., a Console.print may cache the stdout output-stream resource handle once and call wasi:io/streams.[method]write per print).
| Aver effect | WIT import (the glue calls into) |
|---|---|
Args.get |
wasi:cli/environment.get-arguments |
Env.get |
wasi:cli/environment.get-environment |
Env.set |
Compile-rejected — WASI 0.2 environment is read-only by design (no host can ever satisfy a write). Same "cannot-ever-support" category as Terminal.*. |
Console.print / error / warn |
wasi:cli/stdout.get-stdout / wasi:cli/stderr.get-stderr (cached) + wasi:io/streams.output-stream.[method]blocking-write-and-flush. 0.18 uses blocking write-and-flush for command-component semantics and simple replayability — one Console.* call ⇒ at most one host-side flush, easy to record/replay deterministically. WASI output-streams are fundamentally non-blocking with a polling model; blocking-write-and-flush is a binding-level convenience helper that bundles check-write + write + flush + subscribe/poll into one call. Buffered stdout/stderr could land later as an optimisation, but the semantic unit stays the Aver Console call. |
Console.readLine |
wasi:cli/stdin.get-stdin (cached) + wasi:io/streams.input-stream.[method]blocking-read |
Disk.readText / writeText / appendText / exists / delete / deleteDir / listDir / makeDir |
wasi:filesystem/preopens.get-directories (cached) + wasi:filesystem/types.[method]*. Paths outside preopens return Result.Err("path not preopened") — capability model, contract point 8. |
Time.now / unixMs |
wasi:clocks/wall-clock.now (Time.now formats RFC3339 guest-side via Howard Hinnant's civil_from_days) |
Time.sleep |
wasi:clocks/monotonic-clock.subscribe-duration + wasi:io/poll.poll + [resource-drop]pollable (per-call pollable, real wait — not busy-loop) |
Random.int / float |
wasi:random/random.get-random-u64 + Aver-side range scaling. This is the secure wasi:random/random interface (same contract as get-random-bytes, just returning 8 cryptographically-secure bytes packed into a u64); we deliberately do NOT use wasi:random/insecure.get-insecure-random-u64. If we later need finer byte-level control (e.g. for Random.bytes(n)), the switch to get-random-bytes is mechanical. |
Http.{get, head, delete, post, put, patch} |
wasi:http/outgoing-handler.handle + the future-incoming-response / incoming-response choreography (Phase 2 / 0.19 shipped). Method tag selects outgoing-request.set-method. Body-bearing verbs marshal a request body via request.body + outgoing-body.write + chunked blocking-write-and-flush + outgoing-body.finish. Headers (request and response) lower as Map<String, List<String>>; multi-valued field names preserve server emit order. error-code variant discriminants surface as per-variant http: <name> Err messages (39 cases). |
HttpServer.listen |
wasi:http/incoming-handler.handle export (Phase 3 / 0.19 shipped). Requires --world wasi:http/proxy --handler <fn>. The handler wrapper decodes the host-supplied incoming-request into an Aver HttpRequest (method via the 10-case variant, path-with-query split into path/query, headers iteration as Map<String, List<String>>, body via incoming-body.stream + drained input-stream.blocking-read), runs the user's fn(HttpRequest) -> HttpResponse, marshals the result into an outgoing-response (outgoing-response constructor + set-status-code + body via outgoing-body.write + chunked blocking-write-and-flush + outgoing-body.finish), and calls response-outparam.set. Content-Length is synthesised from the response body byte count. The port argument to HttpServer.listen in source is honoured by the VM but ignored by wasip2 codegen — the host's listener flag (wasmtime serve --addr=:N etc.) binds the socket. |
HttpServer.listenWith |
Compile-rejected — deferred one iteration; requires per-instance wasm-global context plumbing. |
Tcp.{connect, close, writeLine, readLine, send, ping} |
wasi:sockets/{instance-network, ip-name-lookup, tcp-create-socket, tcp} (Phase 4 / 0.20 "Pulse" shipped, hardened through five peer-review passes). __rt_tcp_connect walks lazy-network init → resolve-addresses → async pollable loop → first-IPv4 → create-tcp-socket → start/finish-connect → pool-slot allocation via first-free scan → Tcp.Connection materialise. The 256-slot pool refuses the 257th simultaneous connect with Err("tcp: connection limit reached (256 max)") (matches aver-rt::tcp::connect's HashMap-len gate); a closed slot is reusable immediately. Tcp.close drops streams + shutdown + drops socket; subsequent close on the same handle surfaces Err("tcp: unknown connection"). writeLine/readLine thread bytes through chunked blocking-write / 1-byte blocking-read against the pooled streams; writeLine appends \r\n and readLine strips the trailing \r only when terminated by \n (intra-payload CR preserved). Stale-handle paths on writeLine/readLine (null pool, slot-scan miss, in_use == 0) surface Err("tcp: unknown connection") so callers can tell a closed handle apart from a real wasi-side write failure or peer EOF. Tcp.send is fully ephemeral — inline DNS + socket + connect (no pool slot), raw write on the wire (no \r\n appended) + shutdown(send), read-until-EOF capped at 10 MiB; stream errors split as stream-error.last-operation-failed → Err("tcp: stream error") vs stream-error.closed → Ok(buf). Tcp.ping is also ephemeral — same inline dial as send minus the read/write phase, drop streams + socket on success, return Result.Ok(()); no pool slot, so a program holding 256 live Tcp.connect handles can still ping. Tcp.Connection is opaque from the surface (the type checker rejects construction and field reads). IPv6 yields from the resolver are skipped (first-IPv4-wins); no in-line subscribe-duration connect timeout in v1. |
Terminal.* (12 methods) |
Compile-rejected — WASI 0.2 has no raw/cooked-mode operations |
The axis is static target capability vs dynamic host capability. Result.Err stubs are reserved for dynamic host capability gaps: missing preopen (Disk.readText("/etc/passwd") on a host that didn't preopen /), missing env var, denied permission. A target that cannot ever support an effect is a different category and gets a different shape — a compile-time target-effect-unsupported error.
In 0.18:
Terminal.*— WASI 0.2 haswasi:cli/terminal-inputandterminal-outputas TTY signals, but no standardised raw/cooked-mode operations (set-raw-mode,set-echo,get-window-size). The capability is structurally absent.Env.set— WASI 0.2 environment is read-only. There is no host implementation that could ever satisfy a write. Silent no-op would be a trap: source declares "I set X" and the program runs as if it succeeded while the environment is unchanged.HttpServer.listenWith— deferred one iteration; requires per-instance wasm-global context plumbing.
(Earlier 0.18 betas grouped Time.sleep with the structural rejects on the assumption that the pollable model was out of scope. That was a scoping mistake — pollables can be wrapped inside a single helper without leaking to source. Phase 1.4c shipped __rt_time_sleep doing exactly that, so Time.sleep lowers natively now.)
Compile output for any of these:
error[target-effect-unsupported]:
Terminal.readKey requires raw terminal input.
--target wasip2 does not provide Terminal effects.
Use:
--target wasm-gc for browser/interactive terminal hosts
aver run on VM for local terminal programs
Or replace Terminal.* with Console.* / Args / stdin-compatible APIs.
| Phase | Scope | Status |
|---|---|---|
| 0 | Audit legacy coupling, wire wit-component/wit-encoder deps, prove the wrap pipeline |
✅ shipped |
| 1.0 / 1.1 | --target wasip2 CLI plumbing, end-to-end pipeline for no-effect programs |
✅ shipped |
| 1.2 | wasi:cli/stdout + wasi:io/streams glue. Console.print / error / warn → stream write end-to-end |
✅ shipped |
| 1.3 | wasi:cli/stdin + wasi:cli/environment. Console.readLine / Args.get / Env.get |
✅ shipped |
| 1.4 | wasi:clocks/wall-clock.now for Time.now / Time.unixMs; wasi:random for Random.*; wasi:clocks/monotonic-clock.subscribe-duration + wasi:io/poll.poll for Time.sleep. |
✅ shipped |
| 1.5 | wasi:filesystem. All seven Disk.* methods (exists / readText / writeText / appendText / delete / deleteDir / makeDir / listDir). Paths resolve relative to the cached preopen. |
✅ shipped |
| 1.6 | Reject Terminal.* / Env.set at compile time as permanent (WASI 0.2 has no terminal interface; environment is read-only). Http.* / Tcp.* / HttpServer.* deferred to 0.19+. |
✅ shipped |
| 1.7 | aver run --wasip2 (embedded wasmtime + wasmtime-wasi) with CWD preopened as . |
✅ shipped |
| 1.8 | Drop the legacy --target wasm backend (src/codegen/wasm/, wasm-legacy feature, --bridge flag, wasm-runtime subcommand, legacy bundling in src/main/commands.rs) |
✅ shipped |
- Outgoing HTTP (
wasi:http/outgoing-handler) — Phase 2 / 0.19. Direct WIT lowering, same mechanism as Phase 1; just more types to marshal. - HTTP server (
wasi:http/incoming-handler/wasi:http/proxyworld) — Phase 3 / 0.19 or 0.20. Different export shape (handler exposes WIT export, host calls in). - TCP sockets (
wasi:sockets/tcp) — Phase 2 / 0.19. Open question whether Aver wants long-lived socket handles as a language concept. - Resources / streams / pollables on the Aver surface — implementation only in 0.18. If Aver grows a
Resource<T>type, that is a deliberate language decision for 0.19+. - WASI 0.3 — async ABI /
future<T>/stream<T>are real but not finalised. 0.2 hosts will be virtualised by 0.3 hosts per upstream commitment, so we lose nothing by waiting. wasi:keyvalue,wasi:logging,wasi:config,wasi:tls,wasi:blobstore,wasi:nn— none.- Cross-component shared runtime — requires GC types in the canonical ABI; that proposal is upstream pre-proposal. Per-instantiation helpers stay inline.
jco transpileas a derived target for browsers / Node — possible 0.19+ if there is concrete demand.
- WASI 0.2 release tracker: https://github.com/WebAssembly/WASI/releases
- Component Model spec: https://github.com/WebAssembly/component-model
wit-componentcrate: https://docs.rs/wit-componentwit-encodercrate: https://docs.rs/wit-encoderwit_component::metadata(custom section encoding): https://docs.rs/wit-component/latest/wit_component/metadata/- GC in canonical ABI (pre-proposal): WebAssembly/component-model#525