From 243d958df14b85788232aca623b83826115a5eb9 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Sat, 21 Jun 2025 12:27:20 +0100 Subject: rename modules/ to nihil/ --- CMakeLists.txt | 2 +- modules/CMakeLists.txt | 13 - modules/ctype.ccm | 86 ------ modules/fd.ccm | 309 --------------------- modules/generator.ccm | 691 ---------------------------------------------- modules/generic_error.ccm | 26 -- modules/getenv.ccm | 47 ---- modules/guard.ccm | 50 ---- modules/nihil.ccm | 15 - modules/tabulate.ccm | 282 ------------------- nihil/CMakeLists.txt | 13 + nihil/ctype.ccm | 86 ++++++ nihil/fd.ccm | 309 +++++++++++++++++++++ nihil/generator.ccm | 691 ++++++++++++++++++++++++++++++++++++++++++++++ nihil/generic_error.ccm | 26 ++ nihil/getenv.ccm | 47 ++++ nihil/guard.ccm | 50 ++++ nihil/nihil.ccm | 15 + nihil/tabulate.ccm | 282 +++++++++++++++++++ 19 files changed, 1520 insertions(+), 1520 deletions(-) delete mode 100644 modules/CMakeLists.txt delete mode 100644 modules/ctype.ccm delete mode 100644 modules/fd.ccm delete mode 100644 modules/generator.ccm delete mode 100644 modules/generic_error.ccm delete mode 100644 modules/getenv.ccm delete mode 100644 modules/guard.ccm delete mode 100644 modules/nihil.ccm delete mode 100644 modules/tabulate.ccm create mode 100644 nihil/CMakeLists.txt create mode 100644 nihil/ctype.ccm create mode 100644 nihil/fd.ccm create mode 100644 nihil/generator.ccm create mode 100644 nihil/generic_error.ccm create mode 100644 nihil/getenv.ccm create mode 100644 nihil/guard.ccm create mode 100644 nihil/nihil.ccm create mode 100644 nihil/tabulate.ccm diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c12885..9df3b46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ add_compile_options(-Wextra) add_compile_options(-Werror) add_compile_options(-Wpedantic) -add_subdirectory(modules) +add_subdirectory(nihil) add_subdirectory(tests) enable_testing() diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt deleted file mode 100644 index 383dd0a..0000000 --- a/modules/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# This source code is released into the public domain. - -add_library(nihil STATIC) -target_sources(nihil PUBLIC - FILE_SET modules TYPE CXX_MODULES FILES - nihil.ccm - ctype.ccm - fd.ccm - generator.ccm - generic_error.ccm - getenv.ccm - guard.ccm - tabulate.ccm) diff --git a/modules/ctype.ccm b/modules/ctype.ccm deleted file mode 100644 index cc058cd..0000000 --- a/modules/ctype.ccm +++ /dev/null @@ -1,86 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -export module nihil:ctype; - -namespace nihil { - -/* - * ctype_is: wrap std::ctype::is() in a form suitable for use as an algorithm - * predicate, i.e., ctype_is(m) will return a functor object that takes any char - * type as an argument and returns bool. - * - * If the locale is not specified, the current global locale is used by default. - * - * ctype_is copies the locale, so passing a temporary is fine. - */ - -export struct ctype_is final { - ctype_is(std::ctype_base::mask mask_, - std::locale const &locale_ = std::locale()) - : mask(mask_) - , locale(locale_) - {} - - auto operator()(this ctype_is const &self, std::integral auto c) - { - using ctype = std::ctype; - auto &facet = std::use_facet(self.locale); - return facet.is(self.mask, c); - } - -private: - std::ctype_base::mask mask; - std::locale locale; -}; - -// Predefined tests for the current global locale. - -export inline auto is_space = ctype_is(std::ctype_base::space); -export inline auto is_print = ctype_is(std::ctype_base::print); -export inline auto is_cntrl = ctype_is(std::ctype_base::cntrl); -export inline auto is_upper = ctype_is(std::ctype_base::upper); -export inline auto is_lower = ctype_is(std::ctype_base::lower); -export inline auto is_alpha = ctype_is(std::ctype_base::alpha); -export inline auto is_digit = ctype_is(std::ctype_base::digit); -export inline auto is_punct = ctype_is(std::ctype_base::punct); -export inline auto is_xdigit = ctype_is(std::ctype_base::xdigit); -export inline auto is_blank = ctype_is(std::ctype_base::blank); -export inline auto is_alnum = ctype_is(std::ctype_base::alnum); -export inline auto is_graph = ctype_is(std::ctype_base::graph); - -// Predefined tests for the C locale. The C locale is guaranteed to always be -// available, so this doesn't create lifetime issues. - -export inline auto is_c_space = - ctype_is(std::ctype_base::space, std::locale::classic()); -export inline auto is_c_print = - ctype_is(std::ctype_base::print, std::locale::classic()); -export inline auto is_c_cntrl = - ctype_is(std::ctype_base::cntrl, std::locale::classic()); -export inline auto is_c_upper = - ctype_is(std::ctype_base::upper, std::locale::classic()); -export inline auto is_c_lower = - ctype_is(std::ctype_base::lower, std::locale::classic()); -export inline auto is_c_alpha = - ctype_is(std::ctype_base::alpha, std::locale::classic()); -export inline auto is_c_digit = - ctype_is(std::ctype_base::digit, std::locale::classic()); -export inline auto is_c_punct = - ctype_is(std::ctype_base::punct, std::locale::classic()); -export inline auto is_c_xdigit = - ctype_is(std::ctype_base::xdigit, std::locale::classic()); -export inline auto is_c_blank = - ctype_is(std::ctype_base::blank, std::locale::classic()); -export inline auto is_c_alnum = - ctype_is(std::ctype_base::alnum, std::locale::classic()); -export inline auto is_c_graph = - ctype_is(std::ctype_base::graph, std::locale::classic()); - -} // namespace nihil diff --git a/modules/fd.ccm b/modules/fd.ccm deleted file mode 100644 index ad96ea7..0000000 --- a/modules/fd.ccm +++ /dev/null @@ -1,309 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -#include -#include -#include -#include - -export module nihil:fd; - -import :generic_error; - -namespace nihil { - -/* - * Exception thrown when an internal fd error occurs. This is not supposed - * to be caught, since it indicates an internal logic error in the caller. - */ -export struct fd_logic_error final : std::logic_error { - fd_logic_error(std::string what) - : std::logic_error(std::move(what)) - {} -}; - -/* - * fd: a file descriptor. - */ - -export struct fd final { - // Construct an empty (invalid) fd. - fd() noexcept = default; - - // Construct an fd from an exising file destrictor, taking ownership. - fd(int fd_) noexcept : _fd(fd_) {} - - // Destructor. Close the fd, discarding any errors. - ~fd() - { - if (*this) - this->close(); - } - - // Move from another fd, leaving the moved-from fd in an invalid state. - fd(fd &&other) noexcept - : _fd(std::exchange(other._fd, _invalid_fd)) - {} - - // Move assign from another fd. - auto operator=(fd &&other) noexcept -> fd & - { - if (this != &other) - _fd = std::exchange(other._fd, _invalid_fd); - return *this; - } - - // Not copyable. - fd(fd const &) = delete; - fd& operator=(fd const &) = delete; - - // Return true if this fd is valid (open). - explicit operator bool(this fd const &self) noexcept - { - return self._fd != _invalid_fd; - } - - // Close the wrapped fd. - auto close(this fd &self) -> std::expected - { - auto const ret = ::close(self.get()); - self._fd = _invalid_fd; - - if (ret == 0) - return {}; - - return std::unexpected(std::make_error_code(std::errc(errno))); - } - - // Return the stored fd. - auto get(this fd const &self) -> int { - if (self) - return self._fd; - throw fd_logic_error("Attempt to call get() on invalid fd"); - } - - - // Release the stored fd and return it. The caller must close it. - auto release(this fd &&self) -> int { - if (self) - return std::exchange(self._fd, self._invalid_fd); - throw fd_logic_error("Attempt to release an invalid fd"); - } - -private: - static constexpr int _invalid_fd = -1; - - int _fd = _invalid_fd; -}; - -// Create a copy of this fd by calling dup(). -export auto dup(fd const &self) -> std::expected -{ - auto thisfd = self.get(); - - auto const newfd = ::dup(thisfd); - if (newfd != -1) - return {newfd}; - - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -// Create a copy of this fd by calling dup2(). Note that because this results -// in the existing fd and the new fd both being managed by an fd instance, -// there are two potential cases that can cause problems: -// -// - dup()ing an fd to itself (a no-op) -// - dup()ing an fd to an fd which is already managed by an fd instance -// -// In both of these cases, either use raw_dup() instead, or immediately call -// release() on the returned fd to prevent the fd instance from closing it. -export auto dup(fd const &self, int newfd) - -> std::expected -{ - auto thisfd = self.get(); - - auto const ret = ::dup2(thisfd, newfd); - if (ret != -1) - return {newfd}; - - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -// Create a copy of this fd by calling dup(). -export auto raw_dup(fd const &self) -> std::expected -{ - auto thisfd = self.get(); - - auto const newfd = ::dup(thisfd); - if (newfd != -1) - return {newfd}; - - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -// Create a copy of this fd by calling dup2(). -export auto raw_dup(fd const &self, int newfd) - -> std::expected -{ - auto thisfd = self.get(); - - auto const ret = ::dup2(thisfd, newfd); - if (ret != -1) - return {newfd}; - - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -// Return the fnctl flags for this fd. -export auto getflags(fd const &self) -> std::expected -{ - auto const flags = ::fcntl(self.get(), F_GETFL); - if (flags != -1) - return {flags}; - - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -// Replace the fnctl flags for this fd. -export auto replaceflags(fd &self, int newflags) - -> std::expected -{ - auto const ret = ::fcntl(self.get(), F_SETFL, newflags); - if (ret == 0) - return {}; - - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -// Add bits to the fcntl flags for this fd. Returns the new flags. -export auto setflags(fd &self, int newflags) - -> std::expected -{ - auto flags = getflags(self); - if (!flags) - return flags; - - *flags |= newflags; - auto const ret = replaceflags(self, *flags); - if (!ret) - return std::unexpected(ret.error()); - - return {*flags}; -} - -// Remove bits from the fcntl flags for this fd. Returns the new flags. -export auto clearflags(fd &self, int clrflags) - -> std::expected -{ - auto flags = getflags(self); - if (!flags) - return flags; - - *flags &= ~clrflags; - auto const ret = replaceflags(self, *flags); - if (!ret) - return std::unexpected(ret.error()); - - return {*flags}; -} - -// Return the fd flags for this fd. -export auto getfdflags(fd const &self) -> std::expected -{ - auto const flags = ::fcntl(self.get(), F_GETFD); - if (flags != -1) - return {flags}; - - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -// Replace the fd flags for this fd. -export auto replacefdflags(fd &self, int newflags) - -> std::expected -{ - auto const ret = ::fcntl(self.get(), F_SETFD, newflags); - if (ret != -1) - return {}; - - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -// Add bits to the fd flags for this fd. Returns the new flags. -export auto setfdflags(fd &self, int newflags) - -> std::expected -{ - auto flags = getfdflags(self); - if (!flags) - return flags; - - *flags |= newflags; - auto const ret = replacefdflags(self, *flags); - if (!ret) - return std::unexpected(ret.error()); - - return {*flags}; -} - -// Remove bits from the fd flags for this fd. Returns the new flags. -export auto clearfdflags(fd &self, int clrflags) - -> std::expected -{ - auto flags = getfdflags(self); - if (!flags) - return flags; - - *flags &= ~clrflags; - auto ret = replacefdflags(self, *flags); - if (!ret) - return std::unexpected(ret.error()); - - return {*flags}; -} - -// Create two fds by calling pipe() and return them. -export auto pipe() -> std::expected, std::error_code> { - auto fds = std::array{}; - - if (auto const ret = ::pipe(fds.data()); ret != 0) - return std::unexpected(std::make_error_code(std::errc(errno))); - - return {{fd(fds[0]), fd(fds[1])}}; -} - -/* - * Write data to a file descriptor from the provided buffer. Returns the - * number of bytes (not objects) written. Incomplete writes may cause a - * partial object to be written. - */ -export auto write(fd &file, std::ranges::contiguous_range auto &&range) - -> std::expected -{ - auto const ret = ::write(file.get(), std::ranges::data(range), - std::ranges::size(range)); - if (ret >= 0) - return ret; - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -/* - * Read data from a file descriptor into the provided buffer. Returns the - * number of bytes (not objects) read. Incomplete reads may cause a partial - * object to be read. - */ -export auto read(fd &file, std::ranges::contiguous_range auto &&range) - -> std::expected -{ - auto const ret = ::read(file.get(), std::ranges::data(range), - std::ranges::size(range)); - if (ret >= 0) - return ret; - return std::unexpected(std::make_error_code(std::errc(errno))); -} - -} // namespace nihil diff --git a/modules/generator.ccm b/modules/generator.ccm deleted file mode 100644 index 82bcb27..0000000 --- a/modules/generator.ccm +++ /dev/null @@ -1,691 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// Reference implementation of std::generator proposal P2168. -// -// See https://wg21.link/P2168 for details. -// -/////////////////////////////////////////////////////////////////////////////// -// Copyright Lewis Baker, Corentin Jabot -// -// Use, modification and distribution is subject to the Boost Software License, -// Version 1.0. -// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) -/////////////////////////////////////////////////////////////////////////////// - -module; - -#include -#include -#include -#include -#include -#include - -export module nihil:generator; - -namespace nihil { - -template -class __manual_lifetime { - public: - __manual_lifetime() noexcept {} - ~__manual_lifetime() {} - - template - _T& construct(_Args&&... __args) noexcept(std::is_nothrow_constructible_v<_T, _Args...>) { - return *::new (static_cast(std::addressof(__value_))) _T((_Args&&)__args...); - } - - void destruct() noexcept(std::is_nothrow_destructible_v<_T>) { - __value_.~_T(); - } - - _T& get() & noexcept { - return __value_; - } - _T&& get() && noexcept { - return static_cast<_T&&>(__value_); - } - const _T& get() const & noexcept { - return __value_; - } - const _T&& get() const && noexcept { - return static_cast(__value_); - } - - private: - union { - std::remove_const_t<_T> __value_; - }; -}; - -template -class __manual_lifetime<_T&> { - public: - __manual_lifetime() noexcept : __value_(nullptr) {} - ~__manual_lifetime() {} - - _T& construct(_T& __value) noexcept { - __value_ = std::addressof(__value); - return __value; - } - - void destruct() noexcept {} - - _T& get() const noexcept { - return *__value_; - } - - private: - _T* __value_; -}; - -template -class __manual_lifetime<_T&&> { - public: - __manual_lifetime() noexcept : __value_(nullptr) {} - ~__manual_lifetime() {} - - _T&& construct(_T&& __value) noexcept { - __value_ = std::addressof(__value); - return static_cast<_T&&>(__value); - } - - void destruct() noexcept {} - - _T&& get() const noexcept { - return static_cast<_T&&>(*__value_); - } - - private: - _T* __value_; -}; - -struct use_allocator_arg {}; - -namespace ranges { - -export template -struct elements_of { - explicit constexpr elements_of(_Rng&& __rng) noexcept - requires std::is_default_constructible_v<_Allocator> - : __range(static_cast<_Rng&&>(__rng)) - {} - - constexpr elements_of(_Rng&& __rng, _Allocator&& __alloc) noexcept - : __range((_Rng&&)__rng), __alloc((_Allocator&&)__alloc) {} - - constexpr elements_of(elements_of&&) noexcept = default; - - constexpr elements_of(const elements_of &) = delete; - constexpr elements_of &operator=(const elements_of &) = delete; - constexpr elements_of &operator=(elements_of &&) = delete; - - constexpr _Rng&& get() noexcept { - return static_cast<_Rng&&>(__range); - } - - constexpr _Allocator get_allocator() const noexcept { - return __alloc; - } - -private: - [[no_unique_address]] _Allocator __alloc; // \expos - _Rng && __range; // \expos -}; - -export template -elements_of(_Rng &&) -> elements_of<_Rng>; - -export template -elements_of(_Rng &&, Allocator&&) -> elements_of<_Rng, Allocator>; - -} // namespace ranges - -template -static constexpr bool __allocator_needs_to_be_stored = - !std::allocator_traits<_Alloc>::is_always_equal::value || - !std::is_default_constructible_v<_Alloc>; - -// Round s up to next multiple of a. -constexpr size_t __aligned_allocation_size(size_t s, size_t a) { - return (s + a - 1) & ~(a - 1); -} - - -export template , - typename _Allocator = use_allocator_arg> -class generator; - -template -class __promise_base_alloc { - static constexpr std::size_t __offset_of_allocator(std::size_t __frameSize) noexcept { - return __aligned_allocation_size(__frameSize, alignof(_Alloc)); - } - - static constexpr std::size_t __padded_frame_size(std::size_t __frameSize) noexcept { - return __offset_of_allocator(__frameSize) + sizeof(_Alloc); - } - - static _Alloc& __get_allocator(void* __frame, std::size_t __frameSize) noexcept { - return *reinterpret_cast<_Alloc*>( - static_cast(__frame) + __offset_of_allocator(__frameSize)); - } - -public: - template - static void* operator new(std::size_t __frameSize, std::allocator_arg_t, _Alloc __alloc, _Args&...) { - void* __frame = __alloc.allocate(__padded_frame_size(__frameSize)); - - // Store allocator at end of the coroutine frame. - // Assuming the allocator's move constructor is non-throwing (a requirement for allocators) - ::new (static_cast(std::addressof(__get_allocator(__frame, __frameSize)))) _Alloc(std::move(__alloc)); - - return __frame; - } - - template - static void* operator new(std::size_t __frameSize, _This&, std::allocator_arg_t, _Alloc __alloc, _Args&...) { - return __promise_base_alloc::operator new(__frameSize, std::allocator_arg, std::move(__alloc)); - } - - static void operator delete(void* __ptr, std::size_t __frameSize) noexcept { - _Alloc& __alloc = __get_allocator(__ptr, __frameSize); - _Alloc __localAlloc(std::move(__alloc)); - __alloc.~Alloc(); - __localAlloc.deallocate(static_cast(__ptr), __padded_frame_size(__frameSize)); - } -}; - -template - requires (!__allocator_needs_to_be_stored<_Alloc>) -class __promise_base_alloc<_Alloc> { -public: - static void* operator new(std::size_t __size) { - _Alloc __alloc; - return __alloc.allocate(__size); - } - - static void operator delete(void* __ptr, std::size_t __size) noexcept { - _Alloc __alloc; - __alloc.deallocate(static_cast(__ptr), __size); - } -}; - -template -struct __generator_promise_base -{ - template - friend class generator; - - __generator_promise_base* __root_; - std::coroutine_handle<> __parentOrLeaf_; - // Note: Using manual_lifetime here to avoid extra calls to exception_ptr - // constructor/destructor in cases where it is not needed (i.e. where this - // generator coroutine is not used as a nested coroutine). - // This member is lazily constructed by the __yield_sequence_awaiter::await_suspend() - // method if this generator is used as a nested generator. - __manual_lifetime __exception_; - __manual_lifetime<_Ref> __value_; - - explicit __generator_promise_base(std::coroutine_handle<> thisCoro) noexcept - : __root_(this) - , __parentOrLeaf_(thisCoro) - {} - - ~__generator_promise_base() { - if (__root_ != this) { - // This coroutine was used as a nested generator and so will - // have constructed its __exception_ member which needs to be - // destroyed here. - __exception_.destruct(); - } - } - - std::suspend_always initial_suspend() noexcept { - return {}; - } - - void return_void() noexcept {} - - void unhandled_exception() { - if (__root_ != this) { - __exception_.get() = std::current_exception(); - } else { - throw; - } - } - - // Transfers control back to the parent of a nested coroutine - struct __final_awaiter { - bool await_ready() noexcept { - return false; - } - - template - std::coroutine_handle<> - await_suspend(std::coroutine_handle<_Promise> __h) noexcept { - _Promise& __promise = __h.promise(); - __generator_promise_base& __root = *__promise.__root_; - if (&__root != &__promise) { - auto __parent = __promise.__parentOrLeaf_; - __root.__parentOrLeaf_ = __parent; - return __parent; - } - return std::noop_coroutine(); - } - - void await_resume() noexcept {} - }; - - __final_awaiter final_suspend() noexcept { - return {}; - } - - std::suspend_always yield_value(_Ref&& __x) - noexcept(std::is_nothrow_move_constructible_v<_Ref>) { - __root_->__value_.construct((_Ref&&)__x); - return {}; - } - - template - requires - (!std::is_reference_v<_Ref>) && - std::is_convertible_v<_T, _Ref> - std::suspend_always yield_value(_T&& __x) - noexcept(std::is_nothrow_constructible_v<_Ref, _T>) { - __root_->__value_.construct((_T&&)__x); - return {}; - } - - template - struct __yield_sequence_awaiter { - _Gen __gen_; - - __yield_sequence_awaiter(_Gen&& __g) noexcept - // Taking ownership of the generator ensures frame are destroyed - // in the reverse order of their execution. - : __gen_((_Gen&&)__g) { - } - - bool await_ready() noexcept { - return false; - } - - // set the parent, root and exceptions pointer and - // resume the nested - template - std::coroutine_handle<> - await_suspend(std::coroutine_handle<_Promise> __h) noexcept { - __generator_promise_base& __current = __h.promise(); - __generator_promise_base& __nested = *__gen_.__get_promise(); - __generator_promise_base& __root = *__current.__root_; - - __nested.__root_ = __current.__root_; - __nested.__parentOrLeaf_ = __h; - - // Lazily construct the __exception_ member here now that we - // know it will be used as a nested generator. This will be - // destroyed by the promise destructor. - __nested.__exception_.construct(); - __root.__parentOrLeaf_ = __gen_.__get_coro(); - - // Immediately resume the nested coroutine (nested generator) - return __gen_.__get_coro(); - } - - void await_resume() { - __generator_promise_base& __nestedPromise = *__gen_.__get_promise(); - if (__nestedPromise.__exception_.get()) { - std::rethrow_exception(std::move(__nestedPromise.__exception_.get())); - } - } - }; - - template - __yield_sequence_awaiter> - yield_value(nihil::ranges::elements_of> __g) noexcept { - return std::move(__g).get(); - } - - template - __yield_sequence_awaiter, _Allocator>> - yield_value(nihil::ranges::elements_of<_Rng, _Allocator> && __x) { - return [](std::allocator_arg_t, _Allocator, auto && __rng) -> generator<_Ref, std::remove_cvref_t<_Ref>, _Allocator> { - for(auto && e: __rng) - co_yield static_cast(e); - }(std::allocator_arg, __x.get_allocator(), std::forward<_Rng>(__x.get())); - } - - void resume() { - __parentOrLeaf_.resume(); - } - - // Disable use of co_await within this coroutine. - void await_transform() = delete; -}; - -template -struct __generator_promise; - -template -struct __generator_promise, _ByteAllocator, _ExplicitAllocator> final - : public __generator_promise_base<_Ref> - , public __promise_base_alloc<_ByteAllocator> { - __generator_promise() noexcept - : __generator_promise_base<_Ref>(std::coroutine_handle<__generator_promise>::from_promise(*this)) - {} - - generator<_Ref, _Value, _Alloc> get_return_object() noexcept { - return generator<_Ref, _Value, _Alloc>{ - std::coroutine_handle<__generator_promise>::from_promise(*this) - }; - } - - using __generator_promise_base<_Ref>::yield_value; - - template - typename __generator_promise_base<_Ref>::template __yield_sequence_awaiter> - yield_value(nihil::ranges::elements_of<_Rng> && __x) { - static_assert (!_ExplicitAllocator, - "This coroutine has an explicit allocator specified with std::allocator_arg so an allocator needs to be passed " - "explicitely to std::elements_of"); - return [](auto && __rng) -> generator<_Ref, _Value, _Alloc> { - for(auto && e: __rng) - co_yield static_cast(e); - }(std::forward<_Rng>(__x.get())); - } -}; - -template -using __byte_allocator_t = typename std::allocator_traits>::template rebind_alloc; - -} // namespace nihil - -namespace std { - -// Type-erased allocator with default allocator behaviour. -export template -struct coroutine_traits, _Args...> { - using promise_type = nihil::__generator_promise, std::allocator>; -}; - -// Type-erased allocator with std::allocator_arg parameter -export template -struct coroutine_traits, allocator_arg_t, _Alloc, _Args...> { -private: - using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; -public: - using promise_type = nihil::__generator_promise, __byte_allocator, true /*explicit Allocator*/>; -}; - -// Type-erased allocator with std::allocator_arg parameter (non-static member functions) -export template -struct coroutine_traits, _This, allocator_arg_t, _Alloc, _Args...> { -private: - using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; -public: - using promise_type = nihil::__generator_promise, __byte_allocator, true /*explicit Allocator*/>; -}; - -// Generator with specified allocator type -export template -struct coroutine_traits, _Args...> { - using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; -public: - using promise_type = nihil::__generator_promise, __byte_allocator>; -}; - -} // namespace std - -namespace nihil { - -// TODO : make layout compatible promise casts possible -export template -class generator { - using __byte_allocator = __byte_allocator_t<_Alloc>; -public: - using promise_type = __generator_promise, __byte_allocator>; - friend promise_type; -private: - using __coroutine_handle = std::coroutine_handle; -public: - - generator() noexcept = default; - - generator(generator&& __other) noexcept - : __coro_(std::exchange(__other.__coro_, {})) - , __started_(std::exchange(__other.__started_, false)) { - } - - ~generator() noexcept { - if (__coro_) { - if (__started_ && !__coro_.done()) { - __coro_.promise().__value_.destruct(); - } - __coro_.destroy(); - } - } - - generator& operator=(generator && g) noexcept { - swap(g); - return *this; - } - - void swap(generator& __other) noexcept { - std::swap(__coro_, __other.__coro_); - std::swap(__started_, __other.__started_); - } - - struct sentinel {}; - - class iterator { - public: - using iterator_category = std::input_iterator_tag; - using difference_type = std::ptrdiff_t; - using value_type = _Value; - using reference = _Ref; - using pointer = std::add_pointer_t<_Ref>; - - iterator() noexcept = default; - iterator(const iterator &) = delete; - - iterator(iterator&& __other) noexcept - : __coro_(std::exchange(__other.__coro_, {})) { - } - - iterator& operator=(iterator&& __other) { - std::swap(__coro_, __other.__coro_); - return *this; - } - - ~iterator() { - } - - friend bool operator==(const iterator &it, sentinel) noexcept { - return it.__coro_.done(); - } - - iterator &operator++() { - __coro_.promise().__value_.destruct(); - __coro_.promise().resume(); - return *this; - } - void operator++(int) { - (void)operator++(); - } - - reference operator*() const noexcept { - return static_cast(__coro_.promise().__value_.get()); - } - - private: - friend generator; - - explicit iterator(__coroutine_handle __coro) noexcept - : __coro_(__coro) {} - - __coroutine_handle __coro_; - }; - - iterator begin() { - assert(__coro_); - assert(!__started_); - __started_ = true; - __coro_.resume(); - return iterator{__coro_}; - } - - sentinel end() noexcept { - return {}; - } - -private: - explicit generator(__coroutine_handle __coro) noexcept - : __coro_(__coro) { - } - -public: // to get around access restrictions for __yield_sequence_awaitable - std::coroutine_handle<> __get_coro() noexcept { return __coro_; } - promise_type* __get_promise() noexcept { return std::addressof(__coro_.promise()); } - -private: - __coroutine_handle __coro_; - bool __started_ = false; -}; - -// Specialisation for type-erased allocator implementation. -export template -class generator<_Ref, _Value, use_allocator_arg> { - using __promise_base = __generator_promise_base<_Ref>; -public: - - generator() noexcept - : __promise_(nullptr) - , __coro_() - , __started_(false) - {} - - generator(generator&& __other) noexcept - : __promise_(std::exchange(__other.__promise_, nullptr)) - , __coro_(std::exchange(__other.__coro_, {})) - , __started_(std::exchange(__other.__started_, false)) { - } - - ~generator() noexcept { - if (__coro_) { - if (__started_ && !__coro_.done()) { - __promise_->__value_.destruct(); - } - __coro_.destroy(); - } - } - - generator& operator=(generator g) noexcept { - swap(g); - return *this; - } - - void swap(generator& __other) noexcept { - std::swap(__promise_, __other.__promise_); - std::swap(__coro_, __other.__coro_); - std::swap(__started_, __other.__started_); - } - - struct sentinel {}; - - class iterator { - public: - using iterator_category = std::input_iterator_tag; - using difference_type = std::ptrdiff_t; - using value_type = _Value; - using reference = _Ref; - using pointer = std::add_pointer_t<_Ref>; - - iterator() noexcept = default; - iterator(const iterator &) = delete; - - iterator(iterator&& __other) noexcept - : __promise_(std::exchange(__other.__promise_, nullptr)) - , __coro_(std::exchange(__other.__coro_, {})) - {} - - iterator& operator=(iterator&& __other) { - __promise_ = std::exchange(__other.__promise_, nullptr); - __coro_ = std::exchange(__other.__coro_, {}); - return *this; - } - - ~iterator() = default; - - friend bool operator==(const iterator &it, sentinel) noexcept { - return it.__coro_.done(); - } - - iterator& operator++() { - __promise_->__value_.destruct(); - __promise_->resume(); - return *this; - } - - void operator++(int) { - (void)operator++(); - } - - reference operator*() const noexcept { - return static_cast(__promise_->__value_.get()); - } - - private: - friend generator; - - explicit iterator(__promise_base* __promise, std::coroutine_handle<> __coro) noexcept - : __promise_(__promise) - , __coro_(__coro) - {} - - __promise_base* __promise_; - std::coroutine_handle<> __coro_; - }; - - iterator begin() { - assert(__coro_); - assert(!__started_); - __started_ = true; - __coro_.resume(); - return iterator{__promise_, __coro_}; - } - - sentinel end() noexcept { - return {}; - } - -private: - template - friend struct __generator_promise; - - template - explicit generator(std::coroutine_handle<_Promise> __coro) noexcept - : __promise_(std::addressof(__coro.promise())) - , __coro_(__coro) - {} - -public: // to get around access restrictions for __yield_sequence_awaitable - std::coroutine_handle<> __get_coro() noexcept { return __coro_; } - __promise_base* __get_promise() noexcept { return __promise_; } - -private: - __promise_base* __promise_; - std::coroutine_handle<> __coro_; - bool __started_ = false; -}; - -} // namespace nihil - -export namespace std::ranges { - -template -constexpr inline bool enable_view> = true; - -} // namespace std::ranges - diff --git a/modules/generic_error.ccm b/modules/generic_error.ccm deleted file mode 100644 index a582519..0000000 --- a/modules/generic_error.ccm +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -export module nihil:generic_error; - -namespace nihil { - -/* - * generic_error is the base class that all other exceptions derive from. - * It is an std::runtime_error, and what() should always be informative. - */ - -export struct generic_error : std::runtime_error { - template - generic_error(std::format_string fmt, Args &&...args) - : std::runtime_error(std::format(fmt, std::forward(args)...)) - {} -}; - -} // namespace nihil diff --git a/modules/getenv.ccm b/modules/getenv.ccm deleted file mode 100644 index 7397b79..0000000 --- a/modules/getenv.ccm +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include - -#include - -export module nihil:getenv; - -namespace nihil { - -/* - * Find a variable by the given name in the environment by calling getenv_r(). - */ - -export auto getenv(std::string_view varname) - -> std::expected -{ - // Start with a buffer of this size, and double it every iteration. - constexpr auto bufinc = std::size_t{1024}; - - auto cvarname = std::string(varname); - auto buf = std::vector(bufinc); - for (;;) { - auto const ret = ::getenv_r(cvarname.c_str(), - buf.data(), buf.size()); - - if (ret == 0) - return {std::string(buf.data())}; - - if (ret == -1 && errno == ERANGE) { - buf.resize(buf.size() * 2); - continue; - } - - return std::unexpected(std::make_error_code(std::errc(errno))); - } -} - -} // namespace nihil diff --git a/modules/guard.ccm b/modules/guard.ccm deleted file mode 100644 index 18c6d70..0000000 --- a/modules/guard.ccm +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include - -export module nihil:guard; - -namespace nihil { - -/* - * guard: invoke a callable when this object is destroyed; this is similar to - * scope_exit from the library fundamentals TS, which LLVM doesn't implement. - */ -export template -struct guard final { - // Initialise the guard with a callable we will invoke later. - guard(F func) : _func(std::move(func)) {} - - /* - * We are being destroyed, so call the callable. - * If the callable throws, std::terminate() will be called. - */ - ~guard() { - if (_func) - std::invoke(*_func); - } - - // Release the guard. This turns the destructor into a no-op. - void release() noexcept { - _func.reset(); - } - - // Not default-constructible or copyable. - guard() = delete; - guard(guard const &) = delete; - guard(guard &&) noexcept = delete; - guard &operator=(guard const &) = delete; - guard &operator=(guard &&) noexcept = delete; - -private: - // The callable to be invoked when we are destroyed. - std::optional _func; -}; - -} // namespace nihil diff --git a/modules/nihil.ccm b/modules/nihil.ccm deleted file mode 100644 index 69cc282..0000000 --- a/modules/nihil.ccm +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -export module nihil; - -export import :ctype; -export import :generator; -export import :generic_error; -export import :getenv; -export import :guard; -export import :fd; -export import :tabulate; diff --git a/modules/tabulate.ccm b/modules/tabulate.ccm deleted file mode 100644 index debb784..0000000 --- a/modules/tabulate.ccm +++ /dev/null @@ -1,282 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include - -export module nihil:tabulate; - -import :ctype; -import :generic_error; - -namespace nihil { - -/* - * tabulate: format the given range in an ASCII table and write the output - * to the given output iterator. The range's values will be converted to - * strings as if by std::format. - * - * tabulate is implemented by copying the range; this allows it to work on - * input/forward ranges at the cost of slightly increased memory use. - * - * The table spec is a string consisting of zero or more field formats, - * formatted as {flags:fieldname}; both flags and fieldname are optional. - * If there are fewer field formats than fields, the remaining fields - * are formatted as if by {:}. - * - * The following flags are supported: - * - * < left-align this column (default) - * > right-align this column - */ - -// Exception thrown when a table spec is invalid. -export struct table_spec_error : generic_error { - template - table_spec_error(std::format_string fmt, Args &&...args) - : generic_error(fmt, std::forward(args)...) - {} -}; - -/* - * The specification for a single field. - */ -template -struct field_spec { - std::basic_string_view name; - std::size_t width = 0; - enum { left, right } align = left; - - // Ensure the length of this field is at least the given width. - auto ensure_width(std::size_t newwidth) -> void - { - width = std::max(width, newwidth); - } - - // Format an object to a string based on our field spec. - auto format(auto &&obj) const -> std::basic_string - { - std::basic_string format_string{'{', '}'}; - return std::format(std::runtime_format(format_string), obj); - } - - // Print a column value to an output iterator according to our field - // spec. If is_last is true, this is the last field on the line, so - // we won't output any trailling padding. - auto print(std::basic_string_view value, - std::output_iterator auto &out, - bool is_last) - const - { - auto padding = width - value.size(); - - if (align == right) - for (std::size_t i = 0; i < padding; ++i) - *out++ = ' '; - - std::ranges::copy(value, out); - - if (!is_last && align == left) - for (std::size_t i = 0; i < padding; ++i) - *out++ = ' '; - } -}; - -/* - * The specification for an entire table. - */ -template -struct table_spec { - // Add a new field spec to this table. - auto add(field_spec field) - { - _fields.emplace_back(std::move(field)); - } - - // Return the field spec for a given field. If the field doesn't - // exist, this field and any intermediate fields will be created. - auto field(std::size_t fieldnr) -> field_spec& - { - if (_fields.size() < fieldnr + 1) - _fields.resize(fieldnr + 1); - return _fields.at(fieldnr); - } - - // The number of columns in this table. - auto columns() const -> std::size_t - { - return _fields.size(); - } - - // Return all the fields in this table. - auto fields() const - { - return _fields; - } - -private: - std::vector> _fields; -}; - -// Parse the field flags, e.g. '<'. -template Sentinel> -auto parse_field_flags(field_spec &field, Iterator &pos, Sentinel end) - -> void -{ - while (pos < end) { - switch (*pos) { - case '<': - field.align = field_spec::left; - break; - case '>': - field.align = field_spec::right; - break; - case ':': - ++pos; - /*FALLTHROUGH*/ - case '}': - return; - default: - throw table_spec_error( - "Invalid table spec: unknown flag character"); - } - - if (++pos == end) - throw table_spec_error("Invalid table spec: " - "unterminated field"); - } -} - -// Parse a complete field spec, e.g. "{<:NAME}". -template Sentinel> -auto parse_field(Iterator &pos, Sentinel end) - -> field_spec -{ - auto field = field_spec{}; - - if (pos == end) - throw table_spec_error("Invalid table spec: empty field"); - - // The field spec should start with a '{'. - if (*pos != '{') - throw table_spec_error("Invalid table spec: expected '{{'"); - - if (++pos == end) - throw table_spec_error("Invalid table spec: unterminated field"); - - // This consumes 'pos' up to and including the ':'. - parse_field_flags(field, pos, end); - - auto brace = std::ranges::find(pos, end, '}'); - if (brace == end) - throw table_spec_error("Invalid table spec: expected '}}'"); - - field.name = std::basic_string_view(pos, brace); - pos = std::next(brace); - - // The field must be at least as wide as its header. - field.width = field.name.size(); - - return field; -} - -template -auto parse_table_spec(std::basic_string_view spec) -> table_spec -{ - auto table = table_spec(); - - auto pos = std::ranges::begin(spec); - auto end = std::ranges::end(spec); - - for (;;) { - // Skip leading whitespace - while (pos < end && is_c_space(*pos)) - ++pos; - - if (pos == end) - break; - - table.add(parse_field(pos, end)); - } - - return table; -} - -export template Iterator> -auto basic_tabulate(std::basic_string_view table_spec, - Range &&range, - Iterator &&out) - -> void -{ - // Parse the table spec. - auto table = parse_table_spec(table_spec); - - // Create our copy of the input data. - auto data = std::vector>>(); - // Reserve the first row for the header. - data.resize(1); - - // Find the required length of each field. - for (auto &&row : range) { - // LLVM doesn't have std::enumerate_view yet - auto i = std::size_t{0}; - auto &this_row = data.emplace_back(); - - for (auto &&column : row) { - auto &field = table.field(i); - auto &str = this_row.emplace_back(field.format(column)); - field.ensure_width(str.size()); - ++i; - } - } - - // Add the header row. - for (auto &&field : table.fields()) - data.at(0).emplace_back(std::from_range, field.name); - - // Print the values. - for (auto &&row : data) { - for (std::size_t i = 0; i < row.size(); ++i) { - auto &field = table.field(i); - bool is_last = (i == row.size() - 1); - - field.print(row[i], out, is_last); - - if (!is_last) - *out++ = ' '; - } - - *out++ = '\n'; - } -} - -export auto tabulate(std::string_view table_spec, - std::ranges::range auto &&range, - std::output_iterator auto &&out) -{ - return basic_tabulate(table_spec, - std::forward(range), - std::forward(out)); -} - -export auto wtabulate(std::wstring_view table_spec, - std::ranges::range auto &&range, - std::output_iterator auto &&out) -{ - return basic_tabulate(table_spec, - std::forward(range), - std::forward(out)); -} - -} // namespace nihil diff --git a/nihil/CMakeLists.txt b/nihil/CMakeLists.txt new file mode 100644 index 0000000..383dd0a --- /dev/null +++ b/nihil/CMakeLists.txt @@ -0,0 +1,13 @@ +# This source code is released into the public domain. + +add_library(nihil STATIC) +target_sources(nihil PUBLIC + FILE_SET modules TYPE CXX_MODULES FILES + nihil.ccm + ctype.ccm + fd.ccm + generator.ccm + generic_error.ccm + getenv.ccm + guard.ccm + tabulate.ccm) diff --git a/nihil/ctype.ccm b/nihil/ctype.ccm new file mode 100644 index 0000000..cc058cd --- /dev/null +++ b/nihil/ctype.ccm @@ -0,0 +1,86 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +export module nihil:ctype; + +namespace nihil { + +/* + * ctype_is: wrap std::ctype::is() in a form suitable for use as an algorithm + * predicate, i.e., ctype_is(m) will return a functor object that takes any char + * type as an argument and returns bool. + * + * If the locale is not specified, the current global locale is used by default. + * + * ctype_is copies the locale, so passing a temporary is fine. + */ + +export struct ctype_is final { + ctype_is(std::ctype_base::mask mask_, + std::locale const &locale_ = std::locale()) + : mask(mask_) + , locale(locale_) + {} + + auto operator()(this ctype_is const &self, std::integral auto c) + { + using ctype = std::ctype; + auto &facet = std::use_facet(self.locale); + return facet.is(self.mask, c); + } + +private: + std::ctype_base::mask mask; + std::locale locale; +}; + +// Predefined tests for the current global locale. + +export inline auto is_space = ctype_is(std::ctype_base::space); +export inline auto is_print = ctype_is(std::ctype_base::print); +export inline auto is_cntrl = ctype_is(std::ctype_base::cntrl); +export inline auto is_upper = ctype_is(std::ctype_base::upper); +export inline auto is_lower = ctype_is(std::ctype_base::lower); +export inline auto is_alpha = ctype_is(std::ctype_base::alpha); +export inline auto is_digit = ctype_is(std::ctype_base::digit); +export inline auto is_punct = ctype_is(std::ctype_base::punct); +export inline auto is_xdigit = ctype_is(std::ctype_base::xdigit); +export inline auto is_blank = ctype_is(std::ctype_base::blank); +export inline auto is_alnum = ctype_is(std::ctype_base::alnum); +export inline auto is_graph = ctype_is(std::ctype_base::graph); + +// Predefined tests for the C locale. The C locale is guaranteed to always be +// available, so this doesn't create lifetime issues. + +export inline auto is_c_space = + ctype_is(std::ctype_base::space, std::locale::classic()); +export inline auto is_c_print = + ctype_is(std::ctype_base::print, std::locale::classic()); +export inline auto is_c_cntrl = + ctype_is(std::ctype_base::cntrl, std::locale::classic()); +export inline auto is_c_upper = + ctype_is(std::ctype_base::upper, std::locale::classic()); +export inline auto is_c_lower = + ctype_is(std::ctype_base::lower, std::locale::classic()); +export inline auto is_c_alpha = + ctype_is(std::ctype_base::alpha, std::locale::classic()); +export inline auto is_c_digit = + ctype_is(std::ctype_base::digit, std::locale::classic()); +export inline auto is_c_punct = + ctype_is(std::ctype_base::punct, std::locale::classic()); +export inline auto is_c_xdigit = + ctype_is(std::ctype_base::xdigit, std::locale::classic()); +export inline auto is_c_blank = + ctype_is(std::ctype_base::blank, std::locale::classic()); +export inline auto is_c_alnum = + ctype_is(std::ctype_base::alnum, std::locale::classic()); +export inline auto is_c_graph = + ctype_is(std::ctype_base::graph, std::locale::classic()); + +} // namespace nihil diff --git a/nihil/fd.ccm b/nihil/fd.ccm new file mode 100644 index 0000000..ad96ea7 --- /dev/null +++ b/nihil/fd.ccm @@ -0,0 +1,309 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +#include +#include +#include +#include + +export module nihil:fd; + +import :generic_error; + +namespace nihil { + +/* + * Exception thrown when an internal fd error occurs. This is not supposed + * to be caught, since it indicates an internal logic error in the caller. + */ +export struct fd_logic_error final : std::logic_error { + fd_logic_error(std::string what) + : std::logic_error(std::move(what)) + {} +}; + +/* + * fd: a file descriptor. + */ + +export struct fd final { + // Construct an empty (invalid) fd. + fd() noexcept = default; + + // Construct an fd from an exising file destrictor, taking ownership. + fd(int fd_) noexcept : _fd(fd_) {} + + // Destructor. Close the fd, discarding any errors. + ~fd() + { + if (*this) + this->close(); + } + + // Move from another fd, leaving the moved-from fd in an invalid state. + fd(fd &&other) noexcept + : _fd(std::exchange(other._fd, _invalid_fd)) + {} + + // Move assign from another fd. + auto operator=(fd &&other) noexcept -> fd & + { + if (this != &other) + _fd = std::exchange(other._fd, _invalid_fd); + return *this; + } + + // Not copyable. + fd(fd const &) = delete; + fd& operator=(fd const &) = delete; + + // Return true if this fd is valid (open). + explicit operator bool(this fd const &self) noexcept + { + return self._fd != _invalid_fd; + } + + // Close the wrapped fd. + auto close(this fd &self) -> std::expected + { + auto const ret = ::close(self.get()); + self._fd = _invalid_fd; + + if (ret == 0) + return {}; + + return std::unexpected(std::make_error_code(std::errc(errno))); + } + + // Return the stored fd. + auto get(this fd const &self) -> int { + if (self) + return self._fd; + throw fd_logic_error("Attempt to call get() on invalid fd"); + } + + + // Release the stored fd and return it. The caller must close it. + auto release(this fd &&self) -> int { + if (self) + return std::exchange(self._fd, self._invalid_fd); + throw fd_logic_error("Attempt to release an invalid fd"); + } + +private: + static constexpr int _invalid_fd = -1; + + int _fd = _invalid_fd; +}; + +// Create a copy of this fd by calling dup(). +export auto dup(fd const &self) -> std::expected +{ + auto thisfd = self.get(); + + auto const newfd = ::dup(thisfd); + if (newfd != -1) + return {newfd}; + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +// Create a copy of this fd by calling dup2(). Note that because this results +// in the existing fd and the new fd both being managed by an fd instance, +// there are two potential cases that can cause problems: +// +// - dup()ing an fd to itself (a no-op) +// - dup()ing an fd to an fd which is already managed by an fd instance +// +// In both of these cases, either use raw_dup() instead, or immediately call +// release() on the returned fd to prevent the fd instance from closing it. +export auto dup(fd const &self, int newfd) + -> std::expected +{ + auto thisfd = self.get(); + + auto const ret = ::dup2(thisfd, newfd); + if (ret != -1) + return {newfd}; + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +// Create a copy of this fd by calling dup(). +export auto raw_dup(fd const &self) -> std::expected +{ + auto thisfd = self.get(); + + auto const newfd = ::dup(thisfd); + if (newfd != -1) + return {newfd}; + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +// Create a copy of this fd by calling dup2(). +export auto raw_dup(fd const &self, int newfd) + -> std::expected +{ + auto thisfd = self.get(); + + auto const ret = ::dup2(thisfd, newfd); + if (ret != -1) + return {newfd}; + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +// Return the fnctl flags for this fd. +export auto getflags(fd const &self) -> std::expected +{ + auto const flags = ::fcntl(self.get(), F_GETFL); + if (flags != -1) + return {flags}; + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +// Replace the fnctl flags for this fd. +export auto replaceflags(fd &self, int newflags) + -> std::expected +{ + auto const ret = ::fcntl(self.get(), F_SETFL, newflags); + if (ret == 0) + return {}; + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +// Add bits to the fcntl flags for this fd. Returns the new flags. +export auto setflags(fd &self, int newflags) + -> std::expected +{ + auto flags = getflags(self); + if (!flags) + return flags; + + *flags |= newflags; + auto const ret = replaceflags(self, *flags); + if (!ret) + return std::unexpected(ret.error()); + + return {*flags}; +} + +// Remove bits from the fcntl flags for this fd. Returns the new flags. +export auto clearflags(fd &self, int clrflags) + -> std::expected +{ + auto flags = getflags(self); + if (!flags) + return flags; + + *flags &= ~clrflags; + auto const ret = replaceflags(self, *flags); + if (!ret) + return std::unexpected(ret.error()); + + return {*flags}; +} + +// Return the fd flags for this fd. +export auto getfdflags(fd const &self) -> std::expected +{ + auto const flags = ::fcntl(self.get(), F_GETFD); + if (flags != -1) + return {flags}; + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +// Replace the fd flags for this fd. +export auto replacefdflags(fd &self, int newflags) + -> std::expected +{ + auto const ret = ::fcntl(self.get(), F_SETFD, newflags); + if (ret != -1) + return {}; + + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +// Add bits to the fd flags for this fd. Returns the new flags. +export auto setfdflags(fd &self, int newflags) + -> std::expected +{ + auto flags = getfdflags(self); + if (!flags) + return flags; + + *flags |= newflags; + auto const ret = replacefdflags(self, *flags); + if (!ret) + return std::unexpected(ret.error()); + + return {*flags}; +} + +// Remove bits from the fd flags for this fd. Returns the new flags. +export auto clearfdflags(fd &self, int clrflags) + -> std::expected +{ + auto flags = getfdflags(self); + if (!flags) + return flags; + + *flags &= ~clrflags; + auto ret = replacefdflags(self, *flags); + if (!ret) + return std::unexpected(ret.error()); + + return {*flags}; +} + +// Create two fds by calling pipe() and return them. +export auto pipe() -> std::expected, std::error_code> { + auto fds = std::array{}; + + if (auto const ret = ::pipe(fds.data()); ret != 0) + return std::unexpected(std::make_error_code(std::errc(errno))); + + return {{fd(fds[0]), fd(fds[1])}}; +} + +/* + * Write data to a file descriptor from the provided buffer. Returns the + * number of bytes (not objects) written. Incomplete writes may cause a + * partial object to be written. + */ +export auto write(fd &file, std::ranges::contiguous_range auto &&range) + -> std::expected +{ + auto const ret = ::write(file.get(), std::ranges::data(range), + std::ranges::size(range)); + if (ret >= 0) + return ret; + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +/* + * Read data from a file descriptor into the provided buffer. Returns the + * number of bytes (not objects) read. Incomplete reads may cause a partial + * object to be read. + */ +export auto read(fd &file, std::ranges::contiguous_range auto &&range) + -> std::expected +{ + auto const ret = ::read(file.get(), std::ranges::data(range), + std::ranges::size(range)); + if (ret >= 0) + return ret; + return std::unexpected(std::make_error_code(std::errc(errno))); +} + +} // namespace nihil diff --git a/nihil/generator.ccm b/nihil/generator.ccm new file mode 100644 index 0000000..82bcb27 --- /dev/null +++ b/nihil/generator.ccm @@ -0,0 +1,691 @@ +/////////////////////////////////////////////////////////////////////////////// +// Reference implementation of std::generator proposal P2168. +// +// See https://wg21.link/P2168 for details. +// +/////////////////////////////////////////////////////////////////////////////// +// Copyright Lewis Baker, Corentin Jabot +// +// Use, modification and distribution is subject to the Boost Software License, +// Version 1.0. +// (See accompanying file LICENSE or http://www.boost.org/LICENSE_1_0.txt) +/////////////////////////////////////////////////////////////////////////////// + +module; + +#include +#include +#include +#include +#include +#include + +export module nihil:generator; + +namespace nihil { + +template +class __manual_lifetime { + public: + __manual_lifetime() noexcept {} + ~__manual_lifetime() {} + + template + _T& construct(_Args&&... __args) noexcept(std::is_nothrow_constructible_v<_T, _Args...>) { + return *::new (static_cast(std::addressof(__value_))) _T((_Args&&)__args...); + } + + void destruct() noexcept(std::is_nothrow_destructible_v<_T>) { + __value_.~_T(); + } + + _T& get() & noexcept { + return __value_; + } + _T&& get() && noexcept { + return static_cast<_T&&>(__value_); + } + const _T& get() const & noexcept { + return __value_; + } + const _T&& get() const && noexcept { + return static_cast(__value_); + } + + private: + union { + std::remove_const_t<_T> __value_; + }; +}; + +template +class __manual_lifetime<_T&> { + public: + __manual_lifetime() noexcept : __value_(nullptr) {} + ~__manual_lifetime() {} + + _T& construct(_T& __value) noexcept { + __value_ = std::addressof(__value); + return __value; + } + + void destruct() noexcept {} + + _T& get() const noexcept { + return *__value_; + } + + private: + _T* __value_; +}; + +template +class __manual_lifetime<_T&&> { + public: + __manual_lifetime() noexcept : __value_(nullptr) {} + ~__manual_lifetime() {} + + _T&& construct(_T&& __value) noexcept { + __value_ = std::addressof(__value); + return static_cast<_T&&>(__value); + } + + void destruct() noexcept {} + + _T&& get() const noexcept { + return static_cast<_T&&>(*__value_); + } + + private: + _T* __value_; +}; + +struct use_allocator_arg {}; + +namespace ranges { + +export template +struct elements_of { + explicit constexpr elements_of(_Rng&& __rng) noexcept + requires std::is_default_constructible_v<_Allocator> + : __range(static_cast<_Rng&&>(__rng)) + {} + + constexpr elements_of(_Rng&& __rng, _Allocator&& __alloc) noexcept + : __range((_Rng&&)__rng), __alloc((_Allocator&&)__alloc) {} + + constexpr elements_of(elements_of&&) noexcept = default; + + constexpr elements_of(const elements_of &) = delete; + constexpr elements_of &operator=(const elements_of &) = delete; + constexpr elements_of &operator=(elements_of &&) = delete; + + constexpr _Rng&& get() noexcept { + return static_cast<_Rng&&>(__range); + } + + constexpr _Allocator get_allocator() const noexcept { + return __alloc; + } + +private: + [[no_unique_address]] _Allocator __alloc; // \expos + _Rng && __range; // \expos +}; + +export template +elements_of(_Rng &&) -> elements_of<_Rng>; + +export template +elements_of(_Rng &&, Allocator&&) -> elements_of<_Rng, Allocator>; + +} // namespace ranges + +template +static constexpr bool __allocator_needs_to_be_stored = + !std::allocator_traits<_Alloc>::is_always_equal::value || + !std::is_default_constructible_v<_Alloc>; + +// Round s up to next multiple of a. +constexpr size_t __aligned_allocation_size(size_t s, size_t a) { + return (s + a - 1) & ~(a - 1); +} + + +export template , + typename _Allocator = use_allocator_arg> +class generator; + +template +class __promise_base_alloc { + static constexpr std::size_t __offset_of_allocator(std::size_t __frameSize) noexcept { + return __aligned_allocation_size(__frameSize, alignof(_Alloc)); + } + + static constexpr std::size_t __padded_frame_size(std::size_t __frameSize) noexcept { + return __offset_of_allocator(__frameSize) + sizeof(_Alloc); + } + + static _Alloc& __get_allocator(void* __frame, std::size_t __frameSize) noexcept { + return *reinterpret_cast<_Alloc*>( + static_cast(__frame) + __offset_of_allocator(__frameSize)); + } + +public: + template + static void* operator new(std::size_t __frameSize, std::allocator_arg_t, _Alloc __alloc, _Args&...) { + void* __frame = __alloc.allocate(__padded_frame_size(__frameSize)); + + // Store allocator at end of the coroutine frame. + // Assuming the allocator's move constructor is non-throwing (a requirement for allocators) + ::new (static_cast(std::addressof(__get_allocator(__frame, __frameSize)))) _Alloc(std::move(__alloc)); + + return __frame; + } + + template + static void* operator new(std::size_t __frameSize, _This&, std::allocator_arg_t, _Alloc __alloc, _Args&...) { + return __promise_base_alloc::operator new(__frameSize, std::allocator_arg, std::move(__alloc)); + } + + static void operator delete(void* __ptr, std::size_t __frameSize) noexcept { + _Alloc& __alloc = __get_allocator(__ptr, __frameSize); + _Alloc __localAlloc(std::move(__alloc)); + __alloc.~Alloc(); + __localAlloc.deallocate(static_cast(__ptr), __padded_frame_size(__frameSize)); + } +}; + +template + requires (!__allocator_needs_to_be_stored<_Alloc>) +class __promise_base_alloc<_Alloc> { +public: + static void* operator new(std::size_t __size) { + _Alloc __alloc; + return __alloc.allocate(__size); + } + + static void operator delete(void* __ptr, std::size_t __size) noexcept { + _Alloc __alloc; + __alloc.deallocate(static_cast(__ptr), __size); + } +}; + +template +struct __generator_promise_base +{ + template + friend class generator; + + __generator_promise_base* __root_; + std::coroutine_handle<> __parentOrLeaf_; + // Note: Using manual_lifetime here to avoid extra calls to exception_ptr + // constructor/destructor in cases where it is not needed (i.e. where this + // generator coroutine is not used as a nested coroutine). + // This member is lazily constructed by the __yield_sequence_awaiter::await_suspend() + // method if this generator is used as a nested generator. + __manual_lifetime __exception_; + __manual_lifetime<_Ref> __value_; + + explicit __generator_promise_base(std::coroutine_handle<> thisCoro) noexcept + : __root_(this) + , __parentOrLeaf_(thisCoro) + {} + + ~__generator_promise_base() { + if (__root_ != this) { + // This coroutine was used as a nested generator and so will + // have constructed its __exception_ member which needs to be + // destroyed here. + __exception_.destruct(); + } + } + + std::suspend_always initial_suspend() noexcept { + return {}; + } + + void return_void() noexcept {} + + void unhandled_exception() { + if (__root_ != this) { + __exception_.get() = std::current_exception(); + } else { + throw; + } + } + + // Transfers control back to the parent of a nested coroutine + struct __final_awaiter { + bool await_ready() noexcept { + return false; + } + + template + std::coroutine_handle<> + await_suspend(std::coroutine_handle<_Promise> __h) noexcept { + _Promise& __promise = __h.promise(); + __generator_promise_base& __root = *__promise.__root_; + if (&__root != &__promise) { + auto __parent = __promise.__parentOrLeaf_; + __root.__parentOrLeaf_ = __parent; + return __parent; + } + return std::noop_coroutine(); + } + + void await_resume() noexcept {} + }; + + __final_awaiter final_suspend() noexcept { + return {}; + } + + std::suspend_always yield_value(_Ref&& __x) + noexcept(std::is_nothrow_move_constructible_v<_Ref>) { + __root_->__value_.construct((_Ref&&)__x); + return {}; + } + + template + requires + (!std::is_reference_v<_Ref>) && + std::is_convertible_v<_T, _Ref> + std::suspend_always yield_value(_T&& __x) + noexcept(std::is_nothrow_constructible_v<_Ref, _T>) { + __root_->__value_.construct((_T&&)__x); + return {}; + } + + template + struct __yield_sequence_awaiter { + _Gen __gen_; + + __yield_sequence_awaiter(_Gen&& __g) noexcept + // Taking ownership of the generator ensures frame are destroyed + // in the reverse order of their execution. + : __gen_((_Gen&&)__g) { + } + + bool await_ready() noexcept { + return false; + } + + // set the parent, root and exceptions pointer and + // resume the nested + template + std::coroutine_handle<> + await_suspend(std::coroutine_handle<_Promise> __h) noexcept { + __generator_promise_base& __current = __h.promise(); + __generator_promise_base& __nested = *__gen_.__get_promise(); + __generator_promise_base& __root = *__current.__root_; + + __nested.__root_ = __current.__root_; + __nested.__parentOrLeaf_ = __h; + + // Lazily construct the __exception_ member here now that we + // know it will be used as a nested generator. This will be + // destroyed by the promise destructor. + __nested.__exception_.construct(); + __root.__parentOrLeaf_ = __gen_.__get_coro(); + + // Immediately resume the nested coroutine (nested generator) + return __gen_.__get_coro(); + } + + void await_resume() { + __generator_promise_base& __nestedPromise = *__gen_.__get_promise(); + if (__nestedPromise.__exception_.get()) { + std::rethrow_exception(std::move(__nestedPromise.__exception_.get())); + } + } + }; + + template + __yield_sequence_awaiter> + yield_value(nihil::ranges::elements_of> __g) noexcept { + return std::move(__g).get(); + } + + template + __yield_sequence_awaiter, _Allocator>> + yield_value(nihil::ranges::elements_of<_Rng, _Allocator> && __x) { + return [](std::allocator_arg_t, _Allocator, auto && __rng) -> generator<_Ref, std::remove_cvref_t<_Ref>, _Allocator> { + for(auto && e: __rng) + co_yield static_cast(e); + }(std::allocator_arg, __x.get_allocator(), std::forward<_Rng>(__x.get())); + } + + void resume() { + __parentOrLeaf_.resume(); + } + + // Disable use of co_await within this coroutine. + void await_transform() = delete; +}; + +template +struct __generator_promise; + +template +struct __generator_promise, _ByteAllocator, _ExplicitAllocator> final + : public __generator_promise_base<_Ref> + , public __promise_base_alloc<_ByteAllocator> { + __generator_promise() noexcept + : __generator_promise_base<_Ref>(std::coroutine_handle<__generator_promise>::from_promise(*this)) + {} + + generator<_Ref, _Value, _Alloc> get_return_object() noexcept { + return generator<_Ref, _Value, _Alloc>{ + std::coroutine_handle<__generator_promise>::from_promise(*this) + }; + } + + using __generator_promise_base<_Ref>::yield_value; + + template + typename __generator_promise_base<_Ref>::template __yield_sequence_awaiter> + yield_value(nihil::ranges::elements_of<_Rng> && __x) { + static_assert (!_ExplicitAllocator, + "This coroutine has an explicit allocator specified with std::allocator_arg so an allocator needs to be passed " + "explicitely to std::elements_of"); + return [](auto && __rng) -> generator<_Ref, _Value, _Alloc> { + for(auto && e: __rng) + co_yield static_cast(e); + }(std::forward<_Rng>(__x.get())); + } +}; + +template +using __byte_allocator_t = typename std::allocator_traits>::template rebind_alloc; + +} // namespace nihil + +namespace std { + +// Type-erased allocator with default allocator behaviour. +export template +struct coroutine_traits, _Args...> { + using promise_type = nihil::__generator_promise, std::allocator>; +}; + +// Type-erased allocator with std::allocator_arg parameter +export template +struct coroutine_traits, allocator_arg_t, _Alloc, _Args...> { +private: + using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; +public: + using promise_type = nihil::__generator_promise, __byte_allocator, true /*explicit Allocator*/>; +}; + +// Type-erased allocator with std::allocator_arg parameter (non-static member functions) +export template +struct coroutine_traits, _This, allocator_arg_t, _Alloc, _Args...> { +private: + using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; +public: + using promise_type = nihil::__generator_promise, __byte_allocator, true /*explicit Allocator*/>; +}; + +// Generator with specified allocator type +export template +struct coroutine_traits, _Args...> { + using __byte_allocator = nihil::__byte_allocator_t<_Alloc>; +public: + using promise_type = nihil::__generator_promise, __byte_allocator>; +}; + +} // namespace std + +namespace nihil { + +// TODO : make layout compatible promise casts possible +export template +class generator { + using __byte_allocator = __byte_allocator_t<_Alloc>; +public: + using promise_type = __generator_promise, __byte_allocator>; + friend promise_type; +private: + using __coroutine_handle = std::coroutine_handle; +public: + + generator() noexcept = default; + + generator(generator&& __other) noexcept + : __coro_(std::exchange(__other.__coro_, {})) + , __started_(std::exchange(__other.__started_, false)) { + } + + ~generator() noexcept { + if (__coro_) { + if (__started_ && !__coro_.done()) { + __coro_.promise().__value_.destruct(); + } + __coro_.destroy(); + } + } + + generator& operator=(generator && g) noexcept { + swap(g); + return *this; + } + + void swap(generator& __other) noexcept { + std::swap(__coro_, __other.__coro_); + std::swap(__started_, __other.__started_); + } + + struct sentinel {}; + + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = _Value; + using reference = _Ref; + using pointer = std::add_pointer_t<_Ref>; + + iterator() noexcept = default; + iterator(const iterator &) = delete; + + iterator(iterator&& __other) noexcept + : __coro_(std::exchange(__other.__coro_, {})) { + } + + iterator& operator=(iterator&& __other) { + std::swap(__coro_, __other.__coro_); + return *this; + } + + ~iterator() { + } + + friend bool operator==(const iterator &it, sentinel) noexcept { + return it.__coro_.done(); + } + + iterator &operator++() { + __coro_.promise().__value_.destruct(); + __coro_.promise().resume(); + return *this; + } + void operator++(int) { + (void)operator++(); + } + + reference operator*() const noexcept { + return static_cast(__coro_.promise().__value_.get()); + } + + private: + friend generator; + + explicit iterator(__coroutine_handle __coro) noexcept + : __coro_(__coro) {} + + __coroutine_handle __coro_; + }; + + iterator begin() { + assert(__coro_); + assert(!__started_); + __started_ = true; + __coro_.resume(); + return iterator{__coro_}; + } + + sentinel end() noexcept { + return {}; + } + +private: + explicit generator(__coroutine_handle __coro) noexcept + : __coro_(__coro) { + } + +public: // to get around access restrictions for __yield_sequence_awaitable + std::coroutine_handle<> __get_coro() noexcept { return __coro_; } + promise_type* __get_promise() noexcept { return std::addressof(__coro_.promise()); } + +private: + __coroutine_handle __coro_; + bool __started_ = false; +}; + +// Specialisation for type-erased allocator implementation. +export template +class generator<_Ref, _Value, use_allocator_arg> { + using __promise_base = __generator_promise_base<_Ref>; +public: + + generator() noexcept + : __promise_(nullptr) + , __coro_() + , __started_(false) + {} + + generator(generator&& __other) noexcept + : __promise_(std::exchange(__other.__promise_, nullptr)) + , __coro_(std::exchange(__other.__coro_, {})) + , __started_(std::exchange(__other.__started_, false)) { + } + + ~generator() noexcept { + if (__coro_) { + if (__started_ && !__coro_.done()) { + __promise_->__value_.destruct(); + } + __coro_.destroy(); + } + } + + generator& operator=(generator g) noexcept { + swap(g); + return *this; + } + + void swap(generator& __other) noexcept { + std::swap(__promise_, __other.__promise_); + std::swap(__coro_, __other.__coro_); + std::swap(__started_, __other.__started_); + } + + struct sentinel {}; + + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = _Value; + using reference = _Ref; + using pointer = std::add_pointer_t<_Ref>; + + iterator() noexcept = default; + iterator(const iterator &) = delete; + + iterator(iterator&& __other) noexcept + : __promise_(std::exchange(__other.__promise_, nullptr)) + , __coro_(std::exchange(__other.__coro_, {})) + {} + + iterator& operator=(iterator&& __other) { + __promise_ = std::exchange(__other.__promise_, nullptr); + __coro_ = std::exchange(__other.__coro_, {}); + return *this; + } + + ~iterator() = default; + + friend bool operator==(const iterator &it, sentinel) noexcept { + return it.__coro_.done(); + } + + iterator& operator++() { + __promise_->__value_.destruct(); + __promise_->resume(); + return *this; + } + + void operator++(int) { + (void)operator++(); + } + + reference operator*() const noexcept { + return static_cast(__promise_->__value_.get()); + } + + private: + friend generator; + + explicit iterator(__promise_base* __promise, std::coroutine_handle<> __coro) noexcept + : __promise_(__promise) + , __coro_(__coro) + {} + + __promise_base* __promise_; + std::coroutine_handle<> __coro_; + }; + + iterator begin() { + assert(__coro_); + assert(!__started_); + __started_ = true; + __coro_.resume(); + return iterator{__promise_, __coro_}; + } + + sentinel end() noexcept { + return {}; + } + +private: + template + friend struct __generator_promise; + + template + explicit generator(std::coroutine_handle<_Promise> __coro) noexcept + : __promise_(std::addressof(__coro.promise())) + , __coro_(__coro) + {} + +public: // to get around access restrictions for __yield_sequence_awaitable + std::coroutine_handle<> __get_coro() noexcept { return __coro_; } + __promise_base* __get_promise() noexcept { return __promise_; } + +private: + __promise_base* __promise_; + std::coroutine_handle<> __coro_; + bool __started_ = false; +}; + +} // namespace nihil + +export namespace std::ranges { + +template +constexpr inline bool enable_view> = true; + +} // namespace std::ranges + diff --git a/nihil/generic_error.ccm b/nihil/generic_error.ccm new file mode 100644 index 0000000..a582519 --- /dev/null +++ b/nihil/generic_error.ccm @@ -0,0 +1,26 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +export module nihil:generic_error; + +namespace nihil { + +/* + * generic_error is the base class that all other exceptions derive from. + * It is an std::runtime_error, and what() should always be informative. + */ + +export struct generic_error : std::runtime_error { + template + generic_error(std::format_string fmt, Args &&...args) + : std::runtime_error(std::format(fmt, std::forward(args)...)) + {} +}; + +} // namespace nihil diff --git a/nihil/getenv.ccm b/nihil/getenv.ccm new file mode 100644 index 0000000..7397b79 --- /dev/null +++ b/nihil/getenv.ccm @@ -0,0 +1,47 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +#include + +export module nihil:getenv; + +namespace nihil { + +/* + * Find a variable by the given name in the environment by calling getenv_r(). + */ + +export auto getenv(std::string_view varname) + -> std::expected +{ + // Start with a buffer of this size, and double it every iteration. + constexpr auto bufinc = std::size_t{1024}; + + auto cvarname = std::string(varname); + auto buf = std::vector(bufinc); + for (;;) { + auto const ret = ::getenv_r(cvarname.c_str(), + buf.data(), buf.size()); + + if (ret == 0) + return {std::string(buf.data())}; + + if (ret == -1 && errno == ERANGE) { + buf.resize(buf.size() * 2); + continue; + } + + return std::unexpected(std::make_error_code(std::errc(errno))); + } +} + +} // namespace nihil diff --git a/nihil/guard.ccm b/nihil/guard.ccm new file mode 100644 index 0000000..18c6d70 --- /dev/null +++ b/nihil/guard.ccm @@ -0,0 +1,50 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include + +export module nihil:guard; + +namespace nihil { + +/* + * guard: invoke a callable when this object is destroyed; this is similar to + * scope_exit from the library fundamentals TS, which LLVM doesn't implement. + */ +export template +struct guard final { + // Initialise the guard with a callable we will invoke later. + guard(F func) : _func(std::move(func)) {} + + /* + * We are being destroyed, so call the callable. + * If the callable throws, std::terminate() will be called. + */ + ~guard() { + if (_func) + std::invoke(*_func); + } + + // Release the guard. This turns the destructor into a no-op. + void release() noexcept { + _func.reset(); + } + + // Not default-constructible or copyable. + guard() = delete; + guard(guard const &) = delete; + guard(guard &&) noexcept = delete; + guard &operator=(guard const &) = delete; + guard &operator=(guard &&) noexcept = delete; + +private: + // The callable to be invoked when we are destroyed. + std::optional _func; +}; + +} // namespace nihil diff --git a/nihil/nihil.ccm b/nihil/nihil.ccm new file mode 100644 index 0000000..69cc282 --- /dev/null +++ b/nihil/nihil.ccm @@ -0,0 +1,15 @@ +/* + * This source code is released into the public domain. + */ + +module; + +export module nihil; + +export import :ctype; +export import :generator; +export import :generic_error; +export import :getenv; +export import :guard; +export import :fd; +export import :tabulate; diff --git a/nihil/tabulate.ccm b/nihil/tabulate.ccm new file mode 100644 index 0000000..debb784 --- /dev/null +++ b/nihil/tabulate.ccm @@ -0,0 +1,282 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +export module nihil:tabulate; + +import :ctype; +import :generic_error; + +namespace nihil { + +/* + * tabulate: format the given range in an ASCII table and write the output + * to the given output iterator. The range's values will be converted to + * strings as if by std::format. + * + * tabulate is implemented by copying the range; this allows it to work on + * input/forward ranges at the cost of slightly increased memory use. + * + * The table spec is a string consisting of zero or more field formats, + * formatted as {flags:fieldname}; both flags and fieldname are optional. + * If there are fewer field formats than fields, the remaining fields + * are formatted as if by {:}. + * + * The following flags are supported: + * + * < left-align this column (default) + * > right-align this column + */ + +// Exception thrown when a table spec is invalid. +export struct table_spec_error : generic_error { + template + table_spec_error(std::format_string fmt, Args &&...args) + : generic_error(fmt, std::forward(args)...) + {} +}; + +/* + * The specification for a single field. + */ +template +struct field_spec { + std::basic_string_view name; + std::size_t width = 0; + enum { left, right } align = left; + + // Ensure the length of this field is at least the given width. + auto ensure_width(std::size_t newwidth) -> void + { + width = std::max(width, newwidth); + } + + // Format an object to a string based on our field spec. + auto format(auto &&obj) const -> std::basic_string + { + std::basic_string format_string{'{', '}'}; + return std::format(std::runtime_format(format_string), obj); + } + + // Print a column value to an output iterator according to our field + // spec. If is_last is true, this is the last field on the line, so + // we won't output any trailling padding. + auto print(std::basic_string_view value, + std::output_iterator auto &out, + bool is_last) + const + { + auto padding = width - value.size(); + + if (align == right) + for (std::size_t i = 0; i < padding; ++i) + *out++ = ' '; + + std::ranges::copy(value, out); + + if (!is_last && align == left) + for (std::size_t i = 0; i < padding; ++i) + *out++ = ' '; + } +}; + +/* + * The specification for an entire table. + */ +template +struct table_spec { + // Add a new field spec to this table. + auto add(field_spec field) + { + _fields.emplace_back(std::move(field)); + } + + // Return the field spec for a given field. If the field doesn't + // exist, this field and any intermediate fields will be created. + auto field(std::size_t fieldnr) -> field_spec& + { + if (_fields.size() < fieldnr + 1) + _fields.resize(fieldnr + 1); + return _fields.at(fieldnr); + } + + // The number of columns in this table. + auto columns() const -> std::size_t + { + return _fields.size(); + } + + // Return all the fields in this table. + auto fields() const + { + return _fields; + } + +private: + std::vector> _fields; +}; + +// Parse the field flags, e.g. '<'. +template Sentinel> +auto parse_field_flags(field_spec &field, Iterator &pos, Sentinel end) + -> void +{ + while (pos < end) { + switch (*pos) { + case '<': + field.align = field_spec::left; + break; + case '>': + field.align = field_spec::right; + break; + case ':': + ++pos; + /*FALLTHROUGH*/ + case '}': + return; + default: + throw table_spec_error( + "Invalid table spec: unknown flag character"); + } + + if (++pos == end) + throw table_spec_error("Invalid table spec: " + "unterminated field"); + } +} + +// Parse a complete field spec, e.g. "{<:NAME}". +template Sentinel> +auto parse_field(Iterator &pos, Sentinel end) + -> field_spec +{ + auto field = field_spec{}; + + if (pos == end) + throw table_spec_error("Invalid table spec: empty field"); + + // The field spec should start with a '{'. + if (*pos != '{') + throw table_spec_error("Invalid table spec: expected '{{'"); + + if (++pos == end) + throw table_spec_error("Invalid table spec: unterminated field"); + + // This consumes 'pos' up to and including the ':'. + parse_field_flags(field, pos, end); + + auto brace = std::ranges::find(pos, end, '}'); + if (brace == end) + throw table_spec_error("Invalid table spec: expected '}}'"); + + field.name = std::basic_string_view(pos, brace); + pos = std::next(brace); + + // The field must be at least as wide as its header. + field.width = field.name.size(); + + return field; +} + +template +auto parse_table_spec(std::basic_string_view spec) -> table_spec +{ + auto table = table_spec(); + + auto pos = std::ranges::begin(spec); + auto end = std::ranges::end(spec); + + for (;;) { + // Skip leading whitespace + while (pos < end && is_c_space(*pos)) + ++pos; + + if (pos == end) + break; + + table.add(parse_field(pos, end)); + } + + return table; +} + +export template Iterator> +auto basic_tabulate(std::basic_string_view table_spec, + Range &&range, + Iterator &&out) + -> void +{ + // Parse the table spec. + auto table = parse_table_spec(table_spec); + + // Create our copy of the input data. + auto data = std::vector>>(); + // Reserve the first row for the header. + data.resize(1); + + // Find the required length of each field. + for (auto &&row : range) { + // LLVM doesn't have std::enumerate_view yet + auto i = std::size_t{0}; + auto &this_row = data.emplace_back(); + + for (auto &&column : row) { + auto &field = table.field(i); + auto &str = this_row.emplace_back(field.format(column)); + field.ensure_width(str.size()); + ++i; + } + } + + // Add the header row. + for (auto &&field : table.fields()) + data.at(0).emplace_back(std::from_range, field.name); + + // Print the values. + for (auto &&row : data) { + for (std::size_t i = 0; i < row.size(); ++i) { + auto &field = table.field(i); + bool is_last = (i == row.size() - 1); + + field.print(row[i], out, is_last); + + if (!is_last) + *out++ = ' '; + } + + *out++ = '\n'; + } +} + +export auto tabulate(std::string_view table_spec, + std::ranges::range auto &&range, + std::output_iterator auto &&out) +{ + return basic_tabulate(table_spec, + std::forward(range), + std::forward(out)); +} + +export auto wtabulate(std::wstring_view table_spec, + std::ranges::range auto &&range, + std::output_iterator auto &&out) +{ + return basic_tabulate(table_spec, + std::forward(range), + std::forward(out)); +} + +} // namespace nihil -- cgit v1.2.3