zlox/src/vm.zig

273 lines
8.5 KiB
Zig

const std = @import("std");
const debug = std.debug;
const Allocator = std.mem.Allocator;
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 Table = @import("./table.zig").Table;
const compile = @import("./compile.zig").compile;
const compute_hash = @import("./utils.zig").compute_hash;
const DEBUG_TRACE_EXECUTION = @import("./main.zig").DEBUG_TRACE_EXECUTION;
const print_value = @import("./values.zig").print_value;
const STACK_MAX = 256;
pub const InterpretResult = enum {
OK,
COMPILE_ERROR,
RUNTIME_ERROR,
};
pub const VM = struct {
allocator: Allocator,
chunk: ?*Chunk,
ip: ?usize,
stack: std.ArrayList(Value),
// Keeping creating objects in references to destroy objects on cleaning.
// In the book, a linked list between objects is used to handle this.
references: std.ArrayList(*Obj),
strings: Table,
tracing: bool,
pub fn new(allocator: Allocator) VM {
return VM{
.allocator = allocator,
.chunk = null,
.ip = null,
.stack = std.ArrayList(Value).init(allocator),
.references = std.ArrayList(*Obj).init(allocator),
.strings = Table.new(allocator),
.tracing = false,
};
}
pub fn free(self: *VM) void {
self.stack.deinit();
if (self.has_tracing()) {
self.strings.dump();
}
self.strings.deinit();
self.clean_references();
self.references.deinit();
}
pub fn set_trace(self: *VM, tracing: bool) void {
self.tracing = tracing;
}
pub fn has_tracing(self: *VM) bool {
return self.tracing;
}
pub fn interpret(self: *VM, allocator: Allocator, content: []const u8) !InterpretResult {
var chunk = Chunk.new(allocator);
defer chunk.deinit();
const res = try compile(allocator, self, content, &chunk);
if (!res) {
return InterpretResult.COMPILE_ERROR;
}
self.chunk = &chunk;
self.ip = 0;
return try self.run();
}
pub fn run(self: *VM) !InterpretResult {
while (true) {
if (self.has_tracing()) {
if (self.stack.items.len > 0) {
debug.print("{s:32}", .{""});
for (self.stack.items) |item| {
debug.print("[ ", .{});
print_value(item);
debug.print(" ]", .{});
}
debug.print("\n", .{});
}
_ = self.chunk.?.dissassemble_instruction(self.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_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_RETURN) => {
print_value(self.pop());
debug.print("\n", .{});
return InterpretResult.OK;
},
@intFromEnum(OpCode.OP_EQUAL) => {
try self.push(Value.bool_val(self.pop().equals(self.pop())));
},
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.chunk.?.code[self.ip.?];
self.ip.? += 1;
return ret;
}
pub fn read_constant(self: *VM) Value {
return self.chunk.?.constants.values[read_byte(self)];
}
pub fn push(self: *VM, value: Value) !void {
try self.stack.append(value);
}
pub fn pop(self: *VM) Value {
return self.stack.pop();
}
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.pop().as_cstring();
const a = self.pop().as_cstring();
const concat_str = try std.mem.concat(self.chunk.?.allocator, u8, &.{ a, b });
var string_obj = self.take_string(concat_str);
self.add_reference(&string_obj.obj);
try self.push(Value.obj_val(&string_obj.obj));
}
pub fn peek(self: *VM, distance: usize) Value {
return self.stack.items[self.stack.items.len - 1 - distance];
}
pub fn runtime_error(self: *VM, err_msg: []const u8) void {
const instruction = self.ip.?;
const line = self.chunk.?.lines[instruction];
debug.print("err: {s}\n", .{err_msg});
debug.print("[line {d}] in script\n", .{line});
}
pub fn add_reference(self: *VM, obj: *Obj) void {
// do not add duplicate references
for (self.references.items) |item| {
if (item == obj) {
return;
}
}
// XXX TODO catch unreachable to prevents
self.references.append(obj) catch unreachable;
}
pub fn clean_references(self: *VM) void {
for (self.references.items) |item| {
item.destroy();
}
}
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.allocator, source);
_ = self.strings.set(obj_string, Value.nil_val());
return obj_string;
}
};