From c1f174a513f5f5da05591095fbef837432428683 Mon Sep 17 00:00:00 2001 From: kayomn Date: Mon, 10 Jul 2023 01:10:56 +0100 Subject: [PATCH] Get rid of extraneous and unused code --- .vscode/launch.json | 12 +- .vscode/settings.json | 22 +- .vscode/tasks.json | 21 +- build.zig | 44 +- debug/app.ona | 7 +- readme.md | 2 - source/coral/arena.zig | 112 +++--- source/coral/coral.zig | 36 +- source/coral/debug.zig | 6 +- source/coral/io.zig | 593 +++++++++++++-------------- source/coral/list.zig | 152 +------ source/coral/map.zig | 304 ++++++++++++++ source/coral/math.zig | 131 +----- source/coral/slab.zig | 178 -------- source/coral/slots.zig | 236 ----------- source/coral/table.zig | 299 -------------- source/coral/utf8.zig | 354 ++++------------ source/ona/app.zig | 128 ++++++ source/ona/canvas.zig | 23 -- source/ona/ext.zig | 3 +- source/ona/file.zig | 166 ++++---- source/ona/heap.zig | 279 +++++-------- source/ona/kym.zig | 824 ++++++++++++++++++++------------------ source/ona/kym/Ast.zig | 170 +++----- source/ona/kym/State.zig | 116 ++++++ source/ona/kym/tokens.zig | 12 +- source/ona/ona.zig | 216 ++++------ source/runner.zig | 6 +- source/test.zig | 3 - 29 files changed, 1790 insertions(+), 2665 deletions(-) mode change 100755 => 100644 .vscode/launch.json mode change 100755 => 100644 .vscode/settings.json mode change 100755 => 100644 .vscode/tasks.json mode change 100755 => 100644 build.zig mode change 100755 => 100644 source/coral/arena.zig mode change 100755 => 100644 source/coral/coral.zig mode change 100755 => 100644 source/coral/debug.zig mode change 100755 => 100644 source/coral/io.zig mode change 100755 => 100644 source/coral/list.zig create mode 100644 source/coral/map.zig mode change 100755 => 100644 source/coral/math.zig delete mode 100644 source/coral/slab.zig delete mode 100755 source/coral/slots.zig delete mode 100755 source/coral/table.zig mode change 100755 => 100644 source/coral/utf8.zig create mode 100644 source/ona/app.zig delete mode 100755 source/ona/canvas.zig mode change 100755 => 100644 source/ona/ext.zig mode change 100755 => 100644 source/ona/kym.zig create mode 100644 source/ona/kym/State.zig mode change 100755 => 100644 source/ona/ona.zig mode change 100755 => 100644 source/runner.zig delete mode 100644 source/test.zig diff --git a/.vscode/launch.json b/.vscode/launch.json old mode 100755 new mode 100644 index ffbda43..8eeeb8d --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,20 +2,12 @@ "version": "0.2.0", "configurations": [ { - "name": "Runtime", + "name": "Runner", "type": "gdb", "request": "launch", - "target": "${workspaceRoot}/zig-out/bin/ona-runner", + "target": "${workspaceRoot}/zig-out/bin/runner", "cwd": "${workspaceRoot}/debug/", "valuesFormatting": "parseText" }, - { - "name": "Build Script", - "type": "python", - "request": "launch", - "program": "./build.py", - "console": "integratedTerminal", - "justMyCode": true - } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json old mode 100755 new mode 100644 index 5b91ad5..13fdcff --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,13 @@ { - "editor.minimap.maxColumn": 120, - "editor.detectIndentation": false, - "editor.insertSpaces": false, - "editor.rulers": [120], - "files.trimTrailingWhitespace": true, - "files.insertFinalNewline": true, - "zig.formattingProvider": "off", - "zig.zls.enableAutofix": false, - "editor.formatOnSave": false, - "spellright.language": ["en-US-10-1."], - "spellright.documentTypes": ["markdown"], + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + + "[zig]": { + "editor.formatOnSave": false, + "files.eol": "\n", + "editor.minimap.maxColumn": 120, + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.rulers": [120], + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json old mode 100755 new mode 100644 index 3c18e06..8ae1ee3 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,24 +2,23 @@ "version": "2.0.0", "tasks": [ { - "label": "build", - "type": "process", - "command": "zig", - "args": ["build"], + "label": "Build All", + "type": "shell", + "command": "zig build", + "group": { + "kind": "build", + "isDefault": true + }, "problemMatcher": "$gcc", "presentation": { "echo": true, "reveal": "silent", "focus": false, "panel": "shared", - "showReuseMessage": true, + "showReuseMessage": false, "clear": true, "revealProblems": "onProblem" - }, - "group": { - "kind": "build", - "isDefault": true - }, + } } ] -} +} \ No newline at end of file diff --git a/build.zig b/build.zig old mode 100755 new mode 100644 index 079b6d2..2db09f4 --- a/build.zig +++ b/build.zig @@ -1,6 +1,9 @@ const std = @import("std"); pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const coral_module = b.createModule(.{.source_file = .{.path = "./source/coral/coral.zig"}}); const ona_module = b.createModule(.{ @@ -14,37 +17,18 @@ pub fn build(b: *std.Build) void { }, }); - const cross_target = b.standardTargetOptions(.{}); - const optimize_mode = std.builtin.Mode.Debug; - - // Ona runner. - { - const ona_exe = b.addExecutable(.{ - .name = "ona-runner", - .root_source_file = .{.path = "./source/runner.zig"}, - .target = cross_target, - .optimize = optimize_mode, + b.installArtifact(create: { + const runner_exe = b.addExecutable(.{ + .name = "runner", + .root_source_file = .{ .path = "source/runner.zig" }, + .target = target, + .optimize = optimize, }); - ona_exe.addModule("coral", coral_module); - ona_exe.addModule("ona", ona_module); + runner_exe.addModule("ona", ona_module); + runner_exe.linkLibC(); + runner_exe.linkSystemLibrary("SDL2"); - // ona_exe.addIncludeDir("./ext"); - ona_exe.linkSystemLibrary("SDL2"); - ona_exe.linkLibC(); - b.installArtifact(ona_exe); - } - - // Test step. - { - const exe_tests = b.addTest(.{ - .root_source_file = .{.path = "source/test.zig"}, - .target = cross_target, - .optimize = optimize_mode, - }); - - const test_step = b.step("test", "Run unit tests"); - - test_step.dependOn(&exe_tests.step); - } + break: create runner_exe; + }); } diff --git a/debug/app.ona b/debug/app.ona index 6431a29..7b46045 100644 --- a/debug/app.ona +++ b/debug/app.ona @@ -1,6 +1,7 @@ return { - title = "Afterglow", - width = 1280, - height = 800, + title = "Afterglow", + width = 1280, + height = 800, + tick_rate = 60, } diff --git a/readme.md b/readme.md index 227917a..3003306 100755 --- a/readme.md +++ b/readme.md @@ -26,8 +26,6 @@ Ona is also the Catalan word for "wave". * Provide utilities for handling rendering but otherwise leave the higher-level game logic and data structuring to the programmer. -* Provide a simple scripting interface for people who want to do something quick and a powerful plug-in API for engine-level extensions and speed-critical application logic. - ## Technical Details ### Requirements diff --git a/source/coral/arena.zig b/source/coral/arena.zig old mode 100755 new mode 100644 index 40f209e..3600a0d --- a/source/coral/arena.zig +++ b/source/coral/arena.zig @@ -12,46 +12,23 @@ pub const Stacking = struct { allocations: list.Stack(usize), pages: list.Stack(Page), + const AllocationsList = list.Stack(usize); + const Page = struct { - buffer: []u8, + buffer: []io.Byte, used: usize, - const Self = @This(); - - fn available(self: Self) usize { + fn available(self: Page) usize { return self.buffer.len - self.used; } }; - pub fn allocate(self: *Stacking, allocation_size: usize) io.AllocationError![]u8 { - const alignment = @as(usize, 4); - const aligned_allocation_size = (allocation_size + alignment - 1) & ~(alignment - 1); - - if (self.pages.values.len == 0) { - const page = try self.allocate_page(math.max(self.min_page_size, aligned_allocation_size)); - - page.used = allocation_size; - - return page.buffer[0 .. allocation_size]; - } - - var page = self.current_page() orelse unreachable; - - if (page.available() <= aligned_allocation_size) { - page = try self.allocate_page(math.max(self.min_page_size, aligned_allocation_size)); - } - - debug.assert(page.available() >= allocation_size); - - defer page.used += aligned_allocation_size; - - return page.buffer[page.used .. (page.used + allocation_size)]; - } + const PageList = list.Stack(Page); fn allocate_page(self: *Stacking, page_size: usize) io.AllocationError!*Page { - var buffer = try io.allocate_many(self.page_allocator, page_size, u8); + var buffer = try self.page_allocator.reallocate(null, page_size); - errdefer io.deallocate(self.page_allocator, buffer); + errdefer self.page_allocator.deallocate(buffer); try self.pages.push_one(.{ .buffer = buffer, @@ -62,25 +39,10 @@ pub const Stacking = struct { } pub fn as_allocator(self: *Stacking) io.Allocator { - return io.Allocator.bind(Stacking, self, struct { - fn reallocate(stacking: *Stacking, options: io.AllocationOptions) ?[]u8 { - const allocation = options.allocation orelse { - return stacking.allocate(options.size) catch null; - }; - - if (allocation.len == 0) { - return null; - } - - const reallocation = stacking.allocate(allocation.len) catch { - return null; - }; - - io.copy(reallocation, allocation); - - return reallocation; - } - }.reallocate); + return io.Allocator.bind(Stacking, self, .{ + .deallocate = deallocate, + .reallocate = reallocate, + }); } fn current_page(self: Stacking) ?*Page { @@ -91,20 +53,58 @@ pub const Stacking = struct { return &self.pages.values[self.pages.values.len - 1]; } - pub fn deinit(self: *Stacking) void { + pub fn free(self: *Stacking) void { for (self.pages.values) |page| { - io.deallocate(self.page_allocator, page.buffer); + self.page_allocator.deallocate(page.buffer); } - self.pages.deinit(); - self.allocations.deinit(); + self.pages.free(); + self.allocations.free(); } - pub fn init(allocator: io.Allocator, min_page_size: usize) io.AllocationError!Stacking { + pub fn deallocate(_: *Stacking, _: []io.Byte) void { + // TODO: Decide how to implement. + } + + pub fn reallocate(self: *Stacking, return_address: usize, existing_allocation: ?[]io.Byte, size: usize) io.AllocationError![]io.Byte { + // TODO: Safety-check existing allocation is from allocator or null. + _ = return_address; + + const alignment = @as(usize, 4); + const aligned_size = (size + alignment - 1) & ~(alignment - 1); + + if (self.pages.values.len == 0) { + const page = try self.allocate_page(math.max(self.min_page_size, aligned_size)); + + page.used = size; + + return page.buffer[0 .. size]; + } + + var page = self.current_page() orelse unreachable; + + if (page.available() <= aligned_size) { + page = try self.allocate_page(math.max(self.min_page_size, aligned_size)); + } + + debug.assert(page.available() >= size); + + defer page.used += aligned_size; + + const reallocation = page.buffer[page.used .. (page.used + size)]; + + if (existing_allocation) |allocation| { + io.copy(reallocation, allocation); + } + + return reallocation; + } + + pub fn make(allocator: io.Allocator, min_page_size: usize) Stacking { return Stacking{ + .allocations = AllocationsList.make(allocator), + .pages = PageList.make(allocator), .page_allocator = allocator, - .allocations = .{.allocator = allocator}, - .pages = .{.allocator = allocator}, .min_page_size = min_page_size, }; } diff --git a/source/coral/coral.zig b/source/coral/coral.zig old mode 100755 new mode 100644 index 8f6450f..e1c021b --- a/source/coral/coral.zig +++ b/source/coral/coral.zig @@ -1,44 +1,14 @@ -/// -/// Arena-based memory allocation strategies. -/// + pub const arena = @import("./arena.zig"); -/// -/// Debug build-only utilities and sanity-checkers. -/// pub const debug = @import("./debug.zig"); -/// -/// Platform-agnostic data input and output operations. -/// pub const io = @import("./io.zig"); -/// -/// Data structures and utilities for sequential, "list-like" collections. -/// pub const list = @import("./list.zig"); -/// -/// Types and functions designed for mathematics in interactive media applications. -/// +pub const map = @import("./map.zig"); + pub const math = @import("./math.zig"); -/// -/// Data structures and utilities for fragmented, "heap-like" collections. -/// -pub const slab = @import("./slab.zig"); - -/// -/// Data structures and utilities for the highly-specialized "slotmap" collection. -/// -pub const slots = @import("./slots.zig"); - -/// -/// Data structures and utilities for associative, "table-like" collections. -/// -pub const table = @import("./table.zig"); - -/// -/// Converters, parsers, and validators for sequences of bytes treated as UTF8 unicode strings. -/// pub const utf8 = @import("./utf8.zig"); diff --git a/source/coral/debug.zig b/source/coral/debug.zig old mode 100755 new mode 100644 index b38082e..e6a229e --- a/source/coral/debug.zig +++ b/source/coral/debug.zig @@ -1,8 +1,4 @@ -/// -/// Active code comment to assert that `condition` should always be true. -/// -/// Safety-checked behavior is invoked where `condition` evaluates to false. -/// + pub fn assert(condition: bool) void { if (!condition) { unreachable; diff --git a/source/coral/io.zig b/source/coral/io.zig old mode 100755 new mode 100644 index e898b8c..8ff87bd --- a/source/coral/io.zig +++ b/source/coral/io.zig @@ -1,310 +1,283 @@ -const debug = @import("./debug.zig"); - -const math = @import("./math.zig"); - -pub const AllocationError = error { - OutOfMemory, -}; - -pub const AllocationOptions = struct { - return_address: usize, - allocation: ?[]u8 = null, - size: usize, -}; - -pub const Allocator = Generator(?[]u8, AllocationOptions); - -/// -/// -/// -pub const Byte = u8; - -/// -/// Function pointer coupled with an immutable state context for providing dynamic dispatch over a given `Input` and -/// `Output`. -/// -pub fn Functor(comptime Output: type, comptime Input: type) type { - return struct { - context: *const anyopaque, - invoker: *const fn (capture: *const anyopaque, input: Input) Output, - - const Self = @This(); - - pub fn bind(comptime State: type, state: *const State, comptime invoker: fn (capture: *const State, input: Input) Output) Self { - const alignment = @alignOf(State); - const is_zero_aligned = alignment == 0; - - return .{ - .context = if (is_zero_aligned) state else @ptrCast(*const anyopaque, state), - - .invoker = struct { - fn invoke_opaque(context: *const anyopaque, input: Input) Output { - if (is_zero_aligned) { - return invoker(@ptrCast(*const State, context), input); - } - - return invoker(@ptrCast(*const State, @alignCast(alignment, context)), input); - } - }.invoke_opaque, - }; - } - - pub fn invoke(self: Self, input: Input) Output { - return self.invoker(self.context, input); - } - }; -} - -/// -/// Function pointer coupled with a mutable state context for providing dynamic dispatch over a given `Input` and -/// `Output`. -/// -pub fn Generator(comptime Output: type, comptime Input: type) type { - return struct { - context: *anyopaque, - invoker: *const fn (capture: *anyopaque, input: Input) Output, - - const Self = @This(); - - pub fn bind(comptime State: type, state: *State, comptime invoker: fn (capture: *State, input: Input) Output) Self { - const alignment = @alignOf(State); - const is_zero_aligned = alignment == 0; - - return .{ - .context = if (is_zero_aligned) state else @ptrCast(*anyopaque, state), - - .invoker = struct { - fn invoke_opaque(context: *anyopaque, input: Input) Output { - if (is_zero_aligned) { - return invoker(@ptrCast(*State, context), input); - } - - return invoker(@ptrCast(*State, @alignCast(alignment, context)), input); - } - }.invoke_opaque, - }; - } - - pub fn invoke(self: Self, input: Input) Output { - return self.invoker(self.context, input); - } - }; -} - -pub const Reader = Generator(?usize, []u8); - -pub const StreamError = error { - ReadFailure, - WriteFailure, -}; - -pub fn Tag(comptime Element: type) type { - return switch (@typeInfo(Element)) { - .Enum => |info| info.tag_type, - .Union => |info| info.tag_type orelse @compileError(@typeName(Element) ++ " has no tag type"), - else => @compileError("expected enum or union type, found '" ++ @typeName(Element) ++ "'"), - }; -} - -pub const FixedBuffer = struct { - slice: []u8, - - pub fn as_writer(self: *FixedBuffer) Writer { - return Writer.bind(FixedBuffer, self, struct { - fn write(writable_memory: *FixedBuffer, data: []const u8) ?usize { - return writable_memory.write(data); - } - }.write); - } - - pub fn put(self: *FixedBuffer, byte: u8) bool { - if (self.slice.len == 0) { - return false; - } - - self.slice[0] = byte; - self.slice = self.slice[1 ..]; - - return true; - } - - pub fn write(self: *FixedBuffer, bytes: []const u8) usize { - const writable = math.min(self.slice.len, bytes.len); - - copy(self.slice, bytes); - - self.slice = self.slice[writable ..]; - - return writable; - } -}; - -pub const Writer = Generator(?usize, []const Byte); - -pub fn allocate_many(allocator: Allocator, amount: usize, comptime Type: type) AllocationError![]Type { - if (@sizeOf(Type) == 0) { - @compileError("Cannot allocate memory for 0-byte type " ++ @typeName(Type)); - } - - return @ptrCast([*]Type, @alignCast(@alignOf(Type), allocator.invoke(.{ - .size = @sizeOf(Type) * amount, - .return_address = @returnAddress(), - }) orelse return error.OutOfMemory))[0 .. amount]; -} - -pub fn allocate_one(allocator: Allocator, value: anytype) AllocationError!*@TypeOf(value) { - const Type = @TypeOf(value); - - if (@sizeOf(Type) == 0) { - @compileError("Cannot allocate memory for 0-byte type " ++ @typeName(Type)); - } - - const allocation = @ptrCast(*Type, @alignCast(@alignOf(Type), allocator.invoke(.{ - .size = @sizeOf(Type), - .return_address = @returnAddress(), - }) orelse return error.OutOfMemory)); - - allocation.* = value; - - return allocation; -} - -pub fn bytes_of(value: anytype) []const u8 { - const pointer_info = @typeInfo(@TypeOf(value)).Pointer; - - debug.assert(pointer_info.size == .One); - - return @ptrCast([*]const u8, value)[0 .. @sizeOf(pointer_info.child)]; -} - -pub fn compare(this: []const u8, that: []const u8) isize { - const range = math.min(this.len, that.len); - var index: usize = 0; - - while (index < range) : (index += 1) { - const difference = @intCast(isize, this[index]) - @intCast(isize, that[index]); - - if (difference != 0) { - return difference; - } - } - - return @intCast(isize, this.len) - @intCast(isize, that.len); -} - -pub fn copy(target: []u8, source: []const u8) void { - var index: usize = 0; - - while (index < source.len) : (index += 1) target[index] = source[index]; -} - -pub fn deallocate(allocator: Allocator, allocation: anytype) void { - switch (@typeInfo(@TypeOf(allocation))) { - .Pointer => |pointer| { - _ = allocator.invoke(.{ - .allocation = switch (pointer.size) { - .One => @ptrCast([*]u8, allocation)[0 .. @sizeOf(pointer.child)], - .Slice => @ptrCast([*]u8, allocation.ptr)[0 .. (@sizeOf(pointer.child) * allocation.len)], - .Many, .C => @compileError("length of allocation must be known to deallocate"), - }, - - .return_address = @returnAddress(), - .size = 0, - }); - }, - - else => @compileError("cannot deallocate " ++ allocation), - } -} - -pub fn ends_with(target: []const u8, match: []const u8) bool { - if (target.len < match.len) return false; - - var index = @as(usize, 0); - - while (index < match.len) : (index += 1) { - if (target[target.len - (1 + index)] != match[match.len - (1 + index)]) return false; - } - - return true; -} - -pub fn equals(this: []const u8, that: []const u8) bool { - if (this.len != that.len) return false; - - { - var index: usize = 0; - - while (index < this.len) : (index += 1) if (this[index] != that[index]) return false; - } - - return true; -} - -var null_context = @as(usize, 0); - -pub const null_allocator = Allocator.bind(&null_context, struct { - fn reallocate(context: *usize, options: AllocationOptions) ?[]u8 { - debug.assert(context.* == 0); - debug.assert(options.allocation == null); - - return null; - } -}); - -pub const null_writer = Writer.bind(&null_context, struct { - fn write(context: *usize, buffer: []const u8) usize { - debug.assert(context.* == 0); - - return buffer.len; - } -}.write); - -pub fn reallocate(allocator: Allocator, allocation: anytype, amount: usize) AllocationError![]@typeInfo(@TypeOf(allocation)).Pointer.child { - const pointer_info = @typeInfo(@TypeOf(allocation)).Pointer; - const Element = pointer_info.child; - - return @ptrCast([*]Element, @alignCast(@alignOf(Element), (allocator.invoke(switch (pointer_info.size) { - .Slice => .{ - .allocation = @ptrCast([*]u8, allocation.ptr)[0 .. (@sizeOf(Element) * allocation.len)], - .size = @sizeOf(Element) * amount, - }, - - .Many, .C, .One => @compileError("allocation must be a slice to reallocate"), - }) orelse return error.OutOfMemory).ptr))[0 .. amount]; -} - -pub fn sentinel_index(comptime element: type, comptime sentinel: element, sequence: [*:sentinel]const element) usize { - var index: usize = 0; - - while (sequence[index] != sentinel) : (index += 1) {} - - return index; -} - -pub fn stream(output: Writer, input: Reader, buffer: []u8) StreamError!u64 { - var total_written: u64 = 0; - var read = input.invoke(buffer) orelse return error.ReadFailure; - - while (read != 0) { - total_written += output.invoke(buffer[0..read]) orelse return error.WriteFailure; - read = input.invoke(buffer) orelse return error.ReadFailure; - } - - return total_written; -} - -pub fn swap(comptime Element: type, this: *Element, that: *Element) void { - const temp = this.*; - - this.* = that.*; - that.* = temp; -} - -pub fn tag_of(comptime value: anytype) Tag(@TypeOf(value)) { - return @as(Tag(@TypeOf(value)), value); -} - -pub fn zero(target: []u8) void { - for (target) |*t| t.* = 0; -} +const debug = @import("./debug.zig"); + +const math = @import("./math.zig"); + +pub const AllocationError = error { + OutOfMemory, +}; + +pub const Allocator = struct { + context: *anyopaque, + + actions: *const struct { + deallocate: *const fn (context: *anyopaque, allocation: []Byte) void, + reallocate: *const fn (context: *anyopaque, return_address: usize, existing_allocation: ?[]Byte, size: usize) AllocationError![]Byte, + }, + + pub fn Actions(comptime State: type) type { + return struct { + deallocate: fn (state: *State, allocation: []Byte) void, + reallocate: fn (state: *State, return_address: usize, existing_allocation: ?[]Byte, size: usize) AllocationError![]Byte, + }; + } + + pub fn bind(comptime State: type, state: *State, comptime actions: Actions(State)) Allocator { + const is_zero_aligned = @alignOf(State) == 0; + + return .{ + .context = if (is_zero_aligned) state else @ptrCast(state), + + .actions = &.{ + .deallocate = struct { + fn deallocate(context: *anyopaque, allocation: []Byte) void { + if (is_zero_aligned) { + return actions.deallocator(@ptrCast(context), allocation); + } + + return actions.deallocate(@ptrCast(@alignCast(context)), allocation); + } + }.deallocate, + + .reallocate = 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(@alignCast(context)), return_address, existing_allocation, size); + } + }.reallocate, + } + }; + } + + pub fn deallocate(self: Allocator, allocation: anytype) void { + switch (@typeInfo(@TypeOf(allocation))) { + .Pointer => |pointer| { + self.actions.deallocate(self.context, switch (pointer.size) { + .One => @as([*]Byte, @ptrCast(allocation))[0 .. @sizeOf(pointer.child)], + .Slice => @as([*]Byte, @ptrCast(allocation.ptr))[0 .. (@sizeOf(pointer.child) * allocation.len)], + .Many, .C => @compileError("length of allocation must be known to deallocate"), + }); + }, + + else => @compileError("cannot deallocate " ++ allocation), + } + } + + pub fn reallocate(self: Allocator, allocation: ?[]Byte, allocation_size: usize) AllocationError![]Byte { + return self.actions.reallocate(self.context, @returnAddress(), allocation, allocation_size); + } +}; + +pub const Byte = u8; + +pub const FixedBuffer = struct { + bytes: []Byte, + + pub fn as_writer(self: *FixedBuffer) Writer { + return Writer.bind(FixedBuffer, self, struct { + fn write(writable_memory: *FixedBuffer, data: []const Byte) ?usize { + return writable_memory.write(data); + } + }.write); + } + + pub fn put(self: *FixedBuffer, byte: Byte) bool { + if (self.bytes.len == 0) { + return false; + } + + self.bytes[0] = byte; + self.bytes = self.bytes[1 ..]; + + return true; + } + + pub fn write(self: *FixedBuffer, bytes: []const Byte) usize { + const writable = math.min(self.bytes.len, bytes.len); + + copy(self.bytes, bytes); + + self.bytes = self.bytes[writable ..]; + + return writable; + } +}; + +pub fn Functor(comptime Output: type, comptime Input: type) type { + return struct { + context: *const anyopaque, + invoker: *const fn (capture: *const anyopaque, input: Input) Output, + + const Self = @This(); + + pub fn bind(comptime State: type, state: *const State, comptime invoker: fn (capture: *const State, input: Input) Output) Self { + const is_zero_aligned = @alignOf(State) == 0; + + return .{ + .context = if (is_zero_aligned) state else @ptrCast(state), + + .invoker = struct { + fn invoke(context: *const anyopaque, input: Input) Output { + if (is_zero_aligned) { + return invoker(@ptrCast(context), input); + } + + return invoker(@ptrCast(@alignCast(context)), input); + } + }.invoke, + }; + } + + pub fn invoke(self: Self, input: Input) Output { + return self.invoker(self.context, input); + } + }; +} + +pub fn Generator(comptime Output: type, comptime Input: type) type { + return struct { + context: *anyopaque, + invoker: *const fn (capture: *anyopaque, input: Input) Output, + + const Self = @This(); + + pub fn bind(comptime State: type, state: *State, comptime invoker: fn (capture: *State, input: Input) Output) Self { + const is_zero_aligned = @alignOf(State) == 0; + + return .{ + .context = if (is_zero_aligned) state else @ptrCast(state), + + .invoker = struct { + fn invoke(context: *anyopaque, input: Input) Output { + if (is_zero_aligned) { + return invoker(@ptrCast(context), input); + } + + return invoker(@ptrCast(@alignCast(context)), input); + } + }.invoke, + }; + } + + pub fn invoke(self: Self, input: Input) Output { + return self.invoker(self.context, input); + } + }; +} + +pub fn Tag(comptime Element: type) type { + return switch (@typeInfo(Element)) { + .Enum => |info| info.tag_type, + .Union => |info| info.tag_type orelse @compileError(@typeName(Element) ++ " has no tag type"), + else => @compileError("expected enum or union type, found '" ++ @typeName(Element) ++ "'"), + }; +} + +pub const Writer = Generator(?usize, []const Byte); + +pub fn allocate_copy(allocator: Allocator, source: []const Byte) AllocationError![]Byte { + const allocation = try allocator.actions.reallocate(allocator.context, @returnAddress(), null, source.len); + + copy(allocation, source); + + return allocation; +} + +pub fn allocate_one(allocator: Allocator, 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 allocation = @as(*Type, @ptrCast(@alignCast(try allocator.actions.reallocate( + allocator.context, + @returnAddress(), + null, + typeSize)))); + + allocation.* = value; + + return allocation; +} + +pub fn bytes_of(value: anytype) []const Byte { + const pointer_info = @typeInfo(@TypeOf(value)).Pointer; + + return switch (pointer_info.size) { + .One => @as([*]const Byte, @ptrCast(value))[0 .. @sizeOf(pointer_info.child)], + .Slice => @as([*]const Byte, @ptrCast(value.ptr))[0 .. @sizeOf(pointer_info.child) * value.len], + else => @compileError("`value` must be single-element pointer or slice type"), + }; +} + +pub fn copy(target: []Byte, source: []const Byte) void { + var index: usize = 0; + + while (index < source.len) : (index += 1) { + target[index] = source[index]; + } +} + +pub fn ends_with(target: []const Byte, match: []const Byte) bool { + if (target.len < match.len) { + return false; + } + + { + var index = @as(usize, 0); + + while (index < match.len) : (index += 1) { + if (target[target.len - (1 + index)] != match[match.len - (1 + index)]) { + return false; + } + } + } + + return true; +} + +pub fn equals(target: []const Byte, match: []const Byte) bool { + if (target.len != match.len) { + return false; + } + + for (0 .. target.len) |index| { + if (target[index] != match[index]) { + return false; + } + } + + return true; +} + +var null_context = @as(usize, 0); + +pub const null_writer = Writer.bind(usize, &null_context, struct { + fn write(context: *usize, buffer: []const u8) ?usize { + debug.assert(context.* == 0); + + return buffer.len; + } +}.write); + +pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) { + var len = @as(usize, 0); + + while (ptr[len] != sen) { + len += 1; + } + + return ptr[0 .. len:sen]; +} + +pub fn tag_of(comptime value: anytype) Tag(@TypeOf(value)) { + return @as(Tag(@TypeOf(value)), value); +} + +pub fn zero(target: []Byte) void { + for (target) |*t| t.* = 0; +} diff --git a/source/coral/list.zig b/source/coral/list.zig old mode 100755 new mode 100644 index 077bc39..48bc310 --- a/source/coral/list.zig +++ b/source/coral/list.zig @@ -1,97 +1,54 @@ -const debug = @import("./debug.zig"); - const io = @import("./io.zig"); const math = @import("./math.zig"); -/// -/// Returns a dynamically sized stack capable of holding `Value`. -/// +pub const ByteStack = Stack(io.Byte); + pub fn Stack(comptime Value: type) type { return struct { allocator: io.Allocator, - capacity: usize = 0, - values: []Value = &.{}, + capacity: usize, + values: []Value, - /// - /// Stack type. - /// const Self = @This(); - /// - /// Clears all elements from `self` while preserving the current internal buffer. - /// - /// To clean up memory allocations made by the stack and deinitialize it, see [deinit] instead. - /// pub fn clear(self: *Self) void { self.values = self.values[0 .. 0]; } - /// - /// Deinitializes `self` and sets it to an invalid state, freeing all memory allocated by `allocator`. - /// - /// To clear all items from the stack while preserving the current internal buffer, see [clear] instead. - /// - /// *Note* if the `capacity` field of `self` is a non-zero value, `allocator` must reference the same allocation - /// strategy as the one originally used to allocate the current internal buffer. - /// - pub fn deinit(self: *Self) void { + pub fn free(self: *Self) void { if (self.capacity == 0) { return; } - io.deallocate(self.allocator, self.values.ptr[0 .. self.capacity]); + self.allocator.deallocate(self.values.ptr[0 .. self.capacity]); self.values = &.{}; - self.capacity = 0; } - /// - /// Attempts to remove `amount` number of `Value`s from the stack, returning `bool` if it was successful, - /// otherwise `false` if the stack contains fewer elements than `amount`. - /// - pub fn drop(self: *Self, amount: usize) bool { - if (amount > self.values.len) { - return false; - } - - self.values = self.values[0 .. self.values.len - amount]; - - return true; - } - - /// - /// Attempts to grow the internal buffer of `self` by `growth_amount` using `allocator`. - /// - /// The function returns [io.AllocatorError] if `allocator` could not commit the memory required to grow the - /// internal buffer by `growth_amount`, leaving `self` in the same state that it was in prior to starting the - /// grow. - /// - /// Growing ahead of multiple push operations is useful when the upper bound of pushes is well-understood, as it - /// can reduce the number of allocations required per push. - /// pub fn grow(self: *Self, growth_amount: usize) io.AllocationError!void { const grown_capacity = self.capacity + growth_amount; - const values = (try io.allocate_many(self.allocator, grown_capacity, Value))[0 .. self.values.len]; + const buffer = try self.allocator.reallocate(null, @sizeOf(Value) * grown_capacity); - errdefer io.deallocate(self.allocator, values); + errdefer self.allocator.deallocate(buffer); if (self.capacity != 0) { - for (0 .. self.values.len) |index| { - values[index] = self.values[index]; - } - - io.deallocate(self.allocator, self.values.ptr[0 .. self.capacity]); + io.copy(buffer, io.bytes_of(self.values)); + self.allocator.deallocate(self.values.ptr[0 .. self.capacity]); } - self.values = values; + self.values = @as([*]Value, @ptrCast(@alignCast(buffer)))[0 .. self.values.len]; self.capacity = grown_capacity; } - /// - /// Attempts to remove the last element of `self` that was inserted, if one exists, returning it or `null` if - /// `self` is empty. - /// + pub fn make(allocator: io.Allocator) Self { + return .{ + .allocator = allocator, + .capacity = 0, + .values = &.{}, + }; + } + pub fn pop(self: *Self) ?Value { if (self.values.len == 0) { return null; @@ -104,59 +61,6 @@ pub fn Stack(comptime Value: type) type { return self.values[last_index]; } - /// - /// Attempts to push every `Value` in `values` to `self` using `allocator` to grow the internal buffer as - /// necessary. - /// - /// The function returns [io.AllocationError] if `allocator` could not commit the memory required to grow the - /// internal buffer of `self` when necessary. - /// - pub fn push_all(self: *Self, values: []const Value) io.AllocationError!void { - const new_length = self.values.len + values.len; - - if (new_length > self.capacity) { - try self.grow(values.len + values.len); - } - - const offset_index = self.values.len; - - self.values = self.values.ptr[0 .. new_length]; - - for (0 .. values.len) |index| { - self.values[offset_index + index] = values[index]; - } - } - - /// - /// Attempts to push the `Value` in `value` to `self` by `amount` number of times using `allocator` to grow - /// the internal buffer as necessary. - /// - /// The function returns [io.AllocationError] if `allocator` could not commit the memory required to grow the - /// internal buffer of `self` when necessary. - /// - pub fn push_many(self: *Self, value: Value, amount: usize) io.AllocationError!void { - const new_length = self.values.len + amount; - - if (new_length >= self.capacity) { - try self.grow(amount + amount); - } - - const offset_index = self.values.len; - - self.values = self.values.ptr[0 .. new_length]; - - for (0 .. amount) |index| { - self.values[offset_index + index] = value; - } - } - - /// - /// Attempts to push the `Value` in `value` to `self` using `allocator` to grow the internal buffer as - /// necessary. - /// - /// The function returns [io.AllocationError] if `allocator` could not commit the memory required to grow the - /// internal buffer of `self` when necessary. - /// pub fn push_one(self: *Self, value: Value) io.AllocationError!void { if (self.values.len == self.capacity) { try self.grow(math.max(1, self.capacity)); @@ -170,21 +74,3 @@ pub fn Stack(comptime Value: type) type { } }; } - -/// -/// -/// -pub const ByteStack = Stack(io.Byte); - -/// -/// Returns a [io.Writer] instance that binds a reference of `self` to the [write] operation. -/// -pub fn stack_as_writer(self: *ByteStack) io.Writer { - return io.Writer.bind(ByteStack, self, struct { - fn write(stack: *ByteStack, bytes: []const io.Byte) ?usize { - stack.push_all(bytes) catch return null; - - return bytes.len; - } - }.write); -} diff --git a/source/coral/map.zig b/source/coral/map.zig new file mode 100644 index 0000000..875d1e6 --- /dev/null +++ b/source/coral/map.zig @@ -0,0 +1,304 @@ +const debug = @import("./debug.zig"); + +const io = @import("./io.zig"); + +const list = @import("./list.zig"); + +const math = @import("./math.zig"); + +pub fn Slab(comptime Value: type) type { + return struct { + next_index: usize, + entries: EntryList, + + const EntryList = list.Stack(union (enum) { + value: Value, + next_index: usize, + }); + + const Self = @This(); + + pub fn lookup(self: Self, key: usize) ?Value { + if (key == 0 or key > self.entries.values.len) { + return null; + } + + return switch (self.entries.values[key - 1]) { + .value => |value| value, + .next_index => null, + }; + } + + pub fn free(self: *Self) void { + self.entries.free(); + + self.next_index = 0; + } + + pub fn insert(self: *Self, value: Value) io.AllocationError!usize { + if (self.next_index < self.entries.values.len) { + const index = self.next_index; + const entry = &self.entries.values[index]; + + debug.assert(entry.* == .next_index); + + self.next_index = entry.next_index; + entry.* = .{.value = value}; + + return index + 1; + } else { + try self.entries.push_one(.{.value = value}); + + self.next_index += 1; + + return self.next_index; + } + } + + pub fn 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 Table(comptime Key: type, comptime Value: type, comptime traits: TableTraits(Key)) type { + const load_max = 0.75; + + return struct { + allocator: io.Allocator, + count: usize, + entries: []?Entry, + + pub const Entry = struct { + key: Key, + value: Value, + + fn write_into(self: Entry, entry_table: []?Entry) bool { + const hash_max = math.min(math.max_int(@typeInfo(usize).Int), entry_table.len); + var hashed_key = math.wrap(traits.hash(self.key), math.min_int(@typeInfo(usize).Int), hash_max); + var iterations = @as(usize, 0); + + while (true) : (iterations += 1) { + debug.assert(iterations < entry_table.len); + + const table_entry = &(entry_table[hashed_key] orelse { + entry_table[hashed_key] = .{ + .key = self.key, + .value = self.value, + }; + + return true; + }); + + if (traits.match(table_entry.key, self.key)) { + return false; + } + + hashed_key = (hashed_key +% 1) % hash_max; + } + } + }; + + pub const Iterable = struct { + table: *Self, + iterations: usize = 0, + + pub fn next(self: *Iterable) ?Entry { + while (self.iterations < self.table.entries.len) { + defer self.iterations += 1; + + if (self.table.entries[self.iterations]) |entry| { + return entry; + } + } + + return null; + } + }; + + const Self = @This(); + + pub fn replace(self: *Self, key: Key, value: Value) io.AllocationError!?Entry { + try self.rehash(load_max); + + debug.assert(self.entries.len > self.count); + + { + const hash_max = math.min(math.max_int(@typeInfo(usize).Int), self.entries.len); + var hashed_key = math.wrap(traits.hash(key), math.min_int(@typeInfo(usize).Int), hash_max); + + while (true) { + const entry = &(self.entries[hashed_key] orelse { + self.entries[hashed_key] = .{ + .key = key, + .value = value, + }; + + self.count += 1; + + return null; + }); + + if (traits.match(entry.key, key)) { + const original_entry = entry.*; + + entry.* = .{ + .key = key, + .value = value, + }; + + return original_entry; + } + + hashed_key = (hashed_key +% 1) % hash_max; + } + } + } + + pub fn calculate_load_factor(self: Self) f32 { + return if (self.entries.len == 0) 1 else @as(f32, @floatFromInt(self.count)) / @as(f32, @floatFromInt(self.entries.len)); + } + + pub fn clear(self: *Self) void { + for (self.entries) |*entry| { + entry.* = null; + } + + self.count = 0; + } + + pub fn free(self: *Self) void { + if (self.entries.len == 0) { + return; + } + + self.allocator.deallocate(self.entries); + + self.entries = &.{}; + self.count = 0; + } + + pub fn insert(self: *Self, key: Key, value: Value) io.AllocationError!bool { + try self.rehash(load_max); + + debug.assert(self.entries.len > self.count); + + defer self.count += 1; + + const entry = Entry{ + .key = key, + .value = value, + }; + + return entry.write_into(self.entries); + } + + pub fn lookup(self: Self, key: Key) ?Value { + if (self.count == 0) { + return null; + } + + const hash_max = math.min(math.max_int(@typeInfo(usize).Int), self.entries.len); + var hashed_key = math.wrap(traits.hash(key), math.min_int(@typeInfo(usize).Int), hash_max); + 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; + } + + hashed_key = (hashed_key +% 1) % hash_max; + } + + return null; + } + + pub fn make(allocator: io.Allocator) Self { + return .{ + .allocator = allocator, + .count = 0, + .entries = &.{}, + }; + } + + pub fn rehash(self: *Self, max_load: f32) io.AllocationError!void { + if (self.calculate_load_factor() <= max_load) { + return; + } + + const min_count = math.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]; + + errdefer self.allocator.deallocate(allocation); + + self.entries = replace_table: { + for (allocation) |*entry| { + entry.* = null; + } + + 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; + }; + } + }; +} + +pub fn TableTraits(comptime Key: type) type { + return struct { + hash: fn (key: Key) usize, + match: fn (key: Key, key: Key) bool, + }; +} + +pub const string_table_traits = TableTraits([]const io.Byte){ + .hash = struct { + fn hash(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; + } + }.hash, + + .match = io.equals, +}; diff --git a/source/coral/math.zig b/source/coral/math.zig old mode 100755 new mode 100644 index b38a62d..b0cfb32 --- a/source/coral/math.zig +++ b/source/coral/math.zig @@ -1,147 +1,21 @@ const std = @import("std"); -/// -/// Errors that may occur during checked integer arithmetic operations. -/// -pub const CheckedArithmeticError = error { - IntOverflow, -}; - -/// -/// Returns the float type described by `float`. -/// -pub fn Float(comptime float: std.builtin.Type.Float) type { - return @Type(.{.Float = float}); -} - -/// -/// Returns the integer type described by `int`. -/// -pub fn Int(comptime int: std.builtin.Type.Int) type { - return @Type(.{.Int = int}); -} - -/// -/// Two-dimensional vector type. -/// -pub const Vector2 = extern struct { - x: f32, - y: f32, - - /// - /// A [Vector2] with a value of `0` assigned to all of the components. - /// - pub const zero = Vector2{.x = 0, .y = 0}; -}; - -/// -/// Attempts to perform a checked addition between `a` and `b`, returning the result or [CheckedArithmeticError] if the -/// operation tried to invoke safety-checked behavior. -/// -/// `checked_add` can be seen as an alternative to the language-native addition operator (+) that exposes the safety- -/// checked behavior in the form of an error type that may be caught or tried on. -/// -pub fn checked_add(a: anytype, b: anytype) CheckedArithmeticError!@TypeOf(a + b) { - const result = @addWithOverflow(a, b); - - if (result.@"1" != 0) { - return error.IntOverflow; - } - - return result.@"0"; -} - -/// -/// Attempts to perform a checked integer cast to the type expressed by `int` on `value`, returning the result or -/// [CheckedArithmeticError] if the operation tried to invoke safety-checked behavior. -/// -/// `checked_cast` can be seen as an alternative to the language-native `@intCast` builtin that exposes the safety- -/// checked behavior in the form of an error type that may be caught or tried on. -/// -pub fn checked_cast(comptime int: std.builtin.Type.Int, value: anytype) CheckedArithmeticError!Int(int) { - if ((value < min_int(int)) or (value > max_int(int))) { - return error.IntOverflow; - } - - return @intCast(Int(int), value); -} - -/// -/// Attempts to perform a checked multiplication between `a` and `b`, returning the result or [CheckedArithmeticError] -/// if the operation tried to invoke safety-checked behavior. -/// -/// `checked_mul` can be seen as an alternative to the language-native multiplication operator (*) that exposes the -/// safety-checked behavior in the form of an error type that may be caught or tried on. -/// -pub fn checked_mul(a: anytype, b: anytype) CheckedArithmeticError!@TypeOf(a * b) { - const result = @mulWithOverflow(a, b); - - if (result.@"1" != 0) { - return error.IntOverflow; - } - - return result.@"0"; -} - -/// -/// Attempts to perform a checked subtraction between `a` and `b`, returning the result or [CheckedArithmeticError] if -/// the operation tried to invoke safety-checked behavior. -/// -/// `checked_sub` can be seen as an alternative to the language-native subtraction operator (-) that exposes the safety- -/// checked behavior in the form of an error type that may be caught or tried on. -/// -pub fn checked_sub(a: anytype, b: anytype) CheckedArithmeticError!@TypeOf(a - b) { - const result = @subWithOverflow(a, b); - - if (result.@"1" != 0) { - return error.IntOverflow; - } - - return result.@"0"; -} - -/// -/// Returns `value` clamped between the inclusive bounds of `lower` and `upper`. -/// -pub fn clamp(value: anytype, lower: anytype, upper: anytype) @TypeOf(value, lower, upper) { - return max(lower, min(upper, value)); -} - -/// -/// Returns `true` if `value` is clamped within the inclusive bounds of `lower` and `upper`. -/// -pub fn is_clamped(value: anytype, lower: anytype, upper: anytype) bool { - return (value >= lower) and (value <= upper); -} - -/// -/// Returns the maximum value between `a` and `b`. -/// pub fn max(a: anytype, b: anytype) @TypeOf(a, b) { return @max(a, b); } -/// -/// Returns the maximum value that the integer described by `int` may express. -/// pub fn max_int(comptime int: std.builtin.Type.Int) comptime_int { const bit_count = int.bits; if (bit_count == 0) return 0; - return (1 << (bit_count - @boolToInt(int.signedness == .signed))) - 1; + return (1 << (bit_count - @intFromBool(int.signedness == .signed))) - 1; } -/// -/// Returns the minimum value between `a` and `b`. -/// pub fn min(a: anytype, b: anytype) @TypeOf(a, b) { return @min(a, b); } -/// -/// Returns the minimum value that the integer described by `int` may express. -/// pub fn min_int(comptime int: std.builtin.Type.Int) comptime_int { if (int.signedness == .unsigned) return 0; @@ -152,9 +26,6 @@ pub fn min_int(comptime int: std.builtin.Type.Int) comptime_int { return -(1 << (bit_count - 1)); } -/// -/// Returns `value` wrapped around the inclusive bounds of `lower` and `upper`. -/// pub fn wrap(value: anytype, lower: anytype, upper: anytype) @TypeOf(value, lower, upper) { const range = upper - lower; diff --git a/source/coral/slab.zig b/source/coral/slab.zig deleted file mode 100644 index 7b09ca8..0000000 --- a/source/coral/slab.zig +++ /dev/null @@ -1,178 +0,0 @@ -const debug = @import("./debug.zig"); - -const io = @import("./io.zig"); - -const math = @import("./math.zig"); - -const std = @import("std"); - -/// -/// Addressable mapping of integers described by `index_int` to values of type `Value`. -/// -/// Slab maps are similar to slot maps in that they have O(1) insertion and removal, however, use a flat table layout -/// instead of parallel arrays. This reduces memory usage in some cases and can be useful for data that does not need to -/// be quickly iterated over, as values ordering is not guaranteed. -/// -/// *Note* `index_int` values may be as big or as small as desired per the use-case of the consumer, however, integers -/// smaller than `usize` may result in the map reporting it is out of memory due to exhausting the addressable space -/// provided by the integer. -/// -pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type { - return struct { - allocator: io.Allocator, - free_index: Index = 0, - count: Index = 0, - table: []Entry = &.{}, - - /// - /// Table entry which may either store an inserted value or an index to the next free entry in the table. - /// - const Entry = union (enum) { - free_index: Index, - value: Value, - }; - - /// - /// Used for indexing into the slab map. - /// - const Index = math.Int(index_int); - - /// - /// Slab map type. - /// - const Self = @This(); - - /// - /// Overwrites the value referenced by `index` in `self`. - /// - pub fn assign(self: *Self, index: Index, value: Value) void { - const entry = &self.table[index]; - - debug.assert(entry.* == .value); - - entry.value = value; - } - - /// - /// Deinitializes `self` and sets it to an invalid state, freeing all memory allocated by `allocator`. - /// - /// *Note* if the `table` field of `self` is an allocated slice, `allocator` must reference the same allocation - /// strategy as the one originally used to allocate the current table. - /// - pub fn deinit(self: *Self, allocator: io.Allocator) void { - if (self.table.len == 0) { - return; - } - - io.deallocate(allocator, self.table); - - self.table = &.{}; - self.count = 0; - self.free_index = 0; - } - - /// - /// Fetches the value referenced by `index` in `self`, returning it. - /// - pub fn fetch(self: *Self, index: Index) *Value { - const entry = &self.table[index]; - - debug.assert(entry.* == .value); - - return &entry.value; - } - - /// - /// Attempts to grow the internal buffer of `self` by `growth_amount` using `allocator`. - /// - /// The function returns [io.AllocatorError] if `allocator` could not commit the memory required to grow the - /// table by `growth_amount`, leaving `self` in the same state that it was in prior to starting the grow. - /// - /// Growing ahead of multiple insertion operations is useful when the upper bound of insertions is well- - /// understood, as it can reduce the number of allocations required per insertion. - /// - /// *Note* if the `table` field of `self` is an allocated slice, `allocator` must reference the same allocation - /// strategy as the one originally used to allocate the current table. - /// - pub fn grow(self: *Self, allocator: io.Allocator, growth_amount: usize) io.AllocationError!void { - const grown_capacity = self.table.len + growth_amount; - const entries = try io.allocate_many(allocator, grown_capacity, Entry); - - errdefer io.deallocate(allocator, entries); - - if (self.table.len != 0) { - for (0 .. self.table.len) |index| { - entries[index] = self.table[index]; - } - - for (self.table.len .. entries.len) |index| { - entries[index] = .{.free_index = 0}; - } - - io.deallocate(allocator, self.table); - } - - self.table = entries; - } - - /// - /// Attempts to insert `value` into `self` as a new entry using `allocator` as the allocation strategy, - /// returning an index value representing a reference to the inserted value that may be queried through `self` - /// after. - /// - /// The function returns [io.AllocationError] if `allocator` could not commit the memory required to grow the - /// internal buffer of `self` when necessary. - /// - /// *Note* if the `table` field of `self` is an allocated slice, `allocator` must reference the same allocation - /// strategy as the one originally used to allocate the current table. - /// - pub fn insert(self: *Self, value: Value) io.AllocationError!Index { - if (self.count == self.table.len) { - try self.grow(self.allocator, math.max(1, self.count)); - } - - if (self.free_index == self.count) { - const entry_index = self.count; - const entry = &self.table[entry_index]; - - entry.* = .{.value = value}; - - self.count += 1; - self.free_index += 1; - - return entry_index; - } - - const entry_index = self.free_index; - const entry = &self.table[self.free_index]; - - debug.assert(entry.* == .free_index); - - self.count += 1; - self.free_index = entry.free_index; - entry.* = .{.value = value}; - - return entry_index; - } - - /// - /// Returns `true` if `self` contains no values, otherwise `false`. - /// - pub fn is_empty(self: Self) bool { - return self.count == 0; - } - - /// - /// Removes the value referenced by `index` from `self`. - /// - pub fn remove(self: *Self, index: Index) void { - const entry = &self.table[index]; - - debug.assert(entry.* == .value); - - self.count -= 1; - entry.* = .{.free_index = self.free_index}; - self.free_index = index; - } - }; -} diff --git a/source/coral/slots.zig b/source/coral/slots.zig deleted file mode 100755 index 3518ba4..0000000 --- a/source/coral/slots.zig +++ /dev/null @@ -1,236 +0,0 @@ -const debug = @import("./debug.zig"); - -const io = @import("./io.zig"); - -const math = @import("./math.zig"); - -/// -/// Retruns a dense mapping slots that may store `Element`s indexable by a [Slot], where `key` defines how many bits the -/// [Slot] used is made from. -/// -pub fn Map(comptime key: Key, comptime Element: type) type { - const KeySlot = Slot(key); - const Index = math.Unsigned(key.index_bits); - - return struct { - capacity: usize, - values: []Element, - slots: [*]KeySlot, - erase: [*]Index, - next_free: Index, - - const Self = @This(); - - /// - /// Clears all elements from the slots in `self`. - /// - /// *Note* that clearing the slots is not the same as deinitializing them, as it does not deallocate any memory - /// that has already been allocated to the slots structure. - /// - pub fn clear(self: *Self) void { - self.next_free = 0; - self.values = self.values[0 .. 0]; - - { - var index = @as(usize, 0); - - while (index < self.capacity) : (index += 1) { - const slot = &self.slots[index]; - - slot.salt = math.max(slot.salt +% 1, 1); - slot.index = index; - } - } - } - - /// - /// Frees all memory allocated by `allocator` to self. - /// - /// *Note*: if `self` already contains allocated memory then `allocator` must reference the same [io.Allocator] - /// that was used to create the already-allocated memory. - /// - pub fn deinit(self: *Self, allocator: io.Allocator) void { - io.deallocate(allocator, self.values.ptr); - io.deallocate(allocator, self.slots); - io.deallocate(allocator, self.erase); - - self.values = &.{}; - self.slots = null; - self.erase = null; - } - - /// - /// Attempts to fetch the element identified referenced by `slot` from `self`, returning it or `null` if `slot` - /// does not reference a valid element. - /// - pub fn fetch(self: Self, slot: KeySlot) ?*Element { - if (slot.index >= self.values.len) { - return null; - } - - const redirect = &self.slots[slot.index]; - - if (slot.salt != redirect.salt) { - return null; - } - - return &self.values[redirect.index]; - } - - /// - /// Attempts to transactionally grow `self` by `growth_amount` using `allocator`, returning a - /// [io.AllocationError] if it failed. - /// - /// Should growing fail, `self` is left in an unmodified state. - /// - /// *Note*: if `self` already contains allocated memory then `allocator` must reference the same [io.Allocator] - /// that was used to create the already-allocated memory. - /// - pub fn grow(self: *Self, allocator: io.Allocator, growth_amount: usize) io.AllocationError!void { - const grown_capacity = self.capacity + growth_amount; - const values = try io.allocate_many(Element, grown_capacity, allocator); - - errdefer io.deallocate(allocator, values); - - const slots = try io.allocate_many(KeySlot, grown_capacity, allocator); - - errdefer io.deallocate(allocator, slots); - - const erase = try io.allocate_many(Index, grown_capacity, allocator); - - errdefer io.deallocate(allocator, slots); - - self.values = values; - self.slots = slots.ptr; - self.erase = erase.ptr; - self.capacity = grown_capacity; - - // Add new values to the freelist - { - var index = @intCast(Index, self.values.len); - - while (index < self.capacity) : (index += 1) { - const slot = &self.slots.?[index]; - - slot.salt = 1; - slot.index = index; - } - } - } - - /// - /// Attempts to return an initialized slot map with an initial capacity of `initial_capacity` and `allocator` as - /// the memory allocation strategy. - /// - /// Upon failure, a [io.AllocationError] is returned instead. - /// - pub fn init(allocator: io.Allocator, initial_capacity: usize) io.AllocationError!Self { - const values = try io.allocate_many(Element, initial_capacity, allocator); - - errdefer io.deallocate(allocator, values); - - const slots = try io.allocate_many(KeySlot, initial_capacity, allocator); - - errdefer io.deallocate(allocator, slots); - - const erase = try io.allocate_many(Index, initial_capacity, allocator); - - errdefer io.deallocate(allocator, erase); - - return Self{ - .capacity = initial_capacity, - .values = values[0 .. 0], - .slots = slots.ptr, - .erase = erase.ptr, - .next_free = 0, - }; - } - - /// - /// Attempts to insert `value` into `self`, growing the internal buffer with `allocator` if it is full and - /// returning a `Slot` of `key` referencing the inserted element or a [io.AllocationError] if it failed. - /// - /// *Note*: if `self` already contains allocated memory then `allocator` must reference the same [io.Allocator] - /// that was used to create the already-allocated memory. - /// - pub fn insert(self: *Self, allocator: io.Allocator, value: Element) io.AllocationError!KeySlot { - if (self.values.len == self.capacity) { - try self.grow(allocator, math.max(usize, 1, self.capacity)); - } - - const index_of_redirect = self.next_free; - const redirect = &self.slots.?[index_of_redirect]; - - // redirect.index points to the next free slot. - self.next_free = redirect.index; - redirect.index = @intCast(Index, self.values.len); - self.values = self.values.ptr[0 .. self.values.len + 1]; - self.values[redirect.index] = value; - self.erase.?[redirect.index] = index_of_redirect; - - return KeySlot{ - .index = index_of_redirect, - .salt = redirect.salt, - }; - } - - /// - /// Attempts to remove the element referenced by `slot` from `self`, returning `true` if it was successful or - /// `false` if `slot` does not reference a valid slot. - /// - pub fn remove(self: *Self, slot: KeySlot) bool { - const redirect = &self.slots.?[slot.index]; - - if (slot.salt != redirect.salt) { - return false; - } - - const free_index = redirect.index; - - self.values = self.values[0 .. (self.values.len - 1)]; - - if (self.values.len > 0) { - const free_value = &self.values[free_index]; - const free_erase = &self.erase.?[free_index]; - const last_value = &self.values[self.values.len]; - const last_erase = &self.erase.?[self.values.len]; - - free_value.* = last_value.*; - free_erase.* = last_erase.*; - self.slots.?[free_erase.*].index = free_index; - } - - redirect.salt = math.max(Index, redirect.salt +% 1, 1); - redirect.index = self.next_free; - self.next_free = slot.index; - - return true; - } - }; -} - -/// -/// Describes the memory layout of an element-slot mapping. -/// -pub const Key = struct { - index_bits: usize, - salt_bits: usize, -}; - -/// -/// References a slot in a slot mapping. -/// -pub fn Slot(comptime key: Key) type { - return extern struct { - index: math.Unsigned(key.index_bits), - salt: math.Unsigned(key.salt_bits), - }; -} - -/// -/// [Key] that uses the same number of bits as a [usize]. -/// -pub const addressable_key = Key{ - .index_bits = (@bitSizeOf(usize) / 2), - .salt_bits = (@bitSizeOf(usize) / 2), -}; diff --git a/source/coral/table.zig b/source/coral/table.zig deleted file mode 100755 index 2239460..0000000 --- a/source/coral/table.zig +++ /dev/null @@ -1,299 +0,0 @@ -const debug = @import("./debug.zig"); - -const io = @import("./io.zig"); - -const math = @import("./math.zig"); - -/// -/// Hash type used by tables and their associated structures. -/// -pub const Hash = u64; - -/// -/// Returns a table type of `Key`-`Value` pairs implementing a hash-only approach to key-value storage. -/// -/// Entries are hashed using the `keyer` and collisions are resolved by looking for another empty space nearby. This -/// repeats until the load factor exceeds the implementation-defined load maximum, at which point the table will rehash -/// itself to acquire more capacity. -/// -pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Key)) type { - const hash_info = @typeInfo(Hash).Int; - const load_max = 0.75; - const growth_factor = 0.6; - - return struct { - allocator: io.Allocator, - count: usize = 0, - table: []?Entry = &.{}, - - /// - /// Key-value pair bundling. - /// - pub const Entry = struct { - key: Key, - value: Value, - - /// - /// Attempts to write `self` into `entry_table`, returning `true` if no identical entry already existed, - /// otherwise `false`. - /// - /// Note that this does not modify the memory pointed to by `entry_table` in any way, meaning that - /// completely filled entry tables cannot perform the write at all and will invoke safety-checked behavior. - /// - fn write_into(self: Entry, entry_table: []?Entry) bool { - const hash_max = math.min(math.max_int(hash_info), entry_table.len); - var hashed_key = math.wrap(keyer.hasher(self.key), math.min_int(hash_info), hash_max); - var iterations = @as(usize, 0); - - while (true) : (iterations += 1) { - debug.assert(iterations < entry_table.len); - - const table_entry = &(entry_table[hashed_key] orelse { - entry_table[hashed_key] = .{ - .key = self.key, - .value = self.value, - }; - - return true; - }); - - if (keyer.comparer(table_entry.key, self.key) == 0) { - return false; - } - - hashed_key = (hashed_key +% 1) % hash_max; - } - } - }; - - /// - /// Iterable wrapper for [Hashed] instances to make unordered traversal of key-value entries relatively trivial. - /// - pub const Iterable = struct { - hashed_map: *Self, - iterations: usize = 0, - - /// - /// Attempts to move past the current iteration of `self` and onto the next key-value entry, returning it or - /// `null` if there are no more elements in the referenced map. - /// - pub fn next(self: *Iterable) ?Entry { - while (self.iterations < self.hashed_map.table.len) { - defer self.iterations += 1; - - if (self.hashed_map.table[self.iterations]) |entry| { - return entry; - } - } - - return null; - } - }; - - /// - /// Table type. - /// - const Self = @This(); - - /// - /// Attempts to write the `key`-`value` pair into `self`, using `allocator` as the memory allocation strategy, - /// and overwriting any value stored with a matching `key` and returning it if one existed. - /// - /// The function returns [AllocationError] instead if `allocator` cannot commit the memory required to grow the - /// entry table of `self` when necessary. - /// - pub fn assign(self: *Self, key: Key, value: Value) io.AllocationError!?Entry { - if (self.calculate_load_factor() >= load_max) { - const growth_size = @intToFloat(f64, math.max(1, self.table.len)) * growth_factor; - - if (growth_size > math.max_int(@typeInfo(usize).Int)) { - return error.OutOfMemory; - } - - try self.rehash(@floatToInt(usize, growth_size)); - } - - debug.assert(self.table.len > self.count); - - { - const hash_max = math.min(math.max_int(hash_info), self.table.len); - var hashed_key = math.wrap(keyer.hasher(key), math.min_int(hash_info), hash_max); - - while (true) { - const entry = &(self.table[hashed_key] orelse { - self.table[hashed_key] = .{ - .key = key, - .value = value, - }; - - return null; - }); - - if (keyer.comparer(entry.key, key) == 0) { - const original_entry = entry.*; - - entry.* = .{ - .key = key, - .value = value, - }; - - return original_entry; - } - - hashed_key = (hashed_key +% 1) % hash_max; - } - } - - return false; - } - - /// - /// Returns the calculated load factor of `self` at the moment. - /// - pub fn calculate_load_factor(self: Self) f32 { - return if (self.table.len == 0) 1 else @intToFloat(f32, self.count) / @intToFloat(f32, self.table.len); - } - - /// - /// Clears all entries from `self`, resetting the count to `0`. - /// - /// To clean up memory allocations made by the stack and deinitialize it, see [deinit] instead. - /// - pub fn clear(self: *Self) void { - for (self.table) |*entry| { - entry.* = null; - } - - self.count = 0; - } - - /// - /// Deinitializes `self` and sets it to an invalid state, freeing all memory allocated by `allocator`. - /// - /// To clear all items from the table while preserving the current capacity, see [clear] instead. - /// - pub fn deinit(self: *Self) void { - if (self.table.len == 0) { - return; - } - - io.deallocate(self.allocator, self.table); - - self.table = &.{}; - self.count = 0; - } - - /// - /// Attempts to write the `key`-`value` pair into `self`, using `allocator` as the memory allocation strategy, - /// if no value already exists with a matching `key`, returning `true` if it was inserted, otherwise `false`. - /// - /// The function returns [AllocationError] instead if `allocator` cannot commit the memory required to grow the - /// entry table of `self` when necessary. - /// - pub fn insert(self: *Self, key: Key, value: Value) io.AllocationError!bool { - if (self.calculate_load_factor() >= load_max) { - const growth_amount = @intToFloat(f64, self.table.len) * growth_factor; - const min_size = 1; - - try self.rehash(self.table.len + math.max(min_size, @floatToInt(usize, growth_amount))); - } - - debug.assert(self.table.len > self.count); - - defer self.count += 1; - - return (Entry{ - .key = key, - .value = value, - }).write_into(self.table); - } - - /// - /// Attempts to find an entry in `self` matching `key`, returning it or `null` if no matching entry was found. - /// - pub fn lookup(self: Self, key: Key) ?Value { - if (self.count == 0) { - return null; - } - - const hash_max = math.min(math.max_int(hash_info), self.table.len); - var hashed_key = math.wrap(keyer.hasher(key), math.min_int(hash_info), hash_max); - var iterations = @as(usize, 0); - - while (iterations < self.count) : (iterations += 1) { - const entry = &(self.table[hashed_key] orelse return null); - - if (keyer.comparer(entry.key, key) == 0) { - return entry.value; - } - - hashed_key = (hashed_key +% 1) % hash_max; - } - - return null; - } - - /// - /// Attempts to reallocate and regenerate the table capacity in `self` using `allocator` to be equal to or - /// greater than `requested_range`, returning [io.AllocationError] if `allocator` cannot commit the memory - /// required for the table capacity size. - /// - pub fn rehash(self: *Self, requested_range: usize) io.AllocationError!void { - const old_table = self.table; - - self.table = try io.allocate_many(self.allocator, math.max(requested_range, self.count), ?Entry); - - errdefer { - io.deallocate(self.allocator, self.table); - - self.table = old_table; - } - - for (self.table) |*entry| { - entry.* = null; - } - - if (old_table.len != 0) - { - for (old_table) |maybe_entry| { - if (maybe_entry) |entry| { - debug.assert(entry.write_into(self.table)); - } - } - - io.deallocate(self.allocator, old_table); - } - } - }; -} - -/// -/// Returns a function group for defining table keying operations performable on `Key`. -/// -pub fn Keyer(comptime Key: type) type { - return struct { - hasher: fn (key: Key) Hash, - comparer: fn (key_a: Key, key_b: Key) isize, - }; -} - -/// -/// A standard [Keyer] for `[]const u8` types that provides general-purpose string keying. -/// -pub const string_keyer = Keyer([]const u8){ - .hasher = hash_string, - .comparer = io.compare, -}; - -/// -/// Returns a general-purpose, non-cryptographically safe hash value for `string`. -/// -pub fn hash_string(string: []const u8) Hash { - var hash_code = @as(Hash, 5381); - - for (string) |byte| { - hash_code = ((hash_code << 5) + hash_code) + byte; - } - - return hash_code; -} diff --git a/source/coral/utf8.zig b/source/coral/utf8.zig old mode 100755 new mode 100644 index 4b448ba..78697d8 --- a/source/coral/utf8.zig +++ b/source/coral/utf8.zig @@ -1,298 +1,108 @@ -const debug = @import("./debug.zig"); - const io = @import("./io.zig"); const math = @import("./math.zig"); -const std = @import("std"); - -/// -/// -/// pub const DecimalFormat = struct { - delimiter: []const u8 = "", - positive_prefix: enum {none, plus, space} = .none, -}; + delimiter: []const io.Byte, + positive_prefix: enum {none, plus, space}, -/// -/// -/// -pub const HexadecimalFormat = struct { - delimiter: []const u8 = "", - positive_prefix: enum {none, plus, space} = .none, - casing: enum {lower, upper} = .lower, -}; + pub fn parse(self: DecimalFormat, utf8: []const io.Byte, comptime Decimal: type) ?Decimal { + if (utf8.len == 0) { + return null; + } -/// -/// Errors that may occur during any kind of utf8-encoded parsing. -/// -pub const ParseError = error { - BadSyntax, -}; + switch (@typeInfo(Decimal)) { + .Int => |int| { + var has_sign = switch (utf8[0]) { + '-', '+', ' ' => true, + else => false, + }; -/// -/// Errors that may occur during any kind of utf8-encoded printing. -/// -pub const PrintError = error { - PrintFailed, - PrintIncomplete, -}; + var result = @as(Decimal, 0); -/// -/// -/// -pub fn parse_decimal(comptime Decimal: type, utf8: []const u8, format: DecimalFormat) !Decimal { - if (utf8.len == 0) { - return error.BadSyntax; - } + for (@intFromBool(has_sign) .. utf8.len) |index| { + const radix = 10; + const code = utf8[index]; - switch (@typeInfo(Decimal)) { - .Int => |int| { - var has_sign = switch (utf8[0]) { - '-', '+', ' ' => true, - else => false, - }; - - var result = @as(Decimal, 0); - - for (@boolToInt(has_sign) .. utf8.len) |index| { - const radix = 10; - const code = utf8[index]; - - 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')); - }, - - else => { - if (format.delimiter.len == 0 or !io.equals(format.delimiter, utf8[index ..])) { - return error.BadSyntax; - } - }, - } - } - - switch (int.signedness) { - .signed => { - return result * @as(Decimal, if (has_sign and utf8[0] == '-') -1 else 1); - }, - - .unsigned => { - if (has_sign and utf8[0] == '-') { - return error.OutOfMemory; - } - - return result; - }, - } - }, - - .Float => { - // "" - if (utf8.len == 0) { - return error.BadSyntax; - } - - var has_sign = switch (utf8[0]) { - '-', '+', ' ' => true, - else => false, - }; - - // "-" - if (has_sign and utf8.len == 1) { - return error.BadSyntax; - } - - const sign_offset = @boolToInt(has_sign); - var has_decimal = utf8[sign_offset] == '.'; - - // "-." - if (has_decimal and (utf8.len == 2)) { - return error.BadSyntax; - } - - var result = @as(Decimal, 0); - var factor = @as(Decimal, if (has_sign and utf8[0] == '-') -1 else 1); - - for (utf8[0 .. (sign_offset + @boolToInt(has_decimal))]) |code| switch (code) { - '.' => { - if (has_decimal) return error.BadSyntax; - - has_decimal = true; - }, - - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { - if (has_decimal) factor /= 10.0; - - result = ((result * 10.0) + @intToFloat(Decimal, code - '0')); - }, - - else => return error.BadSyntax, - }; - - return result * factor; - }, - - else => @compileError("`" ++ @typeName(Decimal) ++ "` cannot be formatted as a decimal string"), - } -} - -/// -/// Attempts to print `utf8` to `writer`. -/// -/// The function returns [PrintError] if the write failed to complete partially or entirely. -/// -pub fn print(writer: io.Writer, utf8: []const u8) PrintError!void { - if ((writer.invoke(utf8) orelse return error.PrintFailed) != utf8.len) { - return error.PrintIncomplete; - } -} - -/// -/// -/// -pub fn print_formatted(writer: io.Writer, comptime format: []const u8, arguments: anytype) PrintError!void { - switch (@typeInfo(@TypeOf(arguments))) { - .Struct => |arguments_struct| { - comptime var arg_index = 0; - comptime var head = 0; - comptime var tail = 0; - - inline while (tail < format.len) : (tail += 1) { - if (format[tail] == '{') { - if (tail > format.len) { - @compileError("expected an idenifier after opening `{`"); - } - - tail += 1; - - switch (format[tail]) { - '{' => { - try print(writer, format[head .. (tail - 1)]); - - tail += 1; - head = tail; - }, - - '}' => { - if (!arguments_struct.is_tuple) { - @compileError("all format specifiers must be named when using a named struct"); - } - - try print(writer, arguments[arg_index]); - - arg_index += 1; - tail += 1; - head = tail; + 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')); }, else => { - if (arguments_struct.is_tuple) { - @compileError("format specifiers cannot be named when using a tuple struct"); + if (self.delimiter.len == 0 or !io.equals(self.delimiter, utf8[index ..])) { + return null; } - - try print(writer, format[head .. (tail - 1)]); - - head = tail; - tail += 1; - - if (tail >= format.len) { - @compileError("expected closing `}` or another `{` after opening `{`"); - } - - debug.assert(tail < format.len); - - inline while (format[tail] != '}') { - tail += 1; - - debug.assert(tail < format.len); - } - - try print_value(writer, @field(arguments, format[head .. tail])); - - tail += 1; - head = tail; - } + }, } } - } - }, - else => @compileError("`arguments` must be a struct type"), - } -} + switch (int.signedness) { + .signed => { + return result * @as(Decimal, if (has_sign and utf8[0] == '-') -1 else 1); + }, -/// -/// -/// -pub fn print_decimal(writer: io.Writer, value: anytype, format: DecimalFormat) PrintError!void { - if (value == 0) { - return print(writer, switch (format.positive_prefix) { - .none => "0", - .plus => "+0", - .space => " 0", - }); - } + .unsigned => { + if (has_sign and utf8[0] == '-') { + return null; + } - switch (@typeInfo(@TypeOf(value))) { - .Int => |int| { - const radix = 10; - var buffer = [_]u8{0} ** (1 + math.max(int.bits, 1)); - var buffer_start = buffer.len - 1; - - { - var decomposable_value = value; - - while (decomposable_value != 0) : (buffer_start -= 1) { - buffer[buffer_start] = @intCast(u8, (decomposable_value % radix) + '0'); - decomposable_value = (decomposable_value / radix); + return result; + }, } - } + }, - if (int.signedness == .unsigned and value < 0) { - buffer[buffer_start] = '-'; - } else { - switch (format.positive_prefix) { - .none => buffer_start += 1, - .plus => buffer[buffer_start] = '+', - .space => buffer[buffer_start] = ' ', + .Float => { + var has_sign = switch (utf8[0]) { + '-', '+', ' ' => true, + else => false, + }; + + // "-" + if (has_sign and utf8.len == 1) { + return null; } - } - try print(writer, buffer[buffer_start ..]); - }, + const sign_offset = @intFromBool(has_sign); + var has_decimal = utf8[sign_offset] == '.'; - else => @compileError("`arguments` must be a struct type"), + // "-." + if (has_decimal and (utf8.len == 2)) { + return null; + } + + var result = @as(Decimal, 0); + var factor = @as(Decimal, if (has_sign and utf8[0] == '-') -1 else 1); + + for (utf8[sign_offset + @intFromBool(has_decimal) .. utf8.len]) |code| { + switch (code) { + '.' => { + if (has_decimal) { + return null; + } + + has_decimal = true; + }, + + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => { + if (has_decimal) { + factor /= 10.0; + } + + result = ((result * 10.0) + @as(Decimal, @floatFromInt(code - '0'))); + }, + + else => return null, + } + } + + return result * factor; + }, + + else => @compileError("`" ++ @typeName(Decimal) ++ "` cannot be formatted as a decimal string"), + } } -} +}; -pub fn print_hexadecimal(writer: io.Writer, value: anytype, format: HexadecimalFormat) PrintError!void { - // TODO: Implement. - _ = writer; - _ = value; - _ = format; - - unreachable; -} - -noinline fn print_value(writer: io.Writer, value: anytype) PrintError!void { - const Value = @TypeOf(value); - - return switch (@typeInfo(Value)) { - .Int => print_decimal(writer, value, .{}), - .Float => print_decimal(writer, value, .{}), - - .Pointer => |pointer| switch (pointer.size) { - .One, .Many, .C => print_hexadecimal(writer, @ptrToInt(value), .{}), - .Slice => if (pointer.child == u8) print(writer, value) else @compileError(unformattableMessage(Value)), - }, - - else => @compileError(unformattableMessage(Value)), - }; -} - -fn unformattableMessage(comptime Value: type) []const u8 { - return "`" ++ @typeName(Value) ++ "` are not formattable"; -} diff --git a/source/ona/app.zig b/source/ona/app.zig new file mode 100644 index 0000000..a48727b --- /dev/null +++ b/source/ona/app.zig @@ -0,0 +1,128 @@ +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, file_access: file.Access) kym.RuntimeError!void { + const manifest = try env.execute_file(file_access, file.Path.from(&.{"app.ona"})); + + defer env.discard(manifest); + + const title = try env.get_field(manifest, "title"); + + defer env.discard(title); + + const title_string = try env.to_string(title); + + const width = @as(u16, get: { + const ref = try env.get_field(manifest, "width"); + + defer env.discard(ref); + + break: get @intFromFloat(env.to_float(ref) catch @as(f64, @floatFromInt(self.width))); + }); + + const height = @as(u16, get: { + const ref = try env.get_field(manifest, "height"); + + defer env.discard(ref); + + break: get @intFromFloat(env.to_float(ref) catch @as(f64, @floatFromInt(self.height))); + }); + + const tick_rate = @as(f32, get: { + const ref = try env.get_field(manifest, "tick_rate"); + + defer env.discard(ref); + + break: get @floatCast(env.to_float(ref) catch self.tick_rate); + }); + + { + const limited_title_len = coral.math.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]); + } + + self.tick_rate = tick_rate; + self.width = width; + self.height = height; + } +}; + +pub const LogSeverity = enum { + info, + warn, + fail, +}; + +pub const WritableLog = struct { + severity: LogSeverity, + write_buffer: coral.list.ByteStack, + + pub fn as_writer(self: *WritableLog) coral.io.Writer { + return coral.io.Writer.bind(WritableLog, self, struct { + fn write(writable_log: *WritableLog, bytes: []const coral.io.Byte) ?usize { + writable_log.write(bytes) catch return null; + + return bytes.len; + } + }.write); + } + + pub fn free(self: *WritableLog) void { + self.write_buffer.free(); + } + + pub fn make(log_severity: LogSeverity, allocator: coral.io.Allocator) WritableLog { + return .{ + .severity = log_severity, + .write_buffer = coral.list.ByteStack.make(allocator), + }; + } + + pub fn write(self: *WritableLog, bytes: []const coral.io.Byte) coral.io.AllocationError!void { + const format_string = "%.*s"; + var line_written = @as(usize, 0); + + for (bytes) |byte| { + if (byte == '\n') { + ext.SDL_LogError( + ext.SDL_LOG_CATEGORY_APPLICATION, + format_string, + self.write_buffer.values.len, + self.write_buffer.values.ptr); + + self.write_buffer.clear(); + + line_written = 0; + + continue; + } + + try self.write_buffer.push_one(byte); + + line_written += 1; + } + + if (self.write_buffer.values.len == 0) { + ext.SDL_LogError( + ext.SDL_LOG_CATEGORY_APPLICATION, + format_string, + self.write_buffer.values.len, + self.write_buffer.values.ptr); + + self.write_buffer.clear(); + } + } +}; diff --git a/source/ona/canvas.zig b/source/ona/canvas.zig deleted file mode 100755 index 8c707e5..0000000 --- a/source/ona/canvas.zig +++ /dev/null @@ -1,23 +0,0 @@ -const coral = @import("coral"); - -pub const Item = struct { - transform: Transform, - - options: union (enum) { - sprite: struct { - - }, - }, -}; - -pub const Transform = extern struct { - x: coral.math.Vector2, - y: coral.math.Vector2, - origin: coral.math.Vector2, - - pub const identity = Transform{ - .x = .{1, 0}, - .y = .{0, 1}, - .origin = .{0, 0}, - }; -}; diff --git a/source/ona/ext.zig b/source/ona/ext.zig old mode 100755 new mode 100644 index 1be2ca3..757bd38 --- a/source/ona/ext.zig +++ b/source/ona/ext.zig @@ -1,4 +1,3 @@ - pub usingnamespace @cImport({ - @cInclude("SDL2/SDL.h"); + @cInclude("SDL2/SDL.h"); }); diff --git a/source/ona/file.zig b/source/ona/file.zig index cafa1e9..1296aec 100644 --- a/source/ona/file.zig +++ b/source/ona/file.zig @@ -2,53 +2,53 @@ const coral = @import("coral"); const ext = @import("./ext.zig"); -pub const Contents = struct { - allocator: coral.io.Allocator, - data: []u8, +pub const Access = union (enum) { + sandboxed_path: *const Path, - pub const InitError = coral.io.AllocationError || Readable.ReadError; + pub fn open_readable(self: Access, readable_path: Path) ?*Readable { + switch (self) { + .sandboxed_path => |sandboxed_path| { + const readable_path_string = sandboxed_path.joined(readable_path).to_string() orelse return null; - pub fn deinit(self: *Contents) void { - coral.io.deallocate(self.allocator, self.data); + return @ptrCast(ext.SDL_RWFromFile(readable_path_string.ptr, "rb")); + }, + } } - pub fn init(allocator: coral.io.Allocator, readable_file: *Readable) InitError!Contents { - const file_offset = try readable_file.skip(0); - const file_size = try readable_file.seek_end(); + pub fn query(self: Access, path: Path) ?Info { + switch (self) { + .sandboxed_path => |sandboxed_path| { + const path_string = sandboxed_path.joined(path).to_string() 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); - _ = try readable_file.seek(file_offset); + if (ext.SDL_RWclose(rw_ops) != 0 or file_size < 0) { + return null; + } - const allocation = try coral.io.allocate_many(u8, file_size, allocator); - - errdefer coral.io.deallocate(allocator, allocation); - - if (try readable_file.read(allocation) != allocation.len) { - // Read less than was allocated for. - return error.FileUnavailable; + return Info{ + .size = @intCast(file_size), + }; + }, } - - return Contents{ - .allocator = allocator, - .data = allocation, - }; } }; +pub const Info = struct { + size: u64, +}; + pub const Path = extern struct { - data: [4096]u8 = [_]u8{0} ** 4096, + data: [4096]coral.io.Byte = [_]coral.io.Byte{0} ** 4096, pub const cwd = Path.from(&.{"./"}); - pub const ValidationError = error { - PathTooLong, - }; - pub fn from(components: []const []const u8) Path { // TODO: Implement proper parsing / removal of duplicate path delimiters. var path = Path{}; { - var writable_slice = coral.io.FixedBuffer{.slice = &path.data}; + var writable_slice = coral.io.FixedBuffer{.bytes = &path.data}; for (components) |component| { if (writable_slice.write(component) != component.len) { @@ -64,7 +64,7 @@ pub const Path = extern struct { var path = Path{}; { - var writable = coral.io.FixedBuffer{.slice = &path.data}; + var writable = coral.io.FixedBuffer{.bytes = &path.data}; var written = @as(usize, 0); for (&self.data) |byte| { @@ -91,32 +91,20 @@ pub const Path = extern struct { return path; } - pub fn to_string(self: Path) ValidationError![:0]const u8 { - const sentineled_data = get_sentineled_data: { - const last_index = self.data.len - 1; + pub fn to_string(self: Path) ?[:0]const coral.io.Byte { + const last_index = self.data.len - 1; - if (self.data[last_index] != 0) { - return error.PathTooLong; - } + if (self.data[last_index] != 0) { + return null; + } - break: get_sentineled_data self.data[0 .. last_index:0]; - }; - - return sentineled_data[0 .. coral.io.sentinel_index(u8, 0, sentineled_data):0]; + return coral.io.slice_sentineled(@as(coral.io.Byte, 0), @as([*:0]const coral.io.Byte, @ptrCast(&self.data))); } }; -pub const ReadError = error { - FileUnavailable, -}; - pub const Readable = opaque { pub fn as_reader(self: *Readable) coral.io.Reader { - return coral.io.Reader.bind(Readable, self, struct { - fn read(readable: *Readable, buffer: []u8) ?usize { - return readable.read(buffer) catch null; - } - }.read); + return coral.io.Reader.bind(Readable, self, read_into); } pub fn close(self: *Readable) void { @@ -125,97 +113,81 @@ pub const Readable = opaque { } } - pub fn read(self: *Readable, buffer: []u8) ReadError!usize { + pub fn read_into(self: *Readable, buffer: []coral.io.Byte) ?usize { ext.SDL_ClearError(); - const bytes_read = ext.SDL_RWread(rw_ops_cast(self), buffer.ptr, @sizeOf(u8), buffer.len); + const bytes_read = ext.SDL_RWread(rw_ops_cast(self), buffer.ptr, @sizeOf(coral.io.Byte), buffer.len); const error_message = ext.SDL_GetError(); if (bytes_read == 0 and error_message != null and error_message.* != 0) { - return error.FileUnavailable; + return null; } return bytes_read; } - pub fn seek(self: *Readable, cursor: u64) ReadError!u64 { + pub fn seek_head(self: *Readable, cursor: u64) ?u64 { // TODO: Fix safety of int cast. - const byte_offset = ext.SDL_RWseek(rw_ops_cast(self), @intCast(i64, cursor), ext.RW_SEEK_SET); + const byte_offset = ext.SDL_RWseek(rw_ops_cast(self), @intCast(cursor), ext.RW_SEEK_SET); if (byte_offset < 0) { - return error.FileUnavailable; + return null; } - return @intCast(u64, byte_offset); + return @intCast(byte_offset); } - pub fn seek_end(self: *Readable) ReadError!usize { + pub fn seek_tail(self: *Readable) ?usize { const byte_offset = ext.SDL_RWseek(rw_ops_cast(self), 0, ext.RW_SEEK_END); if (byte_offset < 0) { return error.FileUnavailable; } - return @intCast(u64, byte_offset); + return @intCast(byte_offset); } - pub fn skip(self: *Readable, offset: i64) ReadError!u64 { + pub fn skip(self: *Readable, offset: i64) ?u64 { const byte_offset = ext.SDL_RWseek(rw_ops_cast(self), offset, ext.RW_SEEK_CUR); if (byte_offset < 0) { return error.FileUnavailable; } - return @intCast(u64, byte_offset); + return @intCast(byte_offset); } }; -pub const System = union (enum) { - sandboxed_path: *const Path, +pub fn allocate_and_load(allocator: coral.io.Allocator, access: Access, path: Path) coral.io.AllocationError!?[]coral.io.Byte { + const allocation = try allocator.reallocate(null, query_file_size: { + const info = access.query(path) orelse return null; - pub const FileInfo = struct { - size: u64, + break: query_file_size info.size; + }); + + const readable = access.open_readable(path) orelse { + allocator.deallocate(allocation); + + return null; }; - pub const OpenError = Path.ValidationError || error { - FileNotFound, + defer _ = readable.close(); + + const bytes_read = readable.read_into(allocation) orelse { + allocator.deallocate(allocation); + + return null; }; - pub const QueryError = OpenError || ReadError; + if (bytes_read != allocation.len) { + allocator.deallocate(allocation); - pub fn open_readable(self: System, path: Path) OpenError!*Readable { - switch (self) { - .sandboxed_path => |sandboxed_path| { - return @ptrCast(*Readable, ext.SDL_RWFromFile(try sandboxed_path.joined(path).to_string(), "rb") orelse { - return error.FileNotFound; - }); - }, - } + return null; } - pub fn query_info(self: System, path: Path) QueryError!FileInfo { - switch (self) { - .sandboxed_path => |sandboxed_path| { - const file = ext.SDL_RWFromFile(try sandboxed_path.joined(path).to_string(), "rb") orelse { - return error.FileNotFound; - }; - - defer coral.debug.assert(ext.SDL_RWclose(file) == 0); - - const file_size = ext.SDL_RWseek(file, 0, ext.RW_SEEK_END); - - if (file_size < 0) { - return error.FileUnavailable; - } - - return FileInfo{ - .size = @intCast(u64, file_size), - }; - } - } - } -}; + return allocation; +} fn rw_ops_cast(ptr: *anyopaque) *ext.SDL_RWops { - return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), ptr)); + return @ptrCast(@alignCast(ptr)); } diff --git a/source/ona/heap.zig b/source/ona/heap.zig index b6a28f9..b4ec97a 100644 --- a/source/ona/heap.zig +++ b/source/ona/heap.zig @@ -6,121 +6,84 @@ const ext = @import("./ext.zig"); const std = @import("std"); -/// -/// Recorded allocation info state. -/// -const AllocationInfo = struct { - trace: AllocationTrace, - next_info: ?*AllocationInfo, +const AllocationNode = struct { + trace: std.debug.ConfigurableTrace(2, 4, switch (builtin.mode) { + .Debug, .ReleaseSafe => true, + .ReleaseFast, .ReleaseSmall => false, + }), + + next: ?*AllocationNode, size: usize, + + fn alloc(size: usize, return_address: usize) *AllocationNode { + const node = @as(*AllocationNode, @ptrCast(@alignCast(ext.SDL_malloc(@sizeOf(AllocationNode) + size)))); + + node.* = .{ + .size = size, + .next = null, + .trace = .{}, + }; + + node.trace.addAddr(return_address, ""); + + return node; + } + + fn dealloc(self: *AllocationNode) void { + ext.SDL_free(self); + } + + fn realloc(self: *AllocationNode, size: usize, return_address: usize) *AllocationNode { + const node = @as(*AllocationNode, @ptrCast(@alignCast(ext.SDL_realloc(self, @sizeOf(AllocationNode) + size)))); + + node.* = .{ + .size = size, + .next = null, + .trace = .{}, + }; + + node.trace.addAddr(return_address, ""); + + return node; + } + + fn owns_userdata(self: *AllocationNode, other_userdata: []const coral.io.Byte) bool { + const self_userdata = self.userdata(); + + return self_userdata.ptr == other_userdata.ptr and self_userdata.len == other_userdata.len; + } + + fn userdata(self: *AllocationNode) []coral.io.Byte { + return @as([*]coral.io.Byte, @ptrFromInt(@intFromPtr(self) + @sizeOf(AllocationNode)))[0 .. self.size]; + } }; -/// -/// Recorded stack trace of allocation call site. -/// -/// *Note* this structure is reduced to zero bytes in released builds optimized for speed or size. -/// -const AllocationTrace = std.debug.ConfigurableTrace(2, 4, switch (builtin.mode) { - .Debug, .ReleaseSafe => true, - .ReleaseFast, .ReleaseSmall => false, -}); - -/// -/// Heap allocation context. -/// const Context = struct { - allocation_info_head: ?*AllocationInfo = null, + head: ?*AllocationNode = null, - /// - /// Attempts to allocate a buffer of `size` length from `self`, with `return_address` as the location of the - /// allocation request origin. - /// - /// A reference to the allocated buffer is returned via a slice if the allocation was successful, otherwise `null` - /// is returned. - /// - /// *Note* the returned buffer must be deallocated with [deallocate] before program exit or it will cause a memory - /// leak. - /// - /// *Note* allocation checks are disabled in release builds optimized for speed or size. - /// - fn allocate(self: *Context, size: usize, return_address: usize) ?[]u8 { - switch (builtin.mode) { - .Debug, .ReleaseSafe => { - const allocation_info_size = @sizeOf(AllocationInfo); - const total_allocation_size = allocation_info_size + size; - const allocation = ext.SDL_malloc(total_allocation_size) orelse return null; - const allocation_info = @ptrCast(*AllocationInfo, @alignCast(@alignOf(AllocationInfo), allocation)); - - allocation_info.* = .{ - .size = size, - .next_info = self.allocation_info_head, - .trace = .{}, - }; - - allocation_info.trace.addAddr(return_address, ""); - - self.allocation_info_head = allocation_info; - - return @ptrCast([*]u8, allocation)[allocation_info_size .. total_allocation_size]; - }, - - .ReleaseFast, .ReleaseSmall => { - return @ptrCast([*]u8, ext.SDL_malloc(size) orelse return null)[0 .. size]; - }, - } - } - - /// - /// Returns the assumed pointer to the [AllocationInfo] address of `allocation`. - /// - fn allocation_info_of(allocation: [*]u8) *AllocationInfo { - return @intToPtr(*AllocationInfo, @ptrToInt(allocation) - @sizeOf(AllocationInfo)); - } - - /// - /// Deallocates a the allocation buffer referenced by `allocation`. - /// - /// *Note* the pointer and length of `allocation` must match valid values known to `allocator` otherwise safety- - /// checked behavior will occur. - /// - /// *Note* allocation checks are disabled in release builds optimized for speed or size. - /// fn deallocate(self: *Context, allocation: []u8) void { switch (builtin.mode) { .Debug, .ReleaseSafe => { - const target_allocation_info = allocation_info_of(allocation.ptr); + const panic_message = "incorrect allocation address for deallocating"; + var current_node = self.head orelse @panic(panic_message); - if (target_allocation_info.size != allocation.len) { - @panic("incorrect allocation length for deallocating"); + if (current_node.owns_userdata(allocation)) { + self.head = current_node.next; + + return current_node.dealloc(); } - if (self.allocation_info_head) |allocation_info_head| { - if (target_allocation_info == allocation_info_head) { - self.allocation_info_head = allocation_info_head.next_info; + while (true) { + const next_node = current_node.next orelse @panic(panic_message); - ext.SDL_free(target_allocation_info); + if (next_node.owns_userdata(allocation)) { + current_node.next = next_node.next; - return; + return next_node.dealloc(); } - var previous_allocation_info = allocation_info_head; - var current_allocation_info = allocation_info_head.next_info; - - while (current_allocation_info) |allocation_info| { - if (allocation_info == target_allocation_info) { - previous_allocation_info.next_info = allocation_info.next_info; - - ext.SDL_free(target_allocation_info); - - return; - } - - previous_allocation_info = allocation_info; - current_allocation_info = allocation_info.next_info; - } + current_node = next_node; } - - @panic("incorrect allocation address for deallocating"); }, .ReleaseFast, .ReleaseSmall => { @@ -129,111 +92,67 @@ const Context = struct { } } - /// - /// Attempts to reallocate the buffer referenced by `allocation` to be `size` length from `self`. - /// - /// A reference to the reallocated buffer is returned via a slice if the reallocation was successful, otherwise - /// `null` is returned. - /// - /// *Note* the returned buffer must be deallocated with [deallocate] before program exit or it will cause a memory - /// leak. - /// - /// *Note* the pointer and length of `allocation` must match valid values known to `allocator` otherwise safety- - /// checked behavior will occur. - /// - /// *Note* the allocation referenced by `allocation` should be considered invalid once the function returns, - /// discarding it in favor of the return value. - /// - /// *Note* allocation checks are disabled in release builds optimized for speed or size. - /// - fn reallocate(self: *Context, allocation: []u8, size: usize) ?[]u8 { + fn reallocate(self: *Context, return_address: usize, existing_allocation: ?[]u8, size: usize) coral.io.AllocationError![]u8 { switch (builtin.mode) { .Debug, .ReleaseSafe => { - const target_allocation_info = allocation_info_of(allocation.ptr); + if (existing_allocation) |allocation| { + const panic_message = "incorrect allocation address for reallocating"; + var current_node = self.head orelse @panic(panic_message); - if (target_allocation_info.size != allocation.len) { - @panic("incorrect allocation length for reallocating"); - } + if (current_node.owns_userdata(allocation)) { + const node = current_node.realloc(size, return_address); - const allocation_info_size = @sizeOf(AllocationInfo); + self.head = node; - if (self.allocation_info_head) |allocation_info_head| { - if (target_allocation_info == allocation_info_head) { - self.allocation_info_head = allocation_info_head.next_info; - - const allocation_address = ext.SDL_realloc(target_allocation_info, size) orelse return null; - - target_allocation_info.size = size; - - return @ptrCast([*]u8, allocation_address)[ - allocation_info_size .. (allocation_info_size + size)]; + return node.userdata(); } - var previous_allocation_info = allocation_info_head; - var current_allocation_info = allocation_info_head.next_info; + while (true) { + const next_node = current_node.next orelse @panic(panic_message); - while (current_allocation_info) |allocation_info| { - if (allocation_info == target_allocation_info) { - previous_allocation_info.next_info = allocation_info.next_info; + if (next_node.owns_userdata(allocation)) { + const node = next_node.realloc(size, return_address); - const allocation_address = ext.SDL_realloc(target_allocation_info, size) orelse return null; + current_node.next = node; - target_allocation_info.size = size; - - return @ptrCast([*]u8, allocation_address)[ - allocation_info_size .. (allocation_info_size + size)]; + return node.userdata(); } - previous_allocation_info = allocation_info; - current_allocation_info = allocation_info.next_info; + current_node = next_node; } - } + } else { + const node = AllocationNode.alloc(size, return_address); - @panic("incorrect allocation address for reallocating"); + if (self.head) |head| { + node.next = head; + } + + self.head = node; + + return node.userdata(); + } }, .ReleaseFast, .ReleaseSmall => { - return @ptrCast([*]u8, ext.SDL_realloc(allocation.ptr, size) orelse return null)[0 .. size]; + if (existing_allocation) |allocation | { + return @as([*]u8, ext.SDL_realloc(allocation.ptr, size) orelse { + return error.OutOfMemory; + })[0 .. size]; + } + + return @as([*]u8, ext.SDL_malloc(size) orelse return error.OutOfMemory)[0 .. size]; }, } } }; -/// -/// Heap context. -/// var context = Context{}; -/// -/// Heap allocator. -/// -pub const allocator = coral.io.Allocator.bind(Context, &context, struct { - fn reallocate(self: *Context, options: coral.io.AllocationOptions) ?[]u8 { - if (options.size == 0) { - if (options.allocation) |allocation| { - self.deallocate(allocation); +pub const allocator = coral.io.Allocator.bind(Context, &context, .{ + .reallocate = Context.reallocate, + .deallocate = Context.deallocate, +}); - return null; - } - - return self.allocate(0, options.return_address); - } - - if (options.allocation) |allocation| { - return self.reallocate(allocation, options.size); - } - - return self.allocate(options.size, options.return_address); - } -}.reallocate); - -/// -/// Checks for any allocations belonging to the process heap allocated through the [allocator] interface that are still -/// alive and reports the stack traces of any detected allocations to stderr along with the allocation address and -/// length. -/// -/// *Note* this function becomes a no-op in release builds optimized for speed or size. -/// pub fn trace_leaks() void { switch (builtin.mode) { .Debug, .ReleaseSafe => { @@ -242,7 +161,7 @@ pub fn trace_leaks() void { while (current_allocation_info) |allocation_info| : (current_allocation_info = allocation_info.next_info) { std.debug.print("{d} byte leak at 0x{x} detected:\n", .{ allocation_info.size, - @ptrToInt(allocation_info) + @sizeOf(AllocationInfo), + @as(usize, allocation_info) + @sizeOf(AllocationNode), }); allocation_info.trace.dump(); diff --git a/source/ona/kym.zig b/source/ona/kym.zig old mode 100755 new mode 100644 index f0dcf65..c86b4b6 --- a/source/ona/kym.zig +++ b/source/ona/kym.zig @@ -1,63 +1,36 @@ const Ast = @import("./kym/Ast.zig"); +const State = @import("./kym/State.zig"); + const coral = @import("coral"); const file = @import("./file.zig"); const tokens = @import("./kym/tokens.zig"); -/// -/// -/// pub const CallContext = struct { env: *RuntimeEnv, - obj: Value, - args: []const Value = &.{}, + caller: *const RuntimeRef, + callable: *const RuntimeRef, + userdata: []u8, + args: []const *const RuntimeRef = &.{}, - /// - /// - /// - pub fn arg_at(self: CallContext, index: Int) RuntimeError!Value { + pub fn arg_at(self: CallContext, index: u8) RuntimeError!*const RuntimeRef { if (!coral.math.is_clamped(index, 0, self.args.len - 1)) { return self.env.check_fail("argument out of bounds"); } - return self.args[@intCast(usize, index)]; + return self.args[@as(usize, index)]; } }; -const Chunk = struct { - env: *RuntimeEnv, - opcodes: coral.list.Stack(Opcode), +const Compiler = struct { + state: *State, + opcodes: OpcodeList, - /// - /// - /// - const Opcode = union (enum) { - push_nil, - push_true, - push_false, - push_zero, - push_number: Float, - push_array: i32, - push_table: i32, + const OpcodeList = coral.list.Stack(Opcode); - not, - neg, - - add, - sub, - mul, - div, - - eql, - cgt, - clt, - cge, - cle, - }; - - pub fn compile_ast(self: *Chunk, ast: Ast) void { + fn compile_ast(self: *Compiler, ast: Ast) coral.io.AllocationError!void { for (ast.list_statements()) |statement| { switch (statement) { .return_expression => |return_expression| { @@ -65,67 +38,80 @@ const Chunk = struct { }, .return_nothing => { - try self.emit(.push_nil); + try self.opcodes.push_one(.push_nil); }, } } } - pub fn compile_expression(self: *Chunk, expression: Ast.Expression) void { + fn compile_expression(self: *Compiler, expression: Ast.Expression) coral.io.AllocationError!void { + const is_zero = struct { + fn is_zero(utf8: []const u8) bool { + return coral.io.equals(utf8, "0") or coral.io.equals(utf8, "0.0"); + } + }.is_zero; + + const number_format = coral.utf8.DecimalFormat{ + .delimiter = "_", + .positive_prefix = .none, + }; + switch (expression) { - .nil_literal => try self.emit(.push_nil), - .true_literal => try self.emit(.push_true), - .false_literal => try self.emit(.push_false), - .integer_literal => |literal| try self.emit(if (literal == 0) .push_zero else .{.push_integer = literal}), - .float_literal => |literal| try self.emit(if (literal == 0) .push_zero else .{.push_float = literal}), - .string_literal => |literal| try self.emit(.{.push_object = try self.intern(literal)}), + .nil_literal => try self.opcodes.push_one(.push_nil), + .true_literal => try self.opcodes.push_one(.push_true), + .false_literal => try self.opcodes.push_one(.push_false), - .array_literal => |elements| { - if (elements.values.len > coral.math.max_int(@typeInfo(Int).Int)) { - return error.OutOfMemory; - } + .number_literal => |literal| { + const parsed_number = number_format.parse(literal, State.Float); - for (elements.values) |element_expression| { - try self.compile_expression(element_expression); - } + coral.debug.assert(parsed_number != null); - try self.emit(.{.push_array = @intCast(Int, elements.values.len)}); + try self.opcodes.push_one(if (is_zero(literal)) .push_zero else .{.push_number = parsed_number.?}); + }, + + .string_literal => |literal| { + try self.opcodes.push_one(.{ + .push_object = try self.state.acquire_interned(literal, &string_info), + }); }, .table_literal => |fields| { - if (fields.values.len > coral.math.max_int(@typeInfo(Int).Int)) { + if (fields.values.len > coral.math.max_int(@typeInfo(u32).Int)) { return error.OutOfMemory; } for (fields.values) |field| { try self.compile_expression(field.expression); - try self.emit(.{.push_object = try self.intern(field.identifier)}); + + try self.opcodes.push_one(.{ + .push_object = try self.state.acquire_interned(field.identifier, &string_info), + }); } - try self.emit(.{.push_table = @intCast(Int, fields.values.len)}); + try self.opcodes.push_one(.{.push_table = @intCast(fields.values.len)}); }, .binary_operation => |operation| { try self.compile_expression(operation.lhs_expression.*); try self.compile_expression(operation.rhs_expression.*); - try self.emit(switch (operation.operator) { + try self.opcodes.push_one(switch (operation.operator) { .addition => .add, .subtraction => .sub, .multiplication => .mul, .divsion => .div, - .greater_equals_comparison => .compare_eq, - .greater_than_comparison => .compare_gt, - .equals_comparison => .compare_ge, - .less_than_comparison => .compare_lt, - .less_equals_comparison => .compare_le, + .greater_equals_comparison => .eql, + .greater_than_comparison => .cgt, + .equals_comparison => .cge, + .less_than_comparison => .clt, + .less_equals_comparison => .cle, }); }, .unary_operation => |operation| { try self.compile_expression(operation.expression.*); - try self.emit(switch (operation.operator) { + try self.opcodes.push_one(switch (operation.operator) { .boolean_negation => .not, .numeric_negation => .neg, }); @@ -137,405 +123,477 @@ const Chunk = struct { } } - pub fn deinit(self: *Chunk) void { - self.opcodes.deinit(self.env.allocator); + fn free(self: *Compiler) void { + for (self.opcodes.values) |opcode| { + if (opcode == .push_object) { + self.state.release(opcode.push_object); + } + } + + self.opcodes.free(); } - pub fn execute(self: *Chunk) RuntimeError!Value { - _ = self; + fn list_opcodes(self: Compiler) []const Opcode { + return self.opcodes.values; + } - return Value.nil; + fn make(allocator: coral.io.Allocator, state: *State) Compiler { + return .{ + .opcodes = OpcodeList.make(allocator), + .state = state, + }; } }; -pub const Float = f64; +pub const IndexContext = struct { + env: *RuntimeEnv, + indexable: *const RuntimeRef, + index: *const RuntimeRef, + userdata: []u8, +}; -pub const Int = i32; +pub const ObjectInfo = struct { + call: *const fn (context: CallContext) RuntimeError!*RuntimeRef = default_call, + clean: *const fn (userdata: []u8) void = default_clean, + get: *const fn (context: IndexContext) RuntimeError!*RuntimeRef = default_get, + set: *const fn (context: IndexContext, Any: *const RuntimeRef) RuntimeError!void = default_set, -pub const Objectid = u32; + fn cast(object_info: *const anyopaque) *const ObjectInfo { + return @ptrCast(@alignCast(object_info)); + } + + fn default_call(context: CallContext) RuntimeError!*RuntimeRef { + return context.env.raise(error.BadOperation, "attempt to call non-callable"); + } + + fn default_clean(_: []u8) void { + // Nothing to clean up by default. + } + + fn default_get(context: IndexContext) RuntimeError!*RuntimeRef { + return context.env.raise(error.BadOperation, "attempt to get non-indexable"); + } + + fn default_set(context: IndexContext, _: *const RuntimeRef) RuntimeError!void { + return context.env.raise(error.BadOperation, "attempt to set non-indexable"); + } +}; + +pub const Opcode = union (enum) { + push_nil, + push_true, + push_false, + push_zero, + push_number: State.Float, + push_table: u32, + push_object: *State.Object, + + not, + neg, + + add, + sub, + mul, + div, + + eql, + cgt, + clt, + cge, + cle, +}; pub const RuntimeEnv = struct { - output: coral.io.Writer, - stack: coral.list.Stack(u64), - interned: coral.table.Hashed([]const u8, Objectid, coral.table.string_keyer), - objects: coral.slab.Map(@typeInfo(u32).Int, Object), - user_allocator: coral.io.Allocator, + allocator: coral.io.Allocator, + err_writer: coral.io.Writer, + bound_refs: VariantSlab, + state: State, - pub const DataSource = struct { - name: []const u8, - data: []const u8, + pub const Options = struct { + out_writer: coral.io.Writer = coral.io.null_writer, + err_writer: coral.io.Writer = coral.io.null_writer, }; - pub const Limits = struct { - stack_max: u32, - calls_max: u32, + pub const ScriptSource = struct { + name: []const coral.io.Byte, + data: []const coral.io.Byte, }; - const Object = struct { - ref_count: usize, + const Table = struct { + state: *State, + fields: FieldTable, + array: ArrayList, - state: struct { - info: ObjectInfo, - userdata: []u8, + const ArrayList = coral.list.Stack(State.Variant); - fields: coral.table.Hashed(*Object, *Value, .{ - .hasher = struct { - fn hash(object: *Object) coral.table.Hash { - coral.debug.assert(object.state.info.identity == null); + const FieldTable = coral.map.Table([]const coral.io.Byte, struct { + field: *State.Object, + value: State.Variant, - return coral.table.hash_string(object.state.userdata); - } - }.hash, + const Self = @This(); - .comparer = struct { - fn compare(object_a: *Object, object_b: *Object) isize { - coral.debug.assert(object_a.state.info.identity == null); - coral.debug.assert(object_b.state.info.identity == null); + fn release_objects(self: Self, state: *State) void { + state.release(self.field); - return coral.io.compare(object_a.state.userdata, object_b.state.userdata); - } - }.compare, - }), - }, - }; + if (self.value == .object) { + state.release(self.value.object); + } + } + }, coral.map.string_table_traits); - pub const ObjectInfo = struct { - caller: *const fn (caller: Value, context: CallContext) RuntimeError!Value = default_call, - cleaner: *const fn (userdata: []u8) void = default_clean, - getter: *const fn (context: CallContext) RuntimeError!Value = default_get, - identity: ?*const anyopaque = null, - setter: *const fn (context: CallContext) RuntimeError!void = default_set, + fn free(self: *Table) void { + { + var field_iterator = FieldTable.Iterable{.table = &self.fields}; - fn default_call(_: Value, context: CallContext) RuntimeError!Value { - return context.env.fail(error.BadOperation, "attempt to call non-callable"); + while (field_iterator.next()) |entry| { + entry.value.release_objects(self.state); + } + } + + self.fields.free(); + self.array.free(); } - fn default_clean(_: []u8) void { - // Nothing to clean up by default. + fn get_field(self: *Table, field_name: *State.Object) State.Variant { + const field = self.fields.lookup(field_name.userdata) orelse return .nil; + + if (field.value == .object) { + return .{.object = self.state.acquire_instance(field.value.object)}; + } + + return field.value; } - fn default_get(context: CallContext) RuntimeError!Value { - return context.env.fail(error.BadOperation, "attempt to get non-indexable"); + fn get_index(self: *Table, index: usize) State.Variant { + return self.array.values[index]; } - fn default_set(context: CallContext) RuntimeError!void { - return context.env.fail(error.BadOperation, "attempt to set non-indexable"); - } - }; - - pub fn call(self: *RuntimeEnv, caller: Value, maybe_index: ?Value, args: []const Value) RuntimeError!RuntimeVar { - if (maybe_index) |index| { - const callable = try self.get(caller, index); - - defer callable.deinit(); - - return switch (callable.value.unpack()) { - .objectid => |callable_id| .{ - .env = self, - - .value = try self.objects.fetch(callable_id).state.info.caller(.{ - .env = self, - .callable = callable.value, - .obj = caller, - .args = args, - }), - }, - - else => self.fail(error.BadOperation, "attempt to call non-object type"), + fn make(allocator: coral.io.Allocator, state: *State) Table { + return .{ + .state = state, + .fields = FieldTable.make(allocator), + .array = ArrayList.make(allocator), }; } - return self.bind(try self.objects.fetch(try self.to_objectid(caller)).state.info.caller(.{ - .env = self, - .obj = caller, - // .caller = .{.object = self.global_object}, - .args = args, - })); - } + const object_info = ObjectInfo{ + .clean = struct { + fn clean(userdata: []u8) void { + @as(*Table, @ptrCast(@alignCast(userdata.ptr))).free(); + } + }.clean, - pub fn check( - self: *RuntimeEnv, - condition: bool, - runtime_error: RuntimeError, - failure_message: []const u8) RuntimeError!void { + .get = struct { + fn get(context: IndexContext) RuntimeError!*RuntimeRef { + const table = @as(*Table, @ptrCast(@alignCast(context.userdata.ptr))); - if (condition) { - return; + switch (try context.index.fetch(context.env)) { + .nil => return context.env.raise(error.BadOperation, "cannot index a table with nil"), + .true => return context.env.raise(error.BadOperation, "cannot index a table with true"), + .false => return context.env.raise(error.BadOperation, "cannot index a table with false"), + + .object => |index_object| { + const value = table.get_field(index_object); + + errdefer if (value == .object) { + context.env.state.release(value.object); + }; + + return @ptrFromInt(try context.env.bound_refs.insert(value)); + }, + + .number => |index_number| { + const value = table.get_index(@intFromFloat(index_number)); + + errdefer if (value == .object) { + context.env.state.release(value.object); + }; + + return @ptrFromInt(try context.env.bound_refs.insert(value)); + }, + } + } + }.get, + }; + + fn set_field(self: *Table, field_name: *State.Object, value: State.Variant) coral.io.AllocationError!void { + const previous_entry = try self.fields.replace(field_name.userdata, .{ + .field = field_name, + .value = value, + }); + + if (previous_entry) |entry| { + entry.value.release_objects(self.state); + } } - return self.fail(runtime_error, failure_message); + fn set_index(self: *Table, index: usize, value: State.Variant) coral.io.AllocationError!void { + self.array.values[index] = value; + } + }; + + const VariantSlab = coral.map.Slab(State.Variant); + + pub fn discard(self: *RuntimeEnv, ref: *RuntimeRef) void { + coral.debug.assert(self.bound_refs.remove(@intFromPtr(ref)) != null); } - pub fn deinit(self: *RuntimeEnv) void { - self.stack.deinit(); + pub fn execute_chunk(self: *RuntimeEnv, name: []const coral.io.Byte, opcodes: []const Opcode) RuntimeError!*RuntimeRef { + _ = name; + + for (opcodes) |opcode| { + switch (opcode) { + .push_nil => try self.state.push_value(.nil), + .push_true => try self.state.push_value(.true), + .push_false => try self.state.push_value(.false), + .push_zero => try self.state.push_value(.{.number = 0}), + .push_number => |number| try self.state.push_value(.{.number = number}), + + .push_table => |size| { + var table = Table.make(self.allocator, &self.state); + + errdefer table.free(); + + { + var popped = @as(usize, 0); + + while (popped < size) : (popped += 1) { + switch (try self.state.pop_value()) { + .object => |field| try table.set_field(field, try self.state.pop_value()), + else => return self.raise(error.BadOperation, "attempt to set a non-object field"), + } + } + } + + const table_object = try self.state.acquire_new(coral.io.bytes_of(&table), &Table.object_info); + + errdefer self.state.release(table_object); + + try self.state.push_value(.{.object = table_object}); + }, + + .push_object => |object| { + const acquired_object = self.state.acquire_instance(object); + + errdefer self.state.release(acquired_object); + + try self.state.push_value(.{.object = acquired_object}); + }, + + .not => { + try self.state.push_value(switch (try self.state.pop_value()) { + .nil => return self.raise(error.BadOperation, "cannot not nil"), + .false => .true, + .true => .false, + .number => return self.raise(error.BadOperation, "cannot not a number"), + .object => return self.raise(error.BadOperation, "cannot not an object"), + }); + }, + + .neg => { + try self.state.push_value(switch (try self.state.pop_value()) { + .nil => return self.raise(error.BadOperation, "cannot not nil"), + .false => return self.raise(error.BadOperation, "cannot not false"), + .true => return self.raise(error.BadOperation, "cannot not true"), + .number => |number| .{.number = -number}, + .object => return self.raise(error.BadOperation, "cannot not an object"), + }); + }, + + .add => { + + }, + + .sub => { + + }, + + .mul => { + + }, + + .div => { + + }, + + .eql => { + + }, + + .cgt => { + + }, + + .clt => { + + }, + + .cge => { + + }, + + .cle => { + + }, + } + } + + const return_value = try self.state.pop_value(); + + errdefer if (return_value == .object) { + self.state.release(return_value.object); + }; + + return @ptrFromInt(try self.bound_refs.insert(return_value)); } - pub fn execute_data(self: *RuntimeEnv, allocator: coral.io.Allocator, source: DataSource) RuntimeError!RuntimeVar { - var ast = try Ast.init(allocator); + pub fn execute_file(self: *RuntimeEnv, file_access: file.Access, file_path: file.Path) RuntimeError!*RuntimeRef { + const error_message = "failed to load file"; - defer ast.deinit(); + const file_data = (try file.allocate_and_load(self.allocator, file_access, file_path)) orelse { + return self.raise(error.SystemFailure, error_message); + }; + + defer self.allocator.deallocate(file_data); + + return self.execute_script(.{ + .name = file_path.to_string() orelse return self.raise(error.SystemFailure, error_message), + .data = file_data, + }); + } + + pub fn execute_script(self: *RuntimeEnv, source: ScriptSource) RuntimeError!*RuntimeRef { + var ast = Ast.make(self.allocator); + + defer ast.free(); { var tokenizer = tokens.Tokenizer{.source = source.data}; - try ast.parse(&tokenizer); + ast.parse(&tokenizer) catch |parse_error| switch (parse_error) { + error.BadSyntax => return self.raise(error.BadSyntax, ast.error_message), + error.OutOfMemory => return error.OutOfMemory, + }; } - var chunk = Chunk{ + var compiler = Compiler.make(self.allocator, &self.state); + + defer compiler.free(); + + try compiler.compile_ast(ast); + + return self.execute_chunk(source.name, compiler.list_opcodes()); + } + + pub fn free(self: *RuntimeEnv) void { + self.bound_refs.free(); + self.state.free(); + } + + pub fn get_field(self: *RuntimeEnv, indexable: *const RuntimeRef, field: []const u8) RuntimeError!*RuntimeRef { + const interned_field = try self.intern(field); + + defer self.discard(interned_field); + + const indexable_object = try indexable.fetch_object(self); + + return ObjectInfo.cast(indexable_object.userinfo).get(.{ .env = self, - .opcodes = .{.allocator = allocator}, - }; - - const typeid = ""; - - const script = try self.new_object(allocator, coral.io.bytes_of(&chunk), .{ - .identity = typeid, - - .cleaner = struct { - fn clean(userdata: []const u8) void { - @ptrCast(*Chunk, @alignCast(@alignOf(Chunk), userdata)).deinit(); - } - }.clean, - - .caller = struct { - fn call(caller: Value, context: CallContext) RuntimeError!Value { - _ = caller; - - return (context.env.native_cast(context.obj, typeid, Chunk) catch unreachable).execute(); - } - }.call, - }); - - defer script.deinit(); - - return try self.call(script.value, null, &.{}); - } - - pub fn execute_file( - self: *RuntimeEnv, - allocator: coral.io.Allocator, - file_system: file.System, - file_path: file.Path) RuntimeError!RuntimeVar { - - const readable_file = file_system.open_readable(file_path) catch return error.SystemFailure; - - defer readable_file.close(); - - var file_data = coral.list.ByteStack{.allocator = allocator}; - const file_size = (file_system.query_info(file_path) catch return error.SystemFailure).size; - - try file_data.grow(file_size); - - defer file_data.deinit(); - - { - var stream_buffer = [_]u8{0} ** 4096; - - if ((coral.io.stream(coral.list.stack_as_writer(&file_data), readable_file.as_reader(), &stream_buffer) catch { - return error.SystemFailure; - }) != file_size) { - return error.SystemFailure; - } - } - - return try self.execute_data(allocator, .{ - .name = file_path.to_string() catch return error.SystemFailure, - .data = file_data.values, + .indexable = indexable, + .index = interned_field, + .userdata = indexable_object.userdata, }); } - pub fn fail(self: *RuntimeEnv, runtime_error: RuntimeError, failure_message: []const u8) RuntimeError { - // TODO: Call stack and line numbers. - coral.utf8.print_formatted(self.output, "{name}@({line}): {message}\n", .{ - .name = ".ona", - .line = @as(u64, 0), - .message = failure_message, - }) catch return error.SystemFailure; + pub fn intern(self: *RuntimeEnv, data: []const u8) RuntimeError!*RuntimeRef { + const data_object = try self.state.acquire_interned(data, &string_info); - return runtime_error; + errdefer self.state.release(data_object); + + return @ptrFromInt(try self.bound_refs.insert(.{.object = data_object})); } - pub fn get(self: *RuntimeEnv, indexable: Value, index: Value) RuntimeError!RuntimeVar { - const indexable_object = self.objects.fetch(switch (indexable.unpack()) { - .objectid => |indexable_id| indexable_id, - else => return self.fail(error.BadOperation, "attempt to index non-indexable type"), - }); - - return .{ - .env = self, - - .value = try indexable_object.state.info.getter(.{ - .env = self, - .obj = indexable, - .args = &.{index}, - }), - }; - } - - pub fn init(allocator: coral.io.Allocator, output: coral.io.Writer, limits: Limits) RuntimeError!RuntimeEnv { + pub fn make(allocator: coral.io.Allocator, options: Options) RuntimeError!RuntimeEnv { var env = RuntimeEnv{ - .output = output, - .stack = .{.allocator = allocator}, - .objects = .{.allocator = allocator}, - .interned = .{.allocator = allocator}, - .user_allocator = allocator, + .allocator = allocator, + .bound_refs = VariantSlab.make(allocator), + .state = State.make(allocator), + .err_writer = options.err_writer, }; - try env.stack.grow(limits.stack_max * limits.calls_max); - - errdefer env.stack.deinit(); - return env; } - pub fn intern(self: *RuntimeEnv, string: []const u8) RuntimeError!Value { - return Value.pack_objectid(self.interned.lookup(string) orelse { - const interned_value = (try self.new_string(string)).value; + pub fn new_object(self: *RuntimeEnv, userdata: []const u8, info: *const ObjectInfo) RuntimeError!*RuntimeRef { + const data_object = try self.state.acquire_new(userdata, info); - switch (interned_value.unpack()) { - .objectid => |id| coral.debug.assert(try self.interned.insert(string, id)), - else => unreachable, - } + defer self.state.release(data_object); - return interned_value; - }); + return @ptrFromInt(try self.bound_refs.insert(.{.object = data_object})); } - pub fn native_cast(self: *RuntimeEnv, castable: Value, id: *const anyopaque, comptime Type: type) RuntimeError!*Type { - const object = self.objects.fetch(castable.to_objectid() orelse { - return self.fail(error.BadOperation, "attempt to cast non-castable type"); - }); - - const is_expected_type = (object.state.info.identity == id) and (object.state.userdata.len == @sizeOf(Type)); - - try self.check(is_expected_type, "invalid object cast: native type"); - - return @ptrCast(*Type, @alignCast(@alignOf(Type), object.state.userdata)); + pub fn nil(self: *RuntimeEnv) RuntimeError!*RuntimeRef { + return @ptrFromInt(try self.bound_refs.insert(.nil)); } - pub fn new_object(self: *RuntimeEnv, allocator: coral.io.Allocator, userdata: []const u8, info: ObjectInfo) RuntimeError!RuntimeVar { - const allocation = try coral.io.allocate_many(allocator, userdata.len, u8); + pub fn raise(self: *RuntimeEnv, runtime_error: RuntimeError, error_message: []const u8) RuntimeError { + if (self.err_writer.invoke(error_message) == null) { + return error.SystemFailure; + } - errdefer coral.io.deallocate(allocator, allocation); + return runtime_error; + } - coral.io.copy(allocation, userdata); + pub fn to_float(self: *RuntimeEnv, ref: *const RuntimeRef) RuntimeError!State.Float { + return ref.fetch_number(self); + } - const objectid = try self.objects.insert(.{ - .ref_count = 1, - - .state = .{ - .info = info, - .userdata = allocation, - .fields = .{.allocator = allocator}, - }, - }); - - return .{ - .env = self, - .value = .{.data = @as(usize, 0x7FF8000000000002) | (@as(usize, objectid) << 32)}, + pub fn to_string(self: *RuntimeEnv, ref: *const RuntimeRef) RuntimeError![]const u8 { + const object = try switch (try ref.fetch(self)) { + .object => |object| object, + else => self.raise(error.BadOperation, "cannot convert to object"), }; - } - pub fn new_string(self: *RuntimeEnv, data: []const u8) RuntimeError!RuntimeVar { - return try self.new_object(data, .{ - .getter = struct { - fn get_byte(context: CallContext) RuntimeError!*Value { - const string = context.env.string_cast(context.obj) catch unreachable; - const index = try context.env.to_int(try context.arg_at(0)); + if (ObjectInfo.cast(object.userinfo) != &string_info) { + return self.raise(error.BadOperation, "object is not a string"); + } - try context.env.check(coral.math.is_clamped(index, 0, string.len), "index out of string bounds"); - - return context.env.new_int(string[@intCast(usize, index)]); - } - }.get_byte, - }); + return object.userdata; } }; -pub const RuntimeError = coral.io.AllocationError || Ast.ParseError || error { +pub const RuntimeError = coral.io.AllocationError || State.PopError || error { + BadSyntax, BadOperation, - BadArgument, SystemFailure, }; -pub const RuntimeVar = struct { - value: Value, - env: *RuntimeEnv, - - pub fn deinit(self: RuntimeVar) void { - switch (self.value.unpack()) { - .objectid => |id| { - const object = self.env.objects.fetch(id); - - coral.debug.assert(object.ref_count != 0); - - object.ref_count -= 1; - - if (object.ref_count == 0) { - object.state.info.cleaner(object.state.userdata); - // TODO: Free individual key-value pairs of fields - object.state.fields.deinit(); - coral.io.deallocate(self.env.user_allocator, object.state.userdata); - self.env.objects.remove(id); - } - }, - - else => {}, - } - } -}; - -pub const Value = struct { - data: u64, - - pub const Unpacked = union (enum) { - nil, - false, - true, - number: Float, - objectid: Objectid, - }; - - fn pack_number(float: Float) Value { - return @bitCast(Value, float); +pub const RuntimeRef = opaque { + fn fetch(self: *const RuntimeRef, env: *RuntimeEnv) RuntimeError!State.Variant { + return env.bound_refs.lookup(@intFromPtr(self)) orelse env.raise(error.BadOperation, "stale ref"); } - fn pack_objectid(id: Objectid) Value { - return signature.objectid | id; + fn fetch_number(self: *const RuntimeRef, env: *RuntimeEnv) RuntimeError!State.Float { + return switch (try self.fetch(env)) { + .nil => env.raise(error.BadOperation, "cannot convert nil to number"), + .true => env.raise(error.BadOperation, "cannot convert true to number"), + .false => env.raise(error.BadOperation, "cannot convert false to number"), + .number => |number| number, + .object => env.raise(error.BadOperation, "cannot convert object to number"), + }; } - pub const @"false" = @as(Value, nan | 0x0001000000000000); - - const mask = .{ - .sign = @as(u64, 0x8000000000000000), - .exponent = @as(u64, 0x7ff0000000000000), - .quiet = @as(u64, 0x0008000000000000), - .type = @as(u64, 0x0007000000000000), - .signature = @as(u64, 0xffff000000000000), - .object = @as(u64, 0x00000000ffffffff), - }; - - pub const nan = @as(Value, mask.exponent | mask.quiet); - - pub const nil = @as(Value, nan | 0x0003000000000000); - - const signature = .{ - - }; - - pub const @"true" = @as(Value, nan | 0x0002000000000000); - - pub fn unpack(self: Value) Unpacked { - if ((~self.data & mask.exponent) != 0) { - return .{.number = @bitCast(Float, self.data)}; - } - - return switch ((self.data & mask.signature) != 0) { - .signature_nan => .{.number = @bitCast(Float, self.data)}, - .signature_false => .false, - .signature_true => .true, - .signature_object => @intCast(Objectid, self.data & mask.object), - else => return .nil, + fn fetch_object(self: *const RuntimeRef, env: *RuntimeEnv) RuntimeError!*State.Object { + return switch (try self.fetch(env)) { + .nil => env.raise(error.BadOperation, "cannot convert nil to object"), + .true => env.raise(error.BadOperation, "cannot convert true to object"), + .false => env.raise(error.BadOperation, "cannot convert false to object"), + .number => env.raise(error.BadOperation, "cannot convert number to object"), + .object => |object| object, }; } }; + +const string_info = ObjectInfo{ + +}; diff --git a/source/ona/kym/Ast.zig b/source/ona/kym/Ast.zig index 93be261..c0dba35 100755 --- a/source/ona/kym/Ast.zig +++ b/source/ona/kym/Ast.zig @@ -4,49 +4,16 @@ const tokens = @import("./tokens.zig"); allocator: coral.io.Allocator, arena: coral.arena.Stacking, -statements: StatementList, +statements: Statement.List, error_message: []const u8, -pub const BinaryOperator = enum { - addition, - subtraction, - multiplication, - divsion, - equals_comparison, - greater_than_comparison, - greater_equals_comparison, - less_than_comparison, - less_equals_comparison, - - fn token(self: BinaryOperator) tokens.Token { - return switch (self) { - .addition => .symbol_plus, - .subtraction => .symbol_minus, - .multiplication => .symbol_asterisk, - .divsion => .symbol_forward_slash, - .equals_comparison => .symbol_double_equals, - .greater_than_comparison => .symbol_greater_than, - .greater_equals_comparison => .symbol_greater_equals, - .less_than_comparison => .symbol_less_than, - .less_equals_comparison => .symbol_less_equals, - }; - } -}; - pub const Expression = union (enum) { nil_literal, true_literal, false_literal, - integer_literal: []const u8, - float_literal: []const u8, + number_literal: []const u8, string_literal: []const u8, - array_literal: coral.list.Stack(Expression), - - table_literal: coral.list.Stack(struct { - identifier: []const u8, - expression: Expression, - }), - + table_literal: NamedList, grouped_expression: *Expression, binary_operation: struct { @@ -59,13 +26,43 @@ pub const Expression = union (enum) { operator: UnaryOperator, expression: *Expression, }, + + pub const BinaryOperator = enum { + addition, + subtraction, + multiplication, + divsion, + equals_comparison, + greater_than_comparison, + greater_equals_comparison, + less_than_comparison, + less_equals_comparison, + + fn token(self: BinaryOperator) tokens.Token { + return switch (self) { + .addition => .symbol_plus, + .subtraction => .symbol_minus, + .multiplication => .symbol_asterisk, + .divsion => .symbol_forward_slash, + .equals_comparison => .symbol_double_equals, + .greater_than_comparison => .symbol_greater_than, + .greater_equals_comparison => .symbol_greater_equals, + .less_than_comparison => .symbol_less_than, + .less_equals_comparison => .symbol_less_equals, + }; + } + }; + + pub const NamedList = coral.list.Stack(struct { + identifier: []const u8, + expression: Expression, + }); + + pub const List = coral.list.Stack(Expression); }; const ExpressionParser = fn (self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression; -/// -/// -/// pub const ParseError = error { OutOfMemory, BadSyntax, @@ -76,16 +73,16 @@ const Self = @This(); pub const Statement = union (enum) { return_expression: Expression, return_nothing, -}; -const StatementList = coral.list.Stack(Statement); + const List = coral.list.Stack(Statement); +}; const UnaryOperator = enum { boolean_negation, numeric_negation, }; -fn binary_operation_parser(comptime parse_next: ExpressionParser, comptime operators: []const BinaryOperator) ExpressionParser { +fn binary_operation_parser(comptime parse_next: ExpressionParser, comptime operators: []const Expression.BinaryOperator) ExpressionParser { return struct { fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression { var expression = try parse_next(self, tokenizer); @@ -117,30 +114,30 @@ fn binary_operation_parser(comptime parse_next: ExpressionParser, comptime opera }.parse; } -fn check_syntax(self: *Self, condition: bool, error_message: []const u8) ParseError!void { +fn check_syntax(self: *Self, condition: bool, message: []const u8) ParseError!void { if (condition) { return; } - return self.fail_syntax(error_message); + return self.fail_syntax(message); } -pub fn deinit(self: *Self) void { - self.arena.deinit(); - self.statements.deinit(); -} - -fn fail_syntax(self: *Self, error_message: []const u8) ParseError { - self.error_message = error_message; +fn fail_syntax(self: *Self, message: []const u8) ParseError { + self.error_message = message; return error.BadSyntax; } -pub fn init(allocator: coral.io.Allocator) coral.io.AllocationError!Self { +pub fn free(self: *Self) void { + self.arena.free(); + self.statements.free(); +} + +pub fn make(allocator: coral.io.Allocator) Self { return Self{ - .arena = try coral.arena.Stacking.init(allocator, 4096), + .arena = coral.arena.Stacking.make(allocator, 4096), .allocator = allocator, - .statements = .{.allocator = allocator}, + .statements = Statement.List.make(allocator), .error_message = "", }; } @@ -150,16 +147,14 @@ pub fn list_statements(self: Self) []const Statement { } pub fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!void { - self.reset(); + self.free(); - errdefer self.reset(); - - var has_not_returned_yet = true; + var has_returned = false; while (tokenizer.step(.{.include_newlines = false})) { switch (tokenizer.current_token) { .keyword_return => { - try self.check_syntax(has_not_returned_yet, "cannot return more than once per function scope"); + try self.check_syntax(!has_returned, "multiple returns in function scope but expected only one"); try self.statements.push_one(get_statement: { if (tokenizer.step(.{.include_newlines = true})) { @@ -177,7 +172,7 @@ pub fn parse(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!void { break: get_statement .return_nothing; }); - has_not_returned_yet = false; + has_returned = true; }, else => return self.fail_syntax("invalid statement"), @@ -215,16 +210,10 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression return Expression{.grouped_expression = try coral.io.allocate_one(self.arena.as_allocator(), expression)}; }, - .integer => |value| { + .number => |value| { _ = tokenizer.step(.{.include_newlines = false}); - return Expression{.integer_literal = value}; - }, - - .real => |value| { - _ = tokenizer.step(.{.include_newlines = false}); - - return Expression{.float_literal = value}; + return Expression{.number_literal = value}; }, .string => |value| { @@ -233,48 +222,10 @@ fn parse_factor(self: *Self, tokenizer: *tokens.Tokenizer) ParseError!Expression return Expression{.string_literal = value}; }, - .symbol_bracket_left => { - try self.check_syntax(tokenizer.step(.{.include_newlines = false}), "unexpected end of array literal"); - - var expression = Expression{ - .array_literal = .{ - .allocator = self.arena.as_allocator(), - }, - }; - - coral.debug.assert(expression == .array_literal); - - const array_average_maximum = 32; - - try expression.array_literal.grow(array_average_maximum); - - while (true) { - switch (tokenizer.current_token) { - .symbol_bracket_right => { - _ = tokenizer.step(.{.include_newlines = false}); - - return expression; - }, - - else => { - try self.check_syntax( - tokenizer.step(.{.include_newlines = false}), - "expected `]` or expression after `[`"); - - try expression.array_literal.push_one(try self.parse_expression(tokenizer)); - }, - } - } - }, - .symbol_brace_left => { try self.check_syntax(tokenizer.step(.{.include_newlines = false}), "unexpected end of table literal"); - var expression = Expression{ - .table_literal = .{ - .allocator = self.arena.as_allocator(), - }, - }; + var expression = Expression{.table_literal = Expression.NamedList.make(self.arena.as_allocator())}; coral.debug.assert(expression == .table_literal); @@ -356,8 +307,3 @@ const parse_term = binary_operation_parser(parse_factor, &.{ .multiplication, .divsion, }); - -pub fn reset(self: *Self) void { - self.statements.clear(); - self.arena.deinit(); -} diff --git a/source/ona/kym/State.zig b/source/ona/kym/State.zig new file mode 100644 index 0000000..6a2c6c6 --- /dev/null +++ b/source/ona/kym/State.zig @@ -0,0 +1,116 @@ +const coral = @import("coral"); + +allocator: coral.io.Allocator, +interned: SymbolTable, +globals: Object, +values: DataStack, +frames: CallStack, + +pub const Float = f64; + +const CallStack = coral.list.Stack(struct { + callable: *Object, + opcode_index: usize, + stack_index: usize, +}); + +const DataStack = coral.list.Stack(Variant); + +pub const Object = struct { + ref_count: usize, + userdata: []coral.io.Byte, + userinfo: *const anyopaque, +}; + +pub const PopError = error { + StackOverflow, +}; + +const Self = @This(); + +const SymbolTable = coral.map.Table([]const coral.io.Byte, *Object, coral.map.string_table_traits); + +pub const Variant = union (enum) { + nil, + true, + false, + number: Float, + object: *Object, +}; + +pub fn acquire_instance(_: *Self, object: *Object) *Object { + // TODO: safety-check object belongs to state. + object.ref_count += 1; + + return object; +} + +pub fn acquire_interned(self: *Self, userdata: []const u8, userinfo: *const anyopaque) coral.io.AllocationError!*Object { + // TODO: Include userinfo in matching lookup. + if (self.interned.lookup(userdata)) |object| { + return self.acquire_instance(object); + } else { + const data_object = try self.acquire_new(userdata, userinfo); + + errdefer self.release(data_object); + + coral.debug.assert(try self.interned.insert(data_object.userdata, data_object)); + + return data_object; + } +} + +pub fn acquire_new(self: *Self, userdata: []const u8, userinfo: *const anyopaque) coral.io.AllocationError!*Object { + const allocated_userdata = try coral.io.allocate_copy(self.allocator, userdata); + + errdefer self.allocator.deallocate(allocated_userdata); + + const allocated_object = try coral.io.allocate_one(self.allocator, Object{ + .ref_count = 1, + .userdata = allocated_userdata, + .userinfo = userinfo, + }); + + errdefer self.allocator.deallocate(allocated_object); + + return allocated_object; +} + +pub fn free(self: *Self) void { + self.values.free(); + self.frames.free(); + self.interned.free(); +} + +pub fn make(allocator: coral.io.Allocator) Self { + return .{ + .values = DataStack.make(allocator), + .frames = CallStack.make(allocator), + .interned = SymbolTable.make(allocator), + .allocator = allocator, + + .globals = .{ + .ref_count = 0, + .userdata = &.{}, + .userinfo = &.{}, + }, + }; +} + +pub fn pop_value(self: *Self) PopError!Variant { + return self.values.pop() orelse error.StackOverflow; +} + +pub fn push_value(self: *Self, value: Variant) coral.io.AllocationError!void { + return self.values.push_one(value); +} + +pub fn release(self: *Self, object: *Object) void { + coral.debug.assert(object.ref_count != 0); + + object.ref_count -= 1; + + if (object.ref_count == 0) { + self.allocator.deallocate(object); + } +} diff --git a/source/ona/kym/tokens.zig b/source/ona/kym/tokens.zig index 79cd471..3e3ec3a 100755 --- a/source/ona/kym/tokens.zig +++ b/source/ona/kym/tokens.zig @@ -29,8 +29,7 @@ pub const Token = union(enum) { symbol_equals, symbol_double_equals, - integer: []const u8, - real: []const u8, + number: []const u8, string: []const u8, keyword_nil, @@ -41,7 +40,7 @@ pub const Token = union(enum) { pub fn text(self: Token) []const u8 { return switch (self) { - .unknown => |unknown| @ptrCast([*]const u8, &unknown)[0 .. 1], + .unknown => |unknown| @as([*]const u8, @ptrCast(&unknown))[0 .. 1], .newline => "newline", .global => |identifier| identifier, @@ -69,8 +68,7 @@ pub const Token = union(enum) { .symbol_equals => "=", .symbol_double_equals => "==", - .integer => |literal| literal, - .real => |literal| literal, + .number => |literal| literal, .string => |literal| literal, .keyword_nil => "nil", @@ -134,7 +132,7 @@ pub const Tokenizer = struct { else => break, }; - self.current_token = .{.real = self.source[begin .. cursor]}; + self.current_token = .{.number = self.source[begin .. cursor]}; return true; }, @@ -142,7 +140,7 @@ pub const Tokenizer = struct { else => break, }; - self.current_token = .{.integer = self.source[begin .. cursor]}; + self.current_token = .{.number = self.source[begin .. cursor]}; return true; }, diff --git a/source/ona/ona.zig b/source/ona/ona.zig old mode 100755 new mode 100644 index 740c7d6..1fcbace --- a/source/ona/ona.zig +++ b/source/ona/ona.zig @@ -1,6 +1,4 @@ -const builtin = @import("builtin"); - -const canvas = @import("./canvas.zig"); +const app = @import("./app.zig"); const coral = @import("coral"); @@ -8,163 +6,111 @@ const ext = @import("./ext.zig"); pub const file = @import("./file.zig"); -pub const heap = @import("./heap.zig"); +const heap = @import("./heap.zig"); const kym = @import("./kym.zig"); -const AppManifest = struct { - title: [255:0]u8 = [_:0]u8{0} ** 255, - width: u16 = 640, - height: u16 = 480, - - pub fn load_script(self: *AppManifest, env: *kym.RuntimeEnv, fs: file.System, file_path: []const u8) !void { - var manifest = try env.execute_file(heap.allocator, fs, file.Path.from(&.{file_path})); - - defer manifest.deinit(); - - { - var title = try env.get(manifest.value, try env.intern("title")); - - defer title.deinit(); - - const title_string = try env.string_cast(title.value); - - try env.check(title_string.len <= self.title.len, "`title` cannot exceed 255 bytes in length"); - coral.io.copy(&self.title, title_string); - } - - const u16_max = coral.math.max_int(@typeInfo(u16).Int); - - { - const width = try env.get(manifest.value, try env.intern("width")); - - errdefer width.deinit(); - - if (width.value.as_number()) |value| { - if (value < u16_max) { - self.width = @floatToInt(u16, value); - } - } - } - - { - const height = try env.get(manifest.value, try env.intern("height")); - - errdefer height.deinit(); - - if (height.value.as_number()) |value| { - if (value < u16_max) { - self.height = @floatToInt(u16, value); - } - } - } - } +pub const RuntimeError = error { + OutOfMemory, + InitFailure, + BadManifest, }; -fn stack_as_log_writer(self: *coral.list.ByteStack) coral.io.Writer { - return coral.io.Writer.bind(coral.list.ByteStack, self, struct { - fn write(stack: *coral.list.ByteStack, bytes: []const coral.io.Byte) ?usize { - var line_written = @as(usize, 0); - - for (bytes) |byte| { - if (byte == '\n') { - ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%.*s", stack.values.len, stack.values.ptr); - stack.clear(); - - line_written = 0; - - continue; - } - - stack.push_one(byte) catch { - coral.debug.assert(stack.drop(line_written)); - - return null; - }; - - line_written += 1; - } - - return bytes.len; - } - }.write); +fn last_sdl_error() [:0]const u8 { + return coral.io.slice_sentineled(@as(u8, 0), @as([*:0]const u8, @ptrCast(ext.SDL_GetError()))); } -pub fn run_app(base_file_system: file.System) void { - defer heap.trace_leaks(); +pub fn run_app(file_access: file.Access) RuntimeError!void { + var info_log = app.WritableLog.make(.info, heap.allocator); - var log_buffer = coral.list.ByteStack{.allocator = heap.allocator}; + defer info_log.free(); - defer log_buffer.deinit(); + var fail_log = app.WritableLog.make(.fail, heap.allocator); - var script_env = kym.RuntimeEnv.init(heap.allocator, stack_as_log_writer(&log_buffer), .{ - .stack_max = 512, - .calls_max = 512, - }) catch { - return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "failed to initialize Kym vm\n"); - }; - - defer script_env.deinit(); - - const app_file_name = "app.ona"; - var app_manifest = AppManifest{}; - - app_manifest.load_script(&script_env, base_file_system, app_file_name) catch { - return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "failed to load %s\n", app_file_name); - }; + defer fail_log.free(); if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) { - return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%s\n", ext.SDL_GetError()); + try fail_log.write(last_sdl_error()); + + return error.InitFailure; } defer ext.SDL_Quit(); + var script_env = kym.RuntimeEnv.make(heap.allocator, .{ + .out_writer = info_log.as_writer(), + .err_writer = fail_log.as_writer(), + }) catch { + try fail_log.write("failed to initialize script runtime"); + + return error.InitFailure; + }; + + defer script_env.free(); + + var manifest = app.Manifest{}; + + manifest.load(&script_env, file_access) catch { + fail_log.write("failed to load / execute app.ona manifest") catch {}; + + return error.BadManifest; + }; + + const window = create: { + const pos = ext.SDL_WINDOWPOS_CENTERED; + const flags = 0; + + break: create ext.SDL_CreateWindow(&manifest.title, pos, pos, manifest.width, manifest.height, flags) orelse { + fail_log.write(last_sdl_error()) catch {}; + + return error.InitFailure; + }; + }; + + defer ext.SDL_DestroyWindow(window); + + const renderer = create: { + const defaultDriverIndex = -1; + const flags = ext.SDL_RENDERER_ACCELERATED; + + break: create ext.SDL_CreateRenderer(window, defaultDriverIndex, flags) orelse { + fail_log.write(last_sdl_error()) catch {}; + + return error.InitFailure; + }; + }; + + defer ext.SDL_DestroyRenderer(renderer); + { - const base_prefix = ext.SDL_GetBasePath() orelse { - return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%s\n", ext.SDL_GetError()); - }; - - defer ext.SDL_free(base_prefix); - - const window_flags = 0; - const window_pos = ext.SDL_WINDOWPOS_CENTERED; - - const window = ext.SDL_CreateWindow(&app_manifest.title, window_pos, window_pos, app_manifest.width, app_manifest.height, window_flags) orelse { - return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%s\n", ext.SDL_GetError()); - }; - - defer ext.SDL_DestroyWindow(window); - - const renderer_flags = 0; - - const renderer = ext.SDL_CreateRenderer(window, -1, renderer_flags) orelse { - return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%s\n", ext.SDL_GetError()); - }; - - defer ext.SDL_DestroyRenderer(renderer); + var previous_ticks = ext.SDL_GetTicks64(); while (true) { - // TODO: Delta timing. - var event = @as(ext.SDL_Event, undefined); + { + var event = @as(ext.SDL_Event, undefined); - while (ext.SDL_PollEvent(&event) != 0) { - switch (event.type) { - ext.SDL_QUIT => return, - else => {}, + while (ext.SDL_PollEvent(&event) != 0) { + switch (event.type) { + ext.SDL_QUIT => return, + else => {}, + } } } - if (ext.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0) != 0) { - return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%s\n", ext.SDL_GetError()); + { + // 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; + } } - if (ext.SDL_RenderClear(renderer) != 0) { - return ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%s\n", ext.SDL_GetError()); - } - - // TODO: Render here. - - ext.SDL_RenderPresent(renderer); + _ = ext.SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + _ = ext.SDL_RenderClear(renderer); + _ = ext.SDL_RenderPresent(renderer); } } } diff --git a/source/runner.zig b/source/runner.zig old mode 100755 new mode 100644 index 215069b..e24965c --- a/source/runner.zig +++ b/source/runner.zig @@ -1,7 +1,5 @@ -const coral = @import("coral"); - const ona = @import("ona"); -pub fn main() !void { - ona.run_app(.{.sandboxed_path = &ona.file.Path.cwd}); +pub fn main() ona.RuntimeError!void { + try ona.run_app(.{.sandboxed_path = &ona.file.Path.cwd}); } diff --git a/source/test.zig b/source/test.zig deleted file mode 100644 index 0d8561f..0000000 --- a/source/test.zig +++ /dev/null @@ -1,3 +0,0 @@ -const _coral = @import("coral"); - -const _ona = @import("ona");