This is an automated email from the ASF dual-hosted git repository.
cyx-6 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 bdd4989f [REFACTOR][EXTRA] Optimize StructuralMap (#733)
bdd4989f is described below
commit bdd4989f9c9ad4a5e8fd61db01c185577570dc51
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Sep 3 04:58:17 2026 -0400
[REFACTOR][EXTRA] Optimize StructuralMap (#733)
This PR refactor optimizes `StructuralMap`.
- Move successful `Expected` payloads through structural-mutation hooks
without redundant reference-count traffic, while returning conversion
failures through `Expected`/`Unexpected`.
- Require structural visit and mutation early returns to carry node
context, and document the public mutation-unwrapping macro.
- Match callbacks before recursive descent so unmatched nodes skip
identity-remap lookup, error-frame setup, and callback-chain work.
- Keep remap lookup and callback invocation inside each matched
continuation, preserving callback ordering and post-order target
revalidation.
- Add registered-hook and error-context coverage for successful,
nullable, failing, and wrong-type mutation results.
---
include/tvm/ffi/expected.h | 44 ++++-
include/tvm/ffi/extra/structural_mutate.h | 293 ++++++++++++++++++++----------
include/tvm/ffi/extra/structural_visit.h | 53 +++---
src/ffi/extra/structural_mutate.cc | 80 ++++----
src/ffi/extra/structural_visit.cc | 12 +-
tests/cpp/extra/test_structural_mutate.cc | 50 +++++
tests/cpp/testing_object.h | 65 ++++++-
7 files changed, 411 insertions(+), 186 deletions(-)
diff --git a/include/tvm/ffi/expected.h b/include/tvm/ffi/expected.h
index 4d31fdce..bd7417fa 100644
--- a/include/tvm/ffi/expected.h
+++ b/include/tvm/ffi/expected.h
@@ -348,6 +348,29 @@ class Expected<void> {
namespace details {
+// Helper for TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN.
+class AssignOrReturnHelper {
+ public:
+ TVM_FFI_INLINE explicit AssignOrReturnHelper(Any&& data) :
data_(std::move(data)) {}
+
+ template <typename T>
+ TVM_FFI_INLINE Expected<T> TryMove() && {
+ if constexpr (!std::is_same_v<T, Any>) {
+ const TVMFFIAny* data = AnyUnsafe::TVMFFIAnyPtrFromAny(data_);
+ if (TVM_FFI_PREDICT_FALSE(!TypeTraits<T>::CheckAnyStrict(data))) {
+ return Unexpected(Error("TypeError",
+ "Cannot treat type `" +
TypeTraits<T>::GetMismatchTypeInfo(data) +
+ "` as type `" + TypeTraits<T>::TypeStr() +
"`",
+ ""));
+ }
+ }
+ return AnyUnsafe::MoveFromAnyAfterCheck<T>(std::move(data_));
+ }
+
+ private:
+ Any data_;
+};
+
/*!
* \brief Unsafe raw-storage helpers for Expected.
*
@@ -380,10 +403,25 @@ struct ExpectedUnsafe {
}
/*!
- * \brief Return the underlying Any storage.
+ * \brief Return the underlying Any storage as an xvalue; by-value
initialization moves from it.
+ * \tparam T The Expected success type.
+ * \param result The Expected value whose storage will be exposed as an
xvalue.
+ * \return An xvalue reference to the underlying Any storage.
+ *
+ * \note The const overload returns ``const Any&``, which remains const
under ``std::move`` and
+ * therefore selects the copy constructor. The assign-or-return macro
selects this overload
+ * from a non-const result, so its outer ``std::move`` only makes the
move intent explicit.
+ */
+ template <typename T>
+ TVM_FFI_INLINE static Any&& GetData(Expected<T>& result) noexcept {
+ return std::move(result.data_);
+ }
+
+ /*!
+ * \brief Return a const reference to the underlying Any storage.
* \tparam T The Expected success type.
- * \param result The Expected value to inspect.
- * \return Const reference to the raw Any storage.
+ * \param result The Expected value whose storage will be viewed.
+ * \return A const reference to the underlying Any storage.
*/
template <typename T>
TVM_FFI_INLINE static const Any& GetData(const Expected<T>& result) noexcept
{
diff --git a/include/tvm/ffi/extra/structural_mutate.h
b/include/tvm/ffi/extra/structural_mutate.h
index c7916b79..56eff51c 100644
--- a/include/tvm/ffi/extra/structural_mutate.h
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -532,27 +532,66 @@ namespace details {
/// \cond Doxygen_Suppress
// Return from the current mutation function if Result is an Error.
-// Append Node to the mutate error context before returning.
-#define TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(Result, Node)
\
+// Append Node to the mutate error context before returning. Node is required:
dropping it
+// silently degrades every error message produced below this frame.
+// A raw pointer Node must be non-null; pass nullable nodes as ObjectRef or
Any so None is skipped.
+#define TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result, Node)
\
do {
\
auto&& tvm_ffi_res_ = (Result);
\
if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.type_index() ==
::tvm::ffi::TypeIndex::kTVMFFIError)) { \
- if ((Node).type_index() >=
::tvm::ffi::TypeIndex::kTVMFFIStaticObjectBegin) { \
+ auto&& tvm_ffi_mutate_node_owner_ = (Node);
\
+ ::tvm::ffi::AnyView tvm_ffi_mutate_node_ = tvm_ffi_mutate_node_owner_;
\
+ if (tvm_ffi_mutate_node_.type_index() >=
::tvm::ffi::TypeIndex::kTVMFFIStaticObjectBegin) { \
::tvm::ffi::Error tvm_ffi_mutate_err_ = tvm_ffi_res_.error();
\
- ::tvm::ffi::details::UpdateVisitErrorContext(tvm_ffi_mutate_err_,
\
-
(Node).cast<::tvm::ffi::ObjectRef>()); \
+ ::tvm::ffi::details::UpdateVisitErrorContext(
\
+ tvm_ffi_mutate_err_,
tvm_ffi_mutate_node_.cast<::tvm::ffi::ObjectRef>()); \
}
\
return ::std::move(tvm_ffi_res_);
\
}
\
} while (0)
+
+#define TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN_IMPL_(Result, Converted, Type, Name,
ResultExpr, Node) \
+ auto Result = (ResultExpr); /* NOLINT(bugprone-macro-parentheses) */
\
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result, Node);
\
+ auto Converted = /* NOLINT(bugprone-macro-parentheses) */
\
+ ::tvm::ffi::details::AssignOrReturnHelper(
\
+ ::std::move(::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))
\
+ .template TryMove<Type>();
\
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Converted, Node);
\
+ Type Name = ::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(
\
+ ::std::move(::tvm::ffi::details::ExpectedUnsafe::GetData(Converted)))
/// \endcond
+/*!
+ * \brief Unwrap a successful mutation result into a newly declared value or
return its error.
+ *
+ * ``Type`` must be concrete; use a type alias when it contains a top-level
comma. A type mismatch
+ * returns ``Unexpected(TypeError)`` through the surrounding ``Expected``
function without
+ * throwing. This macro declares ``Name`` into the enclosing scope and must be
used in a braced
+ * block, never as an unbraced control-flow body. A raw pointer node must be
non-null; pass nullable
+ * nodes as ``ObjectRef`` or ``Any`` so ``None`` is skipped when constructing
error context.
+ *
+ * Example:
+ * \code{.cpp}
+ * TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+ * ObjectRef, child, mutator->MutateExpected(self->child), self);
+ * \endcode
+ *
+ * \param Type The concrete type of the successful value.
+ * \param Name The name of the value declared in the enclosing scope.
+ * \param ResultExpr An expression producing the ``Expected`` value to unwrap.
+ * \param Node The current mutation node appended to the error context on
failure.
+ */
+#define TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Type, Name, ResultExpr, Node) \
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN_IMPL_( \
+ TVM_FFI_STR_CONCAT(tvm_ffi_mutate_result_, __COUNTER__), \
+ TVM_FFI_STR_CONCAT(tvm_ffi_mutate_converted_, __COUNTER__), Type, Name,
ResultExpr, Node)
+
/*!
* \brief Structural mutator that invokes typed callbacks during recursive
mapping.
*
* \tparam order Callback placement relative to child mapping.
- * \tparam Dispatch Callback dispatcher with signature
- * ``Expected<Any>(AnyView, TVMFFIDefRegionKind)``.
+ * \tparam Dispatch Callback dispatcher.
* \sa StructuralMapCallbackChain
*/
template <WalkOrder order, typename Dispatch>
@@ -679,38 +718,60 @@ class StructuralMapMutatorObj : public
StructuralMutatorObj {
* \return The mutated value or an Error.
*/
Expected<Any> MaybeInplaceMutateImpl(AnyView value) noexcept {
- return MutateWithIdentityRemapExpected(value, [&]() -> Expected<Any> {
- if constexpr (order == WalkOrder::kPreOrder) {
- Expected<Any> callback_result = dispatch_(value, def_region_kind());
-
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(callback_result, value);
- // A pre-order result can be mutated in place if unchanged or uniquely
owned.
- const Any& mapped_value = ExpectedUnsafe::GetData(callback_result);
- const TVMFFIAny* mapped_data =
AnyUnsafe::TVMFFIAnyPtrFromAny(mapped_value);
- const TVMFFIAny input_data = value.CopyToTVMFFIAny();
- if (mapped_data->type_index != input_data.type_index ||
- mapped_data->zero_padding != input_data.zero_padding ||
- mapped_data->v_int64 != input_data.v_int64) {
- const Object* mapped_obj = mapped_value.as<Object>();
- bool can_mutate_mapped_value_inplace = mapped_obj != nullptr &&
mapped_obj->unique();
- Expected<Any> result = can_mutate_mapped_value_inplace
- ?
DefaultMaybeInplaceMutateExpected(mapped_value)
- : DefaultMutateExpected(mapped_value);
- TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result,
mapped_value);
- return result;
- }
- Expected<Any> result = DefaultMaybeInplaceMutateExpected(value);
- TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result, value);
- return result;
- } else {
- Expected<Any> result = DefaultMaybeInplaceMutateExpected(value);
- TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result, value);
-
- const Any& mapped_value = ExpectedUnsafe::GetData(result);
- Expected<Any> callback_result = dispatch_(mapped_value,
def_region_kind());
-
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(callback_result,
mapped_value);
- return callback_result;
- }
- });
+ // The link match is tested before any descent, and the identity remap
lives
+ // inside the match continuation, so the var-type detection only runs when
a
+ // callback signature actually matches. Unmatched nodes skip both it and
the
+ // callback chain.
+ //
+ // The ordering is load-bearing: DefaultMutateExpected's reflected fallback
+ // writes its own remap entry for this node, so testing the match after
descent
+ // would let the remap read that entry, treat the node as already handled,
and
+ // never run the callback.
+ if constexpr (order == WalkOrder::kPreOrder) {
+ return dispatch_(
+ value, def_region_kind(),
+ [&](auto&& invoke_callback) -> Expected<Any> {
+ return MutateWithIdentityRemapExpected(value, [&]() ->
Expected<Any> {
+ Expected<Any> callback_result = invoke_callback(value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(callback_result, value);
+ // A pre-order result can be mutated in place if unchanged or
uniquely owned.
+ const Any& mapped_value =
ExpectedUnsafe::GetData(callback_result);
+ const TVMFFIAny* mapped_data =
AnyUnsafe::TVMFFIAnyPtrFromAny(mapped_value);
+ const TVMFFIAny input_data = value.CopyToTVMFFIAny();
+ if (mapped_data->type_index != input_data.type_index ||
+ mapped_data->zero_padding != input_data.zero_padding ||
+ mapped_data->v_int64 != input_data.v_int64) {
+ const Object* mapped_obj = mapped_value.as<Object>();
+ bool can_mutate_mapped_value_inplace =
+ mapped_obj != nullptr && mapped_obj->unique();
+ Expected<Any> result = can_mutate_mapped_value_inplace
+ ?
DefaultMaybeInplaceMutateExpected(mapped_value)
+ :
DefaultMutateExpected(mapped_value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(result, mapped_value);
+ return result;
+ }
+ Expected<Any> result = DefaultMaybeInplaceMutateExpected(value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(result, value);
+ return result;
+ });
+ },
+ [&]() -> Expected<Any> { return
DefaultMaybeInplaceMutateExpected(value); });
+ } else {
+ return dispatch_(
+ value, def_region_kind(),
+ [&](auto&& invoke_callback) -> Expected<Any> {
+ return MutateWithIdentityRemapExpected(value, [&]() ->
Expected<Any> {
+ Expected<Any> result = DefaultMaybeInplaceMutateExpected(value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(result, value);
+
+ const Any& mapped_value = ExpectedUnsafe::GetData(result);
+ Expected<Any> callback_result = invoke_callback(mapped_value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(callback_result,
mapped_value);
+ return callback_result;
+ });
+ },
+ [&]() -> Expected<Any> { return
DefaultMaybeInplaceMutateExpected(value); });
+ }
}
/*!
@@ -719,25 +780,46 @@ class StructuralMapMutatorObj : public
StructuralMutatorObj {
* \return The mutated value or an Error.
*/
Expected<Any> MutateImpl(AnyView value) noexcept {
- return MutateWithIdentityRemapExpected(value, [&]() -> Expected<Any> {
- if constexpr (order == WalkOrder::kPreOrder) {
- Expected<Any> callback_result = dispatch_(value, def_region_kind());
-
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(callback_result, value);
-
- const Any& mapped_value = ExpectedUnsafe::GetData(callback_result);
- Expected<Any> result = DefaultMutateExpected(mapped_value);
- TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result,
mapped_value);
- return result;
- } else {
- Expected<Any> result = DefaultMutateExpected(value);
- TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result, value);
-
- const Any& mapped_value = ExpectedUnsafe::GetData(result);
- Expected<Any> callback_result = dispatch_(mapped_value,
def_region_kind());
-
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(callback_result,
mapped_value);
- return callback_result;
- }
- });
+ // The link match is tested before any descent, and the identity remap
lives
+ // inside the match continuation, so the var-type detection only runs when
a
+ // callback signature actually matches. Unmatched nodes skip both it and
the
+ // callback chain.
+ //
+ // The ordering is load-bearing: DefaultMutateExpected's reflected fallback
+ // writes its own remap entry for this node, so testing the match after
descent
+ // would let the remap read that entry, treat the node as already handled,
and
+ // never run the callback.
+ if constexpr (order == WalkOrder::kPreOrder) {
+ return dispatch_(
+ value, def_region_kind(),
+ [&](auto&& invoke_callback) -> Expected<Any> {
+ return MutateWithIdentityRemapExpected(value, [&]() ->
Expected<Any> {
+ Expected<Any> callback_result = invoke_callback(value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(callback_result, value);
+
+ const Any& mapped_value =
ExpectedUnsafe::GetData(callback_result);
+ Expected<Any> result = DefaultMutateExpected(mapped_value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(result, mapped_value);
+ return result;
+ });
+ },
+ [&]() -> Expected<Any> { return DefaultMutateExpected(value); });
+ } else {
+ return dispatch_(
+ value, def_region_kind(),
+ [&](auto&& invoke_callback) -> Expected<Any> {
+ return MutateWithIdentityRemapExpected(value, [&]() ->
Expected<Any> {
+ Expected<Any> result = DefaultMutateExpected(value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(result, value);
+
+ const Any& mapped_value = ExpectedUnsafe::GetData(result);
+ Expected<Any> callback_result = invoke_callback(mapped_value);
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(callback_result,
mapped_value);
+ return callback_result;
+ });
+ },
+ [&]() -> Expected<Any> { return DefaultMutateExpected(value); });
+ }
}
/*! \brief Composed callback dispatcher owned by this mutator. */
@@ -747,9 +829,13 @@ class StructuralMapMutatorObj : public
StructuralMutatorObj {
Map<ObjectRef, Any> var_remap_;
};
-/*!
- * \brief Build a callback dispatcher from a typed callback chain.
- */
+// Build a callback dispatcher from a typed callback chain. The dispatcher
answers "which link
+// matches this node" as control flow rather than as a value. It takes the
node, the active
+// def-region kind, an on-match continuation, and an on-no-match continuation,
and returns whichever
+// one applies. The on-match continuation receives a callable
`invoke_callback(AnyView target)` that
+// converts target to the matched link's argument type and invokes it, so the
caller decides what
+// wraps the invocation. A node that matches no link therefore pays one type
test per link and
+// nothing else.
struct StructuralMapCallbackChain {
public:
/*!
@@ -761,37 +847,32 @@ struct StructuralMapCallbackChain {
template <typename... Callbacks>
static auto FromChain(Callbacks... callbacks) {
auto callback_tuple = std::make_tuple(std::move(callbacks)...);
- return [callbacks = std::move(callback_tuple)](
- AnyView value, TVMFFIDefRegionKind kind) mutable ->
Expected<Any> {
- try {
- std::optional<Expected<Any>> result;
- // Fold expression: each TryCallLink returns empty std::optional on
no-match
- // (falsy) or a result on match (truthy); || short-circuits on first
match.
- std::apply(
- [&](auto&... callback) { (... || (result = TryCallLink(callback,
value, kind))); },
- callbacks);
- if (result.has_value()) {
- return *std::move(result);
- }
- return Any(value);
- } catch (const Error& err) {
- return Unexpected(err);
+ return [callbacks = std::move(callback_tuple)](AnyView value,
TVMFFIDefRegionKind kind,
+ auto&& on_match,
+ auto&& on_no_match) mutable
-> Expected<Any> {
+ std::optional<Expected<Any>> result;
+ auto run_match = [&](auto&& invoke_callback) {
+
result.emplace(on_match(std::forward<decltype(invoke_callback)>(invoke_callback)));
+ };
+ // Selection is control flow: the first matching link hands its
invocation thunk to
+ // run_match, while a node matching no link goes directly to on_no_match.
+ bool matched = std::apply(
+ [&](auto&... callback) { return (... || TryCallLink(callback, value,
kind, run_match)); },
+ callbacks);
+ if (matched) {
+ return *std::move(result);
}
+ return on_no_match();
};
}
private:
- /*!
- * \brief Invoke \p callback when \p value matches its first argument.
- * \tparam Callback Callback type whose first argument selects the value
type.
- * \param callback The callback under test.
- * \param value The value to match and pass to the callback.
- * \param kind The active def-region kind.
- * \return The callback result on match, or an empty ``std::optional``
otherwise.
- */
- template <typename Callback>
- TVM_FFI_INLINE static std::optional<Expected<Any>> TryCallLink(Callback&
callback, AnyView value,
-
TVMFFIDefRegionKind kind) {
+ // Hand the matched link's invocation thunk to on_match, or return false on
no match. The
+ // target conversion stays in the thunk so post-order can select on the
input and invoke on the
+ // node whose children have already been mapped.
+ template <typename Callback, typename OnMatch>
+ TVM_FFI_INLINE static bool TryCallLink(Callback& callback, AnyView value,
+ TVMFFIDefRegionKind kind, OnMatch&&
on_match) {
using FuncInfo = FunctionInfo<std::decay_t<Callback>>;
static_assert(FuncInfo::num_args == 1 || FuncInfo::num_args == 2,
"StructuralMap callbacks must take one argument (value) or
two arguments "
@@ -799,15 +880,27 @@ struct StructuralMapCallbackChain {
using FirstArg = std::tuple_element_t<0, typename FuncInfo::ArgType>;
using TSub = std::remove_cv_t<std::remove_reference_t<FirstArg>>;
if constexpr (std::is_same_v<TSub, AnyView>) {
- return InvokeCallbackLink(callback, value, kind);
+ on_match([&](AnyView target) -> Expected<Any> {
+ return InvokeCallbackLink(callback, target, kind);
+ });
+ return true;
} else if constexpr (std::is_same_v<TSub, Any>) {
- return InvokeCallbackLink(callback, Any(value), kind);
- } else {
- if (auto opt = value.template as<TSub>()) {
- return InvokeCallbackLink(callback, *std::move(opt), kind);
- }
+ on_match([&](AnyView target) -> Expected<Any> {
+ return InvokeCallbackLink(callback, Any(target), kind);
+ });
+ return true;
+ } else if (auto opt = value.template as<TSub>()) {
+ on_match([&](AnyView target) -> Expected<Any> {
+ std::optional<TSub> converted = target.template as<TSub>();
+ if (!converted.has_value()) {
+ // A custom post-order hook may change the node type after the input
selected this link.
+ return Any(target);
+ }
+ return InvokeCallbackLink(callback, *std::move(converted), kind);
+ });
+ return true;
}
- return std::nullopt;
+ return false;
}
/*!
@@ -826,10 +919,14 @@ struct StructuralMapCallbackChain {
static_assert(std::is_convertible_v<typename FuncInfo::RetType,
Expected<Any>>,
"StructuralMap callbacks must return a replacement value,
Error, Unexpected, "
"or Expected<U> implicitly convertible to Expected<Any>");
- if constexpr (FuncInfo::num_args == 1) {
- return callback(std::forward<Value>(value));
- } else {
- return callback(std::forward<Value>(value), kind);
+ try {
+ if constexpr (FuncInfo::num_args == 1) {
+ return callback(std::forward<Value>(value));
+ } else {
+ return callback(std::forward<Value>(value), kind);
+ }
+ } catch (const Error& err) {
+ return Unexpected(err);
}
}
};
diff --git a/include/tvm/ffi/extra/structural_visit.h
b/include/tvm/ffi/extra/structural_visit.h
index 463843b6..89f53fac 100644
--- a/include/tvm/ffi/extra/structural_visit.h
+++ b/include/tvm/ffi/extra/structural_visit.h
@@ -448,32 +448,26 @@ namespace details {
/// \cond Doxygen_Suppress
// Return from the current ABI visit function if Result stops traversal.
// Result must evaluate to Expected whose raw storage can be moved to
TVMFFIAny.
-#define TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Result)
\
- do {
\
- auto&& tvm_ffi_res_ = (Result);
\
- if (TVM_FFI_PREDICT_FALSE(
\
-
::tvm::ffi::details::StructuralVisitNeedEarlyReturn(tvm_ffi_res_))) {
\
- return
::tvm::ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(tvm_ffi_res_)); \
- }
\
- } while (0)
-
-// Return from the current ABI visit function if Result stops traversal.
-// If Result is an Error, append Node to the visit error context before
returning.
-#define TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(Result, Node)
\
- do {
\
- auto&& tvm_ffi_res_ = (Result);
\
- if (TVM_FFI_PREDICT_FALSE(
\
-
::tvm::ffi::details::StructuralVisitNeedEarlyReturn(tvm_ffi_res_))) {
\
- if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.type_index() ==
\
- ::tvm::ffi::TypeIndex::kTVMFFIError)) {
\
- if ((Node).type_index() >=
::tvm::ffi::TypeIndex::kTVMFFIStaticObjectBegin) { \
- ::tvm::ffi::Error tvm_ffi_visit_err_ = tvm_ffi_res_.error();
\
- ::tvm::ffi::details::UpdateVisitErrorContext(tvm_ffi_visit_err_,
\
-
(Node).cast<::tvm::ffi::ObjectRef>()); \
- }
\
- }
\
- return
::tvm::ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(tvm_ffi_res_));
\
- }
\
+// If Result is an Error, append Node to the visit error context before
returning. Node is
+// required: dropping it silently degrades every error message produced below
this frame.
+// A raw pointer Node must be non-null; pass nullable nodes as ObjectRef or
Any so None is skipped.
+#define TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Result, Node)
\
+ do {
\
+ auto&& tvm_ffi_res_ = (Result);
\
+ if (TVM_FFI_PREDICT_FALSE(
\
+
::tvm::ffi::details::StructuralVisitNeedEarlyReturn(tvm_ffi_res_))) {
\
+ if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.type_index() ==
\
+ ::tvm::ffi::TypeIndex::kTVMFFIError)) {
\
+ auto&& tvm_ffi_visit_node_owner_ = (Node);
\
+ ::tvm::ffi::AnyView tvm_ffi_visit_node_ = tvm_ffi_visit_node_owner_;
\
+ if (tvm_ffi_visit_node_.type_index() >=
::tvm::ffi::TypeIndex::kTVMFFIStaticObjectBegin) { \
+ ::tvm::ffi::Error tvm_ffi_visit_err_ = tvm_ffi_res_.error();
\
+ ::tvm::ffi::details::UpdateVisitErrorContext(
\
+ tvm_ffi_visit_err_,
tvm_ffi_visit_node_.cast<::tvm::ffi::ObjectRef>()); \
+ }
\
+ }
\
+ return
::tvm::ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(tvm_ffi_res_));
\
+ }
\
} while (0)
/// \endcond
@@ -529,7 +523,7 @@ class StructuralWalkVisitorObj : public
StructuralVisitorObj {
}
if constexpr (order == WalkOrder::kPreOrder) {
auto result = dispatch_(value, this->def_region_kind());
- TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(result, value);
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(result, value);
// Hoist the call out of TVM_FFI_UNSAFE_ASSUME: clang's -Wassume rejects
// arguments that contain a call expression (its potential side effects
// would be discarded), while [[maybe_unused]] keeps -Wunused-variable
@@ -543,11 +537,10 @@ class StructuralWalkVisitorObj : public
StructuralVisitorObj {
}
}
-
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(DefaultVisitExpected(value),
value);
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(DefaultVisitExpected(value), value);
if constexpr (order == WalkOrder::kPostOrder) {
- TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN_WITH_ERROR_CONTEXT(
- dispatch_(value, this->def_region_kind()), value);
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(dispatch_(value,
this->def_region_kind()), value);
}
return details::ExpectedUnsafe::MoveToTVMFFIAny(
diff --git a/src/ffi/extra/structural_mutate.cc
b/src/ffi/extra/structural_mutate.cc
index c240392c..b1b74c0f 100644
--- a/src/ffi/extra/structural_mutate.cc
+++ b/src/ffi/extra/structural_mutate.cc
@@ -50,15 +50,17 @@ namespace details {
Expected<Any> StructuralMapExpected(
AnyView root, const Array<Tuple<int32_t, Function>>& callbacks,
const Array<Tuple<int32_t, Function>>& callbacks_with_def_region_kind, int
order) noexcept {
- auto dispatch = [callbacks, callbacks_with_def_region_kind](
- AnyView x, TVMFFIDefRegionKind kind) -> Expected<Any> {
+ auto dispatch = [callbacks, callbacks_with_def_region_kind](AnyView x,
TVMFFIDefRegionKind kind,
+ auto&& on_match,
+ auto&&
on_no_match) -> Expected<Any> {
for (const auto& entry : callbacks) {
int32_t type_index = entry.template get<0>();
if (!RuntimeTypeIndexMatch(x.type_index(), type_index)) {
continue;
}
Function fn = entry.template get<1>();
- return fn.CallExpected<Any>(x);
+ return on_match(
+ [&](AnyView target) -> Expected<Any> { return
fn.CallExpected<Any>(target); });
}
for (const auto& entry : callbacks_with_def_region_kind) {
int32_t type_index = entry.template get<0>();
@@ -66,9 +68,10 @@ Expected<Any> StructuralMapExpected(
continue;
}
Function fn = entry.template get<1>();
- return fn.CallExpected<Any>(x, kind);
+ return on_match(
+ [&](AnyView target) -> Expected<Any> { return
fn.CallExpected<Any>(target, kind); });
}
- return Any(x);
+ return on_no_match();
};
if (order == static_cast<int>(WalkOrder::kPreOrder)) {
@@ -92,23 +95,19 @@ Expected<Any> StructuralMapExpected(
* \tparam SeqObj The underlying sequence object type.
* \param mutator The active structural mutator.
* \param value The borrowed sequence container.
- * \param source The sequence object stored in \p value.
+ * \param self The sequence object stored in \p value.
* \return The mutated sequence, or an Error.
*/
template <typename SeqObj>
Expected<Any> MutateSeqContainerExpected(StructuralMutatorObj* mutator,
AnyView value,
- const SeqObj* source) noexcept {
+ const SeqObj* self) noexcept {
try {
- int64_t size = static_cast<int64_t>(source->size());
+ int64_t size = static_cast<int64_t>(self->size());
ObjectPtr<SeqObj> output = nullptr;
for (int64_t i = 0; i < size; ++i) {
- const Any& item = source->at(i);
- Expected<Any> mapped_item = mutator->MutateExpected(item);
- if (TVM_FFI_PREDICT_FALSE(mapped_item.is_err())) {
- return Unexpected(std::move(mapped_item).error());
- }
- const Any& mapped_value = details::ExpectedUnsafe::GetData(mapped_item);
+ const Any& item = self->at(i);
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, mapped_value,
mutator->MutateExpected(item), self);
if (output == nullptr) {
if (item.same_as(mapped_value)) {
@@ -116,10 +115,10 @@ Expected<Any>
MutateSeqContainerExpected(StructuralMutatorObj* mutator, AnyView
}
output = SeqObj::CreateRepeated(size, Any());
for (int64_t j = 0; j < i; ++j) {
- output->SetItem(j, source->at(j));
+ output->SetItem(j, self->at(j));
}
}
- output->SetItem(i, mapped_value);
+ output->SetItem(i, std::move(mapped_value));
}
if (output == nullptr) {
@@ -137,23 +136,20 @@ Expected<Any>
MutateSeqContainerExpected(StructuralMutatorObj* mutator, AnyView
* \tparam SeqObj The underlying sequence object type.
* \param mutator The active structural mutator.
* \param value The borrowed sequence container, which must be safe to mutate
in place.
- * \param target The sequence object stored in \p value.
+ * \param self The sequence object stored in \p value.
* \return The mutated sequence, or an Error.
*/
template <typename SeqObj>
Expected<Any> MaybeInplaceMutateSeqContainerExpected(StructuralMutatorObj*
mutator, AnyView value,
- SeqObj* target) noexcept {
+ SeqObj* self) noexcept {
try {
- for (int64_t i = 0; i < static_cast<int64_t>(target->size()); ++i) {
- const Any& item = target->at(i);
- Expected<Any> mapped_item =
mutator->MaybeInplaceMutateIfUniqueExpected(item);
- if (TVM_FFI_PREDICT_FALSE(mapped_item.is_err())) {
- return Unexpected(std::move(mapped_item).error());
- }
- const Any& mapped_value = details::ExpectedUnsafe::GetData(mapped_item);
+ for (int64_t i = 0; i < static_cast<int64_t>(self->size()); ++i) {
+ const Any& item = self->at(i);
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, mapped_value,
+
mutator->MaybeInplaceMutateIfUniqueExpected(item), self);
if (!item.same_as(mapped_value)) {
- target->SetItem(i, mapped_value);
+ self->SetItem(i, std::move(mapped_value));
}
}
return Any(value);
@@ -168,38 +164,33 @@ Expected<Any>
MaybeInplaceMutateSeqContainerExpected(StructuralMutatorObj* mutat
* \tparam MapObjType The underlying map object type.
* \param mutator The active structural mutator.
* \param value The borrowed map container.
- * \param source The map object stored in \p value.
+ * \param self The map object stored in \p value.
* \return The mutated map, or an Error.
*/
template <typename MapObjType>
Expected<Any> MutateMapValuesExpected(StructuralMutatorObj* mutator, AnyView
value,
- const MapObjType* source) noexcept {
+ const MapObjType* self) noexcept {
try {
ObjectPtr<Object> output = nullptr;
MapBaseObj::iterator output_it;
size_t index = 0;
- for (auto source_it = source->begin(); source_it != source->end();
++source_it, ++index) {
+ for (auto source_it = self->begin(); source_it != self->end();
++source_it, ++index) {
const Any& old_value = source_it->second;
- Expected<Any> mapped_value = mutator->MutateExpected(old_value);
- if (TVM_FFI_PREDICT_FALSE(mapped_value.is_err())) {
- return Unexpected(std::move(mapped_value).error());
- }
-
- const Any& new_value = details::ExpectedUnsafe::GetData(mapped_value);
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, new_value,
mutator->MutateExpected(old_value), self);
bool changed = !old_value.same_as(new_value);
if (output == nullptr) {
if (!changed) {
continue;
}
- output = MapObjType::ShallowCopy(source);
+ output = MapObjType::ShallowCopy(self);
output_it = static_cast<MapBaseObj*>(output.get())->begin();
for (size_t i = 0; i < index; ++i) {
++output_it;
}
}
if (changed) {
- output_it->second = new_value;
+ output_it->second = std::move(new_value);
}
++output_it;
}
@@ -219,23 +210,20 @@ Expected<Any>
MutateMapValuesExpected(StructuralMutatorObj* mutator, AnyView val
* \tparam MapObjType The underlying map object type.
* \param mutator The active structural mutator.
* \param value The borrowed map container, which must be safe to mutate in
place.
- * \param target The map object stored in \p value.
+ * \param self The map object stored in \p value.
* \return The mutated map, or an Error.
*/
template <typename MapObjType>
Expected<Any> MaybeInplaceMutateMapValuesExpected(StructuralMutatorObj*
mutator, AnyView value,
- MapObjType* target) noexcept
{
+ MapObjType* self) noexcept {
try {
- for (auto it = target->begin(); it != target->end(); ++it) {
+ for (auto it = self->begin(); it != self->end(); ++it) {
const Any& old_value = it->second;
- Expected<Any> mapped_value =
mutator->MaybeInplaceMutateIfUniqueExpected(old_value);
- if (TVM_FFI_PREDICT_FALSE(mapped_value.is_err())) {
- return Unexpected(std::move(mapped_value).error());
- }
- const Any& new_value = details::ExpectedUnsafe::GetData(mapped_value);
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+ Any, new_value,
mutator->MaybeInplaceMutateIfUniqueExpected(old_value), self);
if (!old_value.same_as(new_value)) {
- it->second = new_value;
+ it->second = std::move(new_value);
}
}
return Any(value);
diff --git a/src/ffi/extra/structural_visit.cc
b/src/ffi/extra/structural_visit.cc
index bcd4ce41..95bf80d8 100644
--- a/src/ffi/extra/structural_visit.cc
+++ b/src/ffi/extra/structural_visit.cc
@@ -85,17 +85,17 @@ Expected<Optional<VisitInterrupt>> StructuralWalkExpected(
}
/*! \brief Visit entries in a sequence container. */
-TVMFFIAny VisitSeqContainer(StructuralVisitorObj* visitor, const SeqBaseObj*
seq) noexcept {
- for (const Any& item : *seq) {
- TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(item));
+TVMFFIAny VisitSeqContainer(StructuralVisitorObj* visitor, const SeqBaseObj*
self) noexcept {
+ for (const Any& item : *self) {
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(item), self);
}
return
ExpectedUnsafe::MoveToTVMFFIAny(Expected<Optional<VisitInterrupt>>(std::nullopt));
}
/*! \brief Visit values in a map container while treating keys as structural
anchors. */
-TVMFFIAny VisitMapContainer(StructuralVisitorObj* visitor, const MapBaseObj*
map) noexcept {
- for (const auto& kv : *map) {
- TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(kv.second));
+TVMFFIAny VisitMapContainer(StructuralVisitorObj* visitor, const MapBaseObj*
self) noexcept {
+ for (const auto& kv : *self) {
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(kv.second),
self);
}
return
ExpectedUnsafe::MoveToTVMFFIAny(Expected<Optional<VisitInterrupt>>(std::nullopt));
}
diff --git a/tests/cpp/extra/test_structural_mutate.cc
b/tests/cpp/extra/test_structural_mutate.cc
index d2feb251..8d8f87a9 100644
--- a/tests/cpp/extra/test_structural_mutate.cc
+++ b/tests/cpp/extra/test_structural_mutate.cc
@@ -38,6 +38,8 @@ using namespace tvm::ffi::testing;
using AnyArray = Array<Any>;
using StringMap = Map<String, Any>;
+TVM_FFI_STATIC_INIT_BLOCK() { TMutatePairObj::RegisterReflection(); }
+
Expected<Any> Increment(int64_t value) { return Any(value + 1); }
template <WalkOrder order>
@@ -88,6 +90,54 @@ TEST(StructuralMap, MapsNestedArrayAndMapInConfiguredOrder) {
CheckNestedArrayMapOrder<WalkOrder::kPostOrder>({"int", "inner-array",
"map", "outer-array"});
}
+TEST(StructuralMap, RegisteredMutateHookUsesAssignOrReturn) {
+ TVar lhs("lhs");
+ TVar rhs("rhs");
+ TMutatePair root(lhs, rhs);
+ TMutatePairObj::StructuralMutateCallCount() = 0;
+
+ TMutatePair mapped =
+ StructuralMap<WalkOrder::kPostOrder>(root, [](const TVarObj* var) ->
Expected<Any> {
+ return Any(TVar(var->name + "-mapped"));
+ }).cast<TMutatePair>();
+
+ EXPECT_EQ(TMutatePairObj::StructuralMutateCallCount(), 1);
+ Optional<TVar> mapped_lhs = mapped->lhs.as<TVar>();
+ Optional<TVar> mapped_rhs = mapped->rhs.as<TVar>();
+ ASSERT_TRUE(mapped_lhs.has_value());
+ ASSERT_TRUE(mapped_rhs.has_value());
+ EXPECT_EQ(mapped_lhs.value()->name, "lhs-mapped");
+ EXPECT_EQ(mapped_rhs.value()->name, "rhs-mapped");
+
+ Expected<Any> failed =
+ StructuralMapExpected<WalkOrder::kPostOrder>(root, [](const TVarObj*
var) -> Expected<Any> {
+ if (var->name == "lhs") {
+ return Unexpected(Error("ValueError", "registered hook child
failed", ""));
+ }
+ return Any(TVar(var->name + "-mapped"));
+ });
+ ASSERT_TRUE(failed.is_err());
+ Optional<VisitErrorContext> context =
VisitErrorContext::TryGetFromError(failed.error());
+ ASSERT_TRUE(context.has_value());
+ const List<ObjectRef>& reverse_pattern =
context.value()->reverse_visit_pattern;
+ ASSERT_EQ(reverse_pattern.size(), 2U);
+ EXPECT_TRUE(reverse_pattern[0].same_as(lhs));
+ EXPECT_TRUE(reverse_pattern[1].same_as(root));
+
+ TMutatePair nullable(ObjectRef(nullptr), rhs);
+ TMutatePair nullable_mapped =
+ StructuralMap<WalkOrder::kPostOrder>(nullable, [](const String& value)
-> Expected<Any> {
+ return Any(value);
+ }).cast<TMutatePair>();
+ EXPECT_FALSE(nullable_mapped->lhs.defined());
+ EXPECT_TRUE(nullable_mapped->rhs.same_as(rhs));
+
+ Expected<Any> wrong_type = StructuralMapExpected<WalkOrder::kPostOrder>(
+ root, [](const TVarObj*) -> Expected<Any> { return Any(int64_t{1}); });
+ ASSERT_TRUE(wrong_type.is_err());
+ EXPECT_EQ(wrong_type.error().kind(), "TypeError");
+}
+
TEST(StructuralMap, PreservesSharedArrayAndMapInputs) {
// A shared Array is copied when one of its elements changes.
{
diff --git a/tests/cpp/testing_object.h b/tests/cpp/testing_object.h
index b5a6a394..32e1e917 100644
--- a/tests/cpp/testing_object.h
+++ b/tests/cpp/testing_object.h
@@ -25,6 +25,7 @@
#include <tvm/ffi/container/map.h>
#include <tvm/ffi/device.h>
#include <tvm/ffi/dtype.h>
+#include <tvm/ffi/extra/structural_mutate.h>
#include <tvm/ffi/extra/structural_visit.h>
#include <tvm/ffi/memory.h>
#include <tvm/ffi/object.h>
@@ -234,6 +235,60 @@ class TPair : public ObjectRef {
TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TPair, ObjectRef, TPairObj);
};
+class TMutatePairObj : public Object {
+ public:
+ ObjectRef lhs;
+ ObjectRef rhs;
+
+ TMutatePairObj(ObjectRef lhs, ObjectRef rhs) : lhs(std::move(lhs)),
rhs(std::move(rhs)) {}
+ explicit TMutatePairObj(UnsafeInit) {}
+
+ // Every test that reads this process-global counter must reset it first.
+ static int& StructuralMutateCallCount() {
+ static int count = 0;
+ return count;
+ }
+
+ static Expected<Any> StructuralMutateExpected(StructuralMutatorObj* mutator,
+ AnyView value) noexcept {
+ ++StructuralMutateCallCount();
+ const auto* self = value.cast<const TMutatePairObj*>();
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ObjectRef, lhs,
mutator->MutateExpected(self->lhs), self);
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ObjectRef, rhs,
mutator->MutateExpected(self->rhs), self);
+ if (lhs.same_as(self->lhs) && rhs.same_as(self->rhs)) {
+ return Any(value);
+ }
+ return Any(ObjectRef(make_object<TMutatePairObj>(std::move(lhs),
std::move(rhs))));
+ }
+
+ static TVMFFIAny StructuralMutate(StructuralMutatorObj* mutator, AnyView
value) noexcept {
+ return
details::ExpectedUnsafe::MoveToTVMFFIAny(StructuralMutateExpected(mutator,
value));
+ }
+
+ static void RegisterReflection() {
+ namespace refl = tvm::ffi::reflection;
+ refl::ObjectDef<TMutatePairObj>()
+ .def_ro("lhs", &TMutatePairObj::lhs)
+ .def_ro("rhs", &TMutatePairObj::rhs);
+ refl::EnsureTypeAttrColumn(refl::type_attr::kStructuralMutate);
+ refl::TypeAttrDef<TMutatePairObj>().attr(
+ refl::type_attr::kStructuralMutate,
+
reinterpret_cast<void*>(static_cast<FStructuralMutate>(&TMutatePairObj::StructuralMutate)));
+ }
+
+ static constexpr TVMFFISEqHashKind _type_s_eq_hash_kind =
kTVMFFISEqHashKindTreeNode;
+ TVM_FFI_DECLARE_OBJECT_INFO_FINAL("test.MutatePair", TMutatePairObj, Object);
+};
+
+class TMutatePair : public ObjectRef {
+ public:
+ TMutatePair(ObjectRef lhs, ObjectRef rhs) {
+ data_ = make_object<TMutatePairObj>(std::move(lhs), std::move(rhs));
+ }
+
+ TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TMutatePair, ObjectRef,
TMutatePairObj);
+};
+
class TObjectPtrHolderObj : public Object {
public:
Arc<TIntObj> value;
@@ -358,10 +413,14 @@ class TFuncObj : public Object {
static TVMFFIAny StructuralVisit(StructuralVisitorObj* visitor, AnyView
value) noexcept {
const auto* self = value.cast<const TFuncObj*>();
- TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->WithDefRegionKind(
- kTVMFFIDefRegionKindRecursive, [&]() { return
visitor->VisitExpected(self->params); }));
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(
+ visitor->WithDefRegionKind(kTVMFFIDefRegionKindRecursive,
+ [&]() { return
visitor->VisitExpected(self->params); }),
+ self);
- return
details::ExpectedUnsafe::MoveToTVMFFIAny(visitor->VisitExpected(self->body));
+ auto body_result = visitor->VisitExpected(self->body);
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(body_result, self);
+ return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(body_result));
}
static void RegisterReflection() {