aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-06-21 12:20:34 +0100
committerLexi Winter <lexi@le-fay.org>2025-06-21 12:20:34 +0100
commit8a36eb498e1a1c2cf2e886356faa4ce67e52e874 (patch)
tree92e44b4d4ddef68ff91d35f44ca57a9d45e7f879
downloadnihil-8a36eb498e1a1c2cf2e886356faa4ce67e52e874.tar.gz
nihil-8a36eb498e1a1c2cf2e886356faa4ce67e52e874.tar.bz2
initial commit
-rw-r--r--.gitignore4
-rw-r--r--CMakeLists.txt18
-rw-r--r--README26
-rw-r--r--modules/CMakeLists.txt13
-rw-r--r--modules/ctype.ccm86
-rw-r--r--modules/fd.ccm309
-rw-r--r--modules/generator.ccm691
-rw-r--r--modules/generic_error.ccm26
-rw-r--r--modules/getenv.ccm47
-rw-r--r--modules/guard.ccm50
-rw-r--r--modules/nihil.ccm15
-rw-r--r--modules/tabulate.ccm282
-rw-r--r--tests/CMakeLists.txt21
-rw-r--r--tests/ctype.cc373
-rw-r--r--tests/fd.cc200
-rw-r--r--tests/generator.cc56
-rw-r--r--tests/generic_error.cc17
-rw-r--r--tests/getenv.cc48
-rw-r--r--tests/guard.cc20
-rw-r--r--tests/tabulate.cc75
20 files changed, 2377 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4cb47c2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/build
+/dist
+*.sw?
+*.core
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..7c12885
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,18 @@
+# This source code is released into the public domain.
+
+cmake_minimum_required(VERSION 3.28)
+
+project(libexi)
+
+set(CMAKE_CXX_STANDARD 26)
+
+add_compile_options(-W)
+add_compile_options(-Wall)
+add_compile_options(-Wextra)
+add_compile_options(-Werror)
+add_compile_options(-Wpedantic)
+
+add_subdirectory(modules)
+add_subdirectory(tests)
+
+enable_testing()
diff --git a/README b/README
new file mode 100644
index 0000000..5d4d2ab
--- /dev/null
+++ b/README
@@ -0,0 +1,26 @@
+nihil: C++ utility library
+==========================
+
+nihil is a C++ library which provides various utilities that might be useful
+in a C++ program. many of the utilities are specific to FreeBSD.
+
+i wrote this primarily for my own programs, but you're welcome to use it too.
+
+license
+-------
+
+all of nihil is in the public domain, with the exception of:
+
+- modules/generator.ccm
+
+requirements
+------------
+
++ FreeBSD
++ a modern C++ compiler. nihil is tested using LLVM 19.x with -std=c++26.
+
+usage
+-----
+
+nihil is intended to be consumed as a CMake subdirectory. better install
+options might be added later.
diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt
new file mode 100644
index 0000000..383dd0a
--- /dev/null
+++ b/modules/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/modules/ctype.ccm b/modules/ctype.ccm
new file mode 100644
index 0000000..cc058cd
--- /dev/null
+++ b/modules/ctype.ccm
@@ -0,0 +1,86 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <concepts>
+#include <locale>
+
+export module nihil:ctype;
+
+namespace nihil {
+
+/*
+ * ctype_is: wrap std::ctype<T>::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<decltype(c)>;
+ auto &facet = std::use_facet<ctype>(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
new file mode 100644
index 0000000..ad96ea7
--- /dev/null
+++ b/modules/fd.ccm
@@ -0,0 +1,309 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include <expected>
+#include <format>
+#include <stdexcept>
+#include <system_error>
+
+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<void, std::error_code>
+ {
+ 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<fd, std::error_code>
+{
+ 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<fd, std::error_code>
+{
+ 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<int, std::error_code>
+{
+ 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<int, std::error_code>
+{
+ 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<int, std::error_code>
+{
+ 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<void, std::error_code>
+{
+ 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<int, std::error_code>
+{
+ 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<int, std::error_code>
+{
+ 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<int, std::error_code>
+{
+ 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<void, std::error_code>
+{
+ 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<int, std::error_code>
+{
+ 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<int, std::error_code>
+{
+ 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::pair<fd, fd>, std::error_code> {
+ auto fds = std::array<int, 2>{};
+
+ 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<std::size_t, std::error_code>
+{
+ 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<std::size_t, std::error_code>
+{
+ 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
new file mode 100644
index 0000000..82bcb27
--- /dev/null
+++ b/modules/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 <cassert>
+#include <coroutine>
+#include <exception>
+#include <memory>
+#include <ranges>
+#include <type_traits>
+
+export module nihil:generator;
+
+namespace nihil {
+
+template <typename _T>
+class __manual_lifetime {
+ public:
+ __manual_lifetime() noexcept {}
+ ~__manual_lifetime() {}
+
+ template <typename... _Args>
+ _T& construct(_Args&&... __args) noexcept(std::is_nothrow_constructible_v<_T, _Args...>) {
+ return *::new (static_cast<void*>(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<const _T&&>(__value_);
+ }
+
+ private:
+ union {
+ std::remove_const_t<_T> __value_;
+ };
+};
+
+template <typename _T>
+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 <typename _T>
+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 <typename _Rng, typename _Allocator = use_allocator_arg>
+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 <typename _Rng>
+elements_of(_Rng &&) -> elements_of<_Rng>;
+
+export template <typename _Rng, typename Allocator>
+elements_of(_Rng &&, Allocator&&) -> elements_of<_Rng, Allocator>;
+
+} // namespace ranges
+
+template <typename _Alloc>
+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 _Ref,
+ typename _Value = std::remove_cvref_t<_Ref>,
+ typename _Allocator = use_allocator_arg>
+class generator;
+
+template<typename _Alloc>
+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<char*>(__frame) + __offset_of_allocator(__frameSize));
+ }
+
+public:
+ template<typename... _Args>
+ 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<void*>(std::addressof(__get_allocator(__frame, __frameSize)))) _Alloc(std::move(__alloc));
+
+ return __frame;
+ }
+
+ template<typename _This, typename... _Args>
+ 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<std::byte*>(__ptr), __padded_frame_size(__frameSize));
+ }
+};
+
+template<typename _Alloc>
+ 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<std::byte*>(__ptr), __size);
+ }
+};
+
+template<typename _Ref>
+struct __generator_promise_base
+{
+ template <typename _Ref2, typename _Value, typename _Alloc>
+ 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<std::exception_ptr> __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 <typename _Promise>
+ 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 <typename _T>
+ 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 <typename _Gen>
+ 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<typename _Promise>
+ 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 <typename _OValue, typename _OAlloc>
+ __yield_sequence_awaiter<generator<_Ref, _OValue, _OAlloc>>
+ yield_value(nihil::ranges::elements_of<generator<_Ref, _OValue, _OAlloc>> __g) noexcept {
+ return std::move(__g).get();
+ }
+
+ template <std::ranges::range _Rng, typename _Allocator>
+ __yield_sequence_awaiter<generator<_Ref, std::remove_cvref_t<_Ref>, _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<decltype(e)>(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<typename _Generator, typename _ByteAllocator, bool _ExplicitAllocator = false>
+struct __generator_promise;
+
+template<typename _Ref, typename _Value, typename _Alloc, typename _ByteAllocator, bool _ExplicitAllocator>
+struct __generator_promise<generator<_Ref, _Value, _Alloc>, _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 <std::ranges::range _Rng>
+ typename __generator_promise_base<_Ref>::template __yield_sequence_awaiter<generator<_Ref, _Value, _Alloc>>
+ 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<decltype(e)>(e);
+ }(std::forward<_Rng>(__x.get()));
+ }
+};
+
+template<typename _Alloc>
+using __byte_allocator_t = typename std::allocator_traits<std::remove_cvref_t<_Alloc>>::template rebind_alloc<std::byte>;
+
+} // namespace nihil
+
+namespace std {
+
+// Type-erased allocator with default allocator behaviour.
+export template<typename _Ref, typename _Value, typename... _Args>
+struct coroutine_traits<nihil::generator<_Ref, _Value>, _Args...> {
+ using promise_type = nihil::__generator_promise<nihil::generator<_Ref, _Value>, std::allocator<std::byte>>;
+};
+
+// Type-erased allocator with std::allocator_arg parameter
+export template<typename _Ref, typename _Value, typename _Alloc, typename... _Args>
+struct coroutine_traits<nihil::generator<_Ref, _Value>, allocator_arg_t, _Alloc, _Args...> {
+private:
+ using __byte_allocator = nihil::__byte_allocator_t<_Alloc>;
+public:
+ using promise_type = nihil::__generator_promise<nihil::generator<_Ref, _Value>, __byte_allocator, true /*explicit Allocator*/>;
+};
+
+// Type-erased allocator with std::allocator_arg parameter (non-static member functions)
+export template<typename _Ref, typename _Value, typename _This, typename _Alloc, typename... _Args>
+struct coroutine_traits<nihil::generator<_Ref, _Value>, _This, allocator_arg_t, _Alloc, _Args...> {
+private:
+ using __byte_allocator = nihil::__byte_allocator_t<_Alloc>;
+public:
+ using promise_type = nihil::__generator_promise<nihil::generator<_Ref, _Value>, __byte_allocator, true /*explicit Allocator*/>;
+};
+
+// Generator with specified allocator type
+export template<typename _Ref, typename _Value, typename _Alloc, typename... _Args>
+struct coroutine_traits<nihil::generator<_Ref, _Value, _Alloc>, _Args...> {
+ using __byte_allocator = nihil::__byte_allocator_t<_Alloc>;
+public:
+ using promise_type = nihil::__generator_promise<nihil::generator<_Ref, _Value, _Alloc>, __byte_allocator>;
+};
+
+} // namespace std
+
+namespace nihil {
+
+// TODO : make layout compatible promise casts possible
+export template <typename _Ref, typename _Value, typename _Alloc>
+class generator {
+ using __byte_allocator = __byte_allocator_t<_Alloc>;
+public:
+ using promise_type = __generator_promise<generator<_Ref, _Value, _Alloc>, __byte_allocator>;
+ friend promise_type;
+private:
+ using __coroutine_handle = std::coroutine_handle<promise_type>;
+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<reference>(__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 <typename _Ref, typename _Value>
+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<reference>(__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<typename _Generator, typename _ByteAllocator, bool _ExplicitAllocator>
+ friend struct __generator_promise;
+
+ template<typename _Promise>
+ 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 <typename _T, typename _U, typename _Alloc>
+constexpr inline bool enable_view<nihil::generator<_T, _U, _Alloc>> = true;
+
+} // namespace std::ranges
+
diff --git a/modules/generic_error.ccm b/modules/generic_error.ccm
new file mode 100644
index 0000000..a582519
--- /dev/null
+++ b/modules/generic_error.ccm
@@ -0,0 +1,26 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <format>
+#include <stdexcept>
+
+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<typename... Args>
+ generic_error(std::format_string<Args...> fmt, Args &&...args)
+ : std::runtime_error(std::format(fmt, std::forward<Args>(args)...))
+ {}
+};
+
+} // namespace nihil
diff --git a/modules/getenv.ccm b/modules/getenv.ccm
new file mode 100644
index 0000000..7397b79
--- /dev/null
+++ b/modules/getenv.ccm
@@ -0,0 +1,47 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <cstdint>
+#include <expected>
+#include <string>
+#include <system_error>
+#include <vector>
+
+#include <unistd.h>
+
+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<std::string, std::error_code>
+{
+ // 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<char>(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
new file mode 100644
index 0000000..18c6d70
--- /dev/null
+++ b/modules/guard.ccm
@@ -0,0 +1,50 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <concepts>
+#include <optional>
+#include <utility>
+
+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<std::invocable F>
+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<F> _func;
+};
+
+} // namespace nihil
diff --git a/modules/nihil.ccm b/modules/nihil.ccm
new file mode 100644
index 0000000..69cc282
--- /dev/null
+++ b/modules/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/modules/tabulate.ccm b/modules/tabulate.ccm
new file mode 100644
index 0000000..debb784
--- /dev/null
+++ b/modules/tabulate.ccm
@@ -0,0 +1,282 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+module;
+
+#include <algorithm>
+#include <cstdlib>
+#include <format>
+#include <ranges>
+#include <iterator>
+#include <vector>
+
+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<typename... Args>
+ table_spec_error(std::format_string<Args...> fmt, Args &&...args)
+ : generic_error(fmt, std::forward<Args>(args)...)
+ {}
+};
+
+/*
+ * The specification for a single field.
+ */
+template<typename Char>
+struct field_spec {
+ std::basic_string_view<Char> 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<Char>
+ {
+ std::basic_string<Char> 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<Char> value,
+ std::output_iterator<Char> 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<typename Char>
+struct table_spec {
+ // Add a new field spec to this table.
+ auto add(field_spec<Char> 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<Char>&
+ {
+ 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<field_spec<Char>> _fields;
+};
+
+// Parse the field flags, e.g. '<'.
+template<typename Char,
+ std::input_iterator Iterator, std::sentinel_for<Iterator> Sentinel>
+auto parse_field_flags(field_spec<Char> &field, Iterator &pos, Sentinel end)
+ -> void
+{
+ while (pos < end) {
+ switch (*pos) {
+ case '<':
+ field.align = field_spec<Char>::left;
+ break;
+ case '>':
+ field.align = field_spec<Char>::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<typename Char,
+ std::input_iterator Iterator, std::sentinel_for<Iterator> Sentinel>
+auto parse_field(Iterator &pos, Sentinel end)
+ -> field_spec<Char>
+{
+ auto field = field_spec<Char>{};
+
+ 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<Char>(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<typename Char>
+auto parse_table_spec(std::basic_string_view<Char> spec) -> table_spec<Char>
+{
+ auto table = table_spec<Char>();
+
+ 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<Char>(pos, end));
+ }
+
+ return table;
+}
+
+export template<typename Char,
+ std::ranges::range Range,
+ std::output_iterator<Char> Iterator>
+auto basic_tabulate(std::basic_string_view<Char> 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<std::vector<std::basic_string<Char>>>();
+ // 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<char> auto &&out)
+{
+ return basic_tabulate<char>(table_spec,
+ std::forward<decltype(range)>(range),
+ std::forward<decltype(out)>(out));
+}
+
+export auto wtabulate(std::wstring_view table_spec,
+ std::ranges::range auto &&range,
+ std::output_iterator<wchar_t> auto &&out)
+{
+ return basic_tabulate<wchar_t>(table_spec,
+ std::forward<decltype(range)>(range),
+ std::forward<decltype(out)>(out));
+}
+
+} // namespace nihil
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 0000000..c6788a3
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,21 @@
+# This source code is released into the public domain.
+
+add_executable(nihil.test
+ ctype.cc
+ fd.cc
+ generator.cc
+ generic_error.cc
+ getenv.cc
+ guard.cc
+ tabulate.cc)
+
+target_link_libraries(nihil.test PRIVATE
+ nihil
+ Catch2::Catch2WithMain
+)
+
+find_package(Catch2 REQUIRED)
+
+include(CTest)
+include(Catch)
+catch_discover_tests(nihil.test)
diff --git a/tests/ctype.cc b/tests/ctype.cc
new file mode 100644
index 0000000..87f5103
--- /dev/null
+++ b/tests/ctype.cc
@@ -0,0 +1,373 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("ctype: space", "[ctype]") {
+ auto is_utf8_space =
+ nihil::ctype_is(std::ctype_base::space,
+ std::locale("C.UTF-8"));
+
+ // '\v' (vertical tab) is a space
+ REQUIRE(nihil::is_space('\v') == true);
+ REQUIRE(nihil::is_space(L'\v') == true);
+
+ REQUIRE(nihil::is_c_space('\v') == true);
+ REQUIRE(nihil::is_c_space(L'\v') == true);
+
+ REQUIRE(is_utf8_space('\v') == true);
+ REQUIRE(is_utf8_space(L'\v') == true);
+
+ // 'x' is not a space
+ REQUIRE(nihil::is_space('x') == false);
+ REQUIRE(nihil::is_space(L'x') == false);
+
+ REQUIRE(nihil::is_c_space('x') == false);
+ REQUIRE(nihil::is_c_space(L'x') == false);
+
+ REQUIRE(is_utf8_space('x') == false);
+ REQUIRE(is_utf8_space(L'x') == false);
+
+ // U+2003 EM SPACE is a space
+ REQUIRE(nihil::is_space(L'\u2003') == false);
+ REQUIRE(nihil::is_c_space(L'\u2003') == false);
+ REQUIRE(is_utf8_space(L'\u2003') == true);
+}
+
+TEST_CASE("ctype: print", "[ctype]") {
+ auto is_utf8_print =
+ nihil::ctype_is(std::ctype_base::print,
+ std::locale("C.UTF-8"));
+
+ // 'x' is printable
+ REQUIRE(nihil::is_print('x') == true);
+ REQUIRE(nihil::is_print(L'x') == true);
+
+ REQUIRE(nihil::is_c_print('x') == true);
+ REQUIRE(nihil::is_c_print(L'x') == true);
+
+ REQUIRE(is_utf8_print('x') == true);
+ REQUIRE(is_utf8_print(L'x') == true);
+
+ // '\003' is not printable
+ REQUIRE(nihil::is_print('\003') == false);
+ REQUIRE(nihil::is_print(L'\003') == false);
+
+ REQUIRE(nihil::is_c_print('\003') == false);
+ REQUIRE(nihil::is_c_print(L'\003') == false);
+
+ REQUIRE(is_utf8_print('\003') == false);
+ REQUIRE(is_utf8_print(L'\003') == false);
+
+ // U+0410 CYRILLIC CAPITAL LETTER A is printable
+ REQUIRE(nihil::is_print(L'\u0410') == false);
+ REQUIRE(nihil::is_c_print(L'\u0410') == false);
+ REQUIRE(is_utf8_print(L'\u0410') == true);
+}
+
+TEST_CASE("ctype: cntrl", "[ctype]") {
+ auto is_utf8_cntrl =
+ nihil::ctype_is(std::ctype_base::cntrl,
+ std::locale("C.UTF-8"));
+
+ // '\003' is a control character
+ REQUIRE(nihil::is_cntrl('\003') == true);
+ REQUIRE(nihil::is_cntrl(L'\003') == true);
+
+ REQUIRE(nihil::is_c_cntrl('\003') == true);
+ REQUIRE(nihil::is_c_cntrl(L'\003') == true);
+
+ REQUIRE(is_utf8_cntrl('\003') == true);
+ REQUIRE(is_utf8_cntrl(L'\003') == true);
+
+
+ // 'x' is not a control character
+ REQUIRE(nihil::is_cntrl('x') == false);
+ REQUIRE(nihil::is_cntrl(L'x') == false);
+
+ REQUIRE(nihil::is_c_cntrl('x') == false);
+ REQUIRE(nihil::is_c_cntrl(L'x') == false);
+
+ REQUIRE(is_utf8_cntrl('x') == false);
+ REQUIRE(is_utf8_cntrl(L'x') == false);
+
+ // U+00AD SOFT HYPHEN is a control character.
+ REQUIRE(nihil::is_cntrl(L'\u00ad') == false);
+ REQUIRE(nihil::is_c_cntrl(L'\u00ad') == false);
+ REQUIRE(is_utf8_cntrl(L'\u00ad') == true);
+}
+
+TEST_CASE("ctype: upper", "[ctype]") {
+ auto is_utf8_upper =
+ nihil::ctype_is(std::ctype_base::upper,
+ std::locale("C.UTF-8"));
+
+ // 'A' is upper case
+ REQUIRE(nihil::is_upper('A') == true);
+ REQUIRE(nihil::is_upper(L'A') == true);
+
+ REQUIRE(nihil::is_c_upper('A') == true);
+ REQUIRE(nihil::is_c_upper(L'A') == true);
+
+ REQUIRE(is_utf8_upper('A') == true);
+ REQUIRE(is_utf8_upper(L'A') == true);
+
+ // 'a' is not upper case
+ REQUIRE(nihil::is_upper('a') == false);
+ REQUIRE(nihil::is_upper(L'a') == false);
+
+ REQUIRE(nihil::is_c_upper('a') == false);
+ REQUIRE(nihil::is_c_upper(L'a') == false);
+
+ REQUIRE(is_utf8_upper('a') == false);
+ REQUIRE(is_utf8_upper(L'a') == false);
+
+ // U+0410 CYRILLIC CAPITAL LETTER A is upper case
+ REQUIRE(nihil::is_upper(L'\u0410') == false);
+ REQUIRE(nihil::is_c_upper(L'\u0410') == false);
+ REQUIRE(is_utf8_upper(L'\u0410') == true);
+}
+
+TEST_CASE("ctype: lower", "[ctype]") {
+ auto is_utf8_lower =
+ nihil::ctype_is(std::ctype_base::lower,
+ std::locale("C.UTF-8"));
+
+ // 'a' is lower case
+ REQUIRE(nihil::is_lower('a') == true);
+ REQUIRE(nihil::is_lower(L'a') == true);
+
+ REQUIRE(nihil::is_c_lower('a') == true);
+ REQUIRE(nihil::is_c_lower(L'a') == true);
+
+ REQUIRE(is_utf8_lower('a') == true);
+ REQUIRE(is_utf8_lower(L'a') == true);
+
+ // 'A' is not lower case
+ REQUIRE(nihil::is_lower('A') == false);
+ REQUIRE(nihil::is_lower(L'A') == false);
+
+ REQUIRE(nihil::is_c_lower('A') == false);
+ REQUIRE(nihil::is_c_lower(L'A') == false);
+
+ REQUIRE(is_utf8_lower('A') == false);
+ REQUIRE(is_utf8_lower(L'A') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_lower(L'\u0430') == false);
+ REQUIRE(nihil::is_c_lower(L'\u0430') == false);
+ REQUIRE(is_utf8_lower(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: alpha", "[ctype]") {
+ auto is_utf8_alpha =
+ nihil::ctype_is(std::ctype_base::alpha,
+ std::locale("C.UTF-8"));
+
+ // 'a' is alphabetical
+ REQUIRE(nihil::is_alpha('a') == true);
+ REQUIRE(nihil::is_alpha(L'a') == true);
+
+ REQUIRE(nihil::is_c_alpha('a') == true);
+ REQUIRE(nihil::is_c_alpha(L'a') == true);
+
+ REQUIRE(is_utf8_alpha('a') == true);
+ REQUIRE(is_utf8_alpha(L'a') == true);
+
+ // '1' is not alphabetical
+ REQUIRE(nihil::is_alpha('1') == false);
+ REQUIRE(nihil::is_alpha(L'1') == false);
+
+ REQUIRE(nihil::is_c_alpha('1') == false);
+ REQUIRE(nihil::is_c_alpha(L'1') == false);
+
+ REQUIRE(is_utf8_alpha('1') == false);
+ REQUIRE(is_utf8_alpha(L'1') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_alpha(L'\u0430') == false);
+ REQUIRE(nihil::is_c_alpha(L'\u0430') == false);
+ REQUIRE(is_utf8_alpha(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: digit", "[ctype]") {
+ auto is_utf8_digit =
+ nihil::ctype_is(std::ctype_base::digit,
+ std::locale("C.UTF-8"));
+
+ // '1' is a digit
+ REQUIRE(nihil::is_digit('1') == true);
+ REQUIRE(nihil::is_digit(L'1') == true);
+
+ REQUIRE(nihil::is_c_digit('1') == true);
+ REQUIRE(nihil::is_c_digit(L'1') == true);
+
+ REQUIRE(is_utf8_digit('1') == true);
+ REQUIRE(is_utf8_digit(L'1') == true);
+
+ // 'a' is not a digit
+ REQUIRE(nihil::is_digit('a') == false);
+ REQUIRE(nihil::is_digit(L'a') == false);
+
+ REQUIRE(nihil::is_c_digit('a') == false);
+ REQUIRE(nihil::is_c_digit(L'a') == false);
+
+ REQUIRE(is_utf8_digit('a') == false);
+ REQUIRE(is_utf8_digit(L'a') == false);
+
+ // U+0660 ARABIC-INDIC DIGIT ZERO
+ REQUIRE(nihil::is_digit(L'\u0660') == false);
+ REQUIRE(nihil::is_c_digit(L'\u0660') == false);
+ REQUIRE(is_utf8_digit(L'\u0660') == true);
+}
+
+TEST_CASE("ctype: punct", "[ctype]") {
+ auto is_utf8_punct =
+ nihil::ctype_is(std::ctype_base::punct,
+ std::locale("C.UTF-8"));
+
+ // ';' is punctuation
+ REQUIRE(nihil::is_punct(';') == true);
+ REQUIRE(nihil::is_punct(L';') == true);
+
+ REQUIRE(nihil::is_c_punct(';') == true);
+ REQUIRE(nihil::is_c_punct(L';') == true);
+
+ REQUIRE(is_utf8_punct(';') == true);
+ REQUIRE(is_utf8_punct(L';') == true);
+
+ // 'a' is not punctuation
+ REQUIRE(nihil::is_punct('a') == false);
+ REQUIRE(nihil::is_punct(L'a') == false);
+
+ REQUIRE(nihil::is_c_punct('a') == false);
+ REQUIRE(nihil::is_c_punct(L'a') == false);
+
+ REQUIRE(is_utf8_punct('a') == false);
+ REQUIRE(is_utf8_punct(L'a') == false);
+
+ // U+00A1 INVERTED EXCLAMATION MARK
+ REQUIRE(nihil::is_punct(L'\u00A1') == false);
+ REQUIRE(nihil::is_c_punct(L'\u00A1') == false);
+ REQUIRE(is_utf8_punct(L'\u00A1') == true);
+}
+
+TEST_CASE("ctype: xdigit", "[ctype]") {
+ auto is_utf8_xdigit =
+ nihil::ctype_is(std::ctype_base::xdigit,
+ std::locale("C.UTF-8"));
+
+ // 'f' is an xdigit
+ REQUIRE(nihil::is_xdigit('f') == true);
+ REQUIRE(nihil::is_xdigit(L'f') == true);
+
+ REQUIRE(nihil::is_c_xdigit('f') == true);
+ REQUIRE(nihil::is_c_xdigit(L'f') == true);
+
+ REQUIRE(is_utf8_xdigit('f') == true);
+ REQUIRE(is_utf8_xdigit(L'f') == true);
+
+ // 'g' is not an xdigit
+ REQUIRE(nihil::is_xdigit('g') == false);
+ REQUIRE(nihil::is_xdigit(L'g') == false);
+
+ REQUIRE(nihil::is_c_xdigit('g') == false);
+ REQUIRE(nihil::is_c_xdigit(L'g') == false);
+
+ REQUIRE(is_utf8_xdigit('g') == false);
+ REQUIRE(is_utf8_xdigit(L'g') == false);
+}
+
+TEST_CASE("ctype: blank", "[ctype]") {
+ auto is_utf8_blank =
+ nihil::ctype_is(std::ctype_base::blank,
+ std::locale("C.UTF-8"));
+
+ // '\t' is a blank
+ REQUIRE(nihil::is_blank('\t') == true);
+ REQUIRE(nihil::is_blank(L'\t') == true);
+
+ REQUIRE(nihil::is_c_blank('\t') == true);
+ REQUIRE(nihil::is_c_blank(L'\t') == true);
+
+ REQUIRE(is_utf8_blank('\t') == true);
+ REQUIRE(is_utf8_blank(L'\t') == true);
+
+ // '\v' is not a blank
+ REQUIRE(nihil::is_blank('\v') == false);
+ REQUIRE(nihil::is_blank(L'\v') == false);
+
+ REQUIRE(nihil::is_c_blank('\v') == false);
+ REQUIRE(nihil::is_c_blank(L'\v') == false);
+
+ REQUIRE(is_utf8_blank('\v') == false);
+ REQUIRE(is_utf8_blank(L'\v') == false);
+
+ // There don't seem to be any UTF-8 blank characters, at least
+ // in FreeBSD libc.
+}
+
+TEST_CASE("ctype: alnum", "[ctype]") {
+ auto is_utf8_alnum =
+ nihil::ctype_is(std::ctype_base::alnum,
+ std::locale("C.UTF-8"));
+
+ // 'a' is alphanumeric
+ REQUIRE(nihil::is_alnum('a') == true);
+ REQUIRE(nihil::is_alnum(L'a') == true);
+
+ REQUIRE(nihil::is_c_alnum('a') == true);
+ REQUIRE(nihil::is_c_alnum(L'a') == true);
+
+ REQUIRE(is_utf8_alnum('a') == true);
+ REQUIRE(is_utf8_alnum(L'a') == true);
+
+ // '\t' is not a alnum
+ REQUIRE(nihil::is_alnum('\t') == false);
+ REQUIRE(nihil::is_alnum(L'\t') == false);
+
+ REQUIRE(nihil::is_c_alnum('\t') == false);
+ REQUIRE(nihil::is_c_alnum(L'\t') == false);
+
+ REQUIRE(is_utf8_alnum('\t') == false);
+ REQUIRE(is_utf8_alnum(L'\t') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_alnum(L'\u0430') == false);
+ REQUIRE(nihil::is_c_alnum(L'\u0430') == false);
+ REQUIRE(is_utf8_alnum(L'\u0430') == true);
+}
+
+TEST_CASE("ctype: graph", "[ctype]") {
+ auto is_utf8_graph =
+ nihil::ctype_is(std::ctype_base::graph,
+ std::locale("C.UTF-8"));
+
+ // 'a' is graphical
+ REQUIRE(nihil::is_graph('a') == true);
+ REQUIRE(nihil::is_graph(L'a') == true);
+
+ REQUIRE(nihil::is_c_graph('a') == true);
+ REQUIRE(nihil::is_c_graph(L'a') == true);
+
+ REQUIRE(is_utf8_graph('a') == true);
+ REQUIRE(is_utf8_graph(L'a') == true);
+
+ // '\t' is not graphical
+ REQUIRE(nihil::is_graph('\t') == false);
+ REQUIRE(nihil::is_graph(L'\t') == false);
+
+ REQUIRE(nihil::is_c_graph('\t') == false);
+ REQUIRE(nihil::is_c_graph(L'\t') == false);
+
+ REQUIRE(is_utf8_graph('\t') == false);
+ REQUIRE(is_utf8_graph(L'\t') == false);
+
+ // U+0430 CYRILLIC SMALL LETTER A
+ REQUIRE(nihil::is_graph(L'\u0430') == false);
+ REQUIRE(nihil::is_c_graph(L'\u0430') == false);
+ REQUIRE(is_utf8_graph(L'\u0430') == true);
+}
diff --git a/tests/fd.cc b/tests/fd.cc
new file mode 100644
index 0000000..fbf353e
--- /dev/null
+++ b/tests/fd.cc
@@ -0,0 +1,200 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <span>
+
+#include <stdio.h>
+#include <fcntl.h>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+using namespace std::literals;
+
+namespace {
+
+// Test if an fd is open.
+auto fd_is_open(int fd) -> bool {
+ auto const ret = ::fcntl(fd, F_GETFL);
+ return ret == 0;
+}
+
+} // anonymous namespace
+
+TEST_CASE("fd: construct empty", "[fd]") {
+ nihil::fd fd;
+
+ REQUIRE(!fd);
+ REQUIRE_THROWS_AS(fd.get(), nihil::fd_logic_error);
+}
+
+TEST_CASE("fd: construct from fd", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ {
+ auto fd = nihil::fd(file);
+ REQUIRE(fd_is_open(fd.get()));
+ }
+
+ REQUIRE(!fd_is_open(file));
+}
+
+TEST_CASE("fd: close", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ REQUIRE(fd);
+
+ auto const ret = fd.close();
+ REQUIRE(ret);
+ REQUIRE(!fd_is_open(file));
+}
+
+TEST_CASE("fd: move construct", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd1 = nihil::fd(file);
+ REQUIRE(fd_is_open(fd1.get()));
+
+ auto fd2(std::move(fd1));
+ REQUIRE(!fd1);
+ REQUIRE(fd2);
+ REQUIRE(fd2.get() == file);
+ REQUIRE(fd_is_open(file));
+}
+
+TEST_CASE("fd: move assign", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd1 = nihil::fd(file);
+ REQUIRE(fd_is_open(fd1.get()));
+
+ auto fd2 = nihil::fd();
+ REQUIRE(!fd2);
+
+ fd2 = std::move(fd1);
+
+ REQUIRE(!fd1);
+ REQUIRE(fd2);
+ REQUIRE(fd2.get() == file);
+ REQUIRE(fd_is_open(file));
+}
+
+TEST_CASE("fd: release", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ auto fdesc = std::move(fd).release();
+ REQUIRE(!fd);
+ REQUIRE(fdesc == file);
+}
+
+TEST_CASE("fd: dup", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+ REQUIRE(fd);
+
+ auto fd2 = dup(fd);
+ REQUIRE(fd2);
+ REQUIRE(fd.get() != fd2->get());
+}
+
+TEST_CASE("fd: dup2", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ REQUIRE(!fd_is_open(666));
+
+ auto fd = nihil::fd(file);
+ auto fd2 = dup(fd, 666);
+
+ REQUIRE(fd);
+ REQUIRE(fd2);
+ REQUIRE(fd2->get() == 666);
+}
+
+TEST_CASE("fd: flags", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+
+ {
+ auto const ret = replaceflags(fd, 0);
+ REQUIRE(ret);
+ REQUIRE(getflags(fd) == 0);
+ }
+
+ {
+ auto const ret = setflags(fd, O_NONBLOCK);
+ REQUIRE(ret == O_NONBLOCK);
+ REQUIRE(getflags(fd) == O_NONBLOCK);
+ }
+
+ {
+ auto const ret = setflags(fd, O_SYNC);
+ REQUIRE(ret == (O_NONBLOCK|O_SYNC));
+ REQUIRE(getflags(fd) == (O_NONBLOCK|O_SYNC));
+ }
+
+ {
+ auto const ret = clearflags(fd, O_NONBLOCK);
+ REQUIRE(ret == O_SYNC);
+ REQUIRE(getflags(fd) == O_SYNC);
+ }
+}
+
+TEST_CASE("fd: fdflags", "[fd]") {
+ auto file = ::open("/dev/null", O_RDONLY);
+ REQUIRE(file > 0);
+
+ auto fd = nihil::fd(file);
+
+ {
+ auto const ret = replacefdflags(fd, 0);
+ REQUIRE(ret);
+ REQUIRE(getfdflags(fd) == 0);
+ }
+
+ {
+ auto const ret = setfdflags(fd, FD_CLOEXEC);
+ REQUIRE(ret == FD_CLOEXEC);
+ REQUIRE(getfdflags(fd) == FD_CLOEXEC);
+ }
+
+ {
+ auto const ret = clearfdflags(fd, FD_CLOEXEC);
+ REQUIRE(ret == 0);
+ REQUIRE(getfdflags(fd) == 0);
+ }
+}
+
+TEST_CASE("fd: pipe, read, write", "[fd]") {
+ auto fds = nihil::pipe();
+ REQUIRE(fds);
+
+ auto [fd1, fd2] = std::move(*fds);
+
+ auto constexpr test_string = "test string"sv;
+
+ auto ret = write(fd1, test_string);
+ REQUIRE(ret);
+ REQUIRE(*ret == test_string.size());
+
+ auto readbuf = std::array<char, test_string.size() * 2>{};
+ ret = read(fd2, readbuf);
+ REQUIRE(ret);
+ REQUIRE(*ret == test_string.size());
+
+ auto read_string = std::string_view(std::span(readbuf).subspan(0, *ret));
+ REQUIRE(read_string == test_string);
+}
diff --git a/tests/generator.cc b/tests/generator.cc
new file mode 100644
index 0000000..8657756
--- /dev/null
+++ b/tests/generator.cc
@@ -0,0 +1,56 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <coroutine>
+#include <ranges>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("generator: basic", "[generator]")
+{
+ auto fn = [] () -> nihil::generator<int> {
+ co_yield 1;
+ co_yield 2;
+ co_yield 3;
+ };
+
+ auto values = std::vector<int>();
+ std::ranges::copy(fn(), std::back_inserter(values));
+
+ REQUIRE(values == std::vector{1, 2, 3});
+}
+
+TEST_CASE("generator: exceptions", "[generator]")
+{
+ auto fn = [] () -> nihil::generator<int> {
+ co_yield 1;
+ throw std::runtime_error("test");
+ };
+
+ auto range = fn();
+ auto it = std::ranges::begin(range);
+ REQUIRE(*it == 1);
+ REQUIRE_THROWS_AS(it++, std::runtime_error);
+}
+
+TEST_CASE("generator: elements_of", "[generator]")
+{
+ auto fn1 = [] -> nihil::generator<int> {
+ co_yield 1;
+ co_yield 2;
+ co_yield 3;
+ };
+
+ auto fn2 = [&fn1] -> nihil::generator<int> {
+ co_yield nihil::ranges::elements_of(fn1());
+ };
+
+ auto values = std::vector<int>();
+ std::ranges::copy(fn2(), std::back_inserter(values));
+
+ REQUIRE(values == std::vector{1, 2, 3});
+}
diff --git a/tests/generic_error.cc b/tests/generic_error.cc
new file mode 100644
index 0000000..b213af9
--- /dev/null
+++ b/tests/generic_error.cc
@@ -0,0 +1,17 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+using namespace std::literals;
+
+TEST_CASE("generic_error: basic", "[generic_error]") {
+ try {
+ throw nihil::generic_error("{} + {} = {}", 1, 2, 3);
+ } catch (nihil::generic_error const &exc) {
+ REQUIRE(exc.what() == "1 + 2 = 3"s);
+ }
+}
diff --git a/tests/getenv.cc b/tests/getenv.cc
new file mode 100644
index 0000000..adfa84f
--- /dev/null
+++ b/tests/getenv.cc
@@ -0,0 +1,48 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <ranges>
+#include <string>
+
+#include <unistd.h>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+TEST_CASE("getenv: existing value", "[getenv]")
+{
+ auto constexpr *name = "LFJAIL_TEST_VAR";
+ auto constexpr *value = "test is a test";
+
+ REQUIRE(::setenv(name, value, 1) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(s);
+ REQUIRE(*s == value);
+}
+
+TEST_CASE("getenv: non-existing value", "[getenv]")
+{
+ auto constexpr *name = "LFJAIL_TEST_VAR";
+
+ REQUIRE(::unsetenv(name) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(!s);
+ REQUIRE(s.error() == std::errc::no_such_file_or_directory);
+}
+
+// Force the call to getenv_r() to reallocate.
+TEST_CASE("getenv: long value")
+{
+ auto constexpr *name = "LFJAIL_TEST_VAR";
+ auto const value = std::string(4096, 'a');
+
+ REQUIRE(::setenv(name, value.c_str(), 1) == 0);
+
+ auto const s = nihil::getenv(name);
+ REQUIRE(s);
+ REQUIRE(*s == value);
+}
diff --git a/tests/guard.cc b/tests/guard.cc
new file mode 100644
index 0000000..f88aa9b
--- /dev/null
+++ b/tests/guard.cc
@@ -0,0 +1,20 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+using namespace std::literals;
+
+TEST_CASE("guard: basic", "[guard]") {
+ int n = 0;
+
+ {
+ auto guard = nihil::guard([&] { n = 1; });
+ REQUIRE(n == 0);
+ }
+
+ REQUIRE(n == 1);
+}
diff --git a/tests/tabulate.cc b/tests/tabulate.cc
new file mode 100644
index 0000000..84f8b33
--- /dev/null
+++ b/tests/tabulate.cc
@@ -0,0 +1,75 @@
+/*
+ * This source code is released into the public domain.
+ */
+
+#include <iterator>
+#include <string>
+#include <vector>
+
+#include <catch2/catch_test_macros.hpp>
+
+import nihil;
+
+using namespace std::literals;
+using namespace nihil;
+
+TEST_CASE("tabulate: basic", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "foo", "b"},
+ std::vector{"bar", "c", "baz"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {:2} {:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a foo b\n"
+"bar c baz\n");
+}
+
+TEST_CASE("tabulate: basic wide", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{L"a", L"foo", L"b"},
+ std::vector{L"bar", L"c", L"baz"},
+ };
+
+ auto result = std::wstring();
+ wtabulate(L"{:1} {:2} {:3}", input, std::back_inserter(result));
+
+ REQUIRE(result ==
+L"1 2 3\n"
+"a foo b\n"
+"bar c baz\n");
+}
+
+TEST_CASE("tabulate: jagged", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "foo", "b"},
+ std::vector{"bar", "baz"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {:2} {:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a foo b\n"
+"bar baz\n");
+}
+
+TEST_CASE("tabulate: align", "[tabulate]")
+{
+ auto input = std::vector{
+ std::vector{"a", "longvalue", "s"},
+ std::vector{"a", "s", "longvalue"},
+ };
+
+ auto result = std::string();
+ tabulate("{:1} {<:2} {>:3}", input, std::back_inserter(result));
+ REQUIRE(result ==
+"1 2 3\n"
+"a longvalue s\n"
+"a s longvalue\n");
+}