Application Context Implementation #4

Closed
kayomn wants to merge 93 commits from event-loop-dev into main
22 changed files with 2349 additions and 1111 deletions

View File

@ -5,5 +5,6 @@ steps:
- name: build & test - name: build & test
image: euantorano/zig:0.9.1 image: euantorano/zig:0.9.1
commands: commands:
- zig build test - apk --no-cache add build-base sdl2-dev
- $(find zig-cache -name test) main.zig - zig build
- ./zig-out/bin/test main.zig

11
.vscode/launch.json vendored
View File

@ -2,23 +2,26 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Build", "name": "Ona",
"type": "gdb", "type": "gdb",
"request": "launch", "request": "launch",
"target": "${workspaceFolder}/zig-out/bin/ona", "target": "${workspaceFolder}/zig-out/bin/ona",
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}",
"valuesFormatting": "parseText", "valuesFormatting": "parseText",
"preLaunchTask": "Build", "preLaunchTask": "Build Debug",
"internalConsoleOptions": "openOnSessionStart",
}, },
{ {
"name": "Test", "name": "Test",
"type": "gdb", "type": "gdb",
"request": "launch", "request": "launch",
"target": "${workspaceFolder}/zig-cache/o/b57ef32c79a05339fbe4a8eb648ff6df/test", "target": "${workspaceFolder}/zig-out/bin/test",
"arguments": "main.zig", "arguments": "main.zig",
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}",
"valuesFormatting": "parseText", "valuesFormatting": "parseText",
"preLaunchTask": "Build Test", "preLaunchTask": "Build Debug",
"internalConsoleOptions": "openOnSessionStart",
}, },
] ]
} }

View File

@ -14,4 +14,5 @@
"git.detectSubmodulesLimit": 0, "git.detectSubmodulesLimit": 0,
"git.ignoreSubmodules": true, "git.ignoreSubmodules": true,
"debug.onTaskErrors": "showErrors",
} }

64
.vscode/tasks.json vendored
View File

@ -1,60 +1,40 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"problemMatcher": {
"source": "zig",
"owner": "cpptools",
"fileLocation": [
"autoDetect",
"${cwd}",
],
"pattern": {
"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5,
}
},
"tasks": [ "tasks": [
{ {
"label": "Build", "label": "Build Debug",
"type": "shell", "type": "shell",
"command": "zig build", "command": "zig build",
"group": "build",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": { "presentation": {
"echo": true, "echo": true,
"reveal": "always", "reveal": "silent",
"focus": true, "focus": true,
"panel": "shared", "panel": "shared",
"showReuseMessage": true, "showReuseMessage": true,
"clear": true, "clear": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
"problemMatcher": {
"source": "gcc",
"owner": "cpptools",
"fileLocation": [
"autoDetect",
"${cwd}",
],
"pattern": {
"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5,
}
}
},
{
"label": "Test",
"type": "shell",
"command": "$(find zig-cache -name test) src/main.zig",
"group": {
"kind": "test",
"isDefault": true
},
},
{
"label": "Build Test",
"type": "shell",
"command": "zig build test",
"group": "test"
}, },
], ],
} }

View File

@ -1,34 +1,45 @@
const std = @import("std"); const std = @import("std");
///
/// Builds the engine, tools, and dependencies of all.
///
pub fn build(builder: *std.build.Builder) void { pub fn build(builder: *std.build.Builder) void {
const target = builder.standardTargetOptions(.{}); const target = builder.standardTargetOptions(.{});
const mode = builder.standardReleaseOptions(); const mode = builder.standardReleaseOptions();
const core_pkg = projectPkg("core", &.{});
// Ona executable. // Ona executable.
{ {
const ona_exe = builder.addExecutable("ona", "./src/main.zig"); const ona = builder.addExecutable("ona", "./src/ona/main.zig");
ona_exe.setTarget(target); ona.addPackage(core_pkg);
ona_exe.setBuildMode(mode); ona.setTarget(target);
ona_exe.install(); ona.setBuildMode(mode);
ona_exe.addIncludeDir("./ext"); ona.install();
ona_exe.linkSystemLibrary("SDL2"); ona.addIncludeDir("./ext");
ona.linkSystemLibrary("SDL2");
const run_cmd = ona_exe.run(); ona.linkLibC();
run_cmd.step.dependOn(builder.getInstallStep());
if (builder.args) |args| run_cmd.addArgs(args);
builder.step("run", "Run Ona application").dependOn(&run_cmd.step);
} }
// Ona tests. // Tests executable.
{ {
const ona_tests = builder.addTestExe("test", "./src/main.zig"); const tests = builder.addTestExe("test", "./src/tests.zig");
ona_tests.setTarget(target); tests.addPackage(core_pkg);
ona_tests.setBuildMode(mode); tests.setTarget(target);
builder.step("test", "Run Ona unit tests").dependOn(&ona_tests.step); tests.setBuildMode(mode);
tests.install();
} }
} }
///
/// Returns a [std.build.Pkg] within the project codebase path at `name` with `dependencies` as its
/// dependencies.
///
fn projectPkg(comptime name: []const u8, dependencies: []const std.build.Pkg) std.build.Pkg {
return std.build.Pkg{
.name = name,
.path = .{.path = "./src/" ++ name ++ "/main.zig"},
.dependencies = dependencies,
};
}

594
src/core/io.zig Normal file
View File

