From 06fafff8e9e9c096cc39bde0306caa53ad3a2351 Mon Sep 17 00:00:00 2001 From: Lexi Winter Date: Wed, 2 Jul 2025 03:43:39 +0100 Subject: move error and match to util --- CMakeLists.txt | 2 - nihil.cli/command.ccm | 1 - nihil.cli/usage_error.ccm | 2 +- nihil.config/CMakeLists.txt | 1 - nihil.config/option.cc | 1 - nihil.config/option.ccm | 2 +- nihil.config/read.ccm | 1 - nihil.config/store.cc | 1 - nihil.config/store.ccm | 2 +- nihil.config/write.ccm | 1 - nihil.error/CMakeLists.txt | 27 ---- nihil.error/error.ccm | 322 --------------------------------------- nihil.error/error.test.cc | 273 --------------------------------- nihil.error/sys_error.ccm | 18 --- nihil.match/CMakeLists.txt | 22 --- nihil.match/match.ccm | 18 --- nihil.match/test.cc | 30 ---- nihil.posix/CMakeLists.txt | 2 - nihil.posix/ensure_dir.ccm | 6 +- nihil.posix/execl.ccm | 1 - nihil.posix/execlp.ccm | 2 +- nihil.posix/execshell.ccm | 1 - nihil.posix/execv.ccm | 2 - nihil.posix/execvp.ccm | 1 - nihil.posix/fd.ccm | 1 - nihil.posix/fd.test.cc | 2 +- nihil.posix/find_in_path.ccm | 2 +- nihil.posix/find_in_path.test.cc | 3 +- nihil.posix/getenv.ccm | 1 - nihil.posix/getenv.test.cc | 2 +- nihil.posix/open.ccm | 1 - nihil.posix/open.test.cc | 2 +- nihil.posix/open_in_path.ccm | 2 +- nihil.posix/open_in_path.test.cc | 2 +- nihil.posix/posix.ccm | 2 - nihil.posix/process.ccm | 2 +- nihil.posix/read_file.ccm | 2 +- nihil.posix/rename.ccm | 1 - nihil.posix/stat.test.cc | 2 +- nihil.posix/tempfile.ccm | 1 - nihil.posix/unistd.ccm | 2 +- nihil.posix/unlink.ccm | 1 - nihil.posix/write_file.ccm | 1 - nihil.ucl/CMakeLists.txt | 2 +- nihil.ucl/integer.ccm | 2 +- nihil.ucl/type.ccm | 2 +- nihil.util/CMakeLists.txt | 6 +- nihil.util/error.ccm | 321 ++++++++++++++++++++++++++++++++++++++ nihil.util/error.test.cc | 273 +++++++++++++++++++++++++++++++++ nihil.util/match.ccm | 18 +++ nihil.util/match.test.cc | 32 ++++ nihil.util/monad.test.cc | 1 - nihil.util/nihil.util.ccm | 3 + nihil.util/parse_size.ccm | 2 +- nihil.util/parse_size.test.cc | 43 +++--- nihil.util/sys_error.ccm | 18 +++ nihil.util/tabulate.ccm | 3 +- 57 files changed, 716 insertions(+), 781 deletions(-) delete mode 100644 nihil.error/CMakeLists.txt delete mode 100644 nihil.error/error.ccm delete mode 100644 nihil.error/error.test.cc delete mode 100644 nihil.error/sys_error.ccm delete mode 100644 nihil.match/CMakeLists.txt delete mode 100644 nihil.match/match.ccm delete mode 100644 nihil.match/test.cc create mode 100644 nihil.util/error.ccm create mode 100644 nihil.util/error.test.cc create mode 100644 nihil.util/match.ccm create mode 100644 nihil.util/match.test.cc create mode 100644 nihil.util/sys_error.ccm diff --git a/CMakeLists.txt b/CMakeLists.txt index da623af..84927d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,9 +64,7 @@ endif() add_subdirectory(nihil.std) add_subdirectory(nihil.cli) add_subdirectory(nihil.core) -add_subdirectory(nihil.error) add_subdirectory(nihil.generator) -add_subdirectory(nihil.match) add_subdirectory(nihil.posix) add_subdirectory(nihil.util) diff --git a/nihil.cli/command.ccm b/nihil.cli/command.ccm index ea52bbf..1bcb13f 100644 --- a/nihil.cli/command.ccm +++ b/nihil.cli/command.ccm @@ -12,7 +12,6 @@ export module nihil.cli:command; import nihil.std; import nihil.core; -import nihil.error; import :usage_error; namespace nihil { diff --git a/nihil.cli/usage_error.ccm b/nihil.cli/usage_error.ccm index 7de178e..8502755 100644 --- a/nihil.cli/usage_error.ccm +++ b/nihil.cli/usage_error.ccm @@ -2,7 +2,7 @@ export module nihil.cli:usage_error; import nihil.std; -import nihil.error; +import nihil.util; namespace nihil { diff --git a/nihil.config/CMakeLists.txt b/nihil.config/CMakeLists.txt index 6ed3651..0d8ffee 100644 --- a/nihil.config/CMakeLists.txt +++ b/nihil.config/CMakeLists.txt @@ -3,7 +3,6 @@ add_library(nihil.config STATIC) target_link_libraries(nihil.config PRIVATE nihil.std - nihil.error nihil.generator nihil.posix nihil.ucl diff --git a/nihil.config/option.cc b/nihil.config/option.cc index acd9d24..5f163ac 100644 --- a/nihil.config/option.cc +++ b/nihil.config/option.cc @@ -2,7 +2,6 @@ module nihil.config; import nihil.std; -import nihil.error; import nihil.ucl; import nihil.util; diff --git a/nihil.config/option.ccm b/nihil.config/option.ccm index 0758c1a..8c64653 100644 --- a/nihil.config/option.ccm +++ b/nihil.config/option.ccm @@ -2,7 +2,7 @@ export module nihil.config:option; import nihil.std; -import nihil.error; +import nihil.util; import nihil.ucl; namespace nihil::config { diff --git a/nihil.config/read.ccm b/nihil.config/read.ccm index c326fed..1c1b897 100644 --- a/nihil.config/read.ccm +++ b/nihil.config/read.ccm @@ -2,7 +2,6 @@ export module nihil.config:read; import nihil.std; -import nihil.error; import nihil.posix; import nihil.ucl; import nihil.util; diff --git a/nihil.config/store.cc b/nihil.config/store.cc index d0842ec..ed8de1d 100644 --- a/nihil.config/store.cc +++ b/nihil.config/store.cc @@ -2,7 +2,6 @@ module nihil.config; import nihil.std; -import nihil.error; import nihil.generator; import nihil.util; diff --git a/nihil.config/store.ccm b/nihil.config/store.ccm index 0a92ef0..e4229a7 100644 --- a/nihil.config/store.ccm +++ b/nihil.config/store.ccm @@ -4,8 +4,8 @@ export module nihil.config:store; // The configuration store. There should only be one of these. import nihil.std; -import nihil.error; import nihil.generator; +import nihil.util; namespace nihil::config { diff --git a/nihil.config/write.ccm b/nihil.config/write.ccm index 89b9c93..be2cfd7 100644 --- a/nihil.config/write.ccm +++ b/nihil.config/write.ccm @@ -2,7 +2,6 @@ export module nihil.config:write; import nihil.std; -import nihil.error; import nihil.posix; import nihil.ucl; import nihil.util; diff --git a/nihil.error/CMakeLists.txt b/nihil.error/CMakeLists.txt deleted file mode 100644 index 81c2cd8..0000000 --- a/nihil.error/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -# This source code is released into the public domain. - -add_library(nihil.error STATIC) -target_link_libraries(nihil.error PRIVATE nihil.std nihil.match) -target_sources(nihil.error - PUBLIC FILE_SET modules TYPE CXX_MODULES FILES - error.ccm - sys_error.ccm -) - -if(NIHIL_TESTS) - enable_testing() - - add_executable(nihil.error.test - error.test.cc) - - target_link_libraries(nihil.error.test PRIVATE - nihil.std - nihil.core - nihil.error - Catch2::Catch2WithMain - ) - - include(CTest) - include(Catch) - catch_discover_tests(nihil.error.test) -endif() diff --git a/nihil.error/error.ccm b/nihil.error/error.ccm deleted file mode 100644 index 833f7f1..0000000 --- a/nihil.error/error.ccm +++ /dev/null @@ -1,322 +0,0 @@ -// This source code is released into the public domain. -export module nihil.error; - -// 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. - -import nihil.std; -import nihil.match; - -export import :sys_error; - -namespace nihil { - -// Things which can be errors. -using error_t = std::variant; - -// A proxy class used when constructing errors. This has implicit constructors from various types, -// which means we don't have to handle every possible combination of types in error itself. -export struct error_proxy -{ - // Construct from... - - // ... a string_view - error_proxy(std::string_view const what) // NOLINT - : m_error(std::string(what)) - { - } - - // ... an std::string; so we can move the string into place if it's an rvalue. - error_proxy(auto &&what) // NOLINT - requires(std::same_as, std::string>) - : m_error(std::forward(what)) - { - } - - // ... a C string - error_proxy(char const *what) // NOLINT - : m_error(std::string(what)) - { - } - - // ... an std::error_code - error_proxy(std::error_code const what) // NOLINT - : m_error(what) - { - } - - // ... an std::error_condition - error_proxy(std::error_condition const what) // NOLINT - : m_error(what) - { - } - - // ... an error code enum - template - requires(std::is_error_code_enum::value) - error_proxy(T what) // NOLINT - : m_error(make_error_code(what)) - { - } - - // ... an error condition enum - template - requires(std::is_error_condition_enum::value) - error_proxy(T what) // NOLINT - : m_error(make_error_condition(what)) - { - } - - // Not copyable. - error_proxy(error_proxy const &) = delete; - auto operator=(error_proxy const &) -> error_proxy & = delete; - - // Not movable. - error_proxy(error_proxy &&) = delete; - auto operator=(error_proxy &&) -> error_proxy & = delete; - - ~error_proxy() = default; - - // Let error extract the error_t. - [[nodiscard]] auto error() && -> error_t && - { - return std::move(m_error); - } - -private: - // The error. - error_t m_error; -}; - -// The error class. -export struct error : std::exception -{ - // Create an empty error, representing success. - error() = default; - - // Destroy an error. - ~error() override = default; - - // Create an error from an error proxy. - explicit error(error_proxy &&proxy) - : m_error(std::move(std::move(proxy).error())) - { - } - - // Create an error from an error proxy and an error cause. - error(error_proxy &&proxy, error cause) - : m_error(std::move(std::move(proxy).error())) - , m_cause(std::make_shared(std::move(cause))) - { - } - - // Create an error from an error proxy and an error_proxy cause. - // For example, error("cannot open file", std::errc::permission_denied). - error(error_proxy &&proxy, error_proxy &&cause) - : m_error(std::move(std::move(proxy).error())) - , m_cause(std::make_shared(std::move(cause))) - { - } - - // Copyable. - error(error const &) = default; - auto operator=(error const &) -> error & = default; - - // Movable. - error(error &&) noexcept = default; - auto operator=(error &&) noexcept -> error & = default; - - // Return the cause of this error. - [[nodiscard]] auto cause(this error const &self) -> std::shared_ptr const & - { - return self.m_cause; - } - - // 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 &self) -> error const & - { - auto const *cause = &self; - while (cause->m_cause) - cause = cause->m_cause.get(); - return *cause; - } - - // Format this error without its cause as a string. - [[nodiscard]] auto this_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; - } - }; - } - - // Format this error and its cause as a string. - [[nodiscard]] auto full_str(this error const &self) -> std::string - { - auto str = self.this_str(); - - auto cause = self.cause(); - while (cause) { - str += ": " + cause->this_str(); - cause = cause->cause(); - } - - return str; - } - - // Return this error's error_code, if any. - [[nodiscard]] auto code(this error const &self) -> std::optional - { - auto const *code = std::get_if(&self.m_error); - if (code != nullptr) - return {*code}; - return {}; - } - - // Return this error's error_condition, if any. - [[nodiscard]] auto condition(this error const &self) -> std::optional - { - auto const *condition = std::get_if(&self.m_error); - if (condition != nullptr) - return {*condition}; - return {}; - } - - // Format this error and its cause as a C string and return it. This is for - // compatibility with std::exception. The lifetime of the returned string - // is the same as the error object. - [[nodiscard]] auto what() const noexcept -> char const * final - { - if (!m_what) - m_what = this->full_str(); - return m_what->c_str(); - } - - // Allow error to be implicitly converted to std::expected and std::unexpected, to make - // using it with std::expected easier. - - template - operator std::expected () && // NOLINT - { - return std::unexpected{std::move(*this)}; - } - - template - operator std::expected () const // NOLINT - { - return std::unexpected{*this}; - } - - operator std::unexpected () && // NOLINT - { - return std::unexpected{std::move(*this)}; - } - - operator std::unexpected () const // NOLINT - { - return std::unexpected{*this}; - } - -private: - // 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; - - // Equality comparison. - [[nodiscard]] friend auto operator==(error const &lhs, error const &rhs) -> bool - { - return lhs.m_error == rhs.m_error; - } - - [[nodiscard]] friend auto operator<=>(error const &lhs, error const &rhs) -> std::strong_ordering - { - return lhs.m_error <=> rhs.m_error; - } - - // Compare an error with an std::error_code. - [[nodiscard]] friend auto operator==(error const &lhs, std::error_code const &rhs) -> bool - { - return lhs.code() == rhs; - } - - // Compare an error to an std::error_condition. - [[nodiscard]] friend auto operator==(error const &lhs, std::error_condition const &rhs) -> bool - { - return lhs.condition() == rhs; - } - - // Compare an error to an std::error_code enum. - [[nodiscard]] friend 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. - [[nodiscard]] friend auto operator==(error const &lhs, auto rhs) -> bool - requires(std::is_error_condition_enum::value) - { - return lhs.condition() == rhs; - } - - // Print an error to a stream. - friend auto operator<<(std::ostream &strm, error const &e) -> std::ostream & - { - return strm << e.full_str(); - } -}; - -} // 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(e.full_str(), ctx.out()).out; - } -}; diff --git a/nihil.error/error.test.cc b/nihil.error/error.test.cc deleted file mode 100644 index db6c773..0000000 --- a/nihil.error/error.test.cc +++ /dev/null @@ -1,273 +0,0 @@ -// This source code is released into the public domain. - -#include - -import nihil.std; -import nihil.core; -import nihil.error; - -namespace { -inline constexpr auto *test_tags = "[nihil][nihil.error]"; - -TEST_CASE("error: invariants", test_tags) -{ - 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); -} - -SCENARIO("A nihil::error can be constructed from a C string", test_tags) -{ - GIVEN ("An error object constructed from a string") { - auto e = nihil::error("an error"); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "an error"); - } - } -} - -SCENARIO("A nihil::error can be constructed from an std::string lvalue", test_tags) -{ - GIVEN ("An error object constructed from an std::string lvalue") { - auto s = std::string("an error"); - auto e = nihil::error(s); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "an error"); - } - } -} - -SCENARIO("A nihil::error can be constructed from an std::string rvalue", test_tags) -{ - GIVEN ("An error object constructed from an std::string rvalue") { - auto e = nihil::error(std::string("an error")); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "an error"); - } - } -} - -SCENARIO("A nihil::error can be constructed from an std::string_view lvalue", test_tags) -{ - GIVEN ("An error object constructed from an std::string_view lvalue") { - auto s = std::string_view("an error"); - auto e = nihil::error(s); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "an error"); - } - } -} - -SCENARIO("A nihil::error can be constructed from an std::string_view rvalue", test_tags) -{ - GIVEN ("An error object constructed from an std::string_view rvalue") { - auto e = nihil::error(std::string_view("an error")); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "an error"); - } - } -} - -SCENARIO("A nihil::error can be constructed from an std::error_condition", test_tags) -{ - GIVEN ("An error object constructed from std::errc::invalid_argument") { - auto e = nihil::error(std::error_condition(std::errc::invalid_argument)); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "Invalid argument"); - } - - AND_THEN ("condition() should return the error code") { - REQUIRE(e.condition().has_value()); - REQUIRE(*e.condition() == std::errc::invalid_argument); - } - - AND_THEN ("The error should be comparable to the error code") { - REQUIRE(e == std::errc::invalid_argument); - } - } -} - -SCENARIO("A nihil::error can be constructed from an std::errc", test_tags) -{ - GIVEN ("An error object constructed from std::errc::invalid_argument") { - auto e = nihil::error(std::errc::invalid_argument); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "Invalid argument"); - } - - AND_THEN ("condition() should return the error code") { - REQUIRE(e.condition().has_value()); - REQUIRE(*e.condition() == std::errc::invalid_argument); - } - - AND_THEN ("The error should be comparable to the error code") { - REQUIRE(e == std::errc::invalid_argument); - } - } -} - -SCENARIO("A nihil::error can be constructed from a nihil::errc", test_tags) -{ - GIVEN ("An error object constructed from std::errc::invalid_argument") { - auto e = nihil::error(nihil::errc::incomplete_command); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "Incomplete command"); - } - - AND_THEN ("condition() should return the error code") { - REQUIRE(e.condition().has_value()); - REQUIRE(*e.condition() == nihil::errc::incomplete_command); - } - - AND_THEN ("The error should be comparable to the error code") { - REQUIRE(e == nihil::errc::incomplete_command); - } - } -} - -SCENARIO("A nihil::error can be constructed with a cause", test_tags) -{ - GIVEN ("An error object constructed with a cause") { - auto e = nihil::error("an error", std::errc::invalid_argument); - - THEN ("full_str() should return the string") { - REQUIRE(e.full_str() == "an error: Invalid argument"); - } - - AND_THEN ("cause() should return the cause") { - REQUIRE(e.cause()); - REQUIRE(*e.cause() == std::errc::invalid_argument); - } - } -} - -SCENARIO("std::format with a nihil::error", test_tags) -{ - GIVEN ("A nihil::error with no cause") { - auto e = nihil::error("an error"); - - THEN ("std::format should return the string") { - REQUIRE(std::format("{}", e) == "an error"); - } - - AND_THEN ("std::format should return the same as full_str()") { - REQUIRE(std::format("{}", e) == e.full_str()); - } - } - - GIVEN ("A nihil::error with a cause") { - auto e = nihil::error("an error", std::errc::invalid_argument); - - THEN ("std::format should return the string") { - REQUIRE(std::format("{}", e) == "an error: Invalid argument"); - } - - AND_THEN ("std::format should return the same as full_str()") { - REQUIRE(std::format("{}", e) == e.full_str()); - } - } -} - -SCENARIO("Print a nihil::error to an std::ostream", test_tags) -{ - GIVEN ("A nihil::error with no cause") { - auto e = nihil::error("an error"); - - THEN ("The error should be printed to the stream") { - auto ss = std::stringstream(); - ss << e; - REQUIRE(ss.str() == "an error"); - } - } - - GIVEN ("A nihil::error with a cause") { - auto e = nihil::error("an error", std::errc::invalid_argument); - - THEN ("The error should be printed to the stream") { - auto ss = std::stringstream(); - ss << e; - REQUIRE(ss.str() == "an error: Invalid argument"); - } - } -} - -SCENARIO("Comparison of nihil::error with operator==", test_tags) -{ - GIVEN ("Two nihil::error objects constructed from the same string") { - auto e1 = nihil::error("an error"); - auto e2 = nihil::error("an error"); - - THEN ("The two objects should be equal") { - REQUIRE(e1 == e2); - } - } - - GIVEN ("Two nihil::error objects constructed from different strings") { - auto e1 = nihil::error("an error"); - auto e2 = nihil::error("another error"); - - THEN ("The two objects should not be equal") { - REQUIRE(e1 != e2); - } - } - - GIVEN ("Two nihil::error objects constructed from the same error code") { - auto e1 = nihil::error(std::errc::invalid_argument); - auto e2 = nihil::error(std::errc::invalid_argument); - - THEN ("The two objects should be equal") { - REQUIRE(e1 == e2); - } - } - - GIVEN ("Two nihil::error objects constructed from different error codes") { - auto e1 = nihil::error(std::errc::invalid_argument); - auto e2 = nihil::error(std::errc::permission_denied); - - THEN ("The two objects should not be equal") { - REQUIRE(e1 != e2); - } - } -} - -SCENARIO("Comparison of nihil::error with operator<", test_tags) -{ - GIVEN ("Two nihil::error objects constructed from the same string") { - auto e1 = nihil::error("aaa"); - auto e2 = nihil::error("zzz"); - - THEN ("aaa should be less than zzz") { - REQUIRE(e1 < e2); - } - } -} - -SCENARIO("Throwing and catching a nihil::error object", test_tags) -{ - GIVEN ("A nihil::error object") { - THEN ("We should be able to throw and catch the error") { - REQUIRE_THROWS_AS(throw nihil::error("an error"), nihil::error); - - try { - throw nihil::error("an error"); - } catch (nihil::error const &e) { - REQUIRE(e.full_str() == "an error"); - }; - } - } -} - -} // anonymous namespace diff --git a/nihil.error/sys_error.ccm b/nihil.error/sys_error.ccm deleted file mode 100644 index 102f4c5..0000000 --- a/nihil.error/sys_error.ccm +++ /dev/null @@ -1,18 +0,0 @@ -// This source code is released into the public domain. -module; - -#include - -export module nihil.error:sys_error; - -import nihil.std; - -namespace nihil { - -// Allow access to errno without having to include . -export [[nodiscard]] auto sys_error() -> std::errc -{ - return static_cast(errno); -} - -} // namespace nihil diff --git a/nihil.match/CMakeLists.txt b/nihil.match/CMakeLists.txt deleted file mode 100644 index da59663..0000000 --- a/nihil.match/CMakeLists.txt +++ /dev/null @@ -1,22 +0,0 @@ -# This source code is released into the public domain. - -add_library(nihil.match STATIC) -target_link_libraries(nihil.match PRIVATE nihil.std) -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 - ) - - include(CTest) - include(Catch) - catch_discover_tests(nihil.match.test) -endif() diff --git a/nihil.match/match.ccm b/nihil.match/match.ccm deleted file mode 100644 index 03730bb..0000000 --- a/nihil.match/match.ccm +++ /dev/null @@ -1,18 +0,0 @@ -// This source code is released into the public domain. -export module nihil.match; - -import nihil.std; - -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 deleted file mode 100644 index 974a58a..0000000 --- a/nihil.match/test.cc +++ /dev/null @@ -1,30 +0,0 @@ -// This source code is released into the public domain. - -#include - -import nihil.std; -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.posix/CMakeLists.txt b/nihil.posix/CMakeLists.txt index a4c174b..3fcf625 100644 --- a/nihil.posix/CMakeLists.txt +++ b/nihil.posix/CMakeLists.txt @@ -4,7 +4,6 @@ add_library(nihil.posix STATIC) target_link_libraries(nihil.posix PRIVATE nihil.std nihil.core - nihil.error nihil.util ) @@ -59,7 +58,6 @@ if(NIHIL_TESTS) target_link_libraries(nihil.posix.test PRIVATE nihil.std - nihil.error nihil.posix nihil.util Catch2::Catch2WithMain diff --git a/nihil.posix/ensure_dir.ccm b/nihil.posix/ensure_dir.ccm index 8d3e7a8..9ae6d80 100644 --- a/nihil.posix/ensure_dir.ccm +++ b/nihil.posix/ensure_dir.ccm @@ -2,13 +2,11 @@ export module nihil.posix:ensure_dir; import nihil.std; -import nihil.error; +import nihil.util; namespace nihil { -/* - * Create the given directory and any parent directories. - */ +// Create the given directory and any parent directories. export [[nodiscard]] auto ensure_dir(std::filesystem::path const &dir) -> std::expected { diff --git a/nihil.posix/execl.ccm b/nihil.posix/execl.ccm index 99b9169..e6b809c 100644 --- a/nihil.posix/execl.ccm +++ b/nihil.posix/execl.ccm @@ -2,7 +2,6 @@ export module nihil.posix:execl; import nihil.std; -import nihil.error; import :argv; import :execv; import :fd; diff --git a/nihil.posix/execlp.ccm b/nihil.posix/execlp.ccm index 12f2c24..ba80504 100644 --- a/nihil.posix/execlp.ccm +++ b/nihil.posix/execlp.ccm @@ -2,7 +2,7 @@ export module nihil.posix:execlp; import nihil.std; -import nihil.error; +import nihil.util; import :argv; import :execvp; diff --git a/nihil.posix/execshell.ccm b/nihil.posix/execshell.ccm index e0263e5..9e640c3 100644 --- a/nihil.posix/execshell.ccm +++ b/nihil.posix/execshell.ccm @@ -2,7 +2,6 @@ export module nihil.posix:execshell; import nihil.std; -import nihil.error; import :execv; import :execl; diff --git a/nihil.posix/execv.ccm b/nihil.posix/execv.ccm index 3bf5745..2a77326 100644 --- a/nihil.posix/execv.ccm +++ b/nihil.posix/execv.ccm @@ -10,8 +10,6 @@ extern char **environ; // NOLINT export module nihil.posix:execv; import nihil.std; -import nihil.error; -import nihil.match; import nihil.util; import :argv; import :executor; diff --git a/nihil.posix/execvp.ccm b/nihil.posix/execvp.ccm index 5b11e7a..0a0106d 100644 --- a/nihil.posix/execvp.ccm +++ b/nihil.posix/execvp.ccm @@ -3,7 +3,6 @@ export module nihil.posix:execvp; import nihil.std; import nihil.core; -import nihil.error; import nihil.util; import :argv; import :execv; diff --git a/nihil.posix/fd.ccm b/nihil.posix/fd.ccm index c00676d..1de343b 100644 --- a/nihil.posix/fd.ccm +++ b/nihil.posix/fd.ccm @@ -7,7 +7,6 @@ module; export module nihil.posix:fd; import nihil.std; -import nihil.error; import nihil.util; namespace nihil { diff --git a/nihil.posix/fd.test.cc b/nihil.posix/fd.test.cc index 65b2ad3..a0828e5 100644 --- a/nihil.posix/fd.test.cc +++ b/nihil.posix/fd.test.cc @@ -6,8 +6,8 @@ #include import nihil.std; -import nihil.error; import nihil.posix; +import nihil.util; using namespace std::literals; diff --git a/nihil.posix/find_in_path.ccm b/nihil.posix/find_in_path.ccm index dabe358..353750b 100644 --- a/nihil.posix/find_in_path.ccm +++ b/nihil.posix/find_in_path.ccm @@ -6,7 +6,7 @@ module; export module nihil.posix:find_in_path; import nihil.std; -import nihil.error; +import nihil.util; import :fd; import :getenv; import :paths; diff --git a/nihil.posix/find_in_path.test.cc b/nihil.posix/find_in_path.test.cc index b2f6240..65cbe87 100644 --- a/nihil.posix/find_in_path.test.cc +++ b/nihil.posix/find_in_path.test.cc @@ -2,8 +2,9 @@ #include -import nihil.error; +import nihil.std; import nihil.posix; +import nihil.util; namespace { diff --git a/nihil.posix/getenv.ccm b/nihil.posix/getenv.ccm index ddffeb3..f3490a2 100644 --- a/nihil.posix/getenv.ccm +++ b/nihil.posix/getenv.ccm @@ -9,7 +9,6 @@ module; export module nihil.posix:getenv; import nihil.std; -import nihil.error; import nihil.util; namespace nihil { diff --git a/nihil.posix/getenv.test.cc b/nihil.posix/getenv.test.cc index 3ba1d94..83c46dc 100644 --- a/nihil.posix/getenv.test.cc +++ b/nihil.posix/getenv.test.cc @@ -5,8 +5,8 @@ #include import nihil.std; -import nihil.error; import nihil.posix; +import nihil.util; namespace { TEST_CASE("getenv: existing value", "[getenv]") diff --git a/nihil.posix/open.ccm b/nihil.posix/open.ccm index ab3d6e1..a2fc9f4 100644 --- a/nihil.posix/open.ccm +++ b/nihil.posix/open.ccm @@ -7,7 +7,6 @@ module; export module nihil.posix:open; import nihil.std; -import nihil.error; import nihil.util; import :fd; diff --git a/nihil.posix/open.test.cc b/nihil.posix/open.test.cc index e49f4c4..932c03b 100644 --- a/nihil.posix/open.test.cc +++ b/nihil.posix/open.test.cc @@ -3,8 +3,8 @@ #include import nihil.std; -import nihil.error; import nihil.posix; +import nihil.util; namespace { diff --git a/nihil.posix/open_in_path.ccm b/nihil.posix/open_in_path.ccm index 0733c8d..577ab9d 100644 --- a/nihil.posix/open_in_path.ccm +++ b/nihil.posix/open_in_path.ccm @@ -2,7 +2,7 @@ export module nihil.posix:open_in_path; import nihil.std; -import nihil.error; +import nihil.util; import :fd; import :getenv; import :open; diff --git a/nihil.posix/open_in_path.test.cc b/nihil.posix/open_in_path.test.cc index ebd1405..ace8f1e 100644 --- a/nihil.posix/open_in_path.test.cc +++ b/nihil.posix/open_in_path.test.cc @@ -3,8 +3,8 @@ #include import nihil.std; -import nihil.error; import nihil.posix; +import nihil.util; namespace { diff --git a/nihil.posix/posix.ccm b/nihil.posix/posix.ccm index c80724d..a6c675b 100644 --- a/nihil.posix/posix.ccm +++ b/nihil.posix/posix.ccm @@ -3,8 +3,6 @@ module; export module nihil.posix; -import nihil.error; - export import :argv; export import :ensure_dir; export import :execl; diff --git a/nihil.posix/process.ccm b/nihil.posix/process.ccm index 9fbf34c..e990f62 100644 --- a/nihil.posix/process.ccm +++ b/nihil.posix/process.ccm @@ -6,7 +6,7 @@ module; export module nihil.posix:process; import nihil.std; -import nihil.error; +import nihil.util; namespace nihil { diff --git a/nihil.posix/read_file.ccm b/nihil.posix/read_file.ccm index 337d0e4..f7d06cb 100644 --- a/nihil.posix/read_file.ccm +++ b/nihil.posix/read_file.ccm @@ -1,7 +1,7 @@ // This source code is released into the public domain. export module nihil.posix:read_file; -import nihil.error; +import nihil.std; import nihil.util; import :fd; import :open; diff --git a/nihil.posix/rename.ccm b/nihil.posix/rename.ccm index c46005e..6b640b1 100644 --- a/nihil.posix/rename.ccm +++ b/nihil.posix/rename.ccm @@ -2,7 +2,6 @@ export module nihil.posix:rename; import nihil.std; -import nihil.error; import nihil.util; namespace nihil { diff --git a/nihil.posix/stat.test.cc b/nihil.posix/stat.test.cc index 535273b..cb199f6 100644 --- a/nihil.posix/stat.test.cc +++ b/nihil.posix/stat.test.cc @@ -1,8 +1,8 @@ // This source code is released into the public domain. import nihil.std; -import nihil.error; import nihil.posix; +import nihil.util; #include diff --git a/nihil.posix/tempfile.ccm b/nihil.posix/tempfile.ccm index a4d3756..15edccb 100644 --- a/nihil.posix/tempfile.ccm +++ b/nihil.posix/tempfile.ccm @@ -2,7 +2,6 @@ export module nihil.posix:tempfile; import nihil.std; -import nihil.error; import nihil.util; import :fd; import :getenv; diff --git a/nihil.posix/unistd.ccm b/nihil.posix/unistd.ccm index 14c19ee..65cd015 100644 --- a/nihil.posix/unistd.ccm +++ b/nihil.posix/unistd.ccm @@ -6,7 +6,7 @@ module; export module nihil.posix:unistd; import nihil.std; -import nihil.error; +import nihil.util; // Symbols from unistd.h that might be useful. diff --git a/nihil.posix/unlink.ccm b/nihil.posix/unlink.ccm index d6c47cd..f2f5faa 100644 --- a/nihil.posix/unlink.ccm +++ b/nihil.posix/unlink.ccm @@ -8,7 +8,6 @@ export module nihil.posix:unlink; // unlink: simple wrapper around ::unlink() import nihil.std; -import nihil.error; import nihil.util; namespace nihil { diff --git a/nihil.posix/write_file.ccm b/nihil.posix/write_file.ccm index e706274..471ef85 100644 --- a/nihil.posix/write_file.ccm +++ b/nihil.posix/write_file.ccm @@ -2,7 +2,6 @@ export module nihil.posix:write_file; import nihil.std; -import nihil.error; import nihil.util; import :fd; import :open; diff --git a/nihil.ucl/CMakeLists.txt b/nihil.ucl/CMakeLists.txt index f0e7ec0..a4d69e6 100644 --- a/nihil.ucl/CMakeLists.txt +++ b/nihil.ucl/CMakeLists.txt @@ -7,7 +7,6 @@ target_link_libraries(nihil.ucl PRIVATE nihil.std nihil.core nihil.util - nihil.error ) target_sources(nihil.ucl @@ -47,6 +46,7 @@ if(NIHIL_TESTS) target_link_libraries(nihil.ucl.test PRIVATE nihil.std + nihil.util nihil.ucl Catch2::Catch2WithMain) diff --git a/nihil.ucl/integer.ccm b/nihil.ucl/integer.ccm index eb7fa6b..e3cf98d 100644 --- a/nihil.ucl/integer.ccm +++ b/nihil.ucl/integer.ccm @@ -7,7 +7,7 @@ export module nihil.ucl:integer; import nihil.std; import nihil.core; -import nihil.error; +import nihil.util; import :object; import :type; diff --git a/nihil.ucl/type.ccm b/nihil.ucl/type.ccm index 476546a..e7843d2 100644 --- a/nihil.ucl/type.ccm +++ b/nihil.ucl/type.ccm @@ -6,7 +6,7 @@ module; export module nihil.ucl:type; import nihil.std; -import nihil.error; +import nihil.util; namespace nihil::ucl { diff --git a/nihil.util/CMakeLists.txt b/nihil.util/CMakeLists.txt index 486a082..30a33b3 100644 --- a/nihil.util/CMakeLists.txt +++ b/nihil.util/CMakeLists.txt @@ -5,7 +5,6 @@ add_library(nihil.util STATIC) target_link_libraries(nihil.util PRIVATE nihil.std nihil.core - nihil.error ) target_sources(nihil.util @@ -15,13 +14,16 @@ target_sources(nihil.util capture_stream.ccm construct.ccm ctype.ccm + error.ccm flagset.ccm guard.ccm + match.ccm monad.ccm parse_size.ccm next_word.ccm save_errno.ccm skipws.ccm + sys_error.ccm tabulate.ccm uuid.ccm ) @@ -32,8 +34,10 @@ if(NIHIL_TESTS) add_executable(nihil.util.test capture_stream.test.cc ctype.test.cc + error.test.cc flagset.test.cc guard.test.cc + match.test.cc monad.test.cc parse_size.test.cc next_word.test.cc diff --git a/nihil.util/error.ccm b/nihil.util/error.ccm new file mode 100644 index 0000000..fd3aac9 --- /dev/null +++ b/nihil.util/error.ccm @@ -0,0 +1,321 @@ +// This source code is released into the public domain. +export module nihil.util:error; + +// 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. + +import nihil.std; + +import :match; + +namespace nihil { + +// Things which can be errors. +using error_t = std::variant; + +// A proxy class used when constructing errors. This has implicit constructors from various types, +// which means we don't have to handle every possible combination of types in error itself. +export struct error_proxy +{ + // Construct from... + + // ... a string_view + error_proxy(std::string_view const what) // NOLINT + : m_error(std::string(what)) + { + } + + // ... an std::string; so we can move the string into place if it's an rvalue. + error_proxy(auto &&what) // NOLINT + requires(std::same_as, std::string>) + : m_error(std::forward(what)) + { + } + + // ... a C string + error_proxy(char const *what) // NOLINT + : m_error(std::string(what)) + { + } + + // ... an std::error_code + error_proxy(std::error_code const what) // NOLINT + : m_error(what) + { + } + + // ... an std::error_condition + error_proxy(std::error_condition const what) // NOLINT + : m_error(what) + { + } + + // ... an error code enum + template + requires(std::is_error_code_enum::value) + error_proxy(T what) // NOLINT + : m_error(make_error_code(what)) + { + } + + // ... an error condition enum + template + requires(std::is_error_condition_enum::value) + error_proxy(T what) // NOLINT + : m_error(make_error_condition(what)) + { + } + + // Not copyable. + error_proxy(error_proxy const &) = delete; + auto operator=(error_proxy const &) -> error_proxy & = delete; + + // Not movable. + error_proxy(error_proxy &&) = delete; + auto operator=(error_proxy &&) -> error_proxy & = delete; + + ~error_proxy() = default; + + // Let error extract the error_t. + [[nodiscard]] auto error() && -> error_t && + { + return std::move(m_error); + } + +private: + // The error. + error_t m_error; +}; + +// The error class. +export struct error : std::exception +{ + // Create an empty error, representing success. + error() = default; + + // Destroy an error. + ~error() override = default; + + // Create an error from an error proxy. + explicit error(error_proxy &&proxy) + : m_error(std::move(std::move(proxy).error())) + { + } + + // Create an error from an error proxy and an error cause. + error(error_proxy &&proxy, error cause) + : m_error(std::move(std::move(proxy).error())) + , m_cause(std::make_shared(std::move(cause))) + { + } + + // Create an error from an error proxy and an error_proxy cause. + // For example, error("cannot open file", std::errc::permission_denied). + error(error_proxy &&proxy, error_proxy &&cause) + : m_error(std::move(std::move(proxy).error())) + , m_cause(std::make_shared(std::move(cause))) + { + } + + // Copyable. + error(error const &) = default; + auto operator=(error const &) -> error & = default; + + // Movable. + error(error &&) noexcept = default; + auto operator=(error &&) noexcept -> error & = default; + + // Return the cause of this error. + [[nodiscard]] auto cause(this error const &self) -> std::shared_ptr const & + { + return self.m_cause; + } + + // 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 &self) -> error const & + { + auto const *cause = &self; + while (cause->m_cause) + cause = cause->m_cause.get(); + return *cause; + } + + // Format this error without its cause as a string. + [[nodiscard]] auto this_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; + } + }; + } + + // Format this error and its cause as a string. + [[nodiscard]] auto full_str(this error const &self) -> std::string + { + auto str = self.this_str(); + + auto cause = self.cause(); + while (cause) { + str += ": " + cause->this_str(); + cause = cause->cause(); + } + + return str; + } + + // Return this error's error_code, if any. + [[nodiscard]] auto code(this error const &self) -> std::optional + { + auto const *code = std::get_if(&self.m_error); + if (code != nullptr) + return {*code}; + return {}; + } + + // Return this error's error_condition, if any. + [[nodiscard]] auto condition(this error const &self) -> std::optional + { + auto const *condition = std::get_if(&self.m_error); + if (condition != nullptr) + return {*condition}; + return {}; + } + + // Format this error and its cause as a C string and return it. This is for + // compatibility with std::exception. The lifetime of the returned string + // is the same as the error object. + [[nodiscard]] auto what() const noexcept -> char const * final + { + if (!m_what) + m_what = this->full_str(); + return m_what->c_str(); + } + + // Allow error to be implicitly converted to std::expected and std::unexpected, to make + // using it with std::expected easier. + + template + operator std::expected () && // NOLINT + { + return std::unexpected{std::move(*this)}; + } + + template + operator std::expected () const // NOLINT + { + return std::unexpected{*this}; + } + + operator std::unexpected () && // NOLINT + { + return std::unexpected{std::move(*this)}; + } + + operator std::unexpected () const // NOLINT + { + return std::unexpected{*this}; + } + +private: + // 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; + + // Equality comparison. + [[nodiscard]] friend auto operator==(error const &lhs, error const &rhs) -> bool + { + return lhs.m_error == rhs.m_error; + } + + [[nodiscard]] friend auto operator<=>(error const &lhs, error const &rhs) -> std::strong_ordering + { + return lhs.m_error <=> rhs.m_error; + } + + // Compare an error with an std::error_code. + [[nodiscard]] friend auto operator==(error const &lhs, std::error_code const &rhs) -> bool + { + return lhs.code() == rhs; + } + + // Compare an error to an std::error_condition. + [[nodiscard]] friend auto operator==(error const &lhs, std::error_condition const &rhs) -> bool + { + return lhs.condition() == rhs; + } + + // Compare an error to an std::error_code enum. + [[nodiscard]] friend 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. + [[nodiscard]] friend auto operator==(error const &lhs, auto rhs) -> bool + requires(std::is_error_condition_enum::value) + { + return lhs.condition() == rhs; + } + + // Print an error to a stream. + friend auto operator<<(std::ostream &strm, error const &e) -> std::ostream & + { + return strm << e.full_str(); + } +}; + +} // 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(e.full_str(), ctx.out()).out; + } +}; diff --git a/nihil.util/error.test.cc b/nihil.util/error.test.cc new file mode 100644 index 0000000..f4ec1ee --- /dev/null +++ b/nihil.util/error.test.cc @@ -0,0 +1,273 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.core; +import nihil.util; + +namespace { +inline constexpr auto *test_tags = "[nihil][nihil.error]"; + +TEST_CASE("error: invariants", test_tags) +{ + 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); +} + +SCENARIO("A nihil::error can be constructed from a C string", test_tags) +{ + GIVEN ("An error object constructed from a string") { + auto e = nihil::error("an error"); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string lvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string lvalue") { + auto s = std::string("an error"); + auto e = nihil::error(s); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string rvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string rvalue") { + auto e = nihil::error(std::string("an error")); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string_view lvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string_view lvalue") { + auto s = std::string_view("an error"); + auto e = nihil::error(s); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::string_view rvalue", test_tags) +{ + GIVEN ("An error object constructed from an std::string_view rvalue") { + auto e = nihil::error(std::string_view("an error")); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error"); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::error_condition", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(std::error_condition(std::errc::invalid_argument)); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Invalid argument"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == std::errc::invalid_argument); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == std::errc::invalid_argument); + } + } +} + +SCENARIO("A nihil::error can be constructed from an std::errc", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(std::errc::invalid_argument); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Invalid argument"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == std::errc::invalid_argument); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == std::errc::invalid_argument); + } + } +} + +SCENARIO("A nihil::error can be constructed from a nihil::errc", test_tags) +{ + GIVEN ("An error object constructed from std::errc::invalid_argument") { + auto e = nihil::error(nihil::errc::incomplete_command); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "Incomplete command"); + } + + AND_THEN ("condition() should return the error code") { + REQUIRE(e.condition().has_value()); + REQUIRE(*e.condition() == nihil::errc::incomplete_command); + } + + AND_THEN ("The error should be comparable to the error code") { + REQUIRE(e == nihil::errc::incomplete_command); + } + } +} + +SCENARIO("A nihil::error can be constructed with a cause", test_tags) +{ + GIVEN ("An error object constructed with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("full_str() should return the string") { + REQUIRE(e.full_str() == "an error: Invalid argument"); + } + + AND_THEN ("cause() should return the cause") { + REQUIRE(e.cause()); + REQUIRE(*e.cause() == std::errc::invalid_argument); + } + } +} + +SCENARIO("std::format with a nihil::error", test_tags) +{ + GIVEN ("A nihil::error with no cause") { + auto e = nihil::error("an error"); + + THEN ("std::format should return the string") { + REQUIRE(std::format("{}", e) == "an error"); + } + + AND_THEN ("std::format should return the same as full_str()") { + REQUIRE(std::format("{}", e) == e.full_str()); + } + } + + GIVEN ("A nihil::error with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("std::format should return the string") { + REQUIRE(std::format("{}", e) == "an error: Invalid argument"); + } + + AND_THEN ("std::format should return the same as full_str()") { + REQUIRE(std::format("{}", e) == e.full_str()); + } + } +} + +SCENARIO("Print a nihil::error to an std::ostream", test_tags) +{ + GIVEN ("A nihil::error with no cause") { + auto e = nihil::error("an error"); + + THEN ("The error should be printed to the stream") { + auto ss = std::stringstream(); + ss << e; + REQUIRE(ss.str() == "an error"); + } + } + + GIVEN ("A nihil::error with a cause") { + auto e = nihil::error("an error", std::errc::invalid_argument); + + THEN ("The error should be printed to the stream") { + auto ss = std::stringstream(); + ss << e; + REQUIRE(ss.str() == "an error: Invalid argument"); + } + } +} + +SCENARIO("Comparison of nihil::error with operator==", test_tags) +{ + GIVEN ("Two nihil::error objects constructed from the same string") { + auto e1 = nihil::error("an error"); + auto e2 = nihil::error("an error"); + + THEN ("The two objects should be equal") { + REQUIRE(e1 == e2); + } + } + + GIVEN ("Two nihil::error objects constructed from different strings") { + auto e1 = nihil::error("an error"); + auto e2 = nihil::error("another error"); + + THEN ("The two objects should not be equal") { + REQUIRE(e1 != e2); + } + } + + GIVEN ("Two nihil::error objects constructed from the same error code") { + auto e1 = nihil::error(std::errc::invalid_argument); + auto e2 = nihil::error(std::errc::invalid_argument); + + THEN ("The two objects should be equal") { + REQUIRE(e1 == e2); + } + } + + GIVEN ("Two nihil::error objects constructed from different error codes") { + auto e1 = nihil::error(std::errc::invalid_argument); + auto e2 = nihil::error(std::errc::permission_denied); + + THEN ("The two objects should not be equal") { + REQUIRE(e1 != e2); + } + } +} + +SCENARIO("Comparison of nihil::error with operator<", test_tags) +{ + GIVEN ("Two nihil::error objects constructed from the same string") { + auto e1 = nihil::error("aaa"); + auto e2 = nihil::error("zzz"); + + THEN ("aaa should be less than zzz") { + REQUIRE(e1 < e2); + } + } +} + +SCENARIO("Throwing and catching a nihil::error object", test_tags) +{ + GIVEN ("A nihil::error object") { + THEN ("We should be able to throw and catch the error") { + REQUIRE_THROWS_AS(throw nihil::error("an error"), nihil::error); + + try { + throw nihil::error("an error"); + } catch (nihil::error const &e) { + REQUIRE(e.full_str() == "an error"); + }; + } + } +} + +} // anonymous namespace diff --git a/nihil.util/match.ccm b/nihil.util/match.ccm new file mode 100644 index 0000000..b72416a --- /dev/null +++ b/nihil.util/match.ccm @@ -0,0 +1,18 @@ +// This source code is released into the public domain. +export module nihil.util:match; + +import nihil.std; + +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.util/match.test.cc b/nihil.util/match.test.cc new file mode 100644 index 0000000..6cc0e27 --- /dev/null +++ b/nihil.util/match.test.cc @@ -0,0 +1,32 @@ +// This source code is released into the public domain. + +#include + +import nihil.std; +import nihil.util; + +namespace { +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"); +} +} // anonymous namespace diff --git a/nihil.util/monad.test.cc b/nihil.util/monad.test.cc index bc9e406..7f39fca 100644 --- a/nihil.util/monad.test.cc +++ b/nihil.util/monad.test.cc @@ -3,7 +3,6 @@ #include import nihil.std; -import nihil.error; import nihil.util; namespace { diff --git a/nihil.util/nihil.util.ccm b/nihil.util/nihil.util.ccm index 9e4795b..49e5687 100644 --- a/nihil.util/nihil.util.ccm +++ b/nihil.util/nihil.util.ccm @@ -4,12 +4,15 @@ export module nihil.util; export import :capture_stream; export import :construct; export import :ctype; +export import :error; export import :flagset; export import :guard; +export import :match; export import :monad; export import :parse_size; export import :next_word; export import :save_errno; export import :skipws; +export import :sys_error; export import :tabulate; export import :uuid; diff --git a/nihil.util/parse_size.ccm b/nihil.util/parse_size.ccm index 6219323..51bfb4f 100644 --- a/nihil.util/parse_size.ccm +++ b/nihil.util/parse_size.ccm @@ -3,9 +3,9 @@ export module nihil.util:parse_size; import nihil.std; import nihil.core; -import nihil.error; import :ctype; +import :error; import :monad; namespace nihil { diff --git a/nihil.util/parse_size.test.cc b/nihil.util/parse_size.test.cc index d79912a..ee97996 100644 --- a/nihil.util/parse_size.test.cc +++ b/nihil.util/parse_size.test.cc @@ -4,9 +4,9 @@ import nihil.std; import nihil.core; -import nihil.error; import nihil.util; +namespace { TEST_CASE("parse_size: empty value", "[nihil]") { using namespace nihil; @@ -20,40 +20,40 @@ TEST_CASE("parse_size: basic", "[nihil]") { using namespace nihil; - SECTION("bare number") { + SECTION ("bare number") { auto n = parse_size("1024").value(); REQUIRE(n == 1024); } - SECTION("max value, unsigned") { + SECTION ("max value, unsigned") { auto n = parse_size("65535").value(); REQUIRE(n == 65535); } - SECTION("max value, signed") { + SECTION ("max value, signed") { auto n = parse_size("32767").value(); REQUIRE(n == 32767); } - SECTION("overflow by 1, unsigned") { + 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") { + 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") { + 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") { + SECTION ("overflow by many, signed") { auto n = parse_size("100000"); REQUIRE(!n); REQUIRE(n.error() == std::errc::result_out_of_range); @@ -79,27 +79,27 @@ TEST_CASE("parse_size: multipliers", "[nihil]") auto sf = static_cast(4); - SECTION("k") { + SECTION ("k") { auto n = parse_size("4k").value(); REQUIRE(n == sf * 1024); } - SECTION("m") { + SECTION ("m") { auto n = parse_size("4m").value(); REQUIRE(n == sf * 1024 * 1024); } - SECTION("g") { + SECTION ("g") { auto n = parse_size("4g").value(); REQUIRE(n == sf * 1024 * 1024 * 1024); } - SECTION("t") { + SECTION ("t") { auto n = parse_size("4t").value(); REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024); } - SECTION("p") { + SECTION ("p") { auto n = parse_size("4p").value(); REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024); } @@ -109,13 +109,13 @@ TEST_CASE("parse_size: multiplier overflow", "[nihil]") { using namespace nihil; - SECTION("signed") { + SECTION ("signed") { auto n = parse_size("64k"); REQUIRE(!n); REQUIRE(n.error() == std::errc::result_out_of_range); } - SECTION("unsigned") { + SECTION ("unsigned") { auto n = parse_size("32k"); REQUIRE(!n); REQUIRE(n.error() == std::errc::result_out_of_range); @@ -126,7 +126,7 @@ TEST_CASE("parse_size: wide", "[nihil]") { using namespace nihil; - SECTION("bare number") { + SECTION ("bare number") { auto n = parse_size(L"1024").value(); REQUIRE(n == 1024); } @@ -138,28 +138,29 @@ TEST_CASE("parse_size: wide multipliers", "[nihil]") auto sf = static_cast(4); - SECTION("k") { + SECTION ("k") { auto n = parse_size(L"4k").value(); REQUIRE(n == sf * 1024); } - SECTION("m") { + SECTION ("m") { auto n = parse_size(L"4m").value(); REQUIRE(n == sf * 1024 * 1024); } - SECTION("g") { + SECTION ("g") { auto n = parse_size(L"4g").value(); REQUIRE(n == sf * 1024 * 1024 * 1024); } - SECTION("t") { + SECTION ("t") { auto n = parse_size(L"4t").value(); REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024); } - SECTION("p") { + SECTION ("p") { auto n = parse_size(L"4p").value(); REQUIRE(n == sf * 1024 * 1024 * 1024 * 1024 * 1024); } } +} // anonymous namespace diff --git a/nihil.util/sys_error.ccm b/nihil.util/sys_error.ccm new file mode 100644 index 0000000..a39552e --- /dev/null +++ b/nihil.util/sys_error.ccm @@ -0,0 +1,18 @@ +// This source code is released into the public domain. +module; + +#include + +export module nihil.util:sys_error; + +import nihil.std; + +namespace nihil { + +// Allow access to errno without having to include . +export [[nodiscard]] auto sys_error() -> std::errc +{ + return static_cast(errno); // NOLINT +} + +} // namespace nihil diff --git a/nihil.util/tabulate.ccm b/nihil.util/tabulate.ccm index 8f5c22e..762e2a3 100644 --- a/nihil.util/tabulate.ccm +++ b/nihil.util/tabulate.ccm @@ -2,8 +2,9 @@ export module nihil.util:tabulate; import nihil.std; -import nihil.error; + import :ctype; +import :error; namespace nihil { -- cgit v1.2.3