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>>);