Port back to Zig
continuous-integration/drone Build is failing Details

This commit is contained in:
kayomn 2023-04-19 00:25:35 +01:00
parent 78ae403f61
commit 359fcd190d
37 changed files with 2772 additions and 2005 deletions

View File

@ -3,7 +3,7 @@ name: continuous integration
steps: steps:
- name: build & test - name: build & test
image: ubuntu:jammy image: euantorano/zig:0.9.1
commands: commands:
- apt update && apt install -y clang libsdl2-dev python3.10 - zig build test
- python3.10 ./build.py - $(find zig-cache -name test) main.zig

5
.gitignore vendored
View File

@ -1,3 +1,2 @@
cache /zig-cache/
runtime /zig-out/
runtime.exe

View File

@ -1,16 +0,0 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/source"
],
"defines": [],
"compilerPath": "/usr/bin/clang++",
"cStandard": "gnu17",
"cppStandard": "c++20",
"intelliSenseMode": "linux-clang-x64"
}
],
"version": 4
}

4
.vscode/launch.json vendored Normal file → Executable file
View File

@ -5,8 +5,8 @@
"name": "Runtime", "name": "Runtime",
"type": "gdb", "type": "gdb",
"request": "launch", "request": "launch",
"target": "./runtime", "target": "./zig-out/bin/ona-runner",
"cwd": "${workspaceRoot}", "cwd": "${workspaceRoot}/debug",
"valuesFormatting": "parseText" "valuesFormatting": "parseText"
}, },
{ {

26
.vscode/settings.json vendored Normal file → Executable file
View File

@ -1,16 +1,18 @@
{ {
"files.associations": { "editor.minimap.maxColumn": 120,
"type_traits": "cpp",
"cassert": "cpp",
"cstddef": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"array": "cpp",
"functional": "cpp",
"tuple": "cpp",
"utility": "cpp"
},
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.insertSpaces": false, "editor.insertSpaces": false,
"C_Cpp.errorSquiggles": "disabled", "editor.rulers": [120],
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"spellright.language": [
"en-US-10-1."
],
"spellright.documentTypes": [
"markdown",
"plaintext",
"zig"
],
"zig.formattingProvider": "off",
"zig.zls.enableAutofix": false,
} }

25
.vscode/tasks.json vendored Executable file
View File

@ -0,0 +1,25 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "process",
"command": "zig",
"args": ["build"],
"problemMatcher": "$gcc",
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true,
"revealProblems": "onProblem"
},
"group": {
"kind": "build",
"isDefault": true
},
}
]
}

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
import os
import subprocess
source_path = "./source/"
cache_path = "./cache/"
if not(os.path.exists(cache_path)):
os.mkdir(cache_path)
compile_command = f"clang++ -g -std=c++20 -fno-exceptions -fmodules -fprebuilt-module-path=./cache"
object_file_paths = []
def compile_module(source_file_path, module_identifier) -> None:
output_path = os.path.join(cache_path, module_identifier)
subprocess.run(f"{compile_command} -Xclang -emit-module-interface -c {source_file_path} -o {output_path}.pcm", shell=True, check=True)
subprocess.run(f"{compile_command} -c {source_file_path} -o {output_path}.o", shell=True, check=True)
object_file_paths.append(f"{output_path}.o")
def compile_package(root_module_name: str) -> None:
root_module_source_path = os.path.join(source_path, root_module_name)
compile_module(f"{root_module_source_path}.cpp", root_module_name)
if os.path.isdir(root_module_source_path):
for file_name in os.listdir(root_module_source_path):
compile_module(os.path.join(root_module_source_path, file_name), f"{root_module_name}.{os.path.splitext(file_name)[0]}")
compile_package("coral")
compile_package("oar")
compile_package("app")
compile_package("runtime")
subprocess.run(f"{compile_command} {' '.join(object_file_paths)} -o ./runtime -lSDL2", shell=True, check=True)

71
build.zig Normal file
View File

@ -0,0 +1,71 @@
const std = @import("std");
pub fn build(builder: *std.Build) void {
const coral_module = builder.createModule(.{.source_file = .{.path = "./source/coral/coral.zig"}});
const ona_module = builder.createModule(.{
.source_file = .{.path = "./source/ona/ona.zig"},
.dependencies = &.{
.{
.name = "coral",
.module = coral_module
},
},
});
const kym_module = builder.createModule(.{
.source_file = .{.path = "./source/kym/kym.zig"},
.dependencies = &.{
.{
.name = "coral",
.module = coral_module
},
},
});
const oar_module = builder.createModule(.{
.source_file = .{.path = "./source/oar/oar.zig"},
.dependencies = &.{
.{
.name = "coral",
.module = coral_module
},
.{
.name = "ona",
.module = ona_module
},
},
});
// Ona Runner.
{
const ona_exe = builder.addExecutable(.{
.name = "ona-runner",
.root_source_file = .{.path = "./source/runner.zig"},
.target = builder.standardTargetOptions(.{}),
.optimize = .Debug,
});
ona_exe.addModule("coral", coral_module);
ona_exe.addModule("ona", ona_module);
ona_exe.addModule("oar", oar_module);
ona_exe.addModule("kym", kym_module);
ona_exe.install();
// ona_exe.addIncludeDir("./ext");
ona_exe.linkSystemLibrary("SDL2");
ona_exe.linkLibC();
const run_cmd = ona_exe.run();
run_cmd.step.dependOn(builder.getInstallStep());
if (builder.args) |args| run_cmd.addArgs(args);
builder.step("run", "Run Ona application").dependOn(&run_cmd.step);
}
}

View File

@ -1,6 +0,0 @@
return {
title = "Demo",
width = 640,
height = 480,
}

22
debug/index.kym Normal file
View File

@ -0,0 +1,22 @@
delta_time = @events.delta_time()
player_sprite = @canvas.create_sprite {
atlas = @import("./player.bmp"),
viewport = @rect(0, 0, 0.1, 0.1),
bounds = @rect(0, 0, 0.05, 0.05)
}
@input.on_axis2d("move", => (axes) {
player_sprite.position = player_sprite.position + (axes * delta_time)
})
@input.on_axis2d("look", => (axes) {
player_sprite.rotation = @atan2(axes.y, axes.x);
})
return {
title = "Demo",
width = 640,
height = 480,
}

View File

@ -1,317 +0,0 @@
module;
#include <SDL2/SDL.h>
export module app;
import coral;
import coral.files;
import coral.image;
import coral.io;
import coral.math;
import oar;
using native_path = coral::fixed_buffer<4096>;
struct native_file : public coral::file_reader, public coral::file_writer {
enum class open_mode {
read_only,
overwrite,
};
enum class [[nodiscard]] close_result {
ok,
io_unavailable,
};
enum class [[nodiscard]] open_result {
ok,
io_unavailable,
access_denied,
not_found,
};
native_file() = default;
close_result close() {
if (SDL_RWclose(this->rw_ops) != 0) return close_result::io_unavailable;
this->rw_ops = nullptr;
return close_result::ok;
}
bool is_open() const {
return this->rw_ops != nullptr;
}
open_result open(native_path const & file_path, open_mode mode) {
if (this->is_open()) switch (this->close()) {
case close_result::ok: break;
case close_result::io_unavailable: return open_result::io_unavailable;
default: coral::unreachable();
}
// No room for zero terminator.
if (file_path.is_full()) return open_result::not_found;
switch (mode) {
case open_mode::read_only: {
this->rw_ops = SDL_RWFromFile(file_path.as_slice().as_chars().begin(), "rb");
break;
}
case open_mode::overwrite: {
this->rw_ops = SDL_RWFromFile(file_path.as_slice().as_chars().begin(), "wb");
break;
}
default: coral::unreachable();
}
if (this->rw_ops == nullptr) return open_result::not_found;
return open_result::ok;
}
coral::expected<coral::usize, coral::io_error> read(coral::slice<coral::u8> const & data) override {
if (!this->is_open()) return coral::io_error::unavailable;
coral::usize const data_read{SDL_RWread(this->rw_ops, data.pointer, sizeof(uint8_t), data.length)};
if ((data_read == 0) && (SDL_GetError() != nullptr)) return coral::io_error::unavailable;
return data_read;
}
coral::expected<coral::u64, coral::io_error> seek(coral::u64 offset) override {
if (!this->is_open()) return coral::io_error::unavailable;
// TODO: Fix cast.
coral::i64 const byte_position{
SDL_RWseek(this->rw_ops, static_cast<coral::i64>(offset), RW_SEEK_SET)};
if (byte_position == -1) return coral::io_error::unavailable;
return static_cast<coral::u64>(byte_position);
}
coral::expected<coral::u64, coral::io_error> tell() override {
if (!this->is_open()) return coral::io_error::unavailable;
coral::i64 const byte_position{SDL_RWseek(this->rw_ops, 0, RW_SEEK_SET)};
if (byte_position == -1) return coral::io_error::unavailable;
return static_cast<coral::u64>(byte_position);
}
coral::expected<coral::usize, coral::io_error> write(coral::slice<coral::u8 const> const & data) override {
if (!this->is_open()) return coral::io_error::unavailable;
coral::usize const data_written{SDL_RWwrite(this->rw_ops, data.pointer, sizeof(uint8_t), data.length)};
if ((data_written == 0) && (SDL_GetError() != nullptr)) return coral::io_error::unavailable;
return data_written;
}
private:
SDL_RWops * rw_ops{nullptr};
};
struct sandboxed_fs : public coral::fs {
sandboxed_fs() {
char * const path{SDL_GetBasePath()};
if (path == nullptr) return;
for (coral::usize index = 0; path[index] != 0; index += 1)
this->sandbox_path.put(path[index]);
SDL_free(path);
this->access_rules.can_read = true;
}
sandboxed_fs(coral::path const & organization_name, coral::path const & app_name) {
char * const path{SDL_GetPrefPath(organization_name.begin(), app_name.begin())};
if (path == nullptr) return;
for (coral::usize index = 0; path[index] != 0; index += 1)
this->sandbox_path.put(path[index]);
SDL_free(path);
this->access_rules.can_read = true;
}
access_rules query_access() override {
return this->access_rules;
}
void read_file(coral::path const & file_path, coral::closure<void(coral::file_reader &)> const & then) override {
if (!this->access_rules.can_read) return;
native_path sandbox_file_path;
{
coral::expected const written = sandbox_file_path.write(this->sandbox_path.as_slice());
if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return;
}
{
coral::expected const written =
sandbox_file_path.write(file_path.as_slice().as_bytes());
if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return;
}
native_file file;
if (file.open(sandbox_file_path, native_file::open_mode::read_only) !=
native_file::open_result::ok) return;
then(file);
if (file.close() != native_file::close_result::ok)
// Error orphaned file handle!
return;
}
void write_file(coral::path const & file_path, coral::closure<void(coral::file_writer &)> const & then) override {
if (!this->access_rules.can_write) return;
native_path sandbox_file_path;
{
coral::expected const written = sandbox_file_path.write(this->sandbox_path.as_slice());
if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return;
}
{
coral::expected const written =
sandbox_file_path.write(file_path.as_slice().as_bytes());
if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return;
}
native_file file;
if (file.open(sandbox_file_path, native_file::open_mode::overwrite) !=
native_file::open_result::ok) return;
then(file);
if (file.close() != native_file::close_result::ok)
// Error orphaned file handle!
return;
}
private:
native_path sandbox_path;
access_rules access_rules{
.can_read = false,
.can_write = false,
};
};
export namespace app {
enum class log_level {
notice,
warning,
error,
};
struct client {
coral::fs & base() {
return this->base_sandbox;
}
void display(coral::u16 screen_width, coral::u16 screen_height) {
SDL_SetWindowSize(this->window, screen_width, screen_height);
SDL_ShowWindow(this->window);
}
void log(log_level level, coral::slice<char const> const & message) {
coral::i32 const length{static_cast<coral::i32>(
coral::min(message.length, static_cast<size_t>(coral::i32_max)))};
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
SDL_LOG_PRIORITY_INFO, "%.*s", length, message.pointer);
}
bool poll() {
while (SDL_PollEvent(&this->event) != 0) {
switch (this->event.type) {
case SDL_QUIT: return false;
}
}
return true;
}
coral::fs & resources() {
return this->resources_archive;
}
static int run(coral::path const & title, coral::closure<int(client &)> const & start) {
constexpr int windowpos {SDL_WINDOWPOS_UNDEFINED};
constexpr coral::u32 windowflags {SDL_WINDOW_HIDDEN};
constexpr int window_width {640};
constexpr int window_height {480};
SDL_Window * const window {SDL_CreateWindow(
title.begin(), windowpos, windowpos, window_width, window_height, windowflags)};
if (window == nullptr) return 0xff;
struct : public coral::allocator {
coral::u8 * reallocate(coral::u8 * allocation, coral::usize requested_size) override {
return reinterpret_cast<coral::u8 *>(SDL_realloc(allocation, requested_size));
}
void deallocate(void * allocation) override {
SDL_free(allocation);
}
} allocator;
client app_client {&allocator, window, title};
return start(app_client);
}
coral::allocator & thread_safe_allocator() {
return *this->allocator;
}
coral::fs & user() {
return this->user_sandbox;
}
private:
client(coral::allocator * allocator, SDL_Window * window,
coral::path const & title) : user_sandbox{"ona", title} {
this->allocator = allocator;
this->window = window;
}
coral::allocator * allocator;
SDL_Window * window;
SDL_Event event;
sandboxed_fs base_sandbox;
sandboxed_fs user_sandbox;
oar::archive resources_archive{&base_sandbox, "base.oar"};
};
}

View File

