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

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


The following commit(s) were added to refs/heads/main by this push:
     new 00cabd2a6d [REFACTOR][PYTHON] Use the native FFI object protocol 
(#20726)
00cabd2a6d is described below

commit 00cabd2a6df5118a5c57651ab5d2613674a03cae
Author: Tianqi Chen <[email protected]>
AuthorDate: Wed Oct 14 13:34:14 2026 -0400

    [REFACTOR][PYTHON] Use the native FFI object protocol (#20726)
    
    Deferred expressions, TE tensor slices, and lazy enum values now expose
    `__tvm_ffi_object__`, the native FFI object protocol. Script builders
    and Relax type-argument helpers use that hook, and the obsolete
    `tvm.runtime.ObjectConvertible` export and `object_generic.py` shim are
    removed.
    
    This migrates TVM consumers before removal of the legacy protocol in
    TVM-FFI.
---
 python/tvm/ir/_overload_prim_expr.py               |  10 +-
 python/tvm/relax/op/base.py                        |  10 +-
 python/tvm/relax/script/ir_builder/op.py           |  17 ++-
 python/tvm/runtime/__init__.py                     |   1 -
 python/tvm/runtime/object_generic.py               |  21 ----
 python/tvm/script/ir_builder/stmt.py               |   6 +-
 python/tvm/te/tensor.py                            |  80 ++++++-------
 python/tvm/tirx/expr.py                            |   5 +-
 python/tvm/tirx/script/ir_builder/op.py            |  26 ++--
 python/tvm/tirx/tile/instruction.py                |   4 +-
 .../python/runtime/test_runtime_object_protocol.py | 132 +++++++++++++++++++++
 tests/python/script/test_basic_usage.py            |   2 +-
 tests/python/sym/test_sym_rewrite_simplify.py      |   6 +-
 tests/python/te/test_te_tensor.py                  |   2 +-
 14 files changed, 213 insertions(+), 109 deletions(-)

diff --git a/python/tvm/ir/_overload_prim_expr.py 
b/python/tvm/ir/_overload_prim_expr.py
index d0fe6df594..f30d3c52f2 100644
--- a/python/tvm/ir/_overload_prim_expr.py
+++ b/python/tvm/ir/_overload_prim_expr.py
@@ -18,7 +18,7 @@
 
 from tvm.ir.location import UNKNOWN_LOC, Location
 
-from ..runtime import DataTypeCode, ObjectConvertible
+from ..runtime import DataTypeCode
 from ._constant import const
 from .expr import Expr, TensorRegion, is_prim_expr
 from .prim import _ffi_api as _prim_ffi_api
@@ -246,7 +246,7 @@ class ExprOp:
         return _prim_ffi_api._cast(self, dtype, loc)  # type: ignore
 
 
-class EqualOp(ObjectConvertible, ExprOp):
+class EqualOp(ExprOp):
     """Deferred equal operator.
 
     This is used to support sugar that a == b can either
@@ -280,7 +280,7 @@ class EqualOp(ObjectConvertible, ExprOp):
     def __bool__(self) -> bool:
         return self.__nonzero__()
 
-    def asobject(self) -> Expr:
+    def __tvm_ffi_object__(self) -> Expr:
         """Convert object."""
         return _prim_ffi_api._OpEQ(self.a, self.b, self.loc)  # type: ignore
 
@@ -292,7 +292,7 @@ class EqualOp(ObjectConvertible, ExprOp):
         return f"EqualOp({self.a!r}, {self.b!r})"
 
 
-class NotEqualOp(ObjectConvertible, ExprOp):
+class NotEqualOp(ExprOp):
     """Deferred NE operator.
 
     This is used to support sugar that a != b can either
@@ -326,7 +326,7 @@ class NotEqualOp(ObjectConvertible, ExprOp):
     def __bool__(self) -> bool:
         return self.__nonzero__()
 
-    def asobject(self) -> Expr:
+    def __tvm_ffi_object__(self) -> Expr:
         """Convert object."""
         return _prim_ffi_api._OpNE(self.a, self.b, self.loc)  # type: ignore
 
diff --git a/python/tvm/relax/op/base.py b/python/tvm/relax/op/base.py
index 688aa08117..8f0125c7cd 100644
--- a/python/tvm/relax/op/base.py
+++ b/python/tvm/relax/op/base.py
@@ -27,7 +27,7 @@ from tvm.ir import Attrs, Call, Op
 from tvm.ir.attrs import make_node as _make_attrs
 from tvm.ir.location import UNKNOWN_LOC, Location
 from tvm.ir.op import _make_op_api
-from tvm.runtime import DataTypeCode, Object, ObjectConvertible
+from tvm.runtime import DataTypeCode, Object
 
 from ..expr import Expr, ExternFunc, GlobalVar, Var
 from ..type import Type
@@ -356,8 +356,8 @@ def call_builtin_with_ctx(
         ty_args = [
             value()
             if callable(value) and not isinstance(value, Type)
-            else value.asobject()
-            if isinstance(value, ObjectConvertible)
+            else value.__tvm_ffi_object__()
+            if hasattr(value, "__tvm_ffi_object__")
             else value
             for value in ty_args
         ]
@@ -779,8 +779,8 @@ def call_pure_packed(
         (
             ty()
             if callable(ty) and not isinstance(ty, Type)
-            else ty.asobject()
-            if isinstance(ty, ObjectConvertible)
+            else ty.__tvm_ffi_object__()
+            if hasattr(ty, "__tvm_ffi_object__")
             else ty
         )
         for ty in ty_args
diff --git a/python/tvm/relax/script/ir_builder/op.py 
b/python/tvm/relax/script/ir_builder/op.py
index fea36c5faf..0c7370501b 100644
--- a/python/tvm/relax/script/ir_builder/op.py
+++ b/python/tvm/relax/script/ir_builder/op.py
@@ -24,8 +24,6 @@ import numbers as _numbers
 from types import SimpleNamespace as _Namespace
 from typing import Any
 
-import tvm_ffi as _ffi
-
 import tvm
 from tvm import ir as _ir
 from tvm import relax as _relax
@@ -236,7 +234,6 @@ from tvm.relax.op import to_vdevice as _to_vdevice
 from tvm.relax.op.builtin import stop_lift_params
 from tvm.relax.type import Type
 from tvm.relax.utils import convert_to_expr
-from tvm.runtime import ObjectConvertible
 from tvm.script.ir_builder import base as _base
 
 from . import distributed as dist  # noqa: F401
@@ -316,8 +313,8 @@ def call_packed(
         (
             ty()
             if callable(ty) and not isinstance(ty, _ir.Type)
-            else ty.asobject()
-            if isinstance(ty, ObjectConvertible)
+            else ty.__tvm_ffi_object__()
+            if hasattr(ty, "__tvm_ffi_object__")
             else ty
         )
         for ty in ty_args
@@ -487,15 +484,17 @@ def select(condition, true_value, false_value):
 
 def if_then_else_(condition: Any, true_value: Any, false_value: Any) -> Any:
     """Implements 
:func:`tvm.script.ir_builder.parser_protocol.if_then_else_`."""
-    if isinstance(condition, _ffi.ObjectConvertible):
-        condition = condition.asobject()
+    if hasattr(condition, "__tvm_ffi_object__"):
+        condition = condition.__tvm_ffi_object__()
     if not isinstance(condition, _ir.Expr):
         return true_value if condition else false_value
     true_value = (
-        true_value.asobject() if isinstance(true_value, 
_ffi.ObjectConvertible) else true_value
+        true_value.__tvm_ffi_object__() if hasattr(true_value, 
"__tvm_ffi_object__") else true_value
     )
     false_value = (
-        false_value.asobject() if isinstance(false_value, 
_ffi.ObjectConvertible) else false_value
+        false_value.__tvm_ffi_object__()
+        if hasattr(false_value, "__tvm_ffi_object__")
+        else false_value
     )
     if _ir.is_prim_expr(condition) and all(
         _ir.is_prim_expr(value)
diff --git a/python/tvm/runtime/__init__.py b/python/tvm/runtime/__init__.py
index 552dfd426a..b57d3af1b2 100644
--- a/python/tvm/runtime/__init__.py
+++ b/python/tvm/runtime/__init__.py
@@ -21,7 +21,6 @@ from tvm_ffi import convert, Object
 from tvm_ffi._dtype import dtype as DataType, DataTypeCode
 
 # class exposures
-from .object_generic import ObjectConvertible
 from .device import Device
 from .memory_scope import MemoryScope
 from ._tensor import Tensor, tensor, empty
diff --git a/python/tvm/runtime/object_generic.py 
b/python/tvm/runtime/object_generic.py
deleted file mode 100644
index 14f54dc82d..0000000000
--- a/python/tvm/runtime/object_generic.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# 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.
-# ruff: noqa: F401
-"""Common implementation of object generic related logic"""
-
-# pylint: disable=unused-import, invalid-name
-from tvm_ffi import ObjectConvertible
diff --git a/python/tvm/script/ir_builder/stmt.py 
b/python/tvm/script/ir_builder/stmt.py
index f99043ee69..ca1265b41d 100644
--- a/python/tvm/script/ir_builder/stmt.py
+++ b/python/tvm/script/ir_builder/stmt.py
@@ -28,8 +28,6 @@ from collections.abc import Sequence
 from functools import partial as _partial
 from typing import Any
 
-import tvm_ffi as _ffi
-
 import tvm
 from tvm import ir as _ir
 from tvm.ir import Expr, TensorRegion, Type, Var
@@ -44,8 +42,8 @@ _Loc = _base.LocationEntry | _ir.Location | None
 
 
 def _as_expr(value):
-    if isinstance(value, _ffi.ObjectConvertible):
-        value = value.asobject()
+    if hasattr(value, "__tvm_ffi_object__"):
+        value = value.__tvm_ffi_object__()
     if isinstance(value, _ir.Expr):
         return value
     if isinstance(value, str):
diff --git a/python/tvm/te/tensor.py b/python/tvm/te/tensor.py
index 2899f50f62..47253d2a79 100644
--- a/python/tvm/te/tensor.py
+++ b/python/tvm/te/tensor.py
@@ -19,19 +19,19 @@
 # pylint: disable=invalid-name
 import tvm_ffi
 
-from tvm.ir import OpaqueExpr, SeqStmt, const, is_prim_expr
+from tvm.ir import Expr, OpaqueExpr, SeqStmt, const, is_prim_expr
 from tvm.ir.location import UNKNOWN_LOC, Location
-from tvm.runtime import Object, ObjectConvertible
+from tvm.runtime import Object
 from tvm.tirx import expr as _expr
 
 from . import _ffi_api, _te_tensor_overload
 
 
 def _as_scalar_operand(value):
-    return value.asobject() if isinstance(value, TensorSlice) else value
+    return value.__tvm_ffi_object__() if isinstance(value, TensorSlice) else 
value
 
 
-class TensorSlice(ObjectConvertible):
+class TensorSlice:
     """Auxiliary data structure for enable slicing syntax from tensor."""
 
     def __init__(self, tensor, indices):
@@ -45,7 +45,7 @@ class TensorSlice(ObjectConvertible):
             indices = (indices,)
         return TensorSlice(self.tensor, self.indices + indices)
 
-    def asobject(self):
+    def __tvm_ffi_object__(self) -> Expr:
         """Convert slice to object."""
         return self.tensor.__call__(*self.indices)
 
@@ -62,139 +62,139 @@ class TensorSlice(ObjectConvertible):
         result = _te_tensor_overload.__add__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__add__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__add__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __radd__(self, other):
         result = _te_tensor_overload.__radd__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__radd__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__radd__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __sub__(self, other):
         result = _te_tensor_overload.__sub__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__sub__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__sub__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rsub__(self, other):
         result = _te_tensor_overload.__rsub__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__rsub__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rsub__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __mul__(self, other):
         result = _te_tensor_overload.__mul__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__mul__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__mul__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rmul__(self, other):
         result = _te_tensor_overload.__rmul__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__rmul__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rmul__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __div__(self, other):
         result = _te_tensor_overload.__div__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__div__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__div__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rdiv__(self, other):
         result = _te_tensor_overload.__rdiv__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__rdiv__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rdiv__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __truediv__(self, other):
         result = _te_tensor_overload.__truediv__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__truediv__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__truediv__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rtruediv__(self, other):
         result = _te_tensor_overload.__rtruediv__(self, other)
         if result is not NotImplemented:
             return result
-        return _expr.ExprOp.__rtruediv__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rtruediv__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __floordiv__(self, other):
-        return _expr.ExprOp.__floordiv__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__floordiv__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rfloordiv__(self, other):
-        return _expr.ExprOp.__rfloordiv__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rfloordiv__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __mod__(self, other):
-        return _expr.ExprOp.__mod__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__mod__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rmod__(self, other):
-        return _expr.ExprOp.__rmod__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rmod__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __neg__(self):
-        return _expr.ExprOp.__neg__(self.asobject())
+        return _expr.ExprOp.__neg__(self.__tvm_ffi_object__())
 
     def __lshift__(self, other):
-        return _expr.ExprOp.__lshift__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__lshift__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rlshift__(self, other):
-        return _expr.ExprOp.__rlshift__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rlshift__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rshift__(self, other):
-        return _expr.ExprOp.__rshift__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rshift__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rrshift__(self, other):
-        return _expr.ExprOp.__rrshift__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rrshift__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __and__(self, other):
-        return _expr.ExprOp.__and__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__and__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rand__(self, other):
-        return _expr.ExprOp.__rand__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rand__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __or__(self, other):
-        return _expr.ExprOp.__or__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__or__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __ror__(self, other):
-        return _expr.ExprOp.__ror__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__ror__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __xor__(self, other):
-        return _expr.ExprOp.__xor__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__xor__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __rxor__(self, other):
-        return _expr.ExprOp.__rxor__(self.asobject(), 
_as_scalar_operand(other))
+        return _expr.ExprOp.__rxor__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __invert__(self):
-        return _expr.ExprOp.__invert__(self.asobject())
+        return _expr.ExprOp.__invert__(self.__tvm_ffi_object__())
 
     def __lt__(self, other):
-        return _expr.ExprOp.__lt__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__lt__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __le__(self, other):
-        return _expr.ExprOp.__le__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__le__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __eq__(self, other):
-        return _expr.ExprOp.__eq__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__eq__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __ne__(self, other):
-        return _expr.ExprOp.__ne__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__ne__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __gt__(self, other):
-        return _expr.ExprOp.__gt__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__gt__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __ge__(self, other):
-        return _expr.ExprOp.__ge__(self.asobject(), _as_scalar_operand(other))
+        return _expr.ExprOp.__ge__(self.__tvm_ffi_object__(), 
_as_scalar_operand(other))
 
     def __nonzero__(self):
-        return _expr.ExprOp.__nonzero__(self.asobject())
+        return _expr.ExprOp.__nonzero__(self.__tvm_ffi_object__())
 
     def __bool__(self):
         return self.__nonzero__()
 
     def equal(self, other, loc: Location = UNKNOWN_LOC):
-        return _expr.ExprOp.equal(self.asobject(), _as_scalar_operand(other), 
loc)
+        return _expr.ExprOp.equal(self.__tvm_ffi_object__(), 
_as_scalar_operand(other), loc)
 
     def astype(self, dtype, loc: Location = UNKNOWN_LOC):
-        return _expr.ExprOp.astype(self.asobject(), dtype, loc)
+        return _expr.ExprOp.astype(self.__tvm_ffi_object__(), dtype, loc)
 
 
 class TensorOpBase:
diff --git a/python/tvm/tirx/expr.py b/python/tvm/tirx/expr.py
index 47b234d9b6..12ebc8e596 100644
--- a/python/tvm/tirx/expr.py
+++ b/python/tvm/tirx/expr.py
@@ -78,10 +78,9 @@ from tvm.ir.prim.expr import (  # noqa: F401
     Shuffle,
     Sub,
 )
-from tvm.runtime import ObjectConvertible
 
 
-class IntImmEnum(ObjectConvertible):
+class IntImmEnum:
     """Lazily evaluate an IntImm in case
     the constructor is not available in runtime.
 
@@ -98,7 +97,7 @@ class IntImmEnum(ObjectConvertible):
         self.value = value
         self.loc = loc
 
-    def asobject(self) -> "IntImm":
+    def __tvm_ffi_object__(self) -> "IntImm":
         """Convert object."""
         return IntImm("int32", self.value, self.loc)  # type: ignore
 
diff --git a/python/tvm/tirx/script/ir_builder/op.py 
b/python/tvm/tirx/script/ir_builder/op.py
index 6f36e164b3..2322c1b0e2 100644
--- a/python/tvm/tirx/script/ir_builder/op.py
+++ b/python/tvm/tirx/script/ir_builder/op.py
@@ -21,10 +21,6 @@ from __future__ import annotations
 import functools
 from typing import Any
 
-import tvm_ffi as _ffi
-
-# isort: off
-# isort: on
 from tvm import ir
 from tvm import ir as _ir
 from tvm import tirx as _tir
@@ -608,7 +604,7 @@ def logical_and(*values):
     Parameters
     ----------
     values : Expr or Python value
-        One or more operands. Object-convertible values are normalized first.
+        One or more operands. Values implementing ``__tvm_ffi_object__`` are 
normalized first.
         Host pairs follow Python logical operations; IR pairs use scalar 
logical
         or vector bitwise operations according to their types.
 
@@ -625,7 +621,8 @@ def logical_and(*values):
     if not values:
         raise TypeError("logical_and requires at least one operand")
     values = [
-        value.asobject() if isinstance(value, _ffi.ObjectConvertible) else 
value for value in values
+        value.__tvm_ffi_object__() if hasattr(value, "__tvm_ffi_object__") 
else value
+        for value in values
     ]
     result = values[0]
     for value in values[1:]:
@@ -643,7 +640,7 @@ def logical_or(*values):
     Parameters
     ----------
     values : Expr or Python value
-        One or more operands. Object-convertible values are normalized first.
+        One or more operands. Values implementing ``__tvm_ffi_object__`` are 
normalized first.
         Host pairs follow Python logical operations; IR pairs use scalar 
logical
         or vector bitwise operations according to their types.
 
@@ -660,7 +657,8 @@ def logical_or(*values):
     if not values:
         raise TypeError("logical_or requires at least one operand")
     values = [
-        value.asobject() if isinstance(value, _ffi.ObjectConvertible) else 
value for value in values
+        value.__tvm_ffi_object__() if hasattr(value, "__tvm_ffi_object__") 
else value
+        for value in values
     ]
     result = values[0]
     for value in values[1:]:
@@ -678,7 +676,7 @@ def logical_not(value):
     Parameters
     ----------
     value : Expr or Python value
-        Operand to negate. Object-convertible values are normalized first;
+        Operand to negate. Values implementing ``__tvm_ffi_object__`` are 
normalized first;
         IR expressions use primitive Not and host values use Python not.
 
     Returns
@@ -686,8 +684,8 @@ def logical_not(value):
     result : Expr or bool
         The logical negation without testing an IR expression as a Python bool.
     """
-    if isinstance(value, _ffi.ObjectConvertible):
-        value = value.asobject()
+    if hasattr(value, "__tvm_ffi_object__"):
+        value = value.__tvm_ffi_object__()
     return _tir.Not(value) if isinstance(value, _ir.Expr) else not value
 
 
@@ -697,7 +695,7 @@ def select(condition, true_value, false_value):
     Parameters
     ----------
     condition : Expr or Python value
-        Scalar IR condition or host truth value. Object-convertible conditions
+        Scalar IR condition or host truth value. Conditions implementing 
``__tvm_ffi_object__``
         are normalized before selection.
     true_value : Expr or Python value
         Value selected when the condition is true.
@@ -714,8 +712,8 @@ def select(condition, true_value, false_value):
     Both Python value arguments are constructed before this call. The generated
     IR conditional evaluates only its selected arm at runtime.
     """
-    if isinstance(condition, _ffi.ObjectConvertible):
-        condition = condition.asobject()
+    if hasattr(condition, "__tvm_ffi_object__"):
+        condition = condition.__tvm_ffi_object__()
     if not isinstance(condition, _ir.Expr):
         return true_value if condition else false_value
     return _tir.if_then_else(condition, true_value, false_value)
diff --git a/python/tvm/tirx/tile/instruction.py 
b/python/tvm/tirx/tile/instruction.py
index 90fc08d7ff..c24b358c6c 100644
--- a/python/tvm/tirx/tile/instruction.py
+++ b/python/tvm/tirx/tile/instruction.py
@@ -204,8 +204,8 @@ def _expr(value):
         return Tuple([_expr(v) for v in value])
     if isinstance(value, str):
         return StringImm(value)
-    if hasattr(value, "asobject"):
-        return value.asobject()
+    if hasattr(value, "__tvm_ffi_object__"):
+        return value.__tvm_ffi_object__()
     return const(value)
 
 
diff --git a/tests/python/runtime/test_runtime_object_protocol.py 
b/tests/python/runtime/test_runtime_object_protocol.py
new file mode 100644
index 0000000000..1f07859b4a
--- /dev/null
+++ b/tests/python/runtime/test_runtime_object_protocol.py
@@ -0,0 +1,132 @@
+# 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.
+"""Native FFI object conversion for TVM wrappers and script helpers."""
+
+import pytest
+import tvm_ffi
+
+import tvm
+import tvm.testing
+from tvm import ir, relax, te, tirx
+from tvm.relax.script.ir_builder import op as relax_op
+from tvm.script.ir_builder.stmt import _as_expr
+from tvm.tirx.script.ir_builder import op as tir_op
+from tvm.tirx.tile.instruction import _expr
+
+
+class ObjectWrapper:
+    def __init__(self, value):
+        self.value = value
+
+    def __tvm_ffi_object__(self) -> tvm_ffi.Object:
+        return self.value
+
+
+def test_object_protocol_identity():
+    value = ir.Var("x", "int32")
+    wrapped = ObjectWrapper(value)
+    assert tvm_ffi.convert(wrapped) is value
+    echo = tvm_ffi.convert_func(lambda x: x)
+    assert echo(wrapped).same_as(value)
+    assert tvm_ffi.convert([wrapped])[0].same_as(value)
+    assert tvm_ffi.convert({"value": [wrapped]})["value"][0].same_as(value)
+    assert _as_expr(wrapped) is value
+    assert _expr(wrapped) is value
+
+
[email protected]("unequal", [False, True])
+def test_deferred_comparison_protocol(unequal):
+    x, y = ir.Var("x", "int32"), ir.Var("y", "int32")
+    comparison = x != y if unequal else x == y
+    assert bool(comparison) is unequal
+    assert bool(x != x if unequal else x == x) is not unequal
+    result = tvm_ffi.convert(comparison)
+    assert isinstance(result, tirx.NE if unequal else tirx.EQ)
+    assert result.a.same_as(x)
+    assert result.b.same_as(y)
+    echo = tvm_ffi.convert_func(lambda x: x)
+    tvm.ir.assert_structural_equal(echo(comparison), result)
+    tvm.ir.assert_structural_equal(tvm_ffi.convert([comparison])[0], result)
+    assert isinstance(tir_op.logical_not(comparison), tirx.Not)
+    assert isinstance(tir_op.logical_and(comparison, True), tirx.And)
+    assert isinstance(tir_op.logical_or(comparison, False), tirx.Or)
+    tvm.ir.assert_structural_equal(_as_expr(comparison), result)
+    tvm.ir.assert_structural_equal(_expr(comparison), result)
+
+
+def test_tensor_slice_protocol():
+    tensor = te.placeholder((2, 3), name="A")
+    sliced = tensor[0][1]
+    result = tvm_ffi.convert(sliced)
+    assert result.op.same_as(ir.Op.get("te.tensor_load"))
+    assert result.args[0].same_as(tensor)
+    assert list(result.args[1:]) == [0, 1]
+    echo = tvm_ffi.convert_func(lambda x: x)
+    tvm.ir.assert_structural_equal(echo(sliced), result)
+    tvm.ir.assert_structural_equal(tvm_ffi.convert([sliced])[0], result)
+
+
+def test_enum_protocol():
+    enum = tirx.CallEffectKind.Pure
+    result = tvm_ffi.convert(enum)
+    assert isinstance(result, tirx.IntImm)
+    assert result.value == 1
+    assert result.ty == ir.PrimType("int32")
+    echo = tvm_ffi.convert_func(lambda x: x)
+    tvm.ir.assert_structural_equal(echo(enum), result)
+    tvm.ir.assert_structural_equal(tvm_ffi.convert([enum])[0], result)
+
+
+def test_builder_protocol_preserves_host_values():
+    host = object()
+    assert tir_op.select(True, host, None) is host
+    assert relax_op.if_then_else_(True, host, None) is host
+    assert tir_op.logical_and(True, host) is host
+    assert tir_op.logical_or(False, host) is host
+    x = ir.Var("x", "int32")
+    condition = ObjectWrapper(tvm_ffi.convert(x == 0))
+    true_value = ObjectWrapper(x)
+    tvm.ir.assert_structural_equal(
+        tir_op.select(condition, true_value, 0), 
tir_op.select(condition.value, x, 0)
+    )
+    tvm.ir.assert_structural_equal(
+        relax_op.if_then_else_(condition, true_value, 0),
+        relax_op.if_then_else_(condition.value, x, 0),
+    )
+
+
[email protected](
+    "make_call", [relax.op.call_builtin_with_ctx, relax.op.call_pure_packed, 
relax_op.call_packed]
+)
+def test_wrapped_type_arguments(make_call):
+    ty = ir.PrimType("int32")
+    call = make_call("custom", (), ty_args=[ObjectWrapper(ty)])
+    assert call.ty_args[0].same_as(ty)
+    call = make_call("custom", (), ty_args=[lambda: ty])
+    assert call.ty_args[0].same_as(ty)
+
+
+def test_invalid_object_protocol_result():
+    echo = tvm_ffi.convert_func(lambda x: x)
+    with pytest.raises(TypeError):
+        echo(ObjectWrapper(1))
+    with pytest.raises(TypeError):
+        tvm_ffi.convert([ObjectWrapper(1)])
+
+
+if __name__ == "__main__":
+    tvm.testing.main()
diff --git a/tests/python/script/test_basic_usage.py 
b/tests/python/script/test_basic_usage.py
index 78fb654bcd..3600886350 100644
--- a/tests/python/script/test_basic_usage.py
+++ b/tests/python/script/test_basic_usage.py
@@ -471,7 +471,7 @@ def 
test_host_equality_result_is_not_arbitrarily_converted(primitive_language):
     M = primitive_language.M
 
     class CustomEquality(EqualOp):
-        def asobject(self):
+        def __tvm_ffi_object__(self):
             raise AssertionError("custom equality subclasses are host values")
 
     result = CustomEquality(ir.Var("x", "int32"), 0)
diff --git a/tests/python/sym/test_sym_rewrite_simplify.py 
b/tests/python/sym/test_sym_rewrite_simplify.py
index 6cebaff321..ded9372e92 100644
--- a/tests/python/sym/test_sym_rewrite_simplify.py
+++ b/tests/python/sym/test_sym_rewrite_simplify.py
@@ -34,9 +34,9 @@ from tvm.tirx import truncmod as tmod
 class TestCase:
     def __init__(self, before, expected, preconditions=None):
         if isinstance(before, tirx.expr.EqualOp):
-            before = before.asobject()
+            before = before.__tvm_ffi_object__()
         if isinstance(expected, tirx.expr.EqualOp):
-            expected = expected.asobject()
+            expected = expected.__tvm_ffi_object__()
 
         self.before = self._convert(before)
         self.expected = self._convert(expected)
@@ -45,7 +45,7 @@ class TestCase:
     @staticmethod
     def _convert(expr):
         if isinstance(expr, tirx.expr.EqualOp):
-            return expr.asobject()
+            return expr.__tvm_ffi_object__()
         elif isinstance(expr, int):
             return T.int32(expr)
         elif isinstance(expr, float):
diff --git a/tests/python/te/test_te_tensor.py 
b/tests/python/te/test_te_tensor.py
index 73dc5d0926..555c9b6790 100644
--- a/tests/python/te/test_te_tensor.py
+++ b/tests/python/te/test_te_tensor.py
@@ -41,7 +41,7 @@ def test_tensor():
     assert T.op.output(0).__hash__() == T.__hash__()
     d = {T.op.output(0): 1}
     assert d[T] == 1
-    load = T[0][0][0].asobject()
+    load = T[0][0][0].__tvm_ffi_object__()
     assert isinstance(load, tvm.ir.Call)
     assert load.op.same_as(tvm.ir.Op.get("te.tensor_load"))
     assert load.args[0].same_as(T)

Reply via email to