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 c5e636bb [EXTRA] Simplify StructuralMap engine policy (#771)
c5e636bb is described below
commit c5e636bbfb097cca610acee841e26d751d47a63b
Author: Tianqi Chen <[email protected]>
AuthorDate: Wed Sep 9 18:33:57 2026 -0400
[EXTRA] Simplify StructuralMap engine policy (#771)
StructuralMap needs identity state only to keep variables consistent
when
descent rewrites a definition. Callback results are separate: callbacks
run
at every occurrence in their selected pre- or post-descent position.
Make default C++ descent the sole owner of FreeVar and DAG remapping,
with
definitions binding rewritten results and unchanged simple definitions
left
absent. Keep the remap ABI stable and use an owned raw-pointer-keyed
store for
efficient identity lookups.
Adopt whole raw TVMFFIAny results by value while leaving
destructor-visible
slot moves pointer-based. Initialize owning Any storage member-wise so
GCC
retains scalar state and avoids an overlapping narrow-store/wide-reload
stall.
Update existing C++ and Python expectations in place and document
StructuralMap as post(D(pre)) alongside StructuralWalk's stateless tree
traversal. Rust remains unchanged for a follow-up.
---
docs/concepts/structural_eq_hash.rst | 47 ++-
include/tvm/ffi/any.h | 31 +-
include/tvm/ffi/expected.h | 9 +-
include/tvm/ffi/extra/structural_mutate.h | 495 ++++++++++++------------------
include/tvm/ffi/extra/structural_visit.h | 30 +-
python/tvm_ffi/structural.py | 9 +-
tests/cpp/extra/test_structural_mutate.cc | 103 +++----
tests/python/test_structural.py | 63 ++--
8 files changed, 380 insertions(+), 407 deletions(-)
diff --git a/docs/concepts/structural_eq_hash.rst
b/docs/concepts/structural_eq_hash.rst
index 74efd998..51840d5d 100644
--- a/docs/concepts/structural_eq_hash.rst
+++ b/docs/concepts/structural_eq_hash.rst
@@ -1092,7 +1092,11 @@ Structural Walk
:func:`~tvm_ffi.structural_walk` invokes an analysis callback at each matching
value. Callback entries are ordered, and only the first matching entry runs.
-A walk is post-order by default.
+A walk is post-order by default. It is a pure tree traversal with no engine
+state: callbacks fire once per occurrence, a var's type is walked under its
+region at every occurrence, and a shared DAG node is visited once per parent.
+To descend a pattern var once or to deduplicate a graph, compose it in a
+pre-order callback with its own visited set that returns ``SKIP`` on a repeat.
A callback may return:
- :attr:`~tvm_ffi.WalkResult.ADVANCE` or ``None`` to continue.
@@ -1135,7 +1139,24 @@ callback returns the mapped value: either its input
unchanged or a replacement.
Mapping always visits all structural children; there is no ``SKIP`` or
``VisitInterrupt`` result. A callback exception aborts the mapping and
is propagated with structural visit context. Mapping is post-order by default,
-so a callback receives a value whose children have already been mapped.
+so a callback receives a value whose children have already been mapped and is
+selected by the type of what descent produced.
+
+The policy is ``mutate(x) = post(D(pre(x)))``. ``D`` is descent with a var
remap
+that keeps the result consistent when a var is rewritten as a cascade effect of
+its fields changing during descent. Callbacks never read or write that remap
and
+fire once per occurrence in whichever position they sit.
+
+Var policy in default ``D``: each var is descended at most once in def, at its
+first occurrence in a pattern def or its only occurrence in a simple def, and
+then returns the rewritten result if any at a use. Definitions are assumed to
+precede uses; a var with no definition is treated as a use, so free vars are
+replaced by a pre-callback, which runs at every occurrence.
+
+Canonical use cases: pre for var replacement to another value or var; post for
+rewriting a tree node after its children are mapped. For a DAG node, the
+post-callback fires at every occurrence, so a graph rewrite keeps its own
+node-to-value memo; the engine does not dedup callback rewrites.
Post-order is natural for bottom-up compiler rewrites because children have
already been mapped when the callback runs:
@@ -1242,17 +1263,17 @@ type defining it must also define ``__s_mutate__``. If
the optional hook is
absent, the engine uses the default non-in-place mutation; generic reflected
fields are never mutated in place automatically.
-When an object marked ``structural_eq="var"`` or ``structural_eq="dag"``
registers
-either ``__s_mutate__`` or ``__s_maybe_inplace_mutate__`` hooks, it should:
-
-1. call ``var_remap_get`` before recursively mutating the value;
-2. immediately return the mapped value on a hit;
-3. compute the final result on a miss; and
-4. call ``var_remap_set`` with that final result before returning it.
-
-Structural-map callbacks apply the same identity rule automatically: the
complete
-callback/default result is recorded on the first occurrence and reused for
later
-occurrences of the same FreeVar or DAG node.
+When an object marked ``structural_eq="var"`` registers either ``__s_mutate__``
+or ``__s_maybe_inplace_mutate__``, its hook owns the same definition-only
+policy as reflected descent: look up first, skip descent and insertion for a
+miss at a use, omit an unchanged simple definition, and record an unchanged
+pattern definition with the unchanged marker, or the var itself. A hook may
+store either, and TVM's hook stores the var. A DAG hook similarly looks up
+first and records its descent result. ``var_remap_set`` itself is a simple
+insertion primitive; the hook decides whether and what to store.
+
+Structural-map callbacks never use this descent remap. They run at every
+occurrence as described above.
C++ APIs
~~~~~~~~
diff --git a/include/tvm/ffi/any.h b/include/tvm/ffi/any.h
index 0177c610..bd077b24 100644
--- a/include/tvm/ffi/any.h
+++ b/include/tvm/ffi/any.h
@@ -72,6 +72,15 @@ class AnyView {
TVM_FFI_INLINE void swap(AnyView& other) noexcept { std::swap(data_,
other.data_); }
/*! \return the internal type index */
TVM_FFI_INLINE int32_t type_index() const noexcept { return
data_.type_index; }
+ /*!
+ * \brief Check if the two AnyView values have the same raw type and value.
+ * \param other The other AnyView.
+ * \return True if the raw values are identical, false otherwise.
+ */
+ TVM_FFI_INLINE bool same_as(const AnyView& other) const noexcept {
+ return data_.type_index == other.data_.type_index &&
+ data_.zero_padding == other.data_.zero_padding && data_.v_int64 ==
other.data_.v_int64;
+ }
/*! \brief Default constructor */
TVM_FFI_INLINE AnyView() {
data_.type_index = TypeIndex::kTVMFFINone;
@@ -530,6 +539,15 @@ class Any {
*/
TVM_FFI_INLINE std::string GetTypeKey() const { return
TypeIndexToTypeKey(data_.type_index); }
+ private:
+ // Member-wise on purpose: an aggregate copy makes GCC re-store type_index
as a partial
+ // write before a wide reload of the same word, which stalls store
forwarding.
+ TVM_FFI_INLINE explicit Any(UnsafeInit, TVMFFIAny raw) noexcept {
+ data_.type_index = raw.type_index;
+ data_.zero_padding = raw.zero_padding;
+ data_.v_int64 = raw.v_int64;
+ }
+
friend struct details::AnyUnsafe;
friend struct AnyHash;
friend struct AnyEqual;
@@ -607,6 +625,15 @@ struct AnyUnsafe : public ObjectUnsafe {
return any;
}
+ /*!
+ * \brief Adopt a raw handle the caller hands over whole, with no storage to
clear.
+ * \param raw The raw handle to adopt.
+ * \return The owning Any.
+ *
+ * The pointer form stays for moving out of a slot whose destructor will
still visit it.
+ */
+ TVM_FFI_INLINE static Any MoveTVMFFIAnyRawToAny(TVMFFIAny raw) { return
Any(UnsafeInit{}, raw); }
+
template <typename T>
TVM_FFI_INLINE static bool CheckAnyStrict(const Any& ref) {
if constexpr (!std::is_same_v<T, Any>) {
@@ -756,7 +783,7 @@ struct AnyHash {
throw details::MoveFromSafeCallRaised();
}
}
- Any result_any = details::AnyUnsafe::MoveTVMFFIAnyToAny(&result);
+ Any result_any = details::AnyUnsafe::MoveTVMFFIAnyRawToAny(result);
TVM_FFI_ICHECK_EQ(result_any.type_index(), TypeIndex::kTVMFFIInt);
return static_cast<uint64_t>(result_any.data_.v_int64);
}
@@ -888,7 +915,7 @@ struct AnyEqual {
throw details::MoveFromSafeCallRaised();
}
}
- Any result_any = details::AnyUnsafe::MoveTVMFFIAnyToAny(&result);
+ Any result_any = details::AnyUnsafe::MoveTVMFFIAnyRawToAny(result);
TVM_FFI_ICHECK(result_any.type_index() == TypeIndex::kTVMFFIBool);
return result_any.data_.v_int64 != 0;
}
diff --git a/include/tvm/ffi/expected.h b/include/tvm/ffi/expected.h
index 77ba7b39..fb3f6ab7 100644
--- a/include/tvm/ffi/expected.h
+++ b/include/tvm/ffi/expected.h
@@ -281,6 +281,8 @@ class Expected {
template <typename>
friend class Expected;
Expected() = default;
+ TVM_FFI_INLINE Expected(UnsafeInit, TVMFFIAny raw) noexcept
+ : data_(details::AnyUnsafe::MoveTVMFFIAnyRawToAny(raw)) {}
friend struct details::ExpectedUnsafe;
@@ -368,6 +370,9 @@ class Expected<void> {
}
private:
+ TVM_FFI_INLINE Expected(UnsafeInit, TVMFFIAny raw) noexcept
+ : data_(details::AnyUnsafe::MoveTVMFFIAnyRawToAny(raw)) {}
+
friend struct details::ExpectedUnsafe;
Any data_; // Invariant: holds FFI None on success or an Error.
@@ -390,9 +395,7 @@ struct ExpectedUnsafe {
*/
template <typename T>
TVM_FFI_INLINE static Expected<T> MoveFromTVMFFIAny(TVMFFIAny raw) {
- Expected<T> result;
- result.data_ = AnyUnsafe::MoveTVMFFIAnyToAny(&raw);
- return result;
+ return Expected<T>(UnsafeInit{}, raw);
}
/*!
diff --git a/include/tvm/ffi/extra/structural_mutate.h
b/include/tvm/ffi/extra/structural_mutate.h
index 73659ae8..3186d035 100644
--- a/include/tvm/ffi/extra/structural_mutate.h
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -27,7 +27,6 @@
#include <tvm/ffi/c_api.h>
#include <tvm/ffi/cast.h>
#include <tvm/ffi/container/array.h>
-#include <tvm/ffi/container/map.h>
#include <tvm/ffi/container/tuple.h>
#include <tvm/ffi/container/variant.h>
#include <tvm/ffi/expected.h>
@@ -44,6 +43,7 @@
#include <string>
#include <tuple>
#include <type_traits>
+#include <unordered_map>
#include <utility>
namespace tvm {
@@ -52,6 +52,12 @@ namespace ffi {
class StructuralMutatorObj;
template <typename T>
class UnchangedOr;
+template <typename Parent, WalkOrder order, typename... Callbacks>
+class StructuralMapEngine;
+template <typename Parent, WalkOrder order>
+class StructuralMapDynEngine;
+template <typename Parent, typename... Callbacks>
+class StructuralMutateEngine;
/*!
* \brief ABI callback type for structural mutation.
@@ -139,6 +145,9 @@ struct StructuralMutatorVTable {
};
namespace details {
+template <typename Parent>
+class StructuralMutateDynEngine;
+
struct UnchangedOrUnsafe;
} // namespace details
@@ -166,7 +175,7 @@ struct Unchanged {
// NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
TVM_FFI_INLINE operator Any() const noexcept {
TVMFFIAny raw = CopyToTVMFFIAny();
- return details::AnyUnsafe::MoveTVMFFIAnyToAny(&raw);
+ return details::AnyUnsafe::MoveTVMFFIAnyRawToAny(raw);
}
};
@@ -348,7 +357,7 @@ class StructuralMutatorObj : public Object {
} else {
TVMFFIAny result = (*vtable_->mutate)(this, value);
if
(TVM_FFI_PREDICT_FALSE(!TypeTraits<Expected<UnchangedOr<T>>>::CheckAnyStrict(&result)))
{
- (void)details::AnyUnsafe::MoveTVMFFIAnyToAny(&result);
+ (void)details::AnyUnsafe::MoveTVMFFIAnyRawToAny(result);
return details::SMutateDeclaredTypeError();
}
return
details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<T>>(result);
@@ -389,7 +398,7 @@ class StructuralMutatorObj : public Object {
} 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);
+ (void)details::AnyUnsafe::MoveTVMFFIAnyRawToAny(result);
return details::SMutateDeclaredTypeError();
}
return
details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<T>>(result);
@@ -423,9 +432,9 @@ class StructuralMutatorObj : public Object {
* \param value The value to mutate.
* \return The mutated value, or an Error if hook dispatch, copying, or
field mutation failed.
*
- * \note A registered ``__s_mutate__`` hook is dispatched before the
reflected fallback and is
- * responsible for variable-remap lookup and insertion when it
represents a FreeVar or DAG
- * identity. Automatic remapping applies only to the reflected
fallback.
+ * \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<Any> DefaultMutateExpected(AnyView value) noexcept {
@@ -525,7 +534,10 @@ class StructuralMutatorObj : public Object {
TVM_FFI_DECLARE_OBJECT_INFO("ffi.StructuralMutator", StructuralMutatorObj,
Object);
/// \endcond
- protected:
+ private:
+ template <typename Parent>
+ friend class details::StructuralMutateDynEngine;
+
/*!
* \brief Diagnostic for a malformed ``__s_mutate__`` registration.
*
@@ -569,22 +581,6 @@ class StructuralMutatorObj : public Object {
return result;
}
- // The cold remainder of DefaultMutateRaw, out of line so that
always-inlined caller stays
- // small at every inlining site. `attr` is the attribute the caller already
read, so the
- // column is never looked up twice.
- /*!
- * \brief Whether a node's identity is remappable, so it maps once and
reuses that result.
- * \param type_index The node's runtime type index.
- * \return True for a FreeVar or DAG node.
- */
- TVM_FFI_INLINE static bool IsRemappableIdentity(int32_t type_index) noexcept
{
- if (type_index < TypeIndex::kTVMFFIStaticObjectBegin) return false;
- const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(type_index);
- return type_info->metadata != nullptr &&
- (type_info->metadata->structural_eq_hash_kind ==
kTVMFFISEqHashKindFreeVar ||
- type_info->metadata->structural_eq_hash_kind ==
kTVMFFISEqHashKindDAGNode);
- }
-
/*! \brief The cold remainder of DefaultMutateRaw: an ffi.Function hook, or
no hook at all. */
TVMFFIAny DefaultMutateRawTail(AnyView value, AnyView attr) noexcept {
if (attr.type_index() != TypeIndex::kTVMFFINone) {
@@ -600,28 +596,42 @@ class StructuralMutatorObj : public Object {
if (value.type_index() < TypeIndex::kTVMFFIStaticObjectBegin) {
return details::AnyUnsafe::MoveAnyToTVMFFIAny(Any(value));
}
- // A FreeVar or DAG node maps once and every later occurrence reuses that
result, so the
- // 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.
+ const TVMFFITypeInfo* type_info = TVMFFIGetTypeInfo(value.type_index());
+ const int32_t identity_kind = type_info->metadata == nullptr
+ ? kTVMFFISEqHashKindUnsupported
+ :
type_info->metadata->structural_eq_hash_kind;
+ const bool is_free_var = identity_kind == kTVMFFISEqHashKindFreeVar;
+ const bool is_dag_node = identity_kind == kTVMFFISEqHashKindDAGNode;
+ if (is_free_var || is_dag_node) {
+ // Only None means no cached descent result; every other value,
including the unchanged
+ // marker used by a pattern definition, is returned directly.
Expected<Any> mapped = VarRemapGetExpected(value);
if (details::ExpectedUnsafe::GetData(mapped).type_index() !=
TypeIndex::kTVMFFINone) {
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(mapped));
}
}
+
+ // A FreeVar outside a definition region is a use. A miss means its
definition was unchanged
+ // (or it is free), so there is no field descent and no remap insertion.
+ if (is_free_var && def_region_kind() == kTVMFFIDefRegionKindNone) {
+ return Unchanged().CopyToTVMFFIAny();
+ }
+
Expected<Any> result = details::MutateReflectedFieldsExpected(this, value);
if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
}
- if (remappable) {
+ if (is_free_var || is_dag_node) {
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())));
+ // Bind the descent result. The one exception is an unchanged simple
definition: its
+ // uses resolve to the var itself on a miss, so there is nothing to
record.
+ if (is_dag_node || def_region_kind() == kTVMFFIDefRegionKindPattern ||
+ result_value.type_index() != TypeIndex::kTVMFFIUnchanged) {
+ Expected<void> set_result = VarRemapSetExpected(value, result_value);
+ if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
+ return details::ExpectedUnsafe::MoveToTVMFFIAny(
+ Expected<Any>(Unexpected(std::move(set_result).error())));
+ }
}
}
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
@@ -661,9 +671,10 @@ class StructuralMutatorObj : public Object {
}
return result;
}
- return DefaultMutateRaw(value);
+ return
details::ExpectedUnsafe::MoveToTVMFFIAny(DefaultMutateExpected(value));
}
+ protected:
/*!
* \brief Construct a structural mutator from an immutable dispatch vtable.
* \param vtable The non-null dispatch table for this mutator. It must
outlive this object.
@@ -828,13 +839,12 @@ namespace details {
/// \cond Doxygen_Suppress
// Return from the current raw or same-T Expected mutation function if Result
is an Error.
// 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())); \
- } \
+#define TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result)
\
+ do {
\
+ auto&& tvm_ffi_res_ = (Result);
\
+ if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.is_err())) {
\
+ return
::tvm::ffi::details::ExpectedReturnHelper(::std::move(tvm_ffi_res_)); \
+ }
\
} while (0)
/// \endcond
@@ -964,16 +974,29 @@ class StructuralMapEngineBase : public
StructuralMutatorObj {
explicit StructuralMapEngineBase(const StructuralMutatorVTable* vtable)
: StructuralMutatorObj(vtable) {}
+ ~StructuralMapEngineBase() {
+ for (const auto& kv : var_remap_) {
+ details::ObjectUnsafe::DecRefObjectHandle(
+ reinterpret_cast<TVMFFIObjectHandle>(const_cast<Object*>(kv.first)));
+ }
+ }
+
protected:
/*! \brief Return the empty state tuple exposed to typed map callbacks. */
TVM_FFI_INLINE StateTupleType StateTuple() const noexcept { return {}; }
+ private:
/// \cond Doxygen_Suppress
// Out of line so its strings stay out of the per-node dispatch function,
which TryLink inlines
// into. Shared by the typed and dynamic engines below.
TVM_FFI_COLD_CODE static Expected<Any> SMutateDescentTypeError() noexcept {
return Unexpected(Error("TypeError", "structural mutate: descent changed
the node type", ""));
}
+
+ TVM_FFI_COLD_CODE static details::UnexpectedReturnHelper
VarRemapKeyTypeError() noexcept {
+ return details::UnexpectedReturnHelper(
+ Unexpected(Error("TypeError", "Variable-remap key must be an
object-backed value", "")));
+ }
/// \endcond
/*!
@@ -1000,6 +1023,7 @@ class StructuralMapEngineBase : public
StructuralMutatorObj {
return details::ExpectedUnsafe::MoveToTVMFFIAny(self->VarRemapSetImpl(var,
mapped_value));
}
+ protected:
/*!
* \brief Append \p node to a failed result's mutate error context.
* \param result The failed result whose Error is annotated.
@@ -1021,20 +1045,13 @@ class StructuralMapEngineBase : public
StructuralMutatorObj {
* \return The owning replacement, FFI None on a miss, or an Error.
*/
Expected<Any> VarRemapGetImpl(AnyView var) noexcept {
- if (var.type_index() < TypeIndex::kTVMFFIStaticObjectBegin) {
- return Unexpected(
- Error("TypeError", "Variable-remap key must be an object-backed
value", ""));
- }
- try {
- ObjectRef var_ref = var.cast<ObjectRef>();
- std::optional<Any> result = var_remap_.Get(var_ref);
- if (!result.has_value()) {
- return Any(nullptr);
- }
- return *std::move(result);
- } catch (const Error& err) {
- return Unexpected(err);
+ if (TVM_FFI_PREDICT_FALSE(var.type_index() <
TypeIndex::kTVMFFIStaticObjectBegin)) {
+ return VarRemapKeyTypeError();
}
+ const Object* var_ptr =
+ details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
Object>(var);
+ auto it = var_remap_.find(var_ptr);
+ return it == var_remap_.end() ? Any(nullptr) : it->second;
}
/*!
@@ -1044,36 +1061,48 @@ class StructuralMapEngineBase : public
StructuralMutatorObj {
* \return Successful completion, or an Error if the binding cannot be
stored.
*/
Expected<void> VarRemapSetImpl(AnyView var, AnyView mapped_value) noexcept {
- if (var.type_index() < TypeIndex::kTVMFFIStaticObjectBegin) {
- return Unexpected(
- Error("TypeError", "Variable-remap key must be an object-backed
value", ""));
+ if (TVM_FFI_PREDICT_FALSE(var.type_index() <
TypeIndex::kTVMFFIStaticObjectBegin)) {
+ return VarRemapKeyTypeError();
}
- try {
- ObjectRef var_ref = var.cast<ObjectRef>();
- Any owned_mapped_value(mapped_value);
- var_remap_.Set(var_ref, owned_mapped_value);
- return Expected<void>();
- } catch (const Error& err) {
- return Unexpected(err);
+ const Object* var_ptr =
+ details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const
Object>(var);
+ Any owned_mapped_value(mapped_value);
+ auto [it, inserted] = var_remap_.try_emplace(var_ptr,
std::move(owned_mapped_value));
+ if (inserted) {
+ details::ObjectUnsafe::IncRefObjectHandle(
+ reinterpret_cast<TVMFFIObjectHandle>(const_cast<Object*>(var_ptr)));
+ } else {
+ it->second = std::move(owned_mapped_value);
}
+ return Expected<void>();
}
private:
- /*! \brief Identity-substitution environment, shared across the whole
traversal. */
- Map<ObjectRef, Any> var_remap_;
+ template <typename Parent, WalkOrder order, typename... Callbacks>
+ friend class StructuralMapEngine;
+ template <typename Parent, WalkOrder order>
+ friend class StructuralMapDynEngine;
+ template <typename Parent, typename... Callbacks>
+ friend class StructuralMutateEngine;
+ template <typename Parent>
+ friend class details::StructuralMutateDynEngine;
+
+ // Raw-pointer key: IncRef once on first insert, DecRef all keys in the
destructor.
+ std::unordered_map<const Object*, Any> var_remap_;
};
/*!
* \brief Callback-dispatched structural mutator with a state-carrying Parent
layer.
*
* Each callback is an ordinary callable and the engine selects it on its
first argument's
- * type, so selection is a compile-time-known ``as<TSub>()`` on the input node.
+ * type, using a compile-time-known ``as<TSub>()`` on the input node in
pre-order and on the
+ * descended node in post-order.
*
* ``Parent`` derives from ``StructuralMapEngineBase``, publishes
``StateTupleType``, accepts
* and forwards the mutator vtable in its constructor, and provides a protected
- * ``StateTuple() const noexcept``. A Parent that overrides expected descent
must hand-write
- * the matching protected raw redirect so neither entry path silently bypasses
the layer. Each
- * callback receives every tuple entry positionally, followed optionally by
+ * ``StateTuple() const noexcept``. Engine-internal descent uses the public
Expected entry points,
+ * so a Parent may override those entries directly. Each callback receives
every tuple entry
+ * positionally, followed optionally by
* ``TVMFFIDefRegionKind``. Descent calls use ``this->``, matching
* ``StructuralWalkEngine``: lookup happens at instantiation in the Parent's
class scope and is
* not virtual dispatch. This deliberately leaves composed deeper-layer
declarations eligible;
@@ -1186,57 +1215,13 @@ class StructuralMapEngine : public Parent {
using FirstArg = std::tuple_element_t<0, typename FuncInfo::ArgType>;
using TSub = std::remove_cv_t<std::remove_reference_t<FirstArg>>;
- // Deliberately duplicated by StructuralMapDynEngine::TryLink below,
- // which differs only in how a link is found and called; keep the two in
step.
- //
- // The match test and the matched-node path live together rather than in
separate functions:
- // selecting the link already computes value.as<TSub>(), and a pre-order
walk invokes on that
- // very node, so the converted value is reused instead of converted twice.
- //
- // Selection is on the input node, before any descent. A post-order walk
must not select on
- // the descended node: the reflected fallback writes its own remap entry,
so testing after
- // descent lets that entry swallow the callback.
- std::optional<TSub> matched;
- if constexpr (!std::is_same_v<TSub, AnyView> && !std::is_same_v<TSub,
Any>) {
- matched = value.template as<TSub>();
- if (!matched.has_value()) return false;
- }
-
- // A statically non-remappable type whose subclasses cannot change kind
discards the remap
- // path at optimization time. Every other case uses runtime metadata:
nullable refs may match
- // None, non-final subclasses may redeclare the kind, and metadata may be
absent.
- const bool remappable = [&]() {
- if constexpr (std::is_base_of_v<ObjectRef, TSub>) {
- using TNode = typename TSub::ContainerType;
- if constexpr ((TNode::_type_final ||
TNode::_type_s_eq_hash_subclass_kind_fixed) &&
- TNode::_type_s_eq_hash_kind != kTVMFFISEqHashKindFreeVar
&&
- TNode::_type_s_eq_hash_kind !=
kTVMFFISEqHashKindDAGNode) {
- return false;
- }
- }
- if constexpr (std::is_pointer_v<TSub> &&
- std::is_base_of_v<Object,
std::remove_cv_t<std::remove_pointer_t<TSub>>>) {
- using TNode = std::remove_cv_t<std::remove_pointer_t<TSub>>;
- if constexpr ((TNode::_type_final ||
TNode::_type_s_eq_hash_subclass_kind_fixed) &&
- TNode::_type_s_eq_hash_kind != kTVMFFISEqHashKindFreeVar
&&
- TNode::_type_s_eq_hash_kind !=
kTVMFFISEqHashKindDAGNode) {
- return false;
- }
- }
- return 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 (ExpectedUnsafe::GetData(mapped).type_index() !=
TypeIndex::kTVMFFINone) {
- *out = std::move(mapped);
- return true;
- }
- }
-
using StateIndices =
std::make_index_sequence<std::tuple_size_v<StateTupleType>>;
if constexpr (order == WalkOrder::kPreOrder) {
+ std::optional<TSub> matched;
+ if constexpr (!std::is_same_v<TSub, AnyView> && !std::is_same_v<TSub,
Any>) {
+ matched = value.template as<TSub>();
+ if (!matched.has_value()) return false;
+ }
// Pre-order: the callback rewrites this node first, then descent runs
over whatever it
// produced, so a replacement subtree is itself mapped.
Expected<Any> callback_result = [&]() -> Expected<Any> {
@@ -1261,11 +1246,7 @@ class StructuralMapEngine : public Parent {
*out = [&]() -> Expected<Any> {
if constexpr (kMaybeInplace) {
// A pre-order result can be mutated in place if unchanged or
uniquely owned.
- const TVMFFIAny descent_data = descent_view.CopyToTVMFFIAny();
- const TVMFFIAny input_data = value.CopyToTVMFFIAny();
- 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) {
+ if (descent_view.same_as(value)) {
return this->DefaultMaybeInplaceMutateExpected(value);
}
const Object* mapped_obj = descent_view.as<Object>();
@@ -1280,61 +1261,34 @@ class StructuralMapEngine : public Parent {
if (ExpectedUnsafe::GetData(*out).type_index() ==
TypeIndex::kTVMFFIUnchanged) {
*out = std::move(mapped_value);
}
+ return true;
} else {
- // Post-order: children are mapped first and the callback sees the
rebuilt node, so it
- // observes its operands already substituted.
- Expected<Any> descended = kMaybeInplace ?
this->DefaultMaybeInplaceMutateExpected(value)
- :
this->DefaultMutateExpected(value);
- if (TVM_FFI_PREDICT_FALSE(descended.is_err())) {
- *out = std::move(descended);
- return true;
- }
+ // Post-order descent is performed once by the engine entry before the
callback probes.
// Descended unchanged uses the original view; otherwise the callback
sees the replacement.
- const Any& descended_value = ExpectedUnsafe::GetData(descended);
+ const Any& descended_value = ExpectedUnsafe::GetData(*out);
const AnyView mapped_view = descended_value.type_index() ==
TypeIndex::kTVMFFIUnchanged
? value
: AnyView(descended_value);
+ std::optional<TSub> matched;
+ if constexpr (!std::is_same_v<TSub, AnyView> && !std::is_same_v<TSub,
Any>) {
+ matched = mapped_view.template as<TSub>();
+ if (!matched.has_value()) return false;
+ }
*out = [&]() -> Expected<Any> {
if constexpr (std::is_same_v<TSub, AnyView>) {
return InvokeTypedCallbackLink(callback, mapped_view,
StateIndices{});
} else if constexpr (std::is_same_v<TSub, Any>) {
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_view.template as<TSub>();
- if (TVM_FFI_PREDICT_FALSE(!descended_sub.has_value())) {
- return this->SMutateDescentTypeError();
- }
- return InvokeTypedCallbackLink(callback, *std::move(descended_sub),
StateIndices{});
+ return InvokeTypedCallbackLink(callback, *std::move(matched),
StateIndices{});
}
}();
if (TVM_FFI_PREDICT_FALSE(out->is_err())) {
this->UpdateVisitErrorContext(*out, mapped_view);
return true;
}
- }
-
- // 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,
AnyView(ExpectedUnsafe::GetData(*out)));
- if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
- *out = Unexpected(std::move(set_result).error());
- }
- }
- return true;
}
/*!
@@ -1354,10 +1308,19 @@ class StructuralMapEngine : public Parent {
*/
TVM_FFI_INLINE TVMFFIAny MutateImplRaw(AnyView value) noexcept {
Expected<Any> out{Any()};
- if (TryLinks<false>(value, &out, std::index_sequence_for<Callbacks...>{}))
{
+ if constexpr (order == WalkOrder::kPostOrder) {
+ out = this->DefaultMutateExpected(value);
+ if (TVM_FFI_PREDICT_FALSE(out.is_err())) {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ }
+ TryLinks<false>(value, &out, std::index_sequence_for<Callbacks...>{});
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ } else {
+ if (TryLinks<false>(value, &out,
std::index_sequence_for<Callbacks...>{})) {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ }
+ return
ExpectedUnsafe::MoveToTVMFFIAny(this->DefaultMutateExpected(value));
}
- return this->DefaultMutateRaw(value);
}
/*!
@@ -1367,10 +1330,19 @@ class StructuralMapEngine : public Parent {
*/
TVM_FFI_INLINE TVMFFIAny MaybeInplaceMutateImplRaw(AnyView value) noexcept {
Expected<Any> out{Any()};
- if (TryLinks<true>(value, &out, std::index_sequence_for<Callbacks...>{})) {
+ if constexpr (order == WalkOrder::kPostOrder) {
+ out = this->DefaultMaybeInplaceMutateExpected(value);
+ if (TVM_FFI_PREDICT_FALSE(out.is_err())) {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ }
+ TryLinks<true>(value, &out, std::index_sequence_for<Callbacks...>{});
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ } else {
+ if (TryLinks<true>(value, &out,
std::index_sequence_for<Callbacks...>{})) {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ }
+ return
ExpectedUnsafe::MoveToTVMFFIAny(this->DefaultMaybeInplaceMutateExpected(value));
}
- return this->DefaultMaybeInplaceMutateRaw(value);
}
/*! \brief The callback links, tested in declaration order. */
@@ -1380,11 +1352,9 @@ class StructuralMapEngine : public Parent {
/*!
* \brief Structural mutator whose links are runtime ffi.Functions keyed by
type index.
*
- * This is the dynamic counterpart of \ref StructuralMapEngine. It remains a
distinct
- * straight-line implementation because a post-order dynamic link must
remember the runtime
- * type index selected before descent and recheck the rebuilt node against
that same index.
- * Moving it into this header makes the same Parent layering available to
downstream dynamic
- * mutators; only its runtime link table and invocation differ from the typed
engine.
+ * This is the dynamic counterpart of \ref StructuralMapEngine. Moving it into
this header makes
+ * the same Parent layering available to downstream dynamic mutators; only its
runtime link table
+ * and invocation differ from the typed engine.
* Its ``Parent`` follows the same descent-layer protocol, while runtime
``Function`` callbacks
* retain their existing ``(value)`` or ``(value, TVMFFIDefRegionKind)`` ABI.
*
@@ -1436,25 +1406,20 @@ class StructuralMapDynEngine : public Parent {
/*!
* \brief Find the first runtime link registered for \p type_index.
- * \param type_index The input node's runtime type index.
+ * \param type_index The candidate node's runtime type index.
* \param with_kind Set when the matched link also takes a def-region kind.
- * \param link_type_index Set to the registered type index the link matched
on, so post-order
- * traversal can recheck the descended node against the same target.
* \return The matched Function, or nullopt when no link applies.
*/
- Optional<Function> FindLink(int32_t type_index, bool* with_kind,
- int32_t* link_type_index) const noexcept {
+ Optional<Function> FindLink(int32_t type_index, bool* with_kind) const
noexcept {
for (const Tuple<int32_t, Function>& entry : callbacks_) {
if (details::RuntimeTypeIndexMatch(type_index, entry.get<0>())) {
*with_kind = false;
- *link_type_index = entry.get<0>();
return entry.get<1>();
}
}
for (const Tuple<int32_t, Function>& entry :
callbacks_with_def_region_kind_) {
if (details::RuntimeTypeIndexMatch(type_index, entry.get<0>())) {
*with_kind = true;
- *link_type_index = entry.get<0>();
return entry.get<1>();
}
}
@@ -1481,29 +1446,10 @@ class StructuralMapDynEngine : public Parent {
*/
template <bool kMaybeInplace>
TVM_FFI_INLINE bool TryLink(AnyView value, Expected<Any>* out) noexcept {
- // Step for step the same walk as StructuralMapEngine::TryLink, and
deliberately so: only
- // link detection and invocation differ. Everything below except finding
and calling the
- // link is shared semantics, so a change to either copy belongs in both.
- bool with_kind = false;
- int32_t link_type_index = TypeIndex::kTVMFFINone;
- // A local, so descending into a matching child cannot change what this
node invokes.
- Optional<Function> matched = FindLink(value.type_index(), &with_kind,
&link_type_index);
- if (!matched.has_value()) return false;
-
- // --- identity remap, entry half -----------------------------------------
- // 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 (ExpectedUnsafe::GetData(mapped).type_index() !=
TypeIndex::kTVMFFINone) {
- *out = std::move(mapped);
- return true;
- }
- }
-
- // --- callback and descent, in walk order --------------------------------
if constexpr (order == WalkOrder::kPreOrder) {
+ bool with_kind = false;
+ Optional<Function> matched = FindLink(value.type_index(), &with_kind);
+ if (!matched.has_value()) return false;
// Pre-order: the callback rewrites this node first, then descent runs
over what it made.
Expected<Any> callback_result =
InvokeLink(*matched, with_kind, value, this->def_region_kind());
@@ -1517,11 +1463,7 @@ class StructuralMapDynEngine : public Parent {
mapped_value.type_index() == TypeIndex::kTVMFFIUnchanged ? value :
AnyView(mapped_value);
*out = [&]() -> Expected<Any> {
if constexpr (kMaybeInplace) {
- const TVMFFIAny descent_data = descent_view.CopyToTVMFFIAny();
- const TVMFFIAny input_data = value.CopyToTVMFFIAny();
- 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) {
+ if (descent_view.same_as(value)) {
return this->DefaultMaybeInplaceMutateExpected(value);
}
const Object* mapped_obj = descent_view.as<Object>();
@@ -1536,27 +1478,17 @@ class StructuralMapDynEngine : public Parent {
if (ExpectedUnsafe::GetData(*out).type_index() ==
TypeIndex::kTVMFFIUnchanged) {
*out = std::move(mapped_value);
}
+ return true;
} else {
- // Post-order: children are mapped first, so the callback sees the
rebuilt node.
- Expected<Any> descended = kMaybeInplace ?
this->DefaultMaybeInplaceMutateExpected(value)
- :
this->DefaultMutateExpected(value);
- if (TVM_FFI_PREDICT_FALSE(descended.is_err())) {
- *out = std::move(descended);
- return true;
- }
+ // Post-order descent is performed once by the engine entry before link
selection.
// See the typed engine: a borrowed view of the original, never an
owning copy of it.
- const Any& descended_value = ExpectedUnsafe::GetData(descended);
+ const Any& descended_value = ExpectedUnsafe::GetData(*out);
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_view.type_index(),
link_type_index))) {
- *out = this->SMutateDescentTypeError();
- this->UpdateVisitErrorContext(*out, mapped_view);
- return true;
- }
+ bool with_kind = false;
+ Optional<Function> matched = FindLink(mapped_view.type_index(),
&with_kind);
+ if (!matched.has_value()) return false;
// 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_view,
this->def_region_kind());
@@ -1564,46 +1496,44 @@ class StructuralMapDynEngine : public Parent {
this->UpdateVisitErrorContext(*out, mapped_view);
return true;
}
- }
-
- // 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,
AnyView(ExpectedUnsafe::GetData(*out)));
- if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
- *out = Unexpected(std::move(set_result).error());
- }
- }
- return true;
}
/*! \brief Mutate a value, invoking the first matching runtime link. */
TVM_FFI_INLINE TVMFFIAny MutateImplRaw(AnyView value) noexcept {
Expected<Any> out{Any()};
- if (TryLink<false>(value, &out)) {
+ if constexpr (order == WalkOrder::kPostOrder) {
+ out = this->DefaultMutateExpected(value);
+ if (TVM_FFI_PREDICT_FALSE(out.is_err())) {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ }
+ TryLink<false>(value, &out);
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ } else {
+ if (TryLink<false>(value, &out)) {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ }
+ return
ExpectedUnsafe::MoveToTVMFFIAny(this->DefaultMutateExpected(value));
}
- return this->DefaultMutateRaw(value);
}
/*! \brief Optionally mutate a value in place through the first matching
runtime link. */
TVM_FFI_INLINE TVMFFIAny MaybeInplaceMutateImplRaw(AnyView value) noexcept {
Expected<Any> out{Any()};
- if (TryLink<true>(value, &out)) {
+ if constexpr (order == WalkOrder::kPostOrder) {
+ out = this->DefaultMaybeInplaceMutateExpected(value);
+ if (TVM_FFI_PREDICT_FALSE(out.is_err())) {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ }
+ TryLink<true>(value, &out);
return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ } else {
+ if (TryLink<true>(value, &out)) {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(out));
+ }
+ return
ExpectedUnsafe::MoveToTVMFFIAny(this->DefaultMaybeInplaceMutateExpected(value));
}
- return this->DefaultMaybeInplaceMutateRaw(value);
}
/*! \brief Runtime links invoked without def-region context. */
@@ -1667,7 +1597,7 @@ class StructuralMutateEngine : public Parent {
}
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
}
- return Parent::DefaultMutateRaw(value);
+ return
details::ExpectedUnsafe::MoveToTVMFFIAny(Parent::DefaultMutateExpected(value));
}
/*! \brief Maybe mutate one value in place, with callback-owned descent. */
@@ -1681,7 +1611,8 @@ class StructuralMutateEngine : public Parent {
}
return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
}
- return Parent::DefaultMaybeInplaceMutateRaw(value);
+ return details::ExpectedUnsafe::MoveToTVMFFIAny(
+ Parent::DefaultMaybeInplaceMutateExpected(value));
}
/*! \brief Try one typed callback and preserve Error as an expected result.
*/
@@ -1745,59 +1676,37 @@ class StructuralMutateEngine : public Parent {
};
/*!
- * \brief Map a structured value graph and invoke typed replacement callbacks.
+ * \brief Structural map: mutate(x) = post(D(pre(x))).
*
- * Each callback is selected by the type of its first argument. The argument
may be ``AnyView``,
- * ``Any``, an object reference type, an object pointer type, or another
FFI-convertible POD type. A
- * callback may optionally take a second ``TVMFFIDefRegionKind`` argument.
Callbacks are tested in
- * declaration order and only the first strict type match is invoked. An
``AnyView`` callback
- * argument is borrowed and must not be retained after the callback returns.
+ * D is descent with a var remap that keeps the result consistent when a var
+ * is rewritten as a cascade effect of its fields changing during descent.
+ * Callbacks never read or write D's remap cache, and fire once per occurrence
+ * in whichever position they sit.
*
- * Each callback should follow map semantics: it must not mutate the input in
place and should
- * 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.
+ * Var policy in default D: each var is descended at most once in def (its
+ * first occurrence in a pattern def, its only occurrence in a simple def),
+ * and then returns the new rewritten result if any in use. Definitions are
+ * assumed to precede uses; a var with no definition is treated as a use.
*
- * Objects marked ``kTVMFFISEqHashKindFreeVar`` or
``kTVMFFISEqHashKindDAGNode`` are
- * identity-substituted. A callback is invoked only for the first occurrence
of each identity; its
- * final result, including an unchanged result, is reused for every later
occurrence in the same
- * structural-map invocation.
+ * Canonical use cases include:
+ * - use pre for var replacement to another value or var
+ * - for a tree node, a post rewrite after its children are mapped
*
- * \sa WalkOrder, StructuralMutator
+ * \note For a DAG type node, post callback fires at every of its occurrences,
+ * so if the intent is to do graph rewrite, post callback needs to have
+ * its own node to value memo; the engine does not dedup callback
rewrites.
*
- * Example:
- *
- * \code{.cpp}
- * Expected<Any> result = StructuralMapExpected<WalkOrder::kPostOrder>(
- * root,
- * [](const IntImm& value) -> Expected<Any> {
- * if (value->value < 0) {
- * return Unexpected(Error("ValueError", "negative constant", ""));
- * }
- * return Any(IntImm(value->value + 1));
- * },
- * [](const Add& add, TVMFFIDefRegionKind kind) -> Expected<Any> {
- * // In post-order, add->lhs and add->rhs have already been mapped.
- * return Any(add);
- * });
- * \endcode
+ * A callback is selected by its first argument type, optionally followed by
+ * ``TVMFFIDefRegionKind``; the first match in declaration order runs. It
+ * returns a replacement, ``Unchanged``, or ``Expected<U>``, and must not
+ * mutate its input. A post-order callback is selected by the type of what
+ * descent produced.
*
- * \tparam order Whether callbacks run before or after recursively mapping
children.
+ * \tparam order Whether callbacks run before or after mapping children.
* \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 replacement,
``Unchanged``, or
- * ``Expected<Any>``.
+ * \param root The owning root value to map; pass ``std::move(root)`` to
permit reuse.
+ * \param callbacks Callbacks tested in declaration order.
* \return The mapped owning value, or an Error if mapping or a callback fails.
- *
- * \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
- * ``self->field = StructuralMap(std::move(self->field), callback)``.
*/
template <WalkOrder order, typename... Callbacks>
// The owning parameter makes caller ownership visible to the uniqueness check.
diff --git a/include/tvm/ffi/extra/structural_visit.h
b/include/tvm/ffi/extra/structural_visit.h
index 8d83b1d3..1aa95bb7 100644
--- a/include/tvm/ffi/extra/structural_visit.h
+++ b/include/tvm/ffi/extra/structural_visit.h
@@ -712,31 +712,23 @@ class StructuralWalkEngine : public Parent {
};
/*!
- * \brief Walk a structured value graph and invoke typed callbacks on selected
values.
+ * \brief Structural walk visits every occurrence as a tree.
*
- * The callbacks are invoked only for values matching the first argument type
of
- * one of the callbacks. The first callback argument may be ``AnyView``,
``Any``,
- * an object reference type, an object pointer type, or another FFI-convertible
- * POD type. It may optionally take ``TVMFFIDefRegionKind`` after the value.
- * Callbacks are tested in order, and the first match is used.
+ * The walk keeps no state. A var's type is walked under its region at every
+ * occurrence, a shared DAG node is visited once per parent, and callbacks
+ * fire once per occurrence. Pre may return Skip to prune a subtree or Stop to
+ * end the walk; post sees a node after its children. Dedup can be composed if
+ * descend once on pattern var is desirable, or in graph case: a pre callback
+ * with its own visited set returns Skip on a repeat.
*
- * Each callback should return ``Expected<WalkResult>``; see ``WalkResult``.
- * - ``WalkResult::Interrupt(...)`` halts traversal.
- * - ``WalkResult::Advance()`` continues traversal.
- * - ``WalkResult::Skip()`` skips children traversal.
- * - ``Error`` indicates traversal failure.
- *
- * \sa WalkOrder, WalkResult
+ * Callbacks are selected the same way as in StructuralMap and return
+ * ``Expected<WalkResult>``.
*
* \tparam order Whether to invoke the callback before or after visiting
children.
* \tparam Callbacks Callback types.
* \param root The root value to visit.
- * \param callbacks Callbacks invoked for matching nodes as ``(value)`` or
- * ``(value, TVMFFIDefRegionKind)``.
- * \return ``std::nullopt`` if traversal completed, or the interrupt returned
by
- * a callback.
- *
- * \note Return type of each callback should be ``Expected<WalkResult>``.
+ * \param callbacks Callbacks invoked for matching nodes.
+ * \return ``std::nullopt`` if traversal completed, or the interrupt a
callback returned.
*/
template <WalkOrder order, typename... Callbacks>
Expected<Optional<VisitInterrupt>> StructuralWalkExpected(AnyView root,
diff --git a/python/tvm_ffi/structural.py b/python/tvm_ffi/structural.py
index a45a1979..d1f369e8 100644
--- a/python/tvm_ffi/structural.py
+++ b/python/tvm_ffi/structural.py
@@ -568,6 +568,10 @@ def structural_walk(
) -> VisitInterrupt | None:
"""Walk a value structurally and invoke the first matching typed callback.
+ The walk keeps no engine state and visits every occurrence as a tree.
+ Deduplication can be composed with a pre-order callback whose own visited
+ set returns :attr:`WalkResult.SKIP` on repeats.
+
Parameters
----------
root
@@ -738,7 +742,10 @@ def structural_map(
"""Structurally map a value using typed replacement callbacks.
Each callback must follow map semantics: it returns the unchanged input or
- a replacement value and must not mutate its input in place.
+ a replacement value and must not mutate its input in place. The policy is
+ ``mutate(x) = post(D(pre(x)))``: callbacks run at every occurrence, while
+ only default descent ``D`` reads or writes the variable-remap cache. A
+ graph rewrite that must preserve sharing keeps its own callback memo.
Parameters
----------
diff --git a/tests/cpp/extra/test_structural_mutate.cc
b/tests/cpp/extra/test_structural_mutate.cc
index 5ad0c89d..78256cf5 100644
--- a/tests/cpp/extra/test_structural_mutate.cc
+++ b/tests/cpp/extra/test_structural_mutate.cc
@@ -259,9 +259,7 @@ Expected<Any> Increment(int64_t value) { return Any(value +
1); }
struct MutateCount {
int value = 0;
- int mutate_raw = 0;
int mutate_expected = 0;
- int maybe_inplace_raw = 0;
int maybe_inplace_expected = 0;
};
@@ -287,16 +285,6 @@ class StructuralMapWithMutateCount : public
StructuralMapEngineBase {
}
protected:
- TVMFFIAny DefaultMutateRaw(AnyView value) noexcept {
- ++count_.mutate_raw;
- return
details::ExpectedUnsafe::MoveToTVMFFIAny(DefaultMutateExpected(value));
- }
-
- TVMFFIAny DefaultMaybeInplaceMutateRaw(AnyView value) noexcept {
- ++count_.maybe_inplace_raw;
- return
details::ExpectedUnsafe::MoveToTVMFFIAny(DefaultMaybeInplaceMutateExpected(value));
- }
-
StateTupleType StateTuple() const noexcept { return StateTupleType(count_,
marker_); }
private:
@@ -344,9 +332,7 @@ TEST(StructuralMap,
ParentLayerOwnsBothDescentsAndProvidesState) {
AnyArray inplace_root{int64_t{1}};
ASSERT_FALSE(mutator->MaybeInplaceMutateExpected(inplace_root).is_err());
- EXPECT_GT(engine->count().mutate_raw, 0);
EXPECT_GT(engine->count().mutate_expected, 0);
- EXPECT_GT(engine->count().maybe_inplace_raw, 0);
EXPECT_GT(engine->count().maybe_inplace_expected, 0);
EXPECT_EQ(callback_counts.size(), 2U);
EXPECT_GT(callback_counts[0], 0);
@@ -356,8 +342,8 @@ TEST(StructuralMap,
ParentLayerOwnsBothDescentsAndProvidesState) {
AnyArray repeated{var, var};
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>()));
+ EXPECT_EQ(var_callback_count, 2);
+ EXPECT_FALSE(mapped[0].cast<TVar>().same_as(mapped[1].cast<TVar>()));
}
TEST(StructuralMutate, CallbackOwnsMutationAndErrorsStayExpected) {
@@ -417,12 +403,12 @@ TEST(StructuralMutate, CallbackControlsRecursion) {
TPair mapped =
StructuralMutate(
root,
- [](const TPair& pair, StructuralMutatorObj* mutator) ->
Expected<Any> {
+ [](const TPair& pair, StructuralMutatorObj* mutator) ->
Expected<UnchangedOr<ObjectRef>> {
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));
+ return UnchangedOr<ObjectRef>(TPair(std::move(lhs), pair->rhs));
},
[](const TInt& value, StructuralMutatorObj*) -> Expected<Any> {
return Any(TInt(value->value + 100));
@@ -509,33 +495,40 @@ TEST(StructuralMutate,
CallbackArityControlsInplaceMutation) {
}
TEST(StructuralMutate, MatchedVarOwnsRemapConsistency) {
- TVar var("n");
- AnyArray root{var, var};
- int callback_count = 0;
-
- AnyArray mapped =
- 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);
- EXPECT_TRUE(mapped[0].cast<TVar>().same_as(mapped[1].cast<TVar>()));
- EXPECT_EQ(mapped[0].cast<TVar>()->name, "n-mapped");
+ TVarWithDep var("n", TVar("type"));
+ TPair root(TDefHolder(TVarWithDep("pattern"), var), var);
+ int type_callback_count = 0;
+
+ TPair mapped = StructuralMap<WalkOrder::kPreOrder>(root, [&](const TVar&
value) -> Expected<Any> {
+ if (!value.defined()) return Unchanged();
+ ++type_callback_count;
+ return Any(TVar(value->name + "-mapped"));
+ }).cast<TPair>();
+ TDefHolder mapped_defs = mapped->lhs.as_or_throw<TDefHolder>();
+ TVarWithDep mapped_def = mapped_defs->def_non_recursive;
+ TVarWithDep mapped_use = mapped->rhs.as_or_throw<TVarWithDep>();
+
+ EXPECT_EQ(type_callback_count, 1);
+ EXPECT_FALSE(mapped_def.same_as(var));
+ EXPECT_TRUE(mapped_def.same_as(mapped_use));
+ ASSERT_TRUE(mapped_def->dep.has_value());
+ EXPECT_EQ(mapped_def->dep.value().as_or_throw<TVar>()->name, "type-mapped");
+
+ TVarWithDep unchanged("unchanged", TVar("unchanged-type"));
+ TPair repeated_definition(TDefHolder(TVarWithDep("first-pattern"),
unchanged),
+ TDefHolder(unchanged, TVarWithDep("last-simple")));
+ int unchanged_type_callback_count = 0;
+
+ StructuralMap<WalkOrder::kPostOrder>(repeated_definition,
+ [&](const TVar& value) -> Expected<Any>
{
+ if (!value.defined()) return
Unchanged();
+ ++unchanged_type_callback_count;
+ return Unchanged();
+ });
+
+ // The unchanged simple definition leaves no binding, so the later pattern
definition descends
+ // the same var once and records the unchanged marker.
+ EXPECT_EQ(unchanged_type_callback_count, 2);
}
template <WalkOrder order>
@@ -887,7 +880,7 @@ TEST(StructuralMap, ContainerCallbackErrorsStayExpected) {
}
template <WalkOrder order>
-void CheckRepeatedVarRemap() {
+void CheckRepeatedFreeVarCallbacks() {
TVar var("n");
StringMap use{{"use", var}};
AnyArray root{var, Any(std::move(use))};
@@ -901,15 +894,15 @@ void CheckRepeatedVarRemap() {
StringMap mapped_uses = mapped[1].cast<StringMap>();
TVar mapped_use = mapped_uses["use"].cast<TVar>();
- EXPECT_EQ(callback_count, 1);
- EXPECT_TRUE(mapped_var.same_as(mapped_use));
+ EXPECT_EQ(callback_count, 2);
+ EXPECT_FALSE(mapped_var.same_as(mapped_use));
EXPECT_EQ(mapped_var->name, "n-mapped");
EXPECT_EQ(var->name, "n");
}
-TEST(StructuralMap, ReusesFinalCallbackResultForRepeatedVar) {
- CheckRepeatedVarRemap<WalkOrder::kPreOrder>();
- CheckRepeatedVarRemap<WalkOrder::kPostOrder>();
+TEST(StructuralMap, InvokesCallbackForEveryFreeVarOccurrence) {
+ CheckRepeatedFreeVarCallbacks<WalkOrder::kPreOrder>();
+ CheckRepeatedFreeVarCallbacks<WalkOrder::kPostOrder>();
}
AnyArray MakeStringAndBytesLeaves() {
@@ -1007,9 +1000,7 @@ TEST(StructuralMapDyn,
PreOrderDescendsOriginalAfterUnchangedCallback) {
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.
+TEST(StructuralMapDyn, InvokesCallbackForEveryFreeVarOccurrence) {
TVar var("x");
AnyArray root{Any(var), Any(var)};
int64_t calls = 0;
@@ -1020,8 +1011,8 @@ TEST(StructuralMapDyn, ReusesRemapResultForRepeatedVar) {
Any mapped = CallDynStructuralMap(
root, {Tuple<int32_t, Function>(TVarObj::RuntimeTypeIndex(), remap)},
WalkOrder::kPostOrder);
auto arr = mapped.cast<AnyArray>();
- EXPECT_EQ(calls, 1);
- EXPECT_TRUE(arr[0].cast<TVar>().same_as(arr[1].cast<TVar>()));
+ EXPECT_EQ(calls, 2);
+ EXPECT_FALSE(arr[0].cast<TVar>().same_as(arr[1].cast<TVar>()));
}
template <WalkOrder order>
diff --git a/tests/python/test_structural.py b/tests/python/test_structural.py
index 2d01511d..8c5a7901 100644
--- a/tests/python/test_structural.py
+++ b/tests/python/test_structural.py
@@ -697,29 +697,52 @@ def test_structural_map_map_value_ownership() -> None:
def test_structural_map_reuses_var_and_dag_callback_results() -> None:
@py_class(structural_eq="var")
class PyMapVar(Object):
- value: int = field(structural_eq="ignore")
-
- @py_class(structural_eq="dag")
- class PyMapDAG(Object):
value: int
+ @py_class(structural_eq="tree")
+ class PyMapRoot(Object):
+ simple_def: PyMapVar = field(structural_eq="def-simple")
+ pattern_defs: tvm_ffi.Array[PyMapVar] =
field(structural_eq="def-pattern")
+ uses: tvm_ffi.Array[PyMapVar]
+
+ var = PyMapVar(1)
+ mapped = tvm_ffi.structural_map(
+ PyMapRoot(var, tvm_ffi.Array([]), tvm_ffi.Array([var])),
+ (int, lambda value: value + 1),
+ )
+ assert mapped.simple_def.value == 2
+ assert mapped.simple_def.same_as(mapped.uses[0])
+ assert not mapped.simple_def.same_as(var)
+
+ unchanged_type_callback_count = 0
+
+ def keep_type(value: int) -> int:
+ nonlocal unchanged_type_callback_count
+ unchanged_type_callback_count += 1
+ return value
+
+ tvm_ffi.structural_map(
+ PyMapRoot(var, tvm_ffi.Array([var, var]), tvm_ffi.Array([])),
+ (int, keep_type),
+ )
+ assert unchanged_type_callback_count == 2
+
for order in (tvm_ffi.WalkOrder.PREORDER, tvm_ffi.WalkOrder.POSTORDER):
- for node_type in (PyMapVar, PyMapDAG):
- node = node_type(1)
- root = tvm_ffi.Array([node, tvm_ffi.Map({"use": node})])
- callback_count = 0
-
- def replace(value: Any) -> Any:
- nonlocal callback_count
- callback_count += 1
- return node_type(value.value + 1)
-
- mapped = tvm_ffi.structural_map(root, (node_type, replace),
order=order)
-
- assert callback_count == 1
- assert mapped[0].same_as(mapped[1]["use"])
- assert not mapped[0].same_as(node)
- assert mapped[0].value == 2
+ node = PyMapVar(1)
+ root = tvm_ffi.Array([node, tvm_ffi.Map({"use": node})])
+ callback_count = 0
+
+ def replace(value: Any) -> Any:
+ nonlocal callback_count
+ callback_count += 1
+ return PyMapVar(value.value + 1)
+
+ mapped = tvm_ffi.structural_map(root, (PyMapVar, replace), order=order)
+
+ assert callback_count == 2
+ assert not mapped[0].same_as(mapped[1]["use"])
+ assert not mapped[0].same_as(node)
+ assert mapped[0].value == 2
def test_structural_map_handles_inline_and_heap_strings_and_bytes() -> None: