diff --git a/.vscode/launch.json b/.vscode/launch.json index 8eeeb8d..5a4674f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,8 @@ "request": "launch", "target": "${workspaceRoot}/zig-out/bin/runner", "cwd": "${workspaceRoot}/debug/", - "valuesFormatting": "parseText" + "valuesFormatting": "parseText", + "preLaunchTask": "Build All" }, ] } diff --git a/debug/app.ona b/debug/app.ona index 042320c..ea265f4 100644 --- a/debug/app.ona +++ b/debug/app.ona @@ -1,10 +1,13 @@ # Test comment. -@log_info("game is loading") -return { - title = "Afterglow", - width = 1280, - height = 800, - tick_rate = 60, +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 ad969c9..39025ee 100644 --- a/source/coral/io.zig +++ b/source/coral/io.zig @@ -2,6 +2,8 @@ const debug = @import("./debug.zig"); const math = @import("./math.zig"); +const std = @import("std"); + pub const AllocationError = error { OutOfMemory, }; @@ -27,7 +29,7 @@ pub const Allocator = struct { const ErasedActions = struct { fn deallocate(context: *anyopaque, allocation: []Byte) void { if (is_zero_aligned) { - return actions.deallocator(@ptrCast(context), allocation); + return actions.deallocate(@ptrCast(context), allocation); } return actions.deallocate(@ptrCast(@alignCast(context)), allocation); @@ -35,7 +37,7 @@ pub const Allocator = struct { fn reallocate(context: *anyopaque, return_address: usize, existing_allocation: ?[]Byte, size: usize) AllocationError![]Byte { if (is_zero_aligned) { - return actions.reallocator(@ptrCast(context), return_address, existing_allocation, size); + return actions.reallocate(@ptrCast(context), return_address, existing_allocation, size); } return actions.reallocate(@ptrCast(@alignCast(context)), return_address, existing_allocation, size); @@ -213,6 +215,27 @@ pub fn allocate_copy(allocator: Allocator, source: []const Byte) AllocationError return allocation; } +pub fn allocate_many(allocator: Allocator, count: usize, value: anytype) AllocationError![]@TypeOf(value) { + const Type = @TypeOf(value); + const typeSize = @sizeOf(Type); + + if (typeSize == 0) { + @compileError("Cannot allocate memory for 0-byte sized type " ++ @typeName(Type)); + } + + const allocations = @as([*]Type, @ptrCast(@alignCast(try allocator.actions.reallocate( + allocator.context, + @returnAddress(), + null, + typeSize * count))))[0 .. count]; + + for (allocations) |*allocation| { + allocation.* = value; + } + + return allocations; +} + pub fn allocate_one(allocator: Allocator, value: anytype) AllocationError!*@TypeOf(value) { const Type = @TypeOf(value); const typeSize = @sizeOf(Type); @@ -232,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; @@ -265,6 +298,16 @@ pub fn compare(this: []const Byte, that: []const Byte) isize { return @as(isize, @intCast(this.len)) - @as(isize, @intCast(that.len)); } +pub fn djb2_hash(comptime int: std.builtin.Type.Int, target: []const Byte) math.Int(int) { + var hash_code = @as(math.Int(int), 5381); + + for (target) |byte| { + hash_code = ((hash_code << 5) +% hash_code) +% byte; + } + + return hash_code; +} + pub fn ends_with(target: []const Byte, match: []const Byte) bool { if (target.len < match.len) { return false; @@ -297,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/list.zig b/source/coral/list.zig index f02b6ee..56797d8 100644 --- a/source/coral/list.zig +++ b/source/coral/list.zig @@ -41,6 +41,10 @@ pub fn Stack(comptime Value: type) type { self.capacity = grown_capacity; } + pub fn is_empty(self: Self) bool { + return self.values.len == 0; + } + pub fn make(allocator: io.Allocator) Self { return .{ .allocator = allocator, diff --git a/source/coral/map.zig b/source/coral/map.zig index 1bc6a96..30a8bdb 100644 --- a/source/coral/map.zig +++ b/source/coral/map.zig @@ -6,118 +6,27 @@ 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, - }); - +pub fn StringTable(comptime Value: type) type { + return Table([]const io.Byte, Value, struct { 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, - }; + fn equals(key_a: []const io.Byte, key_b: []const io.Byte) bool { + return io.equals(key_a, key_b); } - pub fn free(self: *Self) void { - self.entries.free(); - - self.next_index = 0; + fn hash(key: []const io.Byte) usize { + return io.djb2_hash(@typeInfo(usize).Int, key); } - - 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, .{ - .hash = hash_string, - .match = io.equals, }); } -pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTraits(Key)) 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, + traits: Traits, count: usize, entries: []?Entry, @@ -125,24 +34,26 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra key: Key, value: Value, - fn write_into(self: Entry, entry_table: []?Entry) bool { - const hash_max = @min(max_int, entry_table.len); - var hashed_key = math.wrap(traits.hash(self.key), min_int, hash_max); + fn write_into(self: Entry, table: *Self) bool { + const hash_max = @min(max_int, table.entries.len); + var hashed_key = table.hash_key(self.key) % hash_max; var iterations = @as(usize, 0); while (true) : (iterations += 1) { - debug.assert(iterations < entry_table.len); + debug.assert(iterations < table.entries.len); - const table_entry = &(entry_table[hashed_key] orelse { - entry_table[hashed_key] = .{ + const table_entry = &(table.entries[hashed_key] orelse { + table.entries[hashed_key] = .{ .key = self.key, .value = self.value, }; + table.count += 1; + return true; }); - if (traits.match(table_entry.key, self.key)) { + if (table.keys_equal(table_entry.key, self.key)) { return false; } @@ -177,14 +88,18 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra }; } + 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(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 (traits.match(entry.key, key)) { + if (self.keys_equal(entry.key, key)) { const original_entry = entry.*; self.entries[hashed_key] = null; @@ -203,7 +118,8 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra { const hash_max = @min(max_int, self.entries.len); - var hashed_key = math.wrap(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 { @@ -217,15 +133,28 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra return null; }); - if (traits.match(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; @@ -261,14 +190,20 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra debug.assert(self.entries.len > self.count); - defer self.count += 1; - const entry = Entry{ .key = key, .value = value, }; - return entry.write_into(self.entries); + 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 { @@ -277,14 +212,21 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra } const hash_max = @min(max_int, self.entries.len); - var hashed_key = math.wrap(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 (traits.match(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; @@ -293,11 +235,12 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra return null; } - pub fn make(allocator: io.Allocator) Self { + pub fn make(allocator: io.Allocator, traits: Traits) Self { return .{ .allocator = allocator, .count = 0, .entries = &.{}, + .traits = traits, }; } @@ -306,46 +249,26 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime traits: TableTra return; } - const min_count = @max(1, self.count); - const table_size = min_count * 2; - const allocation = @as([*]?Entry, @ptrCast(@alignCast(try self.allocator.reallocate(null, @sizeOf(?Entry) * table_size))))[0 .. table_size]; + var table = make(self.allocator, self.traits); - errdefer self.allocator.deallocate(allocation); + errdefer table.free(); - self.entries = replace_table: { - for (allocation) |*entry| { - entry.* = null; - } + table.entries = allocate: { + const min_count = @max(1, self.count); + const table_size = min_count * 2; - if (self.entries.len != 0) { - for (self.entries) |maybe_entry| { - if (maybe_entry) |entry| { - debug.assert(entry.write_into(allocation)); - } - } - - self.allocator.deallocate(self.entries); - } - - break: replace_table allocation; + break: allocate try io.allocate_many(self.allocator, table_size, @as(?Entry, null)); }; + + for (self.entries) |maybe_entry| { + if (maybe_entry) |entry| { + debug.assert(entry.write_into(&table)); + } + } + + self.free(); + + self.* = table; } }; } - -pub fn TableTraits(comptime Key: type) type { - return struct { - hash: fn (key: Key) usize, - match: fn (key: Key, key: Key) bool, - }; -} - -fn hash_string(key: []const io.Byte) usize { - var hash_code = @as(usize, 5381); - - for (key) |byte| { - hash_code = ((hash_code << 5) +% hash_code) +% byte; - } - - return hash_code; -} diff --git a/source/coral/math.zig b/source/coral/math.zig index de8d37f..360da13 100644 --- a/source/coral/math.zig +++ b/source/coral/math.zig @@ -8,6 +8,24 @@ pub fn clamp(value: anytype, lower: anytype, upper: anytype) @TypeOf(value, lowe return @max(lower, @min(upper, value)); } +pub fn checked_add(a: anytype, b: anytype) ?@TypeOf(a + b) { + const result = @addWithOverflow(a, b); + + return if (result.@"1" == 0) result.@"0" else null; +} + +pub fn checked_mul(a: anytype, b: anytype) ?@TypeOf(a * b) { + const result = @mulWithOverflow(a, b); + + return if (result.@"1" == 0) result.@"0" else null; +} + +pub fn checked_sub(a: anytype, b: anytype) ?@TypeOf(a - b) { + const result = @subWithOverflow(a, b); + + return if (result.@"1" == 0) result.@"0" else null; +} + pub fn clamped_cast(comptime dest_int: std.builtin.Type.Int, value: anytype) Int(dest_int) { const Value = @TypeOf(value); @@ -17,6 +35,8 @@ pub fn clamped_cast(comptime dest_int: std.builtin.Type.Int, value: anytype) Int .unsigned => @intCast(@min(value, max_int(dest_int))), }, + .Float => @intFromFloat(clamp(value, min_int(dest_int), max_int(dest_int))), + else => @compileError("`" ++ @typeName(Value) ++ "` cannot be cast to an int"), }; } @@ -40,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/coral/utf8.zig b/source/coral/utf8.zig index 62fccbc..97fb597 100644 --- a/source/coral/utf8.zig +++ b/source/coral/utf8.zig @@ -33,9 +33,10 @@ pub const DecimalFormat = struct { switch (code) { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { - result = try math.checked_add( - try math.checked_mul(result, radix), - try math.checked_sub(code, '0')); + const offset_code = math.checked_sub(code, '0') orelse return null; + + result = math.checked_mul(result, radix) orelse return null; + result = math.checked_add(result, offset_code) orelse return null; }, else => { diff --git a/source/ona/app.zig b/source/ona/app.zig index 6450445..81d51f5 100644 --- a/source/ona/app.zig +++ b/source/ona/app.zig @@ -13,18 +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"})); + 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 (env.unbox(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); } } @@ -32,13 +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 (env.unbox(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); } } @@ -46,27 +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 (env.unbox(ref).expect_number()) |number| { - break: get @floatCast(number); + break: get @floatCast(try env.unbox_float(ref)); } break: get self.tick_rate; }); - { - const title_ref = try kym.get_field(env, manifest_ref, "title"); + if (try env.get_field(manifest, "title")) |ref| { + defer env.discard(ref); - defer env.discard(title_ref); - - const title_string = env.unbox(title_ref).expect_string() orelse ""; + 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/heap.zig b/source/ona/heap.zig index 8639461..8f5d785 100644 --- a/source/ona/heap.zig +++ b/source/ona/heap.zig @@ -159,7 +159,7 @@ pub fn trace_leaks() void { var current_node = context.head; while (current_node) |node| : (current_node = node.next) { - std.debug.print("{d} byte leak at 0x{x} detected:\n", .{ + std.debug.print("{d} byte leak at 0x{x} detected", .{ node.size, @intFromPtr(node) + @sizeOf(AllocationNode), }); diff --git a/source/ona/kym.zig b/source/ona/kym.zig index 0553851..0ee0442 100644 --- a/source/ona/kym.zig +++ b/source/ona/kym.zig @@ -1,57 +1,11 @@ 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) { - nil, - boolean: bool, - number: Float, - string: []const coral.io.Byte, - dynamic: *DynamicObject, - - pub fn expect_dynamic(self: Any) ?*DynamicObject { - return switch (self) { - .dynamic => |dynamic| dynamic, - else => null, - }; - } - - pub fn expect_number(self: Any) ?Float { - return switch (self) { - .number => |number| number, - 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, - - pub fn as_caller(self: *DynamicObject) Caller { - return Caller.bind(DynamicObject, self, DynamicObject.call); - } - - fn call(self: *DynamicObject, env: *RuntimeEnv) RuntimeError!?*RuntimeRef { - return self.typeinfo.call(env); - } -}; - pub const ErrorHandler = coral.io.Generator(void, ErrorInfo); pub const ErrorInfo = struct { @@ -59,6 +13,8 @@ pub const ErrorInfo = struct { frames: []const Frame, }; +pub const Fixed = i32; + pub const Float = f64; pub const Frame = struct { @@ -67,254 +23,1154 @@ pub const Frame = struct { locals_top: usize, }; -pub const IndexContext = struct { - env: *RuntimeEnv, - userdata: []coral.io.Byte, - index_ref: ?*const RuntimeRef, -}; - pub const RuntimeEnv = struct { + interned_symbols: SymbolSet, allocator: coral.io.Allocator, error_handler: ErrorHandler, - syscallers: SyscallerTable, - local_refs: RefStack, + system_bindings: RefTable, + locals: RefList, frames: FrameStack, - ref_values: RefSlab, + + const Chunk = struct { + env: *RuntimeEnv, + name: []coral.io.Byte, + opcodes: OpcodeList, + constants: ConstList, + + const Constant = union (enum) { + fixed: Fixed, + float: Float, + string: []const coral.io.Byte, + symbol: []const coral.io.Byte, + }; + + 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, + + not, + neg, + + add, + sub, + mul, + div, + + eql, + cgt, + clt, + cge, + cle, + }; + + const OpcodeList = coral.list.Stack(Opcode); + + const CompilationUnit = struct { + local_identifiers_buffer: [255][]const coral.io.Byte = [_][]const coral.io.Byte{""} ** 255, + local_identifiers_count: u8 = 0, + + fn compile_expression(self: *CompilationUnit, chunk: *Chunk, expression: Ast.Expression) RuntimeError!void { + const number_format = coral.utf8.DecimalFormat{ + .delimiter = "_", + .positive_prefix = .none, + }; + + 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), + + .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); + } + } + + 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.system_bindings.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 => |object_call| { + const result = call: { + const callable = try self.env.expect(try self.pop_local()); + + defer self.env.discard(callable); + + try self.env.frames.push_one(.{ + .name = "", + .arg_count = object_call, + .locals_top = self.env.locals.values.len, + }); + + defer coral.debug.assert(self.env.frames.pop() != null); + + 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"), + }; + }; + + for (0 .. object_call) |_| { + if (try self.pop_local()) |popped_arg| { + self.env.discard(popped_arg); + } + } + + 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 SyscallerTable = coral.map.StringTable(Caller); + const RefList = coral.list.Stack(?*RuntimeRef); - const RefStack = coral.list.Stack(?*RuntimeRef); + const RefTable = coral.map.Table(*RuntimeRef, *RuntimeRef, struct { + pub const hash = RuntimeRef.hash; - const RefSlab = coral.map.Slab(struct { - ref_count: usize, - - object: union (enum) { - false, - true, - number: Float, - string: []coral.io.Byte, - dynamic: *DynamicObject, - }, + pub const equals = RuntimeRef.equals; }); - pub const Syscall = struct { - name: []const coral.io.Byte, - caller: Caller, - }; + const SymbolSet = coral.map.StringTable([:0]coral.io.Byte); - pub fn acquire(self: *RuntimeEnv, ref: ?*const RuntimeRef) ?*RuntimeRef { - const key = @intFromPtr(ref orelse return null); - var ref_data = self.ref_values.remove(key); + const Table = struct { + associative: RefTable, + contiguous: RefList, - 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 bind_syscalls(self: *RuntimeEnv, syscalls: []const Syscall) RuntimeError!void { - for (syscalls) |syscall| { - _ = try self.syscallers.replace(syscall.name, syscall.caller); - } - } - - pub fn call( - self: *RuntimeEnv, - name: []const coral.io.Byte, - arg_count: u8, - caller: Caller, - ) RuntimeError!?*RuntimeRef { - try self.frames.push_one(.{ - .name = name, - .arg_count = arg_count, - .locals_top = self.local_refs.values.len, - }); - - defer { - const frame = self.frames.pop(); - - coral.debug.assert(frame != null); + fn typeinfo_destruct(method: Typeinfo.Method) void { + const table = @as(*Table, @ptrCast(@alignCast(method.userdata.ptr))); { - var pops_remaining = (self.local_refs.values.len - frame.?.locals_top) + frame.?.arg_count; + var field_iterable = table.associative.as_iterable(); - while (pops_remaining != 0) : (pops_remaining -= 1) { - const local = self.local_refs.pop(); - - coral.debug.assert(local != null); - - self.discard(local.?); + while (field_iterable.next()) |entry| { + method.env.discard(entry.key); + method.env.discard(entry.value); } } + + table.associative.free(); + + while (table.contiguous.pop()) |value| { + if (value) |ref| { + method.env.discard(ref); + } + } + + table.contiguous.free(); + } + + fn typeinfo_get(method: Typeinfo.Method, index: *const RuntimeRef) RuntimeError!?*RuntimeRef { + const table = @as(*Table, @ptrCast(@alignCast(method.userdata.ptr))); + const acquired_index = try method.env.acquire(index); + + defer method.env.discard(acquired_index); + + if (acquired_index.is_fixed()) |fixed| { + if (fixed < 0) { + // TODO: Negative indexing. + unreachable; + } + + if (fixed < table.contiguous.values.len) { + return method.env.acquire(table.contiguous.values[@intCast(fixed)] orelse return null); + } + } + + if (table.associative.lookup(acquired_index)) |value_ref| { + return method.env.acquire(value_ref); + } + + return null; + } + + fn typeinfo_set(method: Typeinfo.Method, index: *const RuntimeRef, value: ?*const RuntimeRef) RuntimeError!void { + const table = @as(*Table, @ptrCast(@alignCast(method.userdata.ptr))); + const acquired_index = try method.env.acquire(index); + + errdefer method.env.discard(acquired_index); + + if (acquired_index.is_fixed()) |fixed| { + if (fixed < 0) { + // TODO: Negative indexing. + unreachable; + } + + if (fixed < table.contiguous.values.len) { + const maybe_replacing = &table.contiguous.values[@intCast(fixed)]; + + if (maybe_replacing.*) |replacing| { + method.env.discard(replacing); + } + + maybe_replacing.* = if (value) |ref| try method.env.acquire(ref) else null; + + return; + } + } + + const acquired_value = try method.env.acquire(value orelse { + if (table.associative.remove(acquired_index)) |removed| { + method.env.discard(removed.key); + method.env.discard(removed.value); + } + + return; + }); + + errdefer method.env.discard(acquired_value); + + if (try table.associative.replace(acquired_index, acquired_value)) |replaced| { + method.env.discard(replaced.key); + method.env.discard(replaced.value); + } } - return caller.invoke(self); + }; + + 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 discard(self: *RuntimeEnv, ref: ?*RuntimeRef) void { - const key = @intFromPtr(ref orelse return); - var ref_data = self.ref_values.remove(key) orelse unreachable; + pub fn arg(self: *RuntimeEnv, index: usize) RuntimeError!?*RuntimeRef { + const frame = self.frames.peek() orelse return self.raise(error.IllegalState, "stack underflow"); - coral.debug.assert(ref_data.ref_count != 0); + if (index < frame.arg_count) { + if (self.locals.values[frame.locals_top - (1 + index)]) |local| { + return self.acquire(local); + } + } - ref_data.ref_count -= 1; + return null; + } - if (ref_data.ref_count == 0) { - switch (ref_data.object) { - .string => |string| self.allocator.deallocate(string), + pub fn bind_system(self: *RuntimeEnv, name: []const coral.io.Byte, value: *const RuntimeRef) RuntimeError!void { + const name_symbol = try self.new_symbol(name); - .dynamic => |dynamic| { - dynamic.typeinfo.clean(self, dynamic.userdata); - self.allocator.deallocate(dynamic.userdata); - self.allocator.deallocate(dynamic); + errdefer self.discard(name_symbol); + + const acquired_value = try self.acquire(value); + + errdefer self.discard(acquired_value); + + if (try self.system_bindings.replace(name_symbol, acquired_value)) |replaced| { + self.discard(replaced.key); + self.discard(replaced.value); + } + } + + 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, + }), + + else => self.raise(error.TypeMismatch, "object is not callable"), + }; + } + + pub fn discard(self: *RuntimeEnv, value: *RuntimeRef) void { + var object = value.object(); + + coral.debug.assert(object.ref_count != 0); + + object.ref_count -= 1; + + if (object.ref_count == 0) { + switch (object.payload) { + .false, .true, .float, .fixed, .symbol => {}, + + .string => |string| { + coral.debug.assert(string.len >= 0); + self.allocator.deallocate(string.ptr[0 .. @intCast(string.len)]); }, - else => {}, + .dynamic => |dynamic| { + if (dynamic.typeinfo().destruct) |destruct| { + destruct(.{ + .userdata = dynamic.userdata(), + .env = self, + }); + } + + 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 "