Clean up core library.

This commit is contained in:
kayomn 2023-02-18 19:40:12 +00:00
parent 1261ee7e60
commit e3c4ab65f1
10 changed files with 608 additions and 243 deletions

View File

@ -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 {

View File

@ -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;

View File

@ -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,118 +50,230 @@ 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;
}
};
}
// 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() : buffer{0} {}
optional(element const & value) : buffer{0} {
(*reinterpret_cast<element *>(this->buffer)) = value;
@ -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;
}
}

View File

@ -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));
}

40
source/core/lalgebra.cpp Normal file
View File

@ -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;
};
}

View File

@ -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;
};
}

View File

@ -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;
}
core::copy({buffer, buffer_size}, this->elements.as_bytes());
this->elements = {reinterpret_cast<element *>(buffer), requested_capacity};
}
return {};
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();
}
}} {}
};
}

View File

@ -1,7 +1,7 @@
export module kym;
import core;
import core.math;
import core.lalgebra;
export namespace kym {
enum class value_type {

View File

@ -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;
};

View File

@ -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,