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 808e99de [FIX] Restore strict casts on FFI-backed Optional (#842)
808e99de is described below
commit 808e99de587d8196ece1dd156d02502242ead8cf
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Oct 11 18:10:45 2026 -0400
[FIX] Restore strict casts on FFI-backed Optional (#842)
---
include/tvm/ffi/any.h | 25 +++++++---
include/tvm/ffi/optional.h | 115 +++++++++++++++++++++++++++++++++++++++++++--
tests/cpp/test_object.cc | 9 +++-
tests/cpp/test_optional.cc | 19 ++++++--
4 files changed, 152 insertions(+), 16 deletions(-)
diff --git a/include/tvm/ffi/any.h b/include/tvm/ffi/any.h
index 6b65b8be..1231eee5 100644
--- a/include/tvm/ffi/any.h
+++ b/include/tvm/ffi/any.h
@@ -143,6 +143,23 @@ class AnyView {
return this->as<const T*>().value_or(nullptr);
}
+ /*!
+ * \brief Strictly reinterpret the AnyView as a type T or throw.
+ * \tparam T The type to cast to.
+ * \return The casted value.
+ * \note This function will not run fallback conversions.
+ */
+ template <typename T, typename =
std::enable_if_t<TypeTraits<T>::convert_enabled>>
+ TVM_FFI_INLINE T as_or_throw() const {
+ std::optional<T> result = this->as<T>();
+ if (TVM_FFI_PREDICT_FALSE(!result.has_value())) {
+ TVM_FFI_THROW(TypeError) << "Cannot treat type `"
+ << TypeTraits<T>::GetMismatchTypeInfo(&data_)
<< "` as type `"
+ << TypeTraits<T>::TypeStr() << "`";
+ }
+ return *std::move(result);
+ }
+
/*!
* \brief Cast to a type T.
*
@@ -429,13 +446,7 @@ class Any {
if constexpr (std::is_same_v<T, Any>) {
return *this;
} else {
- std::optional<T> result = this->as<T>();
- if (TVM_FFI_PREDICT_FALSE(!result.has_value())) {
- TVM_FFI_THROW(TypeError) << "Cannot treat type `"
- << TypeTraits<T>::GetMismatchTypeInfo(&data_)
<< "` as type `"
- << TypeTraits<T>::TypeStr() << "`";
- }
- return *std::move(result);
+ return AnyView(*this).template as_or_throw<T>();
}
}
diff --git a/include/tvm/ffi/optional.h b/include/tvm/ffi/optional.h
index 7a14bc2a..b7d813ff 100644
--- a/include/tvm/ffi/optional.h
+++ b/include/tvm/ffi/optional.h
@@ -259,14 +259,40 @@ class Optional<T,
/*!
* \brief Try to reinterpret the stored value as a type U (strict, no
conversion).
* \tparam U The type to reinterpret to.
- * \return std::optional<U> for ObjectRef-like U, or const U* when U is an
Object type.
- * \note Returns an empty result when nullopt or the strict type check fails.
+ * \return std::optional<U>, or const U* when U is an Object type.
+ * \note Nullopt is treated as None; no fallback conversions are performed.
*/
- template <typename U>
+ template <typename U,
+ typename = std::enable_if_t<TypeTraits<U>::convert_enabled ||
std::is_same_v<U, Any> ||
+ std::is_base_of_v<Object, U>>>
TVM_FFI_INLINE auto as() const {
return data_.template as<U>();
}
+ /*!
+ * \brief Strictly reinterpret the stored value as U or throw TypeError.
+ * \tparam U The type to cast to.
+ * \return The casted value.
+ * \note Nullopt is treated as None; no fallback conversions are performed.
+ */
+ template <typename U,
+ typename = std::enable_if_t<TypeTraits<U>::convert_enabled ||
std::is_same_v<U, Any>>>
+ TVM_FFI_INLINE auto as_or_throw() const& {
+ return data_.template as_or_throw<U>();
+ }
+
+ /*!
+ * \brief Strictly reinterpret the stored value as U, moving ownership on
success.
+ * \tparam U The type to cast to.
+ * \return The casted value.
+ * \note Throws TypeError without consuming the value if the strict check
fails.
+ */
+ template <typename U,
+ typename = std::enable_if_t<TypeTraits<U>::storage_enabled ||
std::is_same_v<U, Any>>>
+ TVM_FFI_INLINE auto as_or_throw() && {
+ return std::move(data_).template as_or_throw<U>();
+ }
+
/*!
* \brief Direct access to the value.
* \note only use this function after checking has_value()
@@ -441,6 +467,44 @@ class Optional<T,
std::enable_if_t<use_object_ref_optional_v<T>>> {
TVM_FFI_INLINE explicit operator bool() const noexcept { return has_value();
}
TVM_FFI_INLINE bool has_value() const noexcept { return data_ != nullptr; }
+ /*!
+ * \brief Try to reinterpret the stored value as U without conversion.
+ * \tparam U The type to cast to.
+ * \return std::optional<U>, or a borrowed const U* when U is an Object type.
+ * \note Nullopt is treated as None.
+ */
+ template <typename U, typename =
std::enable_if_t<TypeTraits<U>::convert_enabled ||
+ std::is_base_of_v<Object,
U>>>
+ TVM_FFI_INLINE auto as() const {
+ return AnyView(*this).template as<U>();
+ }
+
+ /*!
+ * \brief Strictly reinterpret the stored value as U or throw TypeError.
+ * \tparam U The type to cast to.
+ * \return The casted value.
+ * \note Nullopt is treated as None; no fallback conversions are performed.
+ */
+ template <typename U, typename =
std::enable_if_t<TypeTraits<U>::convert_enabled>>
+ TVM_FFI_INLINE auto as_or_throw() const& {
+ return AnyView(*this).template as_or_throw<U>();
+ }
+
+ /*!
+ * \brief Strictly reinterpret the stored value as U, moving ownership on
success.
+ * \tparam U The type to cast to.
+ * \return The casted value.
+ * \note Throws TypeError without consuming the value if the strict check
fails.
+ */
+ template <typename U, typename =
std::enable_if_t<TypeTraits<U>::storage_enabled>>
+ TVM_FFI_INLINE auto as_or_throw() && {
+ AnyView view(data_);
+ if
(TVM_FFI_PREDICT_FALSE(!details::AnyUnsafe::CheckAnyViewStrict<U>(view))) {
+ return view.template as_or_throw<U>();
+ }
+ return details::AnyUnsafe::MoveFromAnyAfterCheck<U>(Any(std::move(data_)));
+ }
+
TVM_FFI_INLINE T operator*() const& noexcept {
return details::ObjectUnsafe::ObjectRefFromObjectPtr<T>(data_);
}
@@ -629,6 +693,45 @@ class Optional<T, std::enable_if_t<is_object_ptr_type_v<T>
|| is_arc_type_v<T>>>
TVM_FFI_INLINE explicit operator bool() const noexcept { return has_value();
}
TVM_FFI_INLINE bool has_value() const noexcept { return StorageType::get()
!= nullptr; }
+ /*!
+ * \brief Try to reinterpret the stored value as U without conversion.
+ * \tparam U The type to cast to.
+ * \return std::optional<U>, or a borrowed const U* when U is an Object type.
+ * \note Nullopt is treated as None.
+ */
+ template <typename U, typename =
std::enable_if_t<TypeTraits<U>::convert_enabled ||
+ std::is_base_of_v<Object,
U>>>
+ TVM_FFI_INLINE auto as() const {
+ return AnyView(*this).template as<U>();
+ }
+
+ /*!
+ * \brief Strictly reinterpret the stored value as U or throw TypeError.
+ * \tparam U The type to cast to.
+ * \return The casted value.
+ * \note Nullopt is treated as None; no fallback conversions are performed.
+ */
+ template <typename U, typename =
std::enable_if_t<TypeTraits<U>::convert_enabled>>
+ TVM_FFI_INLINE auto as_or_throw() const& {
+ return AnyView(*this).template as_or_throw<U>();
+ }
+
+ /*!
+ * \brief Strictly reinterpret the stored value as U, moving ownership on
success.
+ * \tparam U The type to cast to.
+ * \return The casted value.
+ * \note Throws TypeError without consuming the value if the strict check
fails.
+ */
+ template <typename U, typename =
std::enable_if_t<TypeTraits<U>::storage_enabled>>
+ TVM_FFI_INLINE auto as_or_throw() && {
+ auto& data = static_cast<StorageType&>(*this);
+ AnyView view(data);
+ if
(TVM_FFI_PREDICT_FALSE(!details::AnyUnsafe::CheckAnyViewStrict<U>(view))) {
+ return view.template as_or_throw<U>();
+ }
+ return details::AnyUnsafe::MoveFromAnyAfterCheck<U>(Any(std::move(data)));
+ }
+
TVM_FFI_INLINE T operator*() const& noexcept {
return Traits::Copy(static_cast<const StorageType&>(*this));
}
@@ -675,6 +778,12 @@ struct TypeTraits<Optional<T>> : public TypeTraitsBase {
TVM_FFI_INLINE static void CopyToAnyView(const Optional<T>& src, TVMFFIAny*
result) {
if constexpr (TypeTraits<T>::storage_enabled &&
!use_object_ptr_optional_v<T>) {
*result = src.ToAnyView().CopyToTVMFFIAny();
+ } else if constexpr (use_object_ptr_optional_v<T>) {
+ if (src.has_value()) {
+ TypeTraits<decltype(src.get())>::CopyToAnyView(src.get(), result);
+ } else {
+ TypeTraits<std::nullptr_t>::CopyToAnyView(nullptr, result);
+ }
} else {
if (src.has_value()) {
TypeTraits<T>::CopyToAnyView(*src, result);
diff --git a/tests/cpp/test_object.cc b/tests/cpp/test_object.cc
index 5bd7762a..e047c483 100644
--- a/tests/cpp/test_object.cc
+++ b/tests/cpp/test_object.cc
@@ -672,7 +672,14 @@ TEST(ObjectPtrStorage, OptionalAndVariantComposition) {
OptionalNumberOrInt variant = optional_number;
OptionalNumber variant_value = variant.get<OptionalNumber>();
ASSERT_TRUE(variant_value.has_value());
- EXPECT_EQ(variant_value.value(), number);
+ EXPECT_THROW(std::move(variant_value).as_or_throw<ObjectPtr<TFloatObj>>(),
Error);
+ int use_count = number.use_count();
+ // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ NumberPtr moved_value = std::move(variant_value).as_or_throw<NumberPtr>();
+ EXPECT_EQ(moved_value, number);
+ // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ EXPECT_FALSE(variant_value.has_value());
+ EXPECT_EQ(number.use_count(), use_count);
OptionalNumberOrIntValue optional_variant = Variant<NumberPtr,
int64_t>(number);
Any encoded = optional_variant;
diff --git a/tests/cpp/test_optional.cc b/tests/cpp/test_optional.cc
index 2b562a97..6f2b3bca 100644
--- a/tests/cpp/test_optional.cc
+++ b/tests/cpp/test_optional.cc
@@ -76,7 +76,7 @@ TEST(Optional, TInt) {
EXPECT_EQ(x.value_or(TInt(12))->value, 12);
EXPECT_TRUE(y.has_value());
- EXPECT_EQ(y.value_or(TInt(12))->value, 11);
+ EXPECT_EQ(y.as_or_throw<TInt>()->value, 11);
Any z_any = std::move(y);
EXPECT_TRUE(z_any != nullptr);
@@ -97,7 +97,7 @@ TEST(Optional, ObjectRefPointerABI) {
ObjectRef value = TInt(19);
Optional<ObjectRef> present = value;
ASSERT_TRUE(present.has_value());
- EXPECT_TRUE(present.same_as(value));
+ EXPECT_EQ(present.as<TIntObj>(), value.as<TIntObj>());
EXPECT_EQ(present.use_count(), value.use_count());
Any encoded = present;
@@ -121,7 +121,7 @@ TEST(Optional, double) {
EXPECT_TRUE(x != 12);
EXPECT_TRUE(y.has_value());
- EXPECT_EQ(y.value_or(12), 11);
+ EXPECT_EQ(y.as_or_throw<double>(), 11);
EXPECT_TRUE(y == 11);
EXPECT_TRUE(y != 12);
}
@@ -152,7 +152,7 @@ TEST(Optional, AnyConvertArray) {
auto arr1 = view0.cast<Optional<Array<Array<TNumber>>>>();
EXPECT_EQ(arr_nested.use_count(), 3);
- EXPECT_EQ(arr1.value()[1][1].as<TFloatObj>()->value, 2);
+
EXPECT_EQ(arr1.as_or_throw<Array<Array<TNumber>>>()[1][1].as<TFloatObj>()->value,
2);
Any any1;
auto arr2 = any1.cast<Optional<Array<Array<TNumber>>>>();
@@ -211,10 +211,14 @@ TEST(Optional, OptionalOfOptional) {
TEST(Optional, ValueMove) {
Optional<TInt> y = TInt(11);
- TInt x = std::move(y).value();
+ EXPECT_THROW(std::move(y).as_or_throw<TFloat>(), Error);
+ // A failed strict cast leaves the value available for a subsequent move.
+ // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ TInt x = std::move(y).as_or_throw<TInt>();
// NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
EXPECT_TRUE(!y.has_value());
EXPECT_EQ(x->value, 11);
+ EXPECT_EQ(x.use_count(), 1);
Optional<TInt> opt_tint = TInt(21);
EXPECT_TRUE(opt_tint.has_value());
@@ -253,10 +257,15 @@ TEST(Optional, String) {
opt_str = "hello";
EXPECT_TRUE(opt_str.has_value());
+ EXPECT_THROW(std::move(opt_str).as_or_throw<Bytes>(), Error);
+ // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
EXPECT_EQ(opt_str.value(), "hello");
EXPECT_TRUE(opt_str == "hello");
EXPECT_TRUE(opt_str == String("hello"));
EXPECT_TRUE(opt_str != std::nullopt);
+ EXPECT_EQ(std::move(opt_str).as_or_throw<String>(), "hello");
+ // NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
+ EXPECT_FALSE(opt_str.has_value());
static_assert(sizeof(Optional<String>) == sizeof(Any));
}