diff options
| author | Lexi Winter <lexi@le-fay.org> | 2025-06-28 19:25:55 +0100 |
|---|---|---|
| committer | Lexi Winter <lexi@le-fay.org> | 2025-06-28 19:25:55 +0100 |
| commit | a2d7181700ac64b8e7a4472ec26dfa253b38f188 (patch) | |
| tree | 23c5a9c8ec4089ac346e2e0f9391909c3089b66b /nihil.error | |
| parent | f226d46ee02b57dd76a4793593aa8d66e1c58353 (diff) | |
| download | nihil-a2d7181700ac64b8e7a4472ec26dfa253b38f188.tar.gz nihil-a2d7181700ac64b8e7a4472ec26dfa253b38f188.tar.bz2 | |
split nihil into separate modules
Diffstat (limited to 'nihil.error')
| -rw-r--r-- | nihil.error/CMakeLists.txt | 29 | ||||
| -rw-r--r-- | nihil.error/error.cc | 160 | ||||
| -rw-r--r-- | nihil.error/error.ccm | 199 | ||||
| -rw-r--r-- | nihil.error/test.cc | 169 |
4 files changed, 557 insertions, 0 deletions
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 <iostream> +#include <memory> +#include <optional> +#include <string> +#include <system_error> +#include <variant> + +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<error>(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<error>(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<error>(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<error> +{ + 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<std::error_code> +{ + auto const *code = std::get_if<std::error_code>(&self.m_error); + if (code) + return {*code}; + return {}; +} + +auto error::condition(this error const &self) + -> std::optional<std::error_condition> +{ + auto const *condition = std::get_if<std::error_condition>(&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 <iosfwd> +#include <format> +#include <memory> +#include <optional> +#include <string> +#include <system_error> +#include <utility> +#include <variant> + +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<typename Cause> + requires(std::is_error_code_enum<Cause>::value || + std::is_error_condition_enum<Cause>::value) + error(std::string_view what, Cause &&cause) + : error(what, error(std::forward<Cause>(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<decltype(errc)>::value) + : error(make_error_code(errc), std::move(cause)) + {} + + explicit error(auto errc) + requires(std::is_error_code_enum<decltype(errc)>::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<decltype(errc)>::value) + : error(make_error_condition(errc), std::move(cause)) + {} + + explicit error(auto errc) + requires(std::is_error_condition_enum<decltype(errc)>::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<error>; + + // 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<std::error_code>; + + // Return this error's error_condition, if any. + [[nodiscard]] auto condition(this error const &) + -> std::optional<std::error_condition>; + + [[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<error> m_cause; + + // For std::exception::what(), we need to keep the string valid + // until we're destroyed. + mutable std::optional<std::string> 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<decltype(rhs)>::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<decltype(rhs)>::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<nihil::error, char> +{ + template<typename ParseContext> + constexpr auto parse(ParseContext &ctx) -> ParseContext::iterator + { + return ctx.begin(); + } + + template<typename FormatContext> + 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 <cerrno> +#include <cstring> +#include <system_error> + +#include <catch2/catch_test_macros.hpp> + +import nihil.error; + +TEST_CASE("error: invariants", "[nihil]") +{ + static_assert(std::destructible<nihil::error>); + static_assert(std::default_initializable<nihil::error>); + static_assert(std::move_constructible<nihil::error>); + static_assert(std::copy_constructible<nihil::error>); + static_assert(std::equality_comparable<nihil::error>); + static_assert(std::totally_ordered<nihil::error>); + static_assert(std::swappable<nihil::error>); + static_assert(std::regular<nihil::error>); +} + +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); + } +} |
