Now for the hard part: control flow.
When the VM hits if, it pops the stack. If the value is nonzero, execution continues into the then-branch. If zero, we need to skip forward to the matching else or end. This requires scanning forward through lines, counting nesting depth.
Write vm_find_else(from) -- starting from an if line, scan forward to find the matching else at the same depth. Return the line index, or null if there's no else (just end).
Write vm_find_end(from) -- starting from an if, block, or loop line, scan forward to find the matching end.
fn vm_find_end(from: usize) usize {
var depth: i32 = 1;
var p: usize = from + 1;
while (p < vm_line_count) {
const ln: []const u8 = vm_trim(vm_lines[p]);
if (streq(ln, "if") or starts_with(ln, "if ") or streq(ln, "block") or streq(ln, "loop")) {
depth += 1;
}
if (streq(ln, "end")) {
depth -= 1;
if (depth == 0) {
return p;
}
}
p += 1;
}
return p;
}
fn vm_find_else(from: usize) ?usize {
var depth: i32 = 1;
var p: usize = from + 1;
while (p < vm_line_count) {
const ln: []const u8 = vm_trim(vm_lines[p]);
if (streq(ln, "if") or starts_with(ln, "if ") or streq(ln, "block") or streq(ln, "loop")) {
depth += 1;
}
if (streq(ln, "end")) {
depth -= 1;
if (depth == 0) {
return null; // hit end before else
}
}
if (depth == 1 and streq(ln, "else")) {
return p;
}
p += 1;
}
return null;
}
These are the most subtle functions in the VM. They count nesting -- every if/block/loop increases depth, every end decreases it. An else only matches our if when the depth is back to 1 (our level). Nested ifs inside our branch have their own else/end pairs that we skip over.