aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.error
diff options
context:
space:
mode:
Diffstat (limited to 'nihil.error')
-rw-r--r--nihil.error/CMakeLists.txt29
-rw-r--r--nihil.error/error.cc160
-rw-r--r--nihil.error/error.ccm199
-rw-r--r--nihil.error/test.cc169
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..e4023f9
--- /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; //NOLINT(bugprone-return-const-ref-from-parameter)
+}
+
+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..12d47cc
--- /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.
+ ~error() override;
+
+ // 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);
+ }
+}