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 39d593e7 [FFI][REFACTOR] Introduce UnchangedOr for StructuralMutate
(#768)
39d593e7 is described below
commit 39d593e716a5b437a57ea3241aff8850112caf2e
Author: Tianqi Chen <[email protected]>
AuthorDate: Tue Sep 8 18:22:59 2026 -0400
[FFI][REFACTOR] Introduce UnchangedOr for StructuralMutate (#768)
This change refactors StructuralMutate to introduce `UnchangedOr<T>`,
which can indicate that a mutation did not change the input value; if a
value is changed, it is held in the container. Previously, Mutate simply
returned a fresh copy of the original value when unchanged.
Performance-wise, this results in a refcount bump and decref for every
node examined, even when there is no change. Depending on the hardware
platform, the overhead can be visible. This change introduces a special
`UnchangedOr<T>`, which holds the `kTVMFFIUnchanged` tag to indicate
that a mutation did not result in a change, while a changed value is
propagated through the container. The canonical mutation hook pattern
still works well, in a cleaner form:
```c++
TVMFFIAny FooMutate(StructuralMutatorObj* mutator, AnyView value) noexcept {
const FooNode* self =
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
FooNode>(value);
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, a,
mutator->MutateExpected(self->a));
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, b,
mutator->MutateExpected(self->b));
if (a.UnchangedOrSameAs(self->a) && b.UnchangedOrSameAs(self->b)) {
TVM_FFI_S_MUTATE_RETURN_UNCHANGED();
}
ObjectPtr<FooNode> copy = make_object<FooNode>(*self);
copy->a = std::move(a).ValueOrUnchanged(std::move(copy->a));
copy->b = std::move(b).ValueOrUnchanged(std::move(copy->b));
return details::AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(copy)));
}
```
The engine is updated to be aware of the new convention.
---
docs/concepts/structural_eq_hash.rst | 12 +-
include/tvm/ffi/c_api.h | 2 +
include/tvm/ffi/expected.h | 44 +-
include/tvm/ffi/extra/structural_mutate.h | 579 +++++++++++++++++++++------
include/tvm/ffi/extra/structural_visit.h | 14 +-
include/tvm/ffi/type_traits.h | 2 +
python/tvm_ffi/cython/base.pxi | 1 +
rust/tvm-ffi-sys/src/c_api.rs | 2 +
rust/tvm-ffi/src/extra/structural_mutate.rs | 14 +-
rust/tvm-ffi/tests/test_structural_mutate.rs | 10 +
src/ffi/extra/structural_mutate.cc | 129 +++---
src/ffi/object.cc | 1 +
tests/cpp/extra/test_structural_mutate.cc | 249 ++++++++++--
tests/cpp/testing_object.h | 19 +-
tests/python/test_structural.py | 8 +
15 files changed, 842 insertions(+), 244 deletions(-)
diff --git a/docs/concepts/structural_eq_hash.rst
b/docs/concepts/structural_eq_hash.rst
index b013f11b..74efd998 100644
--- a/docs/concepts/structural_eq_hash.rst
+++ b/docs/concepts/structural_eq_hash.rst
@@ -1230,6 +1230,11 @@ structural child, and returns an interrupt if one occurs:
A custom ``__s_mutate__`` hook similarly receives the active mutator. It
should
recursively call ``mutator.mutate`` and return a new value only when needed.
+In C++, a hook can return ``Unchanged()`` when it produces no new value, or use
+``UnchangedOr<T>`` to carry either that marker or a replacement. The mutator
+propagates the marker through recursive callback-facing entry points. The
+top-level ``StructuralMap`` and ``StructuralMutate`` functions resolve it to
the
+original value, so it never escapes as a mapped value.
An optional ``__s_maybe_inplace_mutate__`` hook may implement an in-place
optimization. The structural-map engine dispatches it only when the input is
safe to mutate, so the optional hook may rely on that ownership guarantee. A
@@ -1287,8 +1292,9 @@ subtree. An unmatched value uses default descent:
return visitor->VisitExpected(pair->lhs);
});
-Walk callbacks return ``Expected<WalkResult>``. Map callbacks return
-``Expected<Any>`` and must obey the same non-in-place callback contract as the
-Python API. For ``Map`` and ``Dict``, both APIs process values and skip keys.
+Walk callbacks return ``Expected<WalkResult>``. Map callbacks may return a
bare
+replacement, ``Unchanged``, or ``Expected<Any>``, and must obey the same
+non-in-place callback contract as the Python API.
+For ``Map`` and ``Dict``, both APIs process values and skip keys.
``StructuralWalk``, ``StructuralVisit`` and ``StructuralMap`` are the
corresponding throwing convenience forms.
diff --git a/include/tvm/ffi/c_api.h b/include/tvm/ffi/c_api.h
index 019bd665..0444a3c5 100644
--- a/include/tvm/ffi/c_api.h
+++ b/include/tvm/ffi/c_api.h
@@ -136,6 +136,8 @@ typedef enum {
kTVMFFISmallStr = 11,
/*! \brief Small bytes on stack */
kTVMFFISmallBytes = 12,
+ /*! \brief Structural-mutation marker indicating that no new value was
produced */
+ kTVMFFIUnchanged = 13,
/*! \brief Start of statically defined objects. */
kTVMFFIStaticObjectBegin = 64,
/*!
diff --git a/include/tvm/ffi/expected.h b/include/tvm/ffi/expected.h
index ffe91a25..77ba7b39 100644
--- a/include/tvm/ffi/expected.h
+++ b/include/tvm/ffi/expected.h
@@ -459,20 +459,46 @@ struct ExpectedUnsafe {
};
/*!
- * \brief Return proxy used by early-return macros in raw or typed functions.
+ * \brief Error-only return proxy used by mutation early-return macros.
+ *
+ * An error has the same shape in every mutation return type, so this proxy
+ * converts to the raw ``TVMFFIAny`` used by hooks or to any ``Expected<T>``.
+ */
+class UnexpectedReturnHelper {
+ public:
+ TVM_FFI_INLINE explicit UnexpectedReturnHelper(Unexpected<Error>&& value)
noexcept
+ : value_(std::move(value)) {}
+
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ TVM_FFI_INLINE operator TVMFFIAny() && noexcept {
+ return ExpectedUnsafe::MoveToTVMFFIAny(Expected<Any>(std::move(value_)));
+ }
+
+ template <typename T>
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ TVM_FFI_INLINE operator Expected<T>() && noexcept {
+ return std::move(value_);
+ }
+
+ private:
+ Unexpected<Error> value_;
+};
+
+/*!
+ * \brief Expected-value return proxy used by visit early-return macros.
* \tparam T The success type, fixed when the proxy stores its ``Expected<T>``.
*
- * A return statement selects either the raw ``TVMFFIAny`` conversion used by
- * hooks or the same ``Expected<T>`` type used by typed helpers. Both
- * conversions are rvalue-qualified because handing off the stored payload is
- * a single move; an lvalue helper cannot accidentally transfer it twice. As
- * with other moved-from values, deliberately converting ``std::move(helper)``
- * twice remains caller error.
+ * A return statement selects the raw ``TVMFFIAny`` conversion used by hooks or
+ * the same ``Expected<T>`` type used by typed helpers. The payload-bearing
+ * conversions are rvalue-qualified because handing it off is a single move; an
+ * lvalue helper cannot accidentally transfer it twice. As with other
moved-from
+ * values, deliberately converting ``std::move(helper)`` twice remains caller
+ * error.
*/
template <typename T>
-class MaybeReturnHelper {
+class ExpectedReturnHelper {
public:
- TVM_FFI_INLINE explicit MaybeReturnHelper(Expected<T>&& value) noexcept
+ TVM_FFI_INLINE explicit ExpectedReturnHelper(Expected<T>&& value) noexcept
: value_(std::move(value)) {}
// NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
diff --git a/include/tvm/ffi/extra/structural_mutate.h
b/include/tvm/ffi/extra/structural_mutate.h
index d6dac496..73659ae8 100644
--- a/include/tvm/ffi/extra/structural_mutate.h
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -50,13 +50,15 @@ namespace tvm {
namespace ffi {
class StructuralMutatorObj;
+template <typename T>
+class UnchangedOr;
/*!
* \brief ABI callback type for structural mutation.
*
* \param mutator The active structural mutator.
* \param value The borrowed value to mutate.
- * \return Raw ``TVMFFIAny`` containing the mutated value or an Error.
+ * \return Raw ``TVMFFIAny`` containing a replacement, the unchanged marker,
or an Error.
*
* \note The hook is exception-free. Representable failures must be returned
as an Error. Hook
* implementations should use non-throwing accessors when the engine's
type dispatch has
@@ -103,7 +105,7 @@ struct StructuralMutatorVTable {
*
* \param mutator The active structural mutator.
* \param value The borrowed value to mutate.
- * \return Raw ``TVMFFIAny`` carrying the mutated value or Error.
+ * \return Raw ``TVMFFIAny`` carrying a replacement, the unchanged marker,
or Error.
*/
FStructuralMutate mutate = nullptr;
/*!
@@ -111,7 +113,7 @@ struct StructuralMutatorVTable {
*
* \param mutator The active structural mutator.
* \param value The borrowed value to mutate.
- * \return Raw ``TVMFFIAny`` carrying the mutated value or Error.
+ * \return Raw ``TVMFFIAny`` carrying a replacement, the unchanged marker,
or Error.
*
* The returned value may refer to the same object as \p value when the
implementation mutates
* that object in place.
@@ -136,6 +138,173 @@ struct StructuralMutatorVTable {
FStructuralVarRemapSet var_remap_set = nullptr;
};
+namespace details {
+struct UnchangedOrUnsafe;
+} // namespace details
+
+/*! \brief Tag for a mutation result that produced no new value. */
+struct Unchanged {
+ /*!
+ * \brief Copy this tag to its raw marker representation.
+ * \return Raw ``TVMFFIAny`` carrying the reserved unchanged type index.
+ */
+ TVM_FFI_INLINE TVMFFIAny CopyToTVMFFIAny() const noexcept {
+ // The marker needs a reserved type index because every ordinary index is
a legal mutation
+ // result. In particular, kTVMFFINone is a valid replacement and cannot
double as the marker.
+ TVMFFIAny raw;
+ raw.type_index = TypeIndex::kTVMFFIUnchanged;
+ // invariance: always set the union padding part to 0
+ raw.zero_padding = 0;
+ raw.v_int64 = 0;
+ return raw;
+ }
+
+ /*!
+ * \brief Convert this tag to its owning marker representation.
+ * \return An owning ``Any`` carrying the reserved unchanged type index.
+ */
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ TVM_FFI_INLINE operator Any() const noexcept {
+ TVMFFIAny raw = CopyToTVMFFIAny();
+ return details::AnyUnsafe::MoveTVMFFIAnyToAny(&raw);
+ }
+};
+
+/*!
+ * \brief A structural-mutation result containing a replacement or no new
value.
+ *
+ * \tparam T The replacement value type.
+ * \note ``UnchangedOr`` is deliberately designed to only have
rvalue-qualified value accessors,
+ * so the compiler forces a value to leave the container exactly once,
via a move.
+ *
+ * \code{.cpp}
+ * // resolves to the original when the descent reported unchanged
+ * copy->a = std::move(a).ValueOrUnchanged(std::move(copy->a));
+ * // already known to be changed, so no original is needed
+ * copy->b = std::move(b).ValueUnchecked();
+ * \endcode
+ */
+template <typename T>
+class UnchangedOr {
+ public:
+ static_assert(!std::is_base_of_v<Error, std::remove_cv_t<T>>,
+ "UnchangedOr<Error> is not supported");
+
+ /*!
+ * \brief Construct an unchanged result from its tag.
+ * \param unchanged The unchanged tag.
+ */
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ TVM_FFI_INLINE UnchangedOr(Unchanged unchanged) noexcept :
data_(static_cast<Any>(unchanged)) {}
+
+ /*!
+ * \brief Construct a changed result from a replacement value.
+ * \param value The replacement value.
+ */
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ TVM_FFI_INLINE UnchangedOr(T value) : data_(Any(std::move(value))) {}
+
+ /// \cond Doxygen_Suppress
+ TVM_FFI_INLINE UnchangedOr(const UnchangedOr&) = default;
+ TVM_FFI_INLINE UnchangedOr(UnchangedOr&&) noexcept = default;
+ /// \endcond
+ TVM_FFI_INLINE ~UnchangedOr() = default;
+ TVM_FFI_INLINE UnchangedOr& operator=(const UnchangedOr&) = default;
+ TVM_FFI_INLINE UnchangedOr& operator=(UnchangedOr&&) noexcept = default;
+
+ /*!
+ * \brief Whether this result asks the caller to preserve the original value.
+ * \return Whether the result is unchanged.
+ */
+ TVM_FFI_INLINE bool IsUnchanged() const& noexcept {
+ return data_.type_index() == TypeIndex::kTVMFFIUnchanged;
+ }
+
+ /*!
+ * \brief Whether this result is unchanged or contains the original object
identity.
+ * \param original The original value.
+ * \return Whether the original identity may be reused.
+ */
+ TVM_FFI_INLINE bool UnchangedOrSameAs(const T& original) const& noexcept {
+ return IsUnchanged() || data_.same_as(original);
+ }
+
+ /*!
+ * \brief Move the replacement, or move \p original when unchanged.
+ * \param original The owned original value.
+ * \return The replacement or original value.
+ * \note Passing a named lvalue transfers ownership and may leave it
moved-from.
+ */
+ TVM_FFI_INLINE T ValueOrUnchanged(T& original) && {
+ return IsUnchanged() ? std::move(original)
+ :
details::AnyUnsafe::MoveFromAnyAfterCheck<T>(std::move(data_));
+ }
+
+ /*!
+ * \brief Move the replacement, or move \p original when unchanged.
+ * \param original The owned original value.
+ * \return The replacement or original value.
+ */
+ TVM_FFI_INLINE T ValueOrUnchanged(T&& original) && {
+ return IsUnchanged() ? std::move(original)
+ :
details::AnyUnsafe::MoveFromAnyAfterCheck<T>(std::move(data_));
+ }
+
+ /*!
+ * \brief Move the replacement, or materialize \p original when unchanged.
+ * \tparam U The replacement type, constrained to ``Any``.
+ * \param original The borrowed original value.
+ * \return The replacement or original value.
+ */
+ template <typename U = T,
+ typename = std::enable_if_t<std::is_same_v<T, Any> &&
std::is_same_v<U, T>>>
+ TVM_FFI_INLINE Any ValueOrUnchanged(AnyView original) && {
+ return IsUnchanged() ? Any(original)
+ :
details::AnyUnsafe::MoveFromAnyAfterCheck<Any>(std::move(data_));
+ }
+
+ /*!
+ * \brief Move the known-changed replacement without checking its state.
+ * \return The replacement value.
+ * \pre The result is not unchanged.
+ */
+ TVM_FFI_INLINE T ValueUnchecked() && {
+ return details::AnyUnsafe::MoveFromAnyAfterCheck<T>(std::move(data_));
+ }
+
+ private:
+ friend struct details::UnchangedOrUnsafe;
+ template <typename, typename>
+ friend struct TypeTraits;
+ struct UnsafeInit {};
+ TVM_FFI_INLINE explicit UnchangedOr(UnsafeInit, Any data) noexcept :
data_(std::move(data)) {}
+ Any data_;
+};
+
+namespace details {
+/*! \brief Unsafe moves between UnchangedOr and its single Any storage. */
+struct UnchangedOrUnsafe {
+ template <typename T>
+ TVM_FFI_INLINE static TVMFFIAny MoveToTVMFFIAny(UnchangedOr<T>&& result)
noexcept {
+ return AnyUnsafe::MoveAnyToTVMFFIAny(std::move(result.data_));
+ }
+};
+
+} // namespace details
+
+namespace details {
+// Out of line so its strings and Error construction stay out of the hot path
of whatever hook
+// body TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN expands into. Same reason as
+// BadStructuralMutateHookError.
+// Takes nothing on purpose. Naming the offending type in the message would
keep the result live
+// across the predicted-not-taken guard in the hot path. The declared type is
already present in
+// the source line to which the diagnostic points.
+TVM_FFI_COLD_CODE inline UnexpectedReturnHelper SMutateDeclaredTypeError()
noexcept {
+ return UnexpectedReturnHelper(Unexpected(
+ Error("TypeError", "structural mutate result does not match the declared
type", "")));
+}
+} // namespace details
+
/*!
* \brief Object node of a structural mutator.
*/
@@ -148,69 +317,104 @@ class StructuralMutatorObj : public Object {
* \brief Mutate a value through the mutator vtable.
*
* \param value The value to mutate.
- * \return The mutated owning value.
+ * \tparam T The declared replacement type.
+ * \return The replacement or unchanged marker.
* \throws Error if mutation fails.
*
* This entry point never intentionally mutates \p value in place. Recursive
mutations
* also use \ref Mutate.
+ *
+ * \code{.cpp}
+ * Expr new_node =
mutator->Mutate<Expr>(node).ValueOrUnchanged(std::move(node));
+ * \endcode
*/
- TVM_FFI_INLINE Any Mutate(AnyView value) { return
MutateExpected(value).value(); }
+ template <typename T = Any>
+ TVM_FFI_INLINE UnchangedOr<T> Mutate(AnyView value) {
+ return std::move(MutateExpected<T>(value)).value();
+ }
/*!
* \brief Exception-free form of \ref Mutate.
*
* \param value The value to mutate.
- * \return The mutated owning value, or an Error if mutation failed.
+ * \tparam T The declared replacement type.
+ * \return The replacement or unchanged marker, or an Error if mutation
failed.
*/
- TVM_FFI_INLINE Expected<Any> MutateExpected(AnyView value) noexcept {
- return
details::ExpectedUnsafe::MoveFromTVMFFIAny<Any>((*vtable_->mutate)(this,
value));
+ template <typename T = Any>
+ TVM_FFI_INLINE Expected<UnchangedOr<T>> MutateExpected(AnyView value)
noexcept {
+ if constexpr (std::is_same_v<T, Any>) {
+ return details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<T>>(
+ (*vtable_->mutate)(this, value));
+ } else {
+ TVMFFIAny result = (*vtable_->mutate)(this, value);
+ if
(TVM_FFI_PREDICT_FALSE(!TypeTraits<Expected<UnchangedOr<T>>>::CheckAnyStrict(&result)))
{
+ (void)details::AnyUnsafe::MoveTVMFFIAnyToAny(&result);
+ return details::SMutateDeclaredTypeError();
+ }
+ return
details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<T>>(result);
+ }
}
/*!
* \brief Mutate a value, permitting an in-place implementation when it is
safe.
*
* \param value The borrowed value to mutate.
- * \return The mutated owning value.
+ * \tparam T The declared replacement type.
+ * \return The replacement or unchanged marker.
* \throws Error if mutation fails.
*
* The returned value may refer to the same object as \p value. Callers must
use the return value
* as the result of the mutation rather than assuming that the input object
was reused.
*/
- TVM_FFI_INLINE Any MaybeInplaceMutate(AnyView value) {
- return MaybeInplaceMutateExpected(value).value();
+ template <typename T = Any>
+ TVM_FFI_INLINE UnchangedOr<T> MaybeInplaceMutate(AnyView value) {
+ return std::move(MaybeInplaceMutateExpected<T>(value)).value();
}
/*!
* \brief Exception-free form of \ref MaybeInplaceMutate.
*
* \param value The borrowed value to mutate.
- * \return The mutated owning value, or an Error if mutation failed.
+ * \tparam T The declared replacement type.
+ * \return The replacement or unchanged marker, or an Error if mutation
failed.
*
* \note Call only from a ``__s_maybe_inplace_mutate__`` hook, which is
dispatched
* only for a value whose entire path from the root is uniquely owned.
*/
- TVM_FFI_INLINE Expected<Any> MaybeInplaceMutateExpected(AnyView value)
noexcept {
- return details::ExpectedUnsafe::MoveFromTVMFFIAny<Any>(
- (*vtable_->maybe_inplace_mutate)(this, value));
+ template <typename T = Any>
+ TVM_FFI_INLINE Expected<UnchangedOr<T>> MaybeInplaceMutateExpected(AnyView
value) noexcept {
+ if constexpr (std::is_same_v<T, Any>) {
+ return details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<T>>(
+ (*vtable_->maybe_inplace_mutate)(this, value));
+ } else {
+ TVMFFIAny result = (*vtable_->maybe_inplace_mutate)(this, value);
+ if
(TVM_FFI_PREDICT_FALSE(!TypeTraits<Expected<UnchangedOr<T>>>::CheckAnyStrict(&result)))
{
+ (void)details::AnyUnsafe::MoveTVMFFIAnyToAny(&result);
+ return details::SMutateDeclaredTypeError();
+ }
+ return
details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<T>>(result);
+ }
}
/*!
* \brief Mutate a value, using in-place mutation only for a uniquely owned
object.
- *
+ * \tparam T The declared replacement type.
* \param value The borrowed value to mutate.
- * \return The mutated owning value, or an Error if mutation failed.
+ * \return The replacement or unchanged marker, or an Error if mutation
failed.
*
* \note The caller must already know the entire path from the root is
uniquely
* owned, either through an owning moved-in root or while handling a
* ``__s_maybe_inplace_mutate__`` hook. This method checks only \p
value
* itself, not its ancestors.
*/
- TVM_FFI_INLINE Expected<Any> MaybeInplaceMutateIfUniqueExpected(AnyView
value) noexcept {
+ template <typename T = Any>
+ TVM_FFI_INLINE Expected<UnchangedOr<T>> MaybeInplaceMutateIfUniqueExpected(
+ AnyView value) noexcept {
const Object* obj = value.as<Object>();
if (obj != nullptr && obj->unique()) {
- return MaybeInplaceMutateExpected(value);
+ return MaybeInplaceMutateExpected<T>(value);
}
- return MutateExpected(value);
+ return MutateExpected<T>(value);
}
/*!
@@ -335,16 +539,16 @@ class StructuralMutatorObj : public Object {
Error("TypeError", "__s_mutate__ must be an opaque function pointer or
ffi.Function", ""));
}
- // Convention: the ABI boundary is a raw TVMFFIAny; everything inside a
callback or hook body
- // works in Expected<Any> and moves out to TVMFFIAny at that boundary.
+ // Convention: the ABI boundary is a raw TVMFFIAny; mutation results inside
a callback or hook
+ // body use Expected<Any> and move out to TVMFFIAny at that boundary.
Unchanged converts to an
+ // Any carrying kTVMFFIUnchanged.
//
// The Raw forms below exist because that boundary is also the default path.
A hook is a C-ABI
// function pointer returning TVMFFIAny, a 16-byte POD that stays in
registers; wrapping the
- // result in Expected<Any> would force it to memory, since Expected<Any> is
not trivially
- // destructible and is therefore classified MEMORY. Descent through an
unmatched node calls a
- // hook and returns its result unchanged, so keeping that path raw removes
the round trip
- // entirely. Only a matched callback pays for an Expected<Any>, and it
materializes an Any
- // anyway to record its remap entry.
+ // result in Expected<Any> would force it to memory because the C++ wrapper
is not
+ // trivially destructible and is therefore classified MEMORY. Descent
through an unmatched node
+ // calls a hook and returns its result unchanged, so keeping that path raw
removes the round trip
+ // entirely. Only a matched callback pays for the Expected wrapper.
//
// Engine-internal: subclasses call the Expected forms above.
/*! \brief Raw default mutation: attr lookup then hook, favouring the fn-ptr
case. */
@@ -400,9 +604,9 @@ class StructuralMutatorObj : public Object {
// reflected walk runs under an identity remap.
const bool remappable = IsRemappableIdentity(value.type_index());
if (remappable) {
+ // Only None means no cached remap; every other result is returned
directly.
Expected<Any> mapped = VarRemapGetExpected(value);
- if (TVM_FFI_PREDICT_FALSE(mapped.is_err()) ||
- details::ExpectedUnsafe::GetData(mapped).type_index() !=
TypeIndex::kTVMFFINone) {
+ if (details::ExpectedUnsafe::GetData(mapped).type_index() !=
TypeIndex::kTVMFFINone) {
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(mapped));
}
}
@@ -411,8 +615,10 @@ class StructuralMutatorObj : public Object {
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
}
if (remappable) {
- Expected<void> set_result =
- VarRemapSetExpected(value, details::ExpectedUnsafe::GetData(result));
+ const Any& result_value = details::ExpectedUnsafe::GetData(result);
+ AnyView value_to_store =
+ result_value.type_index() == TypeIndex::kTVMFFIUnchanged ? value :
AnyView(result_value);
+ Expected<void> set_result = VarRemapSetExpected(value, value_to_store);
if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
return details::ExpectedUnsafe::MoveToTVMFFIAny(
Expected<Any>(Unexpected(std::move(set_result).error())));
@@ -546,7 +752,8 @@ TVM_FFI_INLINE static Expected<Any>
MutateReflectedFieldsExpected(StructuralMuta
return true;
}
- Expected<Any> mutated_field = [&]() -> Expected<Any> {
+ // Reflected fields use the same unchanged-or-value descent protocol.
+ Expected<UnchangedOr<Any>> mutated_field = [&]() ->
Expected<UnchangedOr<Any>> {
if (field_info->flags & kTVMFFIFieldFlagBitMaskSEqHashDefSimple) {
return mutator->WithDefRegionKind(kTVMFFIDefRegionKindSimple,
[&]() {
return mutator->MutateExpected(field_value);
@@ -563,8 +770,11 @@ TVM_FFI_INLINE static Expected<Any>
MutateReflectedFieldsExpected(StructuralMuta
result = Unexpected(std::move(mutated_field).error());
return true;
}
- const Any& new_field =
details::ExpectedUnsafe::GetData(mutated_field);
- if (field_value.same_as(new_field)) {
+ const Any& mutated_field_data =
details::ExpectedUnsafe::GetData(mutated_field);
+ // Unchanged first: it is the common case, and it is one type-index
test where the
+ // resolved form ran a full same_as against a value it had just been
handed back.
+ if (mutated_field_data.type_index() == TypeIndex::kTVMFFIUnchanged ||
+ field_value.same_as(mutated_field_data)) {
return false;
}
@@ -579,8 +789,8 @@ TVM_FFI_INLINE static Expected<Any>
MutateReflectedFieldsExpected(StructuralMuta
return true;
}
- ret_code = reflection::CallFieldSetter(field_info, field_addr,
- reinterpret_cast<const
TVMFFIAny*>(&new_field));
+ ret_code = reflection::CallFieldSetter(
+ field_info, field_addr, reinterpret_cast<const
TVMFFIAny*>(&mutated_field_data));
if (TVM_FFI_PREDICT_FALSE(ret_code != 0)) {
result = Unexpected(details::MoveFromSafeCallRaised());
return true;
@@ -603,7 +813,7 @@ TVM_FFI_INLINE static Expected<Any>
MutateReflectedFieldsExpected(StructuralMuta
return result;
}
if (!field_changed) {
- return Any(value);
+ return Unchanged();
}
return result;
}
@@ -615,36 +825,27 @@ 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.
-// The rvalue-only proxy 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::MaybeReturnHelper(::std::move(tvm_ffi_res_)); \
- }
\
+// 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::UnexpectedReturnHelper( \
+ ::tvm::ffi::Unexpected(::std::move(tvm_ffi_res_).error())); \
+ } \
} while (0)
/// \endcond
-// Out of line so its strings and Error construction stay out of the hot path
of whatever hook
-// body TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN expands into. Same reason as
-// BadStructuralMutateHookError.
-TVM_FFI_COLD_CODE inline Expected<Any> SMutateDeclaredTypeError() noexcept {
- return Unexpected(
- Error("TypeError", "structural mutate result does not match the declared
type", ""));
-}
-
/// \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::MaybeReturnHelper(
\
- ::tvm::ffi::details::SMutateDeclaredTypeError());
\
+ return ::tvm::ffi::details::SMutateDeclaredTypeError();
\
}
\
Type Name = /* NOLINT(bugprone-macro-parentheses) */
\
::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(
\
@@ -657,23 +858,65 @@ TVM_FFI_COLD_CODE inline Expected<Any>
SMutateDeclaredTypeError() noexcept {
* ``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 a
same-T ``Expected<T>``
- * helper. This macro declares ``Name`` into the enclosing scope and must be
used in a braced
- * block, never as an unbraced control-flow body.
+ * 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.
*
* Example:
* \code{.cpp}
- * TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ObjectRef, child,
mutator->MutateExpected(self->child));
+ * TVMFFIAny FooMutate(StructuralMutatorObj* mutator, AnyView value) noexcept {
+ * const FooNode* self =
+ * details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
FooNode>(value);
+ * TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, a,
+ * mutator->MutateExpected(self->a));
+ * TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, b,
+ * mutator->MutateExpected(self->b));
+ * if (a.UnchangedOrSameAs(self->a) && b.UnchangedOrSameAs(self->b)) {
+ * return Unchanged().CopyToTVMFFIAny();
+ * }
+ * ObjectPtr<FooNode> copy = make_object<FooNode>(*self);
+ * copy->a = std::move(a).ValueOrUnchanged(std::move(copy->a));
+ * copy->b = std::move(b).ValueOrUnchanged(std::move(copy->b));
+ * return details::AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(copy)));
+ * }
* \endcode
*
- * \param Type The concrete type of the successful value.
+ * Keep one statement per traversed field. A field skipped intentionally must
be guarded and carry
+ * a ``// skips:`` note.
+ *
+ * \param Type The concrete successful value type.
* \param Name The name of the value declared in the enclosing scope.
* \param ResultExpr An expression producing the ``Expected`` value to unwrap.
+ * \sa Unchanged::CopyToTVMFFIAny
*/
#define TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Type, Name, ResultExpr)
\
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN_IMPL_(TVM_FFI_STR_CONCAT(tvm_ffi_mutate_result_,
__COUNTER__), \
Type, Name, ResultExpr)
+/// \cond Doxygen_Suppress
+#define TVM_FFI_S_MUTATE_UNSAFE_ASSIGN_OR_RETURN_UNCHECKED_IMPL_(Result, Type,
Name, ResultExpr) \
+ auto Result = (ResultExpr); /* NOLINT(bugprone-macro-parentheses) */
\
+ TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result);
\
+ Type Name = /* NOLINT(bugprone-macro-parentheses) */
\
+ ::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(
\
+ ::std::move(::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))
+/// \endcond
+
+/*!
+ * \brief Unwrap a successful mutation result when the caller guarantees it to
be ``Type``.
+ *
+ * This is an unsafe form that can only be used when the mutation contract
guarantees the result
+ * type.
+ *
+ * \param Type The guaranteed 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.
+ * \sa TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN
+ */
+#define TVM_FFI_S_MUTATE_UNSAFE_ASSIGN_OR_RETURN_UNCHECKED(Type, Name,
ResultExpr) \
+ TVM_FFI_S_MUTATE_UNSAFE_ASSIGN_OR_RETURN_UNCHECKED_IMPL_(
\
+ TVM_FFI_STR_CONCAT(tvm_ffi_mutate_result_, __COUNTER__), Type, Name,
ResultExpr)
+
/// \cond Doxygen_Suppress
#define TVM_FFI_UNSAFE_S_MUTATE_ASSIGN_OR_RETURN_SKIP_CHECK_IMPL_(Result,
Type, Name, ResultExpr) \
auto Result = (ResultExpr); /* NOLINT(bugprone-macro-parentheses) */
\
@@ -905,7 +1148,7 @@ class StructuralMapEngine : public Parent {
using FuncInfo = details::FunctionInfo<std::decay_t<Callback>>;
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>");
+ "unchanged marker, or Expected<Any>");
static_assert(
FuncInfo::num_args == 1 + sizeof...(Is) || FuncInfo::num_args == 2 +
sizeof...(Is),
"StructuralMap callback takes (value, state...) with an optional
trailing "
@@ -983,14 +1226,9 @@ class StructuralMapEngine : public Parent {
return this->IsRemappableIdentity(value.type_index());
}();
- // A FreeVar or DAG node maps once and every later occurrence reuses that
result, so if
- // this node already has a cached remap entry, return it instead of
mutating it again.
if (remappable) {
+ // Only None means no cached remap; every other result is returned
directly.
Expected<Any> mapped = this->VarRemapGetExpected(value);
- if (mapped.is_err()) {
- *out = std::move(mapped);
- return true;
- }
if (ExpectedUnsafe::GetData(mapped).type_index() !=
TypeIndex::kTVMFFINone) {
*out = std::move(mapped);
return true;
@@ -1016,29 +1254,32 @@ class StructuralMapEngine : public Parent {
*out = std::move(callback_result);
return true;
}
- // Own the callback's result: it is the only reference from here on, and
moving it out
- // beats holding a reference into an Expected that stays alive across
the descent below.
- Any mapped_value = ExpectedUnsafe::GetData(callback_result);
+ Any mapped_value = std::move(ExpectedUnsafe::GetData(callback_result));
+ const AnyView descent_view =
+ mapped_value.type_index() == TypeIndex::kTVMFFIUnchanged ? value :
AnyView(mapped_value);
// Each descent names the node it actually ran on in the error context.
*out = [&]() -> Expected<Any> {
if constexpr (kMaybeInplace) {
// A pre-order result can be mutated in place if unchanged or
uniquely owned.
- const TVMFFIAny* mapped_data =
AnyUnsafe::TVMFFIAnyPtrFromAny(mapped_value);
+ const TVMFFIAny descent_data = descent_view.CopyToTVMFFIAny();
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) {
+ if (descent_data.type_index == input_data.type_index &&
+ descent_data.zero_padding == input_data.zero_padding &&
+ descent_data.v_int64 == input_data.v_int64) {
return this->DefaultMaybeInplaceMutateExpected(value);
}
- const Object* mapped_obj = mapped_value.as<Object>();
+ const Object* mapped_obj = descent_view.as<Object>();
bool can_inplace = mapped_obj != nullptr && mapped_obj->unique();
- return can_inplace ?
this->DefaultMaybeInplaceMutateExpected(mapped_value)
- : this->DefaultMutateExpected(mapped_value);
+ return can_inplace ?
this->DefaultMaybeInplaceMutateExpected(descent_view)
+ : this->DefaultMutateExpected(descent_view);
} else {
- return this->DefaultMutateExpected(mapped_value);
+ return this->DefaultMutateExpected(descent_view);
}
}();
if (TVM_FFI_PREDICT_FALSE(out->is_err())) return true;
+ if (ExpectedUnsafe::GetData(*out).type_index() ==
TypeIndex::kTVMFFIUnchanged) {
+ *out = std::move(mapped_value);
+ }
} else {
// Post-order: children are mapped first and the callback sees the
rebuilt node, so it
// observes its operands already substituted.
@@ -1048,20 +1289,21 @@ class StructuralMapEngine : public Parent {
*out = std::move(descended);
return true;
}
- // Held by reference, not moved out: the error path below names this
node, so it has to
- // survive the callback. Only a storage-enabled TSub could gain from a
move, and that is
- // exactly the case whose move would empty it.
- const Any& mapped_value = ExpectedUnsafe::GetData(descended);
+ // Descended unchanged uses the original view; otherwise the callback
sees the replacement.
+ const Any& descended_value = ExpectedUnsafe::GetData(descended);
+ const AnyView mapped_view = descended_value.type_index() ==
TypeIndex::kTVMFFIUnchanged
+ ? value
+ : AnyView(descended_value);
*out = [&]() -> Expected<Any> {
if constexpr (std::is_same_v<TSub, AnyView>) {
- return InvokeTypedCallbackLink(callback, AnyView(mapped_value),
StateIndices{});
+ return InvokeTypedCallbackLink(callback, mapped_view,
StateIndices{});
} else if constexpr (std::is_same_v<TSub, Any>) {
- return InvokeTypedCallbackLink(callback, Any(mapped_value),
StateIndices{});
+ return InvokeTypedCallbackLink(callback, Any(mapped_view),
StateIndices{});
} else {
// Re-converted rather than reusing the match: the callback is
invoked on the node
// descent handed back, and must only see the type it asked for.
Default mutation is
// required to preserve the type, so failing here means some hook
broke that.
- std::optional<TSub> descended_sub = mapped_value.template as<TSub>();
+ std::optional<TSub> descended_sub = mapped_view.template as<TSub>();
if (TVM_FFI_PREDICT_FALSE(!descended_sub.has_value())) {
return this->SMutateDescentTypeError();
}
@@ -1069,14 +1311,25 @@ class StructuralMapEngine : public Parent {
}
}();
if (TVM_FFI_PREDICT_FALSE(out->is_err())) {
- this->UpdateVisitErrorContext(*out, mapped_value);
+ this->UpdateVisitErrorContext(*out, mapped_view);
return true;
}
}
- // Bind this node's identity to its final result, so every later
occurrence reuses it.
+ // An unchanged result binds this node's original value for later
occurrences.
+ if (ExpectedUnsafe::GetData(*out).type_index() ==
TypeIndex::kTVMFFIUnchanged) {
+ // Bind this node's identity to its final result, so every later
occurrence reuses it.
+ if (remappable) {
+ Expected<void> set_result = this->VarRemapSetExpected(value, value);
+ if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
+ *out = Unexpected(std::move(set_result).error());
+ }
+ }
+ return true;
+ }
if (remappable) {
- Expected<void> set_result = this->VarRemapSetExpected(value,
ExpectedUnsafe::GetData(*out));
+ Expected<void> set_result =
+ this->VarRemapSetExpected(value,
AnyView(ExpectedUnsafe::GetData(*out)));
if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
*out = Unexpected(std::move(set_result).error());
}
@@ -1241,11 +1494,8 @@ class StructuralMapDynEngine : public Parent {
// A FreeVar or DAG node maps once and every later occurrence reuses that
result.
const bool remappable = this->IsRemappableIdentity(value.type_index());
if (remappable) {
+ // Only None means no cached remap; every other result is returned
directly.
Expected<Any> mapped = this->VarRemapGetExpected(value);
- if (mapped.is_err()) {
- *out = std::move(mapped);
- return true;
- }
if (ExpectedUnsafe::GetData(mapped).type_index() !=
TypeIndex::kTVMFFINone) {
*out = std::move(mapped);
return true;
@@ -1262,25 +1512,30 @@ class StructuralMapDynEngine : public Parent {
*out = std::move(callback_result);
return true;
}
- Any mapped_value = ExpectedUnsafe::GetData(callback_result);
+ Any mapped_value = std::move(ExpectedUnsafe::GetData(callback_result));
+ const AnyView descent_view =
+ mapped_value.type_index() == TypeIndex::kTVMFFIUnchanged ? value :
AnyView(mapped_value);
*out = [&]() -> Expected<Any> {
if constexpr (kMaybeInplace) {
- const TVMFFIAny* mapped_data =
AnyUnsafe::TVMFFIAnyPtrFromAny(mapped_value);
+ const TVMFFIAny descent_data = descent_view.CopyToTVMFFIAny();
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) {
+ if (descent_data.type_index == input_data.type_index &&
+ descent_data.zero_padding == input_data.zero_padding &&
+ descent_data.v_int64 == input_data.v_int64) {
return this->DefaultMaybeInplaceMutateExpected(value);
}
- const Object* mapped_obj = mapped_value.as<Object>();
+ const Object* mapped_obj = descent_view.as<Object>();
bool can_inplace = mapped_obj != nullptr && mapped_obj->unique();
- return can_inplace ?
this->DefaultMaybeInplaceMutateExpected(mapped_value)
- : this->DefaultMutateExpected(mapped_value);
+ return can_inplace ?
this->DefaultMaybeInplaceMutateExpected(descent_view)
+ : this->DefaultMutateExpected(descent_view);
} else {
- return this->DefaultMutateExpected(mapped_value);
+ return this->DefaultMutateExpected(descent_view);
}
}();
if (TVM_FFI_PREDICT_FALSE(out->is_err())) return true;
+ if (ExpectedUnsafe::GetData(*out).type_index() ==
TypeIndex::kTVMFFIUnchanged) {
+ *out = std::move(mapped_value);
+ }
} else {
// Post-order: children are mapped first, so the callback sees the
rebuilt node.
Expected<Any> descended = kMaybeInplace ?
this->DefaultMaybeInplaceMutateExpected(value)
@@ -1289,28 +1544,43 @@ class StructuralMapDynEngine : public Parent {
*out = std::move(descended);
return true;
}
- const Any& mapped_value = ExpectedUnsafe::GetData(descended);
+ // See the typed engine: a borrowed view of the original, never an
owning copy of it.
+ const Any& descended_value = ExpectedUnsafe::GetData(descended);
+ const AnyView mapped_view = descended_value.type_index() ==
TypeIndex::kTVMFFIUnchanged
+ ? value
+ : AnyView(descended_value);
// Selection used the input node. Recheck the descended node against
that same registered
// target before invoking the saved link.
if (TVM_FFI_PREDICT_FALSE(
- !details::RuntimeTypeIndexMatch(mapped_value.type_index(),
link_type_index))) {
+ !details::RuntimeTypeIndexMatch(mapped_view.type_index(),
link_type_index))) {
*out = this->SMutateDescentTypeError();
- this->UpdateVisitErrorContext(*out, mapped_value);
+ this->UpdateVisitErrorContext(*out, mapped_view);
return true;
}
// WithDefRegionKind restores its state through RAII, so this late read
is equivalent to
// the typed engine's invocation-time read even after recursive descent.
- *out = InvokeLink(*matched, with_kind, mapped_value,
this->def_region_kind());
+ *out = InvokeLink(*matched, with_kind, mapped_view,
this->def_region_kind());
if (TVM_FFI_PREDICT_FALSE(out->is_err())) {
- this->UpdateVisitErrorContext(*out, mapped_value);
+ this->UpdateVisitErrorContext(*out, mapped_view);
return true;
}
}
- // --- identity remap, exit half ------------------------------------------
- // Bind this node's identity to its final result for later occurrences.
+ // An unchanged result binds this node's original value for later
occurrences.
+ if (ExpectedUnsafe::GetData(*out).type_index() ==
TypeIndex::kTVMFFIUnchanged) {
+ // --- identity remap, exit half ----------------------------------------
+ // Bind this node's identity to its final result for later occurrences.
+ if (remappable) {
+ Expected<void> set_result = this->VarRemapSetExpected(value, value);
+ if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
+ *out = Unexpected(std::move(set_result).error());
+ }
+ }
+ return true;
+ }
if (remappable) {
- Expected<void> set_result = this->VarRemapSetExpected(value,
ExpectedUnsafe::GetData(*out));
+ Expected<void> set_result =
+ this->VarRemapSetExpected(value,
AnyView(ExpectedUnsafe::GetData(*out)));
if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
*out = Unexpected(std::move(set_result).error());
}
@@ -1484,12 +1754,12 @@ class StructuralMutateEngine : public Parent {
* argument is borrowed and must not be retained after the callback returns.
*
* Each callback should follow map semantics: it must not mutate the input in
place and should
- * return a bare Any-convertible replacement or ``Expected<U>`` where ``U`` is
Any-convertible.
- * A callback may instead return an error value or throw ``Error`` to stop the
mapping. In
- * pre-order, an unchanged input or uniquely owned replacement may
- * continue through ``MaybeInplaceMutate``; a shared replacement uses
``Mutate``. In post-order,
- * the callback runs after the node's optional in-place mutation. In-place
mutation is available
- * only through an explicit ``__s_maybe_inplace_mutate__`` hook.
+ * return a bare Any-convertible replacement, ``Unchanged``,
``UnchangedOr<U>``, or
+ * ``Expected<U>`` where ``U`` is a supported result type. A callback may
instead return an error
+ * value or throw ``Error`` to stop the mapping. In pre-order, an unchanged
input or uniquely owned
+ * replacement may continue through ``MaybeInplaceMutate``; a shared
replacement uses ``Mutate``.
+ * In post-order, the callback runs after the node's optional in-place
mutation. In-place mutation
+ * is available only through an explicit ``__s_maybe_inplace_mutate__`` hook.
*
* Objects marked ``kTVMFFISEqHashKindFreeVar`` or
``kTVMFFISEqHashKindDAGNode`` are
* identity-substituted. A callback is invoked only for the first occurrence
of each identity; its
@@ -1519,11 +1789,11 @@ class StructuralMutateEngine : public Parent {
* \tparam Callbacks Callback types whose first parameters select matching
values.
* \param root The owning root value to map.
* \param callbacks Callbacks tested in declaration order. Each accepts
``(value)`` or
- * ``(value, def_region_kind)`` and returns a bare Any-convertible
replacement,
- * ``Expected<U>`` where ``U`` is Any-convertible, or an error value.
+ * ``(value, def_region_kind)`` and returns a replacement,
``Unchanged``, or
+ * ``Expected<Any>``.
* \return The mapped owning value, or an Error if mapping or a callback fails.
*
- * \note Returning ``Expected<U>`` expresses errors as values; throwing
``Error`` is also
+ * \note Returning ``Expected<Any>`` expresses errors as values; throwing
``Error`` is also
* supported and is converted to the error state.
* \note Pass an owned root with ``std::move(root)`` to permit root reuse. In a
* ``__s_maybe_inplace_mutate__`` hook, a nested owned field follows the
idiom
@@ -1536,7 +1806,11 @@ Expected<Any> StructuralMapExpected(
static_assert(sizeof...(Callbacks) != 0, "StructuralMap requires at least
one callback");
using Mutator = StructuralMapEngine<StructuralMapEngineBase, order,
std::decay_t<Callbacks>...>;
StructuralMutator
mutator(make_object<Mutator>(std::forward<Callbacks>(callbacks)...));
- return mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ auto result = mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ if (TVM_FFI_PREDICT_FALSE(result.is_err())) return
Unexpected(std::move(result).error());
+ UnchangedOr<Any> mapped =
details::AnyUnsafe::MoveFromAnyAfterCheck<UnchangedOr<Any>>(
+ std::move(details::ExpectedUnsafe::GetData(result)));
+ return std::move(mapped).ValueOrUnchanged(std::move(root));
}
/*!
@@ -1549,12 +1823,12 @@ Expected<Any> StructuralMapExpected(
* \tparam Callbacks Callback types whose first parameters select matching
values.
* \param root The owning root value to map.
* \param callbacks Callbacks tested in declaration order. Each accepts
``(value)`` or
- * ``(value, def_region_kind)`` and returns a bare Any-convertible
replacement,
- * ``Expected<U>`` where ``U`` is Any-convertible, or an error value.
+ * ``(value, def_region_kind)`` and returns a replacement,
``Unchanged``, or
+ * ``Expected<Any>``.
* \return The mapped owning value.
* \throws Error if mapping or a callback fails.
*
- * \note Returning ``Expected<U>`` expresses errors as values; throwing
``Error`` is also
+ * \note Returning ``Expected<Any>`` expresses errors as values; throwing
``Error`` is also
* supported and is rethrown by this interface.
* \note Pass an owned root with ``std::move(root)`` to permit root reuse.
*/
@@ -1569,15 +1843,14 @@ Any StructuralMap(Any root,
/*!
* \brief Mutate a structured value with callbacks that own recursion.
*
- * A callback takes one of two signatures:
+ * A callback takes one of two forms, where ``R`` is a supported return type:
*
- * - ``Expected<Any>(const T& value, StructuralMutatorObj* mutator)``
- * - ``Expected<Any>(const T& value, StructuralMutatorObj* mutator, bool
allow_inplace)``
+ * - ``R(const T& value, StructuralMutatorObj* mutator)``
+ * - ``R(const T& value, StructuralMutatorObj* mutator, bool allow_inplace)``
*
- * The returned ``Any`` is the replacement for ``value``; an ``Error`` fails
the
- * mutation. The first argument selects by FFI type; callbacks are tried in
- * declaration order and the first match owns mutation -- it drives its own
- * recursion through the mutator and sets any variable remapping. An unmatched
+ * A callback returns a replacement, ``Unchanged``, or ``Expected<Any>``. The
first argument
+ * selects by FFI type; callbacks are tried in declaration order and the first
match owns mutation
+ * -- it drives its own recursion through the mutator and sets any variable
remapping. An unmatched
* value takes registered or reflected default mutation.
*
* \param root The owning root value to mutate.
@@ -1599,7 +1872,11 @@ Expected<Any> StructuralMutateExpected(
static_assert(sizeof...(Callbacks) != 0, "StructuralMutate requires at least
one callback");
using Mutator = StructuralMutateEngine<StructuralMapEngineBase,
std::decay_t<Callbacks>...>;
StructuralMutator
mutator(make_object<Mutator>(std::forward<Callbacks>(callbacks)...));
- return mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ auto result = mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ if (TVM_FFI_PREDICT_FALSE(result.is_err())) return
Unexpected(std::move(result).error());
+ UnchangedOr<Any> mapped =
details::AnyUnsafe::MoveFromAnyAfterCheck<UnchangedOr<Any>>(
+ std::move(details::ExpectedUnsafe::GetData(result)));
+ return std::move(mapped).ValueOrUnchanged(std::move(root));
}
/*!
@@ -1614,6 +1891,48 @@ Any StructuralMutate(Any root,
return StructuralMutateExpected(std::move(root),
std::forward<Callbacks>(callbacks)...).value();
}
+template <typename T>
+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();
+ }
+
+ TVM_FFI_INLINE static void MoveToAny(UnchangedOr<T> src, TVMFFIAny* result) {
+ *result = details::UnchangedOrUnsafe::MoveToTVMFFIAny(std::move(src));
+ }
+
+ TVM_FFI_INLINE static bool CheckAnyStrict(const TVMFFIAny* src) {
+ if constexpr (std::is_same_v<T, Any>) {
+ return src->type_index != TypeIndex::kTVMFFIError;
+ } else {
+ return src->type_index == TypeIndex::kTVMFFIUnchanged ||
TypeTraits<T>::CheckAnyStrict(src);
+ }
+ }
+
+ TVM_FFI_INLINE static UnchangedOr<T> CopyFromAnyViewAfterCheck(const
TVMFFIAny* src) {
+ if (src->type_index == TypeIndex::kTVMFFIUnchanged) return Unchanged();
+ if constexpr (std::is_same_v<T, Any>) {
+ return UnchangedOr<T>(Any(AnyView::CopyFromTVMFFIAny(*src)));
+ } else {
+ return UnchangedOr<T>(TypeTraits<T>::CopyFromAnyViewAfterCheck(src));
+ }
+ }
+
+ TVM_FFI_INLINE static UnchangedOr<T> MoveFromAnyAfterCheck(TVMFFIAny* src) {
+ return UnchangedOr<T>(typename UnchangedOr<T>::UnsafeInit{},
+ details::AnyUnsafe::MoveTVMFFIAnyToAny(src));
+ }
+ TVM_FFI_INLINE static std::string TypeStr() {
+ return "UnchangedOr<" + details::Type2Str<T>::v() + ">";
+ }
+ TVM_FFI_INLINE static std::string TypeSchema() {
+ return R"({"type":"UnchangedOr","args":[)" + details::TypeSchema<T>::v() +
"]}";
+ }
+};
+
} // namespace ffi
} // namespace tvm
diff --git a/include/tvm/ffi/extra/structural_visit.h
b/include/tvm/ffi/extra/structural_visit.h
index 93fa6cb5..8d83b1d3 100644
--- a/include/tvm/ffi/extra/structural_visit.h
+++ b/include/tvm/ffi/extra/structural_visit.h
@@ -517,13 +517,13 @@ namespace details {
*
* \param Result An expression yielding the descent result to inspect.
*/
-#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::MaybeReturnHelper(::std::move(tvm_ffi_res_)); \
- }
\
+#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::ExpectedReturnHelper(::std::move(tvm_ffi_res_)); \
+ }
\
} while (0)
} // namespace details
diff --git a/include/tvm/ffi/type_traits.h b/include/tvm/ffi/type_traits.h
index 45e8749d..ae08213e 100644
--- a/include/tvm/ffi/type_traits.h
+++ b/include/tvm/ffi/type_traits.h
@@ -77,6 +77,8 @@ struct StaticTypeKey {
static constexpr const char* kTVMFFISmallStr = "ffi.SmallStr";
/*! \brief The type key for SmallBytes */
static constexpr const char* kTVMFFISmallBytes = "ffi.SmallBytes";
+ /*! \brief The type key for Unchanged */
+ static constexpr const char* kTVMFFIUnchanged = "ffi.Unchanged";
/*! \brief The type key for Error */
static constexpr const char* kTVMFFIError = "ffi.Error";
/*! \brief The type key for Bytes */
diff --git a/python/tvm_ffi/cython/base.pxi b/python/tvm_ffi/cython/base.pxi
index da727199..a40da30f 100644
--- a/python/tvm_ffi/cython/base.pxi
+++ b/python/tvm_ffi/cython/base.pxi
@@ -139,6 +139,7 @@ cdef extern from "tvm/ffi/c_api.h":
kTVMFFIObjectRValueRef = 10
kTVMFFISmallStr = 11
kTVMFFISmallBytes = 12
+ kTVMFFIUnchanged = 13
kTVMFFIStaticObjectBegin = 64
kTVMFFIObject = 64
kTVMFFIStr = 65
diff --git a/rust/tvm-ffi-sys/src/c_api.rs b/rust/tvm-ffi-sys/src/c_api.rs
index 50ace68c..3fa37e89 100644
--- a/rust/tvm-ffi-sys/src/c_api.rs
+++ b/rust/tvm-ffi-sys/src/c_api.rs
@@ -56,6 +56,8 @@ pub enum TVMFFITypeIndex {
kTVMFFISmallStr = 11,
/// Small bytes on stack
kTVMFFISmallBytes = 12,
+ /// Structural-mutation marker indicating that no new value was produced
+ kTVMFFIUnchanged = 13,
/// Start of statically defined objects.
kTVMFFIStaticObjectBegin = 64,
/// String object, layout = { TVMFFIObject, TVMFFIByteArray, ... }
diff --git a/rust/tvm-ffi/src/extra/structural_mutate.rs
b/rust/tvm-ffi/src/extra/structural_mutate.rs
index 1ba0986b..effebda0 100644
--- a/rust/tvm-ffi/src/extra/structural_mutate.rs
+++ b/rust/tvm-ffi/src/extra/structural_mutate.rs
@@ -2239,7 +2239,12 @@ fn call_mutator(
};
with_mutator_def_region(mutator, def_region_kind, || unsafe {
let view = AnyView::from_raw_ffi_any(raw);
- result_from_raw(callback(mutator, view))
+ let result = result_from_raw(callback(mutator, view))?;
+ if result.type_index() == TVMFFITypeIndex::kTVMFFIUnchanged as i32 {
+ owned_from_raw(raw)
+ } else {
+ Ok(result)
+ }
})
}
@@ -2279,7 +2284,7 @@ fn call_structural_mutate_hook(
attr: TVMFFIAny,
) -> Result<Any> {
with_mutator_def_region(mutator, def_region_kind, || unsafe {
- match attr.type_index {
+ let result = match attr.type_index {
x if x == TVMFFITypeIndex::kTVMFFIOpaquePtr as i32 => {
let pointer = attr.data_union.v_ptr;
if pointer.is_null() {
@@ -2303,6 +2308,11 @@ fn call_structural_mutate_hook(
"__s_mutate__ must be an opaque function pointer or
ffi.Function",
"",
)),
+ }?;
+ if result.type_index() == TVMFFITypeIndex::kTVMFFIUnchanged as i32 {
+ owned_from_raw(raw)
+ } else {
+ Ok(result)
}
})
}
diff --git a/rust/tvm-ffi/tests/test_structural_mutate.rs
b/rust/tvm-ffi/tests/test_structural_mutate.rs
index feadbebb..0e951bba 100644
--- a/rust/tvm-ffi/tests/test_structural_mutate.rs
+++ b/rust/tvm-ffi/tests/test_structural_mutate.rs
@@ -366,6 +366,16 @@ fn reflected_no_change_returns_original() {
assert_eq!(any_object_pointer(&mapped), any_object_pointer(&source));
}
+#[test]
+fn native_unchanged_hook_returns_original_without_exposing_marker() {
+ let source = Any::from(FfiString::from("unchanged"));
+ let source_pointer = any_object_pointer(&source);
+ let mutated = structural_mutate(source, &mut
ManualIncrement::default()).unwrap();
+
+ assert_eq!(any_object_pointer(&mutated), source_pointer);
+ assert_ne!(mutated.type_index(), TypeIndex::kTVMFFIUnchanged as i32);
+}
+
#[test]
fn reflected_object_without_shallow_copy_is_rejected_even_when_unchanged() {
// Keep the C++ test library linked for its startup registrations.
diff --git a/src/ffi/extra/structural_mutate.cc
b/src/ffi/extra/structural_mutate.cc
index 6060fb5e..9dff14f7 100644
--- a/src/ffi/extra/structural_mutate.cc
+++ b/src/ffi/extra/structural_mutate.cc
@@ -52,15 +52,20 @@ Expected<Any> StructuralMapExpected(
Any root, // NOLINT(performance-unnecessary-value-param)
const Array<Tuple<int32_t, Function>>& callbacks,
const Array<Tuple<int32_t, Function>>& callbacks_with_def_region_kind, int
order) noexcept {
- if (order == static_cast<int>(WalkOrder::kPreOrder)) {
- using Mutator = StructuralMapDynEngine<StructuralMapEngineBase,
WalkOrder::kPreOrder>;
- StructuralMutator mutator(make_object<Mutator>(callbacks,
callbacks_with_def_region_kind));
- return mutator->MaybeInplaceMutateIfUniqueExpected(root);
- } else {
+ Expected<UnchangedOr<Any>> result = [&]() -> Expected<UnchangedOr<Any>> {
+ if (order == static_cast<int>(WalkOrder::kPreOrder)) {
+ using Mutator = StructuralMapDynEngine<StructuralMapEngineBase,
WalkOrder::kPreOrder>;
+ StructuralMutator mutator(make_object<Mutator>(callbacks,
callbacks_with_def_region_kind));
+ return mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ }
using Mutator = StructuralMapDynEngine<StructuralMapEngineBase,
WalkOrder::kPostOrder>;
StructuralMutator mutator(make_object<Mutator>(callbacks,
callbacks_with_def_region_kind));
return mutator->MaybeInplaceMutateIfUniqueExpected(root);
- }
+ }();
+ if (TVM_FFI_PREDICT_FALSE(result.is_err())) return
Unexpected(std::move(result).error());
+ UnchangedOr<Any> mapped = AnyUnsafe::MoveFromAnyAfterCheck<UnchangedOr<Any>>(
+ std::move(ExpectedUnsafe::GetData(result)));
+ return std::move(mapped).ValueOrUnchanged(std::move(root));
}
/*! \brief Runtime counterpart of the typed callback-owned mutate engine. */
@@ -142,7 +147,11 @@ Expected<Any> StructuralMutateExpected(
const Array<Tuple<int32_t, Function, bool>>& callbacks) noexcept {
using Mutator = StructuralMutateDynEngine<StructuralMapEngineBase>;
StructuralMutator mutator(make_object<Mutator>(callbacks));
- return mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ auto result = mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ if (TVM_FFI_PREDICT_FALSE(result.is_err())) return
Unexpected(std::move(result).error());
+ UnchangedOr<Any> mapped = AnyUnsafe::MoveFromAnyAfterCheck<UnchangedOr<Any>>(
+ std::move(ExpectedUnsafe::GetData(result)));
+ return std::move(mapped).ValueOrUnchanged(std::move(root));
}
// ---------------------------------------------------------------------------
@@ -171,8 +180,9 @@ TVM_FFI_INLINE TVMFFIAny
MutateSeqContainerChanged(StructuralMutatorObj* mutator
for (int64_t i = index + 1; i < size; ++i) {
const Any& item = items[i];
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, mapped_value,
mutator->MutateExpected(item));
- output->SetItemAfterCheck(i, std::move(mapped_value));
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, mapped_value,
+ mutator->MutateExpected(item));
+ output->SetItemAfterCheck(i,
std::move(mapped_value).ValueOrUnchanged(AnyView(item)));
}
return AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(output)));
}
@@ -182,24 +192,23 @@ TVM_FFI_INLINE TVMFFIAny
MutateSeqContainerChanged(StructuralMutatorObj* mutator
*
* \tparam SeqObj The underlying sequence object type.
* \param mutator The active structural mutator.
- * \param value The borrowed sequence container.
- * \param self The sequence object stored in \p value.
+ * \param self The source sequence object.
* \return The mutated sequence, or an Error.
*/
template <typename SeqObj>
-TVMFFIAny MutateSeqContainerRaw(StructuralMutatorObj* mutator, AnyView value,
- const SeqObj* self) noexcept {
+TVMFFIAny MutateSeqContainerRaw(StructuralMutatorObj* mutator, const SeqObj*
self) noexcept {
int64_t size = static_cast<int64_t>(self->size());
const Any* items = self->begin();
for (int64_t i = 0; i < size; ++i) {
const Any& item = items[i];
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, mapped_value,
mutator->MutateExpected(item));
- if (!item.same_as(mapped_value)) {
- return MutateSeqContainerChanged(mutator, self, i,
std::move(mapped_value));
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, mapped_value,
+ mutator->MutateExpected(item));
+ if (!mapped_value.UnchangedOrSameAs(item)) {
+ return MutateSeqContainerChanged(mutator, self, i,
std::move(mapped_value).ValueUnchecked());
}
}
- return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
+ return Unchanged().CopyToTVMFFIAny();
}
/*!
@@ -207,23 +216,20 @@ TVMFFIAny MutateSeqContainerRaw(StructuralMutatorObj*
mutator, AnyView value,
*
* \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 self The sequence object stored in \p value.
+ * \param self The sequence object, which must be safe to mutate in place.
* \return The mutated sequence, or an Error.
*/
template <typename SeqObj>
-TVMFFIAny MaybeInplaceMutateSeqContainerRaw(StructuralMutatorObj* mutator,
AnyView value,
- SeqObj* self) noexcept {
+TVMFFIAny MaybeInplaceMutateSeqContainerRaw(StructuralMutatorObj* mutator,
SeqObj* self) noexcept {
for (int64_t i = 0; i < static_cast<int64_t>(self->size()); ++i) {
const Any& item = self->begin()[i];
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, mapped_value,
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, mapped_value,
mutator->MaybeInplaceMutateIfUniqueExpected(item));
-
- if (!item.same_as(mapped_value)) {
- self->SetItemAfterCheck(i, std::move(mapped_value));
+ if (!mapped_value.UnchangedOrSameAs(item)) {
+ self->SetItemAfterCheck(i, std::move(mapped_value).ValueUnchecked());
}
}
- return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
+ return Unchanged().CopyToTVMFFIAny();
}
/*!
@@ -253,9 +259,10 @@ TVM_FFI_INLINE TVMFFIAny
MutateMapValuesChanged(StructuralMutatorObj* mutator,
for (; source_it != self->end(); ++source_it, ++output_it) {
const Any& old_value = source_it->second;
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, new_value,
mutator->MutateExpected(old_value));
- if (!old_value.same_as(new_value)) {
- output_it->second = std::move(new_value);
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, new_value,
+ mutator->MutateExpected(old_value));
+ if (!new_value.UnchangedOrSameAs(old_value)) {
+ output_it->second = std::move(new_value).ValueUnchecked();
}
}
return AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(output)));
@@ -266,22 +273,22 @@ TVM_FFI_INLINE TVMFFIAny
MutateMapValuesChanged(StructuralMutatorObj* mutator,
*
* \tparam MapObjType The underlying map object type.
* \param mutator The active structural mutator.
- * \param value The borrowed map container.
- * \param self The map object stored in \p value.
+ * \param self The source map object.
* \return The mutated map, or an Error.
*/
template <typename MapObjType>
-TVMFFIAny MutateMapValuesRaw(StructuralMutatorObj* mutator, AnyView value,
- const MapObjType* self) noexcept {
+TVMFFIAny MutateMapValuesRaw(StructuralMutatorObj* mutator, const MapObjType*
self) noexcept {
size_t index = 0;
for (auto source_it = self->begin(); source_it != self->end(); ++source_it,
++index) {
const Any& old_value = source_it->second;
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, new_value,
mutator->MutateExpected(old_value));
- if (!old_value.same_as(new_value)) {
- return MutateMapValuesChanged(mutator, self, source_it, index,
std::move(new_value));
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, new_value,
+ mutator->MutateExpected(old_value));
+ if (!new_value.UnchangedOrSameAs(old_value)) {
+ return MutateMapValuesChanged(mutator, self, source_it, index,
+ std::move(new_value).ValueUnchecked());
}
}
- return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
+ return Unchanged().CopyToTVMFFIAny();
}
/*!
@@ -289,77 +296,73 @@ TVMFFIAny MutateMapValuesRaw(StructuralMutatorObj*
mutator, AnyView value,
*
* \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 self The map object stored in \p value.
+ * \param self The map object, which must be safe to mutate in place.
* \return The mutated map, or an Error.
*/
template <typename MapObjType>
-TVMFFIAny MaybeInplaceMutateMapValuesRaw(StructuralMutatorObj* mutator,
AnyView value,
- MapObjType* self) noexcept {
+TVMFFIAny MaybeInplaceMutateMapValuesRaw(StructuralMutatorObj* mutator,
MapObjType* self) noexcept {
for (auto it = self->begin(); it != self->end(); ++it) {
const Any& old_value = it->second;
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, new_value,
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, new_value,
mutator->MaybeInplaceMutateIfUniqueExpected(old_value));
-
- if (!old_value.same_as(new_value)) {
- it->second = std::move(new_value);
+ if (!new_value.UnchangedOrSameAs(old_value)) {
+ it->second = std::move(new_value).ValueUnchecked();
}
}
- return AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
+ return Unchanged().CopyToTVMFFIAny();
}
/*! \brief Identity structural mutation hook for immutable String and Bytes
leaves. */
-TVMFFIAny MutateImmutableLeaf(StructuralMutatorObj*, AnyView value) noexcept {
- Expected<Any> result = Any(value);
- return ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+TVMFFIAny MutateImmutableLeaf(StructuralMutatorObj*, AnyView) noexcept {
+ return Unchanged().CopyToTVMFFIAny();
}
/*! \brief Structural mutation hook for ArrayObj. */
TVMFFIAny MutateArray(StructuralMutatorObj* mutator, AnyView value) noexcept {
return MutateSeqContainerRaw(
- mutator, value,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const ArrayObj>(value));
+ mutator, details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
ArrayObj>(value));
}
/*! \brief Maybe-in-place structural mutation hook for ArrayObj. */
TVMFFIAny MaybeInplaceMutateArray(StructuralMutatorObj* mutator, AnyView
value) noexcept {
return MaybeInplaceMutateSeqContainerRaw(
- mutator, value,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<ArrayObj>(value));
+ mutator,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<ArrayObj>(value));
}
/*! \brief Structural mutation hook for ListObj. */
TVMFFIAny MutateList(StructuralMutatorObj* mutator, AnyView value) noexcept {
return MutateSeqContainerRaw(
- mutator, value,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const ListObj>(value));
+ mutator, details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
ListObj>(value));
}
/*! \brief Maybe-in-place structural mutation hook for ListObj. */
TVMFFIAny MaybeInplaceMutateList(StructuralMutatorObj* mutator, AnyView value)
noexcept {
return MaybeInplaceMutateSeqContainerRaw(
- mutator, value,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<ListObj>(value));
+ mutator,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<ListObj>(value));
}
/*! \brief Structural mutation hook for MapObj. */
TVMFFIAny MutateMap(StructuralMutatorObj* mutator, AnyView value) noexcept {
return MutateMapValuesRaw(
- mutator, value,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const MapObj>(value));
+ mutator, details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
MapObj>(value));
}
/*! \brief Maybe-in-place structural mutation hook for MapObj. */
TVMFFIAny MaybeInplaceMutateMap(StructuralMutatorObj* mutator, AnyView value)
noexcept {
return MaybeInplaceMutateMapValuesRaw(
- mutator, value,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<MapObj>(value));
+ mutator,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<MapObj>(value));
}
/*! \brief Structural mutation hook for DictObj. */
TVMFFIAny MutateDict(StructuralMutatorObj* mutator, AnyView value) noexcept {
return MutateMapValuesRaw(
- mutator, value,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const DictObj>(value));
+ mutator, details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
DictObj>(value));
}
/*! \brief Maybe-in-place structural mutation hook for DictObj. */
TVMFFIAny MaybeInplaceMutateDict(StructuralMutatorObj* mutator, AnyView value)
noexcept {
return MaybeInplaceMutateMapValuesRaw(
- mutator, value,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<DictObj>(value));
+ mutator,
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<DictObj>(value));
}
} // namespace details
@@ -371,10 +374,16 @@ TVM_FFI_STATIC_INIT_BLOCK() {
namespace refl = tvm::ffi::reflection;
refl::ObjectDef<StructuralMutatorObj>(); // NOLINT(bugprone-unused-raii)
refl::GlobalDef()
- .def_method("ffi.StructuralMutatorMutate", &StructuralMutatorObj::Mutate)
- .def_method("ffi.StructuralMutatorDefaultMutate",
+ .def_method("ffi.StructuralMutatorMutate",
[](const StructuralMutator& mutator, AnyView value) {
- return mutator->DefaultMutateExpected(value).value();
+ return
std::move(mutator->Mutate(value)).ValueOrUnchanged(value);
+ })
+ .def_method("ffi.StructuralMutatorDefaultMutate",
+ [](const StructuralMutator& mutator, AnyView value) -> Any {
+ UnchangedOr<Any> result =
+
details::AnyUnsafe::MoveFromAnyAfterCheck<UnchangedOr<Any>>(
+
std::move(mutator->DefaultMutateExpected(value)).value());
+ return std::move(result).ValueOrUnchanged(value);
})
.def_method("ffi.StructuralMutatorVarRemapGet",
[](const StructuralMutator& mutator, AnyView var) {
diff --git a/src/ffi/object.cc b/src/ffi/object.cc
index 4daa9652..5cb84974 100644
--- a/src/ffi/object.cc
+++ b/src/ffi/object.cc
@@ -414,6 +414,7 @@ class TypeTable {
TypeIndex::kTVMFFIObjectRValueRef);
ReserveBuiltinTypeIndex(StaticTypeKey::kTVMFFISmallStr,
TypeIndex::kTVMFFISmallStr);
ReserveBuiltinTypeIndex(StaticTypeKey::kTVMFFISmallBytes,
TypeIndex::kTVMFFISmallBytes);
+ ReserveBuiltinTypeIndex(StaticTypeKey::kTVMFFIUnchanged,
TypeIndex::kTVMFFIUnchanged);
// reserved static type indices for depth 1 object types
ReserveDepthOneObjectTypeIndex(StaticTypeKey::kTVMFFIStr,
TypeIndex::kTVMFFIStr);
ReserveDepthOneObjectTypeIndex(StaticTypeKey::kTVMFFIBytes,
TypeIndex::kTVMFFIBytes);
diff --git a/tests/cpp/extra/test_structural_mutate.cc
b/tests/cpp/extra/test_structural_mutate.cc
index b99b0a3c..5ad0c89d 100644
--- a/tests/cpp/extra/test_structural_mutate.cc
+++ b/tests/cpp/extra/test_structural_mutate.cc
@@ -40,6 +40,160 @@ using namespace tvm::ffi::testing;
using AnyArray = Array<Any>;
using StringMap = Map<String, Any>;
+// ---------------------------------------------------------------------------
+// Unchanged result protocol.
+// ---------------------------------------------------------------------------
+
+Expected<UnchangedOr<String>> ReturnTypedUnchangedExpected() noexcept { return
Unchanged(); }
+
+TEST(UnchangedOr, ErrorRoundTrip) {
+ static_assert(std::is_copy_constructible_v<UnchangedOr<String>>);
+
+ UnchangedOr<String> original = String("unchanged-or special-member value");
+ UnchangedOr<String> copied_value(original);
+ UnchangedOr<String> copy_assigned = Unchanged();
+ copy_assigned = original;
+ EXPECT_EQ(std::move(copied_value).ValueUnchecked(), "unchanged-or
special-member value");
+ EXPECT_EQ(std::move(copy_assigned).ValueUnchecked(), "unchanged-or
special-member value");
+
+ UnchangedOr<String> moved_value(std::move(original));
+ UnchangedOr<String> move_source = String("unchanged-or move-assignment
value");
+ UnchangedOr<String> move_assigned = Unchanged();
+ move_assigned = std::move(move_source);
+ EXPECT_EQ(std::move(moved_value).ValueUnchecked(), "unchanged-or
special-member value");
+ EXPECT_EQ(std::move(move_assigned).ValueUnchecked(), "unchanged-or
move-assignment value");
+
+ Expected<UnchangedOr<Any>> failure = Error("ValueError", "expected failure",
"");
+ const Any copied_storage(failure);
+ Expected<UnchangedOr<Any>> copied =
+
details::AnyUnsafe::CopyFromAnyViewAfterCheck<Expected<UnchangedOr<Any>>>(copied_storage);
+
+ ASSERT_TRUE(copied.is_err());
+ EXPECT_EQ(copied.error().kind(), "ValueError");
+ EXPECT_EQ(copied.error().message(), "expected failure");
+
+ Any moved_storage(failure);
+ Expected<UnchangedOr<Any>> moved =
+ details::AnyUnsafe::MoveFromAnyAfterCheck<Expected<UnchangedOr<Any>>>(
+ std::move(moved_storage));
+
+ ASSERT_TRUE(moved.is_err());
+ EXPECT_EQ(moved.error().kind(), "ValueError");
+ EXPECT_EQ(moved.error().message(), "expected failure");
+
+ Expected<Any> source_error = Error("TypeError", "converted failure", "");
+ Expected<UnchangedOr<Any>> converted_error = std::move(source_error);
+ ASSERT_TRUE(converted_error.is_err());
+ EXPECT_EQ(converted_error.error().kind(), "TypeError");
+ EXPECT_EQ(converted_error.error().message(), "converted failure");
+
+ Expected<Any> unexpected_error = Unexpected(Error("IndexError", "unexpected
failure", ""));
+ ASSERT_TRUE(unexpected_error.is_err());
+ EXPECT_EQ(unexpected_error.error().kind(), "IndexError");
+ EXPECT_EQ(unexpected_error.error().message(), "unexpected failure");
+
+ Expected<UnchangedOr<Any>> unexpected_result =
+ Unexpected(Error("RuntimeError", "unchanged-or unexpected failure", ""));
+ ASSERT_TRUE(unexpected_result.is_err());
+ EXPECT_EQ(unexpected_result.error().kind(), "RuntimeError");
+ EXPECT_EQ(unexpected_result.error().message(), "unchanged-or unexpected
failure");
+}
+
+TEST(StructuralMutate, UnchangedProtocolResolvesAtThrowingEntryPoints) {
+ Expected<Any> raw_tag = []() -> Expected<Any> { return Unchanged(); }();
+ ASSERT_TRUE(raw_tag.is_ok());
+ EXPECT_EQ(details::ExpectedUnsafe::GetData(raw_tag).type_index(),
TypeIndex::kTVMFFIUnchanged);
+ EXPECT_TRUE(ReturnTypedUnchangedExpected().value().IsUnchanged());
+ auto never_matches = [](int64_t, StructuralMutatorObj*) -> Expected<Any> {
return Any(); };
+ using Mutator = StructuralMutateEngine<StructuralMapEngineBase,
decltype(never_matches)>;
+ StructuralMutator mutator(make_object<Mutator>(std::move(never_matches)));
+ String value("value longer than small-string storage");
+
+ auto untyped = mutator->MutateExpected(AnyView(value));
+ ASSERT_TRUE(untyped.is_ok());
+ EXPECT_TRUE(std::move(untyped).value().IsUnchanged());
+ auto untyped_throwing = mutator->Mutate(AnyView(value));
+ EXPECT_TRUE(untyped_throwing.IsUnchanged());
+
EXPECT_TRUE(std::move(untyped_throwing).ValueOrUnchanged(AnyView(value)).same_as(value));
+
+ auto typed = mutator->MutateExpected<String>(value);
+ ASSERT_TRUE(typed.is_ok());
+ EXPECT_TRUE(std::move(typed).value().IsUnchanged());
+ auto typed_throwing = mutator->Mutate<String>(value);
+ EXPECT_TRUE(typed_throwing.IsUnchanged());
+ String moved_value = value;
+ String moved_value_alias = moved_value;
+
EXPECT_EQ(std::move(typed_throwing).ValueOrUnchanged(std::move(moved_value)),
moved_value_alias);
+ EXPECT_TRUE(mutator->MaybeInplaceMutate(AnyView(value)).IsUnchanged());
+ EXPECT_TRUE(mutator->MaybeInplaceMutate<String>(value).IsUnchanged());
+ auto unique = mutator->MaybeInplaceMutateIfUniqueExpected(AnyView(value));
+ ASSERT_TRUE(unique.is_ok());
+ EXPECT_TRUE(std::move(unique).value().IsUnchanged());
+ auto typed_unique =
mutator->MaybeInplaceMutateIfUniqueExpected<String>(value);
+ ASSERT_TRUE(typed_unique.is_ok());
+ EXPECT_TRUE(std::move(typed_unique).value().IsUnchanged());
+ EXPECT_TRUE(StructuralMutateExpected(Any(value),
never_matches).value().same_as(value));
+ EXPECT_TRUE(StructuralMapExpected<WalkOrder::kPostOrder>(
+ Any(value), [](int64_t item) -> Expected<Any> { return
Any(item + 1); })
+ .value()
+ .same_as(value));
+
+ auto replace_int_with_string = [](int64_t value, StructuralMutatorObj*) ->
Expected<Any> {
+ if (value == -1) return Unexpected(Error("ValueError", "direct-forward
failure", ""));
+ return String("wrong replacement uses heap storage");
+ };
+ using WrongTypeMutator =
+ StructuralMutateEngine<StructuralMapEngineBase,
decltype(replace_int_with_string)>;
+ StructuralMutator wrong_type_mutator(
+ make_object<WrongTypeMutator>(std::move(replace_int_with_string)));
+
+ auto any_error = wrong_type_mutator->MutateExpected(int64_t{-1});
+ ASSERT_TRUE(any_error.is_err());
+ EXPECT_EQ(any_error.error().message(), "direct-forward failure");
+ auto any_inplace_error =
wrong_type_mutator->MaybeInplaceMutateExpected(int64_t{-1});
+ ASSERT_TRUE(any_inplace_error.is_err());
+ EXPECT_EQ(any_inplace_error.error().message(), "direct-forward failure");
+ auto typed_error = wrong_type_mutator->MutateExpected<int64_t>(int64_t{-1});
+ ASSERT_TRUE(typed_error.is_err());
+ EXPECT_EQ(typed_error.error().message(), "direct-forward failure");
+ auto typed_inplace_error =
wrong_type_mutator->MaybeInplaceMutateExpected<int64_t>(int64_t{-1});
+ ASSERT_TRUE(typed_inplace_error.is_err());
+ EXPECT_EQ(typed_inplace_error.error().message(), "direct-forward failure");
+
+ Expected<UnchangedOr<int64_t>> result =
wrong_type_mutator->MutateExpected<int64_t>(int64_t{1});
+ ASSERT_TRUE(result.is_err());
+ EXPECT_EQ(result.error().kind(), "TypeError");
+ Expected<UnchangedOr<int64_t>> inplace_result =
+ wrong_type_mutator->MaybeInplaceMutateExpected<int64_t>(int64_t{1});
+ ASSERT_TRUE(inplace_result.is_err());
+ EXPECT_EQ(inplace_result.error().kind(), "TypeError");
+ Expected<UnchangedOr<int64_t>> unique_result =
+
wrong_type_mutator->MaybeInplaceMutateIfUniqueExpected<int64_t>(int64_t{1});
+ ASSERT_TRUE(unique_result.is_err());
+ EXPECT_EQ(unique_result.error().kind(), "TypeError");
+
+ for (WalkOrder order : {WalkOrder::kPreOrder, WalkOrder::kPostOrder}) {
+ TVar root("n");
+ TVar unchanged = (order == WalkOrder::kPreOrder
+ ? StructuralMap<WalkOrder::kPreOrder>(
+ root, [](const TVar&) -> Expected<Any> {
return Unchanged(); })
+ : StructuralMap<WalkOrder::kPostOrder>(
+ root, [](const TVar&) -> Expected<Any> {
return Unchanged(); }))
+ .cast<TVar>();
+ EXPECT_TRUE(unchanged.same_as(root));
+ }
+
+ Function dynamic_unchanged = Function::FromTyped([](int64_t) -> Any { return
Unchanged(); });
+ Array<Tuple<int32_t, Function>> callbacks{
+ Tuple<int32_t, Function>(TypeIndex::kTVMFFIInt, dynamic_unchanged)};
+ Function structural_map = Function::GetGlobalRequired("ffi.StructuralMap");
+ for (WalkOrder order : {WalkOrder::kPreOrder, WalkOrder::kPostOrder}) {
+ Any unchanged = structural_map(int64_t{1}, callbacks, Array<Tuple<int32_t,
Function>>(),
+ static_cast<int32_t>(order));
+ EXPECT_EQ(unchanged.cast<int64_t>(), 1);
+ }
+}
+
class TNestedMapHookObj : public Object {
public:
AnyArray field;
@@ -48,11 +202,13 @@ class TNestedMapHookObj : public Object {
static TVMFFIAny StructuralMutate(StructuralMutatorObj* mutator, AnyView
value) noexcept {
const auto* self = value.cast<const TNestedMapHookObj*>();
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, mapped,
mutator->MutateExpected(self->field));
- AnyArray mapped_field = mapped.cast<AnyArray>();
- if (mapped_field.same_as(self->field)) {
- return details::AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, mapped,
+ mutator->MutateExpected(self->field));
+ if (mapped.UnchangedOrSameAs(Any(self->field))) {
+ return Unchanged().CopyToTVMFFIAny();
}
+ Any mapped_value = std::move(mapped).ValueOrUnchanged(Any(self->field));
+ AnyArray mapped_field = mapped_value.cast<AnyArray>();
return details::AnyUnsafe::MoveAnyToTVMFFIAny(
Any(make_object<TNestedMapHookObj>(std::move(mapped_field))));
}
@@ -198,7 +354,8 @@ TEST(StructuralMap,
ParentLayerOwnsBothDescentsAndProvidesState) {
TVar var("n");
AnyArray repeated{var, var};
- AnyArray mapped = mutator->MutateExpected(repeated).value().cast<AnyArray>();
+ AnyArray mapped =
+
std::move(mutator->Mutate<AnyArray>(repeated)).ValueOrUnchanged(std::move(repeated));
EXPECT_EQ(var_callback_count, 1);
EXPECT_TRUE(mapped[0].cast<TVar>().same_as(mapped[1].cast<TVar>()));
}
@@ -207,7 +364,11 @@ TEST(StructuralMutate,
CallbackOwnsMutationAndErrorsStayExpected) {
std::vector<int64_t> trace;
auto mutate_array = [&](const AnyArray& value, StructuralMutateLayer*
mutator) -> Expected<Any> {
EXPECT_EQ(mutator->callback_tag(), 23);
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, first,
mutator->MutateExpected(value[0]));
+ auto first_result = mutator->MutateExpected(value[0]);
+ if (TVM_FFI_PREDICT_FALSE(first_result.is_err())) {
+ return Unexpected(std::move(first_result).error());
+ }
+ Any first =
std::move(first_result).value().ValueOrUnchanged(AnyView(value[0]));
return Any(AnyArray{std::move(first), int64_t{10}});
};
auto mutate_int = [&](int64_t value, StructuralMutateLayer*) ->
Expected<Any> {
@@ -218,8 +379,8 @@ TEST(StructuralMutate,
CallbackOwnsMutationAndErrorsStayExpected) {
StructuralMutateEngine<StructuralMutateLayer, decltype(mutate_array),
decltype(mutate_int)>;
StructuralMutator mutator(make_object<Mutator>(std::move(mutate_array),
std::move(mutate_int)));
- AnyArray mapped =
- mutator->MutateExpected(AnyArray{int64_t{1},
int64_t{2}}).value().cast<AnyArray>();
+ AnyArray root{int64_t{1}, int64_t{2}};
+ AnyArray mapped =
std::move(mutator->Mutate<AnyArray>(root)).ValueOrUnchanged(std::move(root));
ASSERT_EQ(mapped.size(), 2U);
EXPECT_EQ(mapped[0].cast<int64_t>(), 2);
EXPECT_EQ(mapped[1].cast<int64_t>(), 10);
@@ -257,8 +418,11 @@ TEST(StructuralMutate, CallbackControlsRecursion) {
StructuralMutate(
root,
[](const TPair& pair, StructuralMutatorObj* mutator) ->
Expected<Any> {
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, lhs,
mutator->MutateExpected(pair->lhs));
- return Any(TPair(lhs.cast<ObjectRef>(), pair->rhs));
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<ObjectRef>,
lhs_result,
+
mutator->MutateExpected<ObjectRef>(pair->lhs));
+ ObjectRef original_lhs = pair->lhs;
+ ObjectRef lhs =
std::move(lhs_result).ValueOrUnchanged(std::move(original_lhs));
+ return Any(TPair(std::move(lhs), pair->rhs));
},
[](const TInt& value, StructuralMutatorObj*) -> Expected<Any> {
return Any(TInt(value->value + 100));
@@ -350,21 +514,23 @@ TEST(StructuralMutate, MatchedVarOwnsRemapConsistency) {
int callback_count = 0;
AnyArray mapped =
- StructuralMutate(
- root,
- [&](const TVar& value, StructuralMutatorObj* mutator) ->
Expected<Any> {
- ++callback_count;
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(Any, remapped,
mutator->VarRemapGetExpected(value));
- if (remapped.type_index() != TypeIndex::kTVMFFINone) {
- return remapped;
- }
- Any replacement(TVar(value->name + "-mapped"));
- Expected<void> set_result = mutator->VarRemapSetExpected(value,
replacement);
- if (set_result.is_err()) {
- return Unexpected(std::move(set_result).error());
- }
- return replacement;
- })
+ StructuralMutate(root,
+ [&](const TVar& value, StructuralMutatorObj* mutator)
-> Expected<Any> {
+ ++callback_count;
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>,
remapped_result,
+
mutator->VarRemapGetExpected(value));
+ Any remapped =
std::move(remapped_result).ValueUnchecked();
+ if (remapped.type_index() != TypeIndex::kTVMFFINone) {
+ return remapped;
+ }
+ Any replacement(TVar(value->name + "-mapped"));
+ Expected<void> set_result =
+ mutator->VarRemapSetExpected(value, replacement);
+ if (set_result.is_err()) {
+ return Unexpected(std::move(set_result).error());
+ }
+ return replacement;
+ })
.cast<AnyArray>();
EXPECT_EQ(callback_count, 2);
@@ -610,6 +776,22 @@ TEST(StructuralMap, PreOrderRecursivelyMapsCallbackResult)
{
EXPECT_FALSE(mapped_value.same_as(replacement));
EXPECT_EQ(replacement[0].cast<int64_t>(), 10);
EXPECT_EQ(mapped_value[0].cast<int64_t>(), 11);
+
+ // The changed callback result remains owned while its leaf descent reports
unchanged.
+ String replacement_leaf("replacement longer than small-string storage");
+ String retained =
+ StructuralMap<WalkOrder::kPreOrder>(TVar("n"), [&](const TVar&) ->
Expected<Any> {
+ return Any(replacement_leaf);
+ }).cast<String>();
+ EXPECT_EQ(retained, replacement_leaf);
+
+ // An unchanged pre-order callback still descends the original node.
+ AnyArray unchanged_root{int64_t{1}};
+ AnyArray descended_original =
+ StructuralMap<WalkOrder::kPreOrder>(
+ unchanged_root, [](const AnyArray&) -> Expected<Any> { return
Unchanged(); }, Increment)
+ .cast<AnyArray>();
+ EXPECT_EQ(descended_original[0].cast<int64_t>(), 2);
}
TEST(StructuralMap, AcceptsExpectedCallbackReturnTypes) {
@@ -811,6 +993,20 @@ Any CallDynStructuralMap(AnyView root, const
Array<Tuple<int32_t, Function>>& ca
return fn(root, callbacks, Array<Tuple<int32_t, Function>>(),
static_cast<int32_t>(order));
}
+TEST(StructuralMapDyn, PreOrderDescendsOriginalAfterUnchangedCallback) {
+ AnyArray root{int64_t{1}};
+ Function unchanged = Function::FromTyped([](const AnyArray&) -> Any { return
Unchanged(); });
+ Function increment = Function::FromTyped([](int64_t value) -> Any { return
Any(value + 1); });
+
+ AnyArray mapped =
+ CallDynStructuralMap(root,
+ {Tuple<int32_t, Function>(TypeIndex::kTVMFFIArray,
unchanged),
+ Tuple<int32_t, Function>(TypeIndex::kTVMFFIInt,
increment)},
+ WalkOrder::kPreOrder)
+ .cast<AnyArray>();
+ EXPECT_EQ(mapped[0].cast<int64_t>(), 2);
+}
+
TEST(StructuralMapDyn, ReusesRemapResultForRepeatedVar) {
// A FreeVar maps once and every later occurrence reuses that result. Both
mutators share this
// half of the walk, so it must hold identically here.
@@ -841,7 +1037,8 @@ void CheckDynamicParentLayer() {
Array<Tuple<int32_t, Function>>());
StructuralMutator mutator(engine);
- AnyArray mapped = mutator->Mutate(AnyArray{int64_t{1}}).cast<AnyArray>();
+ AnyArray root{int64_t{1}};
+ AnyArray mapped =
std::move(mutator->Mutate<AnyArray>(root)).ValueOrUnchanged(std::move(root));
EXPECT_EQ(mapped[0].cast<int64_t>(), 2);
EXPECT_EQ(calls, 1);
EXPECT_GT(engine->count().value, 0);
diff --git a/tests/cpp/testing_object.h b/tests/cpp/testing_object.h
index 4d4cc65a..5553c6c3 100644
--- a/tests/cpp/testing_object.h
+++ b/tests/cpp/testing_object.h
@@ -251,14 +251,19 @@ class TMutatePairObj : public Object {
static TVMFFIAny StructuralMutate(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));
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ObjectRef, rhs,
mutator->MutateExpected(self->rhs));
- if (lhs.same_as(self->lhs) && rhs.same_as(self->rhs)) {
- return details::AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
+ const TMutatePairObj* self =
+ details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
TMutatePairObj>(value);
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<ObjectRef>, lhs,
+ mutator->MutateExpected(self->lhs));
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<ObjectRef>, rhs,
+ mutator->MutateExpected(self->rhs));
+ if (lhs.UnchangedOrSameAs(self->lhs) && rhs.UnchangedOrSameAs(self->rhs)) {
+ return Unchanged().CopyToTVMFFIAny();
}
- return details::AnyUnsafe::MoveAnyToTVMFFIAny(
- Any(make_object<TMutatePairObj>(std::move(lhs), std::move(rhs))));
+ ObjectPtr<TMutatePairObj> copy = make_object<TMutatePairObj>(*self);
+ copy->lhs = std::move(lhs).ValueOrUnchanged(std::move(copy->lhs));
+ copy->rhs = std::move(rhs).ValueOrUnchanged(std::move(copy->rhs));
+ return details::AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(copy)));
}
static void RegisterReflection() {
diff --git a/tests/python/test_structural.py b/tests/python/test_structural.py
index 96873830..2d01511d 100644
--- a/tests/python/test_structural.py
+++ b/tests/python/test_structural.py
@@ -413,6 +413,14 @@ def
test_structural_mutate_callback_owned_recursion_and_errors() -> None:
assert nested_trace == [1, 2]
+def test_structural_mutate_callback_resolves_unchanged() -> None:
+ unchanged = tvm_ffi.structural_mutate(
+ tvm_ffi.Array(["unchanged"]),
+ (tvm_ffi.Array, lambda value, mutator: mutator.mutate(value[0])),
+ )
+ assert unchanged == "unchanged"
+
+
def test_structural_walk_nested_containers_and_skips_map_keys() -> None:
root = tvm_ffi.Array(
[