Mutable arrays
A WAST array is a WebAssembly GC array: a heap-allocated, garbage-collected sequence with
one element type and a length fixed at allocation. It is not a slice into linear memory,
and it is not generic. array Samples { mut i32 } declares one nominal type — a second
declaration with the same element type is a different, non-interchangeable type.
// `mut` sits on the element, not the array. Mutability is part of the
// WebAssembly type, so an immutable element type can never be written by
// anyone, anywhere -- there is no `const`-style borrow to launder it through.
array Samples { mut i32 }
func sum(samples: &Samples) -> i32 {
var total: i32 = 0;
// `#samples` is array.len. It is also the only bounds check in play: the
// loop range is what keeps `index` legal. Get that wrong and the read
// below traps -- it does not return garbage and it does not return 0.
for index in 0..#samples {
total = total + samples[index];
}
return total;
}
func check() -> i32 {
// array.new_fixed: the length is a compile-time immediate and every element
// is pushed onto the operand stack, so a literal is only viable for small
// arrays. A runtime length needs wasm.array.new / wasm.array.new_default.
let samples: &Samples = new Samples { 8, 13, 21 };
// array.set. Evaluates receiver, then index, then right-hand side, each
// exactly once. Only plain `=` exists -- `samples[1] += 21` is a syntax
// error, not sugar the compiler forgot.
samples[1] = 34;
if (sum(samples) == 63) {
return 0;
}
return 1;
}
export func check as "check";
Why the parameter is &Samples, not ?Samples
array.get, array.set, and array.len all require a non-null operand, so the compiler
demands &Samples at the use site rather than emitting an implicit null check per access.
A ?Samples parameter is not forbidden — it just cannot be indexed until you narrow it
with is. That pushes the check to the one place where it is cheap and where you know
what to do about a null, instead of paying for it on every element access inside a loop.
Why bounds are not checked by the language
The engine already does it. WebAssembly's array.get/array.set are memory-safe by
construction: an out-of-range index traps. Adding a language-level compare-and-branch on
top would duplicate work the engine performs anyway, and it would have nowhere useful to
go — the target feature set excludes exception handling, so there is nothing to throw and
nothing to catch. A trap unwinds straight to the host.
The practical consequence: #arr is the guard you get, and you have to write it yourself.
wasmtime run --invoke boom out.wasm
error while executing at wasm backtrace:
wasm trap: out of bounds array access
Packed elements are a different read
array Bytes { mut i8 } stores 8 bits per element but there is no i8 value type — a
read produces i32, and the language refuses to guess whether 0xFF means 255 or -1.
So a packed read requires an explicit .s/.u suffix, and, symmetrically, a suffix on a
non-packed element is an error: it would imply a choice that does not exist.
b[0] (i8 element) a packed field or array element cannot be read without an
explicit '.s' or '.u' suffix
w[0].u (i32 element) '.s'/'.u' suffixes are only valid on a packed 'i8'/'i16'
field or array element
Writes are the asymmetric direction: a packed write takes an i32 and silently keeps the
low 8 or 16 bits. That truncation is not diagnosed.
Expected result
check() returns 0. Initial total 42; replacing 13 with 34 makes it 63.
Related rules
- Arrays, including bounds and evaluation order
- Packed storage
- Raw array operations —
array.new,array.fill,array.copy, and the packed accessors