kit

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

commit 701328a47fd27f1af9f38ce77ee463c3e3cd47e6
parent d881c8d951041641ca7361921f94c9d11b06648a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 12 Jun 2026 12:33:47 -0700

feat(wasm): WAT symbolic labels + block/loop/if result types

Two standard constructs that toolchain-produced WASM/WAT relies on:

1. Symbolic block/loop/if labels in WAT (block $l ... br $l), both
   folded and flat forms. The WAT parser kept a lexical label scope that
   resolves a $name branch operand to a relative depth (numeric depths
   still work); br/br_if/br_table all route through it.

2. Block/loop/if result types (block (result i32) ...). Previously
   rejected everywhere with no IR representation. Now modeled on the
   single pre-existing WasmInsn.type byte (one result, no params/typeidx
   -- mirroring the single-result function limit) and threaded through
   the whole pipeline:
     - wat.c   parses the blocktype after the optional label
     - decode.c parses the binary blocktype immediate (was: hard reject)
     - encode.c emits it (was: always 0x40 void)
     - validate.c type-checks block/if/else/br/br_if/br_table arities
     - cg.c lowers the result by spilling the value through a dedicated
       per-frame local so every exit edge (fall-through, else-join,
       br/br_if/br_table) converges on one location the end label reloads

With this, a real emscripten -sSTANDALONE_WASM -sPURE_WASI module (which
uses DataCount, block result types, passive segments + memory.init, and
WASI imports) compiles and runs end-to-end through 'kit run --wasm-wasi'.

Tests: test/wasm/cases/{block_result,block_result_br,if_result,
if_result_flat,labeled_flat_loop,labeled_gcd}, green across all lanes
(wat, binary round-trip, interp, object, native exec).

Co-developed with a subagent (wat/validate/cg); binary decode+encode
completion added here.

Diffstat:
Mlang/wasm/cg.c | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/wasm/decode.c | 35++++++++++++++++++++++++++---------
Msrc/wasm/encode.c | 5++++-
Msrc/wasm/validate.c | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/wasm/wat.c | 233++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Atest/wasm/cases/block_result.expect | 1+
Atest/wasm/cases/block_result.wat | 3+++
Atest/wasm/cases/block_result_br.expect | 1+
Atest/wasm/cases/block_result_br.wat | 5+++++
Atest/wasm/cases/if_result.expect | 1+
Atest/wasm/cases/if_result.wat | 3+++
Atest/wasm/cases/if_result_flat.expect | 1+
Atest/wasm/cases/if_result_flat.wat | 7+++++++
Atest/wasm/cases/labeled_flat_loop.expect | 1+
Atest/wasm/cases/labeled_flat_loop.wat | 21+++++++++++++++++++++
Atest/wasm/cases/labeled_gcd.expect | 1+
Atest/wasm/cases/labeled_gcd.wat | 12++++++++++++
17 files changed, 459 insertions(+), 82 deletions(-)

