650 lines
18 KiB
Zig
650 lines
18 KiB
Zig
const Ast = @import("./kym/Ast.zig");
|
|
|
|
const State = @import("./kym/State.zig");
|
|
|
|
const Table = @import("./kym/Table.zig");
|
|
|
|
const coral = @import("coral");
|
|
|
|
const file = @import("./file.zig");
|
|
|
|
const tokens = @import("./kym/tokens.zig");
|
|
|
|
pub const CallContext = struct {
|
|
env: *RuntimeEnv,
|
|
caller: *const RuntimeRef,
|
|
callable: *const RuntimeRef,
|
|
userdata: []u8,
|
|
args: []const *const RuntimeRef = &.{},
|
|
|
|
pub fn arg_at(self: CallContext, index: u8) RuntimeError!*const RuntimeRef {
|
|
if (!coral.math.is_clamped(index, 0, self.args.len - 1)) {
|
|
return self.env.check_fail("argument out of bounds");
|
|
}
|
|
|
|
return self.args[@as(usize, index)];
|
|
}
|
|
};
|
|
|
|
const Compiler = struct {
|
|
state: *State,
|
|
opcodes: OpcodeList,
|
|
|
|
locals: struct {
|
|
buffer: [255][]const coral.io.Byte = [_][]const coral.io.Byte{""} ** 255,
|
|
count: u8 = 0,
|
|
|
|
const Self = @This();
|
|
|
|
fn declare(self: *Self, identifier: []const u8) CompileError!void {
|
|
if (self.count == self.buffer.len) {
|
|
return error.TooManyLocals;
|
|
}
|
|
|
|
self.buffer[self.count] = identifier;
|
|
self.count += 1;
|
|
}
|
|
|
|
fn resolve(self: *Self, local_identifier: []const coral.io.Byte) ?u8 {
|
|
var index = @as(u8, self.count);
|
|
|
|
while (index != 0) {
|
|
index -= 1;
|
|
|
|
if (coral.io.equals(local_identifier, self.buffer[index])) {
|
|
return index;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
},
|
|
|
|
const CompileError = coral.io.AllocationError || error {
|
|
UndefinedLocal,
|
|
TooManyLocals,
|
|
};
|
|
|
|
const LocalsList = coral.list.Stack([]const u8);
|
|
|
|
const OpcodeList = coral.list.Stack(Opcode);
|
|
|
|
fn compile_ast(self: *Compiler, ast: Ast) CompileError!void {
|
|
for (ast.list_statements()) |statement| {
|
|
switch (statement) {
|
|
.return_expression => |return_expression| {
|
|
try self.compile_expression(return_expression);
|
|
},
|
|
|
|
.return_nothing => {
|
|
try self.opcodes.push_one(.push_nil);
|
|
},
|
|
|
|
.set_local => |local| {
|
|
try self.compile_expression(local.expression);
|
|
|
|
if (self.locals.resolve(local.identifier)) |index| {
|
|
try self.opcodes.push_one(.{.set_local = index});
|
|
} else {
|
|
try self.locals.declare(local.identifier);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn compile_expression(self: *Compiler, expression: Ast.Expression) CompileError!void {
|
|
const is_zero = struct {
|
|
fn is_zero(utf8: []const u8) bool {
|
|
return coral.io.equals(utf8, "0") or coral.io.equals(utf8, "0.0");
|
|
}
|
|
}.is_zero;
|
|
|
|
const number_format = coral.utf8.DecimalFormat{
|
|
.delimiter = "_",
|
|
.positive_prefix = .none,
|
|
};
|
|
|
|
switch (expression) {
|
|
.nil_literal => try self.opcodes.push_one(.push_nil),
|
|
.true_literal => try self.opcodes.push_one(.push_true),
|
|
.false_literal => try self.opcodes.push_one(.push_false),
|
|
|
|
.number_literal => |literal| {
|
|
const parsed_number = number_format.parse(literal, State.Float);
|
|
|
|
coral.debug.assert(parsed_number != null);
|
|
|
|
try self.opcodes.push_one(if (is_zero(literal)) .push_zero else .{.push_number = parsed_number.?});
|
|
},
|
|
|
|
.string_literal => |literal| {
|
|
try self.opcodes.push_one(.{
|
|
.push_object = try self.state.acquire_interned(literal, &string_info),
|
|
});
|
|
},
|
|
|
|
.table_literal => |fields| {
|
|
if (fields.values.len > coral.math.max_int(@typeInfo(u32).Int)) {
|
|
return error.OutOfMemory;
|
|
}
|
|
|
|
for (fields.values) |field| {
|
|
try self.compile_expression(field.expression);
|
|
|
|
try self.opcodes.push_one(.{
|
|
.push_object = try self.state.acquire_interned(field.identifier, &string_info),
|
|
});
|
|
}
|
|
|
|
try self.opcodes.push_one(.{.push_table = @intCast(fields.values.len)});
|
|
},
|
|
|
|
.binary_operation => |operation| {
|
|
try self.compile_expression(operation.lhs_expression.*);
|
|
try self.compile_expression(operation.rhs_expression.*);
|
|
|
|
try self.opcodes.push_one(switch (operation.operator) {
|
|
.addition => .add,
|
|
.subtraction => .sub,
|
|
.multiplication => .mul,
|
|
.divsion => .div,
|
|
.greater_equals_comparison => .eql,
|
|
.greater_than_comparison => .cgt,
|
|
.equals_comparison => .cge,
|
|
.less_than_comparison => .clt,
|
|
.less_equals_comparison => .cle,
|
|
});
|
|
},
|
|
|
|
.unary_operation => |operation| {
|
|
try self.compile_expression(operation.expression.*);
|
|
|
|
try self.opcodes.push_one(switch (operation.operator) {
|
|
.boolean_negation => .not,
|
|
.numeric_negation => .neg,
|
|
});
|
|
},
|
|
|
|
.grouped_expression => |grouped_expression| {
|
|
try self.compile_expression(grouped_expression.*);
|
|
},
|
|
|
|
.get_local => |local| {
|
|
try self.opcodes.push_one(.{
|
|
.get_local = self.locals.resolve(local) orelse return error.UndefinedLocal,
|
|
});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn free(self: *Compiler) void {
|
|
for (self.opcodes.values) |opcode| {
|
|
if (opcode == .push_object) {
|
|
self.state.release(opcode.push_object);
|
|
}
|
|
}
|
|
|
|
self.opcodes.free();
|
|
}
|
|
|
|
fn list_opcodes(self: Compiler) []const Opcode {
|
|
return self.opcodes.values;
|
|
}
|
|
|
|
fn make(allocator: coral.io.Allocator, state: *State) Compiler {
|
|
return .{
|
|
.locals = .{},
|
|
.opcodes = OpcodeList.make(allocator),
|
|
.state = state,
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const IndexContext = struct {
|
|
env: *RuntimeEnv,
|
|
indexable: *const RuntimeRef,
|
|
index: *const RuntimeRef,
|
|
userdata: []u8,
|
|
};
|
|
|
|
pub const ObjectInfo = struct {
|
|
call: *const fn (context: CallContext) RuntimeError!*RuntimeRef = default_call,
|
|
clean: *const fn (userdata: []u8) void = default_clean,
|
|
get: *const fn (context: IndexContext) RuntimeError!*RuntimeRef = default_get,
|
|
set: *const fn (context: IndexContext, value: *const RuntimeRef) RuntimeError!void = default_set,
|
|
|
|
fn cast(object_info: *const anyopaque) *const ObjectInfo {
|
|
return @ptrCast(@alignCast(object_info));
|
|
}
|
|
|
|
fn default_call(context: CallContext) RuntimeError!*RuntimeRef {
|
|
return context.env.raise(error.BadOperation, "attempt to call non-callable");
|
|
}
|
|
|
|
fn default_clean(_: []u8) void {
|
|
// Nothing to clean up by default.
|
|
}
|
|
|
|
fn default_get(context: IndexContext) RuntimeError!*RuntimeRef {
|
|
return context.env.raise(error.BadOperation, "attempt to get non-indexable");
|
|
}
|
|
|
|
fn default_set(context: IndexContext, _: *const RuntimeRef) RuntimeError!void {
|
|
return context.env.raise(error.BadOperation, "attempt to set non-indexable");
|
|
}
|
|
};
|
|
|
|
pub const Opcode = union (enum) {
|
|
push_nil,
|
|
push_true,
|
|
push_false,
|
|
push_zero,
|
|
push_number: State.Float,
|
|
push_table: u32,
|
|
push_object: *State.Object,
|
|
|
|
set_local: u8,
|
|
get_local: u8,
|
|
|
|
not,
|
|
neg,
|
|
|
|
add,
|
|
sub,
|
|
mul,
|
|
div,
|
|
|
|
eql,
|
|
cgt,
|
|
clt,
|
|
cge,
|
|
cle,
|
|
};
|
|
|
|
pub const RuntimeEnv = struct {
|
|
allocator: coral.io.Allocator,
|
|
err_writer: coral.io.Writer,
|
|
bound_refs: VariantSlab,
|
|
state: State,
|
|
|
|
pub const Options = struct {
|
|
out_writer: coral.io.Writer = coral.io.null_writer,
|
|
err_writer: coral.io.Writer = coral.io.null_writer,
|
|
};
|
|
|
|
pub const ScriptSource = struct {
|
|
name: []const coral.io.Byte,
|
|
data: []const coral.io.Byte,
|
|
};
|
|
|
|
const VariantSlab = coral.map.Slab(State.Variant);
|
|
|
|
pub fn discard(self: *RuntimeEnv, ref: *RuntimeRef) void {
|
|
coral.debug.assert(self.bound_refs.remove(@intFromPtr(ref)) != null);
|
|
}
|
|
|
|
pub fn execute_chunk(self: *RuntimeEnv, name: []const coral.io.Byte, opcodes: []const Opcode) RuntimeError!*RuntimeRef {
|
|
_ = name;
|
|
|
|
for (opcodes) |opcode| {
|
|
switch (opcode) {
|
|
.push_nil => try self.state.push_value(.nil),
|
|
.push_true => try self.state.push_value(.true),
|
|
.push_false => try self.state.push_value(.false),
|
|
.push_zero => try self.state.push_value(.{.number = 0}),
|
|
.push_number => |number| try self.state.push_value(.{.number = number}),
|
|
|
|
.push_table => |size| {
|
|
var table = Table.make(self.allocator, &self.state);
|
|
|
|
errdefer table.free();
|
|
|
|
{
|
|
var popped = @as(usize, 0);
|
|
|
|
while (popped < size) : (popped += 1) {
|
|
try table.set_field(
|
|
try to_object(self, try self.state.pop_value()),
|
|
try self.state.pop_value());
|
|
}
|
|
}
|
|
|
|
const table_object = try self.state.acquire_new(coral.io.bytes_of(&table), &table_info);
|
|
|
|
errdefer self.state.release(table_object);
|
|
|
|
try self.state.push_value(.{.object = table_object});
|
|
},
|
|
|
|
.push_object => |object| {
|
|
const acquired_object = self.state.acquire_instance(object);
|
|
|
|
errdefer self.state.release(acquired_object);
|
|
|
|
try self.state.push_value(.{.object = acquired_object});
|
|
},
|
|
|
|
.set_local => |local| {
|
|
if (!self.state.set_value(local, try self.state.pop_value())) {
|
|
return self.raise(error.BadOperation, "invalid local set");
|
|
}
|
|
},
|
|
|
|
.get_local => |local| {
|
|
try self.state.push_value(self.state.get_value(local));
|
|
},
|
|
|
|
.not => {
|
|
try self.state.push_value(switch (try self.state.pop_value()) {
|
|
.nil => return self.raise(error.BadOperation, "cannot convert nil to true or false"),
|
|
.false => .true,
|
|
.true => .false,
|
|
.number => return self.raise(error.BadOperation, "cannot convert a number to true or false"),
|
|
.object => return self.raise(error.BadOperation, "cannot convert an object to true or false"),
|
|
});
|
|
},
|
|
|
|
.neg => {
|
|
try self.state.push_value(.{.number = -(try to_number(self, try self.state.pop_value()))});
|
|
},
|
|
|
|
.add => {
|
|
const lhs_number = try to_number(self, try self.state.pop_value());
|
|
const rhs_number = try to_number(self, try self.state.pop_value());
|
|
|
|
try self.state.push_value(.{.number = lhs_number + rhs_number});
|
|
},
|
|
|
|
.sub => {
|
|
const lhs_number = try to_number(self, try self.state.pop_value());
|
|
const rhs_number = try to_number(self, try self.state.pop_value());
|
|
|
|
try self.state.push_value(.{.number = lhs_number - rhs_number});
|
|
},
|
|
|
|
.mul => {
|
|
const lhs_number = try to_number(self, try self.state.pop_value());
|
|
const rhs_number = try to_number(self, try self.state.pop_value());
|
|
|
|
try self.state.push_value(.{.number = lhs_number * rhs_number});
|
|
},
|
|
|
|
.div => {
|
|
const lhs_number = try to_number(self, try self.state.pop_value());
|
|
const rhs_number = try to_number(self, try self.state.pop_value());
|
|
|
|
try self.state.push_value(.{.number = lhs_number / rhs_number});
|
|
},
|
|
|
|
.eql => {
|
|
const lhs = try self.state.pop_value();
|
|
const rhs = try self.state.pop_value();
|
|
|
|
try self.state.push_value(if (lhs.equals(rhs)) .true else .false);
|
|
},
|
|
|
|
.cgt => {
|
|
const lhs_number = try to_number(self, try self.state.pop_value());
|
|
const rhs_number = try to_number(self, try self.state.pop_value());
|
|
|
|
try self.state.push_value(if (lhs_number > rhs_number) .true else .false);
|
|
},
|
|
|
|
.clt => {
|
|
const lhs_number = try to_number(self, try self.state.pop_value());
|
|
const rhs_number = try to_number(self, try self.state.pop_value());
|
|
|
|
try self.state.push_value(if (lhs_number < rhs_number) .true else .false);
|
|
},
|
|
|
|
.cge => {
|
|
const lhs_number = try to_number(self, try self.state.pop_value());
|
|
const rhs_number = try to_number(self, try self.state.pop_value());
|
|
|
|
try self.state.push_value(if (lhs_number >= rhs_number) .true else .false);
|
|
},
|
|
|
|
.cle => {
|
|
const lhs_number = try to_number(self, try self.state.pop_value());
|
|
const rhs_number = try to_number(self, try self.state.pop_value());
|
|
|
|
try self.state.push_value(if (lhs_number <= rhs_number) .true else .false);
|
|
},
|
|
}
|
|
}
|
|
|
|
const return_value = try self.state.pop_value();
|
|
|
|
errdefer if (return_value == .object) {
|
|
self.state.release(return_value.object);
|
|
};
|
|
|
|
return @ptrFromInt(try self.bound_refs.insert(return_value));
|
|
}
|
|
|
|
pub fn execute_file(self: *RuntimeEnv, file_access: file.Access, file_path: file.Path) RuntimeError!*RuntimeRef {
|
|
const error_message = "failed to load file";
|
|
|
|
const file_data = (try file.allocate_and_load(self.allocator, file_access, file_path)) orelse {
|
|
return self.raise(error.SystemFailure, error_message);
|
|
};
|
|
|
|
defer self.allocator.deallocate(file_data);
|
|
|
|
return self.execute_script(.{
|
|
.name = file_path.to_string() orelse return self.raise(error.SystemFailure, error_message),
|
|
.data = file_data,
|
|
});
|
|
}
|
|
|
|
pub fn execute_script(self: *RuntimeEnv, source: ScriptSource) RuntimeError!*RuntimeRef {
|
|
var ast = Ast.make(self.allocator);
|
|
|
|
defer ast.free();
|
|
|
|
{
|
|
var tokenizer = tokens.Tokenizer{.source = source.data};
|
|
|
|
ast.parse(&tokenizer) catch |parse_error| switch (parse_error) {
|
|
error.BadSyntax => return self.raise(error.BadSyntax, ast.error_message),
|
|
error.OutOfMemory => return error.OutOfMemory,
|
|
};
|
|
}
|
|
|
|
var compiler = Compiler.make(self.allocator, &self.state);
|
|
|
|
defer compiler.free();
|
|
|
|
compiler.compile_ast(ast) catch |compile_error| return switch (compile_error) {
|
|
error.OutOfMemory => error.OutOfMemory,
|
|
error.UndefinedLocal => self.raise(error.BadOperation, "use of undefined local"),
|
|
error.TooManyLocals => self.raise(error.OutOfMemory, "functions cannot contain more than 255 locals"),
|
|
};
|
|
|
|
return self.execute_chunk(source.name, compiler.list_opcodes());
|
|
}
|
|
|
|
pub fn free(self: *RuntimeEnv) void {
|
|
self.bound_refs.free();
|
|
self.state.free();
|
|
}
|
|
|
|
pub fn get_field(self: *RuntimeEnv, indexable: *const RuntimeRef, field: []const u8) RuntimeError!*RuntimeRef {
|
|
const interned_field = try self.intern(field);
|
|
|
|
defer self.discard(interned_field);
|
|
|
|
const indexable_object = try to_object(self, try indexable.fetch(self));
|
|
|
|
return ObjectInfo.cast(indexable_object.userinfo).get(.{
|
|
.env = self,
|
|
.indexable = indexable,
|
|
.index = interned_field,
|
|
.userdata = indexable_object.userdata,
|
|
});
|
|
}
|
|
|
|
pub fn get_float(self: *RuntimeEnv, ref: *const RuntimeRef) RuntimeError!State.Float {
|
|
return to_number(self, try ref.fetch(self));
|
|
}
|
|
|
|
pub fn get_string(self: *RuntimeEnv, ref: *const RuntimeRef) RuntimeError![]const u8 {
|
|
const object = try to_object(self, try ref.fetch(self));
|
|
|
|
if (ObjectInfo.cast(object.userinfo) != &string_info) {
|
|
return self.raise(error.BadOperation, "object is not a string");
|
|
}
|
|
|
|
return object.userdata;
|
|
}
|
|
|
|
pub fn intern(self: *RuntimeEnv, data: []const u8) RuntimeError!*RuntimeRef {
|
|
const data_object = try self.state.acquire_interned(data, &string_info);
|
|
|
|
errdefer self.state.release(data_object);
|
|
|
|
return @ptrFromInt(try self.bound_refs.insert(.{.object = data_object}));
|
|
}
|
|
|
|
pub fn make(allocator: coral.io.Allocator, options: Options) RuntimeError!RuntimeEnv {
|
|
var env = RuntimeEnv{
|
|
.allocator = allocator,
|
|
.bound_refs = VariantSlab.make(allocator),
|
|
.state = State.make(allocator),
|
|
.err_writer = options.err_writer,
|
|
};
|
|
|
|
return env;
|
|
}
|
|
|
|
pub fn new_object(self: *RuntimeEnv, userdata: []const u8, info: *const ObjectInfo) RuntimeError!*RuntimeRef {
|
|
const data_object = try self.state.acquire_new(userdata, info);
|
|
|
|
defer self.state.release(data_object);
|
|
|
|
return @ptrFromInt(try self.bound_refs.insert(.{.object = data_object}));
|
|
}
|
|
|
|
pub fn raise(self: *RuntimeEnv, runtime_error: RuntimeError, error_message: []const u8) RuntimeError {
|
|
// TODO: Print stack trace from state.
|
|
coral.utf8.print_formatted(self.err_writer, "{name}@{line}: {message}", .{
|
|
.name = "???",
|
|
.line = @as(u64, 0),
|
|
.message = error_message,
|
|
}) catch return error.SystemFailure;
|
|
|
|
return runtime_error;
|
|
}
|
|
};
|
|
|
|
pub const RuntimeError = coral.io.AllocationError || State.PopError || error {
|
|
BadSyntax,
|
|
BadOperation,
|
|
SystemFailure,
|
|
};
|
|
|
|
pub const RuntimeRef = opaque {
|
|
fn fetch(self: *const RuntimeRef, env: *RuntimeEnv) RuntimeError!State.Variant {
|
|
return env.bound_refs.lookup(@intFromPtr(self)) orelse env.raise(error.BadOperation, "stale ref");
|
|
}
|
|
};
|
|
|
|
fn table_clean(userdata: []u8) void {
|
|
@as(*Table, @ptrCast(@alignCast(userdata.ptr))).free();
|
|
}
|
|
|
|
fn table_get(context: IndexContext) RuntimeError!*RuntimeRef {
|
|
const table = @as(*Table, @ptrCast(@alignCast(context.userdata.ptr)));
|
|
|
|
switch (try context.index.fetch(context.env)) {
|
|
.nil => return context.env.raise(error.BadOperation, "cannot index a table with nil"),
|
|
.true => return context.env.raise(error.BadOperation, "cannot index a table with true"),
|
|
.false => return context.env.raise(error.BadOperation, "cannot index a table with false"),
|
|
|
|
.object => |index_object| {
|
|
const value = table.get_field(index_object);
|
|
|
|
errdefer if (value == .object) {
|
|
context.env.state.release(value.object);
|
|
};
|
|
|
|
return @ptrFromInt(try context.env.bound_refs.insert(value));
|
|
},
|
|
|
|
.number => |index_number| {
|
|
const value = table.get_index(@intFromFloat(index_number));
|
|
|
|
errdefer if (value == .object) {
|
|
context.env.state.release(value.object);
|
|
};
|
|
|
|
return @ptrFromInt(try context.env.bound_refs.insert(value));
|
|
},
|
|
}
|
|
}
|
|
|
|
const table_info = ObjectInfo{
|
|
.clean = table_clean,
|
|
.get = table_get,
|
|
.set = table_set,
|
|
};
|
|
|
|
fn table_set(context: IndexContext, value: *const RuntimeRef) RuntimeError!void {
|
|
const table = @as(*Table, @ptrCast(@alignCast(context.userdata.ptr)));
|
|
|
|
switch (try context.index.fetch(context.env)) {
|
|
.nil => return context.env.raise(error.BadOperation, "cannot index a table with nil"),
|
|
.true => return context.env.raise(error.BadOperation, "cannot index a table with true"),
|
|
.false => return context.env.raise(error.BadOperation, "cannot index a table with false"),
|
|
|
|
.object => |index_object| {
|
|
const fetched_value = try value.fetch(context.env);
|
|
|
|
if (fetched_value == .object) {
|
|
try table.set_field(index_object, .{
|
|
.object = context.env.state.acquire_instance(fetched_value.object),
|
|
});
|
|
} else {
|
|
try table.set_field(index_object, fetched_value);
|
|
}
|
|
},
|
|
|
|
.number => |index_number| {
|
|
const fetched_value = try value.fetch(context.env);
|
|
|
|
if (fetched_value == .object) {
|
|
try table.set_index(@intFromFloat(index_number), .{
|
|
.object = context.env.state.acquire_instance(fetched_value.object),
|
|
});
|
|
} else {
|
|
try table.set_index(@intFromFloat(index_number), fetched_value);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
fn to_number(env: *RuntimeEnv, variant: State.Variant) RuntimeError!State.Float {
|
|
return switch (variant) {
|
|
.nil => env.raise(error.BadOperation, "cannot convert nil to number"),
|
|
.true => env.raise(error.BadOperation, "cannot convert true to number"),
|
|
.false => env.raise(error.BadOperation, "cannot convert false to number"),
|
|
.number => |number| number,
|
|
.object => env.raise(error.BadOperation, "cannot convert object to number"),
|
|
};
|
|
}
|
|
|
|
fn to_object(env: *RuntimeEnv, variant: State.Variant) RuntimeError!*State.Object {
|
|
return switch (variant) {
|
|
.nil => env.raise(error.BadOperation, "cannot convert nil to object"),
|
|
.true => env.raise(error.BadOperation, "cannot convert true to object"),
|
|
.false => env.raise(error.BadOperation, "cannot convert false to object"),
|
|
.number => env.raise(error.BadOperation, "cannot convert number to object"),
|
|
.object => |object| object,
|
|
};
|
|
}
|
|
|
|
const string_info = ObjectInfo{
|
|
|
|
};
|