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:
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)))