diff --git a/source/app.cpp b/source/app.cpp index 5a4a1b3..84a3f56 100644 --- a/source/app.cpp +++ b/source/app.cpp @@ -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 const & text) const { diff --git a/source/app/sdl.cpp b/source/app/sdl.cpp index ce1825c..c332c6b 100644 --- a/source/app/sdl.cpp +++ b/source/app/sdl.cpp @@ -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 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 path_buffer{&core::null_allocator()}; + constexpr core::slice 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 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; diff --git a/source/core.cpp b/source/core.cpp index af90811..4edacce 100644 --- a/source/core.cpp +++ b/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,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 constexpr scalar max(scalar const & a, scalar const & b) { - return (a > b) ? a : b; - } - - template constexpr scalar min(scalar const & a, scalar const & b) { - return (a < b) ? a : b; - } - - template 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 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 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(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)); } - type & operator[](usize index) const { + constexpr type & operator[](usize index) const { + if (this->length <= index) core::unreachable(); + return this->pointer[index]; } - - slice after(usize index) const { - return {this->pointer + index, this->length - index}; - } - - slice until(usize index) const { - return {this->pointer, index}; - } - - slice 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 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(core::usize requested_size, core::slice 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 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 struct [[nodiscard]] optional { - optional() : buffer{0} { - - } + optional() : buffer{0} {} optional(element const & value) : buffer{0} { (*reinterpret_cast(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(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(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(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 struct [[nodiscard]] expected { expected(value_element const & value) : buffer{0} { (*reinterpret_cast(this->buffer)) = value; @@ -193,28 +342,55 @@ export namespace core { (*reinterpret_cast(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(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(); @@ -229,39 +405,120 @@ export namespace core { template struct callable; - template struct callable { - 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 struct callable { + using function = return_value(*)(argument_values...); + + callable(function callable_function) { + this->dispatcher = [](u8 const * userdata, argument_values... arguments) -> return_value { + return (*reinterpret_cast(userdata))(arguments...); + }; - callable(function callable_function) : dispatcher(dispatch_function) { new (this->capture) function{callable_function}; } callable(callable const &) = delete; - template callable(functor const & callable_functor) : dispatcher(dispatch_functor) { + template callable(functor const & callable_functor) { + this->dispatcher = [](u8 const * userdata, argument_values... arguments) -> return_value { + return (*reinterpret_cast(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(userdata))(arguments...); - } - - template static return_type dispatch_functor(u8 const * userdata, argument_types... arguments) { - return (*reinterpret_cast(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(slice const &)>; + + using writable = callable(slice 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 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; + } + + /** + * 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) 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 stream(writable const & output, + readable const & input, slice 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(slice const &)>; - - using writable = callable(slice const &)>; - - template 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; - } - - expected stream(writable const & output, readable const & input, slice 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; - } } diff --git a/source/core/image.cpp b/source/core/image.cpp index 0a0a75f..5279c3f 100644 --- a/source/core/image.cpp +++ b/source/core/image.cpp @@ -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(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/core/lalgebra.cpp b/source/core/lalgebra.cpp new file mode 100644 index 0000000..d7b988a --- /dev/null +++ b/source/core/lalgebra.cpp @@ -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; + }; +} diff --git a/source/core/math.cpp b/source/core/math.cpp deleted file mode 100644 index 1cc49b5..0000000 --- a/source/core/math.cpp +++ /dev/null @@ -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; - }; -} diff --git a/source/core/sequence.cpp b/source/core/sequence.cpp index 79d0bf4..7e59719 100644 --- a/source/core/sequence.cpp +++ b/source/core/sequence.cpp @@ -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 struct sequence { - sequence() = default; - - sequence(sequence const &) = delete; - virtual ~sequence() {}; - virtual slice as_slice() const = 0; - - virtual optional append(slice 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 const & source_elements) = 0; }; - template struct stack : public sequence { - 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 struct stack : public sequence { + 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 as_slice() const override { - return this->elements; - } - - optional 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 append(slice 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 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(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(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(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(maybe_allocation), requested_capacity}; + 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 append_result::out_of_memory; + } + + core::copy({buffer, buffer_size}, this->elements.as_bytes()); + + this->elements = {reinterpret_cast(buffer), requested_capacity}; } - return {}; + return append_result::ok; } private: - allocator * buffer_allocator; + allocator * dynamic_allocator; usize filled; slice 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 * target_sequence) : writable{[target_sequence](slice const & buffer) -> expected { - optional const maybe_error = target_sequence->append(buffer); - - if (maybe_error.has_value()) return io_error::unavailable; - - return buffer.length; - }} {} + sequence_writer(sequence * output_sequence) : writable{ + [output_sequence](slice const & buffer) -> expected { + switch (output_sequence->append(buffer)) { + case append_result::ok: return buffer.length; + case append_result::out_of_memory: return io_error::unavailable; + default: unreachable(); + } + }} {} }; } diff --git a/source/kym.cpp b/source/kym.cpp index 3ac4528..014aedc 100644 --- a/source/kym.cpp +++ b/source/kym.cpp @@ -1,7 +1,7 @@ export module kym; import core; -import core.math; +import core.lalgebra; export namespace kym { enum class value_type { diff --git a/source/kym/environment.cpp b/source/kym/environment.cpp index 87b26a6..ff0fa36 100644 --- a/source/kym/environment.cpp +++ b/source/kym/environment.cpp @@ -5,6 +5,8 @@ import core.sequence; import kym; +using loggable = core::callable 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 error_message() const { - return this->error_message_buffer.as_slice(); - } - private: core::stack error_message_buffer; }; @@ -71,8 +69,8 @@ export namespace kym { return nil; } - core::expected default_stringify(vm & owning_vm, void * userdata, core::writable const & writable) { - return writable(core::slice("[object]").as_bytes()); + core::expected 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 stringify(core::writable const & writable) { - return this->behavior.stringify(*this->owning_vm, this->userdata, writable); + core::expected stringify(core::writable const & output) { + return this->behavior.stringify(*this->owning_vm, this->userdata, output); } value get_field(core::slice 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 error_message) { + this->log(error_message); + })) { this->allocator->deallocate(source_bytecode); return nil; @@ -176,7 +175,7 @@ export namespace kym { private: core::slice data_stack; - core::callable)> log; + loggable log; core::allocator * allocator; }; diff --git a/source/runtime.cpp b/source/runtime.cpp index 38de729..a933521 100644 --- a/source/runtime.cpp +++ b/source/runtime.cpp @@ -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 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 config_source{&system.thread_safe_allocator()}; - core::u8 config_source_stream_buffer[1024] = {}; + core::stack 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,