The VM
vm-run

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.