@ -0,0 +1,594 @@
const math = @import("./math.zig");
const meta = @import("./meta.zig");
const stack = @import("./stack.zig");
const testing = @import("./testing.zig");
///
/// [AccessError.Inacessible] is a generic catch-all for IO resources that are inaccessible for
/// implementation-specific reasons.
///
pub const AccessError = error {
Inaccessible,
};
///
/// [AllocationError.OutOfMemory] if the requested amount of memory could not be allocated.
///
pub const AllocationError = error {
OutOfMemory,
};
///
/// Memory layout description for a memory allocation.
///
pub const AllocationLayout = struct {
length: usize,
alignment: u29 = 8,
};
///
/// Interface for dynamic memory allocation through the state machine of the wrapped allocator
/// implementation.
///
pub const Allocator = struct {
context: *anyopaque,
vtable: *const struct {
alloc: fn (*anyopaque, AllocationLayout) AllocationError![*]u8,
dealloc: fn (*anyopaque, [*]u8) void,
realloc: fn (*anyopaque, [*]u8, AllocationLayout) AllocationError![*]u8,
},
///
/// Attempts to allocate a block of memory from `allocator` according to `layout`, returning it
/// or [AllocationError] if it failed.
///
pub fn alloc(allocator: Allocator, layout: AllocationLayout) AllocationError![*]u8 {
return allocator.vtable.alloc(allocator.context, layout);
}
///
/// Deallocates the block of memory from `allocator` referenced by `allocation`.
///
pub fn dealloc(allocator: Allocator, allocation: [*]u8) void {
allocator.vtable.dealloc(allocator.context, allocation);
}
///
/// Attempts to reallocate the existing block of memory from `allocator` referenced by
/// `allocation` according to `layout`, returning it or [AllocationError] if it failed.
///
pub fn realloc(allocator: Allocator, allocation: [*]u8,
layout: AllocationLayout) AllocationError![*]u8 {
return allocator.vtable.realloc(allocator.context, allocation, layout);
}
///
/// Wraps `implementation`, returning the [Allocator] value.
///
pub fn wrap(implementation: anytype) Allocator {
const Implementation = @TypeOf(implementation.*);
return .{
.context = @ptrCast(*anyopaque, implementation),
.vtable = switch (@typeInfo(Implementation)) {
.Struct => &.{
.alloc = struct {
fn call(context: *anyopaque, layout: AllocationLayout) AllocationError![*]u8 {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).alloc(layout);
}
}.call,
.dealloc = struct {
fn call(context: *anyopaque, allocation: [*]u8) void {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).dealloc(allocation);
}
}.call,
.realloc = struct {
fn call(context: *anyopaque, allocation: [*]u8,
layout: AllocationLayout) AllocationError![*]u8 {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).realloc(allocation, layout);
}
}.call,
},
.Opaque => &.{
.alloc = struct {
fn call(context: *anyopaque, layout: AllocationLayout) AllocationError![*]u8 {
return @ptrCast(*Implementation, context).alloc(layout);
}
}.call,
.dealloc = struct {
fn call(context: *anyopaque, allocation: [*]u8) void {
return @ptrCast(*Implementation, context).dealloc(allocation);
}
}.call,
.realloc = struct {
fn call(context: *anyopaque, allocation: [*]u8,
layout: AllocationLayout) AllocationError![*]u8 {
return @ptrCast(*Implementation, context).realloc(allocation, layout);
}
}.call,
},
else => @compileError(
"`context` must a single-element pointer referencing a struct or opaque type"),
},
};
}
};
///
/// Returns a state machine for lazily computing all `Element` components of a given source input
/// that match a delimiting pattern.
///
pub fn Spliterator(comptime Element: type) type {
return struct {
source: []const Element,
delimiter: []const Element,
const Self = @This();
///
/// Returns `true` if there is more data to be processed, otherwise `false`.
///
pub fn hasNext(self: Self) bool {
return (self.source.len != 0);
}
///
/// Iterates on `self` and returns the next view of [Spliterator.source] that matches
/// [Spliterator.delimiter], or `null` if there is no more data to be processed.
///
pub fn next(self: *Self) ?[]const Element {
if (!self.hasNext()) return null;
kayomn marked this conversation as resolved Outdated

Comment needs clarify as it appears slightly misleading / ambiguous in its current wording.

Comment needs clarify as it appears slightly misleading / ambiguous in its current wording.

On further thought, having the comment here to begin with only adds confusion.

While slices don't have as trivial of a memory layout as other pointer types, they're still language primitives and should reasonably be treated the same as other pointer kinds.

On further thought, having the comment here to begin with only adds confusion. While slices don't have as trivial of a memory layout as other pointer types, they're still language primitives and should reasonably be treated the same as other pointer kinds.
if (self.delimiter.len == 0) {
defer self.source = self.source[self.source.len .. self.source.len];
return self.source[0 .. self.source.len];
}
if (findFirstOf(Element, self.source, self.delimiter, struct {
fn testEquality(this: Element, that: Element) bool {
return this == that;
}
}.testEquality)) |head| {
defer self.source = self.source[(head + self.delimiter.len) .. self.source.len];
return self.source[0 .. head];
}
defer self.source = self.source[self.source.len .. self.source.len];
return self.source;
}
};
}
test "Spliterator(u8)" {
// Empty source.
{
var spliterator = Spliterator(u8){
.source = "",
.delimiter = " ",
};
try testing.expect(!spliterator.hasNext());
}
// Empty delimiter.
{
var spliterator = Spliterator(u8){
.source = "aaa",
.delimiter = "",
};
try testing.expect(spliterator.hasNext());
try testing.expect(equals(u8, spliterator.next().?, "aaa"));
try testing.expect(!spliterator.hasNext());
}
// Single-character delimiter.
{
var spliterator = Spliterator(u8){
.source = "single.character.separated.hello.world",
.delimiter = ".",
};
const components = [_][]const u8{"single",
"character", "separated", "hello", "world"};
var index = @as(usize, 0);
const components_tail = components.len - 1;
while (spliterator.next()) |split| : (index += 1) {
try testing.expect(spliterator.hasNext() == (index < components_tail));
try testing.expect(equals(u8, split, components[index]));
}
try testing.expect(!spliterator.hasNext());
}
// Multi-character delimiter.
{
var spliterator = Spliterator(u8){
.source = "finding a needle in a needle stack",
.delimiter = "needle",
};
const components = [_][]const u8{"finding a ", " in a ", " stack"};
var index = @as(usize, 0);
const components_tail = components.len - 1;
while (spliterator.next()) |split| : (index += 1) {
try testing.expect(spliterator.hasNext() == (index < components_tail));
try testing.expect(equals(u8, split, components[index]));
}
try testing.expect(!spliterator.hasNext());
}
}
///
/// Interface for capturing a reference to a writable resource like block devices, memory buffers,
/// network sockets, and more.
///
pub const Writer = struct {
context: *anyopaque,
vtable: *const struct {
write: fn (*anyopaque, []const u8) AccessError!usize,
},
///
/// Wraps `implementation`, returning the [Writer] value.
///
pub fn wrap(implementation: anytype) Writer {
kayomn marked this conversation as resolved Outdated

Worth mentioning that it performs linear time, making it O(n) time complexity?

Worth mentioning that it performs linear time, making it O(n) time complexity?
const Implementation = @TypeOf(implementation.*);
return .{
.context = @ptrCast(*anyopaque, implementation),
.vtable = switch (@typeInfo(Implementation)) {
.Struct => &.{
.write = struct {
fn call(context: *anyopaque, buffer: []const u8) AccessError!usize {
return @ptrCast(*Implementation,
@alignCast(@alignOf(Implementation), context)).write(buffer);
}
}.call,
},
.Opaque => &.{
.write = struct {
fn call(context: *anyopaque, buffer: []const u8) AccessError!usize {
return @ptrCast(*Implementation, context).write(buffer);
}
}.call,
},
else => @compileError(
"`context` must a single-element pointer referencing a struct or opaque type"),
kayomn marked this conversation as resolved Outdated

Worth mentioning that this uses linear search?

Worth mentioning that this uses linear search?
},
};
}
///
/// Attempts to write to `buffer` to `writer`, returning the number of successfully written or
/// [AccessError] if it failed.
///
pub fn write(writer: Writer, buffer: []const u8) AccessError!usize {
return writer.vtable.write(writer.context, buffer);
}
};
///
/// Returns a sliced reference of the raw bytes in `pointer`.
///
pub fn bytesOf(pointer: anytype) switch (@typeInfo(@TypeOf(pointer))) {
.Pointer => |info| if (info.is_const) []const u8 else []u8,
else => @compileError("`pointer` must be a pointer type"),
} {
const Pointer = @TypeOf(pointer);
const pointer_info = @typeInfo(Pointer).Pointer;
switch (pointer_info.size) {
.Many => @compileError("`pointer` cannot be an unbound pointer type"),
.C => @compileError("`pointer` cannot be a C-style pointer"),
.One => return @ptrCast(if (pointer_info.is_const) [*]const u8
else [*]u8, pointer)[0 .. @sizeOf(Pointer)],
.Slice => return @ptrCast(if (pointer_info.is_const) [*]const u8 else
[*]u8, pointer.ptr)[0 .. (@sizeOf(Pointer) * pointer.len)],
}
}
test "bytesOf" {
var foo: u32 = 10;
try testing.expect(bytesOf(&foo)[0] == 0x0a);
}
///
/// Compares `this` to `that`, returning the difference between the first byte deviation in the two
/// sequences, otherwise `0` if they are identical.
///
pub fn compareBytes(this: []const u8, that: []const u8) isize {
const range = math.min(usize, this.len, that.len);
var index: usize = 0;
while (index < range) : (index += 1) {
const difference = (this[index] - that[index]);
if (difference != 0) return difference;
}
return (@intCast(isize, this.len) - @intCast(isize, that.len));
}
test "compareBytes" {
try testing.expect(compareBytes(&.{69, 42, 0}, &.{69, 42, 0}) == 0);
try testing.expect(compareBytes(&.{69, 42, 11}, &.{69, 42}) == 1);
try testing.expect(compareBytes(&.{69, 42}, &.{69, 42, 11}) == -1);
}
///
/// Copies the contents of `source` into `target`
///
pub fn copy(comptime Element: type, target: []Element, source: []const Element) void {
for (source) |element, index| target[index] = element;
}
test "copy" {
var buffer = [_]u32{0} ** 20;
const data = [_]u32{3, 20, 8000};
copy(u32, &buffer, &data);
for (data) |datum, index| try testing.expect(buffer[index] == datum);
}
///
/// Returns `true` if `this` is the same length and contains the same data as `that`, otherwise
/// `false`.
///
pub fn equals(comptime Element: type, this: []const Element, that: []const Element) 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;
}
test "equals" {
const bytes_sequence = &.{69, 42, 0};
try testing.expect(equals(u8, bytes_sequence, bytes_sequence));
try testing.expect(!equals(u8, bytes_sequence, &.{69, 42}));
}
///
/// Fills the contents of `target` with `source`.
///
pub fn fill(comptime Element: type, target: []Element, source: Element) void {
for (target) |_, index| target[index] = source;
}
test "fill" {
var buffer = [_]u32{0} ** 8;
fill(u32, &buffer, 1);
for (buffer) |element| try testing.expect(element == 1);
}
///
/// Linearly searches for the first instance of an `Element` equal to `needle` in `haystack`,
/// returning its index or `null` if nothing was found.
///
/// **Note** that this operation has `O(n)` time complexity.
///
pub fn findFirst(comptime Element: type, haystack: []const Element,
needle: Element, comptime testEquality: fn (Element, Element) bool) ?usize {
for (haystack) |element, index| if (testEquality(element, needle)) return index;
return null;
}
test "findFirst" {
const haystack = &.{"", "", "foo"};
const testEquality = struct {
fn testEquality(this: []const u8, that: []const u8) bool {
return equals(u8, this, that);
}
}.testEquality;
try testing.expect(findFirst([]const u8, haystack, "foo", testEquality).? == 2);
try testing.expect(findFirst([]const u8, haystack, "bar", testEquality) == null);
}
///
/// Searches for the first instance of an `Element` sequence equal to the contents of `needle` in
/// `haystack`, returning the starting index or `null` if nothing was found.
///
/// **Note** that this operation has `O(nm)` time complexity.
///
pub fn findFirstOf(comptime Element: type, haystack: []const Element,
needle: []const Element, comptime testEquality: fn (Element, Element) bool) ?usize {
var head: usize = 0;
const tail = (haystack.len - needle.len);
walk_haystack: while (head <= tail) : (head += 1) {
for (needle) |element, index|
if (!testEquality(haystack[head + index], element)) continue: walk_haystack;
return head;
}
return null;
}
test "findFirstOf" {
const haystack = &.{"foo", "bar", "baz"};
const testEquality = struct {
fn testEquality(this: []const u8, that: []const u8) bool {
return equals(u8, this, that);
}
}.testEquality;
try testing.expect(findFirstOf([]const u8, haystack, &.{"bar", "baz"}, testEquality).? == 1);
try testing.expect(findFirstOf([]const u8, haystack, &.{"baz", "bar"}, testEquality) == null);
}
///
/// Frees `allocated_memory` using `allocator`.
///
/// *Note* that only memory known to be freeable by `allocator` should be passed via
/// `allocated_memory`. Anything else will result is considered unreachable logic.
///
pub fn free(allocator: Allocator, allocated_memory: anytype) void {
allocator.dealloc(@ptrCast([*]u8, switch (@typeInfo(@TypeOf(allocated_memory))) {
.Pointer => |info| switch (info.size) {
.One, .Many, .C => allocated_memory,
.Slice => allocated_memory.ptr,
},
else => @compileError("`allocated_memory` must be a pointer"),
}));
}
test "free" {
var buffer = [_]u8{0} ** 4096;
var memory = stack.Fixed(u8){.buffer = &buffer};
const fixed_allocator = stack.fixedAllocator(&memory);
const block_size = 8;
const allocated_block = (try makeMany(u8, fixed_allocator, block_size))[0 .. block_size];
defer free(fixed_allocator, allocated_block);
}
///
/// Returns a deterministic hash code compiled from each byte in `bytes`.
///
/// **Note** that this operation has `O(n)` time complexity.
///
pub fn hashBytes(bytes: []const u8) usize {
var hash = @as(usize, 5381);
for (bytes) |byte| hash = ((hash << 5) + hash) + byte;
return hash;
}
test "hashBytes" {
const bytes_sequence = &.{69, 42, 0};
try testing.expect(hashBytes(bytes_sequence) == hashBytes(bytes_sequence));
try testing.expect(hashBytes(bytes_sequence) != hashBytes(&.{69, 42}));
}
///
/// Attempts to allocate a buffer of `size` `Element`s using `allocator`, returning it or a
/// [MakeError] if it failed.
///
pub fn makeMany(comptime Element: type, allocator: Allocator, size: usize) AllocationError![*]Element {
const alignment = @alignOf(Element);
return @ptrCast([*]Element, @alignCast(alignment, try allocator.alloc(.{
.length = @sizeOf(Element) * size,
.alignment = alignment,
})));
}
test "makeMany" {
var buffer = [_]u8{0} ** 4096;
var memory = stack.Fixed(u8){.buffer = &buffer};
const block_size = 8;
// Don't care about the actual allocation - just assertions about it.
_ = (try makeMany(u8, stack.fixedAllocator(&memory), block_size))[0 .. block_size];
}
///
/// Attempts to allocate a buffer of `1` `Element` using `allocator`, returning it or a [MakeError]
/// if it failed.
///
pub fn makeOne(comptime Element: type, allocator: Allocator) AllocationError!*Element {
const alignment = @alignOf(Element);
return @ptrCast(*Element, @alignCast(alignment, try allocator.alloc(.{
.length = @sizeOf(Element),
.alignment = alignment,
})));
}
test "makeOne" {
var buffer = [_]u8{0} ** 4096;
var memory = stack.Fixed(u8){.buffer = &buffer};
// Don't care about the actual allocation - just assertions about it.
_ = try makeOne(u8, stack.fixedAllocator(&memory));
}
///
/// Swaps the `Data` in `this` with `that`.
///
pub fn swap(comptime Data: type, this: *Data, that: *Data) void {
const temp = this.*;
this.* = that.*;
that.* = temp;
}
test "swap" {
var a: u64 = 0;
var b: u64 = 1;
swap(u64, &a, &b);
try testing.expect(a == 1);
try testing.expect(b == 0);
}
///
/// Mandatory context variable used by [null_writer].
///
const null_context: u64 = 0;
///
/// Thread-safe and lock-free [Writer] that silently consumes all given data without failure and
/// throws it away.
///
/// This is commonly used for testing or redirected otherwise unwanted output data that has to be
/// sent somewhere for whatever reason.
///
pub const null_writer = Writer.wrap(@ptrCast(*const opaque {
const Self = @This();
fn write(_: Self, buffer: []const u8) usize {
return buffer.len;
}
}, &null_context));
test "null_writer" {
const sequence = "foo";
try testing.expect(null_writer.call(sequence) == sequence.len);
}

45
src/core/main.zig Normal file
View File

@ -0,0 +1,45 @@
///
/// Platform-agnostic input and output interfaces for working with memory, files, and networks.
///
pub const io = @import("./io.zig");
///
/// Math types and functions with a focus on graphics-specific linear algebra.
///
pub const math = @import("./math.zig");
///
/// Metaprogramming introspection and generation.
///
pub const meta = @import("./meta.zig");
///
/// Sequential, last-in first-out data structures.
///
pub const stack = @import("./stack.zig");
///
/// Unordered key-value association data structures.
///
pub const table = @import("./table.zig");
///
/// Unit testing suite utilities.
///
pub const testing = @import("./testing.zig");
///
/// Unicode-encoded string analysis and processing with a focus on UTF-8 encoded text.
///
pub const unicode = @import("./unicode.zig");
test {
_ = io;
_ = math;
_ = meta;
_ = stack;
_ = table;
_ = testing;
_ = unicode;
}

61
src/core/math.zig Normal file
View File

@ -0,0 +1,61 @@
const std = @import("std");
const testing = @import("./testing.zig");
// TODO: Remove stdlib dependency.
kayomn marked this conversation as resolved Outdated

