// This source code is released into the public domain. export module nihil.core: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; } };