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 4205a643 [REFACTOR][EXTRA] Allow StructuralWalkVisitor to carry state 
via Parent (#747)
4205a643 is described below

commit 4205a643fa8d861d08f8aa7dc3dedbf1c7bda5f2
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Sep 5 13:51:17 2026 -0400

    [REFACTOR][EXTRA] Allow StructuralWalkVisitor to carry state via Parent 
(#747)
    
    Downstream walks sometimes need to carry state alongside traversal,
    scoped around the subtree they descend into. `StructuralWalkVisitor`
    could not express that because its callback chain was composed before
    the visitor existed, so callbacks could not see visitor-layer state.
    
    This change moves callback matching into a public walk engine and adds a
    `Parent` layer protocol:
    
    - Preserve the exact Parent-free `StructuralWalkExpected<order>` and
    `StructuralWalk<order>` entry points; no additional `WithState` free
    function ships.
    - Expose `tvm::ffi::StructuralWalkEngine<Parent, order, Callbacks...>`
    for layered users and document the complete Parent protocol, including
    dependent unqualified descent lookup rather than virtual dispatch.
    - Preserve protected raw ABI descent and public expected descent; layers
    overriding expected descent provide the paired raw redirect in their own
    class body.
    - Unpack the Parent's const state tuple positionally into every callback
    after the value, supporting multiple and duplicate state types without
    lookup rules.
    - Allow `TVMFFIDefRegionKind` only as an optional trailing argument and
    read it lazily only when requested.
    - Cover public engine construction, state observation, raw descent,
    skip/interrupt behavior, combined state/region dispatch, and
    pre/post-order placement in focused tests.
---
 include/tvm/ffi/extra/structural_visit.h | 292 ++++++++++++++++++-------------
 src/ffi/extra/structural_visit.cc        |   6 +-
 tests/cpp/extra/test_structural_visit.cc | 116 ++++++++++++
 3 files changed, 290 insertions(+), 124 deletions(-)

diff --git a/include/tvm/ffi/extra/structural_visit.h 
b/include/tvm/ffi/extra/structural_visit.h
index 468bfd4f..e8d567ad 100644
--- a/include/tvm/ffi/extra/structural_visit.h
+++ b/include/tvm/ffi/extra/structural_visit.h
@@ -138,6 +138,9 @@ struct StructuralVisitorVTable {
  */
 class StructuralVisitorObj : public Object {
  public:
+  /*! \brief State references made available to callback-aware visitor layers. 
*/
+  using StateTupleType = std::tuple<>;
+
   /*!
    * \brief Visit a value, dispatching through this visitor's vtable.
    *
@@ -246,6 +249,26 @@ class StructuralVisitorObj : public Object {
   /// \endcond
 
  protected:
+  /*!
+   * \brief Redirect raw ABI descent to \ref DefaultVisitExpected.
+   *
+   * A visitor layer that overrides either descent form must redeclare both in
+   * the same layer so dependent member lookup reaches the paired override. The
+   * raw form is deliberately retained for ABI traversal and permits a layer to
+   * forward raw storage without rematerializing a typed ``Expected``.
+   *
+   * \param value The value to descend into.
+   * \return Raw ``Expected<Optional<VisitInterrupt>>`` storage produced by
+   *         ``details::ExpectedUnsafe::MoveToTVMFFIAny``. The caller
+   *         reinterprets this storage without a runtime type check.
+   */
+  TVM_FFI_INLINE TVMFFIAny DefaultVisitRaw(AnyView value) noexcept {
+    return 
details::ExpectedUnsafe::MoveToTVMFFIAny(DefaultVisitExpected(value));
+  }
+
+  /*! \brief Return the state references maintained by this visitor layer. */
+  TVM_FFI_INLINE StateTupleType StateTuple() const noexcept { return {}; }
+
   /*!
    * \brief Construct a structural visitor from an immutable dispatch vtable.
    * \param vtable The non-null dispatch table for this visitor. It must 
outlive this object.
@@ -300,6 +323,17 @@ TVM_FFI_INLINE bool StructuralVisitNeedEarlyReturn(const 
Expected<T>& result) no
   return type_index == TypeIndex::kTVMFFIError || type_index == 
TypeIndex::kTVMFFIVisitInterrupt;
 }
 
+/*!
+ * \brief Return true when raw Expected storage carries a traversal-stopping 
state.
+ * \param result Raw ``Expected<Optional<VisitInterrupt>>`` storage returned by
+ *               a visitor descent hook. Its type contract is trusted without
+ *               a runtime check.
+ * \return Whether \p result carries anything other than successful completion.
+ */
+TVM_FFI_INLINE bool StructuralVisitRawNeedEarlyReturn(const TVMFFIAny& result) 
noexcept {
+  return result.type_index != TypeIndex::kTVMFFINone;
+}
+
 /*!
  * \brief Walk reflected structural fields of object-backed \p obj.
  *
@@ -472,23 +506,79 @@ namespace details {
   } while (0)
 /// \endcond
 
+}  // namespace details
+
 /*!
- * \brief Visitor used by callback-dispatched ``StructuralWalk``.
+ * \brief Callback-dispatched walk engine with a state-carrying Parent layer.
+ *
+ * Every callback receives all entries of ``Parent::StateTupleType``
+ * positionally after the value and may optionally take
+ * ``TVMFFIDefRegionKind`` as the final argument.
+ *
+ * A Parent layer derives from ``StructuralVisitorObj``, declares a public
+ * ``StateTupleType``, accepts and forwards ``const StructuralVisitorVTable*``
+ * in its constructor, and provides an at-least-protected
+ * ``StateTuple() const noexcept`` that returns ``StateTupleType`` by value. 
The
+ * state references in that tuple must outlive the traversal. A layer 
overriding
+ * either ``DefaultVisitExpected`` or ``DefaultVisitRaw`` declares both forms,
+ * at least protected, in that same class. The raw form must return raw
+ * ``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) {}
  *
+ *   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 order Callback placement relative to child traversal.
- * \tparam Dispatch Callable returning ``Expected<WalkResult>`` when invoked 
with ``AnyView`` and
- *                   the active def-region kind. User callbacks wrapped by 
this dispatcher may
- *                   accept either ``(value)`` or ``(value, def_region_kind)``.
+ * \tparam Callbacks Callback links, tested in declaration order.
  */
-template <WalkOrder order, typename Dispatch>
-class StructuralWalkVisitorObj : public StructuralVisitorObj {
+template <typename Parent, WalkOrder order, typename... Callbacks>
+class StructuralWalkEngine : public Parent {
  public:
+  static_assert(std::is_base_of_v<StructuralVisitorObj, Parent>,
+                "StructuralWalk Parent must derive from StructuralVisitorObj");
+  /*! \brief Tuple of const state references supplied by the Parent layer. */
+  using StateTupleType = typename Parent::StateTupleType;
+
   /*!
    * \brief Construct a structural walk visitor.
-   * \param dispatch The composed dispatcher invoked on each visited node.
+   * \param callbacks The typed callback links, tested in declaration order.
    */
-  explicit StructuralWalkVisitorObj(Dispatch dispatch)
-      : StructuralVisitorObj(VTable()), dispatch_(std::move(dispatch)) {}
+  explicit StructuralWalkEngine(Callbacks... callbacks)
+      : Parent(VTable()), callbacks_(std::move(callbacks)...) {}
 
  private:
   /*!
@@ -497,7 +587,7 @@ class StructuralWalkVisitorObj : public 
StructuralVisitorObj {
    */
   static const StructuralVisitorVTable* VTable() {
     static const StructuralVisitorVTable vtable{
-        &StructuralWalkVisitorObj::DispatchVisit,
+        &StructuralWalkEngine::DispatchVisit,
     };
     return &vtable;
   }
@@ -509,7 +599,60 @@ class StructuralWalkVisitorObj : public 
StructuralVisitorObj {
    * \return Interrupt state, or an error if traversal failed.
    */
   static TVMFFIAny DispatchVisit(StructuralVisitorObj* self, AnyView value) 
noexcept {
-    return static_cast<StructuralWalkVisitorObj*>(self)->VisitImpl(value);
+    return static_cast<StructuralWalkEngine*>(self)->VisitImpl(value);
+  }
+
+  /*! \brief Invoke one matched callback with its declared state, if any. */
+  template <typename Callback, typename Value, size_t... Is>
+  TVM_FFI_INLINE Expected<WalkResult> InvokeCallbackLink(Callback& callback, 
Value&& value,
+                                                         
std::index_sequence<Is...>) noexcept {
+    using FuncInfo = details::FunctionInfo<std::decay_t<Callback>>;
+    static_assert(
+        FuncInfo::num_args == 1 + sizeof...(Is) || FuncInfo::num_args == 2 + 
sizeof...(Is),
+        "StructuralWalk callback takes (value, state...) with an optional 
trailing "
+        "definition-region kind");
+    try {
+      static_assert(std::is_same_v<decltype(this->StateTuple()), 
StateTupleType>,
+                    "Parent::StateTuple() must return Parent::StateTupleType 
by value");
+      StateTupleType states = this->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());
+      }
+    } catch (const Error& err) {
+      return Unexpected(err);
+    }
+  }
+
+  /*! \brief Try one callback link, storing its result when the value type 
matches. */
+  template <typename Callback>
+  TVM_FFI_INLINE bool TryLink(Callback& callback, AnyView value,
+                              Expected<WalkResult>* out) noexcept {
+    using FuncInfo = details::FunctionInfo<std::decay_t<Callback>>;
+    static_assert(FuncInfo::num_args >= 1, "StructuralWalk callback requires a 
value argument");
+    using FirstArg = std::tuple_element_t<0, typename FuncInfo::ArgType>;
+    using TSub = std::remove_cv_t<std::remove_reference_t<FirstArg>>;
+    using StateIndices = 
std::make_index_sequence<std::tuple_size_v<StateTupleType>>;
+    if constexpr (std::is_same_v<TSub, AnyView>) {
+      *out = InvokeCallbackLink(callback, value, StateIndices{});
+      return true;
+    } else if constexpr (std::is_same_v<TSub, Any>) {
+      *out = InvokeCallbackLink(callback, Any(value), StateIndices{});
+      return true;
+    } else if (auto matched = value.template as<TSub>()) {
+      *out = InvokeCallbackLink(callback, *std::move(matched), StateIndices{});
+      return true;
+    }
+    return false;
+  }
+
+  /*! \brief Try callback links in declaration order until the first match. */
+  template <size_t... Is>
+  TVM_FFI_INLINE bool TryLinks(AnyView value, Expected<WalkResult>* out,
+                               std::index_sequence<Is...>) noexcept {
+    return (TryLink(std::get<Is>(callbacks_), value, out) || ...);
   }
 
   /*!
@@ -523,7 +666,8 @@ class StructuralWalkVisitorObj : public 
StructuralVisitorObj {
           Expected<Optional<VisitInterrupt>>(std::nullopt));
     }
     if constexpr (order == WalkOrder::kPreOrder) {
-      auto result = dispatch_(value, this->def_region_kind());
+      Expected<WalkResult> result = WalkResult::Advance();
+      TryLinks(value, &result, std::index_sequence_for<Callbacks...>{});
       if 
(TVM_FFI_PREDICT_FALSE(details::StructuralVisitNeedEarlyReturn(result))) {
         if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
           Error err = result.error();
@@ -546,14 +690,15 @@ class StructuralWalkVisitorObj : public 
StructuralVisitorObj {
 
     {
       // DefaultVisitExpected already named `value` if a hook it dispatched 
failed.
-      auto result = DefaultVisitExpected(value);
-      if 
(TVM_FFI_PREDICT_FALSE(details::StructuralVisitNeedEarlyReturn(result))) {
-        return details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(result));
+      TVMFFIAny result = this->DefaultVisitRaw(value);
+      if 
(TVM_FFI_PREDICT_FALSE(details::StructuralVisitRawNeedEarlyReturn(result))) {
+        return result;
       }
     }
 
     if constexpr (order == WalkOrder::kPostOrder) {
-      auto result = dispatch_(value, this->def_region_kind());
+      Expected<WalkResult> result = WalkResult::Advance();
+      TryLinks(value, &result, std::index_sequence_for<Callbacks...>{});
       if 
(TVM_FFI_PREDICT_FALSE(details::StructuralVisitNeedEarlyReturn(result))) {
         if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
           Error err = result.error();
@@ -567,110 +712,17 @@ class StructuralWalkVisitorObj : public 
StructuralVisitorObj {
         Expected<Optional<VisitInterrupt>>(std::nullopt));
   }
 
-  /*! \brief Composed dispatch closure invoked once per visited node. */
-  Dispatch dispatch_;
+  /*! \brief The callback links, tested in declaration order. */
+  std::tuple<Callbacks...> callbacks_;
 };
 
-/*!
- * \brief Compose typed callbacks into a single per-node dispatcher.
- *
- * Each callback dispatches on its first parameter's type; callbacks are tested
- * in declaration order and the first match runs. Callbacks may take an 
optional
- * second ``TVMFFIDefRegionKind`` argument. Nodes that match no callback fall
- * through and traversal continues normally.
- */
-struct StructuralWalkCallbackChain {
-  /*!
-   * \brief Build a dispatcher closure over a chain of typed callbacks.
-   * \tparam Callbacks Callable types whose first parameter selects the 
dispatched
-   *                   value type.
-   * \param callbacks Callbacks to be tested in order.
-   * \return A dispatcher closure of type ``Expected<WalkResult>(AnyView,
-   *         TVMFFIDefRegionKind)``. Each user callback may take either
-   *         ``(value)`` or ``(value, def_region_kind)``.
-   */
-  template <typename... Callbacks>
-  static auto FromChain(Callbacks... callbacks) {
-    return [=](AnyView x, TVMFFIDefRegionKind kind) mutable -> 
Expected<WalkResult> {
-      try {
-        Optional<Expected<WalkResult>> result;
-        // Fold expression: each TryCallLink returns empty Optional on no-match
-        // (falsy) or a result on match (truthy); || short-circuits on first 
match.
-        (... || (result = TryCallLink(callbacks, x, kind)));
-        if (result.has_value()) {
-          return std::move(result).value();
-        }
-        return WalkResult::Advance();
-      } catch (const Error& err) {
-        return Unexpected(err);
-      }
-    };
-  }
-
- private:
-  /*!
-   * \brief Invoke ``callback`` when ``x`` matches its first parameter type.
-   * \tparam Callback Callable whose first parameter selects the value type and
-   *                  whose optional second parameter receives the active 
def-region kind.
-   * \param callback The callback under test.
-   * \param x The value to dispatch on.
-   * \param kind The active def-region kind.
-   * \return The callback result if it matched, empty ``Optional`` otherwise.
-   */
-  template <typename Callback>
-  static Optional<Expected<WalkResult>> TryCallLink(Callback& callback, 
AnyView x,
-                                                    TVMFFIDefRegionKind kind) {
-    using FuncInfo = FunctionInfo<std::decay_t<Callback>>;
-    static_assert(FuncInfo::num_args == 1 || FuncInfo::num_args == 2,
-                  "StructuralWalk callbacks must take one argument (value) or 
two arguments "
-                  "(value, def-region kind)");
-    using FirstArg = std::tuple_element_t<0, typename FuncInfo::ArgType>;
-    using TSub = std::remove_cv_t<std::remove_reference_t<FirstArg>>;
-    if constexpr (std::is_same_v<TSub, AnyView>) {
-      // callback on AnyView
-      return InvokeCallback(callback, x, kind);
-    } else if constexpr (std::is_same_v<TSub, Any>) {
-      // callback on Any
-      return InvokeCallback(callback, Any(x), kind);
-    } else {
-      if (auto opt = x.template as<TSub>()) {
-        return InvokeCallback(callback, *std::move(opt), kind);
-      }
-    }
-    return std::nullopt;
-  }
-
-  /*!
-   * \brief Invoke a matched callback with optional def-region context.
-   * \tparam Callback Callable returning ``Expected<WalkResult>``.
-   * \tparam Value Type of the converted value passed to the callback.
-   * \param callback The matched callback to invoke.
-   * \param value The converted value.
-   * \param kind The active def-region kind.
-   * \return The callback result.
-   */
-  template <typename Callback, typename Value>
-  static Expected<WalkResult> InvokeCallback(Callback& callback, Value&& value,
-                                             TVMFFIDefRegionKind kind) {
-    using FuncInfo = FunctionInfo<std::decay_t<Callback>>;
-    if constexpr (FuncInfo::num_args == 1) {
-      return callback(std::forward<Value>(value));
-    } else {
-      return callback(std::forward<Value>(value), kind);
-    }
-  }
-};
-
-}  // namespace details
-
 /*!
  * \brief Walk a structured value graph and invoke typed callbacks on selected 
values.
  *
  * 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. A callback may also optionally take a second 
``TVMFFIDefRegionKind`` argument
- * to inspect whether the value is being visited in a definition region.
+ * POD type. It may optionally take ``TVMFFIDefRegionKind`` after the value.
  * Callbacks are tested in order, and the first match is used.
  *
  * Each callback should return ``Expected<WalkResult>``; see ``WalkResult``.
@@ -700,9 +752,8 @@ struct StructuralWalkCallbackChain {
  * \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. Each callback may 
take
- *                  either ``(value)`` or ``(value, def_region_kind)`` and 
should return
- *                  ``Expected<WalkResult>``.
+ * \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.
  *
@@ -712,10 +763,8 @@ template <WalkOrder order, typename... Callbacks>
 Expected<Optional<VisitInterrupt>> StructuralWalkExpected(AnyView root,
                                                           Callbacks&&... 
callbacks) noexcept {
   static_assert(sizeof...(Callbacks) != 0, "StructuralWalk requires at least 
one callback");
-  auto dispatch =
-      
details::StructuralWalkCallbackChain::FromChain(std::forward<Callbacks>(callbacks)...);
-  using Visitor = details::StructuralWalkVisitorObj<order, decltype(dispatch)>;
-  StructuralVisitor visitor(make_object<Visitor>(std::move(dispatch)));
+  using Visitor = StructuralWalkEngine<StructuralVisitorObj, order, 
std::decay_t<Callbacks>...>;
+  StructuralVisitor 
visitor(make_object<Visitor>(std::forward<Callbacks>(callbacks)...));
   return visitor->VisitExpected(root);
 }
 
@@ -727,9 +776,8 @@ Expected<Optional<VisitInterrupt>> 
StructuralWalkExpected(AnyView root,
  * \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. Each callback may 
take
- *                  either ``(value)`` or ``(value, def_region_kind)`` and 
should return
- *                  ``Expected<WalkResult>``.
+ * \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.
  * \throws Error if traversal or a callback returned an error.
diff --git a/src/ffi/extra/structural_visit.cc 
b/src/ffi/extra/structural_visit.cc
index a0ca4d9d..62b251dd 100644
--- a/src/ffi/extra/structural_visit.cc
+++ b/src/ffi/extra/structural_visit.cc
@@ -74,11 +74,13 @@ Expected<Optional<VisitInterrupt>> StructuralWalkExpected(
   };
 
   if (order == static_cast<int>(WalkOrder::kPreOrder)) {
-    using Visitor = StructuralWalkVisitorObj<WalkOrder::kPreOrder, 
decltype(dispatch)>;
+    using Visitor =
+        StructuralWalkEngine<StructuralVisitorObj, WalkOrder::kPreOrder, 
decltype(dispatch)>;
     StructuralVisitor visitor(make_object<Visitor>(std::move(dispatch)));
     return visitor->VisitExpected(root);
   } else {
-    using Visitor = StructuralWalkVisitorObj<WalkOrder::kPostOrder, 
decltype(dispatch)>;
+    using Visitor =
+        StructuralWalkEngine<StructuralVisitorObj, WalkOrder::kPostOrder, 
decltype(dispatch)>;
     StructuralVisitor visitor(make_object<Visitor>(std::move(dispatch)));
     return visitor->VisitExpected(root);
   }
diff --git a/tests/cpp/extra/test_structural_visit.cc 
b/tests/cpp/extra/test_structural_visit.cc
index eeb71fc6..d579e6a6 100644
--- a/tests/cpp/extra/test_structural_visit.cc
+++ b/tests/cpp/extra/test_structural_visit.cc
@@ -27,6 +27,7 @@
 
 #include <stdexcept>
 #include <string>
+#include <tuple>
 #include <utility>
 #include <vector>
 
@@ -73,6 +74,40 @@ class TestVisitorObj : public StructuralVisitorObj {
   }
 };
 
+struct VisitCount {
+  int value = 0;
+};
+
+struct VisitTag {
+  int value = 7;
+};
+
+template <typename Parent = StructuralVisitorObj>
+class StructuralWalkWithVisitCount : public Parent {
+ public:
+  using StateTupleType = std::tuple<const VisitCount&, const VisitTag&>;
+
+  explicit StructuralWalkWithVisitCount(const StructuralVisitorVTable* vtable) 
: Parent(vtable) {}
+
+  TVM_FFI_INLINE Expected<Optional<VisitInterrupt>> 
DefaultVisitExpected(AnyView value) noexcept {
+    ++visit_count_.value;
+    return Parent::DefaultVisitExpected(value);
+  }
+
+ protected:
+  TVM_FFI_INLINE TVMFFIAny DefaultVisitRaw(AnyView value) noexcept {
+    return 
details::ExpectedUnsafe::MoveToTVMFFIAny(DefaultVisitExpected(value));
+  }
+
+  TVM_FFI_INLINE StateTupleType StateTuple() const noexcept {
+    return std::tie(visit_count_, visit_tag_);
+  }
+
+ private:
+  VisitCount visit_count_;
+  VisitTag visit_tag_;
+};
+
 StructuralVisitor MakeTestVisitor() { return 
StructuralVisitor(make_object<TestVisitorObj>()); }
 
 TestVisitorObj* AsTestVisitor(const StructuralVisitor& visitor) {
@@ -411,6 +446,87 @@ TEST(StructuralVisitor, WalkReceivesDefRegionKind) {
   ExpectTrace(use_vars, {"x", "y"});
 }
 
+TEST(StructuralVisitor, WalkParentStatePreservesDescentSkipAndInterrupt) {
+  ObjectRef lhs = TVar("lhs");
+  ObjectRef rhs = TVar("rhs");
+  ObjectRef root = TPair(lhs, rhs);
+  std::vector<int> counts;
+  std::vector<int> tags;
+  std::vector<TVMFFIDefRegionKind> kinds;
+
+  auto pair_callback = [&](const TPairObj*, const VisitCount& count, const 
VisitTag& tag,
+                           TVMFFIDefRegionKind kind) -> Expected<WalkResult> {
+    counts.push_back(count.value);
+    tags.push_back(tag.value);
+    kinds.push_back(kind);
+    return WalkResult::Advance();
+  };
+  auto fallback_callback = [&](const ObjectRef&, const VisitCount& count, 
const VisitTag& tag,
+                               TVMFFIDefRegionKind kind) -> 
Expected<WalkResult> {
+    counts.push_back(count.value);
+    tags.push_back(tag.value);
+    kinds.push_back(kind);
+    return WalkResult::Advance();
+  };
+  using PreOrderEngine = StructuralWalkEngine<StructuralWalkWithVisitCount<>, 
WalkOrder::kPreOrder,
+                                              decltype(pair_callback), 
decltype(fallback_callback)>;
+  StructuralVisitor visitor(
+      make_object<PreOrderEngine>(std::move(pair_callback), 
std::move(fallback_callback)));
+  Optional<VisitInterrupt> result = visitor->VisitExpected(root).value();
+
+  EXPECT_FALSE(result.has_value());
+  EXPECT_EQ(counts, (std::vector<int>{0, 1, 2}));
+  EXPECT_EQ(tags, (std::vector<int>{7, 7, 7}));
+  EXPECT_EQ(kinds,
+            (std::vector<TVMFFIDefRegionKind>{kTVMFFIDefRegionKindNone, 
kTVMFFIDefRegionKindNone,
+                                              kTVMFFIDefRegionKindNone}));
+  counts.clear();
+
+  auto interrupt_callback = [&](const ObjectRef& node, const VisitCount& count,
+                                const VisitTag&) -> Expected<WalkResult> {
+    counts.push_back(count.value);
+    if (node.same_as(lhs)) return WalkResult::Skip();
+    if (node.same_as(rhs)) return 
WalkResult::Interrupt(VisitInterrupt(String("done")));
+    return WalkResult::Advance();
+  };
+  using InterruptEngine = StructuralWalkEngine<StructuralWalkWithVisitCount<>, 
WalkOrder::kPreOrder,
+                                               decltype(interrupt_callback)>;
+  StructuralVisitor 
interrupt_visitor(make_object<InterruptEngine>(std::move(interrupt_callback)));
+  Optional<VisitInterrupt> interrupt_result = 
interrupt_visitor->VisitExpected(root).value();
+
+  ASSERT_TRUE(interrupt_result.has_value());
+  EXPECT_EQ(interrupt_result.value()->value.cast<String>(), "done");
+  EXPECT_EQ(counts, (std::vector<int>{0, 1, 1}));
+
+  counts.clear();
+  auto postorder_callback = [&](const ObjectRef&, const VisitCount& count,
+                                const VisitTag&) -> Expected<WalkResult> {
+    counts.push_back(count.value);
+    return WalkResult::Advance();
+  };
+  using PostOrderEngine = StructuralWalkEngine<StructuralWalkWithVisitCount<>,
+                                               WalkOrder::kPostOrder, 
decltype(postorder_callback)>;
+  StructuralVisitor 
postorder_visitor(make_object<PostOrderEngine>(std::move(postorder_callback)));
+  Optional<VisitInterrupt> postorder_result = 
postorder_visitor->VisitExpected(root).value();
+  EXPECT_FALSE(postorder_result.has_value());
+  EXPECT_EQ(counts, (std::vector<int>{2, 3, 3}));
+
+  auto error_callback = [&](const ObjectRef& node, const VisitCount&,
+                            const VisitTag&) -> Expected<WalkResult> {
+    if (node.same_as(lhs)) {
+      return Unexpected(Error("ValueError", "stateful walk failed", ""));
+    }
+    return WalkResult::Advance();
+  };
+  using ErrorEngine = StructuralWalkEngine<StructuralWalkWithVisitCount<>, 
WalkOrder::kPreOrder,
+                                           decltype(error_callback)>;
+  StructuralVisitor 
error_visitor(make_object<ErrorEngine>(std::move(error_callback)));
+  Expected<Optional<VisitInterrupt>> error_result = 
error_visitor->VisitExpected(root);
+  ASSERT_TRUE(error_result.is_err());
+  EXPECT_EQ(error_result.error().kind(), "ValueError");
+  EXPECT_EQ(error_result.error().message(), "stateful walk failed");
+}
+
 TEST(StructuralVisitor, WalkReturnsError) {
   ObjectRef lhs = TVar("lhs");
   ObjectRef rhs = TVar("rhs");

Reply via email to