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 141a187f35 [REFACTOR][IR] Add structural hooks to remaining Type and 
Relax Expr nodes (#20302)
141a187f35 is described below

commit 141a187f35e9b6263db6665bc99eb03146d9b23f
Author: Tianqi Chen <[email protected]>
AuthorDate: Thu Sep 10 10:37:26 2026 -0400

    [REFACTOR][IR] Add structural hooks to remaining Type and Relax Expr nodes 
(#20302)
    
    Add explicit structural traversal hooks to the remaining concrete Type
    and Relax Expr families so generic structural map and walk operations
    preserve node identity and field semantics.
    
    - Add visit, mutate, and maybe-in-place mutate hooks for the remaining
    reachable concrete Type and Expr nodes in scope, including
    DataflowVarNode.
    - Preserve unchanged-node identity with ffi::Unchanged() and retain
    existing definition-region semantics.
    - Keep internal arithmetic temporaries and other excluded families out
    of scope; concrete Stmt registrations were already complete.
    - Rebase onto current upstream main.
    
    Validation:
    - pre-commit checks pass for all modified source files.
    - Full existing C++ cpptest suite passes 132/132.
---
 src/ir/expr.cc                 |  28 ++-
 src/ir/op.cc                   |  25 +++
 src/ir/type.cc                 | 189 ++++++++++++++++++-
 src/relax/distributed/type.cc  |  64 +++++++
 src/relax/ir/dependent_type.cc | 210 +++++++++++++++++++--
 src/relax/ir/expr.cc           | 408 +++++++++++++++++++++++++++++++++++++++++
 src/relax/ir/type.cc           |  28 ++-
 src/tirx/ir/function.cc        |  73 ++++++++
 src/tirx/ir/stmt.cc            |  20 +-
 9 files changed, 1022 insertions(+), 23 deletions(-)

diff --git a/src/ir/expr.cc b/src/ir/expr.cc
index 1af350c70d..b9b51da089 100644
--- a/src/ir/expr.cc
+++ b/src/ir/expr.cc
@@ -278,6 +278,8 @@ TVMFFIAny 
RangeMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyVi
   return ffi::Unchanged().CopyToTVMFFIAny();
 }
 
+// DataflowVarNode duplicates this protocol because structural hooks do not 
inherit.  Keep the two
+// hook triples in lockstep when changing remap, PrimType-skip, or 
definition-region behavior.
 TVMFFIAny VarVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView value) 
