Arrays
Zena's array architecture is tailored to WebAssembly GC and built for both low-level, high-performance use cases and high-level convenient and ergonomic APIs.
To support this Zena has an Array<T> interface and operator overloading on the
[] operator to allow for multiple array implementations. The built-in arrays
include fixed-size mutable and immutable arrays that map directly to Wasm GC,
and familiar growable arrays.
Array classes and interfaces ​
Zena provides three concrete array implementations and two common interfaces:
| Type | Kind | Mutability | Description |
|---|---|---|---|
ImmutableArray<T> |
Extension class | Read-only elements | Fixed-size array; default for array literals |
FixedArray<T> |
Extension class | Mutable elements | Fixed-size array with in-place element mutation |
GrowableArray<T> |
Class | Resizable | Dynamic collection backed by a FixedArray<T> |
Array<T> |
Interface | Read-only interface | Common read-only contract (length, [], map) |
MutableArray<T> |
Interface | Mutable interface | Extends Array<T> with element assignment ([]=) |
FixedArray<T> and ImmutableArray<T> are extension classes over raw
WebAssembly GC arrays. They have no wrapper struct or vtable indirection: an
array reference points directly to an engine-managed GC array. See Performance
and representation for low-level details.
Array literals ​
An array literal consists of a comma-separated list of expressions enclosed in
brackets [...]:
let nums = [1, 2, 3]; // ImmutableArray<i32>
let words = ["alpha", "beta", "gamma"]; // ImmutableArray<String>
Default type: ImmutableArray<T> ​
Array literals produce an ImmutableArray<T> by default. Their element type is
inferred from the types of the contained expressions:
let primes = [2, 3, 5, 7];
primes[0] = 1;
Error:
Contextual typing ​
When an array literal appears in a position where a mutable array or a growable collection is expected, the compiler automatically constructs the requested container:
// Context expects FixedArray:
let mutableList: FixedArray<i32> = [10, 20, 30];
mutableList[0] = 99; // Permitted
// Context expects GrowableArray:
let items: GrowableArray<String> = ["hello", "world"];
items.push("zena"); // Permitted
Construction helpers: fixed() and growable() ​
When constructing arrays in expression position—such as passing arguments to
functions or initializing unannotated let variables—the standard library
provides two zero-cost helpers in zena:core:
import { fixed, growable } from 'zena:core';
// Produces FixedArray<i32> without requiring an explicit type annotation:
let mutTable = fixed([10, 20, 30]);
mutTable[1] = 42;
// Produces GrowableArray<i32> by adopting the literal backing buffer:
let queue = growable([1, 2, 3]);
queue.push(4);
fixed([1, 2, 3]): Supplies the contextual type so the literal compiles directly as aFixedArray. The helper inlines away completely.growable([1, 2, 3]): Compiles the literal as aFixedArrayand passes it directly toGrowableArrayas its initial backing buffer without allocating an intermediate copy.
NOTE
Planned literal decorators In an upcoming release, Zena plans to
introduce literal decorators such as @fixed (or @mutable) and @growable
directly on array literals:
let mutTable = @fixed [10, 20, 30];
let queue = @growable [1, 2, 3];
Until literal decorators land, the fixed() and growable() helper functions
serve as the idiomatic, zero-cost way to construct mutable and growable arrays
in expression position.
Empty literals ​
An empty array literal [] contains no elements from which to infer an element
type. It must receive its container and element type from surrounding context:
-
Variable type annotations:
zenalet emptyFixed: FixedArray<i32> = []; let emptyGrowable: GrowableArray<String> = []; -
Explicit casts (
as): In expression position where no annotation is present, cast the literal directly:zenalet empty = [] as FixedArray<i32>; let emptyImm = [] as ImmutableArray<String>; -
Function parameter positions: Passing
[]to a typed function parameter supplies the contextual type directly:zenafunction setHeaders(headers: FixedArray<String>) { ... } setHeaders([]); // Parameter supplies contextual type FixedArray<String> -
Return positions:
zenafunction getTags(): Array<String> { return []; // Return type supplies contextual type }
Attempting to declare let ambiguous = []; without an expected type or cast
produces a compile-time error. For more details on how context determines
literal types, see Contextual typing.
ImmutableArray<T> ​
ImmutableArray<T> is declared in zena:core as an extension class on the raw
WebAssembly GC immutable array type array<T>:
export final extension class ImmutableArray<T> on array<T>
with IterableUtils<T>
implements Array<T>
Immutability guarantees ​
- Static rejection: The index assignment operator
[]=is not defined onImmutableArray<T>. Attempting to assign to an element produces a compile-time error. - Runtime enforcement: WebAssembly GC enforces array mutability at the bytecode level. An immutable array cannot be modified even through casts (see WebAssembly GC array types).
Covariance ​
Because elements in an ImmutableArray<T> can never be overwritten,
ImmutableArray<T> is covariant in its element type:
class Animal {}
class Dog extends Animal {}
let dogs: ImmutableArray<Dog> = [new Dog(), new Dog()];
let animals: ImmutableArray<Animal> = dogs; // Permitted: covariant subtyping
Creation and mapping ​
WebAssembly provides no instruction to allocate an uninitialized immutable array
and populate it afterwards. Consequently, ImmutableArray has no from()
constructor. It is created exclusively via literals or static constant tables.
Calling arr.map(f) on an ImmutableArray<T> populates and returns a
FixedArray<U>, which satisfies Array<U> via covariant return.
FixedArray<T> ​
FixedArray<T> is an extension class on the raw mutable WebAssembly GC array
type array<var T>:
export final extension class FixedArray<T> on array<var T>
with IterableUtils<T>
implements MutableArray<T>, Iterable<T>
A FixedArray<T> has a fixed length determined upon allocation, but its
individual elements can be read and mutated in place.
Allocation and constructors ​
- Default-value allocation: Creates an array of the specified length with
every slot initialized to
value:zenalet table = new Array<i32>.fixed(10, 0); // 10 elements, all 0 - From sequence: Copies elements from any
Array<T>into a newFixedArray<T>:zenalet copy = FixedArray.from(existingArray);
Mutation ​
Elements are read with arr[i] and mutated with arr[i] = value:
let counts = new Array<i32>.fixed(4, 0);
counts[0] = 10;
counts[1] += 5;
Invariance ​
FixedArray<T> is strictly invariant in its element type to preserve type
safety:
let dogs = new Array<Dog>.fixed(2, new Dog());
// let animals: FixedArray<Animal> = dogs;
Error:
Because elements can be modified in place, allowing subtyping would permit storing an incompatible subtype through an aliased reference.
GrowableArray<T> ​
GrowableArray<T> is a standard library class in zena:core providing dynamic
resizing with amortized O(1) append operations:
export final class GrowableArray<T> implements MutableArray<T>, Iterable<T>
Construction ​
// Empty growable array with default capacity (8):
let list = new Array<String>();
// With explicit initial capacity:
let preallocated = new Array<i32>(64);
// Copied from an existing array:
let cloned = GrowableArray.from([1, 2, 3]);
// Built from a literal without copying backing storage:
let ready = growable([10, 20, 30]);
The Array interface's own constructors build the standard classes, so
new Array() reads like other languages while the result keeps the class's
full API: new Array() and new Array.growable(capacity) return a
GrowableArray<T>, and new Array.fixed(length, value) a FixedArray<T>.
let list = new Array<String>(); // GrowableArray<String>
list.push('a');
let zeros = new Array.fixed(4, 0); // FixedArray<i32>, T inferred from the value
let names: Array<String> = new Array(); // T from the context
Resizing operations ​
list.push(value): Appends an element to the end of the array. When the backing buffer fills,GrowableArrayallocates a newFixedArraywith double the capacity and copies existing elements over.list.pop(): Removes and returns the last element, decrementing the logicallength.list.length: Returns the logical count of elements currently stored (distinct from the backing buffer's capacity).
let stack = new Array<i32>();
stack.push(100);
stack.push(200);
let top = stack.pop(); // 200
let count = stack.length; // 1
Interfaces: Array<T> and MutableArray<T> ​
Zena defines two interfaces in zena:core to allow polymorphic programming
across different array implementations:
export interface Array<T> extends Iterable<T> {
length: i32 { get; }
operator [](index: i32): T;
map<U>(f: (item: T, index: i32, seq: this) => U): Array<U>;
}
export interface MutableArray<T> extends Array<T> {
operator []=(index: i32, value: T): void;
}
ImmutableArray<T>implementsArray<T>.FixedArray<T>andGrowableArray<T>implement bothArray<T>andMutableArray<T>.
Writing polymorphic array functions ​
Functions that only need to read elements and check lengths can accept
Array<T>:
function printAll(items: Array<String>) {
for (let item in items) {
console.log(item);
}
}
For performance-critical code where you want guaranteed specialization without
interface dispatch, use generic bounds (<A extends Array<T>>(arr: A)). See
Polymorphism and generic
specialization for details and
trade-offs.
Indexing and bounds ​
All arrays use zero-based indexing (0 <= index < length):
let arr = [10, 20, 30];
let first = arr[0]; // 10
let last = arr[arr.length - 1]; // 30
Bounds checking behavior ​
Zena enforces bounds safety on all array reads and writes:
FixedArrayandImmutableArray: Bounds checking is performed directly by the WebAssembly GC runtime. Accessing an index outside[0, length)immediately traps with a WebAssemblyout of boundstrap.GrowableArray: Checks indices against its logicallengthproperty and throws anIndexOutOfBoundsErrorwhen an index is invalid.
Slicing and ranges ​
FixedArray<T> provides slicing via the slice() method and range operator
overloads:
The slice() method ​
arr.slice(start, end) returns a shallow copy of a portion of the array from
index start (inclusive) to end (exclusive):
let arr: FixedArray<i32> = [10, 20, 30, 40, 50];
let sub = arr.slice(1, 4); // [20, 30, 40]
- Indices are clamped to valid ranges (
start < 0is clamped to0,end > lengthis clamped tolength). - If
start >= end, an empty array is returned. - Negative indexing relative to the end (e.g.
-1) is not supported; usearr.length - 1explicitly.
Range indexing syntax ​
FixedArray<T> overloads the index operator for range types:
let items: FixedArray<i32> = [0, 1, 2, 3, 4, 5];
let a = items[1..4]; // BoundedRange: [1, 2, 3]
let b = items[2..]; // FromRange: [2, 3, 4, 5]
let c = items[..3]; // ToRange: [0, 1, 2]
let d = items[..]; // FullRange: shallow copy of entire array
Iteration ​
All array implementations implement Iterable<T> and can be iterated using
for-in loops:
let names = ["Ada", "Grace", "Margaret"];
for (let name in names) {
console.log(name);
}
When iterating over concrete FixedArray or ImmutableArray instances, the
compiler optimizes the loop into a direct index counter with zero iterator
allocation. See Loop fusion for concrete
arrays.
Array destructuring ​
Arrays support pattern destructuring in let, var, and parameter positions:
let coords = [10, 20, 30];
// Basic positional destructuring:
let [x, y, z] = coords;
// Skipping elements with holes:
let [first, , third] = coords; // first = 10, third = 30
// Rest pattern collecting remaining elements:
let [head, ...tail] = coords; // head = 10, tail = [20, 30]
Destructuring works identically across ImmutableArray, FixedArray, and
GrowableArray.
Narrow integer arrays ​
Arrays can be instantiated with narrow integer types (u8, i8, u16, i16).
Narrow arrays share the same generic collection API as all other arrays:
let bytes = new Array<u8>.fixed(4, 0);
bytes[0] = 0x48;
bytes[1] = 0x65;
bytes[2] = 0x6c;
bytes[3] = 0x6c;
// Slicing, mapping, and iteration work identically:
let sub = bytes.slice(1, 3);
Explicit widening for arithmetic ​
Because Zena has a sound type system with no implicit widening or sign coercion,
reading an element from FixedArray<u8> yields the u8 type. Combining it with
standard i32 arithmetic requires an explicit widening cast using as:
let byteVal: u8 = bytes[0];
let total: i32 = (bytes[0] as i32) + 10;
See Packed storage for narrow integer types for details on how these types map directly to compact WebAssembly storage.
Performance and representation ​
This section details how Zena's array architecture is implemented under WebAssembly GC and how to write high-performance array code.
WebAssembly GC array types ​
Zena's concrete arrays map directly onto WebAssembly GC array types without wrapper structs or vtables:
;; Immutable 32-bit array:
(type $ImmutableArray_i32 (array i32))
;; Mutable 32-bit array:
(type $FixedArray_i32 (array (mut i32)))
;; Packed 8-bit array (FixedArray<u8> or FixedArray<i8>):
(type $FixedArray_u8 (array (mut i8)))
;; Packed 16-bit array (FixedArray<u16> or FixedArray<i16>):
(type $FixedArray_u16 (array (mut i16)))
Because FixedArray<T> and ImmutableArray<T> are declared as extension class ... on array<...>, the compiler erases the extension class to its underlying
array type at the WebAssembly boundary:
- Methods called on fixed or immutable arrays (such as
.map()or.slice()) compile as static functions where the array is passed as the first argument (this). - Array instances do not require vtables.
- Immutability is enforced by the WebAssembly VM:
(array T)has noarray.setinstruction. Attempting to cast anImmutableArrayto aFixedArrayviaanyreffails with a WebAssemblyref.castruntime trap. - Zero-cost interoperability: Zena arrays can be passed directly to WebAssembly host environments or modules compiled from other languages without conversion.
Packed storage for narrow integer types ​
In WebAssembly, values on the evaluation stack and in registers are always at
least 32 bits (i32, i64, f32, f64). However, the WebAssembly GC
specification supports i8 and i16 as storage types inside GC arrays and
structs.
When generic array classes are instantiated with narrow integer types, the compiler maps them directly to native packed WebAssembly array types:
| Zena Array Type | Underlying WebAssembly GC Type | Bytes per Element |
|---|---|---|
FixedArray<u8> / FixedArray<i8> |
(array (mut i8)) |
1 byte |
ImmutableArray<u8> / ImmutableArray<i8> |
(array i8) |
1 byte |
FixedArray<u16> / FixedArray<i16> |
(array (mut i16)) |
2 bytes |
ImmutableArray<u16> / ImmutableArray<i16> |
(array i16) |
2 bytes |
FixedArray<i32> / FixedArray<u32> |
(array (mut i32)) |
4 bytes |
FixedArray<i64> / FixedArray<u64> |
(array (mut i64)) |
8 bytes |
FixedArray<f32> |
(array (mut f32)) |
4 bytes |
FixedArray<f64> |
(array (mut f64)) |
8 bytes |
Generic FixedArray<T> serves as the typed array: FixedArray<u8> provides
packed byte storage while sharing the exact same collection API as any other
array.
At the engine level:
- Reads: Reading
arr[i]emitsarray.get_u(foru8andu16) orarray.get_s(fori8andi16), automatically zero-extending or sign-extending the packed value to the evaluation stack. - Writes: Mutating
arr[i] = valueonFixedArrayemitsarray.set, which truncates the stack value to 8 or 16 bits upon storage.
Loop fusion for concrete arrays ​
When iterating over concrete FixedArray<T> or ImmutableArray<T> instances,
the compiler's ZIR backend detects the concrete array and fuses the for-in
loop directly into an index counter loop:
;; Fused loop emits direct WebAssembly instructions:
local.get $arr
array.len
...
local.get $arr
local.get $idx
array.get $type
This eliminates the allocation of an iterator object and avoids all interface method dispatch.
Polymorphism and generic specialization ​
Passing a concrete array (FixedArray or ImmutableArray) to a parameter typed
as an interface like Array<T> conceptually packs the array into a fat pointer
(a two-field struct holding the instance reference and vtable pointer),
introducing dynamic dispatch.
In many cases, the compiler's optimizer will optimize this fat pointer away:
- Scalar Replacement of Aggregates (SRoA): Decomposes the two-field fat-pointer struct into separate scalar locals where it does not escape.
- Argument explosion: Passes the instance reference and vtable as separate scalar parameters across function call boundaries.
- Devirtualization and inlining: Resolves single-implementer interface calls
statically and inlines the underlying
array.lenandarray.gettrampolines.
Guaranteed specialization via generic bounds ​
If you want guaranteed specialization directly to native WebAssembly array
instructions without relying on optimization passes, constrain a generic type
parameter by Array<T>:
function sum<A extends Array<i32>>(arr: A): i32 {
var total = 0;
for (var i = 0; i < arr.length; i += 1) {
total += arr[i];
}
return total;
}
The compiler specializes the function for each concrete array type passed to it.
Both .length and arr[i] resolve directly through the bound to native
WebAssembly instructions (array.len and array.get) with zero fat-pointer
overhead and zero indirect calls.
Trade-off: code size versus peak performance ​
Guaranteed specialization relies on monomorphization: the compiler
duplicates the function body for each concrete array type (FixedArray,
ImmutableArray, GrowableArray) it is instantiated with. This trades larger
WebAssembly binary size for maximum loop and element access speed.
For performance-critical inner loops and math kernels, generic specialization is
ideal. For large function bodies or cold utility paths where element access is
not the bottleneck, taking the plain Array<T> interface produces a single
shared function in the binary.