Warrants TODO comment mentioning that this stdlib dependency should be removed in future.

Warrants `TODO` comment mentioning that this stdlib dependency should be removed in future.
pub const IntFittingRange = std.math.IntFittingRange;
///
/// Returns the highest integer value representable by `Integer`.
///
pub fn maxIntValue(comptime Integer: type) comptime_int {
return switch (@typeInfo(Integer)) {
.Int => |info| if (info.bits == 0) 0 else
((1 << (info.bits - @boolToInt(info.signedness == .signed))) - 1),
else => @compileError("`" ++ @typeName(Integer) ++ "` must be an int"),
};
}
test "maxIntValue" {
try testing.expect(maxIntValue(u8) == 255);
try testing.expect(maxIntValue(i8) == 127);
try testing.expect(maxIntValue(u16) == 65535);
try testing.expect(maxIntValue(i16) == 32767);
}
///
/// Returns the highest `Number` value between `this` and `that`.
///
pub fn max(comptime Number: type, this: Number, that: Number) Number {
return switch (@typeInfo(Number)) {
.Int, .Float, .ComptimeInt, .ComptimeFloat => if (this > that) this else that,
else => @compileError("`" ++ @typeName(Number) ++
"` must be an int, float, comptime_int, or comptime_float"),
};
}
test "max" {
try testing.expect(max(f32, 0.1, 1.0) == 1.0);
try testing.expect(max(f64, 1.0, 1.01) == 1.01);
try testing.expect(max(u32, 35615, 2873) == 35615);
}
///
/// Returns the lowest `Number` value between `this` and `that`.
///
pub fn min(comptime Number: type, this: Number, that: Number) Number {
return switch (@typeInfo(Number)) {
.Int, .Float, .ComptimeInt, .ComptimeFloat => if (this < that) this else that,
else => @compileError("`" ++ @typeName(Number) ++
"` must be an int, float, comptime_int, or comptime_float"),
};
}
test "min" {
try testing.expect(min(f32, 0.1, 1.0) == 0.1);
try testing.expect(min(f64, 1.0, 1.01) == 1.0);
try testing.expect(min(u32, 35615, 2873) == 2873);
}

10
src/core/meta.zig Normal file
View File

@ -0,0 +1,10 @@
///
/// Returns the return type of the function type `Fn`.
///
pub fn FnReturn(comptime Fn: type) type {
const type_info = @typeInfo(Fn);
if (type_info != .Fn) @compileError("`Fn` must be a function type");
return type_info.Fn.return_type orelse void;
}

286
src/core/stack.zig Executable file
View File

@ -0,0 +1,286 @@
const io = @import("./io.zig");
const testing = @import("./testing.zig");
///
/// Returns a fixed-size stack type of `Element`s.
///
pub fn Fixed(comptime Element: type) type {
return struct {
filled: usize = 0,
buffer: []Element,
///
/// Stack type.
///
const Self = @This();
///
/// Resets the number of filled items to `0`, otherwise leaving the actual memory contents
/// of the buffer untouched until it is later overwritten by following operations on it.
///
pub fn clear(self: *Self) void {
self.filled = 0;
}
///
/// Returns `true` if `self` has filled its buffer to maximum capacity, otherwise `false`.
///
pub fn isFull(self: Self) bool {
return (self.filled == self.buffer.len);
}
///
/// If `self` is filled with at least `1` value, it is decremented by `1`, otherwise leaving
/// the actual memory contents of the buffer untouched until it is later overwritten by
/// following operations on it.
///
/// The value of the element removed from the list is returned if something existed to be
/// popped, otherwise `null` if it contained no elements.
///
pub fn pop(self: *Self) ?Element {
if (self.filled == 0) return null;
self.filled -= 1;
return self.buffer[self.filled];
}
///
/// Attempts to push `element` into `self`, returning a [FixedPushError] if it failed.
///
pub fn push(self: *Self, element: Element) FixedPushError!void {
if (self.isFull()) return error.BufferOverflow;
self.buffer[self.filled] = element;
self.filled += 1;
}
///
/// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it
/// failed.
///
pub fn pushAll(self: *Self, elements: []const Element) FixedPushError!void {
const filled = (self.filled + elements.len);
if (filled > self.buffer.len) return error.BufferOverflow;
io.copy(Element, self.buffer[self.filled ..], elements);
self.filled = filled;
}
///
/// Attempts to push `count` instances of `element` into `self`, returning a
/// [FixedPushError] if it failed.
///
pub fn pushMany(self: *Self, element: Element, count: usize) FixedPushError!void {
const filled = (self.filled + count);
if (filled > self.buffer.len) return error.BufferOverflow;
io.fill(Element, self.buffer[self.filled ..], element);
self.filled = filled;
}
};
}
test "Fixed([]const u8)" {
const default_value = "";
var buffer = [_][]const u8{default_value} ** 4;
var shopping_list = Fixed([]const u8){.buffer = &buffer};
// Pop empty stack.
{
try testing.expect(shopping_list.pop() == null);
try testing.expect(shopping_list.filled == 0);
try testing.expect(shopping_list.buffer.ptr == &buffer);
try testing.expect(shopping_list.buffer.len == buffer.len);
for (shopping_list.buffer) |item|
try testing.expect(io.equals(u8, item, default_value));
}
// Push single element.
{
try shopping_list.push("milk");
try testing.expect(shopping_list.filled == 1);
try testing.expect(shopping_list.buffer.ptr == &buffer);
try testing.expect(shopping_list.buffer.len == buffer.len);
try testing.expect(io.equals(u8, shopping_list.buffer[0], "milk"));
for (shopping_list.buffer[1 ..]) |item|
try testing.expect(io.equals(u8, item, default_value));
// TODO: Test stack overflow.
}
// Pop single element.
{
try testing.expect(io.equals(u8, shopping_list.pop().?, "milk"));
try testing.expect(shopping_list.filled == 0);
try testing.expect(shopping_list.buffer.ptr == &buffer);
try testing.expect(shopping_list.buffer.len == buffer.len);
try testing.expect(io.equals(u8, shopping_list.buffer[0], "milk"));
for (shopping_list.buffer[1 ..]) |item|
try testing.expect(io.equals(u8, item, default_value));
}
// TODO: Multiple elements.
// TODO: Clear elements.
}
///
/// Potential errors that may occur while trying to push one or more elements into a [Fixed] stack.
///
kayomn marked this conversation as resolved Outdated

Seeing as FixedStack does not depend on any kind of dynamic allocation directly, does it make sense to make PushError a direct alias of io.MakeError?

Would it make more sense to use BufferOverflow instead of OutOfMemory?

Seeing as `FixedStack` does not depend on any kind of dynamic allocation directly, does it make sense to make `PushError` a direct alias of `io.MakeError`? Would it make more sense to use `BufferOverflow` instead of `OutOfMemory`?
pub const FixedPushError = error {
BufferOverflow,
};
///
/// Creates and returns a [io.Allocator] value wrapping `fixed_stack`.
///
/// The returned [io.Allocator] uses `fixed_stack` and its backing memory buffer as a fixed-length
/// memory pool to linearly allocate memory from.
///
pub fn fixedAllocator(fixed_stack: *Fixed(u8)) io.Allocator {
const FixedStack = @TypeOf(fixed_stack.*);
return io.Allocator.wrap(@ptrCast(*opaque {
const Self = @This();
pub fn alloc(self: *Self, layout: io.AllocationLayout) io.AllocationError![*]u8 {
// TODO: Remove stdlib dependency.
const stack = self.stackCast();
const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr +
stack.filled, layout.alignment) orelse return error.OutOfMemory;
kayomn marked this conversation as resolved Outdated

Reallocation could benefit from the same kind of last-alloc check optimization as deallocation.

May be worth clarifying that in the comment and code structure.

Reallocation could benefit from the same kind of last-alloc check optimization as deallocation. May be worth clarifying that in the comment and code structure.
const head = stack.filled + adjusted_offset;
const tail = head + layout.length;
stack.pushMany(0, tail) catch return error.OutOfMemory;
return stack.buffer[head .. tail].ptr;
}
pub fn dealloc(self: *Self, allocation: [*]u8) void {
// Deallocate the memory.
const stack = self.stackCast();
const allocation_address = @ptrToInt(allocation);
const stack_address = @ptrToInt(stack.buffer.ptr);
// Check the buffer is within the address space of the stack buffer. If not, it cannot
// be freed.
if (allocation_address < stack_address or allocation_address >=
(stack_address + stack.filled)) unreachable;
// TODO: Investigate ways of actually freeing if it is the last allocation.
}
pub fn realloc(self: *Self, allocation: [*]u8,
layout: io.AllocationLayout) io.AllocationError![*]u8 {
// TODO: Investigate ways of in-place relocating if it is the last allocation.
// TODO: Remove stdlib dependency.
const stack = self.stackCast();
const allocation_address = @ptrToInt(allocation);
const stack_address = @ptrToInt(stack.buffer.ptr);
// Check the buffer is within the address space of the stack buffer. If not, it cannot
// be reallocated.
if (allocation_address < stack_address or allocation_address >=
(stack_address + stack.filled)) unreachable;
const adjusted_offset = @import("std").mem.alignPointerOffset(stack.buffer.ptr +
stack.filled, layout.alignment) orelse return error.OutOfMemory;
const head = stack.filled + adjusted_offset;
const tail = head + layout.length;
stack.pushMany(0, tail) catch return error.OutOfMemory;
return stack.buffer[head .. tail].ptr;
}
fn stackCast(self: *Self) *Fixed(u8) {
return @ptrCast(*FixedStack, @alignCast(@alignOf(FixedStack), self));
}
}, fixed_stack));
}
test "fixedAllocator" {
var buffer = [_]u8{0} ** 32;
var stack = Fixed(u8){.buffer = &buffer};
const allocator = fixedAllocator(&stack);
// Allocation
var block_memory = try allocator.alloc(.{
.alignment = @alignOf(u64),
.length = @sizeOf(u64),
});
const buffer_address_head = @ptrToInt(&buffer);
const buffer_address_tail = @ptrToInt(&buffer) + buffer.len;
{
const block_memory_address = @ptrToInt(block_memory);
try testing.expect(block_memory_address >= buffer_address_head and
block_memory_address < buffer_address_tail);
}
// Reallocation.
block_memory = try allocator.realloc(block_memory, .{
.alignment = @alignOf(u64),
.length = @sizeOf(u64),
});
{
const block_memory_address = @ptrToInt(block_memory);
try testing.expect(block_memory_address >= buffer_address_head and
block_memory_address < buffer_address_tail);
}
// Deallocation.
allocator.dealloc(block_memory);
}
///
/// Returns an [io.Writer] wrapping `fixed_stack`.
///
/// Writing to the returned [io.Writer] will push values to the underlying [Fixed] stack instance
/// referenced by `fixed_stack` until it is full.
///
pub fn fixedWriter(fixed_stack: *Fixed(u8)) io.Writer {
const FixedStack = @TypeOf(fixed_stack.*);
return io.Writer.wrap(@ptrCast(*opaque {
const Self = @This();
fn stackCast(self: *Self) *Fixed(u8) {
return @ptrCast(*FixedStack, @alignCast(@alignOf(FixedStack), self));
}
pub fn write(self: *Self, buffer: []const u8) io.AccessError!usize {
self.stackCast().pushAll(buffer) catch |err| switch (err) {
error.BufferOverflow => return 0,
};
return buffer.len;
}
}, fixed_stack));
}
test "fixedWriter" {
var buffer = [_]u8{0} ** 4;
var sequence_stack = Fixed(u8){.buffer = &buffer};
const sequence_data = [_]u8{8, 16, 32, 64};
try testing.expect((try fixedWriter(&sequence_stack).
write(&sequence_data)) == sequence_data.len);
try testing.expect(io.equals(u8, sequence_stack.buffer, &sequence_data));
}