noexcept {
   // skips: name
   const VarNode* self =
@@ -381,6 +383,22 @@ TVMFFIAny VarMaybeInplaceMutate(ffi::StructuralMutatorObj* 
mutator, ffi::AnyView
   return ffi::details::UnchangedOrUnsafe::MoveToTVMFFIAny(std::move(result));
 }
 
+TVMFFIAny GlobalVarVisit(ffi::StructuralVisitorObj*, ffi::AnyView) noexcept {
+  // GlobalVar is a module-level symbol.  name_hint is scalar identity and ty 
is derived from the
+  // referenced function, matching GlobalVarNode's custom structural 
equality/hash definition.
+  // It has no definition site where this hook could establish a VarRemap.  A 
callback that renames
+  // GlobalVars is therefore responsible for returning one stable replacement 
per module symbol.
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny GlobalVarMutate(ffi::StructuralMutatorObj*, ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny GlobalVarMaybeInplaceMutate(ffi::StructuralMutatorObj*, 
ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
 TVMFFIAny CallVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView value) 
noexcept {
   // skips: attrs, constant metadata left untouched like the classic Expr 
functors.
   const CallNode* self =
@@ -859,7 +877,15 @@ GlobalVar::GlobalVar(ffi::String name_hint, Span span) {
   data_ = std::move(n);
 }
 
-TVM_FFI_STATIC_INIT_BLOCK() { GlobalVarNode::RegisterReflection(); }
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  GlobalVarNode::RegisterReflection();
+  refl::TypeAttrDef<GlobalVarNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&GlobalVarVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&GlobalVarMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&GlobalVarMaybeInplaceMutate));
+}
 
 // Call
 Call::Call(Type ret_ty, Expr op, ffi::Array<Expr> args, Attrs attrs, 
ffi::Array<Type> ty_args,
diff --git a/src/ir/op.cc b/src/ir/op.cc
index 560cee6c9d..c300806b74 100644
--- a/src/ir/op.cc
+++ b/src/ir/op.cc
@@ -21,6 +21,8 @@
  * \file src/ir/op.cc
  * \brief Primitive operators and intrinsics.
  */
+#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/ir/op.h>
@@ -33,9 +35,32 @@
 
 namespace tvm {
 
+namespace {
+
+TVMFFIAny OpVisit(ffi::StructuralVisitorObj*, ffi::AnyView) noexcept {
+  // Ops are unique registry atoms.  Avoid reflecting through their registry 
metadata.
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny OpMutate(ffi::StructuralMutatorObj*, ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny OpMaybeInplaceMutate(ffi::StructuralMutatorObj*, ffi::AnyView) 
noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+}  // namespace
+
 TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
   ArgumentInfoNode::RegisterReflection();
   OpNode::RegisterReflection();
+  refl::TypeAttrDef<OpNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&OpVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&OpMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&OpMaybeInplaceMutate));
 }
 
 using ffi::Any;
diff --git a/src/ir/type.cc b/src/ir/type.cc
index d4a56cb476..1947f48985 100644
--- a/src/ir/type.cc
+++ b/src/ir/type.cc
@@ -69,6 +69,32 @@ ffi::ObjectPtr<PrimTypeNode> 
GetCachedPrimTypeNode(DLDataType dtype) {
 
 // Structural traversal hooks
 
+TVMFFIAny TypeVisit(ffi::StructuralVisitorObj*, ffi::AnyView) noexcept {
+  // Type::Missing() is the only concrete TypeNode value; span is ignored 
debug metadata.
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny TypeMutate(ffi::StructuralMutatorObj*, ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny TypeMaybeInplaceMutate(ffi::StructuralMutatorObj*, ffi::AnyView) 
noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny OpaqueTypeVisit(ffi::StructuralVisitorObj*, ffi::AnyView) noexcept {
+  // OpaqueType is a field-less construction-time marker; span is ignored 
debug metadata.
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny OpaqueTypeMutate(ffi::StructuralMutatorObj*, ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny OpaqueTypeMaybeInplaceMutate(ffi::StructuralMutatorObj*, 
ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
 TVMFFIAny PrimTypeVisit(ffi::StructuralVisitorObj*, ffi::AnyView) noexcept {
   // dtype is a constant: reflected for StructuralEqual/Hash,
   // not traversed by the visitor/mutator contract.
@@ -87,6 +113,129 @@ TVMFFIAny 
PrimTypeMaybeInplaceMutate(ffi::StructuralMutatorObj*, ffi::AnyView) n
   return ffi::Unchanged().CopyToTVMFFIAny();
 }
 
+TVMFFIAny PointerTypeVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  // skips: storage_scope (scalar)
+  const PointerTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
PointerTypeNode>(value);
+  
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->element_type));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny PointerTypeMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  // skips: storage_scope (scalar)
+  const PointerTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
PointerTypeNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, 
mapped_element_type,
+                                    
mutator->MutateExpected(self->element_type));
+  if (mapped_element_type.UnchangedOrSameAs(self->element_type)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<PointerTypeNode> copy = 
ffi::make_object<PointerTypeNode>(*self);
+  copy->element_type =
+      
std::move(mapped_element_type).ValueOrUnchanged(std::move(copy->element_type));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny PointerTypeMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                        ffi::AnyView value) noexcept {
+  // skips: storage_scope (scalar)
+  PointerTypeNode* self = const_cast<PointerTypeNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
PointerTypeNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      ffi::UnchangedOr<Type>, mapped_element_type,
+      mutator->MaybeInplaceMutateIfUniqueExpected(self->element_type));
+  if (!mapped_element_type.UnchangedOrSameAs(self->element_type)) {
+    self->element_type = std::move(mapped_element_type).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny FuncTypeVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  const FuncTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FuncTypeNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->arg_types));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ret_type));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny FuncTypeMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  const FuncTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FuncTypeNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<Type>>, 
mapped_arg_types,
+                                    mutator->MutateExpected(self->arg_types));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret_type,
+                                    mutator->MutateExpected(self->ret_type));
+  if (mapped_arg_types.UnchangedOrSameAs(self->arg_types) &&
+      mapped_ret_type.UnchangedOrSameAs(self->ret_type)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<FuncTypeNode> copy = ffi::make_object<FuncTypeNode>(*self);
+  copy->arg_types = 
std::move(mapped_arg_types).ValueOrUnchanged(std::move(copy->arg_types));
+  copy->ret_type = 
std::move(mapped_ret_type).ValueOrUnchanged(std::move(copy->ret_type));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny FuncTypeMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                     ffi::AnyView value) noexcept {
+  FuncTypeNode* self = const_cast<FuncTypeNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FuncTypeNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<Type>>, 
mapped_arg_types,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->arg_types));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret_type,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ret_type));
+  if (!mapped_arg_types.UnchangedOrSameAs(self->arg_types)) {
+    self->arg_types = std::move(mapped_arg_types).ValueUnchecked();
+  }
+  if (!mapped_ret_type.UnchangedOrSameAs(self->ret_type)) {
+    self->ret_type = std::move(mapped_ret_type).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny TupleTypeVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  const TupleTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TupleTypeNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->fields));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny TupleTypeMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  const TupleTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TupleTypeNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<Type>>, 
mapped_fields,
+                                    mutator->MutateExpected(self->fields));
+  if (mapped_fields.UnchangedOrSameAs(self->fields)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<TupleTypeNode> copy = ffi::make_object<TupleTypeNode>(*self);
+  copy->fields = 
std::move(mapped_fields).ValueOrUnchanged(std::move(copy->fields));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny TupleTypeMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                      ffi::AnyView value) noexcept {
+  TupleTypeNode* self = const_cast<TupleTypeNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TupleTypeNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<Type>>, 
mapped_fields,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->fields));
+  if (!mapped_fields.UnchangedOrSameAs(self->fields)) {
+    self->fields = std::move(mapped_fields).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny TensorMapTypeVisit(ffi::StructuralVisitorObj*, ffi::AnyView) 
noexcept {
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny TensorMapTypeMutate(ffi::StructuralMutatorObj*, ffi::AnyView) 
noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny TensorMapTypeMaybeInplaceMutate(ffi::StructuralMutatorObj*, 
ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
 }  // namespace
 
 Type Type::Missing() {
@@ -102,9 +251,25 @@ bool Type::IsMissing() const { return 
this->same_as(Type::Missing()); }
 
 OpaqueType::OpaqueType() : Type(ffi::UnsafeInit{}) { data_ = 
ffi::make_object<OpaqueTypeNode>(); }
 
-TVM_FFI_STATIC_INIT_BLOCK() { TypeNode::RegisterReflection(); }
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  TypeNode::RegisterReflection();
+  refl::TypeAttrDef<TypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&TypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&TypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&TypeMaybeInplaceMutate));
+}
 
-TVM_FFI_STATIC_INIT_BLOCK() { OpaqueTypeNode::RegisterReflection(); }
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  OpaqueTypeNode::RegisterReflection();
+  refl::TypeAttrDef<OpaqueTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&OpaqueTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&OpaqueTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&OpaqueTypeMaybeInplaceMutate));
+}
 
 // PrimType
 PrimType::PrimType(DLDataType dtype) : Type(ffi::UnsafeInit{}) {
@@ -227,6 +392,11 @@ TVM_FFI_STATIC_INIT_BLOCK() {
   refl::GlobalDef().def("ir.PointerType", [](Type element_type, ffi::String 
storage_scope = "") {
     return PointerType(element_type, storage_scope);
   });
+  refl::TypeAttrDef<PointerTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&PointerTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&PointerTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&PointerTypeMaybeInplaceMutate));
 }
 
 TVM_FFI_STATIC_INIT_BLOCK() {
@@ -235,6 +405,11 @@ TVM_FFI_STATIC_INIT_BLOCK() {
   refl::GlobalDef().def("ir.FuncType", [](tvm::ffi::Array<Type> arg_types, 
Type ret_type) {
     return FuncType(arg_types, ret_type);
   });
+  refl::TypeAttrDef<FuncTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&FuncTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&FuncTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&FuncTypeMaybeInplaceMutate));
 }
 
 TVM_FFI_STATIC_INIT_BLOCK() {
@@ -245,6 +420,16 @@ TVM_FFI_STATIC_INIT_BLOCK() {
       .def("ir.TupleType",
            [](ffi::Array<Type> fields, Span span) { return TupleType(fields, 
span); })
       .def("ir.TensorMapType", [](Span span) { return TensorMapType(span); });
+  refl::TypeAttrDef<TupleTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&TupleTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&TupleTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&TupleTypeMaybeInplaceMutate));
+  refl::TypeAttrDef<TensorMapTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&TensorMapTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&TensorMapTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&TensorMapTypeMaybeInplaceMutate));
 }
 
 }  // namespace tvm
