Skip to main content

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.