From a2d7181700ac64b8e7a4472ec26dfa253b38f188 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Sat, 28 Jun 2025 19:25:55 +0100 Subject: split nihil into separate modules --- CMakeLists.txt | 10 +- nihil.cli/CMakeLists.txt | 32 ++ nihil.cli/command_map.cc | 267 ++++++++++++++++ nihil.cli/command_map.ccm | 67 ++++ nihil.cli/nihil.cli.ccm | 9 + nihil.cli/test_command_map.cc | 32 ++ nihil.cli/usage_error.ccm | 24 ++ nihil.config/CMakeLists.txt | 7 +- nihil.config/option.cc | 3 +- nihil.config/option.ccm | 2 +- nihil.config/read.cc | 4 +- nihil.config/read.ccm | 2 +- nihil.config/store.cc | 4 +- nihil.config/store.ccm | 2 +- nihil.config/string.cc | 3 +- nihil.config/string.ccm | 1 - nihil.config/write.cc | 4 +- nihil.config/write.ccm | 2 +- nihil.error/CMakeLists.txt | 29 ++ nihil.error/error.cc | 160 ++++++++++ nihil.error/error.ccm | 199 ++++++++++++ nihil.error/test.cc | 169 ++++++++++ nihil.generator/CMakeLists.txt | 23 ++ nihil.generator/generator.ccm | 692 +++++++++++++++++++++++++++++++++++++++++ nihil.generator/test.cc | 56 ++++ nihil.guard/CMakeLists.txt | 23 ++ nihil.guard/guard.ccm | 51 +++ nihil.guard/test.cc | 20 ++ nihil.match/CMakeLists.txt | 23 ++ nihil.match/match.ccm | 23 ++ nihil.match/test.cc | 34 ++ nihil.monad/CMakeLists.txt | 24 ++ nihil.monad/monad.ccm | 289 +++++++++++++++++ nihil.monad/test.cc | 69 ++++ nihil.posix/CMakeLists.txt | 51 +++ nihil.posix/argv.cc | 65 ++++ nihil.posix/argv.ccm | 78 +++++ nihil.posix/ensure_dir.cc | 30 ++ nihil.posix/ensure_dir.ccm | 23 ++ nihil.posix/exec.cc | 71 +++++ nihil.posix/exec.ccm | 105 +++++++ nihil.posix/fd.cc | 220 +++++++++++++ nihil.posix/fd.ccm | 157 ++++++++++ nihil.posix/find_in_path.cc | 52 ++++ nihil.posix/getenv.cc | 45 +++ nihil.posix/nihil.posix.ccm | 45 +++ nihil.posix/open.cc | 31 ++ nihil.posix/open.ccm | 24 ++ nihil.posix/process.cc | 102 ++++++ nihil.posix/process.ccm | 91 ++++++ nihil.posix/read_file.ccm | 48 +++ nihil.posix/rename.cc | 34 ++ nihil.posix/rename.ccm | 23 ++ nihil.posix/spawn.ccm | 249 +++++++++++++++ nihil.posix/test_fd.cc | 199 ++++++++++++ nihil.posix/test_getenv.cc | 49 +++ nihil.posix/test_spawn.cc | 117 +++++++ nihil.posix/write_file.ccm | 82 +++++ nihil.ucl/CMakeLists.txt | 2 +- nihil.ucl/boolean.cc | 2 +- nihil.ucl/errc.cc | 2 - nihil.ucl/integer.cc | 2 +- nihil.ucl/integer.ccm | 1 - nihil.ucl/object.cc | 2 - nihil.ucl/object.ccm | 1 - nihil.ucl/object_cast.ccm | 2 +- nihil.ucl/parser.cc | 2 +- nihil.ucl/parser.ccm | 2 +- nihil.ucl/real.cc | 2 +- nihil.ucl/string.cc | 2 +- nihil.ucl/string.ccm | 1 - nihil.ucl/tests/CMakeLists.txt | 4 +- nihil.ucl/tests/array.cc | 1 - nihil.ucl/tests/emit.cc | 1 - nihil.ucl/tests/parse.cc | 1 - nihil.ucl/type.ccm | 2 +- nihil.util/CMakeLists.txt | 36 +++ nihil.util/ctype.ccm | 87 ++++++ nihil.util/next_word.ccm | 49 +++ nihil.util/nihil.util.ccm | 13 + nihil.util/parse_size.ccm | 107 +++++++ nihil.util/skipws.ccm | 40 +++ nihil.util/tabulate.ccm | 312 +++++++++++++++++++ nihil.util/test_ctype.cc | 373 ++++++++++++++++++++++ nihil.util/test_next_word.cc | 65 ++++ nihil.util/test_parse_size.cc | 168 ++++++++++ nihil.util/test_skipws.cc | 45 +++ nihil.util/test_tabulate.cc | 75 +++++ nihil/CMakeLists.txt | 52 ---- nihil/argv.cc | 65 ---- nihil/argv.ccm | 78 ----- nihil/command_map.cc | 267 ---------------- nihil/command_map.ccm | 67 ---- nihil/ctype.ccm | 87 ------ nihil/ensure_dir.cc | 28 -- nihil/ensure_dir.ccm | 23 -- nihil/errc.cc | 49 --- nihil/errc.ccm | 34 -- nihil/error.cc | 157 ---------- nihil/error.ccm | 199 ------------ nihil/exec.cc | 68 ---- nihil/exec.ccm | 106 ------- nihil/fd.cc | 217 ------------- nihil/fd.ccm | 156 ---------- nihil/find_in_path.cc | 52 ---- nihil/find_in_path.ccm | 24 -- nihil/format_filesystem.ccm | 35 --- nihil/generator.ccm | 692 ----------------------------------------- nihil/getenv.cc | 43 --- nihil/getenv.ccm | 24 -- nihil/guard.ccm | 51 --- nihil/match.ccm | 23 -- nihil/monad.ccm | 289 ----------------- nihil/next_word.ccm | 50 --- nihil/nihil.ccm | 34 -- nihil/open_file.cc | 28 -- nihil/open_file.ccm | 25 -- nihil/parse_size.ccm | 105 ------- nihil/process.cc | 100 ------ nihil/process.ccm | 91 ------ nihil/read_file.ccm | 48 --- nihil/rename_file.cc | 32 -- nihil/rename_file.ccm | 24 -- nihil/skipws.ccm | 40 --- nihil/spawn.ccm | 250 --------------- nihil/tabulate.ccm | 312 ------------------- nihil/tests/CMakeLists.txt | 28 -- nihil/tests/command_map.cc | 32 -- nihil/tests/ctype.cc | 373 ---------------------- nihil/tests/error.cc | 169 ---------- nihil/tests/fd.cc | 198 ------------ nihil/tests/generator.cc | 56 ---- nihil/tests/getenv.cc | 48 --- nihil/tests/guard.cc | 20 -- nihil/tests/monad.cc | 68 ---- nihil/tests/next_word.cc | 65 ---- nihil/tests/parse_size.cc | 167 ---------- nihil/tests/skipws.cc | 45 --- nihil/tests/spawn.cc | 117 ------- nihil/tests/tabulate.cc | 75 ----- nihil/usage_error.ccm | 24 -- nihil/write_file.ccm | 81 ----- 142 files changed, 5718 insertions(+), 5625 deletions(-) create mode 100644 nihil.cli/CMakeLists.txt create mode 100644 nihil.cli/command_map.cc create mode 100644 nihil.cli/command_map.ccm create mode 100644 nihil.cli/nihil.cli.ccm create mode 100644 nihil.cli/test_command_map.cc create mode 100644 nihil.cli/usage_error.ccm create mode 100644 nihil.error/CMakeLists.txt create mode 100644 nihil.error/error.cc create mode 100644 nihil.error/error.ccm create mode 100644 nihil.error/test.cc create mode 100644 nihil.generator/CMakeLists.txt create mode 100644 nihil.generator/generator.ccm create mode 100644 nihil.generator/test.cc create mode 100644 nihil.guard/CMakeLists.txt create mode 100644 nihil.guard/guard.ccm create mode 100644 nihil.guard/test.cc create mode 100644 nihil.match/CMakeLists.txt create mode 100644 nihil.match/match.ccm create mode 100644 nihil.match/test.cc create mode 100644 nihil.monad/CMakeLists.txt create mode 100644 nihil.monad/monad.ccm create mode 100644 nihil.monad/test.cc create mode 100644 nihil.posix/CMakeLists.txt create mode 100644 nihil.posix/argv.cc create mode 100644 nihil.posix/argv.ccm create mode 100644 nihil.posix/ensure_dir.cc create mode 100644 nihil.posix/ensure_dir.ccm create mode 100644 nihil.posix/exec.cc create mode 100644 nihil.posix/exec.ccm create mode 100644 nihil.posix/fd.cc create mode 100644 nihil.posix/fd.ccm create mode 100644 nihil.posix/find_in_path.cc create mode 100644 nihil.posix/getenv.cc create mode 100644 nihil.posix/nihil.posix.ccm create mode 100644 nihil.posix/open.cc create mode 100644 nihil.posix/open.ccm create mode 100644 nihil.posix/process.cc create mode 100644 nihil.posix/process.ccm create mode 100644 nihil.posix/read_file.ccm create mode 100644 nihil.posix/rename.cc create mode 100644 nihil.posix/rename.ccm create mode 100644 nihil.posix/spawn.ccm create mode 100644 nihil.posix/test_fd.cc create mode 100644 nihil.posix/test_getenv.cc create mode 100644 nihil.posix/test_spawn.cc create mode 100644 nihil.posix/write_file.ccm create mode 100644 nihil.util/CMakeLists.txt create mode 100644 nihil.util/ctype.ccm create mode 100644 nihil.util/next_word.ccm create mode 100644 nihil.util/nihil.util.ccm create mode 100644 nihil.util/parse_size.ccm create mode 100644 nihil.util/skipws.ccm create mode 100644 nihil.util/tabulate.ccm create mode 100644 nihil.util/test_ctype.cc create mode 100644 nihil.util/test_next_word.cc create mode 100644 nihil.util/test_parse_size.cc create mode 100644 nihil.util/test_skipws.cc create mode 100644 nihil.util/test_tabulate.cc delete mode 100644 nihil/CMakeLists.txt delete mode 100644 nihil/argv.cc delete mode 100644 nihil/argv.ccm delete mode 100644 nihil/command_map.cc delete mode 100644 nihil/command_map.ccm delete mode 100644 nihil/ctype.ccm delete mode 100644 nihil/ensure_dir.cc delete mode 100644 nihil/ensure_dir.ccm delete mode 100644 nihil/errc.cc delete mode 100644 nihil/errc.ccm delete mode 100644 nihil/error.cc delete mode 100644 nihil/error.ccm delete mode 100644 nihil/exec.cc delete mode 100644 nihil/exec.ccm delete mode 100644 nihil/fd.cc delete mode 100644 nihil/fd.ccm delete mode 100644 nihil/find_in_path.cc delete mode 100644 nihil/find_in_path.ccm delete mode 100644 nihil/format_filesystem.ccm delete mode 100644 nihil/generator.ccm delete mode 100644 nihil/getenv.cc delete mode 100644 nihil/getenv.ccm delete mode 100644 nihil/guard.ccm delete mode 100644 nihil/match.ccm delete mode 100644 nihil/monad.ccm delete mode 100644 nihil/next_word.ccm delete mode 100644 nihil/nihil.ccm delete mode 100644 nihil/open_file.cc delete mode 100644 nihil/open_file.ccm delete mode 100644 nihil/parse_size.ccm delete mode 100644 nihil/process.cc delete mode 100644 nihil/process.ccm delete mode 100644 nihil/read_file.ccm delete mode 100644 nihil/rename_file.cc delete mode 100644 nihil/rename_file.ccm delete mode 100644 nihil/skipws.ccm delete mode 100644 nihil/spawn.ccm delete mode 100644 nihil/tabulate.ccm delete mode 100644 nihil/tests/CMakeLists.txt delete mode 100644 nihil/tests/command_map.cc delete mode 100644 nihil/tests/ctype.cc delete mode 100644 nihil/tests/error.cc delete mode 100644 nihil/tests/fd.cc delete mode 100644 nihil/tests/generator.cc delete mode 100644 nihil/tests/getenv.cc delete mode 100644 nihil/tests/guard.cc delete mode 100644 nihil/tests/monad.cc delete mode 100644 nihil/tests/next_word.cc delete mode 100644 nihil/tests/parse_size.cc delete mode 100644 nihil/tests/skipws.cc delete mode 100644 nihil/tests/spawn.cc delete mode 100644 nihil/tests/tabulate.cc delete mode 100644 nihil/usage_error.ccm delete mode 100644 nihil/write_file.ccm diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a2b6ab..8bf5a42 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,9 +18,17 @@ add_compile_options(-Wextra) add_compile_options(-Werror) add_compile_options(-Wpedantic) +add_subdirectory(nihil.cli) +add_subdirectory(nihil.core) +add_subdirectory(nihil.error) add_subdirectory(nihil.flagset) +add_subdirectory(nihil.generator) +add_subdirectory(nihil.guard) +add_subdirectory(nihil.match) +add_subdirectory(nihil.monad) +add_subdirectory(nihil.posix) +add_subdirectory(nihil.util) add_subdirectory(nihil.uuid) -add_subdirectory(nihil) if(NIHIL_UCL) add_subdirectory(nihil.ucl) diff --git a/nihil.cli/CMakeLists.txt b/nihil.cli/CMakeLists.txt new file mode 100644 index 0000000..a677798 --- /dev/null +++ b/nihil.cli/CMakeLists.txt @@ -0,0 +1,32 @@ +# This source code is released into the public domain. + +add_library(nihil.cli STATIC) +target_link_libraries(nihil.cli PRIVATE nihil.util) +target_sources(nihil.cli + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + nihil.cli.ccm + + command_map.ccm + usage_error.ccm + + PRIVATE + command_map.cc +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.cli.test + test_command_map.cc + ) + target_link_libraries(nihil.cli.test PRIVATE + nihil.cli + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.cli.test) +endif() diff --git a/nihil.cli/command_map.cc b/nihil.cli/command_map.cc new file mode 100644 index 0000000..c656c62 --- /dev/null +++ b/nihil.cli/command_map.cc @@ -0,0 +1,267 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include +#include + +#include + +module nihil.cli; + +/* + * command_map represents a hierarchical list of commands. At each level, + * a command is mapped to a handler, which can either be a function, in + * which case we execute the function, or another command_map, in which + * case we invoke the new map + */ + +namespace nihil { + +/* + * The string tree we store our commands in. This is sort of like a very + * basic hierarchical std::map. Keys are provided as a range of values, + * typically from argv. + */ + +struct command_tree_node final { + command_tree_node() + : _this_word("") + { + } + + command_tree_node(std::string_view this_word) + : _this_word(this_word) + { + } + + command_tree_node(std::string_view this_word, + command value) + : _this_word(this_word) + , _value(std::move(value)) + { + } + + /* + * Return a child node, or NULL if the child doesn't exist. + */ + auto get_child(this command_tree_node const &self, + std::string_view child) + -> command_tree_node const * + { + if (auto it = self.children.find(std::string(child)); + it != self.children.end()) + return &it->second; + + return nullptr; + } + + auto get_child(this command_tree_node &self, + std::string_view child) + -> command_tree_node * + { + if (auto it = self.children.find(std::string(child)); + it != self.children.end()) + return &it->second; + + return nullptr; + } + + /* + * Return a child node if it exists, or insert a new empty node. + */ + auto get_or_create_child(this command_tree_node &self, + std::string_view child) + -> command_tree_node * + { + if (auto ptr = self.get_child(child); ptr != nullptr) + return ptr; + + auto [it, ok] = self.children.emplace( + child, command_tree_node(child)); + return &it->second; + } + + /* + * Return this node's value. + */ + auto value(this command_tree_node const &self) + -> std::optional const & + { + return self._value; + } + + /* + * Set this node's value. + */ + auto value(this command_tree_node &self, command value) -> void + { + self._value = std::move(value); + } + + /* + * Print this node's children in a form useful to humans. + */ + auto print_commands(this command_tree_node const &self, + std::string_view prefix) -> void + { + for (auto &&[name, node] : self.children) { + std::print(" {} {}\n", prefix, name); + } + } + +private: + std::string _this_word; + std::optional _value; + std::unordered_map children; +}; + +struct command_tree { + /* + * Add a node to the tree. Returns false if the node already exists. + */ + auto insert(this command_tree &self, + std::ranges::range auto &&path, + command value) + -> bool + { + auto *this_node = &self._root_node; + + // Find the node for this key. + for (auto &&next : path) { + auto this_word = std::string_view(next); + this_node = this_node->get_or_create_child(this_word); + } + + if (this_node->value()) { + // The value already exists. + return false; + } + + // Set the new value. + this_node->value(std::move(value)); + return true; + } + + /* + * Find a node in the tree. Unlike insert(), iteration stops when + * we find any node with a value. + */ + auto find(this command_tree const &self, int &argc, char **&argv) + -> std::optional + { + auto *this_node = &self._root_node; + + // Assume the caller already stripped the program name from + // argv. This is usually the case since they would have + // called getopt(). + + // Store the command bits we got so far, so we can print them + // in the usage error if needed. + auto path = std::string(); + + while (argv[0] != nullptr) { + auto next = std::string_view(argv[0]); + + auto *next_node = this_node->get_child(next); + + if (next_node == nullptr) { + // The node doesn't exist, so this command is + // not valid. Print a list of valid commands. + std::print(std::cerr, + "{}: unknown command: {} {}\n", + ::getprogname(), path, next); + std::print(std::cerr, + "{}: expected one of:\n", + ::getprogname()); + + this_node->print_commands(path); + return {}; + } + + this_node = next_node; + + if (this_node->value()) + // This node has a value, so return it. + return {this_node->value()}; + + if (!path.empty()) + path += ' '; + path += next; + --argc; + ++argv; + } + + // We didn't find a value, so the key was incomplete. + std::print(std::cerr, "{}: {} command; expected:\n", + ::getprogname(), + path.empty() ? "missing" : "incomplete"); + this_node->print_commands(path); + + return {}; + } + +private: + command_tree_node _root_node; +}; + +/* + * The global command map. + */ +auto get_commands() -> command_tree & { + static auto commands = command_tree(); + return commands; +} + +auto register_command(std::string_view path, command *cmd) noexcept -> void +try { + auto &commands = get_commands(); + if (commands.insert(path | std::views::split(' '), *cmd) == false) { + std::printf("command registration failed\n"); + std::abort(); + } +} catch (...) { + std::printf("command registration failed\n"); + std::abort(); +} + +auto dispatch_command(int argc, char **argv) -> int +{ + auto &commands = get_commands(); + + // The caller should have stripped argv[0] already. find() will + // strip all the remaining elements except the last, which means + // argv[0] will be set to something reasonable for the next call + // to getopt(). + + auto node = commands.find(argc, argv); + + if (!node) + // find() already printed the error message + return 1; + + // Reset getopt here for the command to use. + optreset = 1; + optind = 1; + + // Calling setprogname() makes error messages more relevant. + auto cprogname = std::format("{} {}", ::getprogname(), + node->path()); + ::setprogname(cprogname.c_str()); + + return node->invoke(argc, argv); +} + +void print_usage(std::string_view) +{ +// get_root_node().print_usage(std::string(prefix)); +} + +} // namespace nihil diff --git a/nihil.cli/command_map.ccm b/nihil.cli/command_map.ccm new file mode 100644 index 0000000..8cf9d9c --- /dev/null +++ b/nihil.cli/command_map.ccm @@ -0,0 +1,67 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +export module nihil.cli:command_map; + +import nihil.util; +import :usage_error; + +/* + * command_map represents a hierarchical list of commands. At each level, + * a command is mapped to a handler, which can either be a function, in + * which case we execute the function, or another command_map, in which + * case we invoke the new map + */ + +namespace nihil { + +export struct command; + +/* + * Register a command; used by command<>::command(). + */ +auto register_command(std::string_view path, command *) noexcept -> void; + +/* + * A command that can be invoked. Instantiating a command adds this command + * to the global command table. If an error occurs, the program will abort. + */ +export struct command { + command(std::string_view path, std::string_view usage, auto &&fn) + : m_path(path) + , m_usage(usage) + , m_handler(std::forward(fn)) + { + register_command(path, this); + } + + [[nodiscard]] auto path(this command const &self) -> std::string_view + { + return self.m_path; + } + + auto invoke(this command const &self, int argc, char **argv) -> int + { + return std::invoke(self.m_handler, argc, argv); + } + +private: + std::string_view m_path; + std::string_view m_usage; + std::function m_handler; +}; + +// The public API. +export [[nodiscard]] auto dispatch_command(int argc, char **argv) -> int; +export auto print_usage(std::string_view prefix) -> void; + +} // namespace nihil diff --git a/nihil.cli/nihil.cli.ccm b/nihil.cli/nihil.cli.ccm new file mode 100644 index 0000000..faeb44e --- /dev/null +++ b/nihil.cli/nihil.cli.ccm @@ -0,0 +1,9 @@ +/* + * This source code is released into the public domain. + */ + +module; + +export module nihil.cli; + +export import :command_map; diff --git a/nihil.cli/test_command_map.cc b/nihil.cli/test_command_map.cc new file mode 100644 index 0000000..1b87a49 --- /dev/null +++ b/nihil.cli/test_command_map.cc @@ -0,0 +1,32 @@ +/* + * This source code is released into the public domain. + */ + +#include + +#include + +import nihil.cli; + +namespace { + +auto cmd_sub1_called = false; +auto cmd_sub1 = nihil::command("cmd sub1", "", [](int, char **) -> int +{ + cmd_sub1_called = true; + return 0; +}); + +} // anonymous namespace + +TEST_CASE("command_map: basic", "[command_map]") +{ + auto args = std::vector{ + "cmd", "sub1", nullptr + }; + auto argv = const_cast(args.data()); + + int ret = nihil::dispatch_command(args.size() - 1, argv); + REQUIRE(ret == 0); + REQUIRE(cmd_sub1_called == true); +} diff --git a/nihil.cli/usage_error.ccm b/nihil.cli/usage_error.ccm new file mode 100644 index 0000000..11646e6 --- /dev/null +++ b/nihil.cli/usage_error.ccm @@ -0,0 +1,24 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include + +export module nihil.cli:usage_error; + +import nihil.error; + +namespace nihil { + +/* + * Exception thrown to indicate invalid command-line arguments. + */ +export struct usage_error : error { + usage_error(std::string_view what) : error(what) {} +}; + +} // namespace nihil + + diff --git a/nihil.config/CMakeLists.txt b/nihil.config/CMakeLists.txt index fb2c1db..8a52d3c 100644 --- a/nihil.config/CMakeLists.txt +++ b/nihil.config/CMakeLists.txt @@ -1,7 +1,12 @@ # This source code is released into the public domain. add_library(nihil.config STATIC) -target_link_libraries(nihil.config PUBLIC nihil nihil.ucl) +target_link_libraries(nihil.config PRIVATE + nihil.error + nihil.generator + nihil.posix + nihil.ucl +) target_sources(nihil.config PUBLIC FILE_SET modules TYPE CXX_MODULES FILES nihil.config.ccm diff --git a/nihil.config/option.cc b/nihil.config/option.cc index 588a48f..bcaf167 100644 --- a/nihil.config/option.cc +++ b/nihil.config/option.cc @@ -11,7 +11,8 @@ module; module nihil.config; -import nihil; +import nihil.error; +import nihil.monad; import nihil.ucl; namespace nihil::config { diff --git a/nihil.config/option.ccm b/nihil.config/option.ccm index c6ebcae..4b95793 100644 --- a/nihil.config/option.ccm +++ b/nihil.config/option.ccm @@ -10,7 +10,7 @@ module; export module nihil.config:option; -import nihil; +import nihil.error; import nihil.ucl; namespace nihil::config { diff --git a/nihil.config/read.cc b/nihil.config/read.cc index e7def91..48484fb 100644 --- a/nihil.config/read.cc +++ b/nihil.config/read.cc @@ -13,7 +13,9 @@ module; module nihil.config; -import nihil; +import nihil.error; +import nihil.monad; +import nihil.posix; import nihil.ucl; namespace nihil::config { diff --git a/nihil.config/read.ccm b/nihil.config/read.ccm index fca78eb..9cf28c9 100644 --- a/nihil.config/read.ccm +++ b/nihil.config/read.ccm @@ -9,7 +9,7 @@ module; export module nihil.config:read; -import nihil; +import nihil.error; namespace nihil::config { diff --git a/nihil.config/store.cc b/nihil.config/store.cc index 6f93677..0fb8cc0 100644 --- a/nihil.config/store.cc +++ b/nihil.config/store.cc @@ -12,7 +12,9 @@ module; module nihil.config; -import nihil; +import nihil.error; +import nihil.generator; +import nihil.monad; namespace nihil::config { diff --git a/nihil.config/store.ccm b/nihil.config/store.ccm index 03d09d3..4d37ce0 100644 --- a/nihil.config/store.ccm +++ b/nihil.config/store.ccm @@ -15,7 +15,7 @@ module; export module nihil.config:store; -import nihil; +import nihil.generator; import :option; namespace nihil::config { diff --git a/nihil.config/string.cc b/nihil.config/string.cc index 7d0c038..0ca4605 100644 --- a/nihil.config/string.cc +++ b/nihil.config/string.cc @@ -11,7 +11,8 @@ module; module nihil.config; -import nihil; +import nihil.error; +import nihil.monad; import nihil.ucl; namespace nihil::config { diff --git a/nihil.config/string.ccm b/nihil.config/string.ccm index b6a7e05..668bbc0 100644 --- a/nihil.config/string.ccm +++ b/nihil.config/string.ccm @@ -10,7 +10,6 @@ module; export module nihil.config:string; -import nihil; import nihil.ucl; import :option; diff --git a/nihil.config/write.cc b/nihil.config/write.cc index 8c02c43..80125a8 100644 --- a/nihil.config/write.cc +++ b/nihil.config/write.cc @@ -12,7 +12,9 @@ module; module nihil.config; -import nihil; +import nihil.error; +import nihil.monad; +import nihil.posix; import nihil.ucl; namespace nihil::config { diff --git a/nihil.config/write.ccm b/nihil.config/write.ccm index 8fb8151..564bb20 100644 --- a/nihil.config/write.ccm +++ b/nihil.config/write.ccm @@ -9,7 +9,7 @@ module; export module nihil.config:write; -import nihil; +import nihil.error; namespace nihil::config { diff --git a/nihil.error/CMakeLists.txt b/nihil.error/CMakeLists.txt new file mode 100644 index 0000000..1316b71 --- /dev/null +++ b/nihil.error/CMakeLists.txt @@ -0,0 +1,29 @@ +# This source code is released into the public domain. + +add_library(nihil.error STATIC) +target_link_libraries(nihil.error PRIVATE nihil.match) +target_sources(nihil.error + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + error.ccm + + PRIVATE + error.cc +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.error.test + test.cc) + + target_link_libraries(nihil.error.test PRIVATE + nihil.error + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.error.test) +endif() diff --git a/nihil.error/error.cc b/nihil.error/error.cc new file mode 100644 index 0000000..15805a3 --- /dev/null +++ b/nihil.error/error.cc @@ -0,0 +1,160 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +module nihil.error; + +import nihil.match; + +namespace nihil { + +auto to_string(error const &self) -> std::string +{ + auto ret = self.str(); + + auto cause = self.cause(); + while (cause) { + ret += ": " + cause->str(); + cause = cause->cause(); + } + + return ret; +} + +error::error() +{ +} + +error::~error() = default; + +error::error(std::string_view what, error cause) + : m_error(std::string(what)) + , m_cause(std::make_shared(std::move(cause))) +{ +} +error::error(std::string_view what) + : m_error(std::string(what)) +{ +} + +error::error(std::error_condition what, error cause) + : m_error(what) + , m_cause(std::make_shared(std::move(cause))) +{ +} + +error::error(std::error_condition what) + : m_error(what) +{ +} + +error::error(std::error_code what, error cause) + : m_error(what) + , m_cause(std::make_shared(std::move(cause))) +{ +} + +error::error(std::error_code what) + : m_error(what) +{ +} + +error::error(error const &) = default; +error::error(error &&) noexcept = default; +auto error::operator=(this error &, error const &) -> error & = default; +auto error::operator=(this error &, error &&) noexcept -> error & = default; + +auto error::cause(this error const &self) -> std::shared_ptr +{ + if (self.m_cause) + return self.m_cause; + return {}; +} + +auto error::root_cause(this error const &self) -> error const & +{ + if (self.m_cause) + return self.m_cause->root_cause(); + + return self; +} + +auto error::str(this error const &self) -> std::string +{ + return self.m_error | match { + [] (std::monostate) -> std::string { + return "No error"; + }, + [] (std::error_code const &m) { + return m.message(); + }, + [] (std::error_condition const &m) { + return m.message(); + }, + [] (std::string const &m) { + return m; + } + }; +} + +auto error::code(this error const &self) -> std::optional +{ + auto const *code = std::get_if(&self.m_error); + if (code) + return {*code}; + return {}; +} + +auto error::condition(this error const &self) + -> std::optional +{ + auto const *condition = std::get_if(&self.m_error); + if (condition) + return {*condition}; + return {}; +} + +auto error::what() const noexcept -> char const * +{ + if (!m_what) + m_what = to_string(*this); + + return m_what->c_str(); +} + +auto operator==(error const &lhs, error const &rhs) -> bool +{ + return lhs.m_error == rhs.m_error; +} + +auto operator<=>(error const &lhs, error const &rhs) -> std::strong_ordering +{ + return lhs.m_error <=> rhs.m_error; +} + +auto operator==(error const &lhs, std::error_code const &rhs) -> bool +{ + return lhs.code() == rhs; +} + +// Compare an error to an std::error_condition. +auto operator==(error const &lhs, std::error_condition const &rhs) -> bool +{ + return lhs.condition() == rhs; +} + +auto operator<<(std::ostream &strm, error const &e) -> std::ostream & +{ + return strm << to_string(e); +} + +} // namespace nihil diff --git a/nihil.error/error.ccm b/nihil.error/error.ccm new file mode 100644 index 0000000..b624184 --- /dev/null +++ b/nihil.error/error.ccm @@ -0,0 +1,199 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * error: a type representing an error. + * + * An error consists of an immediate cause, which may be a string or + * std:error_code, and an optional proximate cause, which is another error + * object. Any number of error objects may be stacked. + * + * For example, a failure to open a file might be a stack of two errors: + * + * - string, "failed to open /etc/somefile", + * - std::error_code, "No such file or directory". + * + * Calling .str() will format the entire stack starting at that error, + * for example: "failed to open /etc/somefile: No such file or directory". + * + * Errors may be moved and (relatively) cheaply copied, since the cause + * chain is refcounted. + * + * error derives from std::exception, so it may be thrown and caught and + * provides a useful what(). When throwing errors, creating a derived + * error will make it easier to distinguish errors when catching them. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +export module nihil.error; + +namespace nihil { + +// Things which can be errors. +using error_t = std::variant< + std::monostate, + std::string, + std::error_code, + std::error_condition + >; + +export struct error : std::exception { + // Create an empty error, representing success. + error(); + + // Destroy an error. + virtual ~error(); + + // Create an error from a freeform string. + error(std::string_view what, error cause); + explicit error(std::string_view what); + + template + requires(std::is_error_code_enum::value || + std::is_error_condition_enum::value) + error(std::string_view what, Cause &&cause) + : error(what, error(std::forward(cause))) + {} + + // Create an error from an std::error_code. + error(std::error_condition what, error cause); + explicit error(std::error_condition what); + + // Create an error from an std::error_condition. + error(std::error_code what, error cause); + explicit error(std::error_code what); + + // Create an error from an std::error_code enum. + error(auto errc, error cause) + requires(std::is_error_code_enum::value) + : error(make_error_code(errc), std::move(cause)) + {} + + explicit error(auto errc) + requires(std::is_error_code_enum::value) + : error(make_error_code(errc)) + {} + + // Create an error from an std::error_condition enum. + error(auto errc, error cause) + requires(std::is_error_condition_enum::value) + : error(make_error_condition(errc), std::move(cause)) + {} + + explicit error(auto errc) + requires(std::is_error_condition_enum::value) + : error(make_error_condition(errc)) + {} + + error(error const &); + error(error &&) noexcept; + + auto operator=(this error &, error const &) -> error &; + auto operator=(this error &, error &&) noexcept -> error &; + + // Return the cause of this error. + [[nodiscard]] auto cause(this error const &) -> std::shared_ptr; + + // Return the root cause of this error, which may be this object. + // For errors caused by an OS error, this will typically be the + // error_code error. + [[nodiscard]] auto root_cause(this error const &) -> error const &; + + // Format this error as a string. + [[nodiscard]] auto str(this error const &) -> std::string; + + // Return this error's error_code, if any. + [[nodiscard]] auto code(this error const &) + -> std::optional; + + // Return this error's error_condition, if any. + [[nodiscard]] auto condition(this error const &) + -> std::optional; + + [[nodiscard]] auto what() const noexcept -> char const * final; + +private: + friend auto operator==(error const &, error const &) -> bool; + friend auto operator<=>(error const &, error const &) + -> std::strong_ordering; + + // This error. + error_t m_error = make_error_code(std::errc()); + + // The cause of this error, if any. + std::shared_ptr m_cause; + + // For std::exception::what(), we need to keep the string valid + // until we're destroyed. + mutable std::optional m_what; +}; + +/* + * Format an error and its cause(s) as a string. + */ +export [[nodiscard]] auto to_string(error const &) -> std::string; + +// Compare an error to another error. This only compares the error itself, +// not any nested causes. +export [[nodiscard]] auto operator==(error const &, error const &) + -> bool; +export [[nodiscard]] auto operator<=>(error const &, error const &) + -> std::strong_ordering; + +// Compare an error to an std::error_code. +export [[nodiscard]] auto operator==(error const &, std::error_code const &) + -> bool; + +// Compare an error to an std::error_condition. +export [[nodiscard]] auto operator==(error const &, + std::error_condition const &) + -> bool; + +// Compare an error to an std::error_code enum. +export [[nodiscard]] auto operator==(error const &lhs, auto rhs) -> bool +requires(std::is_error_code_enum::value) +{ + return lhs.code() == rhs; +} + +// Compare an error to an std::error_condition enum. +export [[nodiscard]] auto operator==(error const &lhs, auto rhs) -> bool +requires(std::is_error_condition_enum::value) +{ + return lhs.condition() == rhs; +} + +// Print an error to an ostream. +export [[nodiscard]] auto operator<<(std::ostream &, error const &) + -> std::ostream &; + +} // namespace nihil + +// Make error formattable. +export template<> +struct std::formatter +{ + template + constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator + { + return ctx.begin(); + } + + template + auto format(nihil::error const &e, FormatContext &ctx) const + -> FormatContext::iterator + { + return std::ranges::copy(to_string(e), ctx.out()).out; + } +}; diff --git a/nihil.error/test.cc b/nihil.error/test.cc new file mode 100644 index 0000000..9b3eef1 --- /dev/null +++ b/nihil.error/test.cc @@ -0,0 +1,169 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +import nihil.error; + +TEST_CASE("error: invariants", "[nihil]") +{ + static_assert(std::destructible); + static_assert(std::default_initializable); + static_assert(std::move_constructible); + static_assert(std::copy_constructible); + static_assert(std::equality_comparable); + static_assert(std::totally_ordered); + static_assert(std::swappable); + static_assert(std::regular); +} + +TEST_CASE("error: construct from string", "[nihil]") +{ + using namespace nihil; + + auto e = error("an error"); + REQUIRE(e.str() == to_string(e)); + REQUIRE(to_string(e) == "an error"); + REQUIRE(std::format("{}", e) == to_string(e)); +} + +TEST_CASE("error: construct from std::error_condition", "[nihil]") +{ + using namespace nihil; + + auto code = std::make_error_condition(std::errc::invalid_argument); + auto e = error(code); + + REQUIRE(!e.cause()); + REQUIRE(e.code().has_value() == false); + REQUIRE(e.condition().has_value() == true); + + REQUIRE(e == std::errc::invalid_argument); + REQUIRE(e != std::errc::no_such_file_or_directory); + + REQUIRE(e.str() == to_string(e)); + REQUIRE(to_string(e) == std::strerror(EINVAL)); + REQUIRE(std::format("{}", e) == to_string(e)); +} + +TEST_CASE("error: construct from std::errc", "[nihil]") +{ + using namespace nihil; + + auto e = error(std::errc::invalid_argument); + + REQUIRE(!e.cause()); + REQUIRE(e.code().has_value() == false); + REQUIRE(e.condition().has_value() == true); + + REQUIRE(e == std::errc::invalid_argument); + REQUIRE(e != std::errc::no_such_file_or_directory); + + REQUIRE(e.str() == to_string(e)); + REQUIRE(to_string(e) == std::strerror(EINVAL)); + REQUIRE(std::format("{}", e) == to_string(e)); +} + +TEST_CASE("error: compound error", "[nihil]") +{ + using namespace std::literals; + using namespace nihil; + + auto e = error("cannot open file", + error(std::errc::no_such_file_or_directory)); + + REQUIRE(e.cause()); + REQUIRE(e.code().has_value() == false); + REQUIRE(e.condition().has_value() == false); + + REQUIRE(*e.cause() == std::errc::no_such_file_or_directory); + REQUIRE(e.str() == "cannot open file"); + REQUIRE(to_string(e) == ("cannot open file: "s + + std::strerror(ENOENT))); + REQUIRE(std::format("{}", e) == to_string(e)); +} + +TEST_CASE("error: operator== with strings", "[nihil]") +{ + using namespace nihil; + + auto e1 = error("error"); + auto e2 = error("error"); + auto e3 = error("an error"); + + REQUIRE(e1 == e2); + REQUIRE(e1 != e3); +} + +TEST_CASE("error: operator< with strings", "[nihil]") +{ + using namespace nihil; + + auto e1 = error("aaa"); + auto e2 = error("zzz"); + + REQUIRE(e1 < e2); +} + +TEST_CASE("error: operator== with a cause", "[nihil]") +{ + using namespace nihil; + + auto e1 = error("error", error("cause 1")); + auto e2 = error("error", error("cause 2")); + + REQUIRE(e1 == e2); +} + +TEST_CASE("error: operator== with error_conditions", "[nihil]") +{ + using namespace nihil; + + auto e1 = error(std::errc::invalid_argument); + auto e2 = error(std::errc::invalid_argument); + auto e3 = error(std::errc::permission_denied); + + REQUIRE(e1 == e2); + REQUIRE(e1 != e3); +} + +TEST_CASE("error: std::format with string", "[nihil]") +{ + using namespace nihil; + + auto err = error("an error"); + REQUIRE(std::format("{}", err) == "an error"); +} + +TEST_CASE("error: std::format with std::errc", "[nihil]") +{ + using namespace nihil; + + auto err = error(std::errc::invalid_argument); + REQUIRE(std::format("{}", err) == std::strerror(EINVAL)); +} + +TEST_CASE("error: std::format with cause", "[nihil]") +{ + using namespace nihil; + + auto err = error("an error", std::errc::invalid_argument); + REQUIRE(std::format("{}", err) == "an error: Invalid argument"); +} + +TEST_CASE("error: throw and catch", "[nihil]") +{ + using namespace std::literals; + using namespace nihil; + + try { + throw error("oh no", error(std::errc::invalid_argument)); + } catch (std::exception const &exc) { + REQUIRE(exc.what() == "oh no: Invalid argument"s); + } +} diff --git a/nihil.generator/CMakeLists.txt b/nihil.generator/CMakeLists.txt new file mode 100644 index 0000000..7d278a8 --- /dev/null +++ b/nihil.generator/CMakeLists.txt @@ -0,0 +1,23 @@ +# This source code is released into the public domain. + +add_library(nihil.generator STATIC) +target_sources(nihil.generator + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + generator.ccm +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.generator.test test.cc) + target_link_libraries(nihil.generator.test PRIVATE + nihil.generator + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.generator.test) +endif() diff --git a/nihil.generator/generator.ccm b/nihil.generator/generator.ccm new file mode 100644 index 0000000..f022287 --- /dev/null +++ b/nihil.generator/generator.ccm @@ -0,0 +1,692 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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 +#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.generator/test.cc b/nihil.generator/test.cc new file mode 100644 index 0000000..d167f30 --- /dev/null +++ b/nihil.generator/test.cc @@ -0,0 +1,56 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +import nihil.generator; + +TEST_CASE("generator: basic", "[generator]") +{ + auto fn = [] () -> nihil::generator { + co_yield 1; + co_yield 2; + co_yield 3; + }; + + auto values = std::vector(); + std::ranges::copy(fn(), std::back_inserter(values)); + + REQUIRE(values == std::vector{1, 2, 3}); +} + +TEST_CASE("generator: exceptions", "[generator]") +{ + auto fn = [] () -> nihil::generator { + 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 { + co_yield 1; + co_yield 2; + co_yield 3; + }; + + auto fn2 = [&fn1] -> nihil::generator { + co_yield nihil::ranges::elements_of(fn1()); + }; + + auto values = std::vector(); + std::ranges::copy(fn2(), std::back_inserter(values)); + + REQUIRE(values == std::vector{1, 2, 3}); +} diff --git a/nihil.guard/CMakeLists.txt b/nihil.guard/CMakeLists.txt new file mode 100644 index 0000000..bba4284 --- /dev/null +++ b/nihil.guard/CMakeLists.txt @@ -0,0 +1,23 @@ +# This source code is released into the public domain. + +add_library(nihil.guard STATIC) +target_sources(nihil.guard + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + guard.ccm +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.guard.test test.cc) + target_link_libraries(nihil.guard.test PRIVATE + nihil.guard + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.guard.test) +endif() diff --git a/nihil.guard/guard.ccm b/nihil.guard/guard.ccm new file mode 100644 index 0000000..c586a20 --- /dev/null +++ b/nihil.guard/guard.ccm @@ -0,0 +1,51 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#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) : m_func(std::move(func)) {} + + /* + * We are being destroyed, so call the callable. + * If the callable throws, std::terminate() will be called. + */ + ~guard() { + if (m_func) + std::invoke(*m_func); + } + + // Release the guard. This turns the destructor into a no-op. + void release() noexcept { + m_func.reset(); + } + + // Not default-constructible, movable 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 m_func; +}; + +} // namespace nihil diff --git a/nihil.guard/test.cc b/nihil.guard/test.cc new file mode 100644 index 0000000..11f7d37 --- /dev/null +++ b/nihil.guard/test.cc @@ -0,0 +1,20 @@ +/* + * This source code is released into the public domain. + */ + +#include + +import nihil.guard; + +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/nihil.match/CMakeLists.txt b/nihil.match/CMakeLists.txt new file mode 100644 index 0000000..d7c5875 --- /dev/null +++ b/nihil.match/CMakeLists.txt @@ -0,0 +1,23 @@ +# This source code is released into the public domain. + +add_library(nihil.match STATIC) +target_sources(nihil.match + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + match.ccm +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.match.test test.cc) + target_link_libraries(nihil.match.test PRIVATE + nihil.match + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.match.test) +endif() diff --git a/nihil.match/match.ccm b/nihil.match/match.ccm new file mode 100644 index 0000000..d67bd0b --- /dev/null +++ b/nihil.match/match.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include + +export module nihil.match; + +namespace nihil { + +export template +struct match : Ts... { using Ts::operator()...; }; + +export template +[[nodiscard]] constexpr decltype(auto) operator| + (std::variant const &v, match const &match) +{ + return std::visit(match, v); +} + +} // namespace nihil diff --git a/nihil.match/test.cc b/nihil.match/test.cc new file mode 100644 index 0000000..7dd1c34 --- /dev/null +++ b/nihil.match/test.cc @@ -0,0 +1,34 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include + +#include + +import nihil.match; + +TEST_CASE("match", "[nihil]") +{ + using namespace nihil; + using namespace std::literals; + + auto v = std::variant(42); + + auto s = v | match { + [](int) { return "int"s; }, + [](std::string const &) { return "string"s; } + }; + + REQUIRE(s == "int"); + + v = "test"s; + + s = v | match { + [](int) { return "int"s; }, + [](std::string const &) { return "string"s; } + }; + + REQUIRE(s == "string"); +} diff --git a/nihil.monad/CMakeLists.txt b/nihil.monad/CMakeLists.txt new file mode 100644 index 0000000..b0fa095 --- /dev/null +++ b/nihil.monad/CMakeLists.txt @@ -0,0 +1,24 @@ +# This source code is released into the public domain. + +add_library(nihil.monad STATIC) +target_link_libraries(nihil.monad PRIVATE nihil.error) +target_sources(nihil.monad + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + monad.ccm +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.monad.test test.cc) + target_link_libraries(nihil.monad.test PRIVATE + nihil.monad + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.monad.test) +endif() diff --git a/nihil.monad/monad.ccm b/nihil.monad/monad.ccm new file mode 100644 index 0000000..b462e4c --- /dev/null +++ b/nihil.monad/monad.ccm @@ -0,0 +1,289 @@ +/* + * From https://github.com/toby-allsopp/coroutine_monad + * + * Copyright (c) 2017 Toby Allsopp + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +module; + +#include +#include +#include +#include +#include + +export module nihil.monad; + +namespace nihil { + +/********************************************************************** + * return_object_holder + */ + +// An object that starts out unitialized. Initialized by a call to emplace. +template +using deferred = std::optional; + +template +struct return_object_holder { + // The staging object that is returned (by copy/move) to the caller of + // the coroutine. + deferred stage; + return_object_holder*& p; + + // When constructed, we assign a pointer to ourselves to the supplied + // reference to pointer. + return_object_holder(return_object_holder*& p) + : stage{} + , p(p) + { + p = this; + } + + // Copying doesn't make any sense (which copy should the pointer refer + // to?). + return_object_holder(return_object_holder const&) = delete; + + // To move, we just update the pointer to point at the new object. + return_object_holder(return_object_holder&& other) + : stage(std::move(other.stage)) + , p(other.p) + { + p = this; + } + + // Assignment doesn't make sense. + void operator=(return_object_holder const&) = delete; + void operator=(return_object_holder&&) = delete; + + // A non-trivial destructor is required until + // https://bugs.llvm.org//show_bug.cgi?id=28593 is fixed. + ~return_object_holder() {} + + // Construct the staging value; arguments are perfect forwarded to T's + // constructor. + template + void emplace(Args&&... args) + { + stage.emplace(std::forward(args)...); + } + + // We assume that we will be converted only once, so we can move from + // the staging object. We also assume that `emplace` has been called + // at least once. + operator T() + { + return std::move(*stage); + } +}; + +template +auto make_return_object_holder(return_object_holder*& p) +{ + return return_object_holder{p}; +} + +/********************************************************************** + * std::optional + */ + +template +struct optional_promise { + return_object_holder>* data; + + auto get_return_object() + { + return make_return_object_holder(data); + } + + auto initial_suspend() noexcept -> std::suspend_never + { + return {}; + } + + auto final_suspend() noexcept -> std::suspend_never + { + return {}; + } + + void return_value(T x) + { + data->emplace(std::move(x)); + } + + void unhandled_exception() + { + std::rethrow_exception(std::current_exception()); + } +}; + +} // namespace nihil + +export template +struct std::coroutine_traits, Args...> { + using promise_type = nihil::optional_promise; +}; + +namespace nihil { + +template +struct optional_awaitable { + std::optional o; + + auto await_ready() + { + return o.has_value(); + } + + auto await_resume() + { + return *o; + } + + template + void await_suspend(std::coroutine_handle> h) + { + h.promise().data->emplace(std::nullopt); + h.destroy(); + } +}; + +} // namespace nihil + +namespace std { + +export template +auto operator co_await(std::optional o) { + return nihil::optional_awaitable{std::move(o)}; +} + +} // namespace std + +/********************************************************************** + * std::expected + */ + +namespace nihil { + +export template +struct expected_promise_base { + return_object_holder>* data; + + auto get_return_object() + { + return make_return_object_holder(data); + } + + auto initial_suspend() noexcept -> std::suspend_never + { + return {}; + } + + auto final_suspend() noexcept -> std::suspend_never + { + return {}; + } + + void unhandled_exception() + { + std::rethrow_exception(std::current_exception()); + } +}; + +export template +struct expected_promise : expected_promise_base { + void return_value(this expected_promise &self, std::unexpected err) + { + self.data->emplace(std::move(err)); + } + + void return_value(this expected_promise &self, T o) + { + self.data->emplace(std::move(o)); + } +}; + +export template +struct expected_promise : expected_promise_base { + void return_value(this expected_promise &self, std::unexpected err) + { + self.data->emplace(std::move(err)); + } + + void return_value(this expected_promise &self, + std::expected o) + { + self.data->emplace(std::move(o)); + } +}; + +} // namespace nihil + +export template +struct std::coroutine_traits, Args...> { + using promise_type = nihil::expected_promise; +}; + +namespace nihil { + +export template +struct expected_awaitable_base { + std::expected o; + + auto await_ready() + { + return o.has_value(); + } + + template + void await_suspend(std::coroutine_handle

