Implement initial update loop for canvas usage in scripts

This commit is contained in:
kayomn 2023-11-16 00:34:28 +00:00
parent ef537bef14
commit 9cf2e671d3
13 changed files with 552 additions and 302 deletions

View File

@ -2,6 +2,7 @@
"files.insertFinalNewline": true, "files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true, "files.trimTrailingWhitespace": true,
"zig.initialSetupDone": true, "zig.initialSetupDone": true,
"debug.console.collapseIdenticalLines": false,
"[zig]": { "[zig]": {
"editor.formatOnSave": false, "editor.formatOnSave": false,

View File

@ -1,19 +1,16 @@
let ona = @import("ona")
let tool = "wrench" var timer = 0
var test_param = `monkey {tool} {2 + 1 - 1}`
let printer = lambda (pfx): ona.on_update(lambda (dt):
@print(test_param) timer = timer + dt
return lambda (msg): if (timer > 1):
@print(pfx) @print("test hello world")
@print(msg)
timer = 0
end end
end end)
let pr = printer("this is a final closure")
pr("goodbye")
return { return {
.title = "Game", .title = "Game",

View File

@ -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 { const Invoker = struct {
fn invoke(_: *const anyopaque, input: Input) Output { fn invoke(_: *const anyopaque, input: Input) Output {
return invoker(input); return invoker(input);
@ -358,7 +358,7 @@ pub fn jenkins_hash(comptime int: std.builtin.Type.Int, bytes: []const Byte) mat
return hash; 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) { pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) {
var len = @as(usize, 0); var len = @as(usize, 0);

View File

@ -23,6 +23,7 @@ pub fn StringTable(comptime Value: type) type {
pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) type { pub fn Table(comptime Key: type, comptime Value: type, comptime Traits: type) type {
const load_max = 0.75; const load_max = 0.75;
const max_int = math.max_int(@typeInfo(usize).Int); const max_int = math.max_int(@typeInfo(usize).Int);
const has_traits = @sizeOf(Traits) != 0;
return struct { return struct {
allocator: io.Allocator, 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 { 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 { 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 hash_max = @min(max_int, self.entries.len);
const has_context = @sizeOf(Traits) != 0; 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) { while (true) {
const entry = &(self.entries[hashed_key] orelse { 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 hash_max = @min(max_int, self.entries.len);
const has_context = @sizeOf(Traits) != 0; 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); var iterations = @as(usize, 0);
while (iterations < self.count) : (iterations += 1) { while (iterations < self.count) : (iterations += 1) {

View File

@ -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 { 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); const formatted_len = print_formatted(io.null_writer, format, args);

View File

@ -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,
);
}

View File

@ -11,9 +11,9 @@ pub const Access = union (enum) {
.null => return null, .null => return null,
.sandboxed_path => |sandboxed_path| { .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, .null => return null,
.sandboxed_path => |sandboxed_path| { .sandboxed_path => |sandboxed_path| {
const path_string = sandboxed_path.joined(path).get_string(); const rw_ops = ext.SDL_RWFromFile(sandboxed_path.joined(path).to_bytes(), "rb") orelse return null;
const rw_ops = ext.SDL_RWFromFile(path_string, "rb") orelse return null;
const file_size = ext.SDL_RWseek(rw_ops, 0, ext.RW_SEEK_END); const file_size = ext.SDL_RWseek(rw_ops, 0, ext.RW_SEEK_END);
if (ext.SDL_RWclose(rw_ops) != 0 or file_size < 0) { if (ext.SDL_RWclose(rw_ops) != 0 or file_size < 0) {
@ -43,26 +42,26 @@ pub const Info = struct {
size: u64, size: u64,
}; };
pub const Path = extern struct { pub const Path = struct {
data: [4096]coral.io.Byte = [_]coral.io.Byte{0} ** 4096, 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 { pub const FromError = error {
// TODO: Implement proper parsing / removal of duplicate path delimiters. InvalidPath,
var path = Path{}; TooBig,
};
{ pub fn from_bytes(bytes: []const coral.io.Byte) FromError!Path {
var writable_slice = coral.io.FixedBuffer{.bytes = &path.data}; var self = Path{};
for (components) |component| { if (bytes.len >= self.data.len) {
if (writable_slice.write(component) != component.len) { return error.TooBig;
break;
}
}
} }
return path; coral.io.copy(&self.data, bytes);
return self;
} }
pub fn joined(self: Path, other: Path) Path { pub fn joined(self: Path, other: Path) Path {
@ -96,7 +95,24 @@ pub const Path = extern struct {
return path; 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); 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))); return coral.io.slice_sentineled(@as(coral.io.Byte, 0), @as([*:0]const coral.io.Byte, @ptrCast(&self.data)));

View File

View File

@ -22,6 +22,16 @@ pub const Fixed = i32;
/// ///
pub const Float = f64; 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. /// Supertype for all numeric types.
/// ///
@ -47,11 +57,13 @@ pub const RuntimeEnv = struct {
allocator: coral.io.Allocator, allocator: coral.io.Allocator,
options: Options, options: Options,
interned_symbols: SymbolSet, interned_symbols: SymbolSet,
importer_overrides: ImporterTable,
locals: LocalList, locals: LocalList,
frames: FrameList, frames: FrameList,
const FrameList = coral.list.Stack(struct { const FrameList = coral.list.Stack(struct {
callable: *RuntimeObj, callable: *RuntimeObj,
caller: ?*RuntimeObj,
locals_top: usize, locals_top: usize,
arg_count: u8, 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); const LocalList = coral.list.Stack(?*RuntimeObj);
/// ///
@ -73,13 +95,8 @@ pub const RuntimeEnv = struct {
/// ///
pub const Options = struct { pub const Options = struct {
import_access: file.Access = .null, import_access: file.Access = .null,
print: ?*const Printer = null, print_out: ?*const Print = null,
print_error: ?*const Printer = null, print_err: ?*const Print = null,
///
/// Byte string printing function.
///
pub const Printer = fn ([]const coral.io.Byte) void;
}; };
const SymbolSet = coral.map.StringTable([:0]coral.io.Byte); const SymbolSet = coral.map.StringTable([:0]coral.io.Byte);
@ -95,7 +112,7 @@ pub const RuntimeEnv = struct {
/// ///
/// `self` is returned for function chaining. /// `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 { const frame = self.frames.peek() orelse {
return self.raise(error.IllegalState, "cannot get args outside of a call frame", .{}); 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 /// Attempts to pop the top-most value of `self` and call it with `arg_count` as the number of values prior to it in
/// it in `self` that are intended to be arguments to it. Once the callable returns, the locals marked as arguments /// `self` that are intended to be arguments to it. Once the callable returns, the locals marked as arguments are
/// are popped as well. /// 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. /// `self` during invocation.
/// ///
/// A [RuntimeError] is returned if `self` is out of memory, the top-most local is nil or not callable, or the /// 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. /// `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()); const callable = try self.expect_object(self.pop());
defer self.release(callable); defer self.release(callable);
const result = get_result: { const result = get_result: {
try self.frames.push_one(.{ 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(), .callable = callable.internal().acquire(),
.arg_count = local_arg_count, .caller = caller,
.arg_count = arg_count,
}); });
defer { 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. /// 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 /// After loading, the result of the import is returned.
/// being executed.
/// ///
/// If the loaded script is already compiled, it is immediately executed. /// 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.
/// 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.
/// ///
/// `self` is returned for function chaining. /// `self` is returned for function chaining.
/// ///
pub fn import(self: *RuntimeEnv, file_path: file.Path) RuntimeError!*RuntimeEnv { pub fn import(self: *RuntimeEnv, file_path: file.Path) RuntimeError!*RuntimeEnv {
{ if (self.importer_overrides.lookup(file_path)) |importer| {
var callable = new_chunk: { if (try importer.invoke(self)) |object| {
const file_name = file_path.get_string(); errdefer self.release(object);
const file_data = try self.locals.push_one(object);
(try file.allocate_and_load(self.allocator, self.options.import_access, file_path)) orelse { } else {
return self.raise(error.BadOperation, "failed to open or read `{name}`", .{ try self.locals.push_one(null);
.name = file_name, }
});
};
defer self.allocator.deallocate(file_data); return self;
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.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 .{ return .{
.interned_symbols = SymbolSet.init(allocator, .{}), .interned_symbols = SymbolSet.init(allocator, .{}),
.importer_overrides = ImporterTable.init(allocator, .{}),
.locals = locals, .locals = locals,
.frames = frames, .frames = frames,
.allocator = allocator, .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`. /// 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. /// A [RuntimeError] is returned if `self` is out of memory.
/// ///
/// `self` is returned for function chaining. /// `self` is returned for function chaining.
/// ///
pub fn new_dynamic(self: *RuntimeEnv, userdata: []const coral.io.Byte, typeinfo: *const Typeinfo) RuntimeError!*RuntimeEnv { pub fn new_dynamic(self: *RuntimeEnv, typeinfo: *const Typeinfo, userdata: anytype) RuntimeError!*RuntimeEnv {
coral.debug.assert(userdata.len == typeinfo.size); coral.debug.assert(@sizeOf(@TypeOf(userdata)) == typeinfo.size);
const dynamic = new: { const dynamic = new: {
const dynamic = try self.allocator.reallocate(null, @sizeOf(usize) + typeinfo.size); const dynamic = try self.allocator.reallocate(null, @sizeOf(usize) + typeinfo.size);
@ -1098,7 +1140,7 @@ pub const RuntimeEnv = struct {
errdefer self.allocator.deallocate(dynamic); errdefer self.allocator.deallocate(dynamic);
coral.io.copy(dynamic, coral.io.bytes_of(&typeinfo)); 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, .{ break: new try RuntimeObj.allocate(self.allocator, .{
.ref_count = 1, .ref_count = 1,
@ -1221,7 +1263,7 @@ pub const RuntimeEnv = struct {
errdefer table.deinit(self); 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; 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. /// 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 { pub fn print_err(self: *RuntimeEnv, buffer: []const coral.io.Byte) void {
if (self.options.print) |op| { if (self.options.print_err) |print| {
op(buffer); 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 { pub fn print_out(self: *RuntimeEnv, buffer: []const coral.io.Byte) void {
if (self.options.print_error) |op| { if (self.options.print_out) |print| {
op(buffer); print(buffer);
} }
} }
@ -1359,10 +1411,10 @@ pub const RuntimeEnv = struct {
defer self.allocator.deallocate(formatted_message); defer self.allocator.deallocate(formatted_message);
self.print_error(formatted_message); self.print_err(formatted_message);
if (!self.frames.is_empty()) { if (!self.frames.is_empty()) {
self.print_error("stack trace:"); self.print_err("stack trace:");
var remaining_frames = self.frames.values.len; var remaining_frames = self.frames.values.len;
@ -1392,9 +1444,9 @@ pub const RuntimeEnv = struct {
defer self.allocator.deallocate(chunk_name); defer self.allocator.deallocate(chunk_name);
self.print_error(chunk_name); self.print_err(chunk_name);
} else { } else {
self.print_error(get_name: { self.print_err(get_name: {
const string = name.is_string(); const string = name.is_string();
coral.debug.assert(string != null); coral.debug.assert(string != null);
@ -2083,10 +2135,10 @@ pub const Typeinfo = struct {
/// ///
/// The [RuntimeEnv] is returned for function chaining. /// 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); 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. /// 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); 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. /// 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); 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;
} }
}; };

View File

@ -1,5 +1,3 @@
const app = @import("../app.zig");
const coral = @import("coral"); const coral = @import("coral");
const file = @import("../file.zig"); const file = @import("../file.zig");
@ -221,8 +219,8 @@ const Compiler = struct {
} }
if (try get_binding_index(environment, declaration_set.declaration)) |index| { 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.chunk.write(expression.line, .{.push_binding = index});
try self.compile_expression(environment, declaration_set.assign, null);
if (is_declaration_boxed(declaration_set.declaration)) { if (is_declaration_boxed(declaration_set.declaration)) {
try self.chunk.write(expression.line, .set_box); try self.chunk.write(expression.line, .set_box);
@ -247,6 +245,16 @@ const Compiler = struct {
try self.chunk.write(expression.line, .set_dynamic); 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| { .subscript_get => |subscript_get| {
try self.compile_expression(environment, subscript_get.object, null); try self.compile_expression(environment, subscript_get.object, null);
try self.compile_expression(environment, subscript_get.index, 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); errdefer self.env.release(constant);
@ -483,6 +491,7 @@ pub const Opcode = union (enum) {
get_box, get_box,
set_box, set_box,
call: u8, call: u8,
call_from: u8,
bind: u8, bind: u8,
not, not,
@ -747,7 +756,15 @@ pub fn execute(self: *Self, env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.Runtime
try env.index_set(index, value); 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 => { .not => {
const object = try env.expect_object(env.pop()); 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 { 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", .{}); return env.raise(error.BadOperation, "`@import` requires one argument to be a valid import path", .{});
}; };
defer env.release(arg); 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 { 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); defer env.release(string);
env.print(string.is_string().?); env.print_out(string.is_string().?);
return null; return null;
} }
fn syscall_vec2(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj { fn syscall_vec2(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj {
const x = @as(f32, get_x: { 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", .{}); 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)); 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); defer env.release(y);
return (try env.new_vector2(.{ 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 { fn syscall_vec3(env: *kym.RuntimeEnv) kym.RuntimeError!?*kym.RuntimeObj {
const x = @as(f32, get_x: { 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", .{}); 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)); 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); defer env.release(y);
return (try env.new_vector3(.{ return (try env.new_vector3(.{
.z = @as(f32, get_z: { .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, return env.raise(error.BadOperation,
"a third argument is required to create a vector if a first and second exist", .{}); "a third argument is required to create a vector if a first and second exist", .{});
}; };

View File

@ -57,7 +57,7 @@ fn typeinfo_destruct(context: kym.Typeinfo.DestructContext) void {
fn typeinfo_get(context: kym.Typeinfo.GetContext) kym.RuntimeError!?*kym.RuntimeObj { fn typeinfo_get(context: kym.Typeinfo.GetContext) kym.RuntimeError!?*kym.RuntimeObj {
const table = @as(*Self, @ptrCast(@alignCast(context.userdata))); 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); 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 { fn typeinfo_set(context: kym.Typeinfo.SetContext) kym.RuntimeError!void {
const table = @as(*Self, @ptrCast(@alignCast(context.userdata))); 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); errdefer context.env.release(index);
@ -98,7 +98,7 @@ fn typeinfo_set(context: kym.Typeinfo.SetContext) kym.RuntimeError!void {
context.env.release(replacing); context.env.release(replacing);
} }
if ((try context.push_value()).pop()) |value| { if (context.get_value()) |value| {
errdefer context.env.release(value); errdefer context.env.release(value);
maybe_replacing.* = 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| { if (table.associative.remove(index)) |removed| {
context.env.release(removed.key); context.env.release(removed.key);
context.env.release(removed.value); context.env.release(removed.value);

View File

@ -25,6 +25,7 @@ kind: union (enum) {
declaration_set: DeclarationSet, declaration_set: DeclarationSet,
field_get: FieldGet, field_get: FieldGet,
field_set: FieldSet, field_set: FieldSet,
field_invoke: FieldInvoke,
subscript_get: SubscriptGet, subscript_get: SubscriptGet,
subscript_set: SubscriptSet, subscript_set: SubscriptSet,
binary_op: BinaryOp, binary_op: BinaryOp,
@ -112,6 +113,12 @@ pub const DeclarationSet = struct {
assign: *const Self, assign: *const Self,
}; };
pub const FieldInvoke = struct {
argument: ?*const Self,
identifier: []const coral.io.Byte,
object: *const Self,
};
pub const FieldGet = struct { pub const FieldGet = struct {
identifier: []const coral.io.Byte, identifier: []const coral.io.Byte,
object: *const Self, object: *const Self,
@ -364,7 +371,7 @@ fn parse_factor(root: *tree.Root, stream: *tokens.Stream, environment: *tree.Env
switch (stream.token) { switch (stream.token) {
.symbol_comma => stream.skip_newlines(), .symbol_comma => stream.skip_newlines(),
.symbol_paren_right => break, .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); 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(.{ expression = try root.create_expr(.{
.line = lines_stepped, .line = lines_stepped,
.kind = .{ .kind = switch (expression.kind) {
.invoke = .{ .field_get => |field_get| .{
.argument = first_argument, .field_invoke = .{
.object = unnecessary_temp, .identifier = field_get.identifier,
.argument = first_argument,
.object = field_get.object,
},
}, },
},
else => .{
.invoke = .{
.argument = first_argument,
.object = unnecessary_temp,
},
},
}
}); });
}, },

View File

@ -1,5 +1,3 @@
const app = @import("./app.zig");
const coral = @import("coral"); const coral = @import("coral");
const ext = @import("./ext.zig"); const ext = @import("./ext.zig");
@ -10,84 +8,306 @@ const heap = @import("./heap.zig");
const kym = @import("./kym.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 { fn last_sdl_error() [:0]const u8 {
return coral.io.slice_sentineled(@as(u8, 0), @as([*:0]const u8, @ptrCast(ext.SDL_GetError()))); 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 { pub fn run_app(file_access: file.Access) void {
defer heap.trace_leaks(); defer heap.trace_leaks();
if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) { 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(); 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, .{ var script_env = kym.RuntimeEnv.init(heap.allocator, 255, .{
.print = app.log_info,
.print_error = app.log_fail,
.import_access = file_access, .import_access = file_access,
.print_out = log_info,
.print_err = log_fail,
}) catch { }) catch {
return app.log_fail("failed to initialize script runtime"); return log_fail("failed to initialize script runtime");
}; };
defer script_env.deinit(); 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: { var ticks_previous = ext.SDL_GetTicks64();
const pos = ext.SDL_WINDOWPOS_CENTERED; var accumulated_time = @as(f64, 0);
const flags = 0;
break: create ext.SDL_CreateWindow(&manifest.title, pos, pos, manifest.width, manifest.height, flags) orelse { running_loop: while (true) {
return app.log_fail(last_sdl_error()); 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: { while (accumulated_time >= app.target_frame_time) : (accumulated_time -= app.target_frame_time) {
const default_driver_index = -1; if (!app.poll()) {
const flags = ext.SDL_RENDERER_ACCELERATED; break: running_loop;
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 => {},
}
}
} }
{ app.update(&script_env, @floatCast(app.target_frame_time)) catch return;
// 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.render();
ext.SDL_Delay(1);
} }
} }