WebAssembly
Zena is designed from the ground up specifically for WebAssembly GC (Wasm GC).
Most languages that target WebAssembly fall into one of two categories:
- Linear memory languages (such as C++, Rust, or Go), which compile against raw linear memory bytes and bundle their own custom memory allocators or garbage collection runtimes into the binary.
- Multi-target GC languages (such as Dart, Kotlin, Scala, or Java), which target Wasm GC alongside traditional VMs or native backends. Because their type systems were established for other runtime environments, their compilers must map existing semantics—like generic type erasure, runtime type inspection (RTTI), and legacy object lifecycles—onto WebAssembly's structural GC type system.
Zena is designed primarily with WebAssembly GC in mind: its core language
features—such as constructor initializer lists that compile directly to
struct.new, explicit boxing with no universal any, immutability by default,
and unboxed inline tuples—align naturally with WebAssembly GC primitives from
first principles.
Targeting Wasm GC ​
Targeting Wasm GC natively offers several core architectural advantages:
- Zero-runtime GC bundling: Zena binaries do not embed a custom garbage collector or memory allocator, avoiding fixed runtime footprint overhead in small modules.
- Direct host engine integration: Objects allocated in Zena are managed directly by the host engine's garbage collector (such as V8 in Chrome and Node.js, SpiderMonkey in Firefox, JavaScriptCore in Safari, or Wasmtime). Zena GC references can hold JavaScript DOM nodes and host objects directly without cross-heap handles or memory leak hazards.
- Engine-level optimizations: High-performance Wasm engines provide generational, parallel, and concurrent collectors with deep OS and hardware optimizations that single-language embedded GCs rarely match.
- Direct semantic alignment: Because Zena's semantics match Wasm GC capabilities (such as non-nullable references and immutable struct fields), the compiler emits direct WebAssembly instructions without layers of runtime shims or defensive null checks.
The type section trade-off ​
Because Wasm GC is statically typed at the bytecode level, every distinct class, interface vtable, closure environment, and array shape must be explicitly declared in the WebAssembly module's type section.
This introduces an architectural trade-off: while Zena avoids bundling GC engine code, modules with numerous generic specializations or class hierarchies emit larger type sections. Whole-program reachability analysis and dead-code elimination ensure that only types and methods actually constructed or called are emitted into the final binary.
Type mapping and representation ​
A core architectural principle of Zena is that the compiler always strives to use the most compact and fastest WebAssembly representation possible.
Whenever a Zena variable, field, or parameter is non-nullable and immutable, the
compiler emits strictly non-nullable ((ref $T)) and non-mutable
((field $T)) WebAssembly types. This design yields smaller binary sizes,
enables host JIT optimizations (constant propagation, load hoisting), and
eliminates runtime null checks (ref.as_non_null).
| Zena Type | WebAssembly Representation | Description |
|---|---|---|
i32 |
i32 |
32-bit signed integer |
i64 |
i64 |
64-bit signed integer |
f32 |
f32 |
32-bit IEEE 754 float |
f64 |
f64 |
64-bit IEEE 754 float |
boolean |
i32 |
1 for true, 0 for false |
class Foo |
(ref $Foo) |
Non-nullable typed GC struct reference |
Foo? |
(ref null $Foo) |
Nullable typed GC struct reference |
anyref |
anyref |
Top reference type (references only) |
inline (A, B) |
(result T_A T_B) |
Unboxed multi-value return |
FixedArray<T> |
(ref (array (mut T))) |
Fixed-length mutable Wasm GC array |
ImmutableArray<T> |
(ref (array T)) |
Native immutable Wasm GC array |
Primitive operations ​
Operations on primitive types map directly to single WebAssembly instructions without boxing, tag checks, or allocations:
- Arithmetic and bitwise operations (
+,-,*,/,&,|,^,<<,>>) compile directly to their correspondingi32andf64Wasm instructions. - Identity equality (
===) compiles directly toi32.eq,f64.eq, orref.eq. Value equality (==) dispatches to the overridable==method.
No implicit boxing and no universal top type ​
Unlike TypeScript, JavaScript, Dart, or Java, Zena has no any, dynamic, or
Object type that can hold primitives directly.
anyref is the top type for references only. A primitive i32 or f64
cannot be assigned to anyref without explicitly allocating a container
(new Box<i32>(42)). Because there is no universal type that transparently
accepts both primitives and heap references, the compiler never introduces
hidden boxing allocations.
Inline tuples and multi-value returns ​
Zena supports both first-class heap tuples ((A, B), backed by synthesized GC
structs) and zero-allocation inline tuples.
When a tuple is prefixed with inline (e.g., inline (i32, f64)), it is an
unboxed, stack-bound value that compiles directly to WebAssembly multi-value
function returns:
function getCoordinates(): inline (f64, f64) {
return (10.0, 20.0);
}
Compiles in WebAssembly to:
(func $getCoordinates (result f64 f64)
f64.const 10.0
f64.const 20.0
return
)
When destructured via let (x, y) = getCoordinates();, the values remain in local
registers on the WebAssembly execution stack without touching the GC heap.
Arrays ​
Zena provides specialized array types backed directly by WebAssembly GC (array ...) definitions:
FixedArray<T>: Backed by(ref (array (mut T))), representing a fixed-length array whose elements can be updated in-place viaarray.set.ImmutableArray<T>: Backed by(ref (array T))with non-mutable elements. Array literals ([1, 2, 3]) currently produceFixedArray<T>and are designed to produceImmutableArray<T>, allowing engines to share and constant-fold array data safely.GrowableArray<T>: A class that wraps a backingFixedArray<T>with length and capacity tracking, resizing viaarray.copywhen capacity is exceeded.Array<T>: The common read-only interface implemented byFixedArray<T>,ImmutableArray<T>, andGrowableArray<T>.
Array interface overhead ​
Because Array<T> is an interface rather than a concrete class, references
typed as Array<T> pay the overhead of Fat Pointer interface dispatch (an
ITable lookup).
While alternative designs could have eliminated this indirection, keeping
Array<T> as an interface provides a familiar, unified API across fixed,
immutable, and growable buffers. Future whole-program optimizations will
automatically devirtualize and specialize interface-typed array calls in
monomorphic contexts.
Strings ​
Strings in Zena are abstract sequences of Unicode characters rather than raw byte or code unit arrays.
By hiding underlying bytes and encoding details from userland APIs, the compiler can compile strings to UTF-8/WTF-8 (for WASI and native runtimes) or UTF-16/WTF-16 (for JavaScript and the DOM via the WebAssembly JS String Builtins proposal) to match the host platform and avoid costly re-encoding—without breaking Zena programs.
See Strings and Unicode for full details on string semantics, Unicode indexing, and string operations.
Linear memory and zena:memory ​
While general Zena code uses Wasm GC objects, high-performance systems programming often requires byte-level layout control.
Through the zena:memory module, Zena provides access to standard WebAssembly
Linear Memory (pages of unmanaged bytes). Linear memory is used for:
- SIMD vector operations (
v128). - Low-level binary serialization and network protocol decoding.
- Interoperating with C-ABI libraries or WASI file system calls.
Wasm GC structs and linear memory coexist within the same Zena module.
Classes, polymorphism, and dispatch ​
Zena implements class inheritance and polymorphism by leveraging WebAssembly GC structural subtyping and specialized virtual tables.
Single inheritance with struct.sub ​
Classes with single inheritance map directly to WebAssembly's native subtyping
hierarchy (sub declarations):
;; Base class struct
(type $Shape (sub (struct (field $vtable (ref $Shape_vtable)) (field $color (ref $String)))))
;; Derived class struct extending $Shape
(type $Circle (sub $Shape (struct (field $vtable (ref $Circle_vtable)) (field $color (ref $String)) (field $radius f64))))
Because derived class fields are laid out at identical offsets to their superclasses,
accessing inherited fields on a subclass is a direct struct.get instruction
without dynamic offset lookups.
Virtual method dispatch ​
Method calls on classes are dispatched through static virtual tables (vtables):
- The class instance holds a non-nullable reference to its static vtable struct.
- The vtable struct contains typed function references (
(ref $FuncType)). - The method call loads the function reference from the vtable and invokes it via
call_ref.
When a class or method is marked final, or when reachability analysis proves a
class has no subclasses, the compiler automatically devirtualizes the call into a
direct call $func instruction (see Performance).
Interface dispatch via fat pointers ​
Because a class in Zena can implement multiple interfaces, single-vtable offsets cannot accommodate all interface layouts.
Zena represents interface references as Fat Pointers—a lightweight pair of:
- Instance reference: The underlying object (
anyref). - Interface table (ITable): A specialized vtable tailored specifically to the interface's method offsets for that concrete class.
When an object is upcast to an interface (let drawable: Drawable = circle;),
the compiler constructs this pair. Method calls through the interface load the
adapted function pointer from the ITable and pass the instance as the receiver.
Native checked casts ​
Between class references, dynamic type testing (obj is Circle) and downcasting
(obj as Circle) compile directly to native Wasm GC instructions:
ref.test $Circle: Returns1if the reference is an instance of$Circle,0otherwise.ref.cast $Circle: Downcasts the reference, trapping or failing if the type does not match.
These instructions execute in hardware or JIT machine code without walking prototype chains or checking string type tags.
as is not a single instruction everywhere, because not every as is a test.
Converting to an interface builds the fat pointer above — a struct.new, plus a
null test when the source can be null, so that a null stays null rather than
becoming a pair around nothing. Casting an interface value back to a class reads
the instance out of the pair before the ref.cast. Numeric as compiles to the
corresponding conversion instruction, and to a mask or a shift pair for the
narrow integer types. None of these search a union's members at runtime: which
conversion to emit is settled at compile time.
Struct construction and immutability ​
WebAssembly GC struct fields can be declared as either mutable ((field (mut T)))
or immutable ((field T)).
Immutable fields offer substantial performance advantages: modern JIT compilers can fold constant field reads, cache loads across loops, and optimize field accesses aggressively.
Why constructor initializer lists matter ​
In languages like TypeScript or Java, constructor bodies run after an object is allocated with zeroed or null fields, requiring fields to be mutable during construction.
Zena avoids this through constructor initializer lists:
class Point {
x: f64;
y: f64;
new(this.x, this.y);
}
Because all fields and constructor parameter expressions are fully evaluated
before the struct is created, the compiler emits a single struct.new
instruction:
;; Compiles directly to a single struct.new with initialized arguments
local.get $x
local.get $y
struct.new $Point
This architecture allows Zena to declare class fields as non-mutable and non-nullable in WebAssembly GC structs.
Functions and calling conventions ​
Top-level functions and arrow functions have distinct WebAssembly representations in Zena.
Functions vs. closures ​
- Top-level
functiondeclarations: Emitted as plain WebAssembly functions ((func $name ...)). They have zero allocation overhead and cannot capture surrounding lexical variables. - Arrow functions and closures: Emitted as a WebAssembly GC closure struct
containing a typed function reference (
(ref $func)) and fields for each captured local variable.
Default arguments and arity adaptation ​
WebAssembly functions require fixed, exact parameter lists. To support flexible calling patterns efficiently:
- Default values: Evaluated at the call site whenever arguments are omitted, passing the complete parameter list to the underlying function.
- Arity adaptation: When a callback accepting fewer parameters (e.g.,
(x) => print(x)) is passed to a higher-order function expecting more arguments (e.g.,(item, index)), the compiler generates a lightweight trampoline wrapper that discards unused arguments safely.
Async, exceptions, and runtime execution ​
Zena compiles modern asynchronous control flow and exception handling to native WebAssembly features.
Stackless async transformation ​
Rather than waiting for experimental WebAssembly stack-switching proposals or relying on heavy runtime fibers, Zena uses a stackless state machine split transformation:
- An
asyncfunction is transformed into a state machine struct that stores its local variables and resume step. - Each
awaitpoint splits the function body into continuation blocks. - When an awaited
Futurecompletes, the state machine's resume function is scheduled onto the microtask queue.
This coroutine architecture runs on any standard Wasm GC runtime today, integrating seamlessly with JavaScript Promises and WASI event loops.
Exception handling ​
Zena's throw and try/catch statements compile to the standard WebAssembly
Exception Handling proposal (throw_ref, try_table, and catch).
Exceptions propagate using native engine stack unwinding without manual error-code
polling.
Stack traces and debug symbols ​
Standard WebAssembly does not provide a built-in bytecode instruction for inspecting the call stack or reading exception backtraces from inside a module.
To provide first-class stack traces on exception objects, Zena uses a host import protocol:
- When an
ErrororExceptionis created, Zena invokes host import functions (captureStackTrace,formatStackTrace, orgetStackTrace). - Host implementations: Zena ships implementations of this protocol for both
JavaScript / V8 (extracting frames from
Error.stack) and Wasmtime / Rust (capturingwasmtime::WasmBacktrace).
By default, production WebAssembly binaries strip function names to optimize
file size, resulting in numeric stack traces (e.g., wasm-function[42]). When
compiled with the debug names flag (-g), the compiler emits a standard
WebAssembly name section, allowing host backtraces to format human-readable
traces with original class names, method identifiers, and function names.
Next ​
- Performance — devirtualization, monomorphization, and binary size
- Values and Variables — immutability and variable semantics
- Classes — class inheritance, vtables, and accessors