diff --git a/debug/app.ona b/debug/app.ona index 19d8065..1cdb540 100644 --- a/debug/app.ona +++ b/debug/app.ona @@ -1,17 +1,13 @@ # Test comment. -test = { - .message = "don't you lecture me with your 30 dollar scripting language" -} - -test.message = "game is loading" - -@log_info(test.message) - -return { +options = { .title = "Afterglow", .width = 1280, .height = 800, .tick_rate = 60, } + +# @log_info(options.title) + +return options diff --git a/source/coral/io.zig b/source/coral/io.zig index 8da89fe..39025ee 100644 --- a/source/coral/io.zig +++ b/source/coral/io.zig @@ -255,6 +255,16 @@ pub fn allocate_one(allocator: Allocator, value: anytype) AllocationError!*@Type return allocation; } +pub fn allocate_string(allocator: Allocator, source: []const Byte) AllocationError![:0]Byte { + const allocation = try allocator.actions.reallocate(allocator.context, @returnAddress(), null, source.len + 1); + + copy(allocation[0 .. source.len], source); + + allocation[source.len] = 0; + + return @ptrCast(allocation); +} + pub fn bytes_of(value: anytype) []const Byte { const pointer_info = @typeInfo(@TypeOf(value)).Pointer; @@ -330,6 +340,16 @@ pub fn equals(target: []const Byte, match: []const Byte) bool { return true; } +pub fn find_first(haystack: []const Byte, needle: Byte) ?usize { + for (0 .. haystack.len) |i| { + if (haystack[i] == needle) { + return i; + } + } + + return null; +} + var null_context = @as(usize, 0); pub const null_writer = Writer.bind(usize, &null_context, struct { diff --git a/source/coral/map.zig b/source/coral/map.zig index 81c11d4..30a8bdb 100644 --- a/source/coral/map.zig +++ b/source/coral/map.zig @@ -6,112 +6,15 @@ const list = @import("./list.zig"); const math = @import("./math.zig"); -pub fn Slab(comptime Value: type) type { - return struct { - next_index: usize, - entries: EntryList, - - const EntryList = list.Stack(union (enum) { - value: Value, - next_index: usize, - }); - - const Self = @This(); - - pub fn lookup(self: Self, key: usize) ?Value { - if (key == 0 or key > self.entries.values.len) { - return null; - } - - return switch (self.entries.values[key - 1]) { - .value => |value| value, - .next_index => null, - }; - } - - pub fn free(self: *Self) void { - self.entries.free(); - - self.next_index = 0; - } - - pub fn insert(self: *Self, value: Value) io.AllocationError!usize { - if (self.next_index < self.entries.values.len) { - const index = self.next_index; - const entry = &self.entries.values[index]; - - debug.assert(entry.* == .next_index); - - self.next_index = entry.next_index; - entry.* = .{.value = value}; - - return index + 1; - } else { - try self.entries.push_one(.{.value = value}); - - self.next_index += 1; - - return self.next_index; - } - } - - 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, - .entries = EntryList.make(allocator), - }; - } - - pub fn remove(self: *Self, key: usize) ?Value { - if (key == 0 or key > self.entries.values.len) { - return null; - } - - const index = key - 1; - const entry = &self.entries.values[index]; - - return switch (entry.*) { - .next_index => null, - - .value => get_value: { - const value = entry.value; - - entry.* = .{.next_index = self.next_index}; - self.next_index = index; - - break: get_value value; - }, - }; - } - }; -} - pub fn StringTable(comptime Value: type) type { return Table([]const io.Byte, Value, struct { const Self = @This(); - fn equals(_: Self, key_a: []const io.Byte, key_b: []const io.Byte) bool { + fn equals(key_a: []const io.Byte, key_b: []const io.Byte) bool { return io.equals(key_a, key_b); } - fn hash(_: Self, key: []const io.Byte) usize { + fn hash(key: []const io.Byte) usize { return io.djb2_hash(@typeInfo(usize).Int, key); } }); @@ -119,9 +22,7 @@ pub fn StringTable(comptime Value: type) type { pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) type { const load_max = 0.75; - const hash_int = @typeInfo(usize).Int; - const max_int = math.max_int(hash_int); - const min_int = math.min_int(hash_int); + const max_int = math.max_int(@typeInfo(usize).Int); return struct { allocator: io.Allocator, @@ -135,7 +36,7 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) ty fn write_into(self: Entry, table: *Self) bool { const hash_max = @min(max_int, table.entries.len); - var hashed_key = math.wrap(table.traits.hash(self.key), min_int, hash_max); + var hashed_key = table.hash_key(self.key) % hash_max; var iterations = @as(usize, 0); while (true) : (iterations += 1) { @@ -152,7 +53,7 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) ty return true; }); - if (table.traits.equals(table_entry.key, self.key)) { + if (table.keys_equal(table_entry.key, self.key)) { return false; } @@ -187,14 +88,18 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) ty }; } + fn hash_key(self: Self, key: Key) usize { + return if (@sizeOf(Traits) == 0) Traits.hash(key) else self.traits.hash(key); + } + pub fn remove(self: *Self, key: Key) ?Entry { const hash_max = @min(max_int, self.entries.len); - var hashed_key = math.wrap(self.traits.hash(key), min_int, hash_max); + var hashed_key = self.hash_key(key) % hash_max; while (true) { const entry = &(self.entries[hashed_key] orelse continue); - if (self.traits.equals(entry.key, key)) { + if (self.keys_equal(entry.key, key)) { const original_entry = entry.*; self.entries[hashed_key] = null; @@ -213,7 +118,8 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) ty { const hash_max = @min(max_int, self.entries.len); - var hashed_key = math.wrap(self.traits.hash(key), min_int, hash_max); + const has_context = @sizeOf(Traits) != 0; + var hashed_key = (if (has_context) self.traits.hash(key) else Traits.hash(key)) % hash_max; while (true) { const entry = &(self.entries[hashed_key] orelse { @@ -227,15 +133,28 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) ty return null; }); - if (self.traits.equals(entry.key, key)) { - const original_entry = entry.*; + if (has_context) { + if (self.traits.equals(entry.key, key)) { + const original_entry = entry.*; - entry.* = .{ - .key = key, - .value = value, - }; + entry.* = .{ + .key = key, + .value = value, + }; - return original_entry; + return original_entry; + } + } else { + if (Traits.equals(entry.key, key)) { + const original_entry = entry.*; + + entry.* = .{ + .key = key, + .value = value, + }; + + return original_entry; + } } hashed_key = (hashed_key +% 1) % hash_max; @@ -279,20 +198,35 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) ty return entry.write_into(self); } + fn keys_equal(self: Self, key_a: Key, key_b: Key) bool { + if (@sizeOf(Traits) == 0) { + return Traits.equals(key_a, key_b); + } else { + return self.traits.equals(key_a, key_b); + } + } + pub fn lookup(self: Self, key: Key) ?Value { if (self.count == 0) { return null; } const hash_max = @min(max_int, self.entries.len); - var hashed_key = math.wrap(self.traits.hash(key), min_int, hash_max); + const has_context = @sizeOf(Traits) != 0; + var hashed_key = (if (has_context) self.traits.hash(key) else Traits.hash(key)) % hash_max; var iterations = @as(usize, 0); while (iterations < self.count) : (iterations += 1) { const entry = &(self.entries[hashed_key] orelse return null); - if (self.traits.equals(entry.key, key)) { - return entry.value; + if (has_context) { + if (self.traits.equals(entry.key, key)) { + return entry.value; + } + } else { + if (Traits.equals(entry.key, key)) { + return entry.value; + } } hashed_key = (hashed_key +% 1) % hash_max; diff --git a/source/coral/math.zig b/source/coral/math.zig index 3f0de17..360da13 100644 --- a/source/coral/math.zig +++ b/source/coral/math.zig @@ -60,9 +60,3 @@ pub fn min_int(comptime int: std.builtin.Type.Int) comptime_int { return -(1 << (bit_count - 1)); } - -pub fn wrap(value: anytype, lower: anytype, upper: anytype) @TypeOf(value, lower, upper) { - const range = upper - lower; - - return if (range == 0) lower else lower + @mod((@mod((value - lower), range) + range), range); -} diff --git a/source/ona/app.zig b/source/ona/app.zig index a7d49ba..81d51f5 100644 --- a/source/ona/app.zig +++ b/source/ona/app.zig @@ -13,20 +13,18 @@ pub const Manifest = struct { tick_rate: f32 = 60.0, pub fn load(self: *Manifest, env: *kym.RuntimeEnv, file_access: file.Access) kym.RuntimeError!void { - const manifest_ref = try env.execute_file(file_access, file.Path.from(&.{"app.ona"})) orelse return; + const manifest = try env.expect(try env.execute_file(file_access, file.Path.from(&.{"app.ona"}))); - defer env.discard(manifest_ref); + defer env.discard(manifest); const width = @as(u16, get: { - const ref = try kym.get_field(env, manifest_ref, "width"); + if (try env.get_field(manifest, "width")) |ref| { + defer env.discard(ref); - defer env.discard(ref); + const fixed = try env.unbox_fixed(ref); - if (ref) |live_ref| { - if (env.unbox(live_ref).expect_number()) |number| { - if (number > 0 and number < coral.math.max_int(@typeInfo(@TypeOf(self.width)).Int)) { - break: get @intFromFloat(number); - } + if (fixed > 0 and fixed < coral.math.max_int(@typeInfo(@TypeOf(self.width)).Int)) { + break: get @intCast(fixed); } } @@ -34,15 +32,13 @@ pub const Manifest = struct { }); const height = @as(u16, get: { - const ref = try kym.get_field(env, manifest_ref, "height"); + if (try env.get_field(manifest, "height")) |ref| { + defer env.discard(ref); - defer env.discard(ref); + const fixed = try env.unbox_fixed(ref); - if (ref) |live_ref| { - if (env.unbox(live_ref).expect_number()) |number| { - if (number > 0 and number < coral.math.max_int(@typeInfo(@TypeOf(self.height)).Int)) { - break: get @intFromFloat(number); - } + if (fixed > 0 and fixed < coral.math.max_int(@typeInfo(@TypeOf(self.height)).Int)) { + break: get @intCast(fixed); } } @@ -50,38 +46,25 @@ pub const Manifest = struct { }); const tick_rate = @as(f32, get: { - const ref = try kym.get_field(env, manifest_ref, "tick_rate"); + if (try env.get_field(manifest, "tick_rate")) |ref| { + defer env.discard(ref); - defer env.discard(ref); - - if (ref) |live_ref| { - if (env.unbox(live_ref).expect_number()) |number| { - break: get @floatCast(number); - } + break: get @floatCast(try env.unbox_float(ref)); } break: get self.tick_rate; }); - { - const ref = try kym.get_field(env, manifest_ref, "title"); - + if (try env.get_field(manifest, "title")) |ref| { defer env.discard(ref); - const title_string = unbox: { - if (ref) |live_ref| { - if (env.unbox(live_ref).expect_string()) |string| { - break: unbox string; - } - } - - break: unbox ""; - }; - + const title_string = try env.unbox_string(ref); const limited_title_len = @min(title_string.len, self.title.len); coral.io.copy(&self.title, title_string[0 .. limited_title_len]); coral.io.zero(self.title[limited_title_len .. self.title.len]); + } else { + coral.io.zero(&self.title); } self.tick_rate = tick_rate; diff --git a/source/ona/kym.zig b/source/ona/kym.zig index 42758c9..1b424e4 100644 --- a/source/ona/kym.zig +++ b/source/ona/kym.zig @@ -1,59 +1,13 @@ const Ast = @import("./kym/Ast.zig"); -const Chunk = @import("./kym/Chunk.zig"); - const Table = @import("./kym/Table.zig"); const coral = @import("coral"); const file = @import("./file.zig"); -pub const Any = union (enum) { - boolean: bool, - fixed: Fixed, - float: Float, - string: []const coral.io.Byte, - symbol: []const coral.io.Byte, - lambda, - dynamic: *const DynamicObject, - - pub fn expect_dynamic(self: Any) ?*const DynamicObject { - return switch (self) { - .dynamic => |dynamic| dynamic, - else => null, - }; - } - - pub fn expect_number(self: Any) ?Float { - return switch (self) { - .fixed => |fixed| @floatFromInt(fixed), - .float => |float| float, - else => null, - }; - } - - pub fn expect_string(self: Any) ?[]const coral.io.Byte { - return switch (self) { - .string => |string| string, - else => null, - }; - } -}; - pub const Caller = coral.io.Generator(RuntimeError!?*RuntimeRef, *RuntimeEnv); -pub const DynamicObject = struct { - userdata: []coral.io.Byte, - typeinfo: *const Typeinfo, - - fn call(self: *DynamicObject, env: *RuntimeEnv) RuntimeError!?*RuntimeRef { - return self.typeinfo.call(.{ - .env = env, - .userdata = self.userdata, - }); - } -}; - pub const ErrorHandler = coral.io.Generator(void, ErrorInfo); pub const ErrorInfo = struct { @@ -71,176 +25,890 @@ pub const Frame = struct { locals_top: usize, }; -pub const Method = struct { - env: *RuntimeEnv, - userdata: []coral.io.Byte, -}; - pub const RuntimeEnv = struct { - interned_symbols: RefTable, + interned_symbols: SymbolSet, allocator: coral.io.Allocator, error_handler: ErrorHandler, syscallables: RefTable, - local_refs: RefStack, + locals: RefList, frames: FrameStack, - ref_values: RefSlab, - const FrameStack = coral.list.Stack(Frame); + const Chunk = struct { + env: *RuntimeEnv, + name: []coral.io.Byte, + opcodes: OpcodeList, + constants: ConstList, - const RefStack = coral.list.Stack(?*RuntimeRef); - - const RefTable = coral.map.StringTable(*RuntimeRef); - - const RefSlab = coral.map.Slab(struct { - ref_count: usize, - - object: union (enum) { - false, - true, - float: Float, + const Constant = union (enum) { fixed: Fixed, - string: []coral.io.Byte, - symbol: []coral.io.Byte, - lambda: Caller, - dynamic: *DynamicObject, - }, - }); + float: Float, + string: []const coral.io.Byte, + symbol: []const coral.io.Byte, + }; - pub fn acquire(self: *RuntimeEnv, ref: *const RuntimeRef) *RuntimeRef { - const key = @intFromPtr(ref); - var ref_data = self.ref_values.remove(key); + const Opcode = union (enum) { + push_nil, + push_true, + push_false, + push_const: u16, + push_local: u8, + push_table: u32, + push_system: u16, + local_set: u8, + object_get, + object_set, + object_call: u8, - coral.debug.assert(ref_data != null); + not, + neg, - ref_data.?.ref_count += 1; + add, + sub, + mul, + div, - coral.debug.assert(self.ref_values.insert_at(key, ref_data.?)); + eql, + cgt, + clt, + cge, + cle, + }; - return @ptrFromInt(key); - } + const OpcodeList = coral.list.Stack(Opcode); - pub fn call( - self: *RuntimeEnv, - callable_ref: *const RuntimeRef, - arg_count: u8, - name: []const coral.io.Byte, - ) RuntimeError!?*RuntimeRef { - try self.frames.push_one(.{ - .name = name, - .arg_count = arg_count, - .locals_top = self.local_refs.values.len, - }); + const CompilationUnit = struct { + local_identifiers_buffer: [255][]const coral.io.Byte = [_][]const coral.io.Byte{""} ** 255, + local_identifiers_count: u8 = 0, - defer { - const frame = self.frames.pop() orelse unreachable; + fn compile_expression(self: *CompilationUnit, chunk: *Chunk, expression: Ast.Expression) RuntimeError!void { + const number_format = coral.utf8.DecimalFormat{ + .delimiter = "_", + .positive_prefix = .none, + }; - { - var pops_remaining = (self.local_refs.values.len - frame.locals_top) + frame.arg_count; + switch (expression) { + .nil_literal => try chunk.opcodes.push_one(.push_nil), + .true_literal => try chunk.opcodes.push_one(.push_true), + .false_literal => try chunk.opcodes.push_one(.push_false), - while (pops_remaining != 0) : (pops_remaining -= 1) { - self.discard(self.local_refs.pop() orelse unreachable); + .number_literal => |literal| { + for (literal) |codepoint| { + if (codepoint == '.') { + return chunk.opcodes.push_one(.{ + .push_const = try chunk.declare_constant(.{ + .float = number_format.parse(literal, Float) orelse unreachable, + }), + }); + } + } + + try chunk.opcodes.push_one(.{ + .push_const = try chunk.declare_constant(.{ + .fixed = number_format.parse(literal, Fixed) orelse unreachable, + }), + }); + }, + + .string_literal => |literal| { + try chunk.opcodes.push_one(.{ + .push_const = try chunk.declare_constant(.{.string = literal}), + }); + }, + + .symbol_literal => |literal| { + try chunk.opcodes.push_one(.{ + .push_const = try chunk.declare_constant(.{.symbol = 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(chunk, field.value_expression); + try self.compile_expression(chunk, field.key_expression); + } + + try chunk.opcodes.push_one(.{.push_table = @intCast(fields.values.len)}); + }, + + .binary_operation => |operation| { + try self.compile_expression(chunk, operation.lhs_expression.*); + try self.compile_expression(chunk, operation.rhs_expression.*); + + try chunk.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(chunk, operation.expression.*); + + try chunk.opcodes.push_one(switch (operation.operator) { + .boolean_negation => .not, + .numeric_negation => .neg, + }); + }, + + .invoke => |invoke| { + if (invoke.argument_expressions.values.len > coral.math.max_int(@typeInfo(u8).Int)) { + return chunk.env.raise(error.BadSyntax, "lambdas may contain a maximum of 255 arguments"); + } + + for (invoke.argument_expressions.values) |argument_expression| { + try self.compile_expression(chunk, argument_expression); + } + + try self.compile_expression(chunk, invoke.object_expression.*); + try chunk.opcodes.push_one(.{.object_call = @intCast(invoke.argument_expressions.values.len)}); + }, + + .grouped_expression => |grouped_expression| { + try self.compile_expression(chunk, grouped_expression.*); + }, + + .get_system => |get_system| { + try chunk.opcodes.push_one(.{ + .push_system = try chunk.declare_constant(.{.symbol = get_system}), + }); + }, + + .local_get => |local_get| { + try chunk.opcodes.push_one(.{ + .push_local = self.resolve_local(local_get) orelse { + return chunk.env.raise(error.OutOfMemory, "undefined local"); + }, + }); + }, + + .local_set => |local_set| { + if (self.resolve_local(local_set)) |index| { + try chunk.opcodes.push_one(.{.local_set = index}); + } else { + if (self.local_identifiers_count == self.local_identifiers_buffer.len) { + return chunk.env.raise(error.BadSyntax, "chunks may have a maximum of 255 locals"); + } + + self.local_identifiers_buffer[self.local_identifiers_count] = local_set; + self.local_identifiers_count += 1; + } + }, + + .field_get => |field_get| { + try self.compile_expression(chunk, field_get.object_expression.*); + + try chunk.opcodes.push_one(.{ + .push_const = try chunk.declare_constant(.{.symbol = field_get.identifier}), + }); + + try chunk.opcodes.push_one(.object_get); + }, + + .field_set => |field_set| { + try self.compile_expression(chunk, field_set.object_expression.*); + + try chunk.opcodes.push_one(.{ + .push_const = try chunk.declare_constant(.{.symbol = field_set.identifier}), + }); + + try self.compile_expression(chunk, field_set.value_expression.*); + try chunk.opcodes.push_one(.object_set); + }, } } + + fn compile_statement(self: *CompilationUnit, chunk: *Chunk, statement: Ast.Statement) RuntimeError!void { + switch (statement) { + .@"return" => |@"return"| { + if (@"return".expression) |expression| { + try self.compile_expression(chunk, expression); + } else { + try chunk.opcodes.push_one(.push_nil); + } + }, + + .expression => |expression| try self.compile_expression(chunk, expression), + } + } + + fn resolve_local(self: *CompilationUnit, 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; + } + } + } + }; + + fn compile(self: *Chunk, ast: Ast) RuntimeError!void { + var unit = CompilationUnit{}; + + for (ast.list_statements()) |statement| { + try unit.compile_statement(self, statement); + } } - return switch ((self.ref_values.lookup(@intFromPtr(callable_ref)) orelse unreachable).object) { - .lambda => |lambda| lambda.invoke(self), + fn declare_constant(self: *Chunk, constant: Constant) RuntimeError!u16 { + if (self.constants.values.len == coral.math.max_int(@typeInfo(u16).Int)) { + return self.env.raise(error.BadSyntax, "chunks cannot contain more than 65,535 constants"); + } + const constant_index = self.constants.values.len; + + try self.constants.push_one(try switch (constant) { + .fixed => |fixed| self.env.new_fixed(fixed), + .float => |float| self.env.new_float(float), + .string => |string| self.env.new_string(string), + .symbol => |symbol| self.env.new_symbol(symbol), + }); + + return @intCast(constant_index); + } + + fn execute(self: *Chunk) RuntimeError!?*RuntimeRef { + try self.env.frames.push_one(.{ + .arg_count = 0, + .locals_top = self.env.locals.values.len, + .name = self.name, + }); + + defer coral.debug.assert(self.env.frames.pop() != null); + + for (self.opcodes.values) |opcode| { + switch (opcode) { + .push_nil => try self.env.locals.push_one(null), + .push_true => try self.env.locals.push_one(try self.env.new_boolean(true)), + .push_false => try self.env.locals.push_one(try self.env.new_boolean(false)), + + .push_const => |push_const| { + if (push_const >= self.constants.values.len) { + return self.env.raise(error.IllegalState, "invalid const"); + } + + try self.env.locals.push_one(try self.env.acquire(self.constants.values[push_const])); + }, + + .push_local => |push_local| { + if (self.env.locals.values[push_local]) |local| { + try self.env.locals.push_one(try self.env.acquire(local)); + } else { + try self.env.locals.push_one(null); + } + }, + + .push_table => |push_table| { + const table = try self.env.new_table(); + + errdefer self.env.discard(table); + + { + const dynamic = table.object().payload.dynamic; + const userdata = dynamic.userdata(); + var popped = @as(usize, 0); + + while (popped < push_table) : (popped += 1) { + const index = try self.env.expect(try self.pop_local()); + + defer self.env.discard(index); + + const maybe_value = try self.pop_local(); + + defer { + if (maybe_value) |value| { + self.env.discard(value); + } + } + + try dynamic.typeinfo().set(.{ + .userdata = userdata, + .env = self.env, + }, index, maybe_value); + } + } + + try self.env.locals.push_one(table); + }, + + .push_system => |push_system| { + if (self.env.syscallables.lookup(self.constants.values[push_system])) |syscallable| { + try self.env.locals.push_one(try self.env.acquire(syscallable)); + } else { + try self.env.locals.push_one(null); + } + }, + + .local_set => |local_set| { + const local = &self.env.locals.values[local_set]; + + if (local.*) |previous_local| { + self.env.discard(previous_local); + } + + local.* = try self.pop_local(); + }, + + .object_get => { + const index = (try self.pop_local()) orelse { + return self.env.raise(error.TypeMismatch, "nil is not a valid index"); + }; + + defer self.env.discard(index); + + const indexable = (try self.pop_local()) orelse { + return self.env.raise(error.TypeMismatch, "nil is not a valid indexable"); + }; + + defer self.env.discard(indexable); + + const value = try self.env.get(indexable, index); + + errdefer { + if (value) |ref| { + self.env.discard(ref); + } + } + + try self.env.locals.push_one(value); + }, + + .object_set => { + const value = try self.pop_local(); + + defer { + if (value) |ref| { + self.env.discard(ref); + } + } + + const index = try self.pop_local() orelse { + return self.env.raise(error.TypeMismatch, "nil is not a valid index"); + }; + + defer self.env.discard(index); + + const indexable = try self.pop_local() orelse { + return self.env.raise(error.TypeMismatch, "nil is not a valid indexable"); + }; + + defer self.env.discard(indexable); + + try self.env.set(indexable, index, value); + }, + + .object_call => { + const result = call: { + const callable = try self.env.expect(try self.pop_local()); + + defer self.env.discard(callable); + + break: call try switch (callable.object().payload) { + .dynamic => |dynamic| dynamic.typeinfo().call(.{ + .userdata = dynamic.userdata(), + .env = self.env, + }), + + else => self.env.raise(error.TypeMismatch, "object is not callable"), + }; + }; + + errdefer { + if (result) |ref| { + self.env.discard(ref); + } + } + + try self.env.locals.push_one(result); + }, + + .not => { + if (try self.pop_local()) |value| { + defer self.env.discard(value); + + try self.env.locals.push_one(try self.env.new_boolean(!value.is_truthy())); + } else { + try self.env.locals.push_one(try self.env.new_boolean(true)); + } + }, + + .neg => { + const value = try self.env.expect(try self.pop_local()); + + defer self.env.discard(value); + + try self.env.locals.push_one(try switch (value.object().payload) { + .fixed => |fixed| self.env.new_fixed(-fixed), + .float => |float| self.env.new_float(-float), + else => self.env.raise(error.TypeMismatch, "object is not negatable"), + }); + }, + + .add => { + const rhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(rhs); + + const lhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(lhs); + + try self.env.locals.push_one(try switch (lhs.object().payload) { + .fixed => |lhs_fixed| switch (rhs.object().payload) { + .fixed => |rhs_fixed| add: { + if (coral.math.checked_add(lhs_fixed, rhs_fixed)) |fixed| { + break: add self.env.new_fixed(fixed); + } + + break: add self.env.new_float(@as(Float, + @floatFromInt(lhs_fixed)) + + @as(Float, @floatFromInt(rhs_fixed))); + }, + + .float => |rhs_float| self.env.new_float( + @as(Float, @floatFromInt(lhs_fixed)) + rhs_float), + + else => self.env.raise(error.TypeMismatch, "right-hand object is not addable"), + }, + + .float => |lhs_float| switch (rhs.object().payload) { + .float => |rhs_float| self.env.new_float(lhs_float + rhs_float), + + .fixed => |rhs_fixed| self.env.new_float( + lhs_float + @as(Float, @floatFromInt(rhs_fixed))), + + else => self.env.raise(error.TypeMismatch, "right-hand object is not addable"), + }, + + else => self.env.raise(error.TypeMismatch, "left-hand object is not addable"), + }); + }, + + .sub => { + const rhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(rhs); + + const lhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(lhs); + + try self.env.locals.push_one(try switch (lhs.object().payload) { + .fixed => |lhs_fixed| switch (rhs.object().payload) { + .fixed => |rhs_fixed| sub: { + if (coral.math.checked_sub(lhs_fixed, rhs_fixed)) |fixed| { + break: sub self.env.new_fixed(fixed); + } + + break: sub self.env.new_float(@as(Float, + @floatFromInt(lhs_fixed)) - + @as(Float, @floatFromInt(rhs_fixed))); + }, + + .float => |rhs_float| self.env.new_float( + @as(Float, @floatFromInt(lhs_fixed)) - rhs_float), + + else => self.env.raise(error.TypeMismatch, "right-hand object is not subtractable"), + }, + + .float => |lhs_float| switch (rhs.object().payload) { + .float => |rhs_float| self.env.new_float(lhs_float - rhs_float), + + .fixed => |rhs_fixed| self.env.new_float( + lhs_float - @as(Float, @floatFromInt(rhs_fixed))), + + else => self.env.raise(error.TypeMismatch, "right-hand object is not subtractable"), + }, + + else => self.env.raise(error.TypeMismatch, "left-hand object is not subtractable"), + }); + }, + + .mul => { + const rhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(rhs); + + const lhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(lhs); + + try self.env.locals.push_one(try switch (lhs.object().payload) { + .fixed => |lhs_fixed| switch (rhs.object().payload) { + .fixed => |rhs_fixed| mul: { + if (coral.math.checked_mul(lhs_fixed, rhs_fixed)) |fixed| { + break: mul self.env.new_fixed(fixed); + } + + break: mul self.env.new_float(@as(Float, + @floatFromInt(lhs_fixed)) * + @as(Float, @floatFromInt(rhs_fixed))); + }, + + .float => |rhs_float| self.env.new_float( + @as(Float, @floatFromInt(lhs_fixed)) * rhs_float), + + else => self.env.raise(error.TypeMismatch, "right-hand object is not multiplicable"), + }, + + .float => |lhs_float| switch (rhs.object().payload) { + .float => |rhs_float| self.env.new_float(lhs_float * rhs_float), + + .fixed => |rhs_fixed| self.env.new_float( + lhs_float * @as(Float, @floatFromInt(rhs_fixed))), + + else => self.env.raise(error.TypeMismatch, "right-hand object is not multiplicable"), + }, + + else => self.env.raise(error.TypeMismatch, "left-hand object is not multiplicable"), + }); + }, + + .div => { + const rhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(rhs); + + const lhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(lhs); + + try self.env.locals.push_one(try switch (lhs.object().payload) { + .fixed => |lhs_fixed| switch (rhs.object().payload) { + .fixed => |rhs_fixed| self.env.new_float(@as(Float, + @floatFromInt(lhs_fixed)) / + @as(Float, @floatFromInt(rhs_fixed))), + + .float => |rhs_float| self.env.new_float( + @as(Float, @floatFromInt(lhs_fixed)) / rhs_float), + + else => self.env.raise(error.TypeMismatch, "right-hand object is not divisible"), + }, + + .float => |lhs_float| switch (rhs.object().payload) { + .float => |rhs_float| self.env.new_float(lhs_float / rhs_float), + + .fixed => |rhs_fixed| self.env.new_float( + lhs_float / @as(Float, @floatFromInt(rhs_fixed))), + + else => self.env.raise(error.TypeMismatch, "right-hand object is not divisible"), + }, + + else => self.env.raise(error.TypeMismatch, "left-hand object is not divisible"), + }); + }, + + .eql => { + if (try self.pop_local()) |rhs| { + self.env.discard(rhs); + + if (try self.pop_local()) |lhs| { + self.env.discard(lhs); + + try self.env.locals.push_one(try self.env.new_boolean(lhs.equals(rhs))); + } else { + try self.env.locals.push_one(try self.env.new_boolean(false)); + } + } else { + if (try self.pop_local()) |lhs| { + self.env.discard(lhs); + + try self.env.locals.push_one(try self.env.new_boolean(false)); + } else { + try self.env.locals.push_one(try self.env.new_boolean(true)); + } + } + }, + + .cgt => { + const rhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(rhs); + + const lhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(lhs); + + try self.env.locals.push_one(try self.env.new_boolean(switch (lhs.object().payload) { + .fixed => |lhs_fixed| switch (rhs.object().payload) { + .fixed => |rhs_fixed| lhs_fixed > rhs_fixed, + .float => |rhs_float| @as(Float, @floatFromInt(lhs_fixed)) > rhs_float, + else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"), + }, + + .float => |lhs_float| switch (rhs.object().payload) { + .float => |rhs_float| lhs_float > rhs_float, + .fixed => |rhs_fixed| lhs_float > @as(Float, @floatFromInt(rhs_fixed)), + else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"), + }, + + else => return self.env.raise(error.TypeMismatch, "left-hand object is not comparable"), + })); + }, + + .clt => { + const rhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(rhs); + + const lhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(lhs); + + try self.env.locals.push_one(try self.env.new_boolean(switch (lhs.object().payload) { + .fixed => |lhs_fixed| switch (rhs.object().payload) { + .fixed => |rhs_fixed| lhs_fixed < rhs_fixed, + .float => |rhs_float| @as(Float, @floatFromInt(lhs_fixed)) < rhs_float, + else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"), + }, + + .float => |lhs_float| switch (rhs.object().payload) { + .float => |rhs_float| lhs_float < rhs_float, + .fixed => |rhs_fixed| lhs_float < @as(Float, @floatFromInt(rhs_fixed)), + else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"), + }, + + else => return self.env.raise(error.TypeMismatch, "left-hand object is not comparable"), + })); + }, + + .cge => { + const rhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(rhs); + + const lhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(lhs); + + try self.env.locals.push_one(try self.env.new_boolean(switch (lhs.object().payload) { + .fixed => |lhs_fixed| switch (rhs.object().payload) { + .fixed => |rhs_fixed| lhs_fixed >= rhs_fixed, + .float => |rhs_float| @as(Float, @floatFromInt(lhs_fixed)) >= rhs_float, + else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"), + }, + + .float => |lhs_float| switch (rhs.object().payload) { + .float => |rhs_float| lhs_float >= rhs_float, + .fixed => |rhs_fixed| lhs_float >= @as(Float, @floatFromInt(rhs_fixed)), + else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"), + }, + + else => return self.env.raise(error.TypeMismatch, "left-hand object is not comparable"), + })); + }, + + .cle => { + const rhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(rhs); + + const lhs = try self.env.expect(try self.pop_local()); + + defer self.env.discard(lhs); + + try self.env.locals.push_one(try self.env.new_boolean(switch (lhs.object().payload) { + .fixed => |lhs_fixed| switch (rhs.object().payload) { + .fixed => |rhs_fixed| lhs_fixed <= rhs_fixed, + .float => |rhs_float| @as(Float, @floatFromInt(lhs_fixed)) <= rhs_float, + else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"), + }, + + .float => |lhs_float| switch (rhs.object().payload) { + .float => |rhs_float| lhs_float <= rhs_float, + .fixed => |rhs_fixed| lhs_float <= @as(Float, @floatFromInt(rhs_fixed)), + else => return self.env.raise(error.TypeMismatch, "right-hand object is not comparable"), + }, + + else => return self.env.raise(error.TypeMismatch, "left-hand object is not comparable"), + })); + }, + + } + } + + return self.pop_local(); + } + + fn free(self: *Chunk) void { + while (self.constants.pop()) |constant| { + self.env.discard(constant); + } + + self.constants.free(); + self.opcodes.free(); + self.env.allocator.deallocate(self.name); + } + + fn make(env: *RuntimeEnv, name: []const coral.io.Byte) coral.io.AllocationError!Chunk { + return .{ + .name = try coral.io.allocate_copy(env.allocator, name), + .opcodes = OpcodeList.make(env.allocator), + .constants = ConstList.make(env.allocator), + .env = env, + }; + } + + fn pop_local(self: *Chunk) RuntimeError!?*RuntimeRef { + return self.env.locals.pop() orelse self.env.raise(error.IllegalState, "stack underflow"); + } + }; + + const ConstList = coral.list.Stack(*RuntimeRef); + + const FrameStack = coral.list.Stack(Frame); + + const RefList = coral.list.Stack(?*RuntimeRef); + + const RefTable = coral.map.Table(*RuntimeRef, *RuntimeRef, struct { + pub const hash = RuntimeRef.hash; + + pub const equals = RuntimeRef.equals; + }); + + const SymbolSet = coral.map.StringTable([:0]coral.io.Byte); + + pub fn acquire(self: *RuntimeEnv, value: *const RuntimeRef) RuntimeError!*RuntimeRef { + const object = value.object(); + + object.ref_count = coral.math.checked_add(object.ref_count, 1) orelse { + return self.raise(error.IllegalState, "reference overflow"); + }; + + return @ptrCast(object); + } + + pub fn arg(self: *RuntimeEnv, index: usize) RuntimeError!?*RuntimeRef { + const frame = self.frames.peek() orelse return self.raise(error.IllegalState, "stack underflow"); + + if (index < frame.arg_count) { + if (self.locals.values[frame.locals_top - (1 + index)]) |local| { + return self.acquire(local); + } + } + + return null; + } + + pub fn bind_syscaller(_: *RuntimeEnv, comptime _: []const coral.io.Byte, _: Caller) RuntimeError!void { + + } + + pub fn call(self: *RuntimeEnv, callable: *RuntimeRef, args: []const *RuntimeRef) RuntimeError!?*RuntimeRef { + try self.locals.push_all(args); + + defer coral.io.assert(self.locals.drop(args.len)); + + try self.frames.push_one(.{ + .name = "", + .arg_count = args.len, + .locals_top = self.locals.values.len, + }); + + defer coral.io.assert(self.frames.pop() != null); + + return switch (callable.object().payload) { .dynamic => |dynamic| dynamic.typeinfo.call(.{ + .userdata = dynamic.userdata(), .env = self, - .userdata = dynamic.userdata, }), else => self.raise(error.TypeMismatch, "object is not callable"), }; } - pub fn bind_syscaller(self: *RuntimeEnv, comptime name: []const coral.io.Byte, caller: Caller) RuntimeError!void { - const lambda_ref = try self.new_lambda(caller); + pub fn discard(self: *RuntimeEnv, value: *RuntimeRef) void { + var object = value.object(); - if (try self.syscallables.replace(name, self.acquire(lambda_ref))) |replaced_entry| { - self.discard(replaced_entry.value); - } - } + coral.debug.assert(object.ref_count != 0); - pub fn discard(self: *RuntimeEnv, ref: ?*RuntimeRef) void { - const key = @intFromPtr(ref orelse return); - var ref_data = self.ref_values.remove(key) orelse unreachable; + object.ref_count -= 1; - coral.debug.assert(ref_data.ref_count != 0); + if (object.ref_count == 0) { + switch (object.payload) { + .false, .true, .float, .fixed, .symbol => {}, - ref_data.ref_count -= 1; - - if (ref_data.ref_count == 0) { - switch (ref_data.object) { - .false, .true, .float, .fixed, .lambda => {}, - .string => |string| self.allocator.deallocate(string), - .symbol => |symbol| self.allocator.deallocate(symbol), + .string => |string| { + coral.debug.assert(string.len >= 0); + self.allocator.deallocate(string.ptr[0 .. @intCast(string.len)]); + }, .dynamic => |dynamic| { - dynamic.typeinfo.clean(.{ - .env = self, - .userdata = dynamic.userdata, - }); + if (dynamic.typeinfo().destruct) |destruct| { + destruct(.{ + .userdata = dynamic.userdata(), + .env = self, + }); + } - self.allocator.deallocate(dynamic.userdata); - self.allocator.deallocate(dynamic); + self.allocator.deallocate(dynamic.unpack()); }, } - } else { - coral.debug.assert(self.ref_values.insert_at(key, ref_data)); + + self.allocator.deallocate(object); } } pub fn execute_file(self: *RuntimeEnv, file_access: file.Access, file_path: file.Path) RuntimeError!?*RuntimeRef { - if ((try file.allocate_and_load(self.allocator, file_access, file_path))) |file_data| { - defer self.allocator.deallocate(file_data); + const file_data = (try file.allocate_and_load(self.allocator, file_access, file_path)) orelse { + return self.raise(error.BadOperation, "failed to open or read file specified"); + }; - if (file_path.to_string()) |string| { - return self.execute_script(string, file_data); - } - } + defer self.allocator.deallocate(file_data); - return self.raise(error.BadOperation, "failed to load file"); - } - - pub fn execute_script( - self: *RuntimeEnv, - name: []const coral.io.Byte, - data: []const coral.io.Byte, - ) RuntimeError!?*RuntimeRef { - var ast = Ast.make(self.allocator, name); + const file_name = file_path.to_string() orelse "