Write vm_run() -- the main execution loop. It uses a program counter pc that steps through lines. Most lines call vm_exec_line. Control flow instructions adjust pc directly.
This is the biggest function in the VM. Let's build it in stages.
var vm_pc: usize = 0;
fn vm_run_at(start_pc: usize, end_pc: usize) i64 {
vm_pc = start_pc;
vm_block_depth = 0;
while (vm_pc < end_pc) {
const line: []const u8 = vm_trim(vm_lines[vm_pc]);
// --- Control flow ---
if (streq(line, "if") or starts_with(line, "if ")) {
const cond: i64 = vm_pop();
if (cond != 0) {
// enter then-branch
vm_block_kind[vm_block_depth] = .ift;
vm_block_target[vm_block_depth] = vm_find_end(vm_pc);
vm_block_depth += 1;
vm_pc += 1;
continue;
} else {
// skip to else or end
if (vm_find_else(vm_pc)) |else_pc| {
vm_block_kind[vm_block_depth] = .ift;
vm_block_target[vm_block_depth] = vm_find_end(vm_pc);
vm_block_depth += 1;
vm_pc = else_pc + 1;
continue;
} else {
vm_pc = vm_find_end(vm_pc) + 1;
continue;
}
}
}
if (streq(line, "else")) {
// we were in the then-branch and hit else -- skip to end
if (vm_block_depth > 0) {
vm_pc = vm_block_target[vm_block_depth - 1] + 1;
vm_block_depth -= 1;
continue;
}
}
if (streq(line, "block")) {
vm_block_kind[vm_block_depth] = .block;
vm_block_target[vm_block_depth] = vm_find_end(vm_pc);
vm_block_depth += 1;
vm_pc += 1;
continue;
}
if (streq(line, "loop")) {
vm_block_kind[vm_block_depth] = .loop;
vm_block_target[vm_block_depth] = vm_pc; // loops jump BACK
vm_block_depth += 1;
vm_pc += 1;
continue;
}
if (streq(line, "end")) {
if (vm_block_depth > 0) {
vm_block_depth -= 1;
}
vm_pc += 1;
continue;
}
if (starts_with(line, "br_if ")) {
const depth: i64 = parse_int(line, 6);
const cond: i64 = vm_pop();
if (cond != 0) {
// branch
const target_depth: usize = vm_block_depth - 1 - @as(usize, @intCast(depth));
if (vm_block_kind[target_depth] == .loop) {
vm_pc = vm_block_target[target_depth];
vm_block_depth = target_depth + 1;
} else {
vm_pc = vm_block_target[target_depth] + 1;
vm_block_depth = target_depth;
}
continue;
}
vm_pc += 1;
continue;
}
if (starts_with(line, "br ")) {
const depth: i64 = parse_int(line, 3);
const target_depth: usize = vm_block_depth - 1 - @as(usize, @intCast(depth));
if (vm_block_kind[target_depth] == .loop) {
vm_pc = vm_block_target[target_depth];
vm_block_depth = target_depth + 1;
} else {
vm_pc = vm_block_target[target_depth] + 1;
vm_block_depth = target_depth;
}
continue;
}
if (streq(line, "return")) {
return vm_stack[vm_sp - 1];
}
// --- Everything else ---
vm_exec_line(vm_lines[vm_pc]);
vm_pc += 1;
}
// return top of stack if anything is there
if (vm_sp > 0) {
return vm_stack[vm_sp - 1];
}
return 0;
}
That's a lot of code. Let's break down the control flow logic:
if: Pop the condition. If nonzero, push a block entry and proceed into the then-branch. If zero, scan forward to else (and jump there) or end (and jump past it).
else: We got here by executing the then-branch to completion. Skip to the matching end.
block: Push a block entry. Its target is the end -- branching to a block means exiting forward.
loop: Push a block entry. Its target is the loop line itself -- branching to a loop means jumping backward.
br N: Pop N entries off the block stack. If the target is a loop, jump backward. If it's a block, jump forward past its end.
br_if N: Same as br N, but only if the popped value is nonzero.
end: Pop one block entry and continue.
return: Grab the top of stack and return immediately.