diff --git a/src/relax/distributed/type.cc b/src/relax/distributed/type.cc
index 8fb164c2a8..0a48faad2c 100644
--- a/src/relax/distributed/type.cc
+++ b/src/relax/distributed/type.cc
@@ -22,16 +22,80 @@
  * \brief Relax DTensor type.
  */
 
+#include <tvm/ffi/extra/structural_mutate.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/relax/distributed/type.h>
 namespace tvm {
 namespace relax {
 namespace distributed {
 
+namespace {
+
+TVMFFIAny DTensorTypeVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  const DTensorTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
DTensorTypeNode>(value);
+  
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->device_mesh));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->placement));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->tensor_ty));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny DTensorTypeMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  const DTensorTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
DTensorTypeNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<DeviceMesh>, 
mapped_device_mesh,
+                                    
mutator->MutateExpected(self->device_mesh));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Placement>, 
mapped_placement,
+                                    mutator->MutateExpected(self->placement));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<TensorType>, 
mapped_tensor_ty,
+                                    mutator->MutateExpected(self->tensor_ty));
+  if (mapped_device_mesh.UnchangedOrSameAs(self->device_mesh) &&
+      mapped_placement.UnchangedOrSameAs(self->placement) &&
+      mapped_tensor_ty.UnchangedOrSameAs(self->tensor_ty)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<DTensorTypeNode> copy = 
ffi::make_object<DTensorTypeNode>(*self);
+  copy->device_mesh = 
std::move(mapped_device_mesh).ValueOrUnchanged(std::move(copy->device_mesh));
+  copy->placement = 
std::move(mapped_placement).ValueOrUnchanged(std::move(copy->placement));
+  copy->tensor_ty = 
std::move(mapped_tensor_ty).ValueOrUnchanged(std::move(copy->tensor_ty));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny DTensorTypeMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                        ffi::AnyView value) noexcept {
+  DTensorTypeNode* self = const_cast<DTensorTypeNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
DTensorTypeNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<DeviceMesh>, 
mapped_device_mesh,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->device_mesh));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Placement>, 
mapped_placement,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->placement));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<TensorType>, 
mapped_tensor_ty,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->tensor_ty));
+  if (!mapped_device_mesh.UnchangedOrSameAs(self->device_mesh)) {
+    self->device_mesh = std::move(mapped_device_mesh).ValueUnchecked();
+  }
+  if (!mapped_placement.UnchangedOrSameAs(self->placement)) {
+    self->placement = std::move(mapped_placement).ValueUnchecked();
+  }
+  if (!mapped_tensor_ty.UnchangedOrSameAs(self->tensor_ty)) {
+    self->tensor_ty = std::move(mapped_tensor_ty).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+}  // namespace
+
 TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
   DTensorTypeNode::RegisterReflection();
   PlacementNode::RegisterReflection();
   PlacementSpecNode::RegisterReflection();
+  refl::TypeAttrDef<DTensorTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&DTensorTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&DTensorTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&DTensorTypeMaybeInplaceMutate));
 }
 
 PlacementSpec PlacementSpec::Sharding(int axis) {
diff --git a/src/relax/ir/dependent_type.cc b/src/relax/ir/dependent_type.cc
index ff0bce1fb6..14755ca573 100644
--- a/src/relax/ir/dependent_type.cc
+++ b/src/relax/ir/dependent_type.cc
@@ -21,6 +21,8 @@
  * \file src/relax/ir/dependent_type.cc
  * \brief Relax type nodes.
  */
+#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/relax/analysis.h>
@@ -30,29 +32,203 @@
 namespace tvm {
 namespace relax {
 
+namespace {
+
+TVMFFIAny AnyTypeVisit(ffi::StructuralVisitorObj*, ffi::AnyView) noexcept {
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny AnyTypeMutate(ffi::StructuralMutatorObj*, ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny AnyTypeMaybeInplaceMutate(ffi::StructuralMutatorObj*, ffi::AnyView) 
noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny ShapeTypeVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  // skips: ndim (scalar)
+  const ShapeTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
ShapeTypeNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->values));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny ShapeTypeMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  // skips: ndim (scalar)
+  const ShapeTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
ShapeTypeNode>(value);
+  
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<ffi::Array<PrimExpr>>>,
+                                    mapped_values, 
mutator->MutateExpected(self->values));
+  if (mapped_values.UnchangedOrSameAs(self->values)) return 
ffi::Unchanged().CopyToTVMFFIAny();
+  ffi::ObjectPtr<ShapeTypeNode> copy = ffi::make_object<ShapeTypeNode>(*self);
+  copy->values = 
std::move(mapped_values).ValueOrUnchanged(std::move(copy->values));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny ShapeTypeMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                      ffi::AnyView value) noexcept {
+  // skips: ndim (scalar)
+  ShapeTypeNode* self = const_cast<ShapeTypeNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
ShapeTypeNode>(value));
+  
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<ffi::Array<PrimExpr>>>,
+                                    mapped_values,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->values));
+  if (!mapped_values.UnchangedOrSameAs(self->values)) {
+    self->values = std::move(mapped_values).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny TensorTypeVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  // skips: ndim (scalar)
+  const TensorTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TensorTypeNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->shape));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->dtype));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->vdevice));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny TensorTypeMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  // skips: ndim (scalar)
+  const TensorTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TensorTypeNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<Expr>>, 
mapped_shape,
+                                    mutator->MutateExpected(self->shape));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<PrimType>>, 
mapped_dtype,
+                                    mutator->MutateExpected(self->dtype));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<VDevice>>, 
mapped_vdevice,
+                                    mutator->MutateExpected(self->vdevice));
+  if (mapped_shape.UnchangedOrSameAs(self->shape) && 
mapped_dtype.UnchangedOrSameAs(self->dtype) &&
+      mapped_vdevice.UnchangedOrSameAs(self->vdevice)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<TensorTypeNode> copy = 
ffi::make_object<TensorTypeNode>(*self);
+  copy->shape = 
std::move(mapped_shape).ValueOrUnchanged(std::move(copy->shape));
+  copy->dtype = 
std::move(mapped_dtype).ValueOrUnchanged(std::move(copy->dtype));
+  copy->vdevice = 
std::move(mapped_vdevice).ValueOrUnchanged(std::move(copy->vdevice));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny TensorTypeMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                       ffi::AnyView value) noexcept {
+  // skips: ndim (scalar)
+  TensorTypeNode* self = const_cast<TensorTypeNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TensorTypeNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<Expr>>, 
mapped_shape,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->shape));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<PrimType>>, 
mapped_dtype,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->dtype));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Optional<VDevice>>, 
mapped_vdevice,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->vdevice));
+  if (!mapped_shape.UnchangedOrSameAs(self->shape)) {
+    self->shape = std::move(mapped_shape).ValueUnchecked();
+  }
+  if (!mapped_dtype.UnchangedOrSameAs(self->dtype)) {
+    self->dtype = std::move(mapped_dtype).ValueUnchecked();
+  }
+  if (!mapped_vdevice.UnchangedOrSameAs(self->vdevice)) {
+    self->vdevice = std::move(mapped_vdevice).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny FuncTypeVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  // skips: derive_func (environment-backed callable metadata), purity (scalar)
+  const FuncTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FuncTypeNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->WithDefRegionKind(
+      kTVMFFIDefRegionKindPattern, [&]() { return 
visitor->VisitExpected(self->params); }));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ret));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny FuncTypeMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  // skips: derive_func (environment-backed callable metadata), purity (scalar)
+  const FuncTypeNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FuncTypeNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      ffi::UnchangedOr<ffi::Optional<ffi::Array<Type>>>, mapped_params,
+      mutator->WithDefRegionKind(kTVMFFIDefRegionKindPattern,
+                                 [&]() { return 
mutator->MutateExpected(self->params); }));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret,
+                                    mutator->MutateExpected(self->ret));
+  if (mapped_params.UnchangedOrSameAs(self->params) && 
mapped_ret.UnchangedOrSameAs(self->ret)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<FuncTypeNode> copy = ffi::make_object<FuncTypeNode>(*self);
+  copy->params = 
std::move(mapped_params).ValueOrUnchanged(std::move(copy->params));
+  copy->ret = std::move(mapped_ret).ValueOrUnchanged(std::move(copy->ret));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny FuncTypeMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                     ffi::AnyView value) noexcept {
+  // skips: derive_func (environment-backed callable metadata), purity (scalar)
+  FuncTypeNode* self = const_cast<FuncTypeNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FuncTypeNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      ffi::UnchangedOr<ffi::Optional<ffi::Array<Type>>>, mapped_params,
+      mutator->WithDefRegionKind(kTVMFFIDefRegionKindPattern, [&]() {
+        return mutator->MaybeInplaceMutateIfUniqueExpected(self->params);
+      }));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ret));
+  if (!mapped_params.UnchangedOrSameAs(self->params)) {
+    self->params = std::move(mapped_params).ValueUnchecked();
+  }
+  if (!mapped_ret.UnchangedOrSameAs(self->ret)) {
+    self->ret = std::move(mapped_ret).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+}  // namespace
+
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
   AnyTypeNode::RegisterReflection();
   ShapeTypeNode::RegisterReflection();
   TensorTypeNode::RegisterReflection();
   FuncTypeNode::RegisterReflection();
