Compare commits

..

No commits in common. "ac95993a4bbd63da4ce74451860e2889c1090339" and "7599ce61f2c68a2ef730fa565632847a0d747e99" have entirely different histories.

13 changed files with 643 additions and 437 deletions

12
.vscode/launch.json vendored
View File

@ -10,7 +10,15 @@
"valuesFormatting": "parseText", "valuesFormatting": "parseText",
"preLaunchTask": "Build Debug", "preLaunchTask": "Build Debug",
}, },
{
"name": "Oar",
"type": "gdb",
"request": "launch",
"target": "${workspaceFolder}/zig-out/bin/oar",
"cwd": "${workspaceRoot}",
"valuesFormatting": "parseText",
"preLaunchTask": "Build Debug",
},
{ {
"name": "Test", "name": "Test",
"type": "gdb", "type": "gdb",
@ -19,7 +27,7 @@
"arguments": "main.zig", "arguments": "main.zig",
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}",
"valuesFormatting": "parseText", "valuesFormatting": "parseText",
"preLaunchTask": "Build Debug", "preLaunchTask": "Build Tests",
}, },
] ]
} }

9
.vscode/tasks.json vendored
View File

@ -28,7 +28,7 @@
"group": "build", "group": "build",
"presentation": { "presentation": {
"echo": true, "echo": true,
"reveal": "silent", "reveal": "always",
"focus": true, "focus": true,
"panel": "shared", "panel": "shared",
"showReuseMessage": true, "showReuseMessage": true,
@ -36,5 +36,12 @@
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
}, },
{
"label": "Build Tests",
"type": "shell",
"command": "zig build test",
"group": "build",
},
], ],
} }

View File

@ -10,15 +10,26 @@ pub fn build(builder: *std.build.Builder) void {
// Ona executable. // Ona executable.
{ {
const ona = builder.addExecutable("ona", "./src/ona/main.zig"); const exe = builder.addExecutable("ona", "./src/ona/main.zig");
ona.addPackage(core_pkg); exe.addPackage(projectPkg("oar", &.{core_pkg}));
ona.setTarget(target); exe.addPackage(core_pkg);
ona.setBuildMode(mode); exe.setTarget(target);
ona.install(); exe.setBuildMode(mode);
ona.addIncludeDir("./ext"); exe.install();
ona.linkSystemLibrary("SDL2"); exe.addIncludeDir("./ext");
ona.linkLibC(); exe.linkSystemLibrary("SDL2");
exe.linkLibC();
}
// Oar executable.
{
const exe = builder.addExecutable("oar", "./src/oar/main.zig");
exe.addPackage(core_pkg);
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();
} }
// Tests executable. // Tests executable.
@ -29,6 +40,7 @@ pub fn build(builder: *std.build.Builder) void {
tests.setTarget(target); tests.setTarget(target);
tests.setBuildMode(mode); tests.setBuildMode(mode);
tests.install(); tests.install();
builder.step("test", "Run unit tests").dependOn(&tests.step);
} }
} }

70
src/core/fmt.zig Normal file
View File

@ -0,0 +1,70 @@
const io = @import("io.zig");
///
/// Number formatting modes supported by [writeInt].
///
pub const Radix = enum {
binary,
tinary,
quaternary,
quinary,
senary,
septenary,
octal,
nonary,
decimal,
undecimal,
duodecimal,
tridecimal,
tetradecimal,
pentadecimal,
hexadecimal,
};
///
/// 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) 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.call(.{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"),
}
}

View File

