WASM and WASI
Nexus compiles to the WebAssembly Component Model with WASI for system interfaces. The coeffect system maps directly to WASI capabilities – every require { PermX } in your program corresponds to a concrete WASI interface grant.
Permission-to-Capability Mapping
| Nexus Permission | WASI Capability | CLI Flag | Enforcement |
|---|---|---|---|
PermConsole |
stdin, stdout, stderr |
--allow-console |
Enforced |
PermFs |
wasi:filesystem/preopens |
--allow-fs |
Enforced |
PermNet |
wasi:http/outgoing-handler, wasi:sockets/* |
--allow-net |
Enforced |
PermRandom |
wasi:random/random |
--allow-random |
Statically checked |
PermClock |
wasi:clocks/wall-clock, monotonic-clock |
--allow-clock |
Statically checked |
PermProc |
wasi:cli/exit, environment |
--allow-proc |
Statically checked |
PermEnv |
wasi:cli/environment |
--allow-env |
Statically checked |
Capability Enforcement
Static Verification
The type checker ensures:
- Any function calling a capability-requiring port must itself
requirethat capability or have it satisfied viainject main’srequireclause is the source of truth for the entire program’s capability surface
Binary Encoding
Required permissions are stored in a custom WASM section named nexus:capabilities:
Section name: "nexus:capabilities"
Data format: UTF-8 newline-separated capability names
Example: "Fs\nNet\nConsole\n"
This allows tools to inspect required permissions without executing the binary.
Runtime Enforcement
The Nexus runtime (via wasmtime) configures the WASI context based on declared capabilities:
- Filesystem isolation: if
PermFsis not required, no directories are preopened - Network isolation: if
PermNetis not required, network interfaces are not inherited - Console isolation: if
PermConsoleis not required, stdio is not inherited
ABI
This section documents how Nexus values are represented in WebAssembly. Understanding the ABI is necessary for writing FFI bindings and debugging compiled output.
Type Mapping
Every Nexus type maps to a WASM value type:
| Nexus Type | WASM Type | Representation |
|---|---|---|
i32 |
i32 |
Direct |
i64 |
i64 |
Direct |
f32 |
f32 |
Direct |
f64 / float |
f64 |
Direct |
bool |
i32 |
0 = false, 1 = true |
char |
i32 |
Unicode scalar value |
unit |
(none) | No runtime value |
string |
i64 |
Packed pointer + length |
[T] (list) |
i64 |
Heap pointer |
[| T |] (array) |
i64 |
Heap pointer |
{ ... } (record) |
i64 |
Heap pointer |
| ADT variant | i64 |
Heap pointer |
closure / fn(...) |
i64 |
Heap pointer |
All heap-allocated values (records, ADTs, lists, arrays, closures) are represented as i64 pointers into linear memory. Primitives smaller than 64 bits (bool, char, i32, f32) use their native WASM types.
String Encoding
Strings are packed into a single i64:
bits 63-32: offset (u32, pointer into linear memory)
bits 31-0: length (u32, byte count)
Pack: (offset << 32) | length
Unpack: offset = value >>> 32, length = value & 0xFFFFFFFF
String literal bytes are written to the WASM data section starting at offset 16. Literals are deduplicated by value. The heap base is aligned to 8 bytes after all string data.
Memory Layout
Offset 0-15: Reserved (WASM header)
Offset 16-N: String literals (data section)
Offset align8(N): Heap base — objects grow upward
Allocation strategy:
- With stdlib.wasm: Calls
allocate(bytes: i32) -> i32from the stdlib module. This uses dlmalloc internally. - Without stdlib: Bump allocator using WASM global 0 as the heap pointer. Grows memory on demand via
memory.grow.
All heap objects are stored in 8-byte words.
Heap Object Layout
Constructor (ADT variant)
Word 0: i64 tag
Word 1: i64 field[0]
Word 2: i64 field[1]
...
The tag is computed via FNV-1a: hash(name) ^ arity * FNV_PRIME. Pattern matching compares tags with i64.eq.
Field ordering: Fields are stored in lexicographic order by field name. When a constructor is created with labeled arguments (e.g., Cons(v: x, rest: xs)), the arguments are sorted before storage. Field extraction via pattern matching uses the same sorted index. List literals [a, b, c] desugar to Cons with positional arguments in this sorted order.
Record
Same layout as constructors:
Word 0: i64 tag
Word 1: i64 field[0] (sorted by field name)
Word 2: i64 field[1]
...
The tag is fnv1a("rec" + sorted_field_names) ^ field_count * FNV_PRIME. Fields are stored in lexicographic order by field name.
Closure
Word 0: i64 funcref table index
Word 1: i64 captured value[0]
Word 2: i64 captured value[1]
...
Closures are called via call_indirect. The closure pointer is passed as the first argument (__env: i64), and the callee loads captured values from __env at the appropriate offsets.
Value Packing
When storing values in heap objects, all values are normalized to i64:
| Source Type | Pack to i64 | Unpack from i64 |
|---|---|---|
i64 |
(no-op) | (no-op) |
i32 |
i64.extend_i32_s |
i32.wrap_i64 |
f64 |
i64.reinterpret_f64 |
f64.reinterpret_i64 |
f32 |
i32.reinterpret_f32 then i64.extend_i32_u |
i32.wrap_i64 then f32.reinterpret_i32 |
unit |
i64.const 0 |
(error) |
Calling Convention
- Internal functions: All labeled parameters and arguments are sorted lexicographically by label. Both the function signature and call sites use the same sorted order.
- External functions (FFI): Parameters stay in source (definition) order to match the stdlib WASM ABI. Call arguments are matched to external parameters by label, not by position.
unitparameters generate no WASM parameter.unit-returning functions have an empty WASM result type.- Tail calls use WASM
return_callwhen not inside atryblock.
Indirect Calls (Closures)
Closure calls use call_indirect with an extended signature. The first parameter is always __env: i64 (the closure heap pointer):
// Nexus: let f = fn (x: i64) -> i64 do ... end
// WASM type: (param i64 i64) (result i64)
// ^env ^x
The caller pushes __env, then the normal arguments, then the table index as i32 for call_indirect.
Exception Model
Exceptions use two WASM globals (not WASM exception handling):
(global $exn_flag (mut i32) (i32.const 0)) ;; 0 = no exception
(global $exn_value (mut i64) (i64.const 0)) ;; exception object pointer
Raise: stores the exception value, sets the flag to 1, returns a dummy value.
Catch: after each statement, checks $exn_flag. If set, jumps to the catch handler, clears the flag, and binds $exn_value to the catch parameter.
FFI Boundary
External functions use a different parameter encoding for strings and arrays. The packed i64 representation is split into separate pointer and length parameters:
| Nexus Type | WASM Params (FFI) | Notes |
|---|---|---|
i32, bool, char |
1x i32 |
Direct |
i64 |
1x i64 |
Direct |
f32 |
1x f32 |
Direct |
f64 |
1x f64 |
Direct |
string |
2x i32 (ptr, len) |
Unpacked from packed i64 at boundary |
[| T |] (array) |
2x i32 (ptr, len) |
Same unpacking as string |
%ByteBuffer, opaque |
1x i64 |
Handle passed directly |
unit |
(none) | No parameter generated |
Parameter order: external function parameters preserve source (definition) order, not lexicographic order. This is because the WASM function type signature must match the stdlib export exactly. Call-site arguments are reordered to match by looking up each external parameter’s label.
Return values use the same types as internal functions (strings return as packed i64).
Module Structure
Exports
| Name | Kind | Condition |
|---|---|---|
main |
function | Always (entry point) |
wasi:cli/run@0.2.6#run |
function | Always (WASI run command) |
memory |
memory | If memory is defined or imported |
__conc_<name> |
function | If program uses conc blocks |
Imports
| Module | Name | Condition |
|---|---|---|
<module>.wasm |
<wasm_name> |
Each external binding |
| stdlib module | allocate |
If objects + stdlib present |
nexus:runtime/conc |
__nx_conc_spawn, __nx_conc_join |
If conc blocks present |
nexus:runtime/backtrace |
__nx_bt_push, __nx_bt_pop, __nx_bt_freeze |
If raise/try present |
Custom Sections
| Section Name | Format | Purpose |
|---|---|---|
nexus:capabilities |
UTF-8 newline-separated names | Declared runtime permissions |
Funcref Table
If the program uses closures or function references, a funcref table is emitted:
- Element type:
funcref - Size: number of unique function references
- Active initialization at offset 0
- Used by
call_indirectfor closure dispatch
Nexus Host Bridge
For HTTP networking, Nexus includes a host bridge component (nexus_host_bridge) that translates Nexus FFI calls (prefixed with __nx_http) into WASI HTTP component calls.
When a program requires PermNet, the compiler automatically composes this bridge into the final WASM component. This ensures the Net port works on any WASI-compliant host.
Building and Running
Compile to WASM
nexus build program.nx # outputs main.wasm
nexus build program.nx -o output.wasm # custom output path
The build step requires wasm-merge for dependency bundling. Configure via --wasm-merge PATH or the NEXUS_WASM_MERGE environment variable.
Run with wasmtime
# Minimal (no capabilities)
wasmtime run -Scli main.wasm
# With network
wasmtime run -Scli -Shttp -Sinherit-network -Sallow-ip-name-lookup -Stcp main.wasm
# With filesystem preopens
wasmtime run -Scli --dir ./data main.wasm
Inspect Capabilities
nexus build program.nx --explain-capabilities # list capability names (default)
nexus build program.nx --explain-capabilities=wasmtime # show wasmtime run command with flags
nexus build program.nx --explain-capabilities=none # suppress output
nexus build program.nx --explain-capabilities-format=json # machine-readable JSON