From 9cf2e671d36f84bc21cd94d51d1b0349ea419f91 Mon Sep 17 00:00:00 2001 From: kayomn Date: Thu, 16 Nov 2023 00:34:28 +0000 Subject: [PATCH] Implement initial update loop for canvas usage in scripts --- .vscode/settings.json | 1 + debug/app.ona | 21 +-- source/coral/io.zig | 4 +- source/coral/map.zig | 7 +- source/coral/utf8.zig | 30 ++++ source/ona/app.zig | 101 ----------- source/ona/file.zig | 52 ++++-- source/ona/gfx/canvas.zig | 0 source/ona/kym.zig | 228 ++++++++++++++---------- source/ona/kym/Chunk.zig | 45 +++-- source/ona/kym/Table.zig | 8 +- source/ona/kym/tree/Expr.zig | 29 +++- source/ona/ona.zig | 328 +++++++++++++++++++++++++++++------ 13 files changed, 552 insertions(+), 302 deletions(-) delete mode 100644 source/ona/app.zig create mode 100644 source/ona/gfx/canvas.zig diff --git a/.vscode/settings.json b/.vscode/settings.json index 0fc961c..5f8ace1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "zig.initialSetupDone": true, + "debug.console.collapseIdenticalLines": false, "[zig]": { "editor.formatOnSave": false, diff --git a/debug/app.ona b/debug/app.ona index cf95daf..a3db98d 100644 --- a/debug/app.ona +++ b/debug/app.ona @@ -1,19 +1,16 @@ +let ona = @import("ona") -let tool = "wrench" -var test_param = `monkey {tool} {2 + 1 - 1}` +var timer = 0 -let printer = lambda (pfx): - @print(test_param) +ona.on_update(lambda (dt): + timer = timer + dt - return lambda (msg): - @print(pfx) - @print(msg) + if (timer > 1): + @print("test hello world") + + timer = 0 end -end - -let pr = printer("this is a final closure") - -pr("goodbye") +end) return { .title = "Game", diff --git a/source/coral/io.zig b/source/coral/io.zig index 2f7de22..c8c9918 100644 --- a/source/coral/io.zig +++ b/source/coral/io.zig @@ -178,7 +178,7 @@ pub fn Generator(comptime Output: type, comptime Input: type) type { }; } - pub fn from(comptime invoker: fn (input: Input) Output) Self { + pub fn from_fn(comptime invoker: fn (input: Input) Output) Self { const Invoker = struct { fn invoke(_: *const anyopaque, input: Input) Output { return invoker(input); @@ -358,7 +358,7 @@ pub fn jenkins_hash(comptime int: std.builtin.Type.Int, bytes: []const Byte) mat return hash; } -pub const null_writer = Writer.from(write_null); +pub const null_writer = Writer.from_fn(write_null); pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) { var len = @as(usize, 0); diff --git a/source/coral/map.zig b/source/coral/map.zig index b16f239..bc2d402 100644 --- a/source/coral/map.zig +++ b/source/coral/map.zig @@ -23,6 +23,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 max_int = math.max_int(@typeInfo(usize).Int); + const has_traits = @sizeOf(Traits) != 0; return struct { allocator: io.Allocator, @@ -89,7 +90,7 @@ 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); + return if (has_traits) self.traits.hash(key) else Traits.hash(key); } pub fn remove(self: *Self, key: Key) ?Entry { @@ -119,7 +120,7 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) ty { const hash_max = @min(max_int, self.entries.len); const has_context = @sizeOf(Traits) != 0; - var hashed_key = (if (has_context) self.traits.hash(key) else Traits.hash(key)) % hash_max; + var hashed_key = self.hash_key(key) % hash_max; while (true) { const entry = &(self.entries[hashed_key] orelse { @@ -222,7 +223,7 @@ pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) ty const hash_max = @min(max_int, self.entries.len); const has_context = @sizeOf(Traits) != 0; - var hashed_key = (if (has_context) self.traits.hash(key) else Traits.hash(key)) % hash_max; + var hashed_key = self.hash_key(key) % hash_max; var iterations = @as(usize, 0); while (iterations < self.count) : (iterations += 1) { diff --git a/source/coral/utf8.zig b/source/coral/utf8.zig index f7ddcc6..c6d82b6 100644 --- a/source/coral/utf8.zig +++ b/source/coral/utf8.zig @@ -201,6 +201,36 @@ pub const HexadecimalFormat = struct { } }; +pub const SmallString = extern struct { + data: [256]u8 = [_]u8{0} ** 256, + + pub const FromError = error { + InvalidUtf8, + TooBig, + }; + + pub fn from_units(units: []const u8) FromError!SmallString { + var self = SmallString{}; + const units_range = self.data.len - 1; + + if (units.len > units_range) { + return error.TooBig; + } + + io.copy(self.data[0 .. units_range], units); + + self.data[units_range] = @intCast(units_range - units.len); + + return self; + } + + pub fn to_units(self: *const SmallString) [:0]const u8 { + const units_range = self.data.len - 1; + + return @ptrCast(self.data[0 .. units_range - self.data[units_range]]); + } +}; + pub fn alloc_formatted(allocator: io.Allocator, comptime format: []const u8, args: anytype) io.AllocationError![]io.Byte { const formatted_len = print_formatted(io.null_writer, format, args); diff --git a/source/ona/app.zig b/source/ona/app.zig deleted file mode 100644 index be3044e..0000000 --- a/source/ona/app.zig +++ /dev/null @@ -1,101 +0,0 @@ -const coral = @import("coral"); - -const ext = @import("./ext.zig"); - -const file = @import("./file.zig"); - -const kym = @import("./kym.zig"); - -pub const Manifest = struct { - title: [255:0]coral.io.Byte = [_:0]coral.io.Byte{0} ** 255, - width: u16 = 640, - height: u16 = 480, - tick_rate: f32 = 60.0, - - pub fn load(self: *Manifest, env: *kym.RuntimeEnv) kym.RuntimeError!void { - const manifest = (try env.import(file.Path.from(&.{"app.ona"}))).pop() orelse return; - - defer env.release(manifest); - - const width = @as(u16, get: { - if (try kym.get_field(env, manifest, "width")) |ref| { - defer env.release(ref); - - const fixed = try env.expect_fixed(ref); - - if (fixed > 0 and fixed < coral.math.max_int(@typeInfo(@TypeOf(self.width)).Int)) { - break: get @intCast(fixed); - } - } - - break: get self.width; - }); - - const height = @as(u16, get: { - if (try kym.get_field(env, manifest, "height")) |ref| { - defer env.release(ref); - - const fixed = try env.expect_fixed(ref); - - if (fixed > 0 and fixed < coral.math.max_int(@typeInfo(@TypeOf(self.height)).Int)) { - break: get @intCast(fixed); - } - } - - break: get self.height; - }); - - const tick_rate = @as(f32, get: { - if (try kym.get_field(env, manifest, "tick_rate")) |ref| { - defer env.release(ref); - - break: get @floatCast(try env.expect_float(ref)); - } - - break: get self.tick_rate; - }); - - if (try kym.get_field(env, manifest, "title")) |ref| { - defer env.release(ref); - - const title_string = try env.expect_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; - self.width = width; - self.height = height; - } -}; - -pub fn log_info(message: []const coral.io.Byte) void { - ext.SDL_LogInfo( - ext.SDL_LOG_CATEGORY_APPLICATION, - "%.*s", - coral.math.clamped_cast(@typeInfo(c_int).Int, message.len), - message.ptr, - ); -} - -pub fn log_warn(message: []const coral.io.Byte) void { - ext.SDL_LogWarn( - ext.SDL_LOG_CATEGORY_APPLICATION, - "%.*s", - coral.math.clamped_cast(@typeInfo(c_int).Int, message.len), - message.ptr, - ); -} - -pub fn log_fail(message: []const coral.io.Byte) void { - ext.SDL_LogError( - ext.SDL_LOG_CATEGORY_APPLICATION, - "%.*s", - coral.math.clamped_cast(@typeInfo(c_int).Int, message.len), - message.ptr, - ); -} diff --git a/source/ona/file.zig b/source/ona/file.zig index 73a5b98..80a4aae 100644 --- a/source/ona/file.zig +++ b/source/ona/file.zig @@ -11,9 +11,9 @@ pub const Access = union (enum) { .null => return null, .sandboxed_path => |sandboxed_path| { - const path_string = sandboxed_path.joined(readable_path).get_string(); + const path_bytes = sandboxed_path.joined(readable_path).to_bytes(); - return @ptrCast(ext.SDL_RWFromFile(path_string.ptr, "rb")); + return @ptrCast(ext.SDL_RWFromFile(path_bytes.ptr, "rb")); }, } } @@ -23,8 +23,7 @@ pub const Access = union (enum) { .null => return null, .sandboxed_path => |sandboxed_path| { - const path_string = sandboxed_path.joined(path).get_string(); - const rw_ops = ext.SDL_RWFromFile(path_string, "rb") orelse return null; + const rw_ops = ext.SDL_RWFromFile(sandboxed_path.joined(path).to_bytes(), "rb") orelse return null; const file_size = ext.SDL_RWseek(rw_ops, 0, ext.RW_SEEK_END); if (ext.SDL_RWclose(rw_ops) != 0 or file_size < 0) { @@ -43,26 +42,26 @@ pub const Info = struct { size: u64, }; -pub const Path = extern struct { +pub const Path = struct { data: [4096]coral.io.Byte = [_]coral.io.Byte{0} ** 4096, - pub const cwd = Path.from(&.{"./"}); + pub const cwd = Path.of(&.{"./"}); - pub fn from(components: []const []const u8) Path { - // TODO: Implement proper parsing / removal of duplicate path delimiters. - var path = Path{}; + pub const FromError = error { + InvalidPath, + TooBig, + }; - { - var writable_slice = coral.io.FixedBuffer{.bytes = &path.data}; + pub fn from_bytes(bytes: []const coral.io.Byte) FromError!Path { + var self = Path{}; - for (components) |component| { - if (writable_slice.write(component) != component.len) { - break; - } - } + if (bytes.len >= self.data.len) { + return error.TooBig; } - return path; + coral.io.copy(&self.data, bytes); + + return self; } pub fn joined(self: Path, other: Path) Path { @@ -96,7 +95,24 @@ pub const Path = extern struct { return path; } - pub fn get_string(self: Path) [:0]const coral.io.Byte { + pub fn of(components: []const []const u8) Path { + // TODO: Implement proper parsing / removal of duplicate path delimiters. + var path = Path{}; + + { + var writable_slice = coral.io.FixedBuffer{.bytes = &path.data}; + + for (components) |component| { + if (writable_slice.write(component) != component.len) { + break; + } + } + } + + return path; + } + + pub fn to_bytes(self: Path) [:0]const coral.io.Byte { coral.debug.assert(self.data[self.data.len - 1] == 0); return coral.io.slice_sentineled(@as(coral.io.Byte, 0), @as([*:0]const coral.io.Byte, @ptrCast(&self.data))); diff --git a/source/ona/gfx/canvas.zig b/source/ona/gfx/canvas.zig new file mode 100644 index 0000000..e69de29 diff --git a/source/ona/kym.zig b/source/ona/kym.zig index 09fae63..83d076a 100644 --- a/source/ona/kym.zig +++ b/source/ona/kym.zig @@ -22,6 +22,16 @@ pub const Fixed = i32; /// pub const Float = f64; +/// +/// Specialized import logic hook type for exposing native logic to scripts. +/// +pub const Importer = coral.io.Generator(RuntimeError!?*RuntimeObj, *RuntimeEnv); + +/// +/// Byte string printing function. +/// +pub const Print = fn ([]const coral.io.Byte) void; + /// /// Supertype for all numeric types. /// @@ -47,11 +57,13 @@ pub const RuntimeEnv = struct { allocator: coral.io.Allocator, options: Options, interned_symbols: SymbolSet, + importer_overrides: ImporterTable, locals: LocalList, frames: FrameList, const FrameList = coral.list.Stack(struct { callable: *RuntimeObj, + caller: ?*RuntimeObj, locals_top: usize, arg_count: u8, @@ -66,6 +78,16 @@ pub const RuntimeEnv = struct { } }); + const ImporterTable = coral.map.Table(file.Path, Importer, struct { + pub fn hash(file_path: file.Path) usize { + return coral.io.djb2_hash(@typeInfo(usize).Int, file_path.to_bytes()); + } + + pub fn equals(a: file.Path, b: file.Path) bool { + return coral.io.are_equal(a.to_bytes(), b.to_bytes()); + } + }); + const LocalList = coral.list.Stack(?*RuntimeObj); /// @@ -73,13 +95,8 @@ pub const RuntimeEnv = struct { /// pub const Options = struct { import_access: file.Access = .null, - print: ?*const Printer = null, - print_error: ?*const Printer = null, - - /// - /// Byte string printing function. - /// - pub const Printer = fn ([]const coral.io.Byte) void; + print_out: ?*const Print = null, + print_err: ?*const Print = null, }; const SymbolSet = coral.map.StringTable([:0]coral.io.Byte); @@ -95,7 +112,7 @@ pub const RuntimeEnv = struct { /// /// `self` is returned for function chaining. /// - pub fn arg_get(self: *RuntimeEnv, arg_index: Local) RuntimeError!*RuntimeEnv { + pub fn arg(self: *RuntimeEnv, arg_index: Local) RuntimeError!*RuntimeEnv { const frame = self.frames.peek() orelse { return self.raise(error.IllegalState, "cannot get args outside of a call frame", .{}); }; @@ -112,11 +129,13 @@ pub const RuntimeEnv = struct { } /// - /// Attempts to pop the top-most value of `self` and call it with `local_arg_count` as the number of locals prior to - /// it in `self` that are intended to be arguments to it. Once the callable returns, the locals marked as arguments - /// are popped as well. + /// Attempts to pop the top-most value of `self` and call it with `arg_count` as the number of values prior to it in + /// `self` that are intended to be arguments to it. Once the callable returns, the locals marked as arguments are + /// popped as well. /// - /// A `local_arg_count` of `0` will call the function with no arguments and pop nothing other than the callable from + /// An optional `caller` may be specified for attaching an owning object to the call frame. + /// + /// A `arg_count` value of `0` will call the function with no arguments and pop nothing other than the callable from /// `self` during invocation. /// /// A [RuntimeError] is returned if `self` is out of memory, the top-most local is nil or not callable, or the @@ -124,16 +143,17 @@ pub const RuntimeEnv = struct { /// /// `self` is returned for function chaining. /// - pub fn call(self: *RuntimeEnv, local_arg_count: Local) RuntimeError!*RuntimeEnv { + pub fn call(self: *RuntimeEnv, arg_count: Local, caller: ?*RuntimeObj) RuntimeError!*RuntimeEnv { const callable = try self.expect_object(self.pop()); defer self.release(callable); const result = get_result: { try self.frames.push_one(.{ - .locals_top = self.locals.values.len - local_arg_count, + .locals_top = self.locals.values.len - arg_count, .callable = callable.internal().acquire(), - .arg_count = local_arg_count, + .caller = caller, + .arg_count = arg_count, }); defer { @@ -707,6 +727,27 @@ pub const RuntimeEnv = struct { }; } + /// + /// Attempts to return the caller of the current call frame, if one exists, otherwise returning `null`. + /// + /// A [RuntimeError] is returned if `self` is out of memory or the virtual machine is not inside a managed call + /// frame. + /// + /// *Note* any returned non-null pointer must be released via [RuntimeEnv.release] or else the resources belonging + /// to the objects will never be freed by the runtime. + /// + pub fn get_caller(self: *RuntimeEnv) RuntimeError!?*RuntimeObj { + const frame = self.frames.peek() orelse { + return self.raise(error.IllegalState, "cannot get caller outside of a call frame", .{}); + }; + + if (frame.caller) |object| { + return object.internal().acquire(); + } + + return null; + } + /// /// Attempts to pop the top-most value and index into it with `index`, pushing the retrieved value. /// @@ -846,66 +887,66 @@ pub const RuntimeEnv = struct { } /// - /// Attempts to import a script from the import [file.Access] (specified during [RuntimeEnv.init]) from `file_path`. + /// Attempts to import a script from the the [Importer] (specified during [RuntimeEnv.init]) from `path`. /// - /// If the loaded script is a plain text file, it will be read, parsed, and compiled into an optimized format before - /// being executed. + /// After loading, the result of the import is returned. /// - /// If the loaded script is already compiled, it is immediately executed. - /// - /// After completing execution, the return value is pushed to the top of `self`. - /// - /// A [RuntimeError] is returned if `self` is out of memory, no file at `file_path` could be found by the runtime, - /// the script is a source file and contains invalid syntax, or the imported script raised an error during - /// execution. - /// - /// Any syntax errors are reported using [print_error] prior to raising a runtime error. + /// A [RuntimeError] is returned if `self` is out of memory, no import at `path` could be found by the runtime, + /// import raised an error during execution. /// /// `self` is returned for function chaining. /// pub fn import(self: *RuntimeEnv, file_path: file.Path) RuntimeError!*RuntimeEnv { - { - var callable = new_chunk: { - const file_name = file_path.get_string(); + if (self.importer_overrides.lookup(file_path)) |importer| { + if (try importer.invoke(self)) |object| { + errdefer self.release(object); - const file_data = - (try file.allocate_and_load(self.allocator, self.options.import_access, file_path)) orelse { - return self.raise(error.BadOperation, "failed to open or read `{name}`", .{ - .name = file_name, - }); - }; + try self.locals.push_one(object); + } else { + try self.locals.push_one(null); + } - defer self.allocator.deallocate(file_data); - - var root = try tree.Root.init(self.allocator); - - defer root.deinit(); - - { - var stream = tokens.Stream{.source = file_data}; - - root.parse(&stream) catch |parse_error| { - for (root.error_messages.values) |error_message| { - self.print_error(error_message); - } - - return self.raise(parse_error, "failed to parse `{name}`", .{.name = file_name}); - }; - } - - var chunk = try Chunk.init(self, file_name, &root.environment); - - errdefer chunk.deinit(self); - - break: new_chunk (try self.new_dynamic(coral.io.bytes_of(&chunk), Chunk.typeinfo)).pop().?; - }; - - errdefer self.release(callable); - - try self.locals.push_one(callable); + return self; } - return self.call(0); + const callable = new_chunk: { + const file_name = file_path.to_bytes(); + + const file_data = + (try file.allocate_and_load(self.allocator, self.options.import_access, file_path)) orelse { + return self.raise(error.BadOperation, "failed to open or read `{name}`", .{ + .name = file_name, + }); + }; + + defer self.allocator.deallocate(file_data); + + var root = try tree.Root.init(self.allocator); + + defer root.deinit(); + + { + var stream = tokens.Stream{.source = file_data}; + + root.parse(&stream) catch |parse_error| { + for (root.error_messages.values) |error_message| { + self.print_err(error_message); + } + + return self.raise(parse_error, "failed to parse `{name}`", .{.name = file_name}); + }; + } + + var chunk = try Chunk.init(self, file_name, &root.environment); + + errdefer chunk.deinit(self); + + break: new_chunk (try self.new_dynamic(Chunk.typeinfo, chunk)).pop().?; + }; + + defer self.release(callable); + + return (try self.push(callable)).call(0, null); } /// @@ -929,6 +970,7 @@ pub const RuntimeEnv = struct { return .{ .interned_symbols = SymbolSet.init(allocator, .{}), + .importer_overrides = ImporterTable.init(allocator, .{}), .locals = locals, .frames = frames, .allocator = allocator, @@ -1083,14 +1125,14 @@ pub const RuntimeEnv = struct { /// /// Attempts to create a new dynamic object from `userdata` and `typeinfo`, pushing it to the top of `self`. /// - /// *Note* that the number of bytes in `userdata` must match the size described in `typeinfo` exactly. + /// *Note* the size of the type specified in `userdata` must match the size described in `typeinfo` exactly. /// /// A [RuntimeError] is returned if `self` is out of memory. /// /// `self` is returned for function chaining. /// - pub fn new_dynamic(self: *RuntimeEnv, userdata: []const coral.io.Byte, typeinfo: *const Typeinfo) RuntimeError!*RuntimeEnv { - coral.debug.assert(userdata.len == typeinfo.size); + pub fn new_dynamic(self: *RuntimeEnv, typeinfo: *const Typeinfo, userdata: anytype) RuntimeError!*RuntimeEnv { + coral.debug.assert(@sizeOf(@TypeOf(userdata)) == typeinfo.size); const dynamic = new: { const dynamic = try self.allocator.reallocate(null, @sizeOf(usize) + typeinfo.size); @@ -1098,7 +1140,7 @@ pub const RuntimeEnv = struct { errdefer self.allocator.deallocate(dynamic); coral.io.copy(dynamic, coral.io.bytes_of(&typeinfo)); - coral.io.copy(dynamic[@sizeOf(usize) ..], userdata[0 .. typeinfo.size]); + coral.io.copy(dynamic[@sizeOf(usize) ..], coral.io.bytes_of(&userdata)); break: new try RuntimeObj.allocate(self.allocator, .{ .ref_count = 1, @@ -1221,7 +1263,7 @@ pub const RuntimeEnv = struct { errdefer table.deinit(self); - return self.new_dynamic(coral.io.bytes_of(&table), Table.typeinfo); + return self.new_dynamic(Table.typeinfo, table); } /// @@ -1295,6 +1337,16 @@ pub const RuntimeEnv = struct { return self; } + /// + /// Attempst to override the behavior of all imports at `path` to execute the import logic specified in `importer` + /// instead for `self`. + /// + /// A [RuntimeError] is returned if `self` is out of memory. + /// + pub fn override_import(self: *RuntimeEnv, path: file.Path, importer: Importer) RuntimeError!void { + _ = try self.importer_overrides.replace(path, importer); + } + /// /// Pops the top-most value of `self`, returning the value. /// @@ -1310,20 +1362,20 @@ pub const RuntimeEnv = struct { } /// - /// Prints `buffer` to the printing operation specified in the options of `self`. + /// Prints `buffer` to the error printing operation specified in the options of `self. /// - pub fn print(self: *RuntimeEnv, buffer: []const coral.io.Byte) void { - if (self.options.print) |op| { - op(buffer); + pub fn print_err(self: *RuntimeEnv, buffer: []const coral.io.Byte) void { + if (self.options.print_err) |print| { + print(buffer); } } /// - /// Prints `buffer` to the error printing operation specified in the options of `self. + /// Prints `buffer` to the printing operation specified in the options of `self`. /// - pub fn print_error(self: *RuntimeEnv, buffer: []const coral.io.Byte) void { - if (self.options.print_error) |op| { - op(buffer); + pub fn print_out(self: *RuntimeEnv, buffer: []const coral.io.Byte) void { + if (self.options.print_out) |print| { + print(buffer); } } @@ -1359,10 +1411,10 @@ pub const RuntimeEnv = struct { defer self.allocator.deallocate(formatted_message); - self.print_error(formatted_message); + self.print_err(formatted_message); if (!self.frames.is_empty()) { - self.print_error("stack trace:"); + self.print_err("stack trace:"); var remaining_frames = self.frames.values.len; @@ -1392,9 +1444,9 @@ pub const RuntimeEnv = struct { defer self.allocator.deallocate(chunk_name); - self.print_error(chunk_name); + self.print_err(chunk_name); } else { - self.print_error(get_name: { + self.print_err(get_name: { const string = name.is_string(); coral.debug.assert(string != null); @@ -2083,10 +2135,10 @@ pub const Typeinfo = struct { /// /// The [RuntimeEnv] is returned for function chaining. /// - pub fn push_index(self: *const GetContext) RuntimeError!*RuntimeEnv { + pub fn get_index(self: *const GetContext) *RuntimeObj { coral.debug.assert(self.env.locals.values.len > 0); - return self.env.push(self.env.locals.values[self.env.locals.values.len - 1]); + return self.env.locals.values[self.env.locals.values.len - 1].?.internal().acquire(); } }; @@ -2106,10 +2158,10 @@ pub const Typeinfo = struct { /// /// The [RuntimeEnv] is returned for function chaining. /// - pub fn push_index(self: *const SetContext) RuntimeError!*RuntimeEnv { + pub fn get_index(self: *const SetContext) *RuntimeObj { coral.debug.assert(self.env.locals.values.len > 0); - return self.env.push(self.env.locals.values[self.env.locals.values.len - 1]); + return self.env.locals.values[self.env.locals.values.len - 1].?.internal().acquire(); } /// @@ -2119,10 +2171,10 @@ pub const Typeinfo = struct { /// /// The [RuntimeEnv] is returned for function chaining. /// - pub fn push_value(self: *const SetContext) RuntimeError!*RuntimeEnv { + pub fn get_value(self: *const SetContext) ?*RuntimeObj { coral.debug.assert(self.env.locals.values.len > 1); - return self.env.push(self.env.locals.values[self.env.locals.values.len - 2]); + return if (self.env.locals.values[self.env.locals.values.len - 2]) |object| object.internal().acquire() else null; } }; diff --git a/source/ona/kym/Chunk.zig b/source/ona/kym/Chunk.zig index 4496bcb..69193ae 100644 --- a/source/ona/kym/Chunk.zig +++ b/source/ona/kym/Chunk.zig @@ -1,5 +1,3 @@ -const app = @import("../app.zig"); - const coral = @import("coral"); const file = @import("../file.zig"); @@ -221,8 +219,8 @@ const Compiler = struct { } if (try get_binding_index(environment, declaration_set.declaration)) |index| { - try self.compile_expression(environment, declaration_set.assign, null); try self.chunk.write(expression.line, .{.push_binding = index}); + try self.compile_expression(environment, declaration_set.assign, null); if (is_declaration_boxed(declaration_set.declaration)) { try self.chunk.write(expression.line, .set_box); @@ -247,6 +245,16 @@ const Compiler = struct { try self.chunk.write(expression.line, .set_dynamic); }, + .field_invoke => |field_invoke| { + const argument_count = try self.compile_argument(environment, field_invoke.argument); + + try self.compile_expression(environment, field_invoke.object, null); + try self.chunk.write(expression.line, .{.push_const = try self.declare_symbol(field_invoke.identifier)}); + try self.chunk.write(expression.line, .get_dynamic); + try self.compile_expression(environment, field_invoke.object, null); + try self.chunk.write(expression.line, .{.call_from = argument_count}); + }, + .subscript_get => |subscript_get| { try self.compile_expression(environment, subscript_get.object, null); try self.compile_expression(environment, subscript_get.index, null); @@ -345,7 +353,7 @@ const Compiler = struct { }); } - const constant = (try self.env.new_dynamic(coral.io.bytes_of(&chunk), typeinfo)).pop().?; + const constant = (try self.env.new_dynamic(typeinfo, chunk)).pop().?; errdefer self.env.release(constant); @@ -483,6 +491,7 @@ pub const Opcode = union (enum) { get_box, set_box, call: u8, + call_from: u8, bind: u8, not, @@ -747,7 +756,15 @@ pub fn execute(self: *Self, env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.Runtime try env.index_set(index, value); }, - .call => |call| _ = try env.call(call), + .call => |call| _ = try env.call(call, null), + + .call_from => |call_from| { + const caller = try env.expect_object(env.pop()); + + defer env.release(caller); + + _ = try env.call(call_from, caller); + }, .not => { const object = try env.expect_object(env.pop()); @@ -918,28 +935,28 @@ pub fn init(env: *kym.RuntimeEnv, name: []const coral.io.Byte, environment: *con } fn syscall_import(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { - const arg = (try env.arg_get(0)).pop() orelse { + const arg = (try env.arg(0)).pop() orelse { return env.raise(error.BadOperation, "`@import` requires one argument to be a valid import path", .{}); }; defer env.release(arg); - return (try env.import(file.Path.from(&.{try env.expect_string(arg)}))).pop(); + return (try env.import(file.Path.of(&.{try env.expect_string(arg)}))).pop(); } fn syscall_print(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { - const string = (try (try env.arg_get(0)).to_string()).pop().?; + const string = (try (try env.arg(0)).to_string()).pop().?; defer env.release(string); - env.print(string.is_string().?); + env.print_out(string.is_string().?); return null; } fn syscall_vec2(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { const x = @as(f32, get_x: { - const x = (try env.arg_get(0)).pop() orelse { + const x = (try env.arg(0)).pop() orelse { return env.raise(error.BadOperation, "a first argument is required to create a vector", .{}); }; @@ -948,7 +965,7 @@ fn syscall_vec2(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { break: get_x @floatCast(try env.expect_float(x)); }); - if ((try env.arg_get(1)).pop()) |y| { + if ((try env.arg(1)).pop()) |y| { defer env.release(y); return (try env.new_vector2(.{ @@ -962,7 +979,7 @@ fn syscall_vec2(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { fn syscall_vec3(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { const x = @as(f32, get_x: { - const x = (try env.arg_get(0)).pop() orelse { + const x = (try env.arg(0)).pop() orelse { return env.raise(error.BadOperation, "a first argument is required to create a vector", .{}); }; @@ -971,12 +988,12 @@ fn syscall_vec3(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { break: get_x @floatCast(try env.expect_float(x)); }); - if ((try env.arg_get(1)).pop()) |y| { + if ((try env.arg(1)).pop()) |y| { defer env.release(y); return (try env.new_vector3(.{ .z = @as(f32, get_z: { - const z = (try env.arg_get(0)).pop() orelse { + const z = (try env.arg(0)).pop() orelse { return env.raise(error.BadOperation, "a third argument is required to create a vector if a first and second exist", .{}); }; diff --git a/source/ona/kym/Table.zig b/source/ona/kym/Table.zig index 26b5d11..fad9327 100644 --- a/source/ona/kym/Table.zig +++ b/source/ona/kym/Table.zig @@ -57,7 +57,7 @@ fn typeinfo_destruct(context: kym.Typeinfo.DestructContext) void { fn typeinfo_get(context: kym.Typeinfo.GetContext) kym.RuntimeError!?*kym.RuntimeObj { const table = @as(*Self, @ptrCast(@alignCast(context.userdata))); - const index = (try context.push_index()).pop().?; + const index = context.get_index(); defer context.env.release(index); @@ -81,7 +81,7 @@ fn typeinfo_get(context: kym.Typeinfo.GetContext) kym.RuntimeError!?*kym.Runtime fn typeinfo_set(context: kym.Typeinfo.SetContext) kym.RuntimeError!void { const table = @as(*Self, @ptrCast(@alignCast(context.userdata))); - const index = (try context.push_index()).pop().?; + const index = context.get_index(); errdefer context.env.release(index); @@ -98,7 +98,7 @@ fn typeinfo_set(context: kym.Typeinfo.SetContext) kym.RuntimeError!void { context.env.release(replacing); } - if ((try context.push_value()).pop()) |value| { + if (context.get_value()) |value| { errdefer context.env.release(value); maybe_replacing.* = value; @@ -110,7 +110,7 @@ fn typeinfo_set(context: kym.Typeinfo.SetContext) kym.RuntimeError!void { } } - const value = (try context.push_value()).pop() orelse { + const value = context.get_value() orelse { if (table.associative.remove(index)) |removed| { context.env.release(removed.key); context.env.release(removed.value); diff --git a/source/ona/kym/tree/Expr.zig b/source/ona/kym/tree/Expr.zig index 5d32261..42b86a4 100644 --- a/source/ona/kym/tree/Expr.zig +++ b/source/ona/kym/tree/Expr.zig @@ -25,6 +25,7 @@ kind: union (enum) { declaration_set: DeclarationSet, field_get: FieldGet, field_set: FieldSet, + field_invoke: FieldInvoke, subscript_get: SubscriptGet, subscript_set: SubscriptSet, binary_op: BinaryOp, @@ -112,6 +113,12 @@ pub const DeclarationSet = struct { assign: *const Self, }; +pub const FieldInvoke = struct { + argument: ?*const Self, + identifier: []const coral.io.Byte, + object: *const Self, +}; + pub const FieldGet = struct { identifier: []const coral.io.Byte, object: *const Self, @@ -364,7 +371,7 @@ fn parse_factor(root: *tree.Root, stream: *tokens.Stream, environment: *tree.Env switch (stream.token) { .symbol_comma => stream.skip_newlines(), .symbol_paren_right => break, - else => return root.report_error(stream.line, "expected `,` or `)` after lambda argument", .{}), + else => return root.report_error(stream.line, "expected `,` or `)` after argument", .{}), } const next_argument = try parse(root, stream, environment); @@ -382,12 +389,22 @@ fn parse_factor(root: *tree.Root, stream: *tokens.Stream, environment: *tree.Env expression = try root.create_expr(.{ .line = lines_stepped, - .kind = .{ - .invoke = .{ - .argument = first_argument, - .object = unnecessary_temp, + .kind = switch (expression.kind) { + .field_get => |field_get| .{ + .field_invoke = .{ + .identifier = field_get.identifier, + .argument = first_argument, + .object = field_get.object, + }, }, - }, + + else => .{ + .invoke = .{ + .argument = first_argument, + .object = unnecessary_temp, + }, + }, + } }); }, diff --git a/source/ona/ona.zig b/source/ona/ona.zig index fba6e55..b875c5e 100644 --- a/source/ona/ona.zig +++ b/source/ona/ona.zig @@ -1,5 +1,3 @@ -const app = @import("./app.zig"); - const coral = @import("coral"); const ext = @import("./ext.zig"); @@ -10,84 +8,306 @@ const heap = @import("./heap.zig"); const kym = @import("./kym.zig"); +const App = struct { + renderer: *ext.SDL_Renderer, + window: *ext.SDL_Window, + systems: Systems, + target_frame_time: f64, + + keys: struct { + pressed: [ext.SDL_NUM_SCANCODES]bool = [_]bool{false} ** ext.SDL_NUM_SCANCODES, + released: [ext.SDL_NUM_SCANCODES]bool = [_]bool{false} ** ext.SDL_NUM_SCANCODES, + held: [ext.SDL_NUM_SCANCODES]bool = [_]bool{false} ** ext.SDL_NUM_SCANCODES, + }, + + const DynamicObject = struct { + self: *App, + + fn get(context: kym.Typeinfo.GetContext) kym.RuntimeError!?*kym.RuntimeObj { + const index = context.get_index(); + + defer context.env.release(index); + + const on_update_symbol = (try context.env.new_symbol("on_update")).pop().?; + + defer context.env.release(on_update_symbol); + + if (index.equals(on_update_symbol)) { + return (try context.env.new_syscall(on_update)).pop(); + } + + return null; + } + + fn on_update(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { + const caller_object = try env.expect_object(try env.get_caller()); + + defer env.release(caller_object); + + const app = @as(*DynamicObject, @ptrCast(@alignCast(try env.expect_dynamic(caller_object, typeinfo)))); + + const callable = try env.expect_object((try env.arg(0)).pop()); + + errdefer env.release(callable); + + try app.self.systems.push_one(callable); + + return null; + } + + const typeinfo = &kym.Typeinfo{ + .name = "ona", + .size = @sizeOf(DynamicObject), + .get = get, + }; + }; + + pub const InitError = error { + RendererUnavailable, + WindowUnavailable, + }; + + const Systems = coral.list.Stack(*kym.RuntimeObj); + + pub fn as_importer(self: *App) kym.Importer { + return kym.Importer.bind(App, self, import); + } + + pub fn deinit(self: *App) void { + ext.SDL_DestroyWindow(self.window); + ext.SDL_DestroyRenderer(self.renderer); + self.systems.deinit(); + } + + fn import(self: *App, env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { + return (try env.new_dynamic(DynamicObject.typeinfo, DynamicObject{.self = self})).pop(); + } + + pub fn init() InitError!App { + const window = create: { + const height = 480; + const width = 640; + const pos = ext.SDL_WINDOWPOS_CENTERED; + const flags = ext.SDL_WINDOW_HIDDEN; + + break: create ext.SDL_CreateWindow("", pos, pos, width, height, flags) orelse { + return error.WindowUnavailable; + }; + }; + + errdefer ext.SDL_DestroyWindow(window); + + const renderer = create: { + const default_driver_index = -1; + const flags = ext.SDL_RENDERER_ACCELERATED; + + break: create ext.SDL_CreateRenderer(window, default_driver_index, flags) orelse { + return error.RendererUnavailable; + }; + }; + + errdefer ext.SDL_DestroyRenderer(renderer); + + return .{ + .systems = Systems.init(heap.allocator), + .target_frame_time = 1.0 / 60.0, + .renderer = renderer, + .window = window, + .keys = .{}, + }; + } + + pub fn load(self: *App, env: *kym.RuntimeEnv) kym.RuntimeError!void { + var previous_width = @as(c_int, 0); + var previous_height = @as(c_int, 0); + + ext.SDL_GetWindowSize(self.window, &previous_width, &previous_height); + + var width = previous_width; + var height = previous_height; + + defer { + const pos = ext.SDL_WINDOWPOS_CENTERED; + + ext.SDL_SetWindowSize(self.window, width, height); + ext.SDL_SetWindowPosition(self.window, pos, pos); + ext.SDL_ShowWindow(self.window); + } + + if ((try env.import(file.Path.of(&.{"app.ona"}))).pop()) |manifest| { + defer env.release(manifest); + + if (try kym.get_field(env, manifest, "width")) |object| { + defer env.release(object); + + const fixed = try env.expect_fixed(object); + + if (fixed > 0 and fixed < coral.math.max_int(@typeInfo(@TypeOf(width)).Int)) { + width = @intCast(fixed); + } + } + + if (try kym.get_field(env, manifest, "height")) |object| { + defer env.release(object); + + const fixed = try env.expect_fixed(object); + + if (fixed > 0 and fixed < coral.math.max_int(@typeInfo(@TypeOf(height)).Int)) { + height = @intCast(fixed); + } + } + + if (try kym.get_field(env, manifest, "tick_rate")) |object| { + defer env.release(object); + + self.target_frame_time = 1.0 / try env.expect_float(object); + } + + if (try kym.get_field(env, manifest, "title")) |object| { + defer env.release(object); + + const title = coral.utf8.SmallString.from_units(try env.expect_string(object)) catch |from_error| { + return switch (from_error) { + error.InvalidUtf8 => env.raise(error.TypeMismatch, "`title` cannot contain invalid utf8", .{}), + error.TooBig => env.raise(error.TypeMismatch, "`title` is too long", .{}), + }; + }; + + ext.SDL_SetWindowTitle(self.window, title.to_units()); + } else { + ext.SDL_SetWindowTitle(self.window, "Ona"); + } + } + } + + pub fn poll(self: *App) bool { + var event = @as(ext.SDL_Event, undefined); + + coral.io.zero(@ptrCast(&self.keys.pressed)); + coral.io.zero(@ptrCast(&self.keys.released)); + + while (ext.SDL_PollEvent(&event) != 0) { + switch (event.type) { + ext.SDL_QUIT => return false, + + ext.SDL_KEYDOWN => { + self.keys.pressed[event.key.keysym.scancode] = true; + self.keys.held[event.key.keysym.scancode] = true; + }, + + ext.SDL_KEYUP => { + self.keys.released[event.key.keysym.scancode] = true; + self.keys.held[event.key.keysym.scancode] = false; + }, + + else => {}, + } + } + + return true; + } + + pub fn render(self: *App) void { + _ = ext.SDL_SetRenderDrawColor(self.renderer, 0, 0, 0, 255); + _ = ext.SDL_RenderClear(self.renderer); + _ = ext.SDL_RenderPresent(self.renderer); + } + + pub fn update(self: *App, env: *kym.RuntimeEnv, time_delta: f32) kym.RuntimeError!void { + for (self.systems.values) |system| { + (try (try (try env.new_float(time_delta)).push(system)).call(1, null)).discard(); + } + } +}; + fn last_sdl_error() [:0]const u8 { return coral.io.slice_sentineled(@as(u8, 0), @as([*:0]const u8, @ptrCast(ext.SDL_GetError()))); } +fn load_prompt() void { + +} + +pub fn log_info(message: []const coral.io.Byte) void { + ext.SDL_LogInfo( + ext.SDL_LOG_CATEGORY_APPLICATION, + "%.*s", + coral.math.clamped_cast(@typeInfo(c_int).Int, message.len), + message.ptr, + ); +} + +pub fn log_warn(message: []const coral.io.Byte) void { + ext.SDL_LogWarn( + ext.SDL_LOG_CATEGORY_APPLICATION, + "%.*s", + coral.math.clamped_cast(@typeInfo(c_int).Int, message.len), + message.ptr, + ); +} + +pub fn log_fail(message: []const coral.io.Byte) void { + ext.SDL_LogError( + ext.SDL_LOG_CATEGORY_APPLICATION, + "%.*s", + coral.math.clamped_cast(@typeInfo(c_int).Int, message.len), + message.ptr, + ); +} + +const milliseconds_per_second = 1000; + pub fn run_app(file_access: file.Access) void { defer heap.trace_leaks(); if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) { - return app.log_fail(last_sdl_error()); + return log_fail(last_sdl_error()); } defer ext.SDL_Quit(); + var app = App.init() catch |init_error| { + return log_fail(switch (init_error) { + error.WindowUnavailable => "unable to acquire window", + error.RendererUnavailable => "unable to acquire renderer", + }); + }; + + defer app.deinit(); + var script_env = kym.RuntimeEnv.init(heap.allocator, 255, .{ - .print = app.log_info, - .print_error = app.log_fail, .import_access = file_access, + .print_out = log_info, + .print_err = log_fail, }) catch { - return app.log_fail("failed to initialize script runtime"); + return log_fail("failed to initialize script runtime"); }; defer script_env.deinit(); - var manifest = app.Manifest{}; + script_env.override_import(file.Path.of(&.{"ona"}), app.as_importer()) catch return; - manifest.load(&script_env) catch return; + app.load(&script_env) catch load_prompt(); - const window = create: { - const pos = ext.SDL_WINDOWPOS_CENTERED; - const flags = 0; + var ticks_previous = ext.SDL_GetTicks64(); + var accumulated_time = @as(f64, 0); - break: create ext.SDL_CreateWindow(&manifest.title, pos, pos, manifest.width, manifest.height, flags) orelse { - return app.log_fail(last_sdl_error()); - }; - }; + running_loop: while (true) { + const ticks_current = ext.SDL_GetTicks64(); + const delta_time = @as(f64, @floatFromInt(ticks_current - ticks_previous)) / milliseconds_per_second; - defer ext.SDL_DestroyWindow(window); + ticks_previous = ticks_current; + accumulated_time += delta_time; - const renderer = create: { - const default_driver_index = -1; - const flags = ext.SDL_RENDERER_ACCELERATED; - - break: create ext.SDL_CreateRenderer(window, default_driver_index, flags) orelse { - return app.log_fail(last_sdl_error()); - }; - }; - - defer ext.SDL_DestroyRenderer(renderer); - - { - var previous_ticks = ext.SDL_GetTicks64(); - - while (true) { - { - var event = @as(ext.SDL_Event, undefined); - - while (ext.SDL_PollEvent(&event) != 0) { - switch (event.type) { - ext.SDL_QUIT => return, - else => {}, - } - } + while (accumulated_time >= app.target_frame_time) : (accumulated_time -= app.target_frame_time) { + if (!app.poll()) { + break: running_loop; } - { - // Based on https://fabiensanglard.net/timer_and_framerate/index.php. - const current_ticks = ext.SDL_GetTicks64(); - const milliseconds_per_second = 1000.0; - const tick_frequency = @as(u64, @intFromFloat(milliseconds_per_second / manifest.tick_rate)); - - while (previous_ticks < current_ticks) { - previous_ticks += tick_frequency; - } - } - - _ = ext.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); - _ = ext.SDL_RenderClear(renderer); - _ = ext.SDL_RenderPresent(renderer); + app.update(&script_env, @floatCast(app.target_frame_time)) catch return; } + + app.render(); + ext.SDL_Delay(1); } }