[llvm] [orc-rt] Add Error / Exception interop. (PR #172247)

Lang Hames via llvm-commits llvm-commits at lists.llvm.org
Sun Dec 14 20:55:23 PST 2025


https://github.com/lhames updated https://github.com/llvm/llvm-project/pull/172247

>From bad7dee261c5f219ea2bd9592f255d210409d65c Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Sat, 13 Dec 2025 23:24:54 +1100
Subject: [PATCH 1/3] [orc-rt] Add Error / Exception interop.

The ORC runtime needs to work in diverse codebases, both with and without C++
exceptions enabled (e.g. most LLVM projects compile with exceptions turned off,
but regular C++ codebases will typically have them turned on). This introduces a
tension in the ORC runtime: If a C++ exception is thrown (e.g. by a
client-supplied callback) it can't be ignored, but orc_rt::Error values will
assert if not handled prior to destruction.  That makes the following pattern
fundamentally unsafe in the ORC runtime:

```
if (auto Err = orc_rt_operation(...)) {
  log("failure, bailing out"); // <- may throw if exceptions enabled
  // Exception unwinds stack before Error is handled, triggers Error-not-checked
  // assertion here.
  return Err;
}
```

We can resolve this tension by preventing any exceptions from unwinding through
ORC runtime stack frames. We can do this while preserving exception *values* by
catching all exceptions (using `catch (...)`) and capturing their values as a
std::exception_ptr into an Error.

This patch adds APIs to simplify conversion between C++ exceptions and Errors.
These APIs are available only when enabled when the ORC runtime is configured
with ORC_RT_ENABLE_EXCEPTIONS=On (the default).

- `ExceptionError` wraps a std::exception_ptr.

- `runCapturingExceptions` takes a T() callback and converts any exceptions
thrown by the body into Errors. If T is Expected or Error already then
runCapturingExceptions returns the same type. If T is void then
runCapturingExceptions returns an Error (returning Error::success() if no
exception is thrown). If T is any other type then runCapturingExceptions returns
an Expected<T>.

- A new Error::throwOnFailure method is added that converts failing values into
thrown exceptions according to the following rules:

  1. If the Error is of type ExceptionError then std::rethrow_exception is
     called on the contained std::exception_ptr to rethrow the original
     exception value.
  2. If the Error is of any other type then std::unique_ptr<T> is thrown where
     T is the dynamic type of the Error.

These rules allow exceptions to be propagated through the ORC runtime as Errors,
and for ORC runtime errors to be converted to exceptions by clients.
---
 orc-rt/include/orc-rt/Error.h                 | 205 ++++++++++++++++--
 orc-rt/lib/executor/CMakeLists.txt            |   1 +
 orc-rt/lib/executor/Error.cpp                 |  53 +++++
 orc-rt/unittests/CMakeLists.txt               |   1 +
 .../unittests/ErrorExceptionInteropTest.cpp   | 171 +++++++++++++++
 orc-rt/unittests/ErrorTest.cpp                |  10 +-
 6 files changed, 417 insertions(+), 24 deletions(-)
 create mode 100644 orc-rt/lib/executor/Error.cpp
 create mode 100644 orc-rt/unittests/ErrorExceptionInteropTest.cpp

diff --git a/orc-rt/include/orc-rt/Error.h b/orc-rt/include/orc-rt/Error.h
index a8f411e930223..656e3f31f35d0 100644
--- a/orc-rt/include/orc-rt/Error.h
+++ b/orc-rt/include/orc-rt/Error.h
@@ -9,30 +9,79 @@
 #ifndef ORC_RT_ERROR_H
 #define ORC_RT_ERROR_H
 
+#include "orc-rt-c/config.h"
 #include "orc-rt/CallableTraitsHelper.h"
 #include "orc-rt/Compiler.h"
 #include "orc-rt/RTTI.h"
 
 #include <cassert>
 #include <cstdlib>
+#include <exception>
 #include <memory>
 #include <string>
 #include <type_traits>
 
 namespace orc_rt {
 
+class Error;
+
 /// Base class for all errors.
 class ErrorInfoBase : public RTTIExtends<ErrorInfoBase, RTTIRoot> {
 public:
-  virtual std::string toString() const = 0;
+  virtual std::string toString() const noexcept = 0;
+
+private:
+#if ORC_RT_ENABLE_EXCEPTIONS
+  friend class Error;
+  friend Error restore_error(ErrorInfoBase &&);
+
+  virtual void throwAsException() = 0;
+
+  virtual Error restoreError() noexcept = 0;
+#endif // ORC_RT_ENABLE_EXCEPTIONS
+};
+
+/// Like RTTI-extends, but injects error-related helper methods.
+template <typename ThisT, typename ParentT>
+class ErrorExtends : public ParentT {
+public:
+  static_assert(std::is_base_of_v<ErrorInfoBase, ParentT>,
+                "ErrorExtends must extend ErrorInfoBase derivatives");
+
+  // Inherit constructors and isA methods from ParentT.
+  using ParentT::isA;
+  using ParentT::ParentT;
+
+  static char ID;
+
+  static const void *classID() noexcept { return &ThisT::ID; }
+
+  const void *dynamicClassID() const noexcept override { return &ThisT::ID; }
+
+  bool isA(const void *const ClassID) const noexcept override {
+    return ClassID == classID() || ParentT::isA(ClassID);
+  }
+
+  static bool classof(const RTTIRoot *R) { return R->isA<ThisT>(); }
+
+#if ORC_RT_ENABLE_EXCEPTIONS
+  void throwAsException() override {
+    throw ThisT(std::move(static_cast<ThisT &>(*this)));
+  }
+
+  Error restoreError() noexcept override;
+#endif // ORC_RT_ENABLE_EXCEPTIONS
 };
 
+template <typename ThisT, typename ParentT>
+char ErrorExtends<ThisT, ParentT>::ID = 0;
+
 /// Represents an environmental error.
 class ORC_RT_NODISCARD Error {
 
   template <typename T> friend class Expected;
 
-  friend Error make_error(std::unique_ptr<ErrorInfoBase> Payload);
+  friend Error make_error(std::unique_ptr<ErrorInfoBase> Payload) noexcept;
 
   template <typename... HandlerTs>
   friend Error handleErrors(Error E, HandlerTs &&...Hs);
@@ -48,7 +97,7 @@ class ORC_RT_NODISCARD Error {
   /// Move-construct an error. The newly constructed error is considered
   /// unchecked, even if the source error had been checked. The original error
   /// becomes a checked success value.
-  Error(Error &&Other) {
+  Error(Error &&Other) noexcept {
     setChecked(true);
     *this = std::move(Other);
   }
@@ -57,7 +106,7 @@ class ORC_RT_NODISCARD Error {
   /// you cannot overwrite an unhandled error. The current error is then
   /// considered unchecked. The source error becomes a checked success value,
   /// regardless of its original state.
-  Error &operator=(Error &&Other) {
+  Error &operator=(Error &&Other) noexcept {
     // Don't allow overwriting of unchecked values.
     assertIsChecked();
     setPtr(Other.getPtr());
@@ -73,48 +122,58 @@ class ORC_RT_NODISCARD Error {
   }
 
   /// Create a success value.
-  static Error success() { return Error(); }
+  static Error success() noexcept { return Error(); }
 
   /// Error values convert to true for failure values, false otherwise.
-  explicit operator bool() {
+  explicit operator bool() noexcept {
     setChecked(getPtr() == nullptr);
     return getPtr() != nullptr;
   }
 
   /// Return true if this Error contains a failure value of the given type.
-  template <typename ErrT> bool isA() const {
+  template <typename ErrT> bool isA() const noexcept {
     return getPtr() && getPtr()->isA<ErrT>();
   }
 
+#if ORC_RT_ENABLE_EXCEPTIONS
+  void throwOnFailure() {
+    if (auto P = takePayload())
+      P->throwAsException();
+  }
+#endif // ORC_RT_ENABLE_EXCEPTIONS
+
 private:
-  Error() = default;
+  Error() noexcept = default;
 
-  Error(std::unique_ptr<ErrorInfoBase> ErrInfo) {
+  Error(std::unique_ptr<ErrorInfoBase> ErrInfo) noexcept {
     auto RawErrPtr = reinterpret_cast<uintptr_t>(ErrInfo.release());
     assert((RawErrPtr & 0x1) == 0 && "ErrorInfo is insufficiently aligned");
     ErrPtr = RawErrPtr | 0x1;
   }
 
-  void assertIsChecked() {
+  void assertIsChecked() noexcept {
     if (ORC_RT_UNLIKELY(!isChecked() || getPtr())) {
       fprintf(stderr, "Error must be checked prior to destruction.\n");
       abort(); // Some sort of JIT program abort?
     }
   }
 
-  template <typename ErrT = ErrorInfoBase> ErrT *getPtr() const {
+  template <typename ErrT = ErrorInfoBase> ErrT *getPtr() const noexcept {
     return reinterpret_cast<ErrT *>(ErrPtr & ~uintptr_t(1));
   }
 
-  void setPtr(ErrorInfoBase *Ptr) {
+  void setPtr(ErrorInfoBase *Ptr) noexcept {
     ErrPtr = (reinterpret_cast<uintptr_t>(Ptr) & ~uintptr_t(1)) | (ErrPtr & 1);
   }
 
-  bool isChecked() const { return ErrPtr & 0x1; }
+  bool isChecked() const noexcept { return ErrPtr & 0x1; }
 
-  void setChecked(bool Checked) { ErrPtr = (ErrPtr & ~uintptr_t(1)) | Checked; }
+  void setChecked(bool Checked) noexcept {
+    ErrPtr = (ErrPtr & ~uintptr_t(1)) | Checked;
+  }
 
-  template <typename ErrT = ErrorInfoBase> std::unique_ptr<ErrT> takePayload() {
+  template <typename ErrT = ErrorInfoBase>
+  std::unique_ptr<ErrT> takePayload() noexcept {
     static_assert(std::is_base_of_v<ErrorInfoBase, ErrT>,
                   "ErrT is not an ErrorInfoBase subclass");
     std::unique_ptr<ErrT> Tmp(getPtr<ErrT>());
@@ -127,10 +186,22 @@ class ORC_RT_NODISCARD Error {
 };
 
 /// Create an Error from an ErrorInfoBase.
-inline Error make_error(std::unique_ptr<ErrorInfoBase> Payload) {
+inline Error make_error(std::unique_ptr<ErrorInfoBase> Payload) noexcept {
   return Error(std::move(Payload));
 }
 
+#if ORC_RT_ENABLE_EXCEPTIONS
+
+template <typename ThisT, typename ParentT>
+Error ErrorExtends<ThisT, ParentT>::restoreError() noexcept {
+  return make_error(
+      std::unique_ptr<ThisT>(new ThisT(*static_cast<ThisT *>(this))));
+}
+
+inline Error restore_error(ErrorInfoBase &&EIB) { return EIB.restoreError(); }
+
+#endif // ORC_RT_ENABLE_EXCEPTIONS
+
 /// Construct an error of ErrT with the given arguments.
 template <typename ErrT, typename... ArgTs> Error make_error(ArgTs &&...Args) {
   static_assert(std::is_base_of<ErrorInfoBase, ErrT>::value,
@@ -497,7 +568,7 @@ template <typename T> T &cantFail(Expected<T &> E) {
 
 /// Convert the given error to a string. The error value is consumed in the
 /// process.
-inline std::string toString(Error Err) {
+inline std::string toString(Error Err) noexcept {
   assert(Err && "Cannot convert success value to string");
   std::string ErrMsg;
   handleAllErrors(std::move(Err),
@@ -506,15 +577,111 @@ inline std::string toString(Error Err) {
 }
 
 /// Simple string error type.
-class StringError : public RTTIExtends<StringError, ErrorInfoBase> {
+class StringError : public ErrorExtends<StringError, ErrorInfoBase> {
 public:
   StringError(std::string ErrMsg) : ErrMsg(std::move(ErrMsg)) {}
-  std::string toString() const override { return ErrMsg; }
+  std::string toString() const noexcept override { return ErrMsg; }
 
 private:
   std::string ErrMsg;
 };
 
+/// APIs for C++ exception interop.
+#if ORC_RT_ENABLE_EXCEPTIONS
+
+class ExceptionError : public ErrorExtends<ExceptionError, ErrorInfoBase> {
+public:
+  ExceptionError(std::exception_ptr E) : E(std::move(E)) {}
+  std::string toString() const noexcept override;
+  void throwAsException() override { std::rethrow_exception(E); }
+
+private:
+  mutable std::exception_ptr E;
+};
+
+namespace detail {
+
+// In general we need to wrap a return type of T with an Expected.
+template <typename RetT> struct ErrorWrapImpl {
+  typedef Expected<RetT> return_type;
+
+  template <typename OpFn> static return_type run(OpFn &&Op) { return Op(); }
+};
+
+// If the return is already an Expected value then we don't need to add
+// an additional level of wrapping.
+template <typename RetT> struct ErrorWrapImpl<Expected<RetT>> {
+  typedef Expected<RetT> return_type;
+
+  template <typename OpFn> static return_type run(OpFn &&Op) { return Op(); }
+};
+
+// Errors stay errors.
+template <> struct ErrorWrapImpl<Error> {
+  typedef Error return_type;
+
+  template <typename OpFn> static return_type run(OpFn &&Op) { return Op(); }
+};
+
+// void returns become Error returns.
+template <> struct ErrorWrapImpl<void> {
+  typedef Error return_type;
+
+  template <typename OpFn> static return_type run(OpFn &&Op) {
+    Op();
+    return Error::success();
+  }
+};
+
+template <typename Callable>
+struct ErrorWrap
+    : public CallableTraitsHelper<detail::ErrorWrapImpl, Callable> {};
+
+} // namespace detail
+
+/// Run the given callback capturing any exceptions thrown into an
+/// Error / Expected failure value.
+///
+/// The return type depends on the return type of the callback:
+///   - void callbacks return Error
+///   - Error callbacks return Error
+///   - Expected<T> callbacks return Expected<T>
+///   - other T callbacks return Expected<T>
+///
+/// If the operation succeeds then...
+///   - If its result is non-void it is returned as an Expected<T> success
+///     value
+///   - If its result is void then Error::success() is retured
+///
+/// If the operation fails then...
+///   - If the exception type is std::unique_ptr<ErrorInfoBase> (i.e. a throw
+///     orc_rt failure value) then an Error is constructed to hold the
+///     failure value.
+///   - If the exception has any other type then it's captured as an
+///     ExceptionError.
+///
+/// The scheme allaws...
+///   1. orc_rt::Error values that have been converted to exceptions via
+///      Error::throwOnFailure to be converted back into Errors without loss
+///      of dynamic type info.
+///   2. Other Exceptions caught by this function to be converted back into
+///      exceptions via Error::throwOnFailure without loss of dynamic
+///      type info.
+
+template <typename OpFn>
+typename detail::ErrorWrap<OpFn>::return_type
+runCapturingExceptions(OpFn &&Op) noexcept {
+  try {
+    return detail::ErrorWrap<OpFn>::run(std::forward<OpFn>(Op));
+  } catch (ErrorInfoBase &EIB) {
+    return restore_error(std::move(EIB));
+  } catch (...) {
+    return make_error<ExceptionError>(std::current_exception());
+  }
+}
+
+#endif // ORC_RT_ENABLE_EXCEPTIONS
+
 } // namespace orc_rt
 
 #endif // ORC_RT_ERROR_H
diff --git a/orc-rt/lib/executor/CMakeLists.txt b/orc-rt/lib/executor/CMakeLists.txt
index b5283b9729070..a39e2b63db7b3 100644
--- a/orc-rt/lib/executor/CMakeLists.txt
+++ b/orc-rt/lib/executor/CMakeLists.txt
@@ -1,5 +1,6 @@
 set(files
   AllocAction.cpp
+  Error.cpp
   ResourceManager.cpp
   RTTI.cpp
   Session.cpp
diff --git a/orc-rt/lib/executor/Error.cpp b/orc-rt/lib/executor/Error.cpp
new file mode 100644
index 0000000000000..e9622fd266cb4
--- /dev/null
+++ b/orc-rt/lib/executor/Error.cpp
@@ -0,0 +1,53 @@
+//===- Error.cpp ----------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Contains the implementation of APIs in the orc-rt/Error.h header.
+//
+//===----------------------------------------------------------------------===//
+
+#include "orc-rt/Error.h"
+
+#include <system_error>
+
+namespace orc_rt {
+
+#if ORC_RT_ENABLE_EXCEPTIONS
+
+std::string ExceptionError::toString() const noexcept {
+  std::string Result;
+  try {
+    try {
+      std::rethrow_exception(E);
+    } catch (std::exception &E) {
+      Result = E.what();
+      throw;
+    } catch (std::error_code &EC) {
+      try {
+        // Technically 'message' itself can throw.
+        Result = EC.message();
+      } catch (...) {
+        Result = "std::error_code (.message() call failed)";
+        throw EC;
+      }
+      throw;
+    } catch (std::string &ErrMsg) {
+      Result = ErrMsg;
+      throw;
+    } catch (...) {
+      Result = "c++ exception of unknown type";
+      throw;
+    }
+  } catch (...) {
+    E = std::current_exception();
+  }
+  return Result;
+}
+
+#endif // ORC_RT_ENABLE_EXCEPTIONS
+
+} // namespace orc_rt
diff --git a/orc-rt/unittests/CMakeLists.txt b/orc-rt/unittests/CMakeLists.txt
index 569c8a585bd29..6e95454504830 100644
--- a/orc-rt/unittests/CMakeLists.txt
+++ b/orc-rt/unittests/CMakeLists.txt
@@ -17,6 +17,7 @@ add_orc_rt_unittest(CoreTests
   CallableTraitsHelperTest.cpp
   EndianTest.cpp
   ErrorTest.cpp
+  ErrorExceptionInteropTest.cpp
   ExecutorAddressTest.cpp
   IntervalMapTest.cpp
   IntervalSetTest.cpp
diff --git a/orc-rt/unittests/ErrorExceptionInteropTest.cpp b/orc-rt/unittests/ErrorExceptionInteropTest.cpp
new file mode 100644
index 0000000000000..7399d0e40a40c
--- /dev/null
+++ b/orc-rt/unittests/ErrorExceptionInteropTest.cpp
@@ -0,0 +1,171 @@
+//===- ErrorExceptionInterorTest.cpp --------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Test interoperability between errors and exceptions.
+//
+//===----------------------------------------------------------------------===//
+
+#include "orc-rt-c/config.h"
+#include "orc-rt/Error.h"
+#include "gtest/gtest.h"
+
+using namespace orc_rt;
+
+namespace {
+
+class CustomError : public ErrorExtends<CustomError, ErrorInfoBase> {
+public:
+  std::string toString() const noexcept override { return "CustomError"; }
+};
+
+} // anonymous namespace
+
+#if ORC_RT_ENABLE_EXCEPTIONS
+#define EXCEPTION_TEST(X)                                                      \
+  do {                                                                         \
+    X;                                                                         \
+  } while (false)
+#else
+#define EXCEPTION_TEST(X) GTEST_SKIP() << "Exceptions disabled"
+#endif
+
+TEST(ErrorExceptionInterorTest, SuccessDoesntThrow) {
+  // Test that Error::success values don't throw when throwOnFailure is called.
+  EXCEPTION_TEST({
+    try {
+      auto E = Error::success();
+      E.throwOnFailure();
+    } catch (...) {
+      ADD_FAILURE() << "Unexpected throw";
+    }
+  });
+}
+
+TEST(ErrorExceptionInteropTest, VoidReturnSuccess) {
+  // Test that runCapturingExceptions returns Error::success for void()
+  // function objects.
+  EXCEPTION_TEST({
+    bool Executed = false;
+    auto Result = runCapturingExceptions([&]() { Executed = true; });
+    static_assert(std::is_same_v<decltype(Result), Error>,
+                  "Expected return type to be Error");
+    EXPECT_FALSE(Result); // Error::success() evaluates to false
+    EXPECT_TRUE(Executed);
+  });
+}
+
+TEST(ErrorExceptionInteropTest, ErrorReturnPreserved) {
+  // Test that plain Errors returned from runCapturingExceptions are returned
+  // as expected.
+  EXCEPTION_TEST({
+    auto Result = runCapturingExceptions(
+        []() -> Error { return make_error<StringError>("original error"); });
+    EXPECT_TRUE(!!Result);
+    EXPECT_EQ(toString(std::move(Result)), "original error");
+  });
+}
+
+TEST(ErrorExceptionInteropTest, NonFallibleTReturnWrapped) {
+  // Test that; for function types returning a non-Error, non-Expected type T;
+  // runCapturingExceptions returns an Expected<T>.
+  EXCEPTION_TEST({
+    auto Result = runCapturingExceptions([]() { return 42; });
+    static_assert(std::is_same_v<decltype(Result), Expected<int>>,
+                  "Expected return type to be Expected<int>");
+    EXPECT_TRUE(!!Result);
+    EXPECT_EQ(*Result, 42);
+  });
+}
+
+TEST(ErrorExceptionInteropTest, ExpectedReturnPreserved) {
+  // Test that Expected success values are returned as expected.
+  EXCEPTION_TEST({
+    auto Result = runCapturingExceptions([]() -> Expected<int> { return 42; });
+    EXPECT_TRUE(!!Result);
+    EXPECT_EQ(*Result, 42);
+  });
+}
+
+TEST(ErrorExceptionInteropTest, CatchThrownInt) {
+  // Check that we can capture a thrown exception into an Error and recognize it
+  // as a thrown exception.
+  EXCEPTION_TEST({
+    auto E = runCapturingExceptions([]() { throw 42; });
+    EXPECT_TRUE(!!E);
+    EXPECT_TRUE(E.isA<ExceptionError>());
+    consumeError(std::move(E));
+  });
+}
+
+TEST(ErrorExceptionInteropTest, RoundTripExceptionThroughError) {
+  // Check that we can rethrow an exception that has been captured into an
+  // error without affecting the dynamic type or value (e.g. we don't actually
+  // rethrow the wrong type / value).
+  EXCEPTION_TEST({
+    int Result = 0;
+    try {
+      auto E = runCapturingExceptions([]() { throw 42; });
+      EXPECT_TRUE(!!E);
+      E.throwOnFailure();
+    } catch (int N) {
+      Result = N;
+    } catch (...) {
+      ADD_FAILURE() << "Caught unexpected error type";
+    }
+    EXPECT_EQ(Result, 42);
+  });
+}
+
+static std::string peekAtErrorMessage(Error &Err) {
+  std::string Msg;
+  Err = handleErrors(std::move(Err), [&](std::unique_ptr<ErrorInfoBase> EIB) {
+    Msg = EIB->toString();
+    return make_error(std::move(EIB));
+  });
+  return Msg;
+}
+
+TEST(ErrorExceptionInteropTest, RoundTripErrorThroughException) {
+  // Test Error → Exception → Error preserves type and message
+  EXCEPTION_TEST({
+    auto OriginalErr = make_error<StringError>("hello, error!");
+    std::string OriginalMsg = peekAtErrorMessage(OriginalErr);
+
+    Error RecoveredErr = Error::success();
+    try {
+      OriginalErr.throwOnFailure();
+    } catch (ErrorInfoBase &EIB) {
+      ErrorAsOutParameter _(RecoveredErr);
+      RecoveredErr = restore_error(std::move(EIB));
+    } catch (...) {
+      ADD_FAILURE() << "Caught unexpected error type";
+    }
+
+    EXPECT_TRUE(RecoveredErr.isA<StringError>());
+    EXPECT_EQ(toString(std::move(RecoveredErr)), OriginalMsg);
+  });
+}
+
+TEST(ErrorExceptionInteropTest, ThrowErrorAndCatchAsException) {
+  // Check that we can create an Error value, throw it as an exception, and
+  // match its dynamic type to a catch handler.
+  EXCEPTION_TEST({
+    bool HandlerRan = false;
+    std::string Msg;
+    try {
+      auto E = make_error<CustomError>();
+      E.throwOnFailure();
+    } catch (CustomError &E) {
+      HandlerRan = true;
+    } catch (ErrorInfoBase &E) {
+      ADD_FAILURE() << "Failed to downcase error to dynamic type";
+    } catch (...) {
+      ADD_FAILURE() << "Caught unexpected error type";
+    }
+  });
+}
diff --git a/orc-rt/unittests/ErrorTest.cpp b/orc-rt/unittests/ErrorTest.cpp
index d80ada9529696..d9fee99ea817c 100644
--- a/orc-rt/unittests/ErrorTest.cpp
+++ b/orc-rt/unittests/ErrorTest.cpp
@@ -21,10 +21,10 @@ using namespace orc_rt;
 
 namespace {
 
-class CustomError : public RTTIExtends<CustomError, ErrorInfoBase> {
+class CustomError : public ErrorExtends<CustomError, ErrorInfoBase> {
 public:
   CustomError(int Info) : Info(Info) {}
-  std::string toString() const override {
+  std::string toString() const noexcept override {
     return "CustomError (" + std::to_string(Info) + ")";
   }
   int getInfo() const { return Info; }
@@ -33,13 +33,13 @@ class CustomError : public RTTIExtends<CustomError, ErrorInfoBase> {
   int Info;
 };
 
-class CustomSubError : public RTTIExtends<CustomSubError, CustomError> {
+class CustomSubError : public ErrorExtends<CustomSubError, CustomError> {
 public:
   CustomSubError(int Info, std::string ExtraInfo)
-      : RTTIExtends<CustomSubError, CustomError>(Info),
+      : ErrorExtends<CustomSubError, CustomError>(Info),
         ExtraInfo(std::move(ExtraInfo)) {}
 
-  std::string toString() const override {
+  std::string toString() const noexcept override {
     return "CustomSubError (" + std::to_string(Info) + ", " + ExtraInfo + ")";
   }
   const std::string &getExtraInfo() const { return ExtraInfo; }

>From 8f1943f1194c6eec7046badfa6a28fbd3eed2b93 Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Mon, 15 Dec 2025 15:45:15 +1100
Subject: [PATCH 2/3] Address review feedback:

1. Conditionalize `#include <exception>`: While this header should always be
safe to include we can save ourselves a little compile time by not parsing it
where it's not needed.

2. Simplify ExceptionError::toString and add a unit test for it.
---
 orc-rt/include/orc-rt/Error.h                 |  5 ++-
 orc-rt/lib/executor/Error.cpp                 | 31 ++++++--------
 .../unittests/ErrorExceptionInteropTest.cpp   | 42 +++++++++++++++++++
 3 files changed, 59 insertions(+), 19 deletions(-)

diff --git a/orc-rt/include/orc-rt/Error.h b/orc-rt/include/orc-rt/Error.h
index 656e3f31f35d0..cbeaac3592684 100644
--- a/orc-rt/include/orc-rt/Error.h
+++ b/orc-rt/include/orc-rt/Error.h
@@ -16,11 +16,14 @@
 
 #include <cassert>
 #include <cstdlib>
-#include <exception>
 #include <memory>
 #include <string>
 #include <type_traits>
 
+#if ORC_RT_ENABLE_EXCEPTIONS
+#include <exception>
+#endif // ORC_RT_ENABLE_EXCEPTIONS
+
 namespace orc_rt {
 
 class Error;
diff --git a/orc-rt/lib/executor/Error.cpp b/orc-rt/lib/executor/Error.cpp
index e9622fd266cb4..6cf85f47eddf7 100644
--- a/orc-rt/lib/executor/Error.cpp
+++ b/orc-rt/lib/executor/Error.cpp
@@ -21,28 +21,23 @@ namespace orc_rt {
 std::string ExceptionError::toString() const noexcept {
   std::string Result;
   try {
+    std::rethrow_exception(E);
+  } catch (std::exception &SE) {
+    Result = SE.what();
+    E = std::current_exception();
+  } catch (std::error_code &EC) {
     try {
-      std::rethrow_exception(E);
-    } catch (std::exception &E) {
-      Result = E.what();
-      throw;
-    } catch (std::error_code &EC) {
-      try {
-        // Technically 'message' itself can throw.
-        Result = EC.message();
-      } catch (...) {
-        Result = "std::error_code (.message() call failed)";
-        throw EC;
-      }
-      throw;
-    } catch (std::string &ErrMsg) {
-      Result = ErrMsg;
-      throw;
+      // Technically 'message' itself can throw.
+      Result = EC.message();
     } catch (...) {
-      Result = "c++ exception of unknown type";
-      throw;
+      Result = "std::error_code (.message() call failed)";
     }
+    E = std::current_exception();
+  } catch (std::string &ErrMsg) {
+    Result = ErrMsg;
+    E = std::current_exception();
   } catch (...) {
+    Result = "C++ exception of unknown type";
     E = std::current_exception();
   }
   return Result;
diff --git a/orc-rt/unittests/ErrorExceptionInteropTest.cpp b/orc-rt/unittests/ErrorExceptionInteropTest.cpp
index 7399d0e40a40c..dd106f14ee6e5 100644
--- a/orc-rt/unittests/ErrorExceptionInteropTest.cpp
+++ b/orc-rt/unittests/ErrorExceptionInteropTest.cpp
@@ -14,6 +14,8 @@
 #include "orc-rt/Error.h"
 #include "gtest/gtest.h"
 
+#include <system_error>
+
 using namespace orc_rt;
 
 namespace {
@@ -169,3 +171,43 @@ TEST(ErrorExceptionInteropTest, ThrowErrorAndCatchAsException) {
     }
   });
 }
+
+TEST(ErrorExceptionInteropTest, ErrorExceptionToString) {
+  /// Check that exceptions can be converted to Strings as exepcted.
+  EXCEPTION_TEST({
+
+    {
+      // std::exception should be converted by calling `.what()`;
+      class MyException : public std::exception {
+      public:
+        ~MyException() override {}
+        const char *what() const noexcept override { return "what"; }
+      };
+
+      EXPECT_EQ(toString(runCapturingExceptions([]() { throw MyException(); })),
+                "what");
+    }
+
+    {
+      // std::error_code should be converted by calling `.message()`.
+      auto EC = std::make_error_code(std::errc::cross_device_link);
+      std::string ECErrMsg = EC.message();
+      EXPECT_EQ(toString(runCapturingExceptions([&]() { throw EC; })),
+                ECErrMsg);
+    }
+
+    {
+      // std::string should be converted by copying its value.
+      std::string ErrMsg = "foo";
+      EXPECT_EQ(toString(runCapturingExceptions([&]() { throw ErrMsg; })),
+                ErrMsg);
+    }
+
+    {
+      // Check that exceptions of other types produce the expected
+      // "unrecognized type" error message:
+      EXPECT_EQ(toString(runCapturingExceptions([]() { throw 42; })),
+                "C++ exception of unknown type");
+    }
+  });
+}

>From ec506d28a82bfa76997868690cabc4d25b41e3c7 Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Mon, 15 Dec 2025 15:53:24 +1100
Subject: [PATCH 3/3] Address review feedback: Use make_unique.

---
 orc-rt/include/orc-rt/Error.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/orc-rt/include/orc-rt/Error.h b/orc-rt/include/orc-rt/Error.h
index cbeaac3592684..07a3be98fd21d 100644
--- a/orc-rt/include/orc-rt/Error.h
+++ b/orc-rt/include/orc-rt/Error.h
@@ -198,7 +198,7 @@ inline Error make_error(std::unique_ptr<ErrorInfoBase> Payload) noexcept {
 template <typename ThisT, typename ParentT>
 Error ErrorExtends<ThisT, ParentT>::restoreError() noexcept {
   return make_error(
-      std::unique_ptr<ThisT>(new ThisT(*static_cast<ThisT *>(this))));
+      std::make_unique<ThisT>(std::move(*static_cast<ThisT *>(this))));
 }
 
 inline Error restore_error(ErrorInfoBase &&EIB) { return EIB.restoreError(); }



More information about the llvm-commits mailing list