diff --git a/source/coral/io.zig b/source/coral/io.zig index 2f53b8d..ce7a6b2 100644 --- a/source/coral/io.zig +++ b/source/coral/io.zig @@ -223,6 +223,21 @@ pub fn copy(target: []Byte, source: []const Byte) void { } } +pub fn compare(this: []const Byte, that: []const Byte) isize { + const range = math.min(this.len, that.len); + var index: usize = 0; + + while (index < range) : (index += 1) { + const difference = @as(isize, @intCast(this[index])) - @as(isize, @intCast(that[index])); + + if (difference != 0) { + return difference; + } + } + + return @as(isize, @intCast(this.len)) - @as(isize, @intCast(that.len)); +} + pub fn ends_with(target: []const Byte, match: []const Byte) bool { if (target.len < match.len) { return false; diff --git a/source/coral/list.zig b/source/coral/list.zig index 48bc310..4e040f6 100644 --- a/source/coral/list.zig +++ b/source/coral/list.zig @@ -16,6 +16,16 @@ pub fn Stack(comptime Value: type) type { self.values = self.values[0 .. 0]; } + pub fn drop(self: *Self, amount: usize) bool { + if (amount > self.values.len) { + return false; + } + + self.values = self.values[0 .. self.values.len - amount]; + + return true; + } + pub fn free(self: *Self) void { if (self.capacity == 0) { return; @@ -49,6 +59,28 @@ pub fn Stack(comptime Value: type) type { }; } + pub fn pack(self: *Self) io.AllocationError!void { + const packed_size = self.values.len; + const buffer = try self.allocator.reallocate(null, @sizeOf(Value) * self.values.len); + + io.copy(buffer, io.bytes_of(self.values)); + + if (self.capacity != 0) { + self.allocator.deallocate(self.values.ptr[0 .. self.capacity]); + } + + self.values = @as([*]Value, @ptrCast(@alignCast(buffer)))[0 .. packed_size]; + self.capacity = packed_size; + } + + pub fn peek(self: *Self) ?Value { + if (self.values.len == 0) { + return null; + } + + return &self.values[self.values.len - 1]; + } + pub fn pop(self: *Self) ?Value { if (self.values.len == 0) { return null; diff --git a/source/coral/map.zig b/source/coral/map.zig index 7dbb777..d42b7fc 100644 --- a/source/coral/map.zig +++ b/source/coral/map.zig @@ -55,6 +55,23 @@ pub fn Slab(comptime Value: type) type { } } + pub fn insert_at(self: *Self, key: usize, value: Value) bool { + if (self.entries.values.len < key) { + return false; + } + + const entry = &self.entries.values[key - 1]; + + if (entry.* == .value) { + return false; + } + + self.next_index = entry.next_index; + entry.* = .{.value = value}; + + return true; + } + pub fn make(allocator: io.Allocator) Self { return .{ .next_index = 0, @@ -143,6 +160,25 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra const Self = @This(); + pub fn remove(self: *Self, key: Key) ?Entry { + const hash_max = math.min(math.max_int(@typeInfo(usize).Int), self.entries.len); + var hashed_key = math.wrap(traits.hash(key), math.min_int(@typeInfo(usize).Int), hash_max); + + while (true) { + const entry = &(self.entries[hashed_key] orelse continue); + + if (traits.match(entry.key, key)) { + const original_entry = entry.*; + + self.entries[hashed_key] = null; + + return original_entry; + } + + hashed_key = (hashed_key +% 1) % hash_max; + } + } + pub fn replace(self: *Self, key: Key, value: Value) io.AllocationError!?Entry { try self.rehash(load_max); diff --git a/source/ona/app.zig b/source/ona/app.zig index 22ea8b8..99a2430 100644 --- a/source/ona/app.zig +++ b/source/ona/app.zig @@ -12,39 +12,53 @@ pub const Manifest = struct { height: u16 = 480, tick_rate: f32 = 60.0, - pub fn load(self: *Manifest, env: *kym.RuntimeEnv, file_access: file.Access) kym.RuntimeError!void { - const manifest = try env.execute_file(file_access, file.Path.from(&.{"app.ona"})); + pub fn load(self: *Manifest, env: *kym.RuntimeEnv, global_ref: ?*const kym.RuntimeRef, file_access: file.Access) kym.RuntimeError!void { + var manifest_ref = try env.execute_file(global_ref, file_access, file.Path.from(&.{"app.ona"})); - defer env.discard(manifest); + defer env.discard(&manifest_ref); - const title = try env.get_field(manifest, "title"); + var title_ref = try kym.get_dynamic_field(env, manifest_ref, "title"); - defer env.discard(title); + defer env.discard(&title_ref); - const title_string = try env.get_string(title); + const title_string = switch (env.unbox(title_ref)) { + .string => |string| string, + else => "", + }; const width = @as(u16, get: { - const ref = try env.get_field(manifest, "width"); + var ref = try kym.get_dynamic_field(env, manifest_ref, "width"); - defer env.discard(ref); + defer env.discard(&ref); - break: get @intFromFloat(env.get_float(ref) catch @as(f64, @floatFromInt(self.width))); + // TODO: Add safety-checks to int cast. + break: get switch (env.unbox(ref)) { + .number => |number| @intFromFloat(number), + else => self.width, + }; }); const height = @as(u16, get: { - const ref = try env.get_field(manifest, "height"); + var ref = try kym.get_dynamic_field(env, manifest_ref, "height"); - defer env.discard(ref); + defer env.discard(&ref); - break: get @intFromFloat(env.get_float(ref) catch @as(f64, @floatFromInt(self.height))); + // TODO: Add safety-checks to int cast. + break: get switch (env.unbox(ref)) { + .number => |number| @intFromFloat(number), + else => self.height, + }; }); const tick_rate = @as(f32, get: { - const ref = try env.get_field(manifest, "tick_rate"); + var ref = try kym.get_dynamic_field(env, manifest_ref, "tick_rate"); - defer env.discard(ref); + defer env.discard(&ref); - break: get @floatCast(env.get_float(ref) catch self.tick_rate); + break: get switch (env.unbox(ref)) { + .number => |number| @floatCast(number), + else => self.tick_rate, + }; }); { diff --git a/source/ona/kym.zig b/source/ona/kym.zig index e535e49..6c1d38c 100644 --- a/source/ona/kym.zig +++ b/source/ona/kym.zig @@ -1,6 +1,6 @@ const Ast = @import("./kym/Ast.zig"); -const State = @import("./kym/State.zig"); +const Chunk = @import("./kym/Chunk.zig"); const Table = @import("./kym/Table.zig"); @@ -12,10 +12,9 @@ const tokens = @import("./kym/tokens.zig"); pub const CallContext = struct { env: *RuntimeEnv, - caller: *const RuntimeRef, - callable: *const RuntimeRef, + caller: ?*const RuntimeRef, userdata: []u8, - args: []const *const RuntimeRef = &.{}, + 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)) { @@ -26,408 +25,106 @@ pub const CallContext = struct { } }; -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 { - if (self.count == 0) { - return null; - } - - var index = @as(u8, self.count - 1); - - while (true) : (index -= 1) { - if (coral.io.equals(local_identifier, self.buffer[index])) { - return index; - } - - if (index == 0) { - 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 DynamicObject = struct { + userdata: []coral.io.Byte, + typeinfo: *const Typeinfo, }; +pub const Float = f64; + 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, + userdata: []coral.io.Byte, + index_ref: ?*const RuntimeRef, }; pub const RuntimeEnv = struct { allocator: coral.io.Allocator, err_writer: coral.io.Writer, - bound_refs: VariantSlab, - state: State, + local_refs: RefStack, + frames: FrameStack, + ref_values: RefSlab, + + const FrameStack = coral.list.Stack(struct { + locals_top: usize, + }); + + const RefStack = coral.list.Stack(?*RuntimeRef); pub const Options = struct { out_writer: coral.io.Writer = coral.io.null_writer, err_writer: coral.io.Writer = coral.io.null_writer, }; + const RefSlab = coral.map.Slab(struct { + ref_count: usize, + + object: union (enum) { + false, + true, + number: Float, + string: []coral.io.Byte, + dynamic: *DynamicObject, + }, + }); + pub const ScriptSource = struct { name: []const coral.io.Byte, data: []const coral.io.Byte, }; - const VariantSlab = coral.map.Slab(State.Variant); + pub fn acquire(self: *RuntimeEnv, ref: ?*const RuntimeRef) ?*RuntimeRef { + const key = @intFromPtr(ref); + var ref_data = self.ref_values.remove(key); - pub fn discard(self: *RuntimeEnv, ref: *RuntimeRef) void { - coral.debug.assert(self.bound_refs.remove(@intFromPtr(ref)) != null); + coral.debug.assert(ref_data != null); + + ref_data.?.ref_count += 1; + + coral.debug.assert(self.ref_values.insert_at(key, ref_data.?)); + + return @ptrFromInt(key); } - pub fn execute_chunk(self: *RuntimeEnv, name: []const coral.io.Byte, opcodes: []const Opcode) RuntimeError!*RuntimeRef { - _ = name; + pub fn call( + self: *RuntimeEnv, + caller_ref: *const RuntimeRef, + callable_ref: *const RuntimeRef, + arg_refs: []const *const RuntimeRef, + ) RuntimeError!*RuntimeRef { + const callable = (try callable_ref.fetch(self)).to_object(); - 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}), + return callable.userinfo.call(.{ + .env = self, + .caller = caller_ref, + .callable = callable_ref, + .userdata = callable.userdata, + .args = arg_refs, + }); + } - .push_table => |size| { - var table = Table.make(self.allocator, &self.state); + pub fn discard(self: *RuntimeEnv, ref: *?*RuntimeRef) void { + const key = @intFromPtr(ref.* orelse return); + var ref_data = self.ref_values.remove(key) orelse unreachable; - errdefer table.free(); + coral.debug.assert(ref_data.ref_count != 0); - { - var popped = @as(usize, 0); + ref_data.ref_count -= 1; - 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 rhs_number = try to_number(self, try self.state.pop_value()); - const lhs_number = try to_number(self, try self.state.pop_value()); - - try self.state.push_value(.{.number = lhs_number + rhs_number}); - }, - - .sub => { - const rhs_number = try to_number(self, try self.state.pop_value()); - const lhs_number = try to_number(self, try self.state.pop_value()); - - try self.state.push_value(.{.number = lhs_number - rhs_number}); - }, - - .mul => { - const rhs_number = try to_number(self, try self.state.pop_value()); - const lhs_number = try to_number(self, try self.state.pop_value()); - - try self.state.push_value(.{.number = lhs_number * rhs_number}); - }, - - .div => { - const rhs_number = try to_number(self, try self.state.pop_value()); - const lhs_number = try to_number(self, try self.state.pop_value()); - - try self.state.push_value(.{.number = lhs_number / rhs_number}); - }, - - .eql => { - const rhs = try self.state.pop_value(); - const lhs = try self.state.pop_value(); - - try self.state.push_value(if (lhs.equals(rhs)) .true else .false); - }, - - .cgt => { - const rhs_number = try to_number(self, try self.state.pop_value()); - const lhs_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 rhs_number = try to_number(self, try self.state.pop_value()); - const lhs_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 rhs_number = try to_number(self, try self.state.pop_value()); - const lhs_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 rhs_number = try to_number(self, try self.state.pop_value()); - const lhs_number = try to_number(self, try self.state.pop_value()); - - try self.state.push_value(if (lhs_number <= rhs_number) .true else .false); - }, + if (ref_data.ref_count == 0) { + switch (ref_data.object) { + .string => |string| self.allocator.deallocate(string), + .dynamic => |virtual| self.allocator.deallocate(virtual), + else => {}, } + } else { + coral.debug.assert(self.ref_values.insert_at(key, ref_data)); } - 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)); + ref.* = null; } - pub fn execute_file(self: *RuntimeEnv, file_access: file.Access, file_path: file.Path) RuntimeError!*RuntimeRef { + pub fn execute_file(self: *RuntimeEnv, global_ref: ?*const RuntimeRef, 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 { @@ -436,13 +133,13 @@ pub const RuntimeEnv = struct { defer self.allocator.deallocate(file_data); - return self.execute_script(.{ + return self.execute_script(global_ref, .{ .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 { + pub fn execute_script(self: *RuntimeEnv, global_ref: ?*const RuntimeRef, source: ScriptSource) RuntimeError!?*RuntimeRef { var ast = Ast.make(self.allocator); defer ast.free(); @@ -456,198 +153,271 @@ pub const RuntimeEnv = struct { }; } - var compiler = Compiler.make(self.allocator, &self.state); + var exe = Chunk.make(self); - defer compiler.free(); + defer exe.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"), - }; + try exe.compile_ast(ast); - return self.execute_chunk(source.name, compiler.list_opcodes()); + return exe.execute(global_ref, source.name); } - pub fn free(self: *RuntimeEnv) void { - self.bound_refs.free(); - self.state.free(); + pub fn frame_pop(self: *RuntimeEnv) RuntimeError!void { + const frame = self.frames.pop() orelse return self.raise(error.IllegalState, "stack underflow"); + + coral.debug.assert(self.local_refs.drop(self.local_refs.values.len - frame.locals_top)); } - 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 frame_push(self: *RuntimeEnv) RuntimeError!void { + return self.frames.push_one(.{ + .locals_top = self.local_refs.values.len, }); } - pub fn get_float(self: *RuntimeEnv, ref: *const RuntimeRef) RuntimeError!State.Float { - return to_number(self, try ref.fetch(self)); + pub fn free(self: *RuntimeEnv) void { + self.local_refs.free(); + self.ref_values.free(); } - pub fn get_string(self: *RuntimeEnv, ref: *const RuntimeRef) RuntimeError![]const u8 { - const object = try to_object(self, try ref.fetch(self)); + pub fn get_dynamic(self: *RuntimeEnv, indexable_ref: ?*const RuntimeRef, index_ref: ?*const RuntimeRef) RuntimeError!?*RuntimeRef { + return switch (self.unbox(indexable_ref)) { + .nil => self.raise(error.BadOperation, "nil is immutable"), + .boolean => self.raise(error.BadOperation, "boolean has no such index"), + .number => self.raise(error.BadOperation, "number has no such index"), + .string => self.raise(error.BadOperation, "string has no such index"), - if (ObjectInfo.cast(object.userinfo) != &string_info) { - return self.raise(error.BadOperation, "object is not a string"); - } - - return object.userdata; + .dynamic => |dynamic| dynamic.typeinfo.get(.{ + .userdata = dynamic.userdata, + .env = self, + .index_ref = index_ref, + }), + }; } - pub fn intern(self: *RuntimeEnv, data: []const u8) RuntimeError!*RuntimeRef { - const data_object = try self.state.acquire_interned(data, &string_info); + pub fn local_get(self: *RuntimeEnv, local: u8) RuntimeError!?*RuntimeRef { + return self.local_refs.values[local]; + } - errdefer self.state.release(data_object); + pub fn local_pop(self: *RuntimeEnv) RuntimeError!?*RuntimeRef { + return self.local_refs.pop() orelse self.raise(error.IllegalState, "stack underflow"); + } - return @ptrFromInt(try self.bound_refs.insert(.{.object = data_object})); + pub fn local_push_ref(self: *RuntimeEnv, ref: ?*RuntimeRef) RuntimeError!void { + return self.local_refs.push_one(self.acquire(ref)); + } + + pub fn local_push_boolean(self: *RuntimeEnv, boolean: bool) RuntimeError!void { + return self.local_refs.push_one(try self.new_boolean(boolean)); + } + + pub fn local_push_number(self: *RuntimeEnv, number: Float) RuntimeError!void { + return self.local_refs.push_one(try self.new_number(number)); + } + + pub fn local_set(self: *RuntimeEnv, local: u8, value: ?*RuntimeRef) RuntimeError!void { + self.local_refs.values[local] = self.acquire(value); } 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), + return .{ + .local_refs = RefStack.make(allocator), + .ref_values = RefSlab.make(allocator), + .frames = FrameStack.make(allocator), .err_writer = options.err_writer, + .allocator = allocator, }; - - 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 new_boolean(self: *RuntimeEnv, value: bool) RuntimeError!?*RuntimeRef { + return @ptrFromInt(try self.ref_values.insert(.{ + .ref_count = 1, + .object = if (value) .true else .false, + })); } - pub fn raise(self: *RuntimeEnv, runtime_error: RuntimeError, error_message: []const u8) RuntimeError { + pub fn new_dynamic(self: *RuntimeEnv, userdata: []const coral.io.Byte, typeinfo: *const Typeinfo) RuntimeError!*RuntimeRef { + const userdata_copy = try coral.io.allocate_copy(self.allocator, userdata); + + errdefer self.allocator.deallocate(userdata_copy); + + const dynamic_object = try coral.io.allocate_one(self.allocator, DynamicObject{ + .typeinfo = typeinfo, + .userdata = userdata_copy, + }); + + errdefer self.allocator.deallocate(dynamic_object); + + return @ptrFromInt(try self.ref_values.insert(.{ + .ref_count = 1, + .object = .{.dynamic = dynamic_object}, + })); + } + + pub fn new_number(self: *RuntimeEnv, value: Float) RuntimeError!?*RuntimeRef { + return @ptrFromInt(try self.ref_values.insert(.{ + .ref_count = 1, + .object = .{.number = value}, + })); + } + + pub fn new_string(self: *RuntimeEnv, string_data: []const coral.io.Byte) RuntimeError!?*RuntimeRef { + const string_copy = try coral.io.allocate_copy(self.allocator, string_data); + + errdefer self.allocator.deallocate(string_copy); + + return @ptrFromInt(try self.ref_values.insert(.{ + .ref_count = 1, + .object = .{.string = string_copy}, + })); + } + + pub fn raise(self: *RuntimeEnv, error_value: 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; + }) catch {}; - return runtime_error; + return error_value; + } + + pub fn set_dynamic(self: *RuntimeEnv, indexable_ref: ?*const RuntimeRef, index_ref: ?*const RuntimeRef, value_ref: ?*const RuntimeRef) RuntimeError!void { + return switch (self.unbox(indexable_ref)) { + .nil => self.raise(error.BadOperation, "nil is immutable"), + .boolean => self.raise(error.BadOperation, "boolean is immutable"), + .number => self.raise(error.BadOperation, "number is immutable"), + .string => self.raise(error.BadOperation, "string is immutable"), + + .dynamic => |dynamic| dynamic.typeinfo.set(.{ + .userdata = dynamic.userdata, + .env = self, + .index_ref = index_ref, + }, value_ref), + }; + } + + pub fn unbox(self: *RuntimeEnv, ref: ?*const RuntimeRef) Unboxed { + const ref_data = self.ref_values.lookup(@intFromPtr(ref orelse return .nil)); + + coral.debug.assert(ref_data != null); + + return switch (ref_data.?.object) { + .false => .{.boolean = false}, + .true => .{.boolean = true}, + .number => |number| .{.number = number}, + .string => |string| .{.string = string}, + .dynamic => |dynamic| .{.dynamic = dynamic}, + }; } }; -pub const RuntimeError = coral.io.AllocationError || State.PopError || error { - BadSyntax, - BadOperation, +pub const RuntimeError = coral.io.AllocationError || error { + IllegalState, SystemFailure, + TypeMismatch, + BadOperation, + BadSyntax, }; -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"); +pub const RuntimeRef = opaque {}; + +pub const TestContext = struct { + env: *RuntimeEnv, + userdata: []const coral.io.Byte, + other_ref: ?*const RuntimeRef, +}; + +pub const Typeinfo = struct { + call: *const fn (context: CallContext) RuntimeError!?*RuntimeRef, + clean: *const fn (userdata: []u8) void, + get: *const fn (context: IndexContext) RuntimeError!?*RuntimeRef, + set: *const fn (context: IndexContext, value: ?*const RuntimeRef) RuntimeError!void, + test_difference: *const fn (context: TestContext) RuntimeError!Float = default_test_difference, + test_equality: *const fn (context: TestContext) RuntimeError!bool = default_test_equality, + + fn default_test_difference(context: TestContext) RuntimeError!Float { + return context.env.raise(error.TypeMismatch, "object is not comparable"); + } + + fn default_test_equality(context: TestContext) RuntimeError!bool { + return switch (context.env.unbox(context.other_ref)) { + .dynamic => |dynamic| context.userdata.ptr == dynamic.userdata.ptr, + else => false, + }; } }; -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, +pub const Unboxed = union (enum) { + nil, + boolean: bool, + number: Float, + string: []const u8, + dynamic: *const DynamicObject, }; -fn table_set(context: IndexContext, value: *const RuntimeRef) RuntimeError!void { - const table = @as(*Table, @ptrCast(@alignCast(context.userdata.ptr))); +pub fn get_dynamic_field(env: *RuntimeEnv, indexable: ?*const RuntimeRef, field: []const u8) RuntimeError!?*RuntimeRef { + var interned_field = try env.new_string(field); - 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"), + defer env.discard(&interned_field); - .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); - } - }, - } + return env.get_dynamic(indexable, interned_field); } -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"), +pub fn new_table(env: *RuntimeEnv) RuntimeError!?*RuntimeRef { + var table = Table.make(env); + + errdefer table.free(); + + return try env.new_dynamic(coral.io.bytes_of(&table), &Table.typeinfo); +} + +pub fn test_difference(env: *RuntimeEnv, lhs_ref: ?*const RuntimeRef, rhs_ref: ?*const RuntimeRef) RuntimeError!Float { + return switch (env.unbox(lhs_ref)) { + .nil => env.raise(error.TypeMismatch, "cannot compare nil objects"), + .boolean => env.raise(error.TypeMismatch, "cannot compare boolean objects"), + + .number => |lhs_number| switch (env.unbox(rhs_ref)) { + .number => |rhs_number| rhs_number - lhs_number, + else => env.raise(error.TypeMismatch, "right-hand object is not comparable with number objects"), + }, + + .string => |lhs_string| switch (env.unbox(rhs_ref)) { + .string => |rhs_string| @floatFromInt(coral.io.compare(lhs_string, rhs_string)), + else => env.raise(error.TypeMismatch, "right-hand object is not comparable with string objects"), + }, + + .dynamic => |lhs_dynamic| lhs_dynamic.typeinfo.test_difference(.{ + .env = env, + .userdata = lhs_dynamic.userdata, + .other_ref = rhs_ref, + }), }; } -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, +pub fn test_equality(env: *RuntimeEnv, lhs_ref: ?*const RuntimeRef, rhs_ref: ?*const RuntimeRef) RuntimeError!bool { + return switch (env.unbox(lhs_ref)) { + .nil => (rhs_ref == null), + + .boolean => |lhs_boolean| switch (env.unbox(rhs_ref)) { + .boolean => |rhs_boolean| rhs_boolean == lhs_boolean, + else => false, + }, + + .number => |lhs_number| switch (env.unbox(rhs_ref)) { + .number => |rhs_number| lhs_number == rhs_number, + else => false, + }, + + .string => |lhs_string| switch (env.unbox(rhs_ref)) { + .string => |rhs_string| coral.io.equals(lhs_string, rhs_string), + else => false, + }, + + .dynamic => |lhs_dynamic| try lhs_dynamic.typeinfo.test_equality(.{ + .env = env, + .userdata = lhs_dynamic.userdata, + .other_ref = rhs_ref, + }), }; } - -const string_info = ObjectInfo{ - -}; diff --git a/source/ona/kym/Ast.zig b/source/ona/kym/Ast.zig index cf1bf71..cbf7335 100755 --- a/source/ona/kym/Ast.zig +++ b/source/ona/kym/Ast.zig @@ -5,17 +5,22 @@ const tokens = @import("./tokens.zig"); allocator: coral.io.Allocator, arena: coral.arena.Stacking, statements: Statement.List, -error_message: []const u8, +error_message: []const coral.io.Byte, pub const Expression = union (enum) { nil_literal, true_literal, false_literal, - number_literal: []const u8, - string_literal: []const u8, + number_literal: []const coral.io.Byte, + string_literal: []const coral.io.Byte, table_literal: NamedList, grouped_expression: *Expression, - get_local: []const u8, + get_local: []const coral.io.Byte, + + call_global: struct { + identifier: []const coral.io.Byte, + argument_expressions: List, + }, binary_operation: struct { operator: BinaryOperator, @@ -55,7 +60,7 @@ pub const Expression = union (enum) { }; pub const NamedList = coral.list.Stack(struct { - identifier: []const u8, + identifier: []const coral.io.Byte, expression: Expression, }); @@ -80,6 +85,11 @@ pub const Statement = union (enum) { expression: Expression, }, + call_global: struct { + identifier: []const coral.io.Byte, + argument_expressions: Expression.List, + }, + const List = coral.list.Stack(Statement); }; @@ -88,39 +98,40 @@ const UnaryOperator = enum { numeric_negation, }; -fn binary_operation_parser(comptime parse_next: ExpressionParser, comptime operators: []const Expression.BinaryOperator) ExpressionParser { - return struct { +fn binary_operation_parser( + comptime parse_next: ExpressionParser, + comptime operators: []const Expression.BinaryOperator) ExpressionParser { + + const BinaryOperationParser = struct { fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression { + const allocator = self.arena.as_allocator(); var expression = try parse_next(self, tokenizer); - { - const allocator = self.arena.as_allocator(); + inline for (operators) |operator| { + const token = comptime operator.token(); - inline for (operators) |operator| { - const token = comptime operator.token(); + if (tokenizer.is_token(coral.io.tag_of(token))) { + tokenizer.step(); - if (tokenizer.is_token(coral.io.tag_of(token))) { - tokenizer.step(); - - if (tokenizer.token == null) { - return self.raise( - "expected right-hand side of expression after `" ++ comptime token.text() ++ "`"); - } - - expression = .{ - .binary_operation = .{ - .operator = operator, - .lhs_expression = try coral.io.allocate_one(allocator, expression), - .rhs_expression = try coral.io.allocate_one(allocator, try parse_next(self, tokenizer)), - }, - }; + if (tokenizer.token == null) { + return self.raise("expected other half of expression after `" ++ comptime token.text() ++ "`"); } + + expression = .{ + .binary_operation = .{ + .operator = operator, + .lhs_expression = try coral.io.allocate_one(allocator, expression), + .rhs_expression = try coral.io.allocate_one(allocator, try parse_next(self, tokenizer)), + }, + }; } } return expression; } - }.parse; + }; + + return BinaryOperationParser.parse; } pub fn free(self: *Self) void { @@ -150,9 +161,12 @@ pub fn list_statements(self: Self) []const Statement { pub fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!void { self.free(); + const allocator = self.arena.as_allocator(); var has_returned = false; while (true) { + const no_effect_message = "statement has no effect"; + tokenizer.skip(.newline); switch (tokenizer.token orelse return) { @@ -181,8 +195,6 @@ pub fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!void { .local => |identifier| { tokenizer.step(); - const no_effect_message = "statement has no effect"; - switch (tokenizer.token orelse return self.raise(no_effect_message)) { .newline => return self.raise(no_effect_message), @@ -209,6 +221,45 @@ pub fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!void { } }, + .global => |identifier| { + tokenizer.step(); + + switch (tokenizer.token orelse return self.raise(no_effect_message)) { + .newline => return self.raise(no_effect_message), + + .symbol_paren_left => { + tokenizer.step(); + + var expressions_list = Expression.List.make(allocator); + + while (true) { + if (tokenizer.is_token(.symbol_paren_right)) { + break; + } + + try expressions_list.push_one(try self.parse_expression(tokenizer)); + + switch (tokenizer.token orelse return self.raise("unexpected end after after `(`")) { + .symbol_comma => continue, + .symbol_paren_right => break, + else => return self.raise("expected `)` or argument after `(`"), + } + } + + tokenizer.step(); + + try self.statements.push_one(.{ + .call_global = .{ + .argument_expressions = expressions_list, + .identifier = identifier, + }, + }); + }, + + else => return self.raise("expected `=` after local"), + } + }, + else => return self.raise("invalid statement"), } } @@ -252,6 +303,24 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression return Expression{.grouped_expression = try coral.io.allocate_one(allocator, expression)}; }, + .keyword_nil => { + tokenizer.step(); + + return .nil_literal; + }, + + .keyword_true => { + tokenizer.step(); + + return .true_literal; + }, + + .keyword_false => { + tokenizer.step(); + + return .false_literal; + }, + .number => |value| { tokenizer.step(); @@ -264,6 +333,48 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression return Expression{.string_literal = value}; }, + .global => |identifier| { + tokenizer.skip(.newline); + + var expression_list = Expression.List.make(allocator); + + while (true) { + switch (tokenizer.token orelse return self.raise("expected expression or `)` after `(`")) { + .symbol_paren_right => { + tokenizer.step(); + + return Expression{ + .call_global = .{ + .identifier = identifier, + .argument_expressions = expression_list, + }, + }; + }, + + else => { + try expression_list.push_one(try self.parse_expression(tokenizer)); + + switch (tokenizer.token orelse return self.raise("expected `,` or `)` after argument")) { + .symbol_comma => continue, + + .symbol_paren_right => { + tokenizer.step(); + + return Expression{ + .call_global = .{ + .identifier = identifier, + .argument_expressions = expression_list, + }, + }; + }, + + else => return self.raise("expected `,` or `)` after argument"), + } + }, + } + } + }, + .local => |identifier| { tokenizer.step(); diff --git a/source/ona/kym/Chunk.zig b/source/ona/kym/Chunk.zig new file mode 100644 index 0000000..7ede24b --- /dev/null +++ b/source/ona/kym/Chunk.zig @@ -0,0 +1,504 @@ +const Ast = @import("./Ast.zig"); + +const coral = @import("coral"); + +const kym = @import("../kym.zig"); + +env: *kym.RuntimeEnv, +constant_refs: RefList, +opcodes: OpcodeList, + +const AstCompiler = struct { + chunk: *Self, + local_identifiers_buffer: [255][]const coral.io.Byte = [_][]const coral.io.Byte{""} ** 255, + local_identifiers_count: u8 = 0, + + fn compile_expression(self: *AstCompiler, expression: Ast.Expression) kym.RuntimeError!void { + const number_format = coral.utf8.DecimalFormat{ + .delimiter = "_", + .positive_prefix = .none, + }; + + switch (expression) { + .nil_literal => try self.chunk.append_opcode(.push_nil), + .true_literal => try self.chunk.append_opcode(.push_true), + .false_literal => try self.chunk.append_opcode(.push_false), + + .number_literal => |literal| { + const parsed_number = number_format.parse(literal, kym.Float); + + coral.debug.assert(parsed_number != null); + + try self.chunk.append_opcode(.{ + .push_const = try self.chunk.declare_constant_number(parsed_number.?), + }); + }, + + .string_literal => |literal| { + try self.chunk.append_opcode(.{.push_const = try self.chunk.declare_constant_string(literal)}); + }, + + .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.chunk.append_opcode(.{ + .push_const = try self.chunk.declare_constant_string(field.identifier), + }); + } + + try self.chunk.append_opcode(.{ + .push_table = .{.field_count = @intCast(fields.values.len)}}); + }, + + .binary_operation => |operation| { + try self.compile_expression(operation.lhs_expression.*); + try self.compile_expression(operation.rhs_expression.*); + + try self.chunk.append_opcode(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.chunk.append_opcode(switch (operation.operator) { + .boolean_negation => .not, + .numeric_negation => .neg, + }); + }, + + .grouped_expression => |grouped_expression| { + try self.compile_expression(grouped_expression.*); + }, + + .get_local => |local| { + try self.chunk.append_opcode(.{ + .local_push_ref = self.resolve_local(local) orelse return self.chunk.env.raise(error.OutOfMemory, "undefined local"), + }); + }, + + .call_global => |call| { + if (call.argument_expressions.values.len > coral.math.max_int(@typeInfo(u8).Int)) { + return self.chunk.env.raise(error.OutOfMemory, "functions may receive a maximum of 255 locals"); + } + + for (call.argument_expressions.values) |argument_expression| { + try self.compile_expression(argument_expression); + } + + try self.chunk.append_opcode(.{.push_const = try self.chunk.declare_constant_string(call.identifier)}); + + try self.chunk.append_opcode(.{ + .global_call = .{ + .arg_count = @intCast(call.argument_expressions.values.len), + }, + }); + }, + } + } + + fn compile_statement(self: *AstCompiler, statement: Ast.Statement) kym.RuntimeError!void { + switch (statement) { + .return_expression => |return_expression| try self.compile_expression(return_expression), + .return_nothing => try self.chunk.append_opcode(.push_nil), + + .set_local => |local| { + try self.compile_expression(local.expression); + + if (self.resolve_local(local.identifier)) |index| { + try self.chunk.append_opcode(.{.set_local = index}); + } else { + try self.declare_local(local.identifier); + } + }, + + .call_global => |call| { + if (call.argument_expressions.values.len > coral.math.max_int(@typeInfo(u8).Int)) { + return self.chunk.env.raise(error.OutOfMemory, "functions may receive a maximum of 255 locals"); + } + + for (call.argument_expressions.values) |argument_expression| { + try self.compile_expression(argument_expression); + } + + try self.chunk.append_opcode(.{.push_const = try self.chunk.declare_constant_string(call.identifier)}); + + try self.chunk.append_opcode(.{ + .global_call = .{ + .arg_count = @intCast(call.argument_expressions.values.len), + }, + }); + } + } + } + + fn declare_local(self: *AstCompiler, identifier: []const u8) kym.RuntimeError!void { + if (self.local_identifiers_count == self.local_identifiers_buffer.len) { + return self.chunk.env.raise(error.OutOfMemory, "functions may contain a maximum of 255 locals"); + } + + self.local_identifiers_buffer[self.local_identifiers_count] = identifier; + self.local_identifiers_count += 1; + } + + fn resolve_local(self: *AstCompiler, local_identifier: []const coral.io.Byte) ?u8 { + if (self.local_identifiers_count == 0) { + return null; + } + + var index = @as(u8, self.local_identifiers_count - 1); + + while (true) : (index -= 1) { + if (coral.io.equals(local_identifier, self.local_identifiers_buffer[index])) { + return index; + } + + if (index == 0) { + return null; + } + } + } +}; + +pub const Constant = u16; + +const RefList = coral.list.Stack(?*kym.RuntimeRef); + +const LocalsList = coral.list.Stack([]const u8); + +pub const Opcode = union (enum) { + push_nil, + push_true, + push_false, + push_const: Constant, + local_push_ref: u8, + set_local: u8, + get_global, + + push_table: struct { + field_count: u32, + }, + + global_call: struct { + arg_count: u8, + }, + + not, + neg, + + add, + sub, + mul, + div, + + eql, + cgt, + clt, + cge, + cle, +}; + +const OpcodeList = coral.list.Stack(Opcode); + +const Self = @This(); + +pub fn append_opcode(self: *Self, opcode: Opcode) kym.RuntimeError!void { + return self.opcodes.push_one(opcode); +} + +pub fn compile_ast(self: *Self, ast: Ast) kym.RuntimeError!void { + self.free(); + + try self.constant_refs.grow(coral.math.max_int(@typeInfo(u16).Int)); + + var compiler = AstCompiler{.chunk = self}; + + for (ast.list_statements()) |statement| { + try compiler.compile_statement(statement); + } + + try self.constant_refs.pack(); + try self.opcodes.pack(); +} + +pub fn declare_constant_number(self: *Self, constant: kym.Float) kym.RuntimeError!Constant { + const tail = self.constant_refs.values.len; + + if (tail == coral.math.max_int(@typeInfo(u16).Int)) { + return self.env.raise(error.BadSyntax, "functions may contain a maximum of 65,535 constants"); + } + + var constant_ref = try self.env.new_number(constant); + + errdefer self.env.discard(&constant_ref); + + try self.constant_refs.push_one(constant_ref); + + return @intCast(tail); +} + +pub fn declare_constant_string(self: *Self, constant: []const coral.io.Byte) kym.RuntimeError!Constant { + const tail = self.constant_refs.values.len; + + if (tail == coral.math.max_int(@typeInfo(u16).Int)) { + return self.env.raise(error.BadSyntax, "functions may contain a maximum of 65,535 constants"); + } + + var constant_ref = try self.env.new_string(constant); + + errdefer self.env.discard(&constant_ref); + + try self.constant_refs.push_one(constant_ref); + + return @intCast(tail); +} + +pub fn execute(self: *Self, global_ref: ?*const kym.RuntimeRef, name: []const coral.io.Byte) kym.RuntimeError!?*kym.RuntimeRef { + _ = name; + + try self.env.frame_push(); + + for (self.opcodes.values) |opcode| { + switch (opcode) { + .push_nil => try self.env.local_push_ref(null), + .push_true => try self.env.local_push_boolean(true), + .push_false => try self.env.local_push_boolean(false), + .push_const => |constant| try self.env.local_push_ref(self.constant_refs.values[constant]), + + .push_table => |push_table| { + var table_ref = try kym.new_table(self.env); + + defer self.env.discard(&table_ref); + + { + var popped = @as(usize, 0); + + while (popped < push_table.field_count) : (popped += 1) { + try self.env.set_dynamic(table_ref, try self.env.local_pop(), try self.env.local_pop()); + } + } + + try self.env.local_push_ref(table_ref); + }, + + .local_push_ref => |local| { + var ref = try self.env.local_get(local); + + defer self.env.discard(&ref); + + try self.env.local_push_ref(ref); + }, + + .get_global => { + var identifier_ref = try self.env.local_pop(); + + defer self.env.discard(&identifier_ref); + + var ref = try self.env.get_dynamic(global_ref, identifier_ref); + + defer self.env.discard(&ref); + + try self.env.local_push_ref(ref); + }, + + .set_local => |local| { + var ref = try self.env.local_pop(); + + defer self.env.discard(&ref); + + try self.env.local_set(local, ref); + }, + + .global_call => |_| { + + }, + + .neg => try self.env.local_push_number(switch (self.env.unbox(try self.env.local_pop())) { + .number => |number| -number, + else => return self.env.raise(error.TypeMismatch, "object is not scalar negatable"), + }), + + .not => try self.env.local_push_boolean(switch (self.env.unbox(try self.env.local_pop())) { + .boolean => |boolean| !boolean, + else => return self.env.raise(error.TypeMismatch, "object is not boolean negatable"), + }), + + .add => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_ref(switch (self.env.unbox(lhs_ref)) { + .number => |lhs_number| switch (self.env.unbox(rhs_ref)) { + .number => |rhs_number| try self.env.new_number(lhs_number + rhs_number), + else => return self.env.raise(error.TypeMismatch, "right-hand object is not addable"), + }, + + else => return self.env.raise(error.TypeMismatch, "left-hand object is not addable"), + }); + }, + + .sub => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_ref(switch (self.env.unbox(lhs_ref)) { + .number => |lhs_number| switch (self.env.unbox(rhs_ref)) { + .number => |rhs_number| try self.env.new_number(lhs_number - rhs_number), + else => return self.env.raise(error.TypeMismatch, "right-hand object is not subtractable"), + }, + + else => return self.env.raise(error.TypeMismatch, "left-hand object is not subtractable"), + }); + }, + + .mul => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_ref(switch (self.env.unbox(lhs_ref)) { + .number => |lhs_number| switch (self.env.unbox(rhs_ref)) { + .number => |rhs_number| try self.env.new_number(lhs_number * rhs_number), + else => return self.env.raise(error.TypeMismatch, "right-hand object is not multiplyable"), + }, + + else => return self.env.raise(error.TypeMismatch, "left-hand object is not multiplyable"), + }); + }, + + .div => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_ref(switch (self.env.unbox(lhs_ref)) { + .number => |lhs_number| switch (self.env.unbox(rhs_ref)) { + .number => |rhs_number| try self.env.new_number(lhs_number / rhs_number), + else => return self.env.raise(error.TypeMismatch, "right-hand object is not divisable"), + }, + + else => return self.env.raise(error.TypeMismatch, "left-hand object is not divisable"), + }); + }, + + .eql => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_boolean(try kym.test_equality(self.env, lhs_ref, rhs_ref)); + }, + + .cgt => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_boolean(try kym.test_difference(self.env, lhs_ref, rhs_ref) > 0); + }, + + .clt => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_boolean(try kym.test_difference(self.env, lhs_ref, rhs_ref) < 0); + }, + + .cge => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_boolean(try kym.test_difference(self.env, lhs_ref, rhs_ref) >= 0); + }, + + .cle => { + var rhs_ref = try self.env.local_pop(); + + defer self.env.discard(&rhs_ref); + + var lhs_ref = try self.env.local_pop(); + + defer self.env.discard(&lhs_ref); + + try self.env.local_push_boolean(try kym.test_difference(self.env, lhs_ref, rhs_ref) <= 0); + }, + } + } + + const result = try self.env.local_pop(); + + try self.env.frame_pop(); + + return result; +} + +pub fn free(self: *Self) void { + for (self.constant_refs.values) |*constant| { + self.env.discard(constant); + } + + self.opcodes.free(); + self.constant_refs.free(); +} + +fn is_zero(utf8: []const u8) bool { + return coral.io.equals(utf8, "0") or coral.io.equals(utf8, "0.0"); +} + +pub fn make(env: *kym.RuntimeEnv) Self { + return Self{ + .opcodes = OpcodeList.make(env.allocator), + .constant_refs = RefList.make(env.allocator), + .env = env, + }; +} diff --git a/source/ona/kym/State.zig b/source/ona/kym/State.zig deleted file mode 100644 index 7f29d16..0000000 --- a/source/ona/kym/State.zig +++ /dev/null @@ -1,148 +0,0 @@ -const coral = @import("coral"); - -allocator: coral.io.Allocator, -interned: SymbolTable, -globals: Object, -values: DataStack, -frames: CallStack, - -pub const Float = f64; - -const CallStack = coral.list.Stack(struct { - callable: *Object, - opcode_index: usize, - stack_index: usize, -}); - -const DataStack = coral.list.Stack(Variant); - -pub const Object = struct { - ref_count: usize, - userdata: []coral.io.Byte, - userinfo: *const anyopaque, -}; - -pub const PopError = error { - StackOverflow, -}; - -const Self = @This(); - -const SymbolTable = coral.map.Table([]const coral.io.Byte, *Object, coral.map.string_table_traits); - -pub const Variant = union (enum) { - nil, - true, - false, - number: Float, - object: *Object, - - pub fn equals(self: Variant, other: Variant) bool { - return switch (self) { - .nil => other == .nil, - .true => other == .true, - .false => other == .false, - - .number => |number| switch (other) { - .number => |other_number| number == other_number, - else => false, - }, - - .object => |object| switch (other) { - .object => |other_object| object == other_object, - else => false, - }, - }; - } -}; - -pub fn acquire_instance(_: *Self, object: *Object) *Object { - // TODO: safety-check object belongs to state. - object.ref_count += 1; - - return object; -} - -pub fn acquire_interned(self: *Self, userdata: []const u8, userinfo: *const anyopaque) coral.io.AllocationError!*Object { - // TODO: Include userinfo in matching lookup. - if (self.interned.lookup(userdata)) |object| { - return self.acquire_instance(object); - } else { - const data_object = try self.acquire_new(userdata, userinfo); - - errdefer self.release(data_object); - - coral.debug.assert(try self.interned.insert(data_object.userdata, data_object)); - - return data_object; - } -} - -pub fn acquire_new(self: *Self, userdata: []const u8, userinfo: *const anyopaque) coral.io.AllocationError!*Object { - const allocated_userdata = try coral.io.allocate_copy(self.allocator, userdata); - - errdefer self.allocator.deallocate(allocated_userdata); - - const allocated_object = try coral.io.allocate_one(self.allocator, Object{ - .ref_count = 1, - .userdata = allocated_userdata, - .userinfo = userinfo, - }); - - errdefer self.allocator.deallocate(allocated_object); - - return allocated_object; -} - -pub fn free(self: *Self) void { - self.values.free(); - self.frames.free(); - self.interned.free(); -} - -pub fn get_value(self: *Self, index: u8) Variant { - return if (index < self.values.values.len) self.values.values[index] else .nil; -} - -pub fn make(allocator: coral.io.Allocator) Self { - return .{ - .values = DataStack.make(allocator), - .frames = CallStack.make(allocator), - .interned = SymbolTable.make(allocator), - .allocator = allocator, - - .globals = .{ - .ref_count = 0, - .userdata = &.{}, - .userinfo = &.{}, - }, - }; -} - -pub fn pop_value(self: *Self) PopError!Variant { - return self.values.pop() orelse error.StackOverflow; -} - -pub fn push_value(self: *Self, value: Variant) coral.io.AllocationError!void { - return self.values.push_one(value); -} - -pub fn release(self: *Self, object: *Object) void { - coral.debug.assert(object.ref_count != 0); - - object.ref_count -= 1; - - if (object.ref_count == 0) { - self.allocator.deallocate(object); - } -} - -pub fn set_value(self: *Self, index: u8, value: Variant) bool { - if (index >= self.values.values.len) { - return false; - } - - self.values.values[index] = value; - - return true; -} diff --git a/source/ona/kym/Table.zig b/source/ona/kym/Table.zig index 57f1d0d..8867bcc 100644 --- a/source/ona/kym/Table.zig +++ b/source/ona/kym/Table.zig @@ -1,76 +1,82 @@ -const State = @import("./State.zig"); - const coral = @import("coral"); -state: *State, +const kym = @import("../kym.zig"); + fields: FieldTable, -array: ArrayList, -const ArrayList = coral.list.Stack(State.Variant); - -const Field = struct { - field: *State.Object, - value: State.Variant, - - fn release_objects(self: Field, state: *State) void { - state.release(self.field); - - if (self.value == .object) { - state.release(self.value.object); - } - } -}; - -const FieldTable = coral.map.Table([]const coral.io.Byte, Field, coral.map.string_table_traits); +const FieldTable = coral.map.Table([]const coral.io.Byte, struct { + key_ref: ?*kym.RuntimeRef, + value_ref: ?*kym.RuntimeRef, +}, coral.map.string_table_traits); const Self = @This(); pub fn free(self: *Self) void { - { - var field_iterator = FieldTable.Iterable{.table = &self.fields}; - - while (field_iterator.next()) |entry| { - entry.value.release_objects(self.state); - } - } - self.fields.free(); - self.array.free(); } -pub fn get_field(self: *Self, field_name: *State.Object) State.Variant { - const field = self.fields.lookup(field_name.userdata) orelse return .nil; - - if (field.value == .object) { - return .{.object = self.state.acquire_instance(field.value.object)}; - } - - return field.value; -} - -pub fn get_index(self: *Self, index: usize) State.Variant { - return self.array.values[index]; -} - -pub fn make(allocator: coral.io.Allocator, state: *State) Self { +pub fn make(env: *kym.RuntimeEnv) Self { return .{ - .state = state, - .fields = FieldTable.make(allocator), - .array = ArrayList.make(allocator), + .fields = FieldTable.make(env.allocator), }; } -pub fn set_field(self: *Self, field_name: *State.Object, value: State.Variant) coral.io.AllocationError!void { - const previous_entry = try self.fields.replace(field_name.userdata, .{ - .field = field_name, - .value = value, - }); +pub const typeinfo = kym.Typeinfo{ + .call = typeinfo_call, + .clean = typeinfo_clean, + .get = typeinfo_get, + .set = typeinfo_set, +}; - if (previous_entry) |entry| { - entry.value.release_objects(self.state); +fn typeinfo_call(context: kym.CallContext) kym.RuntimeError!?*kym.RuntimeRef { + return context.env.raise(error.TypeMismatch, "cannot call a table"); +} + +fn typeinfo_clean(userdata: []u8) void { + @as(*Self, @ptrCast(@alignCast(userdata.ptr))).free(); +} + +fn typeinfo_get(context: kym.IndexContext) kym.RuntimeError!?*kym.RuntimeRef { + const table = @as(*Self, @ptrCast(@alignCast(context.userdata.ptr))); + + return switch (context.env.unbox(context.index_ref)) { + .string => |string| context.env.acquire((table.fields.lookup(string) orelse return null).value_ref), + // TODO: Implement number indices in tables. + .number => |_| unreachable, + else => context.env.raise(error.TypeMismatch, "table objects may only be indexed with strings or numbers"), + }; +} + +fn typeinfo_set(context: kym.IndexContext, value_ref: ?*const kym.RuntimeRef) kym.RuntimeError!void { + const table = @as(*Self, @ptrCast(@alignCast(context.userdata.ptr))); + var acquired_value_ref = context.env.acquire(value_ref); + + errdefer context.env.discard(&acquired_value_ref); + + switch (context.env.unbox(context.index_ref)) { + .string => |string| { + var acquired_index_ref = context.env.acquire(context.index_ref); + + errdefer context.env.discard(&acquired_index_ref); + + var displaced_table_entry = if (acquired_value_ref) |ref| try table.fields.replace(string, .{ + .key_ref = acquired_index_ref, + .value_ref = ref, + }) else table.fields.remove(string); + + if (displaced_table_entry) |*entry| { + context.env.discard(&entry.value.key_ref); + context.env.discard(&entry.value.value_ref); + } + }, + + .number => |_| { + // TODO: Implement number indices in tables. + unreachable; + }, + + else => { + return context.env.raise(error.TypeMismatch, "table objects may only be indexed with strings or numbers"); + }, } } - -pub fn set_index(self: *Self, index: usize, value: State.Variant) coral.io.AllocationError!void { - self.array.values[index] = value; -} diff --git a/source/ona/ona.zig b/source/ona/ona.zig index 1fcbace..82ee995 100644 --- a/source/ona/ona.zig +++ b/source/ona/ona.zig @@ -48,9 +48,17 @@ pub fn run_app(file_access: file.Access) RuntimeError!void { defer script_env.free(); + var globals_ref = kym.new_table(&script_env) catch { + try fail_log.write("failed to initialize script runtime"); + + return error.InitFailure; + }; + + defer script_env.discard(&globals_ref); + var manifest = app.Manifest{}; - manifest.load(&script_env, file_access) catch { + manifest.load(&script_env, globals_ref, file_access) catch { fail_log.write("failed to load / execute app.ona manifest") catch {}; return error.BadManifest;