221
src/core/table.zig Normal file
View File

@ -0,0 +1,221 @@
const io = @import("./io.zig");
const stack = @import("./stack.zig");
const testing = @import("./testing.zig");
///
/// Returns a hash-backed table type of `Value`s indexed by `Key` and using `key_context` as the key
/// context.
///
pub fn Hashed(comptime Key: type, comptime Value: type,
comptime key_context: KeyContext(Key)) type {
const Allocator = io.Allocator;
return struct {
allocator: Allocator,
load_limit: f32,
buckets: []Bucket,
filled: usize,
///
/// A slot in the hash table.
///
const Bucket = struct {
maybe_entry: ?struct {
key: Key,
value: Value,
} = null,
maybe_next_index: ?usize = null,
};
///
/// Errors that may occur during initialization of a hash table.
///
pub const InitError = io.AllocationError;
///
/// Hash table type.
///
const Self = @This();
///
/// Deinitializes `self`, preventing any further use.
///
pub fn deinit(self: *Self) void {
io.free(self.allocator, self.buckets);
self.buckets = &.{};
}
///
/// Initializes a [Self] using `allocator` as the memory allocation strategy.
///
/// Returns a new [Self] value or an [InitError] if initializing failed.
///
pub fn init(allocator: Allocator) InitError!Self {
const capacity = 4;
return Self{
.buckets = (try io.makeMany(Bucket, allocator, capacity))[0 .. capacity],
.filled = 0,
.allocator = allocator,
.load_limit = 0.75,
};
}
///
/// Searches for `key` and deletes it from `self.
///
/// The removed value is returned or `null` if no key matching `key` was found.
///
pub fn remove(self: *Self, key: Key) ?Value {
var bucket = &(self.buckets[@mod(key_context.hash(key), self.buckets.len)]);
if (bucket.maybe_entry) |*entry| if (key_context.equals(entry.key, key)) {
defer {
bucket.maybe_entry = null;
self.filled -= 1;
}
return entry.value;
};
while (bucket.maybe_next_index) |index| {
bucket = &(self.buckets[index]);
if (bucket.maybe_entry) |*entry| if (key_context.equals(entry.key, key)) {
defer {
bucket.maybe_entry = null;
self.filled -= 1;
}
return entry.value;
};
}
return null;
}
///
/// Attempts to insert the value at `key` to be `value` in `self`, returning an
/// [InsertError] if it fails.
///
pub fn insert(self: *Self, key: Key, value: Value) InsertError!void {
if (self.isOverloaded()) {
const old_buckets = self.buckets;
defer io.free(self.allocator, old_buckets);
const bucket_count = old_buckets.len * 2;
self.buckets = (try io.makeMany(Bucket, self.allocator,
bucket_count))[0 .. bucket_count];
for (old_buckets) |bucket, index| self.buckets[index] = bucket;
}
var hash = @mod(key_context.hash(key), self.buckets.len);
while (true) {
const bucket = &(self.buckets[hash]);
if (key_context.equals((bucket.maybe_entry orelse {
bucket.maybe_entry = .{
.key = key,
.value = value
};
self.filled += 1;
break;
}).key, key)) return error.KeyExists;
hash = @mod(hash + 1, self.buckets.len);
}
}
kayomn marked this conversation as resolved Outdated

Since this function is only ever used to check if the load factor is at / beyond its maximum, would it make more sense to replace it with isMaxLoad(Self) bool or something like that which meets the requirements of it uses more precisely?

Since this function is only ever used to check if the load factor is at / beyond its maximum, would it make more sense to replace it with `isMaxLoad(Self) bool` or something like that which meets the requirements of it uses more precisely?
///
/// Returns `true` if the current load factor, derived from the number of elements filling
/// the bucket table, is greater than the current load limit.
///
pub fn isOverloaded(self: Self) bool {
return (@intToFloat(f32, self.filled) /
@intToFloat(f32, self.buckets.len)) >= self.load_limit;
kayomn marked this conversation as resolved Outdated

Comment typo: if any key instead of if an key.

Comment typo: `if any key` instead of `if an key`.
}
///
/// Searches for a value indexed with `key` in `self`.
///
/// The found value is returned or `null` if any key matching `key` failed to be found.
///
pub fn lookup(self: Self, key: Key) ?Value {
var bucket = &(self.buckets[@mod(key_context.hash(key), self.buckets.len)]);
if (bucket.maybe_entry) |entry|
if (key_context.equals(entry.key, key)) return entry.value;
while (bucket.maybe_next_index) |index| {
bucket = &(self.buckets[index]);
if (bucket.maybe_entry) |entry|
if (key_context.equals(entry.key, key)) return entry.value;
}
return null;
}
};
}
///
/// [InsertError.KeyExists] occurs when an insertion was attempted on a table with a matching key
/// already present.
///
pub const InsertError = io.AllocationError || error {
KeyExists,
};
///
/// Returns a context type for handling `Key` as a key in a table, associating hashing and equality
/// behaviors to it.
///
pub fn KeyContext(comptime Key: type) type {
return struct {
hash: fn (Key) usize,
equals: fn (Key, Key) bool,
};
}
///
/// A [KeyContext] for dealing with string literal (i.e. []const u8) values.
///
/// **Note** that, while lightweight, this context should only be considered safe to use with string
/// literals or variables pointing to string literals - as the [KeyContext] does not take ownership
/// of its keys beyond copying the reference.
///
pub const string_literal_context = KeyContext([]const u8){
.hash = io.hashBytes,
.equals = struct {
fn stringsEqual(this: []const u8, that: []const u8) bool {
return io.equals(u8, this, that);
}
}.stringsEqual,
};
test "Hashed([]const u8, u32, string_literal_context)" {
var buffer = [_]u8{0} ** 4096;
var memory = stack.Fixed(u8){.buffer = &buffer};
var table = try Hashed([]const u8, u32, string_literal_context).
init(stack.fixedAllocator(&memory));
defer table.deinit();
const foo = 69;
try testing.expect(table.remove("foo") == null);
try table.insert("foo", foo);
try testing.expect(table.remove("foo").? == foo);
try testing.expect(table.remove("foo") == null);
}

22
src/core/testing.zig Normal file
View File

@ -0,0 +1,22 @@
///
/// [TestError.UnexpectedResult] occurs when a conditional that should have been `true` was actually
/// `false`.
///
pub const TestError = error {
UnexpectedResult,
};
///
/// Returns a [TestError] if `ok` is false.
///
pub fn expect(ok: bool) TestError!void {
if (!ok) return error.UnexpectedResult;
}
test "expect" {
try expect(true);
expect(false) catch {};
}
pub const expectError = @import("std").testing.expectError;

119
src/core/unicode.zig Normal file
View File

@ -0,0 +1,119 @@
const io = @import("./io.zig");
const math = @import("./math.zig");
const stack = @import("./stack.zig");
const testing = @import("./testing.zig");
///
/// [PrintError.WriteFailure] occurs when the underlying [io.Writer] implementation failed to write
/// the entirety of a the requested print operation.
///
pub const PrintError = io.AccessError || error {
WriteFailure,
};
///
/// Named identifiers for number formats used in printing functions.
///
pub const Radix = enum {
binary,
tinary,
quaternary,
quinary,
senary,
septenary,
octal,
nonary,
decimal,
undecimal,
duodecimal,
tridecimal,
tetradecimal,
pentadecimal,
hexadecimal,
///
/// Returns the base number of `radix`.
///
pub fn base(radix: Radix) u8 {
return switch (radix) {
.binary => 2,
.tinary => 3,
.quaternary => 4,
.quinary => 5,
.senary => 6,
.septenary => 7,
.octal => 8,
.nonary => 9,
.decimal => 10,
.undecimal => 11,
.duodecimal => 12,
.tridecimal => 13,
.tetradecimal => 14,
.pentadecimal => 15,
.hexadecimal => 16,
};
}
};
///
/// Writes `value` as a ASCII / UTF-8 encoded integer to `writer`, returning `true` if the full
/// sequence was successfully written, otherwise `false`.
///
/// The `radix` argument identifies which base system to format `value` as.
///
pub fn printInt(writer: io.Writer, radix: Radix, value: anytype) PrintError!void {
const Int = @TypeOf(value);
switch (@typeInfo(Int)) {
.Int => |info| {
if (value == 0) {
const zero = "0";
if ((try writer.write(zero)) != zero.len) return error.WriteFailure;
} else {
// Big enough to hold the hexadecimal representation of the integer type, which is
// the largest number format accomodated for in [Radix].
var buffer = [_]u8{0} ** (@sizeOf(Int) * (@bitSizeOf(u8) / 4));
var buffer_count: usize = 0;
var n1 = value;
if (info.signedness == .signed and value < 0) {
// Negative value.
n1 = -value;
buffer[0] = '-';
buffer_count += 1;
}
while (n1 != 0) {
const base = radix.base();
buffer[buffer_count] = @intCast(u8, (n1 % base) + '0');
n1 = (n1 / base);
buffer_count += 1;
}
for (buffer[0 .. (buffer_count / 2)]) |_, i|
io.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]);
if ((try writer.write(buffer[0 .. buffer_count])) != buffer_count)
return error.WriteFailure;
}
},
// Cast comptime int into known-size integer and try again.
.ComptimeInt => return printInt(writer, radix,
@intCast(math.IntFittingRange(value, value), value)),
else => @compileError("`value` must be of type int or comptime_int"),
}
}
test "printInt" {
// Max digits to represent a decimal u8 is 3 (i.e. 127 / 255).
var decimal_buffer = [_]u8{0} ** 3;
var decimal_stack = stack.Fixed(u8){.buffer = &decimal_buffer};
var decimal_writer = stack.fixedWriter(&decimal_stack);
try printInt(decimal_writer, .decimal, 365);
try testing.expect(decimal_stack.isFull());
}

View File

@ -1,148 +0,0 @@
const stack = @import("./stack.zig");
const std = @import("std");
///
/// Opaque interface to a "writable" resource, such as a block device, memory buffer, or network
/// socket.
///
pub const Writer = struct {
context: *anyopaque,
writeContext: fn (*anyopaque, []const u8) usize,
///
/// Radices supported by [writeInt].
///
pub const Radix = enum {
binary,
tinary,
quaternary,
quinary,
senary,
septenary,
octal,
nonary,
decimal,
undecimal,
duodecimal,
tridecimal,
tetradecimal,
pentadecimal,
hexadecimal,
};
///
/// Wraps and returns a reference to `write_context` of type `WriteContext` and its associated
/// `writeContext` writing operation in a [Writer].
///
pub fn wrap(
comptime WriteContext: type,
write_context: *WriteContext,
comptime writeContext: fn (*WriteContext, []const u8) usize
) Writer {
return .{
.context = write_context,
.writeContext = struct {
fn write(context: *anyopaque, buffer: []const u8) usize {
return writeContext(@ptrCast(*WriteContext,
@alignCast(@alignOf(WriteContext), context)), buffer);
}
}.write,
};
}
///
/// Attempts to write `buffer` to `writer`, returning the number of bytes from `buffer` that
/// were successfully written.
///
pub fn write(writer: Writer, buffer: []const u8) usize {
return writer.writeContext(writer.context, buffer);
}
///
/// Writes the singular `byte` to `writer`, returning `true` if it was successfully written,
/// otherwise `false`.
///
pub fn writeByte(writer: Writer, byte: u8) bool {
return (writer.writeContext(writer.context,
@ptrCast([*]const u8, &byte)[0 .. 1]) != 0);
}
///
/// Writes `value` as a ASCII / UTF-8 encoded integer to `writer`, returning `true` if the full
/// sequence was successfully written, otherwise `false`.
///
/// The `radix` argument identifies which base system to encode `value` as, with `10` being
/// decimal, `16` being hexadecimal, `8` being octal`, so on and so forth.
///
pub fn writeInt(writer: Writer, radix: Radix, value: anytype) bool {
const Int = @TypeOf(value);
const type_info = @typeInfo(Int);
switch (type_info) {
.Int => {
if (value == 0) return writer.writeByte('0');
// TODO: Unhardcode this as it will break with large ints.
var buffer = std.mem.zeroes([28]u8);
var buffer_count = @as(usize, 0);
var n1 = value;
if ((type_info.Int.signedness == .signed) and (value < 0)) {
// Negative value.
n1 = -value;
buffer[0] = '-';
buffer_count += 1;
}
while (n1 != 0) {
const base = @enumToInt(radix);
buffer[buffer_count] = @intCast(u8, (n1 % base) + '0');
n1 = (n1 / base);
buffer_count += 1;
}
for (buffer[0 .. (buffer_count / 2)]) |_, i|
std.mem.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]);
return (writer.write(buffer[0 .. buffer_count]) == buffer_count);
},
// Cast comptime int into known-size integer and try again.
.ComptimeInt => return writer.
writeInt(radix, @intCast(std.math.IntFittingRange(value, value), value)),
else => @compileError("value must be of type int"),
}
}
};
///
/// Writer that silently throws consumed data away and never fails.
///
/// This is commonly used for testing or redirected otherwise unwanted output data that can't not be
/// sent somewhere for whatever reason.
///
pub const null_writer = Writer{
.context = undefined,
.writeContext = struct {
fn write(_: *anyopaque, buffer: []const u8) usize {
return buffer.len;
}
}.write,
};
test {
const testing = std.testing;
{
const sequence = "foo";
try testing.expectEqual(null_writer.write(sequence), sequence.len);
}
try testing.expect(null_writer.writeByte(0));
try testing.expect(null_writer.writeInt(.decimal, 420));
}

View File

@ -1,50 +0,0 @@
const ext = @cImport({
@cInclude("SDL2/SDL.h");
});
const io = @import("./io.zig");
const stack = @import("./stack.zig");
const std = @import("std");
const sys = @import("./sys.zig");
///
/// Entry point.
///
pub fn main() anyerror!void {
return sys.runGraphics(anyerror, run);
}
test {
_ = io;
_ = stack;
_ = std;
_ = sys;
}
fn run(event_loop: *sys.EventLoop, graphics: *sys.GraphicsContext) anyerror!void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
{
const file_access = try event_loop.open(.readonly,
try sys.FileSystem.data.joinedPath(&.{"data", "ona.lua"}));
defer event_loop.close(file_access);
const file_size = try file_access.size(event_loop);
const allocator = gpa.allocator();
const buffer = try allocator.alloc(u8, file_size);
defer allocator.free(buffer);
if ((try event_loop.readFile(file_access, buffer)) != file_size)
return error.ScriptLoadFailure;
event_loop.log(.debug, buffer);
}
while (graphics.poll()) |_| {
graphics.present();
}
}

View File

@ -1,87 +0,0 @@
const std = @import("std");
///
/// State machine for lazily computing all components of [Spliterator.source] that match the pattern
/// in [Spliterator.delimiter].
///
pub fn Spliterator(comptime Element: type) type {
return struct {
source: []const Element,
delimiter: []const Element,
const Self = @This();
///
/// Returns `true` if there is more data to be processed, otherwise `false`.
///
pub fn hasNext(self: Self) bool {
return (self.source.len != 0);
}
///
/// Iterates on `self` and returns the next view of [Spliterator.source] that matches
/// [Spliterator.delimiter], or `null` if there is no more data to be processed.
///
pub fn next(self: *Self) ?[]const Element {
if (!self.hasNext()) return null;
if (std.mem.indexOfPos(Element, self.source, 0, self.delimiter)) |index| {
defer self.source = self.source[(index + self.delimiter.len) .. self.source.len];
return self.source[0 .. index];
}
defer self.source = self.source[self.source.len .. self.source.len];
return self.source;
}
};
}
test {
const testing = std.testing;
// Single-character delimiter.
{
var spliterator = Spliterator(u8){
.source = "single.character.separated.hello.world",
.delimiter = ".",
};
const components = [_][]const u8{"single", "character", "separated", "hello", "world"};
var index = @as(usize, 0);
while (spliterator.next()) |split| : (index += 1) {
try testing.expect(std.mem.eql(u8, split, components[index]));
}
}
// Multi-character delimiter.
{
var spliterator = Spliterator(u8){
.source = "finding a needle in a needle stack",
.delimiter = "needle",
};
const components = [_][]const u8{"finding a ", " in a ", " stack"};
var index = @as(usize, 0);
while (spliterator.next()) |split| : (index += 1) {
try testing.expect(std.mem.eql(u8, split, components[index]));
}
}
}
///
/// Searches the slice of `Data` referenced by `data` for the first instance of `sought_datum`,
/// returning its index or `null` if it could not be found.
///
pub fn findFirst(comptime Data: type, data: []const Data, sought_datum: Data) ?usize {
for (data) |datum, index| if (datum == sought_datum) return index;
return null;
}
test {
try std.testing.expectEqual(findFirst(u8, "1234567890", '7'), 6);
}

40
src/ona/main.zig Normal file
View File

@ -0,0 +1,40 @@
const core = @import("core");
const std = @import("std");
const sys = @import("./sys.zig");
///
/// Application entry-point.
///
pub fn main() anyerror!void {
return nosuspend await async sys.display(anyerror, runEngine);
}
///
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
/// Runs the game engine.
///
fn runEngine(app: *sys.App, graphics: *sys.Graphics) anyerror!void {
{
const path = try sys.Path.from(&.{"ona.lua"});
var file_reader = try app.data.openFileReader(path);
defer file_reader.close();
const file_size = (try app.data.query(path)).length;
const allocator = sys.threadSafeAllocator();
const buffer = (try core.io.makeMany(u8, allocator, file_size))[0 .. file_size];
defer core.io.free(allocator, buffer);
if ((try file_reader.read(buffer)) != file_size) return error.ScriptLoadFailure;
app.log(.debug, buffer);
}
while (graphics.poll()) |_| {
graphics.present();
}
}
test {
_ = sys;
}

103
src/ona/oar.zig Normal file
View File

@ -0,0 +1,103 @@
const core = @import("core");
const sys = @import("./sys.zig");
///
/// Metadata of an Oar archive file entry.
///
const Entry = extern struct {
signature: [signature_magic.len]u8 = signature_magic,
path: sys.Path = sys.Path.empty,
data_offset: u64 = 0,
data_length: u64 = 0,
padding: [232]u8 = [_]u8{0} ** 232,
comptime {
const entry_size = @sizeOf(@This());
if (entry_size != 512) @compileError("EntryBlock is not 512 bytes");
}
};
///
/// [FindError.ArchiveUnsupported] occurs when trying to read a file that does not follow an Oar
/// archive format considered valid by this implemenatation.
///
/// [FindError.EntryNotFound] occurs when the queried entry was not found in the archive file.
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
///
pub const FindError = core.io.AccessError || error {
ArchiveUnsupported,
EntryNotFound,
};
///
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
/// Header data that every Oar archive file starts with at byte offset `0`.
///
const Header = extern struct {
signature: [signature_magic.len]u8 = signature_magic,
revision: u8 = revision_magic,
entry_count: u32 = 0,
padding: [502]u8 = [_]u8{0} ** 502,
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
comptime {
const size = @sizeOf(@This());
if (size != 512) @compileError("Header is not 512 bytes");
}
};
///
/// Attempts to find an [Entry] with a path name matching `path` in `archive_reader`.
///
/// An [Entry] value is returned if a match was found, otherwise [FindError] if it failed.
///
pub fn findEntry(archive_reader: sys.FileReader, path: sys.Path) FindError!Entry {
var header = Header{};
const header_size = @sizeOf(Header);
const io = core.io;
if ((try archive_reader.read(io.bytesOf(&header))) != header_size)
return error.ArchiveUnsupported;
if (!io.equals(u8, &header.signature, &signature_magic))
return error.ArchiveUnsupported;
if (header.revision != revision_magic) return error.ArchiveUnsupported;
// Read file table.
var head: u64 = 0;
var tail: u64 = (header.entry_count - 1);
const entry_size = @sizeOf(Entry);
while (head <= tail) {
var entry = Entry{};
const midpoint = head + ((tail - head) / 2);
const offset = header_size + (entry_size * midpoint);
try archive_reader.seek(offset);
if ((try archive_reader.read(io.bytesOf(&entry))) != entry_size)
return error.ArchiveUnsupported;
const comparison = path.compare(entry.path);
if (comparison == 0) return entry;
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
if (comparison > 0) {
head = (midpoint + 1);
} else {
tail = (midpoint - 1);
}
}
return error.EntryNotFound;
}
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
///
/// Magic revision number that this Oar software implementation understands.
///
const revision_magic = 0;
///
/// Magic identifier used to validate [Header] and [Block] data.
///
const signature_magic = [3]u8{'o', 'a', 'r'};

781
src/ona/sys.zig Normal file
View File

@ -0,0 +1,781 @@
const ext = @cImport({
@cInclude("SDL2/SDL.h");
});
const core = @import("core");
const oar = @import("./oar.zig");
const std = @import("std");
///
/// Thread-safe platform abstraction over multiplexing system I/O processing and event handling.
///
pub const App = struct {
message_chain: ?*Message = null,
message_semaphore: *ext.SDL_sem,
message_mutex: *ext.SDL_mutex,
data: FileSystem,
user: FileSystem,
///
/// Enqueues `message` to the message chain in `app`.
///
fn enqueue(app: *App, message: *Message) void {
{
// TODO: Error check these.
_ = ext.SDL_LockMutex(app.message_mutex);
defer _ = ext.SDL_UnlockMutex(app.message_mutex);
if (app.message_chain) |message_chain| {
message_chain.next = message;
} else {
app.message_chain = message;
}
}
// TODO: Error check this.
_ = ext.SDL_SemPost(app.message_semaphore);
}
///
/// Asynchronously executes `procedure` with `arguments` as an anonymous struct of its arguments
/// and `app` as its execution context.
///
/// Once the execution frame resumes, the value returned by executing `procedure` is returned.
///
pub fn schedule(app: *App, procedure: anytype,
arguments: anytype) core.meta.FnReturn(@TypeOf(procedure)) {
const Task = struct {
procedure: @TypeOf(procedure),
arguments: *@TypeOf(arguments),
result: core.meta.FnReturn(@TypeOf(procedure)),
const Task = @This();
fn process(userdata: *anyopaque) void {
const task = @ptrCast(*Task, @alignCast(@alignOf(Task), userdata));
task.result = @call(.{}, task.procedure, task.arguments.*);
}
};
var task = Task{
.procedure = procedure,
.arguments = &arguments,
};
var message = Message{
.kind = .{.task = .{
.data = &task,
.action = Task.process,
.frame = @frame(),
}},
};
suspend app.enqueue(&message);
}
///
/// Asynchronously logs `info` with `logger` as the logging method and `app` as the execution
/// context.
///
pub fn log(app: *App, logger: Logger, info: []const u8) void {
var message = Message{
.kind = .{.log = .{
.logger = logger,
.info = info,
}},
};
app.enqueue(&message);
}
};
///
/// Snapshotted information about the status of a file.
///
pub const FileStatus = struct {
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
length: u64,
};
///
kayomn marked this conversation as resolved
Review

Missing documentation comment.

