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