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 0f8e4a42 [API] Consolidate structural mutation entrypoints and
borrowed fallback (#786)
0f8e4a42 is described below
commit 0f8e4a429b3278dde0f41fe0ca088f9db418473a
Author: Tianqi Chen <[email protected]>
AuthorDate: Mon Sep 14 12:26:46 2026 -0400
[API] Consolidate structural mutation entrypoints and borrowed fallback
(#786)
Consolidate structural mutation into `MutateExpected(AnyView,
InplaceMode)` and `DefaultMutateExpected(AnyView, InplaceMode)`, with a
matching throwing `Mutate` entrypoint. All three default to
`InplaceMode::kDisallow`. `kAllow` permits in-place dispatch only along
an owned path with a unique current value; default descent trusts the
callback's validated mode without rechecking uniqueness. Remove the
superseded entrypoints and migrate recursive callers while preserving
the raw FFI hooks, vtable layout, and copy-on-write behavior.
C++ mutation callbacks receive the explicit mode. Python exposes
`InplaceMode.DISALLOW` and `InplaceMode.ALLOW`; optional callback modes
use their corresponding integer values through the existing FFI enum
conversion.
Add `ValueOrUnchanged(const T&) &&` to copy a borrowed fallback only
when unchanged and move replacements otherwise.
## Migration
This intentionally changes the C++ convenience API. Consumers, including
Apache TVM, must migrate these calls together with their tvm-ffi
dependency update.
| Removed call | Replacement |
| --- | --- |
| `Mutate<T>` / `MutateExpected<T>` | Use `MutateExpected(value,
InplaceMode::kDisallow)` with checked typed unwrapping below, or
`Mutate` at a throwing boundary. |
| `MaybeInplaceMutateIfUniqueExpected`, `MaybeInplaceMutate<T>` /
`MaybeInplaceMutateExpected<T>` | Use `MutateExpected(value,
inplace_mode)`, or `Mutate(value, inplace_mode)` at a throwing boundary.
|
| `DefaultMaybeInplaceMutateExpected` | Use
`DefaultMutateExpected(value, inplace_mode)` for the current node's
default descent with its already-validated mode. |
Change a typed callback's optional third argument to `InplaceMode` and
forward it explicitly during recursion. `kAllow` requires permission
along the entire ownership path, established by an owned root or a valid
in-place hook; a unique borrowed child alone does not grant that
permission. Otherwise use `kDisallow`. `MutateExpected` checks the
current value's uniqueness before callback arguments acquire ownership.
Default descent bypasses the current callback and trusts the established
mode without rechecking uniqueness.
For `Expected` helpers and raw hooks,
`TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN` preserves the fixed typed-mutation
`TypeError` diagnostic. For example, with `Expr` as the required result
type:
```cpp
Expected<Expr> MutateExpr(const Expr& original, StructuralMutatorObj*
mutator,
InplaceMode inplace_mode) {
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
UnchangedOr<Expr>, result, mutator->MutateExpected(original,
inplace_mode));
return std::move(result).ValueOrUnchanged(original);
}
```
Call `.value()` only at a throwing boundary. An explicit
`.ValueOrUnchanged(...).cast<T>()` there is also possible when its
separate runtime cast diagnostic is acceptable; it is not a replacement
for checked narrowing in an `Expected` helper or raw hook.
---
include/tvm/ffi/extra/structural_mutate.h | 353 +++++++++++++-----------------
include/tvm/ffi/reflection/accessor.h | 8 +-
python/tvm_ffi/__init__.py | 2 +
python/tvm_ffi/structural.py | 37 +++-
src/ffi/extra/structural_mutate.cc | 49 +++--
tests/cpp/extra/test_big_int.cc | 4 +-
tests/cpp/extra/test_structural_mutate.cc | 231 +++++++++++--------
tests/cpp/testing_object.h | 4 +-
tests/python/test_structural.py | 51 +++--
9 files changed, 398 insertions(+), 341 deletions(-)
diff --git a/include/tvm/ffi/extra/structural_mutate.h
b/include/tvm/ffi/extra/structural_mutate.h
index 06384b27..62a741d7 100644
--- a/include/tvm/ffi/extra/structural_mutate.h
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -49,6 +49,19 @@
namespace tvm {
namespace ffi {
+/*!
+ * \brief Permission to mutate a value in place along its ownership path.
+ *
+ * \note The numeric values 0 and 1 are part of the Python integer binding
contract,
+ * matching ``InplaceMode.DISALLOW`` and ``InplaceMode.ALLOW``
respectively.
+ */
+enum class InplaceMode : int32_t {
+ /*! \brief Preserve the source through copy-on-write mutation. */
+ kDisallow = 0,
+ /*! \brief Permit in-place mutation when the current value is uniquely
owned. */
+ kAllow = 1,
+};
+
class StructuralMutatorObj;
template <typename T>
class UnchangedOr;
@@ -298,6 +311,16 @@ class UnchangedOr {
:
details::AnyUnsafe::MoveFromAnyAfterCheck<T>(std::move(data_));
}
+ /*!
+ * \brief Move the replacement, or copy \p original when unchanged.
+ * \param original The borrowed original value, which is left unmodified.
+ * \return The replacement or original value.
+ */
+ TVM_FFI_INLINE T ValueOrUnchanged(const T& original) && {
+ return IsUnchanged() ? original
+ :
details::AnyUnsafe::MoveFromAnyAfterCheck<T>(std::move(data_));
+ }
+
/*!
* \brief Move the replacement, or move \p original when unchanged.
* \param original The owned original value.
@@ -374,153 +397,74 @@ class StructuralMutatorObj : public Object {
using MutatorObjType = StructuralMutatorObj;
/*!
- * \brief Mutate a value through the mutator vtable.
- *
- * \param value The value to mutate.
- * \tparam T The declared replacement type.
+ * \brief Throwing form of \ref MutateExpected.
+ * \param value The borrowed value to mutate.
+ * \param inplace_mode Whether the caller permits mutation along this
ownership path.
* \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.
+ * \note The default InplaceMode::kDisallow uses copy-on-write. In-place
mutation is permitted
+ * only when inplace_mode is InplaceMode::kAllow and the current value
is uniquely owned.
+ * See \ref MutateExpected for the full permission and error semantics.
*
+ * Use \ref TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN in Expected-returning helpers
or raw hooks to
+ * preserve checked typed mutation results and the fixed mismatch
diagnostic. At a throwing
+ * boundary, an explicit cast may instead report its own TypeError:
* \code{.cpp}
- * Expr new_node =
mutator->Mutate<Expr>(node).ValueOrUnchanged(std::move(node));
+ * Expr new_node =
mutator->Mutate(node).ValueOrUnchanged(AnyView(node)).cast<Expr>();
* \endcode
*/
- 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.
- * \tparam T The declared replacement type.
- * \return The replacement or unchanged marker, or an Error if mutation
failed.
- */
- 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::MoveTVMFFIAnyRawToAny(result);
- return details::SMutateDeclaredTypeError();
- }
- return
details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<T>>(result);
- }
+ TVM_FFI_INLINE UnchangedOr<Any> Mutate(AnyView value,
+ InplaceMode inplace_mode =
InplaceMode::kDisallow) {
+ return std::move(MutateExpected(value, inplace_mode)).value();
}
/*!
- * \brief Mutate a value, permitting an in-place implementation when it is
safe.
- *
+ * \brief Mutate a value, permitting in-place mutation only when uniquely
owned.
* \param value The borrowed value to mutate.
- * \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.
- */
- 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.
- * \tparam T The declared replacement type.
+ * \param inplace_mode Whether the caller permits mutation along this
ownership path.
* \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.
+ * \note The default InplaceMode::kDisallow uses copy-on-write. In-place
mutation is permitted
+ * only when inplace_mode is InplaceMode::kAllow and the current value
is uniquely owned.
+ * InplaceMode::kAllow requires permission along the entire path from
the root.
+ * Recursive calls must forward their established mode explicitly.
Uniqueness is checked
+ * before callback arguments acquire ownership. This permits in-place
dispatch but does
+ * not guarantee reuse: a hook may return a replacement. In-place
changes completed before
+ * an Error are not rolled back.
*/
- 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>>(
+ TVM_FFI_INLINE Expected<UnchangedOr<Any>> MutateExpected(
+ AnyView value, InplaceMode inplace_mode = InplaceMode::kDisallow)
noexcept {
+ const Object* object = value.as<Object>();
+ // Check uniqueness on the borrowed view before callbacks can acquire
owning references.
+ if (inplace_mode == InplaceMode::kAllow && object != nullptr &&
object->unique()) {
+ return details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<Any>>(
(*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::MoveTVMFFIAnyRawToAny(result);
- return details::SMutateDeclaredTypeError();
- }
- return
details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<T>>(result);
}
+ return details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<Any>>(
+ (*vtable_->mutate)(this, value));
}
/*!
- * \brief Mutate a value, using in-place mutation only for a uniquely owned
object
- * and \p allow_inplace set to true.
- * \tparam T The declared replacement type.
- * \param value The borrowed value to mutate.
- * \param allow_inplace Whether in-place mutation is permitted. Defaults to
true.
- * If false, use ordinary mutation without checking uniqueness.
+ * \brief Apply default structural mutation with validated in-place
permission.
+ * \param value The borrowed current value to mutate.
+ * \param inplace_mode The in-place mode already established by the caller
for value.
* \return The replacement or unchanged marker, or an Error if mutation
failed.
*
- * \note When \p allow_inplace is true, 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.
+ * \note The default InplaceMode::kDisallow is a copy-on-write convenience
for one-off calls.
+ * Recursive code and hooks must explicitly forward the mode
established for the current
+ * value. This method uses that mode without checking uniqueness
again, even if a typed
+ * callback argument has acquired another reference. It bypasses the
current engine
+ * callback. Permission must cover the entire path from the root.
+ * Without an in-place hook, ordinary mutation runs. In-place changes
completed before an
+ * Error are not rolled back. Registered hooks own variable-remap
handling; the reflected
+ * fallback applies it automatically and always uses copy-on-write
mutation.
*/
- template <typename T = Any>
- TVM_FFI_INLINE Expected<UnchangedOr<T>> MaybeInplaceMutateIfUniqueExpected(
- AnyView value, bool allow_inplace = true) noexcept {
- const Object* obj = value.as<Object>();
- if (allow_inplace && obj != nullptr && obj->unique()) {
- return MaybeInplaceMutateExpected<T>(value);
- }
- return MutateExpected<T>(value);
- }
-
- /*!
- * \brief Apply the default structural mutation with copy-on-write behavior.
- *
- * \param value The value to mutate.
- * \return The replacement or unchanged marker, or an Error if hook
dispatch, copying, or field
- * mutation failed.
- *
- * \note A registered ``__s_mutate__`` hook is dispatched before the
reflected fallback. A
- * FreeVar hook owns the definition-only remap policy for that type;
the reflected fallback
- * applies the same policy automatically.
- */
-
- TVM_FFI_INLINE Expected<UnchangedOr<Any>> DefaultMutateExpected(AnyView
value) noexcept {
- return
details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<Any>>(DefaultMutateRaw(value));
- }
-
- /*!
- * \brief Apply default structural mutation with optional in-place
permission.
- *
- * \param value The borrowed value to mutate.
- * \param allow_inplace Whether in-place mutation is permitted. Defaults to
true.
- * If false, call \ref DefaultMutateExpected.
- * \return The replacement or unchanged marker, or an Error if mutation
failed. In-place
- * changes completed before an Error are not rolled back.
- *
- * \note In-place mutation is explicitly opt-in. A registered
- * ``__s_maybe_inplace_mutate__`` hook may rely on its input being
safe to mutate and owns
- * any variable-remap handling. When the hook is absent, this method
calls
- * \ref DefaultMutateExpected. This method does not check uniqueness.
When
- * \p allow_inplace is true, the caller must already know that mutating
- * \p value in place is safe, including ownership of the path from the
root.
- *
- * \code
- * return mutator->DefaultMaybeInplaceMutateExpected(value, allow_inplace);
- * \endcode
- */
- TVM_FFI_INLINE Expected<UnchangedOr<Any>> DefaultMaybeInplaceMutateExpected(
- AnyView value, bool allow_inplace = true) noexcept {
- if (!allow_inplace) return DefaultMutateExpected(value);
+ TVM_FFI_INLINE Expected<UnchangedOr<Any>> DefaultMutateExpected(
+ AnyView value, InplaceMode inplace_mode = InplaceMode::kDisallow)
noexcept {
return details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<Any>>(
- DefaultMaybeInplaceMutateRaw(value));
+ inplace_mode == InplaceMode::kAllow ?
DefaultMaybeInplaceMutateRaw(value)
+ : DefaultMutateRaw(value));
}
/*!
@@ -744,7 +688,8 @@ class StructuralMutatorObj : public Object {
}
return result;
}
- return
details::ExpectedUnsafe::MoveToTVMFFIAny(DefaultMutateExpected(value));
+ return details::ExpectedUnsafe::MoveToTVMFFIAny(
+ DefaultMutateExpected(value, InplaceMode::kDisallow));
}
protected:
@@ -840,14 +785,14 @@ TVM_FFI_INLINE static Expected<Any>
MutateReflectedFieldsExpected(StructuralMuta
Expected<UnchangedOr<Any>> mutated_field = [&]() ->
Expected<UnchangedOr<Any>> {
if (field_info->flags & kTVMFFIFieldFlagBitMaskSEqHashDefSimple) {
return mutator->WithDefRegionKind(kTVMFFIDefRegionKindSimple,
[&]() {
- return mutator->MutateExpected(field_value);
+ return mutator->MutateExpected(field_value,
InplaceMode::kDisallow);
});
} else if (field_info->flags &
kTVMFFIFieldFlagBitMaskSEqHashDefPattern) {
return mutator->WithDefRegionKind(kTVMFFIDefRegionKindPattern,
[&]() {
- return mutator->MutateExpected(field_value);
+ return mutator->MutateExpected(field_value,
InplaceMode::kDisallow);
});
} else {
- return mutator->MutateExpected(field_value);
+ return mutator->MutateExpected(field_value,
InplaceMode::kDisallow);
}
}();
if (TVM_FFI_PREDICT_FALSE(mutated_field.is_err())) {
@@ -945,19 +890,20 @@ namespace details {
* returns ``TypeError`` through the surrounding raw or ``Expected`` function
without throwing,
* reported with a fixed string. The check is omitted when the declared type
subsumes the result's
* success type. Its early returns work from either a raw ``TVMFFIAny`` hook
or an
- * ``Expected<T>`` helper, including one with a different success type. This
macro declares ``Name``
- * into the enclosing scope and must be used in a braced block, never as an
unbraced control-flow
- * body.
+ * ``Expected<T>`` helper, including one with a different success type. This
macro declares
+ * ``Name`` into the enclosing scope and must be used in a braced block, never
as an unbraced
+ * control-flow body.
*
* Example:
* \code{.cpp}
* TVMFFIAny FooMutate(StructuralMutatorObj* mutator, AnyView value) noexcept {
* const FooNode* self =
* details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
FooNode>(value);
+ * constexpr InplaceMode inplace_mode = InplaceMode::kDisallow;
* TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, a,
- * mutator->MutateExpected(self->a));
+ * mutator->MutateExpected(self->a,
inplace_mode));
* TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, b,
- * mutator->MutateExpected(self->b));
+ * mutator->MutateExpected(self->b,
inplace_mode));
* if (a.UnchangedOrSameAs(self->a) && b.UnchangedOrSameAs(self->b)) {
* return Unchanged().CopyToTVMFFIAny();
* }
@@ -1276,14 +1222,14 @@ class StructuralMapEngine : public Parent {
/*!
* \brief Test one link against \p value and, if it matches, mutate the node
through it.
*
- * \tparam kMaybeInplace Whether the caller may mutate a uniquely owned node
in place.
+ * \tparam kInplaceMode Whether the caller may mutate a uniquely owned node
in place.
* \tparam Callback The link's callback type.
* \param callback The link's callback.
* \param value The borrowed value to test and mutate.
* \param out Receives the mutated value or Error when the link matched.
* \return Whether the link matched, in which case \p out was written.
*/
- template <bool kMaybeInplace, typename Callback>
+ template <InplaceMode kInplaceMode, typename Callback>
TVM_FFI_INLINE bool TryLink(Callback& callback, AnyView value,
Expected<Any>* out) noexcept {
using FuncInfo = details::FunctionInfo<std::decay_t<Callback>>;
static_assert(FuncInfo::num_args >= 1,
@@ -1320,17 +1266,18 @@ class StructuralMapEngine : public Parent {
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) {
+ if constexpr (kInplaceMode == InplaceMode::kAllow) {
// A pre-order result can be mutated in place if unchanged or
uniquely owned.
if (descent_view.same_as(value)) {
- return this->DefaultMaybeInplaceMutateExpected(value);
+ return this->DefaultMutateExpected(value, InplaceMode::kAllow);
}
const Object* mapped_obj = descent_view.as<Object>();
- bool can_inplace = mapped_obj != nullptr && mapped_obj->unique();
- return can_inplace ?
this->DefaultMaybeInplaceMutateExpected(descent_view)
- : this->DefaultMutateExpected(descent_view);
+ InplaceMode inplace_mode = mapped_obj != nullptr &&
mapped_obj->unique()
+ ? InplaceMode::kAllow
+ : InplaceMode::kDisallow;
+ return this->DefaultMutateExpected(descent_view, inplace_mode);
} else {
- return this->DefaultMutateExpected(descent_view);
+ return this->DefaultMutateExpected(descent_view,
InplaceMode::kDisallow);
}
}();
if (TVM_FFI_PREDICT_FALSE(out->is_err())) return true;
@@ -1371,10 +1318,10 @@ class StructuralMapEngine : public Parent {
* \brief Test every link in declaration order, stopping at the first that
matches.
* \return Whether some link matched, in which case \p out was written.
*/
- template <bool kMaybeInplace, size_t... Is>
+ template <InplaceMode kInplaceMode, size_t... Is>
TVM_FFI_INLINE bool TryLinks(AnyView value, Expected<Any>* out,
std::index_sequence<Is...>) noexcept {
- return (TryLink<kMaybeInplace>(std::get<Is>(callbacks_), value, out) ||
...);
+ return (TryLink<kInplaceMode>(std::get<Is>(callbacks_), value, out) ||
...);
}
/*!
@@ -1385,17 +1332,18 @@ class StructuralMapEngine : public Parent {
TVM_FFI_INLINE TVMFFIAny MutateImplRaw(AnyView value) noexcept {
Expected<Any> out{Any()};
if constexpr (order == WalkOrder::kPostOrder) {
- out = this->DefaultMutateExpected(value);
+ out = this->DefaultMutateExpected(value, InplaceMode::kDisallow);
if (TVM_FFI_PREDICT_FALSE(out.is_err())) {
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
}
- TryLinks<false>(value, &out, std::index_sequence_for<Callbacks...>{});
+ TryLinks<InplaceMode::kDisallow>(value, &out,
std::index_sequence_for<Callbacks...>{});
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
} else {
- if (TryLinks<false>(value, &out,
std::index_sequence_for<Callbacks...>{})) {
+ if (TryLinks<InplaceMode::kDisallow>(value, &out,
std::index_sequence_for<Callbacks...>{})) {
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
}
- return
ExpectedUnsafe::MoveToTVMFFIAny(this->DefaultMutateExpected(value));
+ return ExpectedUnsafe::MoveToTVMFFIAny(
+ this->DefaultMutateExpected(value, InplaceMode::kDisallow));
}
}
@@ -1407,17 +1355,18 @@ class StructuralMapEngine : public Parent {
TVM_FFI_INLINE TVMFFIAny MaybeInplaceMutateImplRaw(AnyView value) noexcept {
Expected<Any> out{Any()};
if constexpr (order == WalkOrder::kPostOrder) {
- out = this->DefaultMaybeInplaceMutateExpected(value);
+ out = this->DefaultMutateExpected(value, InplaceMode::kAllow);
if (TVM_FFI_PREDICT_FALSE(out.is_err())) {
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
}
- TryLinks<true>(value, &out, std::index_sequence_for<Callbacks...>{});
+ TryLinks<InplaceMode::kAllow>(value, &out,
std::index_sequence_for<Callbacks...>{});
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
} else {
- if (TryLinks<true>(value, &out,
std::index_sequence_for<Callbacks...>{})) {
+ if (TryLinks<InplaceMode::kAllow>(value, &out,
std::index_sequence_for<Callbacks...>{})) {
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
}
- return
ExpectedUnsafe::MoveToTVMFFIAny(this->DefaultMaybeInplaceMutateExpected(value));
+ return ExpectedUnsafe::MoveToTVMFFIAny(
+ this->DefaultMutateExpected(value, InplaceMode::kAllow));
}
}
@@ -1515,12 +1464,12 @@ class StructuralMapDynEngine : public Parent {
/*!
* \brief Test the runtime link table against \p value and mutate through
the first match.
- * \tparam kMaybeInplace Whether a uniquely owned node may be mutated in
place.
+ * \tparam kInplaceMode Whether a uniquely owned node may be mutated in
place.
* \param value The borrowed value to test and mutate.
* \param out Receives the mutated value or Error when a link matched.
* \return Whether a link matched, in which case \p out was written.
*/
- template <bool kMaybeInplace>
+ template <InplaceMode kInplaceMode>
TVM_FFI_INLINE bool TryLink(AnyView value, Expected<Any>* out) noexcept {
if constexpr (order == WalkOrder::kPreOrder) {
bool with_kind = false;
@@ -1538,16 +1487,17 @@ class StructuralMapDynEngine : public Parent {
const AnyView descent_view =
mapped_value.type_index() == TypeIndex::kTVMFFIUnchanged ? value :
AnyView(mapped_value);
*out = [&]() -> Expected<Any> {
- if constexpr (kMaybeInplace) {
+ if constexpr (kInplaceMode == InplaceMode::kAllow) {
if (descent_view.same_as(value)) {
- return this->DefaultMaybeInplaceMutateExpected(value);
+ return this->DefaultMutateExpected(value, InplaceMode::kAllow);
}
const Object* mapped_obj = descent_view.as<Object>();
- bool can_inplace = mapped_obj != nullptr && mapped_obj->unique();
- return can_inplace ?
this->DefaultMaybeInplaceMutateExpected(descent_view)
- : this->DefaultMutateExpected(descent_view);
+ InplaceMode inplace_mode = mapped_obj != nullptr &&
mapped_obj->unique()
+ ? InplaceMode::kAllow
+ : InplaceMode::kDisallow;
+ return this->DefaultMutateExpected(descent_view, inplace_mode);
} else {
- return this->DefaultMutateExpected(descent_view);
+ return this->DefaultMutateExpected(descent_view,
InplaceMode::kDisallow);
}
}();
if (TVM_FFI_PREDICT_FALSE(out->is_err())) return true;
@@ -1580,17 +1530,18 @@ class StructuralMapDynEngine : public Parent {
TVM_FFI_INLINE TVMFFIAny MutateImplRaw(AnyView value) noexcept {
Expected<Any> out{Any()};
if constexpr (order == WalkOrder::kPostOrder) {
- out = this->DefaultMutateExpected(value);
+ out = this->DefaultMutateExpected(value, InplaceMode::kDisallow);
if (TVM_FFI_PREDICT_FALSE(out.is_err())) {
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
}
- TryLink<false>(value, &out);
+ TryLink<InplaceMode::kDisallow>(value, &out);
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
} else {
- if (TryLink<false>(value, &out)) {
+ if (TryLink<InplaceMode::kDisallow>(value, &out)) {
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
}
- return
ExpectedUnsafe::MoveToTVMFFIAny(this->DefaultMutateExpected(value));
+ return ExpectedUnsafe::MoveToTVMFFIAny(
+ this->DefaultMutateExpected(value, InplaceMode::kDisallow));
}
}
@@ -1598,17 +1549,18 @@ class StructuralMapDynEngine : public Parent {
TVM_FFI_INLINE TVMFFIAny MaybeInplaceMutateImplRaw(AnyView value) noexcept {
Expected<Any> out{Any()};
if constexpr (order == WalkOrder::kPostOrder) {
- out = this->DefaultMaybeInplaceMutateExpected(value);
+ out = this->DefaultMutateExpected(value, InplaceMode::kAllow);
if (TVM_FFI_PREDICT_FALSE(out.is_err())) {
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
}
- TryLink<true>(value, &out);
+ TryLink<InplaceMode::kAllow>(value, &out);
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
} else {
- if (TryLink<true>(value, &out)) {
+ if (TryLink<InplaceMode::kAllow>(value, &out)) {
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
}
- return
ExpectedUnsafe::MoveToTVMFFIAny(this->DefaultMaybeInplaceMutateExpected(value));
+ return ExpectedUnsafe::MoveToTVMFFIAny(
+ this->DefaultMutateExpected(value, InplaceMode::kAllow));
}
}
@@ -1655,7 +1607,7 @@ class StructuralMutateEngine : public Parent {
static TVMFFIAny DispatchMutate(StructuralMutatorObj* mutator, AnyView
value) noexcept {
auto* self = static_cast<StructuralMutateEngine*>(mutator);
if constexpr (sizeof...(Callbacks) == 1) {
- return self->template MutateSingleCallbackRaw<false>(value);
+ return self->template
MutateSingleCallbackRaw<InplaceMode::kDisallow>(value);
} else {
return self->MutateImplRaw(value);
}
@@ -1666,7 +1618,7 @@ class StructuralMutateEngine : public Parent {
AnyView value) noexcept {
auto* self = static_cast<StructuralMutateEngine*>(mutator);
if constexpr (sizeof...(Callbacks) == 1) {
- return self->template MutateSingleCallbackRaw<true>(value);
+ return self->template
MutateSingleCallbackRaw<InplaceMode::kAllow>(value);
} else {
return self->MaybeInplaceMutateImplRaw(value);
}
@@ -1675,7 +1627,7 @@ class StructuralMutateEngine : public Parent {
/*! \brief Mutate one value, handing a matched callback ownership of
descent. */
TVMFFIAny MutateImplRaw(AnyView value) noexcept {
Expected<Any> result{Any()};
- if (DispatchCallbacks(value, false, &result)) {
+ if (DispatchCallbacks(value, InplaceMode::kDisallow, &result)) {
if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
// Keep callback-boundary context in addition to the default-descent
// context: a callback may return a rebuilt value, so the two nodes
can differ.
@@ -1683,13 +1635,14 @@ class StructuralMutateEngine : public Parent {
}
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
}
- return
details::ExpectedUnsafe::MoveToTVMFFIAny(Parent::DefaultMutateExpected(value));
+ return details::ExpectedUnsafe::MoveToTVMFFIAny(
+ Parent::DefaultMutateExpected(value, InplaceMode::kDisallow));
}
/*! \brief Maybe mutate one value in place, with callback-owned descent. */
TVMFFIAny MaybeInplaceMutateImplRaw(AnyView value) noexcept {
Expected<Any> result{Any()};
- if (DispatchCallbacks(value, true, &result)) {
+ if (DispatchCallbacks(value, InplaceMode::kAllow, &result)) {
if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
// Keep callback-boundary context in addition to the default-descent
// context: a callback may return a rebuilt value, so the two nodes
can differ.
@@ -1698,17 +1651,17 @@ class StructuralMutateEngine : public Parent {
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
}
return details::ExpectedUnsafe::MoveToTVMFFIAny(
- Parent::DefaultMaybeInplaceMutateExpected(value));
+ Parent::DefaultMutateExpected(value, InplaceMode::kAllow));
}
/*! \brief Invoke a matched callback with the Parent view and preserve
returned/thrown Error. */
template <typename Callback, typename Matched>
TVM_FFI_INLINE Expected<Any> InvokeCallback(Callback& callback, Matched&&
matched,
- bool allow_inplace) noexcept {
+ InplaceMode inplace_mode)
noexcept {
using FuncInfo = details::FunctionInfo<std::decay_t<Callback>>;
static_assert(FuncInfo::num_args == 2 || FuncInfo::num_args == 3,
"StructuralMutate callback must take (value, mutator) or "
- "(value, mutator, allow_inplace)");
+ "(value, mutator, inplace_mode)");
using SecondArg = std::decay_t<std::tuple_element_t<1, typename
FuncInfo::ArgType>>;
using Second = std::remove_pointer_t<SecondArg>;
static_assert(std::is_same_v<Second, typename Parent::MutatorObjType>,
@@ -1716,13 +1669,13 @@ class StructuralMutateEngine : public Parent {
"Parent::MutatorObjType*");
if constexpr (FuncInfo::num_args == 3) {
using ThirdArg = std::decay_t<std::tuple_element_t<2, typename
FuncInfo::ArgType>>;
- static_assert(std::is_same_v<ThirdArg, bool>,
- "third StructuralMutate callback argument must be bool");
+ static_assert(std::is_same_v<ThirdArg, InplaceMode>,
+ "third StructuralMutate callback argument must be
InplaceMode");
}
auto* mutator = static_cast<typename Parent::MutatorObjType*>(this);
try {
if constexpr (FuncInfo::num_args == 3) {
- return callback(std::forward<Matched>(matched), mutator,
allow_inplace);
+ return callback(std::forward<Matched>(matched), mutator, inplace_mode);
} else {
return callback(std::forward<Matched>(matched), mutator);
}
@@ -1740,7 +1693,7 @@ class StructuralMutateEngine : public Parent {
// One callback forwards its result directly, without a callback-chain
envelope.
// AnyView/Any always match; only a typed miss needs the Parent's default
descent.
- template <bool kMaybeInplace>
+ template <InplaceMode kInplaceMode>
TVM_FFI_INLINE TVMFFIAny MutateSingleCallbackRaw(AnyView value) noexcept {
auto& callback = std::get<0>(callbacks_);
using FuncInfo = details::FunctionInfo<std::decay_t<decltype(callback)>>;
@@ -1749,19 +1702,20 @@ class StructuralMutateEngine : public Parent {
TVMFFIAny result;
if constexpr (std::is_same_v<TSub, AnyView>) {
result =
- details::ExpectedUnsafe::MoveToTVMFFIAny(InvokeCallback(callback,
value, kMaybeInplace));
+ details::ExpectedUnsafe::MoveToTVMFFIAny(InvokeCallback(callback,
value, kInplaceMode));
} else if constexpr (std::is_same_v<TSub, Any>) {
result = details::ExpectedUnsafe::MoveToTVMFFIAny(
- InvokeCallback(callback, Any(value), kMaybeInplace));
+ InvokeCallback(callback, Any(value), kInplaceMode));
} else if (auto matched = value.template as<TSub>()) {
result = details::ExpectedUnsafe::MoveToTVMFFIAny(
- InvokeCallback(callback, *std::move(matched), kMaybeInplace));
+ InvokeCallback(callback, *std::move(matched), kInplaceMode));
} else {
- if constexpr (kMaybeInplace) {
+ if constexpr (kInplaceMode == InplaceMode::kAllow) {
return details::ExpectedUnsafe::MoveToTVMFFIAny(
- Parent::DefaultMaybeInplaceMutateExpected(value));
+ Parent::DefaultMutateExpected(value, InplaceMode::kAllow));
} else {
- return
details::ExpectedUnsafe::MoveToTVMFFIAny(Parent::DefaultMutateExpected(value));
+ return details::ExpectedUnsafe::MoveToTVMFFIAny(
+ Parent::DefaultMutateExpected(value, InplaceMode::kDisallow));
}
}
// Release any owning match before naming the callback boundary, as in the
general path.
@@ -1773,19 +1727,19 @@ class StructuralMutateEngine : public Parent {
/*! \brief Write a matched callback result, leaving out untouched on a type
miss. */
template <typename Callback>
- TVM_FFI_INLINE bool TryLink(Callback& callback, AnyView value, bool
allow_inplace,
+ TVM_FFI_INLINE bool TryLink(Callback& callback, AnyView value, InplaceMode
inplace_mode,
Expected<Any>* out) noexcept {
using FuncInfo = details::FunctionInfo<std::decay_t<Callback>>;
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>) {
- *out = InvokeCallback(callback, value, allow_inplace);
+ *out = InvokeCallback(callback, value, inplace_mode);
return true;
} else if constexpr (std::is_same_v<TSub, Any>) {
- *out = InvokeCallback(callback, Any(value), allow_inplace);
+ *out = InvokeCallback(callback, Any(value), inplace_mode);
return true;
} else if (auto matched = value.template as<TSub>()) {
- *out = InvokeCallback(callback, *std::move(matched), allow_inplace);
+ *out = InvokeCallback(callback, *std::move(matched), inplace_mode);
return true;
}
return false;
@@ -1793,15 +1747,15 @@ class StructuralMutateEngine : public Parent {
/*! \brief Fold callbacks in declaration order, stopping at the first match.
*/
template <size_t... Is>
- TVM_FFI_INLINE bool TryLinks(AnyView value, bool allow_inplace,
Expected<Any>* out,
+ TVM_FFI_INLINE bool TryLinks(AnyView value, InplaceMode inplace_mode,
Expected<Any>* out,
std::index_sequence<Is...>) noexcept {
- return (TryLink(std::get<Is>(callbacks_), value, allow_inplace, out) ||
...);
+ return (TryLink(std::get<Is>(callbacks_), value, inplace_mode, out) ||
...);
}
/*! \brief Run the callback chain, returning whether a callback matched. */
- TVM_FFI_INLINE bool DispatchCallbacks(AnyView value, bool allow_inplace,
+ TVM_FFI_INLINE bool DispatchCallbacks(AnyView value, InplaceMode
inplace_mode,
Expected<Any>* out) noexcept {
- return TryLinks(value, allow_inplace, out,
std::index_sequence_for<Callbacks...>{});
+ return TryLinks(value, inplace_mode, out,
std::index_sequence_for<Callbacks...>{});
}
/*! \brief Typed callbacks tested in declaration order, first match wins. */
@@ -1848,7 +1802,7 @@ 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)...));
- auto result = mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ auto result = mutator->MutateExpected(root, InplaceMode::kAllow);
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)));
@@ -1888,7 +1842,7 @@ Any StructuralMap(Any root,
* A callback takes one of two forms, where ``R`` is a supported return type:
*
* - ``R(const T& value, StructuralMutatorObj* mutator)``
- * - ``R(const T& value, StructuralMutatorObj* mutator, bool allow_inplace)``
+ * - ``R(const T& value, StructuralMutatorObj* mutator, InplaceMode
inplace_mode)``
*
* 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
@@ -1899,9 +1853,10 @@ Any StructuralMap(Any root,
* \param callbacks Callbacks tested in declaration order.
* \return The mutated owning value, or an Error if mutation or a callback
fails.
*
- * \note A two-argument callback descends with ``MutateExpected`` and remains
copy-on-write.
- * A three-argument callback receives ``allow_inplace=true`` only when
its value is on a
- * uniquely owned path and may then explicitly use the maybe-in-place
mutator operation.
+ * \note A two-argument callback descends with ``MutateExpected(value)``,
using the default
+ * copy-on-write permission. A three-argument callback receives
InplaceMode::kAllow only
+ * when its value is on a uniquely owned path. Forward this mode to
``MutateExpected`` for
+ * child values or ``DefaultMutateExpected`` for the current value's
default descent.
* \note Pass an owned root with ``std::move(root)`` to permit root reuse. In a
* ``__s_maybe_inplace_mutate__`` hook, the corresponding nested idiom is
* ``self->field = StructuralMap(std::move(self->field), callback)``;
const-correctness
@@ -1914,7 +1869,7 @@ 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)...));
- auto result = mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ auto result = mutator->MutateExpected(root, InplaceMode::kAllow);
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)));
diff --git a/include/tvm/ffi/reflection/accessor.h
b/include/tvm/ffi/reflection/accessor.h
index c0e3d93f..94249176 100644
--- a/include/tvm/ffi/reflection/accessor.h
+++ b/include/tvm/ffi/reflection/accessor.h
@@ -534,10 +534,10 @@ inline constexpr const char* kStructuralMutate =
"__s_mutate__";
*
* ``(StructuralMutator mutator, Any value) -> Any``.
*
- * This hook is optional. When it is absent,
``DefaultMaybeInplaceMutateExpected`` falls back to
- * non-in-place mutation through ``kStructuralMutate`` or reflected structural
fields. In-place
- * mutation is therefore explicitly opt-in and is never inferred from
ownership by the reflected
- * fallback.
+ * This hook is optional. When it is absent, ``DefaultMutateExpected`` with
+ * ``inplace_mode=InplaceMode::kAllow`` falls back to non-in-place mutation
through
+ * ``kStructuralMutate`` or reflected structural fields. In-place mutation is
therefore explicitly
+ * opt-in and is never inferred from ownership by the reflected fallback.
*/
inline constexpr const char* kStructuralMaybeInplaceMutate =
"__s_maybe_inplace_mutate__";
diff --git a/python/tvm_ffi/__init__.py b/python/tvm_ffi/__init__.py
index f3ee8c86..7eafc132 100644
--- a/python/tvm_ffi/__init__.py
+++ b/python/tvm_ffi/__init__.py
@@ -77,6 +77,7 @@ if TYPE_CHECKING or not _is_config_mode():
from .stream import StreamContext, get_raw_stream, use_raw_stream,
use_torch_stream
from .structural import (
DefRegionKind,
+ InplaceMode,
StructuralKey,
StructuralMutator,
StructuralVisitor,
@@ -150,6 +151,7 @@ __all__ = [
"Device",
"Dict",
"Function",
+ "InplaceMode",
"List",
"Map",
"Module",
diff --git a/python/tvm_ffi/structural.py b/python/tvm_ffi/structural.py
index d1f369e8..fc10596a 100644
--- a/python/tvm_ffi/structural.py
+++ b/python/tvm_ffi/structural.py
@@ -33,6 +33,7 @@ from .registry import register_object
__all__ = [
"DefRegionKind",
+ "InplaceMode",
"StructuralKey",
"StructuralMutator",
"StructuralVisitor",
@@ -84,6 +85,25 @@ class WalkResult(IntEnum):
SKIP = 1
+class InplaceMode(IntEnum):
+ """Permission to mutate a value in place along its ownership path.
+
+ DISALLOW preserves the source through copy-on-write. ALLOW permits in-place
+ mutation only when the current value is uniquely owned; it does not require
+ a callback to reuse the value. Structural mutation callbacks receive the
+ corresponding integer, which compares directly with these enum members.
+
+ See Also
+ --------
+ :py:func:`tvm_ffi.structural_mutate`
+ Mutate a value with callbacks that own recursive mutation.
+
+ """
+
+ DISALLOW = 0
+ ALLOW = 1
+
+
class DefRegionKind(IntEnum):
"""Def-region state active during structural visiting.
@@ -697,9 +717,10 @@ def structural_mutate(
"""Mutate a value with callbacks that own recursive mutation.
Each callback receives ``(value, mutator)`` and may optionally receive a
- third ``allow_inplace`` boolean, which is true only when the callback's
- value is on a uniquely owned path. The flag lets a callback choose an
- ownership-aware implementation; recursive descent still uses
+ third ``inplace_mode`` integer corresponding to :class:`InplaceMode`.
+ It equals ``InplaceMode.ALLOW`` only when the callback's value is on a
+ uniquely owned path, and ``InplaceMode.DISALLOW`` otherwise. The mode lets
+ a callback choose an ownership-aware implementation; recursive descent uses
:meth:`StructuralMutator.mutate` for selected children or
:meth:`StructuralMutator.default_mutate` for the matched value's default
mutation. Its returned value is final and is not traversed again. Entries
@@ -726,7 +747,7 @@ def structural_mutate(
(
_callback_type_to_type_index(t, api_name="structural_mutate"),
fn,
- _callback_accepts_allow_inplace(fn),
+ _callback_accepts_inplace_mode(fn),
)
for t, fn in callback_entries
]
@@ -860,8 +881,8 @@ def _normalize_callbacks(
return callback_entries
-def _callback_accepts_allow_inplace(callback: Callable[..., Any]) -> bool:
- """Return whether a StructuralMutate callback accepts its optional flag."""
+def _callback_accepts_inplace_mode(callback: Callable[..., Any]) -> bool:
+ """Return whether a StructuralMutate callback accepts its optional mode."""
try:
signature = inspect.signature(callback)
except (TypeError, ValueError):
@@ -870,14 +891,14 @@ def _callback_accepts_allow_inplace(callback:
Callable[..., Any]) -> bool:
return False
try:
- signature.bind(None, None, False)
+ signature.bind(None, None, InplaceMode.DISALLOW)
except TypeError:
try:
signature.bind(None, None)
except TypeError as err:
raise TypeError(
"structural_mutate callback must accept (value, mutator) or "
- "(value, mutator, allow_inplace)"
+ "(value, mutator, inplace_mode)"
) from err
return False
return True
diff --git a/src/ffi/extra/structural_mutate.cc
b/src/ffi/extra/structural_mutate.cc
index f5f2fcff..3764322e 100644
--- a/src/ffi/extra/structural_mutate.cc
+++ b/src/ffi/extra/structural_mutate.cc
@@ -57,11 +57,11 @@ Expected<Any> StructuralMapExpected(
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);
+ return mutator->MutateExpected(root, InplaceMode::kAllow);
}
using Mutator = StructuralMapDynEngine<StructuralMapEngineBase,
WalkOrder::kPostOrder>;
StructuralMutator mutator(make_object<Mutator>(callbacks,
callbacks_with_def_region_kind));
- return mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ return mutator->MutateExpected(root, InplaceMode::kAllow);
}();
if (TVM_FFI_PREDICT_FALSE(result.is_err())) return
Unexpected(std::move(result).error());
UnchangedOr<Any> mapped = AnyUnsafe::MoveFromAnyAfterCheck<UnchangedOr<Any>>(
@@ -96,12 +96,12 @@ class StructuralMutateDynEngine : public Parent {
return
static_cast<StructuralMutateDynEngine*>(mutator)->MaybeInplaceMutateImplRaw(value);
}
- std::optional<Expected<Any>> DispatchCallback(AnyView value, bool
allow_inplace) noexcept {
+ std::optional<Expected<Any>> DispatchCallback(AnyView value, InplaceMode
inplace_mode) noexcept {
for (const auto& entry : callbacks_) {
if (!RuntimeTypeIndexMatch(value.type_index(), entry.template get<0>()))
continue;
if (entry.template get<2>()) {
return entry.template get<1>().template CallExpected<Any>(
- value, GetRef<StructuralMutator>(this), allow_inplace);
+ value, GetRef<StructuralMutator>(this), inplace_mode);
}
return entry.template get<1>().template CallExpected<Any>(value,
GetRef<StructuralMutator>(this));
@@ -110,7 +110,7 @@ class StructuralMutateDynEngine : public Parent {
}
TVMFFIAny MutateImplRaw(AnyView value) noexcept {
- if (std::optional<Expected<Any>> matched = DispatchCallback(value, false))
{
+ if (std::optional<Expected<Any>> matched = DispatchCallback(value,
InplaceMode::kDisallow)) {
Expected<Any> result = *std::move(matched);
if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
Parent::UpdateVisitErrorContext(result, value);
@@ -121,7 +121,7 @@ class StructuralMutateDynEngine : public Parent {
}
TVMFFIAny MaybeInplaceMutateImplRaw(AnyView value) noexcept {
- if (std::optional<Expected<Any>> matched = DispatchCallback(value, true)) {
+ if (std::optional<Expected<Any>> matched = DispatchCallback(value,
InplaceMode::kAllow)) {
Expected<Any> result = *std::move(matched);
if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
Parent::UpdateVisitErrorContext(result, value);
@@ -137,9 +137,10 @@ class StructuralMutateDynEngine : public Parent {
/*!
* \brief Runtime callback-driven structural mutation.
* \param root The root value to mutate.
- * \param callbacks Runtime ``(type_index, callback, accepts_allow_inplace)``
entries. A callback
- * whose marker is true is invoked as ``callback(value,
mutator, allow_inplace)``;
- * otherwise it is invoked as ``callback(value, mutator)``.
+ * \param callbacks Runtime ``(type_index, callback, accepts_inplace_mode)``
entries. A callback
+ * whose marker is true is invoked as ``callback(value,
mutator, inplace_mode)``;
+ * the mode uses the FFI integer representation of
InplaceMode. Otherwise the
+ * callback is invoked as ``callback(value, mutator)``.
* \return The mutated owning value, or an Error.
*/
// The owning parameter makes caller ownership visible to the uniqueness check.
@@ -148,7 +149,7 @@ Expected<Any> StructuralMutateExpected(
const Array<Tuple<int32_t, Function, bool>>& callbacks) noexcept {
using Mutator = StructuralMutateDynEngine<StructuralMapEngineBase>;
StructuralMutator mutator(make_object<Mutator>(callbacks));
- auto result = mutator->MaybeInplaceMutateIfUniqueExpected(root);
+ auto result = mutator->MutateExpected(root, InplaceMode::kAllow);
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)));
@@ -182,7 +183,7 @@ 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(UnchangedOr<Any>, mapped_value,
- mutator->MutateExpected(item));
+ mutator->MutateExpected(item,
InplaceMode::kDisallow));
output->SetItemAfterCheck(i,
std::move(mapped_value).ValueOrUnchanged(AnyView(item)));
}
return AnyUnsafe::MoveAnyToTVMFFIAny(Any(std::move(output)));
@@ -204,7 +205,7 @@ TVMFFIAny MutateSeqContainerRaw(StructuralMutatorObj*
mutator, const SeqObj* sel
for (int64_t i = 0; i < size; ++i) {
const Any& item = items[i];
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, mapped_value,
- mutator->MutateExpected(item));
+ mutator->MutateExpected(item,
InplaceMode::kDisallow));
if (!mapped_value.UnchangedOrSameAs(item)) {
return MutateSeqContainerChanged(mutator, self, i,
std::move(mapped_value).ValueUnchecked());
}
@@ -225,7 +226,7 @@ TVMFFIAny
MaybeInplaceMutateSeqContainerRaw(StructuralMutatorObj* mutator, SeqOb
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(UnchangedOr<Any>, mapped_value,
-
mutator->MaybeInplaceMutateIfUniqueExpected(item));
+ mutator->MutateExpected(item,
InplaceMode::kAllow));
if (!mapped_value.UnchangedOrSameAs(item)) {
self->SetItemAfterCheck(i, std::move(mapped_value).ValueUnchecked());
}
@@ -261,7 +262,7 @@ 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(UnchangedOr<Any>, new_value,
- mutator->MutateExpected(old_value));
+ mutator->MutateExpected(old_value,
InplaceMode::kDisallow));
if (!new_value.UnchangedOrSameAs(old_value)) {
output_it->second = std::move(new_value).ValueUnchecked();
}
@@ -283,7 +284,7 @@ TVMFFIAny MutateMapValuesRaw(StructuralMutatorObj* mutator,
const MapObjType* se
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(UnchangedOr<Any>, new_value,
- mutator->MutateExpected(old_value));
+ mutator->MutateExpected(old_value,
InplaceMode::kDisallow));
if (!new_value.UnchangedOrSameAs(old_value)) {
return MutateMapValuesChanged(mutator, self, source_it, index,
std::move(new_value).ValueUnchecked());
@@ -305,7 +306,7 @@ TVMFFIAny
MaybeInplaceMutateMapValuesRaw(StructuralMutatorObj* mutator, MapObjTy
for (auto it = self->begin(); it != self->end(); ++it) {
const Any& old_value = it->second;
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Any>, new_value,
-
mutator->MaybeInplaceMutateIfUniqueExpected(old_value));
+ mutator->MutateExpected(old_value,
InplaceMode::kAllow));
if (!new_value.UnchangedOrSameAs(old_value)) {
it->second = std::move(new_value).ValueUnchecked();
}
@@ -377,14 +378,16 @@ TVM_FFI_STATIC_INIT_BLOCK() {
refl::GlobalDef()
.def_method("ffi.StructuralMutatorMutate",
[](const StructuralMutator& mutator, AnyView value) {
- return
std::move(mutator->Mutate(value)).ValueOrUnchanged(value);
- })
- .def_method("ffi.StructuralMutatorDefaultMutate",
- [](const StructuralMutator& mutator, AnyView value) -> Any {
- UnchangedOr<Any> result =
-
std::move(mutator->DefaultMutateExpected(value)).value();
- return std::move(result).ValueOrUnchanged(value);
+ return std::move(mutator->Mutate(value,
InplaceMode::kDisallow))
+ .ValueOrUnchanged(value);
})
+ .def_method(
+ "ffi.StructuralMutatorDefaultMutate",
+ [](const StructuralMutator& mutator, AnyView value) -> Any {
+ UnchangedOr<Any> result =
+ std::move(mutator->DefaultMutateExpected(value,
InplaceMode::kDisallow)).value();
+ return std::move(result).ValueOrUnchanged(value);
+ })
.def_method("ffi.StructuralMutatorVarRemapGet",
[](const StructuralMutator& mutator, AnyView var) {
return mutator->VarRemapGetExpected(var).value();
diff --git a/tests/cpp/extra/test_big_int.cc b/tests/cpp/extra/test_big_int.cc
index 3d85e790..dcd952bf 100644
--- a/tests/cpp/extra/test_big_int.cc
+++ b/tests/cpp/extra/test_big_int.cc
@@ -141,8 +141,8 @@ TEST(BigIntExtra, StructuralMutation) {
auto never_matches = [](int64_t, StructuralMutatorObj*) -> Expected<Any> {
return Any(); };
using Mutator = StructuralMutateEngine<StructuralMapEngineBase,
decltype(never_matches)>;
StructuralMutator mutator(make_object<Mutator>(never_matches));
- EXPECT_TRUE(mutator->Mutate(AnyView(value)).IsUnchanged());
- EXPECT_TRUE(mutator->MaybeInplaceMutate(AnyView(value)).IsUnchanged());
+ EXPECT_TRUE(mutator->Mutate(AnyView(value),
InplaceMode::kDisallow).IsUnchanged());
+ EXPECT_TRUE(mutator->Mutate(AnyView(value),
InplaceMode::kAllow).IsUnchanged());
EXPECT_TRUE(StructuralMutateExpected(Any(value),
never_matches).value().same_as(Any(value)));
auto map_int = [](int64_t x) -> Expected<Any> { return Any(x + 1); };
EXPECT_TRUE(StructuralMapExpected<WalkOrder::kPostOrder>(Any(value), map_int)
diff --git a/tests/cpp/extra/test_structural_mutate.cc
b/tests/cpp/extra/test_structural_mutate.cc
index 5024613e..f60bdf3e 100644
--- a/tests/cpp/extra/test_structural_mutate.cc
+++ b/tests/cpp/extra/test_structural_mutate.cc
@@ -40,13 +40,12 @@ using namespace tvm::ffi::testing;
using AnyArray = Array<Any>;
using StringMap = Map<String, Any>;
+static_assert(std::is_same_v<decltype(std::declval<StructuralMutatorObj&>().MutateExpected(
+ std::declval<AnyView>(),
InplaceMode::kDisallow)),
+ Expected<UnchangedOr<Any>>>);
static_assert(std::is_same_v<decltype(std::declval<StructuralMutatorObj&>().DefaultMutateExpected(
- std::declval<AnyView>())),
+ std::declval<AnyView>(),
InplaceMode::kAllow)),
Expected<UnchangedOr<Any>>>);
-static_assert(
-
std::is_same_v<decltype(std::declval<StructuralMutatorObj&>().DefaultMaybeInplaceMutateExpected(
- std::declval<AnyView>())),
- Expected<UnchangedOr<Any>>>);
// ---------------------------------------------------------------------------
// Unchanged result protocol.
@@ -79,6 +78,21 @@ TEST(UnchangedOr, ConversionsAndAssignmentMacro) {
EXPECT_TRUE(std::move(moved).ValueUnchecked().same_as(original));
EXPECT_EQ(original.use_count(), 1);
+ const TInt borrowed(7);
+ TInt typed_original =
UnchangedOr<TInt>(Unchanged()).ValueOrUnchanged(borrowed);
+ EXPECT_TRUE(typed_original.same_as(borrowed));
+ EXPECT_EQ(borrowed.use_count(), 2);
+ TNumber base_original =
UnchangedOr<TNumber>(Unchanged()).ValueOrUnchanged(borrowed);
+ EXPECT_TRUE(base_original.same_as(borrowed));
+ EXPECT_EQ(borrowed.use_count(), 3);
+
+ UnchangedOr<TNumber> replacement = TInt(8);
+ TNumber replaced = std::move(replacement).ValueOrUnchanged(borrowed);
+ EXPECT_EQ(replaced.as_or_throw<TInt>()->value, 8);
+ EXPECT_TRUE(replaced.unique());
+ EXPECT_EQ(borrowed->value, 7);
+ EXPECT_EQ(borrowed.use_count(), 3);
+
UnchangedOr<double> numeric = UnchangedOr<int>(42);
EXPECT_EQ(AnyView(numeric).type_index(), TypeIndex::kTVMFFIFloat);
EXPECT_DOUBLE_EQ(std::move(numeric).ValueUnchecked(), 42.0);
@@ -165,32 +179,20 @@ TEST(StructuralMutate,
UnchangedProtocolResolvesAtThrowingEntryPoints) {
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)));
+ StructuralMutator mutator(make_object<Mutator>(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());
+ for (InplaceMode inplace_mode : {InplaceMode::kDisallow,
InplaceMode::kAllow}) {
+ Any packed_mode(inplace_mode);
+ EXPECT_EQ(packed_mode.type_index(), TypeIndex::kTVMFFIInt);
+ EXPECT_EQ(packed_mode.cast<int64_t>(), static_cast<int32_t>(inplace_mode));
+ auto result = mutator->MutateExpected(AnyView(value), inplace_mode);
+ ASSERT_TRUE(result.is_ok());
+ EXPECT_TRUE(std::move(result).value().IsUnchanged());
+ auto throwing_result = mutator->Mutate(AnyView(value), inplace_mode);
+ EXPECT_TRUE(throwing_result.IsUnchanged());
+
EXPECT_TRUE(std::move(throwing_result).ValueOrUnchanged(AnyView(value)).same_as(value));
+ }
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); })
@@ -206,30 +208,22 @@ TEST(StructuralMutate,
UnchangedProtocolResolvesAtThrowingEntryPoints) {
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");
+ auto narrow_result = [&](int64_t input,
+ InplaceMode inplace_mode) ->
Expected<UnchangedOr<int64_t>> {
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<int64_t>, result,
+
wrong_type_mutator->MutateExpected(input, inplace_mode));
+ return result;
+ };
+ for (InplaceMode inplace_mode : {InplaceMode::kDisallow,
InplaceMode::kAllow}) {
+ auto failure = narrow_result(-1, inplace_mode);
+ ASSERT_TRUE(failure.is_err());
+ EXPECT_EQ(failure.error().message(), "direct-forward failure");
+ auto wrong_type = narrow_result(1, inplace_mode);
+ ASSERT_TRUE(wrong_type.is_err());
+ EXPECT_EQ(wrong_type.error().kind(), "TypeError");
+ EXPECT_ANY_THROW(
+ wrong_type_mutator->Mutate(1,
inplace_mode).ValueOrUnchanged(AnyView(1)).cast<int64_t>());
+ }
for (WalkOrder order : {WalkOrder::kPreOrder, WalkOrder::kPostOrder}) {
TVar root("n");
@@ -262,7 +256,7 @@ 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(UnchangedOr<Any>, mapped,
- mutator->MutateExpected(self->field));
+ mutator->MutateExpected(self->field,
InplaceMode::kDisallow));
if (mapped.UnchangedOrSameAs(Any(self->field))) {
return Unchanged().CopyToTVMFFIAny();
}
@@ -331,16 +325,15 @@ class StructuralMapWithMutateCount : public
StructuralMapEngineBase {
const MutateCount& count() const { return count_; }
- Expected<UnchangedOr<Any>> DefaultMutateExpected(AnyView value) noexcept {
+ Expected<UnchangedOr<Any>> DefaultMutateExpected(AnyView value,
+ InplaceMode inplace_mode)
noexcept {
++count_.value;
- ++count_.mutate_expected;
- return StructuralMapEngineBase::DefaultMutateExpected(value);
- }
-
- Expected<UnchangedOr<Any>> DefaultMaybeInplaceMutateExpected(AnyView value)
noexcept {
- ++count_.value;
- ++count_.maybe_inplace_expected;
- return StructuralMapEngineBase::DefaultMaybeInplaceMutateExpected(value);
+ if (inplace_mode == InplaceMode::kAllow) {
+ ++count_.maybe_inplace_expected;
+ } else {
+ ++count_.mutate_expected;
+ }
+ return StructuralMapEngineBase::DefaultMutateExpected(value, inplace_mode);
}
protected:
@@ -388,13 +381,13 @@ TEST(StructuralMap,
ParentLayerOwnsBothDescentsAndProvidesState) {
EXPECT_EQ(mutator->VarRemapGetExpected(1).error().kind(), "TypeError");
mutator->VarRemapSetExpected(key, Any(Unchanged())).value();
- ASSERT_FALSE(mutator->MutateExpected(String("unmatched")).is_err());
+ ASSERT_FALSE(mutator->MutateExpected(String("unmatched"),
InplaceMode::kDisallow).is_err());
AnyArray rebuild_root{int64_t{1}};
- ASSERT_FALSE(mutator->MutateExpected(rebuild_root).is_err());
+ ASSERT_FALSE(mutator->MutateExpected(rebuild_root,
InplaceMode::kDisallow).is_err());
-
ASSERT_FALSE(mutator->MaybeInplaceMutateExpected(String("unmatched")).is_err());
+ ASSERT_FALSE(mutator->MutateExpected(String("unmatched"),
InplaceMode::kAllow).is_err());
AnyArray inplace_root{int64_t{1}};
- ASSERT_FALSE(mutator->MaybeInplaceMutateExpected(inplace_root).is_err());
+ ASSERT_FALSE(mutator->MutateExpected(inplace_root,
InplaceMode::kAllow).is_err());
EXPECT_GT(engine->count().mutate_expected, 0);
EXPECT_GT(engine->count().maybe_inplace_expected, 0);
@@ -404,8 +397,9 @@ TEST(StructuralMap,
ParentLayerOwnsBothDescentsAndProvidesState) {
TVar var("n");
AnyArray repeated{var, var};
- AnyArray mapped =
-
std::move(mutator->Mutate<AnyArray>(repeated)).ValueOrUnchanged(std::move(repeated));
+ AnyArray mapped = mutator->Mutate(repeated, InplaceMode::kDisallow)
+ .ValueOrUnchanged(AnyView(repeated))
+ .cast<AnyArray>();
EXPECT_EQ(var_callback_count, 2);
EXPECT_FALSE(mapped[0].cast<TVar>().same_as(mapped[1].cast<TVar>()));
}
@@ -414,7 +408,7 @@ TEST(StructuralMutate,
CallbackOwnsMutationAndErrorsStayExpected) {
std::vector<int64_t> trace;
auto mutate_array = [&](const AnyArray& value, StructuralMutateLayer*
mutator) -> Expected<Any> {
EXPECT_EQ(mutator->callback_tag(), 23);
- auto first_result = mutator->MutateExpected(value[0]);
+ auto first_result = mutator->MutateExpected(value[0],
InplaceMode::kDisallow);
if (TVM_FFI_PREDICT_FALSE(first_result.is_err())) {
return Unexpected(std::move(first_result).error());
}
@@ -430,7 +424,9 @@ TEST(StructuralMutate,
CallbackOwnsMutationAndErrorsStayExpected) {
StructuralMutator mutator(make_object<Mutator>(std::move(mutate_array),
std::move(mutate_int)));
AnyArray root{int64_t{1}, int64_t{2}};
- AnyArray mapped =
std::move(mutator->Mutate<AnyArray>(root)).ValueOrUnchanged(std::move(root));
+ AnyArray mapped = mutator->Mutate(root, InplaceMode::kDisallow)
+ .ValueOrUnchanged(AnyView(root))
+ .cast<AnyArray>();
ASSERT_EQ(mapped.size(), 2U);
EXPECT_EQ(mapped[0].cast<int64_t>(), 2);
EXPECT_EQ(mapped[1].cast<int64_t>(), 10);
@@ -468,8 +464,9 @@ TEST(StructuralMutate, CallbackControlsRecursion) {
StructuralMutate(
root,
[](const TPair& pair, StructuralMutatorObj* mutator) ->
Expected<UnchangedOr<ObjectRef>> {
- TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<ObjectRef>,
lhs_result,
-
mutator->MutateExpected<ObjectRef>(pair->lhs));
+ TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+ UnchangedOr<ObjectRef>, lhs_result,
+ mutator->MutateExpected(pair->lhs, InplaceMode::kDisallow));
ObjectRef original_lhs = pair->lhs;
ObjectRef lhs =
std::move(lhs_result).ValueOrUnchanged(std::move(original_lhs));
return UnchangedOr<ObjectRef>(TPair(std::move(lhs), pair->rhs));
@@ -493,7 +490,7 @@ TEST(StructuralMutate, SingleCallbackCanDelegateToDefault) {
if (auto integer = value.as<int64_t>()) {
return Any(*integer + 1);
}
- return mutator->DefaultMutateExpected(value);
+ return mutator->DefaultMutateExpected(value, InplaceMode::kDisallow);
};
AnyArray root{int64_t{1}, AnyArray{int64_t{2}}};
AnyArray result = StructuralMutate(root, mutate).cast<AnyArray>();
@@ -503,20 +500,56 @@ TEST(StructuralMutate,
SingleCallbackCanDelegateToDefault) {
}
TEST(StructuralMutate, PreservesUniqueContainerIdentity) {
+ auto increment = [](int64_t value, StructuralMutatorObj*) -> Expected<Any> {
+ return Any(value + 1);
+ };
AnyArray inner{int64_t{1}};
const Object* inner_address = inner.get();
AnyArray root{Any(std::move(inner))};
const Object* root_address = root.get();
- AnyArray mapped =
- StructuralMutate(std::move(root), [](int64_t value,
StructuralMutatorObj*) -> Expected<Any> {
- return Any(value + 1);
- }).cast<AnyArray>();
+ AnyArray mapped = StructuralMutate(std::move(root),
increment).cast<AnyArray>();
AnyArray mapped_inner = mapped[0].cast<AnyArray>();
EXPECT_EQ(mapped.get(), root_address);
EXPECT_EQ(mapped_inner.get(), inner_address);
EXPECT_EQ(mapped_inner[0].cast<int64_t>(), 2);
+
+ using Mutator = StructuralMutateEngine<StructuralMapEngineBase,
decltype(increment)>;
+ StructuralMutator mutator(make_object<Mutator>(increment));
+ AnyArray default_original{int64_t{1}};
+ EXPECT_FALSE(mutator->Mutate(default_original)
+ .ValueOrUnchanged(AnyView(default_original))
+ .cast<AnyArray>()
+ .same_as(default_original));
+ EXPECT_FALSE(std::move(mutator->MutateExpected(default_original))
+ .value()
+ .ValueOrUnchanged(AnyView(default_original))
+ .cast<AnyArray>()
+ .same_as(default_original));
+ EXPECT_FALSE(std::move(mutator->DefaultMutateExpected(default_original))
+ .value()
+ .ValueOrUnchanged(AnyView(default_original))
+ .cast<AnyArray>()
+ .same_as(default_original));
+ EXPECT_EQ(default_original[0].cast<int64_t>(), 1);
+ EXPECT_TRUE(default_original.unique());
+
+ for (InplaceMode inplace_mode : {InplaceMode::kDisallow,
InplaceMode::kAllow}) {
+ for (bool shared : {false, true}) {
+ AnyArray original{int64_t{1}};
+ Any alias = shared ? Any(original) : Any();
+ EXPECT_EQ(original.use_count(), shared ? 2 : 1);
+ AnyArray result = std::move(mutator->MutateExpected(original,
inplace_mode))
+ .value()
+ .ValueOrUnchanged(AnyView(original))
+ .cast<AnyArray>();
+ EXPECT_EQ(result.same_as(original), inplace_mode == InplaceMode::kAllow
&& !shared);
+ EXPECT_EQ(original[0].cast<int64_t>(),
+ inplace_mode == InplaceMode::kAllow && !shared ? 2 : 1);
+ EXPECT_EQ(result[0].cast<int64_t>(), 2);
+ }
+ }
}
TEST(StructuralMutate, RootByValueProtectsSharedParentSubvalue) {
@@ -532,6 +565,26 @@ TEST(StructuralMutate,
RootByValueProtectsSharedParentSubvalue) {
EXPECT_NE(mapped.get(), child_address);
EXPECT_EQ(outer[0].cast<AnyArray>()[0].cast<int64_t>(), 1);
EXPECT_EQ(mapped[0].cast<int64_t>(), 2);
+
+ AnyArray shared_outer = outer; //
NOLINT(performance-unnecessary-copy-initialization)
+ bool denied_unique_child = false;
+ auto mutate = [&](AnyView value, StructuralMutatorObj* mutator,
+ InplaceMode inplace_mode) -> Expected<UnchangedOr<Any>> {
+ if (auto integer = value.as<int64_t>()) return Any(*integer + 1);
+ if (value.as<Object>()->unique() && inplace_mode ==
InplaceMode::kDisallow) {
+ denied_unique_child = true;
+ }
+ return mutator->DefaultMutateExpected(value, inplace_mode);
+ };
+ using Mutator = StructuralMutateEngine<StructuralMapEngineBase,
decltype(mutate)>;
+ StructuralMutator mutator(make_object<Mutator>(mutate));
+ AnyArray rebuilt = std::move(mutator->MutateExpected(outer,
InplaceMode::kAllow))
+ .value()
+ .ValueOrUnchanged(AnyView(outer))
+ .cast<AnyArray>();
+ EXPECT_TRUE(denied_unique_child);
+ EXPECT_EQ(shared_outer[0].cast<AnyArray>()[0].cast<int64_t>(), 1);
+ EXPECT_EQ(rebuilt[0].cast<AnyArray>()[0].cast<int64_t>(), 2);
}
TEST(StructuralMutate, CallbackArityControlsInplaceMutation) {
@@ -539,18 +592,19 @@ TEST(StructuralMutate,
CallbackArityControlsInplaceMutation) {
AnyArray copy_on_write_root{int64_t{1}};
const Object* inplace_root_address = inplace_root.get();
const Object* copy_on_write_root_address = copy_on_write_root.get();
- std::vector<bool> allow_inplace_trace;
+ std::vector<InplaceMode> inplace_mode_trace;
AnyArray inplace_mapped =
StructuralMutate(
std::move(inplace_root),
[&](const AnyArray& value, StructuralMutatorObj* mutator,
- bool allow_inplace) -> Expected<Any> {
- allow_inplace_trace.push_back(allow_inplace);
- return mutator->DefaultMaybeInplaceMutateExpected(value,
allow_inplace);
+ InplaceMode inplace_mode) -> Expected<Any> {
+ inplace_mode_trace.push_back(inplace_mode);
+ EXPECT_GT(value.use_count(), 1);
+ return mutator->DefaultMutateExpected(value, inplace_mode);
},
- [&](int64_t value, StructuralMutatorObj*, bool allow_inplace) ->
Expected<Any> {
- allow_inplace_trace.push_back(allow_inplace);
+ [&](int64_t value, StructuralMutatorObj*, InplaceMode inplace_mode)
-> Expected<Any> {
+ inplace_mode_trace.push_back(inplace_mode);
return Any(value + 1);
})
.cast<AnyArray>();
@@ -559,7 +613,7 @@ TEST(StructuralMutate,
CallbackArityControlsInplaceMutation) {
StructuralMutate(
std::move(copy_on_write_root),
[](const AnyArray& value, StructuralMutatorObj* mutator) ->
Expected<Any> {
- return mutator->DefaultMutateExpected(value);
+ return mutator->DefaultMutateExpected(value,
InplaceMode::kDisallow);
},
[](int64_t value, StructuralMutatorObj*) -> Expected<Any> { return
Any(value + 1); })
.cast<AnyArray>();
@@ -568,7 +622,8 @@ TEST(StructuralMutate,
CallbackArityControlsInplaceMutation) {
EXPECT_NE(copy_on_write_mapped.get(), copy_on_write_root_address);
EXPECT_EQ(inplace_mapped[0].cast<int64_t>(), 2);
EXPECT_EQ(copy_on_write_mapped[0].cast<int64_t>(), 2);
- EXPECT_EQ(allow_inplace_trace, (std::vector<bool>{true, false}));
+ EXPECT_EQ(inplace_mode_trace,
+ (std::vector<InplaceMode>{InplaceMode::kAllow,
InplaceMode::kDisallow}));
}
TEST(StructuralMutate, MatchedVarOwnsRemapConsistency) {
@@ -1106,7 +1161,9 @@ void CheckDynamicParentLayer() {
StructuralMutator mutator(engine);
AnyArray root{int64_t{1}};
- AnyArray mapped =
std::move(mutator->Mutate<AnyArray>(root)).ValueOrUnchanged(std::move(root));
+ AnyArray mapped = mutator->Mutate(root, InplaceMode::kDisallow)
+ .ValueOrUnchanged(AnyView(root))
+ .cast<AnyArray>();
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 5553c6c3..366bb96a 100644
--- a/tests/cpp/testing_object.h
+++ b/tests/cpp/testing_object.h
@@ -254,9 +254,9 @@ class TMutatePairObj : public Object {
const TMutatePairObj* self =
details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
TMutatePairObj>(value);
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<ObjectRef>, lhs,
- mutator->MutateExpected(self->lhs));
+ mutator->MutateExpected(self->lhs,
InplaceMode::kDisallow));
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<ObjectRef>, rhs,
- mutator->MutateExpected(self->rhs));
+ mutator->MutateExpected(self->rhs,
InplaceMode::kDisallow));
if (lhs.UnchangedOrSameAs(self->lhs) && rhs.UnchangedOrSameAs(self->rhs)) {
return Unchanged().CopyToTVMFFIAny();
}
diff --git a/tests/python/test_structural.py b/tests/python/test_structural.py
index 8c5a7901..197b5157 100644
--- a/tests/python/test_structural.py
+++ b/tests/python/test_structural.py
@@ -329,6 +329,41 @@ def test_structural_visit_default_visit_binding() -> None:
assert nested_trace == ["array", 1, 2]
+def test_structural_mutate_inplace_mode() -> None:
+ assert tvm_ffi.InplaceMode.DISALLOW == 0
+ assert tvm_ffi.InplaceMode.ALLOW == 1
+ assert not tvm_ffi.InplaceMode.DISALLOW
+ assert tvm_ffi.InplaceMode.ALLOW
+ inplace_trace: list[int] = []
+
+ def mutate_with_mode(value: int, mutator: tvm_ffi.StructuralMutator,
inplace_mode: int) -> int:
+ assert isinstance(mutator, tvm_ffi.StructuralMutator)
+ assert type(inplace_mode) is int
+ inplace_trace.append(inplace_mode)
+ return value + 1
+
+ mode_root = tvm_ffi.Array([1])
+ mode_mapped = tvm_ffi.structural_mutate(mode_root, (int, mutate_with_mode))
+ assert inplace_trace == [tvm_ffi.InplaceMode.DISALLOW]
+ assert not mode_mapped.same_as(mode_root)
+ assert list(mode_root) == [1]
+ assert list(mode_mapped) == [2]
+
+ def mutate_array_with_mode(
+ value: tvm_ffi.Array, mutator: tvm_ffi.StructuralMutator,
inplace_mode: int
+ ) -> object:
+ assert type(inplace_mode) is int
+ inplace_trace.append(inplace_mode)
+ return mutator.default_mutate(value)
+
+ owned_root = tvm_ffi.Array([1])
+ owned_mapped = tvm_ffi.structural_mutate(
+ owned_root._move(), (tvm_ffi.Array, mutate_array_with_mode)
+ )
+ assert inplace_trace == [tvm_ffi.InplaceMode.DISALLOW,
tvm_ffi.InplaceMode.ALLOW]
+ assert list(owned_mapped) == [1]
+
+
def test_structural_mutate_callback_owned_recursion_and_errors() -> None:
trace: list[int | str] = []
@@ -372,22 +407,6 @@ def
test_structural_mutate_callback_owned_recursion_and_errors() -> None:
assert list(default_root) == [3, 4]
assert list(default_mapped) == [4, 5]
- inplace_trace: list[bool] = []
-
- def mutate_with_flag(
- value: int, mutator: tvm_ffi.StructuralMutator, allow_inplace: bool
- ) -> int:
- assert isinstance(mutator, tvm_ffi.StructuralMutator)
- inplace_trace.append(allow_inplace)
- return value + 1
-
- flagged_root = tvm_ffi.Array([1])
- flagged_mapped = tvm_ffi.structural_mutate(flagged_root, (int,
mutate_with_flag))
- assert inplace_trace == [False]
- assert not flagged_mapped.same_as(flagged_root)
- assert list(flagged_root) == [1]
- assert list(flagged_mapped) == [2]
-
direct_trace: list[int] = []
def fail_directly(value: int, mutator: tvm_ffi.StructuralMutator) ->
object: