This is an automated email from the ASF dual-hosted git repository.

spectrometerHBH 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 7fca2e1716 [REFACTOR][IR] New Functor/Visitor/Mutator Infra at Base 
Layer (#20327)
7fca2e1716 is described below

commit 7fca2e17160bae19d0311c327042d5859361a92e
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Sep 12 21:15:00 2026 -0400

    [REFACTOR][IR] New Functor/Visitor/Mutator Infra at Base Layer (#20327)
    
    This PR introduces a new `expr_functor.h` with `ExprMutator` and
    `ExprVisitor` aligned with the `StructuralMutate`/`StructuralVisit`
    system.
    
    Both provide virtual hooks for individual expression kinds. Default
    hooks traverse children, with structural fallback for types without
    exact registrations. `ExprMutator` supports copy-on-write rewriting and
    variable remapping; `ExprVisitor` supports early interruption. The
    header also provides `ExprFunctor<R(...)>` for custom return types and
    arguments, with caller-controlled recursion.
    
    ## Guidelines
    
    - Use `StructuralMap` for common rewriting and `StructuralWalk` or
    `StructuralVisit` for common traversal.
    - Use `ExprMutator` or `ExprVisitor` for per-node virtual overrides with
    default child traversal.
    - Use `ExprFunctor` for typed dispatch with custom results, arguments,
    or recursion.
    
    ## Notes
    
    ### Traversal beyond expressions
    
    `ExprMutator` and `ExprVisitor` are structural mutator and visitor
    objects with expression hooks. Unlike the older expression visitors and
    mutators, they can follow registered structural fields through other IR
    nodes without a dedicated hook, reaching expressions nested inside them.
    Expression types without exact hook registrations also use this
    structural fallback.
    
    ```cpp
    // Visit expressions inside a function through its registered structural 
fields.
    auto visitor = ffi::make_object<ExprVisitor>();
    visitor->VisitExpected(function).value();
    ```
    
    ### Adding hooks in subclasses
    
    Override existing hooks directly. To add a hook for a custom expression
    type, initialize an inherited dispatch table and register the new hook.
    Here, `MyExprNode` is a custom `ExprNode` subtype:
    
    ```cpp
    class MyExprMutator : public ExprMutator {
     public:
      TVM_DEFINE_OBJECT_FUNCTOR_DEFAULT_CONSTRUCTOR(MyExprMutator, ExprMutator)
      using ExprMutator::Mutate_;
      virtual Expected<UnchangedOr<ffi::Any>> Mutate_(
          const MyExprNode* node, bool allow_inplace);
    
     protected:
      static void InitVTable(VTable* table) {
        ExprMutator::InitVTable(table);
        SetDispatch<MyExprMutator, MyExprNode>(table);
      }
    };
    ```
    
    Define the new `Mutate_` hook with the custom rewrite logic. Further
    subclasses can override it; `ExprVisitor` supports the same registration
    pattern with `Visit_` hooks.
---
 3rdparty/tvm-ffi                |   2 +-
 include/tvm/ir/expr_functor.h   | 472 ++++++++++++++++++++++++++
 include/tvm/ir/object_functor.h | 398 +++++++++++++++++++++-
 src/ir/expr_functor.cc          | 722 ++++++++++++++++++++++++++++++++++++++++
 tests/cpp/expr_functor_test.cc  |  95 ++++++
 5 files changed, 1677 insertions(+), 12 deletions(-)

diff --git a/3rdparty/tvm-ffi b/3rdparty/tvm-ffi
index c5e636bbfb..be35ec1297 160000
--- a/3rdparty/tvm-ffi
+++ b/3rdparty/tvm-ffi
@@ -1 +1 @@
-Subproject commit c5e636bbfb097cca610acee841e26d751d47a63b
+Subproject commit be35ec1297d6ddf388b7feb4f2e89d2cf030dda8
diff --git a/include/tvm/ir/expr_functor.h b/include/tvm/ir/expr_functor.h
new file mode 100644
index 0000000000..b7115ed6c5
--- /dev/null
+++ b/include/tvm/ir/expr_functor.h
@@ -0,0 +1,472 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/*!
+ * \file tvm/ir/expr_functor.h
+ * \brief Native visiting and mutation of core IR expressions.
+ */
+#ifndef TVM_IR_EXPR_FUNCTOR_H_
+#define TVM_IR_EXPR_FUNCTOR_H_
+#include <tvm/ir/expr.h>
+#include <tvm/ir/object_functor.h>
+#include <tvm/ir/op.h>
+#include <tvm/ir/prim/expr.h>
+#include <tvm/ir/prim/vector_expr.h>
+
+namespace tvm {
+
+/*!
+ * \brief Type-dispatched expression functor with a caller-selected signature.
+ * \tparam FType A function signature of the form R(const Expr&, Args...).
+ *
+ * Override Dispatch_ for a node type, or DispatchDefault_ for the default
+ * behavior. Unlike structural visitors and mutators, this functor does not
+ * traverse children automatically. Dispatch may use a registered ancestor.
+ * A derived class can add node types with a fresh inherited table.
+ * Use tvm::ExprFunctor explicitly when dialect functors are also in scope.
+ */
+template <typename FType>
+class ExprFunctor;
+
+template <typename R, typename... Args>
+class ExprFunctor<R(const Expr&, Args...)> {
+ private:
+  using TSelf = ExprFunctor<R(const Expr&, Args...)>;
+
+ public:
+  /*! \brief The result type of this functor. */
+  using result_type = R;
+  /*! \brief Construct a functor with the core expression hooks. */
+  ExprFunctor() : ExprFunctor(GlobalVTable()) {}
+  /*!
+   * \brief Dispatch an expression through Dispatch.
+   * \param node The borrowed expression.
+   * \param args Additional arguments forwarded to the selected hook.
+   * \return The hook result.
+   */
+  TVM_FFI_INLINE R operator()(const Expr& node, Args... args) {
+    return Dispatch(node, std::forward<Args>(args)...);
+  }
+  /*!
+   * \brief Dispatch to a node hook, including registered ancestor hooks.
+   * \param node The non-null borrowed expression.
+   * \param args Additional arguments forwarded to the selected hook.
+   * \return The hook result.
+   */
+  TVM_FFI_INLINE virtual R Dispatch(const Expr& node, Args... args) {
+    TVM_FFI_ICHECK(node.defined()) << "Cannot dispatch a null expression";
+    return (*vtable_)(node, this, std::forward<Args>(args)...);
+  }
+
+  virtual R Dispatch_(const OpaqueExprNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const TupleNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const TupleGetItemNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const TensorLoadNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const VarNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const GlobalVarNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const CallNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const IntImmNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const FloatImmNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const OpNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::StringImmNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::CastNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::AddNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::SubNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::MulNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::DivNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::ModNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::FloorDivNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::FloorModNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::MinNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::MaxNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::EQNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::NENode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::LTNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::LENode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::GTNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::GENode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::AndNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::OrNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::NotNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::SelectNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::LetNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::RampNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::BroadcastNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+  virtual R Dispatch_(const prim::ShuffleNode* node, Args... args) {
+    return DispatchDefault_(node, std::forward<Args>(args)...);
+  }
+
+  /*!
+   * \brief Default node behavior, overridden by subclasses that handle 
arbitrary core nodes.
+   * \param node The borrowed node.
+   * \param args Additional arguments supplied to the functor.
+   * \return The hook result.
+   */
+  virtual R DispatchDefault_(const ffi::Object* node, Args... args) {
+    TVM_FFI_THROW(InternalError) << "Do not have a default for " << 
node->GetTypeKey();
+    TVM_FFI_UNREACHABLE();
+  }
+
+ protected:
+  /*! \brief Dispatch table shared by this signature and its subclasses. */
+  using VTable = ObjectFunctor<R(const ffi::ObjectRef&, TSelf*, Args...)>;
+  /*!
+   * \brief Construct a functor with an extended finalized table.
+   * \param vtable The table, which must outlive the functor.
+   */
+  explicit ExprFunctor(const VTable* vtable) : vtable_(vtable) {}
+  /*!
+   * \brief Register core expression hooks in a fresh mutable table.
+   * \param vtable The table to initialize before adding derived registrations.
+   */
+  static void InitVTable(VTable* vtable) {
+    SetDispatch<TSelf, OpaqueExprNode>(vtable);
+    SetDispatch<TSelf, TupleNode>(vtable);
+    SetDispatch<TSelf, TupleGetItemNode>(vtable);
+    SetDispatch<TSelf, TensorLoadNode>(vtable);
+    SetDispatch<TSelf, VarNode>(vtable);
+    SetDispatch<TSelf, GlobalVarNode>(vtable);
+    SetDispatch<TSelf, CallNode>(vtable);
+    SetDispatch<TSelf, IntImmNode>(vtable);
+    SetDispatch<TSelf, FloatImmNode>(vtable);
+    SetDispatch<TSelf, OpNode>(vtable);
+    SetDispatch<TSelf, prim::StringImmNode>(vtable);
+    SetDispatch<TSelf, prim::CastNode>(vtable);
+    SetDispatch<TSelf, prim::AddNode>(vtable);
+    SetDispatch<TSelf, prim::SubNode>(vtable);
+    SetDispatch<TSelf, prim::MulNode>(vtable);
+    SetDispatch<TSelf, prim::DivNode>(vtable);
+    SetDispatch<TSelf, prim::ModNode>(vtable);
+    SetDispatch<TSelf, prim::FloorDivNode>(vtable);
+    SetDispatch<TSelf, prim::FloorModNode>(vtable);
+    SetDispatch<TSelf, prim::MinNode>(vtable);
+    SetDispatch<TSelf, prim::MaxNode>(vtable);
+    SetDispatch<TSelf, prim::EQNode>(vtable);
+    SetDispatch<TSelf, prim::NENode>(vtable);
+    SetDispatch<TSelf, prim::LTNode>(vtable);
+    SetDispatch<TSelf, prim::LENode>(vtable);
+    SetDispatch<TSelf, prim::GTNode>(vtable);
+    SetDispatch<TSelf, prim::GENode>(vtable);
+    SetDispatch<TSelf, prim::AndNode>(vtable);
+    SetDispatch<TSelf, prim::OrNode>(vtable);
+    SetDispatch<TSelf, prim::NotNode>(vtable);
+    SetDispatch<TSelf, prim::SelectNode>(vtable);
+    SetDispatch<TSelf, prim::LetNode>(vtable);
+    SetDispatch<TSelf, prim::RampNode>(vtable);
+    SetDispatch<TSelf, prim::BroadcastNode>(vtable);
+    SetDispatch<TSelf, prim::ShuffleNode>(vtable);
+  }
+  /*!
+   * \brief Register a hook for an additional expression type.
+   * \tparam Self The subclass implementing the hook.
+   * \tparam Node The node type handled by the hook.
+   * \param vtable The mutable table to receive the registration.
+   */
+  template <typename Self, typename Node>
+  static void SetDispatch(VTable* vtable) {
+    vtable->template SetDispatch<Node>(
+        [](const ffi::ObjectRef& node, TSelf* self, Args... args) -> R {
+          return static_cast<Self*>(self)->Dispatch_(static_cast<const 
Node*>(node.get()),
+                                                     
std::forward<Args>(args)...);
+        });
+  }
+
+ private:
+  static const VTable* GlobalVTable() {
+    static const VTable table = [] {
+      VTable table;
+      InitVTable(&table);
+      table.Finalize();
+      return table;
+    }();
+    return &table;
+  }
+  const VTable* vtable_;
+};
+
+/*!
+ * \brief Structural Expr visitor with native overrides for core expressions.
+ *
+ * Prefer StructuralVisit or StructuralWalk for common cases; use ExprVisitor
+ * for extensive per-kind customization or optimization. Default hooks follow
+ * the registered structural child order and skip primitive metadata.
+ * Native hooks match exact node types; derived node types use structural 
fallback
+ * unless separately registered in a fresh inherited table with SetDispatch.
+ * Existing hooks only require overriding. For an extra MyExprNode derived from
+ * ExprNode, initialize an inherited table and register the new virtual hook:
+ * \code
+ * class MyExprVisitor : public ExprVisitor {
+ *  public:
+ *   TVM_DEFINE_OBJECT_FUNCTOR_DEFAULT_CONSTRUCTOR(MyExprVisitor, ExprVisitor)
+ *   using ExprVisitor::Visit_;
+ *   virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const MyExprNode* 
node);
+ *
+ *  protected:
+ *   static void InitVTable(VTable* vtable) {
+ *     ExprVisitor::InitVTable(vtable);
+ *     SetDispatch<MyExprVisitor, MyExprNode>(vtable);
+ *   }
+ * };
+ * \endcode
+ * Use tvm::ExprVisitor explicitly when dialect visitors are also in scope.
+ */
+class TVM_DLL ExprVisitor : public ObjectVisitor {
+ public:
+  /*! \brief Construct a visitor with the core expression hooks. */
+  TVM_DEFINE_OBJECT_FUNCTOR_DEFAULT_CONSTRUCTOR(ExprVisitor, ObjectVisitor)
+
+  using ObjectVisitor::VisitExpected;
+
+  // Override existing hooks directly. Extra types need a fresh inherited 
table.
+  // Hooks borrow the node and return None, an owning interrupt, or an Error.
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const OpaqueExprNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const TupleNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const 
TupleGetItemNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const TensorLoadNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const VarNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const GlobalVarNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const CallNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const IntImmNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const FloatImmNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const OpNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const 
prim::StringImmNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::CastNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::AddNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::SubNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::MulNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::DivNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::ModNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const 
prim::FloorDivNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const 
prim::FloorModNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::MinNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::MaxNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::EQNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::NENode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::LTNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::LENode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::GTNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::GENode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::AndNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::OrNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::NotNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const 
prim::SelectNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::LetNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const prim::RampNode* 
node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const 
prim::BroadcastNode* node);
+  virtual Expected<ffi::Optional<VisitInterrupt>> Visit_(const 
prim::ShuffleNode* node);
+
+ protected:
+  /*!
+   * \brief Construct a visitor with an extended native dispatch table.
+   * \param vtable The finalized table, which must outlive this visitor.
+   */
+  explicit ExprVisitor(const VTable* vtable) : ObjectVisitor(vtable) {}
+  /*!
+   * \brief Register core expression hooks in a fresh mutable table.
+   * \param vtable The table to initialize before adding derived registrations.
+   */
+  static void InitVTable(VTable* vtable);
+};
+
+/*!
+ * \brief Structural Expr mutator with native overrides for core expressions
+ * and structural fallback for other objects.
+ *
+ * Prefer StructuralMap for common cases; use ExprMutator for extensive 
per-kind
+ * customization or optimization.
+ * The default Var hook leaves PrimType variables unchanged before remap 
lookup.
+ *
+ * Native hooks match exact node types; derived node types need their own 
registrations.
+ * Existing hooks only require overriding. For an extra MyExprNode derived from
+ * ExprNode, initialize an inherited table and register the new virtual hook:
+ * \code
+ * class MyExprMutator : public ExprMutator {
+ *  public:
+ *   TVM_DEFINE_OBJECT_FUNCTOR_DEFAULT_CONSTRUCTOR(MyExprMutator, ExprMutator)
+ *   using ExprMutator::Mutate_;
+ *   virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const MyExprNode* node, 
bool allow_inplace);
+ *
+ *  protected:
+ *   static void InitVTable(VTable* vtable) {
+ *     ExprMutator::InitVTable(vtable);
+ *     SetDispatch<MyExprMutator, MyExprNode>(vtable);
+ *   }
+ * };
+ * \endcode
+ * Use tvm::ExprMutator explicitly when dialect mutators are also in scope.
+ */
+class TVM_DLL ExprMutator : public ObjectMutator {
+ public:
+  /*! \brief Construct a mutator with the core expression hooks. */
+  TVM_DEFINE_OBJECT_FUNCTOR_DEFAULT_CONSTRUCTOR(ExprMutator, ObjectMutator)
+
+  using ObjectMutator::MaybeInplaceMutateIfUniqueExpected;
+  using ObjectMutator::MutateExpected;
+
+  /*!
+   * \brief Mutate a borrowed expression without allowing in-place changes.
+   * \param value The borrowed expression to mutate.
+   * \return An Expr replacement, Unchanged, or an Error if mutation fails.
+   * \note This entry trusts the Expr replacement contract of native hooks and 
extensions.
+   */
+  TVM_FFI_INLINE Expected<UnchangedOr<Expr>> MutateExpected(const Expr& value) 
noexcept {
+    return ffi::details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<Expr>>(
+        
ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(ObjectMutator::MutateExpected(value)));
+  }
+  /*!
+   * \brief Forward inherited permission and check the expression's uniqueness.
+   * \param value The borrowed expression to mutate.
+   * \param allow_inplace Whether the path to this expression is already 
uniquely owned.
+   * \return An Expr replacement, Unchanged, or an Error if mutation fails.
+   * \note This entry trusts the Expr replacement contract of native hooks and 
extensions.
+   */
+  TVM_FFI_INLINE Expected<UnchangedOr<Expr>> 
MaybeInplaceMutateIfUniqueExpected(
+      const Expr& value, bool allow_inplace = true) noexcept {
+    return ffi::details::ExpectedUnsafe::MoveFromTVMFFIAny<UnchangedOr<Expr>>(
+        ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(
+            ObjectMutator::MaybeInplaceMutateIfUniqueExpected(value, 
allow_inplace)));
+  }
+
+  // A downstream class overrides any existing hook without rebuilding the 
table.
+  // Extra node types use a fresh inherited table and SetDispatch<Self, 
ExtraNode>.
+  // Hooks borrow the node and return an owning Expr replacement in Any, 
Unchanged, or Error.
+  // Narrower field types are checked separately. Forward allow_inplace on 
every child edge.
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const OpaqueExprNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const TupleNode* node, bool 
allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const TupleGetItemNode* 
node, bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const TensorLoadNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const VarNode* node, bool 
allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const GlobalVarNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const CallNode* node, bool 
allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const IntImmNode* node, bool 
allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const FloatImmNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const OpNode* node, bool 
allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::StringImmNode* 
node,
+                                                  bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::CastNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::AddNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::SubNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::MulNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::DivNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::ModNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::FloorDivNode* 
node,
+                                                  bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::FloorModNode* 
node,
+                                                  bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::MinNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::MaxNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::EQNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::NENode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::LTNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::LENode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::GTNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::GENode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::AndNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::OrNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::NotNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::SelectNode* 
node, bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::LetNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::RampNode* node, 
bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::BroadcastNode* 
node,
+                                                  bool allow_inplace);
+  virtual Expected<UnchangedOr<ffi::Any>> Mutate_(const prim::ShuffleNode* 
node,
+                                                  bool allow_inplace);
+
+ protected:
+  /*!
+   * \brief Construct a mutator with an extended native dispatch table.
+   * \param vtable The finalized table, which must outlive this mutator.
+   */
+  explicit ExprMutator(const VTable* vtable) : ObjectMutator(vtable) {}
+  /*!
+   * \brief Register core expression hooks in a fresh mutable table.
+   * \param vtable The table to initialize before adding derived registrations.
+   */
+  static void InitVTable(VTable* vtable);
+};
+
+}  // namespace tvm
+#endif  // TVM_IR_EXPR_FUNCTOR_H_
diff --git a/include/tvm/ir/object_functor.h b/include/tvm/ir/object_functor.h
index 082c2c1c13..037a1d0673 100644
--- a/include/tvm/ir/object_functor.h
+++ b/include/tvm/ir/object_functor.h
@@ -18,12 +18,15 @@
  */
 /*!
  * \file tvm/ir/object_functor.h
- * \brief Defines the Functor data structures.
+ * \brief Native object dispatch, visiting and mutation with structural 
fallback.
  */
 #ifndef TVM_IR_OBJECT_FUNCTOR_H_
 #define TVM_IR_OBJECT_FUNCTOR_H_
 
 #include <tvm/ffi/error.h>
+#include <tvm/ffi/extra/structural_mutate.h>
+#include <tvm/ffi/extra/structural_visit.h>
+#include <tvm/runtime/base.h>
 
 #include <cstring>
 #include <type_traits>
@@ -75,13 +78,18 @@ namespace tvm {
  *   }
  * \endcode
  *
- * \tparam FType Function signature, with const ffi::ObjectRef& as its first 
argument.
+ * \tparam FType Function signature, with const ffi::ObjectRef& or const 
ffi::Object* as its first
+ * argument.
  */
 template <typename FType>
 class ObjectFunctor;
 
-template <typename R, typename... Args>
-class ObjectFunctor<R(const ffi::ObjectRef& n, Args...)> {
+template <typename R, typename NodeArg, typename... Args>
+class ObjectFunctor<R(NodeArg, Args...)> {
+  static_assert(std::is_same_v<NodeArg, const ffi::ObjectRef&> ||
+                    std::is_same_v<NodeArg, const ffi::Object*>,
+                "ObjectFunctor requires an ObjectRef reference or borrowed 
Object pointer");
+
  public:
   /*! \brief the result type of this functor */
   using result_type = R;
@@ -90,7 +98,7 @@ class ObjectFunctor<R(const ffi::ObjectRef& n, Args...)> {
    * \param n The object to be dispatched.
    * \return Whether a dispatch function is registered for n's type, excluding 
ancestors.
    */
-  bool CanDispatch(const ffi::ObjectRef& n) const {
+  TVM_FFI_INLINE bool CanDispatch(NodeArg n) const {
     uint32_t type_index = n->type_index();
     if (type_index < begin_type_index_) return false;
     type_index -= begin_type_index_;
@@ -102,7 +110,7 @@ class ObjectFunctor<R(const ffi::ObjectRef& n, Args...)> {
    * \param args The additional arguments
    * \return The result.
    */
-  R operator()(const ffi::ObjectRef& n, Args... args) const {
+  TVM_FFI_INLINE R operator()(NodeArg n, Args... args) const {
     uint32_t type_index = n->type_index();
     if (type_index >= begin_type_index_) {
       uint32_t index = type_index - begin_type_index_;
@@ -121,9 +129,7 @@ class ObjectFunctor<R(const ffi::ObjectRef& n, Args...)> {
         }
       }
     }
-    TVM_FFI_THROW(InternalError) << "ObjectFunctor calls un-registered 
function on type "
-                                 << n->GetTypeKey();
-    throw;
+    ThrowUnregistered(n);
   }
   /*!
    * \brief set the dispatcher for type TNode
@@ -132,7 +138,7 @@ class ObjectFunctor<R(const ffi::ObjectRef& n, Args...)> {
    * \return reference to self.
    */
   template <typename TNode>
-  ObjectFunctor& SetDispatch(R (*f)(const ffi::ObjectRef& n, Args...)) {
+  ObjectFunctor& SetDispatch(R (*f)(NodeArg n, Args...)) {
     uint32_t tindex = TNode::RuntimeTypeIndex();
     if (func_.size() <= tindex) {
       func_.resize(tindex + 1, nullptr);
@@ -178,13 +184,383 @@ class ObjectFunctor<R(const ffi::ObjectRef& n, Args...)> 
{
   }
 
  private:
+  [[noreturn]] TVM_FFI_COLD_CODE static void ThrowUnregistered(NodeArg n) {
+    TVM_FFI_THROW(InternalError) << "ObjectFunctor calls un-registered 
function on type "
+                                 << n->GetTypeKey();
+    throw;
+  }
+
   /*! \brief internal function pointer type */
-  using FPointer = R (*)(const ffi::ObjectRef& n, Args...);
+  using FPointer = R (*)(NodeArg n, Args...);
   /*! \brief internal function table */
   std::vector<FPointer> func_;
   /*! \brief start range of func index */
   uint32_t begin_type_index_{0};
 };
 
+using ffi::Expected;
+using ffi::UnchangedOr;
+using ffi::VisitInterrupt;
+
+/*!
+ * \brief Native visiting with exact dispatch and structural fallback.
+ *
+ * Allocate visitors with ffi::make_object<Derived>(). Inputs are borrowed;
+ * interrupts and errors own their values. A hook controls descent into its 
node.
+ * One visitor must not be shared by overlapping traversals.
+ */
+class TVM_DLL ObjectVisitor : public ffi::StructuralVisitorObj {
+ public:
+  /*! \brief Construct a visitor using structural fallback for every value. */
+  ObjectVisitor() : ObjectVisitor(GlobalVTable()) {}
+  /*! \brief Release the managed visitor state. */
+  ~ObjectVisitor() = default;
+  ObjectVisitor(const ObjectVisitor& other) = delete;
+  ObjectVisitor& operator=(const ObjectVisitor& other) = delete;
+
+  /*!
+   * \brief Visit a borrowed object and propagate an interrupt or error.
+   * \param value The borrowed object to visit.
+   * \return None on completion, an owning VisitInterrupt, or an Error.
+   */
+  TVM_FFI_INLINE Expected<ffi::Optional<VisitInterrupt>> VisitExpected(
+      const ffi::ObjectRef& value) noexcept {
+    return VisitExpected(ffi::AnyView(value));
+  }
+  /*!
+   * \brief Visit a borrowed object or inline value.
+   * \param value The borrowed value to visit.
+   * \return None on completion, an owning VisitInterrupt, or an Error.
+   */
+  TVM_FFI_INLINE Expected<ffi::Optional<VisitInterrupt>> VisitExpected(
+      ffi::AnyView value) noexcept {
+    if (const auto* object = value.as<ffi::Object>()) return Dispatch(object);
+    return StructuralVisitDefault(value);
+  }
+
+ protected:
+  /*! \brief Exact native dispatch table with owning interrupt and error 
results. */
+  using VTable =
+      ObjectFunctor<Expected<ffi::Optional<VisitInterrupt>>(const 
ffi::Object*, ObjectVisitor*)>;
+
+  /*!
+   * \brief Construct a visitor with a finalized native dispatch table.
+   * \param vtable The immutable table, which must outlive this visitor.
+   */
+  explicit ObjectVisitor(const VTable* vtable)
+      : ffi::StructuralVisitorObj(StructuralVTable()), native_vtable_(vtable) 
{}
+  /*!
+   * \brief Initialize the base registrations in a fresh mutable table.
+   * \param vtable The table to initialize before adding derived registrations.
+   */
+  static void InitVTable(VTable* vtable) {}
+
+  /*!
+   * \brief Register an exact node type in a fresh table initialized by its 
parent.
+   * Derived node types require separate registrations.
+   * \tparam Self The visitor implementing the hook.
+   * \tparam Node The exact node type handled by the hook.
+   * \param vtable The mutable table to receive the registration.
+   */
+  template <typename Self, typename Node>
+  static void SetDispatch(VTable* vtable) {
+    vtable->template SetDispatch<Node>(DispatchNode<Self, Node>);
+  }
+
+ private:
+  // Native table callback; core instantiations may be shared by the library.
+  template <typename Self, typename Node>
+  static Expected<ffi::Optional<VisitInterrupt>> DispatchNode(const 
ffi::Object* node,
+                                                              ObjectVisitor* 
self) {
+    return static_cast<Self*>(self)->Visit_(static_cast<const Node*>(node));
+  }
+
+  // The AnyView entry establishes that value is a non-null object.
+  TVM_FFI_INLINE Expected<ffi::Optional<VisitInterrupt>> Dispatch(
+      const ffi::Object* value) noexcept {
+    if (native_vtable_->CanDispatch(value)) {
+      try {
+        return DispatchNative(value);
+      } catch (ffi::Error& error) {
+        return AttachVisitErrorContext(error, value);
+      }
+    }
+    return StructuralVisitDefault(value);
+  }
+  // Keep one named return value in this scope so native results can be 
constructed in place.
+  TVM_FFI_INLINE Expected<ffi::Optional<VisitInterrupt>> DispatchNative(const 
ffi::Object* value) {
+    Expected<ffi::Optional<VisitInterrupt>> result = (*native_vtable_)(value, 
this);
+    if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
+      UpdateVisitErrorContext(result, value);
+    }
+    return result;
+  }
+  Expected<ffi::Optional<VisitInterrupt>> StructuralVisitDefault(ffi::AnyView 
value) noexcept {
+    TVMFFIAny result = ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(
+        ffi::StructuralVisitorObj::DefaultVisitExpected(value));
+    if (TVM_FFI_PREDICT_FALSE(result.type_index == 
ffi::TypeIndex::kTVMFFIError)) {
+      result = ffi::details::AttachStructuralVisitErrorContextRaw(result, 
value);
+    }
+    return 
ffi::details::ExpectedUnsafe::MoveFromTVMFFIAny<ffi::Optional<VisitInterrupt>>(result);
+  }
+  TVM_FFI_COLD_CODE static Expected<ffi::Optional<VisitInterrupt>> 
AttachVisitErrorContext(
+      ffi::Error& error, const ffi::Object* value) {
+    if (value) ffi::details::UpdateVisitErrorContext(error, 
ffi::GetRef<ffi::ObjectRef>(value));
+    return ffi::Unexpected(std::move(error));
+  }
+  TVM_FFI_COLD_CODE static void UpdateVisitErrorContext(
+      const Expected<ffi::Optional<VisitInterrupt>>& result, const 
ffi::Object* value) {
+    ffi::Error error = result.error();
+    if (value) ffi::details::UpdateVisitErrorContext(error, 
ffi::GetRef<ffi::ObjectRef>(value));
+  }
+  static const VTable* GlobalVTable() {
+    static const VTable table = [] {
+      VTable table;
+      InitVTable(&table);
+      table.Finalize();
+      return table;
+    }();
+    return &table;
+  }
+  static const ffi::StructuralVisitorVTable* StructuralVTable() {
+    static const ffi::StructuralVisitorVTable table{StructuralVTableVisitImpl};
+    return &table;
+  }
+  static TVMFFIAny StructuralVTableVisitImpl(ffi::StructuralVisitorObj* self,
+                                             ffi::AnyView value) noexcept {
+    return ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(
+        static_cast<ObjectVisitor*>(self)->VisitExpected(value));
+  }
+
+  const VTable* const native_vtable_;
+};
+
+/*!
+ * \brief Native mutation with exact dispatch and structural fallback.
+ *
+ * Allocate mutators with ffi::make_object<Derived>(). Inputs are borrowed;
+ * replacements and errors own their values. Overrides forward allow_inplace
+ * to child calls, following the structural mutation ownership contract.
+ */
+class TVM_DLL ObjectMutator : public ffi::StructuralMapEngineBase {
+ public:
+  /*! \brief Construct a mutator using structural fallback for every object. */
+  ObjectMutator() : ObjectMutator(GlobalVTable()) {}
+  /*! \brief Release the managed mutator state. */
+  ~ObjectMutator() = default;
+  ObjectMutator(const ObjectMutator& other) = delete;
+  ObjectMutator& operator=(const ObjectMutator& other) = delete;
+
+  /*!
+   * \brief Mutate a borrowed value without allowing in-place changes.
+   * \param value The borrowed object to mutate.
+   * \return A replacement, Unchanged, or an Error if mutation fails.
+   */
+  TVM_FFI_INLINE Expected<UnchangedOr<ffi::Any>> MutateExpected(
+      const ffi::ObjectRef& value) noexcept {
+    return Dispatch(value.get(), false);
+  }
+  /*!
+   * \brief Mutate a borrowed value without allowing in-place changes.
+   * \param value The borrowed object or inline value to mutate.
+   * \return A replacement, Unchanged, or an Error if mutation fails.
+   */
+  TVM_FFI_INLINE Expected<UnchangedOr<ffi::Any>> MutateExpected(ffi::AnyView 
value) noexcept {
+    if (const auto* object = value.as<ffi::Object>()) return Dispatch(object, 
false);
+    return StructuralMutateDefault(value, false);
+  }
+
+  /*!
+   * \brief Forward inherited permission and check the value's uniqueness.
+   * \param value The borrowed object to mutate.
+   * \param allow_inplace Whether the path to this value is already uniquely 
owned.
+   * \return A replacement, Unchanged, or an Error if mutation fails.
+   */
+  TVM_FFI_INLINE Expected<UnchangedOr<ffi::Any>> 
MaybeInplaceMutateIfUniqueExpected(
+      const ffi::ObjectRef& value, bool allow_inplace = true) noexcept {
+    return Dispatch(value.get(), allow_inplace && value.defined() && 
value->unique());
+  }
+  /*!
+   * \brief Forward inherited permission and check the value's uniqueness.
+   * \param value The borrowed object or inline value to mutate.
+   * \param allow_inplace Whether the path to this value is already uniquely 
owned.
+   * \return A replacement, Unchanged, or an Error if mutation fails.
+   */
+  TVM_FFI_INLINE Expected<UnchangedOr<ffi::Any>> 
MaybeInplaceMutateIfUniqueExpected(
+      ffi::AnyView value, bool allow_inplace = true) noexcept {
+    const auto* object = value.as<ffi::Object>();
+    if (object) return Dispatch(object, allow_inplace && object->unique());
+    return StructuralMutateDefault(value, false);
+  }
+
+  /*!
+   * \brief Look up a replacement in the variable-remap environment.
+   * \param var The borrowed variable identity to look up.
+   * \return The owning replacement, None on a miss, or an Error.
+   */
+  TVM_FFI_INLINE Expected<ffi::Any> VarRemapGetExpected(ffi::AnyView var) 
noexcept {
+    return VarRemapGetImpl(var);
+  }
+  /*!
+   * \brief Record a replacement in the variable-remap environment.
+   * \param var The borrowed variable identity to bind.
+   * \param mapped_value The borrowed replacement value.
+   * \return Successful completion or an Error.
+   */
+  TVM_FFI_INLINE Expected<void> VarRemapSetExpected(ffi::AnyView var,
+                                                    ffi::AnyView mapped_value) 
noexcept {
+    return VarRemapSetImpl(var, mapped_value);
+  }
+
+ protected:
+  /*! \brief Exact native dispatch table with owning replacement and error 
results. */
+  using VTable =
+      ObjectFunctor<Expected<UnchangedOr<ffi::Any>>(const ffi::Object*, 
ObjectMutator*, bool)>;
+
+  /*!
+   * \brief Construct a mutator with a finalized native dispatch table.
+   * \param vtable The immutable table, which must outlive this mutator.
+   */
+  explicit ObjectMutator(const VTable* vtable)
+      : ffi::StructuralMapEngineBase(StructuralVTable()), 
native_vtable_(vtable) {}
+  /*!
+   * \brief Initialize the base registrations in a fresh mutable table.
+   * \param vtable The table to initialize before adding derived registrations.
+   */
+  static void InitVTable(VTable* vtable) {}
+
+  /*!
+   * \brief Register an exact node type in a fresh table initialized by its 
parent.
+   * Derived node types require separate registrations.
+   * \tparam Self The mutator implementing the hook.
+   * \tparam Node The exact node type handled by the hook.
+   * \param vtable The mutable table to receive the registration.
+   */
+  template <typename Self, typename Node>
+  static void SetDispatch(VTable* vtable) {
+    vtable->template SetDispatch<Node>(DispatchNode<Self, Node>);
+  }
+
+ private:
+  // Native table callback; core instantiations may be shared by the library.
+  template <typename Self, typename Node>
+  static Expected<UnchangedOr<ffi::Any>> DispatchNode(const ffi::Object* node, 
ObjectMutator* self,
+                                                      bool allow_inplace) {
+    return static_cast<Self*>(self)->Mutate_(static_cast<const Node*>(node), 
allow_inplace);
+  }
+
+  using ffi::StructuralMutatorObj::DefaultMaybeInplaceMutateExpected;
+  using ffi::StructuralMutatorObj::DefaultMutateExpected;
+  using ffi::StructuralMutatorObj::MaybeInplaceMutate;
+  using ffi::StructuralMutatorObj::Mutate;
+
+  // Structural ABI entry: the caller guarantees ownership of the entire path.
+  TVM_FFI_INLINE Expected<UnchangedOr<ffi::Any>> MaybeInplaceMutateExpected(
+      const ffi::ObjectRef& value) noexcept {
+    return Dispatch(value.get(), true);
+  }
+  TVM_FFI_INLINE Expected<UnchangedOr<ffi::Any>> MaybeInplaceMutateExpected(
+      ffi::AnyView value) noexcept {
+    if (const auto* object = value.as<ffi::Object>()) return Dispatch(object, 
true);
+    return StructuralMutateDefault(value, true);
+  }
+
+  TVM_FFI_INLINE Expected<UnchangedOr<ffi::Any>> Dispatch(const ffi::Object* 
value,
+                                                          bool allow_inplace) 
noexcept {
+    if (value == nullptr) return ffi::Unchanged();
+    if (native_vtable_->CanDispatch(value)) {
+      try {
+        return DispatchNative(value, allow_inplace);
+      } catch (ffi::Error& error) {
+        return AttachVisitErrorContext(error, value);
+      }
+    }
+    return StructuralMutateDefault(value, allow_inplace);
+  }
+  // Keep one named return value in this scope so native results can be 
constructed in place.
+  TVM_FFI_INLINE Expected<UnchangedOr<ffi::Any>> DispatchNative(const 
ffi::Object* value,
+                                                                bool 
allow_inplace) {
+    Expected<UnchangedOr<ffi::Any>> result = (*native_vtable_)(value, this, 
allow_inplace);
+    if (TVM_FFI_PREDICT_FALSE(result.is_err())) {
+      UpdateVisitErrorContext(result, value);
+    }
+    return result;
+  }
+  Expected<UnchangedOr<ffi::Any>> StructuralMutateDefault(ffi::AnyView value,
+                                                          bool allow_inplace) 
noexcept {
+    if (allow_inplace) {
+      return 
ffi::StructuralMutatorObj::DefaultMaybeInplaceMutateExpected(value);
+    } else {
+      return ffi::StructuralMutatorObj::DefaultMutateExpected(value);
+    }
+  }
+  TVM_FFI_COLD_CODE static Expected<UnchangedOr<ffi::Any>> 
AttachVisitErrorContext(
+      ffi::Error& error, const ffi::Object* value) {
+    if (value) ffi::details::UpdateVisitErrorContext(error, 
ffi::GetRef<ffi::ObjectRef>(value));
+    return ffi::Unexpected(std::move(error));
+  }
+  TVM_FFI_COLD_CODE static void UpdateVisitErrorContext(
+      const Expected<UnchangedOr<ffi::Any>>& result, const ffi::Object* value) 
{
+    ffi::Error error = result.error();
+    if (value) ffi::details::UpdateVisitErrorContext(error, 
ffi::GetRef<ffi::ObjectRef>(value));
+  }
+  static const VTable* GlobalVTable() {
+    static const VTable table = [] {
+      VTable table;
+      InitVTable(&table);
+      table.Finalize();
+      return table;
+    }();
+    return &table;
+  }
+  static const ffi::StructuralMutatorVTable* StructuralVTable() {
+    static const ffi::StructuralMutatorVTable table{
+        StructuralVTableMutateImpl, StructuralVTableMaybeInplaceMutateImpl,
+        StructuralVTableVarRemapGetImpl, StructuralVTableVarRemapSetImpl};
+    return &table;
+  }
+  static TVMFFIAny StructuralVTableMutateImpl(ffi::StructuralMutatorObj* self,
+                                              ffi::AnyView value) noexcept {
+    return ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(
+        static_cast<ObjectMutator*>(self)->MutateExpected(value));
+  }
+  static TVMFFIAny 
StructuralVTableMaybeInplaceMutateImpl(ffi::StructuralMutatorObj* self,
+                                                          ffi::AnyView value) 
noexcept {
+    return ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(
+        static_cast<ObjectMutator*>(self)->MaybeInplaceMutateExpected(value));
+  }
+  static TVMFFIAny StructuralVTableVarRemapGetImpl(ffi::StructuralMutatorObj* 
self,
+                                                   ffi::AnyView key) noexcept {
+    return ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(
+        static_cast<ObjectMutator*>(self)->VarRemapGetImpl(key));
+  }
+  static TVMFFIAny StructuralVTableVarRemapSetImpl(ffi::StructuralMutatorObj* 
self,
+                                                   ffi::AnyView key, 
ffi::AnyView value) noexcept {
+    return ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(
+        static_cast<ObjectMutator*>(self)->VarRemapSetImpl(key, value));
+  }
+
+  const VTable* const native_vtable_;
+};
+
+/*!
+ * \brief Define a default constructor backed by one initialized native 
dispatch table.
+ * \param Class The class whose default constructor is defined.
+ * \param Parent The parent accepting a const VTable pointer in its 
constructor.
+ * \note Class must provide InitVTable(VTable*) including inherited 
registrations.
+ * The table is initialized and finalized once; explicit table-taking 
constructors are separate.
+ */
+#define TVM_DEFINE_OBJECT_FUNCTOR_DEFAULT_CONSTRUCTOR(Class, Parent) \
+  Class()                                                            \
+      : Parent([] {                                                  \
+          static const VTable table = [] {                           \
+            VTable table;                                            \
+            Class::InitVTable(&table);                               \
+            table.Finalize();                                        \
+            return table;                                            \
+          }();                                                       \
+          return &table;                                             \
+        }()) {}
+
 }  // namespace tvm
 #endif  // TVM_IR_OBJECT_FUNCTOR_H_
diff --git a/src/ir/expr_functor.cc b/src/ir/expr_functor.cc
new file mode 100644
index 0000000000..bbdd025639
--- /dev/null
+++ b/src/ir/expr_functor.cc
@@ -0,0 +1,722 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+#include <tvm/ir/expr_functor.h>
+
+namespace tvm {
+
+void ExprVisitor::InitVTable(VTable* vtable) {
+  ObjectVisitor::InitVTable(vtable);
+  SetDispatch<ExprVisitor, OpaqueExprNode>(vtable);
+  SetDispatch<ExprVisitor, TupleNode>(vtable);
+  SetDispatch<ExprVisitor, TupleGetItemNode>(vtable);
+  SetDispatch<ExprVisitor, TensorLoadNode>(vtable);
+  SetDispatch<ExprVisitor, VarNode>(vtable);
+  SetDispatch<ExprVisitor, GlobalVarNode>(vtable);
+  SetDispatch<ExprVisitor, CallNode>(vtable);
+  SetDispatch<ExprVisitor, IntImmNode>(vtable);
+  SetDispatch<ExprVisitor, FloatImmNode>(vtable);
+  SetDispatch<ExprVisitor, OpNode>(vtable);
+  SetDispatch<ExprVisitor, prim::StringImmNode>(vtable);
+  SetDispatch<ExprVisitor, prim::CastNode>(vtable);
+  SetDispatch<ExprVisitor, prim::AddNode>(vtable);
+  SetDispatch<ExprVisitor, prim::SubNode>(vtable);
+  SetDispatch<ExprVisitor, prim::MulNode>(vtable);
+  SetDispatch<ExprVisitor, prim::DivNode>(vtable);
+  SetDispatch<ExprVisitor, prim::ModNode>(vtable);
+  SetDispatch<ExprVisitor, prim::FloorDivNode>(vtable);
+  SetDispatch<ExprVisitor, prim::FloorModNode>(vtable);
+  SetDispatch<ExprVisitor, prim::MinNode>(vtable);
+  SetDispatch<ExprVisitor, prim::MaxNode>(vtable);
+  SetDispatch<ExprVisitor, prim::EQNode>(vtable);
+  SetDispatch<ExprVisitor, prim::NENode>(vtable);
+  SetDispatch<ExprVisitor, prim::LTNode>(vtable);
+  SetDispatch<ExprVisitor, prim::LENode>(vtable);
+  SetDispatch<ExprVisitor, prim::GTNode>(vtable);
+  SetDispatch<ExprVisitor, prim::GENode>(vtable);
+  SetDispatch<ExprVisitor, prim::AndNode>(vtable);
+  SetDispatch<ExprVisitor, prim::OrNode>(vtable);
+  SetDispatch<ExprVisitor, prim::NotNode>(vtable);
+  SetDispatch<ExprVisitor, prim::SelectNode>(vtable);
+  SetDispatch<ExprVisitor, prim::LetNode>(vtable);
+  SetDispatch<ExprVisitor, prim::RampNode>(vtable);
+  SetDispatch<ExprVisitor, prim::BroadcastNode>(vtable);
+  SetDispatch<ExprVisitor, prim::ShuffleNode>(vtable);
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
OpaqueExprNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->ty));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const TupleNode* 
node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->ty));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->fields));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
TupleGetItemNode* node) {
+  // skips: index
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->ty));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->tuple));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
TensorLoadNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->ty));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->source));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->indices));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const VarNode* 
node) {
+  // Primitive types have no children; dynamic type fields are visited.
+  if (!node->ty.as<PrimTypeNode>()) {
+    // Clamp Simple for dynamic type fields; Pattern continues through them.
+    if (this->def_region_kind() == kTVMFFIDefRegionKindSimple) {
+      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->WithDefRegionKind(
+          kTVMFFIDefRegionKindNone, [&]() { return 
this->VisitExpected(node->ty); }));
+    } else {
+      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->ty));
+    }
+  }
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
GlobalVarNode* node) {
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const CallNode* 
node) {
+  // Skip constant attrs and primitive result types.
+  if (!node->ty.as<PrimTypeNode>()) {
+    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->ty));
+  }
+  // Interned operators have no children; function-valued operators are 
visited.
+  if (!node->op.as<OpNode>()) {
+    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->op));
+  }
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->args));
+  // Empty type arguments are skipped, including the container callback.
+  if (!node->ty_args.empty()) {
+    TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->ty_args));
+  }
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const IntImmNode* 
node) {
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
FloatImmNode* node) {
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const OpNode* 
node) {
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::StringImmNode* node) {
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::CastNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->value));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::AddNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::SubNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::MulNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::DivNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::ModNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::FloorDivNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::FloorModNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::MinNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::MaxNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::EQNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::NENode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::LTNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::LENode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::GTNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::GENode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::AndNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::OrNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->b));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::NotNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->a));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::SelectNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->condition));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->true_value));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->false_value));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::LetNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->WithDefRegionKind(
+      kTVMFFIDefRegionKindSimple, [&]() { return 
this->VisitExpected(node->var); }));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->value));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->body));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::RampNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->base));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->stride));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->lanes));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::BroadcastNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->value));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->lanes));
+  return std::nullopt;
+}
+
+Expected<ffi::Optional<VisitInterrupt>> ExprVisitor::Visit_(const 
prim::ShuffleNode* node) {
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->vectors));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(this->VisitExpected(node->indices));
+  return std::nullopt;
+}
+
+void ExprMutator::InitVTable(VTable* vtable) {
+  ObjectMutator::InitVTable(vtable);
+  SetDispatch<ExprMutator, OpaqueExprNode>(vtable);
+  SetDispatch<ExprMutator, TupleNode>(vtable);
+  SetDispatch<ExprMutator, TupleGetItemNode>(vtable);
+  SetDispatch<ExprMutator, TensorLoadNode>(vtable);
+  SetDispatch<ExprMutator, VarNode>(vtable);
+  SetDispatch<ExprMutator, GlobalVarNode>(vtable);
+  SetDispatch<ExprMutator, CallNode>(vtable);
+  SetDispatch<ExprMutator, IntImmNode>(vtable);
+  SetDispatch<ExprMutator, FloatImmNode>(vtable);
+  SetDispatch<ExprMutator, OpNode>(vtable);
+  SetDispatch<ExprMutator, prim::StringImmNode>(vtable);
+  SetDispatch<ExprMutator, prim::CastNode>(vtable);
+  SetDispatch<ExprMutator, prim::AddNode>(vtable);
+  SetDispatch<ExprMutator, prim::SubNode>(vtable);
+  SetDispatch<ExprMutator, prim::MulNode>(vtable);
+  SetDispatch<ExprMutator, prim::DivNode>(vtable);
+  SetDispatch<ExprMutator, prim::ModNode>(vtable);
+  SetDispatch<ExprMutator, prim::FloorDivNode>(vtable);
+  SetDispatch<ExprMutator, prim::FloorModNode>(vtable);
+  SetDispatch<ExprMutator, prim::MinNode>(vtable);
+  SetDispatch<ExprMutator, prim::MaxNode>(vtable);
+  SetDispatch<ExprMutator, prim::EQNode>(vtable);
+  SetDispatch<ExprMutator, prim::NENode>(vtable);
+  SetDispatch<ExprMutator, prim::LTNode>(vtable);
+  SetDispatch<ExprMutator, prim::LENode>(vtable);
+  SetDispatch<ExprMutator, prim::GTNode>(vtable);
+  SetDispatch<ExprMutator, prim::GENode>(vtable);
+  SetDispatch<ExprMutator, prim::AndNode>(vtable);
+  SetDispatch<ExprMutator, prim::OrNode>(vtable);
+  SetDispatch<ExprMutator, prim::NotNode>(vtable);
+  SetDispatch<ExprMutator, prim::SelectNode>(vtable);
+  SetDispatch<ExprMutator, prim::LetNode>(vtable);
+  SetDispatch<ExprMutator, prim::RampNode>(vtable);
+  SetDispatch<ExprMutator, prim::BroadcastNode>(vtable);
+  SetDispatch<ExprMutator, prim::ShuffleNode>(vtable);
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const OpaqueExprNode* 
node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<Type>, ty, 
this->MaybeInplaceMutateIfUniqueExpected(node->ty, allow_inplace));
+  if (ty.UnchangedOrSameAs(node->ty)) return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<OpaqueExprNode*>(node);
+    if (!ty.IsUnchanged()) writable->ty = std::move(ty).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<OpaqueExprNode>(*node);
+    copy->ty = std::move(ty).ValueOrUnchanged(std::move(copy->ty));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const TupleNode* node, 
bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<Type>, ty, 
this->MaybeInplaceMutateIfUniqueExpected(node->ty, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<ffi::Array<Expr>>, fields,
+      this->MaybeInplaceMutateIfUniqueExpected(node->fields, allow_inplace));
+  if (ty.UnchangedOrSameAs(node->ty) && fields.UnchangedOrSameAs(node->fields))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<TupleNode*>(node);
+    if (!ty.IsUnchanged()) writable->ty = std::move(ty).ValueUnchecked();
+    if (!fields.IsUnchanged()) writable->fields = 
std::move(fields).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<TupleNode>(*node);
+    copy->ty = std::move(ty).ValueOrUnchanged(std::move(copy->ty));
+    copy->fields = std::move(fields).ValueOrUnchanged(std::move(copy->fields));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const TupleGetItemNode* 
node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<Type>, ty, 
this->MaybeInplaceMutateIfUniqueExpected(node->ty, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, tuple,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->tuple, allow_inplace));
+  if (ty.UnchangedOrSameAs(node->ty) && tuple.UnchangedOrSameAs(node->tuple))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<TupleGetItemNode*>(node);
+    if (!ty.IsUnchanged()) writable->ty = std::move(ty).ValueUnchecked();
+    if (!tuple.IsUnchanged()) writable->tuple = 
std::move(tuple).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<TupleGetItemNode>(*node);
+    copy->ty = std::move(ty).ValueOrUnchanged(std::move(copy->ty));
+    copy->tuple = std::move(tuple).ValueOrUnchanged(std::move(copy->tuple));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const TensorLoadNode* 
node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<Type>, ty, 
this->MaybeInplaceMutateIfUniqueExpected(node->ty, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<Expr>, source, 
MaybeInplaceMutateIfUniqueExpected(node->source, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<ffi::Array<PrimExpr>>, indices,
+      this->MaybeInplaceMutateIfUniqueExpected(node->indices, allow_inplace));
+  if (ty.UnchangedOrSameAs(node->ty) && source.UnchangedOrSameAs(node->source) 
&&
+      indices.UnchangedOrSameAs(node->indices))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<TensorLoadNode*>(node);
+    if (!ty.IsUnchanged()) writable->ty = std::move(ty).ValueUnchecked();
+    if (!source.IsUnchanged()) writable->source = 
std::move(source).ValueUnchecked();
+    if (!indices.IsUnchanged()) writable->indices = 
std::move(indices).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<TensorLoadNode>(*node);
+    copy->ty = std::move(ty).ValueOrUnchanged(std::move(copy->ty));
+    copy->source = std::move(source).ValueOrUnchanged(std::move(copy->source));
+    copy->indices = 
std::move(indices).ValueOrUnchanged(std::move(copy->indices));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const GlobalVarNode* node,
+                                                     bool allow_inplace) {
+  // Registry atoms and constant leaves do not descend into metadata or types.
+  return ffi::Unchanged();
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const CallNode* node, 
bool allow_inplace) {
+  UnchangedOr<Type> ty = ffi::Unchanged();
+  if (!node->ty.as<PrimTypeNode>()) {
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+        UnchangedOr<Type>, mapped_ty,
+        this->MaybeInplaceMutateIfUniqueExpected(node->ty, allow_inplace));
+    ty = std::move(mapped_ty);
+  }
+  UnchangedOr<Expr> op = ffi::Unchanged();
+  if (!node->op.as<OpNode>()) {
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, mapped_op,
+                                      
MaybeInplaceMutateIfUniqueExpected(node->op, allow_inplace));
+    op = std::move(mapped_op);
+  }
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<ffi::Array<Expr>>, args,
+      this->MaybeInplaceMutateIfUniqueExpected(node->args, allow_inplace));
+  UnchangedOr<ffi::Array<Type>> ty_args = ffi::Unchanged();
+  if (!node->ty_args.empty()) {
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+        UnchangedOr<ffi::Array<Type>>, mapped_ty_args,
+        this->MaybeInplaceMutateIfUniqueExpected(node->ty_args, 
allow_inplace));
+    ty_args = std::move(mapped_ty_args);
+  }
+  if (ty.UnchangedOrSameAs(node->ty) && op.UnchangedOrSameAs(node->op) &&
+      args.UnchangedOrSameAs(node->args) && 
ty_args.UnchangedOrSameAs(node->ty_args))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<CallNode*>(node);
+    if (!ty.IsUnchanged()) writable->ty = std::move(ty).ValueUnchecked();
+    if (!op.IsUnchanged()) writable->op = std::move(op).ValueUnchecked();
+    if (!args.IsUnchanged()) writable->args = std::move(args).ValueUnchecked();
+    if (!ty_args.IsUnchanged()) writable->ty_args = 
std::move(ty_args).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<CallNode>(*node);
+    copy->ty = std::move(ty).ValueOrUnchanged(std::move(copy->ty));
+    copy->op = std::move(op).ValueOrUnchanged(std::move(copy->op));
+    copy->args = std::move(args).ValueOrUnchanged(std::move(copy->args));
+    copy->ty_args = 
std::move(ty_args).ValueOrUnchanged(std::move(copy->ty_args));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const IntImmNode* node, 
bool allow_inplace) {
+  // Registry atoms and constant leaves do not descend into metadata or types.
+  return ffi::Unchanged();
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const FloatImmNode* node, 
bool allow_inplace) {
+  // Registry atoms and constant leaves do not descend into metadata or types.
+  return ffi::Unchanged();
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const OpNode* node, bool 
allow_inplace) {
+  // Registry atoms and constant leaves do not descend into metadata or types.
+  return ffi::Unchanged();
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const 
prim::StringImmNode* node,
+                                                     bool allow_inplace) {
+  // Registry atoms and constant leaves do not descend into metadata or types.
+  return ffi::Unchanged();
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const prim::CastNode* 
node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, value,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->value, allow_inplace));
+  if (value.UnchangedOrSameAs(node->value)) return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<prim::CastNode*>(node);
+    if (!value.IsUnchanged()) writable->value = 
std::move(value).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<prim::CastNode>(*node);
+    copy->value = std::move(value).ValueOrUnchanged(std::move(copy->value));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+#define TVM_IR_BINARY_MUTATE_IMPL(Name)                                        
                    \
