zlox/src/vm.zig

723 lines
27 KiB
Zig

const std = @import("std");
const debug = std.debug;
const Allocator = std.mem.Allocator;
const constants = @import("./constant.zig");
const ZloxAllocator = @import("./memory.zig").ZloxAllocator;
const Chunk = @import("./chunk.zig").Chunk;
const OpCode = @import("./opcode.zig").OpCode;
const Value = @import("./values.zig").Value;
const Obj = @import("./object.zig").Obj;
const ObjType = @import("./object.zig").ObjType;
const Parser = @import("./compile.zig").Parser;
const NativeFn = @import("./object.zig").NativeFn;
const Table = @import("./table.zig").Table;
const natives = @import("./native.zig");
const compile = @import("./compile.zig").compile;
const compute_hash = @import("./utils.zig").compute_hash;
const print_value = @import("./values.zig").print_value;
pub const InterpretResult = enum {
OK,
COMPILE_ERROR,
RUNTIME_ERROR,
};
pub const CallFrame = struct {
closure: *Obj.Closure,
ip: usize,
// pointer to stack index provided to this frame
slots_idx: usize,
};
pub const VM = struct {
allocator: Allocator,
stack: [constants.STACK_MAX]Value,
stack_top: usize,
strings: Table,
globals: Table,
frames: [constants.FRAMES_MAX]CallFrame,
frame_count: usize,
open_upvalues: ?*Obj.Upvalue,
objects: ?*Obj,
parser: ?*Parser,
gray_count: usize,
gray_capacity: usize,
gray_stack: ?[]*Obj,
init_string: ?*Obj.String,
pub fn new() VM {
const vm = VM{
.allocator = undefined,
.stack = undefined,
.stack_top = 0,
.strings = undefined,
.globals = undefined,
.frames = undefined,
.frame_count = 0,
.open_upvalues = null,
.objects = null,
.parser = null,
.gray_capacity = 0,
.gray_count = 0,
.gray_stack = &.{},
.init_string = null,
};
return vm;
}
pub fn init_vm(self: *VM, allocator: Allocator) void {
self.allocator = allocator;
self.globals = Table.new(self.allocator);
self.strings = Table.new(self.allocator);
_ = try self.push(Value.obj_val(&self.copy_string("init").obj));
self.init_string = self.pop().as_string();
self.define_native("clock", natives.clock);
self.define_native("power", natives.power);
self.define_native("str2num", natives.str2num);
self.define_native("num2str", natives.num2str);
}
pub fn destroy(self: *VM) void {
if (constants.DEBUG_PRINT_INTERNAL_STRINGS) {
self.strings.dump();
}
self.strings.destroy();
self.globals.destroy();
self.init_string = null;
self.destroy_objects();
if (self.gray_stack != null) {
self.allocator.free(self.gray_stack.?);
}
}
pub fn destroy_objects(self: *VM) void {
var obj = self.objects;
while (obj != null) {
const obj_next = obj.?.next;
obj.?.destroy();
obj = obj_next;
}
}
pub fn dump_objects(self: *VM) void {
var obj = self.objects;
while (obj != null) {
const obj_next = obj.?.next;
std.debug.print("OBJ: {*} {any}", .{ obj.?, obj.?.kind });
// obj.?.print();
std.debug.print("\n", .{});
obj = obj_next;
}
}
inline fn current_chunk(self: *VM) *Chunk {
return self.frames[self.frame_count - 1].closure.function.chunk;
}
inline fn current_frame(self: *VM) *CallFrame {
return &self.frames[self.frame_count - 1];
}
pub fn interpret(self: *VM, content: []const u8) !InterpretResult {
var function = try compile(self, content);
if (function == null) {
return InterpretResult.COMPILE_ERROR;
}
_ = try self.push(Value.obj_val(&function.?.obj));
const closure: *Obj.Closure = Obj.Closure.new(self, function.?);
_ = self.pop();
_ = try self.push(Value.obj_val(&closure.obj));
_ = self.call(closure, 0);
return try self.run();
}
pub fn run(self: *VM) !InterpretResult {
while (true) {
if (constants.DEBUG_TRACE_EXECUTION) {
if (self.stack_top > 0) {
debug.print("{s:10}", .{""});
for (0..self.stack_top) |item_idx| {
debug.print("[ ", .{});
print_value(self.stack[item_idx]);
debug.print(" ]", .{});
}
debug.print("\n", .{});
}
_ = self.current_chunk().dissassemble_instruction(self.current_frame().ip);
}
const instruction = self.read_byte();
switch (instruction) {
@intFromEnum(OpCode.OP_CONSTANT) => {
const constant = self.read_constant();
try self.push(constant);
},
@intFromEnum(OpCode.OP_NIL) => try self.push(Value.nil_val()),
@intFromEnum(OpCode.OP_FALSE) => try self.push(Value.bool_val(false)),
@intFromEnum(OpCode.OP_TRUE) => try self.push(Value.bool_val(true)),
@intFromEnum(OpCode.OP_POP) => _ = self.pop(),
@intFromEnum(OpCode.OP_ADD),
@intFromEnum(OpCode.OP_SUBSTRACT),
@intFromEnum(OpCode.OP_MULTIPLY),
@intFromEnum(OpCode.OP_DIVIDE),
@intFromEnum(OpCode.OP_LESS),
@intFromEnum(OpCode.OP_GREATER),
=> {
const res = try self.binary_op(@enumFromInt(instruction));
if (res != InterpretResult.OK) {
return res;
}
},
@intFromEnum(OpCode.OP_NOT) => {
try self.push(Value.bool_val(self.pop().is_falsey()));
},
@intFromEnum(OpCode.OP_NEGATE) => {
if (!self.peek(0).is_number()) {
self.runtime_error("Operand must be a number.");
return InterpretResult.RUNTIME_ERROR;
}
try self.push(Value.number_val(-self.pop().as_number()));
},
@intFromEnum(OpCode.OP_PRINT) => {
print_value(self.pop());
debug.print("\n", .{});
},
@intFromEnum(OpCode.OP_RETURN) => {
const result = self.pop();
self.close_upvalues(&self.stack[self.current_frame().slots_idx]);
self.frame_count -= 1;
if (self.frame_count == 0) {
_ = self.pop();
return InterpretResult.OK;
}
self.stack_top = self.frames[self.frame_count].slots_idx;
try self.push(result);
},
@intFromEnum(OpCode.OP_EQUAL) => {
try self.push(Value.bool_val(self.pop().equals(self.pop())));
},
@intFromEnum(OpCode.OP_DEFINE_GLOBAL) => {
const name = self.read_constant().as_string();
_ = self.globals.set(name, self.peek(0));
_ = self.pop();
},
@intFromEnum(OpCode.OP_GET_GLOBAL) => {
const name: *Obj.String = self.read_constant().as_string();
var value = Value.nil_val();
if (!self.globals.get(name, &value)) {
const err_msg = try std.fmt.allocPrint(self.allocator, "Undefined variable '{s}'.", .{name.chars});
defer self.allocator.free(err_msg);
self.runtime_error(err_msg);
return InterpretResult.RUNTIME_ERROR;
}
try self.push(value);
},
@intFromEnum(OpCode.OP_SET_GLOBAL) => {
const name: *Obj.String = self.read_constant().as_string();
if (self.globals.set(name, self.peek(0))) {
_ = self.globals.del(name);
const err_msg = try std.fmt.allocPrint(self.allocator, "Undefined variable '{s}'.", .{name.chars});
defer self.allocator.free(err_msg);
self.runtime_error(err_msg);
return InterpretResult.RUNTIME_ERROR;
}
},
@intFromEnum(OpCode.OP_GET_LOCAL) => {
const slot = self.read_byte();
try self.push(self.stack[self.current_frame().slots_idx + slot]);
},
@intFromEnum(OpCode.OP_SET_LOCAL) => {
const slot = self.read_byte();
self.stack[self.current_frame().slots_idx + slot] = self.peek(0);
},
@intFromEnum(OpCode.OP_JUMP) => {
const offset = self.read_short();
self.current_frame().ip += offset;
},
@intFromEnum(OpCode.OP_JUMP_IF_FALSE) => {
const offset = self.read_short();
if (self.peek(0).is_falsey()) {
self.current_frame().ip += offset;
}
},
@intFromEnum(OpCode.OP_LOOP) => {
const offset = self.read_short();
self.current_frame().ip -= offset;
},
@intFromEnum(OpCode.OP_CALL) => {
const arg_count = self.read_byte();
if (!self.call_value(self.peek(arg_count), arg_count)) {
return InterpretResult.RUNTIME_ERROR;
}
},
@intFromEnum(OpCode.OP_CLOSURE) => {
const function = self.read_constant().as_obj().as_function();
const closure = Obj.Closure.new(self, function);
_ = try self.push(Value.obj_val(&closure.obj));
for (0..closure.upvalue_count) |i| {
const is_local = self.read_byte();
const index = self.read_byte();
if (is_local == 1) {
const value_idx = self.current_frame().slots_idx + index;
closure.upvalues[i] = self.capture_upvalue(&self.stack[value_idx]);
} else {
closure.upvalues[i] = self.current_frame().closure.upvalues[index];
}
}
},
@intFromEnum(OpCode.OP_GET_UPVALUE) => {
const slot = self.read_byte();
try self.push(self.current_frame().closure.upvalues[slot].?.location.*);
},
@intFromEnum(OpCode.OP_SET_UPVALUE) => {
const slot = self.read_byte();
self.current_frame().closure.upvalues[slot].?.location = @constCast(&self.peek(0));
},
@intFromEnum(OpCode.OP_CLOSE_UPVALUE) => {
self.close_upvalues(&self.stack[self.stack_top - 1]);
_ = self.pop();
},
@intFromEnum(OpCode.OP_CLASS) => {
const name: *Obj.String = self.read_constant().as_string();
_ = try self.push(Value.obj_val(&Obj.Class.new(self, name).obj));
},
@intFromEnum(OpCode.OP_GET_PROPERTY) => {
if (!self.peek(0).is_obj() or !self.peek(0).as_obj().is_instance()) {
self.runtime_error("Only instances have properties.");
return InterpretResult.RUNTIME_ERROR;
}
const instance = self.peek(0).as_obj().as_instance();
const name = self.read_constant().as_string();
var value = Value.nil_val();
if (instance.fields.get(name, &value)) {
_ = self.pop();
_ = try self.push(value);
continue;
}
if (!self.bind_method(instance.class, name)) {
return InterpretResult.RUNTIME_ERROR;
}
},
@intFromEnum(OpCode.OP_SET_PROPERTY) => {
if (!self.peek(1).is_obj() or !self.peek(1).as_obj().is_instance()) {
self.runtime_error("Only instances have fields.");
return InterpretResult.RUNTIME_ERROR;
}
const instance = self.peek(1).as_obj().as_instance();
_ = instance.fields.set(self.read_constant().as_string(), self.peek(0));
const value = self.pop();
_ = self.pop();
_ = try self.push(value);
},
@intFromEnum(OpCode.OP_METHOD) => {
self.define_method(self.read_constant().as_string());
},
@intFromEnum(OpCode.OP_INDEX_GET) => {
if (!self.peek(0).is_number() or !self.peek(1).is_string()) {
self.runtime_error("A number and a string are required for indexes.");
return InterpretResult.RUNTIME_ERROR;
}
const index_val = self.pop();
const value = self.pop();
const index: usize = @as(usize, @intFromFloat(index_val.as_number()));
if (index >= value.as_cstring().len) {
self.runtime_error("The index must be set between 0 and string len.");
return InterpretResult.RUNTIME_ERROR;
}
const c = value.as_cstring()[index .. index + 1];
_ = try self.push(Value.obj_val(&self.copy_string(c).obj));
},
@intFromEnum(OpCode.OP_INDEX_SET) => {
const value = self.pop();
const index_val = self.pop();
const origin = self.pop();
if (!value.is_string() or value.as_cstring().len != 1) {
self.runtime_error("Value to assign must be one byte.");
return InterpretResult.RUNTIME_ERROR;
}
const index: usize = @as(usize, @intFromFloat(index_val.as_number()));
var str = self.allocator.dupe(u8, origin.as_cstring()) catch unreachable;
str[index] = value.as_cstring()[0];
_ = try self.push(Value.obj_val(&self.take_string(str).obj));
},
@intFromEnum(OpCode.OP_INVOKE) => {
const method = self.read_constant().as_string();
const arg_count = self.read_byte();
if (!self.invoke(method, arg_count)) {
return InterpretResult.RUNTIME_ERROR;
}
},
@intFromEnum(OpCode.OP_INHERIT) => {
const superclass = self.peek(1);
const subclass = self.peek(0).as_obj().as_class();
if (!superclass.is_obj() or !superclass.as_obj().is_class()) {
self.runtime_error("Superclass must be a class.");
return InterpretResult.RUNTIME_ERROR;
}
superclass.as_obj().as_class().methods.add_all(&subclass.methods);
_ = self.pop(); // subclass
},
@intFromEnum(OpCode.OP_GET_SUPER) => {
const name: *Obj.String = self.read_constant().as_string();
const superclass = self.pop().as_obj().as_class();
if (!self.bind_method(superclass, name)) {
return InterpretResult.RUNTIME_ERROR;
}
},
@intFromEnum(OpCode.OP_SUPER_INVOKE) => {
const method = self.read_constant().as_string();
const arg_count = self.read_byte();
const superclass = self.pop().as_obj().as_class();
if (!self.invoke_from_class(superclass, method, arg_count)) {
return InterpretResult.RUNTIME_ERROR;
}
},
else => {
debug.print("Invalid instruction: {d}\n", .{instruction});
return InterpretResult.RUNTIME_ERROR;
},
}
}
return InterpretResult.OK;
}
// XXX In the book, we're using a ptr to data directly, to avoid dereferencing to a given offset
// How to do that in Zig?
pub fn read_byte(self: *VM) u8 {
const ret = self.current_chunk().code[self.current_frame().ip];
self.current_frame().ip += 1;
return ret;
}
pub fn read_short(self: *VM) u16 {
self.current_frame().ip += 2;
return (@as(u16, self.current_chunk().code[self.current_frame().ip - 2]) << 8) | (@as(u16, self.current_chunk().code[self.current_frame().ip - 1]));
}
pub fn read_constant(self: *VM) Value {
return self.current_chunk().constants.values[read_byte(self)];
}
pub fn push(self: *VM, value: Value) !void {
self.stack[self.stack_top] = value;
self.stack_top += 1;
}
pub fn pop(self: *VM) Value {
const value = self.stack[self.stack_top - 1];
self.stack_top -= 1;
return value;
}
pub fn binary_op(self: *VM, op: OpCode) !InterpretResult {
if (op == OpCode.OP_ADD and self.peek(0).is_string() and self.peek(1).is_string()) {
try self.concatenate();
return InterpretResult.OK;
}
if (!self.peek(0).is_number() or !self.peek(0).is_number()) {
self.runtime_error("Operands must be two numbers or two strings");
return InterpretResult.RUNTIME_ERROR;
}
const b = self.pop().as_number();
const a = self.pop().as_number();
const res: Value = switch (op) {
OpCode.OP_ADD => Value.number_val(a + b),
OpCode.OP_SUBSTRACT => Value.number_val(a - b),
OpCode.OP_MULTIPLY => Value.number_val(a * b),
OpCode.OP_DIVIDE => Value.number_val(a / b),
OpCode.OP_LESS => Value.bool_val(a < b),
OpCode.OP_GREATER => Value.bool_val(a > b),
else => unreachable,
};
try self.push(res);
return InterpretResult.OK;
}
pub fn concatenate(self: *VM) !void {
const b = self.peek(0).as_cstring();
const a = self.peek(1).as_cstring();
const concat_str = try std.mem.concat(self.current_chunk().allocator, u8, &.{ a, b });
const string_obj = self.take_string(concat_str);
_ = self.pop();
_ = self.pop();
try self.push(Value.obj_val(&string_obj.obj));
}
pub fn peek(self: *VM, distance: usize) Value {
return self.stack[self.stack_top - 1 - distance];
}
pub fn runtime_error(self: *VM, err_msg: []const u8) void {
debug.print("err: {s}\n", .{err_msg});
var frame_idx = self.frame_count - 1;
while (true) {
const frame = self.frames[frame_idx];
const closure = frame.closure;
const instruction = frame.ip;
debug.print("[line {d}] in ", .{closure.function.chunk.lines[instruction]});
if (closure.function.name == null) {
debug.print("script\n", .{});
} else {
debug.print("{s}()\n", .{closure.function.name.?.chars});
}
if (frame_idx == 0) {
break;
}
frame_idx -= 1;
}
}
pub fn copy_string(self: *VM, source: []const u8) *Obj.String {
const hash = compute_hash(source);
const obj_string = self.strings.find_string(source, hash);
if (obj_string != null) {
return obj_string.?;
}
const copy: []const u8 = self.allocator.dupe(u8, source) catch unreachable;
return self.allocate_string(copy);
}
pub fn take_string(self: *VM, source: []const u8) *Obj.String {
const hash = compute_hash(source);
const obj_string = self.strings.find_string(source, hash);
if (obj_string != null) {
// free given string
self.allocator.free(source);
return obj_string.?;
}
return self.allocate_string(source);
}
pub fn allocate_string(self: *VM, source: []const u8) *Obj.String {
const obj_string = Obj.String.new(self, source);
_ = try self.push(Value.obj_val(&obj_string.obj));
_ = self.strings.set(obj_string, Value.nil_val());
_ = self.pop();
return obj_string;
}
pub fn call_value(self: *VM, callee: Value, arg_count: usize) bool {
if (callee.is_obj()) {
switch (callee.as_obj().kind) {
ObjType.Function => {
return self.call(callee.as_obj().as_closure(), arg_count);
},
ObjType.Native => {
const native_obj: *Obj.Native = callee.as_obj().as_native();
const value = native_obj.native(
self,
arg_count,
self.stack[self.stack_top - arg_count .. self.stack_top],
);
self.stack_top -= arg_count + 1;
_ = try self.push(value);
return true;
},
ObjType.Closure => {
return self.call(callee.as_obj().as_closure(), arg_count);
},
ObjType.Class => {
const class = callee.as_obj().as_class();
self.stack[self.stack_top - arg_count - 1] = Value.obj_val(&Obj.Instance.new(self, class).obj);
var initializer = Value.nil_val();
if (class.methods.get(self.init_string.?, &initializer) == true) {
return self.call(initializer.as_obj().as_closure(), arg_count);
} else if (arg_count != 0) {
self.runtime_error("Expected 0 arguments."); // XXX show number of arguments.
}
return true;
},
ObjType.BoundMethod => {
const bound_method = callee.as_obj().as_bound_method();
self.stack[self.stack_top - arg_count - 1] = bound_method.receiver;
return self.call(bound_method.method, arg_count);
},
else => {},
}
}
self.runtime_error("Can only call functions and classes.");
return false;
}
pub fn bind_method(self: *VM, class: *Obj.Class, name: *Obj.String) bool {
var method: Value = Value.nil_val();
if (!class.methods.get(name, &method)) {
const err_msg = std.fmt.allocPrint(self.allocator, "Undefined property '{s}'.", .{name.chars}) catch unreachable;
defer self.allocator.free(err_msg);
self.runtime_error(err_msg);
return false;
}
const bound: *Obj.BoundMethod = Obj.BoundMethod.new(self, self.peek(0), method.as_obj().as_closure());
_ = self.pop();
try self.push(Value.obj_val(&bound.obj));
return true;
}
pub fn call(self: *VM, closure: *Obj.Closure, arg_count: usize) bool {
if (arg_count != closure.function.arity) {
const err_msg = std.fmt.allocPrint(self.allocator, "Expected {d} arguments but got {d}.", .{ closure.function.arity, arg_count }) catch unreachable;
defer self.allocator.free(err_msg);
self.runtime_error(err_msg);
return false;
}
if (self.frame_count == constants.FRAMES_MAX) {
self.runtime_error("Stack overflow.");
return false;
}
const frame = &self.frames[self.frame_count];
self.frame_count += 1;
frame.closure = closure;
frame.ip = 0;
frame.slots_idx = self.stack_top - arg_count - 1;
return true;
}
pub fn define_native(self: *VM, name: []const u8, native_fn: NativeFn) void {
_ = try self.push(Value.obj_val(&self.copy_string(name).obj));
_ = try self.push(Value.obj_val(&Obj.Native.new(self, native_fn).obj));
_ = self.globals.set(self.stack[0].as_string(), self.stack[1]);
_ = self.pop();
_ = self.pop();
}
fn capture_upvalue(self: *VM, local: *Value) *Obj.Upvalue {
var prev_upvalue: ?*Obj.Upvalue = null;
var upvalue: ?*Obj.Upvalue = self.open_upvalues;
while (upvalue != null and @intFromPtr(upvalue.?.location) > @intFromPtr(local)) {
prev_upvalue = upvalue;
upvalue = upvalue.?.next;
}
if (upvalue != null and upvalue.?.location == local) {
return upvalue.?;
}
const created_upvalue = Obj.Upvalue.new(self, local);
created_upvalue.next = upvalue;
if (prev_upvalue == null) {
self.open_upvalues = created_upvalue;
} else {
prev_upvalue.?.next = created_upvalue;
}
return created_upvalue;
}
fn close_upvalues(self: *VM, last: *Value) void {
while (self.open_upvalues != null and @intFromPtr(self.open_upvalues.?.location) >= @intFromPtr(last)) {
const upvalue = self.open_upvalues.?;
upvalue.closed = upvalue.location.*;
upvalue.location = &upvalue.closed;
self.open_upvalues = upvalue.next;
}
}
fn define_method(self: *VM, name: *Obj.String) void {
const method = self.peek(0);
const class = self.peek(1).as_obj().as_class();
_ = class.methods.set(name, method);
_ = self.pop();
}
fn invoke(self: *VM, name: *Obj.String, arg_count: usize) bool {
const receiver = self.peek(arg_count);
const instance = receiver.as_obj().as_instance();
var value = Value.nil_val();
if (instance.fields.get(name, &value)) {
self.stack[self.stack_top - arg_count - 1] = value;
return self.call_value(value, arg_count);
}
return self.invoke_from_class(instance.class, name, arg_count);
}
fn invoke_from_class(self: *VM, class: *Obj.Class, name: *Obj.String, arg_count: usize) bool {
var method = Value.nil_val();
if (!class.methods.get(name, &method)) {
const err_msg = std.fmt.allocPrint(self.allocator, "Undefined property '{s}'.", .{name.chars}) catch unreachable;
defer self.allocator.free(err_msg);
self.runtime_error(err_msg);
return false;
}
return self.call(method.as_obj().as_closure(), arg_count);
}
};