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


The following commit(s) were added to refs/heads/main by this push:
     new f17fc6ecb [Rust] Adopt tvm-ffi EnumRef, NativeFunction, Display, 
type_key and field_default (#222)
f17fc6ecb is described below

commit f17fc6ecb29aa7ba1a17e54e804932952ed9f74d
Author: Tianqi Chen <[email protected]>
AuthorDate: Sat Nov 7 08:16:26 2026 -0500

    [Rust] Adopt tvm-ffi EnumRef, NativeFunction, Display, type_key and 
field_default (#222)
    
    Adopt the tvm-ffi and drop the TIRx helpers they replace.
    
    - Bump `3rdparty/tvm-ffi` to apache/tvm-ffi@f3096d8d and regenerate the
    bindings: `MemoryScope`, `ThreadScope` and `Axis` mirror `ffi.Enum` as
    `EnumObj` and implement `EnumRef`; reflection traits come from
    `tvm_ffi::reflection`.
    - Enums: values are looked up with `EnumRef::get` and registered with
    `EnumDef`. `MemoryScope::register` is removed
    (`EnumDef::<MemoryScope>::new`), `ThreadScope::get` is now the `EnumRef`
    lookup, and `Axis::get`, which registers an unknown name, is renamed
    `Axis::get_or_register`.
    - Native hooks: `reflection::NativeFunction` builds `OpDef`'s hooks from
    Rust functions and calls the hooks the registries hold, replacing
    `support::NativeHook`, `support::call_with_refs` and `OpDef`'s C ABI
    adapters. The hook setters take `Copy` functions, and a panic in a hook
    now aborts instead of becoming an error.
    - Error messages print values with `Display` and name types with
    `reflection::type_key`, replacing `support::repr` and
    `support::type_key`.
    - The TensorLoad and TensorStore type attributes are looked up once
    through a `LazyLock`, replacing `support::TypeAttr`.
    - The default `TensorType` alignment is read with
    `reflection::field_default`, replacing `support::field_default`.
    - Drop the tensor scalar check (`TensorTypeObj::IsScalar`,
    `TensorVar::IsScalar`, `tirx.TensorIsScalar`, Python `is_scalar`, Rust
    `tensor_is_scalar`) and inline it into ForceNarrowIndexToInt32, its one
    caller.
---
 3rdparty/tvm-ffi                                |   2 +-
 include/tvm/tirx/expr.h                         |   7 -
 include/tvm/tirx/type.h                         |   3 -
 python/tvm_tirx/tirx/tensor.py                  |  17 --
 rust/src/ir/attrs.rs                            |   4 +-
 rust/src/ir/expr.rs                             |  52 +++---
 rust/src/ir/function.rs                         |   4 +-
 rust/src/ir/global_info.rs                      |   4 +-
 rust/src/ir/location.rs                         |   4 +-
 rust/src/ir/op_def.rs                           | 112 +++----------
 rust/src/ir/prim/expr.rs                        |   3 +-
 rust/src/ir/prim/op.rs                          |  34 ++--
 rust/src/ir/prim/vector_expr.rs                 |   4 +-
 rust/src/ir/stmt.rs                             |  65 ++++----
 rust/src/ir/support.rs                          | 202 ++----------------------
 rust/src/ir/transform/core.rs                   |   2 +-
 rust/src/ir/type.rs                             |   4 +-
 rust/src/ir/type_def.rs                         |   7 +-
 rust/src/runtime.rs                             |  38 ++---
 rust/src/sym/constraint.rs                      |   4 +-
 rust/src/sym/int_set.rs                         |   2 +-
 rust/src/sym/iter_affine_map.rs                 |   2 +-
 rust/src/target.rs                              |   2 +-
 rust/src/tirx/layout.rs                         |  33 ++--
 rust/src/tirx/op.rs                             |  35 ++--
 rust/src/tirx/structural.rs                     |   2 +-
 rust/src/tirx/tensor.rs                         |  36 +----
 rust/src/tirx/thread_scope.rs                   |  38 ++---
 rust/src/tirx/tile/dispatch.rs                  |   4 +-
 rust/src/tirx/tile/instruction.rs               |   2 +-
 rust/src/tirx/transform/function_pass.rs        |   2 +-
 rust/tests/ir/op_def.rs                         |  31 +---
 rust/tests/ir/type.rs                           |   5 +-
 rust/tests/runtime/main.rs                      |   5 +-
 rust/tests/tirx/layout.rs                       |   4 +-
 rust/tests/tirx/main.rs                         |   5 +-
 rust/tests/tirx/tensor.rs                       |   6 +-
 rust/tests/tirx/thread_scope.rs                 |   1 +
 rust/tests/tirx/tile/dispatch.rs                |   2 +-
 src/tirx/ir/tensor.cc                           |   7 -
 src/tirx/ir/type.cc                             |   8 -
 src/tirx/transform/force_narrow_index_to_i32.cc |  12 +-
 42 files changed, 241 insertions(+), 575 deletions(-)

diff --git a/3rdparty/tvm-ffi b/3rdparty/tvm-ffi
index 7bedac88a..f3096d8db 160000
--- a/3rdparty/tvm-ffi
+++ b/3rdparty/tvm-ffi
@@ -1 +1 @@
-Subproject commit 7bedac88ac4f2737d089244bae4d7be9d3599140
+Subproject commit f3096d8db5341b1af3786e360ef07d06f8631966
diff --git a/include/tvm/tirx/expr.h b/include/tvm/tirx/expr.h
index 1891960a0..5f192ce8b 100644
--- a/include/tvm/tirx/expr.h
+++ b/include/tvm/tirx/expr.h
@@ -71,13 +71,6 @@ class TensorVar : public Var {
   /*! \brief Return the storage scope associated with this tensor. */
   runtime::MemoryScope scope() const { return ty()->scope; }
 
-  /*!
-   * \brief Return true if the tensor is a scalar.
-   * \param alloc_or_decl Whether to consider alloc_scalar and decl_scalar as 
scalar. True for
-   * alloc_scalar, False for decl_scalar.
-   */
-  TVM_TIRX_DLL bool IsScalar(bool alloc_or_decl = true) const;
-
   explicit TensorVar(ffi::ObjectPtr<VarObj> node) : 
TensorVar(Var(std::move(node))) {}
   explicit TensorVar(ffi::UnsafeInit tag) : Var(tag) {}
   TVM_FFI_DEFINE_DEFAULT_COPY_MOVE_AND_ASSIGN(TensorVar);
diff --git a/include/tvm/tirx/type.h b/include/tvm/tirx/type.h
index 52ba65f5f..22ffc96b6 100644
--- a/include/tvm/tirx/type.h
+++ b/include/tvm/tirx/type.h
@@ -107,9 +107,6 @@ class TensorTypeObj : public TypeObj {
   /*! \return type of the physical pointer projected by tensor_data_ptr. */
   PtrType DataPtrType() const { return PtrType(dtype, scope); }
 
-  /*! \brief Whether this type supports scalar tensor syntax. */
-  TVM_TIRX_DLL bool IsScalar(bool alloc_or_decl = true) const;
-
   /*! \brief Return the constant element count, or nullopt for symbolic 
extents or overflow. */
   TVM_TIRX_DLL ffi::Optional<int64_t> ConstantAllocationSize() const;
 
diff --git a/python/tvm_tirx/tirx/tensor.py b/python/tvm_tirx/tirx/tensor.py
index 28db73cf5..ce238c1d8 100644
--- a/python/tvm_tirx/tirx/tensor.py
+++ b/python/tvm_tirx/tirx/tensor.py
@@ -149,7 +149,6 @@ class TensorType(Type):
         at(loc, self)
 
     __expr_methods__ = (
-        "is_scalar",
         "ptr_to",
         "view",
         "permute",
@@ -201,22 +200,6 @@ class TensorType(Type):
         _check_tensor_receiver(self, expr)
         return self.scope
 
-    def is_scalar(self, expr, alloc_or_decl=True):
-        """Check if the tensor is a scalar.
-
-        Parameters
-        ----------
-        alloc_or_decl : bool, optional
-            Whether to consider alloc_scalar and decl_scalar as scalar. True 
for alloc_scalar,
-            False for decl_scalar.
-
-        Returns
-        -------
-            bool: True if the tensor is a scalar, False otherwise.
-        """
-        _check_tensor_receiver(self, expr)
-        return _ffi_api.TensorIsScalar(expr, alloc_or_decl)
-
     def ptr_to(self, expr, indices):
         """Get the pointer to the tensor at the given indices (logical 
indices).
 
diff --git a/rust/src/ir/attrs.rs b/rust/src/ir/attrs.rs
index a8531cdbc..eedf5aaba 100644
--- a/rust/src/ir/attrs.rs
+++ b/rust/src/ir/attrs.rs
@@ -31,9 +31,9 @@ use tvm_ffi::Any;
 use tvm_ffi::Map;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
-use tvm_ffi::ShallowCopy;
 use tvm_ffi::String;
-use tvm_ffi::UnsafeInit;
+use tvm_ffi::reflection::ShallowCopy;
+use tvm_ffi::reflection::UnsafeInit;
 // tvm-ffi-stubgen(end)
 
 /// The base of every attribute object, the metadata attached to a call, a 
function or a module.
diff --git a/rust/src/ir/expr.rs b/rust/src/ir/expr.rs
index f697bb813..3494be7f8 100644
--- a/rust/src/ir/expr.rs
+++ b/rust/src/ir/expr.rs
@@ -59,6 +59,9 @@
 // tvm-ffi-stubgen(import-object): tvm_ffi::ObjectRefCore
 // tvm-ffi-stubgen(import-object): tvm_ffi::AnyView
 // tvm-ffi-stubgen(import-object): tvm_ffi::Result
+// tvm-ffi-stubgen(import-object): tvm_ffi::reflection::NativeFunction
+// tvm-ffi-stubgen(import-object): tvm_ffi::reflection::TypeAttrColumn
+// tvm-ffi-stubgen(import-object): std::sync::LazyLock
 // tvm-ffi-stubgen(import-object): tvm_ffi::tvm_ffi_sys::TVMFFITypeIndex
 // tvm-ffi-stubgen(import-object): crate::ir::location::UNKNOWN_LOC
 // tvm-ffi-stubgen(import-object): crate::ir::op::OpObj
@@ -98,6 +101,7 @@ use crate::ir::support;
 use std::borrow::Borrow;
 use std::ops::Deref;
 use std::ptr::addr_of_mut;
+use std::sync::LazyLock;
 use tvm_ffi::Any;
 use tvm_ffi::AnyView;
 use tvm_ffi::Array;
@@ -108,10 +112,12 @@ use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectRefCore;
 use tvm_ffi::Result;
-use tvm_ffi::ShallowCopy;
 use tvm_ffi::String;
-use tvm_ffi::UnsafeInit;
 use tvm_ffi::object::IntoObjectRef;
+use tvm_ffi::reflection::NativeFunction;
+use tvm_ffi::reflection::ShallowCopy;
+use tvm_ffi::reflection::TypeAttrColumn;
+use tvm_ffi::reflection::UnsafeInit;
 use tvm_ffi::reflection::drop_if_defined;
 use tvm_ffi::tvm_ffi_sys::TVMFFITypeIndex;
 // tvm-ffi-stubgen(end)
@@ -595,14 +601,15 @@ fn scalar_int(ty: &PrimType) -> bool {
 /// What a typed expression's conversion error names a value it rejects: an 
expression with its
 /// type, `ir.StringImm[ty=ir.StringType]`, or the type key of another value.
 fn typed_expr_mismatch(value: AnyView<'_>) -> std::string::String {
+    let key = |index: i32| 
tvm_ffi::reflection::type_key(index).unwrap_or_default();
     let Some(expr) = value.downcast_ref::<ExprObj>() else {
-        return support::type_key(value.type_index());
+        return key(value.type_index()).to_string();
     };
     let ty = match expr.ty.downcast_ref::<PrimTypeObj>() {
         Some(prim) => support::dtype_name(prim.dtype),
-        None => support::type_key(AnyView::from(&expr.ty).type_index()),
+        None => key(AnyView::from(&expr.ty).type_index()).to_string(),
     };
-    format!("{}[ty={ty}]", support::type_key(value.type_index()))
+    format!("{}[ty={ty}]", key(value.type_index()))
 }
 
 // 
---------------------------------------------------------------------------------------------
@@ -982,7 +989,7 @@ impl TupleGetItem {
         if index < 0 {
             let message = format!(
                 "Index out of bounds: Tuple {} cannot be accessed with 
negative index {index}",
-                support::repr(&tuple_value)
+                tuple_value
             );
             return Err(support::compare_failed(
                 tvm_ffi::INDEX_ERROR,
@@ -999,7 +1006,7 @@ impl TupleGetItem {
                     let message = format!(
                         "Index out of bounds: Tuple {} is of size {size}, and 
cannot be accessed \
                          with index {index}",
-                        support::repr(&tuple_value)
+                        tuple_value
                     );
                     let condition = "index < tuple_type->fields.size()";
                     let error = support::compare_failed(
@@ -1099,25 +1106,30 @@ impl TensorLoad {
         indices: impl IntoIterator<Item = impl IntoExpr<PrimExpr>>,
         loc: impl Into<Location>,
     ) -> Result<Self> {
-        static INFER_TYPE: support::TypeAttr =
-            support::TypeAttr::new("__tvm_tensor_load_infer_type__");
+        // The native library registers the column when it loads, so one 
lookup suffices.
+        static INFER_TYPE: LazyLock<Option<TypeAttrColumn>> =
+            LazyLock::new(|| 
TypeAttrColumn::new("__tvm_tensor_load_infer_type__"));
         let (source, loc) = (source.into_owned_expr()?, loc.into());
         let indices = indices
             .into_iter()
             .map(IntoExpr::<PrimExpr>::into_owned_expr)
             .collect::<Result<Array<PrimExpr>>>()?;
         let ty_index = AnyView::from(&source.ty).type_index();
-        let Some(infer) = INFER_TYPE.get(ty_index) else {
-            let message =
-                format!("Type {} does not support TensorLoad", 
support::type_key(ty_index));
+        let Some(infer) = INFER_TYPE.and_then(|column| column.get(ty_index)) 
else {
+            let message = format!(
+                "Type {} does not support TensorLoad",
+                tvm_ffi::reflection::type_key(ty_index).unwrap_or_default()
+            );
             return Err(support::check_failed(tvm_ffi::TYPE_ERROR, 
"infer.has_value()", &message));
         };
         // The hook types a load that has no type yet. Such a load is no 
`TensorLoad`, which is
-        // typed by a `PrimType`, so it is made as an `Expr`.
+        // typed by a `PrimType`, so only its object is made.
         let base_ = ExprObj::new(loc.clone(), MissingType::new().into());
         let untyped = TensorLoadObj { base_, source: source.clone(), indices: 
indices.clone() };
-        let untyped = Expr::from(Self { data_: ObjectArc::new(untyped) });
-        let ty: PrimType = 
support::NativeHook::from_any(infer)?.call_as(&untyped)?;
+        // SAFETY: the column holds `PrimType(const TensorLoadObj*)` hooks.
+        let infer: NativeFunction<(&TensorLoadObj,), PrimType> =
+            unsafe { NativeFunction::from_any(infer)? };
+        let ty = infer.call((&*ObjectArc::new(untyped),))?;
         Ok(Self::from_fields(loc, ty, source, indices))
     }
 }
@@ -1801,7 +1813,10 @@ impl Call {
         if op.validator().type_index() == TVMFFITypeIndex::kTVMFFINone as i32 {
             return Ok(());
         }
-        support::NativeHook::from_any(op.validator().clone())?.call_void(self)
+        // SAFETY: an operator validator is a `void(const CallObj*)` hook.
+        let validate: NativeFunction<(&CallObj,)> =
+            unsafe { NativeFunction::from_any(op.validator().clone())? };
+        validate.call((&**self,))
     }
 
     /// Return the result type the operator infers from the current inputs, 
without changing the
@@ -1821,8 +1836,9 @@ impl Call {
         if let Some(fixed) = support::op_attr(op, "TFixedReturnType")? {
             return fixed.try_into();
         }
-        match support::NativeHook::of_op(op, "FInferType")? {
-            Some(infer) => infer.call_as(self),
+        // SAFETY: `FInferType` holds `Type(const CallObj*)` hooks.
+        match unsafe { support::op_hook::<(&CallObj,), Type>(op, 
"FInferType")? } {
+            Some(infer) => infer.call((&**self,)),
             None => Ok(MissingType::new().into()),
         }
     }
diff --git a/rust/src/ir/function.rs b/rust/src/ir/function.rs
index 4e191fa7c..bf4c79e89 100644
--- a/rust/src/ir/function.rs
+++ b/rust/src/ir/function.rs
@@ -43,9 +43,9 @@ use tvm_ffi::Map;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::RValueRef;
 use tvm_ffi::Result;
-use tvm_ffi::ShallowCopy;
 use tvm_ffi::String;
-use tvm_ffi::UnsafeInit;
+use tvm_ffi::reflection::ShallowCopy;
+use tvm_ffi::reflection::UnsafeInit;
 use tvm_ffi::reflection::drop_if_defined;
 // tvm-ffi-stubgen(end)
 
diff --git a/rust/src/ir/global_info.rs b/rust/src/ir/global_info.rs
index bd30b9032..85d10c816 100644
--- a/rust/src/ir/global_info.rs
+++ b/rust/src/ir/global_info.rs
@@ -28,8 +28,8 @@
 use std::ptr::addr_of_mut;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
-use tvm_ffi::ShallowCopy;
-use tvm_ffi::UnsafeInit;
+use tvm_ffi::reflection::ShallowCopy;
+use tvm_ffi::reflection::UnsafeInit;
 // tvm-ffi-stubgen(end)
 
 /// The base of the module-level information an [`IRModule`](super::IRModule) 
keeps by name in
diff --git a/rust/src/ir/location.rs b/rust/src/ir/location.rs
index 48d0b8b12..2ef082ca2 100644
--- a/rust/src/ir/location.rs
+++ b/rust/src/ir/location.rs
@@ -39,9 +39,9 @@ use std::ptr::addr_of_mut;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::Result;
-use tvm_ffi::ShallowCopy;
 use tvm_ffi::String;
-use tvm_ffi::UnsafeInit;
+use tvm_ffi::reflection::ShallowCopy;
+use tvm_ffi::reflection::UnsafeInit;
 // tvm-ffi-stubgen(end)
 
 /// Where an IR node comes from, for diagnostics: an [`UnknownLoc`], a 
[`SourceLoc`] or a
diff --git a/rust/src/ir/op_def.rs b/rust/src/ir/op_def.rs
index e649e189a..544711ce5 100644
--- a/rust/src/ir/op_def.rs
+++ b/rust/src/ir/op_def.rs
@@ -22,11 +22,10 @@
 //! [`AttrKey`], such as the shared ones in [`op_attr`].
 
 use std::any::TypeId;
-use std::ffi::c_void;
 use std::marker::PhantomData;
-use std::panic::{catch_unwind, AssertUnwindSafe};
 
-use tvm_ffi::tvm_ffi_sys::{TVMFFIAny, TVMFFITypeIndex};
+use tvm_ffi::reflection::NativeFunction;
+use tvm_ffi::tvm_ffi_sys::TVMFFITypeIndex;
 use tvm_ffi::{Any, AnyView, Array, Error, ObjectRefCore, Result, String, 
TYPE_ERROR, VALUE_ERROR};
 
 use super::attrs::Attrs;
@@ -34,7 +33,6 @@ use super::expr::{CallObj, Expr, Var};
 use super::op::{Op, OpObj};
 use super::r#type::Type;
 use super::stmt::RegionStmtObj;
-use super::support;
 use sig::{Checks, Specs};
 
 /// A chain of steps that registers a new operator.
@@ -44,6 +42,9 @@ use sig::{Checks, Specs};
 /// returns it. Register an operator before building IR with it, as a
 /// [`tvm_ffi::static_init_block!`] does.
 ///
+/// A hook, such as the one [`OpDef::set_infer_type`] sets, reports failure 
through its `Result`; a
+/// panic in it aborts the process, since it would unwind into native code.
+///
 /// # Examples
 ///
 /// ```
@@ -108,8 +109,8 @@ impl OpDef {
         let args: Vec<AnyView> = lists.iter().map(AnyView::from).collect();
         let def = self.invoke("_set_signature", &args).then(|op| {
             if !op.validator_is_custom() {
-                let hook: Hook = validate_signature::<S::Checks>;
-                op.write_validator(Any::from(hook as *mut c_void), false);
+                let hook: NativeFunction<(&CallObj,)> = 
sig::validate::<S::Checks>.into();
+                op.write_validator(hook.into(), false);
             }
             Ok(())
         });
@@ -168,11 +169,10 @@ impl OpDef {
     /// precedence. A second one fails the chain with a `ValueError`.
     pub fn set_infer_type<F>(self, infer: F) -> Self
     where
-        F: Fn(&CallObj) -> Result<Type> + 'static,
+        F: Fn(&CallObj) -> Result<Type> + Copy + 'static,
     {
-        let _ = infer;
-        let hook: Hook = infer_type_abi::<F>;
-        self.attr("FInferType", Any::from(hook as *mut c_void), false)
+        let hook: NativeFunction<(&CallObj,), Type> = infer.into();
+        self.attr("FInferType", hook.into(), false)
     }
 
     /// Set a custom validator of the operator, `validate`, a function or a 
closure that captures
@@ -182,16 +182,15 @@ impl OpDef {
     /// one fails the chain with a `ValueError`.
     pub fn set_validator<F>(self, validate: F) -> Self
     where
-        F: Fn(&CallObj) -> Result<()> + 'static,
+        F: Fn(&CallObj) -> Result<()> + Copy + 'static,
     {
-        let _ = validate;
+        let hook: NativeFunction<(&CallObj,)> = validate.into();
         self.then(|op| {
             if op.validator_is_custom() {
                 let message = format!("Validator of {} is already registered", 
op.name.as_str());
                 return Err(Error::new(VALUE_ERROR, &message, ""));
             }
-            let hook: Hook = validate_abi::<F>;
-            op.write_validator(Any::from(hook as *mut c_void), true);
+            op.write_validator(hook.into(), true);
             Ok(())
         })
     }
@@ -204,11 +203,10 @@ impl OpDef {
     /// fails the chain with a `ValueError`.
     pub fn set_region_body_params<F>(self, body_params: F) -> Self
     where
-        F: Fn(&CallObj) -> Result<Array<Var>> + 'static,
+        F: Fn(&CallObj) -> Result<Array<Var>> + Copy + 'static,
     {
-        let _ = body_params;
-        let hook: Hook = region_body_params_abi::<F>;
-        self.attr("FRegionGetBodyParams", Any::from(hook as *mut c_void), 
false)
+        let hook: NativeFunction<(&CallObj,), Array<Var>> = body_params.into();
+        self.attr("FRegionGetBodyParams", hook.into(), false)
     }
 
     /// Set the check of the operator's regions, its `FRegionValidate`, to 
`validate`, a function or
@@ -218,11 +216,10 @@ impl OpDef {
     /// the chain with a `ValueError`.
     pub fn set_region_validate<F>(self, validate: F) -> Self
     where
-        F: Fn(&RegionStmtObj) -> Result<()> + 'static,
+        F: Fn(&RegionStmtObj) -> Result<()> + Copy + 'static,
     {
-        let _ = validate;
-        let hook: RegionHook = region_validate_abi::<F>;
-        self.attr("FRegionValidate", Any::from(hook as *mut c_void), false)
+        let hook: NativeFunction<(&RegionStmtObj,)> = validate.into();
+        self.attr("FRegionValidate", hook.into(), false)
     }
 
     fn attr(self, key: &str, value: Any, replace: bool) -> Self {
@@ -367,7 +364,7 @@ pub mod sig {
         ///
         /// By default, the description is the type key of `value`.
         fn mismatch(value: AnyView<'_>) -> std::string::String {
-            support::type_key(value.type_index())
+            
tvm_ffi::reflection::type_key(value.type_index()).unwrap_or_default().to_string()
         }
     }
 
@@ -584,7 +581,11 @@ pub mod sig {
             let actual = match &call.attrs {
                 Some(attrs) if 
attrs.downcast_ref::<T::ContainerType>().is_some() => return Ok(()),
                 Some(attrs) => {
-                    format!("`{}`", 
support::type_key(AnyView::from(attrs).type_index()))
+                    format!(
+                        "`{}`",
+                        
tvm_ffi::reflection::type_key(AnyView::from(attrs).type_index())
+                            .unwrap_or_default()
+                    )
                 }
                 None => "None".into(),
             };
@@ -804,66 +805,3 @@ fn doc_text(doc: &str) -> std::string::String {
         doc.lines().map(|line| 
line.get(indent..).unwrap_or("").trim_end()).collect();
     lines.join("\n").trim().to_string()
 }
-
-/// The C ABI of a native operator hook, `NativeFunctionView<R(const 
CallNode*)>`: it returns
-/// the result, or the error, in an owned `TVMFFIAny`.
-type Hook = extern "C" fn(*const CallObj) -> TVMFFIAny;
-
-/// The C ABI of a native hook on a region statement, 
`NativeFunctionView<R(const
-/// RegionStmtNode*)>`.
-type RegionHook = extern "C" fn(*const RegionStmtObj) -> TVMFFIAny;
-
-extern "C" fn validate_abi<F: Fn(&CallObj) -> Result<()>>(call: *const 
CallObj) -> TVMFFIAny {
-    call_hook(call, zero_sized::<F>())
-}
-
-extern "C" fn infer_type_abi<F: Fn(&CallObj) -> Result<Type>>(call: *const 
CallObj) -> TVMFFIAny {
-    call_hook(call, zero_sized::<F>())
-}
-
-extern "C" fn validate_signature<L: Checks>(call: *const CallObj) -> TVMFFIAny 
{
-    call_hook(call, sig::validate::<L>)
-}
-
-extern "C" fn region_body_params_abi<F: Fn(&CallObj) -> Result<Array<Var>>>(
-    call: *const CallObj,
-) -> TVMFFIAny {
-    call_hook(call, zero_sized::<F>())
-}
-
-extern "C" fn region_validate_abi<F: Fn(&RegionStmtObj) -> Result<()>>(
-    region: *const RegionStmtObj,
-) -> TVMFFIAny {
-    call_hook(region, zero_sized::<F>())
-}
-
-/// The only value of `F`, a function or a closure that captures nothing.
-fn zero_sized<F>() -> F {
-    const {
-        assert!(
-            std::mem::size_of::<F>() == 0,
-            "a native operator hook is a function or a closure that captures 
nothing"
-        )
-    };
-    // SAFETY: a zero-sized type has no bytes to initialize.
-    unsafe { std::mem::zeroed() }
-}
-
-/// Run `hook` on the node C++ passes (the Call or region being typed or 
checked) and return its
-/// result as the hook ABI does. A panic, which must not unwind into C++, 
becomes an error.
-fn call_hook<N, R: Into<Any>>(node: *const N, hook: impl Fn(&N) -> Result<R>) 
-> TVMFFIAny {
-    let result = catch_unwind(AssertUnwindSafe(|| {
-        // SAFETY: C++ passes the node being typed or checked, alive for the 
call.
-        match unsafe { node.as_ref() } {
-            Some(node) => hook(node).map(Into::into),
-            None => Err(Error::new(TYPE_ERROR, "Invalid Op Call", "")),
-        }
-    }));
-    let value = match result {
-        Ok(Ok(value)) => value,
-        Ok(Err(error)) => Any::from(error),
-        Err(payload) => Any::from(tvm_ffi::Error::from_panic(payload)),
-    };
-    // The C++ caller takes ownership of the value.
-    Any::into_raw_ffi_any(value)
-}
diff --git a/rust/src/ir/prim/expr.rs b/rust/src/ir/prim/expr.rs
index 10393cd63..d6f4249ca 100644
--- a/rust/src/ir/prim/expr.rs
+++ b/rust/src/ir/prim/expr.rs
@@ -2210,7 +2210,8 @@ impl Let {
         let Some(var_ty) = var.ty.downcast_ref::<PrimTypeObj>() else {
             let message = format!(
                 "Cannot treat type `{}` as type `ir.PrimType`",
-                support::type_key(AnyView::from(&var.ty).type_index())
+                
tvm_ffi::reflection::type_key(AnyView::from(&var.ty).type_index())
+                    .unwrap_or_default()
             );
             return Err(tvm_ffi::Error::new(tvm_ffi::TYPE_ERROR, &message, ""));
         };
diff --git a/rust/src/ir/prim/op.rs b/rust/src/ir/prim/op.rs
index 967011270..a896f4d92 100644
--- a/rust/src/ir/prim/op.rs
+++ b/rust/src/ir/prim/op.rs
@@ -849,7 +849,7 @@ pub(crate) fn make_const_scalar(
         };
         return Ok(FloatImm::new(ty, value, loc.clone())?.into());
     }
-    let message = format!("cannot make const for type {}", support::repr(&ty));
+    let message = format!("cannot make const for type {ty}");
     Err(Error::new(INTERNAL_ERROR, &message, ""))
 }
 
@@ -1529,8 +1529,7 @@ pub fn logical_not_at(a: impl IntoExpr<PrimExpr>, loc: 
impl Into<Location>) -> R
     check_code(&a, "a", &BOOLEAN, BOOL_CODES, || {
         format!(
             "Expected boolean argument for ! operator (logical NOT), but 
received {} of type {}",
-            support::repr(&a),
-            support::repr(&a.ty)
+            a, a.ty
         )
     })?;
     match const_fold::not(&a)? {
@@ -1711,8 +1710,7 @@ pub fn bitwise_not_at(a: impl IntoExpr<PrimExpr>, loc: 
impl Into<Location>) -> R
         format!(
             "Expected integer or boolean argument for ~ operator (bitwise 
NOT), but received {} \
              of type {}",
-            support::repr(&a),
-            support::repr(&a.ty)
+            a, a.ty
         )
     })?;
     Ok(BitwiseNot::new(a, loc)?.into())
@@ -1738,7 +1736,7 @@ fn fold_shift(
             let message = format!(
                 "Shift amount must be non-negative and less than {} for type 
{}",
                 ty.bits(),
-                support::repr(ty)
+                ty
             );
             return Err(support::check_failed(INTERNAL_ERROR, condition, 
&message));
         }
@@ -1864,7 +1862,7 @@ pub fn max_value_at(ty: impl Into<PrimType>, loc: impl 
Into<Location>) -> Result
         _ => match small_float_bound(code) {
             Some((max, _)) => float(max),
             None => {
-                let message = format!("Cannot decide max_value for type{}", 
support::repr(&ty));
+                let message = format!("Cannot decide max_value for type{ty}");
                 Err(Error::new(INTERNAL_ERROR, &message, ""))
             }
         },
@@ -1891,7 +1889,7 @@ pub fn min_value_at(ty: impl Into<PrimType>, loc: impl 
Into<Location>) -> Result
             Some((_, true)) => float(0.0),
             Some((max, false)) => float(-max),
             None => {
-                let message = format!("Cannot decide min_value for type{}", 
support::repr(&ty));
+                let message = format!("Cannot decide min_value for type{ty}");
                 Err(Error::new(INTERNAL_ERROR, &message, ""))
             }
         },
@@ -1906,7 +1904,7 @@ pub fn infinity_at(ty: impl Into<PrimType>, loc: impl 
Into<Location>) -> Result<
         (support::FLOAT, 64) => Ok(FloatImm::new(ty, f64::INFINITY, 
loc)?.into()),
         (support::FLOAT, 32 | 16) => Ok(FloatImm::new(ty, 
f64::from(f32::INFINITY), loc)?.into()),
         _ => {
-            let message = format!("Cannot decide infinity for type {}", 
support::repr(&ty));
+            let message = format!("Cannot decide infinity for type {ty}");
             Err(Error::new(INTERNAL_ERROR, &message, ""))
         }
     }
@@ -2004,7 +2002,7 @@ fn absolute(x: PrimExpr, loc: Location) -> 
Result<PrimExpr> {
         _ => {
             let message = format!(
                 "Data type {} not supported for absolute op. Skipping absolute 
op...",
-                support::repr(&x.ty)
+                x.ty
             );
             Err(Error::new(INTERNAL_ERROR, &message, ""))
         }
@@ -2042,10 +2040,8 @@ fn infinite(x: PrimExpr, loc: Location) -> 
Result<PrimExpr> {
             logical_and(at_infinity, not_nan)
         }
         _ => {
-            let message = format!(
-                "Data type {} not supported for finiteness ops. Skipping 
it...",
-                support::repr(&x.ty)
-            );
+            let message =
+                format!("Data type {} not supported for finiteness ops. 
Skipping it...", x.ty);
             Err(Error::new(INTERNAL_ERROR, &message, ""))
         }
     }
@@ -2211,8 +2207,7 @@ fn unary_intrinsic(
     if float_only && !ty.matches_code(&[DLDataTypeCode::kDLFloat]) && 
!is_bfloat16(&ty) {
         let condition = "dtype.code() == DLDataTypeCode::kDLFloat || \
                          dtype.MatchesElementType(DLDataTypeCode::kDLBfloat, 
16)";
-        let message =
-            format!("{op} only supports floating-point inputs, but got {}", 
support::repr(&x.ty));
+        let message = format!("{op} only supports floating-point inputs, but 
got {}", x.ty);
         return Err(support::check_failed(TYPE_ERROR, condition, &message));
     }
     if !is_bfloat16(&ty) {
@@ -2313,8 +2308,8 @@ const BOOL_CODES: &str = "DLDataTypeCode::kDLBool";
 /// `TVM_FFI_ICHECK(x.ty().MatchesCode(kDLInt, kDLUInt)) << x` of the integer 
divisions, on both
 /// operands.
 fn check_integers(a: &PrimExpr, b: &PrimExpr) -> Result<()> {
-    check_code(a, "a", &INTEGER, INTEGER_CODES, || support::repr(a))?;
-    check_code(b, "b", &INTEGER, INTEGER_CODES, || support::repr(b))
+    check_code(a, "a", &INTEGER, INTEGER_CODES, || a.to_string())?;
+    check_code(b, "b", &INTEGER, INTEGER_CODES, || b.to_string())
 }
 
 /// The operand checks of an operator `symbol` (`what` names it) that takes 
`kind` arguments:
@@ -2332,8 +2327,7 @@ fn check_operands(
             format!(
                 "Expected {kind} argument as {which} of {symbol} operator 
({what}), but received \
                  {} of type {}",
-                support::repr(operand),
-                support::repr(&operand.ty)
+                operand, operand.ty
             )
         })?;
     }
diff --git a/rust/src/ir/prim/vector_expr.rs b/rust/src/ir/prim/vector_expr.rs
index 1339ddbaf..500f9e926 100644
--- a/rust/src/ir/prim/vector_expr.rs
+++ b/rust/src/ir/prim/vector_expr.rs
@@ -209,7 +209,7 @@ impl Ramp {
                 );
                 let message = format!(
                     "Ramp {field} must be scalar int32, int64, uint32, or 
uint64, but got {}",
-                    support::repr(ty)
+                    ty
                 );
                 return Err(support::check_failed(tvm_ffi::TYPE_ERROR, 
&condition, &message));
             }
@@ -449,7 +449,7 @@ fn vector_lanes(element: &PrimType, lanes: PrimExpr) -> 
Result<(PrimType, PrimEx
         return Ok((ty, IntImm::int32(count).into()));
     }
     let Some(factor) = vscale_factor(&lanes)? else {
-        let message = format!("Invalid expression for scalable lanes {}", 
support::repr(&lanes));
+        let message = format!("Invalid expression for scalable lanes {lanes}");
         return Err(support::check_failed(tvm_ffi::INTERNAL_ERROR, 
"vscale_factor", &message));
     };
     let ty = PrimType::try_from(support::scalable_vector(element.dtype, 
factor)?)?;
diff --git a/rust/src/ir/stmt.rs b/rust/src/ir/stmt.rs
index 363f4f89b..9d288955b 100644
--- a/rust/src/ir/stmt.rs
+++ b/rust/src/ir/stmt.rs
@@ -46,7 +46,10 @@
 // tvm-ffi-stubgen(import-object): tvm_ffi::DLDataTypeCode
 // tvm-ffi-stubgen(import-object): tvm_ffi::ObjectRefCore
 // tvm-ffi-stubgen(import-object): tvm_ffi::Result
-// tvm-ffi-stubgen(import-object): tvm_ffi::tvm_ffi_sys::TVMFFITypeIndex
+// tvm-ffi-stubgen(import-object): tvm_ffi::reflection::NativeFunction
+// tvm-ffi-stubgen(import-object): tvm_ffi::reflection::TypeAttrColumn
+// tvm-ffi-stubgen(import-object): std::sync::LazyLock
+// tvm-ffi-stubgen(import-object): crate::ir::expr::CallObj
 // tvm-ffi-stubgen(import-object): crate::ir::expr::Call
 // tvm-ffi-stubgen(import-object): crate::ir::expr::IntImm
 // tvm-ffi-stubgen(import-object): crate::ir::expr::IntImmObj
@@ -73,6 +76,7 @@
 // tvm-ffi-stubgen(begin): import-section
 use crate::ir::attrs::DictAttrs;
 use crate::ir::expr::Call;
+use crate::ir::expr::CallObj;
 use crate::ir::expr::Expr;
 use crate::ir::expr::IntImm;
 use crate::ir::expr::IntImmObj;
@@ -90,6 +94,7 @@ use crate::ir::r#type::PrimType;
 use crate::ir::support;
 use std::ops::Deref;
 use std::ptr::addr_of_mut;
+use std::sync::LazyLock;
 use tvm_ffi::Any;
 use tvm_ffi::AnyView;
 use tvm_ffi::Array;
@@ -101,12 +106,13 @@ use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectRefCore;
 use tvm_ffi::Result;
-use tvm_ffi::ShallowCopy;
 use tvm_ffi::String;
-use tvm_ffi::UnsafeInit;
 use tvm_ffi::VALUE_ERROR;
+use tvm_ffi::reflection::NativeFunction;
+use tvm_ffi::reflection::ShallowCopy;
+use tvm_ffi::reflection::TypeAttrColumn;
+use tvm_ffi::reflection::UnsafeInit;
 use tvm_ffi::reflection::drop_if_defined;
-use tvm_ffi::tvm_ffi_sys::TVMFFITypeIndex;
 // tvm-ffi-stubgen(end)
 
 /// The base of every statement: its source location `loc`.
@@ -376,8 +382,7 @@ impl AssertStmt {
         if !condition_ty.matches_code(&[DLDataTypeCode::kDLBool]) {
             let message = format!(
                 "AssertStmt should have boolean condition, but received {} 
with dtype {}",
-                support::repr(&condition),
-                support::repr(condition_ty)
+                condition, condition_ty
             );
             let check = "condition_ty.MatchesCode(DLDataTypeCode::kDLBool)";
             return Err(support::check_failed(tvm_ffi::INTERNAL_ERROR, check, 
&message));
@@ -551,8 +556,7 @@ impl For {
                 let message = format!(
                     "TIR For nodes require a scalar integer as the {field}, 
but received {} with \
                      dtype {}",
-                    support::repr(expr),
-                    support::repr(ty)
+                    expr, ty
                 );
                 let condition = "dtype.IsScalar() && 
(dtype.MatchesCode(DLDataTypeCode::kDLUInt, \
                                  DLDataTypeCode::kDLInt))";
@@ -569,8 +573,7 @@ impl For {
                 if !int_imm_value_fits(&imm.value, &var_ty)? {
                     let message = format!(
                         "Literal value {} is not representable in the loop 
variable's dtype ({})",
-                        imm.value,
-                        support::repr(&var_ty)
+                        imm.value, var_ty
                     );
                     let condition = "IntImmValueFits(a->value, var_ty)";
                     return Err(support::check_failed(
@@ -584,8 +587,7 @@ impl For {
             if ty.bits() > var_ty.bits() {
                 let message = format!(
                     " Loop variable's dtype ({}) is narrower than that of 
`begin` or `end` ({})",
-                    support::repr(&var_ty),
-                    support::repr(&ty)
+                    var_ty, ty
                 );
                 let condition = "e_ty.bits() <= var_ty.bits()";
                 return Err(support::check_failed(tvm_ffi::INTERNAL_ERROR, 
condition, &message));
@@ -597,7 +599,7 @@ impl For {
             if ty.dtype == var_ty.dtype {
                 return Ok(());
             }
-            let message = format!("{} vs {}", support::repr(&var_ty), 
support::repr(ty));
+            let message = format!("{var_ty} vs {ty}");
             Err(support::check_failed(tvm_ffi::INTERNAL_ERROR, condition, 
&message))
         };
         let (begin, end) = (promote(begin)?, promote(end)?);
@@ -775,28 +777,31 @@ impl TensorStore {
         value: impl IntoExpr<PrimExpr>,
         loc: impl Into<Location>,
     ) -> Result<Self> {
-        static VALIDATE: support::TypeAttr =
-            support::TypeAttr::new("__tvm_tensor_store_validate__");
+        // The native library registers the column when it loads, so one 
lookup suffices.
+        static VALIDATE: LazyLock<Option<TypeAttrColumn>> =
+            LazyLock::new(|| 
TypeAttrColumn::new("__tvm_tensor_store_validate__"));
         let (dest, value) = (dest.into_owned_expr()?, 
value.into_owned_expr()?);
         let indices = indices
             .into_iter()
             .map(IntoExpr::into_owned_expr)
             .collect::<Result<Array<PrimExpr>>>()?;
         let ty_index = AnyView::from(&dest.ty).type_index();
-        let Some(validate) = VALIDATE.get(ty_index) else {
-            let message =
-                format!("Type {} does not support TensorStore", 
support::type_key(ty_index));
+        let Some(validate) = VALIDATE.and_then(|column| column.get(ty_index)) 
else {
+            let message = format!(
+                "Type {} does not support TensorStore",
+                tvm_ffi::reflection::type_key(ty_index).unwrap_or_default()
+            );
             return Err(support::check_failed(
                 tvm_ffi::TYPE_ERROR,
                 "validate.has_value()",
                 &message,
             ));
         };
-        let result = support::call_with_refs(validate, &dest, &indices, 
&value)?;
-        if result.type_index() != TVMFFITypeIndex::kTVMFFINone as i32 {
-            let message = "Packed hook result type mismatch";
-            return Err(tvm_ffi::Error::new(tvm_ffi::TYPE_ERROR, message, ""));
-        }
+        // SAFETY: the column holds
+        // `void(const Expr&, const Array<PrimExpr>&, const PrimExpr&)` hooks.
+        let validate: NativeFunction<(&Expr, &Array<PrimExpr>, &PrimExpr)> =
+            unsafe { NativeFunction::from_any(validate)? };
+        validate.call((&dest, &indices, &value))?;
         Ok(Self::from_fields(loc.into(), dest, indices, value))
     }
 }
@@ -945,8 +950,11 @@ impl RegionStmt {
             }
         }
         let region = Self::from_fields(loc.into(), op, args, body_params, 
attrs, body, result_vars);
-        if let Some(validate) = support::NativeHook::of_op(&region.op, 
"FRegionValidate")? {
-            validate.call_void(&region)?;
+        // SAFETY: `FRegionValidate` holds `void(const RegionStmtObj*)` hooks.
+        let validate =
+            unsafe { support::op_hook::<(&RegionStmtObj,), ()>(&region.op, 
"FRegionValidate")? };
+        if let Some(validate) = validate {
+            validate.call((&*region,))?;
         }
         Ok(region)
     }
@@ -1362,7 +1370,10 @@ fn int_imm_value_fits(value: &BigInt, ty: &PrimType) -> 
Result<bool> {
 /// `FRegionGetBodyParams` gives once its validator accepts the arguments.
 fn region_body_params(op: &Op, args: &Array<Expr>, attrs: &DictAttrs) -> 
Result<Array<Var>> {
     let name = op.name.as_str();
-    let Some(get_body_params) = support::NativeHook::of_op(op, 
"FRegionGetBodyParams")? else {
+    // SAFETY: `FRegionGetBodyParams` holds `Array<Var>(const CallObj*)` hooks.
+    let get_body_params =
+        unsafe { support::op_hook::<(&CallObj,), Array<Var>>(op, 
"FRegionGetBodyParams")? };
+    let Some(get_body_params) = get_body_params else {
         let message = format!(
             "{name} does not support region construction: FRegionGetBodyParams 
is required"
         );
@@ -1377,7 +1388,7 @@ fn region_body_params(op: &Op, args: &Array<Expr>, attrs: 
&DictAttrs) -> Result<
         Array::from_vec(vec![]),
     );
     signature.validate()?;
-    let params: Array<Var> = get_body_params.call_as(&signature)?;
+    let params = get_body_params.call((&*signature,))?;
     let mut definitions = std::collections::HashSet::new();
     for param in params.iter() {
         if param.ty.downcast_ref::<MissingTypeObj>().is_some() {
diff --git a/rust/src/ir/support.rs b/rust/src/ir/support.rs
index 6860d139e..25d143f5a 100644
--- a/rust/src/ir/support.rs
+++ b/rust/src/ir/support.rs
@@ -23,19 +23,14 @@
 
 use std::cell::{OnceCell, RefCell};
 use std::collections::HashMap;
-use std::ffi::c_void;
 use std::fmt::Display;
 use std::rc::Rc;
-use std::sync::OnceLock;
 use std::thread::LocalKey;
 
-use tvm_ffi::tvm_ffi_sys::{
-    TVMFFIAny, TVMFFIFieldFlagBitMask, TVMFFIGetTypeInfo, TVMFFIObject, 
TVMFFITypeIndex,
-};
 use tvm_ffi::{
-    Any, AnyView, DLDataType, DLDataTypeCode, DLDataTypeExt, Error, ErrorKind, 
Function, ObjectArc,
-    ObjectRefCore, Result, TypeAttrColumn, INTERNAL_ERROR, TYPE_ERROR,
+    Any, DLDataType, DLDataTypeCode, DLDataTypeExt, Error, ErrorKind, Result, 
INTERNAL_ERROR,
 };
+use tvm_ffi::reflection::NativeFunction;
 
 use super::{Op, OpAttrMap, OpObj, PrimType};
 
@@ -108,55 +103,11 @@ pub(crate) fn compare_failed(
     Error::new(kind, &format!("Check failed: {condition} ({lhs} vs. {rhs}) : 
{message}"), "")
 }
 
-/// A value as error messages print it (`ffi.ReprPrint`).
-pub(crate) fn repr<T>(value: &T) -> std::string::String
-where
-    for<'a> AnyView<'a>: From<&'a T>,
-{
-    tvm_ffi::cached_global_func!("ffi.ReprPrint")
-        .call_packed(&[AnyView::from(value)])
-        .and_then(tvm_ffi::String::try_from)
-        .map(|text| text.as_str().to_string())
-        .unwrap_or_default()
-}
-
 /// A dtype as error messages print it, such as `int32`.
 pub(crate) fn dtype_name(dtype: DLDataType) -> std::string::String {
     dtype.to_string()
 }
 
-/// The registered key of a type index.
-pub(crate) fn type_key(type_index: i32) -> std::string::String {
-    // SAFETY: the type index of a live value is registered, and its 
information is static.
-    unsafe {
-        let info = TVMFFIGetTypeInfo(type_index);
-        (*info).type_key.as_str().to_string()
-    }
-}
-
-/// The default value the reflection of the type of `type_index` registers for 
its own field
-/// `name`, or `None` when it registers none or a factory.
-pub(crate) fn field_default(type_index: i32, name: &str) -> Option<Any> {
-    use TVMFFIFieldFlagBitMask::*;
-    // SAFETY: the type information of a registered type is static, and its 
`num_fields` fields
-    // are initialized.
-    let field = unsafe {
-        let info = &*TVMFFIGetTypeInfo(type_index);
-        let fields = std::slice::from_raw_parts(info.fields, info.num_fields 
as usize);
-        fields.iter().find(|field| field.name.as_str() == name)?
-    };
-    let has_default = field.flags & kTVMFFIFieldFlagBitMaskHasDefault as i64 
!= 0;
-    let from_factory = field.flags & kTVMFFIFieldFlagBitMaskDefaultFromFactory 
as i64 != 0;
-    if !has_default || from_factory {
-        return None;
-    }
-    // SAFETY: the registry owns the default value; it is copied, never 
released.
-    let value = std::mem::ManuallyDrop::new(unsafe {
-        Any::from_raw_ffi_any(field.default_value_or_factory)
-    });
-    Some((*value).clone())
-}
-
 /// A double as C++ `std::ostream` prints it by default: `%g`, six significant 
digits.
 #[derive(Clone, Copy)]
 pub(crate) struct CppDouble(pub f64);
@@ -240,120 +191,17 @@ pub(crate) fn scalable_vector(dtype: DLDataType, factor: 
i32) -> Result<DLDataTy
 // 
---------------------------------------------------------------------------------------------
 // Registry hooks
 
-/// A C++ `NativeFunctionView<R(const Node*)>` hook that a registry holds: the 
C ABI adapter its
-/// opaque pointer names, or a packed function a frontend registered in its 
place.
-pub(crate) enum NativeHook {
-    Adapter(extern "C" fn(*const c_void) -> TVMFFIAny),
-    Packed(Function),
-}
-
-impl NativeHook {
-    /// The hook `value` holds.
-    pub(crate) fn from_any(value: Any) -> Result<Self> {
-        if value.type_index() != TVMFFITypeIndex::kTVMFFIOpaquePtr as i32 {
-            return Ok(Self::Packed(value.try_into()?));
-        }
-        let address: *mut c_void = value.try_into()?;
-        // SAFETY: the opaque pointer of a `NativeFunctionView` is its C ABI 
adapter.
-        let adapter = unsafe {
-            std::mem::transmute::<*mut c_void, extern "C" fn(*const c_void) -> 
TVMFFIAny>(address)
-        };
-        Ok(Self::Adapter(adapter))
-    }
-
-    /// The hook `op` registers in the attribute column `column`, if any.
-    pub(crate) fn of_op(op: &OpObj, column: &'static str) -> 
Result<Option<Self>> {
-        op_attr(op, column)?.map(Self::from_any).transpose()
-    }
-
-    /// Call the hook on `node`: its result, or the error it reports.
-    pub(crate) fn call<T: ObjectRefCore>(&self, node: &T) -> Result<Any>
-    where
-        for<'a> AnyView<'a>: From<&'a T>,
-    {
-        let result = match self {
-            // SAFETY: `node` keeps the object alive for the call, and the 
adapter returns an
-            // owned value.
-            Self::Adapter(adapter) => unsafe {
-                let node = ObjectArc::as_ptr(ObjectRefCore::data(node)) as 
*const c_void;
-                Any::from_raw_ffi_any(adapter(node))
-            },
-            Self::Packed(function) => 
function.call_packed(&[AnyView::from(node)])?,
-        };
-        expected(result)
-    }
-
-    /// [`NativeHook::call`] of a hook that returns an `R`; a packed hook's 
result of another type
-    /// is a `TypeError`.
-    pub(crate) fn call_as<T: ObjectRefCore, R: TryFrom<Any>>(&self, node: &T) 
-> Result<R>
-    where
-        for<'a> AnyView<'a>: From<&'a T>,
-    {
-        R::try_from(self.call(node)?).map_err(|_| hook_result_mismatch())
-    }
-
-    /// [`NativeHook::call`] of a hook that returns nothing.
-    pub(crate) fn call_void<T: ObjectRefCore>(&self, node: &T) -> Result<()>
-    where
-        for<'a> AnyView<'a>: From<&'a T>,
-    {
-        match self.call(node)?.type_index() == TVMFFITypeIndex::kTVMFFINone as 
i32 {
-            true => Ok(()),
-            false => Err(hook_result_mismatch()),
-        }
-    }
-}
-
-/// Call a hook that takes three object references, such as a C++
-/// `NativeFunctionView<void(const Expr&, const Array<PrimExpr>&, const 
PrimExpr&)>`, and return its
-/// result or the error it reports. The C ABI adapter takes each reference as 
the address of an
-/// object pointer; a packed function takes the values.
-pub(crate) fn call_with_refs<A, B, C>(hook: Any, a: &A, b: &B, c: &C) -> 
Result<Any>
-where
-    A: ObjectRefCore,
-    B: ObjectRefCore,
-    C: ObjectRefCore,
-    for<'x> AnyView<'x>: From<&'x A> + From<&'x B> + From<&'x C>,
-{
-    if hook.type_index() != TVMFFITypeIndex::kTVMFFIOpaquePtr as i32 {
-        let function: Function = hook.try_into()?;
-        return expected(function.call_packed(&[
-            AnyView::from(a),
-            AnyView::from(b),
-            AnyView::from(c),
-        ])?);
-    }
-    type Adapter = extern "C" fn(
-        *const *const TVMFFIObject,
-        *const *const TVMFFIObject,
-        *const *const TVMFFIObject,
-    ) -> TVMFFIAny;
-    fn object<T: ObjectRefCore>(value: &T) -> *const TVMFFIObject {
-        // The pointer is only read while `value` keeps the object alive.
-        ObjectArc::as_ptr(ObjectRefCore::data(value)) as *const TVMFFIObject
-    }
-    let address: *mut c_void = hook.try_into()?;
-    let (a, b, c) = (object(a), object(b), object(c));
-    // SAFETY: the opaque pointer of a `NativeFunctionView` is its C ABI 
adapter; a C++ object
-    // reference is the address of its single object pointer, which outlives 
the call; and the
-    // adapter returns an owned value.
-    let result = unsafe {
-        let adapter = std::mem::transmute::<*mut c_void, Adapter>(address);
-        Any::from_raw_ffi_any(adapter(&a, &b, &c))
-    };
-    expected(result)
-}
-
-/// The value of an `Expected` result, or its error.
-fn expected(result: Any) -> Result<Any> {
-    match result.type_index() == TVMFFITypeIndex::kTVMFFIError as i32 {
-        true => Err(result.try_into()?),
-        false => Ok(result),
-    }
-}
-
-fn hook_result_mismatch() -> Error {
-    Error::new(TYPE_ERROR, "Packed hook result type mismatch", "")
+/// The hook `op` registers in the attribute column `column`, if any.
+///
+/// # Safety
+///
+/// A native function in the column must take `Args` and return `R`, as its 
declaration says.
+pub(crate) unsafe fn op_hook<Args, R>(
+    op: &OpObj,
+    column: &'static str,
+) -> Result<Option<NativeFunction<Args, R>>> {
+    // SAFETY: the caller vouches for the signature.
+    op_attr(op, column)?.map(|hook| unsafe { NativeFunction::from_any(hook) 
}).transpose()
 }
 
 thread_local! {
@@ -390,27 +238,3 @@ pub(crate) fn cached_op(name: &'static str) -> Result<Op> {
     OPS.with(|ops| ops.borrow_mut().insert(name, op.clone()));
     Ok(op)
 }
-
-/// A type attribute column, looked up once it exists.
-pub(crate) struct TypeAttr {
-    name: &'static str,
-    column: OnceLock<TypeAttrColumn>,
-}
-
-impl TypeAttr {
-    pub(crate) const fn new(name: &'static str) -> Self {
-        Self { name, column: OnceLock::new() }
-    }
-
-    /// The value the type of `type_index` registers in this column, if any.
-    pub(crate) fn get(&self, type_index: i32) -> Option<Any> {
-        let column = match self.column.get() {
-            Some(column) => *column,
-            None => {
-                let column = TypeAttrColumn::new(self.name)?;
-                *self.column.get_or_init(|| column)
-            }
-        };
-        column.get(type_index)
-    }
-}
diff --git a/rust/src/ir/transform/core.rs b/rust/src/ir/transform/core.rs
index 88245c3b9..57a3357d0 100644
--- a/rust/src/ir/transform/core.rs
+++ b/rust/src/ir/transform/core.rs
@@ -36,13 +36,13 @@ use crate::ir::support;
 use std::ops::Deref;
 use tvm_ffi::Any;
 use tvm_ffi::Array;
-use tvm_ffi::FieldGetter;
 use tvm_ffi::Map;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectCore;
 use tvm_ffi::Result;
 use tvm_ffi::String;
+use tvm_ffi::reflection::FieldGetter;
 // tvm-ffi-stubgen(end)
 
 /// A transformation of an [`IRModule`], with a name and an optimization level 
([`PassInfo`]).
diff --git a/rust/src/ir/type.rs b/rust/src/ir/type.rs
index 1ab2cc3cf..5d03a591e 100644
--- a/rust/src/ir/type.rs
+++ b/rust/src/ir/type.rs
@@ -61,9 +61,9 @@ use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectRefCore;
 use tvm_ffi::Result;
-use tvm_ffi::ShallowCopy;
-use tvm_ffi::UnsafeInit;
 use tvm_ffi::VALUE_ERROR;
+use tvm_ffi::reflection::ShallowCopy;
+use tvm_ffi::reflection::UnsafeInit;
 use tvm_ffi::reflection::drop_if_defined;
 // tvm-ffi-stubgen(end)
 
diff --git a/rust/src/ir/type_def.rs b/rust/src/ir/type_def.rs
index d73efee64..a589a4fd7 100644
--- a/rust/src/ir/type_def.rs
+++ b/rust/src/ir/type_def.rs
@@ -22,8 +22,6 @@
 
 use tvm_ffi::{Function, ObjectCore, ObjectRefCore, Result, String};
 
-use super::support;
-
 /// Expose the IR type of the reference `R`, an `rs_object` type, to Python.
 ///
 /// Call it in a [`tvm_ffi::static_init_block!`], so that the type is 
registered when its program or
@@ -71,9 +69,10 @@ use super::support;
 /// }
 /// ```
 pub fn register_type<R: ObjectRefCore>() -> Result<()> {
-    let type_index = R::ContainerType::type_index();
+    // The key of the registered type, which `type_index` registers first.
+    let key = 
tvm_ffi::reflection::type_key(R::ContainerType::type_index()).unwrap_or_default();
     if let Ok(expose) = Function::get_global("ir.expose_type") {
-        
expose.call_tuple((String::from(support::type_key(type_index).as_str()),))?;
+        expose.call_tuple((String::from(key),))?;
     }
     Ok(())
 }
diff --git a/rust/src/runtime.rs b/rust/src/runtime.rs
index 68f3b78e5..d008993e0 100644
--- a/rust/src/runtime.rs
+++ b/rust/src/runtime.rs
@@ -36,28 +36,20 @@
 use tvm_ffi::Any;
 use tvm_ffi::DLDataType;
 use tvm_ffi::DLDevice;
-use tvm_ffi::Object;
+use tvm_ffi::EnumObj;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::Result;
 use tvm_ffi::Shape;
 use tvm_ffi::String;
 use tvm_ffi::Tensor;
-
-/// Header-only stand-in for the builtin `ffi.Enum`; it only carries the 
ancestor depth.
-#[allow(dead_code)]
-#[repr(C)]
-#[derive(tvm_ffi::derive::Object)]
-#[type_key = "ffi.Enum"]
-struct FfiEnumObj {
-    base_: Object,
-}
 // tvm-ffi-stubgen(end)
 
 // `ir.PtrType.storage_scope` refers to this type.
 /// A registered memory space, such as `global` or `shared`, that a pointer or 
tensor lives in.
 ///
 /// Registration order has no meaning, and a backend must support a scope 
before it can allocate
-/// storage in it; [`MemoryScope::register`] adds one.
+/// storage in it; [`EnumDef::new`](tvm_ffi::reflection::EnumDef::new) adds 
one, and
+/// [`EnumRef::get`](tvm_ffi::EnumRef::get) looks one up by name.
 // tvm-ffi-stubgen(begin): object/runtime.MemoryScope
 #[allow(missing_docs)]
 #[repr(C)]
@@ -68,31 +60,25 @@ pub struct MemoryScope {
 
 /// The object behind [`MemoryScope`].
 ///
-/// Opaque: parent 'ffi.Enum' is opaque (no-mirror). Fields are read through 
the C ABI getters.
+/// Complete: reflected fields fill [48, 48) exactly.
 #[allow(missing_docs)]
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "runtime.MemoryScope"]
 #[type_final]
 pub struct MemoryScopeObj {
-    base_: FfiEnumObj,
+    base_: EnumObj,
 }
 
+const _: () = {
+    assert!(::core::mem::size_of::<MemoryScopeObj>() == 48);
+    assert!(::core::mem::align_of::<MemoryScopeObj>() == 8);
+};
+
 unsafe impl tvm_ffi::object::ImmutableObject for MemoryScopeObj {}
-// tvm-ffi-stubgen(end)
 
-impl MemoryScope {
-    /// Register the memory scope `name` (`runtime.MemoryScopeRegister`).
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if `name` is empty, contains a null character, or is 
already registered.
-    pub fn register(name: &str) -> Result<Self> {
-        tvm_ffi::cached_global_func!("runtime.MemoryScopeRegister")
-            .call_tuple((String::from(name),))?
-            .try_into()
-    }
-}
+impl tvm_ffi::EnumRef for MemoryScope {}
+// tvm-ffi-stubgen(end)
 
 /// Allocate an uninitialized tensor on `device`, in the memory scope 
`mem_scope` or the global one
 /// (`runtime.TensorAllocWithScope`).
diff --git a/rust/src/sym/constraint.rs b/rust/src/sym/constraint.rs
index 4bcff25dd..3ebe83c17 100644
--- a/rust/src/sym/constraint.rs
+++ b/rust/src/sym/constraint.rs
@@ -41,8 +41,8 @@ use std::ops::Deref;
 use std::ptr::addr_of_mut;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
-use tvm_ffi::ShallowCopy;
-use tvm_ffi::UnsafeInit;
+use tvm_ffi::reflection::ShallowCopy;
+use tvm_ffi::reflection::UnsafeInit;
 // tvm-ffi-stubgen(end)
 
 /// The base of every fact a [`ConstraintContext`] holds; it owns no analyzer 
state.
diff --git a/rust/src/sym/int_set.rs b/rust/src/sym/int_set.rs
index 014296323..570e6e770 100644
--- a/rust/src/sym/int_set.rs
+++ b/rust/src/sym/int_set.rs
@@ -46,12 +46,12 @@ use crate::sym::analyzer::Analyzer;
 use std::borrow::Borrow;
 use std::ops::Deref;
 use tvm_ffi::Array;
-use tvm_ffi::FieldGetter;
 use tvm_ffi::Map;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectCore;
 use tvm_ffi::Result;
+use tvm_ffi::reflection::FieldGetter;
 // tvm-ffi-stubgen(end)
 
 /// The base of every set of integers, one dimension each, that bounds the 
values an expression
diff --git a/rust/src/sym/iter_affine_map.rs b/rust/src/sym/iter_affine_map.rs
index 278140833..e3b8ceff7 100644
--- a/rust/src/sym/iter_affine_map.rs
+++ b/rust/src/sym/iter_affine_map.rs
@@ -61,13 +61,13 @@ use std::borrow::Borrow;
 use std::ops::Deref;
 use tvm_ffi::AnyView;
 use tvm_ffi::Array;
-use tvm_ffi::FieldGetter;
 use tvm_ffi::Map;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectCore;
 use tvm_ffi::Result;
 use tvm_ffi::String;
+use tvm_ffi::reflection::FieldGetter;
 // tvm-ffi-stubgen(end)
 
 /// The base of the iterator map forms, [`IterSplitExpr`] and [`IterSumExpr`]. 
They hold the result
diff --git a/rust/src/target.rs b/rust/src/target.rs
index 5ccd51896..f396ebdfe 100644
--- a/rust/src/target.rs
+++ b/rust/src/target.rs
@@ -33,7 +33,6 @@
 use crate::ir::module::IRModule;
 use tvm_ffi::Any;
 use tvm_ffi::Array;
-use tvm_ffi::FieldGetter;
 use tvm_ffi::Map;
 use tvm_ffi::Module;
 use tvm_ffi::Object;
@@ -42,6 +41,7 @@ use tvm_ffi::ObjectCore;
 use tvm_ffi::Result;
 use tvm_ffi::String;
 use tvm_ffi::object::ObjectRef;
+use tvm_ffi::reflection::FieldGetter;
 // tvm-ffi-stubgen(end)
 
 /// A compilation target: its `kind`, the `keys` that classify it, such as 
`cpu`, the `attrs` of
diff --git a/rust/src/tirx/layout.rs b/rust/src/tirx/layout.rs
index ba8362e09..827ed534b 100644
--- a/rust/src/tirx/layout.rs
+++ b/rust/src/tirx/layout.rs
@@ -48,22 +48,14 @@ use std::borrow::Borrow;
 use std::ops::Deref;
 use tvm_ffi::Any;
 use tvm_ffi::Array;
-use tvm_ffi::FieldGetter;
+use tvm_ffi::EnumObj;
 use tvm_ffi::Map;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectCore;
 use tvm_ffi::Result;
 use tvm_ffi::String;
-
-/// Header-only stand-in for the builtin `ffi.Enum`; it only carries the 
ancestor depth.
-#[allow(dead_code)]
-#[repr(C)]
-#[derive(tvm_ffi::derive::Object)]
-#[type_key = "ffi.Enum"]
-struct FfiEnumObj {
-    base_: Object,
-}
+use tvm_ffi::reflection::FieldGetter;
 // tvm-ffi-stubgen(end)
 
 /// The base of every layout: a map from the logical indices of a tensor to a 
coordinate on each
@@ -82,7 +74,7 @@ struct FfiEnumObj {
 /// use tvm_tirx::tirx::{Axis, Iter, Layout, TileLayout};
 /// use tvm_tirx::tvm_ffi::Map;
 ///
-/// let m = Axis::get("m");
+/// let m = Axis::get_or_register("m");
 /// let shard = [Iter::new(8, 8, m.clone()), Iter::new(8, 1, m)];
 /// let layout = Layout::from(&TileLayout::new(shard, Vec::<Iter>::new(), 
&Map::new()));
 /// assert_eq!(script(&layout.size(None)?)?, "64");
@@ -398,8 +390,8 @@ unsafe impl tvm_ffi::object::ImmutableObject for IterObj {}
 // The native `tirx.Iter` stores its fields as given, as the generated 
`Iter::new` does.
 
 /// A named coordinate a layout maps to: the memory axis `m`, or a thread 
axis, such as `laneid`,
-/// that indexes the threads of a scope. Each name is one registered axis, 
which [`Axis::get`]
-/// looks up.
+/// that indexes the threads of a scope. Each name is one registered axis, 
which
+/// [`Axis::get_or_register`] looks up, registering it on first use.
 // tvm-ffi-stubgen(begin): object/tirx.Axis
 #[allow(missing_docs)]
 #[repr(C)]
@@ -410,23 +402,30 @@ pub struct Axis {
 
 /// The object behind [`Axis`].
 ///
-/// Opaque: parent 'ffi.Enum' is opaque (no-mirror). Fields are read through 
the C ABI getters.
+/// Complete: reflected fields fill [48, 48) exactly.
 #[allow(missing_docs)]
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "tirx.Axis"]
 #[type_final]
 pub struct AxisObj {
-    base_: FfiEnumObj,
+    base_: EnumObj,
 }
 
+const _: () = {
+    assert!(::core::mem::size_of::<AxisObj>() == 48);
+    assert!(::core::mem::align_of::<AxisObj>() == 8);
+};
+
 unsafe impl tvm_ffi::object::ImmutableObject for AxisObj {}
+
+impl tvm_ffi::EnumRef for Axis {}
 // tvm-ffi-stubgen(end)
 
 impl Axis {
     /// Look up the axis named `name`, such as the memory axis `m`, 
registering it on first use
-    /// (`tirx.AxisGet`).
-    pub fn get(name: &str) -> Self {
+    /// (`tirx.AxisGet`). [`EnumRef::get`](tvm_ffi::EnumRef::get) only looks 
up.
+    pub fn get_or_register(name: &str) -> Self {
         support::infallible(
             tvm_ffi::cached_global_func!("tirx.AxisGet")
                 .call_tuple((String::from(name),))
diff --git a/rust/src/tirx/op.rs b/rust/src/tirx/op.rs
index b5262acd5..4a0c02205 100644
--- a/rust/src/tirx/op.rs
+++ b/rust/src/tirx/op.rs
@@ -183,7 +183,7 @@ pub fn logaddexp_at(
     for (operand, side) in [(&a, "a"), (&b, "b")] {
         if !operand.prim_ty().matches_code(&[DLDataTypeCode::kDLFloat]) {
             let condition = 
format!("{side}.ty().MatchesCode(DLDataTypeCode::kDLFloat)");
-            let message = support::repr(operand);
+            let message = operand.to_string();
             return Err(support::check_failed(INTERNAL_ERROR, &condition, 
&message));
         }
     }
@@ -229,10 +229,7 @@ pub fn reinterpret_at(
     };
     if value.downcast_ref::<StringImmObj>().is_some() {
         if !target_is_pointer {
-            let message = format!(
-                "String reinterpret requires a pointer target, but got {}",
-                support::repr(&ty)
-            );
+            let message = format!("String reinterpret requires a pointer 
target, but got {ty}");
             return type_error("target_ty.as<PtrTypeObj>()", message);
         }
     } else if let Some(target) = ty.downcast_ref::<PrimType>() {
@@ -252,38 +249,28 @@ pub fn reinterpret_at(
                                      
((value_dtype.MatchesCode(DLDataTypeCode::kDLFloat4_e2m1fn) || \
                                      
target_prim.MatchesCode(DLDataTypeCode::kDLFloat4_e2m1fn)) && \
                                      value_dtype.StorageBytes() == 
target_prim.StorageBytes())";
-                    let message = format!(
-                        "Reinterpret requires size match {} vs {}",
-                        support::repr(&ty),
-                        support::repr(&value.ty)
-                    );
+                    let message = format!("Reinterpret requires size match {} 
vs {}", ty, value.ty);
                     return Err(support::check_failed(INTERNAL_ERROR, 
condition, &message));
                 }
             }
         } else {
             if !source_is_pointer {
-                let message = format!(
-                    "Reinterpret source must be PrimType or PtrType, but got 
{}",
-                    support::repr(&value.ty)
-                );
+                let message =
+                    format!("Reinterpret source must be PrimType or PtrType, 
but got {}", value.ty);
                 return type_error("value->ty.as<PtrTypeObj>()", message);
             }
             if !is_scalar_int64(target) {
                 let condition = "target_dtype.value().IsScalar() && 
target_dtype.value().bits() == 64 && \
                                  
target_dtype.value().MatchesCode(DLDataTypeCode::kDLInt, 
DLDataTypeCode::kDLUInt)";
                 let message = format!(
-                    "Pointer reinterpret requires a scalar 64-bit integer 
target, but got {}",
-                    support::repr(&ty)
+                    "Pointer reinterpret requires a scalar 64-bit integer 
target, but got {ty}"
                 );
                 return type_error(condition, message);
             }
         }
     } else {
         if !target_is_pointer {
-            let message = format!(
-                "Reinterpret target must be PrimType or PtrType, but got {}",
-                support::repr(&ty)
-            );
+            let message = format!("Reinterpret target must be PrimType or 
PtrType, but got {ty}");
             return type_error("target_ty.as<PtrTypeObj>()", message);
         }
         if let Some(source) = value.ty.downcast_ref::<PrimType>() {
@@ -292,15 +279,13 @@ pub fn reinterpret_at(
                                  
source_dtype.value().MatchesCode(DLDataTypeCode::kDLInt, 
DLDataTypeCode::kDLUInt)";
                 let message = format!(
                     "Pointer reinterpret requires a scalar 64-bit integer 
source, but got {}",
-                    support::repr(&value.ty)
+                    value.ty
                 );
                 return type_error(condition, message);
             }
         } else if !source_is_pointer {
-            let message = format!(
-                "Reinterpret source must be PrimType or PtrType, but got {}",
-                support::repr(&value.ty)
-            );
+            let message =
+                format!("Reinterpret source must be PrimType or PtrType, but 
got {}", value.ty);
             return type_error("value->ty.as<PtrTypeObj>()", message);
         }
     }
diff --git a/rust/src/tirx/structural.rs b/rust/src/tirx/structural.rs
index bfc48d411..b452bc262 100644
--- a/rust/src/tirx/structural.rs
+++ b/rust/src/tirx/structural.rs
@@ -25,11 +25,11 @@
 
 // tvm-ffi-stubgen(begin): import-section
 use crate::sym::constraint::ConstraintContext;
-use tvm_ffi::FieldGetter;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectCore;
 use tvm_ffi::Result;
+use tvm_ffi::reflection::FieldGetter;
 
 /// Header-only stand-in for the builtin `ffi.StructuralMutator`; it only 
carries the ancestor depth.
 #[allow(dead_code)]
diff --git a/rust/src/tirx/tensor.rs b/rust/src/tirx/tensor.rs
index d28bc7516..763f27786 100644
--- a/rust/src/tirx/tensor.rs
+++ b/rust/src/tirx/tensor.rs
@@ -17,9 +17,9 @@
  * under the License.
  */
 
-//! Tensors. A tensor is a [`Var`] whose type is a [`TensorType`], which fixes 
its shape, element
-//! type, memory scope, alignment and [`Layout`]; loads and stores subscript 
the variable, and
-//! [`TensorVar`] matches one in an operator signature.
+//! Tensors. A tensor is a [`Var`](crate::ir::Var) whose type is a 
[`TensorType`], which fixes its
+//! shape, element type, memory scope, alignment and [`Layout`]; loads and 
stores subscript the
+//! variable, and [`TensorVar`] matches one in an operator signature.
 
 // tvm-ffi-stubgen(missing-docs): allow
 // `custom-new` reserves `new` for a hand-written constructor (defaults); the 
generated
@@ -28,38 +28,30 @@
 
 // Names the hand-written wrappers use: the import section keeps them even 
when no generated block
 // does.
-// tvm-ffi-stubgen(import-object): crate::ir::expr::Var
 // tvm-ffi-stubgen(import-object): crate::sym::analyzer::Analyzer
 // tvm-ffi-stubgen(import-object): tvm_ffi::AnyView
 // tvm-ffi-stubgen(import-object): tvm_ffi::ObjectCore
 // tvm-ffi-stubgen(import-object): tvm_ffi::Result
-// tvm-ffi-stubgen(import-object): crate::ir::support
 // tvm-ffi-stubgen(import-object): crate::ir::sig
 // tvm-ffi-stubgen(import-object): crate::ir::expr::IntoExpr
 // tvm-ffi-stubgen(import-object): crate::ir::expr::PrimExpr
 // A tensor's shape is a list of `PrimExpr`s, as C++ `TensorTypeObj` holds it.
 // tvm-ffi-stubgen(field): tirx.TensorType.shape -> Array<PrimExpr>
 // tvm-ffi-stubgen(import-object): tvm_ffi::Array
-// tvm-ffi-stubgen(import-object): std::borrow::Borrow
-// tvm-ffi-stubgen(import-object): tvm_ffi::IntoObjectRef
 // tvm-ffi-stubgen(begin): import-section
 use crate::ir::expr::IntoExpr;
 use crate::ir::expr::PrimExpr;
-use crate::ir::expr::Var;
 use crate::ir::location::Location;
 use crate::ir::r#type::PrimType;
 use crate::ir::r#type::Type;
 use crate::ir::r#type::TypeObj;
 use crate::ir::sig;
-use crate::ir::support;
 use crate::runtime::MemoryScope;
 use crate::sym::analyzer::Analyzer;
 use crate::tirx::layout::Layout;
-use std::borrow::Borrow;
 use std::ops::Deref;
 use tvm_ffi::AnyView;
 use tvm_ffi::Array;
-use tvm_ffi::IntoObjectRef;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectCore;
 use tvm_ffi::Result;
@@ -78,8 +70,9 @@ use tvm_ffi::Result;
 /// use tvm_tirx::runtime::MemoryScope;
 /// use tvm_tirx::script::script;
 /// use tvm_tirx::tirx::TensorType;
+/// use tvm_tirx::tvm_ffi::reflection::EnumDef;
 ///
-/// let scratch = MemoryScope::register("my_ext.scratch")?;
+/// let scratch = EnumDef::<MemoryScope>::new("my_ext.scratch")?.value();
 /// let ty = TensorType::new([16, 16], PrimType::float(32), &scratch, 0, None, 
UNKNOWN_LOC)?;
 /// assert_eq!(script(&ty)?, r#"T.Tensor((16, 16), T.float32, 
scope="my_ext.scratch")"#);
 ///
@@ -209,8 +202,8 @@ impl TensorType {
     }
 }
 
-/// A signature descriptor ([`sig::ArgType`]) that matches a [`Var`] whose 
type is a
-/// [`TensorType`]; it is not a type of its own.
+/// A signature descriptor ([`sig::ArgType`]) that matches a 
[`Var`](crate::ir::Var) whose type is
+/// a [`TensorType`]; it is not a type of its own.
 pub struct TensorVar;
 
 impl sig::ArgType for TensorVar {
@@ -223,19 +216,6 @@ impl sig::ArgType for TensorVar {
     }
 }
 
-/// Return whether the tensor variable `tensor` is a scalar: one element, the 
default alignment and
-/// a trivial layout; `alloc_or_decl` is true for `alloc_scalar`, false for 
`decl_scalar`
-/// (`tirx.TensorIsScalar`).
-///
-/// # Errors
-///
-/// Returns an error if the type of `tensor` is not a [`TensorType`].
-pub fn tensor_is_scalar(tensor: impl IntoObjectRef<Var>, alloc_or_decl: bool) 
-> Result<bool> {
-    tvm_ffi::cached_global_func!("tirx.TensorIsScalar")
-        .call_tuple((tensor.into_object_ref().borrow(), alloc_or_decl))?
-        .try_into()
-}
-
 /// The runtime's allocation alignment, which a build may configure, read from 
the default the
 /// reflection registers for `TensorType::data_alignment`.
 fn alloc_alignment() -> Result<i32> {
@@ -243,7 +223,7 @@ fn alloc_alignment() -> Result<i32> {
     if let Some(alignment) = ALLOC_ALIGNMENT.get() {
         return Ok(*alignment);
     }
-    let Some(default) = support::field_default(TensorTypeObj::type_index(), 
"data_alignment")
+    let Some(default) = 
tvm_ffi::reflection::field_default::<TensorTypeObj>("data_alignment")
     else {
         let message = "tirx.TensorType registers no default data_alignment";
         return Err(tvm_ffi::Error::new(tvm_ffi::INTERNAL_ERROR, message, ""));
diff --git a/rust/src/tirx/thread_scope.rs b/rust/src/tirx/thread_scope.rs
index 5d0d69fd0..ea69dd257 100644
--- a/rust/src/tirx/thread_scope.rs
+++ b/rust/src/tirx/thread_scope.rs
@@ -30,31 +30,23 @@
 
 // tvm-ffi-stubgen(begin): import-section
 use std::borrow::Borrow;
+use tvm_ffi::EnumObj;
 use tvm_ffi::IntoObjectRef;
-use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::Result;
 use tvm_ffi::String;
-
-/// Header-only stand-in for the builtin `ffi.Enum`; it only carries the 
ancestor depth.
-#[allow(dead_code)]
-#[repr(C)]
-#[derive(tvm_ffi::derive::Object)]
-#[type_key = "ffi.Enum"]
-struct FfiEnumObj {
-    base_: Object,
-}
 // tvm-ffi-stubgen(end)
 
 /// A level of a backend's thread hierarchy, named `backend.level`, such as 
`cuda.warp`.
 ///
-/// Backends register their scopes; [`ThreadScope::get`] only looks one up. 
Within a backend,
+/// Backends register their scopes; [`EnumRef::get`] only looks one up. Within 
a backend,
 /// [`rank`](ThreadScope::rank) orders the levels, a wider scope ranking lower.
 ///
 /// # Examples
 ///
 /// ```
 /// use tvm_tirx::tirx::ThreadScope;
+/// use tvm_tirx::tvm_ffi::EnumRef;
 ///
 /// let warp = ThreadScope::get("cuda.warp")?;
 /// assert_eq!((warp.backend()?, warp.local_name()?), ("cuda".into(), 
"warp".into()));
@@ -63,6 +55,8 @@ struct FfiEnumObj {
 /// assert!(cta.is_higher_than(&warp)?);
 /// # Ok::<(), tvm_tirx::tvm_ffi::Error>(())
 /// ```
+///
+/// [`EnumRef::get`]: tvm_ffi::EnumRef::get
 // tvm-ffi-stubgen(begin): object/tirx.ThreadScope
 #[allow(missing_docs)]
 #[repr(C)]
@@ -73,31 +67,27 @@ pub struct ThreadScope {
 
 /// The object behind [`ThreadScope`].
 ///
-/// Opaque: parent 'ffi.Enum' is opaque (no-mirror). Fields are read through 
the C ABI getters.
+/// Complete: reflected fields fill [48, 48) exactly.
 #[allow(missing_docs)]
 #[repr(C)]
 #[derive(tvm_ffi::derive::Object)]
 #[type_key = "tirx.ThreadScope"]
 #[type_final]
 pub struct ThreadScopeObj {
-    base_: FfiEnumObj,
+    base_: EnumObj,
 }
 
+const _: () = {
+    assert!(::core::mem::size_of::<ThreadScopeObj>() == 48);
+    assert!(::core::mem::align_of::<ThreadScopeObj>() == 8);
+};
+
 unsafe impl tvm_ffi::object::ImmutableObject for ThreadScopeObj {}
+
+impl tvm_ffi::EnumRef for ThreadScope {}
 // tvm-ffi-stubgen(end)
 
 impl ThreadScope {
-    /// Look up the thread scope named `name`, such as `cuda.warp` 
(`tirx.ThreadScopeGet`).
-    ///
-    /// # Errors
-    ///
-    /// Returns an error if no thread scope is registered as `name`.
-    pub fn get(name: &str) -> Result<Self> {
-        tvm_ffi::cached_global_func!("tirx.ThreadScopeGet")
-            .call_tuple((String::from(name),))?
-            .try_into()
-    }
-
     /// Return the backend that registered the scope, such as `cuda` 
(`tirx.ThreadScopeGetBackend`).
     pub fn backend(&self) -> Result<String> {
         
tvm_ffi::cached_global_func!("tirx.ThreadScopeGetBackend").call_tuple((self,))?.try_into()
diff --git a/rust/src/tirx/tile/dispatch.rs b/rust/src/tirx/tile/dispatch.rs
index ab05b394c..06322e2dd 100644
--- a/rust/src/tirx/tile/dispatch.rs
+++ b/rust/src/tirx/tile/dispatch.rs
@@ -47,7 +47,6 @@ use crate::tirx::thread_scope::ThreadScope;
 use tvm_ffi::Any;
 use tvm_ffi::AnyView;
 use tvm_ffi::Array;
-use tvm_ffi::FieldGetter;
 use tvm_ffi::Map;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
@@ -56,6 +55,7 @@ use tvm_ffi::ObjectRefCore;
 use tvm_ffi::Result;
 use tvm_ffi::String;
 use tvm_ffi::object::ObjectRef;
+use tvm_ffi::reflection::FieldGetter;
 // tvm-ffi-stubgen(end)
 
 /// The context a tile instruction call is lowered in: the `target`, the 
`thread_scope` of the
@@ -74,7 +74,7 @@ use tvm_ffi::object::ObjectRef;
 /// use tvm_tirx::target::Target;
 /// use tvm_tirx::tirx::tile::DispatchContext;
 /// use tvm_tirx::tirx::ThreadScope;
-/// use tvm_tirx::tvm_ffi::Map;
+/// use tvm_tirx::tvm_ffi::{EnumRef, Map};
 ///
 /// let tx = PrimVar::new("tx", PrimType::int(32), UNKNOWN_LOC);
 /// let ctx = DispatchContext::new(
diff --git a/rust/src/tirx/tile/instruction.rs 
b/rust/src/tirx/tile/instruction.rs
index ab939455c..ff644684a 100644
--- a/rust/src/tirx/tile/instruction.rs
+++ b/rust/src/tirx/tile/instruction.rs
@@ -232,7 +232,7 @@ pub fn as_tensor_region(value: impl IntoExpr) -> 
Result<TensorRegion> {
 /// use tvm_tirx::ir::{Attrs, Call, OpDef, PrimType, Type, UNKNOWN_LOC};
 /// use tvm_tirx::tirx::tile::{configure_tensor_instruction, ScopeAttrs, 
ScopeAttrsObj};
 /// use tvm_tirx::tirx::ThreadScope;
-/// use tvm_tirx::tvm_ffi::{ensure, ObjectRefCore, Result, VALUE_ERROR};
+/// use tvm_tirx::tvm_ffi::{ensure, EnumRef, ObjectRefCore, Result, 
VALUE_ERROR};
 ///
 /// let op = OpDef::new("my_ext.tile_fill", "Fill a tile.")
 ///     .add_arg("value", "The fill value.")
diff --git a/rust/src/tirx/transform/function_pass.rs 
b/rust/src/tirx/transform/function_pass.rs
index 839837b53..6b9e51a4d 100644
--- a/rust/src/tirx/transform/function_pass.rs
+++ b/rust/src/tirx/transform/function_pass.rs
@@ -38,11 +38,11 @@ use crate::ir::transform::core::PassInfo;
 use crate::ir::transform::core::PassObj;
 use crate::tirx::function::Function;
 use std::ops::Deref;
-use tvm_ffi::FieldGetter;
 use tvm_ffi::ObjectArc;
 use tvm_ffi::ObjectCore;
 use tvm_ffi::RValueRef;
 use tvm_ffi::Result;
+use tvm_ffi::reflection::FieldGetter;
 // tvm-ffi-stubgen(end)
 
 /// A pass that replaces each TIRx function of a module by what a function 
returns for it, or
diff --git a/rust/tests/ir/op_def.rs b/rust/tests/ir/op_def.rs
index 93290469d..0a65e206a 100644
--- a/rust/tests/ir/op_def.rs
+++ b/rust/tests/ir/op_def.rs
@@ -39,8 +39,8 @@ use tvm_tirx::tvm_ffi::static_init::{StaticInitBlock, 
STATIC_INIT_BLOCKS};
 use tvm_tirx::tvm_ffi::tvm_ffi_sys::TVMFFITypeIndex;
 use tvm_tirx::tvm_ffi::{
     self, rs_object, run_static_init_blocks, Any, AnyView, Array, Error, 
ErrorKind, Function, Map,
-    ObjectCore, ObjectRefCore, Result, String as FfiString, ATTRIBUTE_ERROR, 
INTERNAL_ERROR,
-    TYPE_ERROR, VALUE_ERROR,
+    ObjectCore, ObjectRefCore, Result, String as FfiString, ATTRIBUTE_ERROR, 
TYPE_ERROR,
+    VALUE_ERROR,
 };
 
 use super::{int32, parse_dtype, tensor_var};
@@ -100,10 +100,6 @@ fn only_variables(call: &CallObj) -> Result<()> {
     }
 }
 
-fn faulty(_call: &CallObj) -> Result<()> {
-    panic!("validator bug")
-}
-
 #[test]
 fn ops_registered_from_rust_are_used_natively() {
     let _serial = serial();
@@ -156,15 +152,12 @@ fn ops_registered_from_rust_are_used_natively() {
     assert_eq!(INFERRED.load(Ordering::Relaxed), 3);
     validate(&identity).unwrap();
 
-    // An error a Rust hook returns reaches the native caller with its kind 
and message, and a
-    // panic becomes an error instead of unwinding into C++.
+    // An error a Rust hook returns reaches the native caller with its kind 
and message.
     let untyped = OpDef::new("rust_test.untyped", "")
         .set_infer_type(|_: &CallObj| Err(Error::new(TYPE_ERROR, "no result 
type", "")))
         .op()
         .unwrap();
     assert_error(call(&untyped, vec![]), TYPE_ERROR, "no result type");
-    let faulty = OpDef::new("rust_test.faulty", 
"").set_validator(faulty).op().unwrap();
-    assert_error(validate(&call(&faulty, vec![]).unwrap()), INTERNAL_ERROR, 
"validator bug");
 
     // A generated validator rejects what the C++ one generated from the same 
signature does,
     // with the same message.
@@ -444,9 +437,6 @@ tvm_ffi::static_init_block! {
         .set_region_body_params(launch_body_params)
         .set_region_validate(no_results_or_attributes);
 
-    OpDef::new("rust_test.panicking_region", "")
-        .set_region_body_params(|_: &CallObj| -> Result<Array<Var>> { 
panic!("hook bug") });
-
     register_type::<BlockType>()?;
 }
 
@@ -706,19 +696,4 @@ fn static_init_blocks_define_operators_and_types() {
         let cpp = region(&native, tag, extent, params, attrs, results);
         assert_eq!(rust, cpp, "{what}");
     }
-
-    // A panic in a hook becomes an error instead of unwinding into C++.
-    let panicking = Op::get("rust_test.panicking_region").unwrap();
-    let body = Evaluate::new(0, UNKNOWN_LOC);
-    let empty = DictAttrs::new(Map::new());
-    let built = RegionStmt::new(
-        panicking,
-        Vec::<Expr>::new(),
-        Vec::<Var>::new(),
-        empty,
-        body,
-        Vec::<Var>::new(),
-        UNKNOWN_LOC,
-    );
-    assert!(built.is_err(), "a panicking hook fails the constructor");
 }
diff --git a/rust/tests/ir/type.rs b/rust/tests/ir/type.rs
index 87f756776..7c733cf2c 100644
--- a/rust/tests/ir/type.rs
+++ b/rust/tests/ir/type.rs
@@ -28,10 +28,11 @@ use tvm_tirx::ir::{
     OpaqueType, PrimType, PtrType, StringType, TensorRegionType, TupleType, 
Type, TypeObj, Var,
     VarObj,
 };
+use tvm_tirx::tvm_ffi::reflection::{FieldGetter, ShallowCopy};
 use tvm_tirx::tvm_ffi::{rs_object, structural_hash, structural_map};
 use tvm_tirx::tvm_ffi::{
-    Any, AnyView, Array, DLDataType, DLDataTypeExt, FieldGetter, ObjectCore, 
ObjectRefCore,
-    ShallowCopy, String as FfiString, WalkOrder,
+    Any, AnyView, Array, DLDataType, DLDataTypeExt, ObjectCore, ObjectRefCore, 
String as FfiString,
+    WalkOrder,
 };
 
 use super::{scope};
diff --git a/rust/tests/runtime/main.rs b/rust/tests/runtime/main.rs
index 7b424072f..57acb6f3c 100644
--- a/rust/tests/runtime/main.rs
+++ b/rust/tests/runtime/main.rs
@@ -25,6 +25,7 @@ mod common;
 
 use tvm_tirx::runtime::{self, MemoryScope};
 use tvm_tirx::ir::dtype;
+use tvm_tirx::tvm_ffi::reflection::EnumDef;
 use tvm_tirx::tvm_ffi::{DLDevice, DLDeviceType, TypeIndex};
 
 #[test]
@@ -42,8 +43,8 @@ fn tensors() {
 
 #[test]
 fn memory_scopes() {
-    let scope = MemoryScope::register("rust_test.scratch").unwrap();
-    let again = MemoryScope::register("rust_test.scratch");
+    let scope = 
EnumDef::<MemoryScope>::new("rust_test.scratch").unwrap().value();
+    let again = EnumDef::<MemoryScope>::new("rust_test.scratch");
     assert!(again.is_err());
     // The CPU allocator supports the global scope only.
     let cpu = DLDevice::new(DLDeviceType::kDLCPU, 0);
diff --git a/rust/tests/tirx/layout.rs b/rust/tests/tirx/layout.rs
index aea687855..1469babae 100644
--- a/rust/tests/tirx/layout.rs
+++ b/rust/tests/tirx/layout.rs
@@ -30,7 +30,7 @@ use crate::common::{global, serial};
 #[test]
 fn memory_axes() {
     let _serial = serial();
-    let m = Axis::get("m");
+    let m = Axis::get_or_register("m");
     assert!(m.is_memory().unwrap() && !m.is_thread().unwrap());
     assert!(m.backend().unwrap().is_none());
     assert!(m.get_scope().unwrap().is_none());
@@ -73,7 +73,7 @@ fn layouts() {
 fn thread_layout_scope() {
     let _serial = serial();
     global("tirx.cuda.RegisterLayoutAxes").call_tuple(()).unwrap();
-    let laneid = Axis::get("laneid");
+    let laneid = Axis::get_or_register("laneid");
     let lanes =
         TileLayout::new([Iter::new(int32(32), int32(1), laneid)], 
Vec::<Iter>::new(), &Map::new());
     let (inner, outer) = lanes.get_scope().unwrap().unwrap();
diff --git a/rust/tests/tirx/main.rs b/rust/tests/tirx/main.rs
index 5d5a43546..85b996172 100644
--- a/rust/tests/tirx/main.rs
+++ b/rust/tests/tirx/main.rs
@@ -45,6 +45,7 @@ use tvm_tirx::ir::{
 };
 use tvm_tirx::runtime::MemoryScope;
 use tvm_tirx::tirx::{Axis, Function, Iter, TileLayout};
+use tvm_tirx::tvm_ffi::reflection::EnumDef;
 use tvm_tirx::tvm_ffi::{Array, Map, ObjectRefCore, String as FfiString};
 
 use common::global;
@@ -85,12 +86,12 @@ fn as_i64(expr: &impl ObjectRefCore) -> i64 {
 fn memory_scope() -> MemoryScope {
     static COUNT: AtomicUsize = AtomicUsize::new(0);
     let index = COUNT.fetch_add(1, Ordering::Relaxed);
-    MemoryScope::register(&format!("rust.tests.tirx.{index}")).unwrap()
+    
EnumDef::<MemoryScope>::new(&format!("rust.tests.tirx.{index}")).unwrap().value()
 }
 
 /// The row-major layout of an 8 x 8 tile.
 fn tile_8x8() -> TileLayout {
-    let m = Axis::get("m");
+    let m = Axis::get_or_register("m");
     TileLayout::new(
         [Iter::new(int32(8), int32(8), m.clone()), Iter::new(int32(8), 
int32(1), m)],
         Vec::<Iter>::new(),
diff --git a/rust/tests/tirx/tensor.rs b/rust/tests/tirx/tensor.rs
index 69e501008..e24ab8a24 100644
--- a/rust/tests/tirx/tensor.rs
+++ b/rust/tests/tirx/tensor.rs
@@ -20,10 +20,10 @@
 //! Tests of `tirx::tensor`: tensor types.
 
 use tvm_tirx::ir::{UNKNOWN_LOC, UnknownLoc, Expr, PrimExpr, PrimType, 
StringImm};
-use tvm_tirx::tirx::{tensor_is_scalar, Layout, TensorType};
+use tvm_tirx::tirx::{Layout, TensorType};
 use tvm_tirx::tvm_ffi::Array;
 
-use super::{int32, memory_scope, tile_8x8, var};
+use super::{int32, memory_scope, tile_8x8};
 use crate::common::{agree, agree_kind, native, serial};
 
 #[test]
@@ -45,8 +45,6 @@ fn tensor_types() {
     )
     .unwrap();
     assert!(laid_out.has_default_layout(None).unwrap());
-    assert!(tensor_is_scalar(var("A", &ty), true).unwrap());
-    assert!(!tensor_is_scalar(var("B", &laid_out), true).unwrap());
 }
 
 #[test]
diff --git a/rust/tests/tirx/thread_scope.rs b/rust/tests/tirx/thread_scope.rs
index d57467390..5b0791794 100644
--- a/rust/tests/tirx/thread_scope.rs
+++ b/rust/tests/tirx/thread_scope.rs
@@ -20,6 +20,7 @@
 //! Tests of `tirx::thread_scope`.
 
 use tvm_tirx::tirx::ThreadScope;
+use tvm_tirx::tvm_ffi::EnumRef;
 
 use crate::common::serial;
 
diff --git a/rust/tests/tirx/tile/dispatch.rs b/rust/tests/tirx/tile/dispatch.rs
index 0414c99c9..59e9bf2b7 100644
--- a/rust/tests/tirx/tile/dispatch.rs
+++ b/rust/tests/tirx/tile/dispatch.rs
@@ -23,7 +23,7 @@ use tvm_tirx::ir::{UnknownLoc, UNKNOWN_LOC, PrimExpr, 
PrimType, Stmt, Var};
 use tvm_tirx::target::Target;
 use tvm_tirx::tirx::tile::DispatchContext;
 use tvm_tirx::tirx::{TensorType, ThreadScope};
-use tvm_tirx::tvm_ffi::{Any, Map, ObjectRefCore, String as FfiString};
+use tvm_tirx::tvm_ffi::{Any, EnumRef, Map, ObjectRefCore, String as FfiString};
 
 use crate::common::{global, serial, structural_equal};
 use crate::{int32, memory_scope, prim_var, var};
diff --git a/src/tirx/ir/tensor.cc b/src/tirx/ir/tensor.cc
index f8b200210..a20df28c9 100644
--- a/src/tirx/ir/tensor.cc
+++ b/src/tirx/ir/tensor.cc
@@ -418,12 +418,5 @@ ffi::Array<PrimExpr> 
TensorTypeObj::ElemOffset(ffi::Array<PrimExpr> input_indice
 TensorVar::TensorVar(ffi::String name, TensorType type, Location loc)
     : Var(Var(std::move(name), std::move(type), std::move(loc))) {}
 
-bool TensorVar::IsScalar(bool alloc_or_decl) const { return 
ty()->IsScalar(alloc_or_decl); }
-
-TVM_FFI_STATIC_INIT_BLOCK() {
-  namespace refl = tvm::ffi::reflection;
-  refl::GlobalDef().def_method("tirx.TensorIsScalar", &TensorVar::IsScalar);
-}
-
 }  // namespace tirx
 }  // namespace tvm
diff --git a/src/tirx/ir/type.cc b/src/tirx/ir/type.cc
index 4a44b8e91..23ac20a6f 100644
--- a/src/tirx/ir/type.cc
+++ b/src/tirx/ir/type.cc
@@ -60,14 +60,6 @@ bool 
TensorTypeObj::HasDefaultLayout(ffi::Optional<sym::Analyzer> analyzer) cons
   return ffi::GetRef<TileLayout>(tile).IsContiguous(std::move(analyzer));
 }
 
-bool TensorTypeObj::IsScalar(bool alloc_or_decl) const {
-  // TODO(@bohan): logical scope is not considered
-  return shape.size() == 1 && tvm::prim::IsOne(shape[0]) && data_alignment == 
64 &&
-         (!layout.has_value() ||
-          (layout.as<TileLayoutObj>() &&
-           
layout.value().Canonicalize().as_or_throw<TileLayout>().IsTrivial()));
-}
-
 ffi::Optional<int64_t> TensorTypeObj::ConstantAllocationSize() const {
   int64_t result = 1;
   for (const PrimExpr& extent : shape) {
diff --git a/src/tirx/transform/force_narrow_index_to_i32.cc 
b/src/tirx/transform/force_narrow_index_to_i32.cc
index 54b8ccc06..371917fc5 100644
--- a/src/tirx/transform/force_narrow_index_to_i32.cc
+++ b/src/tirx/transform/force_narrow_index_to_i32.cc
@@ -26,6 +26,7 @@
 #include <tvm/ffi/cast.h>
 #include <tvm/ffi/reflection/registry.h>
 #include <tvm/ir/prim/op.h>
+#include <tvm/tirx/layout.h>
 #include <tvm/tirx/op/memory.h>
 #include <tvm/tirx/stmt_functor.h>
 #include <tvm/tirx/transform.h>
@@ -64,13 +65,20 @@ class Int32DTypeNarrower : public IndexDataTypeNormalizer {
     }
   }
 
+  /*! \brief Whether `ty` has the layout of TVMScript scalar storage, not 
merely one element. */
+  static bool IsScalarTensor(const TensorType& ty) {
+    return ty->shape.size() == 1 && prim::IsOne(ty->shape[0]) && 
ty->data_alignment == 64 &&
+           (!ty->layout.has_value() ||
+            (ty->layout.as<TileLayoutObj>() &&
+             
ty->layout.value().Canonicalize().as_or_throw<TileLayout>().IsTrivial()));
+  }
+
   /*! \brief Reject allocated integer tensors wider than int32. */
   void CheckAllocatedTensor(const TensorVar& buf) const {
     // Scalar assignments in TVMScript use local scalar storage.  Keep its 
explicit
     // dtype (e.g. an int64 opaque call result) and cast at narrowed index 
uses.
-    // IsScalar checks the scalar layout contract, not merely the allocation 
size.
     static const auto local = runtime::MemoryScope::Get("local");
-    bool is_local_scalar = buf.scope() == local && buf.IsScalar();
+    bool is_local_scalar = buf.scope() == local && IsScalarTensor(buf.ty());
     if (!is_local_scalar && buf.dtype().MatchesCode(DLDataTypeCode::kDLInt) &&
         buf.dtype().bits() > 32) {
       TVM_FFI_THROW(InternalError)

Reply via email to