441 lines
11 KiB
Zig
441 lines
11 KiB
Zig
const Chunk = @import("./Chunk.zig");
|
|
|
|
const coral = @import("coral");
|
|
|
|
const file = @import("../file.zig");
|
|
|
|
const types = @import("./types.zig");
|
|
|
|
const tokens = @import("./tokens.zig");
|
|
|
|
allocator: coral.io.Allocator,
|
|
heap: ObjectSlab,
|
|
global_object: types.Object,
|
|
interned: InternTable,
|
|
reporter: Reporter,
|
|
values: ValueStack,
|
|
calls: CallStack,
|
|
|
|
const CallStack = coral.list.Stack(struct {
|
|
ip: usize,
|
|
slots: []types.Val,
|
|
});
|
|
|
|
pub const DataSource = struct {
|
|
name: []const u8,
|
|
data: []const u8,
|
|
};
|
|
|
|
pub const ExecuteFileError = file.System.OpenError || coral.io.StreamError || file.ReadError || types.RuntimeError;
|
|
|
|
pub const InitOptions = struct {
|
|
values_max: u32,
|
|
calls_max: u32,
|
|
reporter: Reporter,
|
|
};
|
|
|
|
const InternTable = coral.table.Hashed([]const u8, types.Object, coral.table.string_keyer);
|
|
|
|
const Object = struct {
|
|
ref_count: usize,
|
|
|
|
state: struct {
|
|
info: ObjectInfo,
|
|
userdata: []u8,
|
|
fields: Fields,
|
|
},
|
|
|
|
const Fields = coral.table.Hashed(*Object, types.Val, .{
|
|
.hasher = struct {
|
|
fn hash(object: *Object) coral.table.Hash {
|
|
coral.debug.assert(object.state.info.identity == null);
|
|
|
|
return coral.table.hash_string(object.state.userdata);
|
|
}
|
|
}.hash,
|
|
|
|
.comparer = struct {
|
|
fn compare(object_a: *Object, object_b: *Object) isize {
|
|
coral.debug.assert(object_a.state.info.identity == null);
|
|
coral.debug.assert(object_b.state.info.identity == null);
|
|
|
|
return coral.io.compare(object_a.state.userdata, object_b.state.userdata);
|
|
}
|
|
}.compare,
|
|
});
|
|
|
|
pub fn acquire(self: *Object) void {
|
|
coral.debug.assert(self.ref_count != 0);
|
|
|
|
self.ref_count += 1;
|
|
}
|
|
|
|
pub fn release(self: *Object, env: *Self) bool {
|
|
coral.debug.assert(self.ref_count != 0);
|
|
|
|
self.ref_count -= 1;
|
|
|
|
if (self.ref_count == 0) {
|
|
coral.io.deallocate(env.allocator, self.state.userdata);
|
|
self.state.fields.deinit(env.allocator);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
pub const ObjectInfo = struct {
|
|
caller: *const Caller = default_call,
|
|
deinitializer: *const Deinitializer = default_deinitialize,
|
|
getter: *const Getter = default_get,
|
|
identity: ?*const anyopaque = null,
|
|
setter: *const Setter = default_set,
|
|
|
|
pub const CallContext = struct {
|
|
env: *Self,
|
|
caller: types.Ref,
|
|
callable: types.Ref,
|
|
args: []const types.Ref,
|
|
};
|
|
|
|
pub const Caller = fn (context: CallContext) types.RuntimeError!types.Val;
|
|
|
|
pub const DeinitializeContext = struct {
|
|
env: *Self,
|
|
obj: types.Ref,
|
|
};
|
|
|
|
pub const Deinitializer = fn (context: DeinitializeContext) void;
|
|
|
|
pub const GetContext = struct {
|
|
env: *Self,
|
|
indexable: types.Ref,
|
|
index: types.Ref,
|
|
};
|
|
|
|
pub const Getter = fn (context: GetContext) types.RuntimeError!types.Val;
|
|
|
|
pub const SetContext = struct {
|
|
env: *Self,
|
|
indexable: types.Ref,
|
|
index: types.Ref,
|
|
value: types.Ref,
|
|
};
|
|
|
|
pub const Setter = fn (context: SetContext) types.RuntimeError!void;
|
|
|
|
fn default_call(context: CallContext) types.RuntimeError!types.Val {
|
|
return context.env.fail("attempt to call non-callable");
|
|
}
|
|
|
|
fn default_deinitialize(_: DeinitializeContext) void {
|
|
// Nothing to deinitialize by default.
|
|
}
|
|
|
|
fn default_get(context: GetContext) types.RuntimeError!types.Val {
|
|
return context.env.get_field(context.indexable, context.index);
|
|
}
|
|
|
|
fn default_set(context: SetContext) types.RuntimeError!void {
|
|
return context.env.fail("attempt to set non-indexable");
|
|
}
|
|
};
|
|
|
|
const ObjectSlab = coral.slab.Map(u32, Object);
|
|
|
|
pub const Reporter = coral.io.Functor(void, []const u8);
|
|
|
|
const Self = @This();
|
|
|
|
const ValueStack = coral.list.Stack(types.Ref);
|
|
|
|
pub fn call(self: *Self, caller: types.Ref, maybe_index: ?types.Ref, args: []const types.Ref) types.RuntimeError!types.Val {
|
|
if (maybe_index) |index| {
|
|
try self.check(caller == .object, "invalid type conversion: object");
|
|
|
|
const callable = try self.get_object(caller, index);
|
|
|
|
defer self.discard(callable);
|
|
try self.check(callable == .object, "invalid type conversion: object");
|
|
|
|
return self.heap.fetch(callable.object).state.info.caller(.{
|
|
.env = self,
|
|
.callable = callable.as_ref(),
|
|
.caller = caller,
|
|
.args = args,
|
|
});
|
|
}
|
|
|
|
return self.heap.fetch(caller.object).state.info.caller(.{
|
|
.env = self,
|
|
.callable = caller,
|
|
.caller = .{.object = self.global_object},
|
|
.args = args,
|
|
});
|
|
}
|
|
|
|
pub fn check(self: *Self, condition: bool, failure_message: []const u8) !void {
|
|
if (condition) {
|
|
return;
|
|
}
|
|
|
|
return self.fail(failure_message);
|
|
}
|
|
|
|
pub fn deinit(self: *Self) void {
|
|
self.values.deinit(self.allocator);
|
|
self.calls.deinit(self.allocator);
|
|
}
|
|
|
|
pub fn discard(self: *Self, val: types.Val) void {
|
|
switch (val) {
|
|
.object => |object| {
|
|
if (!self.heap.fetch(object).release(self)) {
|
|
self.heap.remove(object);
|
|
}
|
|
},
|
|
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
pub fn execute_data(self: *Self, source: DataSource) types.RuntimeError!types.Val {
|
|
const typeid = "<chunk>";
|
|
|
|
const Behaviors = struct {
|
|
fn deinitialize(context: ObjectInfo.DeinitializeContext) void {
|
|
(context.env.native_cast(context.obj, typeid, Chunk) catch unreachable).deinit();
|
|
}
|
|
};
|
|
|
|
var chunk = try Chunk.init(self, source.name);
|
|
|
|
errdefer chunk.deinit();
|
|
|
|
chunk.compile(source.data) catch |compile_error| {
|
|
self.reporter.invoke(chunk.error_details());
|
|
|
|
return compile_error;
|
|
};
|
|
|
|
const script = try self.new_object(coral.io.bytes_of(&chunk), .{
|
|
.identity = typeid,
|
|
.deinitializer = Behaviors.deinitialize,
|
|
});
|
|
|
|
defer self.discard(script);
|
|
|
|
return try self.call(script.as_ref(), null, &.{});
|
|
}
|
|
|
|
pub fn execute_file(self: *Self, fs: file.System, file_path: file.Path) ExecuteFileError!types.Val {
|
|
const readable_file = try fs.open_readable(file_path);
|
|
|
|
defer readable_file.close();
|
|
|
|
const file_size = (try fs.query_info(file_path)).size;
|
|
var file_source = try coral.list.Stack(u8).init(self.allocator, file_size);
|
|
|
|
defer file_source.deinit(self.allocator);
|
|
|
|
{
|
|
var file_buffer = file_source.as_buffer(self.allocator);
|
|
var stream_buffer = [_]u8{0} ** 4096;
|
|
|
|
if ((try coral.io.stream(file_buffer.as_writer(), readable_file.as_reader(), &stream_buffer)) != file_size) {
|
|
return error.ReadFailure;
|
|
}
|
|
}
|
|
|
|
return try self.execute_data(.{
|
|
.name = try file_path.to_string(),
|
|
.data = file_source.values,
|
|
});
|
|
}
|
|
|
|
pub fn fail(self: *Self, failure_message: []const u8) types.CheckError {
|
|
self.reporter.invoke(failure_message);
|
|
|
|
return error.CheckFailed;
|
|
}
|
|
|
|
pub fn get_field(self: *Self, indexable: types.Ref, field: types.Ref) !types.Val {
|
|
try self.check(indexable == .object, "invalid type conversion: object");
|
|
try self.check(field == .object, "invalid type conversion: object");
|
|
|
|
const value = self.heap.fetch(indexable.object).state.fields.lookup(self.heap.fetch(field.object)) orelse {
|
|
return .nil;
|
|
};
|
|
|
|
if (value == .object) {
|
|
self.heap.fetch(value.object).acquire();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
pub fn get_object(self: *Self, indexable: types.Ref, index: types.Ref) types.RuntimeError!types.Val {
|
|
try self.check(indexable == .object, "invalid type conversion: object");
|
|
|
|
return self.heap.fetch(indexable.object).state.info.getter(.{
|
|
.env = self,
|
|
.indexable = indexable,
|
|
.index = index,
|
|
});
|
|
}
|
|
|
|
pub fn init(allocator: coral.io.Allocator, options: InitOptions) !Self {
|
|
var values = try ValueStack.init(allocator, options.values_max * options.calls_max);
|
|
|
|
errdefer values.deinit(allocator);
|
|
|
|
var calls = try CallStack.init(allocator, options.calls_max);
|
|
|
|
errdefer calls.deinit(allocator);
|
|
|
|
var interned = try InternTable.init(allocator);
|
|
|
|
errdefer interned.deinit(allocator);
|
|
|
|
var heap = try ObjectSlab.init(allocator);
|
|
|
|
errdefer heap.deinit(allocator);
|
|
|
|
var environment = Self{
|
|
.global_object = 0,
|
|
.allocator = allocator,
|
|
.reporter = options.reporter,
|
|
.interned = interned,
|
|
.values = values,
|
|
.calls = calls,
|
|
.heap = heap,
|
|
};
|
|
|
|
const globals = try environment.new_object(&.{}, .{
|
|
.identity = "KYM GLOBAL OBJECT OC DO NOT STEAL",
|
|
});
|
|
|
|
coral.debug.assert(globals == .object);
|
|
|
|
environment.global_object = globals.object;
|
|
|
|
return environment;
|
|
}
|
|
|
|
pub fn intern(self: *Self, string: []const u8) coral.io.AllocationError!types.Ref {
|
|
return .{.object = self.interned.lookup(string) orelse {
|
|
const reference = try self.new_string(string);
|
|
|
|
coral.debug.assert(reference == .object);
|
|
coral.debug.assert(try self.interned.insert(self.allocator, string, reference.object));
|
|
|
|
return .{.object = reference.object};
|
|
}};
|
|
}
|
|
|
|
pub fn native_cast(self: *Self, castable: types.Ref, id: *const anyopaque, comptime Type: type) types.RuntimeError!*Type {
|
|
try self.check(castable == .object, "invalid type conversion: object");
|
|
|
|
const object = self.heap.fetch(castable.object);
|
|
const alignment = @alignOf(Type);
|
|
const is_expected_type = (object.state.info.identity == id) and (object.state.userdata.len == alignment);
|
|
|
|
try self.check(is_expected_type, "invalid object cast: native type");
|
|
|
|
return @ptrCast(*Type, @alignCast(alignment, object.state.userdata));
|
|
}
|
|
|
|
pub fn new_array(self: *Self) coral.io.AllocationError!types.Val {
|
|
return try self.new_object(.{
|
|
|
|
});
|
|
}
|
|
|
|
pub fn new_object(self: *Self, userdata: []const u8, info: ObjectInfo) coral.io.AllocationError!types.Val {
|
|
const allocation = try coral.io.allocate_many(u8, userdata.len, self.allocator);
|
|
|
|
errdefer coral.io.deallocate(self.allocator, allocation);
|
|
|
|
coral.io.copy(allocation, userdata);
|
|
|
|
var fields = try Object.Fields.init(self.allocator);
|
|
|
|
errdefer fields.deinit(self.allocator);
|
|
|
|
return .{.object = try self.heap.insert(self.allocator, .{
|
|
.ref_count = 1,
|
|
|
|
.state = .{
|
|
.info = info,
|
|
.userdata = allocation,
|
|
.fields = fields,
|
|
},
|
|
})};
|
|
}
|
|
|
|
pub fn new_string(self: *Self, data: []const u8) coral.io.AllocationError!types.Val {
|
|
const Behavior = struct {
|
|
fn get_byte(context: ObjectInfo.GetContext) types.RuntimeError!types.Val {
|
|
switch (context.index) {
|
|
.integer => |integer| {
|
|
const string = context.env.string_cast(context.indexable) catch unreachable;
|
|
|
|
try context.env.check(coral.math.is_clamped(integer, 0, string.len), "index out of string bounds");
|
|
|
|
return types.Val{.integer = string[@intCast(usize, integer)]};
|
|
},
|
|
|
|
else => return context.env.fail("attempt to index string with non-integer value"),
|
|
}
|
|
}
|
|
};
|
|
|
|
return try self.new_object(data, .{
|
|
.getter = Behavior.get_byte,
|
|
});
|
|
}
|
|
|
|
pub fn set_global(self: *Self, global_name: []const u8, value: types.Ref) coral.io.AllocationError!void {
|
|
try self.globals.assign(self.allocator, global_name, value);
|
|
}
|
|
|
|
pub fn set_object(self: *Self, obj: *Object, index: types.Ref, value: types.Ref) types.RuntimeError!void {
|
|
return obj.behavior.setter(.{
|
|
.env = self,
|
|
.obj = obj,
|
|
.index = index,
|
|
.value = value,
|
|
});
|
|
}
|
|
|
|
pub fn string_cast(self: *Self, value: types.Ref) ![]const u8 {
|
|
try self.check(value == .object, "invalid type conversion: object");
|
|
|
|
const object = self.heap.fetch(value.object);
|
|
|
|
try self.check(object.state.info.identity == null, "invalid object cast: string");
|
|
|
|
return object.state.userdata;
|
|
}
|
|
|
|
pub fn to_integer(self: *Self, value: types.Ref) !types.Integer {
|
|
const fail_message = "invalid type conversion: integer";
|
|
|
|
switch (value) {
|
|
.float => |float| {
|
|
const int = @typeInfo(types.Integer).Int;
|
|
|
|
if (coral.math.is_clamped(float, coral.math.min_int(int), coral.math.max_int(int))) {
|
|
return @floatToInt(types.Integer, float);
|
|
}
|
|
},
|
|
|
|
.integer => |integer| return integer,
|
|
else => {},
|
|
}
|
|
|
|
return self.fail(fail_message);
|
|
}
|