h) + { + h.promise().data->emplace(std::unexpected(o.error())); + h.destroy(); + } +}; + +export template +struct expected_awaitable : expected_awaitable_base { + auto await_resume(this expected_awaitable &self) + { + return std::move(*self.o); + } +}; + +export template +struct expected_awaitable : expected_awaitable_base { + auto await_resume(this expected_awaitable &) + { + return std::expected(); + } +}; + +} // namespace nihil + +namespace std { + +export template +auto operator co_await(std::expected o) { + return nihil::expected_awaitable{std::move(o)}; +} + +} // namespace std diff --git a/nihil.monad/test.cc b/nihil.monad/test.cc new file mode 100644 index 0000000..347acdb --- /dev/null +++ b/nihil.monad/test.cc @@ -0,0 +1,69 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +import nihil.error; +import nihil.monad; + +TEST_CASE("monad: co_await std::optional<> with value", "[nihil]") +{ + auto get_value = [] -> std::optional { + return 42; + }; + + auto try_get_value = [&get_value] -> std::optional { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(o == 42); +} + +TEST_CASE("monad: co_await std::optional<> without value", "[nihil]") +{ + auto get_value = [] -> std::optional { + return {}; + }; + + auto try_get_value = [&get_value] -> std::optional { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(!o.has_value()); +} + +TEST_CASE("monad: co_await std::expected<> with value", "[nihil]") +{ + auto get_value = [] -> std::expected { + return 42; + }; + + auto try_get_value = [&get_value] -> std::expected { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(o == 42); +} + +TEST_CASE("monad: co_await std::expected<> with error", "[nihil]") +{ + auto get_value = [] -> std::expected { + return std::unexpected("error"); + }; + + auto try_get_value = [&get_value] -> std::expected { + co_return co_await get_value(); + }; + + auto o = try_get_value(); + REQUIRE(!o); + REQUIRE(o.error() == "error"); +} diff --git a/nihil.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt new file mode 100644 index 0000000..db5e5aa --- /dev/null +++ b/nihil.posix/CMakeLists.txt @@ -0,0 +1,51 @@ +# This source code is released into the public domain. + +add_library(nihil.posix STATIC) +target_link_libraries(nihil.posix PRIVATE nihil.error nihil.guard nihil.monad) + +target_sources(nihil.posix + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + nihil.posix.ccm + argv.ccm + ensure_dir.ccm + exec.ccm + fd.ccm + open.ccm + process.ccm + read_file.ccm + rename.ccm + spawn.ccm + write_file.ccm + + PRIVATE + argv.cc + ensure_dir.cc + exec.cc + getenv.cc + fd.cc + find_in_path.cc + open.cc + process.cc + rename.cc +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.posix.test + test_fd.cc + test_getenv.cc + test_spawn.cc + ) + + target_link_libraries(nihil.posix.test PRIVATE + nihil.posix + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.posix.test) +endif() diff --git a/nihil.posix/argv.cc b/nihil.posix/argv.cc new file mode 100644 index 0000000..e6b1389 --- /dev/null +++ b/nihil.posix/argv.cc @@ -0,0 +1,65 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +module nihil.posix; + +namespace nihil { + +argv::argv() = default; +argv::argv(argv &&) noexcept = default; +auto argv::operator=(this argv &, argv &&) -> argv & = default; + +argv::~argv() +{ + for (auto *arg : m_args) + delete[] arg; +} + +auto argv::data(this argv const &self) -> char const * const * +{ + return self.m_args.data(); +} + +auto argv::data(this argv &self) -> char * const * +{ + return self.m_args.data(); +} + +auto argv::size(this argv const &self) +{ + return self.m_args.size(); +} + +auto argv::begin(this argv const &self) +{ + return self.m_args.begin(); +} + +auto argv::end(this argv const &self) +{ + return self.m_args.end(); +} + + +auto argv::add_arg(this argv &self, std::string_view arg) -> void +{ + // Create a nul-terminated C string. + auto ptr = std::make_unique(arg.size() + 1); + std::ranges::copy(arg, ptr.get()); + ptr[arg.size()] = '\0'; + + // Ensure we won't throw when emplacing the pointer. + self.m_args.reserve(self.m_args.size() + 1); + self.m_args.emplace_back(ptr.release()); +} + +} // namespace nihil + diff --git a/nihil.posix/argv.ccm b/nihil.posix/argv.ccm new file mode 100644 index 0000000..6f60f4b --- /dev/null +++ b/nihil.posix/argv.ccm @@ -0,0 +1,78 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +export module nihil.posix:argv; + +namespace nihil { + +/* + * argv: stores a null-terminated array of nul-terminated C strings. + * argv::data() is suitable for passing to ::execv(). + * + * Create an argv using argv::from_range(), which takes a range of + * string-like objects. + */ + +export struct argv { + /* + * Create a new argv from a range. + */ + argv(std::from_range_t, std::ranges::range auto &&args) + { + for (auto &&arg : args) + add_arg(std::string_view(arg)); + + m_args.push_back(nullptr); + } + + /* + * Create an argv from an initializer list. + */ + template + explicit argv(std::initializer_list &&args) + : argv(std::from_range, std::forward(args)) + { + } + + // Movable. + argv(argv &&) noexcept; + auto operator=(this argv &, argv &&other) -> argv &; + + // Not copyable. TODO: for completeness, it probably should be. + argv(argv const &) = delete; + auto operator=(this argv &, argv const &other) -> argv& = delete; + + ~argv(); + + // Access the stored arguments. + [[nodiscard]] auto data(this argv const &self) -> char const * const *; + [[nodiscard]] auto data(this argv &self) -> char * const *; + [[nodiscard]] auto size(this argv const &self); + + // Range access + [[nodiscard]] auto begin(this argv const &self); + [[nodiscard]] auto end(this argv const &self); + +private: + // Use the from_range() factory method to create new instances. + argv(); + + // The argument pointers, including the null terminator. + // This can't be a vector because we need an array of + // char pointers to pass to exec. + std::vector m_args; + + // Add a new argument to the array. + auto add_arg(this argv &self, std::string_view arg) -> void; +}; + +} // namespace nihil + diff --git a/nihil.posix/ensure_dir.cc b/nihil.posix/ensure_dir.cc new file mode 100644 index 0000000..88e8898 --- /dev/null +++ b/nihil.posix/ensure_dir.cc @@ -0,0 +1,30 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +auto ensure_dir(std::filesystem::path const &dir) -> std::expected +{ + auto err = std::error_code(); + + std::filesystem::create_directories(dir, err); + + if (err) + return std::unexpected(error(err)); + + return {}; +} + +} // namespace nihil diff --git a/nihil.posix/ensure_dir.ccm b/nihil.posix/ensure_dir.ccm new file mode 100644 index 0000000..fa92a90 --- /dev/null +++ b/nihil.posix/ensure_dir.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +export module nihil.posix:ensure_dir; + +import nihil.error; + +namespace nihil { + +/* + * Create the given directory and any parent directories. + */ +export [[nodiscard]] auto ensure_dir(std::filesystem::path const &dir) + -> std::expected; + +} // namespace nihil + diff --git a/nihil.posix/exec.cc b/nihil.posix/exec.cc new file mode 100644 index 0000000..5bdcbf7 --- /dev/null +++ b/nihil.posix/exec.cc @@ -0,0 +1,71 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +#include +#include +#include + +extern char **environ; + +module nihil.posix; + +import nihil.error; +import nihil.monad; + +namespace nihil { + +fexecv::fexecv(fd &&execfd, argv &&args) noexcept + : m_execfd(std::move(execfd)) + , m_args(std::move(args)) +{ +} + +auto fexecv::exec(this fexecv &self) + -> std::expected +{ + ::fexecve(self.m_execfd.get(), self.m_args.data(), environ); + return std::unexpected(error("fexecve failed", + error(std::errc(errno)))); +} + +fexecv::fexecv(fexecv &&) noexcept = default; +auto fexecv::operator=(this fexecv &, fexecv &&) noexcept -> fexecv& = default; + +auto execv(std::string_view path, argv &&argv) + -> std::expected +{ + auto file = co_await open(path, O_EXEC) + .transform_error([&] (error cause) { + return error(std::format("could not open {}", path), + std::move(cause)); + }); + + co_return fexecv(std::move(file), std::move(argv)); +} + +auto execvp(std::string_view file, argv &&argv) + -> std::expected +{ + auto execfd = find_in_path(file); + if (!execfd) + return std::unexpected(error( + std::format("executable not found in path: {}", file))); + return fexecv(std::move(*execfd), std::move(argv)); +} + +auto shell(std::string_view const &command) + -> std::expected +{ + return execl("/bin/sh", "sh", "-c", command); +} + +} // namespace nihil diff --git a/nihil.posix/exec.ccm b/nihil.posix/exec.ccm new file mode 100644 index 0000000..6098318 --- /dev/null +++ b/nihil.posix/exec.ccm @@ -0,0 +1,105 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * Exec providers, mostly used for spawn(). + */ + +#include +#include + +export module nihil.posix:exec; + +import nihil.error; +import :argv; +import :fd; + +namespace nihil { + +/* + * A concept to mark spawn executors. + */ + +export struct exec_tag{}; + +export template +concept executor = + requires (T e) { + std::same_as::tag>; + { e.exec() }; + }; + +/* + * fexecv: use a file descriptor and an argument vector to call ::fexecve(). + * This is the lowest-level executor which all others are implemented + * in terms of. + * + * TODO: Should have a way to pass the environment (envp). + */ +export struct fexecv final { + using tag = exec_tag; + + fexecv(fd &&execfd, argv &&args) noexcept; + + [[nodiscard]] auto exec(this fexecv &self) + -> std::expected; + + // Movable + fexecv(fexecv &&) noexcept; + auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv&; + + // Not copyable (because we hold the open fd object) + fexecv(fexecv const &) = delete; + auto operator=(this fexecv &, fexecv const &) -> fexecv& = delete; + +private: + fd m_execfd; + argv m_args; +}; + +/* + * execv: equivalent to fexecv(), except the command is passed as + * a pathname instead of a file descriptor. Does not search $PATH. + */ +export [[nodiscard]] auto execv(std::string_view path, argv &&argv) + -> std::expected; + +/* + * execvp: equivalent to fexecv(), except the command is passed as + * a filename instead of a file descriptor. If the filename is not + * an absolute path, it will be searched for in $PATH. + */ +export [[nodiscard]] auto execvp(std::string_view file, argv &&argv) + -> std::expected; + +/* + * execl: equivalent to execv, except the arguments are passed as a + * variadic pack of string-like objects. + */ +export [[nodiscard]] auto execl(std::string_view path, auto &&...args) + -> std::expected +{ + return execv(path, argv({std::string_view(args)...})); +} + +/* + * execlp: equivalent to execvp, except the arguments are passed as a + * variadic pack of string-like objects. + */ +export [[nodiscard]] auto execlp(std::string_view file, auto &&...args) + -> std::expected +{ + return execvp(file, argv({std::string_view(args)...})); +} + +/* + * shell: run the process by invoking /bin/sh -c with the single argument, + * equivalent to system(3). + */ +export [[nodiscard]] auto shell(std::string_view const &command) + -> std::expected; + +} // namespace nihil diff --git a/nihil.posix/fd.cc b/nihil.posix/fd.cc new file mode 100644 index 0000000..6d5e54f --- /dev/null +++ b/nihil.posix/fd.cc @@ -0,0 +1,220 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +#include +#include +#include +#include +#include + +module nihil.posix; + +import nihil.error; +import nihil.monad; + +namespace nihil { + +fd::fd() noexcept = default; + +fd::fd(int fileno) noexcept + : m_fileno(fileno) +{ +} + +fd::~fd() +{ + if (*this) + std::ignore = this->close(); +} + +fd::fd(fd &&other) noexcept + : m_fileno(std::exchange(other.m_fileno, invalid_fileno)) +{ +} + +auto fd::operator=(this fd &self, fd &&other) noexcept -> fd & +{ + if (&self != &other) + self.m_fileno = std::exchange(other.m_fileno, invalid_fileno); + return self; +} + +fd::operator bool(this fd const &self) noexcept +{ + return self.m_fileno != invalid_fileno; +} + +auto fd::close(this fd &self) -> std::expected +{ + auto const ret = ::close(self.get()); + self.m_fileno = invalid_fileno; + + if (ret == 0) + return {}; + + return std::unexpected(error(std::errc(errno))); +} + +auto fd::get(this fd const &self) -> int +{ + if (self) + return self.m_fileno; + throw std::logic_error("Attempt to call get() on invalid fd"); +} + +auto fd::release(this fd &&self) -> int +{ + if (self) + return std::exchange(self.m_fileno, invalid_fileno); + throw std::logic_error("Attempt to release an invalid fd"); +} + +auto dup(fd const &self) -> std::expected +{ + auto const newfd = ::dup(self.get()); + if (newfd != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +auto dup(fd const &self, int newfd) -> std::expected +{ + auto const ret = ::dup2(self.get(), newfd); + if (ret != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +auto raw_dup(fd const &self) -> std::expected +{ + auto const newfd = ::dup(self.get()); + if (newfd != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +auto raw_dup(fd const &self, int newfd) -> std::expected +{ + auto const ret = ::dup2(self.get(), newfd); + if (ret != -1) + return newfd; + + return std::unexpected(error(std::errc(errno))); +} + +auto getflags(fd const &self) -> std::expected +{ + auto const flags = ::fcntl(self.get(), F_GETFL); + if (flags != -1) + return flags; + + return std::unexpected(error(std::errc(errno))); +} + +auto replaceflags(fd &self, int newflags) -> std::expected +{ + auto const ret = ::fcntl(self.get(), F_SETFL, newflags); + if (ret == 0) + return {}; + + return std::unexpected(error(std::errc(errno))); +} + +auto setflags(fd &self, int newflags) -> std::expected +{ + auto flags = co_await getflags(self); + + flags |= newflags; + co_await replaceflags(self, flags); + + co_return flags; +} + +auto clearflags(fd &self, int clrflags) -> std::expected +{ + auto flags = co_await getflags(self); + + flags &= ~clrflags; + co_await replaceflags(self, flags); + + co_return flags; +} + +auto getfdflags(fd const &self) -> std::expected +{ + auto const flags = ::fcntl(self.get(), F_GETFD); + if (flags != -1) + return flags; + + return std::unexpected(error(std::errc(errno))); +} + +auto replacefdflags(fd &self, int newflags) -> std::expected +{ + auto const ret = ::fcntl(self.get(), F_SETFD, newflags); + if (ret != -1) + return {}; + + return std::unexpected(error(std::errc(errno))); +} + +auto setfdflags(fd &self, int newflags) -> std::expected +{ + auto flags = co_await getfdflags(self); + + flags |= newflags; + co_await replacefdflags(self, flags); + + co_return flags; +} + +auto clearfdflags(fd &self, int clrflags) -> std::expected +{ + auto flags = co_await getfdflags(self); + + flags &= ~clrflags; + co_await replacefdflags(self, flags); + + co_return flags; +} + +auto pipe() -> std::expected, error> +{ + auto fds = std::array{}; + + if (auto const ret = ::pipe(fds.data()); ret != 0) + return std::unexpected(error(std::errc(errno))); + + return {{fd(fds[0]), fd(fds[1])}}; +} + +auto fd::write(this fd &self, std::span buffer) + -> std::expected +{ + auto const ret = ::write(self.get(), buffer.data(), buffer.size()); + if (ret >= 0) + return ret; + + return std::unexpected(error(std::errc(errno))); +} + +auto fd::read(this fd &self, std::span buffer) + -> std::expected, error> +{ + auto const ret = ::read(self.get(), buffer.data(), buffer.size()); + if (ret >= 0) + return buffer.subspan(0, ret); + + return std::unexpected(error(std::errc(errno))); +} + +} // namespace nihil diff --git a/nihil.posix/fd.ccm b/nihil.posix/fd.ccm new file mode 100644 index 0000000..b937f46 --- /dev/null +++ b/nihil.posix/fd.ccm @@ -0,0 +1,157 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +export module nihil.posix:fd; + +import nihil.error; +import nihil.monad; + +namespace nihil { + +/* + * fd: a file descriptor. + */ + +export struct fd final { + // Construct an empty (invalid) fd. + fd() noexcept; + + // Construct an fd from an exising file destrictor, taking ownership. + fd(int fd_) noexcept; + + // Destructor. Close the fd, discarding any errors. + ~fd(); + + // Move from another fd, leaving the moved-from fd in an invalid state. + fd(fd &&other) noexcept; + auto operator=(this fd &, fd &&other) noexcept -> fd &; + + // Not copyable. + fd(fd const &) = delete; + fd& operator=(this fd &, fd const &) = delete; + + // Return true if this fd is valid (open). + [[nodiscard]] explicit operator bool(this fd const &self) noexcept; + + // Close the wrapped fd. + [[nodiscard]] auto close(this fd &self) -> std::expected; + + // Return the stored fd. + [[nodiscard]] auto get(this fd const &self) -> int; + + // Release the stored fd and return it. The caller must close it. + [[nodiscard]] auto release(this fd &&self) -> int; + + // Write data from the provided buffer to the fd. Returns the + // number of bytes written. + [[nodiscard]] auto write(this fd &self, std::span) + -> std::expected; + + // Read data from the fd to the provided buffer. Returns a + // subspan containing the data which was read. + [[nodiscard]] auto read(this fd &self, std::span) + -> std::expected, error>; + +private: + static constexpr int invalid_fileno = -1; + + int m_fileno = invalid_fileno; +}; + +// Create a copy of this fd by calling dup(). +export [[nodiscard]] auto dup(fd const &self) -> std::expected; + +// 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 [[nodiscard]] auto dup(fd const &self, int newfd) + -> std::expected; + +// Create a copy of this fd by calling dup(). +export [[nodiscard]] auto raw_dup(fd const &self) + -> std::expected; + +// Create a copy of this fd by calling dup2(). +export [[nodiscard]] auto raw_dup(fd const &self, int newfd) + -> std::expected; + +// Return the fnctl flags for this fd. +export [[nodiscard]] auto getflags(fd const &self) + -> std::expected; + +// Replace the fnctl flags for this fd. +export [[nodiscard]] auto replaceflags(fd &self, int newflags) + -> std::expected; + +// Add bits to the fcntl flags for this fd. Returns the new flags. +export [[nodiscard]] auto setflags(fd &self, int newflags) + -> std::expected; + +// Remove bits from the fcntl flags for this fd. Returns the new flags. +export [[nodiscard]] auto clearflags(fd &self, int clrflags) + -> std::expected; + +// Return the fd flags for this fd. +export [[nodiscard]] auto getfdflags(fd const &self) + -> std::expected; + +// Replace the fd flags for this fd. +export [[nodiscard]] auto replacefdflags(fd &self, int newflags) + -> std::expected; + +// Add bits to the fd flags for this fd. Returns the new flags. +export [[nodiscard]] auto setfdflags(fd &self, int newflags) + -> std::expected; + +// Remove bits from the fd flags for this fd. Returns the new flags. +export [[nodiscard]] auto clearfdflags(fd &self, int clrflags) + -> std::expected; + +// Create two fds by calling pipe() and return them. +export [[nodiscard]] auto pipe() -> std::expected, error>; + +/* + * Write data to a file descriptor from the provided range. Returns the + * number of bytes written. + */ +export [[nodiscard]] auto write(fd &file, + std::ranges::contiguous_range auto &&range) + -> std::expected +requires(sizeof(std::ranges::range_value_t) == 1) +{ + return file.write(as_bytes(std::span(range))); +} + +/* + * Read data from a file descriptor into the provided buffer. Returns a + * span containing the data that was read. + */ +export [[nodiscard]] auto read(fd &file, + std::ranges::contiguous_range auto &&range) + -> std::expected< + std::span>, + error> +requires(sizeof(std::ranges::range_value_t) == 1) +{ + auto bspan = as_writable_bytes(std::span(range)); + auto rspan = co_await file.read(bspan); + co_return std::span(range).subspan(0, rspan.size()); +} + +} // namespace nihil diff --git a/nihil.posix/find_in_path.cc b/nihil.posix/find_in_path.cc new file mode 100644 index 0000000..6be963c --- /dev/null +++ b/nihil.posix/find_in_path.cc @@ -0,0 +1,52 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +#include +#include + +module nihil.posix; + +namespace nihil { + +auto find_in_path(std::filesystem::path const &file) + -> std::optional +{ + using namespace std::literals; + + auto try_open = + [](std::filesystem::path const &file) -> std::optional + { + auto ret = open(file, O_EXEC); + if (ret) + return {std::move(*ret)}; + return {}; + }; + + // Absolute pathname skips the search. + if (file.is_absolute()) + return try_open(file); + + auto path = getenv("PATH").value_or(_PATH_DEFPATH); + + for (auto &&dir : path | std::views::split(':')) { + // An empty $PATH element means cwd. + auto sdir = dir.empty() + ? std::filesystem::path(".") + : std::filesystem::path(std::string_view(dir)); + + if (auto ret = try_open(sdir / file); ret) + return ret; + } + + return {}; +} + +} // namespace nihil diff --git a/nihil.posix/getenv.cc b/nihil.posix/getenv.cc new file mode 100644 index 0000000..36df950 --- /dev/null +++ b/nihil.posix/getenv.cc @@ -0,0 +1,45 @@ + +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +#include + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +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(error(std::errc(errno))); + } +} + +} // namespace nihil diff --git a/nihil.posix/nihil.posix.ccm b/nihil.posix/nihil.posix.ccm new file mode 100644 index 0000000..9baecf8 --- /dev/null +++ b/nihil.posix/nihil.posix.ccm @@ -0,0 +1,45 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +export module nihil.posix; + +import nihil.error; + +export import :argv; +export import :ensure_dir; +export import :exec; +export import :fd; +export import :open; +export import :process; +export import :read_file; +export import :rename; +export import :spawn; +export import :write_file; + +export namespace nihil { + +/* + * Find a variable by the given name in the environment by calling getenv_r(). + */ + +[[nodiscard]] auto getenv(std::string_view varname) + -> std::expected; + + +/* + * Find an executable in $PATH, open it with O_EXEC and return the fd. + * If $PATH is not set, uses _PATH_DEFPATH. If the file can't be found + * or opened, returns std::nullopt. + */ +[[nodiscard]] auto find_in_path(std::filesystem::path const &file) + -> std::optional; + +} // namespace nihil diff --git a/nihil.posix/open.cc b/nihil.posix/open.cc new file mode 100644 index 0000000..9ef6538 --- /dev/null +++ b/nihil.posix/open.cc @@ -0,0 +1,31 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include + +#include +#include + +module nihil.posix; + +import nihil.error; +import :fd; + +namespace nihil { + +auto open(std::filesystem::path const &filename, int flags, int mode) + -> std::expected +{ + auto fdno = ::open(filename.c_str(), flags, mode); + if (fdno != -1) + return fd(fdno); + + return std::unexpected(error(std::errc(errno))); +} + +} // namespace nihil diff --git a/nihil.posix/open.ccm b/nihil.posix/open.ccm new file mode 100644 index 0000000..eaedacd --- /dev/null +++ b/nihil.posix/open.ccm @@ -0,0 +1,24 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +export module nihil.posix:open; + +import nihil.error; +import :fd; + +export namespace nihil { + +/* + * Open the given file and return an fd for it. + */ +[[nodiscard]] auto open(std::filesystem::path const &filename, + int flags, int mode = 0777) + -> std::expected; + +} // namespace nihil diff --git a/nihil.posix/process.cc b/nihil.posix/process.cc new file mode 100644 index 0000000..70e84b7 --- /dev/null +++ b/nihil.posix/process.cc @@ -0,0 +1,102 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +auto wait_result::okay(this wait_result const &self) -> bool +{ + return self.status() == 0; +} + +wait_result::operator bool(this wait_result const &self) +{ + return self.okay(); +} + +auto wait_result::status(this wait_result const &self) -> std::optional +{ + if (WIFEXITED(self._status)) + return WEXITSTATUS(self._status); + return {}; +} + +auto wait_result::signal(this wait_result const &self) -> std::optional +{ + if (WIFSIGNALED(self._status)) + return WTERMSIG(self._status); + return {}; +} + +wait_result::wait_result(int status) + : _status(status) +{} + +process::process(::pid_t pid) + : m_pid(pid) +{} + +process::~process() { + if (m_pid == -1) + return; + + auto status = int{}; + std::ignore = waitpid(m_pid, &status, WEXITED); +} + +process::process(process &&other) noexcept + : m_pid(std::exchange(other.m_pid, -1)) +{ +} + +auto process::operator=(this process &self, process &&other) noexcept + -> process & +{ + if (&self != &other) { + self.m_pid = std::exchange(other.m_pid, -1); + } + + return self; +} + +// Get the child's process id. +auto process::pid(this process const &self) noexcept -> ::pid_t +{ + return self.m_pid; +} + +auto process::wait(this process &&self) -> std::expected +{ + auto status = int{}; + auto ret = waitpid(self.m_pid, &status, WEXITED); + if (ret == -1) + return std::unexpected(error(std::errc(errno))); + + return wait_result(status); +} + +auto process::release(this process &&self) -> ::pid_t +{ + auto const ret = self.pid(); + self.m_pid = -1; + return ret; +} + +} // namespace nihil diff --git a/nihil.posix/process.ccm b/nihil.posix/process.ccm new file mode 100644 index 0000000..425deac --- /dev/null +++ b/nihil.posix/process.ccm @@ -0,0 +1,91 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +#include + +export module nihil.posix:process; + +import nihil.error; + +namespace nihil { + +/* + * wait_result: the exit status of a process. + */ +export struct wait_result final { + // Return true if the process exited normally with an exit code of + // zero, otherwise false. + [[nodiscard]] auto okay(this wait_result const &self) -> bool; + [[nodiscard]] explicit operator bool(this wait_result const &self); + + // Return the exit status, if any. + [[nodiscard]] auto status(this wait_result const &self) + -> std::optional; + + // Return the exit signal, if any. + [[nodiscard]] auto signal(this wait_result const &self) + -> std::optional; + +private: + friend struct process; + + int _status; + + // Construct a new wait_result from the output of waitpid(). + wait_result(int status); +}; + +/* + * process: represents a process we created, which can be waited for. + */ +export struct process final { + process() = delete; + + /* + * Create a new process from a pid, which must be a child of the + * current process. + */ + process(::pid_t pid); + + // When destroyed, we automatically wait for the process to + // avoid creating zombie processes. + ~process(); + + // Movable. + process(process &&) noexcept; + auto operator=(this process &, process &&) noexcept -> process &; + + // Not copyable. + process(process const &) = delete; + auto operator=(this process &, process const &) -> process & = delete; + + // Get the child's process id. + [[nodiscard]] auto pid(this process const &self) noexcept -> ::pid_t; + + /* + * Wait for this process to exit (by calling waitpid()) and return + * its exit status. This destroys the process state, leaving this + * object in a moved-from state. + */ + [[nodiscard]] auto wait(this process &&self) + -> std::expected; + + /* + * Release this process so we won't try to wait for it when + * destroying this object. + */ + [[nodiscard]] auto release(this process &&self) -> ::pid_t; + +private: + ::pid_t m_pid; +}; + +} // namespace nihil diff --git a/nihil.posix/read_file.ccm b/nihil.posix/read_file.ccm new file mode 100644 index 0000000..c950f67 --- /dev/null +++ b/nihil.posix/read_file.ccm @@ -0,0 +1,48 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +#include +#include + +export module nihil.posix:read_file; + +import nihil.error; +import nihil.monad; +import :fd; +import :open; + +namespace nihil { + +/* + * Read the contents of a file into an output iterator. + */ +export [[nodiscard]] auto +read_file(std::filesystem::path const &filename, + std::output_iterator auto &&iter) + -> std::expected +{ + auto file = co_await open(filename, O_RDONLY); + + auto constexpr bufsize = std::size_t{1024}; + auto buffer = std::array{}; + + for (;;) { + auto read_buf = co_await(read(file, buffer)); + if (read_buf.empty()) + co_return {}; + + std::ranges::copy(read_buf, iter); + } +} + +} // namespace nihil diff --git a/nihil.posix/rename.cc b/nihil.posix/rename.cc new file mode 100644 index 0000000..9203d08 --- /dev/null +++ b/nihil.posix/rename.cc @@ -0,0 +1,34 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +module nihil.posix; + +import nihil.error; + +namespace nihil { + +/* + * Rename a file. + */ +auto rename_file(std::filesystem::path const &oldp, + std::filesystem::path const &newp) + -> std::expected +{ + auto err = std::error_code(); + + std::filesystem::rename(oldp, newp, err); + + if (err) + return std::unexpected(error(err)); + + return {}; +} + + +} // namespace nihil diff --git a/nihil.posix/rename.ccm b/nihil.posix/rename.ccm new file mode 100644 index 0000000..796ec5b --- /dev/null +++ b/nihil.posix/rename.ccm @@ -0,0 +1,23 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +export module nihil.posix:rename; + +import nihil.error; + +namespace nihil { + +/* + * Rename a file (or directory). + */ +export [[nodiscard]] auto +rename(std::filesystem::path const &oldp, std::filesystem::path const &newp) + -> std::expected; + +} // namespace nihil diff --git a/nihil.posix/spawn.ccm b/nihil.posix/spawn.ccm new file mode 100644 index 0000000..5812716 --- /dev/null +++ b/nihil.posix/spawn.ccm @@ -0,0 +1,249 @@ +/* + * This source code is released into the public domain. + */ + +module; + +/* + * spawn(): fork and execute a child process. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +export module nihil.posix:spawn; + +import nihil.monad; +import :argv; +import :exec; +import :open; +import :process; + +namespace nihil { + +// Useful constants +export inline int constexpr stdin_fileno = STDIN_FILENO; +export inline int constexpr stdout_fileno = STDOUT_FILENO; +export inline int constexpr stderr_fileno = STDERR_FILENO; + +/* + * fd_pipe: create a pipe with one end in the child and the other in the + * parent. The child's side will be dup2()'d to the provided fd number. + * The parent side fd can be retrieved via parent_fd(); + */ +export struct fd_pipe final { + fd_pipe(int fdno, fd &&child_fd, fd &&parent_fd) + : m_fdno(fdno) + , m_child_fd(std::move(child_fd)) + , m_parent_fd(std::move(parent_fd)) + { + } + + auto run_in_child(this fd_pipe &self, process &) -> void + { + auto err = raw_dup(self.m_child_fd, self.m_fdno); + if (!err) { + std::print("dup: {}\n", err.error()); + _exit(1); + } + + /* + * We don't care about errors from close() since the fd + * is still closed. + */ + std::ignore = self.m_parent_fd.close(); + std::ignore = self.m_child_fd.close(); + } + + auto run_in_parent(this fd_pipe &self, process &) -> void + { + std::ignore = self.m_child_fd.close(); + } + + [[nodiscard]] auto parent_fd(this fd_pipe &self) -> fd & + { + return self.m_parent_fd; + } + +private: + int m_fdno; + fd m_child_fd; + fd m_parent_fd; +}; + +export [[nodiscard]] auto +make_fd_pipe(int fdno) -> std::expected +{ + auto fds = co_await pipe(); + co_return fd_pipe(fdno, std::move(fds.first), std::move(fds.second)); +} + +/* + * fd_file: open a file and provide it to the child as a file descriptor. + * open_flags and open_mode are as for ::open(). + */ +export struct fd_file final { + fd_file(int fdno, fd &&file_fd) + : m_fdno(fdno) + , m_file_fd(std::move(file_fd)) + { + } + + auto run_in_parent(this fd_file &self, process &) -> void + { + std::ignore = self.m_file_fd.close(); + } + + auto run_in_child(this fd_file &self, process &) -> void + { + auto err = raw_dup(self.m_file_fd, self.m_fdno); + if (!err) { + std::print("dup: {}\n", err.error()); + _exit(1); + } + + std::ignore = self.m_file_fd.close(); + } + +private: + int m_fdno; + fd m_file_fd; +}; + +export [[nodiscard]] auto +make_fd_file(int fdno, std::filesystem::path const &file, + int flags, int mode = 0777) + -> std::expected +{ + auto fd = co_await open(file, flags, mode); + co_return fd_file(fdno, std::move(fd)); +} + +/* + * Shorthand for fd_file with /dev/null as the file. + */ + +export [[nodiscard]] inline auto +stdin_devnull() -> std::expected +{ + return make_fd_file(stdin_fileno, "/dev/null", O_RDONLY); +} + +export [[nodiscard]] inline auto +stdout_devnull() -> std::expected +{ + return make_fd_file(stdout_fileno, "/dev/null", O_WRONLY); +} + +export [[nodiscard]] inline auto +stderr_devnull() -> std::expected +{ + return make_fd_file(stderr_fileno, "/dev/null", O_WRONLY); +} + +/* + * Capture the output of a pipe in the parent and read it into an + * output iterator. + */ +export template Iterator> +struct fd_capture final { + fd_capture(fd_pipe &&pipe, Iterator it) + : m_pipe(std::move(pipe)) + , m_iterator(std::move(it)) + { + } + + auto run_in_child(this fd_capture &self, process &p) -> void + { + self.m_pipe.run_in_child(p); + } + + auto run_in_parent(this fd_capture &self, process &p) -> void + { + self.m_pipe.run_in_parent(p); + + auto constexpr bufsize = std::size_t{1024}; + auto buffer = std::array(); + auto &fd = self.m_pipe.parent_fd(); + for (;;) { + auto ret = read(fd, buffer); + if (!ret || ret->size() == 0) + break; + + std::ranges::copy(*ret, self.m_iterator); + } + + // We probably want to handle errors here somehow, + // but it's not clear what would be useful behaviour. + } + +private: + fd_pipe m_pipe; + Iterator m_iterator; +}; + +export [[nodiscard]] auto +make_capture(int fdno, std::output_iterator auto &&it) + -> std::expected, error> +{ + auto pipe = co_await make_fd_pipe(fdno); + co_return fd_capture(std::move(pipe), + std::forward(it)); +} + +export [[nodiscard]] auto +make_capture(int fdno, std::string &str) + -> std::expected, error> +{ + auto pipe = co_await make_fd_pipe(fdno); + co_return fd_capture(std::move(pipe), std::back_inserter(str)); +} + +/* + * Spawn a new process with the given arguments and return a struct process. + * Throws exec_error() on failure. + */ +export [[nodiscard]] auto +spawn(executor auto &&executor, auto &&...actions) + -> std::expected +{ + auto const pid = ::fork(); + if (pid == -1) + return std::unexpected(error("fork failed", + error(std::errc(errno)))); + + auto proc = process(pid); + + if (pid == 0) { + // We are in the child. Release the process so we don't + // try to wait for ourselves, then run child handlers and + // exec the process. + + std::ignore = std::move(proc).release(); + (actions.run_in_child(proc), ...); + + auto err = executor.exec(); + std::print("{}\n", err.error()); + _exit(1); + } + + (actions.run_in_parent(proc), ...); + + return proc; +} + +} // namespace nihil diff --git a/nihil.posix/test_fd.cc b/nihil.posix/test_fd.cc new file mode 100644 index 0000000..8dff323 --- /dev/null +++ b/nihil.posix/test_fd.cc @@ -0,0 +1,199 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include + +#include +#include + +#include + +import nihil.error; +import nihil.posix; + +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(), std::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{}; + auto read_buf = read(fd2, readbuf); + REQUIRE(read_buf); + REQUIRE(std::string_view(*read_buf) == test_string); +} diff --git a/nihil.posix/test_getenv.cc b/nihil.posix/test_getenv.cc new file mode 100644 index 0000000..fdb5277 --- /dev/null +++ b/nihil.posix/test_getenv.cc @@ -0,0 +1,49 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +#include + +import nihil.posix; + +TEST_CASE("getenv: existing value", "[getenv]") +{ + auto constexpr *name = "NIHIL_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 = "NIHIL_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 = "NIHIL_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/nihil.posix/test_spawn.cc b/nihil.posix/test_spawn.cc new file mode 100644 index 0000000..da321ff --- /dev/null +++ b/nihil.posix/test_spawn.cc @@ -0,0 +1,117 @@ +/* + * This source code is released into the public domain. + */ + +#include + +import nihil.posix; + +TEST_CASE("spawn: system", "[spawn]") +{ + using namespace nihil; + + auto exec = shell("x=1; echo $x"); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execv", "[spawn]") { + using namespace nihil; + + auto args = argv({"sh", "-c", "x=1; echo $x"}); + auto exec = execv("/bin/sh", std::move(args)); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execvp", "[spawn]") { + using namespace nihil; + + auto args = argv({"sh", "-c", "x=1; echo $x"}); + auto exec = execvp("sh", std::move(args)); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execl", "[spawn]") { + using namespace nihil; + + auto exec = execl("/bin/sh", "sh", "-c", "x=1; echo $x"); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execlp", "[spawn]") { + using namespace nihil; + + auto exec = execlp("sh", "sh", "-c", "x=1; echo $x"); + REQUIRE(exec); + + auto output = std::string(); + auto capture = make_capture(stdout_fileno, output); + REQUIRE(capture); + + auto proc = spawn(*exec, *capture); + REQUIRE(proc); + + auto status = std::move(*proc).wait(); + REQUIRE(status); + + REQUIRE(status->okay()); + REQUIRE(output == "1\n"); +} + +TEST_CASE("spawn: execlp failure", "[spawn]") { + using namespace nihil; + + auto exec = execlp("nihil_no_such_executable", "x"); + REQUIRE(!exec); +} diff --git a/nihil.posix/write_file.ccm b/nihil.posix/write_file.ccm new file mode 100644 index 0000000..867e0db --- /dev/null +++ b/nihil.posix/write_file.ccm @@ -0,0 +1,82 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +#include +#include + +export module nihil.posix:write_file; + +import nihil.error; +import nihil.guard; +import nihil.monad; +import :fd; +import :open; +import :rename; + +namespace nihil { + +/* + * Write the contents of a range to a file. Returns the number of bytes + * written. + */ +export [[nodiscard]] +auto write_file(std::filesystem::path const &filename, + std::ranges::contiguous_range auto &&range, + int mode = 0777) + -> std::expected +{ + auto file = co_await open(filename, O_CREAT|O_WRONLY, mode); + auto nbytes = co_await write(file, range); + co_return nbytes; +} + +/* + * Utility wrapper for non-contiguous ranges. + */ +export [[nodiscard]] +auto write_file(std::filesystem::path const &filename, + std::ranges::range auto &&range) + -> std::expected +requires(!std::ranges::contiguous_range) +{ + return write_file(filename, std::vector(std::from_range, range)); +} + +/* + * Write the contents of a range to a file safely. The data will be written + * to ".tmp", and if the write succeeds, the temporary file will be + * renamed to the target filename. If an error occurs, the target file will + * not be modified. + */ +export [[nodiscard]] +auto safe_write_file(std::filesystem::path const &filename, + std::ranges::range auto &&range) + -> std::expected +{ + auto tmpfile = filename; + tmpfile.remove_filename(); + tmpfile /= (filename.filename().native() + ".tmp"); + + auto tmpfile_guard = guard([&tmpfile] { + ::unlink(tmpfile.c_str()); + }); + + co_await write_file(tmpfile, range); + co_await nihil::rename(tmpfile, filename); + + tmpfile_guard.release(); + co_return {}; +} + + +} // namespace nihil diff --git a/nihil.ucl/CMakeLists.txt b/nihil.ucl/CMakeLists.txt index df13e84..9d8ab3a 100644 --- a/nihil.ucl/CMakeLists.txt +++ b/nihil.ucl/CMakeLists.txt @@ -3,7 +3,7 @@ pkg_check_modules(LIBUCL REQUIRED libucl) add_library(nihil.ucl STATIC) -target_link_libraries(nihil.ucl PUBLIC nihil) +target_link_libraries(nihil.ucl PRIVATE nihil.error nihil.monad) target_sources(nihil.ucl PUBLIC FILE_SET modules TYPE CXX_MODULES FILES diff --git a/nihil.ucl/boolean.cc b/nihil.ucl/boolean.cc index 2a643b9..91f2b17 100644 --- a/nihil.ucl/boolean.cc +++ b/nihil.ucl/boolean.cc @@ -13,7 +13,7 @@ module; module nihil.ucl; -import nihil; +import nihil.error; namespace nihil::ucl { diff --git a/nihil.ucl/errc.cc b/nihil.ucl/errc.cc index fc1d9f8..0b65b86 100644 --- a/nihil.ucl/errc.cc +++ b/nihil.ucl/errc.cc @@ -9,8 +9,6 @@ module; module nihil.ucl; -import nihil; - namespace nihil::ucl { struct ucl_error_category final : std::error_category { diff --git a/nihil.ucl/integer.cc b/nihil.ucl/integer.cc index f4f08ef..825d8f6 100644 --- a/nihil.ucl/integer.cc +++ b/nihil.ucl/integer.cc @@ -13,7 +13,7 @@ module; module nihil.ucl; -import nihil; +import nihil.error; namespace nihil::ucl { diff --git a/nihil.ucl/integer.ccm b/nihil.ucl/integer.ccm index d5ac72a..0ea490c 100644 --- a/nihil.ucl/integer.ccm +++ b/nihil.ucl/integer.ccm @@ -15,7 +15,6 @@ module; export module nihil.ucl:integer; -import nihil; import :object; import :type; diff --git a/nihil.ucl/object.cc b/nihil.ucl/object.cc index ee4968b..9a150fb 100644 --- a/nihil.ucl/object.cc +++ b/nihil.ucl/object.cc @@ -12,8 +12,6 @@ module; module nihil.ucl; -import nihil; - namespace nihil::ucl { object::object(ref_t, ::ucl_object_t const *object) diff --git a/nihil.ucl/object.ccm b/nihil.ucl/object.ccm index 40f2088..dffb54e 100644 --- a/nihil.ucl/object.ccm +++ b/nihil.ucl/object.ccm @@ -18,7 +18,6 @@ module; export module nihil.ucl:object; -import nihil; import :type; namespace nihil::ucl { diff --git a/nihil.ucl/object_cast.ccm b/nihil.ucl/object_cast.ccm index 07588a1..3fa9eba 100644 --- a/nihil.ucl/object_cast.ccm +++ b/nihil.ucl/object_cast.ccm @@ -12,7 +12,7 @@ module; export module nihil.ucl:object_cast; -import nihil; +import nihil.monad; import :type; import :object; import :array; diff --git a/nihil.ucl/parser.cc b/nihil.ucl/parser.cc index 2288c4c..0a08670 100644 --- a/nihil.ucl/parser.cc +++ b/nihil.ucl/parser.cc @@ -12,7 +12,7 @@ module; module nihil.ucl; -import nihil; +import nihil.error; namespace nihil::ucl { diff --git a/nihil.ucl/parser.ccm b/nihil.ucl/parser.ccm index 2630c81..20e5c66 100644 --- a/nihil.ucl/parser.ccm +++ b/nihil.ucl/parser.ccm @@ -16,7 +16,7 @@ module; export module nihil.ucl:parser; -import nihil; +import nihil.monad; import :object; import :map; diff --git a/nihil.ucl/real.cc b/nihil.ucl/real.cc index b3d50c3..6d9e082 100644 --- a/nihil.ucl/real.cc +++ b/nihil.ucl/real.cc @@ -15,7 +15,7 @@ module; module nihil.ucl; -import nihil; +import nihil.error; namespace nihil::ucl { diff --git a/nihil.ucl/string.cc b/nihil.ucl/string.cc index 0fc9808..67e97f4 100644 --- a/nihil.ucl/string.cc +++ b/nihil.ucl/string.cc @@ -14,7 +14,7 @@ module; module nihil.ucl; -import nihil; +import nihil.error; namespace nihil::ucl { diff --git a/nihil.ucl/string.ccm b/nihil.ucl/string.ccm index 9127b2d..c757bf1 100644 --- a/nihil.ucl/string.ccm +++ b/nihil.ucl/string.ccm @@ -14,7 +14,6 @@ module; export module nihil.ucl:string; -import nihil; import :object; import :type; diff --git a/nihil.ucl/tests/CMakeLists.txt b/nihil.ucl/tests/CMakeLists.txt index 93559a7..0257b4f 100644 --- a/nihil.ucl/tests/CMakeLists.txt +++ b/nihil.ucl/tests/CMakeLists.txt @@ -13,9 +13,7 @@ add_executable(nihil.ucl.test string.cc ) -target_link_libraries(nihil.ucl.test PRIVATE - nihil nihil.ucl - Catch2::Catch2WithMain) +target_link_libraries(nihil.ucl.test PRIVATE nihil.ucl Catch2::Catch2WithMain) find_package(Catch2 REQUIRED) diff --git a/nihil.ucl/tests/array.cc b/nihil.ucl/tests/array.cc index 37928aa..866fa45 100644 --- a/nihil.ucl/tests/array.cc +++ b/nihil.ucl/tests/array.cc @@ -11,7 +11,6 @@ #include #include -import nihil; import nihil.ucl; TEST_CASE("ucl: array: invariants", "[ucl]") diff --git a/nihil.ucl/tests/emit.cc b/nihil.ucl/tests/emit.cc index d75255b..a7dcd71 100644 --- a/nihil.ucl/tests/emit.cc +++ b/nihil.ucl/tests/emit.cc @@ -7,7 +7,6 @@ #include -import nihil; import nihil.ucl; TEST_CASE("ucl: emit to std::ostream", "[ucl]") diff --git a/nihil.ucl/tests/parse.cc b/nihil.ucl/tests/parse.cc index c56974e..43ce219 100644 --- a/nihil.ucl/tests/parse.cc +++ b/nihil.ucl/tests/parse.cc @@ -7,7 +7,6 @@ #include #include -import nihil; import nihil.ucl; TEST_CASE("ucl parse: iterate array", "[ucl]") diff --git a/nihil.ucl/type.ccm b/nihil.ucl/type.ccm index cd98c01..f3b3aef 100644 --- a/nihil.ucl/type.ccm +++ b/nihil.ucl/type.ccm @@ -13,7 +13,7 @@ module; export module nihil.ucl:type; -import nihil; +import nihil.error; namespace nihil::ucl { diff --git a/nihil.util/CMakeLists.txt b/nihil.util/CMakeLists.txt new file mode 100644 index 0000000..b809a68 --- /dev/null +++ b/nihil.util/CMakeLists.txt @@ -0,0 +1,36 @@ +# This source code is released into the public domain. + +add_library(nihil.util STATIC) +target_link_libraries(nihil.util PRIVATE nihil.core nihil.error nihil.monad) +target_sources(nihil.util + PUBLIC FILE_SET modules TYPE CXX_MODULES FILES + nihil.util.ccm + + ctype.ccm + parse_size.ccm + next_word.ccm + skipws.ccm + tabulate.ccm +) + +if(NIHIL_TESTS) + enable_testing() + + add_executable(nihil.util.test + test_ctype.cc + test_parse_size.cc + test_next_word.cc + test_skipws.cc + test_tabulate.cc + ) + target_link_libraries(nihil.util.test PRIVATE + nihil.util + Catch2::Catch2WithMain + ) + + find_package(Catch2 REQUIRED) + + include(CTest) + include(Catch) + catch_discover_tests(nihil.util.test) +endif() diff --git a/nihil.util/ctype.ccm b/nihil.util/ctype.ccm new file mode 100644 index 0000000..6d30c4f --- /dev/null +++ b/nihil.util/ctype.ccm @@ -0,0 +1,87 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include + +export module nihil.util: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()) + : m_mask(mask_) + , m_locale(locale_) + {} + + [[nodiscard]] auto operator()(this ctype_is const &self, + std::integral auto c) + { + using ctype = std::ctype; + auto &facet = std::use_facet(self.m_locale); + return facet.is(self.m_mask, c); + } + +private: + std::ctype_base::mask m_mask; + std::locale m_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.util/next_word.ccm b/nihil.util/next_word.ccm new file mode 100644 index 0000000..c5d3ad7 --- /dev/null +++ b/nihil.util/next_word.ccm @@ -0,0 +1,49 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include + +export module nihil.util:next_word; + +import :skipws; + +namespace nihil { + +/* + * Return the next word from a string_view. Skips leading whitespace, so + * calling this repeatedly will return each word from the string. + */ + +export template [[nodiscard]] +auto next_word(std::basic_string_view text, + std::locale const &locale = std::locale()) + -> std::pair, + std::basic_string_view> +{ + text = skipws(text, locale); + + auto is_space = ctype_is(std::ctype_base::space, locale); + auto split_pos = std::ranges::find_if(text, is_space); + + return {{std::ranges::begin(text), split_pos}, + {split_pos, std::ranges::end(text)}}; +} + +export template +auto next_word(std::basic_string_view *text, + std::locale const &locale = std::locale()) + -> std::basic_string_view +{ + auto [word, rest] = next_word(*text, locale); + *text = rest; + return word; +} + +} // namespace nihil diff --git a/nihil.util/nihil.util.ccm b/nihil.util/nihil.util.ccm new file mode 100644 index 0000000..afd513a --- /dev/null +++ b/nihil.util/nihil.util.ccm @@ -0,0 +1,13 @@ +/* + * This source code is released into the public domain. + */ + +module; + +export module nihil.util; + +export import :ctype; +export import :parse_size; +export import :next_word; +export import :skipws; +export import :tabulate; diff --git a/nihil.util/parse_size.ccm b/nihil.util/parse_size.ccm new file mode 100644 index 0000000..c692578 --- /dev/null +++ b/nihil.util/parse_size.ccm @@ -0,0 +1,107 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include +#include +#include + +export module nihil.util:parse_size; + +import nihil.core; +import nihil.error; +import nihil.monad; + +import :ctype; + +namespace nihil { + +template +auto get_multiplier(Char c) -> std::expected +{ + auto ret = std::uint64_t{1}; + + switch (c) { + case 'p': case 'P': ret *= 1024; + case 't': case 'T': ret *= 1024; + case 'g': case 'G': ret *= 1024; + case 'm': case 'M': ret *= 1024; + case 'k': case 'K': ret *= 1024; + return ret; + + default: + return std::unexpected(error(errc::invalid_unit)); + } +} + +/* + * Parse a string containing a human-formatted size, such as "1024" + * or "4g". Parsing is always done in the "C" locale and does not + * recognise thousands separators or negative numbers. + */ +export template [[nodiscard]] +auto parse_size(std::basic_string_view str) + -> std::expected +{ + // Extract the numeric part of the string. + auto it = std::ranges::find_if_not(str, is_c_digit); + auto num_str = std::basic_string_view( + std::ranges::begin(str), it); + + if (num_str.empty()) + co_return std::unexpected(error(errc::empty_string)); + + auto ret = T{0}; + + for (auto c : num_str) { + if (ret > (std::numeric_limits::max() / 10)) + co_return std::unexpected(error( + std::errc::result_out_of_range)); + ret *= 10; + + auto digit = static_cast(c - '0'); + if ((std::numeric_limits::max() - digit) < ret) + co_return std::unexpected(error( + std::errc::result_out_of_range)); + ret += digit; + } + + if (it == str.end()) + // No multiplier. + co_return ret; + + auto mchar = *it++; + + if (it != str.end()) + // Multiplier is more than one character. + co_return std::unexpected(error(errc::invalid_unit)); + + auto mult = co_await get_multiplier(mchar); + + if (std::cmp_greater(ret, std::numeric_limits::max() / mult)) + co_return std::unexpected(error( + std::errc::result_out_of_range)); + + co_return ret * mult; +} + +export template +[[nodiscard]] inline auto parse_size(char const *s) +{ + return parse_size(std::string_view(s)); +} + +export template +[[nodiscard]] inline auto parse_size(wchar_t const *s) +{ + return parse_size(std::wstring_view(s)); +} + +} diff --git a/nihil.util/skipws.ccm b/nihil.util/skipws.ccm new file mode 100644 index 0000000..4813ae8 --- /dev/null +++ b/nihil.util/skipws.ccm @@ -0,0 +1,40 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include + +export module nihil.util:skipws; + +import :ctype; + +namespace nihil { + +/* + * Remove leading whitespace from a string. + */ + +export template [[nodiscard]] +auto skipws(std::basic_string_view text, + std::locale const &locale = std::locale()) + -> std::basic_string_view +{ + auto is_space = ctype_is(std::ctype_base::space, locale); + auto nonws = std::ranges::find_if_not(text, is_space); + return {nonws, std::ranges::end(text)}; +} + +export template +auto skipws(std::basic_string_view *text, + std::locale const &locale = std::locale()) + -> void +{ + *text = skipws(*text, locale); +} + +} // namespace nihil diff --git a/nihil.util/tabulate.ccm b/nihil.util/tabulate.ccm new file mode 100644 index 0000000..5998b24 --- /dev/null +++ b/nihil.util/tabulate.ccm @@ -0,0 +1,312 @@ +/* + * This source code is released into the public domain. + */ + +module; + +#include +#include +#include +#include +#include +#include + +export module nihil.util:tabulate; + +import nihil.error; +import :ctype; + +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 : error { + table_spec_error(std::string_view what) + : error(what) + { + } +}; + +/* + * The specification for a single field. + */ +template +struct field_spec { + enum align_t { left, right }; + + // Get the name of this field. + auto name(this field_spec const &self) + -> std::basic_string_view + { + return self.m_name; + } + + // Set the name of this field. + auto name(this field_spec &self, + std::basic_string_view new_name) + -> void + { + self.m_name = new_name; + } + + // Set this field's alignment. + auto align(this field_spec &self, align_t new_align) -> void + { + self.m_align = new_align; + } + + // Ensure the length of this field is at least the given width. + auto ensure_width(this field_spec &self, std::size_t newwidth) + -> void + { + self.m_width = std::max(self.m_width, newwidth); + } + + // Format an object to a string based on our field spec. + [[nodiscard]] auto format(this field_spec const &, auto &&obj) + -> std::basic_string + { + auto format_string = std::basic_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(this field_spec const &self, + std::basic_string_view value, + std::output_iterator auto &out, + bool is_last) + -> void + { + auto padding = self.m_width - value.size(); + + if (self.m_align == right) + for (std::size_t i = 0; i < padding; ++i) + *out++ = ' '; + + std::ranges::copy(value, out); + + if (!is_last && self.m_align == left) + for (std::size_t i = 0; i < padding; ++i) + *out++ = ' '; + } + +private: + std::basic_string_view m_name; + std::size_t m_width = 0; + align_t m_align = left; +}; + +/* + * The specification for an entire table. + */ +template +struct table_spec { + // Add a new field spec to this table. + auto add(this table_spec &self, field_spec field) -> void + { + self.m_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. + [[nodiscard]] auto field(this table_spec &self, std::size_t fieldnr) + -> field_spec & + { + if (fieldnr >= self.m_fields.size()) + self.m_fields.resize(fieldnr + 1); + return self.m_fields.at(fieldnr); + } + + // The number of columns in this table. + [[nodiscard]] auto columns(this table_spec const &self) -> std::size_t + { + return self.m_fields.size(); + } + + // Return all the fields in this table. + [[nodiscard]] auto fields(this table_spec const &self) + -> std::vector> const & + { + return self.m_fields; + } + +private: + std::vector> m_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> +[[nodiscard]] 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.ensure_width(field.name().size()); + + return field; +} + +template +[[nodiscard]] 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.util/test_ctype.cc b/nihil.util/test_ctype.cc new file mode 100644 index 0000000..62721d1 --- /dev/null +++ b/nihil.util/test_ctype.cc @@ -0,0 +1,373 @@ +/* + * This source code is released into the public domain. + */ + +#include + +import nihil.util; + +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/nihil.util/test_next_word.cc b/nihil.util/test_next_word.cc new file mode 100644 index 0000000..7e61237 --- /dev/null +++ b/nihil.util/test_next_word.cc @@ -0,0 +1,65 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include + +#include + +import nihil.util; + +TEST_CASE("next_word: basic", "[next_word]") +{ + using namespace std::literals; + auto s = "foo bar baz"sv; + + auto words = nihil::next_word(s); + REQUIRE(words.first == "foo"); + REQUIRE(words.second == " bar baz"); + + auto word = nihil::next_word(&s); + REQUIRE(word == "foo"); + REQUIRE(s == " bar baz"); +} + +TEST_CASE("next_word: multiple spaces", "[next_word]") +{ + using namespace std::literals; + auto s = "foo bar baz"sv; + + auto words = nihil::next_word(s); + REQUIRE(words.first == "foo"); + REQUIRE(words.second == " bar baz"); + + auto word = nihil::next_word(&s); + REQUIRE(word == "foo"); + REQUIRE(s == " bar baz"); +} + +TEST_CASE("next_word: leading spaces", "[next_word]") +{ + using namespace std::literals; + auto s = " \tfoo bar baz"sv; + + auto words = nihil::next_word(s); + REQUIRE(words.first == "foo"); + REQUIRE(words.second == " bar baz"); + + auto word = nihil::next_word(&s); + REQUIRE(word == "foo"); + REQUIRE(s == " bar baz"); +} + +TEST_CASE("next_word: locale", "[next_word]") +{ + using namespace std::literals; + auto s = L"\u2003foo\u2003bar\u2003baz"sv; + + auto words = nihil::next_word(s); + REQUIRE(words.first == s); + + words = nihil::next_word(s, std::locale("C.UTF-8")); + REQUIRE(words.first == L"foo"); + REQUIRE(words.second == L"\u2003bar\u2003baz"); +} diff --git a/nihil.util/test_parse_size.cc b/nihil.util/test_parse_size.cc new file mode 100644 index 0000000..4f4d018 --- /dev/null +++ b/nihil.util/test_parse_size.cc @@ -0,0 +1,168 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include + +#include + +import nihil.core; +import nihil.util; + +TEST_CASE("parse_size: empty value", "[nihil]") +{ + using namespace nihil; + + auto n = parse_size(""); + REQUIRE(!n); + REQUIRE(n.error() == nihil::errc::empty_string); +} + +TEST_CASE("parse_size: basic", "[nihil]") +{ + using namespace nihil; + + SECTION("bare number") { + auto n = parse_size("1024").value(); + REQUIRE(n == 1024); + } + + SECTION("max value, unsigned") { + auto n = parse_size("65535").value(); + REQUIRE(n == 65535); + } + + SECTION("max value, signed") { + auto n = parse_size("32767").value(); + REQUIRE(n == 32767); + } + + SECTION("overflow by 1, unsigned") { + auto n = parse_size("65536"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } + + SECTION("overflow by 1, signed") { + auto n = parse_size("32768"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } + + SECTION("overflow by many, unsigned") { + auto n = parse_size("100000"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } + + SECTION("overflow by many, signed") { + auto n = parse_size("100000"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } +} + +TEST_CASE("parse_size: invalid multiplier", "[nihil]") +{ + using namespace nihil; + + auto n = parse_size("4z"); + REQUIRE(!n); + REQUIRE(n.error() == nihil::errc::invalid_unit); + + n = parse_size("4kz"); + REQUIRE(!n); + REQUIRE(n.error() == nihil::errc::invalid_unit); +} + +TEST_CASE("parse_size: multipliers", "[nihil]") +{ + using namespace nihil; + + auto sf = static_cast(4); + + SECTION("k") { + auto n = parse_size("4k").value(); + REQUIRE(n == sf * 1024); + } + + SECTION("m") { + auto n = parse_size("4m").value(); + REQUIRE(n == sf * 1024 * 1024); + } + + SECTION("g") { + auto n = parse_size("4g").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024); + } + + SECTION("t") { + auto n = parse_size("4t").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024); + } + + SECTION("p") { + auto n = parse_size("4p").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024); + } +} + +TEST_CASE("parse_size: multiplier overflow", "[nihil]") +{ + using namespace nihil; + + SECTION("signed") { + auto n = parse_size("64k"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } + + SECTION("unsigned") { + auto n = parse_size("32k"); + REQUIRE(!n); + REQUIRE(n.error() == std::errc::result_out_of_range); + } +} + +TEST_CASE("parse_size: wide", "[nihil]") +{ + using namespace nihil; + + SECTION("bare number") { + auto n = parse_size(L"1024").value(); + REQUIRE(n == 1024); + } +} + +TEST_CASE("parse_size: wide multipliers", "[nihil]") +{ + using namespace nihil; + + auto sf = static_cast(4); + + SECTION("k") { + auto n = parse_size(L"4k").value(); + REQUIRE(n == sf * 1024); + } + + SECTION("m") { + auto n = parse_size(L"4m").value(); + REQUIRE(n == sf * 1024 * 1024); + } + + SECTION("g") { + auto n = parse_size(L"4g").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024); + } + + SECTION("t") { + auto n = parse_size(L"4t").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024); + } + + SECTION("p") { + auto n = parse_size(L"4p").value(); + REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024); + } +} diff --git a/nihil.util/test_skipws.cc b/nihil.util/test_skipws.cc new file mode 100644 index 0000000..837c1f3 --- /dev/null +++ b/nihil.util/test_skipws.cc @@ -0,0 +1,45 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +using namespace std::literals; + +#include + +import nihil.util; + +TEST_CASE("skipws: basic", "[skipws]") +{ + REQUIRE(nihil::skipws("foo"sv) == "foo"); + REQUIRE(nihil::skipws(" foo"sv) == "foo"); + REQUIRE(nihil::skipws("foo "sv) == "foo "); + REQUIRE(nihil::skipws("foo bar"sv) == "foo bar"); +} + +TEST_CASE("skipws: pointer", "[skipws]") +{ + auto s = "foo"sv; + nihil::skipws(&s); + REQUIRE(s == "foo"); + + s = " foo"sv; + nihil::skipws(&s); + REQUIRE(s == "foo"); + + s = "foo "sv; + nihil::skipws(&s); + REQUIRE(s == "foo "); + + s = "foo bar"sv; + nihil::skipws(&s); + REQUIRE(s == "foo bar"); +} + +TEST_CASE("skipws: locale", "[skipws]") +{ + // Assume the default locale is C. + REQUIRE(nihil::skipws(L"\u2003foo"sv) == L"\u2003foo"); + REQUIRE(nihil::skipws(L"\u2003foo"sv, std::locale("C.UTF-8")) == L"foo"); +} diff --git a/nihil.util/test_tabulate.cc b/nihil.util/test_tabulate.cc new file mode 100644 index 0000000..8dee796 --- /dev/null +++ b/nihil.util/test_tabulate.cc @@ -0,0 +1,75 @@ +/* + * This source code is released into the public domain. + */ + +#include +#include +#include + +#include + +import nihil.util; + +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"); +} diff --git a/nihil/CMakeLists.txt b/nihil/CMakeLists.txt deleted file mode 100644 index 64dbe6a..0000000 --- a/nihil/CMakeLists.txt +++ /dev/null @@ -1,52 +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 - argv.ccm - command_map.ccm - ctype.ccm - ensure_dir.ccm - errc.ccm - error.ccm - exec.ccm - fd.ccm - find_in_path.ccm - format_filesystem.ccm - generator.ccm - getenv.ccm - guard.ccm - match.ccm - monad.ccm - next_word.ccm - open_file.ccm - parse_size.ccm - process.ccm - read_file.ccm - rename_file.ccm - skipws.ccm - spawn.ccm - tabulate.ccm - usage_error.ccm - write_file.ccm - - PRIVATE - argv.cc - command_map.cc - ensure_dir.cc - errc.cc - error.cc - exec.cc - fd.cc - find_in_path.cc - getenv.cc - open_file.cc - process.cc - rename_file.cc -) - -if(NIHIL_TESTS) - add_subdirectory(tests) - enable_testing() -endif() diff --git a/nihil/argv.cc b/nihil/argv.cc deleted file mode 100644 index f38293f..0000000 --- a/nihil/argv.cc +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -module nihil; - -namespace nihil { - -argv::argv() = default; -argv::argv(argv &&) noexcept = default; -auto argv::operator=(this argv &, argv &&) -> argv & = default; - -argv::~argv() -{ - for (auto *arg : m_args) - delete[] arg; -} - -auto argv::data(this argv const &self) -> char const * const * -{ - return self.m_args.data(); -} - -auto argv::data(this argv &self) -> char * const * -{ - return self.m_args.data(); -} - -auto argv::size(this argv const &self) -{ - return self.m_args.size(); -} - -auto argv::begin(this argv const &self) -{ - return self.m_args.begin(); -} - -auto argv::end(this argv const &self) -{ - return self.m_args.end(); -} - - -auto argv::add_arg(this argv &self, std::string_view arg) -> void -{ - // Create a nul-terminated C string. - auto ptr = std::make_unique(arg.size() + 1); - std::ranges::copy(arg, ptr.get()); - ptr[arg.size()] = '\0'; - - // Ensure we won't throw when emplacing the pointer. - self.m_args.reserve(self.m_args.size() + 1); - self.m_args.emplace_back(ptr.release()); -} - -} // namespace nihil - diff --git a/nihil/argv.ccm b/nihil/argv.ccm deleted file mode 100644 index b5d6d6a..0000000 --- a/nihil/argv.ccm +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -export module nihil:argv; - -namespace nihil { - -/* - * argv: stores a null-terminated array of nul-terminated C strings. - * argv::data() is suitable for passing to ::execv(). - * - * Create an argv using argv::from_range(), which takes a range of - * string-like objects. - */ - -export struct argv { - /* - * Create a new argv from a range. - */ - argv(std::from_range_t, std::ranges::range auto &&args) - { - for (auto &&arg : args) - add_arg(std::string_view(arg)); - - m_args.push_back(nullptr); - } - - /* - * Create an argv from an initializer list. - */ - template - explicit argv(std::initializer_list &&args) - : argv(std::from_range, std::forward(args)) - { - } - - // Movable. - argv(argv &&) noexcept; - auto operator=(this argv &, argv &&other) -> argv &; - - // Not copyable. TODO: for completeness, it probably should be. - argv(argv const &) = delete; - auto operator=(this argv &, argv const &other) -> argv& = delete; - - ~argv(); - - // Access the stored arguments. - [[nodiscard]] auto data(this argv const &self) -> char const * const *; - [[nodiscard]] auto data(this argv &self) -> char * const *; - [[nodiscard]] auto size(this argv const &self); - - // Range access - [[nodiscard]] auto begin(this argv const &self); - [[nodiscard]] auto end(this argv const &self); - -private: - // Use the from_range() factory method to create new instances. - argv(); - - // The argument pointers, including the null terminator. - // This can't be a vector because we need an array of - // char pointers to pass to exec. - std::vector m_args; - - // Add a new argument to the array. - auto add_arg(this argv &self, std::string_view arg) -> void; -}; - -} // namespace nihil - diff --git a/nihil/command_map.cc b/nihil/command_map.cc deleted file mode 100644 index e02d270..0000000 --- a/nihil/command_map.cc +++ /dev/null @@ -1,267 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include -#include - -#include - -module nihil; - -/* - * command_map represents a hierarchical list of commands. At each level, - * a command is mapped to a handler, which can either be a function, in - * which case we execute the function, or another command_map, in which - * case we invoke the new map - */ - -namespace nihil { - -/* - * The string tree we store our commands in. This is sort of like a very - * basic hierarchical std::map. Keys are provided as a range of values, - * typically from argv. - */ - -struct command_tree_node final { - command_tree_node() - : _this_word("") - { - } - - command_tree_node(std::string_view this_word) - : _this_word(this_word) - { - } - - command_tree_node(std::string_view this_word, - command value) - : _this_word(this_word) - , _value(std::move(value)) - { - } - - /* - * Return a child node, or NULL if the child doesn't exist. - */ - auto get_child(this command_tree_node const &self, - std::string_view child) - -> command_tree_node const * - { - if (auto it = self.children.find(std::string(child)); - it != self.children.end()) - return &it->second; - - return nullptr; - } - - auto get_child(this command_tree_node &self, - std::string_view child) - -> command_tree_node * - { - if (auto it = self.children.find(std::string(child)); - it != self.children.end()) - return &it->second; - - return nullptr; - } - - /* - * Return a child node if it exists, or insert a new empty node. - */ - auto get_or_create_child(this command_tree_node &self, - std::string_view child) - -> command_tree_node * - { - if (auto ptr = self.get_child(child); ptr != nullptr) - return ptr; - - auto [it, ok] = self.children.emplace( - child, command_tree_node(child)); - return &it->second; - } - - /* - * Return this node's value. - */ - auto value(this command_tree_node const &self) - -> std::optional const & - { - return self._value; - } - - /* - * Set this node's value. - */ - auto value(this command_tree_node &self, command value) -> void - { - self._value = std::move(value); - } - - /* - * Print this node's children in a form useful to humans. - */ - auto print_commands(this command_tree_node const &self, - std::string_view prefix) -> void - { - for (auto &&[name, node] : self.children) { - std::print(" {} {}\n", prefix, name); - } - } - -private: - std::string _this_word; - std::optional _value; - std::unordered_map children; -}; - -struct command_tree { - /* - * Add a node to the tree. Returns false if the node already exists. - */ - auto insert(this command_tree &self, - std::ranges::range auto &&path, - command value) - -> bool - { - auto *this_node = &self._root_node; - - // Find the node for this key. - for (auto &&next : path) { - auto this_word = std::string_view(next); - this_node = this_node->get_or_create_child(this_word); - } - - if (this_node->value()) { - // The value already exists. - return false; - } - - // Set the new value. - this_node->value(std::move(value)); - return true; - } - - /* - * Find a node in the tree. Unlike insert(), iteration stops when - * we find any node with a value. - */ - auto find(this command_tree const &self, int &argc, char **&argv) - -> std::optional - { - auto *this_node = &self._root_node; - - // Assume the caller already stripped the program name from - // argv. This is usually the case since they would have - // called getopt(). - - // Store the command bits we got so far, so we can print them - // in the usage error if needed. - auto path = std::string(); - - while (argv[0] != nullptr) { - auto next = std::string_view(argv[0]); - - auto *next_node = this_node->get_child(next); - - if (next_node == nullptr) { - // The node doesn't exist, so this command is - // not valid. Print a list of valid commands. - std::print(std::cerr, - "{}: unknown command: {} {}\n", - ::getprogname(), path, next); - std::print(std::cerr, - "{}: expected one of:\n", - ::getprogname()); - - this_node->print_commands(path); - return {}; - } - - this_node = next_node; - - if (this_node->value()) - // This node has a value, so return it. - return {this_node->value()}; - - if (!path.empty()) - path += ' '; - path += next; - --argc; - ++argv; - } - - // We didn't find a value, so the key was incomplete. - std::print(std::cerr, "{}: {} command; expected:\n", - ::getprogname(), - path.empty() ? "missing" : "incomplete"); - this_node->print_commands(path); - - return {}; - } - -private: - command_tree_node _root_node; -}; - -/* - * The global command map. - */ -auto get_commands() -> command_tree & { - static auto commands = command_tree(); - return commands; -} - -auto register_command(std::string_view path, command *cmd) noexcept -> void -try { - auto &commands = get_commands(); - if (commands.insert(path | std::views::split(' '), *cmd) == false) { - std::printf("command registration failed\n"); - std::abort(); - } -} catch (...) { - std::printf("command registration failed\n"); - std::abort(); -} - -auto dispatch_command(int argc, char **argv) -> int -{ - auto &commands = get_commands(); - - // The caller should have stripped argv[0] already. find() will - // strip all the remaining elements except the last, which means - // argv[0] will be set to something reasonable for the next call - // to getopt(). - - auto node = commands.find(argc, argv); - - if (!node) - // find() already printed the error message - return 1; - - // Reset getopt here for the command to use. - optreset = 1; - optind = 1; - - // Calling setprogname() makes error messages more relevant. - auto cprogname = std::format("{} {}", ::getprogname(), - node->path()); - ::setprogname(cprogname.c_str()); - - return node->invoke(argc, argv); -} - -void print_usage(std::string_view) -{ -// get_root_node().print_usage(std::string(prefix)); -} - -} // namespace nihil diff --git a/nihil/command_map.ccm b/nihil/command_map.ccm deleted file mode 100644 index 3c58ecc..0000000 --- a/nihil/command_map.ccm +++ /dev/null @@ -1,67 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include - -export module nihil:command_map; - -import :next_word; -import :usage_error; - -/* - * command_map represents a hierarchical list of commands. At each level, - * a command is mapped to a handler, which can either be a function, in - * which case we execute the function, or another command_map, in which - * case we invoke the new map - */ - -namespace nihil { - -export struct command; - -/* - * Register a command; used by command<>::command(). - */ -auto register_command(std::string_view path, command *) noexcept -> void; - -/* - * A command that can be invoked. Instantiating a command adds this command - * to the global command table. If an error occurs, the program will abort. - */ -export struct command { - command(std::string_view path, std::string_view usage, auto &&fn) - : m_path(path) - , m_usage(usage) - , m_handler(std::forward(fn)) - { - register_command(path, this); - } - - [[nodiscard]] auto path(this command const &self) -> std::string_view - { - return self.m_path; - } - - auto invoke(this command const &self, int argc, char **argv) -> int - { - return std::invoke(self.m_handler, argc, argv); - } - -private: - std::string_view m_path; - std::string_view m_usage; - std::function m_handler; -}; - -// The public API. -export [[nodiscard]] auto dispatch_command(int argc, char **argv) -> int; -export auto print_usage(std::string_view prefix) -> void; - -} // namespace nihil diff --git a/nihil/ctype.ccm b/nihil/ctype.ccm deleted file mode 100644 index 5f45703..0000000 --- a/nihil/ctype.ccm +++ /dev/null @@ -1,87 +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()) - : m_mask(mask_) - , m_locale(locale_) - {} - - [[nodiscard]] auto operator()(this ctype_is const &self, - std::integral auto c) - { - using ctype = std::ctype; - auto &facet = std::use_facet(self.m_locale); - return facet.is(self.m_mask, c); - } - -private: - std::ctype_base::mask m_mask; - std::locale m_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/ensure_dir.cc b/nihil/ensure_dir.cc deleted file mode 100644 index f20d328..0000000 --- a/nihil/ensure_dir.cc +++ /dev/null @@ -1,28 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -module nihil; - -namespace nihil { - -auto ensure_dir(std::filesystem::path const &dir) -> std::expected -{ - auto err = std::error_code(); - - std::filesystem::create_directories(dir, err); - - if (err) - return std::unexpected(error(err)); - - return {}; -} - -} // namespace nihil diff --git a/nihil/ensure_dir.ccm b/nihil/ensure_dir.ccm deleted file mode 100644 index af2c4c7..0000000 --- a/nihil/ensure_dir.ccm +++ /dev/null @@ -1,23 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -export module nihil:ensure_dir; - -import :error; - -namespace nihil { - -/* - * Create the given directory and any parent directories. - */ -export [[nodiscard]] auto ensure_dir(std::filesystem::path const &dir) - -> std::expected; - -} // namespace nihil - diff --git a/nihil/errc.cc b/nihil/errc.cc deleted file mode 100644 index 1d4e6fa..0000000 --- a/nihil/errc.cc +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -module nihil; - -namespace nihil { - -struct nihil_error_category final : std::error_category { - auto name() const noexcept -> char const * override; - auto message(int err) const -> std::string override; -}; - -auto nihil_category() noexcept -> std::error_category & -{ - static auto category = nihil_error_category(); - return category; -} - -auto make_error_condition(errc ec) -> std::error_condition -{ - return {static_cast(ec), nihil_category()}; -} - -auto nihil_error_category::name() const noexcept -> char const * -{ - return "nihil"; -} - -auto nihil_error_category::message(int err) const -> std::string -{ - switch (static_cast(err)) { - case errc::no_error: - return "No error"; - case errc::empty_string: - return "Empty string is not permitted"; - case errc::invalid_unit: - return "Invalid unit specifier"; - default: - return "Undefined error"; - } -} - -} // namespace nihil diff --git a/nihil/errc.ccm b/nihil/errc.ccm deleted file mode 100644 index eb0389e..0000000 --- a/nihil/errc.ccm +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -export module nihil:errc; - -namespace nihil { - -export enum struct errc { - no_error = 0, - - // Empty string is not allowed. - empty_string, - - // Invalid unit, e.g. in parse_size() - invalid_unit, -}; - -export auto nihil_category() noexcept -> std::error_category &; -export auto make_error_condition(errc ec) -> std::error_condition; - -} // namespace nihil - -namespace std { - -export template<> -struct is_error_condition_enum : true_type {}; - -} // namespace std diff --git a/nihil/error.cc b/nihil/error.cc deleted file mode 100644 index c2cfd49..0000000 --- a/nihil/error.cc +++ /dev/null @@ -1,157 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include - -module nihil; - -namespace nihil { - -auto to_string(error const &self) -> std::string -{ - auto ret = self.str(); - - auto cause = self.cause(); - while (cause) { - ret += ": " + cause->str(); - cause = cause->cause(); - } - - return ret; -} - -error::error() -{ -} - -error::~error() = default; - -error::error(std::string_view what, error cause) - : m_error(std::string(what)) - , m_cause(std::make_shared(std::move(cause))) -{ -} -error::error(std::string_view what) - : m_error(std::string(what)) -{ -} - -error::error(std::error_condition what, error cause) - : m_error(what) - , m_cause(std::make_shared(std::move(cause))) -{ -} - -error::error(std::error_condition what) - : m_error(what) -{ -} - -error::error(std::error_code what, error cause) - : m_error(what) - , m_cause(std::make_shared(std::move(cause))) -{ -} - -error::error(std::error_code what) - : m_error(what) -{ -} - -error::error(error const &) = default; -error::error(error &&) noexcept = default; -auto error::operator=(this error &, error const &) -> error & = default; -auto error::operator=(this error &, error &&) noexcept -> error & = default; - -auto error::cause(this error const &self) -> std::shared_ptr -{ - if (self.m_cause) - return self.m_cause; - return {}; -} - -auto error::root_cause(this error const &self) -> error const & -{ - if (self.m_cause) - return self.m_cause->root_cause(); - - return self; -} - -auto error::str(this error const &self) -> std::string -{ - return self.m_error | match { - [] (std::monostate) -> std::string { - return "No error"; - }, - [] (std::error_code const &m) { - return m.message(); - }, - [] (std::error_condition const &m) { - return m.message(); - }, - [] (std::string const &m) { - return m; - } - }; -} - -auto error::code(this error const &self) -> std::optional -{ - auto const *code = std::get_if(&self.m_error); - if (code) - return {*code}; - return {}; -} - -auto error::condition(this error const &self) - -> std::optional -{ - auto const *condition = std::get_if(&self.m_error); - if (condition) - return {*condition}; - return {}; -} - -auto error::what() const noexcept -> char const * -{ - if (!m_what) - m_what = to_string(*this); - - return m_what->c_str(); -} - -auto operator==(error const &lhs, error const &rhs) -> bool -{ - return lhs.m_error == rhs.m_error; -} - -auto operator<=>(error const &lhs, error const &rhs) -> std::strong_ordering -{ - return lhs.m_error <=> rhs.m_error; -} - -auto operator==(error const &lhs, std::error_code const &rhs) -> bool -{ - return lhs.code() == rhs; -} - -// Compare an error to an std::error_condition. -auto operator==(error const &lhs, std::error_condition const &rhs) -> bool -{ - return lhs.condition() == rhs; -} - -auto operator<<(std::ostream &strm, error const &e) -> std::ostream & -{ - return strm << to_string(e); -} - -} // namespace nihil diff --git a/nihil/error.ccm b/nihil/error.ccm deleted file mode 100644 index 0500f2e..0000000 --- a/nihil/error.ccm +++ /dev/null @@ -1,199 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -/* - * error: a type representing an error. - * - * An error consists of an immediate cause, which may be a string or - * std:error_code, and an optional proximate cause, which is another error - * object. Any number of error objects may be stacked. - * - * For example, a failure to open a file might be a stack of two errors: - * - * - string, "failed to open /etc/somefile", - * - std::error_code, "No such file or directory". - * - * Calling .str() will format the entire stack starting at that error, - * for example: "failed to open /etc/somefile: No such file or directory". - * - * Errors may be moved and (relatively) cheaply copied, since the cause - * chain is refcounted. - * - * error derives from std::exception, so it may be thrown and caught and - * provides a useful what(). When throwing errors, creating a derived - * error will make it easier to distinguish errors when catching them. - */ - -#include -#include -#include -#include -#include -#include -#include -#include - -export module nihil:error; - -namespace nihil { - -// Things which can be errors. -using error_t = std::variant< - std::monostate, - std::string, - std::error_code, - std::error_condition - >; - -export struct error : std::exception { - // Create an empty error, representing success. - error(); - - // Destroy an error. - virtual ~error(); - - // Create an error from a freeform string. - error(std::string_view what, error cause); - explicit error(std::string_view what); - - template - requires(std::is_error_code_enum::value || - std::is_error_condition_enum::value) - error(std::string_view what, Cause &&cause) - : error(what, error(std::forward(cause))) - {} - - // Create an error from an std::error_code. - error(std::error_condition what, error cause); - explicit error(std::error_condition what); - - // Create an error from an std::error_condition. - error(std::error_code what, error cause); - explicit error(std::error_code what); - - // Create an error from an std::error_code enum. - error(auto errc, error cause) - requires(std::is_error_code_enum::value) - : error(make_error_code(errc), std::move(cause)) - {} - - explicit error(auto errc) - requires(std::is_error_code_enum::value) - : error(make_error_code(errc)) - {} - - // Create an error from an std::error_condition enum. - error(auto errc, error cause) - requires(std::is_error_condition_enum::value) - : error(make_error_condition(errc), std::move(cause)) - {} - - explicit error(auto errc) - requires(std::is_error_condition_enum::value) - : error(make_error_condition(errc)) - {} - - error(error const &); - error(error &&) noexcept; - - auto operator=(this error &, error const &) -> error &; - auto operator=(this error &, error &&) noexcept -> error &; - - // Return the cause of this error. - [[nodiscard]] auto cause(this error const &) -> std::shared_ptr; - - // Return the root cause of this error, which may be this object. - // For errors caused by an OS error, this will typically be the - // error_code error. - [[nodiscard]] auto root_cause(this error const &) -> error const &; - - // Format this error as a string. - [[nodiscard]] auto str(this error const &) -> std::string; - - // Return this error's error_code, if any. - [[nodiscard]] auto code(this error const &) - -> std::optional; - - // Return this error's error_condition, if any. - [[nodiscard]] auto condition(this error const &) - -> std::optional; - - [[nodiscard]] auto what() const noexcept -> char const * final; - -private: - friend auto operator==(error const &, error const &) -> bool; - friend auto operator<=>(error const &, error const &) - -> std::strong_ordering; - - // This error. - error_t m_error = make_error_code(std::errc()); - - // The cause of this error, if any. - std::shared_ptr m_cause; - - // For std::exception::what(), we need to keep the string valid - // until we're destroyed. - mutable std::optional m_what; -}; - -/* - * Format an error and its cause(s) as a string. - */ -export [[nodiscard]] auto to_string(error const &) -> std::string; - -// Compare an error to another error. This only compares the error itself, -// not any nested causes. -export [[nodiscard]] auto operator==(error const &, error const &) - -> bool; -export [[nodiscard]] auto operator<=>(error const &, error const &) - -> std::strong_ordering; - -// Compare an error to an std::error_code. -export [[nodiscard]] auto operator==(error const &, std::error_code const &) - -> bool; - -// Compare an error to an std::error_condition. -export [[nodiscard]] auto operator==(error const &, - std::error_condition const &) - -> bool; - -// Compare an error to an std::error_code enum. -export [[nodiscard]] auto operator==(error const &lhs, auto rhs) -> bool -requires(std::is_error_code_enum::value) -{ - return lhs.code() == rhs; -} - -// Compare an error to an std::error_condition enum. -export [[nodiscard]] auto operator==(error const &lhs, auto rhs) -> bool -requires(std::is_error_condition_enum::value) -{ - return lhs.condition() == rhs; -} - -// Print an error to an ostream. -export [[nodiscard]] auto operator<<(std::ostream &, error const &) - -> std::ostream &; - -} // namespace nihil - -// Make error formattable. -export template<> -struct std::formatter -{ - template - constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator - { - return ctx.begin(); - } - - template - auto format(nihil::error const &e, FormatContext &ctx) const - -> FormatContext::iterator - { - return std::ranges::copy(to_string(e), ctx.out()).out; - } -}; diff --git a/nihil/exec.cc b/nihil/exec.cc deleted file mode 100644 index 6a7d614..0000000 --- a/nihil/exec.cc +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include - -#include -#include -#include - -extern char **environ; - -module nihil; - -namespace nihil { - -fexecv::fexecv(fd &&execfd, argv &&args) noexcept - : m_execfd(std::move(execfd)) - , m_args(std::move(args)) -{ -} - -auto fexecv::exec(this fexecv &self) - -> std::expected -{ - ::fexecve(self.m_execfd.get(), self.m_args.data(), environ); - return std::unexpected(error("fexecve failed", - error(std::errc(errno)))); -} - -fexecv::fexecv(fexecv &&) noexcept = default; -auto fexecv::operator=(this fexecv &, fexecv &&) noexcept -> fexecv& = default; - -auto execv(std::string_view path, argv &&argv) - -> std::expected -{ - auto file = co_await open_file(path, O_EXEC) - .transform_error([&] (error cause) { - return error(std::format("could not open {}", path), - std::move(cause)); - }); - - co_return fexecv(std::move(file), std::move(argv)); -} - -auto execvp(std::string_view file, argv &&argv) - -> std::expected -{ - auto execfd = find_in_path(file); - if (!execfd) - return std::unexpected(error( - std::format("executable not found in path: {}", file))); - return fexecv(std::move(*execfd), std::move(argv)); -} - -auto shell(std::string_view const &command) - -> std::expected -{ - return execl("/bin/sh", "sh", "-c", command); -} - -} // namespace nihil diff --git a/nihil/exec.ccm b/nihil/exec.ccm deleted file mode 100644 index 5c72dfe..0000000 --- a/nihil/exec.ccm +++ /dev/null @@ -1,106 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -/* - * Exec providers, mostly used for spawn(). - */ - -#include -#include - -export module nihil:exec; - -import :argv; -import :error; -import :fd; -import :find_in_path; - -namespace nihil { - -/* - * A concept to mark spawn executors. - */ - -export struct exec_tag{}; - -export template -concept executor = - requires (T e) { - std::same_as::tag>; - { e.exec() }; - }; - -/* - * fexecv: use a file descriptor and an argument vector to call ::fexecve(). - * This is the lowest-level executor which all others are implemented - * in terms of. - * - * TODO: Should have a way to pass the environment (envp). - */ -export struct fexecv final { - using tag = exec_tag; - - fexecv(fd &&execfd, argv &&args) noexcept; - - [[nodiscard]] auto exec(this fexecv &self) - -> std::expected; - - // Movable - fexecv(fexecv &&) noexcept; - auto operator=(this fexecv &, fexecv &&) noexcept -> fexecv&; - - // Not copyable (because we hold the open fd object) - fexecv(fexecv const &) = delete; - auto operator=(this fexecv &, fexecv const &) -> fexecv& = delete; - -private: - fd m_execfd; - argv m_args; -}; - -/* - * execv: equivalent to fexecv(), except the command is passed as - * a pathname instead of a file descriptor. Does not search $PATH. - */ -export [[nodiscard]] auto execv(std::string_view path, argv &&argv) - -> std::expected; - -/* - * execvp: equivalent to fexecv(), except the command is passed as - * a filename instead of a file descriptor. If the filename is not - * an absolute path, it will be searched for in $PATH. - */ -export [[nodiscard]] auto execvp(std::string_view file, argv &&argv) - -> std::expected; - -/* - * execl: equivalent to execv, except the arguments are passed as a - * variadic pack of string-like objects. - */ -export [[nodiscard]] auto execl(std::string_view path, auto &&...args) - -> std::expected -{ - return execv(path, argv({std::string_view(args)...})); -} - -/* - * execlp: equivalent to execvp, except the arguments are passed as a - * variadic pack of string-like objects. - */ -export [[nodiscard]] auto execlp(std::string_view file, auto &&...args) - -> std::expected -{ - return execvp(file, argv({std::string_view(args)...})); -} - -/* - * shell: run the process by invoking /bin/sh -c with the single argument, - * equivalent to system(3). - */ -export [[nodiscard]] auto shell(std::string_view const &command) - -> std::expected; - -} // namespace nihil diff --git a/nihil/fd.cc b/nihil/fd.cc deleted file mode 100644 index 066056f..0000000 --- a/nihil/fd.cc +++ /dev/null @@ -1,217 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -#include -#include -#include -#include -#include - -module nihil; - -namespace nihil { - -fd::fd() noexcept = default; - -fd::fd(int fileno) noexcept - : m_fileno(fileno) -{ -} - -fd::~fd() -{ - if (*this) - std::ignore = this->close(); -} - -fd::fd(fd &&other) noexcept - : m_fileno(std::exchange(other.m_fileno, invalid_fileno)) -{ -} - -auto fd::operator=(this fd &self, fd &&other) noexcept -> fd & -{ - if (&self != &other) - self.m_fileno = std::exchange(other.m_fileno, invalid_fileno); - return self; -} - -fd::operator bool(this fd const &self) noexcept -{ - return self.m_fileno != invalid_fileno; -} - -auto fd::close(this fd &self) -> std::expected -{ - auto const ret = ::close(self.get()); - self.m_fileno = invalid_fileno; - - if (ret == 0) - return {}; - - return std::unexpected(error(std::errc(errno))); -} - -auto fd::get(this fd const &self) -> int -{ - if (self) - return self.m_fileno; - throw std::logic_error("Attempt to call get() on invalid fd"); -} - -auto fd::release(this fd &&self) -> int -{ - if (self) - return std::exchange(self.m_fileno, invalid_fileno); - throw std::logic_error("Attempt to release an invalid fd"); -} - -auto dup(fd const &self) -> std::expected -{ - auto const newfd = ::dup(self.get()); - if (newfd != -1) - return newfd; - - return std::unexpected(error(std::errc(errno))); -} - -auto dup(fd const &self, int newfd) -> std::expected -{ - auto const ret = ::dup2(self.get(), newfd); - if (ret != -1) - return newfd; - - return std::unexpected(error(std::errc(errno))); -} - -auto raw_dup(fd const &self) -> std::expected -{ - auto const newfd = ::dup(self.get()); - if (newfd != -1) - return newfd; - - return std::unexpected(error(std::errc(errno))); -} - -auto raw_dup(fd const &self, int newfd) -> std::expected -{ - auto const ret = ::dup2(self.get(), newfd); - if (ret != -1) - return newfd; - - return std::unexpected(error(std::errc(errno))); -} - -auto getflags(fd const &self) -> std::expected -{ - auto const flags = ::fcntl(self.get(), F_GETFL); - if (flags != -1) - return flags; - - return std::unexpected(error(std::errc(errno))); -} - -auto replaceflags(fd &self, int newflags) -> std::expected -{ - auto const ret = ::fcntl(self.get(), F_SETFL, newflags); - if (ret == 0) - return {}; - - return std::unexpected(error(std::errc(errno))); -} - -auto setflags(fd &self, int newflags) -> std::expected -{ - auto flags = co_await getflags(self); - - flags |= newflags; - co_await replaceflags(self, flags); - - co_return flags; -} - -auto clearflags(fd &self, int clrflags) -> std::expected -{ - auto flags = co_await getflags(self); - - flags &= ~clrflags; - co_await replaceflags(self, flags); - - co_return flags; -} - -auto getfdflags(fd const &self) -> std::expected -{ - auto const flags = ::fcntl(self.get(), F_GETFD); - if (flags != -1) - return flags; - - return std::unexpected(error(std::errc(errno))); -} - -auto replacefdflags(fd &self, int newflags) -> std::expected -{ - auto const ret = ::fcntl(self.get(), F_SETFD, newflags); - if (ret != -1) - return {}; - - return std::unexpected(error(std::errc(errno))); -} - -auto setfdflags(fd &self, int newflags) -> std::expected -{ - auto flags = co_await getfdflags(self); - - flags |= newflags; - co_await replacefdflags(self, flags); - - co_return flags; -} - -auto clearfdflags(fd &self, int clrflags) -> std::expected -{ - auto flags = co_await getfdflags(self); - - flags &= ~clrflags; - co_await replacefdflags(self, flags); - - co_return flags; -} - -auto pipe() -> std::expected, error> -{ - auto fds = std::array{}; - - if (auto const ret = ::pipe(fds.data()); ret != 0) - return std::unexpected(error(std::errc(errno))); - - return {{fd(fds[0]), fd(fds[1])}}; -} - -auto fd::write(this fd &self, std::span buffer) - -> std::expected -{ - auto const ret = ::write(self.get(), buffer.data(), buffer.size()); - if (ret >= 0) - return ret; - - return std::unexpected(error(std::errc(errno))); -} - -auto fd::read(this fd &self, std::span buffer) - -> std::expected, error> -{ - auto const ret = ::read(self.get(), buffer.data(), buffer.size()); - if (ret >= 0) - return buffer.subspan(0, ret); - - return std::unexpected(error(std::errc(errno))); -} - -} // namespace nihil diff --git a/nihil/fd.ccm b/nihil/fd.ccm deleted file mode 100644 index 3349fbd..0000000 --- a/nihil/fd.ccm +++ /dev/null @@ -1,156 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include - -export module nihil:fd; - -import :error; - -namespace nihil { - -/* - * fd: a file descriptor. - */ - -export struct fd final { - // Construct an empty (invalid) fd. - fd() noexcept; - - // Construct an fd from an exising file destrictor, taking ownership. - fd(int fd_) noexcept; - - // Destructor. Close the fd, discarding any errors. - ~fd(); - - // Move from another fd, leaving the moved-from fd in an invalid state. - fd(fd &&other) noexcept; - auto operator=(this fd &, fd &&other) noexcept -> fd &; - - // Not copyable. - fd(fd const &) = delete; - fd& operator=(this fd &, fd const &) = delete; - - // Return true if this fd is valid (open). - [[nodiscard]] explicit operator bool(this fd const &self) noexcept; - - // Close the wrapped fd. - [[nodiscard]] auto close(this fd &self) -> std::expected; - - // Return the stored fd. - [[nodiscard]] auto get(this fd const &self) -> int; - - // Release the stored fd and return it. The caller must close it. - [[nodiscard]] auto release(this fd &&self) -> int; - - // Write data from the provided buffer to the fd. Returns the - // number of bytes written. - [[nodiscard]] auto write(this fd &self, std::span) - -> std::expected; - - // Read data from the fd to the provided buffer. Returns a - // subspan containing the data which was read. - [[nodiscard]] auto read(this fd &self, std::span) - -> std::expected, error>; - -private: - static constexpr int invalid_fileno = -1; - - int m_fileno = invalid_fileno; -}; - -// Create a copy of this fd by calling dup(). -export [[nodiscard]] auto dup(fd const &self) -> std::expected; - -// 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 [[nodiscard]] auto dup(fd const &self, int newfd) - -> std::expected; - -// Create a copy of this fd by calling dup(). -export [[nodiscard]] auto raw_dup(fd const &self) - -> std::expected; - -// Create a copy of this fd by calling dup2(). -export [[nodiscard]] auto raw_dup(fd const &self, int newfd) - -> std::expected; - -// Return the fnctl flags for this fd. -export [[nodiscard]] auto getflags(fd const &self) - -> std::expected; - -// Replace the fnctl flags for this fd. -export [[nodiscard]] auto replaceflags(fd &self, int newflags) - -> std::expected; - -// Add bits to the fcntl flags for this fd. Returns the new flags. -export [[nodiscard]] auto setflags(fd &self, int newflags) - -> std::expected; - -// Remove bits from the fcntl flags for this fd. Returns the new flags. -export [[nodiscard]] auto clearflags(fd &self, int clrflags) - -> std::expected; - -// Return the fd flags for this fd. -export [[nodiscard]] auto getfdflags(fd const &self) - -> std::expected; - -// Replace the fd flags for this fd. -export [[nodiscard]] auto replacefdflags(fd &self, int newflags) - -> std::expected; - -// Add bits to the fd flags for this fd. Returns the new flags. -export [[nodiscard]] auto setfdflags(fd &self, int newflags) - -> std::expected; - -// Remove bits from the fd flags for this fd. Returns the new flags. -export [[nodiscard]] auto clearfdflags(fd &self, int clrflags) - -> std::expected; - -// Create two fds by calling pipe() and return them. -export [[nodiscard]] auto pipe() -> std::expected, error>; - -/* - * Write data to a file descriptor from the provided range. Returns the - * number of bytes written. - */ -export [[nodiscard]] auto write(fd &file, - std::ranges::contiguous_range auto &&range) - -> std::expected -requires(sizeof(std::ranges::range_value_t) == 1) -{ - return file.write(as_bytes(std::span(range))); -} - -/* - * Read data from a file descriptor into the provided buffer. Returns a - * span containing the data that was read. - */ -export [[nodiscard]] auto read(fd &file, - std::ranges::contiguous_range auto &&range) - -> std::expected< - std::span>, - error> -requires(sizeof(std::ranges::range_value_t) == 1) -{ - auto bspan = as_writable_bytes(std::span(range)); - auto rspan = co_await file.read(bspan); - co_return std::span(range).subspan(0, rspan.size()); -} - -} // namespace nihil diff --git a/nihil/find_in_path.cc b/nihil/find_in_path.cc deleted file mode 100644 index 2b8a57c..0000000 --- a/nihil/find_in_path.cc +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -#include -#include - -module nihil; - -namespace nihil { - -auto find_in_path(std::filesystem::path const &file) --> std::optional -{ - using namespace std::literals; - - auto try_open = - [](std::filesystem::path const &file) -> std::optional - { - auto ret = open_file(file, O_EXEC); - if (ret) - return {std::move(*ret)}; - return {}; - }; - - // Absolute pathname skips the search. - if (file.is_absolute()) - return try_open(file); - - auto path = getenv("PATH").value_or(_PATH_DEFPATH); - - for (auto &&dir : path | std::views::split(':')) { - // An empty $PATH element means cwd. - auto sdir = dir.empty() - ? std::filesystem::path(".") - : std::filesystem::path(std::string_view(dir)); - - if (auto ret = try_open(sdir / file); ret) - return ret; - } - - return {}; -} - -} // namespace lfjail diff --git a/nihil/find_in_path.ccm b/nihil/find_in_path.ccm deleted file mode 100644 index 3983c40..0000000 --- a/nihil/find_in_path.ccm +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -export module nihil:find_in_path; - -import :fd; - -namespace nihil { - -/* - * Find an executable in $PATH, open it with O_EXEC and return the fd. - * If $PATH is not set, uses _PATH_DEFPATH. If the file can't be found - * or opened, returns std::nullopt. - */ -export [[nodiscard]] auto find_in_path(std::filesystem::path const &file) - -> std::optional; - -} // namespace nihil diff --git a/nihil/format_filesystem.ccm b/nihil/format_filesystem.ccm deleted file mode 100644 index 11d8675..0000000 --- a/nihil/format_filesystem.ccm +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include - -export module nihil:format_filesystem; - -/* - * std::formatter for path was only added in C++26; LLVM 19 doesn't have it. - * This is a basic implementation that doesn't support any format flags. - */ - -#ifndef __cpp_lib_format_path -export template<> -struct std::formatter -{ - template - constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator - { - return ctx.begin(); - } - - template - auto format(std::filesystem::path const &path, FmtContext& ctx) const - -> FmtContext::iterator - { - return std::ranges::copy(path.native(), ctx.out()).out; - } -}; -#endif // !__cpp_lib_format_path diff --git a/nihil/generator.ccm b/nihil/generator.ccm deleted file mode 100644 index b5a23ee..0000000 --- a/nihil/generator.ccm +++ /dev/null @@ -1,692 +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 -#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/getenv.cc b/nihil/getenv.cc deleted file mode 100644 index 3799d09..0000000 --- a/nihil/getenv.cc +++ /dev/null @@ -1,43 +0,0 @@ - -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include - -#include - -module nihil; - -namespace nihil { - -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(error(std::errc(errno))); - } -} - -} // namespace nihil diff --git a/nihil/getenv.ccm b/nihil/getenv.ccm deleted file mode 100644 index a7831e0..0000000 --- a/nihil/getenv.ccm +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include - -export module nihil:getenv; - -import :error; - -namespace nihil { - -/* - * Find a variable by the given name in the environment by calling getenv_r(). - */ - -export [[nodiscard]] auto getenv(std::string_view varname) - -> std::expected; - -} // namespace nihil diff --git a/nihil/guard.ccm b/nihil/guard.ccm deleted file mode 100644 index 554f605..0000000 --- a/nihil/guard.ccm +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#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) : m_func(std::move(func)) {} - - /* - * We are being destroyed, so call the callable. - * If the callable throws, std::terminate() will be called. - */ - ~guard() { - if (m_func) - std::invoke(*m_func); - } - - // Release the guard. This turns the destructor into a no-op. - void release() noexcept { - m_func.reset(); - } - - // Not default-constructible, movable 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 m_func; -}; - -} // namespace nihil diff --git a/nihil/match.ccm b/nihil/match.ccm deleted file mode 100644 index f51ce4b..0000000 --- a/nihil/match.ccm +++ /dev/null @@ -1,23 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include - -export module nihil:match; - -namespace nihil { - -export template -struct match : Ts... { using Ts::operator()...; }; - -export template -constexpr decltype(auto) operator| (std::variant const &v, - match const &match) -{ - return std::visit(match, v); -} - -} // namespace nihil diff --git a/nihil/monad.ccm b/nihil/monad.ccm deleted file mode 100644 index 9a2db2e..0000000 --- a/nihil/monad.ccm +++ /dev/null @@ -1,289 +0,0 @@ -/* - * From https://github.com/toby-allsopp/coroutine_monad - * - * Copyright (c) 2017 Toby Allsopp - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -module; - -#include -#include -#include -#include -#include - -export module nihil:monad; - -namespace nihil { - -/********************************************************************** - * return_object_holder - */ - -// An object that starts out unitialized. Initialized by a call to emplace. -template -using deferred = std::optional; - -template -struct return_object_holder { - // The staging object that is returned (by copy/move) to the caller of - // the coroutine. - deferred stage; - return_object_holder*& p; - - // When constructed, we assign a pointer to ourselves to the supplied - // reference to pointer. - return_object_holder(return_object_holder*& p) - : stage{} - , p(p) - { - p = this; - } - - // Copying doesn't make any sense (which copy should the pointer refer - // to?). - return_object_holder(return_object_holder const&) = delete; - - // To move, we just update the pointer to point at the new object. - return_object_holder(return_object_holder&& other) - : stage(std::move(other.stage)) - , p(other.p) - { - p = this; - } - - // Assignment doesn't make sense. - void operator=(return_object_holder const&) = delete; - void operator=(return_object_holder&&) = delete; - - // A non-trivial destructor is required until - // https://bugs.llvm.org//show_bug.cgi?id=28593 is fixed. - ~return_object_holder() {} - - // Construct the staging value; arguments are perfect forwarded to T's - // constructor. - template - void emplace(Args&&... args) - { - stage.emplace(std::forward(args)...); - } - - // We assume that we will be converted only once, so we can move from - // the staging object. We also assume that `emplace` has been called - // at least once. - operator T() - { - return std::move(*stage); - } -}; - -template -auto make_return_object_holder(return_object_holder*& p) -{ - return return_object_holder{p}; -} - -/********************************************************************** - * std::optional - */ - -template -struct optional_promise { - return_object_holder>* data; - - auto get_return_object() - { - return make_return_object_holder(data); - } - - auto initial_suspend() noexcept -> std::suspend_never - { - return {}; - } - - auto final_suspend() noexcept -> std::suspend_never - { - return {}; - } - - void return_value(T x) - { - data->emplace(std::move(x)); - } - - void unhandled_exception() - { - std::rethrow_exception(std::current_exception()); - } -}; - -} // namespace nihil - -export template -struct std::coroutine_traits, Args...> { - using promise_type = nihil::optional_promise; -}; - -namespace nihil { - -template -struct optional_awaitable { - std::optional o; - - auto await_ready() - { - return o.has_value(); - } - - auto await_resume() - { - return *o; - } - - template - void await_suspend(std::coroutine_handle> h) - { - h.promise().data->emplace(std::nullopt); - h.destroy(); - } -}; - -} // namespace nihil - -namespace std { - -export template -auto operator co_await(std::optional o) { - return nihil::optional_awaitable{std::move(o)}; -} - -} // namespace std - -/********************************************************************** - * std::expected - */ - -namespace nihil { - -export template -struct expected_promise_base { - return_object_holder>* data; - - auto get_return_object() - { - return make_return_object_holder(data); - } - - auto initial_suspend() noexcept -> std::suspend_never - { - return {}; - } - - auto final_suspend() noexcept -> std::suspend_never - { - return {}; - } - - void unhandled_exception() - { - std::rethrow_exception(std::current_exception()); - } -}; - -export template -struct expected_promise : expected_promise_base { - void return_value(this expected_promise &self, std::unexpected err) - { - self.data->emplace(std::move(err)); - } - - void return_value(this expected_promise &self, T o) - { - self.data->emplace(std::move(o)); - } -}; - -export template -struct expected_promise : expected_promise_base { - void return_value(this expected_promise &self, std::unexpected err) - { - self.data->emplace(std::move(err)); - } - - void return_value(this expected_promise &self, - std::expected o) - { - self.data->emplace(std::move(o)); - } -}; - -} // namespace nihil - -export template -struct std::coroutine_traits, Args...> { - using promise_type = nihil::expected_promise; -}; - -namespace nihil { - -export template -struct expected_awaitable_base { - std::expected o; - - auto await_ready() - { - return o.has_value(); - } - - template - void await_suspend(std::coroutine_handle

