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.git


The following commit(s) were added to refs/heads/main by this push:
     new 2ef0ae2609 [REFACTOR][TIR] Use StructuralWalk in place of 
PostOrderVisit (#20308)
2ef0ae2609 is described below

commit 2ef0ae260903a82026abf1fc0d646e99ea72a0db
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Sep 10 12:57:36 2026 -0400

    [REFACTOR][TIR] Use StructuralWalk in place of PostOrderVisit (#20308)
    
    This PR replaces TIR PostOrderVisit and PreOrderVisit usage with
    StructuralWalk.
    
    A tree walk now uses a typed callback, and pre-order boolean descent
    becomes WalkResult::Advance or WalkResult::Skip. Sites retain identity
    sets only where a consumer depends on once-per-object behavior; buffer
    printer use counting instead uses def_region_kind to distinguish
    definitions from uses.
    
    tirx::PostOrderVisit, tirx::PreOrderVisit, their helper visitors, FFI
    globals, and Python wrappers are removed. Python callers use
    tvm_ffi.structural_walk.
---
 include/tvm/tirx/stmt_functor.h                    |  19 ---
 python/tvm/relax/utils.py                          |   2 +-
 .../tvm/s_tir/dlight/analysis/common_analysis.py   |   4 +-
 python/tvm/s_tir/dlight/gpu/fallback.py            |   4 +-
 python/tvm/s_tir/dlight/gpu/general_reduction.py   |   6 +-
 python/tvm/tirx/stmt_functor.py                    |  30 ----
 src/backend/trn/codegen/codegen_trn.cc             |  49 +++---
 src/relax/analysis/tir_op_pattern_kind.cc          |  16 +-
 src/relax/script/printer/dependent_type.cc         |  11 +-
 src/relax/transform/rewrite_cuda_graph.cc          |  26 +--
 .../analysis/sblock_buffer_access_lca_detector.cc  |  28 +--
 .../feature_extractor/per_store_feature.cc         |  67 ++++----
 .../postproc/rewrite_cooperative_fetch.cc          |  39 +++--
 .../meta_schedule/postproc/rewrite_tensorize.cc    |  49 +++---
 src/s_tir/schedule/analysis/analysis.cc            | 187 ++++++++++-----------
 src/s_tir/schedule/analysis/reducer.cc             |  46 +++--
 src/s_tir/schedule/primitive/cache_index.cc        |  56 +++---
 src/s_tir/schedule/primitive/cache_read_write.cc   |  12 +-
 src/s_tir/schedule/primitive/for_kind.cc           |  21 ++-
 .../schedule/primitive/layout_transformation.cc    |  14 +-
 src/s_tir/schedule/primitive/pad_einsum.cc         |  12 +-
 src/s_tir/schedule/primitive/read_write_at.cc      |  13 +-
 src/s_tir/schedule/trace.cc                        |  11 +-
 .../transform/lower_cross_thread_reduction.cc      |  87 +++++-----
 .../transform/memhammer_intermediate_stage.cc      |  34 ++--
 .../transform/memhammer_tensorcore_rewrite.cc      |  25 +--
 src/s_tir/transform/renew_defs.cc                  |  15 +-
 src/te/operation/compute_op.cc                     |  21 ++-
 src/te/tensor.cc                                   |  28 ++-
 src/tirx/ir/index_map.cc                           |  35 ++--
 src/tirx/ir/stmt_functor.cc                        | 104 +-----------
 src/tirx/ir/tir_visitor_with_path.h                |  13 +-
 src/tirx/script/printer/buffer.cc                  |  48 +++---
 src/tirx/script/printer/function.cc                |  27 ++-
 src/tirx/script/printer/utils.h                    |  28 +--
 src/tirx/transform/ir_utils.cc                     |  18 +-
 tests/cpp/ir_functor_test.cc                       |  92 +++++-----
 .../relax/test_backend_dispatch_sort_scan.py       |   3 +-
 tests/python/relax/test_pipeline.py                |   2 +-
 .../s_tir/base/test_sblock_dependence_info.py      |   4 +-
 .../s_tir/schedule/test_tir_schedule_analysis.py   |   6 +-
 .../schedule/test_tir_schedule_block_scope.py      |   2 +-
 .../test_tir_schedule_state_cached_flags.py        |   2 +-
 .../schedule/test_tir_schedule_transform_layout.py |  25 +++
 .../transform/test_s_tir_transform_hoist_if.py     |   4 +-
 .../test_s_tir_transform_inject_double_buffer.py   |   8 +-
 .../test_s_tir_transform_inject_ptx_async_copy.py  |   2 +-
 .../test_s_tir_transform_inject_ptx_ldg32.py       |   6 +-
 .../test_s_tir_transform_inject_virtual_thread.py  |  12 +-
 .../test_s_tir_transform_loop_partition.py         |   3 +-
 ...test_s_tir_transform_lower_thread_all_reduce.py |   4 +-
 ...st_s_tir_transform_memhammer_lower_auto_copy.py |   3 +-
 tests/python/tirx-base/test_tir_base.py            |   2 +-
 .../test_tir_transform_bf16_legalize.py            |  13 +-
 .../test_tir_transform_lower_intrin.py             |   3 +-
 .../test_tir_transform_make_packed_api.py          |   7 +-
 ...est_tir_transform_pointer_value_type_rewrite.py |   4 +-
 .../test_tir_transform_split_host_device.py        |   3 +-
 .../test_tir_transform_storage_rewrite.py          |  21 +--
 .../tirx-transform/test_tir_transform_vectorize.py |   3 +-
 .../python/tirx/codegen/test_codegen_blackwell.py  |   5 +-
 tests/python/tirx/codegen/test_codegen_dsmem.py    |   4 +-
 tests/python/tirx/codegen/test_ptx_addr.py         |   3 +-
 tests/python/tirx/test_hint.py                     |  12 +-
 tests/python/tirx/test_op_namespace_cleanup.py     |   5 +-
 tests/python/tirx/test_parser_printer.py           |  27 ++-
 tests/python/tirx/transform/test_stmt_functor.py   |   4 +-
 .../transform/test_transform_flatten_buffer.py     |  16 +-
 .../tirx/transform/test_transform_lower_tirx.py    |   3 +-
 .../tvmscript/test_tvmscript_parser_source.py      |   2 +-
 70 files changed, 766 insertions(+), 754 deletions(-)

diff --git a/include/tvm/tirx/stmt_functor.h b/include/tvm/tirx/stmt_functor.h
index 54cf329a0a..9aa466f603 100644
--- a/include/tvm/tirx/stmt_functor.h
+++ b/include/tvm/tirx/stmt_functor.h
@@ -366,15 +366,6 @@ class TVM_DLL StmtExprMutator : public ExprMutator, public 
StmtMutator {
   Expr VisitExpr_(const BufferRegionNode* op) override;
 };
 
-/*!
- * \brief Recursively visit a statement or expression in post DFS order, 
applying fvisit.
- * Each node is guaranteed to be visited only once.
- * \param node The statement or expression to be visited.
- * \param fvisit The visitor function to be applied.
- */
-TVM_DLL void PostOrderVisit(const ffi::ObjectRef& node,
-                            std::function<void(const ffi::ObjectRef&)> fvisit);
-
 /*!
  * \brief Substitute the var specified by vmap.
  * \param stmt The source statement to be substituted
@@ -546,16 +537,6 @@ TVM_DLL Stmt SubstituteWithDataTypeLegalization(
 TVM_DLL PrimExpr SubstituteWithDataTypeLegalization(
     PrimExpr expr, std::function<ffi::Optional<PrimExpr>(const Var&)> vmap);
 
-/*!
- * \brief Recursively visit a statement or expression in pre DFS order, 
applying fvisit.
- * If fvisit returns false, it won't visit the children of the node.
- * \param stmt_or_expr The statement or expression to be visited.
- * \param fvisit The visitor function to be applied. If fvisit returns false, 
it won't visit the
- * children of the node
- */
-TVM_DLL void PreOrderVisit(const ffi::ObjectRef& stmt_or_expr,
-                           const std::function<bool(const ffi::ObjectRef&)>& 
fvisit);
-
 /*!
  * \brief Check if the statement contains the specified node type.
  *
diff --git a/python/tvm/relax/utils.py b/python/tvm/relax/utils.py
index 78242bd070..2d9e42523f 100644
--- a/python/tvm/relax/utils.py
+++ b/python/tvm/relax/utils.py
@@ -183,7 +183,7 @@ def gen_call_tir_inputs(
             if isinstance(e, tvm.ir.Var) and e not in tir_var_map:
                 tir_var_map[e] = tvm.ir.Var(e.name, e.ty)
 
-        tirx.stmt_functor.post_order_visit(expr, _visit_expr)
+        tvm_ffi.structural_walk(expr, (tvm.ir.Var, _visit_expr))
 
     def _convert_te_arg(te_args: Any) -> Any:
         """Helper function used to convert Relax expressions to TE tensor.
diff --git a/python/tvm/s_tir/dlight/analysis/common_analysis.py 
b/python/tvm/s_tir/dlight/analysis/common_analysis.py
index 9c29d49d79..1a697ef7ef 100644
--- a/python/tvm/s_tir/dlight/analysis/common_analysis.py
+++ b/python/tvm/s_tir/dlight/analysis/common_analysis.py
@@ -23,7 +23,7 @@ import logging
 from collections import namedtuple
 from typing import Literal
 
-from tvm_ffi import get_global_func
+from tvm_ffi import get_global_func, structural_walk
 
 from tvm import ir, s_tir, tirx
 from tvm.s_tir import Schedule
@@ -423,7 +423,7 @@ def collect_vars_used_in_prim_expr(expr: tirx.Expr) -> 
set[tirx.Var]:
         if ir.is_prim_var(expr):
             tir_vars.add(expr)
 
-    tirx.stmt_functor.post_order_visit(expr, _collect_tir_var)
+    structural_walk(expr, (tirx.Var, _collect_tir_var))
     return tir_vars
 
 
diff --git a/python/tvm/s_tir/dlight/gpu/fallback.py 
b/python/tvm/s_tir/dlight/gpu/fallback.py
index 0acb39a6ba..0c65566632 100644
--- a/python/tvm/s_tir/dlight/gpu/fallback.py
+++ b/python/tvm/s_tir/dlight/gpu/fallback.py
@@ -17,6 +17,8 @@
 # pylint: disable=missing-docstring
 """A fallback schedule rule for GPU operators."""
 
+import tvm_ffi
+
 from tvm import s_tir, tirx
 from tvm.target import Target
 
@@ -40,7 +42,7 @@ def _has_internal_thread_env(stmt: tirx.Stmt) -> bool:
         elif isinstance(node, tirx.For) and node.kind == 
tirx.ForKind.THREAD_BINDING:
             found = True
 
-    tirx.stmt_functor.post_order_visit(stmt, _visit)
+    tvm_ffi.structural_walk(stmt, ((tirx.AttrStmt, tirx.For), _visit))
     return found
 
 
diff --git a/python/tvm/s_tir/dlight/gpu/general_reduction.py 
b/python/tvm/s_tir/dlight/gpu/general_reduction.py
index d3d758afc9..b79d644ac1 100644
--- a/python/tvm/s_tir/dlight/gpu/general_reduction.py
+++ b/python/tvm/s_tir/dlight/gpu/general_reduction.py
@@ -17,6 +17,8 @@
 # pylint: disable=invalid-name
 """Reduction rule for operators including softmax, layer norm, RMS norm, etc"""
 
+import tvm_ffi
+
 from tvm import arith, ir, s_tir, tirx
 from tvm.target import Target
 
@@ -167,8 +169,8 @@ class GeneralReduction(GPUScheduleRule):
                 buffer = buffer_read.buffer
                 if buffer in reduced_buffers:
                     for read_range in buffer_read.region:
-                        tirx.stmt_functor.post_order_visit(read_range.min, 
_visit_expr)
-                        tirx.stmt_functor.post_order_visit(read_range.extent, 
_visit_expr)
+                        tvm_ffi.structural_walk(read_range.min, (tirx.Var, 
_visit_expr))
+                        tvm_ffi.structural_walk(read_range.extent, (tirx.Var, 
_visit_expr))
 
             s_loops = []
             other_loops = []
diff --git a/python/tvm/tirx/stmt_functor.py b/python/tvm/tirx/stmt_functor.py
index eec1c184f9..439e04930b 100644
--- a/python/tvm/tirx/stmt_functor.py
+++ b/python/tvm/tirx/stmt_functor.py
@@ -980,36 +980,6 @@ class StmtExprMutator(StmtMutator, ExprMutator):
         return ExprMutator.visit_expr(self, expr)
 
 
-def post_order_visit(node, fvisit):
-    """Recursively visit a statement or expression in post DFS order, applying 
fvisit.
-       Each node is guaranteed to be visited only once.
-
-    Parameters
-    ----------
-    node : tvm.tirx.Stmt or tvm.ir.Expr
-        The statement or expression to visit.
-
-    fvisit: function
-        The visitor function.
-    """
-    return _ffi_api.PostOrderVisit(node, fvisit)  # type: ignore
-
-
-def pre_order_visit(node, fvisit):
-    """Recursively visit a statement or expression in pre-order, applying 
fvisit.
-       If fvisit returns False, it won't visit the children of the node.
-
-    Parameters
-    ----------
-    node : tvm.tirx.Stmt or tvm.ir.Expr
-        The statement or expression to visit.
-
-    fvisit: function of the signature Object -> bool
-        The visitor function.
-    """
-    return _ffi_api.PreOrderVisit(node, fvisit)  # type: ignore
-
-
 def substitute(node, vmap):
     """Substitute the var specified by vmap.
 
diff --git a/src/backend/trn/codegen/codegen_trn.cc 
b/src/backend/trn/codegen/codegen_trn.cc
index da0f6f744b..5646054de5 100644
--- a/src/backend/trn/codegen/codegen_trn.cc
+++ b/src/backend/trn/codegen/codegen_trn.cc
@@ -22,6 +22,7 @@
  */
 #include "codegen_trn.h"
 
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ir/prim/expr.h>
 #include <tvm/runtime/logging.h>
 #include <tvm/tirx/transform.h>
@@ -309,14 +310,18 @@ std::string CodeGenTrainium::PrintIndices(const 
Array<PrimExpr>& indices) {
   ctx_.buffer_index = 0;
   ctx_.used_var_cnt = 0;
   for (size_t i = 0; i < indices.size(); ++i) {
-    PreOrderVisit(indices[i], [&](const ffi::ObjectRef& node) {
-      if (const auto* v = node.as<VarNode>()) {
-        if (ctx_.tensorized_loop_vars.count(v)) {
-          ctx_.used_var_cnt++;
-        }
+    std::unordered_set<const ffi::Object*> visited;
+    auto walk_fn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
+      const VarNode* v = var.get();
+      if (!visited.insert(v).second) {
+        return ffi::WalkResult::Advance();
       }
-      return true;
-    });
+      if (ctx_.tensorized_loop_vars.count(v)) {
+        ctx_.used_var_cnt++;
+      }
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(indices[i], walk_fn);
   }
   for (size_t i = 0; i < indices.size(); ++i) {
     if (i != 0) {
@@ -515,22 +520,22 @@ void CodeGenTrainium::VisitExpr_(const CallNode* op, 
std::ostream& os) {  // NOL
     LOG(FATAL) << "Trainium codegen does not support call to " << op->op;
   }
   if (ctx_.mask.defined()) {
-    PreOrderVisit(ctx_.mask, [&](const ffi::ObjectRef& node) {
-      if (const auto* v = node.as<VarNode>()) {
-        if (ctx_.tensorized_loop_vars.count(v)) {
-          TVM_FFI_ICHECK(ctx_.loopvar2dim.count(v))
-              << "nki_dim must be specified for tensorized loop variables used 
in mask. However, "
-                 "it is not specified for "
-              << ffi::GetRef<Var>(v);
-          auto dim_str = ctx_.loopvar2dim[v];
-          TVM_FFI_ICHECK(dim_str == "P" || dim_str == "F")
-              << "Only nki_dim = P or F is allowed for tensorized loop 
variables used in mask. "
-                 "However, "
-              << ffi::GetRef<Var>(v) << " has nki_dim = " << dim_str;
-        }
+    auto walk_fn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
+      const VarNode* v = var.get();
+      if (ctx_.tensorized_loop_vars.count(v)) {
+        TVM_FFI_ICHECK(ctx_.loopvar2dim.count(v))
+            << "nki_dim must be specified for tensorized loop variables used 
in mask. However, "
+               "it is not specified for "
+            << ffi::GetRef<Var>(v);
+        auto dim_str = ctx_.loopvar2dim[v];
+        TVM_FFI_ICHECK(dim_str == "P" || dim_str == "F")
+            << "Only nki_dim = P or F is allowed for tensorized loop variables 
used in mask. "
+               "However, "
+            << ffi::GetRef<Var>(v) << " has nki_dim = " << dim_str;
       }
-      return true;
-    });
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(ctx_.mask, walk_fn);
     os << ", mask=" << PrintExpr(ctx_.mask);
   }
   os << ")";
diff --git a/src/relax/analysis/tir_op_pattern_kind.cc 
b/src/relax/analysis/tir_op_pattern_kind.cc
index 5287e56d96..de8a8d06d9 100644
--- a/src/relax/analysis/tir_op_pattern_kind.cc
+++ b/src/relax/analysis/tir_op_pattern_kind.cc
@@ -19,6 +19,7 @@
 
 #include <tvm/arith/iter_affine_map.h>
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/relax/analysis.h>
 #include <tvm/relax/op_attr_types.h>
@@ -260,15 +261,14 @@ class PatternKindAnalyzer : public StmtExprVisitor {
         return false;
       }
     }
+    auto walk_fn = [&](const tirx::Var& var) -> ffi::Expected<ffi::WalkResult> 
{
+      if (auto prim_var = var.as<tirx::PrimVar>()) {
+        vars.erase(prim_var.value().get());
+      }
+      return ffi::WalkResult::Advance();
+    };
     for (const PrimExpr& index : load->indices) {
-      PreOrderVisit(index, [&](const ffi::ObjectRef& node) {
-        if (auto var = node.as<tirx::PrimVar>()) {
-          if (vars.count(var.value().get())) {
-            vars.erase(var.value().get());
-          }
-        }
-        return true;
-      });
+      ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(index, walk_fn);
     }
     return !vars.empty();
   }
diff --git a/src/relax/script/printer/dependent_type.cc 
b/src/relax/script/printer/dependent_type.cc
index af1f126fce..606e37a63f 100644
--- a/src/relax/script/printer/dependent_type.cc
+++ b/src/relax/script/printer/dependent_type.cc
@@ -17,6 +17,7 @@
  * under the License.
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/tirx/stmt_functor.h>
 
 #include "./utils.h"
@@ -44,13 +45,15 @@ ExprDoc PrintShapeVar(const PrimExpr& e, const AccessPath& 
e_p, const IRDocsifie
   // Step 2. Figure out if the PrimExpr contains at least a func var
   bool func_var_mode = false;
   if (f != nullptr) {
-    tirx::PostOrderVisit(e, [f, &func_var_mode](const ffi::ObjectRef& obj) -> 
void {
-      if (auto var = obj.as<tirx::PrimVar>()) {
-        if (f->func_vars->count(var.value().get())) {
+    auto walk_fn = [f, &func_var_mode](const tirx::Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+      if (auto prim_var = var.as<tirx::PrimVar>()) {
+        if (f->func_vars->count(prim_var.value().get())) {
           func_var_mode = true;
         }
       }
-    });
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(e, walk_fn);
   }
   // Step 3. Stringify the PrimExpr if func var exists
   bool is_bare_type_var = false;
diff --git a/src/relax/transform/rewrite_cuda_graph.cc 
b/src/relax/transform/rewrite_cuda_graph.cc
index 8515d9df3e..99b0f2d2d6 100644
--- a/src/relax/transform/rewrite_cuda_graph.cc
+++ b/src/relax/transform/rewrite_cuda_graph.cc
@@ -50,6 +50,7 @@
  * with `CUDAGraphRewriter`.
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/prim/expr.h>
 #include <tvm/relax/analysis.h>
@@ -483,17 +484,22 @@ class CUDAGraphRewritePlanner : public ExprVisitor {
                 [[maybe_unused]] std::vector<const VarNode*>* vars_collector = 
nullptr,
                 std::vector<tirx::PrimVar>* tir_vars_collector = nullptr) {
     bool is_static = true;
-    tirx::PostOrderVisit(expr, [&](const ffi::ObjectRef& e) {
-      if (auto var = e.as<tirx::PrimVar>()) {
-        if (!capture_symbolic_vars_.count(var.value())) {
-          is_static = false;
-          return;
-        }
-        if (tir_vars_collector != nullptr) {
-          tir_vars_collector->push_back(var.value());
-        }
+    std::unordered_set<const ffi::Object*> visited;
+    auto walk_fn = [&](const tirx::Var& var) -> ffi::Expected<ffi::WalkResult> 
{
+      auto prim_var = var.as<tirx::PrimVar>();
+      if (!prim_var || !visited.insert(prim_var.value().get()).second) {
+        return ffi::WalkResult::Advance();
+      }
+      if (!capture_symbolic_vars_.count(prim_var.value())) {
+        is_static = false;
+        return ffi::WalkResult::Advance();
       }
-    });
+      if (tir_vars_collector != nullptr) {
+        tir_vars_collector->push_back(prim_var.value());
+      }
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(expr, walk_fn);
     return is_static;
   }
 
diff --git a/src/s_tir/analysis/sblock_buffer_access_lca_detector.cc 
b/src/s_tir/analysis/sblock_buffer_access_lca_detector.cc
index 1a2fac9415..2ccacf0ca1 100644
--- a/src/s_tir/analysis/sblock_buffer_access_lca_detector.cc
+++ b/src/s_tir/analysis/sblock_buffer_access_lca_detector.cc
@@ -23,6 +23,7 @@
  */
 
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/tirx/analysis.h>
 #include <tvm/tirx/stmt_functor.h>
@@ -150,12 +151,12 @@ class LCADetector : public StmtExprVisitor {
     auto do_collect_itervar_scope = [this](const IterVar& itervar,
                                            const PrimExpr& binding) -> const 
ScopeInfo* {
       const ScopeInfo* highest_scope = nullptr;
-      PostOrderVisit(binding, [this, &highest_scope](const ffi::ObjectRef& 
obj) {
-        if (auto var = obj.as<PrimVar>()) {
-          const VarNode* loop_var = var.value().get();
+      auto walk_fn = [this, &highest_scope](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+        if (auto prim_var = var.as<PrimVar>()) {
+          const VarNode* loop_var = prim_var.value().get();
           auto it = loop_scope_map_.find(loop_var);
           if (it == loop_scope_map_.end()) {
-            return;
+            return ffi::WalkResult::Advance();
           }
           const ScopeInfo* scope = it->second->parent_scope_info;
           if (highest_scope == nullptr) {
@@ -164,7 +165,9 @@ class LCADetector : public StmtExprVisitor {
             highest_scope = scope;
           }
         }
-      });
+        return ffi::WalkResult::Advance();
+      };
+      ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(binding, walk_fn);
       return highest_scope;
     };
 
@@ -200,12 +203,13 @@ class LCADetector : public StmtExprVisitor {
       const BufferVar& buffer = region->buffer;
       const ScopeInfo* scope = ancestor_scopes_.back();
 
-      auto handle_itervar = [&opaque_var_scope, &scope](const ffi::ObjectRef& 
obj) {
-        if (auto var = obj.as<PrimVar>()) {
-          const VarNode* iter_var = var.value().get();
+      auto handle_itervar = [&opaque_var_scope,
+                             &scope](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+        if (auto prim_var = var.as<PrimVar>()) {
+          const VarNode* iter_var = prim_var.value().get();
           auto dom_scope_it = opaque_var_scope.find(iter_var);
           if (dom_scope_it == opaque_var_scope.end()) {
-            return;
+            return ffi::WalkResult::Advance();
           }
           // find the highest loop scope the accessed buffer index has
           // loop carried dependencies to (via opaque iter var binding).
@@ -213,12 +217,14 @@ class LCADetector : public StmtExprVisitor {
             scope = dom_scope_it->second;
           }
         }
+        return ffi::WalkResult::Advance();
       };
 
       // visit region min and max to find the lowest legal lca scope
       for (const Range& range : region->region) {
-        PostOrderVisit(range->min, handle_itervar);
-        PostOrderVisit(range->min + range->extent - 1, handle_itervar);
+        ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(range->min, 
handle_itervar);
+        ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(range->min + 
range->extent - 1,
+                                                        handle_itervar);
       }
 
       // the scope should be above `highest_reduce_scope` for reduce output 
buffer.
diff --git a/src/s_tir/meta_schedule/feature_extractor/per_store_feature.cc 
b/src/s_tir/meta_schedule/feature_extractor/per_store_feature.cc
index 6a20f4aa4b..5fe771d5db 100644
--- a/src/s_tir/meta_schedule/feature_extractor/per_store_feature.cc
+++ b/src/s_tir/meta_schedule/feature_extractor/per_store_feature.cc
@@ -17,6 +17,7 @@
  * under the License.
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/s_tir/transform.h>
 #include <tvm/tirx/transform.h>
@@ -262,13 +263,11 @@ Pass SimplifyForFeatureExtraction() {
 
    private:
     static bool HasBufferLoad(const PrimExpr& expr) {
-      bool found = false;
-      PostOrderVisit(expr, [&found](const ffi::ObjectRef& node) {
-        if (node->IsInstance<TensorLoadNode>()) {
-          found = true;
-        }
-      });
-      return found;
+      auto walk_fn = [](const TensorLoad&) -> ffi::Expected<ffi::WalkResult> {
+        return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
+      };
+      auto result = ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(expr, 
walk_fn);
+      return result.has_value() ? result.value()->value.cast<bool>() : false;
     }
 
     Expr VisitExpr_(const SelectNode* node) final {
@@ -798,28 +797,28 @@ void Feature::Init(const BufferStoreNode* store, int 
n_loops) {
     info.access_type = AccessType::kWrite;
     info.multi_indices.push_back({store->indices.begin(), 
store->indices.end()});
   }
-  PostOrderVisit(store->value, [&buffer_info](const ffi::ObjectRef& obj) -> 
void {
-    if (const TensorLoadNode* load = obj.as<TensorLoadNode>()) {
-      BufferVar buffer = load->source.as_or_throw<tvm::tirx::BufferVar>();
-      Info& info = buffer_info[buffer];
-      switch (info.access_type) {
-        case AccessType::kRead:
-          break;
-        case AccessType::kWrite:
-          info.access_type = AccessType::kReadWrite;
-          break;
-        case AccessType::kReadWrite:
-          break;
-        case AccessType::kUnknownRW:
-        default:
-          info.access_type = AccessType::kRead;
-          break;
-      }
-      if (info.access_type != AccessType::kReadWrite) {
-        info.multi_indices.push_back({load->indices.begin(), 
load->indices.end()});
-      }
+  auto walk_fn = [&buffer_info](const TensorLoad& load) -> 
ffi::Expected<ffi::WalkResult> {
+    BufferVar buffer = load->source.as_or_throw<tvm::tirx::BufferVar>();
+    Info& info = buffer_info[buffer];
+    switch (info.access_type) {
+      case AccessType::kRead:
+        break;
+      case AccessType::kWrite:
+        info.access_type = AccessType::kReadWrite;
+        break;
+      case AccessType::kReadWrite:
+        break;
+      case AccessType::kUnknownRW:
+      default:
+        info.access_type = AccessType::kRead;
+        break;
     }
-  });
+    if (info.access_type != AccessType::kReadWrite) {
+      info.multi_indices.push_back({load->indices.begin(), 
load->indices.end()});
+    }
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(store->value, walk_fn);
   this->sub_features.reserve(buffer_info.size());
   for (const auto& kv : buffer_info) {
     this->sub_features.emplace_back(kv.first, kv.second.access_type,
@@ -920,13 +919,15 @@ void Feature::SubFeature::SetReuse(const LoopNest& 
loop_nest, int64_t top_loop_t
   BufferVar buffer = this->buffer;
   // Step 3.1. Collect all `Var`s that appears in the buffer region
   std::unordered_set<const VarNode*> region_vars;
+  auto walk_fn = [&region_vars](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+    if (auto prim_var = var.as<PrimVar>()) {
+      region_vars.insert(prim_var.value().get());
+    }
+    return ffi::WalkResult::Advance();
+  };
   for (const MultiIndex& multi_index : this->multi_indices) {
     for (const PrimExpr& index : multi_index) {
-      PostOrderVisit(index, [&region_vars](const ffi::ObjectRef& obj) -> void {
-        if (auto var = obj.as<PrimVar>()) {
-          region_vars.insert(var.value().get());
-        }
-      });
+      ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(index, walk_fn);
     }
   }
   // Default case: no reuse
diff --git a/src/s_tir/meta_schedule/postproc/rewrite_cooperative_fetch.cc 
b/src/s_tir/meta_schedule/postproc/rewrite_cooperative_fetch.cc
index ad5bb883b3..2cea200f41 100644
--- a/src/s_tir/meta_schedule/postproc/rewrite_cooperative_fetch.cc
+++ b/src/s_tir/meta_schedule/postproc/rewrite_cooperative_fetch.cc
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/op.h>
 #include <tvm/s_tir/stmt.h>
@@ -91,23 +92,29 @@ bool ParseWarpExecutionAnn(const Schedule& sch, const 
Instruction& inst) {
 
 size_t GetMaxUsedDtypeBytes(SBlock block) {
   size_t max_bytes = 1;
-
-  tirx::PostOrderVisit(block->body, [&](const ffi::ObjectRef& obj) {
-    if (const auto* store = obj.as<tirx::BufferStoreNode>()) {
-      max_bytes = std::max(max_bytes, store->value.ty().StorageBytes());
-    } else if (const auto* load = obj.as<TensorLoadNode>()) {
-      max_bytes = std::max(max_bytes, 
load->ty.as_or_throw<PrimType>().StorageBytes());
-    } else if (const auto* call = obj.as<CallNode>()) {
-      static const Op& q_multiply_shift_per_axis_op = 
Op::Get("tirx.q_multiply_shift_per_axis");
-      static const Op& q_multiply_shift_op = Op::Get("tirx.q_multiply_shift");
-      if (call->op.same_as(q_multiply_shift_per_axis_op) || 
call->op.same_as(q_multiply_shift_op)) {
-        // q_multiply_shift uses 64 bit multiply
-        max_bytes = std::max<size_t>(max_bytes, 8);
-      }
-    } else if (const auto* cast = obj.as<prim::CastNode>()) {
-      max_bytes = std::max(max_bytes, 
cast->ty.as_or_throw<PrimType>().StorageBytes());
+  auto visit_store = [&](const tirx::BufferStore& store) -> 
ffi::Expected<ffi::WalkResult> {
+    max_bytes = std::max(max_bytes, store->value.ty().StorageBytes());
+    return ffi::WalkResult::Advance();
+  };
+  auto visit_load = [&](const TensorLoad& load) -> 
ffi::Expected<ffi::WalkResult> {
+    max_bytes = std::max(max_bytes, 
load->ty.as_or_throw<PrimType>().StorageBytes());
+    return ffi::WalkResult::Advance();
+  };
+  auto visit_call = [&](const Call& call) -> ffi::Expected<ffi::WalkResult> {
+    static const Op& q_multiply_shift_per_axis_op = 
Op::Get("tirx.q_multiply_shift_per_axis");
+    static const Op& q_multiply_shift_op = Op::Get("tirx.q_multiply_shift");
+    if (call->op.same_as(q_multiply_shift_per_axis_op) || 
call->op.same_as(q_multiply_shift_op)) {
+      // q_multiply_shift uses 64 bit multiply
+      max_bytes = std::max<size_t>(max_bytes, 8);
     }
-  });
+    return ffi::WalkResult::Advance();
+  };
+  auto visit_cast = [&](const prim::Cast& cast) -> 
ffi::Expected<ffi::WalkResult> {
+    max_bytes = std::max(max_bytes, 
cast->ty.as_or_throw<PrimType>().StorageBytes());
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(block->body, visit_store, 
visit_load, visit_call,
+                                                  visit_cast);
 
   return max_bytes;
 }
diff --git a/src/s_tir/meta_schedule/postproc/rewrite_tensorize.cc 
b/src/s_tir/meta_schedule/postproc/rewrite_tensorize.cc
index c1b9409406..638706b0b4 100644
--- a/src/s_tir/meta_schedule/postproc/rewrite_tensorize.cc
+++ b/src/s_tir/meta_schedule/postproc/rewrite_tensorize.cc
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/runtime/logging.h>
 #include <tvm/s_tir/meta_schedule/postproc.h>
@@ -37,32 +38,32 @@ void CollectTensorizationJobs(
     const s_tir::Schedule& sch, const ffi::String& func_name, const 
tirx::PrimFuncNode* func,
     bool vectorize_init_loop,
     std::vector<std::tuple<ffi::String, ffi::String, 
std::function<void(s_tir::SBlockRV)>>>* jobs) {
-  tirx::PostOrderVisit(func->body, [=, &jobs](const ffi::ObjectRef& obj) {
-    if (const auto* block = obj.as<tirx::SBlockNode>()) {
-      tirx::StmtSRef block_sref = sch->GetSRef(block);
-      std::string block_name = 
block_sref->StmtAs<tirx::SBlockNode>()->name_hint;
-      if (ffi::Optional<ffi::String> intrin_name =
-              s_tir::GetAnn<ffi::String>(block_sref, 
s_tir::attr::meta_schedule_auto_tensorize)) {
-        if (intrin_name.value() != "") {
-          jobs->emplace_back(block_name, func_name, [sch, 
intrin_name](s_tir::SBlockRV block) {
-            try {
-              sch->Tensorize(block, intrin_name.value());
-            } catch (const std::exception& e) {
-              LOG(WARNING) << "Tensorize failed with error " << e.what();
-            }
-          });
-        } else if (block_name.find("init") && vectorize_init_loop) {
-          jobs->emplace_back(block_name, func_name, [sch](s_tir::SBlockRV 
block) {
-            ffi::Array<SBlockRV> child_blocks = sch->GetChildBlocks(block);
-            TVM_FFI_ICHECK(child_blocks.size() == 1);
-            ffi::Array<LoopRV> init_loops = sch->GetLoops(child_blocks[0]);
-            TVM_FFI_ICHECK(init_loops.size() == 1);
-            sch->Vectorize(init_loops[0]);
-          });
-        }
+  auto walk_fn = [=, &jobs](const tirx::SBlock& block) -> 
ffi::Expected<ffi::WalkResult> {
+    tirx::StmtSRef block_sref = sch->GetSRef(block.get());
+    std::string block_name = block_sref->StmtAs<tirx::SBlockNode>()->name_hint;
+    if (ffi::Optional<ffi::String> intrin_name =
+            s_tir::GetAnn<ffi::String>(block_sref, 
s_tir::attr::meta_schedule_auto_tensorize)) {
+      if (intrin_name.value() != "") {
+        jobs->emplace_back(block_name, func_name, [sch, 
intrin_name](s_tir::SBlockRV block) {
+          try {
+            sch->Tensorize(block, intrin_name.value());
+          } catch (const std::exception& e) {
+            LOG(WARNING) << "Tensorize failed with error " << e.what();
+          }
+        });
+      } else if (block_name.find("init") && vectorize_init_loop) {
+        jobs->emplace_back(block_name, func_name, [sch](s_tir::SBlockRV block) 
{
+          ffi::Array<SBlockRV> child_blocks = sch->GetChildBlocks(block);
+          TVM_FFI_ICHECK(child_blocks.size() == 1);
+          ffi::Array<LoopRV> init_loops = sch->GetLoops(child_blocks[0]);
+          TVM_FFI_ICHECK(init_loops.size() == 1);
+          sch->Vectorize(init_loops[0]);
+        });
       }
     }
-  });
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(func->body, walk_fn);
 }
 
 class RewriteTensorizeNode : public PostprocNode {
diff --git a/src/s_tir/schedule/analysis/analysis.cc 
b/src/s_tir/schedule/analysis/analysis.cc
index 229c03e1ca..342632b6ce 100644
--- a/src/s_tir/schedule/analysis/analysis.cc
+++ b/src/s_tir/schedule/analysis/analysis.cc
@@ -17,6 +17,7 @@
  * under the License.
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/op.h>
 #include <tvm/s_tir/stmt.h>
@@ -143,11 +144,13 @@ ScopeBlockLoopInfo GetScopeBlockLoopInfo(const SBlock& 
scope_block) {
         } else {
           vars = &result.non_spatial_vars;
         }
-        PostOrderVisit(iter_value, [vars](const ffi::ObjectRef& obj) {
-          if (auto var = obj.as<PrimVar>()) {
-            vars->insert(var.value().get());
+        auto walk_fn = [vars](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+          if (auto prim_var = var.as<PrimVar>()) {
+            vars->insert(prim_var.value().get());
           }
-        });
+          return ffi::WalkResult::Advance();
+        };
+        ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(iter_value, walk_fn);
       }
     }
 
@@ -903,36 +906,36 @@ IterVarType GetLoopIterType(const StmtSRef& loop_sref) {
   int n_spatial = 0;
   int n_reduce = 0;
   int n_other = 0;
-  auto f_visit = [&loop_var, &n_spatial, &n_reduce, &n_other](const 
ffi::ObjectRef& obj) -> bool {
-    if (const auto* realize = obj.as<SBlockRealizeNode>()) {
-      const SBlockNode* block = realize->block.get();
-      // Number of block vars and their bindings
-      TVM_FFI_ICHECK_EQ(realize->iter_values.size(), block->iter_vars.size());
-      size_t n = realize->iter_values.size();
-      for (size_t i = 0; i < n; ++i) {
-        const IterVar& iter_var = block->iter_vars[i];
-        const PrimExpr& binding = realize->iter_values[i];
-        // Categorize the current block var
-        int* ref = nullptr;
-        if (iter_var->iter_type == IterVarType::kDataPar) {
-          ref = &n_spatial;
-        } else if (iter_var->iter_type == IterVarType::kCommReduce) {
-          ref = &n_reduce;
-        } else {
-          ref = &n_other;
-        }
-        // Visit the binding to see if `loop_var` appears
-        PostOrderVisit(binding, [&ref, &loop_var](const ffi::ObjectRef& obj) 
-> void {
-          if (obj.same_as(loop_var)) {
-            (*ref) += 1;
-          }
-        });
+  auto f_visit = [&loop_var, &n_spatial, &n_reduce,
+                  &n_other](const SBlockRealize& realize) -> 
ffi::Expected<ffi::WalkResult> {
+    const SBlockNode* block = realize->block.get();
+    // Number of block vars and their bindings
+    TVM_FFI_ICHECK_EQ(realize->iter_values.size(), block->iter_vars.size());
+    size_t n = realize->iter_values.size();
+    for (size_t i = 0; i < n; ++i) {
+      const IterVar& iter_var = block->iter_vars[i];
+      const PrimExpr& binding = realize->iter_values[i];
+      // Categorize the current block var
+      int* ref = nullptr;
+      if (iter_var->iter_type == IterVarType::kDataPar) {
+        ref = &n_spatial;
+      } else if (iter_var->iter_type == IterVarType::kCommReduce) {
+        ref = &n_reduce;
+      } else {
+        ref = &n_other;
       }
-      return false;
+      // Visit the binding to see if `loop_var` appears
+      auto walk_fn = [&ref, &loop_var](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+        if (var.same_as(loop_var)) {
+          (*ref) += 1;
+        }
+        return ffi::WalkResult::Advance();
+      };
+      ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(binding, walk_fn);
     }
-    return true;
+    return ffi::WalkResult::Skip();
   };
-  PreOrderVisit(loop->body, f_visit);
+  ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(loop->body, f_visit);
   if (n_other) {
     return IterVarType::kOpaque;
   } else if (n_spatial && n_reduce) {
@@ -1332,47 +1335,39 @@ bool HasOp(const Stmt& stmt, const ffi::Array<Op>& ops) 
{
   for (const Op& op : ops) {
     op_set.insert(op.operator->());
   }
-  bool found = false;
-  PreOrderVisit(stmt, [&found, &op_set](const ffi::ObjectRef& obj) -> bool {
-    if (found) {
-      return false;
-    }
-    if (const auto* call = obj.as<CallNode>()) {
-      if (op_set.count(call->op.operator->())) {
-        found = true;
-      }
+  auto walk_fn = [&op_set](const Call& call) -> ffi::Expected<ffi::WalkResult> 
{
+    if (op_set.count(call->op.operator->())) {
+      return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
     }
-    return !found;
-  });
-  return found;
+    return ffi::WalkResult::Advance();
+  };
+  auto result = ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(stmt, walk_fn);
+  return result.has_value() ? result.value()->value.cast<bool>() : false;
 }
 
 bool HasIfThenElse(const Stmt& stmt) {
-  bool has_branch = false;
-  auto f_visit = [&has_branch](const ffi::ObjectRef& obj) -> bool {
-    if (has_branch) {
-      // stop visiting
-      return false;
+  auto visit_realize = [](const SBlockRealize& realize) -> 
ffi::Expected<ffi::WalkResult> {
+    if (!is_one(realize->predicate)) {
+      return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
     }
-    if (const auto* realize = obj.as<SBlockRealizeNode>()) {
-      // Case 1: BlockRealize
-      if (!is_one(realize->predicate)) {
-        has_branch = true;
-      }
-    } else if (obj->IsInstance<IfThenElseNode>() || 
obj->IsInstance<SelectNode>()) {
-      // Case 2: IfThenElse / Select
-      has_branch = true;
-    } else if (const auto* call = obj.as<CallNode>()) {
-      // Case 3: Call the `if_then_else` operator
-      static const Op& if_then_else_op = Op::Get("ir.prim.if_then_else");
-      if (call->op.same_as(if_then_else_op)) {
-        has_branch = true;
-      }
+    return ffi::WalkResult::Advance();
+  };
+  auto visit_branch = [](const IfThenElse&) -> ffi::Expected<ffi::WalkResult> {
+    return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
+  };
+  auto visit_select = [](const Select&) -> ffi::Expected<ffi::WalkResult> {
+    return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
+  };
+  auto visit_call = [](const Call& call) -> ffi::Expected<ffi::WalkResult> {
+    static const Op& if_then_else_op = Op::Get("ir.prim.if_then_else");
+    if (call->op.same_as(if_then_else_op)) {
+      return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
     }
-    return !has_branch;
+    return ffi::WalkResult::Advance();
   };
-  PreOrderVisit(stmt, f_visit);
-  return has_branch;
+  auto result = ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(stmt, 
visit_realize, visit_branch,
+                                                               visit_select, 
visit_call);
+  return result.has_value() ? result.value()->value.cast<bool>() : false;
 }
 
 std::tuple</*exists=*/bool,
@@ -1583,22 +1578,16 @@ bool NeedsMultiLevelTiling(const ScheduleState& self, 
const StmtSRef& block_sref
 }
 
 bool IsSpatialPrimFunc(const PrimFunc& func) {
-  bool result = true;
-  PreOrderVisit(func->body, [&result](const ffi::ObjectRef& obj) {
-    if (result == false) {
-      return false;
-    }
-    if (const auto* block = obj.as<SBlockNode>()) {
-      for (const IterVar& iter_var : block->iter_vars) {
-        if (iter_var->iter_type != IterVarType::kDataPar) {
-          result = false;
-          return false;
-        }
+  auto walk_fn = [](const SBlock& block) -> ffi::Expected<ffi::WalkResult> {
+    for (const IterVar& iter_var : block->iter_vars) {
+      if (iter_var->iter_type != IterVarType::kDataPar) {
+        return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(false));
       }
     }
-    return true;
-  });
-  return result;
+    return ffi::WalkResult::Advance();
+  };
+  auto result = ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(func->body, 
walk_fn);
+  return result.has_value() ? result.value()->value.cast<bool>() : true;
 }
 
 std::pair<int64_t, int64_t> GetCumulativeSpaceAndReductionLength(const 
s_tir::ScheduleState& self,
@@ -1738,23 +1727,20 @@ TensorIntrinDescInfo 
ExtractTensorIntrinDescInfo(arith::AnalyzerObj* analyzer,
   const auto* desc_scope_realize = desc_func->body.as<SBlockRealizeNode>();
   TVM_FFI_ICHECK(desc_scope_realize);
   {
-    auto f_visit = [&](const ffi::ObjectRef& obj) -> bool {
-      // Extract the block
-      if (const auto* block = obj.as<SBlockRealizeNode>()) {
-        info.desc_block = block;
-        return false;
-      }
-      // Extract the loops
-      if (const auto* loop = obj.as<ForNode>()) {
-        info.desc_loops.push_back(loop);
-        info.desc_loop_vars.insert(loop->loop_var.get());
-        if (!analyzer->CanProve(loop->min == 0)) {
-          return false;
-        }
+    auto visit_block = [&](const SBlockRealize& block) -> 
ffi::Expected<ffi::WalkResult> {
+      info.desc_block = block.get();
+      return ffi::WalkResult::Advance();
+    };
+    auto visit_loop = [&](const For& loop) -> ffi::Expected<ffi::WalkResult> {
+      info.desc_loops.push_back(loop.get());
+      info.desc_loop_vars.insert(loop->loop_var.get());
+      if (!analyzer->CanProve(loop->min == 0)) {
+        return ffi::WalkResult::Advance();
       }
-      return true;
+      return ffi::WalkResult::Advance();
     };
-    tirx::PostOrderVisit(desc_scope_realize->block->body, f_visit);
+    
ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(desc_scope_realize->block->body,
 visit_block,
+                                                    visit_loop);
     std::reverse(info.desc_loops.begin(), info.desc_loops.end());
     TVM_FFI_ICHECK(info.desc_block);
   }
@@ -2016,13 +2002,14 @@ class AutoTensorizeMappingProposer {
       auto lhs_buffer_it = extractor_->rhs_buffer_map_.find(rhs_buffer);
       TVM_FFI_ICHECK(lhs_buffer_it != extractor_->rhs_buffer_map_.end());
       const BufferVar& lhs_buffer = lhs_buffer_it->second;
+      auto walk_fn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
+        if (auto prim_var = var.as<PrimVar>()) {
+          update_mask(prim_var.value().get(), &lhs_buffer_masks, 
lhs_buffer_index.at(lhs_buffer));
+        }
+        return ffi::WalkResult::Advance();
+      };
       for (const PrimExpr& index : 
extractor_->lhs_buffer_indices_map_.at(lhs_buffer)) {
-        PreOrderVisit(index, [&](const ffi::ObjectRef& obj) -> bool {
-          if (auto var = obj.as<PrimVar>()) {
-            update_mask(var.value().get(), &lhs_buffer_masks, 
lhs_buffer_index.at(lhs_buffer));
-          }
-          return true;
-        });
+        ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(index, walk_fn);
       }
     }
 
diff --git a/src/s_tir/schedule/analysis/reducer.cc 
b/src/s_tir/schedule/analysis/reducer.cc
index ac15128f7c..477b9b38fb 100644
--- a/src/s_tir/schedule/analysis/reducer.cc
+++ b/src/s_tir/schedule/analysis/reducer.cc
@@ -17,6 +17,7 @@
  * under the License.
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/te/operation.h>
 
 #include "../utils.h"
@@ -563,29 +564,21 @@ bool ReductionIterNotIndexOutputBuffer(const SBlock& 
block) {
   for (const MatchBufferRegion& region : block->match_buffers) {
     match_buffer_sources[region->buffer.get()] = region->source->buffer.get();
   }
-  bool affected = false;
-  PreOrderVisit(block->body, [&](const ffi::ObjectRef& obj) {
-    if (affected) {
-      return false;
-    }
-    const auto* block_node = obj.as<SBlockNode>();
-    if (block_node) {
-      for (const MatchBufferRegion& region : block_node->match_buffers) {
-        match_buffer_sources[region->buffer.get()] = 
region->source->buffer.get();
-      }
+  auto visit_block = [&](const SBlock& nested_block) -> 
ffi::Expected<ffi::WalkResult> {
+    for (const MatchBufferRegion& region : nested_block->match_buffers) {
+      match_buffer_sources[region->buffer.get()] = 
region->source->buffer.get();
     }
-    // Inline AllocBufferNode statements (e.g. `T.local_scalar(...)` 
expansions)
+    return ffi::WalkResult::Advance();
+  };
+  auto visit_alloc = [&](const AllocBuffer& alloc) -> 
ffi::Expected<ffi::WalkResult> {
+    // Inline AllocBuffer statements (e.g. `T.local_scalar(...)` expansions)
     // declare buffer-local scratch storage inside the block body; treat them
     // the same as block->alloc_buffers entries for the 
"write-without-signature"
     // check below.
-    if (const auto* alloc = obj.as<AllocBufferNode>()) {
-      buffer_allocated.insert(alloc->buffer.get());
-    }
-    const auto* store = obj.as<BufferStoreNode>();
-    if (!store) {
-      return true;
-    }
-
+    buffer_allocated.insert(alloc->buffer.get());
+    return ffi::WalkResult::Advance();
+  };
+  auto visit_store = [&](const BufferStore& store) -> 
ffi::Expected<ffi::WalkResult> {
     bool write_is_covered_by_match_buffer =
         match_buffer_sources.count(store->buffer.get()) &&
         
buffer_written.count(match_buffer_sources.find(store->buffer.get())->second);
@@ -593,17 +586,18 @@ bool ReductionIterNotIndexOutputBuffer(const SBlock& 
block) {
                       buffer_allocated.count(store->buffer.get()),
                   ValueError)
         << "The buffer \"" << store->buffer
-        << "\" is written in the block but is not in the block's signature nor 
is it covered by "
-           "a match_buffer";
+        << "\" is written in the block but is not in the block's signature nor 
is it covered "
+           "by a match_buffer";
     for (const PrimExpr& index : store->indices) {
       if (f_uses_reduction_block_var(index)) {
-        affected = true;
-        return false;
+        return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(false));
       }
     }
-    return false;
-  });
-  return !affected;
+    return ffi::WalkResult::Skip();
+  };
+  auto result = ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(block->body, 
visit_block,
+                                                               visit_alloc, 
visit_store);
+  return result.has_value() ? result.value()->value.cast<bool>() : true;
 }
 
 class NoMatchedReducerError : public ScheduleError {
diff --git a/src/s_tir/schedule/primitive/cache_index.cc 
b/src/s_tir/schedule/primitive/cache_index.cc
index 4c04ac5940..4afce512d1 100644
--- a/src/s_tir/schedule/primitive/cache_index.cc
+++ b/src/s_tir/schedule/primitive/cache_index.cc
@@ -18,6 +18,7 @@
  */
 #include <tvm/arith/int_set.h>
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 
 #include "../../../tirx/transform/replace_selected_expr.h"
 #include "../utils.h"
@@ -172,21 +173,21 @@ class IndexInfoCollector : public StmtExprVisitor {
 
       // Record the final sub expr with repeat time greater than cse_thresh_
       // In order to make the result stable, sort it by post order and then by 
complexity
-      PostOrderVisit(store->value, [&semantic_comp_done_by_stmt, this](const 
ffi::ObjectRef& node) {
-        if (auto prim = node.as<PrimExpr>()) {
-          PrimExpr this_expr = prim.value();
-          for (auto& it : semantic_comp_done_by_stmt) {
-            if (it.second >= this->cse_thresh_ && EquivalentTerms(this_expr, 
it.first, true)) {
-              auto find_result =
-                  std::find_if(this->exprs_.begin(), this->exprs_.end(),
-                               [&](PrimExpr expr) { return expr.get() == 
it.first.get(); });
-              if (find_result == this->exprs_.end()) {
-                this->exprs_.push_back(it.first);
-              }
+      auto walk_fn = [&semantic_comp_done_by_stmt,
+                      this](const PrimExpr& this_expr) -> 
ffi::Expected<ffi::WalkResult> {
+        for (auto& it : semantic_comp_done_by_stmt) {
+          if (it.second >= this->cse_thresh_ && EquivalentTerms(this_expr, 
it.first, true)) {
+            auto find_result =
+                std::find_if(this->exprs_.begin(), this->exprs_.end(),
+                             [&](PrimExpr expr) { return expr.get() == 
it.first.get(); });
+            if (find_result == this->exprs_.end()) {
+              this->exprs_.push_back(it.first);
             }
           }
         }
-      });
+        return ffi::WalkResult::Advance();
+      };
+      ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(store->value, walk_fn);
       auto cmp = [&](const PrimExpr& lhs, const PrimExpr& rhs) -> bool {
         return CalculateExprComplexity(lhs) > CalculateExprComplexity(rhs);
       };
@@ -235,9 +236,10 @@ ffi::Array<SBlock> MakeIndexCacheStage(IndexInfo* info, 
const ffi::String& stora
 
     // Collect the block vars in original index computation
     info->origin_block_vars.push_back({});
-    PostOrderVisit(index_expr, [&info, &expr_index](const ffi::ObjectRef& 
node) {
-      if (auto var = node.as<PrimVar>()) {
-        Var iter_var = var.value();
+    auto collect_origin_var = [&info,
+                               &expr_index](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+      if (auto prim_var = var.as<PrimVar>()) {
+        Var iter_var = prim_var.value();
         const ffi::Array<Var>& origin_block_var = 
info->origin_block_vars[expr_index];
         auto find_result = std::find_if(origin_block_var.begin(), 
origin_block_var.end(),
                                         [&](Var it) { return it.get() == 
iter_var.get(); });
@@ -245,21 +247,25 @@ ffi::Array<SBlock> MakeIndexCacheStage(IndexInfo* info, 
const ffi::String& stora
           info->origin_block_vars[expr_index].push_back(iter_var);
         }
       }
-    });
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(index_expr, 
collect_origin_var);
 
     // Collect the loop vars corresponding to collected block vars,
     // which will be used to create new loop vars
     std::vector<Var> iter_vars;
-    for (const Var& it : info->origin_block_vars[expr_index]) {
-      PostOrderVisit(info->var_binding.at(it), [/*&info,*/ &iter_vars](const 
ffi::ObjectRef& node) {
-        if (auto var = node.as<PrimVar>()) {
-          Var iter_var = var.value();
-          if (std::find_if(iter_vars.begin(), iter_vars.end(),
-                           [&](Var it) { return it.get() == iter_var.get(); }) 
== iter_vars.end()) {
-            iter_vars.push_back(iter_var);
-          }
+    auto collect_iter_var = [&iter_vars](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+      if (auto prim_var = var.as<PrimVar>()) {
+        Var iter_var = prim_var.value();
+        if (std::find_if(iter_vars.begin(), iter_vars.end(),
+                         [&](Var it) { return it.get() == iter_var.get(); }) 
== iter_vars.end()) {
+          iter_vars.push_back(iter_var);
         }
-      });
+      }
+      return ffi::WalkResult::Advance();
+    };
+    for (const Var& it : info->origin_block_vars[expr_index]) {
+      
ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(info->var_binding.at(it), 
collect_iter_var);
     }
 
     PrimType data_ty = index_expr.ty();
diff --git a/src/s_tir/schedule/primitive/cache_read_write.cc 
b/src/s_tir/schedule/primitive/cache_read_write.cc
index d67d2486d5..878941a4bb 100644
--- a/src/s_tir/schedule/primitive/cache_read_write.cc
+++ b/src/s_tir/schedule/primitive/cache_read_write.cc
@@ -18,6 +18,7 @@
  */
 
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 
 #include <unordered_set>
 
@@ -2332,13 +2333,12 @@ StmtSRef ReIndex(ScheduleState self, const StmtSRef& 
block_sref, int buffer_inde
 
   // Collect block iters appearing in the original_indices
   std::unordered_set<Var> covered;
+  auto walk_fn = [&covered](const Var& var) -> ffi::Expected<ffi::WalkResult> {
+    covered.insert(var);
+    return ffi::WalkResult::Advance();
+  };
   for (const PrimExpr& index : original_indices) {
-    PreOrderVisit(index, [&](const ffi::ObjectRef& obj) -> bool {
-      if (auto var = obj.as<Var>()) {
-        covered.insert(var.value());
-      }
-      return true;
-    });
+    ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(index, walk_fn);
   }
 
   // Step 2. Creating CacheStageInfo
diff --git a/src/s_tir/schedule/primitive/for_kind.cc 
b/src/s_tir/schedule/primitive/for_kind.cc
index c2c7c1c887..833877af21 100644
--- a/src/s_tir/schedule/primitive/for_kind.cc
+++ b/src/s_tir/schedule/primitive/for_kind.cc
@@ -17,6 +17,7 @@
  * under the License.
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 
 #include "../utils.h"
 
@@ -124,18 +125,16 @@ void CheckLoopParallelizableInBlock(const ScheduleState& 
self, ForKind for_kind,
  */
 void CheckParallelizability(const ScheduleState& self, const For& loop, 
ForKind for_kind,
                             runtime::ThreadScope thread_scope) {
-  PreOrderVisit(loop, [&](const ffi::ObjectRef& node) {
-    if (const auto* realize = node.as<SBlockRealizeNode>()) {
-      // If this block doesn't have corresponding StmtSRef in the schedule 
state, it must be a block
-      // inside `tirx.init()`. We don't check the condition for such blocks.
-      if (!self->stmt2ref.count(realize->block.get())) {
-        return false;
-      }
-      CheckLoopParallelizableInBlock(self, for_kind, loop->loop_var,
-                                     ffi::GetRef<SBlockRealize>(realize), 
thread_scope);
+  auto walk_fn = [&](const SBlockRealize& realize) -> 
ffi::Expected<ffi::WalkResult> {
+    // If this block doesn't have corresponding StmtSRef in the schedule 
state, it must be a
+    // block inside `tirx.init()`. We don't check the condition for such 
blocks.
+    if (!self->stmt2ref.count(realize->block.get())) {
+      return ffi::WalkResult::Skip();
     }
-    return true;
-  });
+    CheckLoopParallelizableInBlock(self, for_kind, loop->loop_var, realize, 
thread_scope);
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(loop, walk_fn);
 }
 
 /*!
diff --git a/src/s_tir/schedule/primitive/layout_transformation.cc 
b/src/s_tir/schedule/primitive/layout_transformation.cc
index 696821f322..4fc6ea3dc8 100644
--- a/src/s_tir/schedule/primitive/layout_transformation.cc
+++ b/src/s_tir/schedule/primitive/layout_transformation.cc
@@ -19,6 +19,7 @@
 
 #include <tvm/arith/analyzer.h>
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/runtime/logging.h>
 
 #include <optional>
@@ -1273,21 +1274,22 @@ IterVarType DetectNewBlockIterType(
     const std::unordered_map<const VarNode*, IterVarType>& 
block_iter_type_map) {
   IterVarType result{kOpaque};
   bool found = false;
-  PostOrderVisit(expr, [&](const ffi::ObjectRef& obj) {
-    if (auto var = obj.as<PrimVar>()) {
-      auto it = block_iter_type_map.find(var.value().get());
+  auto walk_fn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
+    if (auto prim_var = var.as<PrimVar>()) {
+      auto it = block_iter_type_map.find(prim_var.value().get());
       if (it != block_iter_type_map.end()) {
         if (!found) {
           found = true;
           result = it->second;
         } else if (result != it->second) {
           result = kOpaque;
-          return false;
+          return ffi::WalkResult::Interrupt();
         }
       }
     }
-    return true;
-  });
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(expr, walk_fn);
   return result;
 }
 
diff --git a/src/s_tir/schedule/primitive/pad_einsum.cc 
b/src/s_tir/schedule/primitive/pad_einsum.cc
index e07d1c550c..6f08e80fb4 100644
--- a/src/s_tir/schedule/primitive/pad_einsum.cc
+++ b/src/s_tir/schedule/primitive/pad_einsum.cc
@@ -18,6 +18,7 @@
  */
 
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/tirx/op.h>
 
 #include "../utils.h"
@@ -423,13 +424,14 @@ void PadEinsum(ScheduleState self, const StmtSRef& 
block_sref, const ffi::Array<
   // Step 4. Find out the block of our interest
   int pos = -1;
   for (int i = 0; i < static_cast<int>(scope_body.size()); ++i) {
-    bool found = false;
-    PostOrderVisit(scope_body[i], [&found, &block](const ffi::ObjectRef& node) 
{
+    auto walk_fn = [&block](const SBlock& node) -> 
ffi::Expected<ffi::WalkResult> {
       if (node.get() == block) {
-        found = true;
+        return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
       }
-    });
-    if (found) {
+      return ffi::WalkResult::Advance();
+    };
+    auto result = 
ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(scope_body[i], walk_fn);
+    if (result.has_value() && result.value()->value.cast<bool>()) {
       pos = i;
       break;
     }
diff --git a/src/s_tir/schedule/primitive/read_write_at.cc 
b/src/s_tir/schedule/primitive/read_write_at.cc
index b7352c272a..ef40ba230f 100644
--- a/src/s_tir/schedule/primitive/read_write_at.cc
+++ b/src/s_tir/schedule/primitive/read_write_at.cc
@@ -18,6 +18,7 @@
  */
 
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/s_tir/stmt.h>
 
 #include <string>
@@ -184,11 +185,7 @@ struct ReadWriteAtImpl {
       bool r_visited = false;
       bool w_visited = false;
       auto f_visit = [this, &relaxed_regions, &r_visited, &w_visited,
-                      &scope](const ffi::ObjectRef& obj) -> bool {
-        const SBlockRealizeNode* realize = obj.as<SBlockRealizeNode>();
-        if (realize == nullptr) {
-          return true;
-        }
+                      &scope](const SBlockRealize& realize) -> 
ffi::Expected<ffi::WalkResult> {
         const SBlockNode* block = realize->block.get();
         bool has_r = HasBuffer(block->reads, src_);
         bool has_w = HasBuffer(block->writes, src_);
@@ -203,12 +200,12 @@ struct ReadWriteAtImpl {
                   
/*low_inclusive=*/ffi::GetRef<StmtSRef>(self_->stmt2ref.at(block)->parent),
                   /*high_exclusive=*/loop_sref_,
                   /*extra_relax_scope=*/scope)),
-              /*bindings=*/GetBindings(ffi::GetRef<SBlockRealize>(realize)),
+              /*bindings=*/GetBindings(realize),
               /*relaxed_regions=*/&relaxed_regions);
         }
-        return false;
+        return ffi::WalkResult::Skip();
       };
-      PreOrderVisit(subtrees[i], f_visit);
+      ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(subtrees[i], f_visit);
       if (r_visited) {
         r_pos.push_back(i);
       }
diff --git a/src/s_tir/schedule/trace.cc b/src/s_tir/schedule/trace.cc
index a8272f0c94..acaeba7aca 100644
--- a/src/s_tir/schedule/trace.cc
+++ b/src/s_tir/schedule/trace.cc
@@ -17,6 +17,7 @@
  * under the License.
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 
 #include <sstream>
@@ -583,11 +584,11 @@ Trace TraceNode::Simplified(bool remove_postproc) const {
         used_rvs.insert(obj.as<ffi::Object>());
         continue;
       } else if (auto prim_expr = obj.as<PrimExpr>()) {
-        PostOrderVisit(*prim_expr, [&used_rvs](const ffi::ObjectRef& obj) -> 
void {
-          if (obj.as<VarNode>()) {
-            used_rvs.insert(obj.get());
-          }
-        });
+        auto walk_fn = [&used_rvs](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+          used_rvs.insert(var.get());
+          return ffi::WalkResult::Advance();
+        };
+        ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(*prim_expr, walk_fn);
       }
     }
   }
diff --git a/src/s_tir/transform/lower_cross_thread_reduction.cc 
b/src/s_tir/transform/lower_cross_thread_reduction.cc
index 474b23b2f9..47c87dfeb9 100644
--- a/src/s_tir/transform/lower_cross_thread_reduction.cc
+++ b/src/s_tir/transform/lower_cross_thread_reduction.cc
@@ -22,6 +22,7 @@
  */
 #include <tvm/arith/analyzer.h>
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/s_tir/stmt.h>
 #include <tvm/s_tir/transform.h>
@@ -80,15 +81,13 @@ bool IsBoundToThreadIdx(const ForNode* loop) {
 bool IsDominantBlock(const SBlock& scope_block, const SBlock& block) {
   // Step 1. Count the number of writers for each buffer written by the scope 
block.
   std::unordered_map<const VarNode*, int> buffer_writer_cnt;
-  PreOrderVisit(scope_block->body, [&buffer_writer_cnt](const ffi::ObjectRef& 
obj) {
-    if (const auto* block = obj.as<SBlockNode>()) {
-      for (const BufferRegion& buffer_region : block->writes) {
-        ++buffer_writer_cnt[buffer_region->buffer.get()];
-      }
-      return false;
+  auto walk_fn = [&buffer_writer_cnt](const SBlock& block) -> 
ffi::Expected<ffi::WalkResult> {
+    for (const BufferRegion& buffer_region : block->writes) {
+      ++buffer_writer_cnt[buffer_region->buffer.get()];
     }
-    return true;
-  });
+    return ffi::WalkResult::Skip();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(scope_block->body, walk_fn);
   // Step 2. Check whether `block` is the only writer of its outputs.
   for (const BufferRegion& buffer_region : block->writes) {
     TVM_FFI_ICHECK(buffer_writer_cnt.count(buffer_region->buffer.get()));
@@ -479,31 +478,33 @@ Stmt TransformReductionBlock(const SBlockRealizeNode* 
realize,
     for (const ForNode* reduction_loop : reduction_loops) {
       reduction_loop_vars.insert(reduction_loop->loop_var.get());
     }
-    PostOrderVisit(realize->predicate,
-                   [&wb_predicate, &reduction_loop_vars](const ffi::ObjectRef& 
obj) {
-                     if (const auto* and_node = obj.as<AndNode>()) {
-                       ffi::Array<PrimExpr> sub_exprs = {and_node->a, 
and_node->b};
-                       for (PrimExpr sub_expr : sub_exprs) {
-                         if (sub_expr->IsInstance<AndNode>()) {
-                           continue;
-                         }
-                         bool is_reduction = [sub_expr, 
&reduction_loop_vars]() {
-                           ffi::Array<Var> vars = UndefinedVars(sub_expr);
-                           for (Var var : vars) {
-                             if (reduction_loop_vars.find(var.get()) != 
reduction_loop_vars.end()) {
-                               return true;
-                             }
-                           }
-                           return false;
-                         }();
-                         if (!is_reduction) {
-                           wb_predicate = wb_predicate && sub_expr;
-                         }
-                       }
-                       return true;
-                     }
-                     return false;
-                   });
+    std::unordered_set<const ffi::Object*> visited_predicate_nodes;
+    auto walk_fn = [&wb_predicate, &reduction_loop_vars, 
&visited_predicate_nodes](
+                       const And& and_expr) -> ffi::Expected<ffi::WalkResult> {
+      if (!visited_predicate_nodes.insert(and_expr.get()).second) {
+        return ffi::WalkResult::Advance();
+      }
+      ffi::Array<PrimExpr> sub_exprs = {and_expr->a, and_expr->b};
+      for (PrimExpr sub_expr : sub_exprs) {
+        if (sub_expr->IsInstance<AndNode>()) {
+          continue;
+        }
+        bool is_reduction = [sub_expr, &reduction_loop_vars]() {
+          ffi::Array<Var> vars = UndefinedVars(sub_expr);
+          for (Var var : vars) {
+            if (reduction_loop_vars.find(var.get()) != 
reduction_loop_vars.end()) {
+              return true;
+            }
+          }
+          return false;
+        }();
+        if (!is_reduction) {
+          wb_predicate = wb_predicate && sub_expr;
+        }
+      }
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(realize->predicate, 
walk_fn);
     if (wb_buffers[0].scope() != "local") {
       for (const ForNode* loop : reduction_loops) {
         if (loop->thread_binding.has_value()) {
@@ -711,18 +712,16 @@ class CrossThreadReductionTransformer : public 
StmtMutator {
 
     // Condition 5. The block should be the last block under the first 
reduction-related loop.
     bool visit = false;
-    PreOrderVisit(ffi::GetRef<For>(reduction_loops[0]), [block, &visit](const 
ffi::ObjectRef& obj) {
-      if (const auto* realize = obj.as<SBlockRealizeNode>()) {
-        TVM_FFI_CHECK(!visit, ValueError)
-            << "Cross-thread reduction cannot be applied when the reduction "
-               "block isn't the last block under its first reduction-related 
loop";
-        if (realize->block.get() == block) {
-          visit = true;
-        }
-        return false;
+    auto walk_fn = [block, &visit](const SBlockRealize& realize) -> 
ffi::Expected<ffi::WalkResult> {
+      TVM_FFI_CHECK(!visit, ValueError)
+          << "Cross-thread reduction cannot be applied when the reduction "
+             "block isn't the last block under its first reduction-related 
loop";
+      if (realize->block.get() == block) {
+        visit = true;
       }
-      return true;
-    });
+      return ffi::WalkResult::Skip();
+    };
+    
ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(ffi::GetRef<For>(reduction_loops[0]),
 walk_fn);
     return std::make_tuple(n_bound_reduction_loops,       //
                            std::move(reducer),            //
                            std::move(reduction_buffers),  //
diff --git a/src/s_tir/transform/memhammer_intermediate_stage.cc 
b/src/s_tir/transform/memhammer_intermediate_stage.cc
index 20272f87d2..3ce33fb6f8 100644
--- a/src/s_tir/transform/memhammer_intermediate_stage.cc
+++ b/src/s_tir/transform/memhammer_intermediate_stage.cc
@@ -17,6 +17,7 @@
  * under the License.
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 
 #include "memhammer_rewrite_rule.h"
 
@@ -282,26 +283,25 @@ std::pair<Stmt, SeqStmt> InsertCacheStage(Stmt stmt, bool 
is_write_cache, ffi::S
   arith::Analyzer analyzer;
   const TensorLoadNode* target_buffer_load = nullptr;
   if (is_write_cache) {
-    tirx::PreOrderVisit(stmt, [&](const ffi::ObjectRef& obj) {
-      if (const auto* buffer_load = obj.as<TensorLoadNode>()) {
-        if (buffer_load->source.as_or_throw<tvm::tirx::BufferVar>().scope() == 
"wmma.accumulator" ||
-            buffer_load->source.as_or_throw<tvm::tirx::BufferVar>().scope() == 
"m16n8k8.matrixC") {
-          if (target_buffer_load == nullptr) {
-            target_buffer_load = buffer_load;
-          } else {
-            
TVM_FFI_ICHECK(target_buffer_load->source.as_or_throw<tvm::tirx::BufferVar>().same_as(
-                buffer_load->source.as_or_throw<tvm::tirx::BufferVar>()))
-                << "More than one target buffer found";
-            TVM_FFI_ICHECK(target_buffer_load->indices.size() == 
buffer_load->indices.size());
-            for (size_t i = 0; i < target_buffer_load->indices.size(); i++) {
-              TVM_FFI_ICHECK(
-                  analyzer->CanProveEqual(target_buffer_load->indices[i], 
buffer_load->indices[i]));
-            }
+    auto walk_fn = [&](const TensorLoad& buffer_load) -> 
ffi::Expected<ffi::WalkResult> {
+      if (buffer_load->source.as_or_throw<tvm::tirx::BufferVar>().scope() == 
"wmma.accumulator" ||
+          buffer_load->source.as_or_throw<tvm::tirx::BufferVar>().scope() == 
"m16n8k8.matrixC") {
+        if (target_buffer_load == nullptr) {
+          target_buffer_load = buffer_load.get();
+        } else {
+          
TVM_FFI_ICHECK(target_buffer_load->source.as_or_throw<tvm::tirx::BufferVar>().same_as(
+              buffer_load->source.as_or_throw<tvm::tirx::BufferVar>()))
+              << "More than one target buffer found";
+          TVM_FFI_ICHECK(target_buffer_load->indices.size() == 
buffer_load->indices.size());
+          for (size_t i = 0; i < target_buffer_load->indices.size(); i++) {
+            TVM_FFI_ICHECK(
+                analyzer->CanProveEqual(target_buffer_load->indices[i], 
buffer_load->indices[i]));
           }
         }
       }
-      return true;
-    });
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(stmt, walk_fn);
     TVM_FFI_ICHECK(target_buffer_load);
   }
 
diff --git a/src/s_tir/transform/memhammer_tensorcore_rewrite.cc 
b/src/s_tir/transform/memhammer_tensorcore_rewrite.cc
index 2de9369125..29092d821b 100644
--- a/src/s_tir/transform/memhammer_tensorcore_rewrite.cc
+++ b/src/s_tir/transform/memhammer_tensorcore_rewrite.cc
@@ -18,6 +18,7 @@
  */
 
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ir/op.h>
 
 #include "./memhammer_rewrite_rule.h"
@@ -228,17 +229,17 @@ Stmt RewriteWmmaStore(Stmt stmt) {
   // TODO(tian): the assumption that the RHS of BufferStore is TensorLoad may 
not be accurate
   const BufferStoreNode* buf_store = TVM_TYPE_AS(body, BufferStoreNode);
   const TensorLoadNode* buf_load = nullptr;
-  PostOrderVisit(buf_store->value, [&](const ffi::ObjectRef& obj) {
-    const TensorLoadNode* load = obj.as<TensorLoadNode>();
-    if (load && load->source.as_or_throw<tvm::tirx::BufferVar>().scope() == 
"wmma.accumulator") {
+  auto walk_fn = [&](const TensorLoad& load) -> ffi::Expected<ffi::WalkResult> 
{
+    if (load->source.as_or_throw<tvm::tirx::BufferVar>().scope() == 
"wmma.accumulator") {
       TVM_FFI_ICHECK(buf_load == nullptr ||
                      
buf_load->source.as_or_throw<tvm::tirx::BufferVar>().same_as(
                          load->source.as_or_throw<tvm::tirx::BufferVar>()))
           << "More than one source buffer of wmma accumulator found";
-      buf_load = load;
+      buf_load = load.get();
     }
-    return true;
-  });
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(buf_store->value, walk_fn);
   BufferVar src_buffer = buf_load->source.as_or_throw<tvm::tirx::BufferVar>();
   BufferVar tgt_buffer = buf_store->buffer;
 
@@ -439,17 +440,17 @@ Stmt RewriteMmaStore(Stmt stmt) {
   // Step 2. Find matrixC buffer
   const BufferStoreNode* buf_store = TVM_TYPE_AS(body, BufferStoreNode);
   const TensorLoadNode* buf_load = nullptr;
-  PostOrderVisit(buf_store->value, [&](const ffi::ObjectRef& obj) {
-    const TensorLoadNode* load = obj.as<TensorLoadNode>();
-    if (load && load->source.as_or_throw<tvm::tirx::BufferVar>().scope() == 
"m16n8k8.matrixC") {
+  auto walk_fn = [&](const TensorLoad& load) -> ffi::Expected<ffi::WalkResult> 
{
+    if (load->source.as_or_throw<tvm::tirx::BufferVar>().scope() == 
"m16n8k8.matrixC") {
       TVM_FFI_ICHECK(buf_load == nullptr ||
                      
buf_load->source.as_or_throw<tvm::tirx::BufferVar>().same_as(
                          load->source.as_or_throw<tvm::tirx::BufferVar>()))
           << "More than one source buffer of mma accumulator found";
-      buf_load = load;
+      buf_load = load.get();
     }
-    return true;
-  });
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(buf_store->value, walk_fn);
 
   // Step 3. Create new mma body
   // We have the assumption that two innermost loops are the 8 * 8 loop 
generated by
diff --git a/src/s_tir/transform/renew_defs.cc 
b/src/s_tir/transform/renew_defs.cc
index 212fd3c224..86fe153054 100644
--- a/src/s_tir/transform/renew_defs.cc
+++ b/src/s_tir/transform/renew_defs.cc
@@ -23,6 +23,7 @@
  */
 
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/s_tir/transform.h>
 #include <tvm/tirx/stmt_functor.h>
@@ -59,14 +60,14 @@ class RenewDefMutator : public StmtExprMutator {
     for (const auto& param : func->params) {
       if (auto opt_buffer = param.as<BufferVar>()) {
         const BufferVar& buffer = opt_buffer.value();
+        auto walk_fn = [&generator](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+          if (generator.remap_.count(var) == 0) {
+            generator.ReDefineVar(var);
+          }
+          return ffi::WalkResult::Advance();
+        };
         for (const PrimExpr& e : buffer->shape) {
-          PostOrderVisit(e, [&generator](const ffi::ObjectRef& obj) {
-            if (auto var = obj.as<Var>()) {
-              if (generator.remap_.count(var.value()) == 0) {
-                generator.ReDefineVar(var.value());
-              }
-            }
-          });
+          ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(e, walk_fn);
         }
       }
     }
diff --git a/src/te/operation/compute_op.cc b/src/te/operation/compute_op.cc
index 4bd50d15bd..1600541d81 100644
--- a/src/te/operation/compute_op.cc
+++ b/src/te/operation/compute_op.cc
@@ -23,6 +23,7 @@
  */
 
 #include <tvm/arith/analyzer.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/function.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/prim/builtin.h>
@@ -163,16 +164,18 @@ TVM_FFI_STATIC_INIT_BLOCK() {
 ffi::Array<Tensor> ComputeOpNode::InputTensors() const {
   ffi::Array<Tensor> ret;
   std::unordered_set<Tensor> visited;
-  auto visit = [&ret, &visited](const PrimExpr& e) {
-    tirx::PostOrderVisit(e, [&ret, &visited](const ffi::ObjectRef& n) {
-      if (auto call = n.as<Call>(); call.has_value() && 
IsTensorLoad(call.value())) {
-        Tensor t = GetTensorFromLoad(call.value());
-        if (!visited.count(t)) {
-          ret.push_back(t);
-          visited.insert(t);
-        }
+  auto walk_fn = [&ret, &visited](const Call& call) -> 
ffi::Expected<ffi::WalkResult> {
+    if (IsTensorLoad(call)) {
+      Tensor t = GetTensorFromLoad(call);
+      if (!visited.count(t)) {
+        ret.push_back(t);
+        visited.insert(t);
       }
-    });
+    }
+    return ffi::WalkResult::Advance();
+  };
+  auto visit = [&walk_fn](const PrimExpr& e) {
+    ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(e, walk_fn);
   };
   for (const PrimExpr& e : body) {
     if (const auto* reduce = e.as<te::ReduceNode>()) {
diff --git a/src/te/tensor.cc b/src/te/tensor.cc
index a617d54bd4..15d644a9fe 100644
--- a/src/te/tensor.cc
+++ b/src/te/tensor.cc
@@ -21,6 +21,8 @@
  * \file tensor.cc
  */
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_mutate.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/function.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/te/operation.h>
@@ -29,6 +31,22 @@
 namespace tvm {
 namespace te {
 
+namespace {
+
+TVMFFIAny TensorVisit(ffi::StructuralVisitorObj*, ffi::AnyView) noexcept {
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny TensorMutate(ffi::StructuralMutatorObj*, ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny TensorMaybeInplaceMutate(ffi::StructuralMutatorObj*, ffi::AnyView) 
noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+}  // namespace
+
 void TensorNode::RegisterReflection() {
   namespace refl = tvm::ffi::reflection;
   refl::ObjectDef<TensorNode>()
@@ -38,7 +56,15 @@ void TensorNode::RegisterReflection() {
       .def_ro("value_index", &TensorNode::value_index);
 }
 
-TVM_FFI_STATIC_INIT_BLOCK() { TensorNode::RegisterReflection(); }
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  TensorNode::RegisterReflection();
+  refl::TypeAttrDef<TensorNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&TensorVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&TensorMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&TensorMaybeInplaceMutate));
+}
 
 IterVar thread_axis(Range dom, std::string tag) {
   return IterVar(dom, PrimVar(tag, dom.defined() ? dom->extent.ty() : 
PrimType::Int(32)),
diff --git a/src/tirx/ir/index_map.cc b/src/tirx/ir/index_map.cc
index e4e473990d..1f53c6395c 100644
--- a/src/tirx/ir/index_map.cc
+++ b/src/tirx/ir/index_map.cc
@@ -25,6 +25,7 @@
 #include <tvm/arith/int_set.h>
 #include <tvm/arith/iter_affine_map.h>
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/unique_name_supply.h>
 #include <tvm/tirx/index_map.h>
@@ -362,23 +363,25 @@ IndexMap IndexMap::RenameVariables(
   if (f_name_map != nullptr) {
     // Collect variables with pre-defined names provided by f_name_map.
     std::unordered_set<const ffi::Object*> visited;
+    auto walk_fn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
+      auto prim_var = var.as<PrimVar>();
+      if (!prim_var) {
+        return ffi::WalkResult::Advance();
+      }
+      if (!visited.insert(prim_var.value().get()).second) {
+        return ffi::WalkResult::Advance();
+      }
+      if (ffi::Optional<ffi::String> opt_name = f_name_map(prim_var.value());
+          opt_name.has_value()) {
+        ffi::String name = opt_name.value();
+        TVM_FFI_ICHECK(!name_supply->ContainsName(name, /*add_prefix=*/false));
+        name_supply->ReserveName(name, /*add_prefix=*/false);
+        var_remap.Set(prim_var.value(), PrimVar(name, prim_var.value().ty()));
+      }
+      return ffi::WalkResult::Advance();
+    };
     std::for_each(n->final_indices.begin(), n->final_indices.end(), [&](const 
PrimExpr& expr) {
-      PostOrderVisit(expr, [&](const ffi::ObjectRef& obj) {
-        auto var = obj.as<PrimVar>();
-        if (!var) {
-          return;
-        }
-        if (visited.count(obj.get())) {
-          return;
-        }
-        visited.emplace(obj.get());
-        if (ffi::Optional<ffi::String> opt_name = f_name_map(var.value()); 
opt_name.has_value()) {
-          ffi::String name = opt_name.value();
-          TVM_FFI_ICHECK(!name_supply->ContainsName(name, 
/*add_prefix=*/false));
-          name_supply->ReserveName(name, /*add_prefix=*/false);
-          var_remap.Set(var.value(), PrimVar(name, var.value().ty()));
-        }
-      });
+      ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(expr, walk_fn);
     });
   }
 
diff --git a/src/tirx/ir/stmt_functor.cc b/src/tirx/ir/stmt_functor.cc
index 251f9d217e..45cd939e91 100644
--- a/src/tirx/ir/stmt_functor.cc
+++ b/src/tirx/ir/stmt_functor.cc
@@ -743,43 +743,7 @@ Stmt StmtMutator::VisitStmt_(const 
tirx::TilePrimitiveCallNode* op) {
   }
 }
 
-// Implementations of PostOrderVisit and Substitute
-class IRApplyVisit : public StmtExprVisitor {
- public:
-  explicit IRApplyVisit(std::function<void(const ffi::ObjectRef&)> f) : f_(f) 
{}
-
-  void VisitExpr(const Expr& node) final {
-    if (visited_.count(node.get()) != 0) return;
-    visited_.insert(node.get());
-    ExprVisitor::VisitExpr(node);
-    f_(node);
-  }
-
-  void VisitStmt(const Stmt& node) final {
-    if (visited_.count(node.get()) != 0) return;
-    visited_.insert(node.get());
-    StmtVisitor::VisitStmt(node);
-    f_(node);
-  }
-
-  void VisitBufferDef(const BufferVar& buffer, bool alloc_data) override {}
-  void VisitBufferUse(const BufferVar& buffer) override {}
-
- private:
-  std::function<void(const ffi::ObjectRef&)> f_;
-  std::unordered_set<const ffi::Object*> visited_;
-};
-
-void PostOrderVisit(const ffi::ObjectRef& node, std::function<void(const 
ffi::ObjectRef&)> fvisit) {
-  if (node.as<StmtNode>()) {
-    IRApplyVisit visitor(fvisit);
-    visitor(node.as_or_throw<Stmt>());
-  } else {
-    IRApplyVisit visitor(fvisit);
-    visitor(node.as_or_throw<Expr>());
-  }
-}
-
+// Implementations of Substitute
 class IRSubstitute : public StmtExprMutator {
  public:
   explicit IRSubstitute(std::function<ffi::Optional<Expr>(const Var&)> vmap) : 
vmap_(vmap) {}
@@ -854,48 +818,6 @@ Expr Substitute(Expr expr, 
std::function<ffi::Optional<Expr>(const Var&)> vmap)
   return IRSubstitute(std::move(vmap))(std::move(expr));
 }
 
-void PreOrderVisit(const ffi::ObjectRef& stmt_or_expr,
-                   const std::function<bool(const ffi::ObjectRef&)>& fvisit) {
-  class PreOrderVisitor : public StmtExprVisitor {
-   public:
-    explicit PreOrderVisitor(const std::function<bool(const ffi::ObjectRef&)>& 
f) : f_(f) {}
-
-   private:
-    void VisitExpr(const Expr& expr) final {
-      const ExprNode* p_expr = expr.get();
-      if (visited_.count(p_expr) == 0) {
-        visited_.insert(p_expr);
-        if (f_(expr)) {
-          ExprVisitor::VisitExpr(expr);
-        }
-      }
-    }
-
-    void VisitStmt(const Stmt& stmt) final {
-      const StmtNode* p_stmt = stmt.get();
-      if (visited_.count(p_stmt) == 0) {
-        visited_.insert(p_stmt);
-        if (f_(stmt)) {
-          StmtVisitor::VisitStmt(stmt);
-        }
-      }
-    }
-
-    const std::function<bool(const ffi::ObjectRef&)>& f_;
-    std::unordered_set<const ffi::Object*> visited_;
-  };
-
-  PreOrderVisitor visitor(fvisit);
-  if (auto stmt = stmt_or_expr.as<Stmt>()) {
-    visitor(stmt.value());
-  } else if (auto expr = stmt_or_expr.as<Expr>()) {
-    visitor(expr.value());
-  } else {
-    TVM_FFI_THROW(InternalError) << "PreOrderVisit does not accept object with 
type: "
-                                 << stmt_or_expr->GetTypeKey();
-  }
-}
-
 class IRSubstituteWithDataTypeLegalization : public DataTypeLegalizer {
  public:
   explicit 
IRSubstituteWithDataTypeLegalization(std::function<ffi::Optional<Expr>(const 
Var&)> vmap)
@@ -951,22 +873,14 @@ PrimExpr SubstituteWithDataTypeLegalization(
 
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef()
-      .def("tirx.PostOrderVisit",
-           [](ffi::ObjectRef node, ffi::Function f) {
-             tirx::PostOrderVisit(node, [f](const ffi::ObjectRef& n) { f(n); 
});
-           })
-      .def("tirx.PreOrderVisit",
-           [](ffi::ObjectRef node, ffi::Function f) {
-             tirx::PreOrderVisit(node, [f](const ffi::ObjectRef& n) { return 
f(n).cast<bool>(); });
-           })
-      .def("tirx.Substitute", [](ffi::ObjectRef node, ffi::Map<Var, Expr> 
vmap) -> ffi::ObjectRef {
-        if (node->IsInstance<StmtNode>()) {
-          return Substitute(node.as_or_throw<Stmt>(), vmap);
-        } else {
-          return Substitute(node.as_or_throw<Expr>(), vmap);
-        }
-      });
+  refl::GlobalDef().def("tirx.Substitute",
+                        [](ffi::ObjectRef node, ffi::Map<Var, Expr> vmap) -> 
ffi::ObjectRef {
+                          if (node->IsInstance<StmtNode>()) {
+                            return Substitute(node.as_or_throw<Stmt>(), vmap);
+                          } else {
+                            return Substitute(node.as_or_throw<Expr>(), vmap);
+                          }
+                        });
 }
 
 }  // namespace tirx
diff --git a/src/tirx/ir/tir_visitor_with_path.h 
b/src/tirx/ir/tir_visitor_with_path.h
index b14ad713f3..13007a32d2 100644
--- a/src/tirx/ir/tir_visitor_with_path.h
+++ b/src/tirx/ir/tir_visitor_with_path.h
@@ -24,6 +24,7 @@
 #ifndef TVM_TIRX_IR_TIR_VISITOR_WITH_PATH_H_
 #define TVM_TIRX_IR_TIR_VISITOR_WITH_PATH_H_
 
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ir/module.h>
 #include <tvm/ir/scope_stack.h>
 #include <tvm/runtime/logging.h>
@@ -273,13 +274,13 @@ class TIRVisitorWithPath : protected 
ExprFunctor<void(const Expr&, ffi::reflecti
     auto shape_path = path->Attr("shape");
     for (size_t i = 0; i < buf->shape.size(); i++) {
       auto dim_path = shape_path->ArrayItem(i);
-      PostOrderVisit(buf->shape[i], [this, &context, &dim_path](const 
ffi::ObjectRef& obj) {
-        if (auto opt = obj.as<Var>()) {
-          if (auto var_def = WithDefIfUndefined(opt.value(), dim_path)) {
-            context.push_back(std::move(var_def).value());
-          }
+      auto walk_fn = [this, &context, &dim_path](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+        if (auto var_def = WithDefIfUndefined(var, dim_path)) {
+          context.push_back(std::move(var_def).value());
         }
-      });
+        return ffi::WalkResult::Advance();
+      };
+      ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(buf->shape[i], walk_fn);
     }
 
     auto strides_path = path->Attr("strides");
diff --git a/src/tirx/script/printer/buffer.cc 
b/src/tirx/script/printer/buffer.cc
index fa5b0b6058..c3c456f390 100644
--- a/src/tirx/script/printer/buffer.cc
+++ b/src/tirx/script/printer/buffer.cc
@@ -40,22 +40,25 @@ ffi::Map<ffi::String, ExprDoc> BufferAttrs(
 
   // Step 0. Set up statistics
   std::unordered_map<const ffi::Object*, int> use_count;
-  auto update_use_count = [&](const Expr& e) {
-    tirx::PostOrderVisit(e, [&](const ffi::ObjectRef& n) {
-      if (const VarNode* var = n.as<VarNode>()) {
-        ++use_count[var];
+  std::unordered_set<const ffi::Object*> def_seen;
+  auto count_buffer_var = [&](const Var& var,
+                              TVMFFIDefRegionKind kind) -> 
ffi::Expected<ffi::WalkResult> {
+    if (kind != kTVMFFIDefRegionKindNone) {
+      if (!def_seen.insert(var.get()).second) {
+        return ffi::WalkResult::Skip();
       }
-    });
+      return ffi::WalkResult::Advance();
+    }
+    ++use_count[var.get()];
+    return ffi::WalkResult::Advance();
   };
-  update_use_count(buffer->elem_offset);
+  ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(buffer, count_buffer_var);
   if (data.has_value()) {
-    update_use_count(data.value());
-  }
-  for (const PrimExpr& e : buffer->strides) {
-    update_use_count(e);
-  }
-  for (const PrimExpr& e : buffer->shape) {
-    update_use_count(e);
+    auto count_data_var = [&](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+      ++use_count[var.get()];
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(data.value(), 
count_data_var);
   }
   auto is_new_var = [&](const Expr& e) { return e->IsInstance<VarNode>() && 
!d->IsVarDefined(e); };
   auto add_out_of_line_var_def = [&](const Var& var, const AccessPath& var_p) {
@@ -90,16 +93,15 @@ ffi::Map<ffi::String, ExprDoc> BufferAttrs(
       bool contains_new_var = false;
       bool contains_compound_shape_var = false;
       std::unordered_set<Var> vars_in_shape;
-      tirx::PostOrderVisit(e, [&](const ffi::ObjectRef& obj) {
-        if (const auto* var_node = obj.as<VarNode>()) {
-          Var var = ffi::GetRef<Var>(var_node);
-          vars_in_shape.insert(var);
-          contains_new_var =
-              contains_new_var || !d->IsVarDefined(var) || 
stringify_shape_vars.count(var);
-          contains_compound_shape_var =
-              contains_compound_shape_var || 
stringify_compound_shape_vars.count(var);
-        }
-      });
+      auto walk_fn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
+        vars_in_shape.insert(var);
+        contains_new_var =
+            contains_new_var || !d->IsVarDefined(var) || 
stringify_shape_vars.count(var);
+        contains_compound_shape_var =
+            contains_compound_shape_var || 
stringify_compound_shape_vars.count(var);
+        return ffi::WalkResult::Advance();
+      };
+      ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(e, walk_fn);
       if (is_new_var(e)) {
         add_out_of_line_var_def(e.as_or_throw<Var>(), e_p);
       }
diff --git a/src/tirx/script/printer/function.cc 
b/src/tirx/script/printer/function.cc
index 554dba11c5..a461cddfc0 100644
--- a/src/tirx/script/printer/function.cc
+++ b/src/tirx/script/printer/function.cc
@@ -93,21 +93,20 @@ TVM_STATIC_IR_FUNCTOR(IRDocsifier, vtable)
           std::unordered_set<tirx::Var> stringify_shape_vars;
           std::unordered_set<tirx::Var> stringify_compound_shape_vars;
           std::unordered_set<tirx::Var> shape_vars;
+          auto walk_fn = [&](const tirx::Var& shape_var) -> 
ffi::Expected<ffi::WalkResult> {
+            shape_vars.insert(shape_var);
+            bool is_type_var = type_vars.count(shape_var.get());
+            if (!use_postponed_annotations && 
!bound_signature_vars.count(shape_var) &&
+                !is_type_var) {
+              stringify_shape_vars.insert(shape_var);
+            }
+            if (!use_postponed_annotations && is_type_var) {
+              stringify_compound_shape_vars.insert(shape_var);
+            }
+            return ffi::WalkResult::Advance();
+          };
           for (const PrimExpr& shape : buffer->shape) {
-            tirx::PostOrderVisit(shape, [&](const ffi::ObjectRef& obj) {
-              if (const auto* shape_var_node = obj.as<tirx::VarNode>()) {
-                tirx::Var shape_var = ffi::GetRef<tirx::Var>(shape_var_node);
-                shape_vars.insert(shape_var);
-                bool is_type_var = type_vars.count(shape_var.get());
-                if (!use_postponed_annotations && 
!bound_signature_vars.count(shape_var) &&
-                    !is_type_var) {
-                  stringify_shape_vars.insert(shape_var);
-                }
-                if (!use_postponed_annotations && is_type_var) {
-                  stringify_compound_shape_vars.insert(shape_var);
-                }
-              }
-            });
+            ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(shape, walk_fn);
           }
           IdDoc lhs = DefineBuffer(buffer, *f, d);
           ExprDoc annotation =
diff --git a/src/tirx/script/printer/utils.h b/src/tirx/script/printer/utils.h
index 4807a37e84..83ddf213c6 100644
--- a/src/tirx/script/printer/utils.h
+++ b/src/tirx/script/printer/utils.h
@@ -20,6 +20,7 @@
 #define TVM_SCRIPT_PRINTER_TIR_UTILS_H_
 
 #include <tvm/ffi/extra/structural_equal.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/prim/expr.h>
 #include <tvm/script/printer/ir_docsifier.h>
@@ -117,21 +118,22 @@ inline void AsDocBody(const tirx::Stmt& stmt, AccessPath 
p, TIRFrameNode* f, con
   if (const auto* seq_stmt = stmt.as<tirx::SeqStmtNode>()) {
     ffi::Array<tirx::Stmt> body = seq_stmt->seq;
     auto value_refs_buffer = [](const PrimExpr& value, const tirx::BufferVar& 
buffer) {
-      bool found = false;
-      tirx::PostOrderVisit(value, [&](const ffi::ObjectRef& node) {
-        if (const auto* load = node.as<TensorLoadNode>()) {
-          if 
(load->source.as_or_throw<tvm::tirx::BufferVar>().same_as(buffer)) {
-            found = true;
-          }
-        } else if (const auto* call = node.as<CallNode>()) {
-          if (call->op.same_as(tirx::builtin::masked_load()) && 
!call->args.empty()) {
-            if (auto var = call->args[0].as<Var>(); var && 
var.value().same_as(buffer.var())) {
-              found = true;
-            }
+      auto visit_load = [&](const TensorLoad& load) -> 
ffi::Expected<ffi::WalkResult> {
+        if (load->source.as_or_throw<tvm::tirx::BufferVar>().same_as(buffer)) {
+          return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
+        }
+        return ffi::WalkResult::Advance();
+      };
+      auto visit_call = [&](const Call& call) -> 
ffi::Expected<ffi::WalkResult> {
+        if (call->op.same_as(tirx::builtin::masked_load()) && 
!call->args.empty()) {
+          if (auto var = call->args[0].as<Var>(); var && 
var.value().same_as(buffer.var())) {
+            return ffi::WalkResult::Interrupt(ffi::VisitInterrupt(true));
           }
         }
-      });
-      return found;
+        return ffi::WalkResult::Advance();
+      };
+      auto result = ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(value, 
visit_load, visit_call);
+      return result.has_value() ? result.value()->value.cast<bool>() : false;
     };
 
     for (int i = 0, n = body.size(); i < n;) {
diff --git a/src/tirx/transform/ir_utils.cc b/src/tirx/transform/ir_utils.cc
index e41223d819..6e7e44b780 100644
--- a/src/tirx/transform/ir_utils.cc
+++ b/src/tirx/transform/ir_utils.cc
@@ -26,6 +26,7 @@
 #include <tvm/arith/analyzer.h>
 #include <tvm/arith/int_solver.h>
 #include <tvm/ffi/cast.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/scope_stack.h>
 #include <tvm/s_tir/stmt.h>
@@ -129,10 +130,12 @@ class IRConvertSSA final : public StmtExprMutator {
             defined_.insert(var_ptr);
           }
         };
+        auto walk_fn = [&](const Var& var) -> ffi::Expected<ffi::WalkResult> {
+          check_var(var);
+          return ffi::WalkResult::Advance();
+        };
         for (const auto& dim : buffer.value()->shape) {
-          PostOrderVisit(dim, [&](const ffi::ObjectRef& obj) {
-            if (auto var = obj.as<Var>()) check_var(var.value());
-          });
+          ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(dim, walk_fn);
         }
         for (const auto& stride : buffer.value()->strides) {
           if (auto var = stride.as<Var>()) check_var(var.value());
@@ -793,9 +796,10 @@ ffi::Optional<arith::IntConstraints> 
ConditionalBoundsContext::TrySolveCondition
         e->IsInstance<prim::EQNode>() || e->IsInstance<prim::NENode>()) {
       bool is_simple = true;
       std::vector<PrimVar> cand_vars;
-      PostOrderVisit(e, [&cand_vars, &is_simple, &e](const ffi::ObjectRef& 
obj) {
+      auto walk_fn = [&cand_vars, &is_simple,
+                      &e](const PrimExpr& obj) -> 
ffi::Expected<ffi::WalkResult> {
         if (obj.same_as(e)) {
-          return;
+          return ffi::WalkResult::Advance();
         } else if (const VarNode* var = obj.as<VarNode>()) {
           PrimType var_ty = var->ty.as_or_throw<PrimType>();
           if (var_ty.MatchesCode(DLDataTypeCode::kDLInt, 
DLDataTypeCode::kDLUInt)) {
@@ -806,7 +810,9 @@ ffi::Optional<arith::IntConstraints> 
ConditionalBoundsContext::TrySolveCondition
                        obj->IsInstance<prim::MulNode>() || 
obj->IsInstance<prim::FloorDivNode>() ||
                        obj->IsInstance<prim::FloorModNode>() || 
obj->IsInstance<IntImmNode>();
         }
-      });
+        return ffi::WalkResult::Advance();
+      };
+      ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(e, walk_fn);
       if (is_simple && !cand_vars.empty()) {
         for (const PrimVar& new_var : cand_vars) {
           if (!std::any_of(vars.begin(), vars.end(),
diff --git a/tests/cpp/ir_functor_test.cc b/tests/cpp/ir_functor_test.cc
index c197b4931c..192f7f5abd 100644
--- a/tests/cpp/ir_functor_test.cc
+++ b/tests/cpp/ir_functor_test.cc
@@ -34,6 +34,7 @@
 #include <tvm/tirx/stmt_functor.h>
 
 #include <initializer_list>
+#include <unordered_set>
 
 TEST(IRF, Basic) {
   using namespace tvm;
@@ -55,13 +56,18 @@ TEST(IRF, CountVar) {
   PrimVar x("x"), y("y");
 
   auto z = x + 1 + y + y;
-  tirx::PostOrderVisit(z, [&n_var](const ffi::ObjectRef& n) {
-    if (n.as<VarNode>()) ++n_var;
-  });
+  std::unordered_set<const ffi::Object*> visited;
+  auto walk_fn = [&n_var, &visited](const Var& var) -> 
ffi::Expected<ffi::WalkResult> {
+    if (visited.insert(var.get()).second) {
+      ++n_var;
+    }
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(z, walk_fn);
   TVM_FFI_ICHECK_EQ(n_var, 2);
 }
 
-TEST(IRF, PreOrderVisit) {
+TEST(IRF, PreOrderStructuralWalk) {
   using namespace tvm;
   using namespace tvm::tirx;
   Stmt init =
@@ -73,24 +79,23 @@ TEST(IRF, PreOrderVisit) {
   bool init_visited = false;
   bool stopped_at_if = true;
   bool body_visited = false;
-  PreOrderVisit(block, [&](const ffi::ObjectRef& n) -> bool {
-    if (n->IsInstance<IfThenElseNode>()) {
-      init_visited = true;
-      return false;
-    }
-    if (const auto* eval = n.as<EvaluateNode>()) {
-      if (const auto* int_imm = eval->value.as<IntImmNode>()) {
-        if (int_imm->value == 0) {
-          stopped_at_if = false;
-        } else if (int_imm->value == 1) {
-          body_visited = true;
-        } else {
-          TVM_FFI_THROW(InternalError) << "Unreachable";
-        }
+  auto visit_if = [&](const IfThenElse&) -> ffi::Expected<ffi::WalkResult> {
+    init_visited = true;
+    return ffi::WalkResult::Skip();
+  };
+  auto visit_evaluate = [&](const Evaluate& eval) -> 
ffi::Expected<ffi::WalkResult> {
+    if (const auto* int_imm = eval->value.as<IntImmNode>()) {
+      if (int_imm->value == 0) {
+        stopped_at_if = false;
+      } else if (int_imm->value == 1) {
+        body_visited = true;
+      } else {
+        TVM_FFI_THROW(InternalError) << "Unreachable";
       }
     }
-    return true;
-  });
+    return ffi::WalkResult::Advance();
+  };
+  ffi::StructuralWalk<ffi::WalkOrder::kPreOrder>(block, visit_if, 
visit_evaluate);
   ASSERT_EQ(init_visited, true);
   ASSERT_EQ(stopped_at_if, true);
   ASSERT_EQ(body_visited, true);
@@ -366,7 +371,8 @@ TEST(IRF, StructuralMapSplicesMappedSeqStmtChild) {
   {
     Stmt input = make_input();
     Stmt shared = input;
-    Stmt mapped = ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(input, 
expand_one).cast<Stmt>();
+    Stmt mapped =
+        ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(input, 
expand_one).as_or_throw<Stmt>();
     EXPECT_FALSE(mapped.same_as(input));
     EXPECT_EQ(shared.as<SeqStmtNode>()->seq.size(), 3);
     check_values(mapped, {5, 2, 3, 4});
@@ -376,8 +382,8 @@ TEST(IRF, StructuralMapSplicesMappedSeqStmtChild) {
     Stmt input = make_input();
     const auto* original = input.get();
     const auto* original_array = input.as<SeqStmtNode>()->seq.GetArrayObj();
-    Stmt mapped =
-        ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(input), 
expand_one).cast<Stmt>();
+    Stmt mapped = 
ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(input), expand_one)
+                      .as_or_throw<Stmt>();
     EXPECT_EQ(mapped.get(), original);
     EXPECT_NE(mapped.as<SeqStmtNode>()->seq.GetArrayObj(), original_array);
     check_values(mapped, {5, 2, 3, 4});
@@ -391,14 +397,14 @@ TEST(IRF, StructuralMapSplicesMappedSeqStmtChild) {
                                 bool expect_array_reuse) {
     Stmt ordinary_input = make_boundary_input();
     Stmt ordinary = 
ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(ordinary_input, transform)
-                        .template cast<Stmt>();
+                        .template as_or_throw<Stmt>();
 
     Stmt inplace_input = make_boundary_input();
     const auto* original_root = inplace_input.get();
     const auto* original_array = 
inplace_input.as<SeqStmtNode>()->seq.GetArrayObj();
     Stmt inplace =
         
ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(inplace_input), 
transform)
-            .template cast<Stmt>();
+            .template as_or_throw<Stmt>();
 
     EXPECT_EQ(inplace.get(), original_root);
     if (expect_array_reuse) {
@@ -477,12 +483,12 @@ TEST(IRF, StructuralMapSplicesMappedSeqStmtChild) {
 
   auto remove_all = [](const Evaluate&) -> Stmt { return Evaluate(0); };
   Stmt ordinary_input = make_boundary_input();
-  Stmt ordinary =
-      ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(ordinary_input, 
remove_all).cast<Stmt>();
+  Stmt ordinary = 
ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(ordinary_input, remove_all)
+                      .as_or_throw<Stmt>();
   Stmt inplace_input = make_boundary_input();
   Stmt inplace =
       ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(inplace_input), 
remove_all)
-          .cast<Stmt>();
+          .as_or_throw<Stmt>();
   EXPECT_TRUE(ffi::StructuralEqual()(ordinary, inplace));
   for (const Stmt& result : {ordinary, inplace}) {
     const auto* evaluate = result.as<EvaluateNode>();
@@ -497,10 +503,11 @@ TEST(IRF, StructuralMapSplicesMappedSeqStmtChild) {
     return value != nullptr && value->value == 4 ? Stmt(evaluate) : 
Stmt(Evaluate(0));
   };
   ordinary_input = make_boundary_input();
-  ordinary = ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(ordinary_input, 
keep_last).cast<Stmt>();
+  ordinary =
+      ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(ordinary_input, 
keep_last).as_or_throw<Stmt>();
   inplace_input = make_boundary_input();
   inplace = 
ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(inplace_input), 
keep_last)
-                .cast<Stmt>();
+                .as_or_throw<Stmt>();
   EXPECT_TRUE(ffi::StructuralEqual()(ordinary, inplace));
   for (const Stmt& result : {ordinary, inplace}) {
     const auto* evaluate = result.as<EvaluateNode>();
@@ -523,8 +530,8 @@ TEST(IRF, StructuralMapPreservesSeqStmtElementUniqueness) {
     Stmt input = SeqStmt({Evaluate(IntImm::Int32(1)), 
Evaluate(IntImm::Int32(3))});
     const auto* original_root = input.get();
     const auto* original_first = 
input.as<SeqStmtNode>()->seq[0].as<EvaluateNode>();
-    Stmt mapped =
-        ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(input), 
replace_one).cast<Stmt>();
+    Stmt mapped = 
ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(input), replace_one)
+                      .as_or_throw<Stmt>();
 
     const auto* mapped_seq = mapped.as<SeqStmtNode>();
     ASSERT_NE(mapped_seq, nullptr);
@@ -537,8 +544,8 @@ TEST(IRF, StructuralMapPreservesSeqStmtElementUniqueness) {
     ffi::Array<Stmt> shared_seq = {Evaluate(IntImm::Int32(1)), 
Evaluate(IntImm::Int32(3))};
     const auto* shared_first = shared_seq[0].as<EvaluateNode>();
     Stmt input = SeqStmt(shared_seq);
-    Stmt mapped =
-        ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(input), 
replace_one).cast<Stmt>();
+    Stmt mapped = 
ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(input), replace_one)
+                      .as_or_throw<Stmt>();
 
     const auto* mapped_seq = mapped.as<SeqStmtNode>();
     ASSERT_NE(mapped_seq, nullptr);
@@ -552,7 +559,7 @@ TEST(IRF, StructuralMapPreservesSeqStmtElementUniqueness) {
     auto no_float_match = [](const FloatImm& value) -> PrimExpr { return 
value; };
     Stmt unchanged =
         
ffi::StructuralMap<ffi::WalkOrder::kPostOrder>(std::move(unchanged_input), 
no_float_match)
-            .cast<Stmt>();
+            .as_or_throw<Stmt>();
     EXPECT_EQ(unchanged.get(), unchanged_root);
     EXPECT_TRUE(unchanged.as<SeqStmtNode>()->seq.same_as(shared_seq));
   }
@@ -577,14 +584,13 @@ TEST(IRF, StructuralHooksPreserveScopeIdDefRegions) {
     int binder = -1;
     int extent = -1;
     int preferred = -1;
-    ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(
-        make_input(),
-        [&](const Var& var, TVMFFIDefRegionKind kind) -> 
ffi::Expected<ffi::WalkResult> {
-          if (var->name == "binder") binder = kind;
-          if (var->name == "extent") extent = kind;
-          if (var->name == "preferred") preferred = kind;
-          return ffi::WalkResult::Advance();
-        });
+    auto walk_fn = [&](const Var& var, TVMFFIDefRegionKind kind) -> 
ffi::Expected<ffi::WalkResult> {
+      if (var->name == "binder") binder = kind;
+      if (var->name == "extent") extent = kind;
+      if (var->name == "preferred") preferred = kind;
+      return ffi::WalkResult::Advance();
+    };
+    ffi::StructuralWalk<ffi::WalkOrder::kPostOrder>(make_input(), walk_fn);
     check_kinds(binder, extent, preferred);
   }
 
diff --git a/tests/python/relax/test_backend_dispatch_sort_scan.py 
b/tests/python/relax/test_backend_dispatch_sort_scan.py
index 7fcd264917..df4aca033a 100644
--- a/tests/python/relax/test_backend_dispatch_sort_scan.py
+++ b/tests/python/relax/test_backend_dispatch_sort_scan.py
@@ -18,6 +18,7 @@
 
 import numpy as np
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.script
@@ -542,7 +543,7 @@ def test_dispatch_cumsum_webgpu_axes_and_dtypes(
             if isinstance(node, tirx.FloorDiv):
                 floor_divisors.append(node.b)
 
-        tirx.stmt_functor.post_order_visit(cumsum.body, collect_floor_divisors)
+        tvm_ffi.structural_walk(cumsum.body, collect_floor_divisors)
         assert floor_divisors
         assert all(
             isinstance(divisor, tirx.IntImm)
diff --git a/tests/python/relax/test_pipeline.py 
b/tests/python/relax/test_pipeline.py
index ae7a889940..dd8a15f819 100644
--- a/tests/python/relax/test_pipeline.py
+++ b/tests/python/relax/test_pipeline.py
@@ -176,7 +176,7 @@ def _has_thread_binding(func: tvm.tirx.PrimFunc) -> bool:
         if isinstance(node, tvm.tirx.For) and node.kind == 
tvm.tirx.ForKind.THREAD_BINDING:
             found = True
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, _visit)
+    tvm_ffi.structural_walk(func.body, _visit)
     return found
 
 
diff --git a/tests/python/s_tir/base/test_sblock_dependence_info.py 
b/tests/python/s_tir/base/test_sblock_dependence_info.py
index eb6ee6841d..6d20e16d49 100644
--- a/tests/python/s_tir/base/test_sblock_dependence_info.py
+++ b/tests/python/s_tir/base/test_sblock_dependence_info.py
@@ -20,6 +20,7 @@ import gc
 import sys
 
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -29,7 +30,6 @@ from tvm.s_tir import SBlockDependenceInfo
 from tvm.s_tir.sblock_scope import DepKind
 from tvm.script import tirx as T
 from tvm.tirx import PrimFunc
-from tvm.tirx.stmt_functor import post_order_visit
 
 # pylint: disable=no-member,invalid-name,unused-variable
 
@@ -94,7 +94,7 @@ def get_sblocks(func: PrimFunc):
             blocks[node.name_hint] = node
 
     # post_order_visit(func.body, lambda node: blocks[node.name_hint] = node 
if isinstance(node, tvm.tirx.SBlock) else None)
-    post_order_visit(func.body, update_blocks)
+    tvm_ffi.structural_walk(func.body, update_blocks)
     return blocks
 
 
diff --git a/tests/python/s_tir/schedule/test_tir_schedule_analysis.py 
b/tests/python/s_tir/schedule/test_tir_schedule_analysis.py
index 48e7097b1b..3fb208e1af 100644
--- a/tests/python/s_tir/schedule/test_tir_schedule_analysis.py
+++ b/tests/python/s_tir/schedule/test_tir_schedule_analysis.py
@@ -18,6 +18,7 @@
 # ruff: noqa: F401, F841
 
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -49,7 +50,6 @@ from tvm.tirx import (
 )
 from tvm.tirx.analysis import expr_deep_equal
 from tvm.tirx.function import TensorIntrin
-from tvm.tirx.stmt_functor import pre_order_visit
 
 
 def _make_vars(*args: str) -> list[Var]:
@@ -227,9 +227,9 @@ def collect_loops(prim_func):
     def callback(node):
         if isinstance(node, tvm.tirx.For):
             loops.append(node)
-        return True
+        return tvm_ffi.WalkResult.ADVANCE
 
-    pre_order_visit(prim_func.body, callback)
+    tvm_ffi.structural_walk(prim_func.body, (object, callback), order="pre")
 
     return loops
 
diff --git a/tests/python/s_tir/schedule/test_tir_schedule_block_scope.py 
b/tests/python/s_tir/schedule/test_tir_schedule_block_scope.py
index f98b45c4ec..f18e49cc48 100644
--- a/tests/python/s_tir/schedule/test_tir_schedule_block_scope.py
+++ b/tests/python/s_tir/schedule/test_tir_schedule_block_scope.py
@@ -19,13 +19,13 @@
 import sys
 
 import pytest
+from tvm_ffi import structural_walk as post_order_visit
 
 import tvm
 import tvm.testing
 from tvm import s_tir, tirx
 from tvm.s_tir.schedule import DepKind
 from tvm.script import tirx as T
-from tvm.tirx.stmt_functor import post_order_visit
 
 # pylint: disable=no-member,invalid-name,unused-variable
 
diff --git 
a/tests/python/s_tir/schedule/test_tir_schedule_state_cached_flags.py 
b/tests/python/s_tir/schedule/test_tir_schedule_state_cached_flags.py
index 02f56a156b..cccc4fd38d 100644
--- a/tests/python/s_tir/schedule/test_tir_schedule_state_cached_flags.py
+++ b/tests/python/s_tir/schedule/test_tir_schedule_state_cached_flags.py
@@ -19,13 +19,13 @@
 import sys
 
 import pytest
+from tvm_ffi import structural_walk as post_order_visit
 
 import tvm
 import tvm.testing
 from tvm import s_tir, tirx
 from tvm.s_tir.schedule.state import CachedFlags
 from tvm.script import tirx as T
-from tvm.tirx.stmt_functor import post_order_visit
 
 # pylint: disable=no-member,invalid-name,unused-variable,unexpected-keyword-arg
 # fmt: off
diff --git a/tests/python/s_tir/schedule/test_tir_schedule_transform_layout.py 
b/tests/python/s_tir/schedule/test_tir_schedule_transform_layout.py
index 4252f88f72..9cd1907cff 100644
--- a/tests/python/s_tir/schedule/test_tir_schedule_transform_layout.py
+++ b/tests/python/s_tir/schedule/test_tir_schedule_transform_layout.py
@@ -19,6 +19,7 @@
 import sys
 
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -404,6 +405,30 @@ def 
test_transform_block_layout_fail_mixed_iter_type(use_block_name):
         )
 
 
+def test_mixed_iter_type_detection_interrupts_walk():
+    spatial = tirx.Var("spatial", "int32")
+    reduction = tirx.Var("reduction", "int32")
+    unreachable = tirx.Var("unreachable", "int32")
+    visited = []
+
+    def detect(var):
+        visited.append(var)
+        if var.same_as(reduction):
+            return tvm_ffi.VisitInterrupt()
+        return None
+
+    result = tvm_ffi.structural_walk(
+        (spatial + reduction) + unreachable,
+        (tirx.Var, detect),
+        order="post",
+    )
+
+    assert isinstance(result, tvm_ffi.VisitInterrupt)
+    assert any(var.same_as(spatial) for var in visited)
+    assert any(var.same_as(reduction) for var in visited)
+    assert not any(var.same_as(unreachable) for var in visited)
+
+
 def test_transform_block_layout_int64_extent(use_block_name):
     @T.prim_func(s_tir=True)
     def elementwise_int64_extent(
diff --git a/tests/python/s_tir/transform/test_s_tir_transform_hoist_if.py 
b/tests/python/s_tir/transform/test_s_tir_transform_hoist_if.py
index d59aedb4d2..8915aeebc8 100644
--- a/tests/python/s_tir/transform/test_s_tir_transform_hoist_if.py
+++ b/tests/python/s_tir/transform/test_s_tir_transform_hoist_if.py
@@ -41,7 +41,7 @@ def verify_structure(stmt, expected_struct):
         key = op
         if isinstance(op, tvm.tirx.IfThenElse):
             global var_list
-            tvm.tirx.stmt_functor.post_order_visit(op.condition, _extract_vars)
+            tvm_ffi.structural_walk(op.condition, _extract_vars)
             val = [(op.then_case, op.else_case), ("tirx.IfThenElse", 
tuple(var_list))]
             var_list.clear()
         elif isinstance(op, tvm.tirx.For):
@@ -52,7 +52,7 @@ def verify_structure(stmt, expected_struct):
             return
         node_dict[key] = val
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt, _visit)
+    tvm_ffi.structural_walk(stmt, _visit)
     for key, val in node_dict.items():
         struct[val[1]] = tuple(
             node_dict[child][1] if child in node_dict else None for child in 
val[0]
diff --git 
a/tests/python/s_tir/transform/test_s_tir_transform_inject_double_buffer.py 
b/tests/python/s_tir/transform/test_s_tir_transform_inject_double_buffer.py
index 5a761178b3..d8824ca6fc 100644
--- a/tests/python/s_tir/transform/test_s_tir_transform_inject_double_buffer.py
+++ b/tests/python/s_tir/transform/test_s_tir_transform_inject_double_buffer.py
@@ -16,6 +16,8 @@
 # under the License.
 # ruff: noqa: F841
 
+import tvm_ffi
+
 import tvm
 import tvm.testing
 from tvm.script import ir as I
@@ -59,7 +61,7 @@ def test_double_buffer():
         if isinstance(op, tvm.tirx.AllocBuffer) and "B" in str(op.buffer.data):
             allocate_node = op
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt, visitor)
+    tvm_ffi.structural_walk(stmt, visitor)
     assert allocate_node is not None
     assert list(allocate_node.buffer.shape) == [m * 2]
 
@@ -70,7 +72,7 @@ def test_double_buffer():
         if isinstance(op, tvm.ir.Call) and 
op.op.same_as(tvm.ir.Op.get("tirx.tvm_storage_sync")):
             count[0] += 1
 
-    tvm.tirx.stmt_functor.post_order_visit(f.body, count_sync)
+    tvm_ffi.structural_walk(f.body, count_sync)
     assert count[0] == 4
 
 
@@ -107,7 +109,7 @@ def test_double_buffer_transform():
         if isinstance(op, tvm.tirx.AllocBuffer):
             allocate_node = op
 
-    tvm.tirx.stmt_functor.post_order_visit(After["main"].body, visitor)
+    tvm_ffi.structural_walk(After["main"].body, visitor)
     assert allocate_node is not None
     assert list(allocate_node.buffer.shape) == [64]
 
diff --git 
a/tests/python/s_tir/transform/test_s_tir_transform_inject_ptx_async_copy.py 
b/tests/python/s_tir/transform/test_s_tir_transform_inject_ptx_async_copy.py
index a53f3c439c..922bcb992f 100644
--- a/tests/python/s_tir/transform/test_s_tir_transform_inject_ptx_async_copy.py
+++ b/tests/python/s_tir/transform/test_s_tir_transform_inject_ptx_async_copy.py
@@ -50,7 +50,7 @@ def count_cp_async(stmt):
         if isinstance(n, tvm.ir.Call) and n.op.name == 
"tirx.s_tir.cp_async_raw":
             num_alloc[0] += 1
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt, verify)
+    tvm_ffi.structural_walk(stmt, verify)
     return num_alloc[0]
 
 
diff --git 
a/tests/python/s_tir/transform/test_s_tir_transform_inject_ptx_ldg32.py 
b/tests/python/s_tir/transform/test_s_tir_transform_inject_ptx_ldg32.py
index 67e568a248..2c19027941 100644
--- a/tests/python/s_tir/transform/test_s_tir_transform_inject_ptx_ldg32.py
+++ b/tests/python/s_tir/transform/test_s_tir_transform_inject_ptx_ldg32.py
@@ -16,6 +16,8 @@
 # under the License.
 # ruff: noqa: F401
 
+import tvm_ffi
+
 import tvm
 import tvm.testing
 from tvm import s_tir
@@ -29,7 +31,7 @@ def _count_alloc(stmt):
         if isinstance(n, tvm.tirx.AllocBuffer):
             num_alloc[0] += 1
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt, visit)
+    tvm_ffi.structural_walk(stmt, visit)
     return num_alloc[0]
 
 
@@ -40,7 +42,7 @@ def _count_ptx_ldg32(stmt):
         if isinstance(n, tvm.ir.Call) and n.op.name == "tirx.s_tir.ldg32":
             num_call[0] += 1
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt, visit)
+    tvm_ffi.structural_walk(stmt, visit)
     return num_call[0]
 
 
diff --git 
a/tests/python/s_tir/transform/test_s_tir_transform_inject_virtual_thread.py 
b/tests/python/s_tir/transform/test_s_tir_transform_inject_virtual_thread.py
index 471974f16c..315ec04de6 100644
--- a/tests/python/s_tir/transform/test_s_tir_transform_inject_virtual_thread.py
+++ b/tests/python/s_tir/transform/test_s_tir_transform_inject_virtual_thread.py
@@ -15,6 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 # ruff: noqa: F841
+import tvm_ffi
+
 import tvm
 import tvm.testing
 from tvm.script import ir as I
@@ -60,7 +62,7 @@ def test_vthread():
         if isinstance(node, tvm.tirx.AllocBuffer):
             allocates.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt.body, find_allocates)
+    tvm_ffi.structural_walk(stmt.body, find_allocates)
     assert len(allocates) == 1
     assert list(allocates[0].buffer.ty.shape) == [B_expected_alloc]
 
@@ -109,7 +111,7 @@ def test_vthread_extern():
         if isinstance(node, tvm.tirx.AllocBuffer):
             allocates.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt.body, find_allocates)
+    tvm_ffi.structural_walk(stmt.body, find_allocates)
     assert len(allocates) == 3
     # Check that we have the expected extents (order may vary)
     extents = sorted([int(a.buffer.ty.shape[0]) for a in allocates])
@@ -145,7 +147,7 @@ def test_vthread_if_then_else():
         if isinstance(node, tvm.tirx.IfThenElse):
             if_nodes.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt.body, find_ifs)
+    tvm_ffi.structural_walk(stmt.body, find_ifs)
 
     assert len(if_nodes) == 2
     # First if has else_case, second does not
@@ -207,7 +209,7 @@ def test_vthread_vectorized():
         if isinstance(op, tvm.tirx.AllocBuffer) and "shared" in 
str(op.buffer.data.ty):
             allocate_node = op
 
-    tvm.tirx.stmt_functor.post_order_visit(after_func.body, visitor)
+    tvm_ffi.structural_walk(after_func.body, visitor)
     assert allocate_node is not None
     assert list(allocate_node.buffer.ty.shape) == [4]
     assert allocate_node.buffer.ty.dtype == "int32x4"
@@ -236,7 +238,7 @@ def test_vthread_rewrites_masked_accesses():
         }:
             masked_calls.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(after.body, visitor)
+    tvm_ffi.structural_walk(after.body, visitor)
     assert len(masked_calls) == 4
     assert all(list(call.args[0].ty.shape) == [8] for call in masked_calls)
     analyzer = tvm.arith.Analyzer()
diff --git 
a/tests/python/s_tir/transform/test_s_tir_transform_loop_partition.py 
b/tests/python/s_tir/transform/test_s_tir_transform_loop_partition.py
index aa111bed1d..be1cfe5f90 100644
--- a/tests/python/s_tir/transform/test_s_tir_transform_loop_partition.py
+++ b/tests/python/s_tir/transform/test_s_tir_transform_loop_partition.py
@@ -17,6 +17,7 @@
 # ruff: noqa: F401
 import numpy
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -26,7 +27,7 @@ from tvm.script import tirx as T
 
 def collect_visit(stmt, f):
     ret = []
-    tvm.tirx.stmt_functor.post_order_visit(stmt, lambda x: ret.append(f(x)))
+    tvm_ffi.structural_walk(stmt, lambda x: ret.append(f(x)))
     return ret
 
 
diff --git 
a/tests/python/s_tir/transform/test_s_tir_transform_lower_thread_all_reduce.py 
b/tests/python/s_tir/transform/test_s_tir_transform_lower_thread_all_reduce.py
index 5c08e66a95..17253785e1 100644
--- 
a/tests/python/s_tir/transform/test_s_tir_transform_lower_thread_all_reduce.py
+++ 
b/tests/python/s_tir/transform/test_s_tir_transform_lower_thread_all_reduce.py
@@ -16,6 +16,8 @@
 # under the License.
 # ruff: noqa: F401, F841
 
+import tvm_ffi
+
 import tvm
 import tvm.testing
 from tvm import s_tir
@@ -31,7 +33,7 @@ def _has_volatile_alloc_buffer(mod):
         if isinstance(node, tvm.tirx.AllocBuffer) and "tirx.volatile" in 
node.annotations:
             has_volatile_alloc = has_volatile_alloc or 
node.annotations["tirx.volatile"] is True
 
-    tvm.tirx.stmt_functor.post_order_visit(mod["main"].body, visit)
+    tvm_ffi.structural_walk(mod["main"].body, visit)
     return has_volatile_alloc
 
 
diff --git 
a/tests/python/s_tir/transform/test_s_tir_transform_memhammer_lower_auto_copy.py
 
b/tests/python/s_tir/transform/test_s_tir_transform_memhammer_lower_auto_copy.py
index 89a66cd4cc..5d050149f8 100644
--- 
a/tests/python/s_tir/transform/test_s_tir_transform_memhammer_lower_auto_copy.py
+++ 
b/tests/python/s_tir/transform/test_s_tir_transform_memhammer_lower_auto_copy.py
@@ -19,6 +19,7 @@
 import sys
 
 import pytest
+import tvm_ffi
 
 import tvm
 from tvm import s_tir
@@ -1146,7 +1147,7 @@ def verify_single_allocation(stmt, alloc_size=None):
             for buf in n.alloc_buffers:
                 alloc_extents.append(buf.shape)
 
-    tvm.tirx.stmt_functor.post_order_visit(stmt, verify)
+    tvm_ffi.structural_walk(stmt, verify)
     assert num_alloc[0] == 1
 
     if alloc_size:
diff --git a/tests/python/tirx-base/test_tir_base.py 
b/tests/python/tirx-base/test_tir_base.py
index dbf1eb623f..c82aad76bc 100644
--- a/tests/python/tirx-base/test_tir_base.py
+++ b/tests/python/tirx-base/test_tir_base.py
@@ -134,7 +134,7 @@ def test_return_stmt_functor_traversal_and_mutation():
     stmt = tirx.Return(x + 1, span)
     visited = []
 
-    tirx.stmt_functor.post_order_visit(stmt, visited.append)
+    tvm_ffi.structural_walk(stmt, visited.append)
     assert any(node.same_as(x) for node in visited)
     assert any(isinstance(node, tirx.Return) for node in visited)
 
diff --git a/tests/python/tirx-transform/test_tir_transform_bf16_legalize.py 
b/tests/python/tirx-transform/test_tir_transform_bf16_legalize.py
index ce14378598..0a23e75b2e 100644
--- a/tests/python/tirx-transform/test_tir_transform_bf16_legalize.py
+++ b/tests/python/tirx-transform/test_tir_transform_bf16_legalize.py
@@ -14,6 +14,8 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+import tvm_ffi
+
 import tvm
 import tvm.script
 from tvm.script import tirx as T
@@ -147,7 +149,16 @@ def test_bf16_masked_load_store_will_legalize():
 
     def collect(mod):
         nodes = []
-        tvm.tirx.stmt_functor.post_order_visit(mod["main"].body, nodes.append)
+        seen = []
+
+        def append_once(node):
+            if isinstance(node, tvm_ffi.Object) and not any(
+                node.same_as(previous) for previous in seen
+            ):
+                seen.append(node)
+                nodes.append(node)
+
+        tvm_ffi.structural_walk(mod["main"].body, append_once)
         buffers = {
             node.buffer.name: str(node.buffer.dtype)
             for node in nodes
diff --git a/tests/python/tirx-transform/test_tir_transform_lower_intrin.py 
b/tests/python/tirx-transform/test_tir_transform_lower_intrin.py
index 052e2522f1..86d986250e 100644
--- a/tests/python/tirx-transform/test_tir_transform_lower_intrin.py
+++ b/tests/python/tirx-transform/test_tir_transform_lower_intrin.py
@@ -17,6 +17,7 @@
 # ruff: noqa: RUF005
 import numpy as np
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -106,7 +107,7 @@ def test_lower_nested_access_ptr():
             elif node.op.name == "tirx.address_of":
                 address_calls.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(lowered.body, collect)
+    tvm_ffi.structural_walk(lowered.body, collect)
     assert not access_ptr_calls
     assert len(address_calls) == 1
     load = address_calls[0].args[0]
diff --git a/tests/python/tirx-transform/test_tir_transform_make_packed_api.py 
b/tests/python/tirx-transform/test_tir_transform_make_packed_api.py
index 81d46061ff..e48c5aac78 100644
--- a/tests/python/tirx-transform/test_tir_transform_make_packed_api.py
+++ b/tests/python/tirx-transform/test_tir_transform_make_packed_api.py
@@ -21,6 +21,7 @@ Runtime error tests are in 
tests/python/codegen/test_codegen_error_handling.py.
 """
 
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -37,7 +38,7 @@ def _find_compute_scope(func):
             nonlocal result
             result = stmt
 
-    tirx.stmt_functor.post_order_visit(func.body, _visitor)
+    tvm_ffi.structural_walk(func.body, _visitor)
 
     return result
 
@@ -215,7 +216,7 @@ def test_pointer_return():
         if isinstance(field, tvm.tirx.IntImm) and int(field) == 13:
             return_type_indices.append(int(node.args[3]))
 
-    tvm.tirx.stmt_functor.post_order_visit(after.body, collect)
+    tvm_ffi.structural_walk(after.body, collect)
     assert 4 in return_type_indices  # ffi::TypeIndex::kTVMFFIOpaquePtr
 
 
@@ -482,7 +483,7 @@ def test_buffer_alignment_attached_to_buffer_var():
         if isinstance(node, tirx.DeclBuffer):
             declared_buffers.append(node.buffer)
 
-    tirx.stmt_functor.post_order_visit(after.body, collect)
+    tvm_ffi.structural_walk(after.body, collect)
     assert len(alignment_nodes) == 1
     assert any(alignment_nodes[0].same_as(buffer) for buffer in 
declared_buffers)
 
diff --git 
a/tests/python/tirx-transform/test_tir_transform_pointer_value_type_rewrite.py 
b/tests/python/tirx-transform/test_tir_transform_pointer_value_type_rewrite.py
index 89fd4011a6..e8a387cee3 100644
--- 
a/tests/python/tirx-transform/test_tir_transform_pointer_value_type_rewrite.py
+++ 
b/tests/python/tirx-transform/test_tir_transform_pointer_value_type_rewrite.py
@@ -16,6 +16,8 @@
 # under the License.
 # pylint: disable=invalid-name, missing-docstring
 
+import tvm_ffi
+
 import tvm
 import tvm.testing
 from tvm.script import ir as I
@@ -161,7 +163,7 @@ def test_decl_buffer_alias_chain_uses_flat_root_map():
 
     decl_buffers = []
     buffer_stores = []
-    tvm.tirx.stmt_functor.post_order_visit(
+    tvm_ffi.structural_walk(
         func.body,
         lambda node: (
             decl_buffers.append(node)
diff --git 
a/tests/python/tirx-transform/test_tir_transform_split_host_device.py 
b/tests/python/tirx-transform/test_tir_transform_split_host_device.py
index 2990efc725..ed5ce0f9a3 100644
--- a/tests/python/tirx-transform/test_tir_transform_split_host_device.py
+++ b/tests/python/tirx-transform/test_tir_transform_split_host_device.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -362,7 +363,7 @@ def test_buffer_used_only_through_data_projection():
         if isinstance(node, tvm.tirx.DeclBuffer):
             declared_buffers.append(node.buffer)
 
-    tvm.tirx.stmt_functor.post_order_visit(kernel.body, collect)
+    tvm_ffi.structural_walk(kernel.body, collect)
     assert len(declared_buffers) == 1
     assert not tvm.tirx.analysis.undefined_vars(kernel.body, kernel.params)
 
diff --git a/tests/python/tirx-transform/test_tir_transform_storage_rewrite.py 
b/tests/python/tirx-transform/test_tir_transform_storage_rewrite.py
index d5b271b468..b3133cba1e 100644
--- a/tests/python/tirx-transform/test_tir_transform_storage_rewrite.py
+++ b/tests/python/tirx-transform/test_tir_transform_storage_rewrite.py
@@ -18,6 +18,7 @@
 import sys
 
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -48,7 +49,7 @@ def test_alloc_seq():
             num_alloc[0] += 1
             assert n.buffer.ty.shape[0].value == 200
 
-    tvm.tirx.stmt_functor.post_order_visit(body, verify)
+    tvm_ffi.structural_walk(body, verify)
     assert num_alloc[0] == 1
 
 
@@ -106,7 +107,7 @@ def test_alloc_different_dtypes():
         offset = offset_generater(dtype_list, length)
 
         body = tvm.tirx.transform.StorageRewrite()(mod)["func"].body
-        tvm.tirx.stmt_functor.post_order_visit(body, verify)
+        tvm_ffi.structural_walk(body, verify)
 
     length = 1024
     dtype_list = ["float16", "int32", "uint16", "int8"]
@@ -161,12 +162,12 @@ def test_address_of():
 
     total_alloc = [0]
     mod = tvm.IRModule.from_expr(before.with_attr("global_symbol", "main"))
-    tvm.tirx.stmt_functor.post_order_visit(mod["main"].body, verify)
+    tvm_ffi.structural_walk(mod["main"].body, verify)
     assert total_alloc[0] == 24
 
     total_alloc[0] = 0
     mod = tvm.tirx.transform.StorageRewrite()(mod)
-    tvm.tirx.stmt_functor.post_order_visit(mod["main"].body, verify)
+    tvm_ffi.structural_walk(mod["main"].body, verify)
     assert total_alloc[0] == 16
 
 
@@ -244,13 +245,13 @@ def test_while_alloc():
         if isinstance(n, tvm.tirx.AllocBuffer):
             num_alloc[0] += 1
 
-    tvm.tirx.stmt_functor.post_order_visit(inner, count_alloc)
+    tvm_ffi.structural_walk(inner, count_alloc)
     assert num_alloc[0] == 2  # j and A allocations
 
     mod = tvm.IRModule.from_expr(func_serial)
     body = tvm.tirx.transform.StorageRewrite()(mod)["func_serial"]
     num_alloc[0] = 0
-    tvm.tirx.stmt_functor.post_order_visit(body.body, count_alloc)
+    tvm_ffi.structural_walk(body.body, count_alloc)
     assert num_alloc[0] == 2  # j and A allocations
 
 
@@ -282,7 +283,7 @@ def test_alloc_seq_type():
             num_alloc[0] += 1
             assert n.buffer.ty.shape[0].value == 500
 
-    tvm.tirx.stmt_functor.post_order_visit(body, verify)
+    tvm_ffi.structural_walk(body, verify)
     assert num_alloc[0] == 1
 
 
@@ -312,7 +313,7 @@ def test_alloc_seq_type2():
             num_alloc[0] += 1
             assert n.buffer.ty.shape[0].value == 200
 
-    tvm.tirx.stmt_functor.post_order_visit(body, verify)
+    tvm_ffi.structural_walk(body, verify)
     assert num_alloc[0] == 1
 
 
@@ -344,7 +345,7 @@ def test_reuse_small_buffer():
             num_alloc[0] += 1
             assert n.buffer.ty.shape[0].value == 800
 
-    tvm.tirx.stmt_functor.post_order_visit(body, verify)
+    tvm_ffi.structural_walk(body, verify)
     assert num_alloc[0] == 1
 
 
@@ -475,7 +476,7 @@ def test_decl_buffer_alias_extends_source_lifetime():
 
     after = 
tvm.tirx.transform.StorageRewrite()(tvm.IRModule.from_expr(func))["func"]
     allocations = []
-    tvm.tirx.stmt_functor.post_order_visit(
+    tvm_ffi.structural_walk(
         after.body,
         lambda node: allocations.append(node) if isinstance(node, 
tvm.tirx.AllocBuffer) else None,
     )
diff --git a/tests/python/tirx-transform/test_tir_transform_vectorize.py 
b/tests/python/tirx-transform/test_tir_transform_vectorize.py
index e7ac072cb7..e0940db94a 100644
--- a/tests/python/tirx-transform/test_tir_transform_vectorize.py
+++ b/tests/python/tirx-transform/test_tir_transform_vectorize.py
@@ -16,6 +16,7 @@
 # under the License.
 # ruff: noqa: F841
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -664,7 +665,7 @@ def test_vectorize_nested_predicates_preserve_both_masks():
         if isinstance(node, tvm.ir.Call) and node.op.name == 
"tirx.masked_store":
             predicates.append(node.args[-1])
 
-    tvm.tirx.stmt_functor.post_order_visit(after.body, collect_predicates)
+    tvm_ffi.structural_walk(after.body, collect_predicates)
     assert len(predicates) == 2
     assert any(
         isinstance(predicate, tvm.ir.Call) and predicate.op.name == 
"ir.prim.bitwise_and"
diff --git a/tests/python/tirx/codegen/test_codegen_blackwell.py 
b/tests/python/tirx/codegen/test_codegen_blackwell.py
index f02103a20b..26138ffd68 100644
--- a/tests/python/tirx/codegen/test_codegen_blackwell.py
+++ b/tests/python/tirx/codegen/test_codegen_blackwell.py
@@ -17,6 +17,7 @@
 # pylint: disable=missing-function-docstring
 import numpy as np
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -53,7 +54,7 @@ def _assert_remote_mbarrier_ir(func, arrive_op_name, 
n_arrives=1):
         if isinstance(node, tvm.ir.Call) and node.op.name == arrive_op_name:
             arrive_calls.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     assert len(bindings) == 1
     assert len(buffers) == 1
     assert len(mapa_calls) == 1
@@ -201,7 +202,7 @@ def 
test_mbarrier_local_arrive_forwards_predicate_and_count():
         if isinstance(node, tvm.ir.Call) and node.op.name == 
"tirx.ptx.mbarrier_arrive":
             arrive_calls.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(test_local_arrive.body, visit)
+    tvm_ffi.structural_walk(test_local_arrive.body, visit)
     assert len(arrive_calls) == 1
     call = arrive_calls[0]
     assert call.args[1].value == 2
diff --git a/tests/python/tirx/codegen/test_codegen_dsmem.py 
b/tests/python/tirx/codegen/test_codegen_dsmem.py
index d3ecd16a62..19481a5e3c 100644
--- a/tests/python/tirx/codegen/test_codegen_dsmem.py
+++ b/tests/python/tirx/codegen/test_codegen_dsmem.py
@@ -17,6 +17,8 @@
 # pylint: disable=missing-function-docstring
 """Tests for cp.async.bulk.shared::cluster.shared::cta PTX instruction 
codegen."""
 
+import tvm_ffi
+
 import tvm
 import tvm.testing
 from tvm.ir import PointerType, PrimType, assert_structural_equal
@@ -123,7 +125,7 @@ def test_mapa_pointer_bind_codegen():
         elif isinstance(node, tvm.ir.TensorLoad):
             loads.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(main.body, collect)
+    tvm_ffi.structural_walk(main.body, collect)
     assert len(binds) == 1
     assert isinstance(binds[0].var.ty, PointerType)
     assert binds[0].var.ty.storage_scope == "shared"
diff --git a/tests/python/tirx/codegen/test_ptx_addr.py 
b/tests/python/tirx/codegen/test_ptx_addr.py
index e333f9bb61..b26750370d 100644
--- a/tests/python/tirx/codegen/test_ptx_addr.py
+++ b/tests/python/tirx/codegen/test_ptx_addr.py
@@ -17,6 +17,7 @@
 """Tests for ``T.ptx.addr(base, byte_offset)``."""
 
 import pytest
+import tvm_ffi
 
 import tvm
 from tvm.ir import Call, Op
@@ -40,7 +41,7 @@ def _calls(func, op_name):
         if isinstance(node, Call) and getattr(node.op, "name", None) == 
op_name:
             calls.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     return calls
 
 
diff --git a/tests/python/tirx/test_hint.py b/tests/python/tirx/test_hint.py
index 1b3ca56487..6186c471e3 100644
--- a/tests/python/tirx/test_hint.py
+++ b/tests/python/tirx/test_hint.py
@@ -16,6 +16,8 @@
 # under the License.
 """Tests for T.hint() — universal directive primitive for TIRx sketch 
language."""
 
+import tvm_ffi
+
 import tvm
 import tvm.script
 import tvm.testing
@@ -50,7 +52,7 @@ def test_hint_statement():
             assert str(stmt.node["message"]) == "persistent tile scheduler 
with L2 swizzle"
             found[0] = True
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     assert found[0], "Expected AttrStmt with attr_key='tirx_hint' not found"
 
 
@@ -74,7 +76,7 @@ def test_hint_context_manager():
             assert str(stmt.node["message"]) == "software pipeline, depth 4"
             found[0] = True
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     assert found[0], "Expected AttrStmt with attr_key='tirx_hint' not found"
 
 
@@ -100,7 +102,7 @@ def test_hint_with_attrs():
             assert str(stmt.node["depth"]) == "4"
             found[0] = True
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     assert found[0], "Expected AttrStmt with attr_key='tirx_hint' not found"
 
 
@@ -220,7 +222,7 @@ def test_hint_no_message():
             assert isinstance(stmt.node["access"], BufferRegion)
             found[0] = True
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     assert found[0], "Expected AttrStmt with attr_key='tirx_hint' containing 
access not found"
 
 
@@ -251,7 +253,7 @@ def test_hint_access_buffer_region():
             assert len(br.region) == 2
             found[0] = True
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     assert found[0], "Expected AttrStmt with structured BufferRegion access 
not found"
 
 
diff --git a/tests/python/tirx/test_op_namespace_cleanup.py 
b/tests/python/tirx/test_op_namespace_cleanup.py
index 09ad39d4a6..78f56aa3d9 100644
--- a/tests/python/tirx/test_op_namespace_cleanup.py
+++ b/tests/python/tirx/test_op_namespace_cleanup.py
@@ -21,6 +21,7 @@ import sys
 import types
 
 import pytest
+import tvm_ffi
 
 import tvm
 from tvm.ir import Op, assert_structural_equal
@@ -36,7 +37,7 @@ def _tile_calls(func):
         if isinstance(stmt, TilePrimitiveCall):
             calls.append(stmt)
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     return calls
 
 
@@ -47,7 +48,7 @@ def _expr_calls(func):
         if isinstance(node, tvm.ir.Call):
             calls.append(node)
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     return calls
 
 
diff --git a/tests/python/tirx/test_parser_printer.py 
b/tests/python/tirx/test_parser_printer.py
index c6f6d5c21e..45ebe11b13 100644
--- a/tests/python/tirx/test_parser_printer.py
+++ b/tests/python/tirx/test_parser_printer.py
@@ -17,6 +17,7 @@
 import math
 
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.script
@@ -1019,6 +1020,16 @@ def test_buffer():
     assert_structural_equal(test, from_source(code))
 
 
+def test_buffer_shape_repeated_var_prints_out_of_line():
+    n = tvm.tirx.Var("n", "int32")
+    buffer = tvm.tirx.decl_buffer((n + n,), name="A")
+    func = tvm.tirx.PrimFunc([buffer], tvm.tirx.Evaluate(0))
+
+    code = func.script()
+    assert "n = T.int32()" in code
+    assert_structural_equal(func, from_source(code))
+
+
 def test_kwargs_op_call():
     # fmt: off
     @T.prim_func(private=True)
@@ -1254,7 +1265,7 @@ def test_tuple_let_binding_and_traversal():
 
     def tuple_value(func):
         visited = []
-        tvm.tirx.stmt_functor.post_order_visit(func.body, visited.append)
+        tvm_ffi.structural_walk(func.body, visited.append)
         bind = next(node for node in visited if isinstance(node, 
tvm.tirx.Bind))
         return bind.value
 
@@ -1366,7 +1377,7 @@ def _collect_buffers(func):
         if isinstance(node, tvm.tirx.DeclBuffer | tvm.tirx.AllocBuffer):
             bufs[node.buffer.name] = node.buffer
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, _visit)
+    tvm_ffi.structural_walk(func.body, _visit)
     return bufs
 
 
@@ -1378,7 +1389,7 @@ def _collect_buffer_sources(func):
         if isinstance(node, tvm.tirx.DeclBuffer):
             sources[node.buffer.name] = node.data
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, _visit)
+    tvm_ffi.structural_walk(func.body, _visit)
     return sources
 
 
@@ -1698,7 +1709,7 @@ def test_pointer_expression_assignment_uses_bind():
     # fmt: on
 
     binds = []
-    tvm.tirx.stmt_functor.post_order_visit(
+    tvm_ffi.structural_walk(
         func.body, lambda node: binds.append(node) if isinstance(node, 
tvm.tirx.Bind) else None
     )
     assert len(binds) == 1
@@ -1735,7 +1746,7 @@ def func() -> None:
     func = tvm.script.from_source(source, extra_vars={"T": T, "ptr": object()})
 
     binds = []
-    tvm.tirx.stmt_functor.post_order_visit(
+    tvm_ffi.structural_walk(
         func.body, lambda node: binds.append(node) if isinstance(node, 
tvm.tirx.Bind) else None
     )
     assert len(binds) == 1
@@ -2731,7 +2742,7 @@ def test_roundtrip_tmem_decl_buffer():
     assert from_source(code).script() == code
     assert_structural_equal(func, from_source(code))
     decls = []
-    tvm.tirx.stmt_functor.post_order_visit(
+    tvm_ffi.structural_walk(
         func.body,
         lambda node: decls.append(node) if isinstance(node, 
tvm.tirx.DeclBuffer) else None,
     )
@@ -2990,7 +3001,7 @@ def test_scope_id_dtype_uint32():
     # fmt: on
 
     scope_defs = []
-    tvm.tirx.stmt_functor.post_order_visit(
+    tvm_ffi.structural_walk(
         func.body,
         lambda s: (
             scope_defs.append(getattr(s, "def")) if isinstance(s, 
tvm.tirx.ScopeIdDefStmt) else None
@@ -3059,7 +3070,7 @@ def test_scope_id_dtype_uint32_deferred_extent():
     # fmt: on
 
     scope_defs = []
-    tvm.tirx.stmt_functor.post_order_visit(
+    tvm_ffi.structural_walk(
         func.body,
         lambda s: (
             scope_defs.append(getattr(s, "def")) if isinstance(s, 
tvm.tirx.ScopeIdDefStmt) else None
diff --git a/tests/python/tirx/transform/test_stmt_functor.py 
b/tests/python/tirx/transform/test_stmt_functor.py
index 92ed2c4765..8722473c57 100644
--- a/tests/python/tirx/transform/test_stmt_functor.py
+++ b/tests/python/tirx/transform/test_stmt_functor.py
@@ -1228,7 +1228,9 @@ def test_op_call_pointer_config_visited_and_mutated():
 
 def test_op_call_nested_config_visited_and_substituted():
     """Nested selector arrays participate in the core visitor and mutator."""
-    from tvm.tirx.stmt_functor import post_order_visit, substitute
+    from tvm_ffi import structural_walk as post_order_visit
+
+    from tvm.tirx.stmt_functor import substitute
 
     @T.prim_func
     def selector(
diff --git a/tests/python/tirx/transform/test_transform_flatten_buffer.py 
b/tests/python/tirx/transform/test_transform_flatten_buffer.py
index b47030edda..3cec134d88 100644
--- a/tests/python/tirx/transform/test_transform_flatten_buffer.py
+++ b/tests/python/tirx/transform/test_transform_flatten_buffer.py
@@ -23,6 +23,8 @@ the rebuilt identity, or SplitHostDevice later sees them as 
undefined
 and hoists dead variables into the kernel ABI.
 """
 
+import tvm_ffi
+
 import tvm
 import tvm.testing
 from tvm.script import tirx as T
@@ -36,7 +38,7 @@ def _collect_defined_buffers(func):
         if isinstance(node, tvm.tirx.AllocBuffer | tvm.tirx.DeclBuffer):
             defined.add(node.buffer)
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     return defined
 
 
@@ -56,7 +58,7 @@ def _assert_loads_reference_defined_buffers(func):
             if isinstance(node, tvm.ir.TensorLoad) and not 
is_defined(node.source):
                 stale.append(f"{where}: load of {node.source.name}")
 
-        tvm.tirx.stmt_functor.post_order_visit(expr, visit)
+        tvm_ffi.structural_walk(expr, visit)
 
     def visit(node):
         if isinstance(node, tvm.ir.TensorLoad | tvm.tirx.BufferStore):
@@ -71,7 +73,7 @@ def _assert_loads_reference_defined_buffers(func):
             if node.buffer.elem_offset is not None:
                 check_expr(node.buffer.elem_offset, f"elem_offset of 
{node.buffer.name}")
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, visit)
+    tvm_ffi.structural_walk(func.body, visit)
     assert not stale, f"stale buffer references after FlattenBuffer: {stale}"
 
 
@@ -122,9 +124,9 @@ def test_flatten_remaps_loads_in_folded_elem_offset():
                 if isinstance(sub, tvm.ir.TensorLoad):
                     found.append(sub)
 
-            tvm.tirx.stmt_functor.post_order_visit(node.indices[0], inner)
+            tvm_ffi.structural_walk(node.indices[0], inner)
 
-    tvm.tirx.stmt_functor.post_order_visit(after.body, visit)
+    tvm_ffi.structural_walk(after.body, visit)
     assert found, "expected the folded elem_offset load in the mbar store 
index"
 
 
@@ -143,7 +145,7 @@ def test_flatten_keeps_identity_of_already_flat_buffers():
         if isinstance(node, tvm.tirx.AllocBuffer):
             before_allocs[node.buffer.name] = node.buffer
 
-    tvm.tirx.stmt_functor.post_order_visit(before.body, collect_before)
+    tvm_ffi.structural_walk(before.body, collect_before)
 
     after = _flatten(before)
     preserved = []
@@ -152,7 +154,7 @@ def test_flatten_keeps_identity_of_already_flat_buffers():
         if isinstance(node, tvm.tirx.AllocBuffer) and node.buffer.name in 
before_allocs:
             
preserved.append(node.buffer.same_as(before_allocs[node.buffer.name]))
 
-    tvm.tirx.stmt_functor.post_order_visit(after.body, visit)
+    tvm_ffi.structural_walk(after.body, visit)
     assert preserved and all(preserved), "already-flat buffer identity was not 
preserved"
 
 
diff --git a/tests/python/tirx/transform/test_transform_lower_tirx.py 
b/tests/python/tirx/transform/test_transform_lower_tirx.py
index d9a571942d..f65050f587 100644
--- a/tests/python/tirx/transform/test_transform_lower_tirx.py
+++ b/tests/python/tirx/transform/test_transform_lower_tirx.py
@@ -16,6 +16,7 @@
 # under the License.
 
 import pytest
+import tvm_ffi
 
 import tvm
 import tvm.testing
@@ -55,7 +56,7 @@ def _launch_thread_extents(func):
         if isinstance(node, tvm.tirx.AttrStmt) and node.attr_key == 
"thread_extent":
             extents[str(node.node.thread_tag)] = int(node.value)
 
-    tvm.tirx.stmt_functor.post_order_visit(func.body, collect)
+    tvm_ffi.structural_walk(func.body, collect)
     return extents
 
 
diff --git a/tests/python/tvmscript/test_tvmscript_parser_source.py 
b/tests/python/tvmscript/test_tvmscript_parser_source.py
index 31c7581d9c..8241317932 100644
--- a/tests/python/tvmscript/test_tvmscript_parser_source.py
+++ b/tests/python/tvmscript/test_tvmscript_parser_source.py
@@ -21,6 +21,7 @@ import inspect
 
 import pytest
 import tvm_ffi
+from tvm_ffi import structural_walk as post_order_visit
 
 import tvm
 import tvm.testing
@@ -30,7 +31,6 @@ from tvm.script.parser.core import doc_core as doc
 from tvm.script.parser.core.diagnostics import Source
 from tvm.script.tirx import tile as Tx
 from tvm.tirx.stmt import TilePrimitiveCall
-from tvm.tirx.stmt_functor import post_order_visit
 
 
 def _tirx_source(func):

Reply via email to