Goose

Running Goose on WebAssembly

A proposal, not implemented. Spec §10.4 says platforms without address-space reservation fall back to "index-based references + bounds-checked growth" and leaves the details open (open issue 15); this note works them out. The cost figures in it are estimates, not measurements.

1. The problem

Each data stack is a region of reserved address space, committed as it is touched (spec §1.2, §10.4, implementation §7). Growth is a pointer bump with no capacity check, and a guard gap past each region turns an overrun into an abort. References into a stack are plain addresses and stay valid across growth because a stack's base never moves, a property the language is built around (tutorial).

WebAssembly has none of this. A module's linear memory grows only at its end, has no unmapped pages inside it, and in wasm32 holds at most 4 GB. A port with a few stacks of a small fixed size each runs, but only toy programs fit.

2. What every option needs: growth checks

Linear memory has no guard pages, so a region that overruns writes into the next one. Every approach therefore checks growth, if (lim - top < n), including a port that keeps fixed regions. The options differ in what a failed check does: abort, or move the stack. Only moving forces a different pointer representation.

The checks go wherever a stack's top moves up:

The top caches (implementation §6.10) become (top, limit) pairs, synchronized at the same points. A loop with a known trip count can reserve once before it.

3. Fixed partitions

Each stack gets a fixed slice of linear memory, and a failed check aborts with the overrun message native builds give. This is mostly configuration of the existing runtime: GS_STACK_RESERVE is the linear memory divided by the region count, and GS_STACK_BUDGET is the linear memory, which the runtime already uses to cap hardware_threads().

4. Relocating stacks

Each stack is a heap block that moves to a larger one when a growth check fails, doubling its size. Since a stack can move, a reference cannot hold an address: it holds a handle.

5. Options that do not work today

6. Where relocation's changes fall

In order of risk, not size:

  1. In-place construction while the same stack grows (the list in §2). Natively, a held pointer to a slot's start or to a prefix still to be patched survives growth. Under relocation it must be a handle, or be derived again after the growth.
  2. The assumption that the base never moves. View hoisting relies on it (its safety argument is that growth writes only the length), and so do the top caches, reference aliases (refalias) and let first .= v.push(...). There are few lines here but most of the potential bugs. One rule contains them: handles everywhere, and decoded pointers only over extents the sync analysis proves.
  3. The C boundary: the runtime, extern fns, the blessed packages and goose.h. The C ABI can stay on plain pointers. Slices and references are decoded at the call, since a C callee can grow a Goose stack only through a builder. A builder parameter gets a staging stack whose contents are appended after the call returns. Packages then recompile unchanged, with gs_bld_append gaining its capacity check.
  4. Access spellings in codegen, which is mechanical work. A rough count is 35 *(T *) dereferences, 140 emitted ->, 26 mem* calls and 21 ->top, mostly through LoadLoc, IndexLoc, DerefLoc, PointeeLv and VarLoc.
  5. Unchanged: the parser, the typechecker, the optimizer, BCE apart from view hoisting, and the standard library, which is Goose source the same codegen compiles. The library's only contact with C is its externs: math on scalars, find_any and find_pair on slices, os's builders, and json's frexp with an i32&.

7. Wasm costs outside the data stacks

Recommendation

  1. Add the growth checks of §2 as a configuration of their own. Native builds can run with them, so the existing test suite covers them. Fixed partitions are then configuration, and they make a working port for programs whose stacks fit.
  2. If that is not enough, add relocation as a native configuration first: stacks in heap blocks that move on growth. Give it a stress setting that moves a stack on every growth, so the test suite exposes every stale decoded pointer, and use the benchmark suite to measure the cost. Only then bring in a Wasm toolchain.