Implement Bytecode Executor for Kym #19

Merged
kayomn merged 9 commits from kym-bytecode-executor into main 2023-07-12 02:58:03 +02:00
11 changed files with 702 additions and 946 deletions
Showing only changes of commit 7604594630 - Show all commits

View File

@ -7,10 +7,10 @@ const list = @import("./list.zig");
const math = @import("./math.zig");
pub const Stacking = struct {
base_allocator: io.Allocator,
page_allocator: io.Allocator,
min_page_size: usize,
allocations: list.Stack(usize) = .{},
pages: list.Stack(Page) = .{},
allocations: list.Stack(usize),
pages: list.Stack(Page),
const Page = struct {
buffer: []u8,
@ -49,11 +49,11 @@ pub const Stacking = struct {
}
fn allocate_page(self: *Stacking, page_size: usize) io.AllocationError!*Page {
var buffer = try io.allocate_many(self.base_allocator, page_size, u8);
var buffer = try io.allocate_many(self.page_allocator, page_size, u8);
errdefer io.deallocate(self.base_allocator, buffer);
errdefer io.deallocate(self.page_allocator, buffer);
try self.pages.push_one(self.base_allocator, .{
try self.pages.push_one(.{
.buffer = buffer,
.used = 0,
});
@ -83,15 +83,6 @@ pub const Stacking = struct {
}.reallocate);
}
pub fn clear_allocations(self: *Stacking) void {
for (self.pages.values) |page| {
io.deallocate(self.base_allocator, page.buffer);
}
self.pages.deinit(self.base_allocator);
self.allocations.deinit(self.base_allocator);
}
fn current_page(self: Stacking) ?*Page {
if (self.pages.values.len == 0) {
return null;
@ -99,4 +90,22 @@ pub const Stacking = struct {
return &self.pages.values[self.pages.values.len - 1];
}
pub fn deinit(self: *Stacking) void {
for (self.pages.values) |page| {
io.deallocate(self.page_allocator, page.buffer);
}
self.pages.deinit();
self.allocations.deinit();
}
pub fn init(allocator: io.Allocator, min_page_size: usize) io.AllocationError!Stacking {
return Stacking{
.page_allocator = allocator,
.allocations = .{.allocator = allocator},
.pages = .{.allocator = allocator},
.min_page_size = min_page_size,
};
}
};

View File

@ -14,6 +14,11 @@ pub const AllocationOptions = struct {
pub const Allocator = Generator(?[]u8, AllocationOptions);
///
///
///
pub const Byte = u8;
///
/// Function pointer coupled with an immutable state context for providing dynamic dispatch over a given `Input` and
/// `Output`.
@ -134,7 +139,7 @@ pub const FixedBuffer = struct {
}
};
pub const Writer = Generator(?usize, []const u8);
pub const Writer = Generator(?usize, []const Byte);
pub fn allocate_many(allocator: Allocator, amount: usize, comptime Type: type) AllocationError![]Type {
if (@sizeOf(Type) == 0) {

View File

@ -9,6 +9,7 @@ const math = @import("./math.zig");
///
pub fn Stack(comptime Value: type) type {
return struct {
allocator: io.Allocator,
capacity: usize = 0,
values: []Value = &.{},
@ -34,12 +35,12 @@ pub fn Stack(comptime Value: type) type {
/// *Note* if the `capacity` field of `self` is a non-zero value, `allocator` must reference the same allocation
/// strategy as the one originally used to allocate the current internal buffer.
///
pub fn deinit(self: *Self, allocator: io.Allocator) void {
pub fn deinit(self: *Self) void {
if (self.capacity == 0) {
return;
}
io.deallocate(allocator, self.values.ptr[0 .. self.capacity]);
io.deallocate(self.allocator, self.values.ptr[0 .. self.capacity]);
self.values = &.{};
self.capacity = 0;
@ -69,21 +70,18 @@ pub fn Stack(comptime Value: type) type {
/// Growing ahead of multiple push operations is useful when the upper bound of pushes is well-understood, as it
/// can reduce the number of allocations required per push.
///
/// *Note* if the `capacity` field of `self` is a non-zero value, `allocator` must reference the same allocation
/// strategy as the one originally used to allocate the current internal buffer.
///
pub fn grow(self: *Self, allocator: io.Allocator, growth_amount: usize) io.AllocationError!void {
pub fn grow(self: *Self, growth_amount: usize) io.AllocationError!void {
const grown_capacity = self.capacity + growth_amount;
const values = (try io.allocate_many(allocator, grown_capacity, Value))[0 .. self.values.len];
const values = (try io.allocate_many(self.allocator, grown_capacity, Value))[0 .. self.values.len];
errdefer io.deallocate(allocator, values);
errdefer io.deallocate(self.allocator, values);
if (self.capacity != 0) {
for (0 .. self.values.len) |index| {
values[index] = self.values[index];
}
io.deallocate(allocator, self.values.ptr[0 .. self.capacity]);
io.deallocate(self.allocator, self.values.ptr[0 .. self.capacity]);
}
self.values = values;
@ -113,14 +111,11 @@ pub fn Stack(comptime Value: type) type {
/// The function returns [io.AllocationError] if `allocator` could not commit the memory required to grow the
/// internal buffer of `self` when necessary.
///
/// *Note* if the `capacity` field of `self` is a non-zero value, `allocator` must reference the same allocation
/// strategy as the one originally used to allocate the current internal buffer.
///
pub fn push_all(self: *Self, allocator: io.Allocator, values: []const Value) io.AllocationError!void {
pub fn push_all(self: *Self, values: []const Value) io.AllocationError!void {
const new_length = self.values.len + values.len;
if (new_length > self.capacity) {
try self.grow(allocator, values.len + values.len);
try self.grow(values.len + values.len);
}
const offset_index = self.values.len;
@ -139,14 +134,11 @@ pub fn Stack(comptime Value: type) type {
/// The function returns [io.AllocationError] if `allocator` could not commit the memory required to grow the
/// internal buffer of `self` when necessary.
///
/// *Note* if the `capacity` field of `self` is a non-zero value, `allocator` must reference the same allocation
/// strategy as the one originally used to allocate the current internal buffer.
///
pub fn push_many(self: *Self, allocator: io.Allocator, value: Value, amount: usize) io.AllocationError!void {
pub fn push_many(self: *Self, value: Value, amount: usize) io.AllocationError!void {
const new_length = self.values.len + amount;
if (new_length >= self.capacity) {
try self.grow(allocator, amount + amount);
try self.grow(amount + amount);
}
const offset_index = self.values.len;
@ -165,12 +157,9 @@ pub fn Stack(comptime Value: type) type {
/// The function returns [io.AllocationError] if `allocator` could not commit the memory required to grow the
/// internal buffer of `self` when necessary.
///
/// *Note* if the `capacity` field of `self` is a non-zero value, `allocator` must reference the same allocation
/// strategy as the one originally used to allocate the current internal buffer.
///
pub fn push_one(self: *Self, allocator: io.Allocator, value: Value) io.AllocationError!void {
pub fn push_one(self: *Self, value: Value) io.AllocationError!void {
if (self.values.len == self.capacity) {
try self.grow(allocator, math.max(1, self.capacity));
try self.grow(math.max(1, self.capacity));
}
const offset_index = self.values.len;
@ -183,47 +172,19 @@ pub fn Stack(comptime Value: type) type {
}
///
/// Bridge context between a list type implement as part of the list module and an allocator, allowing the list resource
/// referenced by the [Writable] instance to be written to directly or virtually via the [io.Writer] interface.
///
/// *Note* if the given list contains an existing allocation, the provided [io.Allocator] instance must reference the
/// same allocation strategy as the one originally used to allocate the list type memory.
///
pub const Writable = struct {
allocator: io.Allocator,
pub const ByteStack = Stack(io.Byte);
list: union (enum) {
stack: *ByteStack,
},
///
/// Returns a [io.Writer] instance that binds a reference of `self` to the [write] operation.
///
pub fn stack_as_writer(self: *ByteStack) io.Writer {
return io.Writer.bind(ByteStack, self, struct {
fn write(stack: *ByteStack, bytes: []const io.Byte) ?usize {
stack.push_all(bytes) catch return null;
///
/// Stack of bytes.
///
const ByteStack = Stack(u8);
///
/// Returns a [io.Writer] instance that binds a reference of `self` to the [write] operation.
///
pub fn as_writer(self: *Writable) io.Writer {
return io.Writer.bind(Writable, self, struct {
fn write(writable: *Writable, bytes: []const u8) ?usize {
writable.write(bytes) catch return null;
return bytes.len;
}
}.write);
}
///
/// Attempts to call the appropriate multi-element writing function for the current list referenced by `self`,
/// passing `bytes` along.
///
/// The function returns [io.AllocationError] if `allocator` could not commit the memory by the list implementation
/// referenced by `self`. See the specific implementation details of the respective list type for more information.
///
pub fn write(self: *Writable, bytes: []const u8) io.AllocationError!void {
return switch (self.list) {
.stack => |stack| stack.push_all(self.allocator, bytes),
};
}
};
return bytes.len;
}
}.write);
}

View File

@ -19,6 +19,7 @@ const std = @import("std");
///
pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type {
return struct {
allocator: io.Allocator,
free_index: Index = 0,
count: Index = 0,
table: []Entry = &.{},
@ -73,12 +74,12 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type
///
/// Fetches the value referenced by `index` in `self`, returning it.
///
pub fn fetch(self: *Self, index: Index) Value {
pub fn fetch(self: *Self, index: Index) *Value {
const entry = &self.table[index];
debug.assert(entry.* == .value);
return entry.value;
return &entry.value;
}
///
@ -125,9 +126,9 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type
/// *Note* if the `table` field of `self` is an allocated slice, `allocator` must reference the same allocation
/// strategy as the one originally used to allocate the current table.
///
pub fn insert(self: *Self, allocator: io.Allocator, value: Value) io.AllocationError!Index {
pub fn insert(self: *Self, value: Value) io.AllocationError!Index {
if (self.count == self.table.len) {
try self.grow(allocator, math.max(1, self.count));
try self.grow(self.allocator, math.max(1, self.count));
}
if (self.free_index == self.count) {

View File

@ -22,6 +22,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
const growth_factor = 0.6;
return struct {
allocator: io.Allocator,
count: usize = 0,
table: []?Entry = &.{},
@ -101,10 +102,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
/// The function returns [AllocationError] instead if `allocator` cannot commit the memory required to grow the
/// entry table of `self` when necessary.
///
/// *Note* `allocator` must reference the same allocation strategy as the one originally used to initialize
/// `self`.
///
pub fn assign(self: *Self, allocator: io.Allocator, key: Key, value: Value) io.AllocationError!?Entry {
pub fn assign(self: *Self, key: Key, value: Value) io.AllocationError!?Entry {
if (self.calculate_load_factor() >= load_max) {
const growth_size = @intToFloat(f64, math.max(1, self.table.len)) * growth_factor;
@ -112,7 +110,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
return error.OutOfMemory;
}
try self.rehash(allocator, @floatToInt(usize, growth_size));
try self.rehash(@floatToInt(usize, growth_size));
}
debug.assert(self.table.len > self.count);
@ -174,15 +172,12 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
///
/// To clear all items from the table while preserving the current capacity, see [clear] instead.
///
/// *Note* `allocator` must reference the same allocation strategy as the one originally used to initialize
/// `self`.
///
pub fn deinit(self: *Self, allocator: io.Allocator) void {
pub fn deinit(self: *Self) void {
if (self.table.len == 0) {
return;
}
io.deallocate(allocator, self.table);
io.deallocate(self.allocator, self.table);
self.table = &.{};
self.count = 0;
@ -195,15 +190,12 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
/// The function returns [AllocationError] instead if `allocator` cannot commit the memory required to grow the
/// entry table of `self` when necessary.
///
/// *Note* `allocator` must reference the same allocation strategy as the one originally used to initialize
/// `self`.
///
pub fn insert(self: *Self, allocator: io.Allocator, key: Key, value: Value) io.AllocationError!bool {
pub fn insert(self: *Self, key: Key, value: Value) io.AllocationError!bool {
if (self.calculate_load_factor() >= load_max) {
const growth_amount = @intToFloat(f64, self.table.len) * growth_factor;
const min_size = 1;
try self.rehash(allocator, self.table.len + math.max(min_size, @floatToInt(usize, growth_amount)));
try self.rehash(self.table.len + math.max(min_size, @floatToInt(usize, growth_amount)));
}
debug.assert(self.table.len > self.count);
@ -246,16 +238,13 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
/// greater than `requested_range`, returning [io.AllocationError] if `allocator` cannot commit the memory
/// required for the table capacity size.
///
/// *Note* `allocator` must reference the same allocation strategy as the one originally used to initialize
/// `self`.
///
pub fn rehash(self: *Self, allocator: io.Allocator, requested_range: usize) io.AllocationError!void {
pub fn rehash(self: *Self, requested_range: usize) io.AllocationError!void {
const old_table = self.table;
self.table = try io.allocate_many(allocator, math.max(requested_range, self.count), ?Entry);
self.table = try io.allocate_many(self.allocator, math.max(requested_range, self.count), ?Entry);
errdefer {
io.deallocate(allocator, self.table);
io.deallocate(self.allocator, self.table);
self.table = old_table;
}
@ -272,7 +261,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
}
}
io.deallocate(allocator, old_table);
io.deallocate(self.allocator, old_table);
}
}
};

View File

@ -1,5 +1,541 @@
pub const Environment = @import("./kym/Environment.zig");
const Ast = @import("./kym/Ast.zig");
const coral = @import("coral");
const types = @import("./kym/types.zig");
const file = @import("./file.zig");
const tokens = @import("./kym/tokens.zig");
///
///
///
pub const CallContext = struct {
env: *RuntimeEnv,
obj: Value,
args: []const Value = &.{},
///
///
///
pub fn arg_at(self: CallContext, index: Int) RuntimeError!Value {
if (!coral.math.is_clamped(index, 0, self.args.len - 1)) {
return self.env.check_fail("argument out of bounds");
}
return self.args[@intCast(usize, index)];
}
};
const Chunk = struct {
env: *RuntimeEnv,
opcodes: coral.list.Stack(Opcode),
///
///
///
const Opcode = union (enum) {
push_nil,
push_true,
push_false,
push_zero,
push_number: Float,
push_array: i32,
push_table: i32,
not,
neg,
add,
sub,
mul,
div,
eql,
cgt,
clt,
cge,
cle,
};
pub fn compile_ast(self: *Chunk, ast: Ast) void {
for (ast.list_statements()) |statement| {
switch (statement) {
.return_expression => |return_expression| {
try self.compile_expression(return_expression);
},
.return_nothing => {
try self.emit(.push_nil);
},
}
}
}
pub fn compile_expression(self: *Chunk, expression: Ast.Expression) void {
switch (expression) {
.nil_literal => try self.emit(.push_nil),
.true_literal => try self.emit(.push_true),
.false_literal => try self.emit(.push_false),
.integer_literal => |literal| try self.emit(if (literal == 0) .push_zero else .{.push_integer = literal}),
.float_literal => |literal| try self.emit(if (literal == 0) .push_zero else .{.push_float = literal}),
.string_literal => |literal| try self.emit(.{.push_object = try self.intern(literal)}),
.array_literal => |elements| {
if (elements.values.len > coral.math.max_int(@typeInfo(Int).Int)) {
return error.OutOfMemory;
}
for (elements.values) |element_expression| {
try self.compile_expression(element_expression);
}
try self.emit(.{.push_array = @intCast(Int, elements.values.len)});
},
.table_literal => |fields| {
if (fields.values.len > coral.math.max_int(@typeInfo(Int).Int)) {
return error.OutOfMemory;
}
for (fields.values) |field| {
try self.compile_expression(field.expression);
try self.emit(.{.push_object = try self.intern(field.identifier)});
}
try self.emit(.{.push_table = @intCast(Int, fields.values.len)});
},
.binary_operation => |operation| {
try self.compile_expression(operation.lhs_expression.*);
try self.compile_expression(operation.rhs_expression.*);
try self.emit(switch (operation.operator) {
.addition => .add,
.subtraction => .sub,
.multiplication => .mul,
.divsion => .div,
.greater_equals_comparison => .compare_eq,
.greater_than_comparison => .compare_gt,
.equals_comparison => .compare_ge,
.less_than_comparison => .compare_lt,
.less_equals_comparison => .compare_le,
});
},
.unary_operation => |operation| {
try self.compile_expression(operation.expression.*);
try self.emit(switch (operation.operator) {
.boolean_negation => .not,
.numeric_negation => .neg,
});
},
.grouped_expression => |grouped_expression| {
try self.compile_expression(grouped_expression.*);
}
}
}
pub fn deinit(self: *Chunk) void {
self.opcodes.deinit(self.env.allocator);
}
pub fn execute(self: *Chunk) RuntimeError!Value {
_ = self;
return Value.nil;
}
};
pub const Float = f64;
pub const Int = i32;
pub const Objectid = u32;
pub const RuntimeEnv = struct {
output: coral.io.Writer,
stack: coral.list.Stack(u64),
interned: coral.table.Hashed([]const u8, Objectid, coral.table.string_keyer),
objects: coral.slab.Map(@typeInfo(u32).Int, Object),
user_allocator: coral.io.Allocator,
pub const DataSource = struct {
name: []const u8,
data: []const u8,
};
pub const Limits = struct {
stack_max: u32,
calls_max: u32,
};
const Object = struct {
ref_count: usize,
state: struct {
info: ObjectInfo,
userdata: []u8,
fields: coral.table.Hashed(*Object, *Value, .{
.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 const ObjectInfo = struct {
caller: *const fn (caller: Value, context: CallContext) RuntimeError!Value = default_call,
cleaner: *const fn (userdata: []u8) void = default_clean,
getter: *const fn (context: CallContext) RuntimeError!Value = default_get,
identity: ?*const anyopaque = null,
setter: *const fn (context: CallContext) RuntimeError!void = default_set,
fn default_call(_: Value, context: CallContext) RuntimeError!Value {
return context.env.fail(error.BadOperation, "attempt to call non-callable");
}
fn default_clean(_: []u8) void {
// Nothing to clean up by default.
}
fn default_get(context: CallContext) RuntimeError!Value {
return context.env.fail(error.BadOperation, "attempt to get non-indexable");
}
fn default_set(context: CallContext) RuntimeError!void {
return context.env.fail(error.BadOperation, "attempt to set non-indexable");
}
};
pub fn call(self: *RuntimeEnv, caller: Value, maybe_index: ?Value, args: []const Value) RuntimeError!RuntimeVar {
if (maybe_index) |index| {
const callable = try self.get(caller, index);
defer callable.deinit();
return switch (callable.value.unpack()) {
.objectid => |callable_id| .{
.env = self,
.value = try self.objects.fetch(callable_id).state.info.caller(.{
.env = self,
.callable = callable.value,
.obj = caller,
.args = args,
}),
},
else => self.fail(error.BadOperation, "attempt to call non-object type"),
};
}
return self.bind(try self.objects.fetch(try self.to_objectid(caller)).state.info.caller(.{
.env = self,
.obj = caller,
// .caller = .{.object = self.global_object},
.args = args,
}));
}
pub fn check(
self: *RuntimeEnv,
condition: bool,
runtime_error: RuntimeError,
failure_message: []const u8) RuntimeError!void {
if (condition) {
return;
}
return self.fail(runtime_error, failure_message);
}
pub fn deinit(self: *RuntimeEnv) void {
self.stack.deinit();
}
pub fn execute_data(self: *RuntimeEnv, allocator: coral.io.Allocator, source: DataSource) RuntimeError!RuntimeVar {
var ast = try Ast.init(allocator);
defer ast.deinit();
{
var tokenizer = tokens.Tokenizer{.source = source.data};
try ast.parse(&tokenizer);
}
var chunk = Chunk{
.env = self,
.opcodes = .{.allocator = allocator},
};
const typeid = "<chunk>";
const script = try self.new_object(allocator, coral.io.bytes_of(&chunk), .{
.identity = typeid,
.cleaner = struct {
fn clean(userdata: []const u8) void {
@ptrCast(*Chunk, @alignCast(@alignOf(Chunk), userdata)).deinit();
}
}.clean,
.caller = struct {
fn call(caller: Value, context: CallContext) RuntimeError!Value {
_ = caller;
return (context.env.native_cast(context.obj, typeid, Chunk) catch unreachable).execute();
}
}.call,
});
defer script.deinit();
return try self.call(script.value, null, &.{});
}
pub fn execute_file(
self: *RuntimeEnv,
allocator: coral.io.Allocator,
file_system: file.System,
file_path: file.Path) RuntimeError!RuntimeVar {
const readable_file = file_system.open_readable(file_path) catch return error.SystemFailure;
defer readable_file.close();
var file_data = coral.list.ByteStack{.allocator = allocator};
const file_size = (file_system.query_info(file_path) catch return error.SystemFailure).size;
try file_data.grow(file_size);
defer file_data.deinit();
{
var stream_buffer = [_]u8{0} ** 4096;
if ((coral.io.stream(coral.list.stack_as_writer(&file_data), readable_file.as_reader(), &stream_buffer) catch {
return error.SystemFailure;
}) != file_size) {
return error.SystemFailure;
}
}
return try self.execute_data(allocator, .{
.name = file_path.to_string() catch return error.SystemFailure,
.data = file_data.values,
});
}
pub fn fail(self: *RuntimeEnv, runtime_error: RuntimeError, failure_message: []const u8) RuntimeError {
// TODO: Call stack and line numbers.
coral.utf8.print_formatted(self.output, "{name}@({line}): {message}\n", .{
.name = ".ona",
.line = @as(u64, 0),
.message = failure_message,
}) catch return error.SystemFailure;
return runtime_error;
}
pub fn get(self: *RuntimeEnv, indexable: Value, index: Value) RuntimeError!RuntimeVar {
const indexable_object = self.objects.fetch(switch (indexable.unpack()) {
.objectid => |indexable_id| indexable_id,
else => return self.fail(error.BadOperation, "attempt to index non-indexable type"),
});
return .{
.env = self,
.value = try indexable_object.state.info.getter(.{
.env = self,
.obj = indexable,
.args = &.{index},
}),
};
}
pub fn init(allocator: coral.io.Allocator, output: coral.io.Writer, limits: Limits) RuntimeError!RuntimeEnv {
var env = RuntimeEnv{
.output = output,
.stack = .{.allocator = allocator},
.objects = .{.allocator = allocator},
.interned = .{.allocator = allocator},
.user_allocator = allocator,
};
try env.stack.grow(limits.stack_max * limits.calls_max);
errdefer env.stack.deinit();
return env;
}
pub fn intern(self: *RuntimeEnv, string: []const u8) RuntimeError!Value {
return Value.pack_objectid(self.interned.lookup(string) orelse {
const interned_value = (try self.new_string(string)).value;
switch (interned_value.unpack()) {
.objectid => |id| coral.debug.assert(try self.interned.insert(string, id)),
else => unreachable,
}
return interned_value;
});
}
pub fn native_cast(self: *RuntimeEnv, castable: Value, id: *const anyopaque, comptime Type: type) RuntimeError!*Type {
const object = self.objects.fetch(castable.to_objectid() orelse {
return self.fail(error.BadOperation, "attempt to cast non-castable type");
});
const is_expected_type = (object.state.info.identity == id) and (object.state.userdata.len == @sizeOf(Type));
try self.check(is_expected_type, "invalid object cast: native type");
return @ptrCast(*Type, @alignCast(@alignOf(Type), object.state.userdata));
}
pub fn new_object(self: *RuntimeEnv, allocator: coral.io.Allocator, userdata: []const u8, info: ObjectInfo) RuntimeError!RuntimeVar {
const allocation = try coral.io.allocate_many(allocator, userdata.len, u8);
errdefer coral.io.deallocate(allocator, allocation);
coral.io.copy(allocation, userdata);
const objectid = try self.objects.insert(.{
.ref_count = 1,
.state = .{
.info = info,
.userdata = allocation,
.fields = .{.allocator = allocator},
},
});
return .{
.env = self,
.value = .{.data = @as(usize, 0x7FF8000000000002) | (@as(usize, objectid) << 32)},
};
}
pub fn new_string(self: *RuntimeEnv, data: []const u8) RuntimeError!RuntimeVar {
return try self.new_object(data, .{
.getter = struct {
fn get_byte(context: CallContext) RuntimeError!*Value {
const string = context.env.string_cast(context.obj) catch unreachable;
const index = try context.env.to_int(try context.arg_at(0));
try context.env.check(coral.math.is_clamped(index, 0, string.len), "index out of string bounds");
return context.env.new_int(string[@intCast(usize, index)]);
}
}.get_byte,
});
}
};
pub const RuntimeError = coral.io.AllocationError || Ast.ParseError || error {
BadOperation,
BadArgument,
SystemFailure,
};
pub const RuntimeVar = struct {
value: Value,
env: *RuntimeEnv,
pub fn deinit(self: RuntimeVar) void {
switch (self.value.unpack()) {
.objectid => |id| {
const object = self.env.objects.fetch(id);
coral.debug.assert(object.ref_count != 0);
object.ref_count -= 1;
if (object.ref_count == 0) {
object.state.info.cleaner(object.state.userdata);
// TODO: Free individual key-value pairs of fields
object.state.fields.deinit();
coral.io.deallocate(self.env.user_allocator, object.state.userdata);
self.env.objects.remove(id);
}
},
else => {},
}
}
};
pub const Value = struct {
data: u64,
pub const Unpacked = union (enum) {
nil,
false,
true,
number: Float,
objectid: Objectid,
};
fn pack_number(float: Float) Value {
return @bitCast(Value, float);
}
fn pack_objectid(id: Objectid) Value {
return signature.objectid | id;
}
pub const @"false" = @as(Value, nan | 0x0001000000000000);
const mask = .{
.sign = @as(u64, 0x8000000000000000),
.exponent = @as(u64, 0x7ff0000000000000),
.quiet = @as(u64, 0x0008000000000000),
.type = @as(u64, 0x0007000000000000),
.signature = @as(u64, 0xffff000000000000),
.object = @as(u64, 0x00000000ffffffff),
};
pub const nan = @as(Value, mask.exponent | mask.quiet);
pub const nil = @as(Value, nan | 0x0003000000000000);
const signature = .{
};
pub const @"true" = @as(Value, nan | 0x0002000000000000);
pub fn unpack(self: Value) Unpacked {
if ((~self.data & mask.exponent) != 0) {
return .{.number = @bitCast(Float, self.data)};
}
return switch ((self.data & mask.signature) != 0) {
.signature_nan => .{.number = @bitCast(Float, self.data)},
.signature_false => .false,
.signature_true => .true,
.signature_object => @intCast(Objectid, self.data & mask.object),
else => return .nil,
};
}
};

View File

@ -2,8 +2,6 @@ const coral = @import("coral");
const tokens = @import("./tokens.zig");
const types = @import("./types.zig");
allocator: coral.io.Allocator,
arena: coral.arena.Stacking,
statements: StatementList,
@ -39,8 +37,8 @@ pub const Expression = union (enum) {
nil_literal,
true_literal,
false_literal,
integer_literal: types.Integer,
float_literal: types.Float,
integer_literal: []const u8,
float_literal: []const u8,
string_literal: []const u8,
array_literal: coral.list.Stack(Expression),
@ -63,7 +61,15 @@ pub const Expression = union (enum) {
},
};
const ExpressionParser = fn (self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!Expression;
const ExpressionParser = fn (self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression;
///
///
///
pub const ParseError = error {
OutOfMemory,
BadSyntax,
};
const Self = @This();
@ -81,7 +87,7 @@ const UnaryOperator = enum {
fn binary_operation_parser(comptime parse_next: ExpressionParser, comptime operators: []const BinaryOperator) ExpressionParser {
return struct {
fn parse(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!Expression {
fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression {
var expression = try parse_next(self, tokenizer);
{
@ -111,7 +117,7 @@ fn binary_operation_parser(comptime parse_next: ExpressionParser, comptime opera
}.parse;
}
fn check_syntax(self: *Self, condition: bool, error_message: []const u8) types.ParseError!void {
fn check_syntax(self: *Self, condition: bool, error_message: []const u8) ParseError!void {
if (condition) {
return;
}
@ -120,11 +126,11 @@ fn check_syntax(self: *Self, condition: bool, error_message: []const u8) types.P
}
pub fn deinit(self: *Self) void {
self.arena.clear_allocations();
self.statements.deinit(self.allocator);
self.arena.deinit();
self.statements.deinit();
}
fn fail_syntax(self: *Self, error_message: []const u8) types.ParseError {
fn fail_syntax(self: *Self, error_message: []const u8) ParseError {
self.error_message = error_message;
return error.BadSyntax;
@ -132,13 +138,9 @@ fn fail_syntax(self: *Self, error_message: []const u8) types.ParseError {
pub fn init(allocator: coral.io.Allocator) coral.io.AllocationError!Self {
return Self{
.arena = .{
.base_allocator = allocator,
.min_page_size = 4096,
},
.arena = try coral.arena.Stacking.init(allocator, 4096),
.allocator = allocator,
.statements = .{},
.statements = .{.allocator = allocator},
.error_message = "",
};
}
@ -147,7 +149,7 @@ pub fn list_statements(self: Self) []const Statement {
return self.statements.values;
}
pub fn parse(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!void {
pub fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!void {
self.reset();
errdefer self.reset();
@ -159,7 +161,7 @@ pub fn parse(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!void {
.keyword_return => {
try self.check_syntax(has_not_returned_yet, "cannot return more than once per function scope");
try self.statements.push_one(self.allocator, get_statement: {
try self.statements.push_one(get_statement: {
if (tokenizer.step(.{.include_newlines = true})) {
if (tokenizer.current_token != .newline) {
break: get_statement .{.return_expression = try self.parse_expression(tokenizer)};
@ -199,7 +201,7 @@ const parse_expression = binary_operation_parser(parse_equality, &.{
.subtraction,
});
fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!Expression {
fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression {
switch (tokenizer.current_token) {
.symbol_paren_left => {
try self.check_syntax(tokenizer.step(.{.include_newlines = false}), "expected an expression after `(`");
@ -216,26 +218,13 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!Expr
.integer => |value| {
_ = tokenizer.step(.{.include_newlines = false});
return Expression{
.integer_literal = coral.utf8.parse_decimal(types.Integer, value, .{}) catch |parse_error| {
return self.fail_syntax(switch (parse_error) {
error.BadSyntax => "invalid integer literal",
error.IntOverflow => "integer literal is too big",
});
},
};
return Expression{.integer_literal = value};
},
.real => |value| {
_ = tokenizer.step(.{.include_newlines = false});
return Expression{
.float_literal = coral.utf8.parse_decimal(types.Float, value, .{}) catch |parse_error| {
return self.fail_syntax(switch (parse_error) {
error.BadSyntax => "invalid float literal",
});
},
};
return Expression{.float_literal = value};
},
.string => |value| {
@ -247,14 +236,17 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!Expr
.symbol_bracket_left => {
try self.check_syntax(tokenizer.step(.{.include_newlines = false}), "unexpected end of array literal");
var expression = Expression{.array_literal = .{}};
var expression = Expression{
.array_literal = .{
.allocator = self.arena.as_allocator(),
},
};
coral.debug.assert(expression == .array_literal);
const allocator = self.arena.as_allocator();
const array_average_maximum = 32;
try expression.array_literal.grow(allocator, array_average_maximum);
try expression.array_literal.grow(array_average_maximum);
while (true) {
switch (tokenizer.current_token) {
@ -269,7 +261,7 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!Expr
tokenizer.step(.{.include_newlines = false}),
"expected `]` or expression after `[`");
try expression.array_literal.push_one(allocator, try self.parse_expression(tokenizer));
try expression.array_literal.push_one(try self.parse_expression(tokenizer));
},
}
}
@ -278,12 +270,14 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!Expr
.symbol_brace_left => {
try self.check_syntax(tokenizer.step(.{.include_newlines = false}), "unexpected end of table literal");
var expression = Expression{.table_literal = .{}};
var expression = Expression{
.table_literal = .{
.allocator = self.arena.as_allocator(),
},
};
coral.debug.assert(expression == .table_literal);
const allocator = self.arena.as_allocator();
while (true) {
switch (tokenizer.current_token) {
.symbol_brace_right => {
@ -299,7 +293,7 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) types.ParseError!Expr
try self.check_syntax(tokenizer.step(.{.include_newlines = false}), "unexpected end after `=`");
try expression.table_literal.push_one(allocator, .{
try expression.table_literal.push_one(.{
.identifier = identifier,
.expression = try self.parse_expression(tokenizer),
});
@ -365,5 +359,5 @@ const parse_term = binary_operation_parser(parse_factor, &.{
pub fn reset(self: *Self) void {
self.statements.clear();
self.arena.clear_allocations();
self.arena.deinit();
}

View File

@ -1,247 +0,0 @@
const Ast = @import("./Ast.zig");
const Environment = @import("./Environment.zig");
const coral = @import("coral");
const types = @import("./types.zig");
const tokens = @import("./tokens.zig");
env: *Environment,
message_name_len: usize,
message_data: Buffer,
bytecode_buffer: Buffer,
const Buffer = coral.list.Stack(u8);
const Opcode = enum (u8) {
ret,
push_nil,
push_true,
push_false,
push_zero,
push_integer,
push_float,
push_object,
push_array,
push_table,
not,
neg,
add,
sub,
mul,
div,
compare_eq,
compare_gt,
compare_lt,
compare_ge,
compare_le,
};
const Self = @This();
fn clear_error_details(self: *Self) void {
coral.debug.assert(self.message_data.values.len >= self.message_name_len);
coral.debug.assert(self.message_data.drop(self.message_data.values.len - self.message_name_len));
}
pub fn compile(self: *Self, data: []const u8) types.RuntimeError!void {
var ast = try Ast.init(self.env.allocator);
defer ast.deinit();
{
var tokenizer = tokens.Tokenizer{.source = data};
ast.parse(&tokenizer) catch |init_error| {
if (init_error == error.BadSyntax) {
self.clear_error_details();
var writable_data = coral.list.Writable{
.allocator = self.env.allocator,
.list = .{.stack = &self.message_data},
};
coral.utf8.print_formatted(writable_data.as_writer(), "@({line}): {name}", .{
.line = tokenizer.lines_stepped,
.name = ast.error_message,
}) catch return error.OutOfMemory;
}
return init_error;
};
}
for (ast.list_statements()) |statement| {
switch (statement) {
.return_expression => |return_expression| {
try self.compile_expression(return_expression);
try self.emit_opcode(.ret);
},
.return_nothing => {
try self.emit_opcode(.push_nil);
try self.emit_opcode(.ret);
},
}
}
}
pub fn compile_expression(self: *Self, expression: Ast.Expression) types.RuntimeError!void {
switch (expression) {
.nil_literal => try self.emit_opcode(.push_nil),
.true_literal => try self.emit_opcode(.push_true),
.false_literal => try self.emit_opcode(.push_false),
.integer_literal => |literal| {
if (literal == 0) {
try self.emit_opcode(.push_zero);
} else {
try self.emit_opcode(.push_integer);
try self.emit_float(0);
}
},
.float_literal => |literal| {
if (literal == 0) {
try self.emit_opcode(.push_zero);
} else {
try self.emit_opcode(.push_float);
try self.emit_float(literal);
}
},
.string_literal => |literal| {
try self.emit_opcode(.push_object);
try self.emit_object(try self.intern(literal));
},
.array_literal => |elements| {
if (elements.values.len > coral.math.max_int(@typeInfo(types.Integer).Int)) {
return error.OutOfMemory;
}
for (elements.values) |element_expression| {
try self.compile_expression(element_expression);
}
try self.emit_opcode(.push_array);
try self.emit_integer(@intCast(types.Integer, elements.values.len));
},
.table_literal => |fields| {
if (fields.values.len > coral.math.max_int(@typeInfo(types.Integer).Int)) {
return error.OutOfMemory;
}
for (fields.values) |field| {
try self.compile_expression(field.expression);
try self.emit_opcode(.push_object);
try self.emit_object(try self.intern(field.identifier));
}
try self.emit_opcode(.push_table);
try self.emit_integer(@intCast(types.Integer, fields.values.len));
},
.binary_operation => |operation| {
try self.compile_expression(operation.lhs_expression.*);
try self.compile_expression(operation.rhs_expression.*);
try self.emit_opcode(switch (operation.operator) {
.addition => .add,
.subtraction => .sub,
.multiplication => .mul,
.divsion => .div,
.greater_equals_comparison => .compare_eq,
.greater_than_comparison => .compare_gt,
.equals_comparison => .compare_ge,
.less_than_comparison => .compare_lt,
.less_equals_comparison => .compare_le,
});
},
.unary_operation => |operation| {
try self.compile_expression(operation.expression.*);
try self.emit_opcode(switch (operation.operator) {
.boolean_negation => .not,
.numeric_negation => .neg,
});
},
.grouped_expression => |grouped_expression| {
try self.compile_expression(grouped_expression.*);
}
}
}
pub fn deinit(self: *Self) void {
self.bytecode_buffer.deinit(self.env.allocator);
self.message_data.deinit(self.env.allocator);
self.message_name_len = 0;
}
pub fn emit_float(self: *Self, float: types.Float) coral.io.AllocationError!void {
try self.bytecode_buffer.push_all(self.env.allocator, coral.io.bytes_of(&float));
}
pub fn emit_integer(self: *Self, integer: types.Integer) coral.io.AllocationError!void {
try self.bytecode_buffer.push_all(self.env.allocator, coral.io.bytes_of(&integer));
}
pub fn emit_object(self: *Self, object: types.Object) coral.io.AllocationError!void {
try self.bytecode_buffer.push_all(self.env.allocator, coral.io.bytes_of(&object));
}
pub fn emit_opcode(self: *Self, opcode: Opcode) coral.io.AllocationError!void {
try self.bytecode_buffer.push_one(self.env.allocator, @enumToInt(opcode));
}
pub fn error_details(self: Self) []const u8 {
coral.debug.assert(self.message_data.values.len >= self.message_name_len);
return self.message_data.values;
}
pub fn execute(self: *Self) types.RuntimeError!types.Val {
_ = self;
// TODO: Implement.
return .nil;
}
pub fn init(env: *Environment, chunk_name: []const u8) coral.io.AllocationError!Self {
var message_data = Buffer{};
try message_data.push_all(env.allocator, chunk_name);
errdefer message_data.deinit(env.allocator);
return Self{
.env = env,
.message_data = message_data,
.bytecode_buffer = .{},
.message_name_len = chunk_name.len,
};
}
pub fn intern(self: *Self, string: []const u8) coral.io.AllocationError!types.Object {
const interned_string = try self.env.intern(string);
coral.debug.assert(interned_string == .object);
return interned_string.object;
}
pub fn name(self: Self) []const u8 {
coral.debug.assert(self.message_data.values.len >= self.message_name_len);
return self.message_data.values[0 .. self.message_name_len];
}

View File

@ -1,466 +0,0 @@
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 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(@typeInfo(u32).Int, 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.object_release(self.global_object);
{
var interned_iterable = InternTable.Iterable{.hashed_map = &self.interned};
while (interned_iterable.next()) |entry| {
self.object_release(entry.value);
}
}
self.interned.deinit(self.allocator);
self.values.deinit(self.allocator);
self.calls.deinit(self.allocator);
coral.debug.assert(self.heap.is_empty());
self.heap.deinit(self.allocator);
}
pub fn discard(self: *Self, val: types.Val) void {
switch (val) {
.object => |object| self.object_release(object),
else => {},
}
}
pub fn execute_data(self: *Self, source: DataSource) types.RuntimeError!types.Val {
const typeid = "<chunk>";
const Behaviors = struct {
fn call(context: ObjectInfo.CallContext) types.RuntimeError!types.Val {
return (context.env.native_cast(context.callable, typeid, Chunk) catch unreachable).execute();
}
fn deinitialize(context: ObjectInfo.DeinitializeContext) void {
(context.env.native_cast(context.obj, typeid, Chunk) catch unreachable).deinit();
}
};
var compiled_chunk = init_compiled_chunk: {
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;
};
break: init_compiled_chunk chunk;
};
const script = try self.new_object(coral.io.bytes_of(&compiled_chunk), .{
.identity = typeid,
.deinitializer = Behaviors.deinitialize,
.caller = Behaviors.call,
});
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();
var file_data = coral.list.Stack(u8){};
const file_size = (try fs.query_info(file_path)).size;
try file_data.grow(self.allocator, file_size);
defer file_data.deinit(self.allocator);
{
var writable_data = coral.list.Writable{
.allocator = self.allocator,
.list = .{.stack = &file_data},
};
var stream_buffer = [_]u8{0} ** 4096;
if ((try coral.io.stream(writable_data.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_data.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 = get_value: {
var field_data = self.heap.fetch(field.object);
break: get_value self.heap.fetch(indexable.object).state.fields.lookup(&field_data) orelse {
return .nil;
};
};
if (value == .object) {
var value_data = self.heap.fetch(value.object);
value_data.acquire();
self.heap.assign(value.object, value_data);
}
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 env = Self{
.global_object = 0,
.allocator = allocator,
.reporter = options.reporter,
.interned = .{},
.values = .{},
.calls = .{},
.heap = .{},
};
errdefer {
env.values.deinit(allocator);
env.calls.deinit(allocator);
}
try env.values.grow(allocator, options.values_max * options.calls_max);
try env.calls.grow(allocator, options.calls_max);
{
const globals = try env.new_object(&.{}, .{
.identity = "KYM GLOBAL OBJECT OC DO NOT STEAL",
});
coral.debug.assert(globals == .object);
env.global_object = globals.object;
}
return env;
}
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 is_expected_type = (object.state.info.identity == id) and (object.state.userdata.len == @sizeOf(Type));
try self.check(is_expected_type, "invalid object cast: native type");
return @ptrCast(*Type, @alignCast(@alignOf(Type), 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(self.allocator, userdata.len, u8);
errdefer coral.io.deallocate(self.allocator, allocation);
coral.io.copy(allocation, userdata);
return .{.object = try self.heap.insert(self.allocator, .{
.ref_count = 1,
.state = .{
.info = info,
.userdata = allocation,
.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 object_release(self: *Self, object: types.Object) void {
var data = self.heap.fetch(object);
coral.debug.assert(data.ref_count != 0);
data.ref_count -= 1;
if (data.ref_count == 0) {
data.state.info.deinitializer(.{
.env = self,
.obj = .{.object = object},
});
// TODO: Free individual key-value pairs of fields
data.state.fields.deinit(self.allocator);
coral.io.deallocate(self.allocator, data.state.userdata);
self.heap.remove(object);
} else {
self.heap.assign(object, data);
}
}
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);
}

View File

@ -1,56 +0,0 @@
const coral = @import("coral");
pub const CheckError = error {
CheckFailed
};
pub const Float = f32;
pub const Integer = i32;
pub const Object = u32;
pub const Primitive = enum {
nil,
false,
true,
float,
integer,
object,
};
pub const Ref = union (Primitive) {
nil,
false,
true,
float: Float,
integer: Integer,
object: Object,
};
pub const ParseError = error {
OutOfMemory,
BadSyntax,
};
pub const RuntimeError = CheckError || ParseError;
pub const Val = union (Primitive) {
nil,
false,
true,
float: Float,
integer: Integer,
object: Object,
pub fn as_ref(self: *const Val) Ref {
return switch (self.*) {
.nil => .nil,
.false => .false,
.true => .true,
.float => .{.float = self.float},
.integer => .{.integer = self.integer},
.object => .{.object = self.object},
};
}
};

View File

@ -17,69 +17,99 @@ const AppManifest = struct {
width: u16 = 640,
height: u16 = 480,
pub fn load_script(self: *AppManifest, env: *kym.Environment, fs: file.System, file_path: []const u8) !void {
const manifest = try env.execute_file(fs, file.Path.from(&.{file_path}));
pub fn load_script(self: *AppManifest, env: *kym.RuntimeEnv, fs: file.System, file_path: []const u8) !void {
var manifest = try env.execute_file(heap.allocator, fs, file.Path.from(&.{file_path}));
defer env.discard(manifest);
const manifest_ref = manifest.as_ref();
defer manifest.deinit();
{
const title = try env.get_field(manifest_ref, try env.intern("title"));
var title = try env.get(manifest.value, try env.intern("title"));
defer env.discard(title);
defer title.deinit();
const title_string = try env.string_cast(title.as_ref());
const title_string = try env.string_cast(title.value);
try env.check(title_string.len <= self.title.len, "`title` cannot exceed 255 bytes in length");
coral.io.copy(&self.title, title_string);
}
const u16_int = @typeInfo(u16).Int;
const u16_max = coral.math.max_int(@typeInfo(u16).Int);
{
const width = try env.get_field(manifest_ref, try env.intern("width"));
const width = try env.get(manifest.value, try env.intern("width"));
errdefer env.discard(width);
errdefer width.deinit();
self.width = try coral.math.checked_cast(u16_int, try env.to_integer(width.as_ref()));
if (width.value.as_number()) |value| {
if (value < u16_max) {
self.width = @floatToInt(u16, value);
}
}
}
{
const height = try env.get_field(manifest_ref, try env.intern("height"));
const height = try env.get(manifest.value, try env.intern("height"));
errdefer env.discard(height);
errdefer height.deinit();
self.width = try coral.math.checked_cast(u16_int, try env.to_integer(height.as_ref()));
if (height.value.as_number()) |value| {
if (value < u16_max) {
self.height = @floatToInt(u16, value);
}
}
}
}
};
fn stack_as_log_writer(self: *coral.list.ByteStack) coral.io.Writer {
return coral.io.Writer.bind(coral.list.ByteStack, self, struct {
fn write(stack: *coral.list.ByteStack, bytes: []const coral.io.Byte) ?usize {
var line_written = @as(usize, 0);
for (bytes) |byte| {
if (byte == '\n') {
ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%.*s", stack.values.len, stack.values.ptr);
stack.clear();
line_written = 0;
continue;
}
stack.push_one(byte) catch {
coral.debug.assert(stack.drop(line_written));
return null;
};
line_written += 1;
}
return bytes.len;
}
}.write);
}
pub fn run_app(base_file_system: file.System) void {
defer heap.trace_leaks();
const Logger = struct {
const Self = @This();
var log_buffer = coral.list.ByteStack{.allocator = heap.allocator};
fn log(_: *const Self, message: []const u8) void {
ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%.*s", message.len, message.ptr);
}
};
defer log_buffer.deinit();
var script_environment = kym.Environment.init(heap.allocator, .{
.values_max = 512,
var script_env = kym.RuntimeEnv.init(heap.allocator, stack_as_log_writer(&log_buffer), .{
.stack_max = 512,
.calls_max = 512,
.reporter = kym.Environment.Reporter.bind(Logger, &.{}, Logger.log),
}) catch {
return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "failed to initialize Kym vm\n");
};
defer script_environment.deinit();
defer script_env.deinit();
const app_file_name = "app.ona";
var app_manifest = AppManifest{};
app_manifest.load_script(&script_environment, base_file_system, app_file_name) catch {
app_manifest.load_script(&script_env, base_file_system, app_file_name) catch {
return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "failed to load %s\n", app_file_name);
};