Missing documentation comment.
/// Interface for working with bi-directional, streamable resources accessed through a file-system.
///
pub const FileReader = struct {
context: *anyopaque,
vtable: *const struct {
close: fn (*anyopaque) void,
read: fn (*anyopaque, []u8) core.io.AccessError!u64,
seek: fn (*anyopaque, u64) core.io.AccessError!void,
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
},
///
/// Closes the `file_reader`, logging a wraning if the `file_reader` is already considered
/// closed.
///
pub fn close(file_reader: FileReader) void {
file_reader.vtable.close(file_reader.context);
}
///
/// Attempts to read from `file_reader` into `buffer`, returning the number of bytes
/// successfully read or [core.io.AccessError] if it failed.
///
pub fn read(file_reader: FileReader, buffer: []u8) core.io.AccessError!u64 {
return file_reader.vtable.read(file_reader.context, buffer);
}
///
/// Attempts to seek from the beginning of `file_reader` to `cursor` bytes in, returning
/// [core.io.AccessError] if it failed.
///
pub fn seek(file_reader: FileReader, cursor: u64) core.io.AccessError!void {
return file_reader.vtable.seek(file_reader.context, cursor);
}
///
/// Wraps `implementation`, returning a [FileReader] value.
///
pub fn wrap(implementation: anytype) FileReader {
const Implementation = @TypeOf(implementation.*);
return .{
.context = @ptrCast(*anyopaque, implementation),
.vtable = switch (@typeInfo(Implementation)) {
.Struct => &.{
.close = struct {
fn call(context: *anyopaque) void {
@ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).close();
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.

Also, should this be public?

Also, should this be public?
}
}.call,
.read = struct {
fn call(context: *anyopaque, buffer: []u8) core.io.AccessError!u64 {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).read(buffer);
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
}
}.call,
.seek = struct {
fn call(context: *anyopaque, cursor: u64) core.io.AccessError!void {
return @ptrCast(*Implementation, @alignCast(
@alignOf(Implementation), context)).seek(cursor);
}
}.call,
},
.Opaque => &.{
.close = struct {
fn call(context: *anyopaque) void {
@ptrCast(*Implementation, context).close();
}
}.call,
.read = struct {
fn call(context: *anyopaque, buffer: []u8) core.io.AccessError!u64 {
return @ptrCast(*Implementation, context).read(buffer);
}
}.call,
.seek = struct {
fn call(context: *anyopaque, cursor: u64) core.io.AccessError!void {
return @ptrCast(*Implementation, context).seek(cursor);
}
}.call,
},
else => @compileError(
"`context` must a single-element pointer referencing a struct or opaque type"),
},
};
}
};
///
/// Platform-agnostic mechanism for working with an abstraction of the underlying file-system(s)
/// available to the application in a sandboxed environment.
///
pub const FileSystem = union(enum) {
native: []const u8,
archive: struct {
file_system: *const FileSystem,
path: Path,
},
///
/// [AccessError.FileNotFound] occurs when a queried file could not be found on the file-system
kayomn marked this conversation as resolved Outdated

Out of date documentation comment.

Out of date documentation comment.
/// by the process. This may mean the file does not exist, however it may also mean that the
/// process does not have sufficient rights to read it.
///
/// [AccessError.FileSystemfailure] denotes a file-system implementation-specific failure to
/// access resources has occured and therefore cannot proceed to access the file.
///
kayomn marked this conversation as resolved Outdated

This should be resolved as part of the PR.

This should be resolved as part of the PR.
pub const AccessError = error {
FileNotFound,
FileSystemFailure,
};
///
/// Attempts to open the file identified by `path` on `file_system` for reading, returning a
/// [FileReader] value that provides access to the opened file or [AccessError] if it failed.
///
pub fn openFileReader(file_system: FileSystem, path: Path) AccessError!FileReader {
switch (file_system) {
.archive => |archive| {
const archive_reader = try archive.file_system.openFileReader(archive.path);
errdefer archive_reader.close();
const entry = oar.findEntry(archive_reader, path) catch |err| return switch (err) {
error.ArchiveUnsupported, error.Inaccessible => error.FileSystemFailure,
error.EntryNotFound => error.FileNotFound,
};
archive_reader.seek(entry.data_offset) catch return error.FileSystemFailure;
const io = core.io;
const allocator = threadSafeAllocator();
const entry_reader = io.makeOne(struct {
allocator: io.Allocator,
base_reader: FileReader,
cursor: u64,
offset: u64,
length: u64,
const Self = @This();
pub fn close(self: *Self) void {
self.base_reader.close();
io.free(self.allocator, self);
}
pub fn read(self: *Self, buffer: []u8) io.AccessError!u64 {
try self.base_reader.seek(self.offset + self.cursor);
return self.base_reader.read(buffer[0 ..
core.math.min(usize, buffer.len, self.length)]);
}
pub fn seek(self: *Self, cursor: u64) io.AccessError!void {
self.cursor = cursor;
}
}, allocator) catch return error.FileSystemFailure;
errdefer io.free(allocator, entry_reader);
entry_reader.* = .{
.allocator = allocator,
.base_reader = archive_reader,
.cursor = 0,
.offset = entry.data_offset,
.length = entry.data_length,
};
return FileReader.wrap(entry_reader);
},
.native => |native| {
if (native.len == 0) return error.FileNotFound;
var path_buffer = [_]u8{0} ** 4096;
const seperator_length = @boolToInt(native[native.len - 1] != Path.seperator);
if ((native.len + seperator_length + path.length) >= path_buffer.len)
return error.FileNotFound;
const io = core.io;
io.copy(u8, &path_buffer, native);
if (seperator_length != 0) path_buffer[native.len] = Path.seperator;
io.copy(u8, path_buffer[native.len .. path_buffer.len],
path.buffer[0 .. path.length]);
ext.SDL_ClearError();
const rw_ops =
ext.SDL_RWFromFile(&path_buffer, "rb") orelse return error.FileNotFound;
errdefer _ = ext.SDL_RWclose(rw_ops);
return FileReader.wrap(@ptrCast(*opaque {
const Self = @This();
fn rwOpsCast(self: *Self) *ext.SDL_RWops {
return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), self));
}
pub fn read(self: *Self, buffer: []u8) core.io.AccessError!u64 {
ext.SDL_ClearError();
const bytes_read =
ext.SDL_RWread(self.rwOpsCast(), buffer.ptr, @sizeOf(u8), buffer.len);
if ((bytes_read == 0) and (ext.SDL_GetError() != null))
return error.Inaccessible;
return bytes_read;
}
pub fn seek(self: *Self, cursor: u64) core.io.AccessError!void {
ext.SDL_ClearError();
const math = core.math;
const min = math.min;
const maxIntValue = math.maxIntValue;
var sought = min(u64, cursor, maxIntValue(i64));
const ops = self.rwOpsCast();
if (ext.SDL_RWseek(ops, @intCast(i64, sought), ext.RW_SEEK_SET) < 0)
return error.Inaccessible;
var to_seek = cursor - sought;
while (to_seek != 0) {
sought = min(u64, to_seek, maxIntValue(i64));
ext.SDL_ClearError();
if (ext.SDL_RWseek(ops, @intCast(i64, sought), ext.RW_SEEK_CUR) < 0)
return error.Inaccessible;
to_seek -= sought;
}
kayomn marked this conversation as resolved Outdated

Missing documentation comment.

Missing documentation comment.
}
pub fn close(self: *Self) void {
ext.SDL_ClearError();
if (ext.SDL_RWclose(self.rwOpsCast()) != 0)
return ext.SDL_LogWarn(ext.SDL_LOG_CATEGORY_APPLICATION,
"Attempt to close an invalid file reference");
}
}, rw_ops));
},
}
}
///
/// Attempts to query the status of the file identified by `path` on `file_system` for reading,
/// returning a [FileStatus] value containing a the state of the file at the moment or
/// [AccessError] if it failed.
///
pub fn query(file_system: FileSystem, path: Path) AccessError!FileStatus {
switch (file_system) {
.archive => |archive| {
const archive_reader = try archive.file_system.openFileReader(archive.path);
defer archive_reader.close();
const entry = oar.findEntry(archive_reader, path) catch |err| return switch (err) {
error.ArchiveUnsupported, error.Inaccessible => error.FileSystemFailure,
error.EntryNotFound => error.FileNotFound,
};
return FileStatus{
.length = entry.data_length,
};
},
.native => |native| {
if (native.len == 0) return error.FileNotFound;
var path_buffer = [_]u8{0} ** 4096;
const seperator_length = @boolToInt(native[native.len - 1] != Path.seperator);
if ((native.len + seperator_length + path.length) >= path_buffer.len)
return error.FileNotFound;
const io = core.io;
io.copy(u8, &path_buffer, native);
if (seperator_length != 0) path_buffer[native.len] = Path.seperator;
io.copy(u8, path_buffer[native.len .. path_buffer.len],
path.buffer[0 .. path.length]);
ext.SDL_ClearError();
const rw_ops =
ext.SDL_RWFromFile(&path_buffer, "rb") orelse return error.FileSystemFailure;
defer if (ext.SDL_RWclose(rw_ops) != 0) unreachable;
ext.SDL_ClearError();
const size = ext.SDL_RWsize(rw_ops);
if (size < 0) return error.FileSystemFailure;
return FileStatus{
.length = @intCast(u64, size),
};
},
}
}
};
///
///
///
pub const Graphics = opaque {
///
///
///
pub const Event = struct {
keys_up: Keys = std.mem.zeroes(Keys),
keys_down: Keys = std.mem.zeroes(Keys),
keys_held: Keys = std.mem.zeroes(Keys),
kayomn marked this conversation as resolved Outdated

Unused and out of date.

Unused and out of date.
const Keys = [256]bool;
};
///
///
///
const Implementation = struct {
event: Event,
};
///
///
///
pub fn poll(graphics: *Graphics) ?*const Event {
_ = graphics;
return null;
}
///
///
///
pub fn present(graphics: *Graphics) void {
// TODO: Implement;
_ = graphics;
}
};
///
/// [Logger.info] logs information that isn't necessarily an error but indicates something useful to
/// be logged.
///
/// [Logger.debug] logs information only when the engine is in debug mode.
///
/// [Logger.warning] logs information to indicate a non-critical error has occured.
///
pub const Logger = enum(u32) {
info = ext.SDL_LOG_PRIORITY_INFO,
debug = ext.SDL_LOG_PRIORITY_DEBUG,
warning = ext.SDL_LOG_PRIORITY_WARN,
};
///
/// Linked list of asynchronous messages chained together to be processed by the work processor.
///
pub const Message = struct {
next: ?*Message = null,
kind: union(enum) {
quit,
log: struct {
logger: Logger,
info: []const u8,
},
task: struct {
data: *anyopaque,
action: fn (*anyopaque) void,
frame: anyframe,
},
},
};
///
/// Path to a file on a [FileSystem].
///
pub const Path = extern struct {
buffer: [255]u8,
length: u8,
///
/// [Error.TooLong] occurs when creating a path that is greater than the maximum path size **in
/// bytes**.
///
pub const FromError = error {
TooLong,
};
///
/// An empty [Path] with a length of `0`.
///
pub const empty = Path{
.buffer = [_]u8{0} ** 255,
.length = 0,
};
///
/// Returns a value above `0` if the path of `this` is greater than `that`, below `0` if it is
/// less, or `0` if they are identical.
///
pub fn compare(this: Path, that: Path) isize {
return core.io.compareBytes(this.buffer[0 ..this.length], that.buffer[0 .. that.length]);
}
///
/// Returns `true` if `this` is equal to `that`, otherwise `false`.
///
pub fn equals(this: Path, that: Path) bool {
return core.io.equals(u8, this.buffer[0 ..this.length], that.buffer[0 .. that.length]);
}
///
/// Attempts to create a [Path] with the path components in `sequences` as a fully qualified
/// path from root.
///
/// A [Path] value is returned containing the fully qualified path from the file-system root or
/// a [FromError] if it could not be created.
///
pub fn from(sequences: []const []const u8) FromError!Path {
var path = empty;
if (sequences.len != 0) {
const last_sequence_index = sequences.len - 1;
for (sequences) |sequence, index| if (sequence.len != 0) {
var components = core.io.Spliterator(u8){
.source = sequence,
.delimiter = "/",
};
while (components.next()) |component| if (component.len != 0) {
for (component) |byte| {
if (path.length == max) return error.TooLong;
path.buffer[path.length] = byte;
path.length += 1;
}
if (components.hasNext()) {
if (path.length == max) return error.TooLong;
path.buffer[path.length] = '/';
path.length += 1;
}
};
if (index < last_sequence_index) {
if (path.length == max) return error.TooLong;
path.buffer[path.length] = '/';
path.length += 1;
}
};
}
return path;
}
///
/// Returns the hash of the text in `path`.
///
pub fn hash(path: Path) usize {
return core.io.hashBytes(path.buffer[0 .. path.length]);
}
///
/// Maximum number of **bytes** in a [Path].
///
pub const max = 255;
///
/// Textual separator between components of a [Path].
///
pub const seperator = '/';
};
///
/// [RunError.InitFailure] occurs when the runtime fails to initialize.
///
pub const RunError = error {
InitFailure,
};
///
/// Returns a thread-safe [core.io.Allocator] value based on the default system allocation strategy.
///
pub fn threadSafeAllocator() core.io.Allocator {
const io = core.io;
return io.Allocator.wrap(@as(*opaque {
const Self = @This();
pub fn alloc(_: *Self, layout: io.AllocationLayout) io.AllocationError![*]u8 {
return @ptrCast([*]u8, ext.SDL_malloc(layout.length) orelse return error.OutOfMemory);
}
pub fn realloc(_: *Self, allocation: [*]u8,
layout: io.AllocationLayout) io.AllocationError![*]u8 {
return @ptrCast([*]u8, ext.SDL_realloc(allocation, layout.length)
orelse return error.OutOfMemory);
}
pub fn dealloc(_: *Self, allocation: [*]u8) void {
ext.SDL_free(allocation);
}
}, undefined));
}
///
/// Runs a graphical application referenced by `run` with `error` as its error set.
///
/// Should an error from `run` occur, an `Error` is returned, otherwise a [RunError] is returned if
/// the underlying runtime fails and is logged.
///
pub fn display(comptime Error: anytype,
comptime run: fn (*App, *Graphics) callconv(.Async) Error!void) (RunError || Error)!void {
const cwd = FileSystem{.native = "./"};
const user_prefix = ext.SDL_GetPrefPath("ona", "ona") orelse return error.InitFailure;
defer ext.SDL_free(user_prefix);
var app = App{
.user = .{.native = std.mem.sliceTo(user_prefix, 0)},
.message_semaphore = ext.SDL_CreateSemaphore(0) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
"Failed to create message semaphore");
return error.InitFailure;
},
.message_mutex = ext.SDL_CreateMutex() orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
"Failed to create message mutex");
return error.InitFailure;
},
.data = .{.archive = .{
.file_system = &cwd,
.path = try Path.from(&.{"./data.oar"}),
}},
};
defer {
ext.SDL_DestroySemaphore(app.message_semaphore);
ext.SDL_DestroyMutex(app.message_mutex);
}
const message_thread = ext.SDL_CreateThread(processMessages, "Message Processor", &app) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create message processor");
return error.InitFailure;
};
defer {
var message = Message{.kind = .quit};
app.enqueue(&message);
{
var status: c_int = 0;
// SDL2 defines waiting on a null thread reference as a no-op. See
// https://wiki.libsdl.org/SDL_WaitThread for more information
ext.SDL_WaitThread(message_thread, &status);
if (status != 0) {
// TODO: Error check this.
}
}
}
if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize runtime");
return error.InitFailure;
}
defer ext.SDL_Quit();
const window = create_window: {
const pos = ext.SDL_WINDOWPOS_UNDEFINED;
var flags = @as(u32, 0);
break: create_window ext.SDL_CreateWindow("Ona", pos, pos, 640, 480, flags) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create window");
return error.InitFailure;
};
};
defer ext.SDL_DestroyWindow(window);
const renderer = create_renderer: {
var flags = @as(u32, 0);
break: create_renderer ext.SDL_CreateRenderer(window, -1, flags) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create renderer");
return error.InitFailure;
};
};
defer ext.SDL_DestroyRenderer(renderer);
var graphics = Graphics.Implementation{
.event = .{
},
};
return run(@ptrCast(*App, &app), @ptrCast(*Graphics, &graphics));
}
///
/// [FileSystemMessage] processing function used by a dedicated worker thread, where `data`
/// is a type-erased reference to a [EventLoop].
///
/// The processor returns `0` if it exited normally or any other value if an erroneous exit
/// occured.
///
pub fn processMessages(userdata: ?*anyopaque) callconv(.C) c_int {
const app = @ptrCast(*App, @alignCast(@alignOf(App), userdata orelse unreachable));
while (true) {
// TODO: Error check these.
_ = ext.SDL_SemWait(app.message_semaphore);
_ = ext.SDL_LockMutex(app.message_mutex);
defer _ = ext.SDL_UnlockMutex(app.message_mutex);
while (app.message_chain) |message| {
switch (message.kind) {
.quit => return 0,
.log => |log| ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION,
@enumToInt(log.logger), "%.*s", log.info.len, log.info.ptr),
.task => |task| {
task.action(task.data);
resume task.frame;
},
}
app.message_chain = message.next;
}
}
}