-  refl::TypeAttrDef<TensorTypeNode>().def(
-      "__subscript_expr_realize__",
-      [](Expr value,
-         ffi::Array<ffi::Variant<
-             ffi::Tuple<ffi::Optional<PrimExpr>, ffi::Optional<PrimExpr>, 
ffi::Optional<PrimExpr>>,
-             PrimExpr>>
-             slice,
-         Span span) -> ffi::ObjectRef {
-        TVM_FFI_CHECK_EQ(slice.size(), 1, IndexError)
-            << "A Relax expression requires exactly one index";
-        auto index = slice[0].as<PrimExpr>();
-        TVM_FFI_CHECK(index.has_value(), TypeError) << "A Relax expression 
requires a point index";
-        const auto* imm = index.value().as<IntImmNode>();
-        TVM_FFI_CHECK(imm != nullptr, TypeError)
-            << "A Relax expression requires a constant integer index";
-        return TupleGetItem(value, static_cast<int>(imm->value), span);
-      });
+  refl::TypeAttrDef<AnyTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&AnyTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&AnyTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&AnyTypeMaybeInplaceMutate));
+  refl::TypeAttrDef<ShapeTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&ShapeTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&ShapeTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&ShapeTypeMaybeInplaceMutate));
+  refl::TypeAttrDef<TensorTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&TensorTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&TensorTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&TensorTypeMaybeInplaceMutate))
+      .def("__subscript_expr_realize__",
+           [](Expr value,
+              ffi::Array<ffi::Variant<ffi::Tuple<ffi::Optional<PrimExpr>, 
ffi::Optional<PrimExpr>,
+                                                 ffi::Optional<PrimExpr>>,
+                                      PrimExpr>>
+                  slice,
+              Span span) -> ffi::ObjectRef {
+             TVM_FFI_CHECK_EQ(slice.size(), 1, IndexError)
+                 << "A Relax expression requires exactly one index";
+             auto index = slice[0].as<PrimExpr>();
+             TVM_FFI_CHECK(index.has_value(), TypeError)
+                 << "A Relax expression requires a point index";
+             const auto* imm = index.value().as<IntImmNode>();
+             TVM_FFI_CHECK(imm != nullptr, TypeError)
+                 << "A Relax expression requires a constant integer index";
+             return TupleGetItem(value, static_cast<int>(imm->value), span);
+           });
+  refl::TypeAttrDef<FuncTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&FuncTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&FuncTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&FuncTypeMaybeInplaceMutate));
 }
 
 AnyType::AnyType(Span span) : Type(ffi::UnsafeInit{}) {
diff --git a/src/relax/ir/expr.cc b/src/relax/ir/expr.cc
index b21cdbad87..d841d7817a 100644
--- a/src/relax/ir/expr.cc
+++ b/src/relax/ir/expr.cc
@@ -16,6 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+#include <tvm/ffi/extra/structural_mutate.h>
+#include <tvm/ffi/extra/structural_visit.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/relax/analysis.h>
 #include <tvm/relax/block_builder.h>
@@ -27,7 +29,360 @@
 namespace tvm {
 namespace relax {
 
+namespace {
+
+// Traverses only ExprNode::ty.  The per-node payload is an intentional 
constant leaf:
+// ConstantNode::data is tensor data; StringImmNode::value and 
DataTypeImmNode::value are scalars;
+// ExternFuncNode::global_symbol is scalar and BaseFuncNode::attrs is metadata.
+template <typename TNode>
+TVMFFIAny TypeOnlyExprVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  const TNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ty));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+template <typename TNode>
+TVMFFIAny TypeOnlyExprMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  const TNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    mutator->MutateExpected(self->ty));
+  if (mapped_ty.UnchangedOrSameAs(self->ty)) return 
ffi::Unchanged().CopyToTVMFFIAny();
+  ffi::ObjectPtr<TNode> copy = ffi::make_object<TNode>(*self);
+  copy->ty = std::move(mapped_ty).ValueOrUnchanged(std::move(copy->ty));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+template <typename TNode>
+TVMFFIAny TypeOnlyExprMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                         ffi::AnyView value) noexcept {
+  TNode* self = const_cast<TNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
TNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ty));
+  if (!mapped_ty.UnchangedOrSameAs(self->ty)) {
+    self->ty = std::move(mapped_ty).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny ShapeExprVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  const ShapeExprNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
ShapeExprNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ty));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->values));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny ShapeExprMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  const ShapeExprNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
ShapeExprNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    mutator->MutateExpected(self->ty));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<PrimExpr>>, 
mapped_values,
+                                    mutator->MutateExpected(self->values));
+  if (mapped_ty.UnchangedOrSameAs(self->ty) && 
mapped_values.UnchangedOrSameAs(self->values)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<ShapeExprNode> copy = ffi::make_object<ShapeExprNode>(*self);
+  copy->ty = std::move(mapped_ty).ValueOrUnchanged(std::move(copy->ty));
+  copy->values = 
std::move(mapped_values).ValueOrUnchanged(std::move(copy->values));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny ShapeExprMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                      ffi::AnyView value) noexcept {
+  ShapeExprNode* self = const_cast<ShapeExprNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
ShapeExprNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ty));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<PrimExpr>>, 
mapped_values,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->values));
+  if (!mapped_ty.UnchangedOrSameAs(self->ty)) self->ty = 
std::move(mapped_ty).ValueUnchecked();
+  if (!mapped_values.UnchangedOrSameAs(self->values)) {
+    self->values = std::move(mapped_values).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+// Hooks do not inherit, so DataflowVar must mirror the base VarNode remap, 
PrimType-skip, and
+// Simple-to-None definition-region protocol.  Keep this hook triple in 
lockstep with VarNode.
+TVMFFIAny DataflowVarVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  const DataflowVarNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
DataflowVarNode>(value);
+  if (!self->ty.as<PrimTypeNode>()) {
+    if (visitor->def_region_kind() == kTVMFFIDefRegionKindSimple) {
+      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->WithDefRegionKind(
+          kTVMFFIDefRegionKindNone, [&]() { return 
visitor->VisitExpected(self->ty); }));
+    } else {
+      TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ty));
+    }
+  }
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny DataflowVarMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  const DataflowVarNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
DataflowVarNode>(value);
+  ffi::Expected<ffi::Any> remap_result = mutator->VarRemapGetExpected(value);
+  TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(remap_result);
+  if (ffi::details::ExpectedUnsafe::GetData(remap_result).type_index() !=
+      ffi::TypeIndex::kTVMFFINone) {
+    return 
ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(remap_result));
+  }
+  if (mutator->def_region_kind() == kTVMFFIDefRegionKindNone) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::UnchangedOr<ffi::Any> result = ffi::Unchanged();
+  ffi::Any mapped_value;
+  if (!self->ty.as<PrimTypeNode>()) {
+    ffi::Expected<ffi::UnchangedOr<ffi::Any>> mapped_ty_result =
+        mutator->def_region_kind() == kTVMFFIDefRegionKindSimple
+            ? mutator->WithDefRegionKind(kTVMFFIDefRegionKindNone,
+                                         [&]() { return 
mutator->MutateExpected(self->ty); })
+            : mutator->MutateExpected(self->ty);
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                      std::move(mapped_ty_result));
+    if (!mapped_ty.UnchangedOrSameAs(self->ty)) {
+      ffi::ObjectPtr<DataflowVarNode> copy = 
ffi::make_object<DataflowVarNode>(*self);
+      copy->ty = std::move(mapped_ty).ValueUnchecked();
+      mapped_value = ffi::Any(std::move(copy));
+      result = mapped_value;
+    }
+  }
+  if (!result.IsUnchanged() || mutator->def_region_kind() == 
kTVMFFIDefRegionKindPattern) {
+    ffi::AnyView value_to_store = result.IsUnchanged() ? value : 
ffi::AnyView(mapped_value);
+    auto set_result = mutator->VarRemapSetExpected(value, value_to_store);
+    if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
+      return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(set_result).error()));
+    }
+  }
+  return ffi::details::UnchangedOrUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+TVMFFIAny DataflowVarMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                        ffi::AnyView value) noexcept {
+  DataflowVarNode* self = const_cast<DataflowVarNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
DataflowVarNode>(value));
+  ffi::Expected<ffi::Any> remap_result = mutator->VarRemapGetExpected(value);
+  TVM_FFI_S_MUTATE_MAYBE_EARLY_RETURN(remap_result);
+  if (ffi::details::ExpectedUnsafe::GetData(remap_result).type_index() !=
+      ffi::TypeIndex::kTVMFFINone) {
+    return 
ffi::details::ExpectedUnsafe::MoveToTVMFFIAny(std::move(remap_result));
+  }
+  if (mutator->def_region_kind() == kTVMFFIDefRegionKindNone) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::UnchangedOr<ffi::Any> result = ffi::Unchanged();
+  ffi::Any mapped_value;
+  if (!self->ty.as<PrimTypeNode>()) {
+    ffi::Expected<ffi::UnchangedOr<ffi::Any>> mapped_ty_result =
+        mutator->def_region_kind() == kTVMFFIDefRegionKindSimple
+            ? mutator->WithDefRegionKind(
+                  kTVMFFIDefRegionKindNone,
+                  [&]() { return 
mutator->MaybeInplaceMutateIfUniqueExpected(self->ty); })
+            : mutator->MaybeInplaceMutateIfUniqueExpected(self->ty);
+    TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                      std::move(mapped_ty_result));
+    if (!mapped_ty.UnchangedOrSameAs(self->ty)) {
+      self->ty = std::move(mapped_ty).ValueUnchecked();
+      mapped_value = ffi::Any(self);
+      result = mapped_value;
+    }
+  }
+  if (!result.IsUnchanged() || mutator->def_region_kind() == 
kTVMFFIDefRegionKindPattern) {
+    ffi::AnyView value_to_store = result.IsUnchanged() ? value : 
ffi::AnyView(mapped_value);
+    auto set_result = mutator->VarRemapSetExpected(value, value_to_store);
+    if (TVM_FFI_PREDICT_FALSE(set_result.is_err())) {
+      return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(set_result).error()));
+    }
+  }
+  return ffi::details::UnchangedOrUnsafe::MoveToTVMFFIAny(std::move(result));
+}
+
+TVMFFIAny SeqExprVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView value) 
noexcept {
+  const SeqExprNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
SeqExprNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->blocks));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ty));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->body));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny SeqExprMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  const SeqExprNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
SeqExprNode>(value);
+  
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<BindingBlock>>, 
mapped_blocks,
+                                    mutator->MutateExpected(self->blocks));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    mutator->MutateExpected(self->ty));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Expr>, mapped_body,
+                                    mutator->MutateExpected(self->body));
+  if (mapped_blocks.UnchangedOrSameAs(self->blocks) && 
mapped_ty.UnchangedOrSameAs(self->ty) &&
+      mapped_body.UnchangedOrSameAs(self->body)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<SeqExprNode> copy = ffi::make_object<SeqExprNode>(*self);
+  copy->blocks = 
std::move(mapped_blocks).ValueOrUnchanged(std::move(copy->blocks));
+  copy->ty = std::move(mapped_ty).ValueOrUnchanged(std::move(copy->ty));
+  copy->body = std::move(mapped_body).ValueOrUnchanged(std::move(copy->body));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny SeqExprMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                    ffi::AnyView value) noexcept {
+  SeqExprNode* self = const_cast<SeqExprNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
SeqExprNode>(value));
+  
TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<BindingBlock>>, 
mapped_blocks,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->blocks));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ty));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Expr>, mapped_body,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->body));
+  if (!mapped_blocks.UnchangedOrSameAs(self->blocks)) {
+    self->blocks = std::move(mapped_blocks).ValueUnchecked();
+  }
+  if (!mapped_ty.UnchangedOrSameAs(self->ty)) self->ty = 
std::move(mapped_ty).ValueUnchecked();
+  if (!mapped_body.UnchangedOrSameAs(self->body)) {
+    self->body = std::move(mapped_body).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny IfVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView value) 
noexcept {
+  const IfNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
IfNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ty));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->cond));
+  
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->true_branch));
+  
TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->false_branch));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny IfMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView value) 
noexcept {
+  const IfNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
IfNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    mutator->MutateExpected(self->ty));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Expr>, mapped_cond,
+                                    mutator->MutateExpected(self->cond));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<SeqExpr>, 
mapped_true_branch,
+                                    
mutator->MutateExpected(self->true_branch));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<SeqExpr>, 
mapped_false_branch,
+                                    
mutator->MutateExpected(self->false_branch));
+  if (mapped_ty.UnchangedOrSameAs(self->ty) && 
mapped_cond.UnchangedOrSameAs(self->cond) &&
+      mapped_true_branch.UnchangedOrSameAs(self->true_branch) &&
+      mapped_false_branch.UnchangedOrSameAs(self->false_branch)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<IfNode> copy = ffi::make_object<IfNode>(*self);
+  copy->ty = std::move(mapped_ty).ValueOrUnchanged(std::move(copy->ty));
+  copy->cond = std::move(mapped_cond).ValueOrUnchanged(std::move(copy->cond));
+  copy->true_branch = 
std::move(mapped_true_branch).ValueOrUnchanged(std::move(copy->true_branch));
+  copy->false_branch =
+      
std::move(mapped_false_branch).ValueOrUnchanged(std::move(copy->false_branch));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny IfMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator, 
ffi::AnyView value) noexcept {
+  IfNode* self = const_cast<IfNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
IfNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ty));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Expr>, mapped_cond,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->cond));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<SeqExpr>, 
mapped_true_branch,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->true_branch));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      ffi::UnchangedOr<SeqExpr>, mapped_false_branch,
+      mutator->MaybeInplaceMutateIfUniqueExpected(self->false_branch));
+  if (!mapped_ty.UnchangedOrSameAs(self->ty)) self->ty = 
std::move(mapped_ty).ValueUnchecked();
+  if (!mapped_cond.UnchangedOrSameAs(self->cond)) {
+    self->cond = std::move(mapped_cond).ValueUnchecked();
+  }
+  if (!mapped_true_branch.UnchangedOrSameAs(self->true_branch)) {
+    self->true_branch = std::move(mapped_true_branch).ValueUnchecked();
+  }
+  if (!mapped_false_branch.UnchangedOrSameAs(self->false_branch)) {
+    self->false_branch = std::move(mapped_false_branch).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+// Parameters precede the reflected ty field in this hook triple so their 
pattern region establishes
+// the remap before the derived function type can refer to those symbolic 
definitions.
+TVMFFIAny FunctionVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  // skips: attrs (metadata), is_pure (scalar)
+  const FunctionNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FunctionNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->WithDefRegionKind(
+      kTVMFFIDefRegionKindPattern, [&]() { return 
visitor->VisitExpected(self->params); }));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ty));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->body));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ret_ty));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny FunctionMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  // skips: attrs (metadata), is_pure (scalar)
+  const FunctionNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FunctionNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<Var>>, 
mapped_params,
+                                    
mutator->WithDefRegionKind(kTVMFFIDefRegionKindPattern, [&]() {
+                                      return 
mutator->MutateExpected(self->params);
+                                    }));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    mutator->MutateExpected(self->ty));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<SeqExpr>, mapped_body,
+                                    mutator->MutateExpected(self->body));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret_ty,
+                                    mutator->MutateExpected(self->ret_ty));
+  if (mapped_params.UnchangedOrSameAs(self->params) && 
mapped_ty.UnchangedOrSameAs(self->ty) &&
+      mapped_body.UnchangedOrSameAs(self->body) && 
mapped_ret_ty.UnchangedOrSameAs(self->ret_ty)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<FunctionNode> copy = ffi::make_object<FunctionNode>(*self);
+  copy->params = 
std::move(mapped_params).ValueOrUnchanged(std::move(copy->params));
+  copy->ty = std::move(mapped_ty).ValueOrUnchanged(std::move(copy->ty));
+  copy->body = std::move(mapped_body).ValueOrUnchanged(std::move(copy->body));
+  copy->ret_ty = 
std::move(mapped_ret_ty).ValueOrUnchanged(std::move(copy->ret_ty));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny FunctionMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                     ffi::AnyView value) noexcept {
+  // skips: attrs (metadata), is_pure (scalar)
+  FunctionNode* self = const_cast<FunctionNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
FunctionNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      ffi::UnchangedOr<ffi::Array<Var>>, mapped_params,
+      mutator->WithDefRegionKind(kTVMFFIDefRegionKindPattern, [&]() {
+        return mutator->MaybeInplaceMutateIfUniqueExpected(self->params);
+      }));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ty,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ty));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<SeqExpr>, mapped_body,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->body));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret_ty,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ret_ty));
+  if (!mapped_params.UnchangedOrSameAs(self->params)) {
+    self->params = std::move(mapped_params).ValueUnchecked();
+  }
+  if (!mapped_ty.UnchangedOrSameAs(self->ty)) self->ty = 
std::move(mapped_ty).ValueUnchecked();
+  if (!mapped_body.UnchangedOrSameAs(self->body)) {
+    self->body = std::move(mapped_body).ValueUnchecked();
+  }
+  if (!mapped_ret_ty.UnchangedOrSameAs(self->ret_ty)) {
+    self->ret_ty = std::move(mapped_ret_ty).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+}  // namespace
+
 TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
   ShapeExprNode::RegisterReflection();
   BindingNode::RegisterReflection();
   DataflowVarNode::RegisterReflection();
