WebAssembly Back-End (wasm32 / wasm64)
This document describes the shipped WebAssembly back-end, the target conventions it uses today, and the remaining ABI boundaries for wasm targets.
Description#
The Silk compiler includes a wasm32 back-end that emits final .wasm
modules from the compiler IR, plus a smaller constant-only fallback path:
- Implementations:
- IR-backed wasm backend (primary path),
- constant-only emitter (fallback path).
- Supported targets:
wasm32-unknown-unknown:- emits a
.wasmmodule exportingmemoryplus exported functions, - when a valid
mainexists, exportsmainfor embedder use. wasm32-wasi:- emits a
.wasmmodule exportingmemoryand_start () -> void, - imports
wasi_snapshot_preview1.proc_exit (exit_code: i32) -> void, _startcalls Silkmainand then callsproc_exitwith the wrapped exit code,- process arguments are read inside parameterless
main()throughstd::args::{argc,argv,current}, backed by WASIargs_sizes_get/args_get. - Export-only modules (no
main): - emit a
.wasmmodule exportingmemoryplus each supportedexport fnin the root package (suitable for JS/Node-style embedding). - FFI mapping (WASM):
ext foo = fn (...) -> ...;becomes an imported functionenv.foo,ext bar = T;becomes an imported globalenv.bar(scalarT).- Shipped capabilities:
- supports multi-module builds (packages + file imports),
- emits static data into the wasm data section (string/byte blobs and other lowered constants),
- supports structured control flow (if/while/break/continue) for the shipped IR lowering path.
- does not yet support the concurrency runtime on wasm targets (
task/asyncare not lowered to a wasm-native scheduler); programs using concurrency constructs remain outside the wasm backend surface and are rejected during code generation.
The CLI exposes these targets via silk build --target ... and the shorthand
silk build --arch wasm32|wasm32-wasi (see CLI reference
and silk(1)).
Quickstart#
WASI executable#
import io from "std/io";
fn main () -> int {
io::println("hello from wasm wasi");
return 0;
}
silk build main.slk --target wasm32-wasi -o build/app.wasm
Embedder-facing module#
export fn add (a: int, b: int) -> int {
return a + b;
}
silk build math.slk --target wasm32-unknown-unknown -o build/math.wasm
The resulting module exports the supported export fn surface from the root
package and can be loaded by a JS or native wasm embedder.
Output Model#
Module kinds#
The shipped back-end emits a final .wasm module (not a relocatable object),
analogous to the current linux/x86_64 “emit a final ELF image” approach.
Relocatable “wasm object” emission is not part of the documented interface; it would require relocation sections and a defined Silk↔WASM link model.
Entry points#
We need two distinct entrypoint conventions:
wasm32-wasi:- emit a
_startfunction (no parameters, no results), _startcalls Silkfn main () -> intand then imports/calls WASIproc_exit(exit_code),- argv access stays inside
main()throughstd::args;_startdoes not receive Silk parameters. wasm32-unknown-unknown:- export an Silk
mainfunction for embedder use. - Silk
intlowers as wasmi64, somain’s return type isi64unless a target-specific wrapper is introduced.
The CLI/ABI must document which convention is used for each target.
Export-only modules (embedder mode)#
For embedder-driven environments (especially wasm32-unknown-unknown / JS),
the toolchain also supports emitting a wasm module with no entry point
(main / _start) when the root package contains exported functions.
In this mode, the compiler emits a .wasm module that exports each supported
export fn declaration from the root package as a wasm export (with parameters
and results lowered according to the current scalar ABI).
Notes:
- For
wasm32-wasi, export-only modules are intended for embedding; they do not include an_startwrapper and are not directly runnable as WASI executables.
Types, Layout, and Memory#
Integer and float types#
intmaps to:i64in wasm32/wasm64 backends (matching current compiler semantics).- Fixed-width ints map to their obvious wasm integer types:
u8/i8/u16/i16/u32/i32lower toi32values (with masking/sign rules applied in codegen),u64/i64lower toi64values.f32andf64map to wasmf32/f64.boolmaps toi32(0/1).
Pointers and string#
Silk’s back-end assumes 64-bit pointers (u64) for native targets. For WASM,
pointer width depends on the target:
wasm32: pointers areu32byte offsets into linear memory.wasm64: pointers areu64byte offsets into linear memory.
string is represented as (ptr, len) and, at the C ABI boundary, as
SilkString { ptr, len }. For WASM:
- In
wasm32, the natural representation is(u32 ptr, i64 len)(or(u32,u32)if we later choose a fully-32-bit ABI for wasm-only code). - In
wasm64,(u64 ptr, i64 len)matches the existing layout.
The chosen WASM ABI for strings must be documented and kept stable.
Static data#
- String literals and other constant data should be emitted into the wasm data section and referenced by linear-memory offsets.
- The compiler must define a deterministic data layout (alignment rules) so field access and pointer arithmetic remain correct.
Calls, Imports, and FFI#
Internal calls#
Internal calls lower to direct wasm calls using wasm’s native calling convention (stack machine with typed locals), with the compiler responsible for lowering Silk IR values onto the wasm value stack.
ext declarations#
ext declarations should map to wasm imports:
- Each
ext foo = fn (...) -> ...;becomes an imported function with a stable module/name convention (for exampleenv.fooby default). - Each
ext bar = T;(external global) becomes an imported global when the environment supports it, or a function-based accessor in environments that do not.
The module/name convention and supported import surface must be documented in
External declarations (ext) (WASM-specific subsection) and in
CLI reference.
WASI integration#
For wasm32-wasi, shipped facilities such as std::io, std::args,
std::env, and supported std::fs operations dispatch through the
target-specific std::runtime::wasi::* modules rather than the POSIX
libc-facing runtime used on native targets. Filesystem access follows WASI
preopen rules. Alternate stdlib roots remain supported through --std-root /
--nostd; any prebuilt stdlib archive is target-ABI-specific.
Tooling and Testing Strategy#
- Zig tests cover constant and IR-backed final modules, imports/exports, control flow, static data, and WASI entrypoint structure.
- Node’s WASI runtime executes end-to-end tests for stdout and exit status, process arguments, environment access, preopened filesystem round trips, metadata, and working-directory behavior when Node is available.
- C99 embedding tests cover in-memory WebAssembly output and export-only
modules through
libsilk. - Tests stay target-scoped and do not require network access.
Design goals#
- Keep final-module emission as the stable default interface for wasm targets.
- Define and document a stable Silk↔WASM ABI for exports, imports, strings, and static data layout.
- Extend the target story to
wasm64only once pointer-width and ABI decisions are validated end-to-end. - Add relocatable/object emission only alongside an explicit relocation and link model.
Source repository · Edit this page · View Markdown