+  Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const prim::Name##Node* 
node,               \
+                                                       bool allow_inplace) {   
                    \
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, a,                
                    \
+                                      
MaybeInplaceMutateIfUniqueExpected(node->a, allow_inplace)); \
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, b,                
                    \
+                                      
MaybeInplaceMutateIfUniqueExpected(node->b, allow_inplace)); \
+    if (a.UnchangedOrSameAs(node->a) && b.UnchangedOrSameAs(node->b)) return 
ffi::Unchanged();     \
+    if (allow_inplace) {                                                       
                    \
+      auto* writable = const_cast<prim::Name##Node*>(node);                    
                    \
+      if (!a.IsUnchanged()) writable->a = std::move(a).ValueUnchecked();       
                    \
+      if (!b.IsUnchanged()) writable->b = std::move(b).ValueUnchecked();       
                    \
+      return ffi::Unchanged();                                                 
                    \
+    } else {                                                                   
                    \
+      auto copy = ffi::make_object<prim::Name##Node>(*node);                   
                    \
+      copy->a = std::move(a).ValueOrUnchanged(std::move(copy->a));             
                    \
+      copy->b = std::move(b).ValueOrUnchanged(std::move(copy->b));             
                    \
+      return ffi::Any(Expr(std::move(copy)));                                  
                    \
+    }                                                                          
                    \
