Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 152 additions & 8 deletions include/tvm/ffi/expected.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.");

/*!
Expand Down Expand Up @@ -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 {

/*!
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -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_
13 changes: 12 additions & 1 deletion include/tvm/ffi/function.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>()) {
Expand Down
87 changes: 87 additions & 0 deletions tests/cpp/test_expected.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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