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 --- nihil.util/error.ccm | 321 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 nihil.util/error.ccm (limited to 'nihil.util/error.ccm') 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; + } +}; -- cgit v1.2.3