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 7fb9aecf [EXTRA] Enable typed mutation result conversions and 
subsumption (#777)
7fb9aecf is described below

commit 7fb9aecf7780f815fd1d38b8d801d4020dd579e5
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Sep 11 18:10:10 2026 -0400

    [EXTRA] Enable typed mutation result conversions and subsumption (#777)
    
    Typed structural mutation helpers need to convert compatible
    `UnchangedOr` results and propagate child errors across different
    success types. The assignment macro also repeats checks for already
    compatible typed results.
    
    Add implicit `UnchangedOr` conversions and storage-subsumption rules for
    `UnchangedOr` and `Expected`. Initialize subsumed conversions directly
    from their stored carrier, avoiding an intermediate assignment. Use
    subsumption to omit redundant assignment checks while retaining checks
    for erased or narrower sources, and propagate child errors through the
    existing error-only return helper. Correct the borrowed `AnyView`
    adapter for wrapper values.
---
 include/tvm/ffi/expected.h                | 30 ++++++++-----
 include/tvm/ffi/extra/structural_mutate.h | 75 ++++++++++++++++++++++---------
 tests/cpp/extra/test_structural_mutate.cc | 42 +++++++++++++++++
 3 files changed, 115 insertions(+), 32 deletions(-)

diff --git a/include/tvm/ffi/expected.h b/include/tvm/ffi/expected.h
index fb3f6ab7..4ba7d292 100644
--- a/include/tvm/ffi/expected.h
+++ b/include/tvm/ffi/expected.h
@@ -76,6 +76,12 @@ Unexpected(E) -> Unexpected<E>;
 template <typename T>
 class Expected;
 
+/// \cond Doxygen_Suppress
+/*! \brief Whether an Expected success value can reuse another success value's 
storage. */
+template <typename T, typename U>
+inline constexpr bool type_subsumes_v<Expected<T>, Expected<U>> = 
type_subsumes_v<T, U>;
+/// \endcond
+
 namespace details {
 
 struct ExpectedUnsafe;
@@ -178,17 +184,19 @@ class Expected {
             typename = std::enable_if_t<!std::is_void_v<U> &&
                                         (type_subsumes_v<T, U> || 
std::is_convertible_v<U, T>)>>
   // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
-  TVM_FFI_INLINE Expected(Expected<U> other) {
-    if constexpr (type_subsumes_v<T, U>) {
-      // data_ holds a T or an Error. Subsumption proves the source 
representation already
-      // satisfies that invariant, so the Any moves without inspecting its 
state. Do not make
-      // this unconditional: value() reads back through 
MoveFromAnyAfterCheck<T>, whose check is
-      // the success/error state, not the type.
-      data_ = std::move(other.data_);
-    } else {
-      data_ = other.is_err() ? Any(std::move(other).error()) : 
Any(T(std::move(other).value()));
-    }
-  }
+  TVM_FFI_INLINE Expected(Expected<U> other)
+      : data_([&other]() {
+          if constexpr (type_subsumes_v<T, U>) {
+            // data_ holds a T or an Error. Subsumption proves the source 
representation already
+            // satisfies that invariant, so adopt the raw storage without 
inspecting its state.
+            // Do not make this unconditional: value() checks the 
success/error state, not the type.
+            return details::AnyUnsafe::MoveTVMFFIAnyRawToAny(
+                
details::AnyUnsafe::MoveAnyToTVMFFIAny(std::move(other.data_)));
+          } else {
+            return other.is_err() ? Any(std::move(other).error())
+                                  : Any(T(std::move(other).value()));
+          }
+        }()) {}
 
   /*!
    * \brief Implicit constructor from an error.
diff --git a/include/tvm/ffi/extra/structural_mutate.h 
b/include/tvm/ffi/extra/structural_mutate.h
index ce4b48d9..127e078f 100644
--- a/include/tvm/ffi/extra/structural_mutate.h
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -52,6 +52,13 @@ namespace ffi {
 class StructuralMutatorObj;
 template <typename T>
 class UnchangedOr;
+
+/// \cond Doxygen_Suppress
+/*! \brief Whether an UnchangedOr replacement can reuse another replacement's 
storage. */
+template <typename T, typename U>
+inline constexpr bool type_subsumes_v<UnchangedOr<T>, UnchangedOr<U>> = 
type_subsumes_v<T, U>;
+/// \endcond
+
 template <typename Parent, WalkOrder order, typename... Callbacks>
 class StructuralMapEngine;
 template <typename Parent, WalkOrder order>
@@ -213,6 +220,26 @@ class UnchangedOr {
   // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
   TVM_FFI_INLINE UnchangedOr(T value) : data_(Any(std::move(value))) {}
 
+  /*!
+   * \brief Implicit converting constructor from another replacement type.
+   * \tparam U Source replacement type whose storage is subsumed by or 
implicitly convertible to T.
+   * \param other The result to convert, copied from an lvalue or moved from 
an rvalue.
+   */
+  template <typename U,
+            typename = std::enable_if_t<type_subsumes_v<T, U> || 
std::is_convertible_v<U, T>>>
+  // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+  TVM_FFI_INLINE UnchangedOr(UnchangedOr<U> other)
+      : data_([&other]() {
+          if constexpr (type_subsumes_v<T, U>) {
+            // Reuse materialized storage, including the unchanged marker.
+            return details::AnyUnsafe::MoveTVMFFIAnyRawToAny(
+                
details::AnyUnsafe::MoveAnyToTVMFFIAny(std::move(other.data_)));
+          } else {
+            return other.IsUnchanged() ? std::move(other.data_)
+                                       : 
Any(T(std::move(other).ValueUnchecked()));
+          }
+        }()) {}
+
   /// \cond Doxygen_Suppress
   TVM_FFI_INLINE UnchangedOr(const UnchangedOr&) = default;
   TVM_FFI_INLINE UnchangedOr(UnchangedOr&&) noexcept = default;
@@ -282,6 +309,8 @@ class UnchangedOr {
   }
 
  private:
+  template <typename>
+  friend class UnchangedOr;
   friend struct details::UnchangedOrUnsafe;
   template <typename, typename>
   friend struct TypeTraits;
@@ -850,28 +879,31 @@ TVM_FFI_INLINE static Expected<Any> 
MutateReflectedFieldsExpected(StructuralMuta
 
 namespace details {
 /// \cond Doxygen_Suppress
-// Return from the current raw or same-T Expected mutation function if Result 
is an Error.
+// Return an error from the current raw or Expected mutation function.
 // The rvalue-only helper lets the enclosing return type select the 
representation.
-#define TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result)                            
    \
-  do {                                                                         
    \
-    auto&& tvm_ffi_res_ = (Result);                                            
    \
-    if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.is_err())) {                        
    \
-      return 
::tvm::ffi::details::ExpectedReturnHelper(::std::move(tvm_ffi_res_)); \
-    }                                                                          
    \
+#define TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result)                   \
+  do {                                                                \
+    auto&& tvm_ffi_res_ = (Result);                                   \
+    if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.is_err())) {               \
+      return ::tvm::ffi::details::UnexpectedReturnHelper(             \
+          ::tvm::ffi::Unexpected(::std::move(tvm_ffi_res_).error())); \
+    }                                                                 \
   } while (0)
 
 /// \endcond
 
 /// \cond Doxygen_Suppress
-#define TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN_IMPL_(Result, Type, Name, 
ResultExpr)    \
-  auto Result = (ResultExpr); /* NOLINT(bugprone-macro-parentheses) */         
    \
-  TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result);                                 
    \
-  if 
(TVM_FFI_PREDICT_FALSE(!::tvm::ffi::details::AnyUnsafe::CheckAnyStrict<Type>( \
-          ::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))) {            
    \
-    return ::tvm::ffi::details::SMutateDeclaredTypeError();                    
    \
-  }                                                                            
    \
-  Type Name = /* NOLINT(bugprone-macro-parentheses) */                         
    \
-      ::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(             
    \
+#define TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN_IMPL_(Result, Type, Name, 
ResultExpr)               \
+  auto Result = (ResultExpr); /* NOLINT(bugprone-macro-parentheses) */         
               \
+  TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result);                                 
               \
+  if constexpr (!::tvm::ffi::type_subsumes_v<::tvm::ffi::Expected<Type>, 
decltype(Result)>) { \
+    if 
(TVM_FFI_PREDICT_FALSE(!::tvm::ffi::details::AnyUnsafe::CheckAnyStrict<Type>(   
       \
+            ::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))) {          
               \
+      return ::tvm::ffi::details::SMutateDeclaredTypeError();                  
               \
+    }                                                                          
               \
+  }                                                                            
               \
+  Type Name = /* NOLINT(bugprone-macro-parentheses) */                         
               \
+      ::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(             
               \
           ::std::move(::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))
 /// \endcond
 
@@ -880,10 +912,11 @@ namespace details {
  *
  * ``Type`` must be concrete; use a type alias when it contains a top-level 
comma. A type mismatch
  * returns ``TypeError`` through the surrounding raw or ``Expected`` function 
without throwing,
- * reported with a fixed string so a correct hook pays only one 
predicted-not-taken branch per
- * field. Its early returns work from either a raw ``TVMFFIAny`` hook or an
- * ``Expected<UnchangedOr<Any>>`` helper. This macro declares ``Name`` into 
the enclosing scope and
- * must be used in a braced block, never as an unbraced control-flow body.
+ * reported with a fixed string. The check is omitted when the declared type 
subsumes the result's
+ * success type. Its early returns work from either a raw ``TVMFFIAny`` hook 
or an
+ * ``Expected<T>`` helper, including one with a different success type. This 
macro declares ``Name``
+ * into the enclosing scope and must be used in a braced block, never as an 
unbraced control-flow
+ * body.
  *
  * Example:
  * \code{.cpp}
@@ -1876,7 +1909,7 @@ inline constexpr bool 
use_default_type_traits_v<UnchangedOr<T>> = false;
 template <typename T>
 struct TypeTraits<UnchangedOr<T>> : public TypeTraitsBase {
   TVM_FFI_INLINE static void CopyToAnyView(const UnchangedOr<T>& src, 
TVMFFIAny* result) {
-    *result = src.data_.CopyToTVMFFIAny();
+    *result = AnyView(src.data_).CopyToTVMFFIAny();
   }
 
   TVM_FFI_INLINE static void MoveToAny(UnchangedOr<T> src, TVMFFIAny* result) {
diff --git a/tests/cpp/extra/test_structural_mutate.cc 
b/tests/cpp/extra/test_structural_mutate.cc
index dd254c80..912efb28 100644
--- a/tests/cpp/extra/test_structural_mutate.cc
+++ b/tests/cpp/extra/test_structural_mutate.cc
@@ -54,6 +54,48 @@ static_assert(
 
 Expected<UnchangedOr<String>> ReturnTypedUnchangedExpected() noexcept { return 
Unchanged(); }
 
+TEST(UnchangedOr, ConversionsAndAssignmentMacro) {
+  static_assert(!std::is_convertible_v<UnchangedOr<Any>, UnchangedOr<int>>);
+  static_assert(type_subsumes_v<Expected<UnchangedOr<TNumber>>, 
Expected<UnchangedOr<TInt>>>);
+  static_assert(!type_subsumes_v<Expected<UnchangedOr<TInt>>, 
Expected<UnchangedOr<TNumber>>>);
+  static_assert(type_subsumes_v<Expected<Any>, Expected<void>>);
+  static_assert(!type_subsumes_v<Expected<void>, Expected<Any>>);
+
+  TInt original(42);
+  UnchangedOr<TInt> source = original;
+  UnchangedOr<Any> copied = std::as_const(source);
+  UnchangedOr<TNumber> moved = std::move(source);
+  EXPECT_EQ(original.use_count(), 3);
+  EXPECT_TRUE(std::move(copied).ValueUnchecked().same_as(original));
+  EXPECT_TRUE(std::move(moved).ValueUnchecked().same_as(original));
+  EXPECT_EQ(original.use_count(), 1);
+
+  UnchangedOr<double> numeric = UnchangedOr<int>(42);
+  EXPECT_EQ(AnyView(numeric).type_index(), TypeIndex::kTVMFFIFloat);
+  EXPECT_DOUBLE_EQ(std::move(numeric).ValueUnchecked(), 42.0);
+  UnchangedOr<double> unchanged = UnchangedOr<int>(Unchanged());
+  EXPECT_TRUE(unchanged.IsUnchanged());
+
+  // One consumer exercises exact, widening, erased and narrowing source types.
+  auto consume = [](auto input, const auto& original) -> Expected<bool> {
+    
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<std::decay_t<decltype(original)>>,
 value,
+                                      std::move(input));
+    return value.UnchangedOrSameAs(original);
+  };
+  Expected<UnchangedOr<TInt>> typed = UnchangedOr<TInt>(original);
+  EXPECT_TRUE(consume(typed, original).value());
+  EXPECT_TRUE(consume(typed, TNumber(original)).value());
+  EXPECT_TRUE(consume(Expected<UnchangedOr<Any>>(typed), original).value());
+  EXPECT_EQ(consume(Expected<UnchangedOr<Any>>(Any(42)), 
original).error().kind(), "TypeError");
+  
EXPECT_EQ(consume(Expected<UnchangedOr<TNumber>>(UnchangedOr<TNumber>(TFloat(1.0))),
 original)
+                .error()
+                .kind(),
+            "TypeError");
+  Error error("ValueError", "child failure", "");
+  Expected<UnchangedOr<Any>> failed = Expected<UnchangedOr<TInt>>(error);
+  EXPECT_TRUE(consume(std::move(failed), original).error().same_as(error));
+}
+
 TEST(UnchangedOr, ErrorRoundTrip) {
   static_assert(std::is_copy_constructible_v<UnchangedOr<String>>);
 

Reply via email to