ADR-006 — Boundary guards at host-import surfaces

Status: Accepted Date: 2026-04-27

This ADR is a sub-decision of ADR-001 — it concerns the boundary thunk strategy where compiled code meets the dynamic world.

Context

A js2wasm-compiled module exists alongside a host environment (Node.js, WASI, browser, or another Wasm component). At every call out to that host — DOM APIs, filesystem APIs, callbacks invoked by the host, dynamic data fetched at runtime — the values flowing back across the boundary arrive as host references (externref). Inside the compiled core, however, values are tracked in their specific WasmGC struct types so that property access and arithmetic can use unboxed instructions.

The two type domains must be reconciled at the boundary. TypeScript annotations on the imported function describe the expected shape, but those annotations are unsound and erased — the compiler cannot trust them as a runtime invariant (see ADR-009). A runtime check is required at the boundary itself.

Decision

Emit a boundary guard at every host-import call site. The guard tests the returning externref against the expected WasmGC type using ref.test, and either narrows the value with br_on_cast for the success path or takes a slow / fallback path. Inside the compiled module, values are then held in their specific struct types and operated on with typed instructions. TypeScript annotations on the imported function are used to choose which type to test against, not to skip the test.

Consequences

Positive: heterogeneous host data is handled correctly under JS semantics — the compiled core can rely on its types being real types. The cost is one Wasm instruction per boundary crossing, which is negligible compared to the host-call cost itself. The guards make the dynamic / static boundary explicit and auditable.

Negative: every boundary requires a corresponding type, even for single-shot host calls — the compiler must materialize a struct type for each shape returned by a host import. Mis-shaped host responses surface as guard-failure paths rather than later, more confusing crashes; this is the intended behavior, but it does require fallback code at every site.

Alternatives rejected