@ -1,12 +1,107 @@
const math = @import("./math.zig");
const meta = @import("./meta.zig"); const meta = @import("./meta.zig");
const stack = @import("./stack.zig"); const stack = @import("./stack.zig");
const testing = @import("./testing.zig"); const std = @import("std");
/// ///
/// ///
/// ///
pub const Allocator = @import("std").mem.Allocator; pub const Allocator = std.mem.Allocator;
///
/// File-system agnostic abstraction for manipulating a file.
///
pub const FileAccess = struct {
context: *anyopaque,
implementation: *const Implementation,
///
/// Provides a set of implementation-specific behaviors to a [FileAccess] instance.
///
pub const Implementation = struct {
close: fn (*anyopaque) void,
queryCursor: fn (*anyopaque) Error!u64,
queryLength: fn (*anyopaque) Error!u64,
read: fn (*anyopaque, []u8) Error!usize,
seek: fn (*anyopaque, u64) Error!void,
seekToEnd: fn (*anyopaque) Error!void,
skip: fn (*anyopaque, i64) Error!void,
};
///
/// [Error.FileInaccessible] is a generic catch-all for a [FileAccess] reference no longer
/// pointing to a file or the file becomming invalid for whatever reason.
///
pub const Error = error {
FileInaccessible,
};
///
/// Close the file referenced by `file_access` on the main thread, invalidating the reference to
/// it and releasing any associated resources.
///
/// Freeing an invalid `file_access` has no effect on the file and logs a warning over the
/// wasted effort.
///
pub fn close(file_access: FileAccess) void {
return file_access.implementation.close(file_access.context);
}
///
/// Attempts to query the current 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
/// [Error] on failure.
///
pub fn queryCursor(file_access: FileAccess) Error!u64 {
return file_access.implementation.queryCursor(file_access.context);
}
///
/// Attempts to query the current length for the file referenced by `file_access`.
///
/// Returns the current length of the file at the time of the operation or a [Error] if the file
/// failed to be queried.
///
pub fn queryLength(file_access: FileAccess) Error!u64 {
return file_access.implementation.queryLength(file_access.context);
}
///
/// Attempts to read `file_access` from the its current position into `buffer`.
///
/// Returns the number of bytes that were available to be read, otherwise an [Error] on failure.
///
pub fn read(file_access: FileAccess, buffer: []u8) Error!usize {
return file_access.implementation.read(file_access.context, buffer);
}
///
/// Attempts to seek `file_access` from the beginning of the file to `cursor` bytes.
///
/// Returns [Error] on failure.
///
pub fn seek(file_access: FileAccess, cursor: u64) Error!void {
return file_access.implementation.seek(file_access.context, cursor);
}
///
/// Attempts to seek `file_access` to the end of the file.
///
/// Returns [Error] on failure.
///
pub fn seekToEnd(file_access: FileAccess) Error!void {
return file_access.implementation.seekToEnd(file_access.context);
}
///
/// Attempts to seek `file_access` by `offset` from the current file position.
///
/// Returns [Error] on failure;
///
pub fn skip(file_access: FileAccess, offset: i64) Error!void {
return file_access.implementation.skip(file_access.context, offset);
}
};
/// ///
/// Closure that captures a reference to readable resources like block devices, memory buffers, /// Closure that captures a reference to readable resources like block devices, memory buffers,
@ -32,26 +127,6 @@ pub fn Spliterator(comptime Element: type) type {
return (self.source.len != 0); return (self.source.len != 0);
} }
test "Check has data" {
var empty_spliterator = Spliterator(u8){
.source = "",
.delimiter = "/",
};
try testing.expect(!empty_spliterator.hasNext());
var stateful_spliterator = Spliterator(u8){
.source = "data",
.delimiter = "/",
};
try testing.expect(stateful_spliterator.hasNext());
_ = try stateful_spliterator.next();
try testing.expect(!stateful_spliterator.hasNext());
}
/// ///
/// Iterates on `self` and returns the next view of [Spliterator.source] that matches /// 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. /// [Spliterator.delimiter], or `null` if there is no more data to be processed.
@ -59,11 +134,7 @@ pub fn Spliterator(comptime Element: type) type {
pub fn next(self: *Self) ?[]const Element { pub fn next(self: *Self) ?[]const Element {
if (!self.hasNext()) return null; if (!self.hasNext()) return null;
if (findFirstOf(Element, self.source, self.delimiter, struct { if (std.mem.indexOfPos(Element, self.source, 0, self.delimiter)) |index| {
fn testEquality(this: Element, that: Element) bool {
return this == that;
}
}.testEquality)) |index| {
defer self.source = self.source[(index + self.delimiter.len) .. self.source.len]; defer self.source = self.source[(index + self.delimiter.len) .. self.source.len];
return self.source[0 .. index]; return self.source[0 .. index];
@ -73,8 +144,12 @@ pub fn Spliterator(comptime Element: type) type {
return self.source; return self.source;
} }
};
}
test "Spliterating text" {
const testing = std.testing;
test "Iterate through data" {
// Single-character delimiter. // Single-character delimiter.
{ {
var spliterator = Spliterator(u8){ var spliterator = Spliterator(u8){
@ -82,13 +157,11 @@ pub fn Spliterator(comptime Element: type) type {
.delimiter = ".", .delimiter = ".",
}; };
const components = [_][]const u8{"single", const components = [_][]const u8{"single", "character", "separated", "hello", "world"};
"character", "separated", "hello", "world"};
var index = @as(usize, 0); var index = @as(usize, 0);
while (spliterator.next()) |split| : (index += 1) { while (spliterator.next()) |split| : (index += 1) {
try testing.expect(equals(u8, split, components[index])); try testing.expect(std.mem.eql(u8, split, components[index]));
} }
} }
@ -103,11 +176,9 @@ pub fn Spliterator(comptime Element: type) type {
var index = @as(usize, 0); var index = @as(usize, 0);
while (spliterator.next()) |split| : (index += 1) { while (spliterator.next()) |split| : (index += 1) {
try testing.expect(equals(u8, split, components[index])); try testing.expect(std.mem.eql(u8, split, components[index]));
} }
} }
}
};
} }
/// ///
@ -127,6 +198,7 @@ pub fn begins(comptime Element: type, elements: []const Element, with: []const E
test "Check memory begins with" { test "Check memory begins with" {
const bytes_sequence = &.{69, 42}; const bytes_sequence = &.{69, 42};
const testing = std.testing;
try testing.expect(begins(u8, &.{69, 42, 0, 89}, bytes_sequence)); try testing.expect(begins(u8, &.{69, 42, 0, 89}, bytes_sequence));
try testing.expect(!begins(u8, &.{69, 89, 42, 0}, bytes_sequence)); try testing.expect(!begins(u8, &.{69, 89, 42, 0}, bytes_sequence));
@ -137,7 +209,7 @@ test "Check memory begins with" {
/// ///
/// **Note** that passing a slice will convert it to a byte slice. /// **Note** that passing a slice will convert it to a byte slice.
/// ///
pub fn bytesOf(pointer: anytype) switch (@typeInfo(@TypeOf(pointer))) { pub fn bytes(pointer: anytype) switch (@typeInfo(@TypeOf(pointer))) {
.Pointer => |info| if (info.is_const) []const u8 else []u8, .Pointer => |info| if (info.is_const) []const u8 else []u8,
else => @compileError("`pointer` must be a pointer type"), else => @compileError("`pointer` must be a pointer type"),
} { } {
@ -157,48 +229,11 @@ pub fn bytesOf(pointer: anytype) switch (@typeInfo(@TypeOf(pointer))) {
} }
test "Bytes of types" { test "Bytes of types" {
const testing = std.testing;
var foo: u32 = 10; var foo: u32 = 10;
try testing.expect(bytesOf(&foo)[0] == 0x0a); testing.expectEqual(bytes(&foo), 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 "Compare bytes" {
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 data" {
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);
} }
/// ///
@ -208,72 +243,23 @@ test "Copy data" {
pub fn equals(comptime Element: type, this: []const Element, that: []const Element) bool { pub fn equals(comptime Element: type, this: []const Element, that: []const Element) bool {
if (this.len != that.len) return false; if (this.len != that.len) return false;
var index: usize = 0; {
var i = std.mem.zeroes(usize);
while (index < this.len) : (index += 1) if (this[index] != that[index]) return false; while (i < this.len) : (i += 1) if (this[i] != that[i]) return false;
}
return true; return true;
} }
test "Check memory is equal" { test "Check memory is equals" {
const bytes_sequence = &.{69, 42, 0}; const bytes_sequence = &.{69, 42, 0};
const testing = std.testing;
try testing.expect(equals(u8, bytes_sequence, bytes_sequence)); try testing.expect(equals(u8, bytes_sequence, bytes_sequence));
try testing.expect(!equals(u8, bytes_sequence, &.{69, 42})); try testing.expect(!equals(u8, bytes_sequence, &.{69, 42}));
} }
///
/// Searches for the first instance of an `Element` equal to `needle` in `haystack`, returning its
/// index or `null` if nothing was found.
///
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 "Find first of element" {
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.
///
pub fn findFirstOf(comptime Element: type, haystack: []const Element,
needle: []const Element, comptime testEquality: fn (Element, Element) bool) ?usize {
var cursor: usize = 0;
const end = (haystack.len - needle.len);
walk_haystack: while (cursor <= end) : (cursor += 1) {
const range = (cursor + needle.len);
var index = cursor;
while (index < range) : (index += 1)
if (testEquality(haystack[index], needle[index])) continue: walk_haystack;
return cursor;
}
return null;
}
test "Find first of sequence" {
}
/// ///
/// Returns a deterministic hash code compiled from each byte in `bytes`. /// Returns a deterministic hash code compiled from each byte in `bytes`.
/// ///
@ -289,30 +275,12 @@ pub fn hashBytes(bytes: []const u8) usize {
test "Hashing bytes" { test "Hashing bytes" {
const bytes_sequence = &.{69, 42, 0}; const bytes_sequence = &.{69, 42, 0};
const testing = std.testing;
try testing.expect(hashBytes(bytes_sequence) == hashBytes(bytes_sequence)); try testing.expect(hashBytes(bytes_sequence) == hashBytes(bytes_sequence));
try testing.expect(hashBytes(bytes_sequence) != hashBytes(&.{69, 42})); try testing.expect(hashBytes(bytes_sequence) != hashBytes(&.{69, 42}));
} }
///
/// 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 "Data swapping" {
var a: u64 = 0;
var b: u64 = 1;
swap(u64, &a, &b);
try testing.expect(a == 1);
try testing.expect(b == 0);
}
/// ///
/// Returns a [Writer] that silently consumes all given data without failure and throws it away. /// Returns a [Writer] that silently consumes all given data without failure and throws it away.
/// ///
@ -320,7 +288,7 @@ test "Data swapping" {
/// sent somewhere for whatever reason. /// sent somewhere for whatever reason.
/// ///
pub fn nullWriter() Writer { pub fn nullWriter() Writer {
return Writer.capture(@as(usize, 0), struct { return Writer.capture(std.mem.zeroes(usize), struct {
fn write(_: usize, buffer: []const u8) usize { fn write(_: usize, buffer: []const u8) usize {
return buffer.len; return buffer.len;
} }
@ -328,7 +296,11 @@ pub fn nullWriter() Writer {
} }
test "Null writing" { test "Null writing" {
const testing = std.testing;
{
const sequence = "foo"; const sequence = "foo";
try testing.expect(nullWriter().call(sequence) == sequence.len); try testing.expectEqual(nullWriter().apply(sequence), sequence.len);
}
} }

View File

@ -5,17 +5,12 @@
pub const io = @import("./io.zig"); pub const io = @import("./io.zig");
/// ///
/// Math types and functions with a focus on graphics-specific linear algebra. /// Metaprogramming introspection and generation utilities.
///
pub const math = @import("./math.zig");
///
/// Metaprogramming introspection and generation.
/// ///
pub const meta = @import("./meta.zig"); pub const meta = @import("./meta.zig");
/// ///
/// Sequential, last-in first-out data structures. /// Sequential last-in first-out data structures.
/// ///
pub const stack = @import("./stack.zig"); pub const stack = @import("./stack.zig");
@ -24,22 +19,9 @@ pub const stack = @import("./stack.zig");
/// ///
pub const table = @import("./table.zig"); 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 { test {
_ = io; _ = io;
_ = math;
_ = meta; _ = meta;
_ = stack; _ = stack;
_ = table; _ = table;
_ = testing;
_ = unicode;
} }

View File

@ -1,26 +0,0 @@
pub const IntFittingRange = @import("std").math.IntFittingRange;
///
/// Returns the maximum value of `Integer`.
///
pub fn maxInt(comptime Integer: type) Integer {
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"),
};
}
///
/// 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"),
};
}

View File

@ -1,3 +1,5 @@
const std = @import("std");
/// ///
/// Returns the return type of the function type `Fn`. /// Returns the return type of the function type `Fn`.
/// ///
@ -10,12 +12,15 @@ pub fn FnReturn(comptime Fn: type) type {
} }
/// ///
/// Returns a single-input single-output closure type where `In` represents the input type, `Out` /// Returns a double-input single-output closure type where `A` represents the first input type, `B`
/// represents the output type, and `captures_size` represents the size of the closure context. /// represents the second, and `Out` represents the output type, and `captures_size` represents the
/// size of the closure context.
/// ///
pub fn Function(comptime captures_size: usize, comptime In: type, comptime Out: type) type { pub fn BiFunction(comptime captures_size: usize, comptime A: type,
comptime B: type, comptime Out: type) type {
return struct { return struct {
callErased: fn (*anyopaque, In) Out, applyErased: fn (*anyopaque, A, B) Out,
context: [captures_size]u8, context: [captures_size]u8,
/// ///
@ -24,38 +29,92 @@ pub fn Function(comptime captures_size: usize, comptime In: type, comptime Out:
const Self = @This(); const Self = @This();
/// ///
/// Invokes `self` with `input`, producing a result according to the current context data. /// Applies `a` and `b` to `self`, producing a result according to the current context data.
/// ///
pub fn call(self: *Self, input: In) Out { pub fn apply(self: *Self, a: A, b: B) Out {
return self.callErased(&self.context, input); return self.applyErased(&self.context, a, b);
} }
/// ///
/// Creates a new [Self] by capturing the `captures` value as the context and `invoke` as /// Creates a new [Self] by capturing the `captures` value as the context and `call` as the
/// the as the behavior executed when [call] or [callErased] is called. /// as the behavior executed when [apply] or [applyErased] is called.
/// ///
/// The newly created [Self] is returned. /// The newly created [Self] is returned.
/// ///
pub fn capture(captures: anytype, comptime invoke: fn (@TypeOf(captures), In) Out) Self { pub fn capture(captures: anytype, comptime call: fn (@TypeOf(captures), A, B) Out) Self {
const Captures = @TypeOf(captures); const Captures = @TypeOf(captures);
if (@sizeOf(Captures) > captures_size) if (@sizeOf(Captures) > captures_size)
@compileError("`captures` exceeds the size limit of the capture context"); @compileError("`captures` must be smaller than or equal to " ++
std.fmt.comptimePrint("{d}", .{captures_size}) ++ " bytes");
var function = Self{
.context = undefined,
.applyErased = struct {
fn applyErased(erased: *anyopaque, a: A, b: B) Out {
return call(if (Captures == void) {} else @ptrCast(*Captures,
@alignCast(@alignOf(Captures), erased)).*, a, b);
}
}.applyErased,
};
if (captures != {}) {
@ptrCast(*Captures, @alignCast(@alignOf(Captures), &function.context)).* = captures;
}
return function;
}
};
}
///
/// Returns a single-input single-output closure type where `In` represents the input type, `Out`
/// represents the output type, and `captures_size` represents the size of the closure context.
///
pub fn Function(comptime captures_size: usize, comptime In: type, comptime Out: type) type {
return struct {
applyErased: fn (*anyopaque, In) Out,
context: [captures_size]u8,
///
/// Function type.
///
const Self = @This();
///
/// Applies `input` to `self`, producing a result according to the current context data.
///
pub fn apply(self: *Self, input: In) Out {
return self.applyErased(&self.context, input);
}
///
/// Creates a new [Self] by capturing the `captures` value as the context and `call` as the
/// as the behavior executed when [apply] or [applyErased] is called.
///
/// The newly created [Self] is returned.
///
pub fn capture(captures: anytype, comptime call: fn (@TypeOf(captures), In) Out) Self {
const Captures = @TypeOf(captures);
if (@sizeOf(Captures) > captures_size)
@compileError("`captures` must be smaller than or equal to " ++
std.fmt.comptimePrint("{d}", .{captures_size}) ++ " bytes");
const captures_align = @alignOf(Captures); const captures_align = @alignOf(Captures);
var function = Self{ var function = Self{
.context = undefined, .context = undefined,
.callErased = struct { .applyErased = struct {
fn callErased(erased: *anyopaque, input: In) Out { fn applyErased(erased: *anyopaque, input: In) Out {
return invoke(if (Captures == void) {} else @ptrCast(*Captures, return call(if (Captures == void) {} else @ptrCast(*Captures,
@alignCast(@alignOf(Captures), erased)).*, input); @alignCast(@alignOf(Captures), erased)).*, input);
} }
}.callErased, }.applyErased,
}; };
if (Captures != void)
@ptrCast(*Captures, @alignCast(captures_align, &function.context)).* = captures; @ptrCast(*Captures, @alignCast(captures_align, &function.context)).* = captures;
return function; return function;

View File

@ -1,5 +1,5 @@
const io = @import("./io.zig"); const io = @import("./io.zig");
const testing = @import("./testing.zig"); const std = @import("std");
/// ///
/// Returns a fixed-size stack type of `Element`s. /// Returns a fixed-size stack type of `Element`s.
@ -15,20 +15,42 @@ pub fn Fixed(comptime Element: type) type {
const Self = @This(); const Self = @This();
/// ///
/// Resets the number of filled items to `0`, otherwise leaving the actual memory contents /// Wraps `self` and returns it in a [io.Writer] value.
/// of the buffer untouched until it is later overwritten by following operations on it. ///
/// 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.capture(self, struct {
fn write(stack: *Self, buffer: []const u8) usize {
stack.pushAll(buffer) catch |err| switch (err) {
error.OutOfMemory => return 0,
};
return buffer.len;
}
}.write);
}
///
/// Clears all elements from `self`.
/// ///
pub fn clear(self: *Self) void { pub fn clear(self: *Self) void {
self.filled = 0; self.filled = 0;
} }
/// ///
/// If `self` is filled with at least `1` value, it is decremented by `1`, otherwise leaving /// Counts and returns the number of pushed elements in `self`.
/// 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 pub fn count(self: Self) usize {
/// popped, otherwise `null` if it contained no elements. 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 { pub fn pop(self: *Self) ?Element {
if (self.filled == 0) return null; if (self.filled == 0) return null;
@ -52,92 +74,40 @@ pub fn Fixed(comptime Element: type) type {
/// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it /// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it
/// failed. /// failed.
/// ///
pub fn pushAll(self: *Self, elements: []const Element) PushError!void { pub fn pushAll(self: *Self, elements: []const u8) PushError!void {
const filled = (self.filled + elements.len); const filled = (self.filled + elements.len);
if (filled > self.buffer.len) return error.OutOfMemory; if (filled > self.buffer.len) return error.OutOfMemory;
io.copy(Element, self.buffer[self.filled ..], elements); std.mem.copy(u8, self.buffer[self.filled ..], elements);
self.filled = filled; self.filled = filled;
} }
}; };
} }
test "Fixed stack of string literals" {
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 stack. /// Potential errors that may occur while trying to push one or more elements into a stack.
/// ///
pub const PushError = io.Allocator.Error; pub const PushError = std.mem.Allocator.Error;
/// test "Fixed stack manipulation" {
/// Returns an [io.Writer] wrapping `fixed_stack`. const testing = std.testing;
/// var buffer = std.mem.zeroes([4]u8);
/// Writing to the returned [io.Writer] will push values to the underlying [Fixed] stack instance var stack = Fixed(u8){.buffer = &buffer};
/// referenced by `fixed_stack` until it is full.
///
pub fn fixedWriter(fixed_stack: *Fixed(u8)) io.Writer {
return io.Writer.capture(fixed_stack, struct {
fn write(stack: *Fixed(u8), buffer: []const u8) usize {
stack.pushAll(buffer) catch |err| switch (err) {
error.OutOfMemory => return 0,
};
return buffer.len; try testing.expectEqual(stack.count(), 0);
} try testing.expectEqual(stack.pop(), null);
}.write); try stack.push(69);
} try testing.expectEqual(stack.count(), 1);
try testing.expectEqual(stack.pop(), 69);
test "Fixed writer" { try stack.pushAll(&.{42, 10, 95, 0});
var buffer = [_]u8{0} ** 4; try testing.expectEqual(stack.count(), 4);
var sequence_stack = Fixed(u8){.buffer = &buffer}; try testing.expectError(PushError.OutOfMemory, stack.push(1));
const sequence_data = [_]u8{8, 16, 32, 64}; try testing.expectError(PushError.OutOfMemory, stack.pushAll(&.{1, 11, 11}));
try testing.expect(fixedWriter(&sequence_stack).call(&sequence_data) == sequence_data.len); stack.clear();
try testing.expect(io.equals(u8, sequence_stack.buffer, &sequence_data));
try testing.expectEqual(stack.count(), 0);
try testing.expectEqual(stack.writer().apply(&.{0, 0, 0, 0}), 4);
} }

View File

@ -1,18 +0,0 @@
///
/// [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;
}
// TODO: Implement tests.
pub const expectError = @import("std").testing.expectError;

View File

@ -1,84 +0,0 @@
const io = @import("./io.zig");
const math = @import("./math.zig");
///
/// [PrintError.WriteFailure] occurs when the underlying [io.Writer] implementation failed to write
/// the entirety of a the requested print operation.
///
pub const PrintError = error {
WriteFailure,
};
///
/// Number formatting modes supported by [printInt].
///
pub const Radix = enum {
binary,
tinary,
quaternary,
quinary,
senary,
septenary,
octal,
nonary,
decimal,
undecimal,
duodecimal,
tridecimal,
tetradecimal,
pentadecimal,
hexadecimal,
};
///
/// 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 => |int_info| {
if (value == 0) return writer.apply("0");
const base = @enumToInt(radix);
const is_signed = (int_info.signedness == .signed);
var buffer = [_]u8{0} ** (math.ceil(math.log(math.
maxInt(Int), base)) + @boolToInt(is_signed));
var buffer_count: usize = 0;
var n1 = value;
if (is_signed and (value < 0)) {
// Negative value.
n1 = -value;
buffer[0] = '-';
buffer_count += 1;
}
while (n1 != 0) {
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 (writer.call(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 "Print 64-bit signed integer" {
// TODO: implement.
}

253
src/oar/main.zig Normal file
View File

@ -0,0 +1,253 @@
const core = @import("core");
const std = @import("std");
///
///
///
pub const Archive = struct {
pub fn deinit(archive: *Archive) {
}
pub fn init(file_system: *const sys.FileSystem, file_path: sys.Path) {
}
};
///
/// An entry block of an Oar archive file.
///
/// Typically, following the block in memory is the file data it holds the meta-information for.
///
pub const Entry = extern struct {
signature: [signature_magic.len]u8 = signature_magic,
revision: u8,
path: Path,
file_size: u64,
absolute_offset: u64,
padding: [232]u8 = std.mem.zeroes([232]u8),
comptime {
const entry_size = @sizeOf(Entry);
if (entry_size != 512)
@compileError("Entry is " ++
std.fmt.comptimePrint("{d}", .{entry_size}) ++ " bytes");
}
///
/// Attempts to read the next [Entry] from `file_access`.
///
/// Returns the read [Entry], `null` if there is no more to read, or a
/// [core.io.FileAccess.Error] if it failed.
///
pub fn next(file_access: core.io.FileAccess) core.io.FileAccess.Error!?Entry {
const mem = std.mem;
var entry = mem.zeroes(Entry);
const origin = try file_access.queryCursor();
if (((try file_access.read(mem.asBytes(&entry))) != @sizeOf(Entry)) and
core.io.equals(u8, &entry.signature, &signature_magic)) {
try file_access.seek(origin);
return null;
}
return entry;
}
///
/// Magic identifier used to validate [Entry] data.
///
pub const signature_magic = [3]u8{'o', 'a', 'r'};
};
///
/// Unique identifier pointing to an entry within an archive.
///
/// A path does not do any verification that the given entry pointed to actually exists.
///
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 Error = error {
TooLong,
};
///
/// An empty [Path] with a length of `0`.
///
pub const empty = std.mem.zeroes(Path);
///
/// Returns `true` if `this_path` is equal to `that_path, otherwise `false`.
///
pub fn equals(this_path: Path, that_path: Path) bool {
return core.io.equals(u8, this_path.buffer[0 ..this_path.
length], that_path.buffer[0 .. that_path.length]);
}
///
/// Returns the hash of the text in `path`.
///
pub fn hash(path: Path) usize {
return core.io.hashBytes(path.buffer[0 .. path.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 [Error] if it could not be created.
///
pub fn joined(sequences: []const []const u8) Error!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;
}
///
/// Maximum number of **bytes** in a [Path].
///
pub const max = 255;
///
/// Textual separator between components of a [Path].
///
pub const seperator = '/';
};
test "Path" {
const testing = std.testing;
const empty_path = Path.empty;
try testing.expectEqual(empty_path.length, 0);
try testing.expect(empty_path.equals(Path.empty));
const joined_component_path = try Path.joined(&.{"path", "to/my", "/file"});
const joined_normalized_path = try Path.joined(&.{"path/to/my/file"});
try testing.expectEqual(joined_component_path.length, joined_normalized_path.length);
try testing.expect(joined_component_path.equals(joined_normalized_path));
}
///
/// Starts the **O**na **Ar**chive packer utility.
///
pub fn main() u8 {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator();
const out_writer = std.io.getStdOut().writer();
const process = std.process;
const args = process.argsAlloc(allocator) catch {
out_writer.print("Failed to allocate args memory\n", .{}) catch undefined;
return 1;
};
defer process.argsFree(allocator, args);
if (args.len < 2) {
out_writer.print("Usage: oar [OPTION]... [FILE]...\n", .{}) catch undefined;
out_writer.print("Options and arguments\n", .{}) catch undefined;
return 0;
}
const arg = std.mem.sliceTo(args[1], 0);
if (core.io.equals(u8, arg, "--create")) {
if (args.len < 3) {
out_writer.print("Expected output file specified after `--create`\n", .{}) catch undefined;
return 1;
}
var archive = Archive.init(allocator, Path.joined(&.{args[2]})) catch {
out_writer.print("Failed to initialize archive for create\n", .{}) catch undefined;
return 1;
};
defer archive.deinit();
for (args[3 .. ]) |input_file_path| {
const file = std.fs.cwd().openFile(input_file_path) catch {
out_writer.print("Failed to open {s}\n", .{input_file_path}) catch undefined;
return 1;
};
defer file.close();
var entry = archive.open(Path.joined(&.{input_file_path})) catch {
out_writer.print("Failed to open {s}\n", .{input_file_path}) catch undefined;
return 1;
};
defer archive.close(entry);
var copy_buffer = std.mem.zeroes([4096]u8);
while (true) {
const read = try file.read(&copy_buffer);
if (read == 0) break;
try entry.write(copy_buffer[read ..]);
}
}
return 0;
}
out_writer.print("Unrecognized command-line option `{s}`\n", .{arg}) catch undefined;
return 0;
}

View File

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