The VM
vm-control-flow

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.