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.