ona/source/core.cpp

573 lines
16 KiB
C++

module;
#include <cstdint>
#include <cstddef>
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;
using size = __ssize_t;
using u8 = uint8_t;
usize const u8_max = 0xff;
using i8 = uint8_t;
using u16 = uint16_t;
usize const u16_max = 0xffff;
using i16 = uint16_t;
using u32 = uint32_t;
using i32 = uint32_t;
usize const i32_max = 0xffffffff;
using u64 = uint32_t;
using i64 = uint32_t;
using f32 = float;
using f64 = double;
/**
* Base type for runtime-pluggable memory allocation strategies used by the core library.
*/
struct allocator {
virtual ~allocator() {};
/**
* If `allocation` is `nullptr`, the allocator will attempt to allocate a new memory block
* of `requested_size` bytes. Otherwise, the allocator will attempt to reallocate
* `allocation` to be `request_size` bytes in size.
*
* The returned address will point to a dynamically allocated buffer of `requested_size` if
* the operation was successful, otherwise `nullptr`.
*
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to
* program exit. This may be achieved through either [deallocate] or implementation-
* specific allocator functionality.
*
* *Note*: Attempting to pass a non-`nullptr` `allocation` address not allocated by the
* allocator *will* result in erroneous implementation-behavior.
*
* *Note*: After invocation, `allocation` should be considered an invalid memory address.
*/
[[nodiscard]] virtual u8 * reallocate(u8 * allocation, usize requested_size) = 0;
/**
* If `allocation` points to a non-`nullptr` address, the allocator will deallocate it.
* Otherwise, the function has no side-effects.
*
* *Note* that attempting to pass a non-`nullptr` `allocation` address not allocated by the
* allocator *will* result in erroneous implementation-behavior.
*/
virtual void deallocate(void * allocation) = 0;
};
/**
* Length-signed pointer type that describes how many elements of `type` it references,
* providing a type-safe wrapper for passing arrays and zero-terminated strings to functions.
*/
template<typename type> struct slice {
/**
* Number of `type` elements referenced.
*/
usize length;
/**
* Base element address referenced.
*/
type * pointer;
constexpr slice() {
this->length = 0;
this->pointer = nullptr;
}
constexpr slice(char const *&& zstring) {
this->pointer = zstring;
this->length = 0;
while (zstring[length] != 0) this->length += 1;
}
constexpr slice(type * slice_pointer, usize slice_length) {
this->pointer = slice_pointer;
this->length = slice_length;
}
constexpr slice(type * slice_begin, type * slice_end) {
this->pointer = slice_begin;
this->length = static_cast<usize>(slice_end - slice_begin);
}
template<usize array_size> constexpr slice(type(&array)[array_size]) {
this->pointer = array;
this->length = array_size;
}
/**
* Reinterprets the data referenced as a series of bytes.
*
* The returned view is constant to protect against inadvertant memory corruption.
*/
slice<u8 const> as_bytes() const {
return {reinterpret_cast<u8 const *>(this->pointer), this->length * sizeof(type)};
}
/**
* Reinterprets the data referenced as a series of chars.
*
* The returned view is constant to protect against inadvertant memory corruption.
*
* *Note* the returned value has no guarantees about the validity of any specific character
* encoding set.
*/
slice<char const> as_chars() const {
return {reinterpret_cast<char const *>(this->pointer), this->length * sizeof(type)};
}
/**
* Returns the base pointer of the slice.
*/
constexpr type * begin() const {
return this->pointer;
}
/**
* Returns the tail pointer of the slice.
*/
constexpr type * end() const {
return this->pointer + this->length;
}
/**
* Returns a new slice with the base-pointer offset by `index` elements and a length of
* `range` elements from `index`.
*
* *Note* that attempting to slice with an `index` or `range` outside of the existing slice
* bounds will result in safety-checked behavior.
*/
constexpr slice sliced(usize index, usize range) const {
if ((this->length <= index) || ((range + index) > this->length)) unreachable();
return {this->pointer + index, range - index};
}
operator slice<type const>() const {
return (*reinterpret_cast<slice<type const> const *>(this));
}
constexpr type & operator[](usize index) const {
if (this->length <= index) core::unreachable();
return this->pointer[index];
}
};
}
// 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;
}
optional(optional const & that) : buffer{0} {
if (that.has_value()) {
(*reinterpret_cast<element *>(this->buffer)) = that.value();
this->buffer[sizeof(element)] = 1;
} else {
this->buffer[sizeof(element)] = 0;
}
}
/**
* Returns `true` if the optional contains a value, otherwise `false`.
*/
bool has_value() const {
return this->buffer[sizeof(element)] == 1;
}
/**
* 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);
}
private:
u8 buffer[sizeof(element) + 1];
};
/**
* Monadic container for a descriminating union of either `value_element` or `error_element`.
*/
template<typename value_element, typename error_element> struct [[nodiscard]] expected {
expected(value_element const & value) : buffer{0} {
(*reinterpret_cast<value_element *>(this->buffer)) = value;
this->buffer[buffer_size] = 1;
}
expected(error_element const & error) : buffer{0} {
(*reinterpret_cast<error_element *>(this->buffer)) = error;
}
/**
* Returns `true` if the optional contains a value, otherwise `false` if it holds an error.
*/
bool is_ok() const {
return this->buffer[buffer_size];
}
/**
* Returns a reference to the contained value.
*
* *Note*: attempting to access the value of an erroneous expected will trigger safety-
* checked behavior.
*/
value_element & value() {
if (!this->is_ok()) unreachable();
return *reinterpret_cast<value_element *>(this->buffer);
}
/**
* Returns the contained value.
*
* *Note*: attempting to access the value of an erroneous expected will trigger safety-
* checked behavior.
*/
value_element const & value() const {
if (!this->is_ok()) unreachable();
return *reinterpret_cast<value_element const *>(this->buffer);
}
/**
* Returns a reference to the contained error.
*
* *Note*: attempting to access the error of a non-erroneous expected will trigger safety-
* checked behavior.
*/
error_element & error() {
if (this->is_ok()) unreachable();
return *reinterpret_cast<error_element *>(this->buffer);
}
/**
* Returns the contained error.
*
* *Note*: attempting to access the error of a non-erroneous expected will trigger safety-
* checked behavior.
*/
error_element const & error() const {
if (this->is_ok()) unreachable();
return *reinterpret_cast<error_element const *>(this->buffer);
}
private:
static constexpr usize buffer_size = max(sizeof(value_element), sizeof(error_element));
u8 buffer[buffer_size + 1];
};
template<typename> struct callable;
/**
* 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...);
};
new (this->capture) function{callable_function};
}
callable(callable const &) = delete;
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_value operator()(argument_values const &... arguments) const {
return this->dispatcher(this->capture, arguments...);
}
private:
static constexpr usize capture_size = 24;
return_value(* dispatcher)(u8 const * userdata, argument_values... arguments);
u8 capture[capture_size];
};
/**
* 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 {
/**
* Compares `a` and `b`, returning the difference between them or `0` if they are identical.
*/
constexpr size compare(slice<u8 const> const & a, slice<u8 const> const & b) {
usize const range = min(a.length, b.length);
for (usize index = 0; index < range; index += 1) {
size const difference = static_cast<size>(a[index]) - static_cast<size>(b[index]);
if (difference != 0) return difference;
}
return static_cast<size>(a.length) - static_cast<size>(b.length);
}
/**
* Copies the contents of `origin` into `target`.
*
* *Note*: safety-checked behavior is triggered if `target` is smaller than `origin`.
*/
void copy(slice<u8> const & target, slice<u8 const> const & origin) {
if (target.length < origin.length) core::unreachable();
for (usize i = 0; i < target.length; i += 1) target[i] = origin[i];
}
/**
* Zeroes the contents of `target`.
*/
void zero(slice<u8> const & target) {
for (usize i = 0; i < target.length; i += 1) target[i] = 0;
}
/**
* Tests the equality of `a` against `b`, returning `true` if they contain identical bytes,
* otherwise `false`.
*/
constexpr bool equals(slice<u8 const> const & a, slice<u8 const> const & b) {
if (a.length != b.length) return false;
for (size_t i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false;
return true;
}
/**
* Returns a hash code generated from the values in `bytes`.
*
* *Note:* the returned hash code is not guaranteed to be unique.
*/
constexpr usize hash(slice<u8 const> const & bytes) {
usize hash_code = 5381;
for (u8 const byte : bytes) hash_code = ((hash_code << 5) + hash_code) + byte;
return hash_code;
}
/**
* 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 {
if (maybe_allocation != nullptr) unreachable();
return nullptr;
}
void deallocate(void * allocation) override {
if (allocation != nullptr) unreachable();
}
} a;
return a;
}
}