@@ -42,6 +397,59 @@ TVM_FFI_STATIC_INIT_BLOCK() {
   IfNode::RegisterReflection();
   FunctionNode::RegisterReflection();
   ExternFuncNode::RegisterReflection();
+  refl::TypeAttrDef<ShapeExprNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&ShapeExprVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&ShapeExprMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&ShapeExprMaybeInplaceMutate));
+  refl::TypeAttrDef<DataflowVarNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&DataflowVarVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&DataflowVarMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&DataflowVarMaybeInplaceMutate));
+  refl::TypeAttrDef<ConstantNode>()
+      .attr(refl::type_attr::kStructuralVisit,
+            reinterpret_cast<void*>(&TypeOnlyExprVisit<ConstantNode>))
+      .attr(refl::type_attr::kStructuralMutate,
+            reinterpret_cast<void*>(&TypeOnlyExprMutate<ConstantNode>))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            
reinterpret_cast<void*>(&TypeOnlyExprMaybeInplaceMutate<ConstantNode>));
+  refl::TypeAttrDef<StringImmNode>()
+      .attr(refl::type_attr::kStructuralVisit,
+            reinterpret_cast<void*>(&TypeOnlyExprVisit<StringImmNode>))
+      .attr(refl::type_attr::kStructuralMutate,
+            reinterpret_cast<void*>(&TypeOnlyExprMutate<StringImmNode>))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            
reinterpret_cast<void*>(&TypeOnlyExprMaybeInplaceMutate<StringImmNode>));
+  refl::TypeAttrDef<DataTypeImmNode>()
+      .attr(refl::type_attr::kStructuralVisit,
+            reinterpret_cast<void*>(&TypeOnlyExprVisit<DataTypeImmNode>))
+      .attr(refl::type_attr::kStructuralMutate,
+            reinterpret_cast<void*>(&TypeOnlyExprMutate<DataTypeImmNode>))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            
reinterpret_cast<void*>(&TypeOnlyExprMaybeInplaceMutate<DataTypeImmNode>));
+  refl::TypeAttrDef<SeqExprNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&SeqExprVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&SeqExprMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&SeqExprMaybeInplaceMutate));
+  refl::TypeAttrDef<IfNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&IfVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&IfMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&IfMaybeInplaceMutate));
+  refl::TypeAttrDef<FunctionNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&FunctionVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&FunctionMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&FunctionMaybeInplaceMutate));
+  refl::TypeAttrDef<ExternFuncNode>()
+      .attr(refl::type_attr::kStructuralVisit,
+            reinterpret_cast<void*>(&TypeOnlyExprVisit<ExternFuncNode>))
+      .attr(refl::type_attr::kStructuralMutate,
+            reinterpret_cast<void*>(&TypeOnlyExprMutate<ExternFuncNode>))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            
reinterpret_cast<void*>(&TypeOnlyExprMaybeInplaceMutate<ExternFuncNode>));
 }
 
 If::If(Expr cond, Expr true_branch, Expr false_branch, Span span) {
diff --git a/src/relax/ir/type.cc b/src/relax/ir/type.cc
index d6fa7ada9c..6664474ce9 100644
--- a/src/relax/ir/type.cc
+++ b/src/relax/ir/type.cc
@@ -21,6 +21,8 @@
  * \file src/relax/ir/type.cc
  * \brief Relax type system.
  */
+#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/relax/type.h>
@@ -28,7 +30,31 @@
 namespace tvm {
 namespace relax {
 
-TVM_FFI_STATIC_INIT_BLOCK() { PackedFuncTypeNode::RegisterReflection(); }
+namespace {
+
+TVMFFIAny PackedFuncTypeVisit(ffi::StructuralVisitorObj*, ffi::AnyView) 
noexcept {
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny PackedFuncTypeMutate(ffi::StructuralMutatorObj*, ffi::AnyView) 
noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny PackedFuncTypeMaybeInplaceMutate(ffi::StructuralMutatorObj*, 
ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+}  // namespace
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
+  PackedFuncTypeNode::RegisterReflection();
+  refl::TypeAttrDef<PackedFuncTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&PackedFuncTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&PackedFuncTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&PackedFuncTypeMaybeInplaceMutate));
+}
 
 PackedFuncType::PackedFuncType(Span span) : Type(ffi::UnsafeInit{}) {
   ffi::ObjectPtr<PackedFuncTypeNode> n = 
ffi::make_object<PackedFuncTypeNode>();
diff --git a/src/tirx/ir/function.cc b/src/tirx/ir/function.cc
index 463a15084f..e0f295c931 100644
--- a/src/tirx/ir/function.cc
+++ b/src/tirx/ir/function.cc
@@ -21,6 +21,8 @@
  * \file src/tirx/ir/function.cc
  * \brief The function data structure.
  */
+#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/relax/expr.h>
@@ -32,9 +34,80 @@
 namespace tvm {
 namespace tirx {
 
+namespace {
+
+TVMFFIAny PrimFuncVisit(ffi::StructuralVisitorObj* visitor, ffi::AnyView 
value) noexcept {
+  // skips: attrs (metadata), ty (derived by InferType)
+  const PrimFuncNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
PrimFuncNode>(value);
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->WithDefRegionKind(
+      kTVMFFIDefRegionKindPattern, [&]() { return 
visitor->VisitExpected(self->params); }));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->ret_type));
+  TVM_FFI_S_VISIT_MAYBE_EARLY_RETURN(visitor->VisitExpected(self->body));
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny PrimFuncMutate(ffi::StructuralMutatorObj* mutator, ffi::AnyView 
value) noexcept {
+  // skips: attrs (metadata), ty (derived by InferType)
+  const PrimFuncNode* self =
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
PrimFuncNode>(value);
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<ffi::Array<Var>>, 
mapped_params,
+                                    
mutator->WithDefRegionKind(kTVMFFIDefRegionKindPattern, [&]() {
+                                      return 
mutator->MutateExpected(self->params);
+                                    }));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret_type,
+                                    mutator->MutateExpected(self->ret_type));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Stmt>, mapped_body,
+                                    mutator->MutateExpected(self->body));
+  if (mapped_params.UnchangedOrSameAs(self->params) &&
+      mapped_ret_type.UnchangedOrSameAs(self->ret_type) &&
+      mapped_body.UnchangedOrSameAs(self->body)) {
+    return ffi::Unchanged().CopyToTVMFFIAny();
+  }
+  ffi::ObjectPtr<PrimFuncNode> copy = ffi::make_object<PrimFuncNode>(*self);
+  copy->params = 
std::move(mapped_params).ValueOrUnchanged(std::move(copy->params));
+  copy->ret_type = 
std::move(mapped_ret_type).ValueOrUnchanged(std::move(copy->ret_type));
+  copy->body = std::move(mapped_body).ValueOrUnchanged(std::move(copy->body));
+  return 
ffi::details::AnyUnsafe::MoveAnyToTVMFFIAny(ffi::Any(std::move(copy)));
+}
+
+TVMFFIAny PrimFuncMaybeInplaceMutate(ffi::StructuralMutatorObj* mutator,
+                                     ffi::AnyView value) noexcept {
+  // skips: attrs (metadata), ty (derived by InferType)
+  PrimFuncNode* self = const_cast<PrimFuncNode*>(
+      ffi::details::AnyUnsafe::RawObjectPtrFromAnyViewAfterCheck<const 
PrimFuncNode>(value));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(
+      ffi::UnchangedOr<ffi::Array<Var>>, mapped_params,
+      mutator->WithDefRegionKind(kTVMFFIDefRegionKindPattern, [&]() {
+        return mutator->MaybeInplaceMutateIfUniqueExpected(self->params);
+      }));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Type>, mapped_ret_type,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->ret_type));
+  TVM_FFI_S_MUTATE_ASSIGN_OR_RETURN(ffi::UnchangedOr<Stmt>, mapped_body,
+                                    
mutator->MaybeInplaceMutateIfUniqueExpected(self->body));
+  if (!mapped_params.UnchangedOrSameAs(self->params)) {
+    self->params = std::move(mapped_params).ValueUnchecked();
+  }
+  if (!mapped_ret_type.UnchangedOrSameAs(self->ret_type)) {
+    self->ret_type = std::move(mapped_ret_type).ValueUnchecked();
+  }
+  if (!mapped_body.UnchangedOrSameAs(self->body)) {
+    self->body = std::move(mapped_body).ValueUnchecked();
+  }
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+}  // namespace
+
 TVM_FFI_STATIC_INIT_BLOCK() {
+  namespace refl = tvm::ffi::reflection;
   PrimFuncNode::RegisterReflection();
   TensorIntrinNode::RegisterReflection();
+  refl::TypeAttrDef<PrimFuncNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&PrimFuncVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&PrimFuncMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&PrimFuncMaybeInplaceMutate));
 }
 
 namespace {
diff --git a/src/tirx/ir/stmt.cc b/src/tirx/ir/stmt.cc
index bd774356b2..644cdd1775 100644
--- a/src/tirx/ir/stmt.cc
+++ b/src/tirx/ir/stmt.cc
@@ -46,6 +46,18 @@ using SubscriptSlice = ffi::Array<ffi::Variant<
     ffi::Tuple<ffi::Optional<PrimExpr>, ffi::Optional<PrimExpr>, 
ffi::Optional<PrimExpr>>,
     PrimExpr>>;
 
+TVMFFIAny BufferRegionTypeVisit(ffi::StructuralVisitorObj*, ffi::AnyView) 
noexcept {
+  return ffi::AnyView(nullptr).CopyToTVMFFIAny();
+}
+
+TVMFFIAny BufferRegionTypeMutate(ffi::StructuralMutatorObj*, ffi::AnyView) 
noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
+TVMFFIAny BufferRegionTypeMaybeInplaceMutate(ffi::StructuralMutatorObj*, 
ffi::AnyView) noexcept {
+  return ffi::Unchanged().CopyToTVMFFIAny();
+}
+
 ffi::ObjectRef RealizeBufferRegionSubscript(Expr value, SubscriptSlice slice, 
Span span) {
   BufferRegion source = value.as_or_throw<BufferRegion>();
   TVM_FFI_CHECK_LE(slice.size(), source->region.size(), IndexError)
@@ -1648,8 +1660,12 @@ BufferRegionType::BufferRegionType() : 
Type(ffi::UnsafeInit{}) {
 TVM_FFI_STATIC_INIT_BLOCK() {
   namespace refl = tvm::ffi::reflection;
   BufferRegionTypeNode::RegisterReflection();
-  refl::TypeAttrDef<BufferRegionTypeNode>().def("__subscript_expr_realize__",
-                                                RealizeBufferRegionSubscript);
+  refl::TypeAttrDef<BufferRegionTypeNode>()
+      .attr(refl::type_attr::kStructuralVisit, 
reinterpret_cast<void*>(&BufferRegionTypeVisit))
+      .attr(refl::type_attr::kStructuralMutate, 
reinterpret_cast<void*>(&BufferRegionTypeMutate))
+      .attr(refl::type_attr::kStructuralMaybeInplaceMutate,
+            reinterpret_cast<void*>(&BufferRegionTypeMaybeInplaceMutate))
+      .def("__subscript_expr_realize__", RealizeBufferRegionSubscript);
 }
 
 BufferRegion::BufferRegion(BufferVar buffer, ffi::Array<Range> region, Span 
span) {

Reply via email to