diff --git a/lang/wasm/cg.c b/lang/wasm/cg.c @@ -2011,15 +2011,54 @@ 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. */ + * 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. */ 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; } 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); +} + +/* 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); +} + +/* 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); + } +} + void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { KitCgBuiltinTypes b = kit_cg_builtin_types(c); WasmCgRuntime rt; @@ -2404,6 +2443,7 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { 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: @@ -2420,6 +2460,7 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { 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; @@ -2437,12 +2478,19 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { 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); 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; @@ -2450,10 +2498,18 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { case WASM_INSN_END: if (!ncontrol) wasm_error(c, in.loc, "wasm: end without block"); 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; @@ -2461,6 +2517,11 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { 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); @@ -2472,6 +2533,13 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { 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); @@ -2481,6 +2549,43 @@ void wasm_emit_cg_into(KitCompiler* c, KitCg* cg, const WasmModule* m) { KitCgSwitch sw; 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]); + } + /* Restore the selector for the switch. */ + kit_cg_push_local(cg, seltmp); + kit_cg_load(cg, wasm_cg_mem(c, b, WASM_VAL_I32)); + } + } KitCgSwitchCase* cases = kit_arena_zarray(arena, KitCgSwitchCase, in.ntargets); for (uint32_t k = 0; k + 1u < in.ntargets; ++k) { diff --git a/src/wasm/decode.c b/src/wasm/decode.c @@ -127,6 +127,20 @@ 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 memarg operand (align/offset/memidx) and append a single memory * instruction of `kind`. Every WASM_OC_MEMARG opcode decodes identically; only * the kind differs, so the base loads/stores (0x28..0x3e) and the 0xfe atomics @@ -156,24 +170,27 @@ 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: - if (bin_u8(&r) != 0x40u) - wasm_error(c, wasm_loc(0, 0), "wasm: block results are unsupported"); + case 0x02: { + WasmValType bt = bin_blocktype(&r); control_depth++; wasm_func_add_insn(c, out, f, WASM_INSN_BLOCK, 0); + f->insns[f->ninsns - 1u].type = (uint8_t)bt; break; - case 0x03: - if (bin_u8(&r) != 0x40u) - wasm_error(c, wasm_loc(0, 0), "wasm: loop results are unsupported"); + } + case 0x03: { + WasmValType bt = bin_blocktype(&r); control_depth++; wasm_func_add_insn(c, out, f, WASM_INSN_LOOP, 0); + f->insns[f->ninsns - 1u].type = (uint8_t)bt; break; - case 0x04: - if (bin_u8(&r) != 0x40u) - wasm_error(c, wasm_loc(0, 0), "wasm: if results are unsupported"); + } + case 0x04: { + WasmValType bt = bin_blocktype(&r); control_depth++; wasm_func_add_insn(c, out, f, WASM_INSN_IF, 0); + f->insns[f->ninsns - 1u].type = (uint8_t)bt; break; + } case 0x05: wasm_func_add_insn(c, out, f, WASM_INSN_ELSE, 0); break; diff --git a/src/wasm/encode.c b/src/wasm/encode.c @@ -314,7 +314,10 @@ static void enc_code(KitWriter* w, const WasmModule* m) { write_byte(body, 0); } else if (in.kind == WASM_INSN_BLOCK || in.kind == WASM_INSN_LOOP || in.kind == WASM_INSN_IF) - write_byte(body, 0x40); + /* 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); 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/validate.c b/src/wasm/validate.c @@ -8,11 +8,22 @@ typedef struct WasmValStack { 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; } 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 WasmValType wasm_global_init_type(const WasmInsn* in) { switch (in->kind) { case WASM_INSN_I32_CONST: @@ -413,6 +424,7 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { 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; @@ -423,41 +435,86 @@ void wasm_validate_func(KitCompiler* c, WasmModule* m, WasmFunc* f) { 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_ELSE: - if (ncontrol <= 1u || control[ncontrol - 1u].kind != WASM_INSN_IF) + 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"); - if (!control[ncontrol - 1u].unreachable && - stack.depth != control[ncontrol - 1u].height) - wasm_error(c, wasm_loc(0, 0), "wasm: if branch result mismatch"); - stack.depth = control[ncontrol - 1u].height; - control[ncontrol - 1u].seen_else = 1; - control[ncontrol - 1u].unreachable = 0; - break; - case WASM_INSN_END: + /* 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"); + } + } + stack.depth = fr->height; + fr->seen_else = 1; + fr->unreachable = 0; + break; + } + case WASM_INSN_END: { + WasmControlFrame* fr = &control[ncontrol - 1u]; if (ncontrol <= 1u) wasm_error(c, wasm_loc(0, 0), "wasm: end without block"); - if (!control[ncontrol - 1u].unreachable && - stack.depth != control[ncontrol - 1u].height) - wasm_error(c, wasm_loc(0, 0), "wasm: block result mismatch"); - stack.depth = control[ncontrol - 1u].height; + /* 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) + 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"); + } + } + 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); break; - case WASM_INSN_BR: + } + case WASM_INSN_BR: { + WasmValType lt; 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_mark_unreachable(&stack, control, ncontrol); break; - case WASM_INSN_BR_IF: + } + case WASM_INSN_BR_IF: { + WasmValType lt; 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 + * 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); + } break; - case WASM_INSN_BR_TABLE: + } + case WASM_INSN_BR_TABLE: { + WasmValType lt0; 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, @@ -466,8 +523,21 @@ 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]]); + for (uint32_t k = 0; k < in->ntargets; ++k) { + WasmValType lt = wasm_branch_label_type( + &control[ncontrol - 1u - in->targets[k]]); + if (lt != lt0) + 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_mark_unreachable(&stack, control, ncontrol); break; + } case WASM_INSN_SELECT: { WasmValType rhs, lhs; wasm_stack_pop(c, &stack, control, ncontrol, WASM_VAL_I32, "select"); diff --git a/src/wasm/wat.c b/src/wasm/wat.c @@ -19,6 +19,15 @@ enum { WT_STRING, }; +/* One entry in the parser's lexical label scope. A block/loop/if pushes its + * optional `$name` (a borrowed slice into the source text; NULL for anonymous + * blocks); the matching end/close pops it. br/br_if/br_table resolve a `$name` + * operand to the relative depth (0 = innermost enclosing). */ +typedef struct WatLabel { + const char* name; /* NULL for an anonymous (unnamed) block */ + size_t name_len; +} WatLabel; + typedef struct WatParser { KitCompiler* c; const char* name; @@ -30,6 +39,11 @@ typedef struct WatParser { WasmTok tok; KitSrcLoc field_loc; WasmModule* module; + /* Heap-grown label scope stack (no fixed cap, hangs off the parser). The + * top of the stack is the innermost enclosing block, i.e. branch depth 0. */ + WatLabel* labels; + uint32_t nlabels; + uint32_t cap_labels; } WatParser; static KitSrcLoc wat_loc(WatParser* p, uint32_t line, uint32_t col) { @@ -373,6 +387,52 @@ static int wat_instr_kind(WasmTok t, WasmInsnKind* out, int* has_imm) { return 0; } +/* Push a block/loop/if onto the lexical label scope. `name`/`name_len` borrow + * a `$name` slice from the source text (NULL for an anonymous block). The + * stack grows on demand off the parser; it is never shrunk below 0 and is + * popped in lockstep with WASM_INSN_END emission. */ +static void wat_label_push(WatParser* p, const char* name, size_t name_len) { + KitHeap* h = p->module->heap; + if (p->nlabels == p->cap_labels) { + uint32_t nc = p->cap_labels ? p->cap_labels * 2u : 8u; + WatLabel* nl = (WatLabel*)wasm_realloc(h, p->labels, + sizeof(WatLabel) * p->cap_labels, + sizeof(WatLabel) * nc); + if (!nl) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), "wasm wat: oom"); + p->labels = nl; + p->cap_labels = nc; + } + p->labels[p->nlabels].name = name; + p->labels[p->nlabels].name_len = name_len; + p->nlabels++; +} + +static void wat_label_pop(WatParser* p) { + if (p->nlabels) p->nlabels--; +} + +/* Resolve a branch operand to a relative depth. A `$name` token is matched + * against the enclosing label scope (innermost = 0); a numeric token is taken + * literally. Fatal on an unknown name or a malformed immediate. */ +static void wat_parse_branch_depth(WatParser* p, int64_t* out) { + if (p->tok.kind == WT_ATOM && p->tok.len && p->tok.p[0] == '$') { + uint32_t i; + for (i = 0; i < p->nlabels; ++i) { + const WatLabel* l = &p->labels[p->nlabels - 1u - i]; + if (l->name && l->name_len == p->tok.len && + memcmp(l->name, p->tok.p, p->tok.len) == 0) { + *out = (int64_t)i; + return; + } + } + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: unknown label name"); + } + if (!wat_parse_i64(p, out) || *out < 0) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: expected branch depth"); +} + static void wat_parse_func_index(WatParser* p, int64_t* out) { uint32_t i; if (p->tok.kind == WT_ATOM && p->tok.len && p->tok.p[0] == '$') { @@ -457,6 +517,10 @@ static void wat_parse_instr_imm(WatParser* p, WasmFunc* f, WasmInsnKind kind, case WASM_INSN_LOCAL_TEE: wat_parse_local_index(p, f, out); break; + case WASM_INSN_BR: + case WASM_INSN_BR_IF: + wat_parse_branch_depth(p, out); + break; default: if (!wat_parse_i64(p, out)) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), @@ -590,23 +654,76 @@ static void wat_parse_mem_attrs(WatParser* p, uint32_t* align, uint64_t* offset, } } -static void wat_reject_inline_result(WatParser* p, const char* what) { - if (p->tok.kind != WT_LPAREN) return; - wat_next(p); - if (!tok_is(p->tok, "result")) { - p->pos = (size_t)(p->tok.p - p->src); - p->line = p->tok.line; - p->col = p->tok.col; - p->tok.kind = WT_LPAREN; - p->tok.p = p->src + p->pos - 1u; - p->tok.len = 1; - p->tok.line = p->line; - p->tok.col = p->col - 1u; - return; +/* Re-synthesize the current token as a '(' that was just consumed via + * wat_next(), rewinding the lexer to just after it. Used when a one-token + * lookahead past a '(' did not match the expected keyword and the '(' must be + * handed to a nested instruction parse. Mirrors the inline idiom used + * throughout this file. */ +static void wat_unget_to_lparen(WatParser* p, size_t lparen_pos, + uint32_t lparen_line, uint32_t lparen_col) { + p->pos = lparen_pos; + p->line = lparen_line; + p->col = lparen_col; + p->tok.kind = WT_LPAREN; + p->tok.p = p->src + p->pos - 1u; + p->tok.len = 1; + p->tok.line = lparen_line; + p->tok.col = lparen_col - 1u; +} + +/* Parse an optional `$label` for a block/loop/if header, pushing it onto the + * label scope (anonymous push when absent). Returns nothing; the keyword has + * already been consumed. */ +static void wat_block_push_label(WatParser* p) { + if (p->tok.kind == WT_ATOM && p->tok.len && p->tok.p[0] == '$') { + const char* nm = p->tok.p; + size_t nlen = p->tok.len; + wat_next(p); + wat_label_push(p, nm, nlen); + } else { + wat_label_push(p, NULL, 0); } - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: %.*s results are unsupported", - KIT_SLICE_ARG(kit_slice_cstr(what))); +} + +/* 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; + 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))); + wat_next(p); + if (p->tok.kind != WT_RPAREN) + wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), + "wasm wat: multi-result %.*s blocktype unsupported", + KIT_SLICE_ARG(kit_slice_cstr(what))); + wat_next(p); + } 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))); + } else { + /* Not a blocktype clause — hand the '(' back to the body parser. */ + wat_unget_to_lparen(p, lp, ll, lc); + break; + } + } + return result; } static void wat_parse_instr(WatParser* p, WasmFunc* f); @@ -806,57 +923,37 @@ 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; kind = tok_is(head, "block") ? WASM_INSN_BLOCK : WASM_INSN_LOOP; wat_next(p); - if (p->tok.kind == WT_LPAREN) { - wat_next(p); - if (tok_is(p->tok, "result")) { - wat_next(p); - if (!tok_is(p->tok, "i32") && !tok_is(p->tok, "i64")) - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: unsupported block result type"); - wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), - "wasm wat: block results are unsupported"); - } - p->pos = (size_t)(p->tok.p - p->src); - p->line = p->tok.line; - p->col = p->tok.col; - p->tok.kind = WT_LPAREN; - p->tok.p = p->src + p->pos - 1u; - p->tok.len = 1; - p->tok.line = p->line; - p->tok.col = p->col - 1u; - } + 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; 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); + wat_label_pop(p); wat_expect(p, WT_RPAREN, "')'"); return; } if (tok_is(head, "if")) { + WasmValType blockty; wat_next(p); + wat_block_push_label(p); + blockty = wat_parse_blocktype(p, "if"); while (p->tok.kind == WT_LPAREN) { WasmTok save_head; size_t save_pos = p->pos; uint32_t save_line = p->line, save_col = p->col; wat_next(p); save_head = p->tok; - p->pos = save_pos; - p->line = save_line; - p->col = save_col; - p->tok.kind = WT_LPAREN; - p->tok.p = p->src + p->pos - 1u; - p->tok.len = 1; - p->tok.line = save_line; - p->tok.col = save_col - 1u; + wat_unget_to_lparen(p, save_pos, save_line, save_col); if (tok_is(save_head, "then") || tok_is(save_head, "else")) break; - if (tok_is(save_head, "result")) - wasm_error(p->c, wasm_loc(save_head.line, save_head.col), - "wasm wat: if results are unsupported"); 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; if (p->tok.kind == WT_LPAREN) { wat_next(p); if (!tok_is(p->tok, "then")) @@ -879,6 +976,7 @@ static void wat_parse_instr_list(WatParser* p, WasmFunc* f) { wat_expect(p, WT_RPAREN, "')'"); } wasm_func_add_insn(p->c, p->module, f, WASM_INSN_END, 0); + wat_label_pop(p); wat_expect(p, WT_RPAREN, "')'"); return; } @@ -921,9 +1019,10 @@ static void wat_parse_instr_list(WatParser* p, WasmFunc* f) { _Alignof(uint32_t)); if (!tmp) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), "wasm wat: oom"); - while (p->tok.kind != WT_RPAREN && p->tok.kind != WT_EOF) { + while (p->tok.kind == WT_ATOM) { int64_t target; - if (!wat_parse_i64(p, &target) || target < 0 || target > UINT32_MAX) + wat_parse_branch_depth(p, &target); + if (target > UINT32_MAX) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), "wasm wat: bad br_table target"); if (n == cap) { @@ -941,6 +1040,7 @@ static void wat_parse_instr_list(WatParser* p, WasmFunc* f) { wasm_func_add_insn(p->c, p->module, f, WASM_INSN_BR_TABLE, 0); wasm_insn_set_targets(p->c, p->module, &f->insns[f->ninsns - 1u], tmp, n); heap->free(heap, tmp, sizeof(uint32_t) * cap); + while (p->tok.kind == WT_LPAREN) wat_parse_instr(p, f); wat_expect(p, WT_RPAREN, "')'"); return; } @@ -1051,10 +1151,25 @@ static void wat_parse_instr(WatParser* p, WasmFunc* f) { p->module->current_loc = wat_tok_loc(p, p->tok); wat_check_instr_feature(p, kind); wat_next(p); - if (kind == WASM_INSN_BLOCK || kind == WASM_INSN_LOOP) - wat_reject_inline_result(p, "block"); - else if (kind == WASM_INSN_IF) - wat_reject_inline_result(p, "if"); + if (kind == WASM_INSN_BLOCK || kind == WASM_INSN_LOOP || + 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; + 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; + return; + } + if (kind == WASM_INSN_END) { + wat_label_pop(p); + wasm_func_add_insn(p->c, p->module, f, kind, 0); + return; + } if (wasm_insn_is_mem(kind)) { uint32_t align = 0, memidx = 0; uint64_t offset = 0; @@ -1074,8 +1189,10 @@ static void wat_parse_instr(WatParser* p, WasmFunc* f) { WasmInsnKind next_kind; int next_has_imm; int64_t target; - if (wat_instr_kind(p->tok, &next_kind, &next_has_imm)) break; - if (!wat_parse_i64(p, &target) || target < 0 || target > UINT32_MAX) + int is_label = p->tok.len && p->tok.p[0] == '$'; + if (!is_label && wat_instr_kind(p->tok, &next_kind, &next_has_imm)) break; + wat_parse_branch_depth(p, &target); + if (target > UINT32_MAX) wasm_error(p->c, wasm_loc(p->tok.line, p->tok.col), "wasm wat: bad br_table target"); if (n == cap) { @@ -1159,6 +1276,10 @@ static void wat_parse_func(WatParser* p) { WasmFunc* f = wasm_add_func(p->c, p->module); uint32_t checked_params = 0; uint32_t checked_results = 0; + /* The label scope is per-function; a well-formed body balances its pushes + * and pops, but reset defensively so a malformed prior function can't leak + * stale labels into this one. */ + p->nlabels = 0; f->loc = p->field_loc; wat_expect(p, WT_LPAREN, "'('"); if (!tok_is(p->tok, "func")) @@ -1936,6 +2057,8 @@ void wasm_parse_wat_body(KitCompiler* c, WasmModule* m, WasmFunc* f, p.field_loc = loc; wat_next(&p); while (p.tok.kind != WT_EOF) wat_parse_instr(&p, f); + if (p.labels) + m->heap->free(m->heap, p.labels, sizeof(WatLabel) * p.cap_labels); } void wasm_parse_wat(KitCompiler* c, KitSlice name, const KitSlice* input, @@ -2009,4 +2132,6 @@ void wasm_parse_wat(KitCompiler* c, KitSlice name, const KitSlice* input, if (p.tok.kind != WT_EOF) wasm_error(c, wasm_loc(p.tok.line, p.tok.col), "wasm wat: trailing tokens after module"); + if (p.labels) + out->heap->free(out->heap, p.labels, sizeof(WatLabel) * p.cap_labels); } diff --git a/test/wasm/cases/block_result.expect b/test/wasm/cases/block_result.expect @@ -0,0 +1 @@ +33 diff --git a/test/wasm/cases/block_result.wat b/test/wasm/cases/block_result.wat @@ -0,0 +1,3 @@ +(module (func (export "test_main") (result i32) + (block (result i32) + (i32.const 33)))) diff --git a/test/wasm/cases/block_result_br.expect b/test/wasm/cases/block_result_br.expect @@ -0,0 +1 @@ +77 diff --git a/test/wasm/cases/block_result_br.wat b/test/wasm/cases/block_result_br.wat @@ -0,0 +1,5 @@ +(module (func (export "test_main") (result i32) + (block $b (result i32) + (i32.const 77) + (br $b) + (i32.const 1)))) diff --git a/test/wasm/cases/if_result.expect b/test/wasm/cases/if_result.expect @@ -0,0 +1 @@ +10 diff --git a/test/wasm/cases/if_result.wat b/test/wasm/cases/if_result.wat @@ -0,0 +1,3 @@ +(module (func (export "test_main") (result i32) + (i32.const 1) + (if (result i32) (then (i32.const 10)) (else (i32.const 20))))) diff --git a/test/wasm/cases/if_result_flat.expect b/test/wasm/cases/if_result_flat.expect @@ -0,0 +1 @@ +20 diff --git a/test/wasm/cases/if_result_flat.wat b/test/wasm/cases/if_result_flat.wat @@ -0,0 +1,7 @@ +(module (func (export "test_main") (result i32) + i32.const 0 + if (result i32) + i32.const 10 + else + i32.const 20 + end)) diff --git a/test/wasm/cases/labeled_flat_loop.expect b/test/wasm/cases/labeled_flat_loop.expect @@ -0,0 +1 @@ +5 diff --git a/test/wasm/cases/labeled_flat_loop.wat b/test/wasm/cases/labeled_flat_loop.wat @@ -0,0 +1,21 @@ +(module (func (export "test_main") (result i32) + (local $i i32) (local $n i32) + i32.const 5 + local.set $n + block $exit + loop $top + local.get $n + i32.eqz + br_if $exit + local.get $n + i32.const 1 + i32.sub + local.set $n + local.get $i + i32.const 1 + i32.add + local.set $i + br $top + end + end + local.get $i)) diff --git a/test/wasm/cases/labeled_gcd.expect b/test/wasm/cases/labeled_gcd.expect @@ -0,0 +1 @@ +12 diff --git a/test/wasm/cases/labeled_gcd.wat b/test/wasm/cases/labeled_gcd.wat @@ -0,0 +1,12 @@ +(module (func (export "test_main") (result i32) + (local $a i32) (local $b i32) + (local.set $a (i32.const 48)) (local.set $b (i32.const 36)) + (block $done + (loop $loop + (br_if $done (i32.eqz (local.get $b))) + (local.set $a (i32.rem_u (local.get $a) (local.get $b))) + (local.set $a (i32.xor (local.get $a) (local.get $b))) + (local.set $b (i32.xor (local.get $a) (local.get $b))) + (local.set $a (i32.xor (local.get $a) (local.get $b))) + (br $loop))) + (local.get $a)))