@ -1,596 +0,0 @@
module;
#include <cstdint>
#include <cstddef>
#include <type_traits>
export module coral;
// Runtime utilities.
export namespace coral {
/**
* Triggers safety-checked behavior in debug mode.
*
* In release mode, the compiler can use this function as a marker to optimize out safety-
* checked logic branches that should never be executed.
*/
[[noreturn]] void unreachable() {
__builtin_unreachable();
}
}
// Concrete and interface types.
export namespace coral {
using usize = size_t;
using size = __ssize_t;
using u8 = uint8_t;
usize const u8_max = 0xff;
using i8 = uint8_t;
using u16 = uint16_t;
usize const u16_max = 0xffff;
using i16 = uint16_t;
using u32 = uint32_t;
using i32 = int32_t;
usize const i32_max = 0xffffffff;
using u64 = uint64_t;
using i64 = int64_t;
using f32 = float;
using f64 = double;
/**
* Base type for runtime-pluggable memory allocation strategies used by the core library.
*/
struct allocator {
virtual ~allocator() {};
/**
* If `allocation` is `nullptr`, the allocator will attempt to allocate a new memory block
* of `requested_size` bytes. Otherwise, the allocator will attempt to reallocate
* `allocation` to be `request_size` bytes in size.
*
* The returned address will point to a dynamically allocated buffer of `requested_size` if
* the operation was successful, otherwise `nullptr`.
*
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to
* program exit. This may be achieved through either [deallocate] or implementation-
* specific allocator functionality.
*
* *Note*: Attempting to pass a non-`nullptr` `allocation` address not allocated by the
* allocator *will* result in erroneous implementation-behavior.
*
* *Note*: After invocation, `allocation` should be considered an invalid memory address.
*/
[[nodiscard]] virtual u8 * reallocate(u8 * allocation, usize requested_size) = 0;
/**
* If `allocation` points to a non-`nullptr` address, the allocator will deallocate it.
* Otherwise, the function has no side-effects.
*
* *Note* that attempting to pass a non-`nullptr` `allocation` address not allocated by the
* allocator *will* result in erroneous implementation-behavior.
*/
virtual void deallocate(void * allocation) = 0;
};
/**
* Length-signed pointer type that describes how many elements of `type` it references,
* providing a type-safe wrapper for passing arrays and zero-terminated strings to functions.
*/
template<typename type> struct slice {
/**
* Number of `type` elements referenced.
*/
usize length{0};
/**
* Base element address referenced.
*/
type * pointer{nullptr};
constexpr slice() = default;
constexpr slice(char const *&& zstring) {
this->pointer = zstring;
this->length = 0;
while (zstring[length] != 0) this->length += 1;
}
constexpr slice(type * slice_pointer, usize slice_length) {
this->pointer = slice_pointer;
this->length = slice_length;
}
constexpr slice(type * slice_begin, type * slice_end) {
this->pointer = slice_begin;
this->length = static_cast<usize>(slice_end - slice_begin);
}
template<usize array_size> constexpr slice(type(&array)[array_size]) {
this->pointer = array;
this->length = array_size;
}
/**
* Reinterprets the data referenced as a series of bytes.
*
* The returned view is constant to protect against inadvertant memory corruption.
*/
slice<u8 const> as_bytes() const {
return {reinterpret_cast<u8 const *>(this->pointer), this->length * sizeof(type)};
}
/**
* Reinterprets the data referenced as a series of chars.
*
* The returned view is constant to protect against inadvertant memory corruption.
*
* *Note* the returned value has no guarantees about the validity of any specific character
* encoding set.
*/
slice<char const> as_chars() const {
return {reinterpret_cast<char const *>(this->pointer), this->length * sizeof(type)};
}
/**
* Returns the base pointer of the slice.
*/
constexpr type * begin() const {
return this->pointer;
}
/**
* Returns the tail pointer of the slice.
*/
constexpr type * end() const {
return this->pointer + this->length;
}
/**
* Returns a new slice with the base-pointer offset by `index` elements and a length of
* `range` elements from `index`.
*
* *Note* that attempting to slice with an `index` or `range` outside of the existing slice
* bounds will result in safety-checked behavior.
*/
constexpr slice sliced(usize index, usize range) const {
if ((this->length <= index) || ((range + index) > this->length)) unreachable();
return {this->pointer + index, range - index};
}
operator slice<type const>() const {
return (*reinterpret_cast<slice<type const> const *>(this));
}
constexpr type & operator[](usize index) const {
if (this->length <= index) unreachable();
return this->pointer[index];
}
};
}
// Math functions.
export namespace coral {
/**
* Returns the maximum value between `a` and `b`.
*/
template<typename scalar> constexpr scalar max(scalar const & a, scalar const & b) {
return (a > b) ? a : b;
}
/**
* Returns the minimum value between `a` and `b`.
*/
template<typename scalar> constexpr scalar min(scalar const & a, scalar const & b) {
return (a < b) ? a : b;
}
/**
* Returns `value` clamped between the range of `min_value` and `max_value` (inclusive).
*/
template<typename scalar> constexpr scalar clamp(scalar const & value, scalar const & min_value, scalar const & max_value) {
return max(min_value, min(max_value, value));
}
/**
* Returns `value` rounded to the nearest whole number.
*/
f32 round32(f32 value) {
return __builtin_roundf(value);
}
}
/**
* Allocates and initializes a type of `requested_size` in `buffer`, returning its base pointer. As
* a result of accepting a pre-allocated buffer, invocation does not allocate any dynamic memory.
*
* *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked
* behavior.
*/
export void * operator new(coral::usize requested_size, coral::slice<coral::u8> const & buffer) {
if (buffer.length < requested_size) coral::unreachable();
return buffer.pointer;
}
/**
* Allocates and initializes a series of types at `requested_size` in `buffer`, returning the base
* pointer. As a result of accepting a pre-allocated buffer, invocation does not allocate any
* dynamic memory.
*
* *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked
* behavior.
*/
export void * operator new[](coral::usize requested_size, coral::slice<coral::u8> const & buffer) {
if (buffer.length < requested_size) coral::unreachable();
return buffer.pointer;
}
/**
* Attempts to allocate and initialize a type of `requested_size` using `allocator`.
*
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to program
* exit. This may be achieved through either [coral::allocator::deallocate] or implementation-
* specific allocator functionality.
*/
export [[nodiscard]] void * operator new(coral::usize requested_size, coral::allocator & allocator) {
return allocator.reallocate(nullptr, requested_size);
}
/**
* Attempts to allocate and initialize a series of types of `requested_size` using `allocator`.
*
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to program
* exit. This may be achieved through either [coral::allocator::deallocate] or implementation-
* specific allocator functionality.
*/
export [[nodiscard]] void * operator new[](coral::usize requested_size, coral::allocator & allocator) {
return allocator.reallocate(nullptr, requested_size);
}
/**
* If `pointer` is a non-`nullptr` value, the referenced memory will be deallocated using
* `allocator`. Otherwise, the function has no side-effects.
*
* *Note*: passing a `pointer` value that was not allocated by `allocator` will result in erroneous
* behavior defined by the [coral::allocator] implementation.
*/
export void operator delete(void * pointer, coral::allocator & allocator) {
return allocator.deallocate(pointer);
}
/**
*
*/
export void operator delete[](void * pointer, coral::allocator & allocator) {
return allocator.deallocate(pointer);
}
// Wrapper types.
export namespace coral {
template<typename> struct closure;
/**
* Type-erasing view wrapper for both function and functor types that have a call operator with
* a return value matching `return_value` and arguments matching `argument_values`.
*
* **Note**: closures take no ownership of allocated memory, making it the responsibility of
* the caller to manage the lifetime of any functor assigned to it.
*/
template<typename returns, typename... arguments> struct closure<returns(arguments...)> {
using function = returns(*)(arguments...);
closure(function callable_function) {
this->dispatch = [](void const * context, arguments... dispatch_arguments) -> returns {
return (reinterpret_cast<function const *>(context))(dispatch_arguments...);
};
this->context = callable_function;
}
template<typename functor> closure(functor * callable_functor) {
this->dispatch = [](void const * context, arguments... dispatch_arguments) -> returns {
return (*reinterpret_cast<functor const*>(context))(dispatch_arguments...);
};
this->context = callable_functor;
}
closure(closure const &) = delete;
template<typename functor> closure(functor && callable_functor) {
this->dispatch = [](void const * context, arguments... dispatch_arguments) -> returns {
return (*reinterpret_cast<functor const*>(context))(dispatch_arguments...);
};
this->context = &callable_functor;
}
returns operator()(arguments const &... call_arguments) const {
return this->dispatch(this->context, call_arguments...);
}
private:
void const * context;
returns(* dispatch)(void const *, arguments...);
};
/**
* Monadic container for a single-`element` value or nothing.
*/
template<typename element> struct [[nodiscard]] optional {
optional() : buffer{0} {}
optional(element const & value) : buffer{0} {
(*reinterpret_cast<element *>(this->buffer)) = value;
this->buffer[sizeof(element)] = 1;
}
optional(optional const & that) : buffer{0} {
if (that.has_value()) {
(*reinterpret_cast<element *>(this->buffer)) = *that;
this->buffer[sizeof(element)] = 1;
} else {
this->buffer[sizeof(element)] = 0;
}
}
/**
* Returns `true` if the optional contains a value, otherwise `false`.
*/
bool has_value() const {
return this->buffer[sizeof(element)] == 1;
}
/**
* Attempts to call `apply` on the contained value, returning a new [optional] of whatever type `apply` returns.
*
* If the optional is empty, an empty optional will always be returned.
*/
template<typename functor> std::invoke_result_t<functor, element> map(functor const & apply) const {
if (this->has_value()) return apply(**this);
return {};
}
/**
* Returns the contained value or `fallback` if the optional is empty.
*/
element const & value_or(element const & fallback) const {
return this->has_value() ? *reinterpret_cast<element const *>(this->buffer) : fallback;
}
element & operator *() {
if (!this->has_value()) unreachable();
return *reinterpret_cast<element *>(this->buffer);
}
element const & operator *() const {
if (!this->has_value()) unreachable();
return *reinterpret_cast<element const *>(this->buffer);
}
private:
u8 buffer[sizeof(element) + 1];
};
/**
* Monadic container for a descriminating union of either `value_element` or `error_element`.
*/
template<typename value_element, typename error_element> struct [[nodiscard]] expected {
expected(value_element const & value) : buffer{0} {
(*reinterpret_cast<value_element *>(this->buffer)) = value;
this->buffer[buffer_size] = 1;
}
expected(error_element const & error) : buffer{0} {
(*reinterpret_cast<error_element *>(this->buffer)) = error;
}
/**
* Monadic function for calling `predicate` conditionally based on whether the expected is
* ok. If ok, the result of `predicate` is returned, otherwise `false` is always returned.
*
* This function may be used to chain conditional checks that depend on the expected being
* ok without creating a new local variable.
*/
bool and_test(closure<bool(value_element const &)> const & predicate) const {
return this->is_ok() && predicate(this->value());
}
/**
* Returns `true` if the optional contains a value, otherwise `false` if it holds an error.
*/
bool is_ok() const {
return this->buffer[buffer_size];
}
/**
* Returns a reference to the contained value.
*
* *Note*: attempting to access the value of an erroneous expected will trigger safety-
* checked behavior.
*/
value_element & value() {
if (!this->is_ok()) unreachable();
return *reinterpret_cast<value_element *>(this->buffer);
}
/**
* Returns the contained value.
*
* *Note*: attempting to access the value of an erroneous expected will trigger safety-
* checked behavior.
*/
value_element const & value() const {
if (!this->is_ok()) unreachable();
return *reinterpret_cast<value_element const *>(this->buffer);
}
/**
* Returns a reference to the contained error.
*
* *Note*: attempting to access the error of a non-erroneous expected will trigger safety-
* checked behavior.
*/
error_element & error() {
if (this->is_ok()) unreachable();
return *reinterpret_cast<error_element *>(this->buffer);
}
/**
* Returns the contained error.
*
* *Note*: attempting to access the error of a non-erroneous expected will trigger safety-
* checked behavior.
*/
error_element const & error() const {
if (this->is_ok()) unreachable();
return *reinterpret_cast<error_element const *>(this->buffer);
}
private:
static constexpr usize buffer_size = max(sizeof(value_element), sizeof(error_element));
u8 buffer[buffer_size + 1];
};
/**
* Errors that may occur while executing an opaque I/O operation via the `readable` and
* `writable` type aliases.
*/
enum class io_error {
unavailable,
};
/**
* Readable resource interface.
*/
struct reader {
virtual ~reader() {}
/**
* Attempts to fill `data` with whatever data the reader has to offer, returning the number
* of bytes actually read.
*
* Should the read operation fail for any reason, a [io_error] is returned instead.
*/
virtual expected<usize, io_error> read(slice<u8> const & data) = 0;
};
/**
* Writable resource interface.
*/
struct writer {
virtual ~writer() {}
/**
* Attempts to write `data` out to the writer, returning the number of bytes actually
* written.
*
* Should the write operation fail for any reason, a [io_error] is returned instead.
*/
virtual expected<usize, io_error> write(slice<u8 const> const & data) = 0;
};
}
// Input/output operations.
export namespace coral {
/**
* Returns `value` reinterpreted as a sequence of bytes.
*/
slice<u8 const> as_bytes(auto const * value) {
return {reinterpret_cast<u8 const *>(value), sizeof(value)};
}
/**
* Compares `a` and `b`, returning the difference between them or `0` if they are identical.
*/
constexpr size compare(slice<u8 const> const & a, slice<u8 const> const & b) {
usize const range = min(a.length, b.length);
for (usize index = 0; index < range; index += 1) {
size const difference = static_cast<size>(a[index]) - static_cast<size>(b[index]);
if (difference != 0) return difference;
}
return static_cast<size>(a.length) - static_cast<size>(b.length);
}
/**
* Copies the contents of `origin` into `target`.
*
* *Note*: safety-checked behavior is triggered if `target` is smaller than `origin`.
*/
void copy(slice<u8> const & target, slice<u8 const> const & origin) {
if (target.length < origin.length) unreachable();
for (usize i = 0; i < origin.length; i += 1) target[i] = origin[i];
}
/**
* Zeroes the contents of `target`.
*/
void zero(slice<u8> const & target) {
for (usize i = 0; i < target.length; i += 1) target[i] = 0;
}
/**
* Tests the equality of `a` against `b`, returning `true` if they contain identical bytes,
* otherwise `false`.
*/
constexpr bool equals(slice<u8 const> const & a, slice<u8 const> const & b) {
if (a.length != b.length) return false;
for (size_t i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false;
return true;
}
/**
* Returns a hash code generated from the values in `bytes`.
*
* *Note:* the returned hash code is not guaranteed to be unique.
*/
constexpr usize hash(slice<u8 const> const & bytes) {
usize hash_code = 5381;
for (u8 const byte : bytes) hash_code = ((hash_code << 5) + hash_code) + byte;
return hash_code;
}
/**
* Swaps the values of `element` in `a` and `b` around using copy semantics.
*/
template<typename element> constexpr void swap(element & a, element & b) {
element const temp = a;
a = b;
b = temp;
}
}

65
source/coral/buffer.zig Normal file
View File

@ -0,0 +1,65 @@
const io = @import("./io.zig");
const math = @import("./math.zig");
pub const Fixed = struct {
data: []u8,
write_index: usize = 0,
pub fn as_writer(self: *Fixed) io.Writer {
return io.Writer.bind(self, Fixed);
}
pub fn remaining(self: Fixed) usize {
return self.data.len - self.write_index;
}
pub fn is_empty(self: Fixed) bool {
return !self.is_full();
}
pub fn is_full(self: Fixed) bool {
return self.write_index == self.data.len;
}
pub fn write(self: *Fixed, buffer: []const u8) usize {
const range = math.min(usize, buffer.len, self.remaining());
io.copy(self.data[self.write_index ..], buffer[0 .. range]);
self.write_index += range;
return range;
}
};
pub const Ring = struct {
data: []u8,
read_index: usize = 0,
write_index: usize = 0,
pub fn as_reader(self: *Ring) io.Reader {
return io.Reader.bind(self, Ring);
}
pub fn as_writer(self: *Ring) io.Writer {
return io.Writer.bind(self, Ring);
}
pub fn filled(self: Ring) usize {
return (self.write_index + (2 * self.data.len *
@boolToInt(self.write_index < self.read_index))) - self.read_index;
}
pub fn is_empty(self: Ring) bool {
return self.write_index == self.read_index;
}
pub fn is_full(self: Ring) bool {
return ((self.write_index + self.data.len) % (self.data.len * 2)) != self.read_index;
}
pub fn remaining(self: Ring) usize {
return self.data.len - self.filled();
}
};

15
source/coral/coral.zig Normal file
View File

@ -0,0 +1,15 @@
pub const buffer = @import("./buffer.zig");
pub const debug = @import("./debug.zig");
pub const format = @import("./format.zig");
pub const io = @import("./io.zig");
pub const math = @import("./math.zig");
pub const stack = @import("./stack.zig");
pub const table = @import("./table.zig");
pub const utf8 = @import("./utf8.zig");

4
source/coral/debug.zig Normal file
View File

@ -0,0 +1,4 @@
pub fn assert(condition: bool) void {
if (!condition) unreachable;
}

View File

@ -1,157 +0,0 @@
export module coral.files;
import coral;
export namespace coral {
/**
* Platform-generalized identifier for a resource in a [file_store].
*/
struct path {
/**
* Maximum path length.
*/
static usize const max = u8_max;
/**
* Common path component separator.
*/
static char const seperator = '/';
constexpr path() : buffer{0} {
this->buffer[max] = max;
}
template<usize text_size> constexpr path(char const(&text)[text_size]) : path{} {
static_assert(text_size <= max);
for (usize i = 0; i < text_size; i += 1) this->buffer[i] = text[i];
this->buffer[max] = max - text_size;
}
/**
* Returns a weak reference to the [path] as a [slice].
*/
constexpr slice<char const> as_slice() const {
return {this->buffer, this->byte_size()};
}
/**
* Returns the base pointer of the path name.
*
* *Note*: the returned buffer pointer is guaranteed to end with a zero terminator.
*/
char const * begin() const {
return reinterpret_cast<char const *>(this->buffer);
}
/**
* Returns the number of bytes composing the path.
*/
constexpr usize byte_size() const {
return max - this->buffer[max];
}
/**
* Compares the path to `that`, returning the difference between the two paths or `0` if
* they are identical.
*/
constexpr size compare(path const & that) const {
return coral::compare(this->as_slice().as_bytes(), that.as_slice().as_bytes());
}
/**
* Returns the tail pointer of the path name.
*/
char const * end() const {
return this->buffer + this->byte_size();
}
/**
* Tests the path against `that` for equality, returning `true` if they are identical,
* otherwise `false`.
*/
constexpr bool equals(path const & that) const {
return coral::equals(this->as_slice().as_bytes(), that.as_slice().as_bytes());
}
/**
* Returns the path hash code.
*
* *Note:* the returned hash code is not guaranteed to be unique.
*/
constexpr u64 hash() const {
return coral::hash(this->as_slice().as_bytes());
}
/**
* Returns a new [path] composed of the current path joined with `text`.
*
* *Note:* should the new path exceed [max] bytes in size, an empty [path] is returned instead.
*/
constexpr path joined(slice<char const> const & text) const {
if (text.length > this->buffer[max]) return path{};
path joined_path = *this;
for (char const c : text) {
joined_path.buffer[joined_path.byte_size()] = c;
joined_path.buffer[max] -= 1;
}
return joined_path;
}
private:
char buffer[max + 1];
};
struct file_reader : public reader {
virtual ~file_reader() {}
virtual expected<u64, io_error> seek(u64 offset) = 0;
virtual expected<u64, io_error> tell() = 0;
};
struct file_writer : public writer {
virtual ~file_writer() {}
};
/**
* Platform-generalized file system interface.
*/
struct fs {
/**
* Descriptor of the various rules that the file-system enforces over access to its files.
*/
struct access_rules {
bool can_read;
bool can_write;
};
virtual ~fs() {};
/**
* Queries the file-system for its global [access_rules], returning them.
*/
virtual access_rules query_access() = 0;
/**
* Attempts to read the file in the file system located at `file_path` relative, calling
* `then` if it was successfully opened for reading.
*
* Once `then` returns, access to the file is closed automatically.
*/
virtual void read_file(path const & file_path, closure<void(file_reader &)> const & then) = 0;
/**
* Attempts to write the file in the file system located at `file_path` relative, calling
* `then` if it was successfully opened for writing.
*
* Once `then` returns, access to the file is closed automatically.
*/
virtual void write_file(path const & file_path, closure<void(file_writer &)> const & then) = 0;
};
}

45
source/coral/format.zig Normal file
View File

@ -0,0 +1,45 @@
const io = @import("./io.zig");
pub const Value = union(enum) {
newline: void,
string: []const u8,
unsigned: u128,
};
pub fn print(writer: io.Writer, values: []const Value) io.WriteError!usize {
var written: usize = 0;
for (values) |value| written += switch (value) {
.newline => try writer.write("\n"),
.string => |string| try writer.write(string),
.unsigned => |unsigned| try print_unsigned(writer, unsigned),
};
return written;
}
pub fn print_unsigned(writer: io.Writer, value: u128) io.WriteError!usize {
if (value == 0) return writer.write("0");
var buffer = [_]u8{0} ** 39;
var buffer_count: usize = 0;
var split_value = value;
while (split_value != 0) : (buffer_count += 1) {
const radix = 10;
buffer[buffer_count] = @intCast(u8, (split_value % radix) + '0');
split_value = (split_value / radix);
}
{
const half_buffer_count = buffer_count / 2;
var index: usize = 0;
while (index < half_buffer_count) : (index += 1) {
io.swap(u8, &buffer[index], &buffer[buffer_count - index - 1]);
}
}
return writer.write(buffer[0 .. buffer_count]);
}

View File

@ -1,58 +0,0 @@
export module coral.image;
import coral;
export namespace coral {
/**
* All-purpose color value for red, green, blue, alpha channel-encoded values.
*/
struct color {
/**
* Red channel.
*/
float r;
/**
* Green channel.
*/
float g;
/**
* Blue channel.
*/
float b;
/**
* Alpha channel.
*/
float a;
/**
* Red channel represented in an 8-bit unsigned value.
*/
u8 to_r8() const {
return static_cast<u8>(round32(clamp(this->r, 0.0f, 1.0f) * u8_max));
}
/**
* Green channel represented in an 8-bit unsigned value.
*/
u8 to_g8() const {
return static_cast<u8>(round32(clamp(this->g, 0.0f, 1.0f) * u8_max));
}
/**
* Blue channel represented in an 8-bit unsigned value.
*/
u8 to_b8() const {
return static_cast<u8>(round32(clamp(this->b, 0.0f, 1.0f) * u8_max));
}
/**
* Alpha channel represented in an 8-bit unsigned value.
*/
u8 to_a8() const {
return static_cast<u8>(round32(clamp(this->a, 0.0f, 1.0f) * u8_max));
}
};
}

View File

@ -1,199 +0,0 @@
export module coral.io;
import coral;
export namespace coral {
/**
* Multiplexing byte-based ring buffer of `capacity` size that may be used for memory-backed
* I/O operations and lightweight data construction.
*/
template<usize capacity> struct fixed_buffer : public writer, public reader {
fixed_buffer() = default;
/**
* Returns a mutable [slice] ranging from the head to the last-filled element.
*
* *Note*: The lifetime and validity of the returned slice is only guaranteed for as long
* as the source [fixed_buffer] is not mutated or out-of-scope.
*/
slice<u8 const> as_slice() const {
return {0, this->filled};
}
/**
* Returns the base pointer of the buffer data.
*/
u8 const * begin() const {
return this->data;
}
/**
* Returns the number of bytes in the buffer that have been filled with data.
*/
usize count() const {
return this->filled;
}
/**
* Returns the tail pointer of the buffer data.
*/
u8 const * end() const {
return this->data + this->cursor;
}
/**
* Returns `true` if the buffer is completely empty of data, otherwise `false`.
*/
bool is_empty() const {
return this->filled == capacity;
}
/**
* Returns `true` if the buffer has been completely filled with data, otherwise `false`.
*/
bool is_full() const {
return this->filled == capacity;
}
/**
* Attempts to write the single value `data` into the buffer, returning `true` if
* successful, otherwise `false` if the buffer is full.
*/
bool put(u8 data) {
if (this->is_full()) return false;
this->filled += 1;
this->data[this->write_index] = data;
this->write_index = (this->write_index + 1) % capacity;
return true;
}
/**
* Reads whatever data is in the buffer into `data`, returning the number of bytes read
* from the buffer.
*/
expected<usize, io_error> read(slice<u8> const & data) override {
slice const readable_data{this->data, min(this->filled, data.length)};
this->filled -= readable_data.length;
for (usize index = 0; index < readable_data.length; index += 1) {
data[index] = this->data[this->read_index];
this->read_index = (this->read_index + 1) % capacity;
}
return readable_data.length;
}
/**
* Attempts to write `data` to the buffer, returning the number of bytes written or
* [io_error::unavailable] if it has been completely filled and no more bytes can be
* written.
*/
expected<usize, io_error> write(slice<u8 const> const & data) override {
if (this->is_full()) return io_error::unavailable;
slice const writable_data{data.sliced(0, min(data.length, this->filled))};
this->filled += writable_data.length;
for (usize index = 0; index < writable_data.length; index += 1) {
this->data[this->write_index] = data[index];
this->write_index = (this->write_index + 1) % capacity;
}
return writable_data.length;
}
private:
usize filled {0};
usize read_index {0};
usize write_index {0};
u8 data[capacity]{0};
};
/**
* Streams the data from `input` to `output`, using `buffer` as temporary transfer space.
*
* The returned [expected] can be used to introspect if `input` or `output` encountered any
* issues during streaming, otherwise it will contain the number of bytes streamed.
*
* *Note*: if `buffer` has a length of `0`, no data will be streamed as there is nowhere to
* temporarily place data during streaming.
*/
expected<usize, io_error> stream(writer & output, reader & input, slice<u8> const & buffer) {
usize total_bytes_written = 0;
expected bytes_read = input.read(buffer);
if (!bytes_read.is_ok()) return bytes_read.error();
usize read = bytes_read.value();
while (read != 0) {
expected const bytes_written = output.write(buffer.sliced(0, read));
if (!bytes_written.is_ok()) return bytes_read.error();
total_bytes_written += bytes_written.value();
bytes_read = input.read(buffer);
if (!bytes_read.is_ok()) return bytes_read.error();
read = bytes_read.value();
}
return total_bytes_written;
}
/**
* Returns a reference to a shared [allocator] which will always return `nullptr` on calls to
* [allocator::reallocate].
*/
allocator & null_allocator() {
static struct : public allocator {
u8 * reallocate(u8 * maybe_allocation, usize requested_size) override {
if (maybe_allocation != nullptr) unreachable();
return nullptr;
}
void deallocate(void * allocation) override {
if (allocation != nullptr) unreachable();
}
} a;
return a;
}
/**
* Attempts to format and print `value` as an unsigned integer out to `output`.
*
* The returned [expected] can be used to introspect if `output` encountered any issues during
* printing, otherwise it will contain the number of characters used to print `value` as text.
*/
expected<usize, io_error> print_unsigned(writer & output, u64 value) {
if (value == 0) return output.write(slice{"0"}.as_bytes());
u8 buffer[20]{0};
usize buffer_count{0};
while (value != 0) {
constexpr usize radix{10};
buffer[buffer_count] = static_cast<u8>((value % radix) + '0');
value = (value / radix);
buffer_count += 1;
}
usize const half_buffer_count{buffer_count / 2};
for (usize i = 0; i < half_buffer_count; i += 1)
swap(buffer[i], buffer[buffer_count - i - 1]);
return output.write({buffer, buffer_count});
}
}

219
source/coral/io.zig Normal file
View File

@ -0,0 +1,219 @@
const debug = @import("./debug.zig");
const math = @import("./math.zig");
pub const MemoryArena = struct {
pub fn as_allocator(self: *MemoryArena) MemoryAllocator {
return MemoryAllocator.bind(self, MemoryArena);
}
};
pub const MemoryAllocator = struct {
context: *anyopaque,
call: *const fn (capture: *anyopaque, maybe_allocation: ?[*]u8, size: usize) ?[*]u8,
const Capture = [@sizeOf(usize)]u8;
pub fn bind(state: anytype, comptime Actions: type) MemoryAllocator {
const State = @TypeOf(state);
const state_info = @typeInfo(State);
if (state_info != .Pointer) @compileError("`@typeOf(state)` must be a pointer type");
return .{
.context = state,
.call = struct {
fn reallocate(context: *anyopaque, maybe_allocation: ?[*]u8, size: usize) ?[*]u8 {
return Actions.reallocate(@ptrCast(State, @alignCast(@alignOf(state_info.Pointer.child), context)), maybe_allocation, size);
}
}.reallocate,
};
}
pub fn allocate_many(self: MemoryAllocator, comptime Type: type, amount: usize) ?[*]Type {
return @ptrCast(?[*]Type, @alignCast(@alignOf(Type), self.call(self.context, null, @sizeOf(Type) * amount)));
}
pub fn allocate_one(self: MemoryAllocator, comptime Type: type) ?*Type {
return @ptrCast(?*Type, @alignCast(@alignOf(Type), self.call(self.context, null, @sizeOf(Type))));
}
pub fn deallocate(self: MemoryAllocator, maybe_allocation: anytype) void {
if (@typeInfo(@TypeOf(maybe_allocation)) != .Pointer)
@compileError("`maybe_allocation` must be a pointer type");
debug.assert(self.call(self.context, @ptrCast(?[*]u8, maybe_allocation), 0) == null);
}
pub fn reallocate(self: MemoryAllocator, comptime Type: type, maybe_allocation: ?[*]Type, amount: usize) ?[*]Type {
return @ptrCast(?[*]Type, @alignCast(@alignOf(Type),
self.call(self.context, @ptrCast(?[*]u8, maybe_allocation), @sizeOf(Type) * amount)));
}
};
pub const ReadError = error{
IoUnavailable,
};
pub const Reader = struct {
context: *anyopaque,
call: *const fn (context: *anyopaque, buffer: []u8) ReadError!usize,
pub fn bind(state: anytype, comptime Actions: type) Reader {
const State = @TypeOf(state);
const state_info = @typeInfo(State);
if (@typeInfo(State) != .Pointer) @compileError("`@typeOf(state)` must be a pointer type");
return .{
.context = @ptrCast(*anyopaque, state),
.call = struct {
fn read(context: *anyopaque, buffer: []u8) ReadError!usize {
return Actions.read(@ptrCast(State, @alignCast(@alignOf(state_info.Pointer.child), context)), buffer);
}
}.read,
};
}
pub fn read(self: Reader, buffer: []u8) ReadError!usize {
return self.call(self.context, buffer);
}
};
pub fn Tag(comptime Element: type) type {
return switch (@typeInfo(Element)) {
.Enum => |info| info.tag_type,
.Union => |info| info.tag_type orelse @compileError(@typeName(Element) ++ " has no tag type"),
else => @compileError("expected enum or union type, found '" ++ @typeName(Element) ++ "'"),
};
}
pub const WriteError = error{
IoUnavailable,
};
pub const Writer = struct {
context: *anyopaque,
call: *const fn (context: *anyopaque, buffer: []const u8) WriteError!usize,
pub fn bind(state: anytype, comptime Actions: type) Writer {
const State = @TypeOf(state);
const state_info = @typeInfo(State);
if (state_info != .Pointer) @compileError("`@typeOf(state)` must be a pointer type");
return .{
.context = @ptrCast(*anyopaque, state),
.call = struct {
fn write(context: *anyopaque, buffer: []const u8) ReadError!usize {
return Actions.write(@ptrCast(State, @alignCast(@alignOf(state_info.Pointer.child), context)), buffer);
}
}.write,
};
}
pub fn write(self: Writer, buffer: []const u8) WriteError!usize {
return self.call(self.context, buffer);
}
};
pub fn bytes_of(value: anytype) []const u8 {
const pointer_info = @typeInfo(@TypeOf(value)).Pointer;
debug.assert(pointer_info.size == .One);
return @ptrCast([*]const u8, value)[0 .. @sizeOf(pointer_info.child)];
}
pub fn compare(this: []const u8, that: []const u8) isize {
const range = math.min(usize, this.len, that.len);
var index: usize = 0;
while (index < range) : (index += 1) {
const difference = @intCast(isize, this[index]) - @intCast(isize, that[index]);
if (difference != 0) return difference;
}
return @intCast(isize, this.len) - @intCast(isize, that.len);
}
pub fn copy(target: []u8, source: []const u8) void {
var index: usize = 0;
while (index < source.len) : (index += 1) target[index] = source[index];
}
pub fn ends_with(target: []const u8, match: []const u8) bool {
if (target.len < match.len) return false;
var index = @as(usize, 0);
while (index < match.len) : (index += 1) {
if (target[target.len - (1 + index)] != match[match.len - (1 + index)]) return false;
}
return true;
}
pub fn equals(this: []const u8, that: []const u8) bool {
if (this.len != that.len) return false;
{
var index: usize = 0;
while (index < this.len) : (index += 1) if (this[index] != that[index]) return false;
}
return true;
}
var null_context = @as(usize, 0);
pub const null_writer = Writer.bind(&null_context, struct {
pub fn write(context: *usize, buffer: []const u8) usize {
debug.assert(context.* == 0);
return buffer.len;
}
});
pub fn slice_sentineled(comptime element: type, comptime sentinel: element, sequence: [*:sentinel]const element) []const element {
var length: usize = 0;
while (sequence[length] != sentinel) : (length += 1) {}
return sequence[0..length];
}
pub fn stream(output: Writer, input: Reader, buffer: []u8) (ReadError || WriteError)!u64 {
var total_written: u64 = 0;
var read = try input.read(buffer);
while (read != 0) {
total_written += try output.write(buffer[0..read]);
read = try input.read(buffer);
}
return total_written;
}
pub fn swap(comptime Element: type, this: *Element, that: *Element) void {
const temp = this.*;
this.* = that.*;
that.* = temp;
}
pub fn tag(value: anytype) Tag(@TypeOf(value)) {
return @as(Tag(@TypeOf(value)), value);
}
pub fn zero(target: []u8) void {
for (target) |*t| t.* = 0;
}

View File

@ -1,40 +0,0 @@
export module coral.math;
import coral;
export namespace coral {
/**
* Two-component vector type backed by 32-bit floating point values.
*/
struct vector2 {
/**
* "X" axis spatial component.
*/
f32 x;
/**
* "Y" axis spatial component.
*/
f32 y;
};
/**
* Three-component vector type backed by 32-bit floating point values.
*/
struct vector3 {
/**
* "X" axis spatial component.
*/
f32 x;
/**
* "Y" axis spatial component.
*/
f32 y;
/**
* "Z" axis spatial component.
*/
f32 z;
};
}

73
source/coral/math.zig Normal file
View File

@ -0,0 +1,73 @@
pub const CheckedArithmeticError = error {
IntOverflow,
};
pub fn Float(comptime bits: comptime_int) type {
return @Type(.{.Float = .{.bits = bits}});
}
pub fn Signed(comptime bits: comptime_int) type {
return @Type(.{.Int = .{
.signedness = .signed,
.bits = bits,
}});
}
pub fn Unsigned(comptime bits: comptime_int) type {
return @Type(.{.Int = .{
.signedness = .unsigned,
.bits = bits,
}});
}
pub const Vector2 = struct {
x: f32,
y: f32,
pub const zero = Vector2{.x = 0, .y = 0};
};
pub fn checked_add(a: anytype, b: anytype) CheckedArithmeticError!@TypeOf(a + b) {
const result = @addWithOverflow(a, b);
if (result.@"1" != 0) return error.IntOverflow;
return result.@"0";
}
pub fn checked_mul(a: anytype, b: anytype) CheckedArithmeticError!@TypeOf(a * b) {
const result = @mulWithOverflow(a, b);
if (result.@"1" != 0) return error.IntOverflow;
return result.@"0";
}
pub fn checked_sub(a: anytype, b: anytype) CheckedArithmeticError!@TypeOf(a - b) {
const result = @subWithOverflow(a, b);
if (result.@"1" != 0) return error.IntOverflow;
return result.@"0";
}
pub fn clamp(comptime Scalar: type, value: Scalar, min_value: Scalar, max_value: Scalar) Scalar {
return max(Scalar, min_value, min(Scalar, max_value, value));
}
pub fn max(comptime Scalar: type, this: Scalar, that: Scalar) Scalar {
return if (this > that) this else that;
}
pub fn max_int(comptime Int: type) Int {
const info = @typeInfo(Int);
const bit_count = info.Int.bits;
if (bit_count == 0) return 0;
return (1 << (bit_count - @boolToInt(info.Int.signedness == .signed))) - 1;
}
pub fn min(comptime Scalar: type, this: Scalar, that: Scalar) Scalar {
return if (this < that) this else that;
}

View File

@ -1,253 +0,0 @@
export module coral.stack;
import coral;
// Collections.
export namespace coral {
/**
* Result codes used by [contiguous_range]-derived types when they are appended to in any way.
*
* [push_result::ok] indicates that an push operation was successful.
*
* [push_result::out_of_memory] alerts that the memory required to perform the push operation
* failed.
*/
enum class [[nodiscard]] push_result {
ok,
out_of_memory,
};
/**
* Base type for all stack types.
*
* Sequences are any data structure which owns a linear, non-unique set of elements which may
* be queried and/or mutated.
*/
template<typename element> struct stack {
virtual ~stack() {};
/**
* Returns a read-only [slice] of the current range values.
*
* *Note*: the behavior of retaining the returned value past the scope of the source
* [stack] or any subsequent modifications to it is implementation-defined.
*/
virtual slice<element const> as_slice() const = 0;
/**
* Attempts to append `source_elements` to the stack.
*
* The returned [push_result] indicates whether the operation was successful or not.
*
* If the returned [push_result] is anything but [push_result::ok], the [stack] will be
* left in an implementation-defined state.
*/
virtual push_result push_all(slice<element const> const & source_elements) = 0;
};
/**
* Last-in-first-out contiguous sequence of `element` values optimized for small numbers of
* small-sized elements.
*
* [small_stack] types will default to using an inline array of `init_capacity` at first. After
* all local storage has been exhausted, the [small_stack] will switch to a dynamic buffer.
* Because of this, it is recommended to use larger `init_capacity` values for data which has a
* known or approximate upper bound at compile-time. Otherwise, the `init_capacity` value may
* be left at its default.
*
* *Note*: the [allocator] referenced in the stack must remain valid for the duration of the
* stack lifetime.
*/
template<typename element, usize init_capacity = 1> struct small_stack : public stack<element> {
small_stack(allocator * dynamic_allocator) {
this->dynamic_allocator = dynamic_allocator;
}
~small_stack() override {
if (this->is_dynamic()) {
for (element & e : this->elements) e.~element();
this->dynamic_allocator->deallocate(this->elements.pointer);
}
}
/**
* Returns a read-only [slice] of the current stack values.
*
* *Note*: the returned slice should be considered invalid if any mutable operation is
* performed on the source [stack] or it is no longer in scope.
*/
slice<element const> as_slice() const override {
return this->elements.sliced(0, this->filled);
}
/**
* Returns `true` if the stack is backed by dynamic memory, otherwise `false`.
*/
bool is_dynamic() const {
return this->elements.pointer != reinterpret_cast<element const *>(this->local_buffer);
}
/**
* Attempts to append `source_element` to the top of the stack.
*
* The returned [push_result] indicates whether the operation was successful or not.
*
* If the returned [push_result] is anything but [push_result::ok], the stack will be
* be left in an empty but valid state.
*
* *Note* that [push_all] is recommended when appending many values at once.
*/
push_result push(element const & source_element) {
if (this->filled == this->elements.length) {
push_result const result = this->reserve(this->elements.length);
if (result != push_result::ok) return result;
}
this->elements[this->filled] = source_element;
this->filled += 1;
return push_result::ok;
}
/**
* Attempts to append `source_elements` to the top of the stack.
*
* The returned [push_result] indicates whether the operation was successful or not.
*
* If the returned [push_result] is anything but [push_result::ok], the stack will be left
* in an empty but valid state.
*
* *Note* that [push] is recommended when appending singular values.
*/
push_result push_all(slice<element const> const & source_elements) override {
usize const updated_fill = this->filled + source_elements.length;
if (updated_fill >= this->elements.length) {
push_result const result = this->reserve(updated_fill);
if (result != push_result::ok) return result;
}
for (usize i = 0; i < source_elements.length; i += 1)
this->elements[this->filled + i] = source_elements[i];
this->filled = updated_fill;
return push_result::ok;
}
/**
* Attempts to reserve `capacity` number of elements additional space on the stack, forcing
* it to use dynamic memory _even_ if it hasn't exhausted the local buffer yet.
*
* The returned [push_result] indicates whether the operation was successful or not.
*
* If the returned [push_result] is anything but [push_result::ok], the stack will be left
* in an empty but valid state.
*
* *Note* that manual invocation is not recommended if the [stack] has a large
* `initial_capacity` argument.
*/
push_result reserve(usize capacity) {
usize const requested_capacity = this->filled + capacity;
if (this->is_dynamic()) {
u8 * const buffer = this->dynamic_allocator->reallocate(
reinterpret_cast<u8 *>(this->elements.pointer),
sizeof(element) * requested_capacity);
if (buffer == nullptr) {
this->elements = {};
return push_result::out_of_memory;
}
this->elements = {reinterpret_cast<element *>(buffer), requested_capacity};
} else {
usize const buffer_size = sizeof(element) * requested_capacity;
u8 * const buffer = this->dynamic_allocator->reallocate(nullptr, buffer_size);
if (buffer == nullptr) {
this->elements = {};
return push_result::out_of_memory;
}
copy({buffer, buffer_size}, this->elements.as_bytes());
this->elements = {reinterpret_cast<element *>(buffer), requested_capacity};
}
return push_result::ok;
}
private:
allocator * dynamic_allocator{nullptr};
usize filled{0};
slice<element> elements{reinterpret_cast<element *>(local_buffer), init_capacity};
u8 local_buffer[init_capacity]{0};
};
}
using byte_stack = coral::stack<coral::u8>;
// Reader / writers.
export namespace coral {
/**
* Readable type for streaming data from a [stack] containing [u8] values.
*/
struct stack_reader : public reader {
stack_reader(byte_stack const * stack) {
this->cursor = 0;
this->stack = stack;
}
/**
* Reads the data from the target stack into `buffer`, returning the number bytes read.
*/
expected<usize, io_error> read(slice<u8> const & buffer) override {
slice const stack_elements {this->stack->as_slice()};
usize const read {min(buffer.length, stack_elements.length - this->cursor)};
copy(buffer, stack_elements.sliced(cursor, read));
this->cursor += read;
return read;
}
private:
usize cursor {0};
byte_stack const * stack {nullptr};
};
/**
* Writable type for appending data to a [contiguous_range] containing [u8] values.
*/
struct stack_writer : public writer {
stack_writer(byte_stack * stack) {
this->stack = stack;
}
/**
* Attempts to write `buffer` to the target stack, returning the number of bytes written or
* an [io_error] if it failed to commit `buffer` to the stack memory.
*/
expected<usize, io_error> write(slice<u8 const> const & buffer) override {
switch (this->stack->push_all(buffer)) {
case push_result::ok: return buffer.length;
case push_result::out_of_memory: return io_error::unavailable;
default: unreachable();
}
}
private:
byte_stack * stack {nullptr};
};
}

132
source/coral/stack.zig Normal file
View File

@ -0,0 +1,132 @@
const debug = @import("./debug.zig");
const io = @import("./io.zig");
const math = @import("./math.zig");
pub fn Dense(comptime Element: type) type {
return struct {
allocator: io.MemoryAllocator,
capacity: usize,
values: []Element,
const Self = @This();
pub fn clear(self: *Self) void {
self.values = self.values[0 .. 0];
}
pub fn deinit(self: *Self) void {
self.allocator.deallocate(self.values.ptr);
}
pub fn drop(self: *Self, amount: usize) bool {
if (amount > self.values.len) return false;
self.values = self.values[0 .. self.values.len - amount];
return true;
}
pub fn grow(self: *Self, growth_amount: usize) GrowError!void {
const grown_capacity = self.capacity + growth_amount;
self.values = (self.allocator.reallocate(Element, self.values.ptr,
grown_capacity) orelse return error.OutOfMemory)[0 .. self.values.len];
self.capacity = grown_capacity;
}
pub fn init(allocator: io.MemoryAllocator, initial_capacity: usize) !Self {
return Self{
.values = (allocator.allocate_many(Element, initial_capacity) orelse return error.OutOfMemory)[0 .. 0],
.allocator = allocator,
.capacity = initial_capacity,
};
}
pub fn push_all(self: *Self, values: []const Element) GrowError!void {
const new_length = self.values.len + values.len;
if (new_length >= self.capacity) try self.grow(math.min(usize, new_length, self.capacity));
const offset_index = self.values.len;
self.values = self.values.ptr[0 .. new_length];
{
var index: usize = 0;
while (index < values.len) : (index += 1) self.values[offset_index + index] = values[index];
}
}
pub fn push_many(self: *Self, value: Element, amount: usize) GrowError!void {
const new_length = self.values.len + amount;
if (new_length >= self.capacity) try self.grow(math.min(usize, new_length, self.capacity));
const offset_index = self.values.len;
self.values = self.values.ptr[0 .. new_length];
{
var index: usize = 0;
while (index < amount) : (index += 1) self.values[offset_index + index] = value;
}
}
pub fn push_one(self: *Self, value: Element) GrowError!void {
if (self.values.len == self.capacity) try self.grow(math.min(usize, 1, self.capacity));
const offset_index = self.values.len;
self.values = self.values.ptr[0 .. self.values.len + 1];
self.values[offset_index] = value;
}
};
}
pub const GrowError = error {
OutOfMemory
};
pub fn as_dense_allocator(stack: *Dense(u8)) io.MemoryAllocator {
return io.MemoryAllocator.bind(stack, struct {
pub fn reallocate(writable_stack: *Dense(u8), existing_allocation: ?[*]u8, allocation_size: usize) ?[*]u8 {
if (allocation_size == 0) return null;
writable_stack.push_all(io.bytes_of(&allocation_size)) catch return null;
const usize_size = @sizeOf(usize);
errdefer debug.assert(writable_stack.drop(usize_size));
const allocation_index = writable_stack.values.len;
if (existing_allocation) |allocation| {
const existing_allocation_size = @intToPtr(*const usize, @ptrToInt(allocation) - usize_size).*;
writable_stack.push_all(allocation[0 .. existing_allocation_size]) catch return null;
} else {
writable_stack.push_many(0, allocation_size) catch return null;
}
return @ptrCast([*]u8, &writable_stack.values[allocation_index]);
}
});
}
pub fn as_dense_writer(stack: *Dense(u8)) io.Writer {
return io.Writer.bind(stack, struct {
pub fn write(writable_stack: *Dense(u8), buffer: []const u8) io.WriteError!usize {
writable_stack.push_all(buffer) catch |grow_error| switch (grow_error) {
error.OutOfMemory => return error.IoUnavailable,
};
return buffer.len;
}
});
}

62
source/coral/table.zig Normal file
View File

@ -0,0 +1,62 @@
const io = @import("./io.zig");
pub fn Hashed(comptime key: Key, comptime Element: type) type {
const Entry = struct {
key: key.Element,
value: Element,
};
return struct {
allocator: io.MemoryAllocator,
entries: []?Entry,
const Self = @This();
pub fn clear(self: *Self) void {
for (self.entries) |*entry| entry.* = null;
}
pub fn deinit(self: *Self) void {
self.allocator.deallocate(self.entries.ptr);
}
pub fn init(allocator: io.MemoryAllocator) !Self {
const size = 4;
const entries = (allocator.allocate_many(?Entry, size) orelse return error.OutOfMemory)[0 .. size];
errdefer allocator.deallocate(entries);
return Self{
.entries = entries,
.allocator = allocator,
};
}
pub fn assign(self: *Self, key_element: key.Element, value_element: Element) !void {
_ = self;
_ = key_element;
_ = value_element;
}
pub fn insert(self: *Self, key_element: key.Element, value_element: Element) !void {
_ = self;
_ = key_element;
_ = value_element;
}
pub fn lookup(self: *Self, key_element: key.Element) ?Element {
_ = self;
_ = key_element;
return null;
}
};
}
pub const Key = struct {
Element: type,
};
pub const string_key = Key{
.Element = []const u8,
};

101
source/coral/utf8.zig Normal file
View File

@ -0,0 +1,101 @@
const math = @import("./math.zig");
pub const IntParseError = math.CheckedArithmeticError || ParseError;
pub const ParseError = error {
BadSyntax,
};
pub fn parse_float(comptime bits: comptime_int, utf8: []const u8) ParseError!math.Float(bits) {
// ""
if (utf8.len == 0) return error.BadSyntax;
const is_negative = utf8[0] == '-';
// "-"
if (is_negative and (utf8.len == 1)) return error.BadSyntax;
const negative_offset = @boolToInt(is_negative);
var has_decimal = utf8[negative_offset] == '.';
// "-."
if (has_decimal and (utf8.len == 2)) return error.BadSyntax;
const Float = math.Float(bits);
var result: Float = 0;
var factor: Float = 1;
for (utf8[0 .. negative_offset + @boolToInt(has_decimal)]) |code| switch (code) {
'.' => {
if (has_decimal) return error.BadSyntax;
has_decimal = true;
},
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' => {
if (has_decimal) factor /= 10.0;
result = ((result * 10.0) + @intToFloat(Float, code - '0'));
},
else => return error.BadSyntax,
};
return result * factor;
}
pub fn parse_signed(comptime bits: comptime_int, utf8: []const u8) IntParseError!math.Unsigned(bits) {
// ""
if (utf8.len == 0) return error.BadSyntax;
const is_negative = utf8[0] == '-';
// "-"
if (is_negative and (utf8.len == 1)) return error.BadSyntax;
var result: math.Unsigned(bits) = 0;
{
var index: usize = 0;
while (index < utf8.len) : (index += 1) {
const code = utf8[index];
switch (code) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' =>
result = try math.checked_add(try math.checked_mul(result, 10), try math.checked_sub(code, '0')),
else => return error.BadSyntax,
}
}
}
return result;
}
pub fn parse_unsigned(comptime bits: comptime_int, utf8: []const u8) IntParseError!math.Unsigned(bits) {
// ""
if (utf8.len == 0) return error.BadSyntax;
// "-..."
if (utf8[0] == '-') return error.BadSyntax;
var result: math.Unsigned(bits) = 0;
{
var index: usize = 0;
while (index < utf8.len) : (index += 1) {
const code = utf8[index];
switch (code) {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' =>
result = try math.checked_add(try math.checked_mul(result, 10), try math.checked_sub(code, '0')),
else => return error.BadSyntax,
}
}
}
return result;
}

