Clean up core library.
This commit is contained in:
parent
1261ee7e60
commit
e3c4ab65f1
|
@ -2,7 +2,7 @@ export module app;
|
|||
|
||||
import core;
|
||||
import core.image;
|
||||
import core.math;
|
||||
import core.lalgebra;
|
||||
|
||||
export namespace app {
|
||||
struct path {
|
||||
|
@ -32,8 +32,8 @@ export namespace app {
|
|||
return 0;
|
||||
}
|
||||
|
||||
bool equals(path const & that) {
|
||||
return core::equals(this->as_slice(), that.as_slice());
|
||||
bool equals(path const & that) const {
|
||||
return core::equals(this->as_slice().as_bytes(), that.as_slice().as_bytes());
|
||||
}
|
||||
|
||||
constexpr path joined(core::slice<char const> const & text) const {
|
||||
|
|
|
@ -9,19 +9,24 @@ import app;
|
|||
import core;
|
||||
import core.image;
|
||||
import core.sequence;
|
||||
import core.math;
|
||||
import core.lalgebra;
|
||||
|
||||
struct bundled_file_store : public app::file_store {
|
||||
void read_file(app::path const & file_path, core::callable<void(core::readable const &)> const & then) override {
|
||||
// Path is guaranteed to never be greater than 512 characters long (file_path is max 256 and prefix is 2).
|
||||
core::stack<char, 512> path_buffer{&core::null_allocator()};
|
||||
constexpr core::slice<char const> path_prefix = "./";
|
||||
constexpr core::usize path_max = 512;
|
||||
|
||||
if (path_buffer.append("./").has_value()) core::unreachable();
|
||||
if ((file_path.size() + path_prefix.length) > path_max) core::unreachable();
|
||||
|
||||
core::stack<char, path_max> path_buffer{&core::null_allocator()};
|
||||
|
||||
if (path_buffer.append("./") != core::append_result::ok) core::unreachable();
|
||||
|
||||
// File path is guaranteed to be null-terminated.
|
||||
if (path_buffer.append(file_path.as_slice()).has_value()) core::unreachable();
|
||||
if (path_buffer.append(file_path.as_slice()) != core::append_result::ok)
|
||||
core::unreachable();
|
||||
|
||||
SDL_RWops * rw_ops = ::SDL_RWFromFile(path_buffer.as_slice().pointer, "r");
|
||||
SDL_RWops * rw_ops = ::SDL_RWFromFile(path_buffer.begin(), "r");
|
||||
|
||||
if (rw_ops == nullptr) return;
|
||||
|
||||
|
|
473
source/core.cpp
473
source/core.cpp
|
@ -5,6 +5,20 @@ module;
|
|||
|
||||
export module core;
|
||||
|
||||
// Runtime utilities.
|
||||
export namespace core {
|
||||
/**
|
||||
* 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 core {
|
||||
using usize = size_t;
|
||||
|
||||
|
@ -36,119 +50,231 @@ export namespace core {
|
|||
|
||||
using f64 = double;
|
||||
|
||||
/**
|
||||
* Base type for runtime-pluggable memory allocation strategies used by the core library.
|
||||
*/
|
||||
struct allocator {
|
||||
allocator() = default;
|
||||
|
||||
allocator(allocator const &) = delete;
|
||||
|
||||
virtual ~allocator() {};
|
||||
|
||||
virtual u8 * reallocate(u8 * maybe_allocation, usize requested_size) = 0;
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
}
|
||||
|
||||
export void * operator new(core::usize requested_size, core::u8 * allocation_placement) {
|
||||
return allocation_placement;
|
||||
}
|
||||
|
||||
export void * operator new[](core::usize requested_size, core::u8 * allocation_placement) {
|
||||
return allocation_placement;
|
||||
}
|
||||
|
||||
export void * operator new(core::usize requested_size, core::allocator & allocator) {
|
||||
return allocator.reallocate(nullptr, requested_size);
|
||||
}
|
||||
|
||||
export void * operator new[](core::usize requested_size, core::allocator & allocator) {
|
||||
return allocator.reallocate(nullptr, requested_size);
|
||||
}
|
||||
|
||||
export namespace core {
|
||||
[[noreturn]] void unreachable() {
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
template<typename scalar> constexpr scalar max(scalar const & a, scalar const & b) {
|
||||
return (a > b) ? a : b;
|
||||
}
|
||||
|
||||
template<typename scalar> constexpr scalar min(scalar const & a, scalar const & b) {
|
||||
return (a < b) ? a : b;
|
||||
}
|
||||
|
||||
template<typename scalar> constexpr scalar clamp(scalar const & value, scalar const & min_value, scalar const & max_value) {
|
||||
return max(min_value, min(max_value, value));
|
||||
}
|
||||
|
||||
f32 round32(f32 value) {
|
||||
return __builtin_roundf(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Length-signed pointer type that describes how many elements of `type` it references,
|
||||
* providing a type-safe wrapper for passing arrays and zero-terminated strings to functions.
|
||||
*/
|
||||
template<typename type> struct slice {
|
||||
/**
|
||||
* Number of `type` elements referenced.
|
||||
*/
|
||||
usize length;
|
||||
|
||||
/**
|
||||
* Base element address referenced.
|
||||
*/
|
||||
type * pointer;
|
||||
|
||||
constexpr slice() : length{0}, pointer{nullptr} {
|
||||
|
||||
constexpr slice() {
|
||||
this->length = 0;
|
||||
this->pointer = nullptr;
|
||||
}
|
||||
|
||||
constexpr slice(char const *&& zstring) : length{0}, pointer{zstring} {
|
||||
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) : length{slice_length}, pointer{slice_pointer} {
|
||||
|
||||
constexpr slice(type * slice_pointer, usize slice_length) {
|
||||
this->pointer = slice_pointer;
|
||||
this->length = slice_length;
|
||||
}
|
||||
|
||||
template<usize array_size> constexpr slice(type(&array)[array_size]) : length{array_size}, pointer{array} {
|
||||
|
||||
constexpr slice(type * slice_begin, type * slice_end) {
|
||||
this->pointer = slice_begin;
|
||||
this->length = static_cast<usize>(slice_end - slice_begin);
|
||||
}
|
||||
|
||||
template<usize array_size> constexpr slice(type(&array)[array_size]) {
|
||||
this->pointer = array;
|
||||
this->length = array_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinterprets the data referenced as a series of bytes.
|
||||
*
|
||||
* The returned view is constant to protect against inadvertant memory corruption.
|
||||
*/
|
||||
slice<u8 const> as_bytes() const {
|
||||
return {reinterpret_cast<u8 const *>(this->pointer), this->length * sizeof(type)};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinterprets the data referenced as a series of chars.
|
||||
*
|
||||
* The returned view is constant to protect against inadvertant memory corruption.
|
||||
*
|
||||
* *Note* the returned value has no guarantees about the validity of any specific character
|
||||
* encoding set.
|
||||
*/
|
||||
slice<char const> as_chars() const {
|
||||
return {reinterpret_cast<char const *>(this->pointer), this->length * sizeof(type)};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base pointer of the slice.
|
||||
*/
|
||||
constexpr type * begin() const {
|
||||
return this->pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tail pointer of the slice.
|
||||
*/
|
||||
constexpr type * end() const {
|
||||
return this->pointer + this->length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new slice with the base-pointer offset by `index` elements and a length of
|
||||
* `range` elements from `index`.
|
||||
*
|
||||
* *Note* that attempting to slice with an `index` or `range` outside of the existing slice
|
||||
* bounds will result in safety-checked behavior.
|
||||
*/
|
||||
constexpr slice sliced(usize index, usize range) const {
|
||||
if ((this->length <= index) || ((range + index) > this->length)) unreachable();
|
||||
|
||||
return {this->pointer + index, range - index};
|
||||
}
|
||||
|
||||
operator slice<type const>() const {
|
||||
return (*reinterpret_cast<slice<type const> const *>(this));
|
||||
}
|
||||
|
||||
type & operator[](usize index) const {
|
||||
constexpr type & operator[](usize index) const {
|
||||
if (this->length <= index) core::unreachable();
|
||||
|
||||
return this->pointer[index];
|
||||
}
|
||||
|
||||
slice<type> after(usize index) const {
|
||||
return {this->pointer + index, this->length - index};
|
||||
}
|
||||
|
||||
slice<type> until(usize index) const {
|
||||
return {this->pointer, index};
|
||||
}
|
||||
|
||||
slice<type> between(usize a, usize b) const {
|
||||
return {this->pointer + a, b};
|
||||
}
|
||||
|
||||
constexpr type * begin() const {
|
||||
return this->pointer;
|
||||
}
|
||||
|
||||
constexpr type * end() const {
|
||||
return this->pointer + this->length;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
template<typename element> struct [[nodiscard]] optional {
|
||||
optional() : buffer{0} {
|
||||
|
||||
// Math functions.
|
||||
export namespace core {
|
||||
/**
|
||||
* Returns the maximum value between `a` and `b`.
|
||||
*/
|
||||
template<typename scalar> constexpr scalar max(scalar const & a, scalar const & b) {
|
||||
return (a > b) ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum value between `a` and `b`.
|
||||
*/
|
||||
template<typename scalar> constexpr scalar min(scalar const & a, scalar const & b) {
|
||||
return (a < b) ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `value` clamped between the range of `min_value` and `max_value` (inclusive).
|
||||
*/
|
||||
template<typename scalar> constexpr scalar clamp(scalar const & value, scalar const & min_value, scalar const & max_value) {
|
||||
return max(min_value, min(max_value, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `value` rounded to the nearest whole number.
|
||||
*/
|
||||
f32 round32(f32 value) {
|
||||
return __builtin_roundf(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates and initializes a type of `requested_size` in `buffer`, returning its base pointer. As
|
||||
* a result of accepting a pre-allocated buffer, invocation does not allocate any dynamic memory.
|
||||
*
|
||||
* *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked
|
||||
* behavior.
|
||||
*/
|
||||
export void * operator new(core::usize requested_size, core::slice<core::u8> const & buffer) {
|
||||
if (buffer.length < requested_size) core::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[](core::usize requested_size, core::slice<core::u8> const & buffer) {
|
||||
if (buffer.length < requested_size) core::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 [core::allocator::deallocate] or implementation-
|
||||
* specific allocator functionality.
|
||||
*/
|
||||
export [[nodiscard]] void * operator new(core::usize requested_size, core::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 [core::allocator::deallocate] or implementation-
|
||||
* specific allocator functionality.
|
||||
*/
|
||||
export [[nodiscard]] void * operator new[](core::usize requested_size, core::allocator & allocator) {
|
||||
return allocator.reallocate(nullptr, requested_size);
|
||||
}
|
||||
|
||||
// Wrapper types.
|
||||
export namespace core {
|
||||
/**
|
||||
* Monadic container for a single-`element` value or nothing.
|
||||
*/
|
||||
template<typename element> struct [[nodiscard]] optional {
|
||||
optional() : buffer{0} {}
|
||||
|
||||
optional(element const & value) : buffer{0} {
|
||||
(*reinterpret_cast<element *>(this->buffer)) = value;
|
||||
this->buffer[sizeof(element)] = 1;
|
||||
|
@ -163,18 +289,38 @@ export namespace core {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the optional contains a value, otherwise `false`.
|
||||
*/
|
||||
bool has_value() const {
|
||||
return this->buffer[sizeof(element)] == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contained value or `fallback` if the optional is empty.
|
||||
*/
|
||||
element const & value_or(element const & fallback) const {
|
||||
return this->has_value() ? *reinterpret_cast<element const *>(this->buffer) : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the contained value.
|
||||
*
|
||||
* *Note*: attempting to access the value of an empty optional will trigger safety-checked
|
||||
* behavior.
|
||||
*/
|
||||
element & value() {
|
||||
if (!this->has_value()) unreachable();
|
||||
|
||||
return *reinterpret_cast<element *>(this->buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contained value.
|
||||
*
|
||||
* *Note*: attempting to access the value of an empty optional will trigger safety-checked
|
||||
* behavior.
|
||||
*/
|
||||
element const & value() const {
|
||||
return *reinterpret_cast<element const *>(this->buffer);
|
||||
}
|
||||
|
@ -183,6 +329,9 @@ export namespace core {
|
|||
u8 buffer[sizeof(element) + 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Monadic container for a descriminating union of either `value_element` or `error_element`.
|
||||
*/
|
||||
template<typename value_element, typename error_element> struct [[nodiscard]] expected {
|
||||
expected(value_element const & value) : buffer{0} {
|
||||
(*reinterpret_cast<value_element *>(this->buffer)) = value;
|
||||
|
@ -193,28 +342,55 @@ export namespace core {
|
|||
(*reinterpret_cast<error_element *>(this->buffer)) = error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the optional contains a value, otherwise `false` if it holds an error.
|
||||
*/
|
||||
bool is_ok() const {
|
||||
return this->buffer[buffer_size];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the contained value.
|
||||
*
|
||||
* *Note*: attempting to access the value of an erroneous expected will trigger safety-
|
||||
* checked behavior.
|
||||
*/
|
||||
value_element & value() {
|
||||
if (!this->is_ok()) unreachable();
|
||||
|
||||
return *reinterpret_cast<value_element *>(this->buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contained value.
|
||||
*
|
||||
* *Note*: attempting to access the value of an erroneous expected will trigger safety-
|
||||
* checked behavior.
|
||||
*/
|
||||
value_element const & value() const {
|
||||
if (!this->is_ok()) unreachable();
|
||||
|
||||
return *reinterpret_cast<value_element const *>(this->buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the contained error.
|
||||
*
|
||||
* *Note*: attempting to access the error of a non-erroneous expected will trigger safety-
|
||||
* checked behavior.
|
||||
*/
|
||||
error_element & error() {
|
||||
if (this->is_ok()) unreachable();
|
||||
|
||||
return *reinterpret_cast<error_element *>(this->buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contained error.
|
||||
*
|
||||
* *Note*: attempting to access the error of a non-erroneous expected will trigger safety-
|
||||
* checked behavior.
|
||||
*/
|
||||
error_element const & error() const {
|
||||
if (this->is_ok()) unreachable();
|
||||
|
||||
|
@ -229,39 +405,120 @@ export namespace core {
|
|||
|
||||
template<typename> struct callable;
|
||||
|
||||
template<typename return_type, typename... argument_types> struct callable<return_type(argument_types...)> {
|
||||
using function = return_type(*)(argument_types...);
|
||||
/**
|
||||
* Type-erasing wrapper for functor types that have a call operator with a return value
|
||||
* matching `return_value` and arguments matching `argument_values`.
|
||||
*/
|
||||
template<typename return_value, typename... argument_values> struct callable<return_value(argument_values...)> {
|
||||
using function = return_value(*)(argument_values...);
|
||||
|
||||
callable(function callable_function) {
|
||||
this->dispatcher = [](u8 const * userdata, argument_values... arguments) -> return_value {
|
||||
return (*reinterpret_cast<function const *>(userdata))(arguments...);
|
||||
};
|
||||
|
||||
callable(function callable_function) : dispatcher(dispatch_function) {
|
||||
new (this->capture) function{callable_function};
|
||||
}
|
||||
|
||||
callable(callable const &) = delete;
|
||||
|
||||
template<typename functor> callable(functor const & callable_functor) : dispatcher(dispatch_functor<functor>) {
|
||||
template<typename functor> callable(functor const & callable_functor) {
|
||||
this->dispatcher = [](u8 const * userdata, argument_values... arguments) -> return_value {
|
||||
return (*reinterpret_cast<functor const*>(userdata))(arguments...);
|
||||
};
|
||||
|
||||
new (this->capture) functor{callable_functor};
|
||||
}
|
||||
|
||||
return_type operator()(argument_types const &... arguments) const {
|
||||
return_value operator()(argument_values const &... arguments) const {
|
||||
return this->dispatcher(this->capture, arguments...);
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr usize capture_size = 24;
|
||||
|
||||
return_type(* dispatcher)(u8 const * userdata, argument_types... arguments);
|
||||
return_value(* dispatcher)(u8 const * userdata, argument_values... arguments);
|
||||
|
||||
u8 capture[capture_size];
|
||||
|
||||
static return_type dispatch_function(u8 const * userdata, argument_types... arguments) {
|
||||
return (*reinterpret_cast<function const *>(userdata))(arguments...);
|
||||
}
|
||||
|
||||
template<typename functor_type> static return_type dispatch_functor(u8 const * userdata, argument_types... arguments) {
|
||||
return (*reinterpret_cast<functor_type const*>(userdata))(arguments...);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Errors that may occur while executing an opaque I/O operation via the `readable` and
|
||||
* `writable` type aliases.
|
||||
*/
|
||||
enum class io_error {
|
||||
unavailable,
|
||||
};
|
||||
|
||||
using readable = callable<expected<usize, io_error>(slice<u8> const &)>;
|
||||
|
||||
using writable = callable<expected<usize, io_error>(slice<u8 const> const &)>;
|
||||
}
|
||||
|
||||
// Input/output operations.
|
||||
export namespace core {
|
||||
/**
|
||||
* Tests the equality of `a` against `b`, returning `true` if they contain identical bytes,
|
||||
* otherwise `false`.
|
||||
*/
|
||||
bool equals(slice<u8 const> const & a, slice<u8 const> const & b) {
|
||||
if (a.length != b.length) return false;
|
||||
|
||||
for (size_t i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the contents of `origin` into `target`.
|
||||
*
|
||||
* *Note*: safety-checked behavior is triggered if `target` is smaller than `origin`.
|
||||
*/
|
||||
void copy(slice<u8> const & target, slice<u8 const> const & origin) {
|
||||
if (target.length < origin.length) core::unreachable();
|
||||
|
||||
for (usize i = 0; i < origin.length; i += 1) target[i] = origin[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Streams the data from `input` to `output`, using `buffer` as temporary transfer space.
|
||||
*
|
||||
* The returned [expected] can be used to introspect if `input` or `output` encountered any
|
||||
* issues during streaming, otherwise it will contain the number of bytes streamed.
|
||||
*
|
||||
* *Note*: if `buffer` has a length of `0`, no data will be streamed as there is nowhere to
|
||||
* temporarily place data during streaming.
|
||||
*/
|
||||
expected<usize, io_error> stream(writable const & output,
|
||||
readable const & input, slice<u8> const & buffer) {
|
||||
|
||||
usize written = 0;
|
||||
expected maybe_read = input(buffer);
|
||||
|
||||
if (!maybe_read.is_ok()) return maybe_read.error();
|
||||
|
||||
usize read = maybe_read.value();
|
||||
|
||||
while (read != 0) {
|
||||
expected const maybe_written = output(buffer.sliced(0, read));
|
||||
|
||||
if (!maybe_written.is_ok()) return maybe_read.error();
|
||||
|
||||
written += maybe_written.value();
|
||||
maybe_read = input(buffer);
|
||||
|
||||
if (!maybe_read.is_ok()) return maybe_read.error();
|
||||
|
||||
read = maybe_read.value();
|
||||
}
|
||||
|
||||
return 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 {
|
||||
|
@ -277,44 +534,4 @@ export namespace core {
|
|||
|
||||
return a;
|
||||
}
|
||||
|
||||
enum class io_error {
|
||||
unavailable,
|
||||
};
|
||||
|
||||
using readable = callable<expected<usize, io_error>(slice<u8> const &)>;
|
||||
|
||||
using writable = callable<expected<usize, io_error>(slice<u8 const> const &)>;
|
||||
|
||||
template<typename element_type> bool equals(slice<element_type const> const & a, slice<element_type const> const & b) {
|
||||
if (a.length != b.length) return false;
|
||||
|
||||
for (size_t i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
expected<usize, io_error> stream(writable const & output, readable const & input, slice<u8> const & buffer) {
|
||||
usize written = 0;
|
||||
expected maybe_read = input(buffer);
|
||||
|
||||
if (!maybe_read.is_ok()) return maybe_read.error();
|
||||
|
||||
usize read = maybe_read.value();
|
||||
|
||||
while (read != 0) {
|
||||
expected const maybe_written = output(buffer.until(read));
|
||||
|
||||
if (!maybe_written.is_ok()) return maybe_read.error();
|
||||
|
||||
written += maybe_written.value();
|
||||
maybe_read = input(buffer);
|
||||
|
||||
if (!maybe_read.is_ok()) return maybe_read.error();
|
||||
|
||||
read = maybe_read.value();
|
||||
}
|
||||
|
||||
return written;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,27 +3,54 @@ export module core.image;
|
|||
import core;
|
||||
|
||||
export namespace core {
|
||||
/**
|
||||
* All-purpose color value for red, green, blue, alpha channel-encoded values.
|
||||
*/
|
||||
struct color {
|
||||
/**
|
||||
* Red channel.
|
||||
*/
|
||||
float r;
|
||||
|
||||
/**
|
||||
* Green channel.
|
||||
*/
|
||||
float g;
|
||||
|
||||
/**
|
||||
* Blue channel.
|
||||
*/
|
||||
float b;
|
||||
|
||||
/**
|
||||
* Alpha channel.
|
||||
*/
|
||||
float a;
|
||||
|
||||
/**
|
||||
* Red channel represented in an 8-bit unsigned value.
|
||||
*/
|
||||
u8 to_r8() const {
|
||||
return static_cast<u8>(round32(clamp(this->r, 0.0f, 1.0f) * u8_max));
|
||||
}
|
||||
|
||||
/**
|
||||
* Green channel represented in an 8-bit unsigned value.
|
||||
*/
|
||||
u8 to_g8() const {
|
||||
return static_cast<u8>(round32(clamp(this->g, 0.0f, 1.0f) * u8_max));
|
||||
}
|
||||
|
||||
/**
|
||||
* Blue channel represented in an 8-bit unsigned value.
|
||||
*/
|
||||
u8 to_b8() const {
|
||||
return static_cast<u8>(round32(clamp(this->b, 0.0f, 1.0f) * u8_max));
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha channel represented in an 8-bit unsigned value.
|
||||
*/
|
||||
u8 to_a8() const {
|
||||
return static_cast<u8>(round32(clamp(this->a, 0.0f, 1.0f) * u8_max));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
export module core.lalgebra;
|
||||
|
||||
import core;
|
||||
|
||||
export namespace core {
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
export module core.math;
|
||||
|
||||
import core;
|
||||
|
||||
export namespace core {
|
||||
struct vector2 {
|
||||
core::f32 x;
|
||||
|
||||
core::f32 y;
|
||||
};
|
||||
|
||||
struct vector3 {
|
||||
core::f32 x;
|
||||
|
||||
core::f32 y;
|
||||
|
||||
core::f32 z;
|
||||
};
|
||||
}
|
|
@ -3,116 +3,212 @@ export module core.sequence;
|
|||
import core;
|
||||
|
||||
export namespace core {
|
||||
enum class reserve_error {
|
||||
/**
|
||||
* Result codes used by [sequence]-derived types when they are appended to in any way.
|
||||
*
|
||||
* [append_result::ok] indicates that an append operation was successful.
|
||||
*
|
||||
* [append_result::out_of_memory] alerts that the memory required to perform the append
|
||||
* operation failed.
|
||||
*/
|
||||
enum class [[nodiscard]] append_result {
|
||||
ok,
|
||||
out_of_memory,
|
||||
};
|
||||
|
||||
/**
|
||||
* Base type for all sequence-like types.
|
||||
*
|
||||
* Sequences are any data structure which owns a linear, non-unique set of elements which may
|
||||
* be queried and/or mutated.
|
||||
*/
|
||||
template<typename element> struct sequence {
|
||||
sequence() = default;
|
||||
|
||||
sequence(sequence const &) = delete;
|
||||
|
||||
virtual ~sequence() {};
|
||||
|
||||
virtual slice<element const> as_slice() const = 0;
|
||||
|
||||
virtual optional<reserve_error> append(slice<element const> const & appended_elements) = 0;
|
||||
/**
|
||||
* Attempts to append `source_elements` to the sequence.
|
||||
*
|
||||
* The returned [append_result] indicates whether the operation was successful or not.
|
||||
*
|
||||
* If the returned [append_result] is anything but [append_result::ok], the [sequence] will
|
||||
* be left in an implementation-defined state.
|
||||
*/
|
||||
virtual append_result append(slice<element const> const & source_elements) = 0;
|
||||
};
|
||||
|
||||
template<typename element, usize initial_capacity = 1> struct stack : public sequence<element> {
|
||||
stack(allocator * buffer_allocator) : filled{0},
|
||||
buffer{0}, buffer_allocator{buffer_allocator} {
|
||||
|
||||
this->elements = this->buffer;
|
||||
/**
|
||||
* Last-in-first-out linear sequence of `element` values.
|
||||
*
|
||||
* [stack] types will default to using an inline array of `init_capacity` at first. After all
|
||||
* local storage has been exhausted, the [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.
|
||||
*/
|
||||
template<typename element, usize init_capacity = 1> struct stack : public sequence<element> {
|
||||
stack(allocator * dynamic_allocator) : local_buffer{0} {
|
||||
this->dynamic_allocator = dynamic_allocator;
|
||||
this->filled = 0;
|
||||
this->elements = this->local_buffer;
|
||||
}
|
||||
|
||||
~stack() override {
|
||||
if (this->elements.pointer != this->buffer)
|
||||
this->buffer_allocator->deallocate(this->elements.pointer);
|
||||
if (this->is_dynamic()) this->dynamic_allocator->deallocate(this->elements.pointer);
|
||||
}
|
||||
|
||||
slice<element const> as_slice() const override {
|
||||
return this->elements;
|
||||
}
|
||||
|
||||
optional<reserve_error> push(element const & pushed_element) {
|
||||
if (this->filled == this->elements.length) {
|
||||
optional const maybe_error = this->reserve(this->elements.length);
|
||||
|
||||
if (maybe_error.has_value()) return maybe_error;
|
||||
}
|
||||
|
||||
this->elements[this->filled] = pushed_element;
|
||||
this->filled += 1;
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
optional<reserve_error> append(slice<element const> const & pushed_elements) override {
|
||||
usize const updated_fill = this->filled + pushed_elements.length;
|
||||
/**
|
||||
* Attempts to append `source_elements` to the top of the stack.
|
||||
*
|
||||
* The returned [append_result] indicates whether the operation was successful or not.
|
||||
*
|
||||
* If the returned [append_result] is anything but [append_result::ok], the stack will be
|
||||
* be left in an empty but valid state.
|
||||
*
|
||||
* *Note* that [push] is recommended when appending singular values.
|
||||
*/
|
||||
append_result append(slice<element const> const & source_elements) override {
|
||||
usize const updated_fill = this->filled + source_elements.length;
|
||||
|
||||
if (updated_fill >= this->elements.length) {
|
||||
optional const maybe_error = this->reserve(max(this->elements.length, updated_fill));
|
||||
append_result const result =
|
||||
this->reserve(max(this->elements.length, updated_fill));
|
||||
|
||||
if (maybe_error.has_value()) return maybe_error;
|
||||
if (result != append_result::ok) return result;
|
||||
}
|
||||
|
||||
for (usize i = 0; i < pushed_elements.length; i += 1)
|
||||
this->elements[this->filled + i] = pushed_elements[i];
|
||||
for (usize i = 0; i < source_elements.length; i += 1)
|
||||
this->elements[this->filled + i] = source_elements[i];
|
||||
|
||||
this->filled = updated_fill;
|
||||
|
||||
return {};
|
||||
return append_result::ok;
|
||||
}
|
||||
|
||||
optional<reserve_error> reserve(usize capacity) {
|
||||
/**
|
||||
* Returns the beginning of the elements as a mutable pointer.
|
||||
*/
|
||||
element * begin() {
|
||||
return this->elements.pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the beginning of the elements as a const pointer.
|
||||
*/
|
||||
element const * begin() const {
|
||||
return this->elements.pointer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ending of the elements as a mutable pointer.
|
||||
*/
|
||||
element * end() {
|
||||
return this->elements.pointer + this->filled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ending of the elements as a const pointer.
|
||||
*/
|
||||
element const * end() const {
|
||||
return this->elements.pointer + this->filled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the stack is backed by dynamic memory, otherwise `false`.
|
||||
*/
|
||||
bool is_dynamic() const {
|
||||
return this->elements.pointer != this->local_buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to append `source_element` to the top of the stack.
|
||||
*
|
||||
* The returned [append_result] indicates whether the operation was successful or not.
|
||||
*
|
||||
* If the returned [append_result] is anything but [append_result::ok], the stack will be
|
||||
* be left in an empty but valid state.
|
||||
*
|
||||
* *Note* that [append] is recommended when appending many values at once.
|
||||
*/
|
||||
append_result push(element const & source_element) {
|
||||
if (this->filled == this->elements.length) {
|
||||
append_result const result = this->reserve(this->elements.length);
|
||||
|
||||
if (result != append_result::ok) return result;
|
||||
}
|
||||
|
||||
this->elements[this->filled] = source_element;
|
||||
this->filled += 1;
|
||||
|
||||
return append_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 [append_result] indicates whether the operation was successful or not.
|
||||
*
|
||||
* If the returned [append_result] is anything but [append_result::ok], the stack will be
|
||||
* be left in an empty but valid state.
|
||||
*
|
||||
* *Note* that manual invocation is not recommended if the [stack] has a large
|
||||
* `initial_capacity` argument.
|
||||
*/
|
||||
append_result reserve(usize capacity) {
|
||||
usize const requested_capacity = this->elements.length + capacity;
|
||||
|
||||
if (this->elements.pointer == this->buffer) {
|
||||
u8 * const maybe_allocation = this->buffer_allocator->
|
||||
reallocate(nullptr, sizeof(element) * requested_capacity);
|
||||
|
||||
if (maybe_allocation == nullptr) {
|
||||
this->elements = {};
|
||||
|
||||
return reserve_error::out_of_memory;
|
||||
}
|
||||
|
||||
this->elements = {reinterpret_cast<element *>(maybe_allocation), requested_capacity};
|
||||
} else {
|
||||
u8 * const maybe_allocation = this->buffer_allocator->reallocate(
|
||||
if (this->is_dynamic()) {
|
||||
// Grow dynamic buffer (bailing out if failed).
|
||||
u8 * const buffer = this->dynamic_allocator->reallocate(
|
||||
reinterpret_cast<u8 *>(this->elements.pointer),
|
||||
sizeof(element) * requested_capacity);
|
||||
|
||||
if (maybe_allocation == nullptr) {
|
||||
if (buffer == nullptr) {
|
||||
this->elements = {};
|
||||
|
||||
return reserve_error::out_of_memory;
|
||||
return append_result::out_of_memory;
|
||||
}
|
||||
|
||||
this->elements = {reinterpret_cast<element *>(maybe_allocation), requested_capacity};
|
||||
this->elements = {reinterpret_cast<element *>(buffer), requested_capacity};
|
||||
} else {
|
||||
usize const buffer_size = sizeof(element) * requested_capacity;
|
||||
u8 * const buffer = this->dynamic_allocator->reallocate(nullptr, buffer_size);
|
||||
|
||||
if (buffer == nullptr) {
|
||||
this->elements = {};
|
||||
|
||||
return append_result::out_of_memory;
|
||||
}
|
||||
|
||||
return {};
|
||||
core::copy({buffer, buffer_size}, this->elements.as_bytes());
|
||||
|
||||
this->elements = {reinterpret_cast<element *>(buffer), requested_capacity};
|
||||
}
|
||||
|
||||
return append_result::ok;
|
||||
}
|
||||
|
||||
private:
|
||||
allocator * buffer_allocator;
|
||||
allocator * dynamic_allocator;
|
||||
|
||||
usize filled;
|
||||
|
||||
slice<element> elements;
|
||||
|
||||
element buffer[initial_capacity];
|
||||
element local_buffer[init_capacity];
|
||||
};
|
||||
|
||||
/**
|
||||
* Writable type for appending data to a [sequence] containing [u8] values.
|
||||
*/
|
||||
struct sequence_writer : public writable {
|
||||
sequence_writer(sequence<u8> * target_sequence) : writable{[target_sequence](slice<u8 const> const & buffer) -> expected<usize, io_error> {
|
||||
optional const maybe_error = target_sequence->append(buffer);
|
||||
|
||||
if (maybe_error.has_value()) return io_error::unavailable;
|
||||
|
||||
return buffer.length;
|
||||
sequence_writer(sequence<u8> * output_sequence) : writable{
|
||||
[output_sequence](slice<u8 const> const & buffer) -> expected<usize, io_error> {
|
||||
switch (output_sequence->append(buffer)) {
|
||||
case append_result::ok: return buffer.length;
|
||||
case append_result::out_of_memory: return io_error::unavailable;
|
||||
default: unreachable();
|
||||
}
|
||||
}} {}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
export module kym;
|
||||
|
||||
import core;
|
||||
import core.math;
|
||||
import core.lalgebra;
|
||||
|
||||
export namespace kym {
|
||||
enum class value_type {
|
||||
|
|
|
@ -5,6 +5,8 @@ import core.sequence;
|
|||
|
||||
import kym;
|
||||
|
||||
using loggable = core::callable<void(core::slice<char const> const &)>;
|
||||
|
||||
export namespace kym {
|
||||
struct vm;
|
||||
}
|
||||
|
@ -42,7 +44,7 @@ struct bytecode {
|
|||
|
||||
}
|
||||
|
||||
bool compile(tokenizer bytecode_tokenizer) {
|
||||
bool compile(tokenizer bytecode_tokenizer, loggable const & log_error) {
|
||||
for (;;) {
|
||||
token const initial_token = bytecode_tokenizer.next();
|
||||
|
||||
|
@ -58,10 +60,6 @@ struct bytecode {
|
|||
return kym::nil;
|
||||
}
|
||||
|
||||
core::slice<char const> error_message() const {
|
||||
return this->error_message_buffer.as_slice();
|
||||
}
|
||||
|
||||
private:
|
||||
core::stack<char> error_message_buffer;
|
||||
};
|
||||
|
@ -71,8 +69,8 @@ export namespace kym {
|
|||
return nil;
|
||||
}
|
||||
|
||||
core::expected<core::usize, core::io_error> default_stringify(vm & owning_vm, void * userdata, core::writable const & writable) {
|
||||
return writable(core::slice("[object]").as_bytes());
|
||||
core::expected<core::usize, core::io_error> default_stringify(vm & owning_vm, void * userdata, core::writable const & output) {
|
||||
return output(core::slice("[object]").as_bytes());
|
||||
}
|
||||
|
||||
struct bound_object {
|
||||
|
@ -100,8 +98,8 @@ export namespace kym {
|
|||
return this->behavior.call(*this->owning_vm, this->userdata, arguments);
|
||||
}
|
||||
|
||||
core::expected<core::usize, core::io_error> stringify(core::writable const & writable) {
|
||||
return this->behavior.stringify(*this->owning_vm, this->userdata, writable);
|
||||
core::expected<core::usize, core::io_error> stringify(core::writable const & output) {
|
||||
return this->behavior.stringify(*this->owning_vm, this->userdata, output);
|
||||
}
|
||||
|
||||
value get_field(core::slice<char const> const & field_name) {
|
||||
|
@ -143,8 +141,9 @@ export namespace kym {
|
|||
|
||||
if (source_bytecode == nullptr) return nil;
|
||||
|
||||
if (source_bytecode->compile(tokenizer{source})) {
|
||||
this->log(source_bytecode->error_message());
|
||||
if (!source_bytecode->compile(tokenizer{source}, [&](core::slice<char const> error_message) {
|
||||
this->log(error_message);
|
||||
})) {
|
||||
this->allocator->deallocate(source_bytecode);
|
||||
|
||||
return nil;
|
||||
|
@ -176,7 +175,7 @@ export namespace kym {
|
|||
private:
|
||||
core::slice<value> data_stack;
|
||||
|
||||
core::callable<void(core::slice<char const>)> log;
|
||||
loggable log;
|
||||
|
||||
core::allocator * allocator;
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ import app;
|
|||
import app.sdl;
|
||||
|
||||
import core;
|
||||
import core.math;
|
||||
import core.lalgebra;
|
||||
import core.sequence;
|
||||
|
||||
import kym;
|
||||
|
@ -15,7 +15,7 @@ extern "C" int main(int argc, char const * const * argv) {
|
|||
constexpr app::path config_path = app::path::empty().joined("config.kym");
|
||||
bool is_config_loaded = false;
|
||||
|
||||
system.bundle().read_file(config_path, [&](core::readable const & config_readable) {
|
||||
system.bundle().read_file(config_path, [&](core::readable const & file) {
|
||||
kym::vm vm{&system.thread_safe_allocator(), [&system](core::slice<char const> const & error_message) {
|
||||
system.log(app::log_level::error, error_message);
|
||||
}};
|
||||
|
@ -29,14 +29,14 @@ extern "C" int main(int argc, char const * const * argv) {
|
|||
return;
|
||||
}
|
||||
|
||||
core::stack<core::u8> config_source{&system.thread_safe_allocator()};
|
||||
core::u8 config_source_stream_buffer[1024] = {};
|
||||
core::stack<core::u8> script_source{&system.thread_safe_allocator()};
|
||||
core::u8 stream_buffer[1024] = {};
|
||||
|
||||
if (!core::stream(core::sequence_writer{&config_source},
|
||||
config_readable, config_source_stream_buffer).is_ok()) return;
|
||||
if (!core::stream(core::sequence_writer{&script_source}, file, stream_buffer).is_ok())
|
||||
return;
|
||||
|
||||
vm.with_object(vm.compile(config_source.as_slice().as_chars()), [&](kym::bound_object & config_script) {
|
||||
vm.with_object(config_script.call({}), [&](kym::bound_object & config) {
|
||||
vm.with_object(vm.compile(core::slice{script_source.begin(), script_source.end()}.as_chars()), [&](kym::bound_object & script) {
|
||||
vm.with_object(script.call({}), [&](kym::bound_object & config) {
|
||||
core::u16 const width = config.get_field("width").as_u16().value_or(0);
|
||||
|
||||
if (width == 0) return system.log(app::log_level::error,
|
||||
|
|
Loading…
Reference in New Issue