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 214ea3c4 [REFACTOR][EXTRA] Add StructuralVisit, a callback-driven
traversal primitive (#750)
214ea3c4 is described below
commit 214ea3c4e25c685ff7e05d4a7997ca9f5a4d89ac
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Sep 5 19:43:56 2026 -0400
[REFACTOR][EXTRA] Add StructuralVisit, a callback-driven traversal
primitive (#750)
`StructuralWalk` owns descent, so callbacks cannot choose individual
children or control traversal order. Context-carrying passes need a
lower-level primitive where the callback owns the subtree.
This adds a separate public `StructuralVisitEngine<Parent,
Callbacks...>` with declaration-ordered typed dispatch. A matched
callback receives the active `Parent::VisitorObjType*`, owns descent,
and returns the final result for that value; an unmatched value follows
registered or reflected default traversal from the parent layer.
High-level changes:
- add `StructuralVisitExpected` and `StructuralVisit` with exact
`(value, visitor)` callbacks;
- expose callback-owned visiting and default descent through the dynamic
and Python APIs;
- attach visit error context once at each walk/visit engine boundary,
including reflected fallback;
- support raw and typed `Expected` early-return contexts;
- document and test pruning, interruption, errors, and composed visitor
layers.
---
docs/concepts/structural_eq_hash.rst | 48 ++++-
docs/reference/python/index.rst | 1 +
include/tvm/ffi/expected.h | 29 +++
include/tvm/ffi/extra/structural_mutate.h | 47 ++---
include/tvm/ffi/extra/structural_visit.h | 297 ++++++++++++++++++++++--------
python/tvm_ffi/__init__.py | 2 +
python/tvm_ffi/_ffi_api.py | 4 +
python/tvm_ffi/structural.py | 62 +++++++
src/ffi/extra/structural_visit.cc | 33 ++++
tests/cpp/extra/test_structural_visit.cc | 84 +++++++++
tests/python/test_structural.py | 70 +++++++
11 files changed, 564 insertions(+), 113 deletions(-)
diff --git a/docs/concepts/structural_eq_hash.rst
b/docs/concepts/structural_eq_hash.rst
index 6b998918..b7182560 100644
--- a/docs/concepts/structural_eq_hash.rst
+++ b/docs/concepts/structural_eq_hash.rst
@@ -22,8 +22,9 @@ TVM FFI provides ``structural_equal`` and ``structural_hash``
for the
object graph. These compare objects by **content** — recursively walking
fields — rather than by pointer identity.
-The same reflection metadata also drives ``structural_walk`` for analyses and
-``structural_map`` for rewrites. Their low-level engines,
+The same reflection metadata also drives ``structural_walk`` for analyses,
+``structural_visit`` for callback-owned descent, and ``structural_map`` for
rewrites. Their
+low-level engines,
``StructuralVisitor`` and ``StructuralMutator``, let custom object types
participate in the same traversal protocol.
@@ -1016,6 +1017,9 @@ There are two layers of API:
* - :func:`~tvm_ffi.structural_walk`
- Inspect a value graph without replacing values
- Collect information, validate IR, or stop at a match
+ * - :func:`~tvm_ffi.structural_visit`
+ - Give each matching callback control over child traversal
+ - Visit selected children in a chosen order or definition scope
* - :func:`~tvm_ffi.structural_map`
- Recursively replace values and rebuild changed paths
- Rewriting and compiler optimization passes
@@ -1026,11 +1030,11 @@ There are two layers of API:
- Low-level recursive mutation engine
- Implementing custom mutation hooks and identity substitution
-``structural_walk`` and ``structural_map`` construct the corresponding
low-level
-object, install callback-aware dispatch, run it on the root, and return the
final
-result. Applications normally use these two functions directly. Custom object
-hooks receive the low-level visitor or mutator so that recursive calls remain
in
-the same traversal.
+``structural_walk``, ``structural_visit``, and ``structural_map`` construct the
+corresponding low-level object, install callback-aware dispatch, run it on the
root, and
+return the final result. Applications normally use these functions directly.
Custom object
+hooks receive the low-level visitor or mutator so that recursive calls remain
in the same
+traversal.
StructuralVisitor and StructuralMutator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1040,10 +1044,21 @@ definition-region kind, and any early-interruption
state. Its main operations
are:
- ``visitor.visit(value)`` visits a child with the same visitor.
+- ``visitor.default_visit(value)`` bypasses the active engine callback for that
+ value but still dispatches its registered ``__s_visit__`` hook.
- ``visitor.def_region_kind()`` reports the active definition-region kind.
- ``visitor.with_def_region_kind(kind, callback)`` temporarily changes that
kind
while ``callback`` performs recursive visits.
+.. warning::
+
+ A ``__s_visit__`` hook must not call ``default_visit`` on the same value
+ currently being visited. Doing so re-enters that hook without a recursion
+ guard, causing stack overflow and a process crash. Use ``default_visit`` on
+ a child whose default traversal is wanted. It is also safe for a
+ ``structural_visit`` engine callback to call ``default_visit`` on its
matched
+ value; that bypasses engine callback dispatch for the value.
+
The default visitor dispatches to a type's ``__s_visit__`` hook when present.
Otherwise POD values are leaves and object-backed values are visited through
their reflected structural fields. Array and List have built-in hooks that
@@ -1262,8 +1277,23 @@ type and accepts an optional second
``TVMFFIDefRegionKind`` argument. The
return FoldAdd(add);
});
+``StructuralVisitExpected`` is the callback-driven form. A matched callback
+receives the active visitor, owns descent into its value, and returns the final
+result for that subtree. Returning without calling the visitor prunes the
+subtree. An unmatched value uses default descent:
+
+.. code-block:: cpp
+
+ Expected<Optional<VisitInterrupt>> result = StructuralVisitExpected(
+ root,
+ [&](const Pair& pair, StructuralVisitorObj* visitor)
+ -> Expected<Optional<VisitInterrupt>> {
+ // The callback owns descent: visit lhs, never visit rhs.
+ return visitor->VisitExpected(pair->lhs);
+ });
+
Walk callbacks return ``Expected<WalkResult>``. Map callbacks return
``Expected<Any>`` and must obey the same non-in-place callback contract as the
Python API. For ``Map`` and ``Dict``, both APIs process values and skip keys.
-``StructuralWalk`` and ``StructuralMap`` are the corresponding throwing
-convenience forms.
+``StructuralWalk``, ``StructuralVisit`` and ``StructuralMap`` are the
+corresponding throwing convenience forms.
diff --git a/docs/reference/python/index.rst b/docs/reference/python/index.rst
index 80a66725..57a2be9f 100644
--- a/docs/reference/python/index.rst
+++ b/docs/reference/python/index.rst
@@ -92,6 +92,7 @@ Structural
structural_equal
structural_hash
structural_walk
+ structural_visit
structural_map
diff --git a/include/tvm/ffi/expected.h b/include/tvm/ffi/expected.h
index 6ebd9ab5..6bf6051f 100644
--- a/include/tvm/ffi/expected.h
+++ b/include/tvm/ffi/expected.h
@@ -431,6 +431,35 @@ struct ExpectedUnsafe {
}
};
+/*!
+ * \brief Return proxy used by early-return macros in raw or typed functions.
+ * \tparam T The success type, fixed when the proxy stores its ``Expected<T>``.
+ *
+ * A return statement selects either the raw ``TVMFFIAny`` conversion used by
+ * hooks or the same ``Expected<T>`` type used by typed helpers. Both
+ * conversions are rvalue-qualified because handing off the stored payload is
+ * a single move; an lvalue helper cannot accidentally transfer it twice. As
+ * with other moved-from values, deliberately converting ``std::move(helper)``
+ * twice remains caller error.
+ */
+template <typename T>
+class MaybeReturnHelper {
+ public:
+ TVM_FFI_INLINE explicit MaybeReturnHelper(Expected<T>&& value) noexcept
+ : value_(std::move(value)) {}
+
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ TVM_FFI_INLINE operator TVMFFIAny() && noexcept {
+ return ExpectedUnsafe::MoveToTVMFFIAny(std::move(value_));
+ }
+
+ // NOLINTNEXTLINE(google-explicit-constructor,runtime/explicit)
+ TVM_FFI_INLINE operator Expected<T>() && noexcept { return
std::move(value_); }
+
+ private:
+ Expected<T> value_;
+};
+
} // namespace details
// TypeTraits specialization for Expected<T>
diff --git a/include/tvm/ffi/extra/structural_mutate.h
b/include/tvm/ffi/extra/structural_mutate.h
index b3ab3b13..a1b9c8ea 100644
--- a/include/tvm/ffi/extra/structural_mutate.h
+++ b/include/tvm/ffi/extra/structural_mutate.h
@@ -141,6 +141,9 @@ struct StructuralMutatorVTable {
*/
class StructuralMutatorObj : public Object {
public:
+ /*! \brief Callback-facing mutator type used by composed callback-driven
engines. */
+ using MutatorObjType = StructuralMutatorObj;
+
/*!
* \brief Mutate a value through the mutator vtable.
*
@@ -598,16 +601,14 @@ TVM_FFI_INLINE static Expected<Any>
MutateReflectedFieldsExpected(StructuralMuta
namespace details {
/// \cond Doxygen_Suppress
-// Return from the current mutation function if Result is an Error.
-// Append Node to the mutate error context before returning. Node is required:
dropping it
-// silently degrades every error message produced below this frame.
-// A raw pointer Node must be non-null; pass nullable nodes as ObjectRef or
Any so None is skipped.
-#define TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result)
\
- do {
\
- auto&& tvm_ffi_res_ = (Result);
\
- if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.is_err())) {
\
- return
::tvm::ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(::std::move(tvm_ffi_res_));
\
- }
\
+// Return from the current raw or same-T Expected mutation function if Result
is an Error.
+// The rvalue-only proxy lets the enclosing return type select the
representation.
+#define TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result)
\
+ do {
\
+ auto&& tvm_ffi_res_ = (Result);
\
+ if (TVM_FFI_PREDICT_FALSE(tvm_ffi_res_.is_err())) {
\
+ return
::tvm::ffi::details::MaybeReturnHelper(::std::move(tvm_ffi_res_)); \
+ }
\
} while (0)
/// \endcond
@@ -615,9 +616,9 @@ namespace details {
// Out of line so its strings and Error construction stay out of the hot path
of whatever hook
// body TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN expands into. Same reason as
// BadStructuralMutateHookError.
-TVM_FFI_COLD_CODE inline TVMFFIAny SMutateDeclaredTypeErrorRaw() noexcept {
- return AnyUnsafe::MoveAnyToTVMFFIAny(
- Any(Error("TypeError", "structural mutate result does not match the
declared type", "")));
+TVM_FFI_COLD_CODE inline Expected<Any> SMutateDeclaredTypeError() noexcept {
+ return Unexpected(
+ Error("TypeError", "structural mutate result does not match the declared
type", ""));
}
/// \cond Doxygen_Suppress
@@ -626,7 +627,8 @@ TVM_FFI_COLD_CODE inline TVMFFIAny
SMutateDeclaredTypeErrorRaw() noexcept {
TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(Result);
\
if
(TVM_FFI_PREDICT_FALSE(!::tvm::ffi::details::AnyUnsafe::CheckAnyStrict<Type>( \
::tvm::ffi::details::ExpectedUnsafe::GetData(Result)))) {
\
- return ::tvm::ffi::details::SMutateDeclaredTypeErrorRaw();
\
+ return ::tvm::ffi::details::MaybeReturnHelper(
\
+ ::tvm::ffi::details::SMutateDeclaredTypeError());
\
}
\
Type Name = /* NOLINT(bugprone-macro-parentheses) */
\
::tvm::ffi::details::AnyUnsafe::MoveFromAnyAfterCheck<Type>(
\
@@ -637,16 +639,15 @@ TVM_FFI_COLD_CODE inline TVMFFIAny
SMutateDeclaredTypeErrorRaw() noexcept {
* \brief Unwrap a successful mutation result into a newly declared value or
return its error.
*
* ``Type`` must be concrete; use a type alias when it contains a top-level
comma. A type mismatch
- * returns ``Unexpected(TypeError)`` through the surrounding ``Expected``
function without
- * throwing, reported with a fixed string so a correct hook pays only one
predicted-not-taken
- * branch per field. This macro declares ``Name`` into the enclosing scope and
must be used in a
- * braced block, never as an unbraced control-flow body. A raw pointer node
must be non-null; pass
- * nullable nodes as ``ObjectRef`` or ``Any`` so ``None`` is skipped when
constructing error
- * context.
+ * returns ``TypeError`` through the surrounding raw or ``Expected`` function
without throwing,
+ * reported with a fixed string so a correct hook pays only one
predicted-not-taken branch per
+ * field. Its early returns work from either a raw ``TVMFFIAny`` hook or a
same-T ``Expected<T>``
+ * helper. This macro declares ``Name`` into the enclosing scope and must be
used in a braced
+ * block, never as an unbraced control-flow body.
*
* Example:
* \code{.cpp}
- * TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ObjectRef, child,
mutator->MutateExpected(self->child), self);
+ * TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ObjectRef, child,
mutator->MutateExpected(self->child));
* \endcode
*
* \param Type The concrete type of the successful value.
@@ -672,8 +673,8 @@ TVM_FFI_COLD_CODE inline TVMFFIAny
SMutateDeclaredTypeErrorRaw() noexcept {
/*!
* \brief \ref TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN without the type check.
*
- * Same signature and same error propagation; the difference is only what
happens to a
- * successful result that is not of type \p Type.
+ * Same signature, raw-or-same-T early-return support, and error propagation;
the difference is
+ * only what happens to a successful result that is not of type \p Type.
*
* The caller must guarantee the result has the declared type; a mismatch is
undefined behavior
* in a release build, and debug builds catch it with ``TVM_FFI_DCHECK``.
diff --git a/include/tvm/ffi/extra/structural_visit.h
b/include/tvm/ffi/extra/structural_visit.h
index e8d567ad..b8d41948 100644
--- a/include/tvm/ffi/extra/structural_visit.h
+++ b/include/tvm/ffi/extra/structural_visit.h
@@ -138,6 +138,8 @@ struct StructuralVisitorVTable {
*/
class StructuralVisitorObj : public Object {
public:
+ /*! \brief Callback-facing visitor type used by composed callback-driven
engines. */
+ using VisitorObjType = StructuralVisitorObj;
/*! \brief State references made available to callback-aware visitor layers.
*/
using StateTupleType = std::tuple<>;
@@ -200,6 +202,12 @@ class StructuralVisitorObj : public Object {
* \brief Visit using the structural visit behavior registered by
kStructuralVisit for each type,
* or reflected structural fields when no custom behavior is registered.
*
+ * \note Dispatches to the value type's registered ``__s_visit__`` hook,
falling back to
+ * reflected fields only when no hook is registered. Call it on a child, or
on a matched
+ * value from a ``StructuralVisit`` callback to bypass callback dispatch for
that value.
+ * Called on the value whose own hook is running, it re-enters that hook --
there is no way
+ * to request the reflected path from inside a hook.
+ *
* \param value The value to visit.
* \return Expected interrupt state. An error means traversal failed.
*/
@@ -208,27 +216,16 @@ class StructuralVisitorObj : public Object {
static reflection::TypeAttrColumn
column(reflection::type_attr::kStructuralVisit);
AnyView attr = column[type_index];
- // Hooks propagate errors untouched; this is the engine dispatching into
`value`, so the
- // node is named here and nowhere else -- exactly one frame per node.
// case 1: Type-specific override registered as an opaque ABI visit
function pointer.
if (attr.type_index() == TypeIndex::kTVMFFIOpaquePtr) {
auto* visit_fn = reinterpret_cast<FStructuralVisit>(attr.cast<void*>());
TVMFFIAny raw = (*visit_fn)(this, value);
- if (TVM_FFI_PREDICT_FALSE(raw.type_index == TypeIndex::kTVMFFIError)) {
- details::UpdateVisitErrorContext(raw, value);
- }
return
details::ExpectedUnsafe::MoveFromTVMFFIAny<Optional<VisitInterrupt>>(raw);
}
// case 2: Type-specific override registered as an ffi::Function.
if (attr.type_index() == TypeIndex::kTVMFFIFunction) {
- Expected<Optional<VisitInterrupt>> result =
- attr.cast<Function>().CallExpected<Optional<VisitInterrupt>>(this,
value);
- if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
- Error err = result.error();
- details::UpdateVisitErrorContext(err, value);
- }
- return result;
+ return
attr.cast<Function>().CallExpected<Optional<VisitInterrupt>>(this, value);
}
if (TVM_FFI_PREDICT_FALSE(attr.type_index() != TypeIndex::kTVMFFINone)) {
@@ -491,18 +488,15 @@ enum class WalkOrder : int32_t {
namespace details {
/// \cond Doxygen_Suppress
-// Return from the current ABI visit function if Result stops traversal.
-// Result must evaluate to Expected whose raw storage can be moved to
TVMFFIAny.
-// If Result is an Error, append Node to the visit error context before
returning. Node is
-// required: dropping it silently degrades every error message produced below
this frame.
-// A raw pointer Node must be non-null; pass nullable nodes as ObjectRef or
Any so None is skipped.
-#define TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Result)
\
- do {
\
- auto&& tvm_ffi_res_ = (Result);
\
- if (TVM_FFI_PREDICT_FALSE(
\
-
::tvm::ffi::details::StructuralVisitNeedEarlyReturn(tvm_ffi_res_))) {
\
- return
::tvm::ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(tvm_ffi_res_)); \
- }
\
+// Return from the current raw or same-T Expected visit function if Result
stops traversal.
+// The rvalue-only proxy lets the enclosing return type select the
representation.
+#define TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(Result)
\
+ do {
\
+ auto&& tvm_ffi_res_ = (Result);
\
+ if (TVM_FFI_PREDICT_FALSE(
\
+
::tvm::ffi::details::StructuralVisitNeedEarlyReturn(tvm_ffi_res_))) { \
+ return
::tvm::ffi::details::MaybeReturnHelper(::std::move(tvm_ffi_res_)); \
+ }
\
} while (0)
/// \endcond
@@ -525,43 +519,16 @@ namespace details {
* ``Expected<Optional<VisitInterrupt>>`` storage produced by
* ``details::ExpectedUnsafe::MoveToTVMFFIAny``; the engine propagates it
without
* a runtime type check. This deliberate pair keeps the raw ABI path available
- * without rematerializing a typed ``Expected``. Calls use ``this->``, so
lookup
- * happens at instantiation in the Parent's class scope; it is not virtual
- * dispatch.
- *
- * \code
- * class CountingLayer : public StructuralVisitorObj {
- * public:
- * using StateTupleType = std::tuple<const int&>;
- * explicit CountingLayer(const StructuralVisitorVTable* vtable)
- * : StructuralVisitorObj(vtable) {}
+ * without rematerializing a typed ``Expected``. Engine calls use ``Parent::``
+ * qualification; this is static layer dispatch, not virtual dispatch. A layer
+ * must still define its own raw boilerplate because boilerplate inherited from
+ * a base resolves its unqualified typed call in that base's scope.
*
- * Expected<Optional<VisitInterrupt>> DefaultVisitExpected(AnyView value)
noexcept {
- * ++count_;
- * return StructuralVisitorObj::DefaultVisitExpected(value);
- * }
- *
- * protected:
- * TVMFFIAny DefaultVisitRaw(AnyView value) noexcept {
- * return
details::ExpectedUnsafe::MoveToTVMFFIAny(DefaultVisitExpected(value));
- * }
- * StateTupleType StateTuple() const noexcept { return std::tie(count_); }
- *
- * private:
- * int count_ = 0;
- * };
- *
- * auto callback = [](const ObjectRef&, const int&) -> Expected<WalkResult> {
- * return WalkResult::Advance();
- * };
- * using Engine = StructuralWalkEngine<CountingLayer, WalkOrder::kPreOrder,
- * decltype(callback)>;
- * StructuralVisitor visitor(make_object<Engine>(std::move(callback)));
- * auto result = visitor->VisitExpected(root);
- * \endcode
- *
- * \tparam Parent Visitor layer that supplies descent and callback state
through
- * the complete protocol above.
+ * \tparam Parent Traversal layer extended by the engine. Each layer that
+ * customizes typed descent must define its own ``Default*Raw``
+ * boilerplate; inherited boilerplate resolves its unqualified
+ * typed call in the base layer's scope. Engine protocol and
+ * descent calls are ``Parent::``-qualified.
* \tparam order Callback placement relative to child traversal.
* \tparam Callbacks Callback links, tested in declaration order.
*/
@@ -612,14 +579,14 @@ class StructuralWalkEngine : public Parent {
"StructuralWalk callback takes (value, state...) with an optional
trailing "
"definition-region kind");
try {
- static_assert(std::is_same_v<decltype(this->StateTuple()),
StateTupleType>,
+ static_assert(std::is_same_v<decltype(Parent::StateTuple()),
StateTupleType>,
"Parent::StateTuple() must return Parent::StateTupleType
by value");
- StateTupleType states = this->StateTuple();
+ StateTupleType states = Parent::StateTuple();
if constexpr (FuncInfo::num_args == 1 + sizeof...(Is)) {
return callback(std::forward<Value>(value), std::get<Is>(states)...);
} else {
return callback(std::forward<Value>(value), std::get<Is>(states)...,
- this->def_region_kind());
+ Parent::def_region_kind());
}
} catch (const Error& err) {
return Unexpected(err);
@@ -689,9 +656,11 @@ class StructuralWalkEngine : public Parent {
}
{
- // DefaultVisitExpected already named `value` if a hook it dispatched
failed.
- TVMFFIAny result = this->DefaultVisitRaw(value);
+ TVMFFIAny result = Parent::DefaultVisitRaw(value);
if
(TVM_FFI_PREDICT_FALSE(details::StructuralVisitRawNeedEarlyReturn(result))) {
+ if (TVM_FFI_PREDICT_FALSE(result.type_index ==
TypeIndex::kTVMFFIError)) {
+ details::UpdateVisitErrorContext(result, value);
+ }
return result;
}
}
@@ -733,22 +702,6 @@ class StructuralWalkEngine : public Parent {
*
* \sa WalkOrder, WalkResult
*
- * Example:
- *
- * \code
- * int num_adds = 0;
- *
- * Expected<Optional<VisitInterrupt>> result =
StructuralWalkExpected<WalkOrder::kPreOrder>(
- * root,
- * [&](const Add& add) -> Expected<WalkResult> {
- * ++num_adds;
- * return WalkResult::Advance();
- * },
- * [&](const Mul& mul) -> Expected<WalkResult> {
- * return WalkResult::Skip();
- * });
- * \endcode
- *
* \tparam order Whether to invoke the callback before or after visiting
children.
* \tparam Callbacks Callback types.
* \param root The root value to visit.
@@ -789,6 +742,188 @@ Optional<VisitInterrupt> StructuralWalk(AnyView root,
Callbacks&&... callbacks)
return StructuralWalkExpected<order>(root,
std::forward<Callbacks>(callbacks)...).value();
}
+// ---------------------------------------------------------------------------
+// Structural Visit API.
+// ---------------------------------------------------------------------------
+
+/*!
+ * \brief Engine of the callback-dispatched \ref tvm::ffi::StructuralVisit.
+ *
+ * A matched callback owns descent into its value, and its result is final. A
+ * value matching no callback uses the Parent layer's default descent. The
local
+ * typed callback fold preserves declaration-order first match and converts an
+ * ``Error`` thrown by a matched callback into the visit result.
+ *
+ * \tparam Parent Traversal layer extended by the engine. Each layer that
+ * customizes typed descent must define its own ``Default*Raw``
+ * boilerplate; inherited boilerplate resolves its unqualified
+ * typed call in the base layer's scope. Engine protocol and
+ * descent calls are ``Parent::``-qualified.
+ * \tparam Callbacks Callable types whose first parameter selects the
dispatched value type.
+ */
+template <typename Parent, typename... Callbacks>
+class StructuralVisitEngine : public Parent {
+ public:
+ static_assert(std::is_base_of_v<StructuralVisitorObj, Parent>,
+ "StructuralVisit Parent must derive from
StructuralVisitorObj");
+ /*!
+ * \brief Construct a visit engine over a chain of typed callbacks.
+ * \param callbacks Callbacks tested in declaration order; the first match
runs.
+ */
+ explicit StructuralVisitEngine(Callbacks... callbacks)
+ : Parent(VTable()), callbacks_(std::move(callbacks)...) {}
+
+ private:
+ /*!
+ * \brief Return this engine's callback-aware visitor vtable.
+ * \return Pointer to the immutable visitor vtable for this specialization.
+ */
+ static const StructuralVisitorVTable* VTable() {
+ static const StructuralVisitorVTable vtable{
+ &StructuralVisitEngine::DispatchVisit,
+ };
+ return &vtable;
+ }
+
+ /*!
+ * \brief Dispatch from the erased visitor pointer to the concrete engine.
+ * \param self The erased structural visitor object.
+ * \param value The value to visit.
+ * \return Interrupt state, or an error if traversal failed.
+ */
+ static TVMFFIAny DispatchVisit(StructuralVisitorObj* self, AnyView value)
noexcept {
+ return static_cast<StructuralVisitEngine*>(self)->VisitImpl(value);
+ }
+
+ /*!
+ * \brief Visit one value, handing a matched callback ownership of its
descent.
+ * \param value The value to visit.
+ * \return Interrupt state, or an error if traversal failed.
+ */
+ TVMFFIAny VisitImpl(AnyView value) noexcept {
+ if (TVM_FFI_PREDICT_FALSE(value.type_index() == TypeIndex::kTVMFFINone)) {
+ return details::ExpectedUnsafe::MoveToTVMFFIAny(
+ Expected<Optional<VisitInterrupt>>(std::nullopt));
+ }
+ if (std::optional<Expected<Optional<VisitInterrupt>>> matched =
DispatchCallbacks(value)) {
+ // The matched callback already traversed as much of `value` as it
wanted, so its
+ // result is final and the engine does not descend on its own.
+ Expected<Optional<VisitInterrupt>> result = *std::move(matched);
+ if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
+ Error err = result.error();
+ details::UpdateVisitErrorContext(err, value);
+ }
+ return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+ }
+ // No callback claimed `value`. The Parent layer owns default descent.
+ TVMFFIAny result = Parent::DefaultVisitRaw(value);
+ if (TVM_FFI_PREDICT_FALSE(result.type_index == TypeIndex::kTVMFFIError)) {
+ details::UpdateVisitErrorContext(result, value);
+ }
+ return result;
+ }
+
+ /*! \brief Try one typed callback and preserve Error as an expected result.
*/
+ template <typename Callback>
+ TVM_FFI_INLINE std::optional<Expected<Optional<VisitInterrupt>>>
TryLink(Callback& callback,
+
AnyView value) noexcept {
+ using FuncInfo = details::FunctionInfo<std::decay_t<Callback>>;
+ static_assert(FuncInfo::num_args == 2, "StructuralVisit callback takes
(value, visitor)");
+ using FirstArg = std::tuple_element_t<0, typename FuncInfo::ArgType>;
+ using TSub = std::remove_cv_t<std::remove_reference_t<FirstArg>>;
+ 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::VisitorObjType>,
+ "second StructuralVisit callback argument must be "
+ "exactly Parent::VisitorObjType*");
+ auto* visitor = static_cast<typename Parent::VisitorObjType*>(this);
+ try {
+ if constexpr (std::is_same_v<TSub, AnyView>) {
+ return callback(value, visitor);
+ } else if constexpr (std::is_same_v<TSub, Any>) {
+ return callback(Any(value), visitor);
+ } else if (auto matched = value.template as<TSub>()) {
+ return callback(*std::move(matched), visitor);
+ }
+ } catch (const Error& err) {
+ return Unexpected(err);
+ }
+ return std::nullopt;
+ }
+
+ /*! \brief Fold this engine's callback tuple in declaration order. */
+ template <size_t... Is>
+ TVM_FFI_INLINE std::optional<Expected<Optional<VisitInterrupt>>> TryLinks(
+ AnyView value, std::index_sequence<Is...>) noexcept {
+ std::optional<Expected<Optional<VisitInterrupt>>> result;
+ (... || (result = TryLink(std::get<Is>(callbacks_), value)).has_value());
+ return result;
+ }
+
+ /*!
+ * \brief Run the callback chain on \p value.
+ * \param value The value to dispatch on.
+ * \return The matched callback's result, or an empty optional when none
matched.
+ *
+ * \note An unmatched value is reported as such rather than folded into a
"continue"
+ * result: the engine has to tell "the callback chose to stop here" apart
from "no
+ * callback claimed this value".
+ */
+ std::optional<Expected<Optional<VisitInterrupt>>> DispatchCallbacks(AnyView
value) noexcept {
+ return TryLinks(value, std::index_sequence_for<Callbacks...>{});
+ }
+
+ /*! \brief Typed callbacks tested in declaration order, first match wins. */
+ std::tuple<Callbacks...> callbacks_;
+};
+
+/*!
+ * \brief Visit a structured value, letting a matched callback own descent.
+ *
+ * Each callback takes ``(value, StructuralVisitorObj* visitor)`` and returns
+ * ``Expected<Optional<VisitInterrupt>>``. The first argument follows the same
+ * matching rules as ``StructuralWalk``; callbacks are tested in declaration
+ * order and the first match is used.
+ *
+ * A matched callback owns descent into its value, and its result is final.
+ * Returning ``std::nullopt`` completes that subtree, a ``VisitInterrupt``
halts
+ * the traversal, and an ``Error`` fails it. A value matching no callback uses
+ * the visitor's default descent.
+ *
+ * \sa StructuralWalkExpected, StructuralVisitorObj, VisitInterrupt
+ *
+ * \tparam Callbacks Callback types.
+ * \param root The root value to visit.
+ * \param callbacks Callbacks invoked for matching nodes.
+ * \return ``std::nullopt`` if traversal completed, or the interrupt that
halted it.
+ */
+template <typename... Callbacks>
+Expected<Optional<VisitInterrupt>> StructuralVisitExpected(AnyView root,
+ Callbacks&&...
callbacks) noexcept {
+ static_assert(sizeof...(Callbacks) != 0, "StructuralVisit requires at least
one callback");
+ using Engine = StructuralVisitEngine<StructuralVisitorObj,
std::decay_t<Callbacks>...>;
+ StructuralVisitor
visitor(make_object<Engine>(std::forward<Callbacks>(callbacks)...));
+ return visitor->VisitExpected(root);
+}
+
+/*!
+ * \brief Throwing error over \ref tvm::ffi::StructuralVisitExpected.
+ *
+ * See \ref tvm::ffi::StructuralVisitExpected for callback semantics and
traversal behavior.
+ *
+ * \tparam Callbacks Callback types.
+ * \param root The root value to visit.
+ * \param callbacks Callbacks invoked for matching nodes. Each callback takes
+ * ``(value, StructuralVisitorObj*)`` and should return
+ * ``Expected<Optional<VisitInterrupt>>``.
+ * \return ``std::nullopt`` if traversal completed, or the interrupt that
halted it.
+ * \throws Error if traversal or a callback returned an error.
+ */
+template <typename... Callbacks>
+Optional<VisitInterrupt> StructuralVisit(AnyView root, Callbacks&&...
callbacks) {
+ return StructuralVisitExpected(root,
std::forward<Callbacks>(callbacks)...).value();
+}
+
} // namespace ffi
} // namespace tvm
#endif // TVM_FFI_EXTRA_STRUCTURAL_VISIT_H_
diff --git a/python/tvm_ffi/__init__.py b/python/tvm_ffi/__init__.py
index 59d55d89..1f4e812d 100644
--- a/python/tvm_ffi/__init__.py
+++ b/python/tvm_ffi/__init__.py
@@ -87,6 +87,7 @@ if TYPE_CHECKING or not _is_config_mode():
structural_equal,
structural_hash,
structural_map,
+ structural_visit,
structural_walk,
)
from . import serialization
@@ -188,6 +189,7 @@ __all__ = [
"structural_equal",
"structural_hash",
"structural_map",
+ "structural_visit",
"structural_walk",
"system_lib",
"use_raw_stream",
diff --git a/python/tvm_ffi/_ffi_api.py b/python/tvm_ffi/_ffi_api.py
index c19828eb..09fa770d 100644
--- a/python/tvm_ffi/_ffi_api.py
+++ b/python/tvm_ffi/_ffi_api.py
@@ -118,7 +118,9 @@ if TYPE_CHECKING:
def StructuralMutatorVarRemapGet(_0: _StructuralMutator, _1: Any, /) ->
Any: ...
def StructuralMutatorVarRemapSet(_0: _StructuralMutator, _1: Any, _2: Any,
/) -> None: ...
def StructuralMutatorWithDefRegionKind(_0: _StructuralMutator, _1: int,
_2: Callable[..., Any], /) -> Any: ...
+ def StructuralVisit(_0: Any, _1: Sequence[tuple[int, Callable[..., Any]]],
/) -> _VisitInterrupt | None: ...
def StructuralVisitorDefRegionKind(_0: _StructuralVisitor, /) -> int: ...
+ def StructuralVisitorDefaultVisit(_0: _StructuralVisitor, _1: Any, /) ->
_VisitInterrupt | None: ...
def StructuralVisitorVisit(_0: _StructuralVisitor, _1: Any, /) ->
_VisitInterrupt | None: ...
def StructuralVisitorWithDefRegionKind(_0: _StructuralVisitor, _1: int,
_2: Callable[..., Any], /) -> Any: ...
def StructuralWalk(_0: Any, _1: Sequence[tuple[int, Callable[..., Any]]],
_2: Sequence[tuple[int, Callable[..., Any]]], _3: int, /) -> _VisitInterrupt |
None: ...
@@ -216,7 +218,9 @@ __all__ = [
"StructuralMutatorVarRemapGet",
"StructuralMutatorVarRemapSet",
"StructuralMutatorWithDefRegionKind",
+ "StructuralVisit",
"StructuralVisitorDefRegionKind",
+ "StructuralVisitorDefaultVisit",
"StructuralVisitorVisit",
"StructuralVisitorWithDefRegionKind",
"StructuralWalk",
diff --git a/python/tvm_ffi/structural.py b/python/tvm_ffi/structural.py
index 7a91ef6d..60d3b229 100644
--- a/python/tvm_ffi/structural.py
+++ b/python/tvm_ffi/structural.py
@@ -42,6 +42,7 @@ __all__ = [
"structural_equal",
"structural_hash",
"structural_map",
+ "structural_visit",
"structural_walk",
]
@@ -379,6 +380,33 @@ class StructuralVisitor(Object):
"""
return _ffi_api.StructuralVisitorVisit(self, value)
+ def default_visit(self, value: Any) -> VisitInterrupt | None:
+ """Visit ``value`` using its registered or reflected child traversal.
+
+ .. warning::
+ Never call ``default_visit`` on the value whose ``__s_visit__``
hook
+ is currently running. Doing so re-enters the same hook recursively
+ and can exhaust the C stack, crashing the process.
+
+ This bypasses the active engine callback dispatch for ``value`` itself,
+ but still invokes the value type's registered ``__s_visit__`` hook when
+ one exists. Use it for a child whose default traversal is wanted.
+ Recursive children still use this visitor.
+
+ Parameters
+ ----------
+ value
+ Value whose default children should be traversed.
+
+ Returns
+ -------
+ result
+ ``None`` if traversal should continue, otherwise a
+ :class:`VisitInterrupt` carrying the early-exit payload.
+
+ """
+ return _ffi_api.StructuralVisitorDefaultVisit(self, value)
+
def def_region_kind(self) -> DefRegionKind:
"""Low-level API to return the currently active structural def-region
kind.
@@ -619,6 +647,40 @@ def structural_walk(
return _ffi_api.StructuralWalk(root, entries,
entries_with_def_region_kind, order_int)
+def structural_visit(
+ root: Any,
+ callbacks: tuple | Sequence | Callable = (),
+) -> VisitInterrupt | None:
+ """Visit a value structurally with callbacks that own child traversal.
+
+ Each callback receives ``(value, visitor)``. It may call
+ :meth:`StructuralVisitor.visit` for selected children or
+ :meth:`StructuralVisitor.default_visit` for the value's registered/default
+ descent. Returning without either call prunes that value's subtree.
+
+ Parameters
+ ----------
+ root
+ Root value to traverse.
+
+ callbacks
+ A callback, ``(type, callback)`` entry, grouped type entry, or sequence
+ of entries. Entries are tried in order and the first match owns
descent.
+
+ Returns
+ -------
+ result
+ ``None`` if traversal completed, otherwise a :class:`VisitInterrupt`.
+
+ """
+ callback_entries = _normalize_callbacks(callbacks,
api_name="structural_visit")
+ entries: list[tuple[int, Callable[[Any, StructuralVisitor], Any]]] = [
+ (_callback_type_to_type_index(t, api_name="structural_visit"), fn)
+ for t, fn in callback_entries
+ ]
+ return _ffi_api.StructuralVisit(root, entries)
+
+
def structural_map(
root: Any,
callbacks: tuple | Sequence | Callable = (),
diff --git a/src/ffi/extra/structural_visit.cc
b/src/ffi/extra/structural_visit.cc
index 62b251dd..e1f73a5f 100644
--- a/src/ffi/extra/structural_visit.cc
+++ b/src/ffi/extra/structural_visit.cc
@@ -86,6 +86,30 @@ Expected<Optional<VisitInterrupt>> StructuralWalkExpected(
}
}
+/*!
+ * \brief Runtime callback-driven structural visit.
+ * \param root The root value to visit.
+ * \param callbacks Runtime callback entries of ``(type_index,
ffi::Function)`` invoked as
+ * ``callback(value, visitor)``.
+ * \return Expected interrupt state. An error means traversal failed.
+ */
+Expected<Optional<VisitInterrupt>> StructuralVisitExpected(
+ AnyView root, const Array<Tuple<int32_t, Function>>& callbacks) noexcept {
+ auto dispatch = [callbacks](AnyView value,
+ StructuralVisitorObj* visitor) ->
Expected<Optional<VisitInterrupt>> {
+ for (const auto& entry : callbacks) {
+ if (!RuntimeTypeIndexMatch(value.type_index(), entry.template get<0>()))
continue;
+ return entry.template get<1>().CallExpected<Optional<VisitInterrupt>>(
+ value, GetRef<StructuralVisitor>(visitor));
+ }
+ return visitor->DefaultVisitExpected(value);
+ };
+
+ using Visitor = StructuralVisitEngine<StructuralVisitorObj,
decltype(dispatch)>;
+ StructuralVisitor visitor(make_object<Visitor>(std::move(dispatch)));
+ return visitor->VisitExpected(root);
+}
+
/*! \brief Visit entries in a sequence container. */
TVMFFIAny VisitSeqContainer(StructuralVisitorObj* visitor, const SeqBaseObj*
self) noexcept {
for (const Any& item : *self) {
@@ -140,6 +164,10 @@ TVM_FFI_STATIC_INIT_BLOCK() {
refl::GlobalDef()
.def("ffi.VisitInterrupt", [](Any value) { return
VisitInterrupt(std::move(value)); })
.def_method("ffi.StructuralVisitorVisit", &StructuralVisitorObj::Visit)
+ .def_method("ffi.StructuralVisitorDefaultVisit",
+ [](const StructuralVisitor& visitor, AnyView value) {
+ return visitor->DefaultVisitExpected(value).value();
+ })
.def_method("ffi.StructuralVisitorDefRegionKind",
&StructuralVisitorObj::def_region_kind)
.def_method(
"ffi.StructuralVisitorWithDefRegionKind",
@@ -153,6 +181,11 @@ TVM_FFI_STATIC_INIT_BLOCK() {
return details::StructuralWalkExpected(root, callbacks,
callbacks_with_def_region_kind,
order)
.value();
+ })
+ .def("ffi.StructuralVisit",
+ [](AnyView root,
+ const Array<Tuple<int32_t, Function>>& callbacks) ->
Optional<VisitInterrupt> {
+ return details::StructuralVisitExpected(root, callbacks).value();
});
refl::EnsureTypeAttrColumn(refl::type_attr::kStructuralVisit);
refl::TypeAttrDef<ArrayObj>().attr(
diff --git a/tests/cpp/extra/test_structural_visit.cc
b/tests/cpp/extra/test_structural_visit.cc
index d579e6a6..82a6bccf 100644
--- a/tests/cpp/extra/test_structural_visit.cc
+++ b/tests/cpp/extra/test_structural_visit.cc
@@ -85,10 +85,13 @@ struct VisitTag {
template <typename Parent = StructuralVisitorObj>
class StructuralWalkWithVisitCount : public Parent {
public:
+ using VisitorObjType = StructuralWalkWithVisitCount;
using StateTupleType = std::tuple<const VisitCount&, const VisitTag&>;
explicit StructuralWalkWithVisitCount(const StructuralVisitorVTable* vtable)
: Parent(vtable) {}
+ int callback_tag() const noexcept { return visit_tag_.value; }
+
TVM_FFI_INLINE Expected<Optional<VisitInterrupt>>
DefaultVisitExpected(AnyView value) noexcept {
++visit_count_.value;
return Parent::DefaultVisitExpected(value);
@@ -108,6 +111,12 @@ class StructuralWalkWithVisitCount : public Parent {
VisitTag visit_tag_;
};
+template <typename Parent>
+class StructuralVisitOuterLayer : public Parent {
+ public:
+ explicit StructuralVisitOuterLayer(const StructuralVisitorVTable* vtable) :
Parent(vtable) {}
+};
+
StructuralVisitor MakeTestVisitor() { return
StructuralVisitor(make_object<TestVisitorObj>()); }
TestVisitorObj* AsTestVisitor(const StructuralVisitor& visitor) {
@@ -576,6 +585,7 @@ TEST(StructuralVisitor, WalkCatchesError) {
Expected<Optional<VisitInterrupt>> result =
StructuralWalkExpected<WalkOrder::kPreOrder>(
root, [&](const ObjectRef&) -> Expected<WalkResult> {
TVM_FFI_THROW(ValueError) << "walk callback threw";
+ return WalkResult::Advance();
});
ASSERT_TRUE(result.is_err());
@@ -666,4 +676,78 @@ TEST(StructuralVisitor, WalkAnyFallback) {
ExpectTrace(trace, {"object-ref", "object-ref"});
}
+// ---------------------------------------------------------------------------
+// StructuralVisit behavior.
+// ---------------------------------------------------------------------------
+
+TEST(StructuralVisit, CallbackDrivenTraversal) {
+ TVarWithDep lhs("lhs", TVarWithDep("pruned-dependency"));
+ TVarWithDep stop("stop");
+ TVarWithDep skipped("skipped");
+ std::vector<std::pair<std::string, TVMFFIDefRegionKind>> trace;
+ Expected<Optional<VisitInterrupt>> result = StructuralVisitExpected(
+ TPair(Array<ObjectRef>{lhs}, Array<ObjectRef>{stop, skipped}),
+ [](const TPairObj* pair,
+ StructuralVisitorObj* visitor) -> Expected<Optional<VisitInterrupt>> {
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->WithDefRegionKind(
+ kTVMFFIDefRegionKindRecursive, [&] { return
visitor->VisitExpected(pair->lhs); }));
+ TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->WithDefRegionKind(
+ kTVMFFIDefRegionKindNonRecursive, [&] { return
visitor->VisitExpected(pair->rhs); }));
+ return Optional<VisitInterrupt>(std::nullopt);
+ },
+ [&](const TVarWithDepObj* var,
+ StructuralVisitorObj* visitor) -> Expected<Optional<VisitInterrupt>>
{
+ trace.emplace_back(var->name, visitor->def_region_kind());
+ if (var->name == "stop") {
+ return Optional<VisitInterrupt>(VisitInterrupt(String("found
stop")));
+ }
+ return Optional<VisitInterrupt>(std::nullopt);
+ });
+ ASSERT_TRUE(result.is_ok());
+ ASSERT_TRUE(result.value().has_value());
+ EXPECT_EQ(result.value().value()->value.cast<String>(), "found stop");
+ ASSERT_EQ(trace.size(), 2u);
+ EXPECT_EQ(trace[0], std::make_pair(std::string("lhs"),
kTVMFFIDefRegionKindRecursive));
+ EXPECT_EQ(trace[1], std::make_pair(std::string("stop"),
kTVMFFIDefRegionKindNonRecursive));
+
+ using CallbackLayer = StructuralWalkWithVisitCount<>;
+ using ComposedLayer = StructuralVisitOuterLayer<CallbackLayer>;
+ bool layer_callback_ran = false;
+ bool base_callback_ran = false;
+ auto layer_callback = [&](const TVarWithDepObj*,
+ CallbackLayer* visitor) ->
Expected<Optional<VisitInterrupt>> {
+ EXPECT_EQ(visitor->callback_tag(), 7);
+ layer_callback_ran = true;
+ return Optional<VisitInterrupt>(std::nullopt);
+ };
+ auto base_callback = [&](const TVarObj*, CallbackLayer*) ->
Expected<Optional<VisitInterrupt>> {
+ base_callback_ran = true;
+ return Optional<VisitInterrupt>(std::nullopt);
+ };
+ using ComposedEngine =
+ StructuralVisitEngine<ComposedLayer, decltype(layer_callback),
decltype(base_callback)>;
+ static_assert(std::is_same_v<typename ComposedEngine::VisitorObjType,
CallbackLayer>);
+ StructuralVisitor composed(
+ make_object<ComposedEngine>(std::move(layer_callback),
std::move(base_callback)));
+ ASSERT_FALSE(composed->VisitExpected(Array<ObjectRef>{TVarWithDep("layer"),
TVar("base")})
+ .value()
+ .has_value());
+ EXPECT_TRUE(layer_callback_ran);
+ EXPECT_TRUE(base_callback_ran);
+
+ std::vector<std::string> error_trace;
+ result = StructuralVisitExpected(
+ Array<ObjectRef>{TVar("throw"), TVar("after")},
+ [&](const TVarObj* var, StructuralVisitorObj*) ->
Expected<Optional<VisitInterrupt>> {
+ error_trace.emplace_back(var->name);
+ if (var->name == "throw") {
+ TVM_FFI_THROW(ValueError) << "visit callback threw";
+ }
+ return Optional<VisitInterrupt>(std::nullopt);
+ });
+ ASSERT_TRUE(result.is_err());
+ EXPECT_EQ(result.error().message(), "visit callback threw");
+ ExpectTrace(error_trace, {"throw"});
+}
+
} // namespace
diff --git a/tests/python/test_structural.py b/tests/python/test_structural.py
index 51d4e6f7..9ad0f3ae 100644
--- a/tests/python/test_structural.py
+++ b/tests/python/test_structural.py
@@ -259,6 +259,76 @@ def test_structural_walk_interrupt() -> None:
assert tvm_ffi.structural_equal(result.value, {"found": 2})
+def test_structural_visit_default_visit_binding() -> None:
+ trace: list[int | str] = []
+ root = tvm_ffi.Array([tvm_ffi.Array([1, 2, 3])])
+
+ def visit_array(
+ value: tvm_ffi.Array, visitor: tvm_ffi.StructuralVisitor
+ ) -> tvm_ffi.VisitInterrupt | None:
+ trace.append("array")
+ return visitor.default_visit(value[0])
+
+ def interrupt_on_two(
+ value: int, visitor: tvm_ffi.StructuralVisitor
+ ) -> tvm_ffi.VisitInterrupt | None:
+ assert isinstance(visitor, tvm_ffi.StructuralVisitor)
+ trace.append(value)
+ return tvm_ffi.VisitInterrupt("done") if value == 2 else None
+
+ result = tvm_ffi.structural_visit(
+ root,
+ [
+ (tvm_ffi.Array, visit_array),
+ (int, interrupt_on_two),
+ ],
+ )
+
+ # The root callback asks for the nested Array's registered/default descent.
+ # Its matching Array callback is bypassed, while recursive ints re-enter
+ # this visitor and reach their callback.
+ assert trace == ["array", 1, 2]
+ assert isinstance(result, tvm_ffi.VisitInterrupt)
+ assert result.value == "done"
+
+ direct_trace: list[int] = []
+
+ def fail_directly(
+ value: int, visitor: tvm_ffi.StructuralVisitor
+ ) -> tvm_ffi.VisitInterrupt | None:
+ assert isinstance(visitor, tvm_ffi.StructuralVisitor)
+ direct_trace.append(value)
+ raise ValueError("direct structural visit failure")
+
+ with pytest.raises(ValueError, match="direct structural visit failure"):
+ tvm_ffi.structural_visit(tvm_ffi.Array([1, 2]), [(int, fail_directly)])
+ assert direct_trace == [1]
+
+ nested_trace: list[int | str] = []
+
+ def visit_outer_array(
+ value: tvm_ffi.Array, visitor: tvm_ffi.StructuralVisitor
+ ) -> tvm_ffi.VisitInterrupt | None:
+ nested_trace.append("array")
+ return visitor.default_visit(value[0])
+
+ def fail_nested(
+ value: int, visitor: tvm_ffi.StructuralVisitor
+ ) -> tvm_ffi.VisitInterrupt | None:
+ assert isinstance(visitor, tvm_ffi.StructuralVisitor)
+ nested_trace.append(value)
+ if value == 2:
+ raise ValueError("nested structural visit failure")
+ return None
+
+ with pytest.raises(ValueError, match="nested structural visit failure"):
+ tvm_ffi.structural_visit(
+ tvm_ffi.Array([tvm_ffi.Array([1, 2, 3]), tvm_ffi.Array([4])]),
+ [(tvm_ffi.Array, visit_outer_array), (int, fail_nested)],
+ )
+ assert nested_trace == ["array", 1, 2]
+
+
def test_structural_walk_nested_containers_and_skips_map_keys() -> None:
root = tvm_ffi.Array(
[