WebAssembly Target [WASM-TARGET]
osprey --target=wasm32 --compile emits a wasm32-wasip1 command module.
The backend reuses Osprey's LLVM IR, compiles it with clang, and links it with
wasm-ld, wasi-libc, and the portable Osprey runtime archive.
The portable language core runs under a WASI host. CI compiles and validates the
hello fixture, runs it through Node's WASI host and the browser shim, then runs
OSPREY_TARGET=wasm32 zsh crates/run_test_corpus.sh — the same corpus harness
the native backend uses, pointed at the other code generator. It compiles every
program under tests/ to wasm32, runs it under Node's WASI host, and compares
stdout byte-for-byte to the same .expectedoutput golden the native run must
match. The harness classifies an undefined symbol compile error as SKIP and
names each one; every other build or runtime error fails. CI requires
TEST_CORPUS_FAIL=0, TEST_CORPUS_GOLDEN_FAIL=0 and
TEST_CORPUS_GOLDEN_MISSING=0, plus a golden floor (103) so coverage cannot
quietly shrink.
The wasm runtime includes strings, persistent collections, JSON, test and coverage hooks, the effect-handler stack, profiler stubs, and the browser host bridge. It excludes fibers, sockets, HTTP/WebSocket, process and file APIs, terminal APIs, FFI, random/input, and resumable effect continuations. A program that calls an excluded symbol fails at link time.
Target Triple [WASM-TARGET-TRIPLE]
The target is wasm32-wasip1 (the current spelling of wasm32-wasi). wasi-libc
provides the C allocation, string, formatting, memory, and stdout functions used
by the portable runtime. Browser execution uses the WASI Preview 1 shim in
examples/wasm/wasi-shim.mjs.
Target-Neutral LLVM IR [WASM-TARGET-IR]
Osprey's textual IR contains no target triple or data layout; clang supplies
both. Osprey int remains i64. Heap handles round-trip through i64, so a
32-bit wasm pointer is zero-extended when boxed and truncated when recovered.
LLVM computes target-specific aggregate offsets.
ILP32 Width Rules [WASM-TARGET-WIDTH]
Three paths require an explicit target-independent ABI:
- String length and concatenation call
osp_strlen, whose C implementation converts targetsize_tto Ospreyint(int64_t). - Integer formatting uses
%lld, because wasm32longis 32-bit while Ospreyintis 64-bit. - A returned
Result<T, E>is repacked to the declared success-slot type so its discriminator and error-pointer offsets agree on both LP64 and ILP32.
WASI Entry Point [WASM-ENTRY]
wasi-libc's command startup calls __main_void. The driver appends a thunk that
calls Osprey's i32 @main() and returns its status. The resulting module starts
through _start under wasmtime, Node's WASI implementation, or the browser
shim.
Compile and Link Pipeline [WASM-TARGET-LINK]
The driver:
- invokes clang with
--target=wasm32-wasip1 -O2 -c; - locates the WASI sysroot from
OSPREY_WASI_SYSROOT,WASI_SDK_PATH, or the supported platform paths; - invokes
wasm-ldwithcrt1-command.o, the program object,libosprey_runtime_wasm.a, and wasi-libc.
OSPREY_WASM_CC, OSPREY_WASM_LD, and OSPREY_WASM_RUN override the clang,
linker, and runtime commands. Direct linking does not require clang's wasm
compiler-rt archive.
Portable Runtime Archive [WASM-TARGET-RUNTIME]
make _runtime_wasm cross-compiles the units named by WASM_RT_SRC and creates
compiler/bin/libosprey_runtime_wasm.a. Static archive members are linked on
demand, so a command-line program that does not use the browser bridge has no
osprey_web imports.
Browser Host ABI [WASM-WEB-ABI]
The optional browser bridge passes NUL-terminated UTF-8 messages, conventionally JSON, across a coarse event/render boundary. Osprey declares:
extern fn osprey_web_render(message: string) -> int
extern fn osprey_web_command(message: string) -> int
compiler/runtime/web_runtime.c imports render(pointer) and
command(pointer) from the osprey_web import module. The host must decode the
message synchronously; each Osprey wrapper returns status 0.
A program may define the event entry point:
fn osprey_web_dispatch(message: string) -> int = 0
The linker always exports osp_alloc. When the dispatcher exists, it also
exports osprey_web_dispatch. For projects, the driver discovers the flattened
mangled function and emits a stable forwarding thunk. The host allocates a
NUL-terminated message with osp_alloc, refreshes its view of memory.buffer,
copies the bytes, and calls the exported dispatcher. At the JavaScript boundary
the i64 allocation size and dispatcher status use BigInt.
Effect Support [WASM-TARGET-EFFECTS]
The handler-stack portion of effects_runtime.c is portable and is included in
the wasm archive. Resumable continuations use pthreads and are compiled out
under __wasm__; an expression that needs __osprey_coro_* therefore fails to
link. The golden harness classifies that known undefined-symbol case as SKIP.
Memory Backend [WASM-TARGET-MEMORY]
The wasm archive contains memory_runtime.c, the same non-reclaiming
osp_alloc implementation used by native --memory=default. The wasm driver
does not receive the parsed --memory value, so --memory=gc and
--memory=arc also link this default archive.
The native conservative collector is not in WASM_RT_SRC: it depends on native
stack/register/data-segment scanning, setjmp, and pthread synchronization.
Osprey also does not emit WebAssembly-GC reference types; its values live in
ordinary wasm linear memory.
Verification
cargo test -p osprey-cli wasm::testsmake wasmwasm-validate examples/wasm/build/hello.wasmnode scripts/wasm-smoke.mjs examples/wasm/build/hello.wasm examples/wasm/hello.expectedoutputnode scripts/wasm-browser-smoke.mjs examples/wasm/build/hello.wasm examples/wasm/hello.expectedoutputOSPREY_TARGET=wasm32 zsh crates/run_test_corpus.sh(ormake _test_wasm_goldens)
The CI wasm job runs the validate, Node-WASI, browser-shim, and golden-harness
checks with a pinned WASI sysroot.