552
source/kym/bytecode.zig Normal file
View File

@ -0,0 +1,552 @@
const coral = @import("coral");
const tokens = @import("./tokens.zig");
pub const Chunk = struct {
constant_buffer: Buffer,
bytecode_buffer: Buffer,
constants: Constants,
const Buffer = coral.stack.Dense(u8);
const Constants = coral.stack.Dense(Constant);
pub fn compile(self: *Chunk, script: []const u8) !void {
self.reset();
var tokenizer = tokens.Tokenizer{.source = script};
var parser = Parser{
.chunk = self,
.tokenizer = &tokenizer,
};
errdefer self.reset();
try parser.parse_statement();
}
pub fn deinit(self: *Chunk) void {
self.bytecode_buffer.deinit();
self.constant_buffer.deinit();
self.constants.deinit();
}
pub fn emit_byte(self: *Chunk, byte: u8) !void {
return self.bytecode_buffer.push_one(byte);
}
pub fn emit_opcode(self: *Chunk, opcode: Opcode) !void {
return self.bytecode_buffer.push_one(@enumToInt(opcode));
}
pub fn emit_operand(self: *Chunk, operand: Operand) !void {
return self.bytecode_buffer.push_all(coral.io.bytes_of(&operand));
}
pub fn intern_string(self: *Chunk, string: []const u8) !u64 {
var constant_slot = @as(u64, 0);
for (self.constants.values) |interned_constant| {
switch (interned_constant) {
.string => |interned_string| if (coral.io.equals(interned_string, string)) return constant_slot,
}
constant_slot += 1;
}
const constant_allocator = coral.stack.as_dense_allocator(&self.constant_buffer);
const allocation = constant_allocator.allocate_many(u8, string.len + 1) orelse return error.OutOfMemory;
errdefer constant_allocator.deallocate(allocation);
// Zero-terminate string.
allocation[string.len] = 0;
// Write string contents.
{
const allocated_string = allocation[0 .. string.len];
coral.io.copy(allocated_string, string);
try self.constants.push_one(.{.string = @ptrCast([:0]u8, allocated_string)});
}
return constant_slot;
}
pub fn fetch_byte(self: Chunk, cursor: *usize) ?u8 {
if (cursor.* >= self.bytecode_buffer.values.len) return null;
defer cursor.* += 1;
return self.bytecode_buffer.values[cursor.*];
}
pub fn fetch_constant(self: Chunk, cursor: *usize) ?*Constant {
return &self.constants.values[self.fetch_operand(cursor) orelse return null];
}
pub fn fetch_opcode(self: Chunk, cursor: *usize) ?Opcode {
return @intToEnum(Opcode, self.fetch_byte(cursor) orelse return null);
}
pub fn fetch_operand(self: Chunk, cursor: *usize) ?Operand {
const operand_size = @sizeOf(Operand);
const updated_cursor = cursor.* + operand_size;
if (updated_cursor > self.bytecode_buffer.values.len) return null;
var operand_bytes align(@alignOf(Operand)) = [_]u8{0} ** operand_size;
coral.io.copy(&operand_bytes, self.bytecode_buffer.values[cursor.* .. updated_cursor]);
cursor.* = updated_cursor;
return @bitCast(Operand, operand_bytes);
}
pub fn init(allocator: coral.io.MemoryAllocator) !Chunk {
const page_size = 1024;
var constant_buffer = try Buffer.init(allocator, page_size);
errdefer constant_buffer.deinit();
const assumed_average_bytecode_size = 1024;
var bytecode_buffer = try Buffer.init(allocator, assumed_average_bytecode_size);
errdefer bytecode_buffer.deinit();
const assumed_average_constant_count = 512;
var constants = try Constants.init(allocator, assumed_average_constant_count);
errdefer constants.deinit();
return Chunk{
.constant_buffer = constant_buffer,
.bytecode_buffer = bytecode_buffer,
.constants = constants,
};
}
pub fn reset(self: *Chunk) void {
self.bytecode_buffer.clear();
self.constant_buffer.clear();
}
};
pub const Constant = union (enum) {
string: [:0]u8,
};
pub const Opcode = enum(u8) {
push_nil,
push_true,
push_false,
push_zero,
push_integer,
push_float,
push_string,
push_array,
push_table,
not,
neg,
add,
sub,
div,
mul,
call,
get_field,
set_field,
get_x,
set_x,
get_y,
set_y,
get_global,
set_global,
get_local,
};
pub const Operand = u64;
const ParseError = SyntaxError || error{
OutOfMemory,
};
const Parser = struct {
tokenizer: *tokens.Tokenizer,
scope_depth: u16 = 0,
chunk: *Chunk,
locals: SmallStack(Local, Local.empty) = .{},
const Local = struct {
name: []const u8,
depth: u16,
const empty = Local{ .name = "", .depth = 0 };
};
const Operations = SmallStack(Operator, .not);
const Operator = enum {
not,
negate,
add,
subtract,
divide,
multiply,
fn opcode(self: Operator) Opcode {
return switch (self) {
.not => .not,
.negate => .neg,
.add => .add,
.subtract => .sub,
.multiply => .mul,
.divide => .div,
};
}
fn precedence(self: Operator) isize {
return switch (self) {
.not => 13,
.negate => 13,
.add => 11,
.subtract => 11,
.divide => 12,
.multiply => 12,
};
}
};
fn declare_local(self: *Parser, name: []const u8) !void {
return self.locals.push(.{
.name = name,
.depth = self.scope_depth,
});
}
fn error_unexpected_end(self: *Parser) SyntaxError {
_ = self;
return error.BadSyntax;
}
fn error_unexpected_token(self: *Parser, token: tokens.Token) SyntaxError {
_ = self;
_ = token;
// _ = self.error_writer.write("unexpected token `") catch {};
// _ = self.error_writer.write(token.text()) catch {};
// _ = self.error_writer.write("`") catch {};
return error.BadSyntax;
}
fn error_integer_overflow(self: *Parser, integer_literal: []const u8) SyntaxError {
// TODO: Implement.
_ = self;
_ = integer_literal;
return error.BadSyntax;
}
pub fn parse_expression(self: *Parser, initial_token: tokens.Token) ParseError!void {
var operations = Operations{};
var previous_token = initial_token;
while (self.tokenizer.next()) |current_token| {
switch (current_token) {
.newline => {
previous_token = current_token;
break;
},
else => previous_token = try self.parse_operation(&operations, previous_token, current_token),
}
}
while (operations.pop()) |operator| try self.chunk.emit_opcode(operator.opcode());
}
fn parse_arguments(self: *Parser) ParseError!tokens.Token {
var operations = Operations{};
var previous_token = @as(tokens.Token, .symbol_paren_left);
var argument_count = @as(Operand, 0);
while (self.tokenizer.next()) |current_token| {
switch (current_token) {
.symbol_paren_right => {
while (operations.pop()) |operator| try self.chunk.emit_opcode(operator.opcode());
try self.chunk.emit_opcode(.call);
try self.chunk.emit_operand(argument_count);
return .symbol_paren_right;
},
.symbol_comma => {
while (operations.pop()) |operator| try self.chunk.emit_opcode(operator.opcode());
previous_token = current_token;
argument_count += 1;
},
else => previous_token = try self.parse_operation(&operations, previous_token, current_token),
}
}
return previous_token;
}
fn parse_group(_: *Parser) ParseError!tokens.Token {
return error.BadSyntax;
}
pub fn parse_operation(self: *Parser, operations: *Operations,
previous_token: tokens.Token, current_token: tokens.Token) ParseError!tokens.Token {
switch (current_token) {
.integer_literal => |literal| {
const value = coral.utf8.parse_signed(@bitSizeOf(i64), literal) catch |err| switch (err) {
error.BadSyntax => unreachable,
error.IntOverflow => return self.error_integer_overflow(literal),
};
if (value == 0) {
try self.chunk.emit_opcode(.push_zero);
} else {
try self.chunk.emit_opcode(.push_integer);
try self.chunk.emit_operand(@bitCast(u64, value));
}
},
.real_literal => |literal| {
try self.chunk.emit_operand(@bitCast(u64, coral.utf8.parse_float(@bitSizeOf(f64), literal) catch |err| {
switch (err) {
// Already validated to be a real by the tokenizer so this cannot fail, as real syntax is a
// subset of float syntax.
error.BadSyntax => unreachable,
}
}));
},
.string_literal => |literal| {
try self.chunk.emit_opcode(.push_string);
try self.chunk.emit_operand(try self.chunk.intern_string(literal));
},
.global_identifier => |identifier| {
try self.chunk.emit_opcode(.get_global);
try self.chunk.emit_operand(try self.chunk.intern_string(identifier));
},
.local_identifier => |identifier| {
if (self.resolve_local(identifier)) |local| {
try self.chunk.emit_opcode(.get_local);
try self.chunk.emit_byte(local);
} else {
try self.chunk.emit_opcode(.push_nil);
}
},
.symbol_bang => try operations.push(.not),
.symbol_plus => while (operations.pop()) |operator| {
if (Operator.add.precedence() < operator.precedence()) break try operations.push(operator);
try self.chunk.emit_opcode(operator.opcode());
},
.symbol_dash => while (operations.pop()) |operator| {
if (Operator.subtract.precedence() < operator.precedence()) break try operations.push(operator);
try self.chunk.emit_opcode(operator.opcode());
},
.symbol_asterisk => while (operations.pop()) |operator| {
if (Operator.multiply.precedence() < operator.precedence()) break try operations.push(operator);
try self.chunk.emit_opcode(operator.opcode());
},
.symbol_forward_slash => while (operations.pop()) |operator| {
if (Operator.divide.precedence() < operator.precedence()) break try operations.push(operator);
try self.chunk.emit_opcode(operator.opcode());
},
.symbol_period => {
const field_token = self.tokenizer.next() orelse return self.error_unexpected_end();
switch (field_token) {
.local_identifier => |identifier| {
try self.chunk.emit_opcode(.get_field);
try self.chunk.emit_operand(try self.chunk.intern_string(identifier));
return field_token;
},
else => return self.error_unexpected_token(field_token),
}
},
.symbol_paren_left => return try switch (previous_token) {
.local_identifier, .global_identifier => self.parse_arguments(),
else => self.parse_group(),
},
.symbol_brace_left => {
try self.parse_table();
switch (previous_token) {
.local_identifier, .global_identifier => {
// Created as function argument.
try self.chunk.emit_opcode(.call);
try self.chunk.emit_operand(1);
},
else => {},
}
return .symbol_brace_right;
},
else => return self.error_unexpected_token(current_token),
}
return current_token;
}
pub fn parse_statement(self: *Parser) ParseError!void {
// TODO: Implement.
return self.error_unexpected_end();
}
fn parse_table(self: *Parser) ParseError!void {
var field_count = @as(Operand, 0);
while (self.tokenizer.next()) |field_token| {
switch (field_token) {
.newline => {},
.local_identifier => |field_identifier| {
const operation_token = self.tokenizer.next() orelse return self.error_unexpected_end();
const interned_identifier = try self.chunk.intern_string(field_identifier);
field_count += 1;
switch (operation_token) {
.symbol_assign => {
var operations = Operations{};
var previous_token = @as(tokens.Token, .symbol_assign);
while (self.tokenizer.next()) |token| : (previous_token = token) switch (token) {
.newline => {},
.symbol_comma => break,
.symbol_brace_right => {
try self.chunk.emit_opcode(.push_string);
try self.chunk.emit_operand(interned_identifier);
try self.chunk.emit_opcode(.push_table);
try self.chunk.emit_operand(field_count);
return;
},
else => previous_token = try self.parse_operation(&operations, previous_token, token),
};
while (operations.pop()) |operator| try self.chunk.emit_opcode(operator.opcode());
try self.chunk.emit_opcode(.push_string);
try self.chunk.emit_operand(interned_identifier);
},
.symbol_comma => {
try self.chunk.emit_opcode(.push_string);
try self.chunk.emit_operand(interned_identifier);
},
.symbol_brace_right => {
try self.chunk.emit_opcode(.push_string);
try self.chunk.emit_operand(interned_identifier);
try self.chunk.emit_opcode(.push_table);
try self.chunk.emit_operand(field_count);
return;
},
else => return self.error_unexpected_token(operation_token),
}
},
.symbol_brace_right => {
try self.chunk.emit_opcode(.push_table);
try self.chunk.emit_operand(field_count);
return;
},
else => return self.error_unexpected_token(field_token),
}
}
return self.error_unexpected_end();
}
fn resolve_local(self: *Parser, name: []const u8) ?u8 {
var count = @as(u8, self.locals.buffer.len);
while (count != 0) {
const index = count - 1;
if (coral.io.equals(name, self.locals.buffer[index].name)) return index;
count = index;
}
return null;
}
};
fn SmallStack(comptime Element: type, comptime default: Element) type {
const maximum = 255;
return struct {
buffer: [maximum]Element = [_]Element{default} ** maximum,
count: u8 = 0,
const Self = @This();
fn peek(self: Self) ?Element {
if (self.count == 0) return null;
return self.buffer[self.count - 1];
}
fn pop(self: *Self) ?Element {
if (self.count == 0) return null;
self.count -= 1;
return self.buffer[self.count];
}
fn push(self: *Self, local: Element) !void {
if (self.count == maximum) return error.OutOfMemory;
self.buffer[self.count] = local;
self.count += 1;
}
};
}
const SymbolTable = coral.table.Hashed(coral.table.string_key, usize);
const SyntaxError = error{
BadSyntax,
};

