The Compiler
c-factor-term-expression

Write c_factor(), c_term(), c_add_sub(), c_expression(). They mirror the interpreter exactly -- same structure, same operator checks, same recursion -- but emit instead of compute.

Here's the key comparison. The interpreter's term():

fn term() i64 {
    var val: i64 = factor();
    while (cur() == '*' or cur() == '/') {
        const op: u8 = cur();
        pos += 1; skip();
        const right: i64 = factor();
        if (op == '*') { val = val * right; } else { val = @divTrunc(val, right); }
    }
    return val;
}

The compiler's c_term():

fn c_term() void {
    c_factor();
    while (cur() == '*' or cur() == '/') {
        const op: u8 = cur();
        pos += 1; skip();
        c_factor();
        if (op == '*') { emit_op("i64.mul"); } else { emit_op("i64.div_s"); }
    }
}

Same structure. Where the interpreter said val = val * right, the compiler says emit_op("i64.mul"). This works because WAT is a stack machine: c_factor() emits instructions that leave a value on the stack, and i64.mul pops two values and pushes the product.

fn c_factor() void {
    if (cur() == '(') {
        pos += 1; skip();
        c_expression();
        if (cur() == ')') { pos += 1; skip(); }
        return;
    }
    if (is_letter(cur())) {
        const name: []const u8 = read_name();
        if (cur() == '(') {
            c_call(name);
            return;
        }
        if (c_is_global(name)) {
            emit_str("  global.get $");
        } else {
            emit_str("  local.get $");
        }
        emit_str(name);
        emit_byte('\n');
        return;
    }
    c_number();
}

fn c_term() void {
    c_factor();
    while (cur() == '*' or cur() == '/') {
        const op: u8 = cur();
        pos += 1; skip();
        c_factor();
        if (op == '*') { emit_op("i64.mul"); } else { emit_op("i64.div_s"); }
    }
}

fn c_add_sub() void {
    c_term();
    while (cur() == '+' or cur() == '-') {
        const op: u8 = cur();
        pos += 1; skip();
        c_term();
        if (op == '+') { emit_op("i64.add"); } else { emit_op("i64.sub"); }
    }
}

fn c_expression() void {
    c_add_sub();
    if (cur() == '>') {
        pos += 1; skip(); c_add_sub(); emit_op("i64.gt_s");
    } else if (cur() == '<') {
        pos += 1; skip(); c_add_sub(); emit_op("i64.lt_s");
    } else if (cur() == '=' and pos + 1 < source.len and source[pos + 1] == '=') {
        pos += 2; skip(); c_add_sub(); emit_op("i64.eq");
    } else if (cur() == '!' and pos + 1 < source.len and source[pos + 1] == '=') {
        pos += 2; skip(); c_add_sub(); emit_op("i64.ne");
    }
}

Notice: c_factor already checks c_is_global(name) to decide between global.get and local.get. We'll define c_is_global shortly when we set up the global tracking.