h) - { - h.promise().data->emplace(std::unexpected(o.error())); - h.destroy(); - } -}; - -export template -struct expected_awaitable : expected_awaitable_base { - auto await_resume(this expected_awaitable &self) - { - return std::move(*self.o); - } -}; - -export template -struct expected_awaitable : expected_awaitable_base { - auto await_resume(this expected_awaitable &) - { - return std::expected(); - } -}; - -} // namespace nihil - -namespace std { - -export template -auto operator co_await(std::expected o) { - return nihil::expected_awaitable{std::move(o)}; -} - -} // namespace std diff --git a/nihil/next_word.ccm b/nihil/next_word.ccm deleted file mode 100644 index 6c988a0..0000000 --- a/nihil/next_word.ccm +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include - -export module nihil:next_word; - -import :ctype; -import :skipws; - -namespace nihil { - -/* - * Return the next word from a string_view. Skips leading whitespace, so - * calling this repeatedly will return each word from the string. - */ - -export template [[nodiscard]] -auto next_word(std::basic_string_view text, - std::locale const &locale = std::locale()) - -> std::pair, - std::basic_string_view> -{ - text = skipws(text, locale); - - auto is_space = ctype_is(std::ctype_base::space, locale); - auto split_pos = std::ranges::find_if(text, is_space); - - return {{std::ranges::begin(text), split_pos}, - {split_pos, std::ranges::end(text)}}; -} - -export template -auto next_word(std::basic_string_view *text, - std::locale const &locale = std::locale()) - -> std::basic_string_view -{ - auto [word, rest] = next_word(*text, locale); - *text = rest; - return word; -} - -} // namespace nihil diff --git a/nihil/nihil.ccm b/nihil/nihil.ccm deleted file mode 100644 index cbcadaf..0000000 --- a/nihil/nihil.ccm +++ /dev/null @@ -1,34 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -export module nihil; - -export import :argv; -export import :command_map; -export import :ctype; -export import :ensure_dir; -export import :errc; -export import :error; -export import :exec; -export import :fd; -export import :find_in_path; -export import :format_filesystem; -export import :generator; -export import :getenv; -export import :guard; -export import :match; -export import :monad; -export import :next_word; -export import :open_file; -export import :parse_size; -export import :process; -export import :read_file; -export import :rename_file; -export import :skipws; -export import :spawn; -export import :tabulate; -export import :usage_error; -export import :write_file; diff --git a/nihil/open_file.cc b/nihil/open_file.cc deleted file mode 100644 index 49a3930..0000000 --- a/nihil/open_file.cc +++ /dev/null @@ -1,28 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include - -#include -#include - -module nihil; - -namespace nihil { - -auto open_file(std::filesystem::path const &filename, int flags, int mode) - -> std::expected -{ - auto fdno = ::open(filename.c_str(), flags, mode); - if (fdno != -1) - return fd(fdno); - - return std::unexpected(error(std::errc(errno))); -} - -} // namespace nihil diff --git a/nihil/open_file.ccm b/nihil/open_file.ccm deleted file mode 100644 index 1333785..0000000 --- a/nihil/open_file.ccm +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include - -export module nihil:open_file; - -import :error; -import :fd; - -namespace nihil { - -/* - * Open the given file and return an fd for it. - */ -export [[nodiscard]] auto open_file(std::filesystem::path const &filename, - int flags, int mode = 0777) - -> std::expected; - -} // namespace nihil diff --git a/nihil/parse_size.ccm b/nihil/parse_size.ccm deleted file mode 100644 index a449431..0000000 --- a/nihil/parse_size.ccm +++ /dev/null @@ -1,105 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include -#include -#include - -export module nihil:parse_size; - -import :ctype; -import :errc; -import :error; - -namespace nihil { - -template -auto get_multiplier(Char c) -> std::expected -{ - auto ret = std::uint64_t{1}; - - switch (c) { - case 'p': case 'P': ret *= 1024; - case 't': case 'T': ret *= 1024; - case 'g': case 'G': ret *= 1024; - case 'm': case 'M': ret *= 1024; - case 'k': case 'K': ret *= 1024; - return ret; - - default: - return std::unexpected(error(errc::invalid_unit)); - } -} - -/* - * Parse a string containing a human-formatted size, such as "1024" - * or "4g". Parsing is always done in the "C" locale and does not - * recognise thousands separators or negative numbers. - */ -export template [[nodiscard]] -auto parse_size(std::basic_string_view str) - -> std::expected -{ - // Extract the numeric part of the string. - auto it = std::ranges::find_if_not(str, is_c_digit); - auto num_str = std::basic_string_view( - std::ranges::begin(str), it); - - if (num_str.empty()) - co_return std::unexpected(error(errc::empty_string)); - - auto ret = T{0}; - - for (auto c : num_str) { - if (ret > (std::numeric_limits::max() / 10)) - co_return std::unexpected(error( - std::errc::result_out_of_range)); - ret *= 10; - - auto digit = static_cast(c - '0'); - if ((std::numeric_limits::max() - digit) < ret) - co_return std::unexpected(error( - std::errc::result_out_of_range)); - ret += digit; - } - - if (it == str.end()) - // No multiplier. - co_return ret; - - auto mchar = *it++; - - if (it != str.end()) - // Multiplier is more than one character. - co_return std::unexpected(error(errc::invalid_unit)); - - auto mult = co_await get_multiplier(mchar); - - if (std::cmp_greater(ret, std::numeric_limits::max() / mult)) - co_return std::unexpected(error( - std::errc::result_out_of_range)); - - co_return ret * mult; -} - -export template -[[nodiscard]] inline auto parse_size(char const *s) -{ - return parse_size(std::string_view(s)); -} - -export template -[[nodiscard]] inline auto parse_size(wchar_t const *s) -{ - return parse_size(std::wstring_view(s)); -} - -} diff --git a/nihil/process.cc b/nihil/process.cc deleted file mode 100644 index 5cfe79e..0000000 --- a/nihil/process.cc +++ /dev/null @@ -1,100 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -module nihil; - -namespace nihil { - -auto wait_result::okay(this wait_result const &self) -> bool -{ - return self.status() == 0; -} - -wait_result::operator bool(this wait_result const &self) -{ - return self.okay(); -} - -auto wait_result::status(this wait_result const &self) -> std::optional -{ - if (WIFEXITED(self._status)) - return WEXITSTATUS(self._status); - return {}; -} - -auto wait_result::signal(this wait_result const &self) -> std::optional -{ - if (WIFSIGNALED(self._status)) - return WTERMSIG(self._status); - return {}; -} - -wait_result::wait_result(int status) - : _status(status) -{} - -process::process(::pid_t pid) - : m_pid(pid) -{} - -process::~process() { - if (m_pid == -1) - return; - - auto status = int{}; - std::ignore = waitpid(m_pid, &status, WEXITED); -} - -process::process(process &&other) noexcept - : m_pid(std::exchange(other.m_pid, -1)) -{ -} - -auto process::operator=(this process &self, process &&other) noexcept - -> process & -{ - if (&self != &other) { - self.m_pid = std::exchange(other.m_pid, -1); - } - - return self; -} - -// Get the child's process id. -auto process::pid(this process const &self) noexcept -> ::pid_t -{ - return self.m_pid; -} - -auto process::wait(this process &&self) -> std::expected -{ - auto status = int{}; - auto ret = waitpid(self.m_pid, &status, WEXITED); - if (ret == -1) - return std::unexpected(error(std::errc(errno))); - - return wait_result(status); -} - -auto process::release(this process &&self) -> ::pid_t -{ - auto const ret = self.pid(); - self.m_pid = -1; - return ret; -} - -} // namespace nihil diff --git a/nihil/process.ccm b/nihil/process.ccm deleted file mode 100644 index 6d74513..0000000 --- a/nihil/process.ccm +++ /dev/null @@ -1,91 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -#include - -export module nihil:process; - -import :error; - -namespace nihil { - -/* - * wait_result: the exit status of a process. - */ -export struct wait_result final { - // Return true if the process exited normally with an exit code of - // zero, otherwise false. - [[nodiscard]] auto okay(this wait_result const &self) -> bool; - [[nodiscard]] explicit operator bool(this wait_result const &self); - - // Return the exit status, if any. - [[nodiscard]] auto status(this wait_result const &self) - -> std::optional; - - // Return the exit signal, if any. - [[nodiscard]] auto signal(this wait_result const &self) - -> std::optional; - -private: - friend struct process; - - int _status; - - // Construct a new wait_result from the output of waitpid(). - wait_result(int status); -}; - -/* - * process: represents a process we created, which can be waited for. - */ -export struct process final { - process() = delete; - - /* - * Create a new process from a pid, which must be a child of the - * current process. - */ - process(::pid_t pid); - - // When destroyed, we automatically wait for the process to - // avoid creating zombie processes. - ~process(); - - // Movable. - process(process &&) noexcept; - auto operator=(this process &, process &&) noexcept -> process &; - - // Not copyable. - process(process const &) = delete; - auto operator=(this process &, process const &) -> process & = delete; - - // Get the child's process id. - [[nodiscard]] auto pid(this process const &self) noexcept -> ::pid_t; - - /* - * Wait for this process to exit (by calling waitpid()) and return - * its exit status. This destroys the process state, leaving this - * object in a moved-from state. - */ - [[nodiscard]] auto wait(this process &&self) - -> std::expected; - - /* - * Release this process so we won't try to wait for it when - * destroying this object. - */ - [[nodiscard]] auto release(this process &&self) -> ::pid_t; - -private: - ::pid_t m_pid; -}; - -} // namespace nihil diff --git a/nihil/read_file.ccm b/nihil/read_file.ccm deleted file mode 100644 index cc93f7e..0000000 --- a/nihil/read_file.ccm +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include - -#include -#include - -export module nihil:read_file; - -import :error; -import :fd; -import :monad; -import :open_file; - -namespace nihil { - -/* - * Read the contents of a file into an output iterator. - */ -export [[nodiscard]] auto -read_file(std::filesystem::path const &filename, - std::output_iterator auto &&iter) - -> std::expected -{ - auto file = co_await open_file(filename, O_RDONLY); - - auto constexpr bufsize = std::size_t{1024}; - auto buffer = std::array{}; - - for (;;) { - auto read_buf = co_await(read(file, buffer)); - if (read_buf.empty()) - co_return {}; - - std::ranges::copy(read_buf, iter); - } -} - -} // namespace nihil diff --git a/nihil/rename_file.cc b/nihil/rename_file.cc deleted file mode 100644 index c712e44..0000000 --- a/nihil/rename_file.cc +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -module nihil; - -namespace nihil { - -/* - * Rename a file. - */ -auto rename_file(std::filesystem::path const &oldp, - std::filesystem::path const &newp) - -> std::expected -{ - auto err = std::error_code(); - - std::filesystem::rename(oldp, newp, err); - - if (err) - return std::unexpected(error(err)); - - return {}; -} - - -} // namespace nihil diff --git a/nihil/rename_file.ccm b/nihil/rename_file.ccm deleted file mode 100644 index 4abe975..0000000 --- a/nihil/rename_file.ccm +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include - -export module nihil:rename_file; - -import :error; - -namespace nihil { - -/* - * Rename a file (or directory). - */ -export [[nodiscard]] auto -rename_file(std::filesystem::path const &oldp, - std::filesystem::path const &newp) - -> std::expected; - -} // namespace nihil diff --git a/nihil/skipws.ccm b/nihil/skipws.ccm deleted file mode 100644 index 3d760a9..0000000 --- a/nihil/skipws.ccm +++ /dev/null @@ -1,40 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include - -export module nihil:skipws; - -import :ctype; - -namespace nihil { - -/* - * Remove leading whitespace from a string. - */ - -export template [[nodiscard]] -auto skipws(std::basic_string_view text, - std::locale const &locale = std::locale()) - -> std::basic_string_view -{ - auto is_space = ctype_is(std::ctype_base::space, locale); - auto nonws = std::ranges::find_if_not(text, is_space); - return {nonws, std::ranges::end(text)}; -} - -export template -auto skipws(std::basic_string_view *text, - std::locale const &locale = std::locale()) - -> void -{ - *text = skipws(*text, locale); -} - -} // namespace nihil diff --git a/nihil/spawn.ccm b/nihil/spawn.ccm deleted file mode 100644 index c15ab8d..0000000 --- a/nihil/spawn.ccm +++ /dev/null @@ -1,250 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -/* - * spawn(): fork and execute a child process. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -export module nihil:spawn; - -import :argv; -import :exec; -import :fd; -import :monad; -import :open_file; -import :process; - -namespace nihil { - -// Useful constants -export inline int constexpr stdin_fileno = STDIN_FILENO; -export inline int constexpr stdout_fileno = STDOUT_FILENO; -export inline int constexpr stderr_fileno = STDERR_FILENO; - -/* - * fd_pipe: create a pipe with one end in the child and the other in the - * parent. The child's side will be dup2()'d to the provided fd number. - * The parent side fd can be retrieved via parent_fd(); - */ -export struct fd_pipe final { - fd_pipe(int fdno, fd &&child_fd, fd &&parent_fd) - : m_fdno(fdno) - , m_child_fd(std::move(child_fd)) - , m_parent_fd(std::move(parent_fd)) - { - } - - auto run_in_child(this fd_pipe &self, process &) -> void - { - auto err = raw_dup(self.m_child_fd, self.m_fdno); - if (!err) { - std::print("dup: {}\n", err.error()); - _exit(1); - } - - /* - * We don't care about errors from close() since the fd - * is still closed. - */ - std::ignore = self.m_parent_fd.close(); - std::ignore = self.m_child_fd.close(); - } - - auto run_in_parent(this fd_pipe &self, process &) -> void - { - std::ignore = self.m_child_fd.close(); - } - - [[nodiscard]] auto parent_fd(this fd_pipe &self) -> fd & - { - return self.m_parent_fd; - } - -private: - int m_fdno; - fd m_child_fd; - fd m_parent_fd; -}; - -export [[nodiscard]] auto -make_fd_pipe(int fdno) -> std::expected -{ - auto fds = co_await pipe(); - co_return fd_pipe(fdno, std::move(fds.first), std::move(fds.second)); -} - -/* - * fd_file: open a file and provide it to the child as a file descriptor. - * open_flags and open_mode are as for ::open(). - */ -export struct fd_file final { - fd_file(int fdno, fd &&file_fd) - : m_fdno(fdno) - , m_file_fd(std::move(file_fd)) - { - } - - auto run_in_parent(this fd_file &self, process &) -> void - { - std::ignore = self.m_file_fd.close(); - } - - auto run_in_child(this fd_file &self, process &) -> void - { - auto err = raw_dup(self.m_file_fd, self.m_fdno); - if (!err) { - std::print("dup: {}\n", err.error()); - _exit(1); - } - - std::ignore = self.m_file_fd.close(); - } - -private: - int m_fdno; - fd m_file_fd; -}; - -export [[nodiscard]] auto -make_fd_file(int fdno, std::filesystem::path const &file, - int flags, int mode = 0777) - -> std::expected -{ - auto fd = co_await open_file(file, flags, mode); - co_return fd_file(fdno, std::move(fd)); -} - -/* - * Shorthand for fd_file with /dev/null as the file. - */ - -export [[nodiscard]] inline auto -stdin_devnull() -> std::expected -{ - return make_fd_file(stdin_fileno, "/dev/null", O_RDONLY); -} - -export [[nodiscard]] inline auto -stdout_devnull() -> std::expected -{ - return make_fd_file(stdout_fileno, "/dev/null", O_WRONLY); -} - -export [[nodiscard]] inline auto -stderr_devnull() -> std::expected -{ - return make_fd_file(stderr_fileno, "/dev/null", O_WRONLY); -} - -/* - * Capture the output of a pipe in the parent and read it into an - * output iterator. - */ -export template Iterator> -struct fd_capture final { - fd_capture(fd_pipe &&pipe, Iterator it) - : m_pipe(std::move(pipe)) - , m_iterator(std::move(it)) - { - } - - auto run_in_child(this fd_capture &self, process &p) -> void - { - self.m_pipe.run_in_child(p); - } - - auto run_in_parent(this fd_capture &self, process &p) -> void - { - self.m_pipe.run_in_parent(p); - - auto constexpr bufsize = std::size_t{1024}; - auto buffer = std::array(); - auto &fd = self.m_pipe.parent_fd(); - for (;;) { - auto ret = read(fd, buffer); - if (!ret || ret->size() == 0) - break; - - std::ranges::copy(*ret, self.m_iterator); - } - - // We probably want to handle errors here somehow, - // but it's not clear what would be useful behaviour. - } - -private: - fd_pipe m_pipe; - Iterator m_iterator; -}; - -export [[nodiscard]] auto -make_capture(int fdno, std::output_iterator auto &&it) - -> std::expected, error> -{ - auto pipe = co_await make_fd_pipe(fdno); - co_return fd_capture(std::move(pipe), - std::forward(it)); -} - -export [[nodiscard]] auto -make_capture(int fdno, std::string &str) - -> std::expected, error> -{ - auto pipe = co_await make_fd_pipe(fdno); - co_return fd_capture(std::move(pipe), std::back_inserter(str)); -} - -/* - * Spawn a new process with the given arguments and return a struct process. - * Throws exec_error() on failure. - */ -export [[nodiscard]] auto -spawn(executor auto &&executor, auto &&...actions) - -> std::expected -{ - auto const pid = ::fork(); - if (pid == -1) - return std::unexpected(error("fork failed", - error(std::errc(errno)))); - - auto proc = process(pid); - - if (pid == 0) { - // We are in the child. Release the process so we don't - // try to wait for ourselves, then run child handlers and - // exec the process. - - std::ignore = std::move(proc).release(); - (actions.run_in_child(proc), ...); - - auto err = executor.exec(); - std::print("{}\n", err.error()); - _exit(1); - } - - (actions.run_in_parent(proc), ...); - - return proc; -} - -} // namespace nihil diff --git a/nihil/tabulate.ccm b/nihil/tabulate.ccm deleted file mode 100644 index 9faf885..0000000 --- a/nihil/tabulate.ccm +++ /dev/null @@ -1,312 +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 :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 : error { - table_spec_error(std::string_view what) - : error(what) - { - } -}; - -/* - * The specification for a single field. - */ -template -struct field_spec { - enum align_t { left, right }; - - // Get the name of this field. - auto name(this field_spec const &self) - -> std::basic_string_view - { - return self.m_name; - } - - // Set the name of this field. - auto name(this field_spec &self, - std::basic_string_view new_name) - -> void - { - self.m_name = new_name; - } - - // Set this field's alignment. - auto align(this field_spec &self, align_t new_align) -> void - { - self.m_align = new_align; - } - - // Ensure the length of this field is at least the given width. - auto ensure_width(this field_spec &self, std::size_t newwidth) - -> void - { - self.m_width = std::max(self.m_width, newwidth); - } - - // Format an object to a string based on our field spec. - [[nodiscard]] auto format(this field_spec const &, auto &&obj) - -> std::basic_string - { - auto format_string = std::basic_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(this field_spec const &self, - std::basic_string_view value, - std::output_iterator auto &out, - bool is_last) - -> void - { - auto padding = self.m_width - value.size(); - - if (self.m_align == right) - for (std::size_t i = 0; i < padding; ++i) - *out++ = ' '; - - std::ranges::copy(value, out); - - if (!is_last && self.m_align == left) - for (std::size_t i = 0; i < padding; ++i) - *out++ = ' '; - } - -private: - std::basic_string_view m_name; - std::size_t m_width = 0; - align_t m_align = left; -}; - -/* - * The specification for an entire table. - */ -template -struct table_spec { - // Add a new field spec to this table. - auto add(this table_spec &self, field_spec field) -> void - { - self.m_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. - [[nodiscard]] auto field(this table_spec &self, std::size_t fieldnr) - -> field_spec & - { - if (fieldnr >= self.m_fields.size()) - self.m_fields.resize(fieldnr + 1); - return self.m_fields.at(fieldnr); - } - - // The number of columns in this table. - [[nodiscard]] auto columns(this table_spec const &self) -> std::size_t - { - return self.m_fields.size(); - } - - // Return all the fields in this table. - [[nodiscard]] auto fields(this table_spec const &self) - -> std::vector> const & - { - return self.m_fields; - } - -private: - std::vector> m_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> -[[nodiscard]] 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.ensure_width(field.name().size()); - - return field; -} - -template -[[nodiscard]] 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/tests/CMakeLists.txt b/nihil/tests/CMakeLists.txt deleted file mode 100644 index dd2d376..0000000 --- a/nihil/tests/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -# This source code is released into the public domain. - -add_executable(nihil.test - command_map.cc - ctype.cc - error.cc - fd.cc - generator.cc - getenv.cc - guard.cc - monad.cc - next_word.cc - parse_size.cc - skipws.cc - spawn.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/nihil/tests/command_map.cc b/nihil/tests/command_map.cc deleted file mode 100644 index de78be5..0000000 --- a/nihil/tests/command_map.cc +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include - -#include - -import nihil; - -namespace { - -auto cmd_sub1_called = false; -auto cmd_sub1 = nihil::command("cmd sub1", "", [](int, char **) -> int -{ - cmd_sub1_called = true; - return 0; -}); - -} // anonymous namespace - -TEST_CASE("command_map: basic", "[command_map]") -{ - auto args = std::vector{ - "cmd", "sub1", nullptr - }; - auto argv = const_cast(args.data()); - - int ret = nihil::dispatch_command(args.size() - 1, argv); - REQUIRE(ret == 0); - REQUIRE(cmd_sub1_called == true); -} diff --git a/nihil/tests/ctype.cc b/nihil/tests/ctype.cc deleted file mode 100644 index 87f5103..0000000 --- a/nihil/tests/ctype.cc +++ /dev/null @@ -1,373 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include - -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/nihil/tests/error.cc b/nihil/tests/error.cc deleted file mode 100644 index 31124e3..0000000 --- a/nihil/tests/error.cc +++ /dev/null @@ -1,169 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include -#include - -#include - -import nihil; - -TEST_CASE("error: invariants", "[nihil]") -{ - static_assert(std::destructible); - static_assert(std::default_initializable); - static_assert(std::move_constructible); - static_assert(std::copy_constructible); - static_assert(std::equality_comparable); - static_assert(std::totally_ordered); - static_assert(std::swappable); - static_assert(std::regular); -} - -TEST_CASE("error: construct from string", "[nihil]") -{ - using namespace nihil; - - auto e = error("an error"); - REQUIRE(e.str() == to_string(e)); - REQUIRE(to_string(e) == "an error"); - REQUIRE(std::format("{}", e) == to_string(e)); -} - -TEST_CASE("error: construct from std::error_condition", "[nihil]") -{ - using namespace nihil; - - auto code = std::make_error_condition(std::errc::invalid_argument); - auto e = error(code); - - REQUIRE(!e.cause()); - REQUIRE(e.code().has_value() == false); - REQUIRE(e.condition().has_value() == true); - - REQUIRE(e == std::errc::invalid_argument); - REQUIRE(e != std::errc::no_such_file_or_directory); - - REQUIRE(e.str() == to_string(e)); - REQUIRE(to_string(e) == std::strerror(EINVAL)); - REQUIRE(std::format("{}", e) == to_string(e)); -} - -TEST_CASE("error: construct from std::errc", "[nihil]") -{ - using namespace nihil; - - auto e = error(std::errc::invalid_argument); - - REQUIRE(!e.cause()); - REQUIRE(e.code().has_value() == false); - REQUIRE(e.condition().has_value() == true); - - REQUIRE(e == std::errc::invalid_argument); - REQUIRE(e != std::errc::no_such_file_or_directory); - - REQUIRE(e.str() == to_string(e)); - REQUIRE(to_string(e) == std::strerror(EINVAL)); - REQUIRE(std::format("{}", e) == to_string(e)); -} - -TEST_CASE("error: compound error", "[nihil]") -{ - using namespace std::literals; - using namespace nihil; - - auto e = error("cannot open file", - error(std::errc::no_such_file_or_directory)); - - REQUIRE(e.cause()); - REQUIRE(e.code().has_value() == false); - REQUIRE(e.condition().has_value() == false); - - REQUIRE(*e.cause() == std::errc::no_such_file_or_directory); - REQUIRE(e.str() == "cannot open file"); - REQUIRE(to_string(e) == ("cannot open file: "s + - std::strerror(ENOENT))); - REQUIRE(std::format("{}", e) == to_string(e)); -} - -TEST_CASE("error: operator== with strings", "[nihil]") -{ - using namespace nihil; - - auto e1 = error("error"); - auto e2 = error("error"); - auto e3 = error("an error"); - - REQUIRE(e1 == e2); - REQUIRE(e1 != e3); -} - -TEST_CASE("error: operator< with strings", "[nihil]") -{ - using namespace nihil; - - auto e1 = error("aaa"); - auto e2 = error("zzz"); - - REQUIRE(e1 < e2); -} - -TEST_CASE("error: operator== with a cause", "[nihil]") -{ - using namespace nihil; - - auto e1 = error("error", error("cause 1")); - auto e2 = error("error", error("cause 2")); - - REQUIRE(e1 == e2); -} - -TEST_CASE("error: operator== with error_conditions", "[nihil]") -{ - using namespace nihil; - - auto e1 = error(std::errc::invalid_argument); - auto e2 = error(std::errc::invalid_argument); - auto e3 = error(std::errc::permission_denied); - - REQUIRE(e1 == e2); - REQUIRE(e1 != e3); -} - -TEST_CASE("error: std::format with string", "[nihil]") -{ - using namespace nihil; - - auto err = error("an error"); - REQUIRE(std::format("{}", err) == "an error"); -} - -TEST_CASE("error: std::format with std::errc", "[nihil]") -{ - using namespace nihil; - - auto err = error(std::errc::invalid_argument); - REQUIRE(std::format("{}", err) == std::strerror(EINVAL)); -} - -TEST_CASE("error: std::format with cause", "[nihil]") -{ - using namespace nihil; - - auto err = error("an error", std::errc::invalid_argument); - REQUIRE(std::format("{}", err) == "an error: Invalid argument"); -} - -TEST_CASE("error: throw and catch", "[nihil]") -{ - using namespace std::literals; - using namespace nihil; - - try { - throw error("oh no", error(std::errc::invalid_argument)); - } catch (std::exception const &exc) { - REQUIRE(exc.what() == "oh no: Invalid argument"s); - } -} diff --git a/nihil/tests/fd.cc b/nihil/tests/fd.cc deleted file mode 100644 index 59054fa..0000000 --- a/nihil/tests/fd.cc +++ /dev/null @@ -1,198 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include - -#include -#include - -#include - -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(), std::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{}; - auto read_buf = read(fd2, readbuf); - REQUIRE(read_buf); - REQUIRE(std::string_view(*read_buf) == test_string); -} diff --git a/nihil/tests/generator.cc b/nihil/tests/generator.cc deleted file mode 100644 index 8657756..0000000 --- a/nihil/tests/generator.cc +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include -#include - -#include - -import nihil; - -TEST_CASE("generator: basic", "[generator]") -{ - auto fn = [] () -> nihil::generator { - co_yield 1; - co_yield 2; - co_yield 3; - }; - - auto values = std::vector(); - std::ranges::copy(fn(), std::back_inserter(values)); - - REQUIRE(values == std::vector{1, 2, 3}); -} - -TEST_CASE("generator: exceptions", "[generator]") -{ - auto fn = [] () -> nihil::generator { - 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 { - co_yield 1; - co_yield 2; - co_yield 3; - }; - - auto fn2 = [&fn1] -> nihil::generator { - co_yield nihil::ranges::elements_of(fn1()); - }; - - auto values = std::vector(); - std::ranges::copy(fn2(), std::back_inserter(values)); - - REQUIRE(values == std::vector{1, 2, 3}); -} diff --git a/nihil/tests/getenv.cc b/nihil/tests/getenv.cc deleted file mode 100644 index adfa84f..0000000 --- a/nihil/tests/getenv.cc +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include - -#include - -#include - -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/nihil/tests/guard.cc b/nihil/tests/guard.cc deleted file mode 100644 index f88aa9b..0000000 --- a/nihil/tests/guard.cc +++ /dev/null @@ -1,20 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include - -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/nihil/tests/monad.cc b/nihil/tests/monad.cc deleted file mode 100644 index 3964494..0000000 --- a/nihil/tests/monad.cc +++ /dev/null @@ -1,68 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include -#include - -#include - -import nihil; - -TEST_CASE("monad: co_await std::optional<> with value", "[nihil]") -{ - auto get_value = [] -> std::optional { - return 42; - }; - - auto try_get_value = [&get_value] -> std::optional { - co_return co_await get_value(); - }; - - auto o = try_get_value(); - REQUIRE(o == 42); -} - -TEST_CASE("monad: co_await std::optional<> without value", "[nihil]") -{ - auto get_value = [] -> std::optional { - return {}; - }; - - auto try_get_value = [&get_value] -> std::optional { - co_return co_await get_value(); - }; - - auto o = try_get_value(); - REQUIRE(!o.has_value()); -} - -TEST_CASE("monad: co_await std::expected<> with value", "[nihil]") -{ - auto get_value = [] -> std::expected { - return 42; - }; - - auto try_get_value = [&get_value] -> std::expected { - co_return co_await get_value(); - }; - - auto o = try_get_value(); - REQUIRE(o == 42); -} - -TEST_CASE("monad: co_await std::expected<> with error", "[nihil]") -{ - auto get_value = [] -> std::expected { - return std::unexpected("error"); - }; - - auto try_get_value = [&get_value] -> std::expected { - co_return co_await get_value(); - }; - - auto o = try_get_value(); - REQUIRE(!o); - REQUIRE(o.error() == "error"); -} diff --git a/nihil/tests/next_word.cc b/nihil/tests/next_word.cc deleted file mode 100644 index 4055485..0000000 --- a/nihil/tests/next_word.cc +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include - -#include - -import nihil; - -TEST_CASE("next_word: basic", "[next_word]") -{ - using namespace std::literals; - auto s = "foo bar baz"sv; - - auto words = nihil::next_word(s); - REQUIRE(words.first == "foo"); - REQUIRE(words.second == " bar baz"); - - auto word = nihil::next_word(&s); - REQUIRE(word == "foo"); - REQUIRE(s == " bar baz"); -} - -TEST_CASE("next_word: multiple spaces", "[next_word]") -{ - using namespace std::literals; - auto s = "foo bar baz"sv; - - auto words = nihil::next_word(s); - REQUIRE(words.first == "foo"); - REQUIRE(words.second == " bar baz"); - - auto word = nihil::next_word(&s); - REQUIRE(word == "foo"); - REQUIRE(s == " bar baz"); -} - -TEST_CASE("next_word: leading spaces", "[next_word]") -{ - using namespace std::literals; - auto s = " \tfoo bar baz"sv; - - auto words = nihil::next_word(s); - REQUIRE(words.first == "foo"); - REQUIRE(words.second == " bar baz"); - - auto word = nihil::next_word(&s); - REQUIRE(word == "foo"); - REQUIRE(s == " bar baz"); -} - -TEST_CASE("next_word: locale", "[next_word]") -{ - using namespace std::literals; - auto s = L"\u2003foo\u2003bar\u2003baz"sv; - - auto words = nihil::next_word(s); - REQUIRE(words.first == s); - - words = nihil::next_word(s, std::locale("C.UTF-8")); - REQUIRE(words.first == L"foo"); - REQUIRE(words.second == L"\u2003bar\u2003baz"); -} diff --git a/nihil/tests/parse_size.cc b/nihil/tests/parse_size.cc deleted file mode 100644 index fb8188d..0000000 --- a/nihil/tests/parse_size.cc +++ /dev/null @@ -1,167 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include - -#include - -import nihil; - -TEST_CASE("parse_size: empty value", "[nihil]") -{ - using namespace nihil; - - auto n = parse_size(""); - REQUIRE(!n); - REQUIRE(n.error() == nihil::errc::empty_string); -} - -TEST_CASE("parse_size: basic", "[nihil]") -{ - using namespace nihil; - - SECTION("bare number") { - auto n = parse_size("1024").value(); - REQUIRE(n == 1024); - } - - SECTION("max value, unsigned") { - auto n = parse_size("65535").value(); - REQUIRE(n == 65535); - } - - SECTION("max value, signed") { - auto n = parse_size("32767").value(); - REQUIRE(n == 32767); - } - - SECTION("overflow by 1, unsigned") { - auto n = parse_size("65536"); - REQUIRE(!n); - REQUIRE(n.error() == std::errc::result_out_of_range); - } - - SECTION("overflow by 1, signed") { - auto n = parse_size("32768"); - REQUIRE(!n); - REQUIRE(n.error() == std::errc::result_out_of_range); - } - - SECTION("overflow by many, unsigned") { - auto n = parse_size("100000"); - REQUIRE(!n); - REQUIRE(n.error() == std::errc::result_out_of_range); - } - - SECTION("overflow by many, signed") { - auto n = parse_size("100000"); - REQUIRE(!n); - REQUIRE(n.error() == std::errc::result_out_of_range); - } -} - -TEST_CASE("parse_size: invalid multiplier", "[nihil]") -{ - using namespace nihil; - - auto n = parse_size("4z"); - REQUIRE(!n); - REQUIRE(n.error() == nihil::errc::invalid_unit); - - n = parse_size("4kz"); - REQUIRE(!n); - REQUIRE(n.error() == nihil::errc::invalid_unit); -} - -TEST_CASE("parse_size: multipliers", "[nihil]") -{ - using namespace nihil; - - auto sf = static_cast(4); - - SECTION("k") { - auto n = parse_size("4k").value(); - REQUIRE(n == sf * 1024); - } - - SECTION("m") { - auto n = parse_size("4m").value(); - REQUIRE(n == sf * 1024 * 1024); - } - - SECTION("g") { - auto n = parse_size("4g").value(); - REQUIRE(n == sf * 1024 * 1024 * 1024); - } - - SECTION("t") { - auto n = parse_size("4t").value(); - REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024); - } - - SECTION("p") { - auto n = parse_size("4p").value(); - REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024); - } -} - -TEST_CASE("parse_size: multiplier overflow", "[nihil]") -{ - using namespace nihil; - - SECTION("signed") { - auto n = parse_size("64k"); - REQUIRE(!n); - REQUIRE(n.error() == std::errc::result_out_of_range); - } - - SECTION("unsigned") { - auto n = parse_size("32k"); - REQUIRE(!n); - REQUIRE(n.error() == std::errc::result_out_of_range); - } -} - -TEST_CASE("parse_size: wide", "[nihil]") -{ - using namespace nihil; - - SECTION("bare number") { - auto n = parse_size(L"1024").value(); - REQUIRE(n == 1024); - } -} - -TEST_CASE("parse_size: wide multipliers", "[nihil]") -{ - using namespace nihil; - - auto sf = static_cast(4); - - SECTION("k") { - auto n = parse_size(L"4k").value(); - REQUIRE(n == sf * 1024); - } - - SECTION("m") { - auto n = parse_size(L"4m").value(); - REQUIRE(n == sf * 1024 * 1024); - } - - SECTION("g") { - auto n = parse_size(L"4g").value(); - REQUIRE(n == sf * 1024 * 1024 * 1024); - } - - SECTION("t") { - auto n = parse_size(L"4t").value(); - REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024); - } - - SECTION("p") { - auto n = parse_size(L"4p").value(); - REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024); - } -} diff --git a/nihil/tests/skipws.cc b/nihil/tests/skipws.cc deleted file mode 100644 index 2159e2e..0000000 --- a/nihil/tests/skipws.cc +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include -using namespace std::literals; - -#include - -import nihil; - -TEST_CASE("skipws: basic", "[skipws]") -{ - REQUIRE(nihil::skipws("foo"sv) == "foo"); - REQUIRE(nihil::skipws(" foo"sv) == "foo"); - REQUIRE(nihil::skipws("foo "sv) == "foo "); - REQUIRE(nihil::skipws("foo bar"sv) == "foo bar"); -} - -TEST_CASE("skipws: pointer", "[skipws]") -{ - auto s = "foo"sv; - nihil::skipws(&s); - REQUIRE(s == "foo"); - - s = " foo"sv; - nihil::skipws(&s); - REQUIRE(s == "foo"); - - s = "foo "sv; - nihil::skipws(&s); - REQUIRE(s == "foo "); - - s = "foo bar"sv; - nihil::skipws(&s); - REQUIRE(s == "foo bar"); -} - -TEST_CASE("skipws: locale", "[skipws]") -{ - // Assume the default locale is C. - REQUIRE(nihil::skipws(L"\u2003foo"sv) == L"\u2003foo"); - REQUIRE(nihil::skipws(L"\u2003foo"sv, std::locale("C.UTF-8")) == L"foo"); -} diff --git a/nihil/tests/spawn.cc b/nihil/tests/spawn.cc deleted file mode 100644 index 2821fb6..0000000 --- a/nihil/tests/spawn.cc +++ /dev/null @@ -1,117 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include - -import nihil; - -TEST_CASE("spawn: system", "[spawn]") -{ - using namespace nihil; - - auto exec = shell("x=1; echo $x"); - REQUIRE(exec); - - auto output = std::string(); - auto capture = make_capture(stdout_fileno, output); - REQUIRE(capture); - - auto proc = spawn(*exec, *capture); - REQUIRE(proc); - - auto status = std::move(*proc).wait(); - REQUIRE(status); - - REQUIRE(status->okay()); - REQUIRE(output == "1\n"); -} - -TEST_CASE("spawn: execv", "[spawn]") { - using namespace nihil; - - auto args = argv({"sh", "-c", "x=1; echo $x"}); - auto exec = execv("/bin/sh", std::move(args)); - REQUIRE(exec); - - auto output = std::string(); - auto capture = make_capture(stdout_fileno, output); - REQUIRE(capture); - - auto proc = spawn(*exec, *capture); - REQUIRE(proc); - - auto status = std::move(*proc).wait(); - REQUIRE(status); - - REQUIRE(status->okay()); - REQUIRE(output == "1\n"); -} - -TEST_CASE("spawn: execvp", "[spawn]") { - using namespace nihil; - - auto args = argv({"sh", "-c", "x=1; echo $x"}); - auto exec = execvp("sh", std::move(args)); - REQUIRE(exec); - - auto output = std::string(); - auto capture = make_capture(stdout_fileno, output); - REQUIRE(capture); - - auto proc = spawn(*exec, *capture); - REQUIRE(proc); - - auto status = std::move(*proc).wait(); - REQUIRE(status); - - REQUIRE(status->okay()); - REQUIRE(output == "1\n"); -} - -TEST_CASE("spawn: execl", "[spawn]") { - using namespace nihil; - - auto exec = execl("/bin/sh", "sh", "-c", "x=1; echo $x"); - REQUIRE(exec); - - auto output = std::string(); - auto capture = make_capture(stdout_fileno, output); - REQUIRE(capture); - - auto proc = spawn(*exec, *capture); - REQUIRE(proc); - - auto status = std::move(*proc).wait(); - REQUIRE(status); - - REQUIRE(status->okay()); - REQUIRE(output == "1\n"); -} - -TEST_CASE("spawn: execlp", "[spawn]") { - using namespace nihil; - - auto exec = execlp("sh", "sh", "-c", "x=1; echo $x"); - REQUIRE(exec); - - auto output = std::string(); - auto capture = make_capture(stdout_fileno, output); - REQUIRE(capture); - - auto proc = spawn(*exec, *capture); - REQUIRE(proc); - - auto status = std::move(*proc).wait(); - REQUIRE(status); - - REQUIRE(status->okay()); - REQUIRE(output == "1\n"); -} - -TEST_CASE("spawn: execlp failure", "[spawn]") { - using namespace nihil; - - auto exec = execlp("nihil_no_such_executable", "x"); - REQUIRE(!exec); -} diff --git a/nihil/tests/tabulate.cc b/nihil/tests/tabulate.cc deleted file mode 100644 index 84f8b33..0000000 --- a/nihil/tests/tabulate.cc +++ /dev/null @@ -1,75 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -#include -#include -#include - -#include - -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"); -} diff --git a/nihil/usage_error.ccm b/nihil/usage_error.ccm deleted file mode 100644 index abbd6f0..0000000 --- a/nihil/usage_error.ccm +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include - -export module nihil:usage_error; - -import :error; - -namespace nihil { - -/* - * Exception thrown to indicate invalid command-line arguments. - */ -export struct usage_error : error { - usage_error(std::string_view what) : error(what) {} -}; - -} // namespace nihil - - diff --git a/nihil/write_file.ccm b/nihil/write_file.ccm deleted file mode 100644 index 6722d5f..0000000 --- a/nihil/write_file.ccm +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This source code is released into the public domain. - */ - -module; - -#include -#include -#include -#include -#include -#include - -#include -#include - -export module nihil:write_file; - -import :error; -import :guard; -import :monad; -import :open_file; -import :rename_file; - -namespace nihil { - -/* - * Write the contents of a range to a file. Returns the number of bytes - * written. - */ -export [[nodiscard]] -auto write_file(std::filesystem::path const &filename, - std::ranges::contiguous_range auto &&range, - int mode = 0777) - -> std::expected -{ - auto file = co_await open_file(filename, O_CREAT|O_WRONLY, mode); - auto nbytes = co_await write(file, range); - co_return nbytes; -} - -/* - * Utility wrapper for non-contiguous ranges. - */ -export [[nodiscard]] -auto write_file(std::filesystem::path const &filename, - std::ranges::range auto &&range) - -> std::expected -requires(!std::ranges::contiguous_range) -{ - return write_file(filename, std::vector(std::from_range, range)); -} - -/* - * Write the contents of a range to a file safely. The data will be written - * to ".tmp", and if the write succeeds, the temporary file will be - * renamed to the target filename. If an error occurs, the target file will - * not be modified. - */ -export [[nodiscard]] -auto safe_write_file(std::filesystem::path const &filename, - std::ranges::range auto &&range) - -> std::expected -{ - auto tmpfile = filename; - tmpfile.remove_filename(); - tmpfile /= (filename.filename().native() + ".tmp"); - - auto tmpfile_guard = guard([&tmpfile] { - ::unlink(tmpfile.c_str()); - }); - - co_await write_file(tmpfile, range); - co_await rename_file(tmpfile, filename); - - tmpfile_guard.release(); - co_return {}; -} - - -} // namespace nihil -- cgit v1.2.3