diff --git a/build.py b/build.py index 785f475..03f9b26 100755 --- a/build.py +++ b/build.py @@ -30,6 +30,6 @@ def compile_package(root_module_name: str) -> None: compile_package("coral") compile_package("oar") -compile_package("app") +compile_package("turtle") compile_package("runtime") subprocess.run(f"{compile_command} {' '.join(object_file_paths)} -o ./runtime -lSDL2", shell=True, check=True) diff --git a/source/app.cpp b/source/app.cpp deleted file mode 100644 index de3f7d2..0000000 --- a/source/app.cpp +++ /dev/null @@ -1,317 +0,0 @@ -module; - -#include - -export module app; - -import coral; -import coral.files; -import coral.image; -import coral.io; -import coral.math; - -import oar; - -using native_path = coral::fixed_buffer<4096>; - -struct native_file : public coral::file_reader, public coral::file_writer { - enum class open_mode { - read_only, - overwrite, - }; - - enum class [[nodiscard]] close_result { - ok, - io_unavailable, - }; - - enum class [[nodiscard]] open_result { - ok, - io_unavailable, - access_denied, - not_found, - }; - - native_file() = default; - - close_result close() { - if (SDL_RWclose(this->rw_ops) != 0) return close_result::io_unavailable; - - this->rw_ops = nullptr; - - return close_result::ok; - } - - bool is_open() const { - return this->rw_ops != nullptr; - } - - open_result open(native_path const & file_path, open_mode mode) { - if (this->is_open()) switch (this->close()) { - case close_result::ok: break; - case close_result::io_unavailable: return open_result::io_unavailable; - default: coral::unreachable(); - } - - // No room for zero terminator. - if (file_path.is_full()) return open_result::not_found; - - switch (mode) { - case open_mode::read_only: { - this->rw_ops = SDL_RWFromFile(file_path.as_slice().as_chars().begin(), "rb"); - - break; - } - - case open_mode::overwrite: { - this->rw_ops = SDL_RWFromFile(file_path.as_slice().as_chars().begin(), "wb"); - - break; - } - - default: coral::unreachable(); - } - - if (this->rw_ops == nullptr) return open_result::not_found; - - return open_result::ok; - } - - coral::expected read(coral::slice const & data) override { - if (!this->is_open()) return coral::io_error::unavailable; - - coral::usize const data_read{SDL_RWread(this->rw_ops, data.pointer, sizeof(uint8_t), data.length)}; - - if ((data_read == 0) && (SDL_GetError() != nullptr)) return coral::io_error::unavailable; - - return data_read; - } - - coral::expected seek(coral::u64 offset) override { - if (!this->is_open()) return coral::io_error::unavailable; - - // TODO: Fix cast. - coral::i64 const byte_position{ - SDL_RWseek(this->rw_ops, static_cast(offset), RW_SEEK_SET)}; - - if (byte_position == -1) return coral::io_error::unavailable; - - return static_cast(byte_position); - } - - coral::expected tell() override { - if (!this->is_open()) return coral::io_error::unavailable; - - coral::i64 const byte_position{SDL_RWseek(this->rw_ops, 0, RW_SEEK_SET)}; - - if (byte_position == -1) return coral::io_error::unavailable; - - return static_cast(byte_position); - } - - coral::expected write(coral::slice const & data) override { - if (!this->is_open()) return coral::io_error::unavailable; - - coral::usize const data_written{SDL_RWwrite(this->rw_ops, data.pointer, sizeof(uint8_t), data.length)}; - - if ((data_written == 0) && (SDL_GetError() != nullptr)) return coral::io_error::unavailable; - - return data_written; - } - - private: - SDL_RWops * rw_ops{nullptr}; -}; - -struct sandboxed_fs : public coral::fs { - sandboxed_fs() { - char * const path{SDL_GetBasePath()}; - - if (path == nullptr) return; - - for (coral::usize index = 0; path[index] != 0; index += 1) - this->sandbox_path.put(path[index]); - - SDL_free(path); - - this->access_rules.can_read = true; - } - - sandboxed_fs(coral::path const & organization_name, coral::path const & app_name) { - char * const path{SDL_GetPrefPath(organization_name.begin(), app_name.begin())}; - - if (path == nullptr) return; - - for (coral::usize index = 0; path[index] != 0; index += 1) - this->sandbox_path.put(path[index]); - - SDL_free(path); - - this->access_rules.can_read = true; - } - - access_rules query_access() override { - return this->access_rules; - } - - void read_file(coral::path const & file_path, coral::closure const & then) override { - if (!this->access_rules.can_read) return; - - native_path sandbox_file_path; - { - coral::expected const written = sandbox_file_path.write(this->sandbox_path.as_slice()); - - if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return; - } - - { - coral::expected const written = - sandbox_file_path.write(file_path.as_slice().as_bytes()); - - if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return; - } - - native_file file; - - if (file.open(sandbox_file_path, native_file::open_mode::read_only) != - native_file::open_result::ok) return; - - then(file); - - if (file.close() != native_file::close_result::ok) - // Error orphaned file handle! - return; - } - - void write_file(coral::path const & file_path, coral::closure const & then) override { - if (!this->access_rules.can_write) return; - - native_path sandbox_file_path; - { - coral::expected const written = sandbox_file_path.write(this->sandbox_path.as_slice()); - - if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return; - } - - { - coral::expected const written = - sandbox_file_path.write(file_path.as_slice().as_bytes()); - - if (!written.is_ok() || (written.value() != this->sandbox_path.count())) return; - } - - native_file file; - - if (file.open(sandbox_file_path, native_file::open_mode::overwrite) != - native_file::open_result::ok) return; - - then(file); - - if (file.close() != native_file::close_result::ok) - // Error orphaned file handle! - return; - } - - private: - native_path sandbox_path; - - access_rules access_rules{ - .can_read = false, - .can_write = false, - }; -}; - -export namespace app { - enum class log_level { - notice, - warning, - error, - }; - - struct client { - coral::fs & base() { - return this->base_sandbox; - } - - void display(coral::u16 screen_width, coral::u16 screen_height) { - SDL_SetWindowSize(this->window, screen_width, screen_height); - SDL_ShowWindow(this->window); - } - - void log(log_level level, coral::slice const & message) { - coral::i32 const length{static_cast( - coral::min(message.length, static_cast(coral::i32_max)))}; - - SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, - SDL_LOG_PRIORITY_INFO, "%.*s", length, message.pointer); - } - - bool poll() { - while (SDL_PollEvent(&this->event) != 0) { - switch (this->event.type) { - case SDL_QUIT: return false; - } - } - - return true; - } - - coral::fs & resources() { - return this->resources_archive; - } - - static int run(coral::path const & title, coral::closure const & start) { - constexpr int windowpos {SDL_WINDOWPOS_UNDEFINED}; - constexpr coral::u32 windowflags {SDL_WINDOW_HIDDEN}; - constexpr int window_width {640}; - constexpr int window_height {480}; - - SDL_Window * const window {SDL_CreateWindow( - title.begin(), windowpos, windowpos, window_width, window_height, windowflags)}; - - if (window == nullptr) return 0xff; - - struct : public coral::allocator { - coral::u8 * reallocate(coral::u8 * allocation, coral::usize requested_size) override { - return reinterpret_cast(SDL_realloc(allocation, requested_size)); - } - - void deallocate(void * allocation) override { - SDL_free(allocation); - } - } allocator; - - client app_client {&allocator, window, title}; - - return start(app_client); - } - - coral::allocator & thread_safe_allocator() { - return *this->allocator; - } - - coral::fs & user() { - return this->user_sandbox; - } - - private: - client(coral::allocator * allocator, SDL_Window * window, - coral::path const & title) : user_sandbox{"ona", title} { - - this->allocator = allocator; - this->window = window; - } - - coral::allocator * allocator; - - SDL_Window * window; - - SDL_Event event; - - sandboxed_fs base_sandbox; - - sandboxed_fs user_sandbox; - - oar::archive resources_archive{&base_sandbox, "base.oar"}; - }; -} diff --git a/source/turtle.cpp b/source/turtle.cpp new file mode 100755 index 0000000..d68811a --- /dev/null +++ b/source/turtle.cpp @@ -0,0 +1,604 @@ +module; + +#include +#include +#include +#include + +export module turtle; + +import coral; +import coral.files; +import coral.io; + +using coral::closure; +using coral::io_error; +using coral::path; +using coral::slice; +using coral::unreachable; +using coral::usize; + +export namespace turtle { + /** + * Path to a native I/O resource that is big enough for every supported platform. + */ + struct native_path { + /** + * Errors that may occur during a path joining operation. + * + * [join_error::overflow] signals that the given path join exceeds the maximum valid length of a native path. + */ + enum class join_error { + overflow, + }; + + /** + * Maximum number of bytes in a native path. + */ + static usize const max = 4095; + + native_path() = default; + + /** + * Constructs a native path from `text`, raising a static assertion if it is larger than [max]. + */ + template constexpr native_path(char const(&text)[text_size]) { + static_assert(text_size <= max); + + for (usize i = 0; i < text_size; i += 1) this->buffer[i] = text[i]; + + this->buffer[text_size] = 0; + } + + /** + * Returns a weak reference to the native path as a [coral::slice]. + * + * *Note*: this is an `O(N)` time function, where `N` is the path length. + */ + constexpr slice as_slice() const { + return {this->buffer, this->filled()}; + } + + /** + * Returns the number of bytes composing the native path. + * + * *Note*: this is an `O(N)` time function, where `N` is the path length. + */ + constexpr usize filled() const { + usize length {0}; + + while (this->buffer[length]) length += 1; + + return length; + } + + /** + * Attempts to create a new native path from the current native path joined with `text`, returning it or a + * [join_error]. + * + * *Note*: this is an `O(N)` time function, where `N` is the path length. + */ + constexpr coral::expected joined(slice const & text) const { + usize buffer_filled {this->filled()}; + + if (text.length >= (max - buffer_filled)) return join_error::overflow; + + native_path joined_path {*this}; + + for (char const c : text) { + joined_path.buffer[buffer_filled] = c; + buffer_filled += 1; + } + + return joined_path; + } + + private: + char buffer[max + 1] {0}; + }; + + /** + * Results from a native I/O resource closing operation. + * + * [close_result::ok] means that no errors occured and the resource has been successfully closed if it was + * open. + * + * [close_result::io_unavailable] is a generic error to communicate that something between the hardware and the + * operating system layer failed while closing the resource. + * + * [close_result::access_denied] reports that the process does not have the required permissions to open the + * resource. This is a rare but possible error that is only possible by a system changing the underlying + * resource permissions after the native resource has already opened it. + */ + enum class [[nodiscard]] close_result { + ok, + io_unavailable, + access_denied, + }; + + /** + * Results from a native I/O resource opening operation. + * + * [open_result::ok] means that no errors occured and the resource has been successfully opened. + * + * [open_result::io_unavailable] is a generic error to communicate that something between the hardware and the + * operating system layer failed while opening the resource. + * + * [open_result::access_denied] reports that the process does not have the required permissions to open the + * resource. While all platforms implement some form of file permissions, the specifics of this error are + * opaque to the caller. + * + * [open_result::not_found] indicates that no resource matching the opening query was found. + * + * [open_result::too_many] signals that there are too many files open in the current process and / or the + * wider operating system at the moment to open the resource. + * + * [open_result::too_big] signals that the resource is too big to open. The usual cause of this is error is + * attempting to open a file bigger than the addressable file range supported by the compiled application. + * + * [open_result::out_of_memory] signals that the system does not have enough memory remaining to open the resource. + */ + enum class [[nodiscard]] open_result { + ok, + io_unavailable, + access_denied, + not_found, + too_many, + too_big, + out_of_memory, + }; + + /** + * Maps a [close_result] to the equivalent [open_result] and returns the value. + */ + open_result close_to_open_result(close_result result) { + switch (result) { + case close_result::ok: return open_result::ok; + case close_result::io_unavailable: return open_result::io_unavailable; + case close_result::access_denied: return open_result::access_denied; + default: unreachable(); + } + } + + /** + * Provides unmanaged access to a native file. + */ + struct native_file final : public coral::file_reader, public coral::file_writer { + /** + * Opening modes for files. + * + * [open_mode::read_only] requests read-only access to an existing file. Specifying a path to an invalid file + * will result in the open request failing. + * + * [open_mode::overwrite] requests write-only access to a new file. Specifying a path to an existing file will + * result in it being wiped and overwritten (assuming it is not protected by the underlying operating system). + * + * [open_mode::append] requests write-only access to an existing file. Specifying a path to an invalid file + * will result in the open request failing. The write cursor begins at the end of any existing data in the file. + */ + enum class open_mode { + read_only, + overwrite, + append, + }; + + native_file() = default; + + /** + * Attempts to close any currently open file. + * + * A [close_result] is returned containing either [close_result::ok] to indicate success or any other value to + * indicate an error. See [close_result] for more details. + * + * *Note*: failing to close should not be treated as a reason to retry the closing operation, and should instead + * be used to inform the end-user that the operation failed or that the process should exit. + */ + close_result close() { + errno = 0; + + if (::close(this->fd) != 0) switch (errno) { + case EINTR: case EIO: case ENOSPC: return close_result::io_unavailable; + case EDQUOT: return close_result::access_denied; + default: unreachable(); + } + + this->fd = 0; + + return close_result::ok; + } + + /** + * Returns `true` if a file is currently open, otherwise `false`. + */ + bool is_open() const { + return this->fd > 0; + } + + /** + * Attempts to open a native file at `file_path` using `file_open_mode` for the access policy. See [open_mode] + * for more information on how files may be opened. + * + * An [open_result] is returned containing either [open_result::ok] to indicate success or any other value to + * indicate an error. See [open_result] for more details. + * + * *Note*: the opened file must be closed using [close] once no longer needed or the process will leak file + * handles. + * + * *Note*: it is recommended to prefer performing file I/O via [sandboxed_fs] unless direct file access is + * required. + */ + open_result open(native_path const & file_path, open_mode file_open_mode) { + if (this->is_open()) { + open_result const result {close_to_open_result(this->close())}; + + if (result != open_result::ok) return result; + } + + constexpr int perms {S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH}; + + errno = 0; + + switch (file_open_mode) { + case open_mode::read_only: { + this->fd = ::open(file_path.as_slice().as_chars().pointer, O_RDONLY, perms); + + break; + } + + case open_mode::overwrite: { + this->fd = ::open(file_path.as_slice().as_chars().pointer, O_WRONLY | O_CREAT | O_TRUNC, perms); + + break; + } + + case open_mode::append: { + this->fd = ::open(file_path.as_slice().as_chars().pointer, O_WRONLY | O_APPEND | O_CREAT, perms); + + break; + } + + default: unreachable(); + } + + if (!this->is_open()) switch (errno) { + case EACCES: case EDQUOT: case ENXIO: case EPERM: + case EROFS: case ETXTBSY: return open_result::access_denied; + + case EINTR: case ENOSPC: return open_result::io_unavailable; + + case EINVAL: case EISDIR: case ELOOP: case ENAMETOOLONG: + case ENOENT: case ENOTDIR: return open_result::not_found; + + case EOVERFLOW: case EFBIG: return open_result::too_big; + case EMFILE: case ENFILE: return open_result::too_many; + case ENOMEM: return open_result::out_of_memory; + default: unreachable(); + } + + return open_result::ok; + } + + /** + * See [coral::file_reader::read]. + */ + coral::expected read(slice const & data) override { + if (!this->is_open()) return io_error::unavailable; + + coral::size const data_read {::read(this->fd, data.pointer, sizeof(coral::u8) * data.length)}; + + if (data_read < 0) return io_error::unavailable; + + return data_read; + } + + /** + * See [coral::file_reader::seek] and [coral::file_writer::seek]. + */ + coral::expected seek(coral::u64 offset) override { + if (!this->is_open()) return io_error::unavailable; + + coral::i64 const data_position {::lseek(this->fd, + static_cast(coral::min(offset, coral::i64_max)), SEEK_SET)}; + + if (data_position == -1) return io_error::unavailable; + + return static_cast(data_position); + } + + /** + * See [coral::file_reader::tell] and [coral::file_writer::tell]. + */ + coral::expected tell() override { + if (!this->is_open()) return io_error::unavailable; + + coral::i64 const data_position {::lseek(this->fd, 0, SEEK_SET)}; + + if (data_position == -1) return io_error::unavailable; + + return static_cast(data_position); + } + + /** + * See [coral::file_writer::write]. + */ + coral::expected write(slice const & data) override { + if (!this->is_open()) return io_error::unavailable; + + coral::size const data_written {::write(this->fd, data.pointer, sizeof(coral::u8) * data.length)}; + + if (data_written < 0) return io_error::unavailable; + + return data_written; + } + + private: + int fd {0}; + }; + + /** + * Provides unmanaged access to a native directory. + */ + struct native_directory final : public coral::file_walker { + native_directory() = default; + + /** + * Attempts to close any currently open directory. + * + * A [close_result] is returned containing either [close_result::ok] to indicate success or any other value to + * indicate an error. See [close_result] for more details. + * + * *Note*: failing to close should not be treated as a reason to retry the closing operation, and should instead + * be used to inform the end-user that the operation failed or that the process should exit. + */ + close_result close() { + if (closedir(this->dir) == 0) return close_result::io_unavailable; + + return close_result::ok; + } + + /** + * See [coral::file_walker::has_next]. + */ + bool has_next() override { + return this->entry != nullptr; + } + + /** + * Returns `true` if a directory is currently open, otherwise `false`. + */ + bool is_open() const { + return this->dir != nullptr; + } + + /** + * See [coral::file_walker::next]. + */ + coral::expected next() override { + usize name_length {0}; + constexpr usize name_max {sizeof(dirent::d_name) / sizeof(char)}; + + while ((name_length < name_max) && (this->entry->d_name[name_length] != 0)) name_length += 1; + + path current_path {path{}.joined(slice{this->entry->d_name, name_length})}; + + errno = 0; + this->entry = readdir(this->dir); + + if (this->entry == nullptr) switch (errno) { + case EBADF: return io_error::unavailable; + default: unreachable(); + } + + return current_path; + } + + /** + * Attempts to open a native directory at `directory_path`. + * + * An [open_result] is returned containing either [open_result::ok] to indicate success or any other value to + * indicate an error. See [open_result] for more details. + * + * *Note*: the opened directory must be closed using [close] once no longer needed or the process will leak + * directory streams. + * + * *Note*: if a directory is currently open under the native directory, it will attempt to close it before + * proceeding with opening the next. This means that [open] is safe to call without first calling [close]. + * + * *Note*: it is recommended to prefer performing file I/O via [sandboxed_fs] unless direct file access is + * required. + */ + open_result open(native_path const & directory_path) { + if (this->is_open()) { + open_result const result {close_to_open_result(this->close())}; + + if (result != open_result::ok) return result; + } + + // No room for zero terminator. + errno = 0; + this->dir = opendir(directory_path.as_slice().as_chars().pointer); + + if (!this->is_open()) switch (errno) { + case EACCES: return open_result::access_denied; + case EMFILE: case ENFILE: return open_result::too_many; + case ENOENT: case ENOTDIR: return open_result::not_found; + case ENOMEM: return open_result::out_of_memory; + } + + errno = 0; + this->entry = readdir(this->dir); + + if (this->entry == nullptr) switch (errno) { + case EBADF: { + if (this->is_open()) { + open_result const result {close_to_open_result(this->close())}; + + if (result != open_result::ok) return result; + } + + return open_result::io_unavailable; + } + + default: unreachable(); + } + + return open_result::ok; + } + + private: + DIR * dir {nullptr}; + + dirent * entry {nullptr}; + }; + + /** + * [coral::fs] wrapper around native file system access to provide a managed and system-agnostic environment for + * performing file I/O. + */ + struct sandboxed_fs : public coral::fs { + /** + * Permission flags that a [sandboxed_fs] may specify for restricting access to it. + */ + struct permissions { + bool can_read; + + bool can_write; + + bool can_walk; + }; + + /** + * Constructs a sandbox located at `sandbox_path` with `sandbox_permissions` as the permissions given to users + * of it. + */ + sandboxed_fs(native_path const & sandbox_path, permissions const & access_permissions) { + this->path = sandbox_path; + this->access_permissions = access_permissions; + } + + /** + * Returns a reference to a [sandboxed_fs] that provides access to the base directory which, on most systems, is + * the current working directory. + * + * The base directory may be used to access things loose files created outside of the application, such as user- + * generated files and modifications. + * + * *Note*: The base file system does not permit being written to. + */ + static sandboxed_fs & base() { + static sandboxed_fs base_fs {"./", { + .can_read = true, + .can_walk = true + }}; + + return base_fs; + } + + /** + * Returns a reference to a [sandboxed_fs] that operates as a temporary file store. + * + * As the name implies, the existence of files that exist here are not guaranteed beyond the duration of an + * application run lifetime. The purpose of the temporary file store is to support transactional I/O + * operations that only want to replace files if new ones has been successfully constructed already. + * + * *Note*: The temp file system does not permit being walked. + */ + static sandboxed_fs & temp() { + static sandboxed_fs base_fs {"/tmp", { + .can_read = true, + .can_write = false, + }}; + + return base_fs; + } + + /** + * See [coral::fs::read_file]. + * + * *Note*: this function will only work on sandboxes with the [permissions::can_read] flag enabled. + */ + void read_file(path const & target_path, closure const & then) override { + if (!this->access_permissions.can_read) return; + + this->path.joined(target_path.as_slice()).and_then([&](native_path const & native_file_path) -> void { + native_file file; + + if (file.open(native_file_path, native_file::open_mode::read_only) != open_result::ok) return; + + then(file); + + // TODO: Error orphaned file handle! + if (file.close() != close_result::ok) return; + }); + } + + /** + * See [coral::fs::walk_files]. + * + * *Note*: this function will only work on sandboxes with the [permissions::can_walk] flag enabled. + */ + void walk_files(path const & target_path, closure const & then) override { + if (!this->access_permissions.can_walk) return; + + this->path.joined(target_path.as_slice()).and_then([&](native_path const & native_directory_path) -> void { + native_directory directory; + + if (directory.open(native_directory_path) == open_result::ok) return; + + then(directory); + + // TODO: Error orphaned file handle! + if (directory.close() != close_result::ok) return; + }); + } + + /** + * See [coral::fs::write_file]. + * + * *Note*: this function will only work on sandboxes with the [permissions::can_write] flag enabled. + */ + void write_file(path const & target_path, closure const & then) override { + if (!this->access_permissions.can_write) return; + + this->path.joined(target_path.as_slice()).and_then([&](native_path const & native_file_path) -> void { + native_file file; + + if (file.open(native_file_path, native_file::open_mode::overwrite) != open_result::ok) return; + + then(file); + + // TODO: Error orphaned file handle! + if (file.close() != close_result::ok) return; + }); + } + + private: + native_path path; + + permissions access_permissions { + .can_read = false, + .can_write = false, + .can_walk = false, + }; + }; + + /** + * Returns a reference to the process-wide output device used for writing data out from to the wider system. + * + * This [coral::writer] is particularly useful for command-line tools which require communicating with another + * process via pipes or an end-user via the shell. + */ + coral::writer & output() { + static struct : public coral::writer { + coral::expected write(slice const & data) override { + coral::size const data_written {::write(STDOUT_FILENO, data.pointer, sizeof(coral::u8) * data.length)}; + + if (data_written == -1) return io_error::unavailable; + + return data_written; + }; + } output_writer; + + return output_writer; + } +} diff --git a/source/turtle/io.cpp b/source/turtle/io.cpp new file mode 100644 index 0000000..96732c0 --- /dev/null +++ b/source/turtle/io.cpp @@ -0,0 +1,54 @@ +export module turtle.io; + +import coral; +import coral.files; + +import turtle; + +export namespace turtle { + enum class log_level { + notice, + warning, + error, + }; + + struct event_loop { + void log(log_level level, coral::slice const & message) { + static_cast(output().write(message.as_chars().as_bytes())); + static_cast(output().write(coral::slice{"\n"}.as_bytes())); + } + + bool poll() { + return false; + } + + static int run(coral::path const & title, coral::closure execute) { + event_loop loop{title}; + + return execute(loop); + } + + private: + coral::path title; + + event_loop(coral::path const & title) { + this->title = title; + } + }; + + struct system_allocator : public coral::allocator { + system_allocator() = default; + + // TODO: implement thread-safety. + + coral::u8 * reallocate(coral::u8 * maybe_allocation, coral::usize requested_size) override { + if (maybe_allocation != nullptr) coral::unreachable(); + + return nullptr; + } + + void deallocate(void * allocation) override { + if (allocation != nullptr) coral::unreachable(); + } + }; +};