The Compiler
c-stmt-c-block

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.