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 7ec946256 [FEAT][RUST] Generate TVMScript builders for registered 
operators (#58)
7ec946256 is described below

commit 7ec9462564c2be39ad165f814963696b374c6ecf
Author: Linzhang Li <[email protected]>
AuthorDate: Sun Oct 25 14:40:19 2026 -0400

    [FEAT][RUST] Generate TVMScript builders for registered operators (#58)
    
    ## Summary
    
    An operator registered from Rust is now printed and parsed by TVMScript
    without a builder written
    for it in Python. `tvm_tirx.tirx.script.expose_script_op(name)`
    generates the builder of a
    registered operator from its registration, under the name the printer
    gives its calls, and
    `OpDef::op` calls it for every operator a `register_ops!` block defines,
    once Python has imported
    `tvm_tirx`. An operator registered from Python or C++ gets its builder
    by calling it.
    
    ```rust
    tvm_tirx::register_ops! {
        OpDef::new("tirx.demo_clamp", "Clamp an index to the range `[0, 
bound)`.")
            .signature((sig::arg::<IntExpr>("index", ""), 
sig::arg::<IntExpr>("bound", "")))
            .set_infer_type(first_arg_type);
        OpDef::new("my_ext.tiles", "Run the body for each tile in `[0, 
count)`.")
            .signature(sig::arg::<IntExpr>("count", ""))
            .set_region_body_params(tile_index);
    }
    ```
    
    ```python
    tvm_ffi.load_module(path).tirx_register_ops()  # the user's Rust library
    
    @T.function
    def kernel(i: T.int32, n: T.int32):
        T.evaluate(T.demo_clamp(i, n))
        with my_ext.tiles(n) as t:
            T.evaluate(T.demo_clamp(t, n))
    
    assert tvm_ffi.structural_equal(tvm_tirx.script.parse(kernel.script(), 
extra_vars={"T": T}), kernel)
    ```
---
 docs/reference/python/tirx.script.rst             |  1 +
 python/tvm_tirx/tirx/script/ir_builder/op.py      | 66 ++++++++++++++++
 rust/examples/define_ops.rs                       |  9 +--
 rust/src/ir/op_def.rs                             |  9 ++-
 rust/src/lib.rs                                   |  7 +-
 tests/python/ir/test_op_rust.py                   | 33 +++-----
 tests/python/tirx/script/test_expose_script_op.py | 92 +++++++++++++++++++++++
 7 files changed, 185 insertions(+), 32 deletions(-)

diff --git a/docs/reference/python/tirx.script.rst 
b/docs/reference/python/tirx.script.rst
index 6aa50b31d..e74a3e4ab 100644
--- a/docs/reference/python/tirx.script.rst
+++ b/docs/reference/python/tirx.script.rst
@@ -368,6 +368,7 @@ Functions
    min
    max
    register_script_namespace
+   expose_script_op
    abs
    acos
    acosh
diff --git a/python/tvm_tirx/tirx/script/ir_builder/op.py 
b/python/tvm_tirx/tirx/script/ir_builder/op.py
index cf815098c..4285d8232 100644
--- a/python/tvm_tirx/tirx/script/ir_builder/op.py
+++ b/python/tvm_tirx/tirx/script/ir_builder/op.py
@@ -18,8 +18,12 @@
 
 from __future__ import annotations
 
+import sys
+from types import SimpleNamespace
 from typing import Any
 
+import tvm_ffi
+
 from tvm_tirx import ir
 from tvm_tirx import ir as _ir
 from tvm_tirx import tirx as _tir
@@ -258,6 +262,67 @@ def register_script_namespace(name: str, namespace: 
object) -> object:
     return namespace
 
 
+def expose_script_op(name: str) -> Any:
+    """Give a registered Op the TVMScript builder its printed calls name, and 
return it.
+
+    The printer spells a call to ``tirx.<path>``, ``prim.<path>`` or 
``ir.<path>`` as
+    ``T.<path>``, and a call to any other Op by its name, whose first part is 
then a
+    parser namespace. The builder is generated from the Op's registration, as 
backend
+    builders are, and a missing namespace of its path is created. An existing 
callable
+    keeps the name, so a hand-written builder takes precedence. Operators of 
Rust
+    ``register_ops!`` blocks get their builders when they are registered.
+
+    Parameters
+    ----------
+    name : str
+        Name of a registered Op.
+
+    Returns
+    -------
+    builder : Callable
+        The builder the printed calls name.
+    """
+    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
+    if parts[0] in ("tirx", "prim", "ir"):
+        container, parts = sys.modules["tvm_tirx.tirx.script.ir_builder"], 
parts[1:]
+
+    def lookup(part):
+        if container is None:
+            return parser._NAMESPACES.get(part)
+        return getattr(container, part, None)
+
+    def bind(part, value):
+        if container is None:
+            parser.register_namespace(part, value)
+        else:
+            setattr(container, part, value)
+
+    *namespaces, leaf = parts
+    for part in namespaces:
+        if lookup(part) is None:
+            bind(part, SimpleNamespace())
+        container = lookup(part)
+    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
+
+
+# Rust operators call this when they are registered.
+tvm_ffi.register_global_func("tirx.script.expose_script_op", expose_script_op)
+
+
 abs = _auto_emit_void_call(_tir_op.abs)  # pylint: disable=redefined-builtin
 
 
@@ -831,6 +896,7 @@ __all__ = [
     "exp",
     "exp2",
     "exp10",
+    "expose_script_op",
     "fabs",
     "floor",
     "floordiv",
diff --git a/rust/examples/define_ops.rs b/rust/examples/define_ops.rs
index 7bea93fc3..931a8069b 100644
--- a/rust/examples/define_ops.rs
+++ b/rust/examples/define_ops.rs
@@ -22,9 +22,8 @@
 //! `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. As a C++ 
operator
-//! does, each has its TVMScript builder written in Python, which the test 
writes:
-//! `T.example_clamp(i, n)`, `T.example_mark()`, `T.example_hint(i, 
attrs={..})` and
+//! 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:`.
 
 use tvm_tirx::ir::op_attr::{CALL_EFFECT_KIND, FIXED_RETURN_TYPE};
@@ -51,8 +50,8 @@ tvm_tirx::register_ops! {
         .set_infer_type(first_arg_type)
         .set_attr(CALL_EFFECT_KIND, CallEffectKind::Pure);
 
-    // `Call.attrs` holds a `DictAttrs`.
-    OpDef::new("tirx.example_hint", "Return the value, with hints for the code 
generator.")
+    // `Call.attrs` holds a `DictAttrs`; outside `tirx.`, the calls print as 
`my_ext.hint(..)`.
+    OpDef::new("my_ext.hint", "Return the value, with hints for the code 
generator.")
         .signature((sig::arg::<Expr>("value", "The value."), 
sig::call_attrs::<DictAttrs>()))
         .set_infer_type(first_arg_type)
         .set_attr(CALL_EFFECT_KIND, CallEffectKind::ExprAnnotation);
diff --git a/rust/src/ir/op_def.rs b/rust/src/ir/op_def.rs
index 5eb608047..1b96b13f0 100644
--- a/rust/src/ir/op_def.rs
+++ b/rust/src/ir/op_def.rs
@@ -93,9 +93,14 @@ impl OpDef {
         Self(registered.and_then(|_| Op::get(name)))
     }
 
-    /// The operator, or the error of the step that failed.
+    /// The operator, or the error of the step that failed. Once Python has 
imported `tvm_tirx`,
+    /// this also gives the operator its TVMScript builder 
(`tirx.script.expose_script_op`).
     pub fn op(self) -> Result<Op> {
-        self.0
+        let op = self.0?;
+        if let Ok(expose) = 
tvm_ffi::Function::get_global("tirx.script.expose_script_op") {
+            expose.call_tuple((op.name.clone(),))?;
+        }
+        Ok(op)
     }
 
     /// C++ `OpDef::signature`: replace the argument descriptions and the 
attrs type, and the
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 1706935ef..ed1f56047 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -142,9 +142,10 @@
 //! 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. As for a C++ operator, the TVMScript builder of an operator 
(`T.<name>` for
-//! `tirx.<name>`) is a Python function written for it. 
`examples/define_ops.rs` builds a library
-//! of its own the same way, which 
`tvm_ffi.load_module(path).tirx_register_ops()` registers.
+//! 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.
 //!
 //! # Defining a type in Rust
 //!
diff --git a/tests/python/ir/test_op_rust.py b/tests/python/ir/test_op_rust.py
index a40da764a..b5c43f6d1 100644
--- a/tests/python/ir/test_op_rust.py
+++ b/tests/python/ir/test_op_rust.py
@@ -18,7 +18,7 @@
 
 ``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:
-their TVMScript builders are Python functions written for them.
+registering them also gives them their TVMScript builders.
 """
 
 import os
@@ -113,8 +113,6 @@ def 
test_rust_operators_are_registered_by_their_library(define_ops_library):
         import tvm_tirx
         from tvm_tirx import ir
         from tvm_tirx.script import tirx as T
-        from tvm_tirx.script.ir_builder.base import auto_emit_void_call
-        from tvm_tirx.tirx.script import ir_builder
 
         # Registered by the library, with the doc comment and signature the 
Rust definition gives.
         tvm_ffi.load_module(sys.argv[1]).tirx_register_ops()
@@ -123,7 +121,7 @@ def 
test_rust_operators_are_registered_by_their_library(define_ops_library):
         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("tirx.example_hint")
+        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.
@@ -138,23 +136,14 @@ def 
test_rust_operators_are_registered_by_their_library(define_ops_library):
         else:
             raise AssertionError("a float bound passed validation")
 
-        # The TVMScript builders, written in Python as those of the C++ 
operators are in
-        # `tirx/script/ir_builder/op.py`, under the names the printer gives 
the calls.
-        def example_clamp(index, bound):
-            return ir.Call(clamp, [index, bound])
+        # 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
 
-        @auto_emit_void_call
-        def example_mark():
-            return ir.Call(mark, [])
-
-        def example_hint(value, attrs=None):
-            return ir.Call(hint, [value], attrs=attrs)
-
-        def example_loop(extent):
-            return T.region(loop, [extent])
-
-        for builder in (example_clamp, example_mark, example_hint, 
example_loop):
-            setattr(ir_builder, builder.__name__, builder)
+        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,
@@ -163,14 +152,14 @@ def 
test_rust_operators_are_registered_by_their_library(define_ops_library):
         def func(i: T.int32, n: T.int32):
             T.evaluate(T.example_clamp(i + 1, n))
             T.example_mark()
-            T.evaluate(T.example_hint(i, attrs={"note": "hot"}))
+            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 'T.example_hint(i, attrs={"note": "hot"})' 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()
diff --git a/tests/python/tirx/script/test_expose_script_op.py 
b/tests/python/tirx/script/test_expose_script_op.py
new file mode 100644
index 000000000..ece863efe
--- /dev/null
+++ b/tests/python/tirx/script/test_expose_script_op.py
@@ -0,0 +1,92 @@
+# 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.
+"""TVMScript builders generated for registered operators."""
+
+import pytest
+import tvm_ffi
+
+import tvm_tirx
+from tvm_tirx.ir import Call, Op, PrimType, 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
+
+
+def define(name, args, result):
+    """Register an operator once per process, as the registry keeps operators 
for good."""
+    if name not in Op.list_op_names():
+        tvm_ffi.get_global_func("ir.RegisterOp")(name, f"Test operator 
{name}.")
+        Op.get(name).set_signature(args)
+        register_op_attr(name, "TFixedReturnType", PrimType(result))
+    return Op.get(name)
+
+
[email protected]
+def exposed():
+    """The names a test exposes on T, which are removed after it."""
+    names = []
+    yield names
+    for name in names:
+        vars(builder).pop(name, None)
+        parser._NAMESPACES.pop(name, None)
+
+
+def test_builders_round_trip_calls_under_every_root(exposed):
+    exposed += ["test_script_scale", "test_script_mark", "test_script_ns", 
"test_script_twice"]
+    exposed += ["test_script_root", "test_script_bare"]
+    scale = define("tirx.test_script_scale", ["x", "factor"], "int32")
+    mark = define("tirx.test_script_mark", ["tag"], "void")
+    touch = define("tirx.test_script_ns.touch", ["x"], "void")
+    twice = define("ir.test_script_twice", ["x"], "int32")
+    other = define("test_script_root.sub.op", ["x"], "void")
+    bare = define("test_script_bare", ["x"], "void")
+    for op in (scale, mark, touch, twice, other, bare):
+        T.expose_script_op(op.name)
+    test_script_root, test_script_bare = parser.test_script_root, 
parser.test_script_bare
+    assert T.expose_script_op(scale.name) is T.test_script_scale
+    assert T.test_script_scale.__doc__ == scale.doc
+    assert_structural_equal(T.test_script_scale(x=1, factor=2), Call(scale, 
[1, 2]))
+
+    @T.function
+    def func(i: T.int32):
+        T.evaluate(T.test_script_scale(i, 2))
+        T.test_script_mark(i)
+        T.test_script_ns.touch(i)
+        T.evaluate(T.test_script_twice(i))
+        test_script_root.sub.op(i)
+        test_script_bare(i)
+
+    text = func.script()
+    assert "T.test_script_ns.touch(i)" in text, text
+    assert "T.test_script_twice(i)" in text, text
+    assert "test_script_root.sub.op(i)" in text, text
+    assert "\n    test_script_bare(i)" in text, text
+    assert_structural_equal(tvm_tirx.script.parse(text, extra_vars={"T": T}), 
func)
+
+
+def test_existing_names_keep_their_values(exposed):
+    exposed += ["test_script_written"]
+    written = define("tirx.test_script_written", ["x"], "int32")
+
+    def test_script_written(x):
+        return Call(written, [x])
+
+    builder.test_script_written = test_script_written
+    assert T.expose_script_op(written.name) is test_script_written
+    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")

Reply via email to