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

spectrometerHBH pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-tirx.git


The following commit(s) were added to refs/heads/main by this push:
     new 502a57d93 [FEAT][RUST] Deduplicate register_ops! blocks across 
libraries (#42)
502a57d93 is described below

commit 502a57d938877bf7f96625f8b8f8e57696906622
Author: Linzhang Li <[email protected]>
AuthorDate: Mon Oct 26 14:17:47 2026 -0400

    [FEAT][RUST] Deduplicate register_ops! blocks across libraries (#42)
    
    ## Summary
    
    The operators of `register_ops!` blocks were registered only when
    something called
    `tirx_register_ops`: `import tvm_tirx` did for `libtvm_tirx_rust` (#36),
    but a library of
    operators that another crate builds on `tvm-tirx` had to be registered
    by hand. And each `cdylib`
    links its own copy of every crate it is built from, so two libraries
    that depend on the same crate
    of operators both contain its blocks, and the second one to run them
    failed:
    
    ```
    AttributeError: Operator tirx.my_clamp is registered before
    ```
    
    A block now runs when its library loads, as a C++
    `TVM_FFI_STATIC_INIT_BLOCK` does, and once per
    process: a block that another library has registered, from the same
    version of its crate, is
    skipped. Python only loads the library: its operators are registered
    and, as for every operator of
    a `register_ops!` block (#58), get their TVMScript builders, so the
    parser and printer treat them
    as those of a C++ library, whichever crate defines them:
    
    ```rust
    // my_ops: a crate of operators.
    tvm_tirx::register_ops! {
        OpDef::new("tirx.my_clamp", "Clamp an index to the range `[0, bound)`.")
            .signature((sig::arg::<IntExpr>("index", ""), 
sig::arg::<IntExpr>("bound", "")))
            .set_infer_type(first_arg_type);
    }
    ```
    
    ```rust
    // lib_a and lib_b: two cdylibs that both depend on my_ops.
    use my_ops as _; // a crate that is never named is not linked
    ```
    
    ```python
    import tvm_ffi
    from tvm_tirx.script import tirx as T
    
    tvm_ffi.load_module("target/release/liblib_a.so")  # registers 
tirx.my_clamp and T.my_clamp
    tvm_ffi.load_module("target/release/liblib_b.so")  # failed before; now 
skips my_ops' block
    ```
    
    Each block adds a constructor to its library (`.init_array`, or
    `__mod_init_func` on macOS). The
    first one runs every block of the library and keeps the library loaded,
    as the operators keep its
    hooks. An error at that point is printed rather than aborting the
    process, and `tirx_register_ops`
    raises it again, as `import tvm_tirx` does for `libtvm_tirx_rust`. A
    block that has registered its
    operators is marked by a global function
    `ir.registered_op_block.<module:line@version#hash>` in the
    TVM FFI registry, which the libraries of a process share; nothing
    changes on the C++ side.
    
    The builders come from `tirx.script.expose_script_op`, which `import
    tvm_tirx` registers, so a
    library loaded before `tvm_tirx` is imported registers its operators
    without builders.
---
 python/tvm_tirx/backend/rust/__init__.py |   3 +-
 rust/examples/define_ops.rs              |   7 +-
 rust/src/ir/op_def.rs                    | 196 +++++++++++++++++++++++++++----
 rust/src/lib.rs                          |  31 +++--
 rust/tests/ir/op_def.rs                  |  86 ++++++++++++--
 tests/python/ir/test_op_rust.py          | 164 +++++++++++++++++++-------
 6 files changed, 392 insertions(+), 95 deletions(-)

diff --git a/python/tvm_tirx/backend/rust/__init__.py 
b/python/tvm_tirx/backend/rust/__init__.py
index 13aebfd49..74d8e5ea7 100644
--- a/python/tvm_tirx/backend/rust/__init__.py
+++ b/python/tvm_tirx/backend/rust/__init__.py
@@ -37,7 +37,8 @@ def register_backend():
     except (OSError, FileNotFoundError, RuntimeError):
         pass
     else:
-        # Unlike a runtime sidecar, the library registers its operators only 
when asked to.
+        # 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()
     return None
 
diff --git a/rust/examples/define_ops.rs b/rust/examples/define_ops.rs
index 931a8069b..4bdb49e32 100644
--- a/rust/examples/define_ops.rs
+++ b/rust/examples/define_ops.rs
@@ -21,10 +21,9 @@
 //!
 //! `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.
-//! `tests/python/ir/test_op_rust.py` loads it and calls `tirx_register_ops`, 
as `import tvm_tirx`
-//! does for `libtvm_tirx_rust`, which registers these operators and gives 
each its TVMScript
-//! builder: `T.example_clamp(i, n)`, `T.example_mark()`, `my_ext.hint(i, 
attrs={..})` and
-//! `with T.example_loop(n) as j:`.
+//! `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:`.
 
 use tvm_tirx::ir::op_attr::{CALL_EFFECT_KIND, FIXED_RETURN_TYPE};
 use tvm_tirx::ir::sig::{self, IntExpr};
diff --git a/rust/src/ir/op_def.rs b/rust/src/ir/op_def.rs
index 1b96b13f0..db244c1a3 100644
--- a/rust/src/ir/op_def.rs
+++ b/rust/src/ir/op_def.rs
@@ -28,11 +28,10 @@
 //! `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 [`register_defined_ops`] runs them all. 
Unlike a C++ static
-//! init block, a block does not run when its library loads. Every library 
built on this crate
-//! exports [`register_defined_ops`] as the TVM FFI function 
`tirx_register_ops`, which
-//! `import tvm_tirx` calls on the library built from this crate 
(`libtvm_tirx_rust`) right after
-//! it loads it, so Python sees its operators as it sees the C++ ones.
+//! 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.
 
 use std::any::TypeId;
 use std::cell::RefCell;
@@ -40,11 +39,11 @@ use std::ffi::c_void;
 use std::marker::PhantomData;
 use std::panic::{catch_unwind, AssertUnwindSafe};
 use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
-use std::sync::OnceLock;
+use std::sync::{Once, OnceLock};
 
 use tvm_ffi::tvm_ffi_sys::{TVMFFIAny, TVMFFIObject, TVMFFITypeIndex};
 use tvm_ffi::{
-    Any, AnyView, Array, Error, ObjectArc, ObjectRefCore, Result, String, 
INTERNAL_ERROR,
+    Any, AnyView, Array, Error, Function, ObjectArc, ObjectRefCore, Result, 
String, INTERNAL_ERROR,
     TYPE_ERROR, VALUE_ERROR,
 };
 
@@ -322,9 +321,9 @@ impl From<CallEffectKind> for Any {
 
 /// A handle to an operator, kept in a `static` as C++ keeps one in `const Op& 
xxx_op()`. The
 /// operator is looked up by name once per thread; [`OpHandle::get`] returns 
it and
-/// [`OpHandle::is`] compares with it. The first lookup of an operator that is 
not registered runs
-/// the [`register_ops!`](crate::register_ops) blocks first, so that a Rust 
program can use the
-/// operators they define without registering them itself.
+/// [`OpHandle::is`] compares with it. The 
[`register_ops!`](crate::register_ops) blocks run when
+/// their program or library loads; where the loader does not run them, the 
first lookup of an
+/// operator that is not registered runs them.
 ///
 /// ```no_run
 /// use tvm_tirx::ir::sig::{self, IntExpr};
@@ -442,6 +441,10 @@ pub struct OpRegistration {
     /// The module and line of the block.
     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
+    /// line identify the block in every library that links the crate.
+    version: &'static str,
+    text_hash: u64,
     register: fn() -> Result<()>,
     /// The outcome of the block, which runs once: the error's text if it 
failed.
     outcome: OnceLock<std::result::Result<(), std::string::String>>,
@@ -455,16 +458,40 @@ pub static OP_REGISTRATIONS: [OpRegistration];
 
 impl OpRegistration {
     #[doc(hidden)]
-    pub const fn __new(module: &'static str, line: u32, register: fn() -> 
Result<()>) -> Self {
-        Self { module, line, register, outcome: OnceLock::new() }
+    pub const fn __new(
+        module: &'static str,
+        line: u32,
+        version: &'static str,
+        text: &str,
+        register: fn() -> Result<()>,
+    ) -> Self {
+        let text_hash = text_hash(text);
+        Self { 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
+    /// library that links the block's crate, and another one if the crate 
version or the block's
+    /// text differs.
+    #[doc(hidden)]
+    pub fn identity(&self) -> std::string::String {
+        format!("{}:{}@{}#{:016x}", self.module, self.line, self.version, 
self.text_hash)
+    }
+
+    /// 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
+    /// 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())
     }
 
     /// Run the block if it has not run. The caller that runs a failing block 
gets its error;
     /// later ones get an error that names the block.
-    fn register(&self) -> Result<()> {
+    #[doc(hidden)]
+    pub fn register(&self) -> Result<()> {
         let mut failure = None;
         let outcome = self.outcome.get_or_init(|| {
-            (self.register)().map_err(|error| {
+            self.run().map_err(|error| {
                 let text = format!("{}: {}", error.kind(), error.message());
                 failure = Some(error);
                 text
@@ -480,20 +507,51 @@ impl OpRegistration {
         }
     }
 
+    /// 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
+    /// is marked once it succeeds, so that one that fails fails in every 
library.
+    fn run(&self) -> Result<()> {
+        let marker = self.marker();
+        if Function::get_global(&marker).is_err() {
+            (self.register)()?;
+            Function::register_global(&marker, Function::from_packed(|_| 
Ok(Any::default())))?;
+        }
+        Ok(())
+    }
+
     /// Where the block is, for an error.
     fn place(&self) -> std::string::String {
         format!("{}:{}", self.module, self.line)
     }
 }
 
+/// The FNV-1a hash of `text` without its whitespace, which does not depend on 
how the compiler
+/// spaces the tokens of a block it stringifies.
+const fn text_hash(text: &str) -> u64 {
+    let bytes = text.as_bytes();
+    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
+    let mut i = 0;
+    while i < bytes.len() {
+        if !bytes[i].is_ascii_whitespace() {
+            hash = (hash ^ bytes[i] as u64).wrapping_mul(0x0100_0000_01b3);
+        }
+        i += 1;
+    }
+    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.
+/// 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.
 ///
-/// `import tvm_tirx` calls it on `libtvm_tirx_rust` through the TVM FFI 
function
-/// `tirx_register_ops`, which every library built on this crate exports. A 
Rust program calls it
-/// before it builds IR, as the registry does not synchronize registration 
with readers; otherwise
-/// the first lookup through an [`OpHandle`] of an operator that is not 
registered calls it.
+/// 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
+/// 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();
     blocks.sort_by_key(|block| (block.module, block.line));
@@ -509,6 +567,69 @@ pub fn register_defined_ops() -> Result<()> {
     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.
+#[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) {
+            Ok(Ok(())) => {}
+            Ok(Err(error)) => eprintln!(
+                "{library}: registering operators failed: {}: {}",
+                error.kind(),
+                error.message()
+            ),
+            Err(_) => eprintln!("{library}: registering operators 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
+/// 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> {
+    use std::ffi::{c_char, c_int, CStr};
+
+    #[repr(C)]
+    struct DlInfo {
+        fname: *const c_char,
+        fbase: *mut c_void,
+        sname: *const c_char,
+        saddr: *mut c_void,
+    }
+    extern "C" {
+        fn dladdr(addr: *const c_void, info: *mut DlInfo) -> c_int;
+        fn dlopen(filename: *const c_char, flags: c_int) -> *mut c_void;
+    }
+    // RTLD_LAZY | RTLD_NOLOAD | RTLD_NODELETE.
+    #[cfg(target_os = "linux")]
+    const FLAGS: c_int = 0x1 | 0x4 | 0x1000;
+    #[cfg(target_os = "macos")]
+    const FLAGS: c_int = 0x1 | 0x10 | 0x80;
+
+    let null = std::ptr::null_mut();
+    let mut info = DlInfo { fname: null as _, fbase: null, sname: null as _, 
saddr: null };
+    // SAFETY: `dladdr` fills `info` for an address of this library, whose 
path stays valid while
+    // the library is loaded; `dlopen` of a loaded library only marks it, and 
its handle is kept.
+    unsafe {
+        if dladdr(keep_loaded as *const c_void, &mut info) == 0 || 
info.fname.is_null() {
+            return None;
+        }
+        dlopen(info.fname, FLAGS);
+        Some(CStr::from_ptr(info.fname).to_string_lossy().into_owned())
+    }
+}
+
+#[cfg(not(any(target_os = "linux", target_os = "macos")))]
+fn keep_loaded() -> Option<std::string::String> {
+    None
+}
+
 /// The text of `doc` without the indentation its lines share and without the 
blank lines around
 /// it. Text that is not indented is unchanged.
 fn doc_text(doc: &str) -> std::string::String {
@@ -551,15 +672,26 @@ fn doc_text(doc: &str) -> std::string::String {
 ///         .set_attr(CALL_EFFECT_KIND, CallEffectKind::Opaque);
 /// }
 ///
-/// tvm_tirx::ir::register_defined_ops()?;
+/// // Registered when the program loaded.
 /// assert_eq!(Op::get("my_ext.clamp")?.doc.as_str(), "Clamp an index to a 
bound.");
 /// # Ok::<(), tvm_tirx::tvm_ffi::Error>(())
 /// ```
 ///
-/// Unlike a C++ static init block, a block does not run when its library 
loads, but when the
-/// operators are registered: [`register_defined_ops`] runs every block of the 
program or library,
-/// which `import tvm_tirx` does through `tirx_register_ops`, and an 
[`OpHandle`] runs them on the
-/// first lookup of an operator that is not registered. A block runs once.
+/// 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
+/// 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
+/// loader does not run the constructor, an [`OpHandle`] runs the blocks on 
the first lookup of an
+/// operator that is not registered.
+///
+/// A block runs once per process. Every library built on the crate links its 
own copy of the
+/// crates it uses, so a block of a crate that several libraries link is in 
each of them; the first
+/// library that runs it registers its operators, and the others skip it. A 
block of another
+/// version of the crate, or whose text differs, is another block, and 
registering its operators
+/// again is an error. A change to a hook's body alone does not make another 
block: rebuild every
+/// library that links a crate after changing it.
 #[macro_export]
 macro_rules! register_ops {
     ($($def:expr);* $(;)?) => {
@@ -570,8 +702,20 @@ macro_rules! register_ops {
             }
             
#[$crate::__private::linkme::distributed_slice($crate::ir::OP_REGISTRATIONS)]
             #[linkme(crate = $crate::__private::linkme)]
-            static REGISTRATION: $crate::ir::OpRegistration =
-                $crate::ir::OpRegistration::__new(module_path!(), line!(), 
register);
+            static REGISTRATION: $crate::ir::OpRegistration = 
$crate::ir::OpRegistration::__new(
+                module_path!(),
+                line!(),
+                match option_env!("CARGO_PKG_VERSION") {
+                    Some(version) => version,
+                    None => "",
+                },
+                stringify!($($def);*),
+                register,
+            );
+            #[used]
+            #[cfg_attr(target_os = "linux", link_section = ".init_array")]
+            #[cfg_attr(target_os = "macos", link_section = 
"__DATA,__mod_init_func")]
+            static REGISTER_AT_LOAD: extern "C" fn() = 
$crate::ir::__register_at_load;
         };
     };
 }
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index ed1f56047..261f235f8 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -129,23 +129,30 @@
 //! ```
 //!
 //! An [`ir::OpHandle`] in a `static` keeps an operator as C++ keeps one in 
`const Op& xxx_op()`.
-//! A block does not run when its library loads, as a C++ one does, but when 
the operators are
-//! registered: [`ir::register_defined_ops`] runs every block, wherever it is, 
and every library
-//! built on the crate exports it as the TVM FFI function `tirx_register_ops`; 
a handle runs them on
-//! the first lookup of an operator that is not registered.
+//! 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`.
 //!
 //! # 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`) and calls 
`tirx_register_ops`, so the operators
-//! the crate defines are registered by the time the import returns, as the 
C++ ones are: `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. `examples/define_ops.rs` builds 
a library of its own
-//! the same way, which `tvm_ffi.load_module(path).tirx_register_ops()` 
registers.
+//! 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.
 //!
 //! # Defining a type in Rust
 //!
@@ -290,7 +297,7 @@ pub mod tirx;
 
 pub use tvm_ffi;
 
-// The registration entry of every library built on the crate, which `import 
tvm_tirx` calls on
+// 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
 // the library as `tvm_ffi.load_module` loads it.
 tvm_ffi::tvm_ffi_dll_export_typed_func!(tirx_register_ops, 
ir::register_defined_ops);
diff --git a/rust/tests/ir/op_def.rs b/rust/tests/ir/op_def.rs
index 56857164a..928cfa5b5 100644
--- a/rust/tests/ir/op_def.rs
+++ b/rust/tests/ir/op_def.rs
@@ -32,14 +32,14 @@ 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, PrimType, RegionStmt, 
RegionStmtObj, StringImm, Type,
-    Var, VarObj, UNKNOWN_LOC,
+    FuncType, IntImm, Op, OpDef, OpHandle, OpRegistration, PrimType, 
RegionStmt, RegionStmtObj,
+    StringImm, Type, Var, VarObj, OP_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, Map, ObjectRefCast, ObjectRefCore, Result, 
String as FfiString,
-    ATTRIBUTE_ERROR, INTERNAL_ERROR, TYPE_ERROR, VALUE_ERROR,
+    Any, Array, Error, ErrorKind, Function, Map, ObjectRefCast, ObjectRefCore, 
Result,
+    String as FfiString, ATTRIBUTE_ERROR, INTERNAL_ERROR, TYPE_ERROR, 
VALUE_ERROR,
 };
 
 use super::{int32, parse_dtype, tensor_var};
@@ -457,6 +457,21 @@ mod failing {
     }
 }
 
+/// The blocks of `module` in this test binary.
+fn blocks_of(module: &str) -> Vec<&'static OpRegistration> {
+    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()
+}
+
+fn is_block_registered(block: &OpRegistration) -> bool {
+    Function::get_global(&block.marker()).is_ok()
+}
+
 static IDENTITY: OpHandle = OpHandle::new("rust_test.defined_identity");
 static LAUNCH: OpHandle = OpHandle::new("rust_test.launch");
 static PANICKING: OpHandle = OpHandle::new("rust_test.panicking_region");
@@ -528,11 +543,10 @@ fn region(
 fn register_ops_blocks_define_operators() {
     let _serial = serial();
 
-    // The first lookup through a handle of an operator not registered runs 
every block; a block
-    // that fails does not keep the others from registering theirs.
-    assert!(Op::get("rust_test.defined_identity").is_err(), "not registered 
before");
-    let identity = IDENTITY.get().unwrap();
-    assert!(identity.same_as(&Op::get("rust_test.defined_identity").unwrap()));
+    // The blocks ran when the test binary loaded; a block that fails does not 
keep the others from
+    // registering theirs.
+    let identity = Op::get("rust_test.defined_identity").unwrap();
+    assert!(identity.same_as(&IDENTITY.get().unwrap()));
     assert_eq!(identity.doc.as_str(), "Return the value.\n\nThe documentation 
is dedented.");
     assert_eq!(identity.args_info.get(0).unwrap().name.as_str(), "value");
     let effect = Op::attr_map::<i64>("TCallEffectKind").unwrap().unwrap();
@@ -540,7 +554,7 @@ fn register_ops_blocks_define_operators() {
     let elsewhere = Op::get("rust_test.defined_elsewhere").unwrap();
     assert_eq!(elsewhere.doc.as_str(), "An operator in another module.");
     let fixed = Op::attr_map::<Type>("TFixedReturnType").unwrap().unwrap();
-    
assert!(fixed.get(&elsewhere).unwrap().unwrap().same_as(&PrimType::void()));
+    assert!(structural_equal(&fixed.get(&elsewhere).unwrap().unwrap(), 
&PrimType::void()));
 
     // `register_defined_ops` 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.
@@ -557,6 +571,13 @@ fn register_ops_blocks_define_operators() {
     assert!(Op::get("rust_test.after_broken").is_err(), "the block stopped");
     assert_eq!(register_defined_ops().unwrap_err().message(), message);
 
+    // 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.
+    
assert!(blocks_of("").into_iter().chain(blocks_of("::elsewhere")).all(is_block_registered));
+    let failing = blocks_of("::failing");
+    assert_eq!(failing.len(), 2);
+    assert!(!failing.into_iter().any(is_block_registered));
+
     // The handle keeps the operator: the Rust `FInferType` types a Call of 
it, and another thread
     // sees the same operator.
     assert!(IDENTITY.get().unwrap().same_as(&identity));
@@ -631,3 +652,48 @@ fn register_ops_blocks_define_operators() {
     );
     assert!(built.is_err(), "a panicking hook fails the constructor");
 }
+
+#[test]
+fn a_block_another_library_registered_is_skipped() {
+    let _serial = serial();
+    fn register() -> Result<()> {
+        OpDef::new("rust_test.registered_once", "").op()?;
+        Ok(())
+    }
+    // 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 (first, second) = (block(), block());
+    assert_eq!(first.marker(), second.marker());
+
+    // The first library registers the operator and marks the block; the 
second skips it, where
+    // registering the name again would fail.
+    first.register().unwrap();
+    assert!(is_block_registered(&first));
+    let op = Op::get("rust_test.registered_once").unwrap();
+    second.register().unwrap();
+    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);
+    assert!(other.register().unwrap_err().message().contains("is registered 
before"));
+}
+
+#[test]
+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 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}");
+
+    // The same block in another library, whose compiler may space the tokens 
otherwise.
+    let spaced = "OpDef :: new(\"ext.clamp\", \"Clamp.\")\n    . 
set_infer_type(first_arg_type)";
+    assert_eq!(identity("0.1.0", spaced).identity(), block);
+    // A block of another version of the crate, or whose text differs, is 
another block.
+    assert_ne!(identity("0.1.1", text).identity(), block);
+    let renamed = text.replace("ext.clamp", "ext.clip");
+    assert_ne!(identity("0.1.0", &renamed).identity(), block);
+}
diff --git a/tests/python/ir/test_op_rust.py b/tests/python/ir/test_op_rust.py
index b5c43f6d1..57928ae5c 100644
--- a/tests/python/ir/test_op_rust.py
+++ b/tests/python/ir/test_op_rust.py
@@ -14,11 +14,13 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-"""Operators defined in Rust are registered when their library is loaded, and 
used as C++ ones are.
+"""Operators defined in Rust are registered when their library loads, as C++ 
ones are.
 
-``rust/examples/define_ops.rs`` is built by cargo and loaded by a fresh 
interpreter, as
-``import tvm_tirx`` loads libtvm_tirx_rust, which then uses its operators as 
it uses C++ ones:
-registering them also gives them their TVMScript builders.
+``rust/examples/define_ops.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.
 """
 
 import os
@@ -72,22 +74,61 @@ def define_ops_library():
     return target_dir / "debug" / "examples" / name
 
 
-def run_python(code, library):
-    """Run `code` in a fresh interpreter, with the path of `library` as its 
argument.
+def run_python(*code, libraries):
+    """Run `code` in a fresh interpreter, with the paths of `libraries` as its 
arguments, and
+    return its standard error.
 
     The code runs from a file: TVMScript reads the source of the functions it 
parses, which Python
     keeps for code passed with `-c` only from 3.13 on.
     """
     with tempfile.TemporaryDirectory() as tmp:
         script = Path(tmp) / "script.py"
-        script.write_text(textwrap.dedent(code))
+        script.write_text("\n".join(textwrap.dedent(part) for part in code))
         result = subprocess.run(
-            [sys.executable, str(script), str(library)],
+            [sys.executable, str(script), *map(str, libraries)],
             capture_output=True,
             text=True,
             check=False,
         )
     assert result.returncode == 0, result.stderr
+    return result.stderr
+
+
+# With the TVMScript builders that registering the example operators gives 
them, TVMScript writes,
+# prints and parses their calls as it does those of C++ operators: a call with 
a value, a call
+# emitted as a statement, `Call.attrs`, and a region, whose body parameter the 
Rust hook creates.
+ROUND_TRIP = """
+    import tvm_ffi
+    import tvm_tirx
+    from tvm_tirx import ir
+    from tvm_tirx.script import tirx as T
+
+    # Each builder is generated from the signature of its operator under the 
name the printer
+    # gives its calls: `T.<name>` for `tirx.<name>`, and outside `tirx.`, in 
the parser namespace
+    # its root names.
+    assert T.example_clamp.__doc__ == ir.Op.get("tirx.example_clamp").doc
+    assert T.example_loop.__doc__ == ir.Op.get("tirx.example_loop").doc
+    from tvm_tirx.script.parser import my_ext
+
+    assert my_ext.hint.__doc__ == ir.Op.get("my_ext.hint").doc
+
+    @T.function
+    def func(i: T.int32, n: T.int32):
+        T.evaluate(T.example_clamp(i + 1, n))
+        T.example_mark()
+        T.evaluate(my_ext.hint(i, attrs={"note": "hot"}))
+        with T.example_loop(n) as j:
+            T.evaluate(T.example_clamp(j, n))
+
+    text = func.script()
+    assert "T.example_clamp(i + 1, n)" in text, text
+    assert "T.example_mark()" in text, text
+    assert 'my_ext.hint(i, attrs={"note": "hot"})' in text, text
+    assert "with T.example_loop(n) as j:" in text, text
+    parsed = tvm_tirx.script.parse(text, extra_vars={"T": T})
+    assert tvm_ffi.structural_equal(parsed, func), parsed.script()
+    assert parsed.script() == text
+"""
 
 
 def test_library_is_loaded_on_import():
@@ -110,19 +151,16 @@ def 
test_rust_operators_are_registered_by_their_library(define_ops_library):
         import sys
 
         import tvm_ffi
-        import tvm_tirx
         from tvm_tirx import ir
-        from tvm_tirx.script import tirx as T
 
-        # Registered by the library, with the doc comment and signature the 
Rust definition gives.
+        # 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()
         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"]
         mark = ir.Op.get("tirx.example_mark")
         assert mark.doc.startswith("Mark a point in the program.\\n\\nThe call 
has no value")
-        hint = ir.Op.get("my_ext.hint")
-        loop = ir.Op.get("tirx.example_loop")
 
         # A Call built in Python runs the Rust type inference and validator.
         i, n = ir.Var("i", "int32"), ir.Var("n", "int32")
@@ -135,35 +173,77 @@ def 
test_rust_operators_are_registered_by_their_library(define_ops_library):
             assert "`Call.args[1]` (`bound`) expected `ir.IntExpr`" in 
str(error), error
         else:
             raise AssertionError("a float bound passed validation")
+        """,
+        ROUND_TRIP,
+        libraries=[define_ops_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)
+    stderr = run_python(
+        """
+        import ctypes
+        import gc
+        import sys
+
+        import tvm_ffi
+        from tvm_tirx import ir
+
+        library, copy = sys.argv[1:]
+
+        # The library of another crate: importing tvm_tirx does not register 
its operators.
+        try:
+            ir.Op.get("tirx.example_clamp")
+        except Exception:
+            pass
+        else:
+            raise AssertionError("registered before the library loaded")
+
+        # Loading it registers them, as loading a C++ library does. The 
library stays loaded when
+        # Python drops its module, as the operators keep its hooks.
+        module = tvm_ffi.load_module(library)
+        del module
+        gc.collect()
+        clamp = ir.Op.get("tirx.example_clamp")
+        i, n = ir.Var("i", "int32"), ir.Var("n", "int32")
+        assert tvm_ffi.structural_equal(ir.Call(clamp, [i, n]).ty, i.ty)
+
+        # The copy has the blocks of the library, as two libraries that link 
the same crate do:
+        # loading it registers nothing again.
+        ctypes.CDLL(copy)
+        assert ir.Op.get("tirx.example_clamp").same_as(clamp)
+        """,
+        ROUND_TRIP,
+        libraries=[define_ops_library, copy],
+    )
+    assert "registering operators failed" not in stderr, stderr
+
 
-        # Registration gave each its TVMScript builder, generated from its 
signature under the name
-        # the printer gives its calls.
-        assert T.example_clamp.__doc__ == clamp.doc
-        assert T.example_loop.__doc__ == loop.doc
-        # Outside `tirx.`, in the parser namespace its root names.
-        from tvm_tirx.script.parser import my_ext
-
-        assert my_ext.hint.__doc__ == hint.doc
-
-        # With them, TVMScript writes, prints and parses the calls as it does 
those of C++
-        # operators: a call with a value, a call emitted as a statement, 
`Call.attrs`, and a region,
-        # whose body parameter the Rust hook creates.
-        @T.function
-        def func(i: T.int32, n: T.int32):
-            T.evaluate(T.example_clamp(i + 1, n))
-            T.example_mark()
-            T.evaluate(my_ext.hint(i, attrs={"note": "hot"}))
-            with T.example_loop(n) as j:
-                T.evaluate(T.example_clamp(j, n))
-
-        text = func.script()
-        assert "T.example_clamp(i + 1, n)" in text, text
-        assert "T.example_mark()" in text, text
-        assert 'my_ext.hint(i, attrs={"note": "hot"})' in text, text
-        assert "with T.example_loop(n) as j:" in text, text
-        parsed = tvm_tirx.script.parse(text, extra_vars={"T": T})
-        assert tvm_ffi.structural_equal(parsed, func), parsed.script()
-        assert parsed.script() == text
+def 
test_a_block_that_fails_when_its_library_loads_is_reported(define_ops_library):
+    stderr = run_python(
+        """
+        import sys
+
+        import tvm_ffi
+        from tvm_tirx import ir
+
+        # Another library registered the name of an example operator: the 
block that defines it
+        # fails when the library loads, and the other block registers its 
operators.
+        tvm_ffi.get_global_func("ir.RegisterOp")("tirx.example_mark", "")
+        module = tvm_ffi.load_module(sys.argv[1])
+        ir.Op.get("tirx.example_clamp")
+
+        # `tirx_register_ops` raises the error again.
+        try:
+            module.tirx_register_ops()
+        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")
         """,
-        define_ops_library,
+        libraries=[define_ops_library],
     )
+    assert "registering operators failed" in stderr, stderr
+    assert "Operator tirx.example_mark is registered before" in stderr, stderr

Reply via email to