Skip to content

wasm: make JS WebAssembly.instantiate operand stack/heap tunable#20

Open
e-fu wants to merge 1 commit into
elixir-volt:masterfrom
ZenHive:wasm-tunable-operand-stack
Open

wasm: make JS WebAssembly.instantiate operand stack/heap tunable#20
e-fu wants to merge 1 commit into
elixir-volt:masterfrom
ZenHive:wasm-tunable-operand-stack

Conversation

@e-fu

@e-fu e-fu commented Jun 19, 2026

Copy link
Copy Markdown

What

Makes the WASM operand stack and auxiliary heap for guests started via the
JavaScript WebAssembly.instantiate path tunable, via two new runtime-level options
:wasm_stack_size / :wasm_heap_size (both default 65536behavior is unchanged
unless a consumer opts in
).

Why

The JS WebAssembly.instantiate path hardcoded a 64 KB operand stack (and 64 KB aux
heap) when starting an instance, while the native QuickBEAM.WASM NIF path already
accepts caller-supplied :stack_size / :heap_size. Guests with deep initialization
(e.g. Go GOOS=js) overflow the 64 KB operand stack at boot — the native path can boot
them, the JS path can't. This closes that parity gap.

The new values are threaded from the runtime/pool opts down to the instantiate site,
mirroring exactly how the existing :max_stack_size (the JS call stack, a separate
8 MB limit) is already plumbed. The standard instantiate(bytes, importObject) JS
signature stays spec-faithful — no extra JS argument; the limit comes purely from
per-runtime config.

How

  • New wasm_stack_size / wasm_heap_size fields (default 65_536) on the two config
    carriers RuntimeData (types.zig) and PoolData (context_types.zig), and on the
    per-context ContextState (wasm_js.zig).
  • Threaded through the single install chokepoint
    (worker.zigwasm_js.installensure_context_state), the pool's
    PoolData → RuntimeData copy (context_worker.zig), the opts parsing
    (quickbeam.zig start_runtime + pool_start), and the Elixir Keyword.take
    allow-lists (runtime.ex, context_pool.ex).
  • The instantiate site in wasm_js.zig now reads state.wasm_stack_size /
    state.wasm_heap_size instead of the 65_536, 65_536 literals.
  • Opt parsing bounds-checks the u64 → u32 cast (std.math.cast): an out-of-range
    value returns a controlled error instead of trapping (Debug) / wrapping (ReleaseFast).
    Note: the pre-existing max_convert_depth / max_convert_nodes casts in the same
    function share this pattern and were left unchanged to keep this diff focused.
  • Docs updated on QuickBEAM, QuickBEAM.Runtime, QuickBEAM.ContextPool, and
    QuickBEAM.WASM (clarifying the NIF path keeps its per-call :stack_size/:heap_size).

Tests

test/wasm_test.exs adds a small recursive rec(n) fixture (WAMR keeps call frames on
the stack_size buffer, so deep recursion is the canonical operand-stack-overflow repro)
and three regression assertions:

  • default 64 KB → rec(10000) raises a …stack… error,
  • QuickBEAM.start(wasm_stack_size: 8 MB)rec(10000) returns {:ok, 0},
  • the same through a QuickBEAM.ContextPool (exercises the pool PoolData → RuntimeData
    threading path, which the standalone test doesn't cover).

Verification

Built and tested in the CI-pinned container (OTP 27.0 / Elixir 1.18.3 / Zig 0.15.2,
MIX_ENV=test, Debug): 56 tests, 0 failures, mix compile clean. The diff also
compiles clean against this branch's base.

Copilot AI review requested due to automatic review settings June 19, 2026 12:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes the JS WebAssembly.instantiate guest WASM operand stack and aux heap sizes configurable at the runtime/pool level (:wasm_stack_size / :wasm_heap_size, defaulting to 65_536), bringing it in line with the native QuickBEAM.WASM path which already supports caller-supplied sizing.

Changes:

  • Thread new runtime/pool options (:wasm_stack_size, :wasm_heap_size) through Elixir → Zig config carriers → JS WebAssembly.instantiate implementation.
  • Replace hardcoded 65_536 stack/heap literals in the JS instantiate path with per-context state values.
  • Add regression tests covering stack overflow at the default size and success with a raised :wasm_stack_size, including the ContextPool path.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/wasm_test.exs Adds JS WebAssembly.instantiate regression tests for default-stack overflow and raised :wasm_stack_size (runtime + pool).
lib/quickbeam/worker.zig Passes runtime-configured WASM stack/heap sizes into the WASM JS install hook.
lib/quickbeam/wasm.ex Clarifies docs: native WASM options vs JS instantiate runtime-level sizing.
lib/quickbeam/wasm_js.zig Stores per-context WASM stack/heap sizes and uses them when starting managed instances from JS.
lib/quickbeam/types.zig Extends RuntimeData with wasm_stack_size / wasm_heap_size defaults.
lib/quickbeam/runtime.ex Allows :wasm_stack_size / :wasm_heap_size through to the runtime NIF opts.
lib/quickbeam/quickbeam.zig Parses and bounds-checks wasm_stack_size / wasm_heap_size from Elixir opts for runtime + pool.
lib/quickbeam/context_worker.zig Copies pool WASM sizing into each created context’s RuntimeData.
lib/quickbeam/context_types.zig Extends PoolData with wasm_stack_size / wasm_heap_size defaults.
lib/quickbeam/context_pool.ex Documents and forwards the new pool options to the NIF.
lib/quickbeam.ex Documents new runtime options and corrects/aligns :max_stack_size default wording.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/quickbeam/types.zig
Comment on lines +27 to +29
// WASM operand stack / heap for the JS `WebAssembly.instantiate` path
// (distinct from `max_stack_size`, the JS call stack). Default mirrors the
// WASM NIF path; raised via the runtime `:wasm_stack_size` opt.
Comment on lines +144 to +147
// WASM operand stack / heap for the JS `WebAssembly.instantiate` path
// (distinct from `max_stack_size`, the JS call stack). Default mirrors the
// WASM NIF path; raised via the pool `:wasm_stack_size` opt. Copied into
// each context's RuntimeData at create time.
Comment thread lib/quickbeam/wasm_js.zig
Comment on lines +20 to +24
// WASM operand stack / auxiliary heap for instances started via the JS
// `WebAssembly.instantiate` path. Distinct from the JS call stack
// (`max_stack_size`). Default mirrors the WASM NIF path; a consumer raises
// it (via the runtime/pool `:wasm_stack_size` opt) for guests whose deep
// init would otherwise overflow the 64 KB default.
Comment thread test/wasm_test.exs
Comment on lines +724 to +732
test "JS instantiate path honors a raised :wasm_stack_size" do
{:ok, rt} = QuickBEAM.start(wasm_stack_size: 8 * 1024 * 1024)

assert {:ok, 0} =
QuickBEAM.eval(rt, """
const bytes = #{@stack_deep_wasm};
const {instance} = await WebAssembly.instantiate(bytes);
instance.exports.rec(10000);
""")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants