kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Msrc/arch/wasm/emit.c | 32++++++++++++++++++++++++++++----
Mtest/wasm/run.sh | 37+++++++++++++++++++++++++++++++++++++
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); }