463
source/kym/kym.zig Normal file
View File

@ -0,0 +1,463 @@
const bytecode = @import("./bytecode.zig");
const coral = @import("coral");
pub const NewError = error {
OutOfMemory,
};
pub const Object = opaque {
fn cast(object_instance: *ObjectInstance) *Object {
return @ptrCast(*Object, object_instance);
}
pub fn userdata(object: *Object) ObjectUserdata {
return ObjectInstance.cast(object).userdata;
}
};
pub const ObjectBehavior = struct {
caller: *const ObjectCaller = default_call,
getter: *const ObjectGetter = default_get,
setter: *const ObjectSetter = default_set,
fn default_call(_: *Vm, _: *Object, _: *Object, _: []const Value) RuntimeError!Value {
return error.IllegalOperation;
}
fn default_get(vm: *Vm, object: *Object, index: Value) RuntimeError!Value {
return vm.get_field(object, ObjectInstance.cast(try vm.new_string_value(index)).userdata.string);
}
fn default_set(vm: *Vm, object: *Object, index: Value, value: Value) RuntimeError!void {
try vm.set_field(object, ObjectInstance.cast(try vm.new_string_value(index)).userdata.string, value);
}
};
pub const ObjectCaller = fn (vm: *Vm, object: *Object, context: *Object, arguments: []const Value) RuntimeError!Value;
pub const ObjectGetter = fn (vm: *Vm, object: *Object, index: Value) RuntimeError!Value;
const ObjectInstance = struct {
behavior: ObjectBehavior,
userdata: ObjectUserdata,
fields: ?ValueTable = null,
fn cast(object: *Object) *ObjectInstance {
return @ptrCast(*ObjectInstance, @alignCast(@alignOf(ObjectInstance), object));
}
};
pub const ObjectSetter = fn (vm: *Vm, object: *Object, index: Value, value: Value) RuntimeError!void;
pub const ObjectUserdata = union (enum) {
none,
native: *anyopaque,
string: []u8,
chunk: bytecode.Chunk
};
pub const RuntimeError = NewError || error {
StackOverflow,
IllegalOperation,
UnsupportedOperation,
};
pub const Value = union(enum) {
nil,
false,
true,
float: Float,
integer: Integer,
vector2: coral.math.Vector2,
object: *Object,
pub const Integer = i64;
pub const Float = f64;
pub fn to_float(self: Value) ?Float {
return switch (self) {
.float => |float| float,
.integer => |integer| @intToFloat(Float, integer),
else => null,
};
}
pub fn to_object(self: Value) ?*Object {
return switch (self) {
.object => |object| object,
else => null,
};
}
pub fn to_integer(self: Value) ?Integer {
return switch (self) {
.integer => |integer| integer,
// TODO: Verify safety of cast.
.float => |float| @floatToInt(Float, float),
else => null,
};
}
pub fn to_vector2(self: Value) ?coral.math.Vector2 {
return switch (self) {
.vector2 => |vector2| vector2,
else => null,
};
}
};
const ValueTable = coral.table.Hashed(coral.table.string_key, Value);
pub const Vm = struct {
allocator: coral.io.MemoryAllocator,
heap: struct {
count: u32 = 0,
free_head: u32 = 0,
allocations: []HeapAllocation,
global_instance: ObjectInstance,
const Self = @This();
},
stack: struct {
top: u32 = 0,
values: []Value,
const Self = @This();
fn pop(self: *Self) ?Value {
if (self.top == 0) return null;
self.top -= 1;
return self.values[self.top];
}
fn push(self: *Self, value: Value) !void {
if (self.top == self.values.len) return error.StackOverflow;
self.values[self.top] = value;
self.top += 1;
}
},
pub const CompileError = error {
BadSyntax,
OutOfMemory,
};
const HeapAllocation = union(enum) {
next_free: u32,
instance: ObjectInstance,
};
pub const InitOptions = struct {
stack_max: u32,
objects_max: u32,
};
pub fn call_get(self: *Vm, object: *Object, index: Value, arguments: []const Value) RuntimeError!Value {
return switch (self.get(object, index)) {
.object => |callable| ObjectInstance.cast(object).behavior.caller(self, callable, object, arguments),
else => error.IllegalOperation,
};
}
pub fn call_self(self: *Vm, object: *Object, arguments: []const Value) RuntimeError!Value {
return ObjectInstance.cast(object).behavior.caller(self, object, self.globals(), arguments);
}
pub fn deinit(self: *Vm) void {
self.allocator.deallocate(self.heap.allocations.ptr);
self.allocator.deallocate(self.stack.values.ptr);
}
pub fn globals(self: *Vm) *Object {
return Object.cast(&self.heap.global_instance);
}
pub fn init(allocator: coral.io.MemoryAllocator, init_options: InitOptions) !Vm {
const heap_allocations = (allocator.allocate_many(HeapAllocation,
init_options.objects_max) orelse return error.OutOfMemory)[0 .. init_options.objects_max];
errdefer allocator.deallocate(heap_allocations);
for (heap_allocations) |*heap_allocation| heap_allocation.* = .{.next_free = 0};
const values = (allocator.allocate_many(Value, init_options.stack_max) orelse return error.OutOfMemory)[0 .. init_options.stack_max];
errdefer allocator.deallocate(values);
for (values) |*value| value.* = .nil;
const global_values = try ValueTable.init(allocator);
errdefer global_values.deinit();
var vm = Vm{
.allocator = allocator,
.stack = .{.values = values},
.heap = .{
.allocations = heap_allocations,
.global_instance = .{
.behavior = .{},
.userdata = .none,
},
},
};
return vm;
}
pub fn get(self: *Vm, index: Value) RuntimeError!Value {
return ObjectInstance.cast(self).behavior.getter(self, index);
}
pub fn get_field(_: *Vm, object: *Object, field: []const u8) Value {
const fields = &(ObjectInstance.cast(object).fields orelse return .nil);
return fields.lookup(field) orelse .nil;
}
pub fn new(self: *Vm, object_userdata: ObjectUserdata, object_behavior: ObjectBehavior) NewError!*Object {
if (self.heap.count == self.heap.allocations.len) return error.OutOfMemory;
defer self.heap.count += 1;
if (self.heap.free_head != self.heap.count) {
const free_list_next = self.heap.allocations[self.heap.free_head].next_free;
const index = self.heap.free_head;
const allocation = &self.heap.allocations[index];
allocation.* = .{.instance = .{
.userdata = object_userdata,
.behavior = object_behavior,
}};
self.heap.free_head = free_list_next;
return Object.cast(&allocation.instance);
}
const allocation = &self.heap.allocations[self.heap.count];
allocation.* = .{.instance = .{
.userdata = object_userdata,
.behavior = object_behavior,
}};
self.heap.free_head += 1;
return Object.cast(&allocation.instance);
}
pub fn new_array(self: *Vm, _: Value.Integer) NewError!*Object {
// TODO: Implement.
return self.new(.none, .{});
}
pub fn new_closure(self: *Vm, caller: *const ObjectCaller) NewError!*Object {
// TODO: Implement.
return self.new(.none, .{.caller = caller});
}
pub fn new_script(self: *Vm, script_source: []const u8) CompileError!*Object {
var chunk = try bytecode.Chunk.init(self.allocator);
errdefer chunk.deinit();
try chunk.compile(script_source);
return self.new(.{.chunk = chunk}, .{
.caller = struct {
fn chunk_cast(context: *Object) *bytecode.Chunk {
return @ptrCast(*bytecode.Chunk, @alignCast(@alignOf(bytecode.Chunk), context.userdata().native));
}
fn call(vm: *Vm, object: *Object, _: *Object, arguments: []const Value) RuntimeError!Value {
return execute_chunk(chunk_cast(object).*, vm, arguments);
}
}.call,
});
}
pub fn new_string(self: *Vm, string_data: []const u8) NewError!*Object {
return self.new(.{.string = allocate_copy: {
if (string_data.len == 0) break: allocate_copy &.{};
const string_copy = (self.allocator.allocate_many(
u8, string_data.len) orelse return error.OutOfMemory)[0 .. string_data.len];
coral.io.copy(string_copy, string_data);
break: allocate_copy string_copy;
}}, .{
});
}
pub fn new_string_value(self: *Vm, value: Value) NewError!*Object {
// TODO: Implement.
return switch (value) {
.nil => self.new_string(""),
else => unreachable,
};
}
pub fn new_table(self: *Vm) NewError!*Object {
// TODO: Implement.
return self.new(.none, .{});
}
pub fn set(self: *Vm, object: *Object, index: Value, value: Value) RuntimeError!void {
return ObjectInstance.cast(object).behavior.setter(self, object, index, value);
}
pub fn set_field(self: *Vm, object: *Object, field: []const u8, value: Value) NewError!void {
const object_instance = ObjectInstance.cast(object);
if (object_instance.fields == null) object_instance.fields = try ValueTable.init(self.allocator);
try object_instance.fields.?.assign(field, value);
}
};
fn execute_chunk(chunk: bytecode.Chunk, vm: *Vm, arguments: []const Value) RuntimeError!Value {
const old_stack_top = vm.stack.top;
errdefer vm.stack.top = old_stack_top;
for (arguments) |argument| try vm.stack.push(argument);
if (arguments.len > coral.math.max_int(Value.Integer)) return error.IllegalOperation;
try vm.stack.push(.{.integer = @intCast(Value.Integer, arguments.len)});
{
var cursor = @as(usize, 0);
while (chunk.fetch_opcode(&cursor)) |code| switch (code) {
.push_nil => try vm.stack.push(.nil),
.push_true => try vm.stack.push(.true),
.push_false => try vm.stack.push(.false),
.push_zero => try vm.stack.push(.{.integer = 0}),
.push_integer => try vm.stack.push(.{
.integer = @bitCast(Value.Integer, chunk.fetch_operand(&cursor) orelse {
return error.IllegalOperation;
})
}),
.push_float => try vm.stack.push(.{.float = @bitCast(Value.Float, chunk.fetch_operand(&cursor) orelse {
return error.IllegalOperation;
})}),
.push_string => {
const constant = chunk.fetch_constant(&cursor) orelse {
return error.IllegalOperation;
};
if (constant.* != .string) return error.IllegalOperation;
// TODO: Implement string behavior.
try vm.stack.push(.{.object = try vm.new(.{.string = constant.string}, .{})});
},
.push_array => {
const element_count = @bitCast(Value.Integer,
chunk.fetch_operand(&cursor) orelse return error.IllegalOperation);
const array_object = try vm.new_array(element_count);
{
var element_index = Value{.integer = 0};
var array_start = @intCast(Value.Integer, vm.stack.top) - element_count;
while (element_index.integer < element_count) : (element_index.integer += 1) {
try vm.set(array_object, element_index, vm.stack.values[
@intCast(usize, array_start + element_index.integer)]);
}
vm.stack.top = @intCast(u32, array_start);
}
},
.push_table => {
const field_count = chunk.fetch_operand(&cursor) orelse return error.IllegalOperation;
if (field_count > coral.math.max_int(Value.Integer)) return error.OutOfMemory;
const table_object = try vm.new_table();
{
var field_index = @as(bytecode.Operand, 0);
while (field_index < field_count) : (field_index += 1) {
// Assigned to temporaries to explicitly preserve stack popping order.
const field_key = vm.stack.pop() orelse return error.IllegalOperation;
const field_value = vm.stack.pop() orelse return error.IllegalOperation;
try vm.set(table_object, field_key, field_value);
}
}
try vm.stack.push(.{.object = table_object});
},
.get_local => {
try vm.stack.push(vm.stack.values[
vm.stack.top - (chunk.fetch_byte(&cursor) orelse return error.IllegalOperation)]);
},
.get_global => {
const field = chunk.fetch_constant(&cursor) orelse return error.IllegalOperation;
if (field.* != .string) return error.IllegalOperation;
try vm.stack.push(vm.get_field(vm.globals(), field.string));
},
.not => {
},
// .neg,
// .add,
// .sub,
// .div,
// .mul,
// .call,
// .set,
// .get,
else => return error.IllegalOperation,
};
}
const return_value = vm.stack.pop() orelse return error.IllegalOperation;
vm.stack.top = coral.math.checked_sub(vm.stack.top, @intCast(u32, arguments.len + 1)) catch |sub_error| {
switch (sub_error) {
error.IntOverflow => return error.IllegalOperation,
}
};
return return_value;
}
pub fn object_argument(_: *Vm, arguments: []const Value, argument_index: usize) RuntimeError!*Object {
// TODO: Record error message in Vm.
if (argument_index >= arguments.len) return error.IllegalOperation;
const argument = arguments[argument_index];
if (argument != .object) return error.IllegalOperation;
return argument.object;
}

