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 75b9411b5 [FEAT][RUST] Register types of register_types! blocks for 
Python and TVMScript (#87)
75b9411b5 is described below

commit 75b9411b5a176b67342211a0368ac0a53ff5109e
Author: Linzhang Li <[email protected]>
AuthorDate: Mon Oct 26 18:47:02 2026 -0400

    [FEAT][RUST] Register types of register_types! blocks for Python and 
TVMScript (#87)
    
    An IR type defined in Rust could be built and printed, but Python could
    not construct it, a JSON
    graph could not read it back, and TVMScript could not parse it:
    `rs_class` registers the fields of
    a type, but not a way to make one. A C++ type gets that from
    `refl::ObjectDef`, which also
    registers a creator of an empty object that tvm-ffi fills through the
    reflection of the fields.
    
    A type is now written as before, an `rs_class` object and its
    `def_object_ref!`, inside a new
    `register_types!` block. As a `register_ops!` block does (#42), the
    block runs when its library
    loads, once per process, and Python then treats the type as it treats a
    C++ one:
    
    ```rust
    tvm_tirx::register_types! {
        /// The type of a pair of values, printed as `my_ext.Pair(first, 
second)`.
        #[rs_class("my_ext.PairType", final)]
        pub struct PairTypeObj {
            base: TypeObj,
            pub first: Type,
            #[reflect(kw_only)]
            pub second: Type,
        }
        def_object_ref!(pub PairType, Type, PairTypeObj);
    }
    ```
    
    ```python
    import tvm_ffi
    import tvm_tirx
    from tvm_tirx import ir
    from tvm_tirx.script import parser
    from tvm_tirx.script import tirx as T
    
    tvm_ffi.load_module("target/release/libmy_ext.so")  # registers 
my_ext.PairType
    my_ext = parser.my_ext
    
    pair = my_ext.Pair(T.int32, second=T.float32)
    
    @T.function
    def func(p: my_ext.Pair(T.int32, T.float32)):
        T.evaluate(p)
    
    assert tvm_ffi.structural_equal(tvm_tirx.script.parse(func.script(), 
extra_vars={"T": T}), func)
    assert tvm_ffi.structural_equal(ir.load_json(ir.save_json(func)), func)
    ```
---
 docs/reference/python/tirx.script.rst             |   1 +
 python/tvm_tirx/backend/rust/__init__.py          |   6 +-
 python/tvm_tirx/tirx/script/ir_builder/op.py      |  79 +++++++-
 rust/Cargo.toml                                   |   4 +-
 rust/examples/{define_ops.rs => define_ir.rs}     |  28 ++-
 rust/src/ir.rs                                    |   2 +
 rust/src/ir/op_def.rs                             | 115 +++++++-----
 rust/src/ir/type_def.rs                           | 215 ++++++++++++++++++++++
 rust/src/lib.rs                                   |  43 +++--
 rust/tests/ir/op_def.rs                           |  76 ++++++--
 tests/python/ir/test_op_rust.py                   |  86 ++++++---
 tests/python/tirx/script/test_expose_script_op.py |  57 +++++-
 12 files changed, 585 insertions(+), 127 deletions(-)

diff --git a/docs/reference/python/tirx.script.rst 
b/docs/reference/python/tirx.script.rst
index e74a3e4ab..51a755b35 100644
--- a/docs/reference/python/tirx.script.rst
+++ b/docs/reference/python/tirx.script.rst
@@ -369,6 +369,7 @@ Functions
    max
    register_script_namespace
    expose_script_op
+   expose_script_type
    abs
    acos
    acosh
diff --git a/python/tvm_tirx/backend/rust/__init__.py 
b/python/tvm_tirx/backend/rust/__init__.py
index 74d8e5ea7..38e1e2d6c 100644
--- a/python/tvm_tirx/backend/rust/__init__.py
+++ b/python/tvm_tirx/backend/rust/__init__.py
@@ -37,9 +37,9 @@ def register_backend():
     except (OSError, FileNotFoundError, RuntimeError):
         pass
     else:
-        # Loading the library registers its operators, and keeps it, and the 
hooks they have,
-        # loaded for the process; ``tirx_register_ops`` raises the error of a 
block that failed.
-        
tvm_ffi.load_module(_LOADED_LIBS["tvm_tirx_rust"]._name).tirx_register_ops()
+        # Loading the library registers its operators and types, and keeps it, 
and their hooks,
+        # loaded for the process; ``tirx_register_ir`` raises the error of a 
block that failed.
+        
tvm_ffi.load_module(_LOADED_LIBS["tvm_tirx_rust"]._name).tirx_register_ir()
     return None
 
 
diff --git a/python/tvm_tirx/tirx/script/ir_builder/op.py 
b/python/tvm_tirx/tirx/script/ir_builder/op.py
index 4285d8232..f1233f0be 100644
--- a/python/tvm_tirx/tirx/script/ir_builder/op.py
+++ b/python/tvm_tirx/tirx/script/ir_builder/op.py
@@ -282,9 +282,28 @@ def expose_script_op(name: str) -> Any:
     builder : Callable
         The builder the printed calls name.
     """
+    op = _ir.Op.get(name)
+    leaf, lookup, bind = _script_slot(name)
+    existing = lookup(leaf)
+    if existing is not None:
+        if not callable(existing):
+            raise ValueError(f"Op {name!r} conflicts with a non-callable 
Python attribute")
+        return existing
+    call = _ir.op._make_op_api(op, __name__)
+    if op.get_attr("FRegionGetBodyParams") is None:
+        call = _auto_emit_void_call(call)
+    bind(leaf, call)
+    return call
+
+
+def _script_slot(name: str):
+    """The last part of ``name``, and the lookup and binding of a name where 
its first parts are.
+
+    ``tirx.<path>``, ``prim.<path>`` and ``ir.<path>`` are printed as 
``T.<path>``, and the first
+    part of any other name is a parser namespace. A missing namespace of the 
path is created.
+    """
     from tvm_tirx.script import parser  # pylint: 
disable=import-outside-toplevel
 
-    op = _ir.Op.get(name)
     parts = name.split(".")
     # None stands for the parser's namespaces, which hold the first part of 
other names.
     container = None
@@ -307,20 +326,61 @@ def expose_script_op(name: str) -> Any:
         if lookup(part) is None:
             bind(part, SimpleNamespace())
         container = lookup(part)
+    return leaf, lookup, bind
+
+
+def expose_script_type(type_key: str) -> Any:
+    """Give a registered Type the class its printed constructor names, and 
return it.
+
+    The printer spells a Type ``<namespace>.<Name>Type`` without a printer 
hook as a call of
+    ``<Name>`` in its namespace, with its fields as arguments, which is placed 
as
+    :func:`expose_script_op` places a builder. The class is the one registered 
for the type,
+    or, when the type has none or only the fallback class of an object that 
reached Python
+    first, a class registered now, which constructs the type through its 
``__ffi_init__``. As for
+    an Op, an existing callable keeps the name, so a hand-written builder 
takes precedence, except
+    a class of the type, which is replaced: a hand-written class takes the 
name when this is called
+    after it is registered. Types of Rust ``register_types!`` blocks get their 
classes when they
+    are registered.
+
+    Parameters
+    ----------
+    type_key : str
+        Key of a registered Type.
+
+    Returns
+    -------
+    cls : Callable
+        The class, or the builder, the printed constructors name.
+    """
+    type_key = str(type_key)
+    info = tvm_ffi.core._lookup_or_register_type_info_from_type_key(type_key)
+    namespace, _, name = type_key.rpartition(".")
+    cls = info.type_cls
+    # The fallback class tvm_ffi makes for an object of a type without a class 
has no __init__.
+    if cls is None or "__init__" not in vars(cls):
+        parent = info.parent_type_info
+        base = parent.type_cls
+        if base is None or "__init__" not in vars(base):
+            base = expose_script_type(parent.type_key)
+        attrs = {"__slots__": (), "__module__": namespace}
+        cls = tvm_ffi.register_object(type_key)(type(name, (base,), attrs))
+    # The printer drops the "Type" suffix (DefaultTypeTranslate).
+    if len(name) > 4 and name.endswith("Type"):
+        name = name[:-4]
+    leaf, lookup, bind = _script_slot(f"{namespace}.{name}")
     existing = lookup(leaf)
-    if existing is not None:
+    existing_info = getattr(existing, "__tvm_ffi_type_info__", None)
+    if existing is not None and getattr(existing_info, "type_key", None) != 
type_key:
         if not callable(existing):
-            raise ValueError(f"Op {name!r} conflicts with a non-callable 
Python attribute")
+            raise ValueError(f"Type {type_key!r} conflicts with a non-callable 
Python attribute")
         return existing
-    call = _ir.op._make_op_api(op, __name__)
-    if op.get_attr("FRegionGetBodyParams") is None:
-        call = _auto_emit_void_call(call)
-    bind(leaf, call)
-    return call
+    bind(leaf, cls)
+    return cls
 
 
-# Rust operators call this when they are registered.
+# Rust operators and types call these when they are registered.
 tvm_ffi.register_global_func("tirx.script.expose_script_op", expose_script_op)
+tvm_ffi.register_global_func("tirx.script.expose_script_type", 
expose_script_type)
 
 
 abs = _auto_emit_void_call(_tir_op.abs)  # pylint: disable=redefined-builtin
@@ -897,6 +957,7 @@ __all__ = [
     "exp2",
     "exp10",
     "expose_script_op",
+    "expose_script_type",
     "fabs",
     "floor",
     "floordiv",
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index 3d63e5dbb..80f96d402 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -33,9 +33,9 @@ crate-type = ["rlib", "cdylib"]
 linkme = "0.3"
 tvm-ffi = { path = "../3rdparty/tvm-ffi/rust/tvm-ffi" }
 
-# A library of operators defined in Rust, which 
`tests/python/ir/test_op_rust.py` loads.
+# A library of operators and a type defined in Rust, which 
`tests/python/ir/test_op_rust.py` loads.
 [[example]]
-name = "define_ops"
+name = "define_ir"
 crate-type = ["cdylib"]
 
 # Standalone crate: not a member of any enclosing workspace.
diff --git a/rust/examples/define_ops.rs b/rust/examples/define_ir.rs
similarity index 76%
rename from rust/examples/define_ops.rs
rename to rust/examples/define_ir.rs
index 4bdb49e32..d05332913 100644
--- a/rust/examples/define_ops.rs
+++ b/rust/examples/define_ir.rs
@@ -17,18 +17,21 @@
  * under the License.
  */
 
-//! Operators defined in Rust, in a library of their own.
+//! Operators and a type defined in Rust, in a library of their own.
 //!
-//! `cargo build --example define_ops` builds this as 
`target/debug/examples/libdefine_ops.so`,
-//! which exports `tirx_register_ops`, as every library built on `tvm_tirx` 
does.
+//! `cargo build --example define_ir` builds this as 
`target/debug/examples/libdefine_ir.so`,
+//! which exports `tirx_register_ir`, as every library built on `tvm_tirx` 
does.
 //! `tests/python/ir/test_op_rust.py` loads it, which registers these 
operators and gives each its
 //! TVMScript builder, as `import tvm_tirx` loads `libtvm_tirx_rust`: 
`T.example_clamp(i, n)`,
-//! `T.example_mark()`, `my_ext.hint(i, attrs={..})` and `with 
T.example_loop(n) as j:`.
+//! `T.example_mark()`, `my_ext.hint(i, attrs={..})` and `with 
T.example_loop(n) as j:`; and
+//! registers the type, with its class `T.ExamplePair`.
 
 use tvm_tirx::ir::op_attr::{CALL_EFFECT_KIND, FIXED_RETURN_TYPE};
 use tvm_tirx::ir::sig::{self, IntExpr};
-use tvm_tirx::ir::{CallEffectKind, CallObj, DictAttrs, Expr, OpDef, PrimType, 
Type, Var, UNKNOWN_LOC};
-use tvm_tirx::tvm_ffi::{Array, ObjectRefCast, Result};
+use tvm_tirx::ir::{
+    CallEffectKind, CallObj, DictAttrs, Expr, OpDef, PrimType, Type, TypeObj, 
Var, UNKNOWN_LOC,
+};
+use tvm_tirx::tvm_ffi::{self, rs_class, Array, ObjectRefCast, Result};
 
 fn first_arg_type(call: &CallObj) -> Result<Type> {
     Ok(call.args.get(0)?.ty.clone())
@@ -61,6 +64,19 @@ tvm_tirx::register_ops! {
         .set_region_body_params(loop_index);
 }
 
+// A type, which Python constructs from its fields, `T.ExamplePair(T.int32, 
T.float32)`, as it
+// constructs a C++ type.
+tvm_tirx::register_types! {
+    /// The type of a pair of values.
+    #[rs_class("tirx.ExamplePairType", final)]
+    pub struct ExamplePairTypeObj {
+        base: TypeObj,
+        pub first: Type,
+        pub second: Type,
+    }
+    def_object_ref!(pub ExamplePairType, Type, ExamplePairTypeObj);
+}
+
 // A block can be anywhere in the library.
 mod effects {
     use super::*;
diff --git a/rust/src/ir.rs b/rust/src/ir.rs
index 27872d54b..ca5c92e2d 100644
--- a/rust/src/ir.rs
+++ b/rust/src/ir.rs
@@ -45,6 +45,7 @@ pub mod stmt;
 pub(crate) mod support;
 pub mod transform;
 pub mod r#type;
+pub mod type_def;
 pub mod unique_name_supply;
 
 pub use attrs::*;
@@ -58,6 +59,7 @@ pub use op::*;
 pub use op_def::*;
 pub use stmt::*;
 pub use r#type::*;
+pub use type_def::*;
 pub use unique_name_supply::*;
 
 // tvm-ffi-stubgen(begin): import-section
diff --git a/rust/src/ir/op_def.rs b/rust/src/ir/op_def.rs
index bd5adec79..05b055f6d 100644
--- a/rust/src/ir/op_def.rs
+++ b/rust/src/ir/op_def.rs
@@ -27,11 +27,11 @@
 //! packing arguments. A signature generates its validator from its argument 
types, as C++
 //! `OpDef::signature` does from `sig::arg<T>`; each signature is its own 
compiled function.
 //!
-//! The [`register_ops!`](crate::register_ops) blocks are gathered at link 
time, wherever in the
-//! program or library they are, and run when it loads, as a C++ static init 
block does: once
-//! Python loads a library built on this crate, by any means, it sees its 
operators as it sees
-//! those of a C++ library. A block that several libraries of a process have, 
as each links its
-//! own copy of a crate they share, registers its operators once.
+//! The [`register_ops!`](crate::register_ops) and 
[`register_types!`](crate::register_types)
+//! blocks are gathered at link time, wherever in the program or library they 
are, and run when it
+//! loads, as a C++ static init block does: once Python loads a library built 
on this crate, by any
+//! means, it sees its operators and types as it sees those of a C++ library. 
A block that several
+//! libraries of a process have, as each links its own copy of a crate they 
share, runs once.
 
 use std::any::TypeId;
 use std::cell::RefCell;
@@ -412,7 +412,7 @@ impl OpHandle {
             Some(op) => op,
             None => {
                 let op = Op::get(self.name).or_else(|missing| {
-                    let registered = register_defined_ops();
+                    let registered = register_defined_ir();
                     Op::get(self.name).map_err(|_| match registered {
                         Ok(()) => missing,
                         Err(failed) => {
@@ -435,10 +435,12 @@ impl OpHandle {
     }
 }
 
-/// A [`register_ops!`](crate::register_ops) block, gathered at link time.
+/// A [`register_ops!`](crate::register_ops) or 
[`register_types!`](crate::register_types) block,
+/// gathered at link time.
 #[doc(hidden)]
-pub struct OpRegistration {
-    /// The module and line of the block.
+pub struct Registration {
+    /// The macro of the block, and its module and line.
+    kind: &'static str,
     module: &'static str,
     line: u32,
     /// The version of the block's crate and a hash of the block's text, which 
with the module and
@@ -450,15 +452,17 @@ pub struct OpRegistration {
     outcome: OnceLock<std::result::Result<(), std::string::String>>,
 }
 
-/// The blocks of [`register_ops!`](crate::register_ops), gathered at link 
time from the whole
-/// program or library.
+/// The blocks of [`register_ops!`](crate::register_ops) and
+/// [`register_types!`](crate::register_types), gathered at link time from the 
whole program or
+/// library.
 #[doc(hidden)]
 #[linkme::distributed_slice]
-pub static OP_REGISTRATIONS: [OpRegistration];
+pub static REGISTRATIONS: [Registration];
 
-impl OpRegistration {
+impl Registration {
     #[doc(hidden)]
     pub const fn __new(
+        kind: &'static str,
         module: &'static str,
         line: u32,
         version: &'static str,
@@ -466,7 +470,7 @@ impl OpRegistration {
         register: fn() -> Result<()>,
     ) -> Self {
         let text_hash = text_hash(text);
-        Self { module, line, version, text_hash, register, outcome: 
OnceLock::new() }
+        Self { kind, module, line, version, text_hash, register, outcome: 
OnceLock::new() }
     }
 
     /// The identity of the block in the process, `module:line@version#hash`: 
the same in every
@@ -478,11 +482,11 @@ impl OpRegistration {
     }
 
     /// The name of the global function that marks the block registered in the 
process,
-    /// `ir.registered_op_block.<identity>`. The libraries of a process share 
the global function
+    /// `ir.registered_block.<identity>`. The libraries of a process share the 
global function
     /// table of TVM FFI, and their statics are their own.
     #[doc(hidden)]
     pub fn marker(&self) -> std::string::String {
-        format!("ir.registered_op_block.{}", self.identity())
+        format!("ir.registered_block.{}", self.identity())
     }
 
     /// Run the block if it has not run. The caller that runs a failing block 
gets its error;
@@ -501,15 +505,15 @@ impl OpRegistration {
             (Some(error), _) => Err(error),
             (None, Ok(())) => Ok(()),
             (None, Err(text)) => {
-                let message = format!("The register_ops! block at {} failed: 
{text}", self.place());
+                let message = format!("The {} block at {} failed: {text}", 
self.kind, self.place());
                 Err(Error::new(INTERNAL_ERROR, &message, ""))
             }
         }
     }
 
     /// Run the block unless another library of the process has run it. Every 
library links its
-    /// own copy of the crates it is built from, so the same block can be in 
several, and its
-    /// operators are registered once, with the hooks of the first library 
that runs it. The block
+    /// own copy of the crates it is built from, so the same block can be in 
several, and what it
+    /// defines is registered once, with the hooks of the first library that 
runs it. The block
     /// is marked once it succeeds, so that one that fails fails in every 
library.
     fn run(&self) -> Result<()> {
         let marker = self.marker();
@@ -541,55 +545,59 @@ const fn text_hash(text: &str) -> u64 {
     hash
 }
 
-/// Run every [`register_ops!`](crate::register_ops) block of this program or 
library that has
-/// not run, in the order of their modules and lines; running again does 
nothing. Every block is
-/// tried, and the error lists those that failed. A block that another library 
of the process has
-/// registered, from the same version of its crate and the same text, is 
skipped: libraries that
-/// link the same crate register its operators once.
+/// Run every [`register_ops!`](crate::register_ops) and 
[`register_types!`](crate::register_types)
+/// block of this program or library that has not run, in the order of their 
modules and lines;
+/// running again does nothing. Every block is tried, and the error lists 
those that failed. A
+/// block that another library of the process has registered, from the same 
version of its crate
+/// and the same text, is skipped: libraries that link the same crate register 
its operators and
+/// types once.
 ///
 /// The blocks run when their program or library loads, so this reports the 
errors of those that
 /// failed. Every library built on this crate exports it as the TVM FFI 
function
-/// `tirx_register_ops`, which `import tvm_tirx` calls on `libtvm_tirx_rust` 
to raise them. Where
+/// `tirx_register_ir`, which `import tvm_tirx` calls on `libtvm_tirx_rust` to 
raise them. Where
 /// the loader does not run the blocks, the first lookup through an 
[`OpHandle`] of an operator
 /// that is not registered calls it.
-pub fn register_defined_ops() -> Result<()> {
-    let mut blocks: Vec<&OpRegistration> = OP_REGISTRATIONS.iter().collect();
+pub fn register_defined_ir() -> Result<()> {
+    let mut blocks: Vec<&Registration> = REGISTRATIONS.iter().collect();
     blocks.sort_by_key(|block| (block.module, block.line));
-    let mut failures: Vec<(&OpRegistration, Error)> =
+    let mut failures: Vec<(&Registration, Error)> =
         blocks.into_iter().filter_map(|block| block.register().err().map(|e| 
(block, e))).collect();
     if failures.len() <= 1 {
         return failures.pop().map_or(Ok(()), |(_, error)| Err(error));
     }
-    let mut message = format!("{} register_ops! blocks failed:", 
failures.len());
+    let mut kinds: Vec<&str> = failures.iter().map(|(block, _)| 
block.kind).collect();
+    kinds.sort();
+    kinds.dedup();
+    let mut message = format!("{} {} blocks failed:", failures.len(), 
kinds.join(" and "));
     for (block, error) in &failures {
         message.push_str(&format!("\n  {}: {}: {}", block.place(), 
error.kind(), error.message()));
     }
     Err(Error::new(failures[0].1.kind(), &message, ""))
 }
 
-/// The constructor of every [`register_ops!`](crate::register_ops) block, 
which the loader runs
-/// when the program or library loads: the first one keeps the library loaded 
and runs every block
-/// of it. An error cannot reach a caller here, so it is printed; 
[`register_defined_ops`] reports
-/// it again.
+/// The constructor of every [`register_ops!`](crate::register_ops) and
+/// [`register_types!`](crate::register_types) block, which the loader runs 
when the program or
+/// library loads: the first one keeps the library loaded and runs every block 
of it. An error
+/// cannot reach a caller here, so it is printed; [`register_defined_ir`] 
reports it again.
 #[doc(hidden)]
 pub extern "C" fn __register_at_load() {
     static LOADED: Once = Once::new();
     LOADED.call_once(|| {
         let library = keep_loaded().unwrap_or_else(|| 
"register_ops!".to_string());
-        match catch_unwind(register_defined_ops) {
+        match catch_unwind(register_defined_ir) {
             Ok(Ok(())) => {}
             Ok(Err(error)) => eprintln!(
-                "{library}: registering operators failed: {}: {}",
+                "{library}: registering operators and types failed: {}: {}",
                 error.kind(),
                 error.message()
             ),
-            Err(_) => eprintln!("{library}: registering operators panicked"),
+            Err(_) => eprintln!("{library}: registering operators and types 
panicked"),
         }
     });
 }
 
-/// Keep the library that holds this code loaded for the rest of the process, 
as the operators it
-/// registers keep its hooks: unloading it, as `tvm_ffi.load_module` does when 
Python drops the
+/// Keep the library that holds this code loaded for the rest of the process, 
as the operators and
+/// types it registers keep its hooks: unloading it, as `tvm_ffi.load_module` 
does when Python drops the
 /// module, would leave them dangling. Returns the path of the library.
 #[cfg(any(target_os = "linux", target_os = "macos"))]
 fn keep_loaded() -> Option<std::string::String> {
@@ -679,10 +687,10 @@ fn doc_text(doc: &str) -> std::string::String {
 ///
 /// As a C++ static init block, a block runs when its program or library 
loads: it adds a
 /// constructor, which the loader runs, and the first constructor runs every 
block of the program
-/// or library with [`register_defined_ops`]. Python sees the operators of a 
library once it loads
+/// or library with [`register_defined_ir`]. Python sees the operators of a 
library once it loads
 /// it, by any means, and the library stays loaded for the process, as the 
operators keep its
 /// hooks. An error cannot reach a caller when the library loads: it is 
printed, and
-/// [`register_defined_ops`] (`tirx_register_ops`) and the handles report it 
again. Where the
+/// [`register_defined_ir`] (`tirx_register_ir`) and the handles report it 
again. Where the
 /// loader does not run the constructor, an [`OpHandle`] runs the blocks on 
the first lookup of an
 /// operator that is not registered.
 ///
@@ -695,22 +703,33 @@ fn doc_text(doc: &str) -> std::string::String {
 #[macro_export]
 macro_rules! register_ops {
     ($($def:expr);* $(;)?) => {
+        $crate::__register_block!("register_ops!", stringify!($($def);*), || {
+            $($crate::ir::OpDef::op($def)?;)*
+            Ok(())
+        });
+    };
+}
+
+/// The registration of a [`register_ops!`](crate::register_ops) or
+/// [`register_types!`](crate::register_types) block: gathered at link time, 
and run by a
+/// constructor when its program or library loads.
+#[doc(hidden)]
+#[macro_export]
+macro_rules! __register_block {
+    ($kind:literal, $text:expr, $register:expr) => {
         const _: () = {
-            fn register() -> $crate::tvm_ffi::Result<()> {
-                $($crate::ir::OpDef::op($def)?;)*
-                Ok(())
-            }
-            
#[$crate::__private::linkme::distributed_slice($crate::ir::OP_REGISTRATIONS)]
+            
#[$crate::__private::linkme::distributed_slice($crate::ir::REGISTRATIONS)]
             #[linkme(crate = $crate::__private::linkme)]
-            static REGISTRATION: $crate::ir::OpRegistration = 
$crate::ir::OpRegistration::__new(
+            static REGISTRATION: $crate::ir::Registration = 
$crate::ir::Registration::__new(
+                $kind,
                 module_path!(),
                 line!(),
                 match option_env!("CARGO_PKG_VERSION") {
                     Some(version) => version,
                     None => "",
                 },
-                stringify!($($def);*),
-                register,
+                $text,
+                $register,
             );
             #[used]
             #[cfg_attr(target_os = "linux", link_section = ".init_array")]
diff --git a/rust/src/ir/type_def.rs b/rust/src/ir/type_def.rs
new file mode 100644
index 000000000..5f8165d36
--- /dev/null
+++ b/rust/src/ir/type_def.rs
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Registering IR types from Rust: the 
[`register_types!`](crate::register_types) blocks that
+//! define `rs_class` types, as C++ defines types with `refl::ObjectDef` in a
+//! `TVM_FFI_STATIC_INIT_BLOCK`, and the [`Placeholder`] values of their 
fields.
+//!
+//! `rs_class` registers the fields of a type, but not a way to make one. C++ 
`refl::ObjectDef`
+//! also registers a creator of an empty object, which tvm-ffi fills through 
the reflection of the
+//! fields to construct the type from Python (the `__ffi_init__` it generates) 
and to read it from
+//! a JSON graph. A Rust object cannot be empty, as its references are not 
null, so a block
+//! registers `__ffi_new__`, which makes an object whose fields hold 
[`Placeholder`]s, and tvm-ffi
+//! then sets each field as it sets those of a C++ object.
+
+use tvm_ffi::tvm_ffi_sys::{TVMFFIByteArray, TVMFFITypeRegisterAttr};
+use tvm_ffi::{Any, AnyCompatible, Array, Function, ObjectCore, ObjectRefCore, 
Result, String};
+
+use super::expr::{Expr, IntImm};
+use super::location::Location;
+use super::r#type::{MissingType, PrimType, Type, TypeObj};
+use super::support;
+
+/// A valid value of a field type, which an object of a 
[`register_types!`](crate::register_types)
+/// block holds until reflection sets the field: `__ffi_new__` makes an object 
with one in every
+/// field, and a field `__ffi_init__` does not take (`#[reflect(init_off)]`) 
keeps it. A block
+/// implements it for the objects and references it defines, and this crate 
for the common field
+/// types; implement it for a field type of your own crate.
+pub trait Placeholder {
+    /// The value.
+    fn placeholder() -> Self;
+}
+
+macro_rules! default_placeholder {
+    ($($ty:ty),*) => {
+        $(impl Placeholder for $ty {
+            fn placeholder() -> Self {
+                Self::default()
+            }
+        })*
+    };
+}
+
+default_placeholder!(bool, i8, i16, i32, i64, u8, u16, u32, u64, f32, f64);
+
+impl Placeholder for String {
+    fn placeholder() -> Self {
+        String::from("")
+    }
+}
+
+impl<T: AnyCompatible + Clone + 'static> Placeholder for Array<T> {
+    fn placeholder() -> Self {
+        Array::new(vec![])
+    }
+}
+
+impl<T> Placeholder for Option<T> {
+    fn placeholder() -> Self {
+        None
+    }
+}
+
+impl Placeholder for Location {
+    fn placeholder() -> Self {
+        Location::unknown()
+    }
+}
+
+impl Placeholder for TypeObj {
+    fn placeholder() -> Self {
+        TypeObj::new(Location::unknown())
+    }
+}
+
+impl Placeholder for Type {
+    fn placeholder() -> Self {
+        MissingType::new().into()
+    }
+}
+
+impl Placeholder for PrimType {
+    fn placeholder() -> Self {
+        PrimType::void()
+    }
+}
+
+impl Placeholder for Expr {
+    fn placeholder() -> Self {
+        IntImm::new(Location::unknown(), PrimType::int(32), 0).into()
+    }
+}
+
+/// Register the type of the reference `R` of a 
[`register_types!`](crate::register_types) block:
+/// `__ffi_new__`, then the `__ffi_init__` tvm-ffi generates from the 
reflected fields, then, once
+/// Python has imported `tvm_tirx`, its TVMScript class 
(`tirx.script.expose_script_type`).
+#[doc(hidden)]
+pub fn __register_type<R>() -> Result<()>
+where
+    R: ObjectRefCore + AnyCompatible + Placeholder + 'static,
+{
+    let type_index = R::ContainerType::type_index();
+    let new = Function::from_typed(|| -> Result<R> { Ok(R::placeholder()) });
+    register_type_attr(type_index, "__ffi_new__", Any::from(new))?;
+    
tvm_ffi::cached_global_func!("ffi._RegisterFFIInit").call_tuple((type_index,))?;
+    if let Ok(expose) = Function::get_global("tirx.script.expose_script_type") 
{
+        
expose.call_tuple((String::from(support::type_key(type_index).as_str()),))?;
+    }
+    Ok(())
+}
+
+/// Register `value` as the type attribute `name` of the type of `type_index`.
+fn register_type_attr(type_index: i32, name: &str, mut value: Any) -> 
Result<()> {
+    // SAFETY: `name` and `value` outlive the call, which copies them.
+    let name = unsafe { TVMFFIByteArray::from_str(name) };
+    tvm_ffi::check_safe_call!(unsafe {
+        TVMFFITypeRegisterAttr(type_index, &name, Any::as_data_ptr(&mut value))
+    })
+}
+
+/// Define IR types as C++ does with `refl::ObjectDef` in a 
`TVM_FFI_STATIC_INIT_BLOCK`: each item
+/// is an `rs_class` object struct and the `def_object_ref!` of its reference, 
written as outside a
+/// block.
+///
+/// ```no_run
+/// use tvm_tirx::ir::{Type, TypeObj};
+/// use tvm_tirx::tvm_ffi::{self, rs_class};
+///
+/// tvm_tirx::register_types! {
+///     /// The type of a pair of values, printed as `my_ext.Pair(first, 
second)`.
+///     #[rs_class("my_ext.PairType", final)]
+///     pub struct PairTypeObj {
+///         base: TypeObj,
+///         pub first: Type,
+///         #[reflect(kw_only)]
+///         pub second: Type,
+///     }
+///     def_object_ref!(pub PairType, Type, PairTypeObj);
+/// }
+///
+/// ```
+///
+/// The block registers its types when its program or library loads, as a
+/// [`register_ops!`](crate::register_ops) block registers its operators, and 
once per process.
+/// Python then constructs a type from its fields, `my_ext.Pair(T.int32, 
second=T.float32)`, as it
+/// constructs a C++ type, a JSON graph holds it, and, when Python has 
imported `tvm_tirx` before it
+/// loads the library, the type gets the class its TVMScript constructor 
names. The type of each
+/// field and the parent's object implement [`Placeholder`].
+#[macro_export]
+macro_rules! register_types {
+    ($(
+        $(#[$attr:meta])*
+        $vis:vis struct $obj:ident {
+            $base:ident: $base_ty:ty
+            $(, $(#[$field_attr:meta])* $field_vis:vis $field:ident: 
$field_ty:ty)* $(,)?
+        }
+        def_object_ref!($ref_vis:vis $ref:ident, $parent:ty, $ref_obj:ident 
$(,)?);
+    )*) => {
+        $(
+            $(#[$attr])*
+            $vis struct $obj {
+                $base: $base_ty,
+                $($(#[$field_attr])* $field_vis $field: $field_ty,)*
+            }
+
+            $crate::tvm_ffi::def_object_ref!($ref_vis $ref, $parent, $ref_obj);
+
+            impl $crate::ir::Placeholder for $obj {
+                fn placeholder() -> Self {
+                    $obj::new(
+                        <$base_ty as $crate::ir::Placeholder>::placeholder(),
+                        $(<$field_ty as 
$crate::ir::Placeholder>::placeholder(),)*
+                    )
+                }
+            }
+
+            impl $crate::ir::Placeholder for $ref {
+                fn placeholder() -> Self {
+                    <$obj as $crate::ir::Placeholder>::placeholder().into()
+                }
+            }
+        )*
+
+        $crate::__register_block!(
+            "register_types!",
+            stringify!($(
+                $(#[$attr])*
+                $vis struct $obj {
+                    $base: $base_ty
+                    $(, $(#[$field_attr])* $field_vis $field: $field_ty)*
+                }
+                def_object_ref!($ref_vis $ref, $parent, $ref_obj);
+            )*),
+            || {
+                $($crate::ir::__register_type::<$ref>()?;)*
+                Ok(())
+            }
+        );
+    };
+}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 261f235f8..01c5db7ef 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -130,29 +130,31 @@
 //!
 //! An [`ir::OpHandle`] in a `static` keeps an operator as C++ keeps one in 
`const Op& xxx_op()`.
 //! A block runs when its program or library loads, as a C++ one does, 
wherever in it the block
-//! is. Every library built on the crate exports [`ir::register_defined_ops`], 
which reports the
-//! errors of the blocks that failed, as the TVM FFI function 
`tirx_register_ops`.
+//! is. Every library built on the crate exports [`ir::register_defined_ir`], 
which reports the
+//! errors of the blocks that failed, as the TVM FFI function 
`tirx_register_ir`.
 //!
 //! # The library Python loads
 //!
 //! The crate is also built as a shared library, which the TIRx build installs 
as
 //! `libtvm_tirx_rust` next to `libtvm_tirx_compiler` when it finds a cargo as 
new as the crate's
 //! `rust-version` (`TVM_TIRX_USE_RUST`, `AUTO` by default). `import tvm_tirx` 
loads it through
-//! the backend autoload (`tvm_tirx.backend.rust`), which registers the 
operators the crate defines
-//! by the time the import returns, as the C++ ones are, and calls 
`tirx_register_ops` to raise the
-//! error of a block that failed. Python uses the operators as it uses the C++ 
ones: `Op.get` finds
-//! them, a `Call` built in Python runs their hooks, and the TVMScript printer 
prints their calls.
-//! The TVMScript builder of an operator (`T.<name>` for `tirx.<name>`, 
`my_ext.<name>` for
-//! `my_ext.<name>`) is generated by `tvm_tirx.tirx.script.expose_script_op`, 
unless Python has one
-//! written for it, as for a C++ operator.
-//!
-//! A library of operators of its own, a `cdylib` built on the crate as 
`examples/define_ops.rs`
-//! is, registers its operators when Python loads it, by `tvm_ffi.load_module` 
or `ctypes`, as a
-//! C++ library does, and stays loaded for the process. Each library links its 
own copy of the
-//! crates it is built from, so several libraries can have the same block, as 
those of a crate they
-//! share: the first library that runs it registers its operators and the 
others skip it, so the
-//! libraries load together. A crate that a library depends on but never names 
is not linked into
-//! it, nor are its blocks: name it, as `use my_ops as _;` does.
+//! the backend autoload (`tvm_tirx.backend.rust`), which registers the 
operators and types the
+//! crate defines by the time the import returns, as the C++ ones are, and 
calls `tirx_register_ir`
+//! to raise the error of a block that failed. Python uses the operators as it 
uses the C++ ones:
+//! `Op.get` finds them, a `Call` built in Python runs their hooks, and the 
TVMScript printer prints
+//! their calls. The TVMScript builder of an operator (`T.<name>` for 
`tirx.<name>`, `my_ext.<name>`
+//! for `my_ext.<name>`) is generated by 
`tvm_tirx.tirx.script.expose_script_op`, unless Python has
+//! one written for it, as for a C++ operator, and a type of a
+//! [`register_types!`](crate::register_types) block gets the class its 
TVMScript constructor names
+//! (`T.<Name>` for `tirx.<Name>Type`) from `expose_script_type`.
+//!
+//! A library of operators and types of its own, a `cdylib` built on the crate 
as
+//! `examples/define_ir.rs` is, registers them when Python loads it, by 
`tvm_ffi.load_module` or
+//! `ctypes`, as a C++ library does, and stays loaded for the process. Each 
library links its own
+//! copy of the crates it is built from, so several libraries can have the 
same block, as those of a
+//! crate they share: the first library that runs it registers what it defines 
and the others skip
+//! it, so the libraries load together. A crate that a library depends on but 
never names is not
+//! linked into it, nor are its blocks: name it, as `use my_ops as _;` does.
 //!
 //! # Defining a type in Rust
 //!
@@ -197,6 +199,9 @@
 //! The generated code names `tvm_ffi`, which `use tvm_tirx::tvm_ffi` brings 
into scope in a crate
 //! that depends only on `tvm-tirx`. The parent's object must have a public 
`new` and implement
 //! `ShallowCopy`; the bindings generate both for a complete type that is not 
final, as `ir.Type`.
+//! Written with its `def_object_ref!` in a 
[`register_types!`](crate::register_types) block, the
+//! type is also registered with the operators, so that Python constructs it 
from its fields and
+//! TVMScript parses it, as a C++ type.
 //!
 //! # Generated and hand-written code
 //!
@@ -298,9 +303,9 @@ pub mod tirx;
 pub use tvm_ffi;
 
 // The errors of the blocks of every library built on the crate, which `import 
tvm_tirx` raises for
-// `libtvm_tirx_rust`: `__tvm_ffi_tirx_register_ops`, the TVM FFI function 
`tirx_register_ops` of
+// `libtvm_tirx_rust`: `__tvm_ffi_tirx_register_ir`, the TVM FFI function 
`tirx_register_ir` of
 // the library as `tvm_ffi.load_module` loads it.
-tvm_ffi::tvm_ffi_dll_export_typed_func!(tirx_register_ops, 
ir::register_defined_ops);
+tvm_ffi::tvm_ffi_dll_export_typed_func!(tirx_register_ir, 
ir::register_defined_ir);
 
 /// What the exported macros expand to.
 #[doc(hidden)]
diff --git a/rust/tests/ir/op_def.rs b/rust/tests/ir/op_def.rs
index 928cfa5b5..0601595bb 100644
--- a/rust/tests/ir/op_def.rs
+++ b/rust/tests/ir/op_def.rs
@@ -18,7 +18,7 @@
  */
 
 //! Tests of `ir::op_def`: operators registered from Rust, by an `OpDef` or in 
`register_ops!`
-//! blocks, which `register_defined_ops` or the first lookup through an 
`OpHandle` runs, are used by
+//! blocks, which `register_defined_ir` or the first lookup through an 
`OpHandle` runs, are used by
 //! the native registry, `Call` constructor and validation, and their type 
inference, validators
 //! and region contracts are native hooks.
 //!
@@ -31,18 +31,19 @@ use std::sync::atomic::{AtomicUsize, Ordering};
 use tvm_tirx::ir::op_attr::{CALL_EFFECT_KIND, FIXED_RETURN_TYPE};
 use tvm_tirx::ir::sig::{self, IntExpr, PrimExpr, PrimVar};
 use tvm_tirx::ir::{
-    register_defined_ops, AttrKey, Attrs, Call, CallEffectKind, CallObj, 
DictAttrs, Evaluate, Expr,
-    FuncType, IntImm, Op, OpDef, OpHandle, OpRegistration, PrimType, 
RegionStmt, RegionStmtObj,
-    StringImm, Type, Var, VarObj, OP_REGISTRATIONS, UNKNOWN_LOC,
+    register_defined_ir, AttrKey, Attrs, Call, CallEffectKind, CallObj, 
DictAttrs, Evaluate, Expr,
+    FuncType, IntImm, Op, OpDef, OpHandle, PrimType, RegionStmt, 
RegionStmtObj, Registration,
+    StringImm, Type, Var, VarObj, REGISTRATIONS, UNKNOWN_LOC,
 };
 use tvm_tirx::tirx::{CallFFIKernelAttr, TensorVar};
 use tvm_tirx::tvm_ffi::tvm_ffi_sys::TVMFFITypeIndex;
 use tvm_tirx::tvm_ffi::{
-    Any, Array, Error, ErrorKind, Function, Map, ObjectRefCast, ObjectRefCore, 
Result,
-    String as FfiString, ATTRIBUTE_ERROR, INTERNAL_ERROR, TYPE_ERROR, 
VALUE_ERROR,
+    Any, AnyView, Array, Error, ErrorKind, Function, Map, ObjectCore, 
ObjectRefCast, ObjectRefCore,
+    Result, String as FfiString, ATTRIBUTE_ERROR, INTERNAL_ERROR, TYPE_ERROR, 
VALUE_ERROR,
 };
 
 use super::{int32, parse_dtype, tensor_var};
+use types::{BlockType, BlockTypeObj};
 use crate::common::{global, serial, structural_equal};
 
 fn var(name: &str, dtype: &str) -> Var {
@@ -442,7 +443,25 @@ mod elsewhere {
     }
 }
 
-// Two blocks that fail, which `register_defined_ops` reports together while 
it runs the others: a
+// A type of a `register_types!` block, registered with the operators when the 
test binary loads.
+mod types {
+    use tvm_tirx::ir::{Type, TypeObj};
+    use tvm_tirx::tvm_ffi::{self, rs_class};
+
+    tvm_tirx::register_types! {
+        /// A type and a count.
+        #[rs_class("rust_test.BlockType", final)]
+        pub struct BlockTypeObj {
+            base: TypeObj,
+            pub elem: Type,
+            #[reflect(kw_only)]
+            pub count: i64,
+        }
+        def_object_ref!(pub BlockType, Type, BlockTypeObj);
+    }
+}
+
+// Two blocks that fail, which `register_defined_ir` reports together while it 
runs the others: a
 // name C++ registers, and a second custom validator, after which the block 
stops.
 mod failing {
     use super::*;
@@ -458,17 +477,17 @@ mod failing {
 }
 
 /// The blocks of `module` in this test binary.
-fn blocks_of(module: &str) -> Vec<&'static OpRegistration> {
+fn blocks_of(module: &str) -> Vec<&'static Registration> {
     let prefix = format!("ir::op_def{module}:");
     let of_module = |identity: &str| {
         identity
             .strip_prefix(&prefix)
             .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit()))
     };
-    OP_REGISTRATIONS.iter().filter(|block| 
of_module(&block.identity())).collect()
+    REGISTRATIONS.iter().filter(|block| of_module(&block.identity())).collect()
 }
 
-fn is_block_registered(block: &OpRegistration) -> bool {
+fn is_block_registered(block: &Registration) -> bool {
     Function::get_global(&block.marker()).is_ok()
 }
 
@@ -556,9 +575,9 @@ fn register_ops_blocks_define_operators() {
     let fixed = Op::attr_map::<Type>("TFixedReturnType").unwrap().unwrap();
     assert!(structural_equal(&fixed.get(&elsewhere).unwrap().unwrap(), 
&PrimType::void()));
 
-    // `register_defined_ops` reports the blocks that failed together, each 
with its first error;
+    // `register_defined_ir` reports the blocks that failed together, each 
with its first error;
     // a block stops at its first failure, and a failed block does not run 
again.
-    let error = register_defined_ops().unwrap_err();
+    let error = register_defined_ir().unwrap_err();
     let message = error.message();
     assert!(message.starts_with("2 register_ops! blocks failed:"), 
"{message}");
     assert!(message.contains("op_def::failing:"), "{message}");
@@ -569,7 +588,30 @@ fn register_ops_blocks_define_operators() {
     assert!(message.contains("AttributeError: "), "{message}");
     assert!(message.contains("is registered before"), "{message}");
     assert!(Op::get("rust_test.after_broken").is_err(), "the block stopped");
-    assert_eq!(register_defined_ops().unwrap_err().message(), message);
+    assert_eq!(register_defined_ir().unwrap_err().message(), message);
+
+    // The type of a `register_types!` block is registered with them: tvm-ffi 
constructs it from its
+    // fields, positionally or by keyword, and reads it back from a JSON graph.
+    let init = Function::from_type_attr(BlockTypeObj::type_index(), 
"__ffi_init__").unwrap();
+    let kwargs = global("ffi.GetKwargsObject").call_tuple(()).unwrap();
+    let (elem, count, four) = (Type::from(PrimType::int(32)), 
FfiString::from("count"), 4i64);
+    let built = init
+        .call_packed(&[
+            AnyView::from(&elem),
+            AnyView::from(&kwargs),
+            AnyView::from(&count),
+            AnyView::from(&four),
+        ])
+        .unwrap();
+    let block = BlockType::try_from(built).unwrap();
+    assert!(block.elem.same_as(&elem));
+    assert_eq!(block.count, 4);
+    let positional = init.call_packed(&[AnyView::from(&elem), 
AnyView::from(&four)]);
+    assert!(positional.is_err_and(|error| error.kind() == TYPE_ERROR), 
"`count` is keyword-only");
+    let json = global("ffi.ToJSONGraph").call_packed(&[AnyView::from(&block), 
AnyView::new()]);
+    let read = 
global("ffi.FromJSONGraph").call_packed(&[AnyView::from(&json.unwrap())]);
+    let read = BlockType::try_from(read.unwrap()).unwrap();
+    assert!(!read.same_as(&block) && structural_equal(&read, &block));
 
     // A block that succeeds is marked registered for the other libraries of 
the process; one that
     // fails is not, so it fails in each of them.
@@ -662,7 +704,7 @@ fn a_block_another_library_registered_is_skipped() {
     }
     // One block in two libraries, as one of a crate both link.
     let text = r#"OpDef::new("rust_test.registered_once", "")"#;
-    let block = || OpRegistration::__new("ext::shared", 3, "0.1.0", text, 
register);
+    let block = || Registration::__new("register_ops!", "ext::shared", 3, 
"0.1.0", text, register);
     let (first, second) = (block(), block());
     assert_eq!(first.marker(), second.marker());
 
@@ -675,7 +717,7 @@ fn a_block_another_library_registered_is_skipped() {
     assert!(Op::get("rust_test.registered_once").unwrap().same_as(&op));
 
     // Another version of the crate is another block, whose name is already 
registered.
-    let other = OpRegistration::__new("ext::shared", 3, "0.2.0", text, 
register);
+    let other = Registration::__new("register_ops!", "ext::shared", 3, 
"0.2.0", text, register);
     assert!(other.register().unwrap_err().message().contains("is registered 
before"));
 }
 
@@ -684,7 +726,9 @@ fn 
register_ops_block_identity_is_its_crate_version_and_text() {
     fn register() -> Result<()> {
         Ok(())
     }
-    let identity = |version, text| OpRegistration::__new("ext::ops", 7, 
version, text, register);
+    let identity = |version, text| {
+        Registration::__new("register_ops!", "ext::ops", 7, version, text, 
register)
+    };
     let text = r#"OpDef::new("ext.clamp", 
"Clamp.").set_infer_type(first_arg_type)"#;
     let block = identity("0.1.0", text).identity();
     assert!(block.starts_with("ext::ops:[email protected]#"), "{block}");
diff --git a/tests/python/ir/test_op_rust.py b/tests/python/ir/test_op_rust.py
index 57928ae5c..3e2e6e863 100644
--- a/tests/python/ir/test_op_rust.py
+++ b/tests/python/ir/test_op_rust.py
@@ -16,11 +16,11 @@
 # under the License.
 """Operators defined in Rust are registered when their library loads, as C++ 
ones are.
 
-``rust/examples/define_ops.rs`` is built by cargo into a library of its own, 
which a fresh
+``rust/examples/define_ir.rs`` is built by cargo into a library of its own, 
which a fresh
 interpreter loads, as ``import tvm_tirx`` loads libtvm_tirx_rust, and then 
uses its operators as it
-uses C++ ones: registering them also gives them their TVMScript builders. A 
copy of the library is
-a second library with the same ``register_ops!`` blocks, as libraries that 
link the same crate
-have.
+uses C++ ones: registering them also gives them their TVMScript builders. Its 
type is registered
+with them. A copy of the library is a second library with the same blocks, as 
libraries that link
+the same crate have.
 """
 
 import os
@@ -55,8 +55,8 @@ def find_cargo():
 
 
 @pytest.fixture(scope="module")
-def define_ops_library():
-    """The example library of Rust operators, built by cargo."""
+def define_ir_library():
+    """The example library of Rust operators and types, built by cargo."""
     cargo = find_cargo()
     if cargo is None or not shutil.which("tvm-ffi-config") or not 
shutil.which("tvm-tirx-config"):
         pytest.skip(
@@ -65,11 +65,11 @@ def define_ops_library():
         )
     manifest = RUST_DIR / "Cargo.toml"
     subprocess.run(
-        [cargo, "build", "--manifest-path", str(manifest), "--example", 
"define_ops"], check=True
+        [cargo, "build", "--manifest-path", str(manifest), "--example", 
"define_ir"], check=True
     )
     target_dir = Path(os.environ.get("CARGO_TARGET_DIR", RUST_DIR / "target"))
-    name = {"darwin": "libdefine_ops.dylib", "win32": "define_ops.dll"}.get(
-        sys.platform, "libdefine_ops.so"
+    name = {"darwin": "libdefine_ir.dylib", "win32": "define_ir.dll"}.get(
+        sys.platform, "libdefine_ir.so"
     )
     return target_dir / "debug" / "examples" / name
 
@@ -145,7 +145,7 @@ def test_library_is_loaded_on_import():
     assert "tvm_tirx_rust" in _LOADED_LIBS
 
 
-def test_rust_operators_are_registered_by_their_library(define_ops_library):
+def test_rust_operators_are_registered_by_their_library(define_ir_library):
     run_python(
         """
         import sys
@@ -155,7 +155,7 @@ def 
test_rust_operators_are_registered_by_their_library(define_ops_library):
 
         # Registered when the library loads, as libtvm_tirx_rust is when 
`import tvm_tirx` loads
         # it, with the doc comment and signature the Rust definition gives.
-        tvm_ffi.load_module(sys.argv[1]).tirx_register_ops()
+        tvm_ffi.load_module(sys.argv[1])
         clamp = ir.Op.get("tirx.example_clamp")
         assert clamp.doc == "Clamp an index to the range `[0, bound)`."
         assert [arg.name for arg in clamp.args_info] == ["index", "bound"]
@@ -175,13 +175,55 @@ def 
test_rust_operators_are_registered_by_their_library(define_ops_library):
             raise AssertionError("a float bound passed validation")
         """,
         ROUND_TRIP,
-        libraries=[define_ops_library],
+        libraries=[define_ir_library],
     )
 
 
-def 
test_a_library_registers_its_operators_when_python_loads_it(define_ops_library, 
tmp_path):
-    copy = tmp_path / define_ops_library.name
-    shutil.copy(define_ops_library, copy)
+def test_rust_types_are_registered_by_their_library(define_ir_library):
+    run_python(
+        """
+        import sys
+
+        import tvm_ffi
+        import tvm_tirx
+        from tvm_tirx import ir
+        from tvm_tirx.script import tirx as T
+
+        # Registered with the operators when the library loads, the type gets 
the class its
+        # TVMScript constructor names, which constructs it from its fields, as 
a C++ type is
+        # constructed.
+        tvm_ffi.load_module(sys.argv[1])
+        pair = T.ExamplePair(T.int32, second=T.float32)
+        assert pair.script() == "T.ExamplePair(T.int32, T.float32)"
+        assert tvm_ffi.structural_equal(pair.first, T.int32)
+
+        # TVMScript prints and parses it, and a JSON graph holds it.
+        @T.function
+        def func(p: T.ExamplePair(T.int32, T.float32)):
+            T.evaluate(p)
+
+        text = func.script()
+        assert "p: T.ExamplePair(T.int32, T.float32)" in text, text
+        parsed = tvm_tirx.script.parse(text, extra_vars={"T": T})
+        assert tvm_ffi.structural_equal(parsed, func), parsed.script()
+        assert tvm_ffi.structural_equal(ir.load_json(ir.save_json(func)), func)
+
+        # A class written for it in Python takes the name.
+        @tvm_ffi.register_object("tirx.ExamplePairType")
+        class ExamplePair(ir.Type):
+            pass
+
+        assert T.expose_script_type("tirx.ExamplePairType") is ExamplePair
+        assert T.ExamplePair is ExamplePair
+        assert type(T.ExamplePair(T.int32, T.float32)) is ExamplePair
+        """,
+        libraries=[define_ir_library],
+    )
+
+
+def 
test_a_library_registers_its_operators_when_python_loads_it(define_ir_library, 
tmp_path):
+    copy = tmp_path / define_ir_library.name
+    shutil.copy(define_ir_library, copy)
     stderr = run_python(
         """
         import ctypes
@@ -216,12 +258,12 @@ def 
test_a_library_registers_its_operators_when_python_loads_it(define_ops_libra
         assert ir.Op.get("tirx.example_clamp").same_as(clamp)
         """,
         ROUND_TRIP,
-        libraries=[define_ops_library, copy],
+        libraries=[define_ir_library, copy],
     )
-    assert "registering operators failed" not in stderr, stderr
+    assert "registering operators and types failed" not in stderr, stderr
 
 
-def 
test_a_block_that_fails_when_its_library_loads_is_reported(define_ops_library):
+def 
test_a_block_that_fails_when_its_library_loads_is_reported(define_ir_library):
     stderr = run_python(
         """
         import sys
@@ -235,15 +277,15 @@ def 
test_a_block_that_fails_when_its_library_loads_is_reported(define_ops_librar
         module = tvm_ffi.load_module(sys.argv[1])
         ir.Op.get("tirx.example_clamp")
 
-        # `tirx_register_ops` raises the error again.
+        # `tirx_register_ir` raises the error again.
         try:
-            module.tirx_register_ops()
+            module.tirx_register_ir()
         except Exception as error:
             assert "tirx.example_mark is registered before" in str(error), 
error
         else:
             raise AssertionError("the block that failed was not reported")
         """,
-        libraries=[define_ops_library],
+        libraries=[define_ir_library],
     )
-    assert "registering operators failed" in stderr, stderr
+    assert "registering operators and types failed" in stderr, stderr
     assert "Operator tirx.example_mark is registered before" in stderr, stderr
diff --git a/tests/python/tirx/script/test_expose_script_op.py 
b/tests/python/tirx/script/test_expose_script_op.py
index ece863efe..e5f311c31 100644
--- a/tests/python/tirx/script/test_expose_script_op.py
+++ b/tests/python/tirx/script/test_expose_script_op.py
@@ -14,13 +14,14 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-"""TVMScript builders generated for registered operators."""
+"""TVMScript builders generated for registered operators, and classes for 
registered types."""
 
 import pytest
 import tvm_ffi
+from tvm_ffi.dataclasses import py_class
 
 import tvm_tirx
-from tvm_tirx.ir import Call, Op, PrimType, assert_structural_equal, 
register_op_attr
+from tvm_tirx.ir import Call, Op, PrimType, Type, assert_structural_equal, 
register_op_attr
 from tvm_tirx.script import parser
 from tvm_tirx.script import tirx as T
 from tvm_tirx.tirx.script import ir_builder as builder
@@ -35,6 +36,32 @@ def define(name, args, result):
     return Op.get(name)
 
 
+@py_class("tirx.TestScriptPairType")
+class ScriptPair(Type):
+    """A pair of types, printed as ``T.TestScriptPair(first, second)``."""
+
+    first: Type
+    second: Type
+
+
+@py_class("test_script_types.BoxType")
+class ScriptBox(Type):
+    """A type and a count, printed as ``test_script_types.Box(elem, 
count)``."""
+
+    elem: Type
+    count: int
+
+
+@py_class("tirx.evaluateType")
+class ScriptEvaluate(Type):
+    """A type whose printed name, ``T.evaluate``, is taken by a builder."""
+
+
+@py_class("tirx.TestScriptConstantType")
+class ScriptConstant(Type):
+    """A type whose printed name, ``T.TestScriptConstant``, a test takes by a 
non-callable."""
+
+
 @pytest.fixture
 def exposed():
     """The names a test exposes on T, which are removed after it."""
@@ -90,3 +117,29 @@ def test_existing_names_keep_their_values(exposed):
     define("tirx.supports_mutable_declarations", ["x"], "int32")
     with pytest.raises(ValueError, match="conflicts with a non-callable"):
         T.expose_script_op("tirx.supports_mutable_declarations")
+
+
+def test_type_classes_round_trip_types_under_every_root(exposed):
+    exposed += ["TestScriptPair", "test_script_types"]
+    assert T.expose_script_type("tirx.TestScriptPairType") is ScriptPair
+    assert T.expose_script_type("test_script_types.BoxType") is ScriptBox
+    assert T.expose_script_type("tirx.TestScriptPairType") is T.TestScriptPair
+    test_script_types = parser.test_script_types
+
+    @T.function
+    def func(p: T.TestScriptPair(T.int32, T.float32), b: 
test_script_types.Box(T.int8, 4)):
+        T.evaluate(p)
+        T.evaluate(b)
+
+    text = func.script()
+    assert "p: T.TestScriptPair(T.int32, T.float32)" in text, text
+    assert "b: test_script_types.Box(T.int8, 4)" in text, text
+    assert_structural_equal(tvm_tirx.script.parse(text, extra_vars={"T": T}), 
func)
+
+
+def test_existing_names_keep_their_values_over_types(exposed):
+    exposed += ["TestScriptConstant"]
+    assert T.expose_script_type("tirx.evaluateType") is builder.evaluate
+    builder.TestScriptConstant = 1
+    with pytest.raises(ValueError, match="conflicts with a non-callable"):
+        T.expose_script_type("tirx.TestScriptConstantType")

Reply via email to