diff --git a/.drone.yml b/.drone.yml index 2047905..f9941e1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,7 +3,7 @@ name: continuous integration steps: - name: build & test - image: ubuntu:jammy + image: euantorano/zig:0.9.1 commands: - - apt update && apt install -y clang libsdl2-dev python3.10 - - python3.10 ./build.py + - zig build test + - $(find zig-cache -name test) main.zig diff --git a/.gitignore b/.gitignore index fa7dadd..d864d9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -cache -runtime -runtime.exe +/zig-cache/ +/zig-out/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json deleted file mode 100644 index a0dc464..0000000 --- a/.vscode/c_cpp_properties.json +++ /dev/null @@ -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 -} diff --git a/.vscode/launch.json b/.vscode/launch.json old mode 100644 new mode 100755 index 7b90bde..a386639 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,8 +5,8 @@ "name": "Runtime", "type": "gdb", "request": "launch", - "target": "./runtime", - "cwd": "${workspaceRoot}", + "target": "./zig-out/bin/ona-runner", + "cwd": "${workspaceRoot}/debug", "valuesFormatting": "parseText" }, { diff --git a/.vscode/settings.json b/.vscode/settings.json old mode 100644 new mode 100755 index 7f4ecdd..2dfaba5 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,16 +1,18 @@ { - "files.associations": { - "type_traits": "cpp", - "cassert": "cpp", - "cstddef": "cpp", - "string_view": "cpp", - "system_error": "cpp", - "array": "cpp", - "functional": "cpp", - "tuple": "cpp", - "utility": "cpp" - }, + "editor.minimap.maxColumn": 120, "editor.detectIndentation": 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, } diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100755 index 0000000..3c18e06 --- /dev/null +++ b/.vscode/tasks.json @@ -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 + }, + } + ] +} diff --git a/build.py b/build.py deleted file mode 100755 index 785f475..0000000 --- a/build.py +++ /dev/null @@ -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) diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..ac48ef9 --- /dev/null +++ b/build.zig @@ -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); + } +} diff --git a/config.kym b/config.kym deleted file mode 100644 index 187e4f2..0000000 --- a/config.kym +++ /dev/null @@ -1,6 +0,0 @@ - -return { - title = "Demo", - width = 640, - height = 480, -} diff --git a/debug/index.kym b/debug/index.kym new file mode 100644 index 0000000..627f26f --- /dev/null +++ b/debug/index.kym @@ -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, +} diff --git a/source/app.cpp b/source/app.cpp deleted file mode 100644 index de3f7d2..0000000 --- a/source/app.cpp +++ /dev/null @@ -1,317 +0,0 @@ -module; - -#include - -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 read(coral::slice 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 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(offset), RW_SEEK_SET)}; - - if (byte_position == -1) return coral::io_error::unavailable; - - return static_cast(byte_position); - } - - coral::expected 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(byte_position); - } - - coral::expected write(coral::slice 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 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 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 const & message) { - coral::i32 const length{static_cast( - coral::min(message.length, static_cast(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 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(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"}; - }; -} diff --git a/source/coral.cpp b/source/coral.cpp deleted file mode 100644 index 27e163e..0000000 --- a/source/coral.cpp +++ /dev/null @@ -1,596 +0,0 @@ -module; - -#include -#include -#include - -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 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(slice_end - slice_begin); - } - - template 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 as_bytes() const { - return {reinterpret_cast(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 as_chars() const { - return {reinterpret_cast(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() const { - return (*reinterpret_cast 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 constexpr scalar max(scalar const & a, scalar const & b) { - return (a > b) ? a : b; - } - - /** - * Returns the minimum value between `a` and `b`. - */ - template 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 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 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 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 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 struct closure { - using function = returns(*)(arguments...); - - closure(function callable_function) { - this->dispatch = [](void const * context, arguments... dispatch_arguments) -> returns { - return (reinterpret_cast(context))(dispatch_arguments...); - }; - - this->context = callable_function; - } - - template closure(functor * callable_functor) { - this->dispatch = [](void const * context, arguments... dispatch_arguments) -> returns { - return (*reinterpret_cast(context))(dispatch_arguments...); - }; - - this->context = callable_functor; - } - - closure(closure const &) = delete; - - template closure(functor && callable_functor) { - this->dispatch = [](void const * context, arguments... dispatch_arguments) -> returns { - return (*reinterpret_cast(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 struct [[nodiscard]] optional { - optional() : buffer{0} {} - - optional(element const & value) : buffer{0} { - (*reinterpret_cast(this->buffer)) = value; - this->buffer[sizeof(element)] = 1; - } - - optional(optional const & that) : buffer{0} { - if (that.has_value()) { - (*reinterpret_cast(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 std::invoke_result_t 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(this->buffer) : fallback; - } - - element & operator *() { - if (!this->has_value()) unreachable(); - - return *reinterpret_cast(this->buffer); - } - - element const & operator *() const { - if (!this->has_value()) unreachable(); - - return *reinterpret_cast(this->buffer); - } - - private: - u8 buffer[sizeof(element) + 1]; - }; - - /** - * Monadic container for a descriminating union of either `value_element` or `error_element`. - */ - template struct [[nodiscard]] expected { - expected(value_element const & value) : buffer{0} { - (*reinterpret_cast(this->buffer)) = value; - this->buffer[buffer_size] = 1; - } - - expected(error_element const & error) : buffer{0} { - (*reinterpret_cast(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 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(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(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(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(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 read(slice 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 write(slice const & data) = 0; - }; -} - -// Input/output operations. -export namespace coral { - /** - * Returns `value` reinterpreted as a sequence of bytes. - */ - slice as_bytes(auto const * value) { - return {reinterpret_cast(value), sizeof(value)}; - } - - /** - * Compares `a` and `b`, returning the difference between them or `0` if they are identical. - */ - constexpr size compare(slice const & a, slice const & b) { - usize const range = min(a.length, b.length); - - for (usize index = 0; index < range; index += 1) { - size const difference = static_cast(a[index]) - static_cast(b[index]); - - if (difference != 0) return difference; - } - - return static_cast(a.length) - static_cast(b.length); - } - - /** - * Copies the contents of `origin` into `target`. - * - * *Note*: safety-checked behavior is triggered if `target` is smaller than `origin`. - */ - void copy(slice const & target, slice 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 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 const & a, slice 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 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 constexpr void swap(element & a, element & b) { - element const temp = a; - a = b; - b = temp; - } -} diff --git a/source/coral/buffer.zig b/source/coral/buffer.zig new file mode 100644 index 0000000..621dada --- /dev/null +++ b/source/coral/buffer.zig @@ -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(); + } +}; diff --git a/source/coral/coral.zig b/source/coral/coral.zig new file mode 100644 index 0000000..33e24f5 --- /dev/null +++ b/source/coral/coral.zig @@ -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"); diff --git a/source/coral/debug.zig b/source/coral/debug.zig new file mode 100644 index 0000000..d6dd2a6 --- /dev/null +++ b/source/coral/debug.zig @@ -0,0 +1,4 @@ + +pub fn assert(condition: bool) void { + if (!condition) unreachable; +} diff --git a/source/coral/files.cpp b/source/coral/files.cpp deleted file mode 100644 index 100a35c..0000000 --- a/source/coral/files.cpp +++ /dev/null @@ -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 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 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(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 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 seek(u64 offset) = 0; - - virtual expected 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 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 const & then) = 0; - }; -} diff --git a/source/coral/format.zig b/source/coral/format.zig new file mode 100644 index 0000000..bf5a4f2 --- /dev/null +++ b/source/coral/format.zig @@ -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]); +} diff --git a/source/coral/image.cpp b/source/coral/image.cpp deleted file mode 100644 index 049ccc4..0000000 --- a/source/coral/image.cpp +++ /dev/null @@ -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(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(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(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(round32(clamp(this->a, 0.0f, 1.0f) * u8_max)); - } - }; -} diff --git a/source/coral/io.cpp b/source/coral/io.cpp deleted file mode 100644 index 1ae6019..0000000 --- a/source/coral/io.cpp +++ /dev/null @@ -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 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 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 read(slice 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 write(slice 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 stream(writer & output, reader & input, slice 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 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((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}); - } -} diff --git a/source/coral/io.zig b/source/coral/io.zig new file mode 100644 index 0000000..309de59 --- /dev/null +++ b/source/coral/io.zig @@ -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; +} diff --git a/source/coral/math.cpp b/source/coral/math.cpp deleted file mode 100644 index feb6a0e..0000000 --- a/source/coral/math.cpp +++ /dev/null @@ -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; - }; -} diff --git a/source/coral/math.zig b/source/coral/math.zig new file mode 100644 index 0000000..5737634 --- /dev/null +++ b/source/coral/math.zig @@ -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; +} diff --git a/source/coral/stack.cpp b/source/coral/stack.cpp deleted file mode 100644 index 50b74dd..0000000 --- a/source/coral/stack.cpp +++ /dev/null @@ -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 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 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 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 struct small_stack : public stack { - 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 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(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 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(this->elements.pointer), - sizeof(element) * requested_capacity); - - if (buffer == nullptr) { - this->elements = {}; - - return push_result::out_of_memory; - } - - this->elements = {reinterpret_cast(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(buffer), requested_capacity}; - } - - return push_result::ok; - } - - private: - allocator * dynamic_allocator{nullptr}; - - usize filled{0}; - - slice elements{reinterpret_cast(local_buffer), init_capacity}; - - u8 local_buffer[init_capacity]{0}; - }; -} - -using byte_stack = coral::stack; - -// 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 read(slice 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 write(slice 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}; - }; -} diff --git a/source/coral/stack.zig b/source/coral/stack.zig new file mode 100644 index 0000000..ea5dec8 --- /dev/null +++ b/source/coral/stack.zig @@ -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; + } + }); +} diff --git a/source/coral/table.zig b/source/coral/table.zig new file mode 100644 index 0000000..41b563d --- /dev/null +++ b/source/coral/table.zig @@ -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, +}; diff --git a/source/coral/utf8.zig b/source/coral/utf8.zig new file mode 100644 index 0000000..fdc1794 --- /dev/null +++ b/source/coral/utf8.zig @@ -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; +} diff --git a/source/kym/bytecode.zig b/source/kym/bytecode.zig new file mode 100644 index 0000000..1b7324a --- /dev/null +++ b/source/kym/bytecode.zig @@ -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, +}; diff --git a/source/kym/kym.zig b/source/kym/kym.zig new file mode 100644 index 0000000..730789d --- /dev/null +++ b/source/kym/kym.zig @@ -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; +} diff --git a/source/kym/tokens.zig b/source/kym/tokens.zig new file mode 100644 index 0000000..aa05a70 --- /dev/null +++ b/source/kym/tokens.zig @@ -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; + } +}; diff --git a/source/oar.cpp b/source/oar.cpp deleted file mode 100644 index a60a902..0000000 --- a/source/oar.cpp +++ /dev/null @@ -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
(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(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 read(coral::slice 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 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 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 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 const & then) override { - - // Read-only file system. - } - - private: - coral::fs * backing_fs; - - coral::path archive_path; - }; -} diff --git a/source/oar/oar.zig b/source/oar/oar.zig new file mode 100644 index 0000000..988c48a --- /dev/null +++ b/source/oar/oar.zig @@ -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 = '/'; +}; diff --git a/source/ona/ext.zig b/source/ona/ext.zig new file mode 100644 index 0000000..1be2ca3 --- /dev/null +++ b/source/ona/ext.zig @@ -0,0 +1,4 @@ + +pub usingnamespace @cImport({ + @cInclude("SDL2/SDL.h"); +}); diff --git a/source/ona/files.zig b/source/ona/files.zig new file mode 100644 index 0000000..e0be9fc --- /dev/null +++ b/source/ona/files.zig @@ -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; + } +}; diff --git a/source/ona/gfx.zig b/source/ona/gfx.zig new file mode 100644 index 0000000..da63017 --- /dev/null +++ b/source/ona/gfx.zig @@ -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, +}; diff --git a/source/ona/ona.zig b/source/ona/ona.zig new file mode 100644 index 0000000..e8d318e --- /dev/null +++ b/source/ona/ona.zig @@ -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); +} diff --git a/source/runner.zig b/source/runner.zig new file mode 100644 index 0000000..b4eec6f --- /dev/null +++ b/source/runner.zig @@ -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()) { + + } +} diff --git a/source/runtime.cpp b/source/runtime.cpp deleted file mode 100644 index 9beb574..0000000 --- a/source/runtime.cpp +++ /dev/null @@ -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 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 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; - }); -}