+  }
+TVM_IR_BINARY_MUTATE_IMPL(Add)
+TVM_IR_BINARY_MUTATE_IMPL(Sub)
+TVM_IR_BINARY_MUTATE_IMPL(Mul)
+TVM_IR_BINARY_MUTATE_IMPL(Div)
+TVM_IR_BINARY_MUTATE_IMPL(Mod)
+TVM_IR_BINARY_MUTATE_IMPL(FloorDiv)
+TVM_IR_BINARY_MUTATE_IMPL(FloorMod)
+TVM_IR_BINARY_MUTATE_IMPL(Min)
+TVM_IR_BINARY_MUTATE_IMPL(Max)
+TVM_IR_BINARY_MUTATE_IMPL(EQ)
+TVM_IR_BINARY_MUTATE_IMPL(NE)
+TVM_IR_BINARY_MUTATE_IMPL(LT)
+TVM_IR_BINARY_MUTATE_IMPL(LE)
+TVM_IR_BINARY_MUTATE_IMPL(GT)
+TVM_IR_BINARY_MUTATE_IMPL(GE)
+TVM_IR_BINARY_MUTATE_IMPL(And)
+TVM_IR_BINARY_MUTATE_IMPL(Or)
+#undef TVM_IR_BINARY_MUTATE_IMPL
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const prim::NotNode* node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, a,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->a, allow_inplace));
+  if (a.UnchangedOrSameAs(node->a)) return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<prim::NotNode*>(node);
+    if (!a.IsUnchanged()) writable->a = std::move(a).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<prim::NotNode>(*node);
+    copy->a = std::move(a).ValueOrUnchanged(std::move(copy->a));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const prim::SelectNode* 
node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<PrimExpr>, condition,
+      MaybeInplaceMutateIfUniqueExpected(node->condition, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<PrimExpr>, true_value,
+      MaybeInplaceMutateIfUniqueExpected(node->true_value, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<PrimExpr>, false_value,
+      MaybeInplaceMutateIfUniqueExpected(node->false_value, allow_inplace));
+  if (condition.UnchangedOrSameAs(node->condition) &&
+      true_value.UnchangedOrSameAs(node->true_value) &&
+      false_value.UnchangedOrSameAs(node->false_value))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<prim::SelectNode*>(node);
+    if (!condition.IsUnchanged()) writable->condition = 
std::move(condition).ValueUnchecked();
+    if (!true_value.IsUnchanged()) writable->true_value = 
std::move(true_value).ValueUnchecked();
+    if (!false_value.IsUnchanged()) writable->false_value = 
std::move(false_value).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<prim::SelectNode>(*node);
+    copy->condition = 
std::move(condition).ValueOrUnchanged(std::move(copy->condition));
+    copy->true_value = 
std::move(true_value).ValueOrUnchanged(std::move(copy->true_value));
+    copy->false_value = 
std::move(false_value).ValueOrUnchanged(std::move(copy->false_value));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const prim::LetNode* node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<Var>, var, WithDefRegionKind(kTVMFFIDefRegionKindSimple, [&] 
{
+        return MaybeInplaceMutateIfUniqueExpected(node->var, allow_inplace);
+      }));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, value,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->value, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, body,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->body, allow_inplace));
+  if (var.UnchangedOrSameAs(node->var) && value.UnchangedOrSameAs(node->value) 
&&
+      body.UnchangedOrSameAs(node->body))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<prim::LetNode*>(node);
+    if (!var.IsUnchanged()) writable->var = std::move(var).ValueUnchecked();
+    if (!value.IsUnchanged()) writable->value = 
std::move(value).ValueUnchecked();
+    if (!body.IsUnchanged()) writable->body = std::move(body).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<prim::LetNode>(*node);
+    copy->var = std::move(var).ValueOrUnchanged(std::move(copy->var));
+    copy->value = std::move(value).ValueOrUnchanged(std::move(copy->value));
+    copy->body = std::move(body).ValueOrUnchanged(std::move(copy->body));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const prim::RampNode* 
node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, base,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->base, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<PrimExpr>, stride,
+      MaybeInplaceMutateIfUniqueExpected(node->stride, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, lanes,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->lanes, allow_inplace));
+  if (base.UnchangedOrSameAs(node->base) && 
stride.UnchangedOrSameAs(node->stride) &&
+      lanes.UnchangedOrSameAs(node->lanes))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<prim::RampNode*>(node);
+    if (!base.IsUnchanged()) writable->base = std::move(base).ValueUnchecked();
+    if (!stride.IsUnchanged()) writable->stride = 
std::move(stride).ValueUnchecked();
+    if (!lanes.IsUnchanged()) writable->lanes = 
std::move(lanes).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<prim::RampNode>(*node);
+    copy->base = std::move(base).ValueOrUnchanged(std::move(copy->base));
+    copy->stride = std::move(stride).ValueOrUnchanged(std::move(copy->stride));
+    copy->lanes = std::move(lanes).ValueOrUnchanged(std::move(copy->lanes));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const 
prim::BroadcastNode* node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, value,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->value, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<PrimExpr>, lanes,
+                                    
MaybeInplaceMutateIfUniqueExpected(node->lanes, allow_inplace));
+  if (value.UnchangedOrSameAs(node->value) && 
lanes.UnchangedOrSameAs(node->lanes))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<prim::BroadcastNode*>(node);
+    if (!value.IsUnchanged()) writable->value = 
std::move(value).ValueUnchecked();
+    if (!lanes.IsUnchanged()) writable->lanes = 
std::move(lanes).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<prim::BroadcastNode>(*node);
+    copy->value = std::move(value).ValueOrUnchanged(std::move(copy->value));
+    copy->lanes = std::move(lanes).ValueOrUnchanged(std::move(copy->lanes));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const prim::ShuffleNode* 
node,
+                                                     bool allow_inplace) {
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<ffi::Array<PrimExpr>>, vectors,
+      this->MaybeInplaceMutateIfUniqueExpected(node->vectors, allow_inplace));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      UnchangedOr<ffi::Array<PrimExpr>>, indices,
+      this->MaybeInplaceMutateIfUniqueExpected(node->indices, allow_inplace));
+  if (vectors.UnchangedOrSameAs(node->vectors) && 
indices.UnchangedOrSameAs(node->indices))
+    return ffi::Unchanged();
+  if (allow_inplace) {
+    auto* writable = const_cast<prim::ShuffleNode*>(node);
+    if (!vectors.IsUnchanged()) writable->vectors = 
std::move(vectors).ValueUnchecked();
+    if (!indices.IsUnchanged()) writable->indices = 
std::move(indices).ValueUnchecked();
+    return ffi::Unchanged();
+  } else {
+    auto copy = ffi::make_object<prim::ShuffleNode>(*node);
+    copy->vectors = 
std::move(vectors).ValueOrUnchanged(std::move(copy->vectors));
+    copy->indices = 
std::move(indices).ValueOrUnchanged(std::move(copy->indices));
+    return ffi::Any(Expr(std::move(copy)));
+  }
+}
+
+Expected<UnchangedOr<ffi::Any>> ExprMutator::Mutate_(const VarNode* node, bool 
allow_inplace) {
+  if (node->ty.as<PrimTypeNode>()) return ffi::Unchanged();
+  if (TVM_FFI_PREDICT_TRUE(var_remap_.empty() && def_region_kind() == 
kTVMFFIDefRegionKindNone)) {
+    return ffi::Unchanged();
+  }
+  Expected<ffi::Any> remap_result = VarRemapGetExpected(ffi::AnyView(node));
+  TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(remap_result);
+  if (ffi::details::ExpectedUnsafe::GetData(remap_result).type_index() !=
+      ffi::TypeIndex::kTVMFFINone) {
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Expr>, mapped, 
std::move(remap_result));
+    return mapped;
+  }
+  if (def_region_kind() == kTVMFFIDefRegionKindNone) return ffi::Unchanged();
+  UnchangedOr<ffi::Any> result = ffi::Unchanged();
+  ffi::Any mapped_value = ffi::Unchanged();
+  // PrimType has no children; dynamic type fields inherit Pattern but are 
visited outside Simple.
+  if (!node->ty.as<PrimTypeNode>()) {
+    Expected<UnchangedOr<ffi::Any>> mapped_ty_result =
+        def_region_kind() == kTVMFFIDefRegionKindSimple
+            ? WithDefRegionKind(
+                  kTVMFFIDefRegionKindNone,
+                  [&] { return 
this->MaybeInplaceMutateIfUniqueExpected(node->ty, allow_inplace); })
+            : this->MaybeInplaceMutateIfUniqueExpected(node->ty, 
allow_inplace);
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(UnchangedOr<Type>, mapped_ty, 
std::move(mapped_ty_result));
+    if (!mapped_ty.UnchangedOrSameAs(node->ty)) {
+      if (allow_inplace) {
+        const_cast<VarNode*>(node)->ty = std::move(mapped_ty).ValueUnchecked();
+        mapped_value = ffi::Any(node);
+      } else {
+        auto copy = ffi::make_object<VarNode>(*node);
+        copy->ty = std::move(mapped_ty).ValueUnchecked();
+        mapped_value = ffi::Any(std::move(copy));
+      }
+      result = mapped_value;
+    }
+  }
+  if (!result.IsUnchanged() || def_region_kind() == 
kTVMFFIDefRegionKindPattern) {
+    auto set_result = VarRemapSetExpected(ffi::AnyView(node), mapped_value);
+    TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(set_result);
+  }
+  return result;
+}
+
+}  // namespace tvm
diff --git a/tests/cpp/expr_functor_test.cc b/tests/cpp/expr_functor_test.cc
new file mode 100644
index 0000000000..06cb2c962e
--- /dev/null
+++ b/tests/cpp/expr_functor_test.cc
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+#include <gtest/gtest.h>
+#include <tvm/ffi/reflection/registry.h>
+#include <tvm/ir/expr_functor.h>
+
+#include <vector>
+
+namespace tvm {
+namespace {
+
+class PairExprNode : public ExprNode {
+ public:
+  Expr left;
+  Expr right;
+  static void RegisterReflection() {
+    ffi::reflection::ObjectDef<PairExprNode>()
+        .def_ro("left", &PairExprNode::left)
+        .def_ro("right", &PairExprNode::right);
+  }
+  TVM_FFI_DECLARE_OBJECT_INFO_FINAL("test.NativePairExpr", PairExprNode, 
ExprNode);
+};
+class PairExpr : public Expr {
+ public:
+  PairExpr(Expr left, Expr right) {
+    auto node = ffi::make_object<PairExprNode>();
+    node->left = std::move(left);
+    node->right = std::move(right);
+    data_ = std::move(node);
+  }
+  TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(PairExpr, Expr, PairExprNode);
+};
+TVM_FFI_STATIC_INIT_BLOCK() { PairExprNode::RegisterReflection(); }
+
+class Collect : public ExprVisitor {
+ public:
+  using ExprVisitor::Visit_;
+  std::vector<int64_t> values;
+  Expected<ffi::Optional<VisitInterrupt>> Visit_(const IntImmNode* node) 
override {
+    values.push_back(node->value);
+    return std::nullopt;
+  }
+};
+
+TEST(ExprVisitor, StructuralFallback) {
+  auto visitor = ffi::make_object<Collect>();
+  PairExpr pair(IntImm::Int32(1), IntImm::Int32(2));
+  auto result = visitor->VisitExpected(pair);
+  ASSERT_TRUE(result.is_ok());
+  EXPECT_FALSE(result.value().has_value());
+  EXPECT_EQ(visitor->values, (std::vector<int64_t>{1, 2}));
+  EXPECT_EQ(pair->left.as<IntImmNode>()->value, 1);
+  EXPECT_EQ(pair->right.as<IntImmNode>()->value, 2);
+}
+
+class Rewrite : public ExprMutator {
+ public:
+  using ExprMutator::Mutate_;
+  Expected<UnchangedOr<ffi::Any>> Mutate_(const IntImmNode* node, bool 
allow_inplace) override {
+    return ffi::Any(IntImm::Int32(node->value + 1));
+  }
+};
+
+TEST(ExprMutator, StructuralFallback) {
+  auto mutator = ffi::make_object<Rewrite>();
+  PairExpr pair(IntImm::Int32(1), IntImm::Int32(2));
+  auto result = mutator->MutateExpected(pair).value();
+  ASSERT_FALSE(result.IsUnchanged());
+  auto replacement = std::move(result).ValueUnchecked();
+  const auto* changed = replacement.as<PairExprNode>();
+  ASSERT_NE(changed, nullptr);
+  EXPECT_EQ(changed->left.as<IntImmNode>()->value, 2);
+  EXPECT_EQ(changed->right.as<IntImmNode>()->value, 3);
+  EXPECT_EQ(pair->left.as<IntImmNode>()->value, 1);
+  EXPECT_EQ(pair->right.as<IntImmNode>()->value, 2);
+}
+
+}  // namespace
+}  // namespace tvm

Reply via email to