This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new a849ee3e [FEAT] Add Expected<void> specialization (#673)
a849ee3e is described below

commit a849ee3e5354a47321bceeaf562d97b5e5b3217a
Author: Kathryn (Jinqi) Chen <[email protected]>
AuthorDate: Thu Jul 16 16:12:23 2026 -0700

    [FEAT] Add Expected<void> specialization (#673)
    
    This PR adds Expected<void> for operations that can succeed without
    returning a value. Success uses the canonical FFI None representation,
    while failures retain ffi::Error.
    
    It extends ExpectedUnsafe and TypeTraits for Expected<void>, adds
    Function::CallExpected<void> support with result validation, and
    includes tests covering construction, access, FFI conversion, error
    propagation, and typed function integration.
---
 include/tvm/ffi/expected.h | 160 ++++++++++++++++++++++++++++++++++++++++++---
 include/tvm/ffi/function.h |  13 +++-
 tests/cpp/test_expected.cc |  87 ++++++++++++++++++++++++
 3 files changed, 251 insertions(+), 9 deletions(-)

diff --git a/include/tvm/ffi/expected.h b/include/tvm/ffi/expected.h
index ee5f7d35..14412ac9 100644
--- a/include/tvm/ffi/expected.h
+++ b/include/tvm/ffi/expected.h
@@ -76,7 +76,8 @@ struct ExpectedUnsafe;
  * Expected<T> is similar to Rust's Result<T, Error> or C++23's std::expected.
  * It can hold either a success value of type T or an error of type Error.
  *
- * \tparam T The success type. Must be Any-compatible and cannot be Error.
+ * \tparam T The success type. Must be Any-compatible and cannot be Error. The 
``void`` type is
+ *         supported as a success state without a value. \sa Expected<void>
  *
  * Usage:
  * \code
@@ -98,6 +99,9 @@ struct ExpectedUnsafe;
 template <typename T>
 class Expected {
  public:
+  static_assert(
+      !std::is_void_v<T>,
+      "Expected with a cv-qualified void success type is not allowed. Use 
Expected<void>.");
   static_assert(!std::is_same_v<T, Error>, "Expected<Error> is not allowed. 
Use Error directly.");
 
   /*!
@@ -202,6 +206,83 @@ class Expected {
   Any data_;  // Invariant: holds a T (type_index != kTVMFFIError) or an Error.
 };
 
+/*!
+ * \brief Specialization of Expected for a successful operation without a 
value.
+ *
+ * Expected<void> stores FFI None when successful and an Error when 
unsuccessful. A successful
+ * value is constructed with the default constructor, and \ref value validates 
success without
+ * returning a value.
+ */
+template <>
+class Expected<void> {
+ public:
+  /*! \brief Construct a successful Expected<void>. */
+  Expected() = default;
+
+  /*!
+   * \brief Implicit constructor from an error.
+   * \param error The error value.
+   */
+  // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+  Expected(Error error) : data_(Any(std::move(error))) {}
+
+  /*! \brief Implicit constructor from an Unexpected wrapper. */
+  template <typename E, typename = std::enable_if_t<std::is_base_of_v<Error, 
std::remove_cv_t<E>>>>
+  // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+  Expected(Unexpected<E> unexpected) : 
data_(Any(std::move(unexpected).error())) {}
+
+  /*! \brief Return the raw stored type index. */
+  TVM_FFI_INLINE int32_t type_index() const noexcept { return 
data_.type_index(); }
+
+  /*! \brief Returns true if this Expected represents successful completion. */
+  TVM_FFI_INLINE bool is_ok() const noexcept {
+    return data_.type_index() != TypeIndex::kTVMFFIError;
+  }
+
+  /*! \brief Returns true if this Expected contains an error. */
+  TVM_FFI_INLINE bool is_err() const noexcept {
+    return data_.type_index() == TypeIndex::kTVMFFIError;
+  }
+
+  /*! \brief Alias for is_ok(). */
+  TVM_FFI_INLINE bool has_value() const noexcept { return is_ok(); }
+
+  /*! \brief Validate successful completion, or throw the contained error. */
+  TVM_FFI_INLINE void value() const& {
+    if (TVM_FFI_PREDICT_FALSE(is_err())) {
+      throw details::AnyUnsafe::CopyFromAnyViewAfterCheck<Error>(data_);
+    }
+  }
+
+  /*! \brief Validate successful completion, or throw the contained error by 
move. */
+  TVM_FFI_INLINE void value() && {
+    if (TVM_FFI_PREDICT_FALSE(is_err())) {
+      throw details::AnyUnsafe::MoveFromAnyAfterCheck<Error>(std::move(data_));
+    }
+  }
+
+  /*! \brief Returns the contained error, or throws RuntimeError if is_ok(). */
+  TVM_FFI_INLINE Error error() const& {
+    if (is_ok()) {
+      TVM_FFI_THROW(RuntimeError) << "Bad expected access: contains value, not 
error";
+    }
+    return details::AnyUnsafe::CopyFromAnyViewAfterCheck<Error>(data_);
+  }
+
+  /*! \brief Returns the contained error (moved out), or throws RuntimeError 
if is_ok(). */
+  TVM_FFI_INLINE Error error() && {
+    if (is_ok()) {
+      TVM_FFI_THROW(RuntimeError) << "Bad expected access: contains value, not 
error";
+    }
+    return details::AnyUnsafe::MoveFromAnyAfterCheck<Error>(std::move(data_));
+  }
+
+ private:
+  friend struct details::ExpectedUnsafe;
+
+  Any data_;  // Invariant: holds FFI None on success or an Error.
+};
+
 namespace details {
 
 /*!
@@ -248,20 +329,27 @@ struct ExpectedUnsafe {
 
   /*!
    * \brief Read an Expected success value as a compatible raw storage type.
-   * \tparam T The type to read from the underlying Any storage.
+   * \tparam T The type to read from the underlying Any storage, or ``void`` 
to validate an
+   *           ``Expected<void>`` success state.
    * \tparam U The Expected success type.
    * \param result The Expected value to read from.
-   * \return The stored value decoded as T.
+   * \return The stored value decoded as T, or no value when T is ``void``.
    *
-   * \note This assumes \p result stores T-compatible Any storage, or Error.
+   * \note When T is ``void``, U must also be ``void``. Otherwise, this 
assumes \p result stores
+   *       T-compatible Any storage, or Error.
    */
   template <typename T, typename U>
   TVM_FFI_INLINE static T ValueAs(const Expected<U>& result) {
-    const Any& data = result.data_;
-    if (TVM_FFI_PREDICT_TRUE(data.type_index() != TypeIndex::kTVMFFIError)) {
-      return AnyUnsafe::CopyFromAnyViewAfterCheck<T>(data);
+    if constexpr (std::is_void_v<T>) {
+      static_assert(std::is_void_v<U>, "ExpectedUnsafe::ValueAs<void> requires 
an Expected<void>");
+      result.value();
+    } else {
+      const Any& data = result.data_;
+      if (TVM_FFI_PREDICT_TRUE(data.type_index() != TypeIndex::kTVMFFIError)) {
+        return AnyUnsafe::CopyFromAnyViewAfterCheck<T>(data);
+      }
+      throw AnyUnsafe::CopyFromAnyViewAfterCheck<Error>(data);
     }
-    throw AnyUnsafe::CopyFromAnyViewAfterCheck<Error>(data);
   }
 };
 
@@ -327,6 +415,62 @@ struct TypeTraits<Expected<T>> : public TypeTraitsBase {
   }
 };
 
+/*! \brief TypeTraits specialization for Expected<void>. */
+template <>
+struct TypeTraits<Expected<void>> : public TypeTraitsBase {
+  TVM_FFI_INLINE static void CopyToAnyView(const Expected<void>& src, 
TVMFFIAny* result) {
+    if (src.is_err()) {
+      TypeTraits<Error>::CopyToAnyView(src.error(), result);
+    } else {
+      TypeTraits<std::nullptr_t>::CopyToAnyView(nullptr, result);
+    }
+  }
+
+  TVM_FFI_INLINE static void MoveToAny(Expected<void> src, TVMFFIAny* result) {
+    if (src.is_err()) {
+      TypeTraits<Error>::MoveToAny(std::move(src).error(), result);
+    } else {
+      TypeTraits<std::nullptr_t>::MoveToAny(nullptr, result);
+    }
+  }
+
+  TVM_FFI_INLINE static bool CheckAnyStrict(const TVMFFIAny* src) {
+    return TypeTraits<std::nullptr_t>::CheckAnyStrict(src) ||
+           TypeTraits<Error>::CheckAnyStrict(src);
+  }
+
+  TVM_FFI_INLINE static Expected<void> CopyFromAnyViewAfterCheck(const 
TVMFFIAny* src) {
+    if (TypeTraits<std::nullptr_t>::CheckAnyStrict(src)) {
+      return Expected<void>();
+    }
+    return TypeTraits<Error>::CopyFromAnyViewAfterCheck(src);
+  }
+
+  TVM_FFI_INLINE static Expected<void> MoveFromAnyAfterCheck(TVMFFIAny* src) {
+    if (TypeTraits<std::nullptr_t>::CheckAnyStrict(src)) {
+      return Expected<void>();
+    }
+    return TypeTraits<Error>::MoveFromAnyAfterCheck(src);
+  }
+
+  TVM_FFI_INLINE static std::optional<Expected<void>> TryCastFromAnyView(const 
TVMFFIAny* src) {
+    if (TypeTraits<std::nullptr_t>::CheckAnyStrict(src)) {
+      return Expected<void>();
+    }
+    if (auto opt_err = TypeTraits<Error>::TryCastFromAnyView(src)) {
+      return Expected<void>(*std::move(opt_err));
+    }
+    return std::nullopt;
+  }
+
+  TVM_FFI_INLINE static std::string TypeStr() { return "Expected<void>"; }
+
+  TVM_FFI_INLINE static std::string TypeSchema() {
+    return R"({"type":"Expected","args":[)" + 
TypeTraits<std::nullptr_t>::TypeSchema() +
+           R"(,{"type":"ffi.Error"}]})";
+  }
+};
+
 }  // namespace ffi
 }  // namespace tvm
 #endif  // TVM_FFI_EXPECTED_H_
diff --git a/include/tvm/ffi/function.h b/include/tvm/ffi/function.h
index 6f2dec25..30aed159 100644
--- a/include/tvm/ffi/function.h
+++ b/include/tvm/ffi/function.h
@@ -672,7 +672,18 @@ class Function : public ObjectRef {
 
     if (ret_code == 0) {
       if constexpr (std::is_same_v<T, Any>) {
-        return std::move(result);
+        return result;
+      } else if constexpr (std::is_same_v<T, void>) {
+        if (result.type_index() == TypeIndex::kTVMFFINone) {
+          return Expected<void>();
+        }
+        if (auto err = result.template try_cast<Error>()) {
+          return Unexpected(std::move(*err));
+        }
+        return Unexpected(Error(
+            "TypeError",
+            "CallExpected: result type mismatch, expected void, but got " + 
result.GetTypeKey(),
+            ""));
       } else {
         // Try T first (fast path), then Error
         if (auto val = result.template try_cast<T>()) {
diff --git a/tests/cpp/test_expected.cc b/tests/cpp/test_expected.cc
index 4f432a8a..d4d2ac26 100644
--- a/tests/cpp/test_expected.cc
+++ b/tests/cpp/test_expected.cc
@@ -401,4 +401,91 @@ TEST(ExpectedRvalueMove, PodTypesCompile) {
   EXPECT_EQ(std::move(Expected<DLDataType>(dtype)).value().bits, 64);
 }
 
+// Test the default successful state of Expected<void>.
+TEST(ExpectedVoid, BasicOk) {
+  Expected<void> result;
+
+  EXPECT_TRUE(result.is_ok());
+  EXPECT_FALSE(result.is_err());
+  EXPECT_TRUE(result.has_value());
+  EXPECT_EQ(result.type_index(), TypeIndex::kTVMFFINone);
+  EXPECT_NO_THROW(result.value());
+  EXPECT_NO_THROW(std::move(result).value());
+}
+
+// Test construction of Expected<void> from Error and Unexpected.
+TEST(ExpectedVoid, ErrorConstruction) {
+  Expected<void> direct_error = Error("ValueError", "void operation failed", 
"");
+  EXPECT_FALSE(direct_error.is_ok());
+  EXPECT_TRUE(direct_error.is_err());
+  EXPECT_FALSE(direct_error.has_value());
+  EXPECT_EQ(direct_error.type_index(), TypeIndex::kTVMFFIError);
+  EXPECT_EQ(direct_error.error().kind(), "ValueError");
+  EXPECT_EQ(direct_error.error().message(), "void operation failed");
+
+  Expected<void> unexpected = Unexpected(Error("TypeError", "unexpected void 
failure", ""));
+  EXPECT_TRUE(unexpected.is_err());
+  EXPECT_EQ(unexpected.error().kind(), "TypeError");
+  EXPECT_EQ(unexpected.error().message(), "unexpected void failure");
+}
+
+// Test that both value() overloads throw the original contained Error.
+TEST(ExpectedVoid, BadValueAccessThrowsOriginalError) {
+  Expected<void> lvalue_result = Error("LValueError", "lvalue void failure", 
"");
+  try {
+    lvalue_result.value();
+    FAIL() << "Expected Error to be thrown";
+  } catch (const Error& err) {
+    EXPECT_EQ(err.kind(), "LValueError");
+    EXPECT_EQ(err.message(), "lvalue void failure");
+  }
+
+  Expected<void> rvalue_result = Error("RValueError", "rvalue void failure", 
"");
+  try {
+    std::move(rvalue_result).value();
+    FAIL() << "Expected Error to be thrown";
+  } catch (const Error& err) {
+    EXPECT_EQ(err.kind(), "RValueError");
+    EXPECT_EQ(err.message(), "rvalue void failure");
+  }
+}
+
+// Test successful Expected<void> conversion through AnyView and Any.
+TEST(ExpectedVoid, TypeTraitsSuccessRoundtrip) {
+  Expected<void> original;
+
+  AnyView view = original;
+  EXPECT_EQ(view.type_index(), TypeIndex::kTVMFFINone);
+  Expected<void> copied = view.cast<Expected<void>>();
+  EXPECT_TRUE(copied.is_ok());
+  EXPECT_NO_THROW(copied.value());
+
+  Any owned = original;
+  EXPECT_EQ(owned.type_index(), TypeIndex::kTVMFFINone);
+  Expected<void> moved = std::move(owned).cast<Expected<void>>();
+  EXPECT_TRUE(moved.is_ok());
+  EXPECT_NO_THROW(std::move(moved).value());
+}
+
+// Test failed Expected<void> conversion and rejection of incompatible values.
+TEST(ExpectedVoid, TypeTraitsErrorRoundtrip) {
+  Expected<void> original = Error("TypeError", "void conversion failed", "");
+
+  AnyView view = original;
+  Expected<void> copied = view.cast<Expected<void>>();
+  ASSERT_TRUE(copied.is_err());
+  EXPECT_EQ(copied.error().kind(), "TypeError");
+  EXPECT_EQ(copied.error().message(), "void conversion failed");
+
+  Any owned = std::move(original);
+  EXPECT_EQ(owned.type_index(), TypeIndex::kTVMFFIError);
+  Expected<void> moved = std::move(owned).cast<Expected<void>>();
+  ASSERT_TRUE(moved.is_err());
+  EXPECT_EQ(moved.error().kind(), "TypeError");
+  EXPECT_EQ(moved.error().message(), "void conversion failed");
+
+  Any incompatible = int64_t{1};
+  EXPECT_FALSE(incompatible.try_cast<Expected<void>>().has_value());
+}
+
 }  // namespace

Reply via email to