ona/source/ona/kym.zig
kayomn dc674ae2c3
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Add local variables to Kym
2023-07-12 01:51:32 +01:00

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{
};