The VM
vm-function-calls

Add function calls to vm_run_at. When we hit call $name:

1. Find the function in the table
2. Pop arguments from the stack (in reverse -- last arg was pushed last)
3. Save the current state: PC, locals, block depth, stack pointer
4. Set up new locals with the parameter values
5. Run the function body
6. Restore state
7. Push the return value

We need a call stack:

var vm_call_pc: [32]usize = undefined;
var vm_call_sp: [32]usize = undefined;
var vm_call_ln: [32][64][]const u8 = undefined;
var vm_call_lv: [32][64]i64 = undefined;
var vm_call_lc: [32]usize = undefined;
var vm_call_bd: [32]usize = undefined;
var vm_call_depth: usize = 0;

Add to vm_run_at, before the vm_exec_line fallthrough:

        if (starts_with(line, "call $")) {
            const fname: []const u8 = parse_name(line, 0);
            // find function
            var fi: usize = 0;
            while (fi < vm_fn_count) {
                if (streq(vm_fn_names[fi], fname)) { break; }
                fi += 1;
            }
            if (fi < vm_fn_count) {
                // pop args
                var args: [8]i64 = undefined;
                var ai: usize = vm_fn_param_count[fi];
                while (ai > 0) {
                    ai -= 1;
                    args[ai] = vm_pop();
                }
                // save state
                const cd: usize = vm_call_depth;
                vm_call_pc[cd] = vm_pc + 1;
                vm_call_sp[cd] = vm_sp;
                vm_call_lc[cd] = vm_local_count;
                vm_call_bd[cd] = vm_block_depth;
                for (0..vm_local_count) |li| {
                    vm_call_ln[cd][li] = vm_local_names[li];
                    vm_call_lv[cd][li] = vm_local_values[li];
                }
                vm_call_depth += 1;
                // set up params as locals
                vm_local_count = 0;
                for (0..vm_fn_param_count[fi]) |pi| {
                    vm_set_local(vm_fn_params[fi][pi], args[pi]);
                }
                // run function body
                const result: i64 = vm_run_at(vm_fn_start[fi], vm_fn_end[fi]);
                // restore state
                vm_call_depth -= 1;
                vm_sp = vm_call_sp[cd];
                vm_local_count = vm_call_lc[cd];
                vm_block_depth = vm_call_bd[cd];
                for (0..vm_local_count) |li| {
                    vm_local_names[li] = vm_call_ln[cd][li];
                    vm_local_values[li] = vm_call_lv[cd][li];
                }
                // push result
                vm_push(result);
                vm_pc = vm_call_pc[cd];
                continue;
            }
            vm_pc += 1;
            continue;
        }

This is the same save/restore pattern we used in the interpreter's call_fn. Save everything, set up new locals, run the body, restore, push the result. The VM's function call is structurally identical to the interpreter's -- different data, same idea.