stack traces in the brush editor
after 35 thousand years it's finally here good erro message
This commit is contained in:
parent
c1612b2a94
commit
e49885c83a
11 changed files with 710 additions and 150 deletions
|
@ -22,7 +22,10 @@ pub const std_options: std.Options = .{
|
|||
};
|
||||
|
||||
pub fn enableLogScope(scope: @TypeOf(.enum_literal)) bool {
|
||||
if (scope == .vm) return false else return true;
|
||||
if (scope == .vm)
|
||||
return false
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allocator
|
||||
|
@ -124,14 +127,21 @@ export fn haku2_vm_run_main(
|
|||
chunk.* = bytecode.Chunk{
|
||||
.bytecode = code[0..code_len],
|
||||
};
|
||||
const closure = value.Closure{
|
||||
.chunk = chunk,
|
||||
.start = 0,
|
||||
.param_count = 0,
|
||||
.local_count = local_count,
|
||||
.captures = &[_]value.Value{},
|
||||
const closure = scratch.allocator().create(value.Closure) catch {
|
||||
vm.outOfMemory() catch {};
|
||||
return false;
|
||||
};
|
||||
vm.run(scratch.allocator(), &closure, vm.stack_top) catch return false;
|
||||
closure.* = value.Closure{
|
||||
.name = "(brush)",
|
||||
.param_count = 0,
|
||||
.impl = .{ .bytecode = .{
|
||||
.chunk = chunk,
|
||||
.start = 0,
|
||||
.local_count = local_count,
|
||||
.captures = &[_]value.Value{},
|
||||
} },
|
||||
};
|
||||
vm.run(scratch.allocator(), closure, vm.stack_top) catch return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -182,6 +192,33 @@ export fn haku2_vm_exception_render(vm: *const Vm, buffer: [*]u8) void {
|
|||
_ = exn.format(buffer[0..exn.len], &exn.args);
|
||||
}
|
||||
|
||||
export fn haku2_vm_stackframe_count(vm: *const Vm) usize {
|
||||
return vm.stackFrameCount();
|
||||
}
|
||||
|
||||
export fn haku2_vm_stackframe_is_system(vm: *const Vm, index: usize) bool {
|
||||
const stack_frame = vm.stackFrame(index);
|
||||
return stack_frame.closure.impl == .system;
|
||||
}
|
||||
|
||||
export fn haku2_vm_stackframe_pc(vm: *const Vm, index: usize) i32 {
|
||||
const stack_frame = vm.stackFrame(index);
|
||||
if (stack_frame.closure.impl == .bytecode)
|
||||
if (stack_frame.ip) |ip|
|
||||
return stack_frame.closure.programCounter(ip);
|
||||
return -1; // no return address
|
||||
}
|
||||
|
||||
export fn haku2_vm_stackframe_function_name_len(vm: *const Vm, index: usize) usize {
|
||||
const stack_frame = vm.stackFrame(index);
|
||||
return stack_frame.closure.name.len;
|
||||
}
|
||||
|
||||
export fn haku2_vm_stackframe_function_name(vm: *const Vm, index: usize) [*]const u8 {
|
||||
const stack_frame = vm.stackFrame(index);
|
||||
return stack_frame.closure.name.ptr;
|
||||
}
|
||||
|
||||
// Renderer
|
||||
|
||||
export fn haku2_render(vm: *Vm, canvas: *Canvas, max_depth: usize) bool {
|
||||
|
|
|
@ -62,7 +62,12 @@ pub const Context = struct {
|
|||
}
|
||||
};
|
||||
|
||||
pub const Fn = *const fn (Context) Vm.Error!Value;
|
||||
pub const Fn = struct {
|
||||
pub const Impl = *const fn (Context) Vm.Error!Value;
|
||||
|
||||
closure: value.Closure,
|
||||
impl: Impl,
|
||||
};
|
||||
pub const FnTable = [256]Fn;
|
||||
|
||||
fn invalid(cx: Context) !Value {
|
||||
|
@ -150,13 +155,24 @@ fn intoReturn(cx: Context, any: anytype) Vm.Error!Value {
|
|||
};
|
||||
}
|
||||
|
||||
fn rawFn(name: []const u8, func: Fn.Impl) Fn {
|
||||
return .{
|
||||
.closure = .{
|
||||
.name = name,
|
||||
.param_count = 0, // TODO
|
||||
.impl = .{ .system = void{} },
|
||||
},
|
||||
.impl = func,
|
||||
};
|
||||
}
|
||||
|
||||
/// Erase a well-typed function into a function that operates on raw values.
|
||||
/// The erased function performs all the necessary conversions automatically.
|
||||
///
|
||||
/// Note that the argument order is important---function arguments go first, then context (such as
|
||||
/// the VM or the allocator.) Otherwise argument indices will not match up.
|
||||
fn erase(comptime name: []const u8, comptime func: anytype) Fn {
|
||||
return Erased(name, func).call;
|
||||
return rawFn(name, Erased(name, func).call);
|
||||
}
|
||||
|
||||
fn countParams(comptime func: anytype) usize {
|
||||
|
@ -191,7 +207,7 @@ const SparseFn = struct { u8, Fn };
|
|||
const SparseFnTable = []const SparseFn;
|
||||
|
||||
fn makeFnTable(init: SparseFnTable) FnTable {
|
||||
var table = [_]Fn{invalid} ** @typeInfo(FnTable).array.len;
|
||||
var table = [_]Fn{rawFn("(invalid)", invalid)} ** @typeInfo(FnTable).array.len;
|
||||
for (init) |entry| {
|
||||
const index, const func = entry;
|
||||
table[index] = func;
|
||||
|
@ -249,12 +265,12 @@ pub const fns = makeFnTable(&[_]SparseFn{
|
|||
.{ 0x44, erase("<=", lessOrEqual) },
|
||||
.{ 0x45, erase(">", greater) },
|
||||
.{ 0x46, erase(">=", greaterOrEqual) },
|
||||
.{ 0x80, vec },
|
||||
.{ 0x80, rawFn("vec", vec) },
|
||||
.{ 0x81, erase("vecX", vecX) },
|
||||
.{ 0x82, erase("vecY", vecY) },
|
||||
.{ 0x83, erase("vecZ", vecZ) },
|
||||
.{ 0x84, erase("vecW", vecW) },
|
||||
.{ 0x85, rgba },
|
||||
.{ 0x85, rawFn("rgba", rgba) },
|
||||
.{ 0x86, erase("rgbaR", rgbaR) },
|
||||
.{ 0x87, erase("rgbaG", rgbaG) },
|
||||
.{ 0x88, erase("rgbaB", rgbaB) },
|
||||
|
|
|
@ -77,7 +77,7 @@ pub const Value = union(enum) {
|
|||
inline .tag, .number => |x| try std.fmt.format(writer, "{d}", .{x}),
|
||||
inline .vec4, .rgba => |x| try std.fmt.format(writer, "{s}{d}", .{ @tagName(value), x }),
|
||||
.ref => |ref| switch (ref.*) {
|
||||
.closure => |c| try std.fmt.format(writer, "function({})", .{c.param_count}),
|
||||
.closure => |c| try std.fmt.format(writer, "function {s}", .{c.name}),
|
||||
.list => |l| {
|
||||
try std.fmt.formatBuf("[", options, writer);
|
||||
for (l, 0..) |elem, i| {
|
||||
|
@ -128,11 +128,31 @@ pub const Ref = union(enum) {
|
|||
};
|
||||
|
||||
pub const Closure = struct {
|
||||
chunk: *const bytecode.Chunk,
|
||||
start: bytecode.Loc,
|
||||
pub const Impl = union(enum) {
|
||||
bytecode: Bytecode,
|
||||
// Currently unimplemented; only used as a discriminator for system functions on
|
||||
// the call stack that do not have bytecode.
|
||||
system: void,
|
||||
|
||||
pub const Bytecode = struct {
|
||||
chunk: *const bytecode.Chunk,
|
||||
start: bytecode.Loc,
|
||||
local_count: u8,
|
||||
captures: []Value,
|
||||
};
|
||||
};
|
||||
|
||||
name: []const u8,
|
||||
param_count: u8,
|
||||
local_count: u8,
|
||||
captures: []Value,
|
||||
impl: Impl,
|
||||
|
||||
pub fn bytecodePtr(c: *const Closure, pc: usize) [*]const u8 {
|
||||
return c.impl.bytecode.chunk.bytecode[c.impl.bytecode.start + pc ..].ptr;
|
||||
}
|
||||
|
||||
pub fn programCounter(c: *const Closure, ip: [*]const u8) u16 {
|
||||
return @truncate(ip - c.impl.bytecode.chunk.bytecode.ptr);
|
||||
}
|
||||
};
|
||||
|
||||
pub const List = []Value;
|
||||
|
|
|
@ -19,6 +19,10 @@ 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,
|
||||
|
@ -27,7 +31,7 @@ pub const Limits = struct {
|
|||
|
||||
pub const CallFrame = struct {
|
||||
closure: *const value.Closure,
|
||||
ip: [*]const u8,
|
||||
return_addr: ?[*]const u8,
|
||||
bottom: u32,
|
||||
};
|
||||
|
||||
|
@ -59,8 +63,10 @@ pub fn reset(vm: *Vm, fuel: u32) void {
|
|||
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));
|
||||
|
@ -92,6 +98,45 @@ 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,
|
||||
.ip = vm.ip,
|
||||
};
|
||||
} 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
@ -162,16 +207,22 @@ pub fn def(vm: *Vm, index: u16) Error!*Value {
|
|||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -181,7 +232,8 @@ inline fn read(comptime T: type, ip: *[*]const u8) T {
|
|||
return result;
|
||||
}
|
||||
|
||||
inline fn readOpcode(ip: *[*]const u8) bytecode.Opcode {
|
||||
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;
|
||||
|
@ -200,53 +252,59 @@ pub fn run(
|
|||
) 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);
|
||||
|
||||
var closure = init_closure;
|
||||
var ip: [*]const u8 = closure.chunk.bytecode[closure.start..].ptr;
|
||||
var ip: [*]const u8 = closure.bytecodePtr(0);
|
||||
var bottom = init_bottom;
|
||||
var fuel = vm.fuel;
|
||||
|
||||
for (0..closure.local_count) |_| {
|
||||
vm.closure = closure;
|
||||
vm.ip = ip;
|
||||
|
||||
for (0..closure.impl.bytecode.local_count) |_| {
|
||||
try vm.push(.nil);
|
||||
}
|
||||
|
||||
const call_bottom = vm.call_stack_top;
|
||||
try vm.pushCall(.{
|
||||
.closure = closure,
|
||||
.ip = ip,
|
||||
.return_addr = null, // nowhere to return to
|
||||
.bottom = bottom,
|
||||
});
|
||||
|
||||
next: switch (readOpcode(&ip)) {
|
||||
next: switch (vm.readOpcode(&ip)) {
|
||||
.nil => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
try vm.push(.nil);
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.false => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
try vm.push(.false);
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.true => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
try vm.push(.true);
|
||||
continue :next readOpcode(&ip);
|
||||
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 readOpcode(&ip);
|
||||
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 readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.rgba => {
|
||||
|
@ -256,7 +314,7 @@ pub fn run(
|
|||
const b = read(u8, &ip);
|
||||
const a = read(u8, &ip);
|
||||
try vm.push(.{ .rgba = value.rgbaFrom8(value.Rgba8{ r, g, b, a }) });
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.local => {
|
||||
|
@ -264,7 +322,7 @@ pub fn run(
|
|||
const index = read(u8, &ip);
|
||||
const l = try vm.local(bottom, index);
|
||||
try vm.push(l.*);
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.set_local => {
|
||||
|
@ -273,16 +331,17 @@ pub fn run(
|
|||
const new = try vm.pop();
|
||||
const l = try vm.local(bottom, index);
|
||||
l.* = new;
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.capture => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
const index = read(u8, &ip);
|
||||
try vm.validateBytecode(index < closure.captures.len, "capture index out of bounds", .{});
|
||||
const capture = closure.captures[index];
|
||||
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 readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.def => {
|
||||
|
@ -290,7 +349,7 @@ pub fn run(
|
|||
const index = read(u16, &ip);
|
||||
const d = try vm.def(index);
|
||||
try vm.push(d.*);
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.set_def => {
|
||||
|
@ -299,7 +358,7 @@ pub fn run(
|
|||
const new = try vm.pop();
|
||||
const d = try vm.def(index);
|
||||
d.* = new;
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.list => {
|
||||
|
@ -312,62 +371,70 @@ pub fn run(
|
|||
const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
|
||||
ref.* = .{ .list = list };
|
||||
try vm.push(.{ .ref = ref });
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.function => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
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.chunk.bytecode[then..].ptr;
|
||||
ip = closure.bytecodePtr(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: {
|
||||
try vm.validateBytecode(index < closure.captures.len, "capture index out of bounds", .{});
|
||||
break :blk closure.captures[index];
|
||||
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{
|
||||
.chunk = closure.chunk,
|
||||
.start = @truncate(body - closure.chunk.bytecode.ptr),
|
||||
.name = name,
|
||||
.param_count = param_count,
|
||||
.local_count = local_count,
|
||||
.captures = captures,
|
||||
.impl = .{ .bytecode = .{
|
||||
.chunk = closure.impl.bytecode.chunk,
|
||||
.start = @truncate(body - closure.bytecodePtr(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 readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.jump => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
const offset = read(u16, &ip);
|
||||
ip = closure.chunk.bytecode[offset..].ptr;
|
||||
continue :next readOpcode(&ip);
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
ip = closure.bytecodePtr(offset);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.jump_if_not => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
const offset = read(u16, &ip);
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
const condition = try vm.pop();
|
||||
if (!condition.isTruthy()) {
|
||||
ip = closure.chunk.bytecode[offset..].ptr;
|
||||
ip = closure.bytecodePtr(offset);
|
||||
}
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.field => {
|
||||
|
@ -389,27 +456,31 @@ pub fn run(
|
|||
}
|
||||
|
||||
if (found_index) |index| {
|
||||
try vm.validateBytecode(index < closure.captures.len, "field index out of bounds", .{});
|
||||
const field = closure.captures[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 readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.call => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
|
||||
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 {}",
|
||||
|
@ -419,7 +490,7 @@ pub fn run(
|
|||
|
||||
const call_frame = CallFrame{
|
||||
.closure = closure,
|
||||
.ip = ip,
|
||||
.return_addr = ip,
|
||||
.bottom = bottom,
|
||||
};
|
||||
|
||||
|
@ -427,29 +498,61 @@ pub fn run(
|
|||
try vm.validateBytecode(overflow == 0, "not enough values on the stack for arguments", .{});
|
||||
|
||||
closure = called_closure;
|
||||
ip = closure.chunk.bytecode[closure.start..].ptr;
|
||||
ip = closure.bytecodePtr(0);
|
||||
bottom = new_bottom;
|
||||
|
||||
for (0..closure.local_count) |_| {
|
||||
// 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);
|
||||
}
|
||||
|
||||
try vm.pushCall(call_frame);
|
||||
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
.system => {
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
|
||||
const index = read(u8, &ip);
|
||||
const arg_count = read(u8, &ip);
|
||||
const system_fn = system.fns[index];
|
||||
const system_fn = &system.fns[index];
|
||||
|
||||
log.debug("system index={} arg_count={} system_fn={p}", .{ index, arg_count, system_fn });
|
||||
try vm.consumeFuel(&fuel, 1);
|
||||
|
||||
vm.storeContext(.{ .fuel = fuel });
|
||||
const result = try system_fn(.{
|
||||
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],
|
||||
|
@ -460,7 +563,16 @@ pub fn run(
|
|||
vm.stack_top -= arg_count;
|
||||
try vm.push(result);
|
||||
|
||||
continue :next readOpcode(&ip);
|
||||
// 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 => {
|
||||
|
@ -474,26 +586,28 @@ pub fn run(
|
|||
"called function popped too many values. bottom={} stack_top={}",
|
||||
.{ bottom, vm.stack_top },
|
||||
);
|
||||
try vm.validateBytecode(
|
||||
call_bottom <= vm.call_stack_top,
|
||||
"called function popped too many call frames. call_bottom={} call_stack_top={}",
|
||||
.{ call_bottom, vm.call_stack_top },
|
||||
);
|
||||
|
||||
vm.stack_top = bottom;
|
||||
try vm.push(result);
|
||||
|
||||
if (vm.call_stack_top == call_bottom) {
|
||||
// If this is the last call frame from this `run` instance, return from the function.
|
||||
vm.storeContext(Context{ .fuel = fuel });
|
||||
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;
|
||||
}
|
||||
|
||||
closure = call_frame.closure;
|
||||
ip = call_frame.ip;
|
||||
bottom = call_frame.bottom;
|
||||
|
||||
continue :next readOpcode(&ip);
|
||||
continue :next vm.readOpcode(&ip);
|
||||
},
|
||||
|
||||
_ => |invalid_opcode| {
|
||||
|
@ -531,11 +645,14 @@ pub fn runDotter(
|
|||
}) catch return vm.outOfMemory();
|
||||
const ref = allocator.create(value.Ref) catch return vm.outOfMemory();
|
||||
ref.* = value.Ref{ .closure = .{
|
||||
.chunk = &system.record_dotter,
|
||||
.start = 0,
|
||||
.name = "dotter",
|
||||
.param_count = 1,
|
||||
.local_count = 0,
|
||||
.captures = data,
|
||||
.impl = .{ .bytecode = .{
|
||||
.chunk = &system.record_dotter,
|
||||
.start = 0,
|
||||
.local_count = 0,
|
||||
.captures = data,
|
||||
} },
|
||||
} };
|
||||
|
||||
const bottom = vm.stack_top;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue