commit 0472aa10c48a2d616c7e4d7da3e347ebe23687cf parent f606edf7a5d3cbcd8ce1ac62dd8586940f763c6e Author: Ryan Sepassi <rsepassi@gmail.com> Date: Fri, 12 Jun 2026 17:32:19 -0700 feat(wasm,cg): multi-value blocks + structured-API frontend migration Ingest/represent real WebAssembly multi-value: block/loop/if carrying N params AND N results (incl. typeidx blocktypes and multi-result func types in the type section), and move the Wasm frontend onto the structured CG control-flow API instead of raw labels + manual spills. Module layer (src/wasm): WasmFunc/WasmFuncType.results become heap vectors; WASM_BLOCKTYPE_TYPEIDX sentinel in WasmInsn.type with the index in .imm (no new field); decode reads the s33 blocktype; encode writes the typeidx as sleb (single-result shorthand stays one byte for fidelity); wat.c parses (type idx)?(param)*(result)* blocktypes and multi-result func/import sigs; validate.c control frames carry param/result vectors. emit.c gains a stack buffer for the now-pointer results. CG layer (src/cg): new KitCgScopeSig + kit_cg_{scope,block}_begin_sig, kit_cg_scope_store_params, kit_cg_scope_end_unreachable, kit_cg_stack_depth. ApiCgScope holds N result + N loop-param carry locals (inline, heap spill). The single-result wrappers reduce to the old op sequence byte-for-byte; no backend/recording-IR/optimizer change needed. Frontend (lang/wasm/cg.c): delete the manual spill machinery; drive the structured scopes. if/else = block scope + else label + arena param snapshot; br = break / loop store-params+continue; br_if = branch-around; br_table = per-distinct-target trampolines; new unreachable tracking (dead flag + per-frame base, scope_end_unreachable, dead frames). Tests: extend cg_control_test.c with multi-value block/loop cases; add 15 cases/* and 5 err/* covering multi-value blocks, loop params, if/else params, br/br_if/br_table carrying values, dead code, and validation errors. Existing control-flow .expect exit codes unchanged. Diffstat:
47 files changed, 1437 insertions(+), 479 deletions(-)
diff --git a/include/kit/cg.h b/include/kit/cg.h @@ -547,7 +547,42 @@ KIT_API KitCgScope kit_cg_scope_begin(KitCg*, KitCgTypeId result_type); * backends with structured control flow (Wasm) emit `block`/`end` directly * instead of recognizing arbitrary forward-only label-and-jump patterns. */ KIT_API KitCgScope kit_cg_block_begin(KitCg*, KitCgTypeId result_type); + +/* Multi-value scopes: a block/loop that carries N params and/or N results + * (the value-stack analogue of a Wasm multi-value block). params/results are + * listed value-stack bottom -> top. + * + * block_begin_sig: params stay on the value stack for the body to consume; + * break / scope_end carry `nresults` values across the exit edge. + * scope_begin_sig (loop): the API snapshots the `nparams` params off the + * stack into loop-param locals, then reloads them so every back edge sees + * the same shape. A back edge re-supplies fresh params via + * kit_cg_scope_store_params (which pops `nparams` into those locals, no + * jump) followed by a jump to kit_cg_scope_continue_label. + * + * kit_cg_scope_begin / kit_cg_block_begin are the nparams==0, nresults<=1 + * special cases and emit an identical op sequence. break, break_true, + * break_false, the continue ops and scope_end all generalize to N values. */ +typedef struct KitCgScopeSig { + const KitCgTypeId* params; /* value-stack bottom -> top */ + uint32_t nparams; + const KitCgTypeId* results; /* value-stack bottom -> top */ + uint32_t nresults; +} KitCgScopeSig; +KIT_API KitCgScope kit_cg_scope_begin_sig(KitCg*, const KitCgScopeSig*); /*loop*/ +KIT_API KitCgScope kit_cg_block_begin_sig(KitCg*, const KitCgScopeSig*); +/* Pop `nparams` values into the loop's param locals (bottom->top), without + * jumping. The caller then jumps kit_cg_scope_continue_label to take the back + * edge. Valid only on a loop scope (one opened with a continue label). */ +KIT_API void kit_cg_scope_store_params(KitCg*, KitCgScope); + KIT_API void kit_cg_scope_end(KitCg*, KitCgScope); +/* Close a scope whose fall-through is unreachable (its body ended in a + * branch/return so there are no fall-through results to consume). Unlike + * kit_cg_scope_end it does not pop results off the stack; the value stack must + * already be at the scope's base depth. The scope's results (set by whatever + * branch exited it) are pushed on exit, exactly as kit_cg_scope_end does. */ +KIT_API void kit_cg_scope_end_unreachable(KitCg*, KitCgScope); /* Expose the labels CG minted for this scope. Frontends that emit * unstructured `jump`/`branch` ops (rather than going through the * scope_break/scope_continue helpers) can use these to land control at @@ -650,6 +685,10 @@ KIT_API void kit_cg_dup2(KitCg*); /* duplicates the top two slots */ KIT_API void kit_cg_swap(KitCg*); KIT_API void kit_cg_drop(KitCg*); KIT_API void kit_cg_rot3(KitCg*); /* [..., a, b, c] -> [..., b, c, a] */ +/* The current value-stack depth. Lets a frontend record a base depth and later + * drop back to it (e.g. discarding the operands a Wasm polymorphic/unreachable + * region left behind). */ +KIT_API uint32_t kit_cg_stack_depth(KitCg*); /* Returns nonzero when the current top-of-stack value is a compile-time known * integer-like immediate VALUE. The value is not popped. */ diff --git a/lang/wasm/cg.c b/lang/wasm/cg.c @@ -2011,52 +2011,190 @@ static void wasm_cg_cache_funcref_entry( } /* Per-function control-stack entry for the opcode walk in wasm_emit_cg_into: - * one frame per structured block/loop/if, carrying its CG labels and — for a - * result-typed block — the spill local that carries the single result value - * across the structured-control merge. The wasm operand stack maps onto the - * cg value stack, which is fragile across labels/branches; routing a block - * result through a dedicated local makes every exit edge (fall-through, - * else-jump, br/br_if/br_table) converge on one location that the end label - * reloads. */ + * one frame per structured block/loop/if. Each live frame owns a structured CG + * scope (kit_cg_block_begin_sig / kit_cg_scope_begin_sig); the wasm operand + * stack maps onto the CG value stack, and the scope's carry locals move the + * block's results (and a loop's params) across the structured-control edges. + * + * A frame opened in dead code (after a branch terminator, before the matching + * else/end) is marked `entry_dead` and owns no CG scope — it only tracks + * nesting so the matching end pops the right depth. `base` is the CG value-stack + * depth just below the block's result region, used to discard the operands a + * Wasm polymorphic/unreachable region leaves behind. */ typedef struct WasmCgControl { - uint8_t kind; - uint8_t result; /* WasmValType of the block result, 0 == void */ - int seen_else; - KitCgLabel start; - KitCgLabel end; - KitCgLabel else_label; - KitCgLocal result_local; /* valid iff result != 0 */ - KitCgMemAccess result_mem; + uint8_t kind; /* WASM_INSN_BLOCK / LOOP / IF */ + uint8_t seen_else; /* an else has been processed (if frames) */ + uint8_t entry_dead; /* opened in dead code => no CG scope */ + uint8_t pad; + KitCgScope scope; /* valid iff !entry_dead */ + uint32_t base; /* CG value-stack depth below the block's results */ + KitCgTypeId* params; /* CG types, arena-owned; valid iff !entry_dead */ + uint32_t nparams; + KitCgTypeId* results; + uint32_t nresults; + KitCgLabel else_label; /* if frames: the false-arm landing pad */ + KitCgLocal* param_tmps; /* if frames: nparams snapshot locals for the else */ } WasmCgControl; -/* Pop the cg value-stack top into a frame's result spill local. */ -static void wasm_cg_spill_result(KitCg* cg, const WasmCgControl* fr) { - kit_cg_push_local(cg, fr->result_local); - kit_cg_swap(cg); - kit_cg_store(cg, fr->result_mem); +/* Resolve a block/loop/if instruction's signature into CG-type arrays stored on + * `fr` (arena-owned). */ +static void wasm_cg_resolve_sig(KitCompiler* c, KitCgBuiltinTypes b, + const WasmModule* m, KitArena* arena, + const WasmInsn* in, WasmCgControl* fr) { + WasmValType scratch; + WasmBlockSig sig; + uint32_t k; + wasm_block_sig(m, in, &scratch, &sig); + fr->nparams = sig.nparams; + fr->nresults = sig.nresults; + fr->params = + sig.nparams ? kit_arena_array(arena, KitCgTypeId, sig.nparams) : NULL; + fr->results = + sig.nresults ? kit_arena_array(arena, KitCgTypeId, sig.nresults) : NULL; + for (k = 0; k < sig.nparams; ++k) + fr->params[k] = wasm_cg_type(c, b, sig.params[k]); + for (k = 0; k < sig.nresults; ++k) + fr->results[k] = wasm_cg_type(c, b, sig.results[k]); } -/* Push a frame's result spill local back onto the cg value stack. */ -static void wasm_cg_reload_result(KitCg* cg, const WasmCgControl* fr) { - kit_cg_push_local(cg, fr->result_local); - kit_cg_load(cg, fr->result_mem); +/* Discard value-stack operands down to `base`. */ +static void wasm_cg_drop_to(KitCg* cg, uint32_t base) { + while (kit_cg_stack_depth(cg) > base) kit_cg_drop(cg); } -/* Initialise a freshly-pushed control frame's result spill state. `rt_result` - * is the block's WasmValType (0 == void); for a void block this leaves the - * frame's result fields zeroed. */ -static void wasm_cg_control_set_result(KitCompiler* c, KitCg* cg, - KitCgBuiltinTypes b, WasmCgControl* fr, - uint8_t rt_result) { - fr->result = rt_result; - if (rt_result) { - KitCgLocalAttrs attrs; - memset(&attrs, 0, sizeof attrs); - attrs.flags = KIT_CG_LOCAL_COMPILER_TEMP; - fr->result_local = - kit_cg_local(cg, wasm_cg_type(c, b, (WasmValType)rt_result), attrs); - fr->result_mem = wasm_cg_mem(c, b, (WasmValType)rt_result); +/* Re-push `n` snapshot temps onto the stack (bottom->top). */ +static void wasm_cg_push_temps(KitCg* cg, const KitCgTypeId* types, + const KitCgLocal* tmps, uint32_t n) { + uint32_t k; + for (k = 0; k < n; ++k) + kit_cg_local_read(cg, tmps[k], wasm_cg_mem_type(types[k])); +} + +/* Pop `n` values (bottom->top types[]) into fresh temp locals; with `restore`, + * re-push them so the originals stay on the stack (a duplicate snapshot). + * Returns the temp locals (arena-owned). */ +static KitCgLocal* wasm_cg_save_values(KitCg* cg, KitArena* arena, + const KitCgTypeId* types, uint32_t n, + int restore) { + KitCgLocal* tmps; + KitCgLocalAttrs attrs; + uint32_t k; + if (!n) return NULL; + tmps = kit_arena_array(arena, KitCgLocal, n); + memset(&attrs, 0, sizeof attrs); + attrs.flags = KIT_CG_LOCAL_COMPILER_TEMP; + for (k = 0; k < n; ++k) tmps[k] = kit_cg_local(cg, types[k], attrs); + for (k = 0; k < n; ++k) + kit_cg_local_write(cg, tmps[n - 1u - k], wasm_cg_mem_type(types[n - 1u - k])); + if (restore) wasm_cg_push_temps(cg, types, tmps, n); + return tmps; +} + +/* Branch to a live control frame carrying its label values (already on the + * stack): a loop takes its params (store + jump continue), a block/if takes its + * results (break). Consumes the label values. */ +static void wasm_cg_branch_to(KitCg* cg, WasmCgControl* fr) { + if (fr->kind == WASM_INSN_LOOP) { + kit_cg_scope_store_params(cg, fr->scope); + kit_cg_jump(cg, kit_cg_scope_continue_label(cg, fr->scope)); + } else { + kit_cg_break(cg, fr->scope); + } +} + +/* The label values a branch to `fr` transfers (loop -> params, else results). */ +static const KitCgTypeId* wasm_cg_label_types(const WasmCgControl* fr, + uint32_t* n) { + if (fr->kind == WASM_INSN_LOOP) { + *n = fr->nparams; + return fr->params; } + *n = fr->nresults; + return fr->results; +} + +/* Exit an if's then-arm and enter its else arm: carry then-results to the block + * break (or discard them if the then-arm terminated), place the else label, and + * re-push the snapshotted params for the else arm. */ +static void wasm_cg_enter_else(KitCg* cg, WasmCgControl* fr, int then_dead) { + if (then_dead) + wasm_cg_drop_to(cg, fr->base); + else + kit_cg_break(cg, fr->scope); + kit_cg_label_place(cg, fr->else_label); + wasm_cg_push_temps(cg, fr->params, fr->param_tmps, fr->nparams); +} + +/* Open a block/loop/if frame into `fr`. In dead code the frame owns no CG scope + * (just nesting); otherwise it opens the structured scope, and an `if` also + * lifts the condition, snapshots its params for the else arm, and branches the + * false arm to a fresh else label. */ +static void wasm_cg_open_frame(KitCompiler* c, KitCg* cg, KitCgBuiltinTypes b, + const WasmModule* m, KitArena* arena, + WasmCgControl* fr, const WasmInsn* in, int dead) { + KitCgScopeSig sig; + memset(fr, 0, sizeof *fr); + fr->kind = in->kind; + fr->entry_dead = dead ? 1u : 0u; + if (dead) return; + wasm_cg_resolve_sig(c, b, m, arena, in, fr); + memset(&sig, 0, sizeof sig); + sig.params = fr->params; + sig.nparams = fr->nparams; + sig.results = fr->results; + sig.nresults = fr->nresults; + if (in->kind == WASM_INSN_IF) { + fr->else_label = kit_cg_label_new(cg); + if (fr->nparams == 0) { + fr->scope = kit_cg_block_begin_sig(cg, &sig); + kit_cg_branch_false(cg, fr->else_label); /* pop cond */ + } else { + KitCgLocalAttrs attrs; + KitCgLocal cond_tmp; + KitCgMemAccess i32_mem = wasm_cg_mem(c, b, WASM_VAL_I32); + memset(&attrs, 0, sizeof attrs); + attrs.flags = KIT_CG_LOCAL_COMPILER_TEMP; + cond_tmp = kit_cg_local(cg, b.id[KIT_CG_BUILTIN_I32], attrs); + /* Lift the condition above the params, snapshot the params for the else + * arm, then re-test the condition once the block scope is open. */ + kit_cg_local_write(cg, cond_tmp, i32_mem); + fr->param_tmps = wasm_cg_save_values(cg, arena, fr->params, fr->nparams, 1); + fr->scope = kit_cg_block_begin_sig(cg, &sig); + kit_cg_local_read(cg, cond_tmp, i32_mem); + kit_cg_branch_false(cg, fr->else_label); + } + } else if (in->kind == WASM_INSN_LOOP) { + fr->scope = kit_cg_scope_begin_sig(cg, &sig); + } else { + fr->scope = kit_cg_block_begin_sig(cg, &sig); + } + fr->base = kit_cg_stack_depth(cg) - fr->nparams; +} + +/* Process an `else`: returns the new dead state (the else arm's reachability). */ +static int wasm_cg_handle_else(KitCg* cg, WasmCgControl* fr, int dead) { + if (fr->entry_dead) return dead; /* else of a dead if; stays dead */ + wasm_cg_enter_else(cg, fr, dead); + fr->seen_else = 1u; + return 0; +} + +/* Process an `end`: closes the frame's scope and returns the post-end dead + * state. The caller pops the frame afterwards. */ +static int wasm_cg_handle_end(KitCg* cg, WasmCgControl* fr, int dead) { + if (fr->entry_dead) return dead; /* dead frame; nothing to close */ + if (fr->kind == WASM_INSN_IF && !fr->seen_else) { + /* No else arm: the implicit empty else passes the params through as the + * results (the validator guarantees params == results). */ + wasm_cg_enter_else(cg, fr, dead); + kit_cg_scope_end(cg, fr->scope); + return 0; + } + if (dead) + kit_cg_scope_end_unreachable(cg, fr->scope); + else + kit_cg_scope_end(cg, fr->scope); + return 0; } void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { @@ -2421,49 +2559,33 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { wasm_cg_push_zero(c, cg, b, f->locals[j]); kit_cg_store(cg, wasm_cg_mem(c, b, f->locals[j])); } + /* The CG value-stack depth at the function's statement level (operands + * empty), and whether the current position is unreachable (after a branch + * terminator). Dead non-control ops are skipped; the structured scope ops + * still track nesting so the matching end pops the right depth. */ + uint32_t func_base = kit_cg_stack_depth(cg); + int dead = 0; for (j = 0; j < f->ninsns; ++j) { WasmInsn in = f->insns[j]; + uint32_t cur_base; kit_cg_set_loc(cg, in.loc); + /* Skip dead value ops; only block/loop/if/else/end keep their bookkeeping + * so nesting (and the eventual scope close) stays balanced. */ + if (dead && in.kind != WASM_INSN_BLOCK && in.kind != WASM_INSN_LOOP && + in.kind != WASM_INSN_IF && in.kind != WASM_INSN_ELSE && + in.kind != WASM_INSN_END) + continue; + cur_base = ncontrol ? control[ncontrol - 1u].base : func_base; switch (in.kind) { case WASM_INSN_UNREACHABLE: wasm_cg_trap_unreachable(cg, &rt); + wasm_cg_drop_to(cg, cur_base); + dead = 1; break; case WASM_INSN_NOP: break; case WASM_INSN_BLOCK: - if (ncontrol == control_cap) { - uint32_t new_cap = control_cap * 2u; - void* p = heap->realloc( - heap, control, sizeof(WasmCgControl) * control_cap, - sizeof(WasmCgControl) * new_cap, _Alignof(WasmCgControl)); - if (!p) wasm_error(c, in.loc, "wasm: out of memory"); - control = (WasmCgControl*)p; - control_cap = new_cap; - } - memset(&control[ncontrol], 0, sizeof control[ncontrol]); - control[ncontrol].kind = WASM_INSN_BLOCK; - control[ncontrol].end = kit_cg_label_new(cg); - wasm_cg_control_set_result(c, cg, b, &control[ncontrol], in.type); - ncontrol++; - break; case WASM_INSN_LOOP: - if (ncontrol == control_cap) { - uint32_t new_cap = control_cap * 2u; - void* p = heap->realloc( - heap, control, sizeof(WasmCgControl) * control_cap, - sizeof(WasmCgControl) * new_cap, _Alignof(WasmCgControl)); - if (!p) wasm_error(c, in.loc, "wasm: out of memory"); - control = (WasmCgControl*)p; - control_cap = new_cap; - } - memset(&control[ncontrol], 0, sizeof control[ncontrol]); - control[ncontrol].kind = WASM_INSN_LOOP; - control[ncontrol].start = kit_cg_label_new(cg); - control[ncontrol].end = kit_cg_label_new(cg); - wasm_cg_control_set_result(c, cg, b, &control[ncontrol], in.type); - kit_cg_label_place(cg, control[ncontrol].start); - ncontrol++; - break; case WASM_INSN_IF: if (ncontrol == control_cap) { uint32_t new_cap = control_cap * 2u; @@ -2474,147 +2596,130 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { control = (WasmCgControl*)p; control_cap = new_cap; } - memset(&control[ncontrol], 0, sizeof control[ncontrol]); - control[ncontrol].kind = WASM_INSN_IF; - control[ncontrol].else_label = kit_cg_label_new(cg); - control[ncontrol].end = kit_cg_label_new(cg); - /* Pop the condition before allocating the result local so the spill - * temp does not capture it. */ - kit_cg_branch_false(cg, control[ncontrol].else_label); - wasm_cg_control_set_result(c, cg, b, &control[ncontrol], in.type); + wasm_cg_open_frame(c, cg, b, m, arena, &control[ncontrol], &in, dead); ncontrol++; break; case WASM_INSN_ELSE: if (!ncontrol || control[ncontrol - 1u].kind != WASM_INSN_IF) wasm_error(c, in.loc, "wasm: else without if"); - /* The then-arm's result is on the value stack; spill it so it joins - * the else-arm's result at the shared end. */ - if (control[ncontrol - 1u].result) - wasm_cg_spill_result(cg, &control[ncontrol - 1u]); - kit_cg_jump(cg, control[ncontrol - 1u].end); - kit_cg_label_place(cg, control[ncontrol - 1u].else_label); - control[ncontrol - 1u].seen_else = 1; + dead = wasm_cg_handle_else(cg, &control[ncontrol - 1u], dead); break; case WASM_INSN_END: if (!ncontrol) wasm_error(c, in.loc, "wasm: end without block"); + dead = wasm_cg_handle_end(cg, &control[ncontrol - 1u], dead); ncontrol--; - /* Spill the fall-through result before the merge point so it joins - * any branch-supplied results in the same local, then reload it - * after the end label as the block's single output. The validator - * has already proven the stack is well-typed here. */ - if (control[ncontrol].result) - wasm_cg_spill_result(cg, &control[ncontrol]); - if (control[ncontrol].kind == WASM_INSN_IF && - !control[ncontrol].seen_else) - kit_cg_label_place(cg, control[ncontrol].else_label); - kit_cg_label_place(cg, control[ncontrol].end); - if (control[ncontrol].result) - wasm_cg_reload_result(cg, &control[ncontrol]); break; case WASM_INSN_BR: { uint32_t depth = (uint32_t)in.imm; - uint32_t idx; if (depth >= ncontrol) wasm_error(c, in.loc, "wasm: branch depth out of range"); - idx = ncontrol - 1u - depth; - /* A branch to a block/if carries the block's result; spill the - * value-stack top into that target's result local before the jump - * (a branch to a loop carries nothing). */ - if (control[idx].kind != WASM_INSN_LOOP && control[idx].result) - wasm_cg_spill_result(cg, &control[idx]); - kit_cg_jump(cg, control[idx].kind == WASM_INSN_LOOP - ? control[idx].start - : control[idx].end); + wasm_cg_branch_to(cg, &control[ncontrol - 1u - depth]); + wasm_cg_drop_to(cg, cur_base); + dead = 1; break; } case WASM_INSN_BR_IF: { uint32_t depth = (uint32_t)in.imm; uint32_t idx; + WasmCgControl* tgt; + const KitCgTypeId* lt; + uint32_t nlt; + KitCgLocal cond_tmp; + KitCgLocal* carry_tmps; + KitCgLabel skip; + KitCgLocalAttrs attrs; + KitCgMemAccess i32_mem = wasm_cg_mem(c, b, WASM_VAL_I32); if (depth >= ncontrol) wasm_error(c, in.loc, "wasm: branch depth out of range"); idx = ncontrol - 1u - depth; - /* Conditional branch: the carried value must reach the target on the - * taken path and remain on the stack for the fall-through. Spill - * (taken-path copy) then reload (fall-through copy) before testing. */ - if (control[idx].kind != WASM_INSN_LOOP && control[idx].result) { - wasm_cg_spill_result(cg, &control[idx]); - wasm_cg_reload_result(cg, &control[idx]); - } - kit_cg_branch_true(cg, control[idx].kind == WASM_INSN_LOOP - ? control[idx].start - : control[idx].end); + tgt = &control[idx]; + lt = wasm_cg_label_types(tgt, &nlt); + /* Branch-around: lift the condition, snapshot the carried label + * values (so they survive on the fall-through), and on the taken arm + * re-push the copies and branch — consuming them — to the target. */ + memset(&attrs, 0, sizeof attrs); + attrs.flags = KIT_CG_LOCAL_COMPILER_TEMP; + cond_tmp = kit_cg_local(cg, b.id[KIT_CG_BUILTIN_I32], attrs); + kit_cg_local_write(cg, cond_tmp, i32_mem); /* pop cond */ + carry_tmps = wasm_cg_save_values(cg, arena, lt, nlt, 1); + skip = kit_cg_label_new(cg); + kit_cg_local_read(cg, cond_tmp, i32_mem); + kit_cg_branch_false(cg, skip); + wasm_cg_push_temps(cg, lt, carry_tmps, nlt); + wasm_cg_branch_to(cg, tgt); + kit_cg_label_place(cg, skip); break; } case WASM_INSN_BR_TABLE: { KitCgSwitch sw; + KitCgSwitchCase* cases; + WasmCgControl* dflt; + const KitCgTypeId* lt; + uint32_t nlt; + KitCgLocal sel_tmp; + KitCgLocal* carry_tmps; + KitCgLabel* distinct_tramp; + uint32_t* distinct_ctrl; + KitCgLabel* per_target; + uint32_t ndistinct = 0; + uint32_t k, q; + KitCgLocalAttrs attrs; + KitCgMemAccess i32_mem = wasm_cg_mem(c, b, WASM_VAL_I32); if (in.ntargets == 0) wasm_error(c, in.loc, "wasm: bad br_table target count"); - /* If the targets carry a result, spill it into each distinct - * target's result local before the switch consumes the selector. - * The selector sits above the result on the value stack, so lift it - * into a temp, store the single result into the default target's - * local, copy it into the other distinct targets, then restore the - * selector. The validator guarantees every target shares the same - * label type, so one value services them all. */ - { - uint32_t dflt = ncontrol - 1u - in.targets[in.ntargets - 1u]; - if (dflt < ncontrol && control[dflt].kind != WASM_INSN_LOOP && - control[dflt].result) { - KitCgLocalAttrs sa; - KitCgLocal seltmp; - memset(&sa, 0, sizeof sa); - sa.flags = KIT_CG_LOCAL_COMPILER_TEMP; - seltmp = kit_cg_local(cg, b.id[KIT_CG_BUILTIN_I32], sa); - kit_cg_push_local(cg, seltmp); - kit_cg_swap(cg); - kit_cg_store(cg, wasm_cg_mem(c, b, WASM_VAL_I32)); - /* Stack top is now the single result value: spill into default. */ - wasm_cg_spill_result(cg, &control[dflt]); - for (uint32_t k = 0; k + 1u < in.ntargets; ++k) { - uint32_t tidx = ncontrol - 1u - in.targets[k]; - int dup = (tidx == dflt); - for (uint32_t q = 0; q < k && !dup; ++q) - if (ncontrol - 1u - in.targets[q] == tidx) dup = 1; - if (dup || control[tidx].kind == WASM_INSN_LOOP || - !control[tidx].result) - continue; - wasm_cg_reload_result(cg, &control[dflt]); - wasm_cg_spill_result(cg, &control[tidx]); + for (k = 0; k < in.ntargets; ++k) + if (in.targets[k] >= ncontrol) + wasm_error(c, in.loc, "wasm: branch depth out of range"); + dflt = &control[ncontrol - 1u - in.targets[in.ntargets - 1u]]; + lt = wasm_cg_label_types(dflt, &nlt); + /* Lift the selector, then save the carried label values once so each + * per-target trampoline can re-push and branch with them. */ + memset(&attrs, 0, sizeof attrs); + attrs.flags = KIT_CG_LOCAL_COMPILER_TEMP; + sel_tmp = kit_cg_local(cg, b.id[KIT_CG_BUILTIN_I32], attrs); + kit_cg_local_write(cg, sel_tmp, i32_mem); /* pop selector */ + carry_tmps = wasm_cg_save_values(cg, arena, lt, nlt, 0); + /* One trampoline per distinct target control frame. */ + distinct_tramp = kit_arena_array(arena, KitCgLabel, in.ntargets); + distinct_ctrl = kit_arena_array(arena, uint32_t, in.ntargets); + per_target = kit_arena_array(arena, KitCgLabel, in.ntargets); + for (k = 0; k < in.ntargets; ++k) { + uint32_t ci = ncontrol - 1u - in.targets[k]; + int found = -1; + for (q = 0; q < ndistinct; ++q) + if (distinct_ctrl[q] == ci) { + found = (int)q; + break; } - /* Restore the selector for the switch. */ - kit_cg_push_local(cg, seltmp); - kit_cg_load(cg, wasm_cg_mem(c, b, WASM_VAL_I32)); + if (found < 0) { + distinct_ctrl[ndistinct] = ci; + distinct_tramp[ndistinct] = kit_cg_label_new(cg); + found = (int)ndistinct++; } + per_target[k] = distinct_tramp[found]; } - KitCgSwitchCase* cases = - kit_arena_zarray(arena, KitCgSwitchCase, in.ntargets); - for (uint32_t k = 0; k + 1u < in.ntargets; ++k) { - uint32_t idx; - if (in.targets[k] >= ncontrol) - wasm_error(c, in.loc, "wasm: branch depth out of range"); - idx = ncontrol - 1u - in.targets[k]; + cases = kit_arena_zarray(arena, KitCgSwitchCase, in.ntargets); + for (k = 0; k + 1u < in.ntargets; ++k) { cases[k].value = k; - cases[k].label = control[idx].kind == WASM_INSN_LOOP - ? control[idx].start - : control[idx].end; + cases[k].label = per_target[k]; } - if (in.targets[in.ntargets - 1u] >= ncontrol) - wasm_error(c, in.loc, "wasm: branch depth out of range"); + kit_cg_local_read(cg, sel_tmp, i32_mem); memset(&sw, 0, sizeof sw); sw.selector_type = b.id[KIT_CG_BUILTIN_I32]; sw.cases = cases; sw.ncases = in.ntargets - 1u; - sw.default_label = - control[ncontrol - 1u - in.targets[in.ntargets - 1u]].kind == - WASM_INSN_LOOP - ? control[ncontrol - 1u - in.targets[in.ntargets - 1u]].start - : control[ncontrol - 1u - in.targets[in.ntargets - 1u]].end; - /* br_table is dense-by-construction (case values 0..N-1); - * tell targets that care that a jump table is the natural - * lowering. Targets that don't honour the hint fall back to - * a cmp chain, which is still correct. */ + sw.default_label = per_target[in.ntargets - 1u]; + /* br_table is dense-by-construction (case values 0..N-1); hint a + * jump table. Targets that ignore the hint fall back to a cmp chain. */ sw.hint = KIT_CG_SWITCH_JUMP_TABLE; kit_cg_switch(cg, sw); + for (q = 0; q < ndistinct; ++q) { + kit_cg_label_place(cg, distinct_tramp[q]); + wasm_cg_push_temps(cg, lt, carry_tmps, nlt); + wasm_cg_branch_to(cg, &control[distinct_ctrl[q]]); + } + wasm_cg_drop_to(cg, cur_base); + dead = 1; break; } case WASM_INSN_SELECT: { @@ -2944,6 +3049,8 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { } case WASM_INSN_RETURN: kit_cg_ret(cg); + wasm_cg_drop_to(cg, cur_base); + dead = 1; break; case WASM_INSN_DROP: kit_cg_drop(cg); @@ -4046,11 +4153,11 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { } } } - /* If the body's final instruction was an explicit RETURN (or a - * required-tail call), the SValue stack is already empty and the - * function's prologue/epilogue is closed — emitting another ret here - * underflows. Skip in that case; the wasm validator already proved - * every reachable exit returns the right shape. */ + /* If the body's final reachable position is unreachable (it ended in a + * branch/return/unreachable), the SValue stack is already balanced and the + * function's epilogue is closed — emitting another ret here would + * underflow. `dead` tracks exactly this; the textual-terminator check is a + * belt-and-suspenders fallback. */ { uint32_t last = f->ninsns; int body_terminates; @@ -4062,7 +4169,7 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { f->insns[last - 1u].kind == WASM_INSN_RETURN_CALL_INDIRECT || f->insns[last - 1u].kind == WASM_INSN_RETURN_CALL_REF || f->insns[last - 1u].kind == WASM_INSN_UNREACHABLE); - if (!body_terminates) kit_cg_ret(cg); + if (!dead && !body_terminates) kit_cg_ret(cg); } kit_cg_func_end(cg); heap->free(heap, control, sizeof(WasmCgControl) * control_cap); diff --git a/src/arch/wasm/emit.c b/src/arch/wasm/emit.c @@ -674,9 +674,10 @@ void wasm_func_begin(CGTarget* tg, const CGFuncDesc* d) { if (!abi->has_sret && abi->ret.kind == ABI_ARG_DIRECT && abi->ret.nparts == 1) { const ABIArgPart* p = &abi->ret.parts[0]; - f->results[0] = valtype_for_size_kind( - t, p->size, p->cls == ABI_CLASS_FP ? ABI_SC_FLOAT : ABI_SC_INT); - f->nresults = 1; + wasm_func_push_result( + t->c, t->module, f, + valtype_for_size_kind( + t, p->size, p->cls == ABI_CLASS_FP ? ABI_SC_FLOAT : ABI_SC_INT)); } f->typeidx = wasm_intern_func_type(t->c, t->module, f); @@ -941,9 +942,15 @@ static u32 abi_to_wasm_func_type(WTarget* t, const ABIFuncInfo* abi, WasmValType* result_out, u32* nresults_out, const char* what) { WasmFunc tmp; + /* results is a pointer now (multi-value); back it with a small stack buffer + * just like the caller-supplied params buffer, so tmp.results[0] = ... below + * is not a NULL deref. A direct return lowers to at most one result. */ + WasmValType results[4]; memset(&tmp, 0, sizeof tmp); tmp.params = params; tmp.cap_params = cap; + tmp.results = results; + tmp.cap_results = 4u; if (abi->has_sret) { if (tmp.nparams >= cap) wfail(t, "wasm: %s has too many params", what); params[tmp.nparams++] = WASM_VAL_I32; @@ -1052,8 +1059,7 @@ static void promote_import_func(WTarget* t, ObjSymId sym, WasmFunc* f, /* Mirror the synthesized params/results onto the WasmFunc so the import * encoder writes the matching signature. */ wasm_func_set_params(t->c, t->module, f, params, nparams); - f->nresults = nresults; - if (nresults) f->results[0] = result_vt; + wasm_func_set_results(t->c, t->module, f, &result_vt, nresults); /* Resolve module/name overrides set via __attribute__((import_module/ * import_name)) on the C declaration. */ (void)wasm_imports_get(t->obj, os->name, &attr_module, &attr_name); diff --git a/src/cg/control.c b/src/cg/control.c @@ -472,49 +472,140 @@ ApiCgScope* api_scope_from_handle(KitCg* g, KitCgScope scope, int require_top, return s; } -int api_scope_has_result(const ApiCgScope* s) { - return s->result_type != KIT_CG_TYPE_NONE; +static int api_require_loop_scope(KitCg* g, const ApiCgScope* s, + const char* op); + +int api_scope_has_result(const ApiCgScope* s) { return s->nresults != 0; } + +/* Store one already-popped value into a scope carry local. Shared by the result + * and loop-param paths; `local`/`type` select which carry slot. */ +static void api_scope_store_one(KitCg* g, CGLocal local, KitCgTypeId type, + ApiSValue* v) { + Operand dst = api_op_local(local, type); + Operand src = + api_sv_op_is_local_or_imm(v) ? v->op : api_force_local(g, v, type); + g->target->store(g->target, dst, src, api_mem_for_lvalue(g, &dst, type)); + api_release(g, v); } -void api_scope_store_result(KitCg* g, ApiCgScope* s, ApiSValue* result) { - Operand dst; - Operand src; - if (!api_scope_has_result(s)) return; - dst = api_op_local(s->result_local, s->result_type); - src = api_sv_op_is_local_or_imm(result) - ? result->op - : api_force_local(g, result, s->result_type); - g->target->store(g->target, dst, src, - api_mem_for_lvalue(g, &dst, s->result_type)); - api_release(g, result); +/* Reload one carry local onto the value stack (fresh temp, load, push). */ +static void api_scope_reload_one(KitCg* g, CGLocal local, KitCgTypeId type) { + CGLocal r = api_alloc_temp_local(g, type); + Operand dst = api_op_local(r, type); + Operand src = api_op_local(local, type); + g->target->load(g->target, dst, src, api_mem_for_lvalue(g, &src, type)); + api_push(g, api_make_sv(dst, type)); } -void api_scope_push_result(KitCg* g, ApiCgScope* s) { - Operand dst; - Operand src; - CGLocal r; - if (!api_scope_has_result(s)) return; - r = api_alloc_temp_local(g, s->result_type); - dst = api_op_local(r, s->result_type); - src = api_op_local(s->result_local, s->result_type); - g->target->load(g->target, dst, src, - api_mem_for_lvalue(g, &src, s->result_type)); - api_push(g, api_make_sv(dst, s->result_type)); +/* Pop the scope's N results into a caller array (out[i] = result i, value-stack + * bottom->top). The top of stack — the last result — is popped first. */ +static void api_scope_pop_results(KitCg* g, ApiCgScope* s, ApiSValue* out) { + u32 k; + for (k = 0; k < s->nresults; ++k) + out[s->nresults - 1u - k] = api_pop(g); +} + +/* Pop N results off the value stack and store each into its carry local. */ +void api_scope_store_results(KitCg* g, ApiCgScope* s) { + u32 k; + for (k = 0; k < s->nresults; ++k) { + u32 idx = s->nresults - 1u - k; /* TOS popped first -> highest index */ + ApiSValue v = api_pop(g); + api_scope_store_one(g, s->result_locals[idx], s->result_types[idx], &v); + } +} + +/* Reload all N result carry locals onto the value stack (inverse of store). */ +void api_scope_push_results(KitCg* g, ApiCgScope* s) { + u32 k; + for (k = 0; k < s->nresults; ++k) + api_scope_reload_one(g, s->result_locals[k], s->result_types[k]); +} + +/* Pop N params off the value stack into the loop-param carry locals. */ +static void api_scope_store_params(KitCg* g, ApiCgScope* s) { + u32 k; + for (k = 0; k < s->nparams; ++k) { + u32 idx = s->nparams - 1u - k; + ApiSValue v = api_pop(g); + api_scope_store_one(g, s->param_locals[idx], s->param_types[idx], &v); + } +} + +/* Reload all N loop-param carry locals onto the value stack. */ +static void api_scope_reload_params(KitCg* g, ApiCgScope* s) { + u32 k; + for (k = 0; k < s->nparams; ++k) + api_scope_reload_one(g, s->param_locals[k], s->param_types[k]); } -static KitCgScope api_scope_begin_kind(KitCg* g, u8 kind, - KitCgTypeId result_type) { +/* Allocate a memory-resident carry local of the given type. `resolved` is the + * canonicalized type id (for the local's type); `raw` is the as-supplied id used + * for the ABI size/align query, matching the historical single-result path. */ +static CGLocal api_scope_alloc_carry_local(KitCg* g, KitCgTypeId resolved, + KitCgTypeId raw) { + CGLocalDesc ld; + memset(&ld, 0, sizeof ld); + ld.type = resolved; + ld.size = abi_cg_sizeof(g->c->abi, raw); + ld.align = abi_cg_alignof(g->c->abi, raw); + ld.flags = CG_LOCAL_MEMORY_REQUIRED; + return g->target->local(g->target, &ld); +} + +/* Point a scope's carry-local vectors at storage: the inline buffers for the + * common small arity (no allocation), or one heap block when either count + * exceeds API_CG_SCOPE_SIG_INLINE. Returns 0 only on heap OOM. */ +static int api_scope_setup_sig(KitCg* g, ApiCgScope* s, + const KitCgScopeSig* sig) { + u32 nr = sig ? sig->nresults : 0u; + u32 np = sig ? sig->nparams : 0u; + s->nresults = nr; + s->nparams = np; + s->heap_block = NULL; + if (nr <= API_CG_SCOPE_SIG_INLINE && np <= API_CG_SCOPE_SIG_INLINE) { + s->result_locals = s->result_locals_inl; + s->result_types = s->result_types_inl; + s->param_locals = s->param_locals_inl; + s->param_types = s->param_types_inl; + return 1; + } + { + /* CGLocal and KitCgTypeId are both u32; pack the four vectors into one + * block as [result_locals][result_types][param_locals][param_types]. */ + Heap* h = g->c->ctx->heap; + u32 total = 2u * nr + 2u * np; + u32* blk = (u32*)h->alloc(h, sizeof(u32) * total, _Alignof(u32)); + if (!blk) return 0; + s->heap_block = blk; + s->result_locals = blk; + s->result_types = blk + nr; + s->param_locals = blk + 2u * nr; + s->param_types = blk + 2u * nr + np; + return 1; + } +} + +static void api_scope_free_sig(KitCg* g, ApiCgScope* s) { + if (s->heap_block) { + Heap* h = g->c->ctx->heap; + u32 total = 2u * s->nresults + 2u * s->nparams; + h->free(h, s->heap_block, sizeof(u32) * total); + s->heap_block = NULL; + } +} + +static KitCgScope api_scope_begin_sig_kind(KitCg* g, u8 kind, + const KitCgScopeSig* sig) { Label break_lbl, cont_lbl; CGScopeDesc d; ApiCgScope* s; CGScope target_scope; - u32 idx; + u32 idx, k; if (!g) return 0; break_lbl = g->target->label_new(g->target); cont_lbl = (kind == SCOPE_LOOP) ? g->target->label_new(g->target) : LABEL_NONE; - api_local_const_control_boundary(g); - if (cont_lbl != LABEL_NONE) g->target->label_place(g->target, cont_lbl); if (g->nscopes >= API_CG_MAX_SCOPES) { /* Depth past what the 16-bit handle index can address (see @@ -530,39 +621,91 @@ static KitCgScope api_scope_begin_kind(KitCg* g, u8 kind, s = &g->scopes[idx]; s->break_lbl = break_lbl; s->continue_lbl = cont_lbl; - s->result_type = resolve_type(g->c, result_type); s->generation = ++g->scope_generation; if (s->generation == 0) s->generation = ++g->scope_generation; s->active = 1; + if (!api_scope_setup_sig(g, s, sig)) { + compiler_panic(g->c, g->cur_loc, "KitCg: out of memory for scope signature"); + return 0; + } + for (k = 0; k < s->nresults; ++k) + s->result_types[k] = resolve_type(g->c, sig->results[k]); + for (k = 0; k < s->nparams; ++k) + s->param_types[k] = resolve_type(g->c, sig->params[k]); + + /* Loop preheader: snapshot the params off the value stack into their carry + * locals BEFORE the loop header, so the pop runs once in the preheader and + * every back edge (store_params + jump cont) re-stores into the same locals. + * Allocating a local emits nothing; the param stores belong before cont_lbl. */ + if (kind == SCOPE_LOOP && s->nparams) { + for (k = 0; k < s->nparams; ++k) + s->param_locals[k] = + api_scope_alloc_carry_local(g, s->param_types[k], sig->params[k]); + api_scope_store_params(g, s); + } + + api_local_const_control_boundary(g); + if (cont_lbl != LABEL_NONE) g->target->label_place(g->target, cont_lbl); g->nscopes++; memset(&d, 0, sizeof d); d.kind = kind; d.break_label = break_lbl; d.continue_label = cont_lbl; - d.result_type = s->result_type; + /* CGScopeDesc.result_type stays a single informational field (backends ignore + * it): the single-result type, or NONE for void / multi-value. Multi-value is + * realized purely through the CG carry locals above. */ + d.result_type = (s->nresults == 1u) ? s->result_types[0] : KIT_CG_TYPE_NONE; target_scope = g->target->scope_begin(g->target, &d); s->target_scope = target_scope; - s->result_local = CG_LOCAL_NONE; - if (api_scope_has_result(s)) { - CGLocalDesc ld; - memset(&ld, 0, sizeof ld); - ld.type = s->result_type; - ld.size = abi_cg_sizeof(g->c->abi, result_type); - ld.align = abi_cg_alignof(g->c->abi, result_type); - ld.flags = CG_LOCAL_MEMORY_REQUIRED; - s->result_local = g->target->local(g->target, &ld); - } + + /* Allocate the result carry locals after scope_begin, matching the historical + * single-result ordering. */ + for (k = 0; k < s->nresults; ++k) + s->result_locals[k] = + api_scope_alloc_carry_local(g, s->result_types[k], sig->results[k]); + + /* The loop body observes the params on the value stack: reload them after the + * header so a back edge re-establishes exactly this shape. */ + if (kind == SCOPE_LOOP && s->nparams) api_scope_reload_params(g, s); return api_scope_handle(idx, s->generation); } KitCgScope kit_cg_scope_begin(KitCg* g, KitCgTypeId result_type) { - return api_scope_begin_kind(g, (u8)SCOPE_LOOP, result_type); + KitCgScopeSig sig; + memset(&sig, 0, sizeof sig); + if (g && resolve_type(g->c, result_type) != KIT_CG_TYPE_NONE) { + sig.results = &result_type; + sig.nresults = 1u; + } + return api_scope_begin_sig_kind(g, (u8)SCOPE_LOOP, &sig); } KitCgScope kit_cg_block_begin(KitCg* g, KitCgTypeId result_type) { - return api_scope_begin_kind(g, (u8)SCOPE_BLOCK, result_type); + KitCgScopeSig sig; + memset(&sig, 0, sizeof sig); + if (g && resolve_type(g->c, result_type) != KIT_CG_TYPE_NONE) { + sig.results = &result_type; + sig.nresults = 1u; + } + return api_scope_begin_sig_kind(g, (u8)SCOPE_BLOCK, &sig); +} + +KitCgScope kit_cg_scope_begin_sig(KitCg* g, const KitCgScopeSig* sig) { + return api_scope_begin_sig_kind(g, (u8)SCOPE_LOOP, sig); +} + +KitCgScope kit_cg_block_begin_sig(KitCg* g, const KitCgScopeSig* sig) { + return api_scope_begin_sig_kind(g, (u8)SCOPE_BLOCK, sig); +} + +void kit_cg_scope_store_params(KitCg* g, KitCgScope scope) { + ApiCgScope* s = + api_scope_from_handle(g, scope, 0, "KitCg: scope_store_params"); + if (!s) return; + if (!api_require_loop_scope(g, s, "scope_store_params")) return; + api_scope_store_params(g, s); } KitCgLabel kit_cg_scope_break_label(KitCg* g, KitCgScope scope) { @@ -580,14 +723,15 @@ KitCgLabel kit_cg_scope_continue_label(KitCg* g, KitCgScope scope) { void kit_cg_scope_end(KitCg* g, KitCgScope scope) { ApiCgScope* s = api_scope_from_handle(g, scope, 1, "KitCg: scope_end"); if (!s) return; - if (api_scope_has_result(s)) { - ApiSValue result = api_pop(g); - api_scope_store_result(g, s, &result); - } + /* The fall-through results are on TOS; store them so they join any + * branch-supplied copies in the same carry locals, then reload after the + * break label as the scope's outputs. */ + api_scope_store_results(g, s); api_local_const_control_boundary(g); g->target->label_place(g->target, s->break_lbl); g->target->scope_end(g->target, s->target_scope); - api_scope_push_result(g, s); + api_scope_push_results(g, s); + api_scope_free_sig(g, s); s->active = 0; g->nscopes--; } @@ -595,80 +739,93 @@ void kit_cg_scope_end(KitCg* g, KitCgScope scope) { void kit_cg_break(KitCg* g, KitCgScope scope) { ApiCgScope* s = api_scope_from_handle(g, scope, 0, "KitCg: break"); if (!s) return; - if (api_scope_has_result(s)) { - ApiSValue result = api_pop(g); - api_scope_store_result(g, s, &result); - } + api_scope_store_results(g, s); api_local_const_control_boundary(g); g->target->jump(g->target, s->break_lbl); } -void kit_cg_break_true(KitCg* g, KitCgScope scope) { - ApiCgScope* s; - ApiSValue cond; - if (!g || scope == 0) return; - s = api_scope_from_handle(g, scope, 0, "KitCg: break_true"); +void kit_cg_scope_end_unreachable(KitCg* g, KitCgScope scope) { + /* Close a scope whose fall-through is unreachable (its body terminated via a + * branch/return). There are no fall-through results to pop; the carry locals + * already hold whatever the branch that exited stored. Mirrors scope_end but + * skips the leading store_results. The caller must have the value stack at the + * scope's base depth. */ + ApiCgScope* s = + api_scope_from_handle(g, scope, 1, "KitCg: scope_end_unreachable"); if (!s) return; - cond = api_pop(g); + api_local_const_control_boundary(g); + g->target->label_place(g->target, s->break_lbl); + g->target->scope_end(g->target, s->target_scope); + api_scope_push_results(g, s); + api_scope_free_sig(g, s); + s->active = 0; + g->nscopes--; +} - if (api_scope_has_result(s)) { - ApiSValue result = api_pop(g); - if (cond.kind == SV_OPERAND && cond.op.kind == OPK_IMM) { - if (cond.op.v.imm != 0) { - api_scope_store_result(g, s, &result); - api_local_const_control_boundary(g); - g->target->jump(g->target, s->break_lbl); - } else { - api_release(g, &result); - } - api_release(g, &cond); - } else { - Label skip = g->target->label_new(g->target); - api_branch_if(g, &cond, 0, skip); - api_scope_store_result(g, s, &result); - api_local_const_control_boundary(g); - g->target->jump(g->target, s->break_lbl); - api_local_const_control_boundary(g); - g->target->label_place(g->target, skip); - } - } else { - api_branch_if(g, &cond, 1, s->break_lbl); - } +/* Store the pre-popped result array into the scope's carry locals (forward). */ +static void api_scope_store_results_from(KitCg* g, ApiCgScope* s, + ApiSValue* rs) { + u32 k; + for (k = 0; k < s->nresults; ++k) + api_scope_store_one(g, s->result_locals[k], s->result_types[k], &rs[k]); } -void kit_cg_break_false(KitCg* g, KitCgScope scope) { +/* Shared body of break_true / break_false: pop the condition, then — when the + * scope carries results — pop them all (so they are consumed on both the taken + * and not-taken paths, per the API contract), and store-then-jump only when the + * branch is taken. `break_when` selects the sense. The single-result path is + * byte-identical to the historical code. */ +static void api_break_cond(KitCg* g, KitCgScope scope, int break_when, + const char* who) { ApiCgScope* s; ApiSValue cond; if (!g || scope == 0) return; - s = api_scope_from_handle(g, scope, 0, "KitCg: break_false"); + s = api_scope_from_handle(g, scope, 0, who); if (!s) return; cond = api_pop(g); - if (api_scope_has_result(s)) { - ApiSValue result = api_pop(g); + if (!api_scope_has_result(s)) { + api_branch_if(g, &cond, break_when, s->break_lbl); + return; + } + { + Heap* h = g->c->ctx->heap; + ApiSValue* rs = + (ApiSValue*)h->alloc(h, sizeof(ApiSValue) * s->nresults, + _Alignof(ApiSValue)); + if (!rs) compiler_panic(g->c, g->cur_loc, "KitCg: out of memory"); + api_scope_pop_results(g, s, rs); if (cond.kind == SV_OPERAND && cond.op.kind == OPK_IMM) { - if (cond.op.v.imm == 0) { - api_scope_store_result(g, s, &result); + if ((cond.op.v.imm != 0) == !!break_when) { + api_scope_store_results_from(g, s, rs); api_local_const_control_boundary(g); g->target->jump(g->target, s->break_lbl); } else { - api_release(g, &result); + u32 k; + for (k = 0; k < s->nresults; ++k) api_release(g, &rs[k]); } api_release(g, &cond); } else { Label skip = g->target->label_new(g->target); - api_branch_if(g, &cond, 1, skip); - api_scope_store_result(g, s, &result); + api_branch_if(g, &cond, !break_when, skip); + api_scope_store_results_from(g, s, rs); api_local_const_control_boundary(g); g->target->jump(g->target, s->break_lbl); api_local_const_control_boundary(g); g->target->label_place(g->target, skip); } - } else { - api_branch_if(g, &cond, 0, s->break_lbl); + h->free(h, rs, sizeof(ApiSValue) * s->nresults); } } +void kit_cg_break_true(KitCg* g, KitCgScope scope) { + api_break_cond(g, scope, 1, "KitCg: break_true"); +} + +void kit_cg_break_false(KitCg* g, KitCgScope scope) { + api_break_cond(g, scope, 0, "KitCg: break_false"); +} + /* continue jumps to the loop header, which a forward-only block scope does not * have (its continue_lbl is LABEL_NONE). Reject it with a clean diagnostic * rather than emitting a jump to a nonexistent label. */ diff --git a/src/cg/internal.h b/src/cg/internal.h @@ -120,12 +120,34 @@ typedef struct ApiSValue { * atomics. */ #define CG_MAX_ATOMIC_SIZE 8u +/* Arity that fits inline in an ApiCgScope without a heap allocation. The common + * void/single-result scope (every C/toy statement) stays inline; a multi-value + * block whose param or result count exceeds this spills its carry-local vectors + * to one heap block (see api_scope_setup_sig), freed at scope_end. */ +#define API_CG_SCOPE_SIG_INLINE 4u + typedef struct ApiCgScope { Label break_lbl; Label continue_lbl; CGScope target_scope; - KitCgTypeId result_type; - CGLocal result_local; + /* The carry locals (and their resolved types) that move the scope's results + * across its exit edges and — for a loop — its params across back edges. A + * value is stored into result_locals[i] before a break/end and reloaded after + * the break label; loop params are snapshotted into param_locals[i] and + * reloaded after the continue label. The pointers target the inline buffers + * for small arity (no heap) or one heap block for larger; nresults<=1 and + * nparams==0 reproduces the old single-result path byte-for-byte. */ + CGLocal* result_locals; + KitCgTypeId* result_types; + CGLocal* param_locals; + KitCgTypeId* param_types; + u32 nresults; + u32 nparams; + CGLocal result_locals_inl[API_CG_SCOPE_SIG_INLINE]; + KitCgTypeId result_types_inl[API_CG_SCOPE_SIG_INLINE]; + CGLocal param_locals_inl[API_CG_SCOPE_SIG_INLINE]; + KitCgTypeId param_types_inl[API_CG_SCOPE_SIG_INLINE]; + u32* heap_block; /* non-NULL when arity spilled to heap; freed at scope_end */ u32 generation; u8 active; u8 pad[3]; @@ -334,10 +356,14 @@ KitCgScope api_scope_handle(u32 idx, u32 generation); ApiCgScope* api_scope_from_handle(KitCg* g, KitCgScope scope, int require_top, const char* who); int api_scope_has_result(const ApiCgScope* s); -void api_scope_store_result(KitCg* g, ApiCgScope* s, ApiSValue* result); -void api_scope_push_result(KitCg* g, ApiCgScope* s); +void api_scope_store_results(KitCg* g, ApiCgScope* s); +void api_scope_push_results(KitCg* g, ApiCgScope* s); KitCgScope kit_cg_scope_begin(KitCg* g, KitCgTypeId result_type); +KitCgScope kit_cg_scope_begin_sig(KitCg* g, const KitCgScopeSig* sig); +KitCgScope kit_cg_block_begin_sig(KitCg* g, const KitCgScopeSig* sig); +void kit_cg_scope_store_params(KitCg* g, KitCgScope scope); void kit_cg_scope_end(KitCg* g, KitCgScope scope); +void kit_cg_scope_end_unreachable(KitCg* g, KitCgScope scope); void kit_cg_break(KitCg* g, KitCgScope scope); void kit_cg_break_true(KitCg* g, KitCgScope scope); void kit_cg_break_false(KitCg* g, KitCgScope scope); @@ -405,6 +431,7 @@ void kit_cg_dup(KitCg* g); void kit_cg_dup2(KitCg* g); void kit_cg_swap(KitCg* g); void kit_cg_drop(KitCg* g); +uint32_t kit_cg_stack_depth(KitCg* g); int kit_cg_top_const_int(KitCg* g, int64_t* out_value); void kit_cg_rot3(KitCg* g); KitStatus kit_cg_new(KitCompiler* c, KitCg** cg_out); diff --git a/src/cg/memory.c b/src/cg/memory.c @@ -637,6 +637,8 @@ void kit_cg_drop(KitCg* g) { api_release(g, &v); } +uint32_t kit_cg_stack_depth(KitCg* g) { return g ? g->sp : 0u; } + int kit_cg_top_const_int(KitCg* g, int64_t* out_value) { ApiSValue* v; KitCgTypeId ty; diff --git a/src/wasm/decode.c b/src/wasm/decode.c @@ -127,18 +127,33 @@ static WasmValType bin_val_type(BinReader* r, int refs_ok) { return WASM_VAL_I32; } -/* Decode a structured-control blocktype immediate. kit's block model carries a - * single optional result and no params (mirroring the single-result function - * limit), so accept the empty type (0x40 -> void, returned as 0) and a single - * value type; reject a typeidx blocktype (multi-value / block params), matching - * the WAT parser. */ -static WasmValType bin_blocktype(BinReader* r) { - uint8_t b = bin_u8(r); - if (b == 0x40u) return 0; - if (wasm_is_frontend_value_type((WasmValType)b)) return (WasmValType)b; - wasm_error(r->c, wasm_loc(0, 0), - "wasm: unsupported block result type 0x%02x", b); - return 0; +/* Decode a structured-control blocktype immediate into `in` (already appended). + * The blocktype is an s33: a negative value is the shorthand form (0x40 -> void, + * a value-type byte for a single result), a non-negative value is a function- + * type index (the multi-value form: block params and/or multiple results). + * The shorthand result type lands in in->type (0 == void); the typeidx form is + * recorded via wasm_insn_set_blocktype_typeidx (sentinel in type, index in + * imm). */ +static void bin_blocktype(BinReader* r, WasmModule* out, WasmInsn* in) { + int64_t s = bin_sleb(r, 33); + if (s < 0) { + uint8_t b = (uint8_t)(s & 0x7fu); + if (b == 0x40u) { + in->type = 0; + return; + } + if (wasm_is_frontend_value_type((WasmValType)b)) { + in->type = b; + return; + } + wasm_error(r->c, wasm_loc(0, 0), + "wasm: unsupported block result type 0x%02x", b); + in->type = 0; + return; + } + if ((uint64_t)s >= out->ntypes) + wasm_error(r->c, wasm_loc(0, 0), "wasm: block type index out of range"); + wasm_insn_set_blocktype_typeidx(in, (uint32_t)s); } /* Decode a memarg operand (align/offset/memidx) and append a single memory @@ -170,27 +185,21 @@ static void decode_body_insn(BinReader* rp, WasmModule* out, WasmFunc* f, case 0x01: wasm_func_add_insn(c, out, f, WASM_INSN_NOP, 0); break; - case 0x02: { - WasmValType bt = bin_blocktype(&r); + case 0x02: control_depth++; wasm_func_add_insn(c, out, f, WASM_INSN_BLOCK, 0); - f->insns[f->ninsns - 1u].type = (uint8_t)bt; + bin_blocktype(&r, out, &f->insns[f->ninsns - 1u]); break; - } - case 0x03: { - WasmValType bt = bin_blocktype(&r); + case 0x03: control_depth++; wasm_func_add_insn(c, out, f, WASM_INSN_LOOP, 0); - f->insns[f->ninsns - 1u].type = (uint8_t)bt; + bin_blocktype(&r, out, &f->insns[f->ninsns - 1u]); break; - } - case 0x04: { - WasmValType bt = bin_blocktype(&r); + case 0x04: control_depth++; wasm_func_add_insn(c, out, f, WASM_INSN_IF, 0); - f->insns[f->ninsns - 1u].type = (uint8_t)bt; + bin_blocktype(&r, out, &f->insns[f->ninsns - 1u]); break; - } case 0x05: wasm_func_add_insn(c, out, f, WASM_INSN_ELSE, 0); break; @@ -829,10 +838,8 @@ void wasm_decode_binary(KitCompiler* c, const KitSlice* input, for (j = 0; j < nparam; ++j) wasm_type_push_param(c, out, t, bin_val_type(&r, 1)); nresult = bin_uleb(&r); - if (nresult > 1u) - wasm_error(c, wasm_loc(0, 0), "wasm: multi-result unsupported"); - t->nresults = nresult; - for (j = 0; j < nresult; ++j) t->results[j] = bin_val_type(&r, 1); + for (j = 0; j < nresult; ++j) + wasm_type_push_result(c, out, t, bin_val_type(&r, 1)); } } else if (id == 2) { uint32_t i, count = bin_uleb(&r); @@ -853,9 +860,8 @@ void wasm_decode_binary(KitCompiler* c, const KitSlice* input, f->has_typeidx = 1; wasm_func_set_params(c, out, f, out->types[typeidx].params, out->types[typeidx].nparams); - f->nresults = out->types[typeidx].nresults; - memcpy(f->results, out->types[typeidx].results, - sizeof(f->results[0]) * f->nresults); + wasm_func_set_results(c, out, f, out->types[typeidx].results, + out->types[typeidx].nresults); } else if (kind == 1) { WasmTable* t = wasm_add_table(c, out); t->is_import = 1; @@ -910,9 +916,8 @@ void wasm_decode_binary(KitCompiler* c, const KitSlice* input, f->has_typeidx = 1; wasm_func_set_params(c, out, f, out->types[typeidx].params, out->types[typeidx].nparams); - f->nresults = out->types[typeidx].nresults; - memcpy(f->results, out->types[typeidx].results, - sizeof(f->results[0]) * f->nresults); + wasm_func_set_results(c, out, f, out->types[typeidx].results, + out->types[typeidx].nresults); nfunc_types++; } } else if (id == 5) { diff --git a/src/wasm/encode.c b/src/wasm/encode.c @@ -313,11 +313,17 @@ static void enc_code(KitWriter* w, const WasmModule* m) { } else if (in.kind == WASM_INSN_ATOMIC_FENCE) { write_byte(body, 0); } else if (in.kind == WASM_INSN_BLOCK || in.kind == WASM_INSN_LOOP || - in.kind == WASM_INSN_IF) - /* Blocktype: 0x40 for void, else the single result value type (the - * WasmValType byte is its own encoding). kit's model has no typeidx - * (multi-value/param) blocktype. */ - write_byte(body, in.type ? (uint8_t)in.type : 0x40u); + in.kind == WASM_INSN_IF) { + /* Blocktype: a typeidx blocktype (multi-value) emits the index as an + * s33 signed LEB; otherwise 0x40 for void, or the single result value + * type (the WasmValType byte is its own encoding). A type index is + * always >= 0, so its sleb encoding stays positive and is unambiguous + * against the negative-encoded shorthand bytes. */ + if (wasm_insn_blocktype_is_typeidx(&in)) + write_sleb(body, (int64_t)wasm_insn_blocktype_typeidx(&in)); + else + write_byte(body, in.type ? (uint8_t)in.type : 0x40u); + } else if (in.kind == WASM_INSN_I32_CONST || in.kind == WASM_INSN_I64_CONST) write_sleb(body, in.imm); else if (in.kind == WASM_INSN_F32_CONST) diff --git a/src/wasm/insn.c b/src/wasm/insn.c @@ -14,6 +14,28 @@ int wasm_is_frontend_value_type(WasmValType vt) { return wasm_is_num_type(vt) || vt == WASM_VAL_FUNCREF; } +void wasm_block_sig(const WasmModule* m, const WasmInsn* in, + WasmValType* result_scratch, WasmBlockSig* out) { + out->params = NULL; + out->nparams = 0; + out->results = NULL; + out->nresults = 0; + if (wasm_insn_blocktype_is_typeidx(in)) { + uint32_t idx = wasm_insn_blocktype_typeidx(in); + if (idx < m->ntypes) { + const WasmFuncType* t = &m->types[idx]; + out->params = t->params; + out->nparams = t->nparams; + out->results = t->results; + out->nresults = t->nresults; + } + } else if (in->type) { + result_scratch[0] = (WasmValType)in->type; + out->results = result_scratch; + out->nresults = 1; + } +} + int wasm_feature_enabled(const WasmModule* m, WasmFeatureSet feature) { return (m->features & (uint32_t)feature) != 0; } diff --git a/src/wasm/module.c b/src/wasm/module.c @@ -52,6 +52,8 @@ void wasm_module_free(WasmModule* m) { wasm_free_str(m->heap, &t->name); if (t->params) m->heap->free(m->heap, t->params, sizeof(*t->params) * t->cap_params); + if (t->results) + m->heap->free(m->heap, t->results, sizeof(*t->results) * t->cap_results); } if (m->types) m->heap->free(m->heap, m->types, sizeof(*m->types) * m->cap_types); @@ -69,6 +71,8 @@ void wasm_module_free(WasmModule* m) { } if (f->params) m->heap->free(m->heap, f->params, sizeof(*f->params) * f->cap_params); + if (f->results) + m->heap->free(m->heap, f->results, sizeof(*f->results) * f->cap_results); if (f->locals) m->heap->free(m->heap, f->locals, sizeof(*f->locals) * f->cap_locals); if (f->insns) { @@ -218,6 +222,20 @@ void wasm_func_set_params(KitCompiler* c, WasmModule* m, WasmFunc* f, f->nparams = n; } +uint32_t wasm_func_push_result(KitCompiler* c, WasmModule* m, WasmFunc* f, + WasmValType vt) { + wasm_grow_valtypes(c, m, &f->results, &f->cap_results, f->nresults + 1u); + f->results[f->nresults] = vt; + return f->nresults++; +} + +void wasm_func_set_results(KitCompiler* c, WasmModule* m, WasmFunc* f, + const WasmValType* src, uint32_t n) { + wasm_grow_valtypes(c, m, &f->results, &f->cap_results, n); + if (n) memcpy(f->results, src, sizeof(WasmValType) * n); + f->nresults = n; +} + void wasm_func_set_local_name(KitCompiler* c, WasmModule* m, WasmFunc* f, uint32_t idx, const char* name, size_t len) { if (idx >= f->cap_local_names) { @@ -246,6 +264,13 @@ uint32_t wasm_type_push_param(KitCompiler* c, WasmModule* m, WasmFuncType* t, return t->nparams++; } +uint32_t wasm_type_push_result(KitCompiler* c, WasmModule* m, WasmFuncType* t, + WasmValType vt) { + wasm_grow_valtypes(c, m, &t->results, &t->cap_results, t->nresults + 1u); + t->results[t->nresults] = vt; + return t->nresults++; +} + uint32_t wasm_intern_func_type(KitCompiler* c, WasmModule* m, const WasmFunc* f) { uint32_t i; @@ -254,8 +279,9 @@ uint32_t wasm_intern_func_type(KitCompiler* c, WasmModule* m, if (t->nparams == f->nparams && t->nresults == f->nresults && (t->nparams == 0 || memcmp(t->params, f->params, sizeof(t->params[0]) * t->nparams) == 0) && - memcmp(t->results, f->results, sizeof(t->results[0]) * t->nresults) == - 0) + (t->nresults == 0 || + memcmp(t->results, f->results, + sizeof(t->results[0]) * t->nresults) == 0)) return i; } { @@ -264,8 +290,10 @@ uint32_t wasm_intern_func_type(KitCompiler* c, WasmModule* m, t->nparams = f->nparams; if (f->nparams) memcpy(t->params, f->params, sizeof(t->params[0]) * f->nparams); + wasm_grow_valtypes(c, m, &t->results, &t->cap_results, f->nresults); t->nresults = f->nresults; - memcpy(t->results, f->results, sizeof(t->results[0]) * f->nresults); + if (f->nresults) + memcpy(t->results, f->results, sizeof(t->results[0]) * f->nresults); return m->ntypes - 1u; } } diff --git a/src/wasm/validate.c b/src/wasm/validate.c @@ -6,22 +6,45 @@ typedef struct WasmValStack { KitSrcLoc loc; } WasmValStack; +/* Inline cap for a block/loop/if signature's param and result vectors. Block + * signatures are tiny in practice; a function type referenced by a typeidx + * blocktype with more entries than this is rejected with a clean diagnostic + * rather than overflowing. */ +#define WASM_CTRL_SIG_MAX 64u + typedef struct WasmControlFrame { uint8_t kind; - /* Single result value type for this block/loop/if (0 == void). kit's block - * model supports at most one result (and no block params), mirroring the - * single-result function limit. */ - uint8_t result; - uint32_t height; int seen_else; int unreachable; + /* Stack height at block entry, recorded AFTER the block params were popped + * and BEFORE they were pushed back (the spec's push_ctrl height). */ + uint32_t height; + /* The block's resolved signature [params*] -> [results*]. A branch to a + * loop transfers its params; a branch to a block/if transfers its results + * (see wasm_branch_label_types). */ + WasmValType params[WASM_CTRL_SIG_MAX]; + uint32_t nparams; + WasmValType results[WASM_CTRL_SIG_MAX]; + uint32_t nresults; } WasmControlFrame; -/* The value type a branch to control frame `f` transfers. A branch to a - * block/if targets that frame's result; a branch to a loop targets its params - * (none in kit's model), so it carries nothing. */ -static WasmValType wasm_branch_label_type(const WasmControlFrame* f) { - return f->kind == WASM_INSN_LOOP ? 0 : (WasmValType)f->result; +static int wasm_valtypes_eq(const WasmValType* a, uint32_t na, + const WasmValType* b, uint32_t nb) { + return na == nb && (na == 0 || memcmp(a, b, sizeof(WasmValType) * na) == 0); +} + +/* The value types a branch to control frame `f` transfers: a branch to a loop + * targets its params (continues the loop), a branch to a block/if targets its + * results (exits the block). */ +static void wasm_branch_label_types(const WasmControlFrame* f, + const WasmValType** types, uint32_t* n) { + if (f->kind == WASM_INSN_LOOP) { + *types = f->params; + *n = f->nparams; + } else { + *types = f->results; + *n = f->nresults; + } } static WasmValType wasm_global_init_type(const WasmInsn* in) { @@ -112,6 +135,49 @@ static void wasm_mark_unreachable(WasmValStack* s, WasmControlFrame* frames, top->unreachable = 1; } +/* Pop a value vector in reverse (top of stack is types[n-1]). */ +static void wasm_stack_pop_vec(KitCompiler* c, WasmValStack* s, + WasmControlFrame* frames, uint32_t nframes, + const WasmValType* types, uint32_t n, + const char* what) { + uint32_t k; + for (k = 0; k < n; ++k) + wasm_stack_pop(c, s, frames, nframes, types[n - 1u - k], what); +} + +/* Push a value vector bottom-to-top. */ +static void wasm_stack_push_vec(KitCompiler* c, WasmValStack* s, + const WasmValType* types, uint32_t n) { + uint32_t k; + for (k = 0; k < n; ++k) wasm_stack_push(c, s, types[k]); +} + +/* Resolve a block/loop/if instruction's blocktype into `fr`'s param/result + * vectors: void (no params, no results), the single-result shorthand, or a + * function-type index (the multi-value form). */ +static void wasm_resolve_blocktype(KitCompiler* c, WasmModule* m, + const WasmInsn* in, WasmControlFrame* fr) { + fr->nparams = 0; + fr->nresults = 0; + if (wasm_insn_blocktype_is_typeidx(in)) { + uint32_t idx = wasm_insn_blocktype_typeidx(in); + const WasmFuncType* t; + if (idx >= m->ntypes) + wasm_error(c, in->loc, "wasm: block type index out of range"); + t = &m->types[idx]; + if (t->nparams > WASM_CTRL_SIG_MAX || t->nresults > WASM_CTRL_SIG_MAX) + wasm_error(c, in->loc, "wasm: block signature too large"); + fr->nparams = t->nparams; + if (t->nparams) memcpy(fr->params, t->params, sizeof(WasmValType) * t->nparams); + fr->nresults = t->nresults; + if (t->nresults) + memcpy(fr->results, t->results, sizeof(WasmValType) * t->nresults); + } else if (in->type) { + fr->nresults = 1; + fr->results[0] = (WasmValType)in->type; + } +} + /* Double the control-frame stack. Caller passes the current capacity by * reference; it is updated to the new capacity. */ static WasmControlFrame* wasm_ctrl_grow(KitCompiler* c, WasmModule* m, @@ -232,8 +298,6 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { m->heap->free(m->heap, control, sizeof(WasmControlFrame) * control_cap); return; } - if (f->nresults > 1u) - wasm_error(c, wasm_loc(0, 0), "wasm: multi-result unsupported"); { for (j = 0; j < f->ninsns; ++j) { WasmInsn* in = &f->insns[j]; @@ -278,8 +342,9 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { if (in->kind == WASM_INSN_RETURN_CALL) { wasm_require_feature(c, m, WASM_FEATURE_TAIL_CALLS, "tail calls", "return_call"); - if (m->funcs[in->imm].nresults != f->nresults || - (f->nresults && m->funcs[in->imm].results[0] != f->results[0])) + if (!wasm_valtypes_eq(m->funcs[in->imm].results, + m->funcs[in->imm].nresults, f->results, + f->nresults)) wasm_error(c, wasm_loc(0, 0), "wasm: return_call result type mismatch"); } @@ -290,8 +355,9 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { } if (in->kind == WASM_INSN_RETURN_CALL) { wasm_mark_unreachable(&stack, control, ncontrol); - } else if (m->funcs[in->imm].nresults) { - wasm_stack_push(c, &stack, m->funcs[in->imm].results[0]); + } else { + wasm_stack_push_vec(c, &stack, m->funcs[in->imm].results, + m->funcs[in->imm].nresults); } break; case WASM_INSN_CALL_INDIRECT: { @@ -310,7 +376,7 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { wasm_stack_pop(c, &stack, control, ncontrol, t->params[param], "call_indirect argument"); } - if (t->nresults) wasm_stack_push(c, &stack, t->results[0]); + wasm_stack_push_vec(c, &stack, t->results, t->nresults); break; } case WASM_INSN_RETURN_CALL_INDIRECT: { @@ -324,8 +390,8 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { wasm_error(c, wasm_loc(0, 0), "wasm: return_call_indirect table index out of range"); t = &m->types[in->imm]; - if (t->nresults != f->nresults || - (f->nresults && t->results[0] != f->results[0])) + if (!wasm_valtypes_eq(t->results, t->nresults, f->results, + f->nresults)) wasm_error(c, wasm_loc(0, 0), "wasm: return_call_indirect result type mismatch"); wasm_stack_pop(c, &stack, control, ncontrol, WASM_VAL_I32, @@ -373,8 +439,8 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { "wasm: call_ref type index out of range"); t = &m->types[in->imm]; if (in->kind == WASM_INSN_RETURN_CALL_REF && - (t->nresults != f->nresults || - (f->nresults && t->results[0] != f->results[0]))) + !wasm_valtypes_eq(t->results, t->nresults, f->results, + f->nresults)) wasm_error(c, wasm_loc(0, 0), "wasm: return_call_ref result type mismatch"); wasm_stack_pop_ref(c, &stack, control, ncontrol, "call_ref callee"); @@ -385,8 +451,8 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { } if (in->kind == WASM_INSN_RETURN_CALL_REF) wasm_mark_unreachable(&stack, control, ncontrol); - else if (t->nresults) - wasm_stack_push(c, &stack, t->results[0]); + else + wasm_stack_push_vec(c, &stack, t->results, t->nresults); break; } case WASM_INSN_GLOBAL_GET: @@ -403,9 +469,8 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { "global"); break; case WASM_INSN_RETURN: - if (f->nresults) - wasm_stack_pop(c, &stack, control, ncontrol, f->results[0], - "return"); + wasm_stack_pop_vec(c, &stack, control, ncontrol, f->results, + f->nresults, "return"); wasm_mark_unreachable(&stack, control, ncontrol); break; case WASM_INSN_DROP: @@ -421,44 +486,41 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { break; case WASM_INSN_BLOCK: case WASM_INSN_LOOP: + case WASM_INSN_IF: { + WasmControlFrame* fr; + if (in->kind == WASM_INSN_IF) + wasm_stack_pop(c, &stack, control, ncontrol, WASM_VAL_I32, "if"); if (ncontrol == control_cap) control = wasm_ctrl_grow(c, m, control, &control_cap, in->loc); - control[ncontrol].kind = in->kind; - control[ncontrol].result = in->type; - control[ncontrol].height = stack.depth; - control[ncontrol].seen_else = 0; - control[ncontrol].unreachable = 0; - ncontrol++; - break; - case WASM_INSN_IF: - wasm_stack_pop(c, &stack, control, ncontrol, WASM_VAL_I32, "if"); - if (ncontrol == control_cap) - control = wasm_ctrl_grow(c, m, control, &control_cap, in->loc); - control[ncontrol].kind = in->kind; - control[ncontrol].result = in->type; - control[ncontrol].height = stack.depth; - control[ncontrol].seen_else = 0; - control[ncontrol].unreachable = 0; + fr = &control[ncontrol]; + fr->kind = in->kind; + fr->seen_else = 0; + fr->unreachable = 0; + wasm_resolve_blocktype(c, m, in, fr); + /* Pop the block's params off the enclosing stack, record the entry + * height there, then re-push the params so the block body sees them. + * A branch back to a loop or out of a block re-establishes exactly + * this shape. */ + wasm_stack_pop_vec(c, &stack, control, ncontrol, fr->params, + fr->nparams, "block param"); + fr->height = stack.depth; ncontrol++; + wasm_stack_push_vec(c, &stack, fr->params, fr->nparams); break; + } case WASM_INSN_ELSE: { WasmControlFrame* fr = &control[ncontrol - 1u]; if (ncontrol <= 1u || fr->kind != WASM_INSN_IF) wasm_error(c, wasm_loc(0, 0), "wasm: else without if"); - /* The then-arm must leave exactly the block's result on the stack - * (above the entry height). A result-less if also forbids an else - * that is fed a value-less then arm only — the height check covers - * both via the per-result expected depth. */ - if (!fr->unreachable) { - if (fr->result) { - if (stack.depth != fr->height + 1u || - stack.vals[stack.depth - 1u] != (WasmValType)fr->result) - wasm_error(c, wasm_loc(0, 0), "wasm: if branch result mismatch"); - } else if (stack.depth != fr->height) { - wasm_error(c, wasm_loc(0, 0), "wasm: if branch result mismatch"); - } - } + /* The then-arm must leave exactly the block's results on the stack + * (above the entry height). */ + wasm_stack_pop_vec(c, &stack, control, ncontrol, fr->results, + fr->nresults, "if branch result"); + if (!fr->unreachable && stack.depth != fr->height) + wasm_error(c, wasm_loc(0, 0), "wasm: if branch result mismatch"); + /* The else-arm re-enters with the block's params on the stack. */ stack.depth = fr->height; + wasm_stack_push_vec(c, &stack, fr->params, fr->nparams); fr->seen_else = 1; fr->unreachable = 0; break; @@ -467,54 +529,52 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { WasmControlFrame* fr = &control[ncontrol - 1u]; if (ncontrol <= 1u) wasm_error(c, wasm_loc(0, 0), "wasm: end without block"); - /* A result-typed if with no else arm is ill-typed: the implicit - * empty else cannot produce the result. */ - if (fr->kind == WASM_INSN_IF && fr->result && !fr->seen_else) + /* An if with no else arm has an implicit empty else, well-typed only + * when the block's params already equal its results. */ + if (fr->kind == WASM_INSN_IF && !fr->seen_else && + !wasm_valtypes_eq(fr->params, fr->nparams, fr->results, + fr->nresults)) wasm_error(c, wasm_loc(0, 0), "wasm: if without else cannot yield a result"); - if (!fr->unreachable) { - if (fr->result) { - if (stack.depth != fr->height + 1u || - stack.vals[stack.depth - 1u] != (WasmValType)fr->result) - wasm_error(c, wasm_loc(0, 0), "wasm: block result mismatch"); - } else if (stack.depth != fr->height) { - wasm_error(c, wasm_loc(0, 0), "wasm: block result mismatch"); - } - } + wasm_stack_pop_vec(c, &stack, control, ncontrol, fr->results, + fr->nresults, "block result"); + if (!fr->unreachable && stack.depth != fr->height) + wasm_error(c, wasm_loc(0, 0), "wasm: block result mismatch"); stack.depth = fr->height; ncontrol--; - /* Push the block's result for the enclosing scope to consume. */ - if (fr->result) wasm_stack_push(c, &stack, (WasmValType)fr->result); + /* Push the block's results for the enclosing scope to consume. */ + wasm_stack_push_vec(c, &stack, fr->results, fr->nresults); break; } case WASM_INSN_BR: { - WasmValType lt; + const WasmValType* lt; + uint32_t nlt; if (in->imm < 0 || (uint64_t)in->imm >= ncontrol - 1u) wasm_error(c, wasm_loc(0, 0), "wasm: branch depth out of range"); - lt = wasm_branch_label_type( - &control[ncontrol - 1u - (uint32_t)in->imm]); - if (lt) wasm_stack_pop(c, &stack, control, ncontrol, lt, "br"); + wasm_branch_label_types(&control[ncontrol - 1u - (uint32_t)in->imm], + <, &nlt); + wasm_stack_pop_vec(c, &stack, control, ncontrol, lt, nlt, "br"); wasm_mark_unreachable(&stack, control, ncontrol); break; } case WASM_INSN_BR_IF: { - WasmValType lt; + const WasmValType* lt; + uint32_t nlt; if (in->imm < 0 || (uint64_t)in->imm >= ncontrol - 1u) wasm_error(c, wasm_loc(0, 0), "wasm: branch depth out of range"); wasm_stack_pop(c, &stack, control, ncontrol, WASM_VAL_I32, "br_if"); - /* The branch label type stays on the stack for the fall-through - * path; it is required to be present (and re-supplied) on the + /* The branch label types stay on the stack for the fall-through + * path; they are required to be present (and re-supplied) on the * taken path. */ - lt = wasm_branch_label_type( - &control[ncontrol - 1u - (uint32_t)in->imm]); - if (lt) { - wasm_stack_pop(c, &stack, control, ncontrol, lt, "br_if"); - wasm_stack_push(c, &stack, lt); - } + wasm_branch_label_types(&control[ncontrol - 1u - (uint32_t)in->imm], + <, &nlt); + wasm_stack_pop_vec(c, &stack, control, ncontrol, lt, nlt, "br_if"); + wasm_stack_push_vec(c, &stack, lt, nlt); break; } case WASM_INSN_BR_TABLE: { - WasmValType lt0; + const WasmValType* lt0; + uint32_t nlt0; if (in->ntargets == 0) wasm_error(c, wasm_loc(0, 0), "wasm: br_table without targets"); wasm_stack_pop(c, &stack, control, ncontrol, WASM_VAL_I32, @@ -523,18 +583,22 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { if (in->targets[k] >= ncontrol - 1u) wasm_error(c, wasm_loc(0, 0), "wasm: br_table depth out of range"); - /* All targets must share one label type (the default's), and that - * value must be on the stack. */ - lt0 = wasm_branch_label_type( - &control[ncontrol - 1u - in->targets[in->ntargets - 1u]]); + /* All targets must share one label signature (the default's), and + * those values must be on the stack. */ + wasm_branch_label_types( + &control[ncontrol - 1u - in->targets[in->ntargets - 1u]], <0, + &nlt0); for (uint32_t k = 0; k < in->ntargets; ++k) { - WasmValType lt = wasm_branch_label_type( - &control[ncontrol - 1u - in->targets[k]]); - if (lt != lt0) + const WasmValType* lt; + uint32_t nlt; + wasm_branch_label_types(&control[ncontrol - 1u - in->targets[k]], + <, &nlt); + if (!wasm_valtypes_eq(lt, nlt, lt0, nlt0)) wasm_error(c, wasm_loc(0, 0), "wasm: br_table target type mismatch"); } - if (lt0) wasm_stack_pop(c, &stack, control, ncontrol, lt0, "br_table"); + wasm_stack_pop_vec(c, &stack, control, ncontrol, lt0, nlt0, + "br_table"); wasm_mark_unreachable(&stack, control, ncontrol); break; } @@ -972,12 +1036,11 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { if (ncontrol != 1u) wasm_error(c, wasm_loc(0, 0), "wasm: unterminated control block"); if (!control[0].unreachable) { - if (f->nresults) { - if (stack.depth != 1u || stack.vals[0] != f->results[0]) + if (stack.depth != f->nresults) + wasm_error(c, wasm_loc(0, 0), "wasm: function result count mismatch"); + for (j = 0; j < f->nresults; ++j) + if (stack.vals[j] != f->results[j]) wasm_error(c, wasm_loc(0, 0), "wasm: function result type mismatch"); - } else if (stack.depth != 0) { - wasm_error(c, wasm_loc(0, 0), "wasm: function leaves extra values"); - } } } m->heap->free(m->heap, control, sizeof(WasmControlFrame) * control_cap); diff --git a/src/wasm/wasm.h b/src/wasm/wasm.h @@ -278,6 +278,13 @@ typedef enum WasmInsnKind { WASM_INSN_TABLE_FILL, } WasmInsnKind; +/* Blocktype sentinel stored in WasmInsn.type for a block/loop/if whose + * signature is a function-type index (multi-value: block params and/or >1 + * result). 0 == void, a valtype byte (0x6f..0x7f) == the single-result + * shorthand, and this value (0x01, outside the valtype range and != 0) == "the + * blocktype is the typeidx held in WasmInsn.imm". See wasm_insn_blocktype_*. */ +#define WASM_BLOCKTYPE_TYPEIDX 0x01u + typedef struct WasmInsn { KitSrcLoc loc; uint8_t kind; @@ -299,6 +306,23 @@ typedef struct WasmInsn { uint32_t* targets; } WasmInsn; +/* Blocktype accessors for block/loop/if. A typeidx blocktype keeps its + * function-type index in `imm` and flags it with the WASM_BLOCKTYPE_TYPEIDX + * sentinel in `type`; the shorthand forms keep their valtype (or 0 for void) + * directly in `type`. No dedicated struct field — this keeps every instruction + * the same small size (mirroring the br_table targets pointer trick). */ +static inline int wasm_insn_blocktype_is_typeidx(const WasmInsn* in) { + return in->type == WASM_BLOCKTYPE_TYPEIDX; +} +static inline void wasm_insn_set_blocktype_typeidx(WasmInsn* in, + uint32_t typeidx) { + in->type = (uint8_t)WASM_BLOCKTYPE_TYPEIDX; + in->imm = (int64_t)typeidx; +} +static inline uint32_t wasm_insn_blocktype_typeidx(const WasmInsn* in) { + return (uint32_t)in->imm; +} + typedef struct WasmFunc { KitSrcLoc loc; char* name; @@ -320,8 +344,13 @@ typedef struct WasmFunc { uint32_t cap_locals; char** local_names; uint32_t cap_local_names; - WasmValType results[1]; + /* results mirrors params: a heap-grown vector (size = nresults, capacity = + * cap_results). Multi-value blocks reference multi-result function types, so + * the type section — and therefore func/import signatures — can carry more + * than one result. */ + WasmValType* results; uint32_t nresults; + uint32_t cap_results; char* export_name; WasmInsn* insns; uint32_t ninsns; @@ -333,8 +362,9 @@ typedef struct WasmFuncType { WasmValType* params; uint32_t nparams; uint32_t cap_params; - WasmValType results[1]; + WasmValType* results; uint32_t nresults; + uint32_t cap_results; } WasmFuncType; typedef struct WasmMemory { @@ -491,6 +521,14 @@ uint32_t wasm_func_push_local(KitCompiler* c, WasmModule* m, WasmFunc* f, * when possible; otherwise grows. */ void wasm_func_set_params(KitCompiler* c, WasmModule* m, WasmFunc* f, const WasmValType* src, uint32_t n); +/* Push a single result type onto a WasmFunc's signature, growing as needed. + * Returns the new result's index. */ +uint32_t wasm_func_push_result(KitCompiler* c, WasmModule* m, WasmFunc* f, + WasmValType vt); +/* Bulk-set the results array from a source buffer. Reuses existing capacity + * when possible; otherwise grows. */ +void wasm_func_set_results(KitCompiler* c, WasmModule* m, WasmFunc* f, + const WasmValType* src, uint32_t n); /* Assign a name to wasm-local index `idx`, growing local_names as needed. * `idx` may be any value < nparams + nlocals (or any future index the caller * intends to fill before encoding). */ @@ -499,6 +537,9 @@ void wasm_func_set_local_name(KitCompiler* c, WasmModule* m, WasmFunc* f, /* Push a single parameter type onto a WasmFuncType, growing as needed. */ uint32_t wasm_type_push_param(KitCompiler* c, WasmModule* m, WasmFuncType* t, WasmValType vt); +/* Push a single result type onto a WasmFuncType, growing as needed. */ +uint32_t wasm_type_push_result(KitCompiler* c, WasmModule* m, WasmFuncType* t, + WasmValType vt); void wasm_func_add_insn(KitCompiler* c, WasmModule* m, WasmFunc* f, WasmInsnKind kind, int64_t imm); @@ -513,6 +554,22 @@ void wasm_func_add_fp_insn(KitCompiler* c, WasmModule* m, WasmFunc* f, void wasm_insn_set_targets(KitCompiler* c, WasmModule* m, WasmInsn* in, const uint32_t* targets, uint32_t ntargets); +/* The resolved param/result signature of a block/loop/if instruction. For the + * shorthand forms the vectors point into a caller-provided scratch buffer; for + * the typeidx form they point into m->types[idx]. Lifetime follows whichever + * backs them (the scratch or the module). */ +typedef struct WasmBlockSig { + const WasmValType* params; + uint32_t nparams; + const WasmValType* results; + uint32_t nresults; +} WasmBlockSig; +/* Resolve a block/loop/if instruction's blocktype into *out. `result_scratch` + * is a caller-owned single-WasmValType buffer used to hold the shorthand result + * type (so it need not outlive the call beyond *out's use). */ +void wasm_block_sig(const WasmModule* m, const WasmInsn* in, + WasmValType* result_scratch, WasmBlockSig* out); + int wasm_is_num_type(WasmValType vt); int wasm_is_ref_type(WasmValType vt); int wasm_is_frontend_value_type(WasmValType vt); diff --git a/src/wasm/wat.c b/src/wasm/wat.c @@ -685,45 +685,127 @@ static void wat_block_push_label(WatParser* p) { } } +/* The resolved blocktype of a block/loop/if header: either the void/single- + * result shorthand (is_typeidx == 0; `result` is the value type, 0 == void) or + * a function-type index (is_typeidx == 1; the multi-value form carrying block + * params and/or multiple results). wat_parse_blocktype_apply stamps it onto a + * freshly-added instruction. */ +typedef struct WatBlockType { + int is_typeidx; + uint32_t typeidx; + WasmValType result; +} WatBlockType; + +#define WAT_BLOCKTYPE_MAX 64u + /* Parse an optional structured-control blocktype that follows a block/loop/if - * header (after the label). Returns the single result value type (0 == void). - * Only an empty type and a single `(result <valtype>)` are representable in - * kit's block model; a `(param ...)` or multi-result or `(type $t)` blocktype - * is rejected with a clean fatal diagnostic. `what` names the construct for the - * message. */ -static WasmValType wat_parse_blocktype(WatParser* p, const char* what) { - WasmValType result = 0; + * header (after the label): `(type idx)? (param valtype*)* (result valtype*)*`. + * The fidelity rule mirrors the binary blocktype: void or a single-`(result)` + * with no params stays the scalar shorthand; any param or a second result (or + * an explicit `(type idx)`) resolves to a function-type index, interned into + * the type section when not named directly. `what` names the construct for + * diagnostics. */ +static WatBlockType wat_parse_blocktype(WatParser* p, const char* what) { + WatBlockType bt; + WasmFunc sig; + WasmValType pbuf[WAT_BLOCKTYPE_MAX]; + WasmValType rbuf[WAT_BLOCKTYPE_MAX]; + int64_t explicit_typeidx = -1; + memset(&bt, 0, sizeof bt); + memset(&sig, 0, sizeof sig); + sig.params = pbuf; + sig.cap_params = WAT_BLOCKTYPE_MAX; + sig.results = rbuf; + sig.cap_results = WAT_BLOCKTYPE_MAX; while (p->tok.kind == WT_LPAREN) { size_t lp = p->pos; uint32_t ll = p->line, lc = p->col; wat_next(p); - if (tok_is(p->tok, "result")) { - wat_next(p); - if (!wat_val_type(p->tok, &result) || !wasm_is_frontend_value_type(result)) - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: unsupported %.*s result type", - KIT_SLICE_ARG(kit_slice_cstr(what))); + if (tok_is(p->tok, "type")) { wat_next(p); - if (p->tok.kind != WT_RPAREN) + wat_parse_type_index(p, &explicit_typeidx); + if (explicit_typeidx < 0 || + (uint64_t)explicit_typeidx >= p->module->ntypes) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: multi-result %.*s blocktype unsupported", + "wasm wat: %.*s blocktype type index out of range", KIT_SLICE_ARG(kit_slice_cstr(what))); wat_next(p); + wat_expect(p, WT_RPAREN, "')'"); } else if (tok_is(p->tok, "param")) { - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: %.*s blocktype params are unsupported", - KIT_SLICE_ARG(kit_slice_cstr(what))); - } else if (tok_is(p->tok, "type")) { - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: %.*s (type ...) blocktype is unsupported", - KIT_SLICE_ARG(kit_slice_cstr(what))); + wat_next(p); + while (p->tok.kind != WT_RPAREN && p->tok.kind != WT_EOF) { + WasmValType vt; + if (p->tok.kind == WT_ATOM && p->tok.len && p->tok.p[0] == '$') { + wat_next(p); + continue; + } + if (!wat_val_type(p->tok, &vt) || !wasm_is_frontend_value_type(vt)) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: unsupported %.*s param type", + KIT_SLICE_ARG(kit_slice_cstr(what))); + if (sig.nparams >= WAT_BLOCKTYPE_MAX) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: too many %.*s blocktype params", + KIT_SLICE_ARG(kit_slice_cstr(what))); + sig.params[sig.nparams++] = vt; + wat_next(p); + } + wat_expect(p, WT_RPAREN, "')'"); + } else if (tok_is(p->tok, "result")) { + wat_next(p); + while (p->tok.kind != WT_RPAREN && p->tok.kind != WT_EOF) { + WasmValType vt; + if (!wat_val_type(p->tok, &vt) || !wasm_is_frontend_value_type(vt)) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: unsupported %.*s result type", + KIT_SLICE_ARG(kit_slice_cstr(what))); + if (sig.nresults >= WAT_BLOCKTYPE_MAX) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: too many %.*s blocktype results", + KIT_SLICE_ARG(kit_slice_cstr(what))); + sig.results[sig.nresults++] = vt; + wat_next(p); + } + wat_expect(p, WT_RPAREN, "')'"); } else { /* Not a blocktype clause — hand the '(' back to the body parser. */ wat_unget_to_lparen(p, lp, ll, lc); break; } } - return result; + if (explicit_typeidx >= 0) { + const WasmFuncType* t = &p->module->types[explicit_typeidx]; + /* If param/result clauses also appear, they must match the named type. */ + if ((sig.nparams || sig.nresults) && + (sig.nparams != t->nparams || sig.nresults != t->nresults || + (sig.nparams && + memcmp(sig.params, t->params, sizeof(WasmValType) * sig.nparams)) || + (sig.nresults && + memcmp(sig.results, t->results, sizeof(WasmValType) * sig.nresults)))) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: %.*s blocktype does not match named type", + KIT_SLICE_ARG(kit_slice_cstr(what))); + bt.is_typeidx = 1; + bt.typeidx = (uint32_t)explicit_typeidx; + return bt; + } + /* Shorthand: no params and at most one result stays a scalar blocktype for + * round-trip fidelity; anything richer interns a function type. */ + if (sig.nparams == 0 && sig.nresults <= 1) { + bt.result = sig.nresults ? sig.results[0] : 0; + return bt; + } + bt.is_typeidx = 1; + bt.typeidx = wasm_intern_func_type(p->c, p->module, &sig); + return bt; +} + +/* Stamp a parsed blocktype onto the most-recently-added instruction `in`. */ +static void wat_parse_blocktype_apply(WasmInsn* in, WatBlockType bt) { + if (bt.is_typeidx) + wasm_insn_set_blocktype_typeidx(in, bt.typeidx); + else + in->type = (uint8_t)bt.result; } static void wat_parse_instr(WatParser* p, WasmFunc* f); @@ -923,13 +1005,13 @@ static void wat_parse_instr_list(WatParser* p, WasmFunc* f) { head = p->tok; p->module->current_loc = wat_tok_loc(p, head); if (tok_is(head, "block") || tok_is(head, "loop")) { - WasmValType blockty; + WatBlockType blockty; kind = tok_is(head, "block") ? WASM_INSN_BLOCK : WASM_INSN_LOOP; wat_next(p); wat_block_push_label(p); blockty = wat_parse_blocktype(p, tok_is(head, "block") ? "block" : "loop"); wasm_func_add_insn(p->c, p->module, f, kind, 0); - f->insns[f->ninsns - 1u].type = (uint8_t)blockty; + wat_parse_blocktype_apply(&f->insns[f->ninsns - 1u], blockty); while (p->tok.kind != WT_RPAREN && p->tok.kind != WT_EOF) wat_parse_instr(p, f); wasm_func_add_insn(p->c, p->module, f, WASM_INSN_END, 0); @@ -938,7 +1020,7 @@ static void wat_parse_instr_list(WatParser* p, WasmFunc* f) { return; } if (tok_is(head, "if")) { - WasmValType blockty; + WatBlockType blockty; wat_next(p); wat_block_push_label(p); blockty = wat_parse_blocktype(p, "if"); @@ -953,7 +1035,7 @@ static void wat_parse_instr_list(WatParser* p, WasmFunc* f) { wat_parse_instr(p, f); } wasm_func_add_insn(p->c, p->module, f, WASM_INSN_IF, 0); - f->insns[f->ninsns - 1u].type = (uint8_t)blockty; + wat_parse_blocktype_apply(&f->insns[f->ninsns - 1u], blockty); if (p->tok.kind == WT_LPAREN) { wat_next(p); if (!tok_is(p->tok, "then")) @@ -1155,14 +1237,14 @@ static void wat_parse_instr(WatParser* p, WasmFunc* f) { kind == WASM_INSN_IF) { /* Flat-form structured control: `block $l (result T) ... end`. Parse the * optional label and blocktype, push the label scope, and record the - * single result type on the instruction. */ - WasmValType blockty; + * resolved blocktype on the instruction. */ + WatBlockType blockty; wat_block_push_label(p); blockty = wat_parse_blocktype( p, kind == WASM_INSN_IF ? "if" : (kind == WASM_INSN_LOOP ? "loop" : "block")); wasm_func_add_insn(p->c, p->module, f, kind, 0); - f->insns[f->ninsns - 1u].type = (uint8_t)blockty; + wat_parse_blocktype_apply(&f->insns[f->ninsns - 1u], blockty); return; } if (kind == WASM_INSN_END) { @@ -1323,9 +1405,9 @@ static void wat_parse_func(WatParser* p) { wasm_func_set_params(p->c, p->module, f, p->module->types[typeidx].params, p->module->types[typeidx].nparams); - f->nresults = p->module->types[typeidx].nresults; - memcpy(f->results, p->module->types[typeidx].results, - sizeof(f->results[0]) * f->nresults); + wasm_func_set_results(p->c, p->module, f, + p->module->types[typeidx].results, + p->module->types[typeidx].nresults); wat_next(p); wat_expect(p, WT_RPAREN, "')'"); } else if (tok_is(p->tok, "param")) { @@ -1370,25 +1452,26 @@ static void wat_parse_func(WatParser* p) { } wat_expect(p, WT_RPAREN, "')'"); } else if (tok_is(p->tok, "result")) { - WasmValType vt; wat_next(p); - if (!wat_val_type(p->tok, &vt)) - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: expected result type"); - if (!wasm_is_frontend_value_type(vt)) - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: unsupported result type"); - if (f->has_typeidx) { - if (checked_results >= f->nresults || - f->results[checked_results] != vt) + while (p->tok.kind != WT_RPAREN && p->tok.kind != WT_EOF) { + WasmValType vt; + if (!wat_val_type(p->tok, &vt)) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: result does not match type"); - checked_results++; - } else { - f->results[0] = vt; - f->nresults = 1; + "wasm wat: expected result type"); + if (!wasm_is_frontend_value_type(vt)) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: unsupported result type"); + if (f->has_typeidx) { + if (checked_results >= f->nresults || + f->results[checked_results] != vt) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: result does not match type"); + checked_results++; + } else { + wasm_func_push_result(p->c, p->module, f, vt); + } + wat_next(p); } - wat_next(p); wat_expect(p, WT_RPAREN, "')'"); } else if (tok_is(p->tok, "local")) { WasmTok pending_name; @@ -1473,10 +1556,7 @@ static void wat_parse_type_field(WatParser* p) { if (!wat_val_type(p->tok, &vt) || !wasm_is_num_type(vt)) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), "wasm wat: expected result type"); - if (t->nresults >= 1u) - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: multi-result unsupported"); - t->results[t->nresults++] = vt; + wasm_type_push_result(p->c, p->module, t, vt); wat_next(p); } } else { @@ -1691,9 +1771,9 @@ static void wat_parse_import_field(WatParser* p) { wasm_func_set_params(p->c, p->module, f, p->module->types[typeidx].params, p->module->types[typeidx].nparams); - f->nresults = p->module->types[typeidx].nresults; - memcpy(f->results, p->module->types[typeidx].results, - sizeof(f->results[0]) * f->nresults); + wasm_func_set_results(p->c, p->module, f, + p->module->types[typeidx].results, + p->module->types[typeidx].nresults); wat_next(p); } else if (tok_is(p->tok, "param")) { wat_next(p); @@ -1716,10 +1796,7 @@ static void wat_parse_import_field(WatParser* p) { if (!wat_val_type(p->tok, &vt) || !wasm_is_frontend_value_type(vt)) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), "wasm wat: expected result type"); - if (f->nresults >= 1u) - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: multi-result unsupported"); - f->results[f->nresults++] = vt; + wasm_func_push_result(p->c, p->module, f, vt); wat_next(p); } } else { diff --git a/test/api/cg_control_test.c b/test/api/cg_control_test.c @@ -170,6 +170,198 @@ static void build_loop_fn(KitCompiler* c, KitCg* cg, const char* name, kit_cg_func_end(cg); } +/* ---- Multi-value scopes (kit_cg_*_begin_sig) ------------------------- * + * Drives the multi-value structured-scope surface added for Wasm multi-value + * blocks: a block carrying N results, and a loop carrying N params snapshotted + * across the back edge via kit_cg_scope_store_params. Each builds `int f(int)` + * so the same interpreter/emit harness as the conditional variants applies. */ + +/* Boilerplate: declare `int <name>(int)` and open it; returns the param + * local and fills *out_i32 with the i32 type id and *out_ma with an i32 access. */ +static KitCgLocal begin_int_int_fn(KitCompiler* c, KitCg* cg, const char* name, + KitCgTypeId* out_i32, KitCgMemAccess* out_ma) { + KitCgBuiltinTypes bi = kit_cg_builtin_types(c); + KitCgTypeId i32 = bi.id[KIT_CG_BUILTIN_I32]; + KitCgFuncParam param; + KitCgFuncResult result; + KitCgFuncSig sig; + KitCgDecl decl; + KitCgSym sym; + KitCgLocalAttrs la; + memset(¶m, 0, sizeof param); + param.type = i32; + memset(&result, 0, sizeof result); + result.type = i32; + memset(&sig, 0, sizeof sig); + sig.result = result; + sig.params = ¶m; + sig.nparams = 1; + sig.call_conv = KIT_CG_CC_TARGET_C; + memset(&decl, 0, sizeof decl); + decl.kind = KIT_CG_DECL_FUNC; + decl.linkage_name = kit_sym_intern(c, kit_slice_cstr(name)); + decl.display_name = decl.linkage_name; + decl.type = kit_cg_type_func(c, sig); + decl.sym.bind = KIT_SB_GLOBAL; + decl.sym.visibility = KIT_CG_VIS_DEFAULT; + sym = kit_cg_decl(cg, decl); + EXPECT(sym != KIT_CG_SYM_NONE, "%s: decl failed", name); + kit_cg_func_begin(cg, sym); + memset(&la, 0, sizeof la); + *out_i32 = i32; + memset(out_ma, 0, sizeof *out_ma); + out_ma->type = i32; + out_ma->align = kit_cg_type_align(c, i32); + return kit_cg_param(cg, 0, i32, la); +} + +/* int two_result_block(int x) { return (x+1) + (x+2); } // == 2x + 3 + * via a block carrying two i32 results that scope_end yields bottom->top. */ +static void build_two_result_block(KitCompiler* c, KitCg* cg, const char* name) { + KitCgTypeId i32; + KitCgMemAccess ma; + KitCgLocal x = begin_int_int_fn(c, cg, name, &i32, &ma); + KitCgTypeId results[2] = {i32, i32}; + KitCgScopeSig sig; + KitCgScope blk; + memset(&sig, 0, sizeof sig); + sig.results = results; + sig.nresults = 2; + blk = kit_cg_block_begin_sig(cg, &sig); + kit_cg_local_read(cg, x, ma); /* x */ + kit_cg_push_int(cg, 1, i32); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); /* x+1 */ + kit_cg_local_read(cg, x, ma); + kit_cg_push_int(cg, 2, i32); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); /* x+2 */ + kit_cg_scope_end(cg, blk); /* yields [x+1, x+2] */ + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); /* (x+1)+(x+2) */ + kit_cg_ret(cg); + kit_cg_func_end(cg); +} + +/* int one_param_loop(int n) { int acc=0; for (int i=0;i<n;i++) acc+=i; return acc; } + * The loop carries i as a single param across the back edge; acc is a plain + * local. Exercises scope_begin_sig(1 param) + store_params + continue, with the + * loop exited by falling through the body bottom (no break). */ +static void build_one_param_loop(KitCompiler* c, KitCg* cg, const char* name) { + KitCgTypeId i32; + KitCgMemAccess ma; + KitCgLocal n = begin_int_int_fn(c, cg, name, &i32, &ma); + KitCgLocalAttrs la; + KitCgLocal i_l, acc; + KitCgTypeId params[1]; + KitCgScopeSig sig; + KitCgScope loop; + KitCgLabel done; + memset(&la, 0, sizeof la); + i_l = kit_cg_local(cg, i32, la); + acc = kit_cg_local(cg, i32, la); + kit_cg_push_int(cg, 0, i32); + kit_cg_local_write(cg, acc, ma); + kit_cg_push_int(cg, 0, i32); /* initial loop param i = 0 */ + params[0] = i32; + memset(&sig, 0, sizeof sig); + sig.params = params; + sig.nparams = 1; + loop = kit_cg_scope_begin_sig(cg, &sig); + done = kit_cg_label_new(cg); + kit_cg_local_write(cg, i_l, ma); /* pop the loop param i */ + kit_cg_local_read(cg, i_l, ma); + kit_cg_local_read(cg, n, ma); + kit_cg_int_cmp(cg, KIT_CG_INT_LT_S); /* i < n ? */ + kit_cg_branch_false(cg, done); /* exit when i >= n */ + kit_cg_local_read(cg, acc, ma); + kit_cg_local_read(cg, i_l, ma); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); + kit_cg_local_write(cg, acc, ma); /* acc += i */ + kit_cg_local_read(cg, i_l, ma); + kit_cg_push_int(cg, 1, i32); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); + kit_cg_local_write(cg, i_l, ma); /* i += 1 */ + kit_cg_local_read(cg, i_l, ma); /* fresh loop param */ + kit_cg_scope_store_params(cg, loop); + kit_cg_jump(cg, kit_cg_scope_continue_label(cg, loop)); + kit_cg_label_place(cg, done); + kit_cg_scope_end(cg, loop); + kit_cg_local_read(cg, acc, ma); + kit_cg_ret(cg); + kit_cg_func_end(cg); +} + +/* int two_param_loop(int n) { ... same sum, both i and acc carried as the two + * loop params, and the result carried out as the loop's single result. } + * Exercises scope_begin_sig(2 params, 1 result) + reverse-order store_params + + * a value-carrying fall-through scope_end. */ +static void build_two_param_loop(KitCompiler* c, KitCg* cg, const char* name) { + KitCgTypeId i32; + KitCgMemAccess ma; + KitCgLocal n = begin_int_int_fn(c, cg, name, &i32, &ma); + KitCgLocalAttrs la; + KitCgLocal i_l, acc_l; + KitCgTypeId params[2]; + KitCgTypeId results[1]; + KitCgScopeSig sig; + KitCgScope loop; + KitCgLabel done; + memset(&la, 0, sizeof la); + i_l = kit_cg_local(cg, i32, la); + acc_l = kit_cg_local(cg, i32, la); + kit_cg_push_int(cg, 0, i32); /* i */ + kit_cg_push_int(cg, 0, i32); /* acc */ + params[0] = i32; + params[1] = i32; + results[0] = i32; + memset(&sig, 0, sizeof sig); + sig.params = params; + sig.nparams = 2; + sig.results = results; + sig.nresults = 1; + loop = kit_cg_scope_begin_sig(cg, &sig); + done = kit_cg_label_new(cg); + /* stack on body entry: [i, acc] (bottom->top). Pop acc then i. */ + kit_cg_local_write(cg, acc_l, ma); + kit_cg_local_write(cg, i_l, ma); + kit_cg_local_read(cg, i_l, ma); + kit_cg_local_read(cg, n, ma); + kit_cg_int_cmp(cg, KIT_CG_INT_LT_S); + kit_cg_branch_false(cg, done); /* exit (with acc) when i >= n */ + kit_cg_local_read(cg, acc_l, ma); + kit_cg_local_read(cg, i_l, ma); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); + kit_cg_local_write(cg, acc_l, ma); /* acc += i */ + kit_cg_local_read(cg, i_l, ma); + kit_cg_push_int(cg, 1, i32); + kit_cg_int_binop(cg, KIT_CG_INT_ADD, 0); + kit_cg_local_write(cg, i_l, ma); /* i += 1 */ + kit_cg_local_read(cg, i_l, ma); /* fresh param i */ + kit_cg_local_read(cg, acc_l, ma); /* fresh param acc */ + kit_cg_scope_store_params(cg, loop); /* reverse-pop into [i,acc] locals */ + kit_cg_jump(cg, kit_cg_scope_continue_label(cg, loop)); + kit_cg_label_place(cg, done); + kit_cg_local_read(cg, acc_l, ma); /* fall-through result */ + kit_cg_scope_end(cg, loop); /* yields acc */ + kit_cg_ret(cg); + kit_cg_func_end(cg); +} + +typedef struct { + const char* name; + void (*build)(KitCompiler*, KitCg*, const char*); +} MvDesc; +static const MvDesc MVS[] = { + {"two_result_block", build_two_result_block}, + {"one_param_loop", build_one_param_loop}, + {"two_param_loop", build_two_param_loop}, +}; +enum { NMV = (int)(sizeof MVS / sizeof MVS[0]) }; + +/* Reference value for the multi-value builders given input x. */ +static int64_t mv_expected(const char* name, int64_t x) { + if (strcmp(name, "two_result_block") == 0) return 2 * x + 3; + return loop_expected(x); /* the two loops both compute sum(0..x-1) */ +} + /* ---- Execution coverage (in-process interpreter) -------------------- */ static const int64_t NS[] = {0, 1, 2, 5, 10}; @@ -204,6 +396,7 @@ static void run_exec(void) { snprintf(nm, sizeof nm, "loop_%s", VARIANTS[i].name); build_loop_fn(c, cg, nm, VARIANTS[i].variant); } + for (i = 0; i < NMV; ++i) MVS[i].build(c, cg, MVS[i].name); EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "exec: finish"); EXPECT(kit_cg_detach(cg) == KIT_OK, "exec: detach"); @@ -224,6 +417,21 @@ static void run_exec(void) { } } + for (i = 0; i < NMV; ++i) { + KitInterpFunc* fn = kit_interp_lookup(pp, kit_slice_cstr(MVS[i].name)); + EXPECT(fn != NULL, "exec: %s not captured", MVS[i].name); + if (!fn) continue; + for (j = 0; j < NNS; ++j) { + uint64_t args[1] = {(uint64_t)NS[j]}; + int64_t ret = -1; + int64_t want = mv_expected(MVS[i].name, NS[j]); + KitInterpStatus s = kit_interp_call_args(pp, fn, args, 1, &ret); + EXPECT(s == KIT_INTERP_DONE && ret == want, + "%s(%lld): want %lld got %lld (status %d)", MVS[i].name, + (long long)NS[j], (long long)want, (long long)ret, (int)s); + } + } + kit_cg_free(cg); kit_obj_builder_free(ob); kit_interp_program_free(pp); @@ -256,6 +464,10 @@ static void run_emit(KitArchKind arch, KitOSKind os, KitObjFmt fmt, snprintf(nm, sizeof nm, "emit_%s_o%d_%s", tag, opt_level, VARIANTS[i].name); build_loop_fn(c, cg, nm, VARIANTS[i].variant); } + for (i = 0; i < NMV; ++i) { + snprintf(nm, sizeof nm, "emit_%s_o%d_%s", tag, opt_level, MVS[i].name); + MVS[i].build(c, cg, nm); + } EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "%s/O%d: finish failed", tag, opt_level); EXPECT(kit_cg_detach(cg) == KIT_OK, "%s/O%d: detach failed", tag, opt_level); diff --git a/test/wasm/cases/block_multi_result.expect b/test/wasm/cases/block_multi_result.expect @@ -0,0 +1 @@ +30 +\ No newline at end of file diff --git a/test/wasm/cases/block_multi_result.wat b/test/wasm/cases/block_multi_result.wat @@ -0,0 +1,5 @@ +(module (func (export "test_main") (result i32) + (i32.const 10) + (i32.const 20) + (block (param i32 i32) (result i32 i32)) + (i32.add))) diff --git a/test/wasm/cases/block_result_shorthand_roundtrip.expect b/test/wasm/cases/block_result_shorthand_roundtrip.expect @@ -0,0 +1 @@ +42 +\ No newline at end of file diff --git a/test/wasm/cases/block_result_shorthand_roundtrip.wat b/test/wasm/cases/block_result_shorthand_roundtrip.wat @@ -0,0 +1,5 @@ +(module (func (export "test_main") (result i32) + (block (result i32) + (block + (nop)) + (i32.const 42)))) diff --git a/test/wasm/cases/blocktype_typeidx.expect b/test/wasm/cases/blocktype_typeidx.expect @@ -0,0 +1 @@ +7 +\ No newline at end of file diff --git a/test/wasm/cases/blocktype_typeidx.wat b/test/wasm/cases/blocktype_typeidx.wat @@ -0,0 +1,7 @@ +(module + (type $bt (func (param i32 i32) (result i32 i32))) + (func (export "test_main") (result i32) + (i32.const 3) + (i32.const 4) + (block (type $bt)) + (i32.add))) diff --git a/test/wasm/cases/br_multi_value.expect b/test/wasm/cases/br_multi_value.expect @@ -0,0 +1 @@ +38 +\ No newline at end of file diff --git a/test/wasm/cases/br_multi_value.wat b/test/wasm/cases/br_multi_value.wat @@ -0,0 +1,6 @@ +(module (func (export "test_main") (result i32) + (block (result i32 i32) + (i32.const 40) + (i32.const 2) + (br 0)) + (i32.sub))) diff --git a/test/wasm/cases/br_outer_from_inner.expect b/test/wasm/cases/br_outer_from_inner.expect @@ -0,0 +1 @@ +88 +\ No newline at end of file diff --git a/test/wasm/cases/br_outer_from_inner.wat b/test/wasm/cases/br_outer_from_inner.wat @@ -0,0 +1,7 @@ +(module (func (export "test_main") (result i32) + (block $outer (result i32) + (block $inner (result i32) + (i32.const 88) + (br $outer)) + (i32.const 1) + (i32.add)))) diff --git a/test/wasm/cases/br_table_multi.expect b/test/wasm/cases/br_table_multi.expect @@ -0,0 +1 @@ +17 +\ No newline at end of file diff --git a/test/wasm/cases/br_table_multi.wat b/test/wasm/cases/br_table_multi.wat @@ -0,0 +1,8 @@ +(module (func (export "test_main") (result i32) + (block $a (result i32) + (block $b (result i32) + (i32.const 7) + (i32.const 0) + (br_table $b $a)) + (i32.const 10) + (i32.add)))) diff --git a/test/wasm/cases/dead_code_multi_br.expect b/test/wasm/cases/dead_code_multi_br.expect @@ -0,0 +1 @@ +33 +\ No newline at end of file diff --git a/test/wasm/cases/dead_code_multi_br.wat b/test/wasm/cases/dead_code_multi_br.wat @@ -0,0 +1,9 @@ +(module (func (export "test_main") (result i32) + (block (result i32 i32) + (i32.const 11) + (i32.const 22) + (br 0) + (i32.const 99) + (i32.const 99) + (drop) (drop) (drop)) + (i32.add))) diff --git a/test/wasm/cases/if_no_else_params.expect b/test/wasm/cases/if_no_else_params.expect @@ -0,0 +1 @@ +18 +\ No newline at end of file diff --git a/test/wasm/cases/if_no_else_params.wat b/test/wasm/cases/if_no_else_params.wat @@ -0,0 +1,5 @@ +(module (func (export "test_main") (result i32) + (i32.const 9) + (i32.const 1) + (if (param i32) (result i32) + (then (i32.const 2) (i32.mul))))) diff --git a/test/wasm/cases/if_params_results.expect b/test/wasm/cases/if_params_results.expect @@ -0,0 +1 @@ +7 +\ No newline at end of file diff --git a/test/wasm/cases/if_params_results.wat b/test/wasm/cases/if_params_results.wat @@ -0,0 +1,7 @@ +(module (func (export "test_main") (result i32) + (i32.const 10) + (i32.const 3) + (i32.const 1) + (if (param i32 i32) (result i32) + (then (i32.sub)) + (else (i32.add))))) diff --git a/test/wasm/cases/loop_if_multi.expect b/test/wasm/cases/loop_if_multi.expect @@ -0,0 +1 @@ +10 +\ No newline at end of file diff --git a/test/wasm/cases/loop_if_multi.wat b/test/wasm/cases/loop_if_multi.wat @@ -0,0 +1,11 @@ +(module (func (export "test_main") (result i32) (local $i i32) (local $acc i32) + (local.set $acc (i32.const 0)) + (local.set $i (i32.const 0)) + (block $done + (loop $l + (local.get $i) (i32.const 5) (i32.ge_s) + (if (then (br $done))) + (local.set $acc (i32.add (local.get $acc) (local.get $i))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $l))) + (local.get $acc))) diff --git a/test/wasm/cases/loop_param.expect b/test/wasm/cases/loop_param.expect @@ -0,0 +1 @@ +1 +\ No newline at end of file diff --git a/test/wasm/cases/loop_param.wat b/test/wasm/cases/loop_param.wat @@ -0,0 +1,5 @@ +(module (func (export "test_main") (result i32) + (i32.const 0) + (loop $l (param i32) (result i32) + (i32.const 1) + (i32.add)))) diff --git a/test/wasm/cases/loop_param_brif.expect b/test/wasm/cases/loop_param_brif.expect @@ -0,0 +1 @@ +5 +\ No newline at end of file diff --git a/test/wasm/cases/loop_param_brif.wat b/test/wasm/cases/loop_param_brif.wat @@ -0,0 +1,12 @@ +(module (func (export "test_main") (result i32) (local $i i32) + (i32.const 0) + (loop $l (param i32) (result i32) + (local.set $i) + (local.get $i) + (i32.const 1) + (i32.add) + (local.tee $i) + (local.get $i) + (i32.const 5) + (i32.lt_s) + (br_if $l)))) diff --git a/test/wasm/cases/multi_result_type_section.expect b/test/wasm/cases/multi_result_type_section.expect @@ -0,0 +1 @@ +3 +\ No newline at end of file diff --git a/test/wasm/cases/multi_result_type_section.wat b/test/wasm/cases/multi_result_type_section.wat @@ -0,0 +1,7 @@ +(module + (type $pair (func (param i32 i32) (result i32 i32))) + (func (export "test_main") (result i32) + (i32.const 8) + (i32.const 5) + (block (type $pair)) + (i32.sub))) diff --git a/test/wasm/cases/nested_dead_block.expect b/test/wasm/cases/nested_dead_block.expect @@ -0,0 +1 @@ +55 +\ No newline at end of file diff --git a/test/wasm/cases/nested_dead_block.wat b/test/wasm/cases/nested_dead_block.wat @@ -0,0 +1,7 @@ +(module (func (export "test_main") (result i32) + (block (result i32) + (i32.const 55) + (br 0) + (block (result i32) + (i32.const 99)) + (drop)))) diff --git a/test/wasm/err/block_param_mismatch.wat b/test/wasm/err/block_param_mismatch.wat @@ -0,0 +1,3 @@ +(module (func (export "test_main") (result i32) + (f32.const 1) + (block (param i32) (result i32)))) diff --git a/test/wasm/err/block_result_arity.wat b/test/wasm/err/block_result_arity.wat @@ -0,0 +1,4 @@ +(module (func (export "test_main") (result i32) + (block (result i32 i32) + (i32.const 2)) + (drop))) diff --git a/test/wasm/err/blocktype_typeidx_oob.wat b/test/wasm/err/blocktype_typeidx_oob.wat @@ -0,0 +1,3 @@ +(module (func (export "test_main") (result i32) + (block (type 5)) + (i32.const 0))) diff --git a/test/wasm/err/br_multi_value_mismatch.wat b/test/wasm/err/br_multi_value_mismatch.wat @@ -0,0 +1,6 @@ +(module (func (export "test_main") (result i32) + (block (result i32 i32) + (i32.const 1) + (f32.const 2) + (br 0)) + (drop))) diff --git a/test/wasm/err/if_no_else_multi.wat b/test/wasm/err/if_no_else_multi.wat @@ -0,0 +1,5 @@ +(module (func (export "test_main") (result i32) + (i32.const 1) + (if (result i32 i32) + (then (i32.const 2) (i32.const 3))) + (i32.add)))