274
source/kym/tokens.zig Normal file
View File

@ -0,0 +1,274 @@
const coral = @import("coral");
pub const Token = union(enum) {
unknown: u8,
newline,
global_identifier: []const u8,
local_identifier: []const u8,
symbol_assign,
symbol_plus,
symbol_dash,
symbol_asterisk,
symbol_forward_slash,
symbol_paren_left,
symbol_paren_right,
symbol_bang,
symbol_comma,
symbol_at,
symbol_brace_left,
symbol_brace_right,
symbol_bracket_left,
symbol_bracket_right,
symbol_period,
integer_literal: []const u8,
real_literal: []const u8,
string_literal: []const u8,
keyword_nil,
keyword_false,
keyword_true,
keyword_return,
keyword_self,
pub fn text(self: Token) []const u8 {
return switch (self) {
.unknown => |unknown| @ptrCast([*]const u8, &unknown)[0 .. 1],
.newline => "newline",
.global_identifier => |identifier| identifier,
.local_identifier => |identifier| identifier,
.symbol_assign => "=",
.symbol_plus => "+",
.symbol_dash => "-",
.symbol_asterisk => "*",
.symbol_forward_slash => "/",
.symbol_paren_left => "(",
.symbol_paren_right => ")",
.symbol_bang => "!",
.symbol_comma => ",",
.symbol_at => "@",
.symbol_brace_left => "{",
.symbol_brace_right => "}",
.symbol_bracket_left => "[",
.symbol_bracket_right => "]",
.symbol_period => ".",
.integer_literal => |literal| literal,
.real_literal => |literal| literal,
.string_literal => |literal| literal,
.keyword_nil => "nil",
.keyword_false => "false",
.keyword_true => "true",
.keyword_return => "return",
};
}
};
pub const Tokenizer = struct {
source: []const u8,
cursor: usize = 0,
pub fn next(self: *Tokenizer) ?Token {
while (self.cursor < self.source.len) switch (self.source[self.cursor]) {
' ', '\t' => self.cursor += 1,
'\n' => {
self.cursor += 1;
return .newline;
},
'0' ... '9' => {
const begin = self.cursor;
self.cursor += 1;
while (self.cursor < self.source.len) switch (self.source[self.cursor]) {
'0' ... '9' => self.cursor += 1,
'.' => {
self.cursor += 1;
while (self.cursor < self.source.len) switch (self.source[self.cursor]) {
'0' ... '9' => self.cursor += 1,
else => break,
};
return Token{.real_literal = self.source[begin .. self.cursor]};
},
else => break,
};
return Token{.integer_literal = self.source[begin .. self.cursor]};
},
'A' ... 'Z', 'a' ... 'z', '_' => {
const begin = self.cursor;
self.cursor += 1;
while (self.cursor < self.source.len) switch (self.source[self.cursor]) {
'0'...'9', 'A'...'Z', 'a'...'z', '_' => self.cursor += 1,
else => break,
};
const identifier = self.source[begin..self.cursor];
coral.debug.assert(identifier.len != 0);
switch (identifier[0]) {
'n' => if (coral.io.ends_with(identifier, "il")) return .keyword_nil,
'f' => if (coral.io.ends_with(identifier, "alse")) return .keyword_false,
't' => if (coral.io.ends_with(identifier, "rue")) return .keyword_true,
'r' => if (coral.io.ends_with(identifier, "eturn")) return .keyword_return,
's' => if (coral.io.ends_with(identifier, "elf")) return .keyword_self,
else => {},
}
return Token{.local_identifier = identifier};
},
'@' => {
self.cursor += 1;
if (self.cursor < self.source.len) switch (self.source[self.cursor]) {
'A'...'Z', 'a'...'z', '_' => {
const begin = self.cursor;
self.cursor += 1;
while (self.cursor < self.source.len) switch (self.source[self.cursor]) {
'0'...'9', 'A'...'Z', 'a'...'z', '_' => self.cursor += 1,
else => break,
};
return Token{.global_identifier = self.source[begin..self.cursor]};
},
'"' => {
self.cursor += 1;
const begin = self.cursor;
self.cursor += 1;
while (self.cursor < self.source.len) switch (self.source[self.cursor]) {
'"' => break,
else => self.cursor += 1,
};
defer self.cursor += 1;
return Token{.global_identifier = self.source[begin..self.cursor]};
},
else => {},
};
return .symbol_at;
},
'"' => {
self.cursor += 1;
const begin = self.cursor;
self.cursor += 1;
while (self.cursor < self.source.len) switch (self.source[self.cursor]) {
'"' => break,
else => self.cursor += 1,
};
defer self.cursor += 1;
return Token{.string_literal = self.source[begin..self.cursor]};
},
'{' => {
self.cursor += 1;
return .symbol_brace_left;
},
'}' => {
self.cursor += 1;
return .symbol_brace_right;
},
',' => {
self.cursor += 1;
return .symbol_comma;
},
'!' => {
self.cursor += 1;
return .symbol_bang;
},
')' => {
self.cursor += 1;
return .symbol_paren_right;
},
'(' => {
self.cursor += 1;
return .symbol_paren_left;
},
'/' => {
self.cursor += 1;
return .symbol_forward_slash;
},
'*' => {
self.cursor += 1;
return .symbol_asterisk;
},
'-' => {
self.cursor += 1;
return .symbol_dash;
},
'+' => {
self.cursor += 1;
return .symbol_plus;
},
'=' => {
self.cursor += 1;
return .symbol_assign;
},
'.' => {
self.cursor += 1;
return .symbol_period;
},
else => {
defer self.cursor += 1;
return Token{.unknown = self.source[self.cursor]};
},
};
return null;
}
};

