Write c_stmt() and c_block(). This is where stack hygiene matters.
WAT has strict stack discipline -- every instruction sequence must leave the stack in a predictable state. So c_stmt returns true when it leaves a value on the stack (bare expressions) and false when it doesn't (var declarations, assignments, control flow). c_block uses this to drop intermediate values and ensure exactly one value remains at the end.
fn c_stmt() bool {
if (is_letter(cur())) {
const save: usize = pos;
const word: []const u8 = read_name();
if (streq(word, "var") or streq(word, "const")) {
const vname: []const u8 = read_name();
if (cur() == ':') { pos += 1; skip(); _ = read_name(); }
if (cur() == '=') { pos += 1; skip(); }
c_expression();
if (c_is_global(vname)) {
emit_str(" global.set $");
} else {
emit_str(" local.set $");
}
emit_str(vname);
emit_byte('\n');
if (cur() == ';') { pos += 1; skip(); }
return false;
}
if (streq(word, "return")) {
c_expression();
emit_op("return");
if (cur() == ';') { pos += 1; skip(); }
return false;
}
if (streq(word, "if")) {
if (cur() == '(') { pos += 1; skip(); }
c_expression();
if (cur() == ')') { pos += 1; skip(); }
emit_str(" if (result i64)\n");
c_block();
if (is_letter(cur())) {
const s2: usize = pos;
const w2: []const u8 = read_name();
if (streq(w2, "else")) {
emit_op("else");
c_block();
} else {
pos = s2;
emit_str(" else\n");
emit_const(0);
}
} else {
emit_str(" else\n");
emit_const(0);
}
emit_op("end");
return true; // if (result i64) leaves one value
}
if (streq(word, "while")) {
emit_op("block");
emit_op("loop");
if (cur() == '(') { pos += 1; skip(); }
c_expression();
if (cur() == ')') { pos += 1; skip(); }
emit_op("i64.eqz");
emit_op("br_if 1");
c_block();
emit_op("drop");
emit_op("br 0");
emit_op("end");
emit_op("end");
return false;
}
if (streq(word, "fn")) {
c_fn_def();
return false;
}
// assignment: name = expr; (but not == comparison)
if (cur() == '=' and (pos + 1 >= source.len or source[pos + 1] != '=')) {
pos += 1; skip();
c_expression();
if (c_is_global(word)) {
emit_str(" global.set $");
} else {
emit_str(" local.set $");
}
emit_str(word);
emit_byte('\n');
if (cur() == ';') { pos += 1; skip(); }
return false;
}
pos = save; // backtrack -- expression starting with name
}
c_expression();
if (cur() == ';') { pos += 1; skip(); }
return true; // bare expression leaves a value on the stack
}
fn c_block() void {
if (cur() == '{') { pos += 1; skip(); }
var last_had_value: bool = false;
while (cur() != '}' and cur() != 0) {
if (last_had_value) {
emit_op("drop");
}
last_had_value = c_stmt();
}
if (!last_had_value) {
emit_const(0);
}
if (cur() == '}') { pos += 1; skip(); }
}
The if handler uses if (result i64) -- both branches must produce exactly one value. c_block guarantees this. When there's no else, we synthesize one that pushes 0. The if returns true because it leaves a value on the stack.
The while handler drops the block value before br 0 (the loop-back jump). Without the drop, each iteration would pile another value on the stack.
The assignment handler checks source[pos + 1] != '=' to avoid confusing x = 5 with x == 5. Without this, x == 5 would be parsed as assignment, storing 0.