464 lines
12 KiB
Zig
464 lines
12 KiB
Zig
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 = error {
|
|
BadSyntax,
|
|
OutOfMemory,
|
|
};
|
|
|
|
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;
|
|
}
|