Port back to Zig
continuous-integration/drone Build is failing
Details
continuous-integration/drone Build is failing
Details
This commit is contained in:
parent
78ae403f61
commit
359fcd190d
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
cache
|
/zig-cache/
|
||||||
runtime
|
/zig-out/
|
||||||
runtime.exe
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
35
build.py
35
build.py
|
@ -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)
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
return {
|
|
||||||
title = "Demo",
|
|
||||||
width = 640,
|
|
||||||
height = 480,
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
}
|
317
source/app.cpp
317
source/app.cpp
|
@ -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"};
|
|
||||||
};
|
|
||||||
}
|
|
596
source/coral.cpp
596
source/coral.cpp
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
|
@ -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");
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
pub fn assert(condition: bool) void {
|
||||||
|
if (!condition) unreachable;
|
||||||
|
}
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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]);
|
||||||
|
}
|
|
@ -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));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -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,
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
253
source/oar.cpp
253
source/oar.cpp
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
// Fowler–Noll–Vo 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 = '/';
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
pub usingnamespace @cImport({
|
||||||
|
@cInclude("SDL2/SDL.h");
|
||||||
|
});
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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,
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
|
@ -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()) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in New Issue