695 lines
23 KiB
Zig
695 lines
23 KiB
Zig
const std = @import("std");
|
|
const mem = std.mem;
|
|
const debug = std.debug;
|
|
const log = std.log.scoped(.vm);
|
|
const testAllocator = std.testing.allocator;
|
|
|
|
const bytecode = @import("bytecode.zig");
|
|
const Canvas = @import("canvas.zig");
|
|
const system = @import("system.zig");
|
|
const value = @import("value.zig");
|
|
const Value = value.Value;
|
|
|
|
const Vm = @This();
|
|
|
|
stack: []Value = &.{},
|
|
stack_top: u32 = 0,
|
|
call_stack: []CallFrame = &.{},
|
|
call_stack_top: u32 = 0,
|
|
defs: []Value = &.{},
|
|
fuel: u32 = 0, // NOTE: VM must be refueled via reset() before running code
|
|
exception: ?Exception = null,
|
|
// closure, ip are for exception reporting. These variables are read-only, system functions
|
|
// cannot affect control flow using them.
|
|
closure: ?*const value.Closure = null,
|
|
ip: ?[*]const u8 = null,
|
|
|
|
pub const Limits = struct {
|
|
stack_capacity: usize = 256,
|
|
call_stack_capacity: usize = 256,
|
|
};
|
|
|
|
pub const CallFrame = struct {
|
|
closure: ?*const value.Closure,
|
|
return_addr: ?[*]const u8,
|
|
bottom: u32,
|
|
};
|
|
|
|
pub const Exception = struct {
|
|
len: usize,
|
|
format: *const fn (buf: []u8, args: *align(64) const anyopaque) []u8,
|
|
args: [40]u8 align(64), // increase the size if we ever throw a larger exception
|
|
};
|
|
|
|
/// All errors coming from inside the VM get turned into a single Exception type, which signals
|
|
/// any kind of error that prevented the machine from running any further.
|
|
/// Details about the exception can be found in the VM's `exception` field.
|
|
pub const Error = error{Exception};
|
|
|
|
// NOTE: A VM is only ever initialized. There is no function for deallocating its resources.
|
|
// The intent is to use it with a freshly-reset Scratch, and deallocate its resources through that.
|
|
pub fn init(a: mem.Allocator, defs: *const bytecode.Defs, limits: *const Limits) !Vm {
|
|
return .{
|
|
.stack = try a.alloc(Value, limits.stack_capacity),
|
|
.call_stack = try a.alloc(CallFrame, limits.call_stack_capacity),
|
|
.defs = try a.alloc(Value, defs.num_defs),
|
|
};
|
|
}
|
|
|
|
pub fn reset(vm: *Vm, fuel: u32) void {
|
|
vm.stack_top = 0;
|
|
vm.call_stack_top = 0;
|
|
vm.fuel = fuel;
|
|
vm.exception = null;
|
|
}
|
|
|
|
// NOTE: When the VM throws an exception, it must be reset. There's no resuming from an exception.
|
|
pub fn throw(vm: *Vm, comptime fmt: []const u8, args: anytype) Error {
|
|
log.info("throw: fmt={s}", .{fmt});
|
|
log.debug("implicit stack frame: closure={?any} ip={?*}", .{ vm.closure, vm.ip });
|
|
|
|
const Args = @TypeOf(args);
|
|
const max_args_size = @sizeOf(@TypeOf(vm.exception.?.args));
|
|
if (@sizeOf(Args) > max_args_size) {
|
|
@compileError(std.fmt.comptimePrint(
|
|
"format arguments are too large; size={}, max={}",
|
|
.{ @sizeOf(Args), max_args_size },
|
|
));
|
|
}
|
|
|
|
const Formatter = struct {
|
|
fn format(buf: []u8, erased_args: *align(64) const anyopaque) []u8 {
|
|
return std.fmt.bufPrint(buf, fmt, @as(*const Args, @ptrCast(erased_args)).*) catch unreachable;
|
|
}
|
|
};
|
|
|
|
var exn = Exception{
|
|
.len = @truncate(std.fmt.count(fmt, args)),
|
|
.format = Formatter.format,
|
|
.args = undefined,
|
|
};
|
|
@memcpy(exn.args[0..@sizeOf(Args)], mem.asBytes(&args));
|
|
vm.exception = exn;
|
|
|
|
return error.Exception;
|
|
}
|
|
|
|
pub fn outOfMemory(vm: *Vm) Error {
|
|
return vm.throw("out of memory", .{});
|
|
}
|
|
|
|
// NOTE: Different from CallFrame, this is a helper type for obtaining stack traces.
|
|
pub const StackFrame = struct {
|
|
closure: ?*const value.Closure,
|
|
ip: ?[*]const u8,
|
|
};
|
|
|
|
pub fn stackFrameCount(vm: *const Vm) usize {
|
|
if (vm.closure != null) {
|
|
return vm.call_stack_top + 1;
|
|
} else {
|
|
return vm.call_stack_top;
|
|
}
|
|
}
|
|
|
|
pub fn stackFrame(vm: *const Vm, index: usize) StackFrame {
|
|
if (vm.closure) |closure| {
|
|
if (index == 0) {
|
|
return .{
|
|
.closure = closure,
|
|
// Span info hairiness:
|
|
// Because call frames place the instruction pointer past the call instruction,
|
|
// the frontend subtracts 1 from it to get a position within the chunk that
|
|
// has the correct line info.
|
|
// This subtracting by one doesn't work for the implicit stack frame though, because
|
|
// its ip gets updated at the _start_ of the instruction---meaning that if you
|
|
// subtract one from it, it'll point to the _previous_ instruction, while the actual
|
|
// error occurred in the current one.
|
|
// To alleviate this, add one here to point to either the middle or the end of the
|
|
// current instruction.
|
|
.ip = if (vm.ip) |ip| ip + 1 else null,
|
|
};
|
|
} else {
|
|
// NOTE: Minus two, because remember index == 0 is occupied by the current stack frame.
|
|
const call_frame_index = vm.call_stack_top - 1 - (index - 1);
|
|
const call_frame = vm.call_stack[call_frame_index];
|
|
return .{
|
|
.closure = call_frame.closure,
|
|
.ip = call_frame.return_addr,
|
|
};
|
|
}
|
|
} else {
|
|
const call_frame = vm.call_stack[vm.call_stack_top - 1 - index];
|
|
return .{
|
|
.closure = call_frame.closure,
|
|
.ip = call_frame.return_addr,
|
|
};
|
|
}
|
|
}
|
|
|
|
const StackFmt = struct {
|
|
stack: []const Value,
|
|
|
|
pub fn format(f: *const StackFmt, writer: *std.Io.Writer) !void {
|
|
try writer.writeAll("[");
|
|
for (f.stack, 0..) |val, i| {
|
|
if (i != 0)
|
|
try writer.writeAll(", ");
|
|
try val.format(writer);
|
|
}
|
|
try writer.writeAll("]");
|
|
}
|
|
};
|
|
|
|
fn stackFmt(stack: []const Value) StackFmt {
|
|
return .{ .stack = stack };
|
|
}
|
|
|
|
/// Debug assertion for bytecode validity.
|
|
/// In future versions, this may become disabled in release builds.
|
|
fn validateBytecode(vm: *Vm, ok: bool, comptime fmt: []const u8, args: anytype) Error!void {
|
|
if (!ok) {
|
|
return vm.throw("corrupted bytecode: " ++ fmt, args);
|
|
}
|
|
}
|
|
|
|
pub fn consumeFuel(vm: *Vm, fuel: *u32, amount: u32) Error!void {
|
|
const new_fuel, const overflow = @subWithOverflow(fuel.*, amount);
|
|
if (overflow > 0) {
|
|
return vm.throw("code ran for too long (out of fuel!)", .{});
|
|
}
|
|
fuel.* = new_fuel;
|
|
}
|
|
|
|
pub fn push(vm: *Vm, val: Value) Error!void {
|
|
if (vm.stack_top >= vm.stack.len) {
|
|
return vm.throw("too many live temporary values (local variables and expression operands)", .{});
|
|
}
|
|
log.debug("PUSH {f} <- {f}", .{ stackFmt(vm.stack[0..vm.stack_top]), val });
|
|
vm.stack[vm.stack_top] = val;
|
|
vm.stack_top += 1;
|
|
}
|
|
|
|
pub fn pop(vm: *Vm) Error!Value {
|
|
try vm.validateBytecode(vm.stack_top > 0, "value stack underflow", .{});
|
|
vm.stack_top -= 1;
|
|
const result = vm.stack[vm.stack_top];
|
|
log.debug("POP {f} -> {f}", .{ stackFmt(vm.stack[0..vm.stack_top]), result });
|
|
return vm.stack[vm.stack_top];
|
|
}
|
|
|
|
pub fn top(vm: *const Vm) Value {
|
|
if (vm.stack_top > 0) {
|
|
return vm.stack[vm.stack_top - 1];
|
|
} else {
|
|
return .nil;
|
|
}
|
|
}
|
|
|
|
pub fn pushCall(vm: *Vm, frame: CallFrame) Error!void {
|
|
if (vm.call_stack_top >= vm.call_stack.len) {
|
|
return vm.throw("too much recursion", .{});
|
|
}
|
|
log.debug("PUSH CALL {}", .{frame});
|
|
vm.call_stack[vm.call_stack_top] = frame;
|
|
vm.call_stack_top += 1;
|
|
}
|
|
|
|
pub fn popCall(vm: *Vm) Error!CallFrame {
|
|
try vm.validateBytecode(vm.call_stack_top > 0, "call stack underflow", .{});
|
|
log.debug("POP CALL", .{});
|
|
vm.call_stack_top -= 1;
|
|
return vm.call_stack[vm.call_stack_top];
|
|
}
|
|
|
|
pub fn local(vm: *Vm, bottom: u32, offset: u8) Error!*Value {
|
|
const index = bottom + offset;
|
|
try vm.validateBytecode(index < vm.stack_top, "stack index out of bounds", .{});
|
|
return &vm.stack[bottom + offset];
|
|
}
|
|
|
|
pub fn def(vm: *Vm, index: u16) Error!*Value {
|
|
try vm.validateBytecode(index < vm.defs.len, "def index out of bounds", .{});
|
|
return &vm.defs[index];
|
|
}
|
|
|
|
// Utility struct for storing and restoring the VM's state variables across FFI boundaries.
|
|
const Context = struct {
|
|
ip: ?[*]const u8,
|
|
closure: ?*const value.Closure,
|
|
fuel: u32,
|
|
};
|
|
|
|
fn restoreContext(vm: *Vm) Context {
|
|
return .{
|
|
.ip = vm.ip,
|
|
.closure = vm.closure,
|
|
.fuel = vm.fuel,
|
|
};
|
|
}
|
|
|
|
fn storeContext(vm: *Vm, context: Context) void {
|
|
vm.ip = context.ip;
|
|
vm.closure = context.closure;
|
|
vm.fuel = context.fuel;
|
|
}
|
|
|
|
inline fn read(comptime T: type, ip: *[*]const u8) T {
|
|
const result = mem.readInt(T, ip.*[0..@sizeOf(T)], std.builtin.Endian.little);
|
|
ip.* = ip.*[@sizeOf(T)..];
|
|
return result;
|
|
}
|
|
|
|
inline fn readOpcode(vm: *Vm, ip: *[*]const u8) bytecode.Opcode {
|
|
vm.ip = ip.*;
|
|
const opcode: bytecode.Opcode = @enumFromInt(read(u8, ip));
|
|
log.debug("OP {*} {}", .{ ip.*, opcode });
|
|
return opcode;
|
|
}
|
|
|
|
/// Before calling this, vm.stack_top should be saved and the appropriate amount of parameters must
|
|
/// be pushed onto the stack to execute the provided closure.
|
|
/// The saved vm.stack_top must be passed to init_bottom.
|
|
///
|
|
/// allocator should be a scratch buffer; there is no way to free the memory allocated by a VM.
|
|
pub fn run(
|
|
vm: *Vm,
|
|
allocator: mem.Allocator,
|
|
init_closure: *const value.Closure,
|
|
init_bottom: u32,
|
|
) Error!void {
|
|
log.debug("BEGIN RUN {}", .{init_closure});
|
|
|
|
// NOTE: This will need swapping out once we implement first-class system functions, because
|
|
// system functions should be able to call themselves (like: map (range 1 10) sin)
|
|
debug.assert(init_closure.impl == .bytecode);
|
|
|
|
log.debug("BYTECODE {any}", .{init_closure.impl.bytecode.chunk.bytecode});
|
|
|
|
var closure = init_closure;
|
|
var ip: [*]const u8 = closure.relBytecodePtr(0);
|
|
var bottom = init_bottom;
|
|
var fuel = vm.fuel;
|
|
|
|
vm.closure = closure;
|
|
vm.ip = ip;
|
|
|
|
for (0..closure.impl.bytecode.local_count) |_| {
|
|
try vm.push(.nil);
|
|
}
|
|
|
|
try vm.pushCall(.{
|
|
.closure = null,
|
|
.return_addr = null, // nowhere to return to
|
|
.bottom = bottom,
|
|
});
|
|
|
|
next: switch (vm.readOpcode(&ip)) {
|
|
.nil => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
try vm.push(.nil);
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.false => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
try vm.push(.false);
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.true => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
try vm.push(.true);
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.tag => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const tag_id: value.TagId = @enumFromInt(read(u16, &ip));
|
|
try vm.push(.{ .tag = tag_id });
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.number => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const number: f32 = @bitCast(read(u32, &ip));
|
|
try vm.push(.{ .number = number });
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.rgba => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const r = read(u8, &ip);
|
|
const g = read(u8, &ip);
|
|
const b = read(u8, &ip);
|
|
const a = read(u8, &ip);
|
|
try vm.push(.{ .rgba = value.rgbaFrom8(value.Rgba8{ r, g, b, a }) });
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.local => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const index = read(u8, &ip);
|
|
const l = try vm.local(bottom, index);
|
|
try vm.push(l.*);
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.set_local => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const index = read(u8, &ip);
|
|
const new = try vm.pop();
|
|
const l = try vm.local(bottom, index);
|
|
l.* = new;
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.capture => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const index = read(u8, &ip);
|
|
const captures = closure.impl.bytecode.captures;
|
|
try vm.validateBytecode(index < captures.len, "capture index out of bounds", .{});
|
|
const capture = captures[index];
|
|
try vm.push(capture);
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.def => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const index = read(u16, &ip);
|
|
const d = try vm.def(index);
|
|
try vm.push(d.*);
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.set_def => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const index = read(u16, &ip);
|
|
const new = try vm.pop();
|
|
const d = try vm.def(index);
|
|
d.* = new;
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.list => {
|
|
const len = read(u16, &ip);
|
|
try vm.consumeFuel(&fuel, len);
|
|
const list_end = vm.stack_top;
|
|
vm.stack_top -= len;
|
|
const elements = vm.stack[vm.stack_top..list_end];
|
|
const list = allocator.dupe(Value, elements) catch return vm.outOfMemory();
|
|
const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
|
|
ref.* = .{ .list = list };
|
|
try vm.push(.{ .ref = ref });
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.function => {
|
|
const param_count = read(u8, &ip);
|
|
const then = read(u16, &ip);
|
|
const name_len = read(u8, &ip);
|
|
const name = ip[0..name_len];
|
|
ip += name_len;
|
|
const body = ip;
|
|
ip = closure.globalBytecodePtr(then);
|
|
|
|
const local_count = read(u8, &ip);
|
|
const capture_count = read(u8, &ip);
|
|
|
|
try vm.consumeFuel(&fuel, 1 + capture_count);
|
|
const captures = allocator.alloc(Value, capture_count) catch return vm.outOfMemory();
|
|
for (captures) |*capture| {
|
|
const capture_kind = read(u8, &ip);
|
|
const index = read(u8, &ip);
|
|
vm.ip = ip;
|
|
capture.* = switch (capture_kind) {
|
|
bytecode.capture_local => (try vm.local(bottom, index)).*,
|
|
bytecode.capture_capture => blk: {
|
|
const closureCaptures = closure.impl.bytecode.captures;
|
|
try vm.validateBytecode(index < closureCaptures.len, "capture index out of bounds", .{});
|
|
break :blk closureCaptures[index];
|
|
},
|
|
else => .nil,
|
|
};
|
|
}
|
|
|
|
const new_closure = value.Closure{
|
|
.name = name,
|
|
.param_count = param_count,
|
|
.impl = .{ .bytecode = .{
|
|
.chunk = closure.impl.bytecode.chunk,
|
|
.start = @truncate(body - closure.relBytecodePtr(0)),
|
|
.local_count = local_count,
|
|
.captures = captures,
|
|
} },
|
|
};
|
|
const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
|
|
ref.* = .{ .closure = new_closure };
|
|
try vm.push(.{ .ref = ref });
|
|
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.jump => {
|
|
const offset = read(u16, &ip);
|
|
try vm.consumeFuel(&fuel, 1);
|
|
log.debug("JUMP {}", .{offset});
|
|
ip = closure.globalBytecodePtr(offset);
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.jump_if_not => {
|
|
const offset = read(u16, &ip);
|
|
try vm.consumeFuel(&fuel, 1);
|
|
const condition = try vm.pop();
|
|
if (!condition.isTruthy()) {
|
|
log.debug("CJUMP {} JUMPED", .{offset});
|
|
ip = closure.globalBytecodePtr(offset);
|
|
} else {
|
|
log.debug("CJUMP {} SKIPPED", .{offset});
|
|
}
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.field => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
|
|
const tag_count = read(u8, &ip);
|
|
const field_tag = try vm.pop();
|
|
if (field_tag != .tag) {
|
|
return vm.throw("name of data field to look up must be a tag (starting with an uppercase letter)", .{});
|
|
}
|
|
const field_tag_id = field_tag.tag;
|
|
|
|
var found_index: ?usize = null;
|
|
for (0..tag_count) |i| {
|
|
const tag_id: value.TagId = @enumFromInt(read(u16, &ip));
|
|
if (tag_id == field_tag_id) {
|
|
found_index = i;
|
|
}
|
|
}
|
|
|
|
if (found_index) |index| {
|
|
const fields = closure.impl.bytecode.captures;
|
|
try vm.validateBytecode(index < fields.len, "field index out of bounds", .{});
|
|
const field = fields[index];
|
|
try vm.push(field);
|
|
} else {
|
|
return vm.throw("field with this name does not exist", .{});
|
|
}
|
|
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.call => {
|
|
const arg_count = read(u8, &ip);
|
|
|
|
try vm.consumeFuel(&fuel, 1);
|
|
|
|
const function_value = try vm.pop();
|
|
if (function_value != .ref or function_value.ref.* != .closure) {
|
|
return vm.throw("attempt to call a value that is not a function", .{});
|
|
}
|
|
const called_closure = &function_value.ref.closure;
|
|
|
|
// NOTE: Will need replacing for first-class system functions.
|
|
debug.assert(called_closure.impl == .bytecode);
|
|
|
|
if (arg_count != called_closure.param_count) {
|
|
return vm.throw(
|
|
"function expects {} arguments, but it received {}",
|
|
.{ called_closure.param_count, arg_count },
|
|
);
|
|
}
|
|
|
|
const call_frame = CallFrame{
|
|
.closure = closure,
|
|
.return_addr = ip,
|
|
.bottom = bottom,
|
|
};
|
|
|
|
const new_bottom, const overflow = @subWithOverflow(vm.stack_top, arg_count);
|
|
try vm.validateBytecode(overflow == 0, "not enough values on the stack for arguments", .{});
|
|
|
|
closure = called_closure;
|
|
ip = closure.relBytecodePtr(0);
|
|
bottom = new_bottom;
|
|
|
|
// pushCall before setting vm.closure and vm.ip, such that stack overflow errors are
|
|
// reported at the call site rather than within the called function.
|
|
try vm.pushCall(call_frame);
|
|
|
|
// Remember to update the closure used for exception reporting, since readOpcode does
|
|
// not do that as it does with ip.
|
|
// ip also needs updating, because the topmost stack frame will assume that vm.ip is
|
|
// based in the new closure.
|
|
// We want errors for no stack space being left for locals to appear at the start of the
|
|
// function in source code, not earlier or later. Hence we do this before local slots
|
|
// are pushed onto the stack.
|
|
vm.closure = closure;
|
|
vm.ip = ip;
|
|
|
|
for (0..closure.impl.bytecode.local_count) |_| {
|
|
try vm.push(.nil);
|
|
}
|
|
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.system => {
|
|
const index = read(u8, &ip);
|
|
const arg_count = read(u8, &ip);
|
|
const system_fn = &system.fns[index];
|
|
|
|
try vm.consumeFuel(&fuel, 1);
|
|
|
|
log.debug("system index={} arg_count={} system_fn={s}", .{ index, arg_count, system_fn.closure.name });
|
|
|
|
// Push the current call frame onto the stack, such that the stack trace displays it
|
|
// in addition to the ongoing system call.
|
|
try vm.pushCall(.{
|
|
.closure = closure,
|
|
.return_addr = ip,
|
|
.bottom = bottom,
|
|
});
|
|
|
|
// Also now, push the system function onto the stack, because nulling out vm.closure
|
|
// and vm.ip removes the extra "current function" stack entry.
|
|
try vm.pushCall(.{
|
|
.closure = &system_fn.closure,
|
|
.return_addr = null,
|
|
.bottom = vm.stack_top - arg_count,
|
|
});
|
|
|
|
vm.storeContext(.{
|
|
.fuel = fuel,
|
|
.closure = null,
|
|
.ip = null,
|
|
});
|
|
const result = try system_fn.impl(.{
|
|
.vm = vm,
|
|
.allocator = allocator,
|
|
.args = vm.stack[vm.stack_top - arg_count .. vm.stack_top],
|
|
});
|
|
const context = vm.restoreContext();
|
|
fuel = context.fuel;
|
|
|
|
vm.stack_top -= arg_count;
|
|
try vm.push(result);
|
|
|
|
// Restore the exception handling ip and closure to what it was.
|
|
log.debug("system restore", .{});
|
|
vm.ip = ip;
|
|
vm.closure = closure;
|
|
|
|
// Remove the temporary call frames.
|
|
_ = try vm.popCall(); // systemClosure
|
|
_ = try vm.popCall(); // closure
|
|
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
.ret => {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
|
|
const result = try vm.pop();
|
|
const call_frame = try vm.popCall();
|
|
|
|
try vm.validateBytecode(
|
|
bottom <= vm.stack_top,
|
|
"called function popped too many values. bottom={} stack_top={}",
|
|
.{ bottom, vm.stack_top },
|
|
);
|
|
|
|
vm.stack_top = bottom;
|
|
try vm.push(result);
|
|
|
|
if (call_frame.return_addr) |addr| {
|
|
closure = call_frame.closure.?;
|
|
ip = addr;
|
|
bottom = call_frame.bottom;
|
|
|
|
vm.closure = closure;
|
|
vm.ip = ip;
|
|
} else {
|
|
// If there's nowhere to return to, stop interpreting.
|
|
vm.storeContext(Context{
|
|
.closure = closure,
|
|
.ip = ip,
|
|
.fuel = fuel,
|
|
});
|
|
break :next;
|
|
}
|
|
|
|
continue :next vm.readOpcode(&ip);
|
|
},
|
|
|
|
_ => |invalid_opcode| {
|
|
try vm.consumeFuel(&fuel, 1);
|
|
// NOTE: Not a validateBytecode call because this is zero-cost on the happy path,
|
|
// so we don't need to disable it on release builds.
|
|
return vm.throw("corrupted bytecode: invalid opcode {}", .{invalid_opcode});
|
|
},
|
|
}
|
|
|
|
log.debug("END RUN", .{});
|
|
}
|
|
|
|
pub const Dotter = struct {
|
|
from: value.Vec4,
|
|
to: value.Vec4,
|
|
num: f32,
|
|
};
|
|
|
|
/// NOTE: Assumes the value at the top is a dotter reticle.
|
|
pub fn runDotter(
|
|
vm: *Vm,
|
|
allocator: mem.Allocator,
|
|
from: value.Vec4,
|
|
to: value.Vec4,
|
|
num: f32,
|
|
) Error!void {
|
|
const reticle = try vm.pop();
|
|
const draw = reticle.ref.reticle.dotter.draw; // parameter count checked on construction
|
|
|
|
const data = allocator.dupe(Value, &[_]Value{
|
|
.{ .vec4 = from },
|
|
.{ .vec4 = to },
|
|
.{ .number = num },
|
|
}) catch return vm.outOfMemory();
|
|
const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
|
|
ref.* = value.Ref{ .closure = .{
|
|
.name = "dotter",
|
|
.param_count = 1,
|
|
.impl = .{ .bytecode = .{
|
|
.chunk = &system.record_dotter,
|
|
.start = 0,
|
|
.local_count = 0,
|
|
.captures = data,
|
|
} },
|
|
} };
|
|
|
|
const bottom = vm.stack_top;
|
|
try vm.push(.{ .ref = ref });
|
|
try vm.run(allocator, draw, bottom);
|
|
}
|