View File

@ -1,117 +0,0 @@
const io = @import("./io.zig");
const std = @import("std");
pub fn Fixed(comptime Element: type) type {
return struct {
filled: usize = 0,
buffer: []Element,
const Self = @This();
///
/// Wraps `self` and returns it in a [io.Writer] value.
///
/// Note that this will raise a compilation error if [Element] is not `u8`.
///
pub fn writer(self: *Self) io.Writer {
if (Element != u8) @compileError("Cannot coerce fixed stack of type " ++
@typeName(Element) ++ " into a Writer");
return io.Writer.wrap(Self, self, struct {
fn write(stack: *Self, buffer: []const u8) usize {
stack.pushAll(buffer) catch |err| switch (err) {
error.Overflow => return 0,
};
return buffer.len;
}
}.write);
}
///
/// Clears all elements from `self`.
///
pub fn clear(self: *Self) void {
self.filled = 0;
}
///
/// Counts and returns the number of pushed elements in `self`.
///
pub fn count(self: Self) usize {
return self.filled;
}
///
/// Attempts to pop the tail-end of `self`, returning the element value or `null` if the
/// stack is empty.
///
pub fn pop(self: *Self) ?Element {
if (self.filled == 0) return null;
self.filled -= 1;
return self.buffer[self.filled];
}
///
/// Attempts to push `element` into `self`, returning a [FixedPushError] if it failed.
///
pub fn push(self: *Self, element: Element) FixedPushError!void {
if (self.filled == self.buffer.len) return error.Overflow;
self.buffer[self.filled] = element;
self.filled += 1;
}
///
/// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it
/// failed.
///
pub fn pushAll(self: *Self, elements: []const u8) FixedPushError!void {
const filled = (self.filled + elements.len);
if (filled > self.buffer.len) return error.Overflow;
std.mem.copy(u8, self.buffer[self.filled ..], elements);
self.filled = filled;
}
};
}
///
/// Potential errors that may occur while trying to push one or more elements into a stack of a
/// known maximum size.
///
/// [FinitePushError.Overflow] is returned if the stack does not have sufficient capacity to hold a
/// given set of elements.
///
pub const FixedPushError = error {
Overflow,
};
test {
const testing = std.testing;
var buffer = std.mem.zeroes([4]u8);
var stack = Fixed(u8){.buffer = &buffer};
try testing.expectEqual(stack.count(), 0);
try testing.expectEqual(stack.pop(), null);
try stack.push(69);
try testing.expectEqual(stack.count(), 1);
try testing.expectEqual(stack.pop(), 69);
try stack.pushAll(&.{42, 10, 95, 0});
try testing.expectEqual(stack.count(), 4);
try testing.expectError(FixedPushError.Overflow, stack.push(1));
try testing.expectError(FixedPushError.Overflow, stack.pushAll(&.{1, 11, 11}));
stack.clear();
try testing.expectEqual(stack.count(), 0);
const writer = stack.writer();
try testing.expectEqual(writer.write(&.{0, 0, 0, 0}), 4);
try testing.expectEqual(writer.writeByte(0), false);
}

View File