View File

@ -1,253 +0,0 @@
export module oar;
import coral;
import coral.files;
/**
* Length of the full magic signature at the beginning of an Oar file.
*/
constexpr coral::usize signature_length {4};
/**
* Length of the magic signature at the beginning of an Oar file without the version indicator
* byte.
*/
constexpr coral::usize signature_identifier_length {signature_length - 1};
/**
* Hardcoded signature magic value that this implementation of Oar expects when reading archives.
*/
constexpr coral::u8 signature_magic[signature_length] {'o', 'a', 'r', 0};
/**
* Oar file header format.
*/
struct header {
coral::u8 signature_magic[signature_length];
coral::u32 entry_count;
coral::u8 padding[504];
};
static_assert(sizeof(header) == 512);
/**
* Oar file header format.
*/
struct entry {
coral::path path;
coral::u64 data_offset;
coral::u64 data_length;
coral::u8 padding[240];
};
static_assert(sizeof(entry) == 512);
/**
* Archive file access interface.
*/
struct archive_file : public coral::file_reader {
/**
* Results of a find operation performed on an [archive_file].
*
* [find_result::ok] means that the find operation was successful.
*
* [find_result::io_unavailable] signals a failure to communicate with the underlying
* [coral::file_reader] for whatever reason.
*
* [find_result::archive_invalid] signals that data was read but it does not match the format
* of an Oar archive. This is typically because the underlying [coral::file_reader] is not
* reading from an Oar archive file.
*
* [find_result::archive_unsupported] signals that data was read and was formatted as expected
* for an Oar archive, however, it is from an unsupported version of the archive format.
*
* [find_result::not_found] indicates that no entry in the archive could be found that matches
* the given query.
*/
enum class [[nodiscard]] find_result {
ok,
io_unavailable,
archive_invalid,
archive_unsupported,
not_found,
};
archive_file(coral::file_reader * archive_reader) {
this->archive_reader = archive_reader;
}
/**
* Performs a lookup for a file entry matching the path `file_path` in the archive, returning
* [find_result] to indicate the result of the operation.
*/
find_result find(coral::path const & file_path) {
this->data_offset = 0;
this->data_length = 0;
this->data_cursor = 0;
if (!this->archive_reader->seek(0).is_ok()) return find_result::io_unavailable;
constexpr coral::usize header_size {sizeof(header)};
coral::u8 archive_header_buffer[header_size] {0};
if (!this->archive_reader->read(archive_header_buffer).and_test(
[](coral::usize value) -> bool { return value == header_size; }))
return find_result::archive_invalid;
header const * const archive_header {
reinterpret_cast<header const *>(archive_header_buffer)};
if (!coral::equals({archive_header->signature_magic, signature_identifier_length},
{signature_magic, signature_identifier_length})) return find_result::archive_invalid;
if (archive_header->signature_magic[signature_identifier_length] !=
signature_magic[signature_identifier_length]) return find_result::archive_unsupported;
// Read file table.
coral::u64 head {0};
coral::u64 tail {archive_header->entry_count - 1};
constexpr coral::usize entry_size {sizeof(entry)};
coral::u8 archive_entry_buffer[entry_size] {0};
while (head <= tail) {
coral::u64 const midpoint {head + ((tail - head) / 2)};
if (!this->archive_reader->seek(header_size + (entry_size * midpoint)).is_ok())
return find_result::archive_invalid;
if (!this->archive_reader->read(archive_entry_buffer).and_test(
[](coral::usize value) -> bool { return value == entry_size; }))
return find_result::archive_invalid;
entry const * const archive_entry {
reinterpret_cast<entry const *>(archive_entry_buffer)};
coral::size const comparison {file_path.compare(archive_entry->path)};
if (comparison == 0) {
this->data_offset = archive_entry->data_offset;
this->data_length = archive_entry->data_length;
this->data_cursor = archive_entry->data_offset;
return find_result::ok;
}
if (comparison > 0) {
head = (midpoint + 1);
} else {
tail = (midpoint - 1);
}
}
return find_result::not_found;
}
/**
* Attempts to read `data.length` bytes from the file and fill `data` with it, returning the
* number of bytes actually read or a [coral::io_error] value to indicate an error occured.
*/
coral::expected<coral::usize, coral::io_error> read(coral::slice<coral::u8> const & data) override {
if (this->data_offset < sizeof(header)) return coral::io_error::unavailable;
coral::usize const data_tail {this->data_offset + this->data_length};
if (!this->archive_reader->seek(coral::clamp(this->data_offset + this->data_cursor,
this->data_offset, data_tail)).is_ok()) return coral::io_error::unavailable;
coral::expected const data_read {this->archive_reader->read(
data.sliced(0, coral::min(data.length, data_tail - this->data_cursor)))};
if (data_read.is_ok()) this->data_cursor += data_read.value();
return data_read;
}
/**
* Attempts to seek to `offset` absolute position in the file, returning the new absolute
* cursor or a [coral::io_error] value to indicate an error occured.
*/
coral::expected<coral::u64, coral::io_error> seek(coral::u64 offset) override {
if (this->data_offset < sizeof(header)) return coral::io_error::unavailable;
this->data_cursor = offset;
return coral::io_error::unavailable;
}
/**
* Attempts to read to read the absolute file cursor position, returning it or a
* [coral::io_error] value to indicate an error occured.
*/
coral::expected<coral::u64, coral::io_error> tell() override {
if (this->data_offset < sizeof(header)) return coral::io_error::unavailable;
return this->data_cursor;
}
private:
coral::file_reader * archive_reader {nullptr};
coral::u64 data_offset {0};
coral::u64 data_length {0};
coral::u64 data_cursor {0};
};
export namespace oar {
struct archive : public coral::fs {
archive(coral::fs * backing_fs, coral::path const & archive_path) {
this->backing_fs = backing_fs;
this->archive_path = archive_path;
}
/**
* Queries the archive for the [coral::fs::access_rules] and returns them.
*/
access_rules query_access() override {
return {
.can_read = true,
.can_write = false,
};
}
/**
* Attempts to open a readable context for reading from the archive file identified by
* `file_path`, doing nothing if the requested file could not be found.
*/
void read_file(coral::path const & file_path,
coral::closure<void(coral::file_reader &)> const & then) override {
if ((this->backing_fs == nullptr) || (this->archive_path.byte_size() == 0)) return;
this->backing_fs->read_file(this->archive_path,
[&](coral::file_reader & archive_reader) {
archive_file file{&archive_reader};
if (file.find(file_path) != archive_file::find_result::ok) return;
then(file);
});
}
/**
* Attempts to open a writable context for reading from the archive file identified by
* `file_path`, however this will always do nothing as archive file-systems are read-only.
*/
void write_file(coral::path const & file_path,
coral::closure<void(coral::file_writer &)> const & then) override {
// Read-only file system.
}
private:
coral::fs * backing_fs;
coral::path archive_path;
};
}

