ona/source/kym/kym.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;
}