const bytecode = @import("./bytecode.zig"); const coral = @import("coral"); pub const NewError = error { OutOfMemory, }; pub const Object = opaque { fn cast(object_instance: *ObjectInstance) *Object { return @ptrCast(*Object, object_instance); } pub fn userdata(object: *Object) ObjectUserdata { return ObjectInstance.cast(object).userdata; } }; pub const ObjectBehavior = struct { caller: *const ObjectCaller = default_call, getter: *const ObjectGetter = default_get, setter: *const ObjectSetter = default_set, fn default_call(_: *Vm, _: *Object, _: *Object, _: []const Value) RuntimeError!Value { return error.IllegalOperation; } fn default_get(vm: *Vm, object: *Object, index: Value) RuntimeError!Value { return vm.get_field(object, ObjectInstance.cast(try vm.new_string_value(index)).userdata.string); } fn default_set(vm: *Vm, object: *Object, index: Value, value: Value) RuntimeError!void { try vm.set_field(object, ObjectInstance.cast(try vm.new_string_value(index)).userdata.string, value); } }; pub const ObjectCaller = fn (vm: *Vm, object: *Object, context: *Object, arguments: []const Value) RuntimeError!Value; pub const ObjectGetter = fn (vm: *Vm, object: *Object, index: Value) RuntimeError!Value; const ObjectInstance = struct { behavior: ObjectBehavior, userdata: ObjectUserdata, fields: ?ValueTable = null, fn cast(object: *Object) *ObjectInstance { return @ptrCast(*ObjectInstance, @alignCast(@alignOf(ObjectInstance), object)); } }; pub const ObjectSetter = fn (vm: *Vm, object: *Object, index: Value, value: Value) RuntimeError!void; pub const ObjectUserdata = union (enum) { none, native: *anyopaque, string: []u8, chunk: bytecode.Chunk }; pub const RuntimeError = NewError || error { StackOverflow, IllegalOperation, UnsupportedOperation, }; pub const Value = union(enum) { nil, false, true, float: Float, integer: Integer, vector2: coral.math.Vector2, object: *Object, pub const Integer = i64; pub const Float = f64; pub fn to_float(self: Value) ?Float { return switch (self) { .float => |float| float, .integer => |integer| @intToFloat(Float, integer), else => null, }; } pub fn to_object(self: Value) ?*Object { return switch (self) { .object => |object| object, else => null, }; } pub fn to_integer(self: Value) ?Integer { return switch (self) { .integer => |integer| integer, // TODO: Verify safety of cast. .float => |float| @floatToInt(Float, float), else => null, }; } pub fn to_vector2(self: Value) ?coral.math.Vector2 { return switch (self) { .vector2 => |vector2| vector2, else => null, }; } }; const ValueTable = coral.table.Hashed(coral.table.string_key, Value); pub const Vm = struct { allocator: coral.io.MemoryAllocator, heap: struct { count: u32 = 0, free_head: u32 = 0, allocations: []HeapAllocation, global_instance: ObjectInstance, const Self = @This(); }, stack: struct { top: u32 = 0, values: []Value, const Self = @This(); fn pop(self: *Self) ?Value { if (self.top == 0) return null; self.top -= 1; return self.values[self.top]; } fn push(self: *Self, value: Value) !void { if (self.top == self.values.len) return error.StackOverflow; self.values[self.top] = value; self.top += 1; } }, pub const CompileError = bytecode.ParseError; const HeapAllocation = union(enum) { next_free: u32, instance: ObjectInstance, }; pub const InitOptions = struct { stack_max: u32, objects_max: u32, }; pub fn call_get(self: *Vm, object: *Object, index: Value, arguments: []const Value) RuntimeError!Value { return switch (self.get(object, index)) { .object => |callable| ObjectInstance.cast(object).behavior.caller(self, callable, object, arguments), else => error.IllegalOperation, }; } pub fn call_self(self: *Vm, object: *Object, arguments: []const Value) RuntimeError!Value { return ObjectInstance.cast(object).behavior.caller(self, object, self.globals(), arguments); } pub fn deinit(self: *Vm) void { self.allocator.deallocate(self.heap.allocations.ptr); self.allocator.deallocate(self.stack.values.ptr); } pub fn globals(self: *Vm) *Object { return Object.cast(&self.heap.global_instance); } pub fn init(allocator: coral.io.MemoryAllocator, init_options: InitOptions) !Vm { const heap_allocations = (allocator.allocate_many(HeapAllocation, init_options.objects_max) orelse return error.OutOfMemory)[0 .. init_options.objects_max]; errdefer allocator.deallocate(heap_allocations); for (heap_allocations) |*heap_allocation| heap_allocation.* = .{.next_free = 0}; const values = (allocator.allocate_many(Value, init_options.stack_max) orelse return error.OutOfMemory)[0 .. init_options.stack_max]; errdefer allocator.deallocate(values); for (values) |*value| value.* = .nil; const global_values = try ValueTable.init(allocator); errdefer global_values.deinit(); var vm = Vm{ .allocator = allocator, .stack = .{.values = values}, .heap = .{ .allocations = heap_allocations, .global_instance = .{ .behavior = .{}, .userdata = .none, }, }, }; return vm; } pub fn get(self: *Vm, index: Value) RuntimeError!Value { return ObjectInstance.cast(self).behavior.getter(self, index); } pub fn get_field(_: *Vm, object: *Object, field: []const u8) Value { const fields = &(ObjectInstance.cast(object).fields orelse return .nil); return fields.lookup(field) orelse .nil; } pub fn new(self: *Vm, object_userdata: ObjectUserdata, object_behavior: ObjectBehavior) NewError!*Object { if (self.heap.count == self.heap.allocations.len) return error.OutOfMemory; defer self.heap.count += 1; if (self.heap.free_head != self.heap.count) { const free_list_next = self.heap.allocations[self.heap.free_head].next_free; const index = self.heap.free_head; const allocation = &self.heap.allocations[index]; allocation.* = .{.instance = .{ .userdata = object_userdata, .behavior = object_behavior, }}; self.heap.free_head = free_list_next; return Object.cast(&allocation.instance); } const allocation = &self.heap.allocations[self.heap.count]; allocation.* = .{.instance = .{ .userdata = object_userdata, .behavior = object_behavior, }}; self.heap.free_head += 1; return Object.cast(&allocation.instance); } pub fn new_array(self: *Vm, _: Value.Integer) NewError!*Object { // TODO: Implement. return self.new(.none, .{}); } pub fn new_closure(self: *Vm, caller: *const ObjectCaller) NewError!*Object { // TODO: Implement. return self.new(.none, .{.caller = caller}); } pub fn new_script(self: *Vm, script_source: []const u8) CompileError!*Object { var chunk = try bytecode.Chunk.init(self.allocator); errdefer chunk.deinit(); try chunk.compile(script_source); return self.new(.{.chunk = chunk}, .{ .caller = struct { fn chunk_cast(context: *Object) *bytecode.Chunk { return @ptrCast(*bytecode.Chunk, @alignCast(@alignOf(bytecode.Chunk), context.userdata().native)); } fn call(vm: *Vm, object: *Object, _: *Object, arguments: []const Value) RuntimeError!Value { return execute_chunk(chunk_cast(object).*, vm, arguments); } }.call, }); } pub fn new_string(self: *Vm, string_data: []const u8) NewError!*Object { return self.new(.{.string = allocate_copy: { if (string_data.len == 0) break: allocate_copy &.{}; const string_copy = (self.allocator.allocate_many( u8, string_data.len) orelse return error.OutOfMemory)[0 .. string_data.len]; coral.io.copy(string_copy, string_data); break: allocate_copy string_copy; }}, .{ }); } pub fn new_string_value(self: *Vm, value: Value) NewError!*Object { // TODO: Implement. return switch (value) { .nil => self.new_string(""), else => unreachable, }; } pub fn new_table(self: *Vm) NewError!*Object { // TODO: Implement. return self.new(.none, .{}); } pub fn set(self: *Vm, object: *Object, index: Value, value: Value) RuntimeError!void { return ObjectInstance.cast(object).behavior.setter(self, object, index, value); } pub fn set_field(self: *Vm, object: *Object, field: []const u8, value: Value) NewError!void { const object_instance = ObjectInstance.cast(object); if (object_instance.fields == null) object_instance.fields = try ValueTable.init(self.allocator); try object_instance.fields.?.assign(field, value); } }; fn execute_chunk(chunk: bytecode.Chunk, vm: *Vm, arguments: []const Value) RuntimeError!Value { const old_stack_top = vm.stack.top; errdefer vm.stack.top = old_stack_top; for (arguments) |argument| try vm.stack.push(argument); if (arguments.len > coral.math.max_int(Value.Integer)) return error.IllegalOperation; try vm.stack.push(.{.integer = @intCast(Value.Integer, arguments.len)}); { var cursor = @as(usize, 0); while (chunk.fetch_opcode(&cursor)) |code| switch (code) { .push_nil => try vm.stack.push(.nil), .push_true => try vm.stack.push(.true), .push_false => try vm.stack.push(.false), .push_zero => try vm.stack.push(.{.integer = 0}), .push_integer => try vm.stack.push(.{ .integer = @bitCast(Value.Integer, chunk.fetch_operand(&cursor) orelse { return error.IllegalOperation; }) }), .push_float => try vm.stack.push(.{.float = @bitCast(Value.Float, chunk.fetch_operand(&cursor) orelse { return error.IllegalOperation; })}), .push_string => { const constant = chunk.fetch_constant(&cursor) orelse { return error.IllegalOperation; }; if (constant.* != .string) return error.IllegalOperation; // TODO: Implement string behavior. try vm.stack.push(.{.object = try vm.new(.{.string = constant.string}, .{})}); }, .push_array => { const element_count = @bitCast(Value.Integer, chunk.fetch_operand(&cursor) orelse return error.IllegalOperation); const array_object = try vm.new_array(element_count); { var element_index = Value{.integer = 0}; var array_start = @intCast(Value.Integer, vm.stack.top) - element_count; while (element_index.integer < element_count) : (element_index.integer += 1) { try vm.set(array_object, element_index, vm.stack.values[ @intCast(usize, array_start + element_index.integer)]); } vm.stack.top = @intCast(u32, array_start); } }, .push_table => { const field_count = chunk.fetch_operand(&cursor) orelse return error.IllegalOperation; if (field_count > coral.math.max_int(Value.Integer)) return error.OutOfMemory; const table_object = try vm.new_table(); { var field_index = @as(bytecode.Operand, 0); while (field_index < field_count) : (field_index += 1) { // Assigned to temporaries to explicitly preserve stack popping order. const field_key = vm.stack.pop() orelse return error.IllegalOperation; const field_value = vm.stack.pop() orelse return error.IllegalOperation; try vm.set(table_object, field_key, field_value); } } try vm.stack.push(.{.object = table_object}); }, .get_local => { try vm.stack.push(vm.stack.values[ vm.stack.top - (chunk.fetch_byte(&cursor) orelse return error.IllegalOperation)]); }, .get_global => { const field = chunk.fetch_constant(&cursor) orelse return error.IllegalOperation; if (field.* != .string) return error.IllegalOperation; try vm.stack.push(vm.get_field(vm.globals(), field.string)); }, .not => { }, // .neg, // .add, // .sub, // .div, // .mul, // .call, // .set, // .get, else => return error.IllegalOperation, }; } const return_value = vm.stack.pop() orelse return error.IllegalOperation; vm.stack.top = coral.math.checked_sub(vm.stack.top, @intCast(u32, arguments.len + 1)) catch |sub_error| { switch (sub_error) { error.IntOverflow => return error.IllegalOperation, } }; return return_value; } pub fn object_argument(_: *Vm, arguments: []const Value, argument_index: usize) RuntimeError!*Object { // TODO: Record error message in Vm. if (argument_index >= arguments.len) return error.IllegalOperation; const argument = arguments[argument_index]; if (argument != .object) return error.IllegalOperation; return argument.object; }