230
source/oar/oar.zig Normal file
View File

@ -0,0 +1,230 @@
const coral = @import("coral");
const ona = @import("ona");
pub const Archive = struct {
state_table: [state_max]State = [_]State{.{}} ** state_max,
file_accessor: ona.files.FileAccessor,
file_path: []const u8,
const State = struct {
readable_file: ?*ona.files.ReadableFile = null,
data_head: u64 = 0,
data_size: u64 = 0,
data_cursor: u64 = 0,
fn cast(archived_file: *ArchivedFile) *Archive.State {
return @ptrCast(*State, @alignCast(@alignOf(State), archived_file));
}
};
const state_max = 64;
pub fn open_archived(self: *Archive, path: Path) OpenError!*ArchivedFile {
const state_index = find_available_state: {
var index: usize = 0;
while (index < self.state_table.len) : (index += 1) {
if (self.state_table[index].readable_file == null) break :find_available_state index;
}
return error.TooManyFiles;
};
const archive_file = try self.file_accessor.open_readable(self.file_path);
errdefer _ = archive_file.close();
var archive_header = Header.empty;
if ((try archive_file.read(&archive_header.bytes)) != Header.size) return error.ArchiveInvalid;
// Read file table.
var head: u64 = 0;
var tail: u64 = archive_header.layout.entry_count - 1;
const path_hash = path.hash();
while (head <= tail) {
const midpoint = head + ((tail - head) / 2);
var archive_block = Block.empty;
try archive_file.seek(Header.size + archive_header.layout.total_data_size + (Block.size * midpoint));
if ((try archive_file.read(&archive_block.bytes)) != Block.size) return error.ArchiveInvalid;
const path_hash_comparison = path_hash - archive_block.layout.path_hash;
if (path_hash_comparison > 0) {
head = (midpoint + 1);
continue;
}
if (path_hash_comparison < 0) {
tail = (midpoint - 1);
continue;
}
const path_comparison = path.compare(archive_block.layout.path);
if (path_comparison > 0) {
head = (midpoint + 1);
continue;
}
if (path_comparison < 0) {
tail = (midpoint - 1);
continue;
}
self.state_table[state_index] = .{
.readable_file = archive_file,
.data_head = archive_block.layout.data_head,
.data_size = archive_block.layout.data_size,
.data_cursor = 0,
};
return @ptrCast(*ArchivedFile, &(self.state_table[state_index]));
}
return error.FileNotFound;
}
};
pub const ArchivedFile = opaque {
pub fn as_reader(self: *ArchivedFile) coral.io.Reader {
return coral.io.Reader.bind(self, ArchivedFile);
}
pub fn close(self: *ArchivedFile) bool {
const state = Archive.State.cast(self);
if (state.readable_file) |readable_file| {
defer state.readable_file = null;
return readable_file.close();
}
return true;
}
pub fn read(self: *ArchivedFile, buffer: []u8) coral.io.ReadError!usize {
const state = Archive.State.cast(self);
if (state.readable_file) |readable_file| {
const actual_cursor = coral.math.min(u64,
state.data_head + state.data_cursor, state.data_head + state.data_size);
try readable_file.seek(actual_cursor);
const buffer_read = coral.math.min(usize, buffer.len, state.data_size - actual_cursor);
defer state.data_cursor += buffer_read;
return readable_file.read(buffer[0..buffer_read]);
}
return error.IoUnavailable;
}
pub fn size(self: *ArchivedFile) u64 {
return Archive.State.cast(self).data_size;
}
};
const Block = extern union {
layout: extern struct {
path: Path,
path_hash: u64,
data_head: u64,
data_size: u64,
},
bytes: [size]u8,
const empty = Block{ .bytes = [_]u8{0} ** size };
const size = 512;
};
const Header = extern union {
layout: extern struct {
signature: [signature_magic.len]u8,
entry_count: u32,
total_data_size: u64,
},
bytes: [size]u8,
const empty = Header{ .bytes = [_]u8{0} ** size };
const signature_magic = [_]u8{ 'o', 'a', 'r', 1 };
const size = 16;
};
pub const OpenError = ona.files.OpenError || coral.io.ReadError || error{
ArchiveInvalid,
};
pub const Path = extern struct {
buffer: [maximum + 1]u8,
pub const DataError = error{
PathCorrupt,
};
pub const ParseError = error{
TooLong,
};
pub fn compare(self: Path, other: Path) isize {
return coral.io.compare(&self.buffer, &other.buffer);
}
pub fn data(self: Path) DataError![:0]const u8 {
// Verify presence of zero terminator.
if (self.buffer[self.filled()] != 0) return error.PathCorrupt;
return @ptrCast([:0]const u8, self.buffer[0..self.filled()]);
}
pub fn filled(self: Path) usize {
return maximum - self.remaining();
}
pub fn hash(self: Path) u64 {
// FowlerNollVo hash function is used here as it has a lower collision rate for smaller inputs.
const fnv_prime = 0x100000001b3;
var hash_code = @as(u64, 0xcbf29ce484222325);
for (self.buffer[0..self.filled()]) |byte| {
hash_code = hash_code ^ byte;
hash_code = hash_code *% fnv_prime;
}
return hash_code;
}
pub const maximum = 255;
pub fn parse(bytes: []const u8) ParseError!Path {
if (bytes.len > maximum) return error.TooLong;
// Int cast is safe as bytes length is confirmed to be smaller than or equal to u8 maximum.
var parsed_path = Path{ .buffer = ([_]u8{0} ** maximum) ++ [_]u8{maximum - @intCast(u8, bytes.len)} };
coral.io.copy(&parsed_path.buffer, bytes);
return parsed_path;
}
pub fn remaining(self: Path) usize {
return self.buffer[maximum];
}
pub const seperator = '/';
};