@ -1,642 +0,0 @@
const ext = @cImport({
@cInclude("SDL2/SDL.h");
});
const io = @import("./io.zig");
const mem = @import("./mem.zig");
const stack = @import("./stack.zig");
const std = @import("std");
///
/// A thread-safe platform abstraction over multiplexing system I/O processing and event handling.
///
pub const EventLoop = opaque {
///
/// Linked list of messages chained together to be processed by the internal file system message
/// processor of an [EventLoop].
///
const FileSystemMessage = struct {
next: ?*FileSystemMessage = null,
frame: anyframe,
request: union(enum) {
exit,
close: struct {
file_access: *FileAccess,
},
log: struct {
message: []const u8,
kind: LogKind,
},
open: struct {
mode: OpenMode,
file_system_path: *const FileSystem.Path,
result: OpenError!*FileAccess = error.NotFound,
},
read_file: struct {
file_access: *FileAccess,
buffer: []const u8,
result: FileError!usize = error.Inaccessible,
},
seek_file: struct {
file_access: *FileAccess,
origin: SeekOrigin,
offset: usize,
result: FileError!void = error.Inaccessible,
},
tell_file: struct {
file_access: *FileAccess,
result: FileError!usize = error.Inaccessible,
},
},
};
///
/// Internal state of the event loop hidden from the API consumer.
///
const Implementation = struct {
user_prefix: []const u8,
file_system_semaphore: *ext.SDL_sem,
file_system_mutex: *ext.SDL_mutex,
file_system_thread: *ext.SDL_Thread,
file_system_messages: ?*FileSystemMessage = null,
///
/// Casts `event_loop` to a [Implementation] reference.
///
/// *Note* that if `event_loop` does not have the same alignment as [Implementation],
/// safety-checked undefined behavior will occur.
///
fn cast(event_loop: *EventLoop) *Implementation {
return @ptrCast(*Implementation, @alignCast(@alignOf(Implementation), event_loop));
}
};
///
/// [LogKind.info] represents a log message which is purely informative and does not indicate
/// any kind of issue.
///
/// [LogKind.debug] represents a log message which is purely for debugging purposes and will
/// only occurs in debug builds.
///
/// [LogKind.warning] represents a log message which is a warning about a issue that does not
/// break anything important but is not ideal.
///
pub const LogKind = enum(c_int) {
info = ext.SDL_LOG_PRIORITY_INFO,
debug = ext.SDL_LOG_PRIORITY_DEBUG,
warning = ext.SDL_LOG_PRIORITY_WARN,
};
///
/// [OpenError.NotFound] is a catch-all for when a file could not be located to be opened. This
/// may be as simple as it doesn't exist or the because the underlying file-system will not /
/// cannot give access to it at this time.
///
pub const OpenError = error {
NotFound,
};
///
/// [OpenMode.readonly] indicates that an existing file is opened in a read-only state,
/// disallowing write access.
///
/// [OpenMode.overwrite] indicates that an empty file has been created or an existing file has
/// been completely overwritten into.
///
/// [OpenMode.append] indicates that an existing file that has been opened for reading from and
/// writing to on the end of existing data.
///
pub const OpenMode = enum {
readonly,
overwrite,
append,
};
///
/// [SeekOrigin.head] indicates that a seek operation will seek from the offset origin of the
/// file beginning, or "head".
///
/// [SeekOrigin.tail] indicates that a seek operation will seek from the offset origin of the
/// file end, or "tail".
///
/// [SeekOrigin.cursor] indicates that a seek operation will seek from the current position of
/// the file cursor.
///
pub const SeekOrigin = enum {
head,
tail,
cursor,
};
///
/// Closes access to the file referenced by `file_access` via `event_loop`.
///
/// *Note* that nothing happens to `file_access` if it is already closed.
///
pub fn close(event_loop: *EventLoop, file_access: *FileAccess) void {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.close = .{.file_access = file_access}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
}
///
/// Enqueues `message` to the file system message processor to be processed at a later, non-
/// deterministic point.
///
fn enqueueFileSystemMessage(event_loop: *EventLoop, message: *FileSystemMessage) void {
const implementation = Implementation.cast(event_loop);
// TODO: Error check this.
_ = ext.SDL_LockMutex(implementation.file_system_mutex);
if (implementation.file_system_messages) |messages| {
messages.next = message;
} else {
implementation.file_system_messages = message;
}
// TODO: Error check these.
_ = ext.SDL_UnlockMutex(implementation.file_system_mutex);
_ = ext.SDL_SemPost(implementation.file_system_semaphore);
}
///
/// Writes `message` to the application log with `kind` via `event_loop`.
///
/// *Note* that `message` is not guaranteed to be partly, wholely, or at all written.
///
pub fn log(event_loop: *EventLoop, kind: LogKind, message: []const u8) void {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.log = .{
.message = message,
.kind = kind,
}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
}
///
/// Attempts to open access to a file referenced at `file_system_path` using `mode` as the way
/// to open it via `event_loop`.
///
/// A [FileAccess] pointer is returned referencing the opened file or a [OpenError] if the file
/// could not be opened.
///
/// *Note* that all files are opened in "binary-mode", or Unix-mode. There are no conversions
/// applied when data is accessed from a file.
///
pub fn open(event_loop: *EventLoop, mode: OpenMode,
file_system_path: FileSystem.Path) OpenError!*FileAccess {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.open = .{
.mode = mode,
.file_system_path = &file_system_path,
}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
return file_system_message.request.open.result;
}
///
/// [FileSystemMessage] processing function used by a dedicated worker thread, where `data` is
/// a type-erased reference to a [EventLoop].
///
/// The processor returns `0` if it exited normally or any other value if an erroneous exit
/// occured.
///
fn processFileSystemMessages(data: ?*anyopaque) callconv(.C) c_int {
const implementation = Implementation.cast(@ptrCast(*EventLoop, data orelse unreachable));
while (true) {
while (implementation.file_system_messages) |messages| {
switch (messages.request) {
.exit => return 0,
.log => |*log_request| ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION,
@enumToInt(log_request.priority), log_request.message),
.open => |*open_request| {
switch (open_request.path.file_system) {
.data => {
// TODO: Implement
open_request.result = error.NotFound;
},
.user => {
var path_buffer = std.mem.zeroes([4096]u8);
var path = stack.Fixed(u8){.buffer = path_buffer[0 .. ]};
path.pushAll(implementation.user_prefix) catch {
open_request.result = error.BadFileSystem;
continue;
};
if (!open_request.path.write(path.writer())) {
open_request.result = error.NotFound;
continue;
}
if (ext.SDL_RWFromFile(&path_buffer, switch (open_request.mode) {
.readonly => "rb",
.overwrite => "wb",
.append => "ab",
})) |rw_ops| {
open_request.result = @ptrCast(*FileAccess, rw_ops);
} else {
open_request.result = error.NotFound;
}
},
}
},
.close => |*close_request| {
// TODO: Use this result somehow.
_ = ext.SDL_RWclose(@ptrCast(*ext.SDL_RWops, @alignCast(
@alignOf(ext.SDL_RWops), close_request.file_access)));
},
.read_file => |read_request| {
// TODO: Implement.
_ = read_request;
},
.seek_file => |seek_request| {
// TODO: Implement.
_ = seek_request;
},
.tell_file => |tell_request| {
// TODO: Implement.
_ = tell_request;
},
}
resume messages.frame;
implementation.file_system_messages = messages.next;
}
// TODO: Error check this.
_ = ext.SDL_SemWait(implementation.file_system_semaphore);
}
}
///
/// Attempts to read the contents of the file referenced by `file_access` at the current file
/// cursor position into `buffer`.
///
/// The number of bytes that could be read / fitted into `buffer` is returned or a [FileError]
/// if the file failed to be read.
///
pub fn readFile(event_loop: *EventLoop, file_access: *FileAccess,
buffer: []const u8) FileError!usize {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.read_file = .{
.file_access = file_access,
.buffer = buffer,
}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
return file_system_message.request.read_file.result;
}
///
/// Attempts to tell the current file cursor position for the file referenced by `file_access`.
///
/// Returns the number of bytes into the file that the cursor is relative to its beginning or a
/// [FileError] if the file failed to be queried.
///
pub fn queryFile(event_loop: *EventLoop, file_access: *FileAccess) FileError!usize {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.tell_file = .{.file_access = file_access}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
return file_system_message.request.tell_file.result;
}
///
/// Attempts to seek the file cursor through the file referenced by `file_access` from `origin`
/// to `offset` via `event_loop`, returning a [FileError] if the file failed to be sought.
///
pub fn seekFile(event_loop: *EventLoop, file_access: *FileAccess,
origin: SeekOrigin, offset: usize) FileError!void {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{
.seek_file = .{
.file_access = file_access,
.origin = origin,
.offset = offset,
},
},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
return file_system_message.request.seek_file.result;
}
};
///
/// File-system agnostic abstraction for manipulating a file.
///
pub const FileAccess = opaque {
///
/// Scans the number of bytes in the file referenced by `file_access` via `event_loop`, returing
/// its byte size or a [FileError] if it failed.
///
pub fn size(file_access: *FileAccess, event_loop: *EventLoop) FileError!usize {
// Save cursor to return to it later.
const origin_cursor = try event_loop.queryFile(file_access);
try event_loop.seekFile(file_access, .tail, 0);
const ending_cursor = try event_loop.queryFile(file_access);
// Return to original cursor.
try event_loop.seekFile(file_access, .head, origin_cursor);
return ending_cursor;
}
};
///
/// With files typically being backed by a block device, they can produce a variety of errors -
/// from physical to virtual errors - these are all encapsulated by the API as general
/// [Error.Inaccessible] errors.
///
pub const FileError = error {
Inaccessible,
};
///
/// Platform-agnostic mechanism for working with an abstraction of the underlying file-system(s)
/// available to the application in a sandboxed environment.
///
pub const FileSystem = enum {
data,
user,
///
/// Platform-agnostic mechanism for referencing files and directories on a [FileSystem].
///
pub const Path = struct {
file_system: FileSystem,
length: u16,
buffer: [max]u8,
///
/// Returns `true` if the length of `path` is empty, otherwise `false`.
///
pub fn isEmpty(path: Path) bool {
return (path.length == 0);
}
///
/// Returns `true` if `this` is equal to `that`, otherwise `false`.
///
pub fn equals(this: Path, that: Path) bool {
return std.mem.eql(u8, this.buffer[0 .. this.length], that.buffer[0 .. that.length]);
}
///
/// The maximum possible byte-length of a [Path].
///
/// Note that paths are encoded using UTF-8, meaning that a character may be bigger than one
/// byte. Because of this, it is not safe to asume that a path may hold [max] individual
/// characters.
///
pub const max = 1000;
///
///
///
pub fn write(path: Path, writer: io.Writer) bool {
return (writer.write(path.buffer[0 .. path.length]) == path.length);
}
};
///
/// [PathError.TooLong] occurs when creating a path that is greater than the maximum size **in
/// bytes**.
///
pub const PathError = error {
TooLong,
};
///
/// Creates and returns a [Path] value in the file system to the location specified by the
/// joining of the `sequences` path values.
///
pub fn joinedPath(file_system: FileSystem, sequences: []const []const u8) PathError!Path {
var path = Path{
.file_system = file_system,
.buffer = std.mem.zeroes([Path.max]u8),
.length = 0,
};
for (sequences) |sequence| if (sequence.len != 0) {
var components = mem.Spliterator(u8){
.source = sequence,
.delimiter = "/",
};
while (components.next()) |component| if (component.len != 0) {
for (component) |byte| {
if (path.length == Path.max) return error.TooLong;
path.buffer[path.length] = byte;
path.length += 1;
}
if (path.length == Path.max) return error.TooLong;
path.buffer[path.length] = '/';
path.length += 1;
};
};
return path;
}
};
///
///
///
pub const GraphicsContext = opaque {
///
///
///
pub const Event = struct {
keys_up: Keys = std.mem.zeroes(Keys),
keys_down: Keys = std.mem.zeroes(Keys),
keys_held: Keys = std.mem.zeroes(Keys),
const Keys = [256]bool;
};
const Implementation = struct {
event: Event,
};
///
///
///
pub fn poll(graphics_context: *GraphicsContext) ?*const Event {
_ = graphics_context;
return null;
}
///
///
///
pub fn present(graphics_context: *GraphicsContext) void {
// TODO: Implement;
_ = graphics_context;
}
};
///
///
///
pub fn GraphicsRunner(comptime Errors: type) type {
return fn (*EventLoop, *GraphicsContext) Errors!void;
}
///
///
///
pub fn runGraphics(comptime Errors: anytype, run: GraphicsRunner(Errors)) Errors!void {
if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize runtime");
return error.InitFailure;
}
defer ext.SDL_Quit();
const pref_path = create_pref_path: {
const path = ext.SDL_GetPrefPath("ona", "ona") orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load user path");
return error.InitFailure;
};
break: create_pref_path path[0 .. std.mem.len(path)];
};
defer ext.SDL_free(pref_path.ptr);
const window = create_window: {
const pos = ext.SDL_WINDOWPOS_UNDEFINED;
var flags = @as(u32, 0);
break: create_window ext.SDL_CreateWindow("Ona", pos, pos, 640, 480, flags) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create window");
return error.InitFailure;
};
};
defer ext.SDL_DestroyWindow(window);
const renderer = create_renderer: {
var flags = @as(u32, 0);
break: create_renderer ext.SDL_CreateRenderer(window, -1, flags) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create renderer");
return error.InitFailure;
};
};
defer ext.SDL_DestroyRenderer(renderer);
var event_loop = EventLoop.Implementation{
.file_system_semaphore = ext.SDL_CreateSemaphore(0) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
"Failed to create file-system work scheduler");
return error.InitFailure;
},
.file_system_mutex = ext.SDL_CreateMutex() orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
"Failed to create file-system work lock");
return error.InitFailure;
},
.file_system_thread = unreachable,
.user_prefix = pref_path,
};
event_loop.file_system_thread = ext.SDL_CreateThread(
EventLoop.processFileSystemMessages, "File System Worker", &event_loop) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
"Failed to create file-system work processor");
return error.InitFailure;
};
defer {
ext.SDL_DestroyThread(event_loop.file_system_thread);
ext.SDL_DestroySemaphore(event_loop.file_system_mutex);
ext.SDL_DestroySemaphore(event_loop.file_system_semaphore);
}
var graphics_context = GraphicsContext.Implementation{
.event = .{
},
};
var message = EventLoop.FileSystemMessage{
.frame = @frame(),
.request = .exit,
};
@ptrCast(*EventLoop, event_loop).enqueueFileSystemMessage(&message);
var status = @as(c_int, 0);
ext.SDL_WaitThread(event_loop.file_system_thread, &status);
if (status != 0) {
// TODO: Error check this.
}
return run(@ptrCast(*EventLoop, &event_loop), @ptrCast(*GraphicsContext, &graphics_context));
}

4
src/tests.zig Normal file
View File

@ -0,0 +1,4 @@
test {
_ = @import("./core/main.zig");
_ = @import("./ona/main.zig");
}