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 bbd115877d [REFACTOR][IR] Consolidate expression operator overloading 
into the base layer (#20248)
bbd115877d is described below

commit bbd115877dc9faf6bdc81b8d04332e468022ad35
Author: Tianqi Chen <[email protected]>
AuthorDate: Tue Sep 1 07:43:39 2026 -0400

    [REFACTOR][IR] Consolidate expression operator overloading into the base 
layer (#20248)
    
    ## Rationale
    
    Python expression operators should have one shared base-layer protocol
    instead of parallel TIRx and Relax mixins. A single surface keeps proxy
    realization and type-directed dispatch consistent while leaving dialect
    implementations behind their existing hooks.
    
    ## Changes
    
    - expose public `ExprOperand` and `ExprWithOp` base-layer classes
    - route TIRx and Relax expression nodes through the shared operator
    surface
    - preserve unresolved Relax operator chains and TE equality with
    primitive expressions
    - propagate `NotImplemented` for unsupported binary operators so Python
    reflected dispatch remains available
    - keep subscription and `SubscriptProxy` behavior outside this change
---
 python/tvm/ir/__init__.py                      |   1 +
 python/tvm/ir/expr.py                          |  94 +++++++++++------
 python/tvm/ir/function.py                      |   7 +-
 python/tvm/relax/expr.py                       | 136 +++++--------------------
 python/tvm/relax/op/__init__.py                |  25 ++++-
 python/tvm/te/tensor.py                        |   4 +-
 python/tvm/tirx/expr.py                        |  30 ++----
 python/tvm/tirx/op.py                          |   4 +-
 tests/python/relax/test_expr.py                |  52 ++++++++++
 tests/python/tirx-base/test_tir_constructor.py |  29 ++++++
 10 files changed, 211 insertions(+), 171 deletions(-)

diff --git a/python/tvm/ir/__init__.py b/python/tvm/ir/__init__.py
index 8a2d241cd1..412bd89061 100644
--- a/python/tvm/ir/__init__.py
+++ b/python/tvm/ir/__init__.py
@@ -38,6 +38,7 @@ from .expr import (
     Call,
     Expr,
     ExprOperand,
+    ExprWithOp,
     GlobalVar,
     OpaqueExpr,
     Range,
diff --git a/python/tvm/ir/expr.py b/python/tvm/ir/expr.py
index 14d467263b..3cbd19c356 100644
--- a/python/tvm/ir/expr.py
+++ b/python/tvm/ir/expr.py
@@ -149,97 +149,113 @@ class ExprOperand:
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__add__(self, other)
-        return _tensor_expr_overload.__add__(self, other)
+        result = _tensor_expr_overload.__add__(self, other)
+        return result
 
     def __radd__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__radd__(self, other)
-        return _tensor_expr_overload.__radd__(self, other)
+        result = _tensor_expr_overload.__radd__(self, other)
+        return result
 
     def __sub__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__sub__(self, other)
-        return _tensor_expr_overload.__sub__(self, other)
+        result = _tensor_expr_overload.__sub__(self, other)
+        return result
 
     def __rsub__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__rsub__(self, other)
-        return _tensor_expr_overload.__rsub__(self, other)
+        result = _tensor_expr_overload.__rsub__(self, other)
+        return result
 
     def __mul__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__mul__(self, other)
-        return _tensor_expr_overload.__mul__(self, other)
+        result = _tensor_expr_overload.__mul__(self, other)
+        return result
 
     def __rmul__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__rmul__(self, other)
-        return _tensor_expr_overload.__rmul__(self, other)
+        result = _tensor_expr_overload.__rmul__(self, other)
+        return result
 
     def __div__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__div__(self, other)
-        return _tensor_expr_overload.__div__(self, other)
+        result = _tensor_expr_overload.__div__(self, other)
+        return result
 
     def __rdiv__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__rdiv__(self, other)
-        return _tensor_expr_overload.__rdiv__(self, other)
+        result = _tensor_expr_overload.__rdiv__(self, other)
+        return result
 
     def __truediv__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__truediv__(self, other)
-        return _tensor_expr_overload.__truediv__(self, other)
+        result = _tensor_expr_overload.__truediv__(self, other)
+        return result
 
     def __rtruediv__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__rtruediv__(self, other)
-        return _tensor_expr_overload.__rtruediv__(self, other)
+        result = _tensor_expr_overload.__rtruediv__(self, other)
+        return result
 
     def __floordiv__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__floordiv__(self, other)
-        return _tensor_expr_overload.__floordiv__(self, other)
+        result = _tensor_expr_overload.__floordiv__(self, other)
+        return result
 
     def __rfloordiv__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__rfloordiv__(self, other)
-        return _tensor_expr_overload.__rfloordiv__(self, other)
+        result = _tensor_expr_overload.__rfloordiv__(self, other)
+        return result
 
     def __mod__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__mod__(self, other)
-        return _tensor_expr_overload.__mod__(self, other)
+        result = _tensor_expr_overload.__mod__(self, other)
+        return result
 
     def __rmod__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__rmod__(self, other)
-        return _tensor_expr_overload.__rmod__(self, other)
+        result = _tensor_expr_overload.__rmod__(self, other)
+        return result
 
     def __pow__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return NotImplemented
-        return _tensor_expr_overload.__pow__(self, other)
+        result = _tensor_expr_overload.__pow__(self, other)
+        return result
 
     def __rpow__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return NotImplemented
-        return _tensor_expr_overload.__rpow__(self, other)
+        result = _tensor_expr_overload.__rpow__(self, other)
+        return result
 
     def __neg__(self):
         self = _realize_operand(self)
@@ -250,7 +266,7 @@ class ExprOperand:
             return result
         result = _tensor_expr_overload.__neg__(self)
         if result is NotImplemented:
-            raise TypeError("Tensor expression overload negative is not 
registered")
+            raise TypeError(f"Operator overloading is not supported for 
expression type {self.ty}")
         return result
 
     def __lshift__(self, other):
@@ -320,19 +336,21 @@ class ExprOperand:
             if result is NotImplemented:
                 raise TypeError("Primitive expression overload __invert__ is 
not registered")
             return result
-        return NotImplemented
+        raise TypeError(f"Operator overloading is not supported for expression 
type {self.ty}")
 
     def __lt__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__lt__(self, other)
-        return _tensor_expr_overload.__lt__(self, other)
+        result = _tensor_expr_overload.__lt__(self, other)
+        return result
 
     def __le__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__le__(self, other)
-        return _tensor_expr_overload.__le__(self, other)
+        result = _tensor_expr_overload.__le__(self, other)
+        return result
 
     def __eq__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
@@ -350,13 +368,15 @@ class ExprOperand:
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__gt__(self, other)
-        return _tensor_expr_overload.__gt__(self, other)
+        result = _tensor_expr_overload.__gt__(self, other)
+        return result
 
     def __ge__(self, other):
         self, other = _realize_operand(self), _realize_operand(other)
         if is_prim_expr(self):
             return _overload_prim_expr.__ge__(self, other)
-        return _tensor_expr_overload.__ge__(self, other)
+        result = _tensor_expr_overload.__ge__(self, other)
+        return result
 
     def __nonzero__(self):
         raise ValueError(
@@ -369,6 +389,8 @@ class ExprOperand:
 
     def equal(self, other, span=None):
         self, other = _realize_operand(self), _realize_operand(other)
+        if not is_prim_expr(self):
+            raise TypeError(f"Operator overloading is not supported for 
expression type {self.ty}")
         result = _overload_prim_expr.equal(self, other, span)
         if result is NotImplemented:
             raise TypeError("Primitive expression overload equal is not 
registered")
@@ -383,20 +405,26 @@ class ExprOperand:
             return result
         result = _tensor_expr_overload.astype(self, dtype, span)
         if result is NotImplemented:
-            raise TypeError("Tensor expression overload astype is not 
registered")
+            raise TypeError(f"Operator overloading is not supported for 
expression type {self.ty}")
         return result
 
+
+class _ExprCallable:
+    """Function-call capability for expression operands that can denote 
functions."""
+
+    __slots__ = ()
+
     def __call__(self, *args, attrs=None):
         self, args = _realize_operand(self), tuple(_realize_operand(arg) for 
arg in args)
         if is_prim_expr(self):
             raise TypeError("A primitive-valued expression cannot be called")
         result = _tensor_expr_overload.__call__(self, *args, attrs=attrs)
         if result is NotImplemented:
-            raise TypeError("Tensor expression overload __call__ is not 
registered")
+            raise TypeError(f"Expression of type {self.ty} cannot be called")
         return result
 
 
-class _ExprWithOp(ExprOperand, Expr, Scriptable):
+class ExprWithOp(ExprOperand, Expr, Scriptable):
     """Common type-directed operator behavior for core expressions."""
 
     __hash__ = Expr.__hash__
@@ -405,7 +433,11 @@ class _ExprWithOp(ExprOperand, Expr, Scriptable):
         return self
 
 
-class SubscriptProxy(ExprOperand, ObjectConvertible):
+class _CallableExprWithOp(_ExprCallable, ExprWithOp):
+    """Common operator behavior for expression nodes that support function 
calls."""
+
+
+class SubscriptProxy(_ExprCallable, ExprOperand, ObjectConvertible):
     """An immutable, lazily-realized subscription of an :class:`Expr`.
 
     Point subscriptions may be chained to accumulate dimensions.  A proxy
@@ -480,7 +512,7 @@ class SubscriptProxy(ExprOperand, ObjectConvertible):
 
 
 @tvm_ffi.register_object("ir.Tuple")
-class Tuple(_ExprWithOp):
+class Tuple(_CallableExprWithOp):
     """Tuple expression that groups several fields together.
 
     Parameters
@@ -513,7 +545,7 @@ class Tuple(_ExprWithOp):
 
 
 @tvm_ffi.register_object("ir.TupleGetItem")
-class TupleGetItem(_ExprWithOp):
+class TupleGetItem(_CallableExprWithOp):
     """Get the index-th item from a tuple.
 
     Parameters
@@ -537,7 +569,7 @@ class TupleGetItem(_ExprWithOp):
 
 
 @tvm_ffi.register_object("ir.TensorLoad")
-class TensorLoad(_ExprWithOp):
+class TensorLoad(_CallableExprWithOp):
     """An indexed load from an expression source.
 
     TensorLoad objects are constructed by a dialect-specific helper that
@@ -555,7 +587,7 @@ class TensorLoad(_ExprWithOp):
 
 
 @tvm_ffi.register_object("ir.Call")
-class Call(_ExprWithOp):
+class Call(_CallableExprWithOp):
     """Core function call node."""
 
     op: Expr
@@ -594,7 +626,7 @@ class Call(_ExprWithOp):
 
 
 @tvm_ffi.register_object("ir.Var")
-class Var(_ExprWithOp):
+class Var(_CallableExprWithOp):
     """A canonical local variable in the IR.
 
     Parameters
diff --git a/python/tvm/ir/function.py b/python/tvm/ir/function.py
index 5bc7e368dc..7a58e4c576 100644
--- a/python/tvm/ir/function.py
+++ b/python/tvm/ir/function.py
@@ -26,7 +26,7 @@ from tvm.runtime import Object
 
 from . import _ffi_api
 from .attrs import DictAttrs
-from .expr import Expr
+from .expr import _CallableExprWithOp
 
 
 class CallingConv(IntEnum):
@@ -38,9 +38,12 @@ class CallingConv(IntEnum):
 
 
 @tvm_ffi.register_object("ir.BaseFunc")
-class BaseFunc(Expr):
+class BaseFunc(_CallableExprWithOp):
     """Base class of all functions."""
 
+    def __bool__(self) -> bool:
+        return True
+
     @property
     def attrs(self):
         """Return the attrs member of the function."""
diff --git a/python/tvm/relax/expr.py b/python/tvm/relax/expr.py
index 51c63e21d1..fed8d1c3bc 100644
--- a/python/tvm/relax/expr.py
+++ b/python/tvm/relax/expr.py
@@ -32,6 +32,7 @@ import tvm.runtime
 from tvm import DataType
 
 from ..ir import BaseFunc, Node, Span
+from ..ir.expr import _CallableExprWithOp
 from ..runtime import Scriptable
 from . import _ffi_api
 
@@ -87,9 +88,15 @@ Type.is_base_of = _relax_type_is_base_of  # type: 
ignore[attr-defined]
 _op_ffi_api = None  # pylint: disable=invalid-name
 
 
-def _binary_op_helper(lhs: "ExprWithOp", rhs: "ExprWithOp", op: Callable) -> 
"ExprWithOp":
+def _is_tensor_or_missing_type(ty: Type) -> bool:
+    return isinstance(ty, tvm.relax.TensorType) or ty.is_missing()
+
+
+def _binary_op_helper(lhs: Expr, rhs: Expr, op: Callable):
     if not isinstance(lhs, Expr):  # type: ignore
         raise ValueError("lhs must be Expr")
+    if not _is_tensor_or_missing_type(lhs.ty):
+        return NotImplemented
     if isinstance(rhs, Expr):  # type: ignore
         return op(lhs, rhs)
     elif isinstance(rhs, Number):
@@ -98,117 +105,14 @@ def _binary_op_helper(lhs: "ExprWithOp", rhs: 
"ExprWithOp", op: Callable) -> "Ex
         raise TypeError(f"type {type(rhs)} not supported")
 
 
-def _binary_rhs_helper(rhs: "ExprWithOp") -> "ExprWithOp":
+def _binary_rhs_helper(rhs: Expr):
     if isinstance(rhs, Number):
         raise TypeError(f"Please convert {rhs} with `const` first")
     raise TypeError(f"type {type(rhs)} not supported")
 
 
-class ExprWithOp(Expr, Scriptable):
-    """Basetype of all relax expressions that defines op overloading."""
-
-    def astype(self, dtype: str | DataType) -> "ExprWithOp":
-        """Cast the content type of the current data to dtype.
-
-        Parameters
-        ----------
-        dtype : str
-            The target data type.
-
-        Note
-        ----
-        This function only works for TensorType Exprs.
-
-        Returns
-        -------
-        result : ExprWithOp
-            The result expression.
-        """
-        return _op_ffi_api.astype(self, dtype)  # type: ignore
-
-    def __neg__(self) -> "ExprWithOp":
-        return _op_ffi_api.negative(self)  # type: ignore
-
-    def __lt__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.less)  # type: ignore
-
-    def __gt__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.greater)  # type: 
ignore
-
-    def __ge__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.greater_equal)  # 
type: ignore
-
-    def __le__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.less_equal)  # type: 
ignore
-
-    # NOTE: Cannot override __eq__ and __ne__, which will influence object 
equal
-
-    def __add__(self, other: Expr) -> "ExprWithOp":
-        if isinstance(self.ty, tvm.relax.TupleType) and isinstance(other, 
tuple):
-            return tuple([*self, *other])
-
-        return _binary_op_helper(self, other, _op_ffi_api.add)  # type: ignore
-
-    def __radd__(self, other: Expr) -> "ExprWithOp":
-        return self.__add__(other)
-
-    def __sub__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.subtract)  # type: 
ignore
-
-    def __rsub__(self, other: Expr) -> "ExprWithOp":
-        return _binary_rhs_helper(other)
-
-    def __mul__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.multiply)  # type: 
ignore
-
-    def __rmul__(self, other: Expr) -> "ExprWithOp":
-        return self.__mul__(other)
-
-    def __truediv__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.divide)  # type: 
ignore
-
-    def __rtruediv__(self, other: Expr) -> "ExprWithOp":
-        return _binary_rhs_helper(other)
-
-    def __floordiv__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.floor_divide)  # 
type: ignore
-
-    def __rfloordiv__(self, other: Expr) -> "ExprWithOp":
-        return _binary_rhs_helper(other)
-
-    def __mod__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.mod)  # type: ignore
-
-    def __rmod__(self, other: Expr) -> "ExprWithOp":
-        return _binary_rhs_helper(other)
-
-    def __pow__(self, other: Expr) -> "ExprWithOp":
-        return _binary_op_helper(self, other, _op_ffi_api.power)  # type: 
ignore
-
-    def __rpow__(self, other: Expr) -> "ExprWithOp":
-        return _binary_rhs_helper(other)
-
-    def __call__(self, *args: list[Expr], attrs: dict[str, Any] | None = None) 
-> "ExprWithOp":
-        """Call the variable (if it represents a function).
-
-        Parameters
-        ----------
-        args: List[Expr]
-            The arguments to the call.
-
-        attr: Optional[Dict[str, object]]
-            The additional attributes to the call.
-
-        Returns
-        -------
-        call: ExprWithOp
-            A call taking the variable as a function.
-        """
-        return tvm.ir.Call(self, args, attrs=attrs)
-
-
 @tvm_ffi.register_object("relax.expr.If")
-class If(ExprWithOp):
+class If(_CallableExprWithOp):
     """A conditional expression in Relax.
 
     Parameters
@@ -231,6 +135,9 @@ class If(ExprWithOp):
     false_branch: Expr
     span: Span | None
 
+    def __bool__(self) -> bool:
+        return True
+
     def __init__(self, cond: Expr, true_branch: Expr, false_branch: Expr, 
span: Span | None = None):
         self.__init_handle_by_constructor__(
             _ffi_api.If,
@@ -247,7 +154,7 @@ TupleGetItem = tvm.ir.TupleGetItem
 
 
 @tvm_ffi.register_object("relax.expr.ShapeExpr")
-class ShapeExpr(ExprWithOp):
+class ShapeExpr(_CallableExprWithOp):
     """A shape expression which allows users to construct a shape containing 
Expr.
 
     Parameters
@@ -277,6 +184,9 @@ class ShapeExpr(ExprWithOp):
     def __len__(self):
         return len(self.values)
 
+    def __bool__(self) -> bool:
+        return len(self) != 0
+
 
 def make_shape(shape: list[Any] | tuple[Any, ...]) -> ShapeExpr:
     if isinstance(shape, list | tuple):
@@ -288,7 +198,7 @@ def make_shape(shape: list[Any] | tuple[Any, ...]) -> 
ShapeExpr:
 
 
 @tvm_ffi.register_object("relax.expr.Constant")
-class Constant(ExprWithOp):
+class Constant(_CallableExprWithOp):
     """Constant Tensor
 
     Parameters
@@ -310,6 +220,9 @@ class Constant(ExprWithOp):
     data: tvm.runtime.Tensor
     span: Span | None
 
+    def __bool__(self) -> bool:
+        return True
+
     def __init__(
         self,
         data: tvm.runtime.Tensor,
@@ -488,13 +401,16 @@ class DataflowBlock(BindingBlock):
 
 
 @tvm_ffi.register_object("relax.expr.SeqExpr")
-class SeqExpr(ExprWithOp):
+class SeqExpr(_CallableExprWithOp):
     """A sequence of binding blocks followed by an expression."""
 
     blocks: list[BindingBlock]
     body: Expr
     span: Span | None
 
+    def __bool__(self) -> bool:
+        return True
+
     def __init__(self, blocks: list[BindingBlock], body: Expr, span: Span | 
None = None) -> None:
         self.__init_handle_by_constructor__(_ffi_api.SeqExpr, blocks, body, 
span)  # type: ignore
 
@@ -639,7 +555,7 @@ class Function(BaseFunc, Scriptable):
 
 
 @tvm_ffi.register_object("relax.expr.ExternFunc")
-class ExternFunc(BaseFunc, ExprWithOp):
+class ExternFunc(BaseFunc):
     """extern function, which represents a PackedFunc."""
 
     global_symbol: String
diff --git a/python/tvm/relax/op/__init__.py b/python/tvm/relax/op/__init__.py
index 21a25e16b4..d8628c806d 100644
--- a/python/tvm/relax/op/__init__.py
+++ b/python/tvm/relax/op/__init__.py
@@ -182,14 +182,29 @@ def _register_op_make():
             return tuple([*lhs, *rhs])
         return expr._binary_op_helper(lhs, rhs, _ffi_api.add)
 
-    def _rhs(_lhs, rhs):
+    def _rhs(lhs, rhs):
+        if not expr._is_tensor_or_missing_type(lhs.ty):
+            return NotImplemented
         return expr._binary_rhs_helper(rhs)
 
-    _tensor_expr_overload.astype = lambda lhs, dtype, _span=None: 
_ffi_api.astype(lhs, dtype)
-    _tensor_expr_overload.__call__ = lambda func, *args, attrs=None: 
expr.tvm.ir.Call(
-        func, args, attrs=attrs
+    def _unary(lhs, op):
+        if not expr._is_tensor_or_missing_type(lhs.ty):
+            return NotImplemented
+        return op(lhs)
+
+    def _call(func, *args, attrs=None):
+        if not (
+            isinstance(func.ty, expr.tvm.ir.FuncType | expr.tvm.relax.FuncType)
+            or func.ty.is_missing()
+        ):
+            return NotImplemented
+        return expr.tvm.ir.Call(func, args, attrs=attrs)
+
+    _tensor_expr_overload.astype = lambda lhs, dtype, _span=None: (
+        _ffi_api.astype(lhs, dtype) if expr._is_tensor_or_missing_type(lhs.ty) 
else NotImplemented
     )
-    _tensor_expr_overload.__neg__ = lambda lhs: _ffi_api.negative(lhs)
+    _tensor_expr_overload.__call__ = _call
+    _tensor_expr_overload.__neg__ = lambda lhs: _unary(lhs, _ffi_api.negative)
     _tensor_expr_overload.__lt__ = lambda lhs, rhs: 
expr._binary_op_helper(lhs, rhs, _ffi_api.less)
     _tensor_expr_overload.__le__ = lambda lhs, rhs: expr._binary_op_helper(
         lhs, rhs, _ffi_api.less_equal
diff --git a/python/tvm/te/tensor.py b/python/tvm/te/tensor.py
index c124e7639c..de42bc714b 100644
--- a/python/tvm/te/tensor.py
+++ b/python/tvm/te/tensor.py
@@ -19,7 +19,7 @@
 # pylint: disable=invalid-name
 import tvm_ffi
 
-from tvm.ir import OpaqueExpr
+from tvm.ir import OpaqueExpr, is_prim_expr
 from tvm.runtime import Object, ObjectConvertible, const
 from tvm.tirx import expr as _expr
 
@@ -271,7 +271,7 @@ class Tensor(OpaqueExpr, TensorOpBase):
 
     def __eq__(self, other):
         if not isinstance(other, Tensor):
-            if isinstance(other, _expr.ExprOp):
+            if isinstance(other, _expr.ExprOp) or is_prim_expr(other):
                 return _expr.EqualOp(self, other)
             return False
         if self.ndim == 0 and other.ndim == 0:
diff --git a/python/tvm/tirx/expr.py b/python/tvm/tirx/expr.py
index d46f231668..142fc1fee4 100644
--- a/python/tvm/tirx/expr.py
+++ b/python/tvm/tirx/expr.py
@@ -414,29 +414,21 @@ class IntImmEnum(ObjectConvertible):
         return IntImm("int32", self.value, self.span)  # type: ignore
 
 
-class ExprWithOp(ExprOp, Expr, Scriptable):
-    """Helper base class to inherit from Expr."""
-
-    # In Python3, We have to explicitly tell interpreter to retain __hash__ if 
we overide __eq__
-    # https://docs.python.org/3.1/reference/datamodel.html#object.__hash__
-    __hash__ = Expr.__hash__
-
-
-class ConstExpr(ExprWithOp):
+class ConstExpr(ir.ExprWithOp):
     pass
 
 
-class BinaryOpExpr(ExprWithOp):
+class BinaryOpExpr(ir.ExprWithOp):
     a: Expr
     b: Expr
 
 
-class CmpExpr(ExprWithOp):
+class CmpExpr(ir.ExprWithOp):
     a: Expr
     b: Expr
 
 
-class LogicalExpr(ExprWithOp):
+class LogicalExpr(ir.ExprWithOp):
     pass
 
 
@@ -569,7 +561,7 @@ class CommReducer(Object, Scriptable):
 
 
 @tvm_ffi.register_object("tirx.Reduce")
-class Reduce(ExprWithOp):
+class Reduce(ir.ExprWithOp):
     """Reduce node.
 
     Parameters
@@ -738,7 +730,7 @@ class StringImm(ConstExpr):
 
 
 @tvm_ffi.register_object("tirx.Cast")
-class Cast(ExprWithOp):
+class Cast(ir.ExprWithOp):
     """Cast expression.
 
     Parameters
@@ -1124,7 +1116,7 @@ class Not(LogicalExpr):
 
 
 @tvm_ffi.register_object("tirx.Select")
-class Select(ExprWithOp):
+class Select(ir.ExprWithOp):
     """Select node.
 
     Note
@@ -1191,7 +1183,7 @@ def BufferLoad(buffer: Buffer, indices: list[Expr], span: 
Span | None = None) ->
 
 
 @tvm_ffi.register_object("tirx.Ramp")
-class Ramp(ExprWithOp):
+class Ramp(ir.ExprWithOp):
     """Ramp node.
 
     Parameters
@@ -1224,7 +1216,7 @@ class Ramp(ExprWithOp):
 
 
 @tvm_ffi.register_object("tirx.Broadcast")
-class Broadcast(ExprWithOp):
+class Broadcast(ir.ExprWithOp):
     """Broadcast node.
 
     Parameters
@@ -1247,7 +1239,7 @@ class Broadcast(ExprWithOp):
 
 
 @tvm_ffi.register_object("tirx.Shuffle")
-class Shuffle(ExprWithOp):
+class Shuffle(ir.ExprWithOp):
     """Shuffle node.
 
     Parameters
@@ -1286,7 +1278,7 @@ class CallEffectKind:
 
 
 @tvm_ffi.register_object("tirx.Let")
-class Let(ExprWithOp):
+class Let(ir.ExprWithOp):
     """Let node.
 
     Parameters
diff --git a/python/tvm/tirx/op.py b/python/tvm/tirx/op.py
index 5621e93195..2ce3402adc 100644
--- a/python/tvm/tirx/op.py
+++ b/python/tvm/tirx/op.py
@@ -24,7 +24,7 @@ from tvm_ffi import Array
 
 import tvm
 from tvm import tirx
-from tvm.ir import Call, Expr, Op, PointerType, PrimType, TensorLoad
+from tvm.ir import Call, Expr, ExprWithOp, Op, PointerType, PrimType, 
TensorLoad
 from tvm.ir.base import Span
 from tvm.ir.expr import _realize_operand
 from tvm.ir.type import TensorMapType
@@ -32,7 +32,7 @@ from tvm.runtime import const
 
 from . import _ffi_api
 from .buffer import Buffer, buffer_data, is_buffer_var
-from .expr import BufferLoad, CommReducer, ExprOp, ExprWithOp, IntImm, Var
+from .expr import BufferLoad, CommReducer, ExprOp, IntImm, Var
 
 tir = tirx  # alias for backward compat with upstream tir.convert() calls
 
diff --git a/tests/python/relax/test_expr.py b/tests/python/relax/test_expr.py
index 12a650be02..fc53813afe 100644
--- a/tests/python/relax/test_expr.py
+++ b/tests/python/relax/test_expr.py
@@ -379,6 +379,58 @@ def test_call_raises_error_for_missing_operator():
         rx.Call(None, [])
 
 
+def test_shared_operator_surface_preserves_relax_semantics():
+    tensor_ty = rx.TensorType([2], "float32")
+    x = rx.Var("x", tensor_ty)
+    y = rx.Var("y", tensor_ty)
+
+    assert (x == x) is True
+    assert (x != x) is False
+    assert (x == y) is False
+    assert (x != y) is True
+    assert isinstance(hash(x), int)
+
+    assert isinstance(x + y, tvm.ir.Call)
+    assert isinstance(-x, tvm.ir.Call)
+    assert isinstance(x.astype("float16"), tvm.ir.Call)
+
+    with pytest.raises(ValueError, match="Cannot use and"):
+        bool(x)
+
+
+def test_shared_operator_surface_rejects_non_tensor_relax_values():
+    shape = rx.ShapeExpr([1, 2])
+    message = "Operator overloading is not supported for expression type"
+
+    assert (shape == shape) is True
+    assert (shape != shape) is False
+    assert bool(shape)
+
+    with pytest.raises(TypeError, match="unsupported operand type"):
+        shape + shape
+
+    for operation in (
+        lambda: -shape,
+        lambda: shape.equal(shape),
+        lambda: shape.astype("float32"),
+    ):
+        with pytest.raises(TypeError, match=message):
+            operation()
+
+
+def test_shared_operator_surface_calls_function_typed_values():
+    tensor_ty = rx.TensorType([2], "float32")
+    arg = rx.Var("arg", tensor_ty)
+    func_ty = rx.FuncType([tensor_ty], tensor_ty)
+
+    for func in (rx.Var("func", func_ty), rx.ExternFunc("extern_func", 
func_ty)):
+        call = func(arg)
+        assert isinstance(call, tvm.ir.Call)
+        assert call.op.same_as(func)
+        assert len(call.args) == 1
+        assert call.args[0].same_as(arg)
+
+
 if __name__ == "__main__":
     tvm.testing.main()
 
diff --git a/tests/python/tirx-base/test_tir_constructor.py 
b/tests/python/tirx-base/test_tir_constructor.py
index 73b37f4b4d..65be8c07c9 100644
--- a/tests/python/tirx-base/test_tir_constructor.py
+++ b/tests/python/tirx-base/test_tir_constructor.py
@@ -214,6 +214,35 @@ def test_expr_constructor():
     assert x.body == v
 
 
+def test_operator_base_categories_have_primitive_type():
+    var = tvm.tirx.Var("x", "int32")
+    buffer = tvm.tirx.decl_buffer([4], "float32")
+    expressions = [
+        tvm.tirx.IntImm("int32", 1),
+        tvm.tirx.Add(var, 1),
+        tvm.tirx.LT(var, 1),
+        tvm.tirx.And(var < 1, var < 2),
+        tvm.tirx.Reduce(
+            None,
+            [1],
+            [tvm.tirx.IterVar((0, 1), "i", tvm.tirx.IterVar.CommReduce)],
+            None,
+            0,
+        ),
+        tvm.tirx.Cast("float32", var),
+        tvm.tirx.Select(var < 1, var, 1),
+        tvm.tirx.BufferLoad(buffer, [0]),
+        tvm.tirx.Ramp(0, 1, 4),
+        tvm.tirx.Broadcast(var, 4),
+        tvm.tirx.Shuffle([tvm.tirx.Broadcast(var, 2)], [0]),
+        tvm.tirx.Let(var, 1, var),
+    ]
+
+    for expression in expressions:
+        assert isinstance(expression, tvm.ir.ExprWithOp)
+        assert isinstance(expression.ty, tvm.ir.PrimType)
+
+
 def test_stmt_constructor():
     v = tvm.tirx.Var("aa", "int32")
     nop = tvm.tirx.Evaluate(1)

Reply via email to