4
source/ona/ext.zig Normal file
View File

@ -0,0 +1,4 @@
pub usingnamespace @cImport({
@cInclude("SDL2/SDL.h");
});

201
source/ona/files.zig Normal file
View File

@ -0,0 +1,201 @@
const coral = @import("coral");
const ext = @import("./ext.zig");
pub const FileAccessor = struct {
context: *anyopaque,
actions: *const struct {
open_readable: *const fn (context: *anyopaque, file_path: []const u8) OpenError!*ReadableFile,
open_writable: *const fn (context: *anyopaque, file_path: []const u8) OpenError!*WritableFile,
query: *const fn (context: *anyopaque, file_path: []const u8) QueryError!FileInfo,
},
pub fn bind(comptime State: type, state: *State) FileAccessor {
const Actions = struct {
fn as_concrete(context: *anyopaque) *State {
return @ptrCast(*State, @alignCast(@alignOf(State), context));
}
fn open_readable(context: *anyopaque, file_path: []const u8) OpenError!*ReadableFile {
return as_concrete(context).open_readable(file_path);
}
fn open_writable(context: *anyopaque, file_path: []const u8) OpenError!*WritableFile {
return as_concrete(context).open_writable(file_path);
}
fn query(context: *anyopaque, file_path: []const u8) QueryError!FileInfo {
return as_concrete(context).query(file_path);
}
};
return .{
.context = @ptrCast(*anyopaque, state),
.actions = &.{
.open_readable = Actions.open_readable,
.open_writable = Actions.open_writable,
.query = Actions.query,
},
};
}
pub fn open_readable(self: FileAccessor, file_path: []const u8) OpenError!*ReadableFile {
return self.actions.open_readable(self.context, file_path);
}
pub fn open_writable(self: FileAccessor, file_path: []const u8) OpenError!*WritableFile {
return self.actions.open_readable(self.context, file_path);
}
pub fn query(self: FileAccessor, file_path: []const u8) QueryError!FileInfo {
return self.actions.query(self.context, file_path);
}
};
pub const FileInfo = struct {
size: u64,
};
pub const FileSandbox = struct {
prefix: []const u8,
flags: packed struct {
is_readable: bool = false,
is_writable: bool = false,
is_queryable: bool = false,
},
const native_path_max = 4095;
fn native_path_of(file_sandbox: *FileSandbox, file_path: []const u8) [native_path_max + 1]u8 {
var native_path = [_]u8{0} ** (native_path_max + 1);
if ((file_sandbox.prefix.len + file_path.len) < native_path_max) {
coral.io.copy(&native_path, file_sandbox.prefix);
coral.io.copy(native_path[file_sandbox.prefix.len ..], file_path);
}
return native_path;
}
pub fn open_readable(file_sandbox: *FileSandbox, file_path: []const u8) OpenError!*ReadableFile {
if (!file_sandbox.flags.is_readable) return error.AccessDenied;
return @ptrCast(*ReadableFile, ext.SDL_RWFromFile(&file_sandbox.native_path_of(file_path), "rb") orelse {
return error.FileNotFound;
});
}
pub fn open_writable(file_sandbox: *FileSandbox, file_path: []const u8) OpenError!*WritableFile {
if (!file_sandbox.flags.is_writable) return error.AccessDenied;
return @ptrCast(*WritableFile, ext.SDL_RWFromFile(&file_sandbox.native_path_of(file_path), "wb") orelse {
return error.FileNotFound;
});
}
pub fn query(file_sandbox: *FileSandbox, file_path: []const u8) QueryError!FileInfo {
if (!file_sandbox.flags.is_queryable) return error.AccessDenied;
const rw_ops = ext.SDL_RWFromFile(&file_sandbox.native_path_of(file_path), "rb") orelse {
return error.FileNotFound;
};
defer _ = ext.SDL_RWclose(rw_ops);
const file_size = ext.SDL_RWsize(rw_ops);
if (file_size < 0) return error.FileNotFound;
return FileInfo{
.size = @intCast(u64, file_size),
};
}
};
pub const OpenError = QueryError || error {TooManyFiles};
pub const ReadableFile = opaque {
pub fn as_reader(self: *ReadableFile) coral.io.Reader {
return coral.io.Reader.bind(self, ReadableFile);
}
fn as_rw_ops(self: *ReadableFile) *ext.SDL_RWops {
return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), self));
}
pub fn close(self: *ReadableFile) bool {
return ext.SDL_RWclose(self.as_rw_ops()) != 0;
}
pub fn read(self: *ReadableFile, buffer: []u8) coral.io.ReadError!usize {
ext.SDL_ClearError();
const buffer_read = ext.SDL_RWread(self.as_rw_ops(), buffer.ptr, @sizeOf(u8), buffer.len);
if ((buffer_read == 0) and (ext.SDL_GetError().* != 0)) return error.IoUnavailable;
return buffer_read;
}
pub fn rewind(self: *ReadableFile) SeekError!void {
return self.seek(0);
}
pub fn seek(self: *ReadableFile, absolute: u64) SeekError!void {
ext.SDL_ClearError();
// TODO: Fix int cast.
const sought = ext.SDL_RWseek(self.as_rw_ops(), @intCast(i64, absolute), ext.RW_SEEK_SET);
if ((sought == -1) and (ext.SDL_GetError().* != 0)) return error.IoUnavailable;
}
};
pub const QueryError = error {
FileNotFound,
AccessDenied,
};
pub const SeekError = error {
IoUnavailable,
};
pub const WritableFile = opaque {
pub fn as_writer(self: *WritableFile) coral.io.Writer {
return coral.io.Writer.bind(WritableFile, self);
}
fn as_rw_ops(self: *WritableFile) *ext.SDL_RWops {
return @ptrCast(*ext.SDL_RWops, @alignCast(@alignOf(ext.SDL_RWops), self));
}
pub fn close(self: *WritableFile) bool {
return ext.SDL_RWclose(self.as_rw_ops()) != 0;
}
pub fn rewind(self: *WritableFile) SeekError!void {
return self.seek(0);
}
pub fn seek(self: *WritableFile, absolute: u64) SeekError!void {
ext.SDL_ClearError();
// TODO: Fix int cast.
const sought = ext.SDL_RWseek(self.as_rw_ops(), @intCast(i64, absolute), ext.RW_SEEK_SET);
if ((sought == -1) and (ext.SDL_GetError().* != 0)) return error.IoUnavailable;
}
pub fn write(self: *WritableFile, buffer: []const u8) coral.io.WriteError!usize {
ext.SDL_ClearError();
const buffer_read = ext.SDL_RWwrite(self.as_rw_ops(), buffer.ptr, @sizeOf(u8), buffer.len);
if ((buffer_read == 0) and (ext.SDL_GetError().* != 0)) return error.IoUnavailable;
return buffer_read;
}
};

16
source/ona/gfx.zig Normal file
View File

@ -0,0 +1,16 @@
const coral = @import("coral");
pub const Canvas = struct {
pub fn create_sprite(_: *Canvas, _: SpriteProperties) void {
}
};
pub const Sprite = struct {
};
pub const SpriteProperties = struct {
position: coral.math.Vector2,
rotation: f32,
};

109
source/ona/ona.zig Normal file
View File

@ -0,0 +1,109 @@
const coral = @import("coral");
const ext = @import("./ext.zig");
pub const files = @import("./files.zig");
pub const gfx = @import("./gfx.zig");
pub const App = opaque {
pub fn Starter(comptime errors: type) type {
return fn (app_state: *App) errors!void;
}
const State = struct {
last_event: ext.SDL_Event,
base_file_sandbox: files.FileSandbox,
canvas: gfx.Canvas,
fn cast(self: *App) *State {
return @ptrCast(*State, @alignCast(@alignOf(State), self));
}
};
pub fn canvas(self: *App) *gfx.Canvas {
return &State.cast(self).canvas;
}
pub fn data_fs(self: *App) files.FileAccessor {
return files.FileAccessor.bind(files.FileSandbox, &State.cast(self).base_file_sandbox);
}
pub fn poll(self: *App) bool {
const state = State.cast(self);
while (ext.SDL_PollEvent(&state.last_event) != 0) switch (state.last_event.type) {
ext.SDL_QUIT => return false,
else => {},
};
return true;
}
pub fn run(comptime errors: type, start: *const Starter(errors)) errors!void {
const base_prefix = ext.SDL_GetBasePath() orelse {
return log_error(&.{.{.string = coral.io.slice_sentineled(u8, 0, ext.SDL_GetError())}});
};
defer ext.SDL_free(base_prefix);
var state = App.State{
.last_event = undefined,
.base_file_sandbox = .{
.prefix = coral.io.slice_sentineled(u8, 0, base_prefix),
.flags = .{
.is_readable = true,
.is_queryable = true,
}
},
.canvas = .{},
};
return start(@ptrCast(*App, &state));
}
};
pub const allocator = coral.io.MemoryAllocator.bind(&heap, @TypeOf(heap));
var heap = struct {
live_allocations: usize = 0,
const Self = @This();
pub fn reallocate(self: *Self, maybe_allocation: ?*anyopaque, size: usize) ?[*]u8 {
if (size == 0) {
ext.SDL_free(maybe_allocation);
self.live_allocations -= 1;
return null;
}
if (ext.SDL_realloc(maybe_allocation, size)) |allocation| {
self.live_allocations += 1;
return @ptrCast([*]u8, allocation);
}
return null;
}
}{};
pub fn log_debug(values: []const coral.format.Value) void {
var message_memory = [_]u8{0} ** 4096;
var message_buffer = coral.buffer.Fixed{.data = &message_memory};
const message_length = coral.format.print(message_buffer.as_writer(), values) catch return;
ext.SDL_LogDebug(ext.SDL_LOG_CATEGORY_APPLICATION, "%.*s\n", message_length, &message_buffer);
}
pub fn log_error(values: []const coral.format.Value) void {
var message_memory = [_]u8{0} ** 4096;
var message_buffer = coral.buffer.Fixed{.data = &message_memory};
const message_length = coral.format.print(message_buffer.as_writer(), values) catch return;
ext.SDL_LogError(ext.SDL_LOG_CATEGORY_APPLICATION, "%.*s\n", message_length, &message_buffer);
}

68
source/runner.zig Normal file
View File

@ -0,0 +1,68 @@
const coral = @import("coral");
const kym = @import("kym");
const ona = @import("ona");
fn create_canvas_binding(binding_vm: *kym.Vm, canvas: *ona.gfx.Canvas) !*kym.Object {
const canvas_object = try binding_vm.new(.{.native = canvas}, .{});
const Object = kym.Object;
const Value = kym.Value;
try binding_vm.set_field(canvas_object, "create_sprite", .{.object = try binding_vm.new_closure(struct {
fn call(calling_vm: *kym.Vm, _: *Object, context: *Object, arguments: []const Value) kym.RuntimeError!Value {
const properties = try kym.object_argument(calling_vm, arguments, 0);
@ptrCast(*ona.gfx.Canvas, context.userdata().native).create_sprite(.{
.position = calling_vm.get_field(properties, "position").to_vector2() orelse coral.math.Vector2.zero,
.rotation = @floatCast(f32, calling_vm.get_field(properties, "rotation").to_float() orelse 0.0),
});
return .nil;
}
}.call)});
return canvas_object;
}
pub fn main() anyerror!void {
return ona.App.run(anyerror, start);
}
fn start(app: *ona.App) anyerror!void {
var vm = try kym.Vm.init(ona.allocator, .{
.stack_max = 256,
.objects_max = 512,
});
defer vm.deinit();
try vm.set_field(vm.globals(), "events", .{.object = try create_canvas_binding(&vm, app.canvas())});
{
const index_path = "index.kym";
const index_file = try app.data_fs().open_readable(index_path);
defer if (!index_file.close()) ona.log_error(&.{.{.string = "failed to close "}, .{.string = index_path}});
const index_size = (try app.data_fs().query(index_path)).size;
const index_allocation = ona.allocator.allocate_many(u8, index_size) orelse return error.OutOfMemory;
defer ona.allocator.deallocate(index_allocation);
var index_buffer = coral.buffer.Fixed{.data = index_allocation[0 .. index_size]};
{
var stream_buffer = [_]u8{0} ** 1024;
if ((try coral.io.stream(index_buffer.as_writer(), index_file.as_reader(), &stream_buffer)) != index_size)
return error.IoUnavailable;
}
_ = try vm.call_self(try vm.new_script(index_buffer.data), &.{});
}
while (app.poll()) {
}
}

View File

@ -1,55 +0,0 @@
export module runtime;
import app;
import coral;
import coral.files;
import coral.io;
import coral.math;
import coral.stack;
extern "C" int main(int argc, char const * const * argv) {
return app::client::run("Ona Runtime", [](app::client & client) -> int {
constexpr coral::path config_path{"config.kym"};
bool is_config_loaded{false};
client.resources().read_file(config_path, [&](coral::reader & file) {
coral::allocator * const allocator{&client.thread_safe_allocator()};
coral::small_stack<coral::u8> script_source{allocator};
{
coral::u8 stream_buffer[1024]{0};
coral::stack_writer script_writer{&script_source};
if (!coral::stream(script_writer, file, stream_buffer).is_ok()) return;
}
client.log(app::log_level::notice, script_source.as_slice().as_chars());
is_config_loaded = true;
});
if (!is_config_loaded) {
coral::small_stack<coral::u8> error_message{&client.thread_safe_allocator()};
{
coral::stack_writer error_writer{&error_message};
if (!error_writer.write(coral::slice{"failed to load "}.as_bytes()).is_ok())
return coral::u8_max;
if (!error_writer.write(config_path.as_slice().as_bytes()).is_ok())
return coral::u8_max;
}
return coral::u8_max;
}
client.display(1280, 800);
while (client.poll()) {
}
return 0;
});
}