aboutsummaryrefslogtreecommitdiffstats
path: root/nihil.core/error.ccm
diff options
context:
space:
mode:
authorLexi Winter <lexi@le-fay.org>2025-07-02 04:00:06 +0100
committerLexi Winter <lexi@le-fay.org>2025-07-02 04:00:06 +0100
commit5adeb648f74c1771164c0686d6e0fc584cf36d9e (patch)
tree060cd918d3dd9e931a1541a43c9edff1a404ff47 /nihil.core/error.ccm
parent06fafff8e9e9c096cc39bde0306caa53ad3a2351 (diff)
downloadnihil-5adeb648f74c1771164c0686d6e0fc584cf36d9e.tar.gz
nihil-5adeb648f74c1771164c0686d6e0fc584cf36d9e.tar.bz2
move everything from util to core
Diffstat (limited to 'nihil.core/error.ccm')
-rw-r--r--nihil.core/error.ccm321
1 files changed, 321 insertions, 0 deletions
diff --git a/nihil.core/error.ccm b/nihil.core/error.ccm
new file mode 100644
index 0000000..5aa23f5
--- /dev/null
+++ b/nihil.core/error.ccm
@@ -0,0 +1,321 @@
+// 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<std::monostate, std::string, std::error_code, std::error_condition>;
+
+// 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::remove_cvref_t<decltype(what)>, std::string>)
+ : m_error(std::forward<decltype(what)>(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 <typename T>
+ requires(std::is_error_code_enum<T>::value)
+ error_proxy(T what) // NOLINT
+ : m_error(make_error_code(what))
+ {
+ }
+
+ // ... an error condition enum
+ template <typename T>
+ requires(std::is_error_condition_enum<T>::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<error>(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<error>(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<error> 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<std::error_code>
+ {
+ auto const *code = std::get_if<std::error_code>(&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<std::error_condition>
+ {
+ auto const *condition = std::get_if<std::error_condition>(&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 <typename T>
+ operator std::expected<T, error> () && // NOLINT
+ {
+ return std::unexpected{std::move(*this)};
+ }
+
+ template<typename T>
+ operator std::expected<T, error> () const // NOLINT
+ {
+ return std::unexpected{*this};
+ }
+
+ operator std::unexpected<error> () && // NOLINT
+ {
+ return std::unexpected{std::move(*this)};
+ }
+
+ operator std::unexpected<error> () 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<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;
+
+ // 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<decltype(rhs)>::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<decltype(rhs)>::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<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(e.full_str(), ctx.out()).out;
+ }
+};