commit 00369dc5e19be75c9a467c98adf499cda78edb6d
parent aee8d70c68032e6d2a769479f9f26a4f37add9a9
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 1 Jun 2026 17:14:26 -0700
arch/wasm: load frame-resident pointer locals in indirect addressing
An OPK_INDIRECT base/index may name an address-taken (frame-resident) pointer
local rather than a materialized register; the CG defers loading it to the
backend. Dispatch on register-vs-frame-slot when pushing the address component
and when deciding i64->i32 narrowing, instead of assuming a register.
Also bind canned host imports in the wasm C-lane harness so modules that
declare imports run there, mirroring jit_runner.c / start_wasm.c.
Diffstat:
2 files changed, 65 insertions(+), 4 deletions(-)
diff --git a/src/arch/wasm/emit.c b/src/arch/wasm/emit.c
@@ -2365,6 +2365,31 @@ static void queue_symbol_addr_fixup(WTarget* t, ObjSymId sym, i64 addend) {
fx->addend = addend;
}
+/* Push the value of an OPK_INDIRECT base/index component. The CG defers loading
+ * the pointer value of an address-taken (frame-resident) pointer local to the
+ * backend: the deref of such a local arrives as an OPK_INDIRECT whose base names
+ * the local itself, not a materialized register (see fold_ea_into_operand, and
+ * native_direct_target's nd_cache_reg_for, which loads it from the home). In the
+ * wasm backend each id is either a register (reg_to_local set) or a frame slot,
+ * never both — so dispatch on that: a register is fetched directly; a
+ * frame-resident local is read from its home like any other WOP_LOCAL operand. */
+static void emit_push_addr_component(WTarget* t, Reg id) {
+ if (id < t->reg_cap && t->reg_to_local[id] != 0xffffffffu) {
+ emit_push_operand_reg(t, id);
+ } else {
+ WSlot* s = slot_for(t, id);
+ emit_push_operand(t, WOP_LOCAL, (i64)id, REG_NONE, s->type);
+ }
+}
+
+/* Value type of an indirect component, whether it lives in a register or a
+ * frame slot (used to decide i64->i32 address narrowing). */
+static WasmValType addr_component_valtype(WTarget* t, Reg id) {
+ if (id < t->reg_cap && t->reg_to_local[id] != 0xffffffffu && t->reg_type[id])
+ return type_valtype(t, t->reg_type[id]);
+ return type_valtype(t, slot_for(t, id)->type);
+}
+
static void emit_addr_operand(WTarget* t, Operand addr, uint64_t* offset_out) {
*offset_out = 0;
if (addr.kind == OPK_LOCAL) {
@@ -2376,11 +2401,10 @@ static void emit_addr_operand(WTarget* t, Operand addr, uint64_t* offset_out) {
return;
}
if (addr.kind == OPK_INDIRECT) {
- emit_push_operand_reg(t, addr.v.ind.base);
+ emit_push_addr_component(t, addr.v.ind.base);
if (addr.v.ind.index != REG_NONE) {
- emit_push_operand_reg(t, addr.v.ind.index);
- if (addr.v.ind.index < t->reg_cap &&
- type_valtype(t, t->reg_type[addr.v.ind.index]) == WASM_VAL_I64) {
+ emit_push_addr_component(t, addr.v.ind.index);
+ if (addr_component_valtype(t, addr.v.ind.index) == WASM_VAL_I64) {
emit_insn(t, WASM_INSN_I32_WRAP_I64, 0);
}
if (addr.v.ind.log2_scale != 0) {
diff --git a/test/wasm/run.sh b/test/wasm/run.sh
@@ -109,8 +109,42 @@ if [ "$RUN_C" -eq 1 ]; then
cat > "$C_WRAPPER_SRC" <<'EOF'
#include <stdint.h>
#include <stdlib.h>
+#include <string.h>
extern void __cfree_wasm_init(void *);
extern int32_t test_main(void *);
+
+/* Canned host import, mirroring jit_runner.c / start_wasm.c / driver/run.c. */
+static int32_t test_host_add(void *inst, int32_t a, int32_t b) {
+ (void)inst;
+ return a + b;
+}
+
+/* Import descriptors emitted by lang/wasm/cg.c. The C lane compiles for the
+ * native target (aarch64/x86_64-*), so this is the 64-bit layout used by
+ * start_wasm.c, with full module/field name pointers — match by name and bind
+ * the canned host function into the instance import slot before
+ * __cfree_wasm_init. Weak DEFAULTS (not weak externs): a module with imports
+ * emits its own strong __cfree_wasm_imports/__cfree_wasm_nimports that override
+ * these; a module with none falls back to nimports=0. (A weak *undefined* ref
+ * would be rejected by the Mach-O static linker.) */
+typedef struct {
+ const char *module;
+ const char *field;
+ unsigned int typeidx;
+ unsigned int slot_offset;
+} WasmImportDesc;
+__attribute__((weak)) const unsigned int __cfree_wasm_nimports = 0;
+__attribute__((weak)) const WasmImportDesc __cfree_wasm_imports[1] = {{0, 0, 0, 0}};
+
+static void bind_canned_imports(void *instance) {
+ for (unsigned int i = 0; i < __cfree_wasm_nimports; ++i) {
+ const WasmImportDesc *d = &__cfree_wasm_imports[i];
+ if (strcmp(d->module, "env") == 0 && strcmp(d->field, "host_add") == 0)
+ *(void **)((unsigned char *)instance + d->slot_offset) =
+ (void *)(uintptr_t)test_host_add;
+ }
+}
+
typedef struct { unsigned char *data; unsigned long long pages;
unsigned long long max_pages; unsigned int flags; } WasmStartMemoryPrefix;
#define WASM_START_MEMORY_PREFIX_COUNT 8u
@@ -123,6 +157,9 @@ int main(void) {
for (unsigned int i = 0; i < WASM_START_MEMORY_PREFIX_COUNT; ++i)
((WasmStartMemoryPrefix *)instance)[i].data =
memory + i * (WASM_START_MEMORY_SIZE / WASM_START_MEMORY_PREFIX_COUNT);
+ /* After the memory prefix (which can overlap import slots for memory-less
+ * modules), before init (which consumes the bound slots). */
+ bind_canned_imports(instance);
__cfree_wasm_init(instance);
return (int)test_main(instance);
}