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

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


The following commit(s) were added to refs/heads/main by this push:
     new 501635deb [FEAT] Add Rust primitive expression operator overloads 
(#157)
501635deb is described below

commit 501635debfd322189116b5ffe0113aa55cb349f2
Author: Tianqi Chen <[email protected]>
AuthorDate: Sun Nov 1 22:12:10 2026 -0500

    [FEAT] Add Rust primitive expression operator overloads (#157)
    
    Add `def_expr_op_over_loads!(Type)` for existing Rust reference
    wrappers, with owned and borrowed operators, contextual scalar operands,
    and named operations. Register native primitive-expression counterparts
    plus `Expr`, `Var`, and `Call` explicitly in `rust/src/ir/expr.rs`,
    preserving object layouts, allocation, and ancestor conversions.
    
    Rename the fallible operator result to `ExprResult`, remove `ex()` and
    `.expr()`, and support standard `TryFrom<ExprResult> for Expr`
    extraction. Update examples and the Rust guide to use expression
    operators directly while retaining ordinary Rust borrowing.
---
 docs/ir_guides/working_with_ir_in_rust.rst |  22 ++-
 examples/extension/src/lib.rs              |   5 +-
 examples/ir_guides/rust/src/lib.rs         |   8 +-
 rust/src/ir/expr.rs                        |  54 +++++-
 rust/src/ir/prim/op.rs                     | 258 +++++++++++++----------------
 rust/src/lib.rs                            |   5 +-
 rust/src/macro.rs                          |  21 +++
 rust/src/macro/def_expr_op_overloads.rs    | 148 +++++++++++++++++
 rust/src/tirx/op.rs                        |  15 +-
 rust/tests/ir/prim/op.rs                   |  78 +++++----
 10 files changed, 404 insertions(+), 210 deletions(-)

diff --git a/docs/ir_guides/working_with_ir_in_rust.rst 
b/docs/ir_guides/working_with_ir_in_rust.rst
index cb0e658e5..fbba2c262 100644
--- a/docs/ir_guides/working_with_ir_in_rust.rst
+++ b/docs/ir_guides/working_with_ir_in_rust.rst
@@ -103,7 +103,7 @@ key constructs are:
 
    * - Construct
      - Purpose
-   * - Constructors, ``ex()``
+   * - Constructors, expression operators
      - Build expressions with ordinary Rust operators
    * - ``FooObj`` / ``Foo``
      - Underlying object and its named managed reference: read fields
@@ -138,13 +138,13 @@ The output is:
    x * 2 + 1
 
 |Var::new|_ takes a name, a type, and a source location; ``UNKNOWN_LOC``
-means there is none. |ex|_ wraps ``x`` so that ``*`` and ``+`` build IR,
-and the literals ``2`` and ``1`` become constants of the type of ``x``.
+means there is none. The operators ``*`` and ``+`` build IR directly
+from ``x``, and the literals ``2`` and ``1`` become constants of its type.
 
-An operator can fail, for example on mismatched types, so ``ex`` carries
-the first error along, much like a chain of ``Result``\ s. ``.expr()``
-takes the result out and ``?`` passes an error on. |script|_ prints the
-node as TVMScript.
+An operator can fail, for example on mismatched types, so ``ExprResult``
+carries the first error along, much like a chain of ``Result``\ s. The
+standard ``.try_into()`` conversion extracts an ``Expr``, and ``?`` passes
+an error on. |script|_ prints the node as TVMScript.
 
 We now have an expression; next, let's see what kind of Rust value it is.
 
@@ -546,8 +546,8 @@ both visits and mutations. A ``#[dispatch(mutate)]`` method 
that takes a
 trailing ``&mut Mutator`` gets the mutation counterpart of
 ``VisitContext``, with ``default_mutate`` for the children and
 ``def_region_kind()``. Let's reverse the loop
-by replacing each use of ``i`` with ``n - 1 - i``, built with ``ex`` as
-before:
+by replacing each use of ``i`` with ``n - 1 - i``. As before,
+``.try_into()?`` extracts the built expression or returns its error:
 
 .. literalinclude:: ../../examples/ir_guides/rust/src/lib.rs
    :language: rust
@@ -605,7 +605,7 @@ Quick Reference
        borrowed object in an ``Option``
    * - ``.into()``
      - Upcasts a managed reference to a parent type
-   * - |Var::new|_, |ex|_\ ``(...)``, ``.expr()``
+   * - |Var::new|_, expression operators, ``.try_into()?``
      - Creates a node; builds an expression with operators
    * - |structural_walk|_\ ``(&func, callbacks, WalkOrder::PreOrder)``
      - Walks with typed callbacks that return ``WalkResult``
@@ -654,8 +654,6 @@ Quick Reference
 .. _VisitCallbacks: 
../reference/rust/generated/tvm_ffi/extra/structural_visit/struct.VisitCallbacks.html
 .. |MutateCallbacks| replace:: ``MutateCallbacks``
 .. _MutateCallbacks: 
../reference/rust/generated/tvm_ffi/extra/structural_mutate/struct.MutateCallbacks.html
-.. |ex| replace:: ``ex``
-.. _ex: ../reference/rust/generated/tvm_tirx/ir/prim/op/fn.ex.html
 .. |ObjectIdentity| replace:: ``ObjectIdentity``
 .. _ObjectIdentity: 
../reference/rust/generated/tvm_ffi/object/struct.ObjectIdentity.html
 .. |StructuralVisitorWithConstraintContext| replace:: 
``StructuralVisitorWithConstraintContext``
diff --git a/examples/extension/src/lib.rs b/examples/extension/src/lib.rs
index e50168731..0f6fdeda1 100644
--- a/examples/extension/src/lib.rs
+++ b/examples/extension/src/lib.rs
@@ -21,7 +21,6 @@
 //! See docs/advanced/extending_ir_in_rust.rst; the my_ext Python package 
loads this library.
 
 use tvm_tirx::ir::op_attr::CALL_EFFECT_KIND;
-use tvm_tirx::ir::prim::ex;
 use tvm_tirx::ir::sig::{self, PrimExpr};
 use tvm_tirx::ir::transform::{Pass, PassContext, PassInfo};
 use tvm_tirx::ir::{
@@ -207,8 +206,8 @@ impl Lowering {
             let (a, b) = self.parts(&call.args.get(0)?)?;
             let (c, d) = self.parts(&call.args.get(1)?)?;
             // (a + bi)(c + di) = (ac - bd) + (ad + bc)i
-            let re = ex(&a) * ex(&c) - ex(&b) * ex(&d);
-            let im = ex(&a) * ex(&d) + ex(&b) * ex(&c);
+            let re = &a * &c - &b * &d;
+            let im = &a * &d + &b * &c;
             return Ok(Call::new(
                 None,
                 COMPLEX.get()?,
diff --git a/examples/ir_guides/rust/src/lib.rs 
b/examples/ir_guides/rust/src/lib.rs
index 1f100d28b..8c9a74ce4 100644
--- a/examples/ir_guides/rust/src/lib.rs
+++ b/examples/ir_guides/rust/src/lib.rs
@@ -24,7 +24,7 @@
 
 use std::collections::HashMap;
 
-use tvm_tirx::ir::prim::{ex, make_const, Add, AddObj, Mul};
+use tvm_tirx::ir::prim::{make_const, Add, AddObj, Mul};
 use tvm_tirx::ir::{
     Expr, For, ForObj, IntImm, PrimType, TensorStore, TensorStoreObj, Var, 
VarObj, UNKNOWN_LOC,
 };
@@ -46,7 +46,7 @@ fn lines(items: impl IntoIterator<Item = String>) -> Strings {
 // [construct.begin]
 fn x_times_2_plus_1() -> Result<(Var, Expr)> {
     let x = Var::new("x", PrimType::int(32), UNKNOWN_LOC);
-    let expr = (ex(&x) * 2 + 1).expr()?;
+    let expr = (&x * 2 + 1).try_into()?;
     Ok((x, expr))
 }
 
@@ -101,7 +101,7 @@ fn describe(expr: &Expr) -> Result<String> {
 // [matching.begin]
 fn matching() -> Result<Strings> {
     let (x, expr) = x_times_2_plus_1()?;
-    let difference = (ex(&x) - 1).expr()?;
+    let difference = (&x - 1).try_into()?;
     Ok(lines([describe(&expr)?, describe(&difference)?]))
 }
 // [matching.end]
@@ -241,7 +241,7 @@ struct ReverseLoops {
 #[dispatch(mutate)]
 impl ReverseLoops {
     fn mutate_for(&mut self, loop_: For, mutator: &mut Mutator) -> Result<Any> 
{
-        let index = (ex(&loop_.end) - 1 - &loop_.var).expr()?;
+        let index = (&loop_.end - 1 - &loop_.var).try_into()?;
         self.reversed_index.insert(ObjectIdentity::of(&loop_.var), index);
         mutator.default_mutate(self, &loop_)
     }
diff --git a/rust/src/ir/expr.rs b/rust/src/ir/expr.rs
index d42798b85..2dbd74e67 100644
--- a/rust/src/ir/expr.rs
+++ b/rust/src/ir/expr.rs
@@ -1045,10 +1045,10 @@ impl From<bool> for IntImm {
 }
 
 /// What converts to an expression, possibly failing: anything that converts 
into an [`Expr`] (a
-/// node, a `Var`, a Rust `i32`, `f32` and the like), and an 
[`Ex`](crate::ir::prim::Ex), an
+/// node, a `Var`, a Rust `i32`, `f32` and the like), and an 
[`ExprResult`](crate::ir::prim::ExprResult), an
 /// expression under construction, which gives the first error any of its 
steps raised. The
 /// constructors and functions that can fail take their expression operands as 
`impl IntoExpr`, so
-/// an `Ex` passes straight in and `?` on their result covers it.
+/// an `ExprResult` passes straight in and `?` on their result covers it.
 pub trait IntoExpr {
     fn into_expr(self) -> Result<Expr>;
 }
@@ -1825,3 +1825,53 @@ impl Range {
         }
     }
 }
+
+// Operator attachment is separate from generated layouts and upcasts. Keep 
the native
+// primitive-expression counterparts here, plus Expr, Var and Call for general 
handles whose
+// primitive type is checked when an operator is applied.
+crate::def_expr_op_over_loads!(Expr);
+crate::def_expr_op_over_loads!(Var);
+crate::def_expr_op_over_loads!(Call);
+crate::def_expr_op_over_loads!(IntImm);
+crate::def_expr_op_over_loads!(FloatImm);
+crate::def_expr_op_over_loads!(TensorLoad);
+
+// Primitive operator and vector reference classes inherit C++ PrimExpr.
+crate::def_expr_op_over_loads!(crate::ir::prim::Cast);
+crate::def_expr_op_over_loads!(crate::ir::prim::LShift);
+crate::def_expr_op_over_loads!(crate::ir::prim::RShift);
+crate::def_expr_op_over_loads!(crate::ir::prim::BitwiseAnd);
+crate::def_expr_op_over_loads!(crate::ir::prim::BitwiseOr);
+crate::def_expr_op_over_loads!(crate::ir::prim::BitwiseXor);
+crate::def_expr_op_over_loads!(crate::ir::prim::BitwiseNot);
+crate::def_expr_op_over_loads!(crate::ir::prim::Add);
+crate::def_expr_op_over_loads!(crate::ir::prim::Sub);
+crate::def_expr_op_over_loads!(crate::ir::prim::Mul);
+crate::def_expr_op_over_loads!(crate::ir::prim::Div);
+crate::def_expr_op_over_loads!(crate::ir::prim::Mod);
+crate::def_expr_op_over_loads!(crate::ir::prim::FloorDiv);
+crate::def_expr_op_over_loads!(crate::ir::prim::FloorMod);
+crate::def_expr_op_over_loads!(crate::ir::prim::Min);
+crate::def_expr_op_over_loads!(crate::ir::prim::Max);
+crate::def_expr_op_over_loads!(crate::ir::prim::EQ);
+crate::def_expr_op_over_loads!(crate::ir::prim::NE);
+crate::def_expr_op_over_loads!(crate::ir::prim::LT);
+crate::def_expr_op_over_loads!(crate::ir::prim::LE);
+crate::def_expr_op_over_loads!(crate::ir::prim::GT);
+crate::def_expr_op_over_loads!(crate::ir::prim::GE);
+crate::def_expr_op_over_loads!(crate::ir::prim::And);
+crate::def_expr_op_over_loads!(crate::ir::prim::Or);
+crate::def_expr_op_over_loads!(crate::ir::prim::Not);
+crate::def_expr_op_over_loads!(crate::ir::prim::Select);
+crate::def_expr_op_over_loads!(crate::ir::prim::Let);
+crate::def_expr_op_over_loads!(crate::ir::prim::Ramp);
+crate::def_expr_op_over_loads!(crate::ir::prim::Broadcast);
+crate::def_expr_op_over_loads!(crate::ir::prim::Shuffle);
+
+// These symbolic reference classes also inherit C++ PrimExpr. The opaque Rust 
mirrors retain
+// their existing allocation restrictions; attaching operators only forwards 
their handles.
+crate::def_expr_op_over_loads!(crate::sym::iter_affine_map::IterMapExpr);
+crate::def_expr_op_over_loads!(crate::sym::iter_affine_map::IterSplitExpr);
+crate::def_expr_op_over_loads!(crate::sym::iter_affine_map::IterSumExpr);
+crate::def_expr_op_over_loads!(crate::sym::analyzer::SplitExpr);
+crate::def_expr_op_over_loads!(crate::sym::analyzer::SumExpr);
diff --git a/rust/src/ir/prim/op.rs b/rust/src/ir/prim/op.rs
index ec62cf30e..f1b092f72 100644
--- a/rust/src/ir/prim/op.rs
+++ b/rust/src/ir/prim/op.rs
@@ -30,7 +30,7 @@
 //!
 //! Each function builds at the unknown location, as the C++ default argument 
does; the `_at`
 //! variant takes a location. An operand is an expression, an expression under 
construction (an
-//! [`Ex`], see below) or a literal, typed as C++ types it: an integer next to 
an expression is
+//! [`ExprResult`], see below) or a literal, typed as C++ types it: an integer 
next to an expression is
 //! `MakeConst(other.ty(), v)`, an `f32` a float32 constant, an `f64` a 
float64 one, and a literal
 //! on its own the constant [`From`] makes of it (an `i32` is an `int32`):
 //!
@@ -44,10 +44,10 @@
 //! # Ok::<(), tvm_tirx::tvm_ffi::Error>(())
 //! ```
 //!
-//! `&x + 1` is the operator syntax of [`Ex`]: the C++ operators on an 
expression, a `Var` or an
-//! `Ex`, with literals typed the same way. An `Ex` holds the expression under 
construction or the
+//! `&x + 1` builds an [`ExprResult`]: the C++ operators on expression 
handles, with literals
+//! typed the same way. An `ExprResult` holds the expression under 
construction or the
 //! first error any step raised; it passes into any function or constructor 
that takes
-//! `impl IntoExpr`, whose `?` then reports that error, and [`Ex::expr`] takes 
the expression out.
+//! `impl IntoExpr`, whose `?` then reports that error, and `Expr::try_from` 
takes the expression out.
 //! The operations Rust has no operator for are the [`PrimOps`] methods 
(`x.floordiv(4)`,
 //! `x.less(y)`, `x.cast(ty)`).
 
@@ -302,7 +302,7 @@ fn check_operands(
 // 
---------------------------------------------------------------------------------------------
 // Operands
 
-/// An operand of an operator: an expression, an expression under construction 
([`Ex`]) or a
+/// An operand of an operator: an expression, an expression under construction 
([`ExprResult`]) or a
 /// literal, which is typed after the expression it meets (C++
 /// `TVM_DEFINE_BINOP_CONST_VAL_OVERLOAD` and friends): an `i32` or `i64` is
 /// `MakeConst(other.ty(), v)`, an `f32` a float32 constant, an `f64` a 
float64 one. A literal
@@ -315,14 +315,14 @@ pub trait Operand {
 /// What an [`Operand`] is.
 #[doc(hidden)]
 pub enum OperandKind {
-    Expr(Ex),
+    Expr(ExprResult),
     Int32(i32),
     Int64(i64),
     Float32(f32),
     Float64(f64),
 }
 
-impl<T: Into<Ex>> Operand for T {
+impl<T: Into<ExprResult>> Operand for T {
     fn classify(self) -> OperandKind {
         OperandKind::Expr(self.into())
     }
@@ -356,27 +356,27 @@ impl Operand for f64 {
 }
 
 /// An operand next to `peer`: a literal is typed after it.
-fn typed_after(operand: OperandKind, peer: &Ex) -> Ex {
+fn typed_after(operand: OperandKind, peer: &ExprResult) -> ExprResult {
     let constant = |value: i64| {
-        Ex(peer.dtype().and_then(|dtype| 
make_const(PrimType::from_dtype(dtype)?, value)))
+        ExprResult(peer.dtype().and_then(|dtype| 
make_const(PrimType::from_dtype(dtype)?, value)))
     };
     match operand {
-        OperandKind::Expr(ex) => ex,
+        OperandKind::Expr(expr) => expr,
         OperandKind::Int32(value) => constant(i64::from(value)),
         OperandKind::Int64(value) => constant(value),
-        OperandKind::Float32(value) => Ex(Ok(FloatImm::float32(value).into())),
-        OperandKind::Float64(value) => Ex(Ok(FloatImm::float64(value).into())),
+        OperandKind::Float32(value) => 
ExprResult(Ok(FloatImm::float32(value).into())),
+        OperandKind::Float64(value) => 
ExprResult(Ok(FloatImm::float64(value).into())),
     }
 }
 
 /// An operand on its own: a literal is the constant [`From`] makes of it.
-fn on_its_own(operand: OperandKind) -> Ex {
+fn on_its_own(operand: OperandKind) -> ExprResult {
     match operand {
-        OperandKind::Expr(ex) => ex,
-        OperandKind::Int32(value) => Ex(Ok(IntImm::int32(value).into())),
-        OperandKind::Int64(value) => Ex(Ok(IntImm::int64(value).into())),
-        OperandKind::Float32(value) => Ex(Ok(FloatImm::float32(value).into())),
-        OperandKind::Float64(value) => Ex(Ok(FloatImm::float64(value).into())),
+        OperandKind::Expr(expr) => expr,
+        OperandKind::Int32(value) => 
ExprResult(Ok(IntImm::int32(value).into())),
+        OperandKind::Int64(value) => 
ExprResult(Ok(IntImm::int64(value).into())),
+        OperandKind::Float32(value) => 
ExprResult(Ok(FloatImm::float32(value).into())),
+        OperandKind::Float64(value) => 
ExprResult(Ok(FloatImm::float64(value).into())),
     }
 }
 
@@ -394,7 +394,7 @@ fn operands(a: impl Operand, b: impl Operand) -> 
Result<(Expr, Expr)> {
         }
         (a, b) => (on_its_own(a), on_its_own(b)),
     };
-    Ok((a.expr()?, b.expr()?))
+    Ok((a.into_expr()?, b.into_expr()?))
 }
 
 /// The functions without a location: each builds at the unknown location 
through its `_at`
@@ -1725,14 +1725,14 @@ binary_intrinsics! {
 }
 
 // 
---------------------------------------------------------------------------------------------
-// `Ex`: expressions under construction
+// `ExprResult`: expressions under construction
 
 /// An expression being built by operators: either the expression, or the 
first error any step
-/// raised. Later steps pass an error through untouched. An `Ex` is an 
[`IntoExpr`], so it passes
+/// raised. Later steps pass an error through untouched. An `ExprResult` is an 
[`IntoExpr`], so it passes
 /// into any function or constructor that takes one, whose `?` then reports 
the error;
-/// [`Ex::expr`] takes the expression out.
+/// `Expr::try_from` takes the expression out.
 ///
-/// Any expression node converts into an `Ex`, as it converts into a C++ 
`PrimExpr`. The operators
+/// Registered expression wrappers convert into an `ExprResult`. The operators
 /// `+`, `-`, `*`, `&`, `|`, `^`, `<<`, `>>`, unary `-` and `!` are the C++ 
ones, typed after C++:
 /// an integer literal on either side is `MakeConst(other.ty(), v)`, an `f32` 
a float32 constant,
 /// an `f64` a float64 one. `/` and `%` are not defined: name the division
@@ -1741,15 +1741,19 @@ binary_intrinsics! {
 /// Comparisons, `&&` and `||` are [`PrimOps`] methods too, because Rust's 
comparison traits
 /// return `bool`.
 #[derive(Clone)]
-#[must_use = "an `Ex` holds the result; pass it on, or take it with `.expr()`"]
-pub struct Ex(Result<Expr>);
+#[must_use = "an `ExprResult` holds the result; pass it on, or convert it with 
`Expr::try_from`"]
+pub struct ExprResult(Result<Expr>);
 
-impl Ex {
-    /// The built expression, or the first error.
-    pub fn expr(self) -> Result<Expr> {
-        self.0
+impl TryFrom<ExprResult> for Expr {
+    type Error = Error;
+
+    /// The built expression, or the first error raised while constructing it.
+    fn try_from(value: ExprResult) -> Result<Self> {
+        value.0
     }
+}
 
+impl ExprResult {
     /// The dtype a literal next to this expression takes 
(`MakeConst(this.ty(), v)`).
     fn dtype(&self) -> Result<DLDataType> {
         match &self.0 {
@@ -1759,100 +1763,80 @@ impl Ex {
     }
 }
 
-impl IntoExpr for Ex {
+impl IntoExpr for ExprResult {
     fn into_expr(self) -> Result<Expr> {
         self.0
     }
 }
 
-impl IntoExpr for &Ex {
+impl IntoExpr for &ExprResult {
     fn into_expr(self) -> Result<Expr> {
         self.0.clone()
     }
 }
 
-/// `value` as an [`Ex`], to start an expression from something that is not 
one yet.
-pub fn ex(value: impl Into<Ex>) -> Ex {
-    value.into()
-}
-
-impl<T: Into<Expr>> From<Result<T>> for Ex {
+impl<T: Into<Expr>> From<Result<T>> for ExprResult {
     fn from(value: Result<T>) -> Self {
-        Ex(value.map(Into::into))
+        ExprResult(value.map(Into::into))
     }
 }
 
-impl From<&Ex> for Ex {
-    fn from(value: &Ex) -> Self {
+impl From<&ExprResult> for ExprResult {
+    fn from(value: &ExprResult) -> Self {
         value.clone()
     }
 }
 
-macro_rules! ex_from_nodes {
-    ($($ty:ty),+ $(,)?) => {
-        $(
-            impl From<$ty> for Ex {
-                fn from(value: $ty) -> Self {
-                    Ex(Ok(value.into()))
-                }
-            }
-            impl From<&$ty> for Ex {
-                fn from(value: &$ty) -> Self {
-                    Ex(Ok(value.clone().into()))
-                }
-            }
-        )+
-    };
-}
-
-ex_from_nodes!(
-    Expr, Var, IntImm, FloatImm, Call, TensorLoad, Add, And, BitwiseAnd, 
BitwiseNot, BitwiseOr,
-    BitwiseXor, Broadcast, Cast, Div, EQ, FloorDiv, FloorMod, GE, GT, LE, 
LShift, LT, Let, Max,
-    Min, Mod, Mul, NE, Not, Or, RShift, Ramp, Select, Shuffle, Sub,
-);
-
 /// `lhs <op> rhs`; a literal `rhs` is typed after `lhs`.
-fn binary(lhs: Ex, rhs: impl Operand, op: impl FnOnce(Expr, Expr) -> 
Result<Expr>) -> Ex {
+fn binary(
+    lhs: ExprResult,
+    rhs: impl Operand,
+    op: impl FnOnce(Expr, Expr) -> Result<Expr>,
+) -> ExprResult {
     let rhs = typed_after(rhs.classify(), &lhs);
     match (lhs.0, rhs.0) {
-        (Ok(a), Ok(b)) => Ex(op(a, b)),
-        (Err(error), _) | (_, Err(error)) => Ex(Err(error)),
+        (Ok(a), Ok(b)) => ExprResult(op(a, b)),
+        (Err(error), _) | (_, Err(error)) => ExprResult(Err(error)),
     }
 }
 
 /// `lhs <op> rhs` for a literal `lhs`, typed after `rhs`.
-fn binary_literal(lhs: impl Operand, rhs: Ex, op: impl FnOnce(Expr, Expr) -> 
Result<Expr>) -> Ex {
+fn binary_literal(
+    lhs: impl Operand,
+    rhs: ExprResult,
+    op: impl FnOnce(Expr, Expr) -> Result<Expr>,
+) -> ExprResult {
     let lhs = typed_after(lhs.classify(), &rhs);
     binary(lhs, rhs, op)
 }
 
 mod operators {
-    //! The operator traits of [`Ex`], and of the expressions and literals 
that start one.
+    //! The operator traits of [`ExprResult`], and of the expressions and 
literals that start one.
 
     use std::ops::{
         Add, AddAssign, BitAnd, BitOr, BitXor, Mul, MulAssign, Neg, Not, Shl, 
Shr, Sub, SubAssign,
     };
 
-    use super::{binary, binary_literal, Ex, Expr, Operand, Var};
+    use super::{binary, binary_literal, ExprResult, Operand};
 
     macro_rules! binary_operators {
         ($($Trait:ident::$method:ident => $op:ident),+ $(,)?) => {
             $(
-                impl<R: Operand> $Trait<R> for Ex {
-                    type Output = Ex;
-                    fn $method(self, rhs: R) -> Ex {
+                impl<R: Operand> $Trait<R> for ExprResult {
+                    type Output = ExprResult;
+                    fn $method(self, rhs: R) -> ExprResult {
                         binary(self, rhs, super::$op)
                     }
                 }
-                binary_operators!(@lhs $Trait::$method => $op; &Ex, Expr, 
&Expr, Var, &Var);
+                binary_operators!(@lhs $Trait::$method => $op; &ExprResult);
                 binary_operators!(@literal $Trait::$method => $op; i32, i64, 
f32, f64);
             )+
         };
         (@lhs $Trait:ident::$method:ident => $op:ident; $($lhs:ty),+) => {
             $(
                 impl<R: Operand> $Trait<R> for $lhs {
-                    type Output = Ex;
-                    fn $method(self, rhs: R) -> Ex {
+                    type Output = ExprResult;
+                    fn $method(self, rhs: R) -> ExprResult {
                         binary(self.into(), rhs, super::$op)
                     }
                 }
@@ -1860,14 +1844,14 @@ mod operators {
         };
         (@literal $Trait:ident::$method:ident => $op:ident; $($lit:ty),+) => {
             $(
-                binary_operators!(@literal_rhs $Trait::$method => $op; $lit; 
Ex, &Ex, Expr, &Expr, Var, &Var);
+                binary_operators!(@literal_rhs $Trait::$method => $op; $lit; 
ExprResult, &ExprResult);
             )+
         };
         (@literal_rhs $Trait:ident::$method:ident => $op:ident; $lit:ty; 
$($rhs:ty),+) => {
             $(
                 impl $Trait<$rhs> for $lit {
-                    type Output = Ex;
-                    fn $method(self, rhs: $rhs) -> Ex {
+                    type Output = ExprResult;
+                    fn $method(self, rhs: $rhs) -> ExprResult {
                         binary_literal(self, rhs.into(), super::$op)
                     }
                 }
@@ -1889,7 +1873,7 @@ mod operators {
     macro_rules! assign_operators {
         ($($Trait:ident::$method:ident => $op:ident),+ $(,)?) => {
             $(
-                impl<R: Operand> $Trait<R> for Ex {
+                impl<R: Operand> $Trait<R> for ExprResult {
                     fn $method(&mut self, rhs: R) {
                         *self = binary(self.clone(), rhs, super::$op);
                     }
@@ -1905,23 +1889,23 @@ mod operators {
             $(
                 /// C++ unary `-` (`neg`).
                 impl Neg for $ty {
-                    type Output = Ex;
-                    fn neg(self) -> Ex {
-                        Ex(super::neg(Ex::from(self)))
+                    type Output = ExprResult;
+                    fn neg(self) -> ExprResult {
+                        ExprResult(super::neg(ExprResult::from(self)))
                     }
                 }
                 /// C++ `!` (`logical_not`).
                 impl Not for $ty {
-                    type Output = Ex;
-                    fn not(self) -> Ex {
-                        Ex(super::logical_not(Ex::from(self)))
+                    type Output = ExprResult;
+                    fn not(self) -> ExprResult {
+                        ExprResult(super::logical_not(ExprResult::from(self)))
                     }
                 }
             )+
         };
     }
 
-    unary_operators!(Ex, &Ex, Expr, &Expr, Var, &Var);
+    unary_operators!(ExprResult, &ExprResult);
 }
 
 /// [`PrimOps`] methods that are a function of one expression.
@@ -1929,8 +1913,8 @@ macro_rules! unary_methods {
     ($($(#[$doc:meta])* $name:ident;)+) => {
         $(
             $(#[$doc])*
-            fn $name(&self) -> Ex {
-                Ex($name(self.to_ex()))
+            fn $name(&self) -> ExprResult {
+                ExprResult($name(self.to_expr_result()))
             }
         )+
     };
@@ -1942,52 +1926,52 @@ macro_rules! binary_methods {
     ($($(#[$doc:meta])* $name:ident;)+) => {
         $(
             $(#[$doc])*
-            fn $name(&self, rhs: impl Operand) -> Ex {
-                binary(self.to_ex(), rhs, $name)
+            fn $name(&self, rhs: impl Operand) -> ExprResult {
+                binary(self.to_expr_result(), rhs, $name)
             }
         )+
     };
 }
 
 /// The C++ `PrimExpr` operations that are functions rather than Rust 
operators, as methods of
-/// an [`Ex`], an [`Expr`] or a [`Var`]. Each takes an expression or a literal 
on the right, typed
+/// an [`ExprResult`] or an expression handle. Each takes an expression or a 
literal on the right, typed
 /// as for the operators, and leaves `self` usable; the comparisons are named 
after the C++
 /// functions (`less`, `equal`), since Rust's `eq` and `lt` return a `bool`.
 pub trait PrimOps {
-    /// This expression as an [`Ex`] (a reference-count increment).
-    fn to_ex(&self) -> Ex;
+    /// This expression as an [`ExprResult`] (a reference-count increment).
+    fn to_expr_result(&self) -> ExprResult;
 
     /// C++ `a < b` ([`lt`]).
-    fn less(&self, rhs: impl Operand) -> Ex {
-        binary(self.to_ex(), rhs, lt)
+    fn less(&self, rhs: impl Operand) -> ExprResult {
+        binary(self.to_expr_result(), rhs, lt)
     }
     /// C++ `a <= b` ([`le`]).
-    fn less_equal(&self, rhs: impl Operand) -> Ex {
-        binary(self.to_ex(), rhs, le)
+    fn less_equal(&self, rhs: impl Operand) -> ExprResult {
+        binary(self.to_expr_result(), rhs, le)
     }
     /// C++ `a > b` ([`gt`]).
-    fn greater(&self, rhs: impl Operand) -> Ex {
-        binary(self.to_ex(), rhs, gt)
+    fn greater(&self, rhs: impl Operand) -> ExprResult {
+        binary(self.to_expr_result(), rhs, gt)
     }
     /// C++ `a >= b` ([`ge`]).
-    fn greater_equal(&self, rhs: impl Operand) -> Ex {
-        binary(self.to_ex(), rhs, ge)
+    fn greater_equal(&self, rhs: impl Operand) -> ExprResult {
+        binary(self.to_expr_result(), rhs, ge)
     }
     /// C++ `a == b` ([`eq`]).
-    fn equal(&self, rhs: impl Operand) -> Ex {
-        binary(self.to_ex(), rhs, eq)
+    fn equal(&self, rhs: impl Operand) -> ExprResult {
+        binary(self.to_expr_result(), rhs, eq)
     }
     /// C++ `a != b` ([`ne`]).
-    fn not_equal(&self, rhs: impl Operand) -> Ex {
-        binary(self.to_ex(), rhs, ne)
+    fn not_equal(&self, rhs: impl Operand) -> ExprResult {
+        binary(self.to_expr_result(), rhs, ne)
     }
     /// C++ `a && b` ([`logical_and`]).
-    fn logical_and(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(logical_and(self.to_ex(), rhs))
+    fn logical_and(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(logical_and(self.to_expr_result(), rhs))
     }
     /// C++ `a || b` ([`logical_or`]).
-    fn logical_or(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(logical_or(self.to_ex(), rhs))
+    fn logical_or(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(logical_or(self.to_expr_result(), rhs))
     }
 
     binary_methods! {
@@ -2014,32 +1998,32 @@ pub trait PrimOps {
     }
 
     /// C++ `pow(a, b)` ([`pow`]).
-    fn pow(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(pow(self.to_ex(), rhs))
+    fn pow(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(pow(self.to_expr_result(), rhs))
     }
     /// C++ `fmod(a, b)` ([`fmod`]).
-    fn fmod(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(fmod(self.to_ex(), rhs))
+    fn fmod(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(fmod(self.to_expr_result(), rhs))
     }
     /// C++ `atan2(a, b)` ([`atan2`]).
-    fn atan2(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(atan2(self.to_ex(), rhs))
+    fn atan2(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(atan2(self.to_expr_result(), rhs))
     }
     /// C++ `nextafter(a, b)` ([`nextafter`]).
-    fn nextafter(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(nextafter(self.to_ex(), rhs))
+    fn nextafter(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(nextafter(self.to_expr_result(), rhs))
     }
     /// C++ `copysign(a, b)` ([`copysign`]).
-    fn copysign(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(copysign(self.to_ex(), rhs))
+    fn copysign(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(copysign(self.to_expr_result(), rhs))
     }
     /// C++ `hypot(a, b)` ([`hypot`]).
-    fn hypot(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(hypot(self.to_ex(), rhs))
+    fn hypot(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(hypot(self.to_expr_result(), rhs))
     }
     /// C++ `ldexp(a, b)` ([`ldexp`]).
-    fn ldexp(&self, rhs: impl IntoExpr) -> Ex {
-        Ex(ldexp(self.to_ex(), rhs))
+    fn ldexp(&self, rhs: impl IntoExpr) -> ExprResult {
+        ExprResult(ldexp(self.to_expr_result(), rhs))
     }
 
     unary_methods! {
@@ -2120,35 +2104,23 @@ pub trait PrimOps {
     }
 
     /// C++ `cast(a, t)` ([`cast`]).
-    fn cast(&self, ty: impl Into<PrimType>) -> Ex {
-        Ex(cast(self.to_ex(), ty))
+    fn cast(&self, ty: impl Into<PrimType>) -> ExprResult {
+        ExprResult(cast(self.to_expr_result(), ty))
     }
 
     /// C++ `if_then_else(self, t, f)` ([`if_then_else`]), with `self` the 
condition.
-    fn if_then_else(&self, t: impl IntoExpr, f: impl IntoExpr) -> Ex {
-        Ex(if_then_else(self.to_ex(), t, f))
+    fn if_then_else(&self, t: impl IntoExpr, f: impl IntoExpr) -> ExprResult {
+        ExprResult(if_then_else(self.to_expr_result(), t, f))
     }
 
     /// The raw `Select(self, t, f)` ([`Select::new`]), with `self` the 
condition.
-    fn select(&self, t: impl IntoExpr, f: impl IntoExpr) -> Ex {
-        Ex(Select::new(self.to_ex(), t, f, UNKNOWN_LOC).map(Into::into))
+    fn select(&self, t: impl IntoExpr, f: impl IntoExpr) -> ExprResult {
+        ExprResult(Select::new(self.to_expr_result(), t, f, 
UNKNOWN_LOC).map(Into::into))
     }
 }
 
-impl PrimOps for Ex {
-    fn to_ex(&self) -> Ex {
+impl PrimOps for ExprResult {
+    fn to_expr_result(&self) -> ExprResult {
         self.clone()
     }
 }
-
-impl PrimOps for Expr {
-    fn to_ex(&self) -> Ex {
-        self.into()
-    }
-}
-
-impl PrimOps for Var {
-    fn to_ex(&self) -> Ex {
-        self.into()
-    }
-}
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
index 344b1ab51..f92a0945e 100644
--- a/rust/src/lib.rs
+++ b/rust/src/lib.rs
@@ -95,8 +95,8 @@
 //! its siblings match their operand types and fold constants as C++ `add` 
does, and the math
 //! functions ([`ir::prim::op::floor`], [`ir::prim::op::exp`], ...) build the 
same calls. They
 //! take expressions or literals typed as C++ types them and build at the 
unknown location (the
-//! `_at` variants take one), and an [`ir::prim::op::Ex`] gives them operator 
syntax:
-//! `floordiv(&x + 1, 4)?` is the C++ `floordiv(x + 1, 4)`. An `Ex` passes 
into any function or
+//! `_at` variants take one), and an [`ir::prim::op::ExprResult`] gives them 
operator syntax:
+//! `floordiv(&x + 1, 4)?` is the C++ `floordiv(x + 1, 4)`. An `ExprResult` 
passes into any function or
 //! constructor that takes an [`ir::IntoExpr`], as any expression does.
 //!
 //! Operators and types are defined from Rust in a 
[`tvm_ffi::static_init_block!`], as C++ defines
@@ -305,6 +305,7 @@ impl Drop for ExitOnDrop {
 pub mod backend;
 pub mod codegen;
 pub mod ir;
+mod r#macro;
 pub mod runtime;
 pub mod script;
 pub mod sym;
diff --git a/rust/src/macro.rs b/rust/src/macro.rs
new file mode 100644
index 000000000..d4ac44359
--- /dev/null
+++ b/rust/src/macro.rs
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+//! Macros for defining Rust IR bindings.
+
+mod def_expr_op_overloads;
diff --git a/rust/src/macro/def_expr_op_overloads.rs 
b/rust/src/macro/def_expr_op_overloads.rs
new file mode 100644
index 000000000..4b38b1185
--- /dev/null
+++ b/rust/src/macro/def_expr_op_overloads.rs
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+//! Expression operators for existing reference wrappers.
+
+/// Add primitive-expression operators to an existing reference wrapper.
+///
+/// Pass a module-scope path to a type implementing `Clone` and `Into<Expr>`. 
Define its object
+/// layout, reference wrapper and ancestor upcasts separately, using 
`tvm_ffi::def_object_ref!` or
+/// generated bindings. For a deeper parent, also provide the upcast to `Expr`.
+///
+/// Owned operands move their handle; borrowed operands clone the handle. 
Operators return
+/// [`ExprResult`](crate::ir::prim::ExprResult), preserving contextual literal 
typing and the
+/// first construction error. Import [`PrimOps`](crate::ir::prim::PrimOps) for 
named operations
+/// such as `floordiv` and `less`. Assignment operators apply only to 
`ExprResult`.
+///
+/// ```text
+/// tvm_ffi::def_object_ref!(pub MyExpr, Expr, MyExprObj);
+/// tvm_tirx::def_expr_op_over_loads!(MyExpr);
+/// ```
+#[macro_export]
+macro_rules! def_expr_op_over_loads {
+    (crate::$($ty:ident)::+ $(,)?) => {
+        $crate::def_expr_op_over_loads!(@impl crate::$($ty)::+);
+    };
+    (self::$($ty:ident)::+ $(,)?) => {
+        $crate::def_expr_op_over_loads!(@impl self::$($ty)::+);
+    };
+    (super::$($ty:ident)::+ $(,)?) => {
+        $crate::def_expr_op_over_loads!(@impl super::$($ty)::+);
+    };
+    (::$($ty:ident)::+ $(,)?) => {
+        $crate::def_expr_op_over_loads!(@impl ::$($ty)::+);
+    };
+    ($($ty:ident)::+ $(,)?) => {
+        // Anchor caller names before introducing generic operator parameters.
+        $crate::def_expr_op_over_loads!(@impl self::$($ty)::+);
+    };
+    (@impl $ty:ty) => {
+        impl ::core::convert::From<$ty> for $crate::ir::prim::ExprResult {
+            fn from(value: $ty) -> Self {
+                Self::from(::core::result::Result::<$crate::ir::Expr, 
$crate::tvm_ffi::Error>::Ok(
+                    ::core::convert::Into::<$crate::ir::Expr>::into(value),
+                ))
+            }
+        }
+
+        impl ::core::convert::From<&$ty> for $crate::ir::prim::ExprResult {
+            fn from(value: &$ty) -> Self {
+                Self::from(<$ty as ::core::clone::Clone>::clone(value))
+            }
+        }
+
+        impl $crate::ir::prim::PrimOps for $ty {
+            fn to_expr_result(&self) -> $crate::ir::prim::ExprResult {
+                ::core::convert::From::from(self)
+            }
+        }
+
+        $crate::def_expr_op_over_loads!(@binary $ty;
+            Add::add, Sub::sub, Mul::mul, BitAnd::bitand, BitOr::bitor,
+            BitXor::bitxor, Shl::shl, Shr::shr
+        );
+        $crate::def_expr_op_over_loads!(@unary $ty; Neg::neg, Not::not);
+    };
+    (@binary $ty:ty; $($trait:ident::$method:ident),+ $(,)?) => {
+        $(
+            impl<R: $crate::ir::prim::Operand> ::core::ops::$trait<R> for $ty {
+                type Output = $crate::ir::prim::ExprResult;
+                fn $method(self, rhs: R) -> Self::Output {
+                    <Self::Output as ::core::ops::$trait<R>>::$method(
+                        ::core::convert::From::from(self), rhs,
+                    )
+                }
+            }
+
+            impl<R: $crate::ir::prim::Operand> ::core::ops::$trait<R> for &$ty 
{
+                type Output = $crate::ir::prim::ExprResult;
+                fn $method(self, rhs: R) -> Self::Output {
+                    <Self::Output as ::core::ops::$trait<R>>::$method(
+                        ::core::convert::From::from(self), rhs,
+                    )
+                }
+            }
+
+            $crate::def_expr_op_over_loads!(@literal $ty, $trait::$method;
+                ::core::primitive::i32, ::core::primitive::i64,
+                ::core::primitive::f32, ::core::primitive::f64);
+        )+
+    };
+    (@literal $ty:ty, $trait:ident::$method:ident; $($literal:ty),+ $(,)?) => {
+        $(
+            impl ::core::ops::$trait<$ty> for $literal {
+                type Output = $crate::ir::prim::ExprResult;
+                fn $method(self, rhs: $ty) -> Self::Output {
+                    <Self as ::core::ops::$trait<Self::Output>>::$method(
+                        self, ::core::convert::From::from(rhs),
+                    )
+                }
+            }
+
+            impl ::core::ops::$trait<&$ty> for $literal {
+                type Output = $crate::ir::prim::ExprResult;
+                fn $method(self, rhs: &$ty) -> Self::Output {
+                    <Self as ::core::ops::$trait<Self::Output>>::$method(
+                        self, ::core::convert::From::from(rhs),
+                    )
+                }
+            }
+        )+
+    };
+    (@unary $ty:ty; $($trait:ident::$method:ident),+ $(,)?) => {
+        $(
+            impl ::core::ops::$trait for $ty {
+                type Output = $crate::ir::prim::ExprResult;
+                fn $method(self) -> Self::Output {
+                    <Self::Output as ::core::ops::$trait>::$method(
+                        ::core::convert::From::from(self),
+                    )
+                }
+            }
+
+            impl ::core::ops::$trait for &$ty {
+                type Output = $crate::ir::prim::ExprResult;
+                fn $method(self) -> Self::Output {
+                    <Self::Output as ::core::ops::$trait>::$method(
+                        ::core::convert::From::from(self),
+                    )
+                }
+            }
+        )+
+    };
+}
diff --git a/rust/src/tirx/op.rs b/rust/src/tirx/op.rs
index 3ce71436a..7ea2f6bd0 100644
--- a/rust/src/tirx/op.rs
+++ b/rust/src/tirx/op.rs
@@ -35,10 +35,9 @@
 // tvm-ffi-stubgen(import-object): crate::ir::expr::FloatImm
 // tvm-ffi-stubgen(import-object): crate::ir::expr::StringImmObj
 // tvm-ffi-stubgen(import-object): crate::ir::location::UNKNOWN_LOC
-// tvm-ffi-stubgen(import-object): crate::ir::prim::op::Ex
+// tvm-ffi-stubgen(import-object): crate::ir::prim::op::ExprResult
 // tvm-ffi-stubgen(import-object): crate::ir::prim::op::PrimOps
 // tvm-ffi-stubgen(import-object): crate::ir::prim::op::add
-// tvm-ffi-stubgen(import-object): crate::ir::prim::op::ex
 // tvm-ffi-stubgen(import-object): crate::ir::prim::op::exp
 // tvm-ffi-stubgen(import-object): crate::ir::prim::op::log
 // tvm-ffi-stubgen(import-object): crate::ir::prim::op::match_types_at
@@ -63,10 +62,9 @@ use crate::ir::expr::IntoExpr;
 use crate::ir::expr::StringImmObj;
 use crate::ir::location::Location;
 use crate::ir::location::UNKNOWN_LOC;
-use crate::ir::prim::op::Ex;
+use crate::ir::prim::op::ExprResult;
 use crate::ir::prim::op::PrimOps;
 use crate::ir::prim::op::add;
-use crate::ir::prim::op::ex;
 use crate::ir::prim::op::exp;
 use crate::ir::prim::op::log;
 use crate::ir::prim::op::match_types_at;
@@ -308,8 +306,9 @@ pub fn reinterpret_at(
 pub fn fast_erf_float_expr(arg: impl IntoExpr, bits: u8) -> Result<Expr> {
     let dtype = PrimType::float(bits).dtype;
     // The C++ coefficients are `float` literals.
-    let constant =
-        |value: f32| -> Ex { FloatImm::from_dtype(dtype, f64::from(value), 
UNKNOWN_LOC).into() };
+    let constant = |value: f32| -> ExprResult {
+        FloatImm::from_dtype(dtype, f64::from(value), UNKNOWN_LOC).into()
+    };
     // alpha_1, alpha_3, ..., alpha_13: the odd coefficients of the numerator.
     #[allow(clippy::excessive_precision)]
     let alpha: [f32; 7] = [
@@ -330,7 +329,7 @@ pub fn fast_erf_float_expr(arg: impl IntoExpr, bits: u8) -> 
Result<Expr> {
         -2.13374055278905e-04,
         -1.45660718464996e-05,
     ];
-    let x = ex(arg.into_expr()?).min(constant(4.0)).max(constant(-4.0));
+    let x = arg.into_expr()?.min(constant(4.0)).max(constant(-4.0));
     let x2 = &x * &x;
     // Horner's rule from the highest coefficient down, as C++ writes it out.
     let mut p = &x2 * constant(alpha[6]) + constant(alpha[5]);
@@ -342,7 +341,7 @@ pub fn fast_erf_float_expr(arg: impl IntoExpr, bits: u8) -> 
Result<Expr> {
     for coefficient in beta[..3].iter().rev() {
         q = &x2 * q + constant(*coefficient);
     }
-    p.div(q).expr()
+    p.div(q).try_into()
 }
 
 /// A return from the current GPU thread, without a value 
(`tirx.gpu_thread_return`).
diff --git a/rust/tests/ir/prim/op.rs b/rust/tests/ir/prim/op.rs
index d19061a41..0179290ae 100644
--- a/rust/tests/ir/prim/op.rs
+++ b/rust/tests/ir/prim/op.rs
@@ -20,10 +20,11 @@
 //! Tests of `ir::prim::op`: the type-promoting operators and the math 
functions written in Rust,
 //! against the native functions they follow (`prim._OpAdd` and the like). 
Every binary operator
 //! runs on every pair of a grid of operands, which must give the same 
expression or an error of
-//! the same kind with the same message. Literals are typed, and `-`, `!` and 
an `Ex` chain build,
+//! the same kind with the same message. Literals are typed, and `-`, `!` and 
an `ExprResult` chain build,
 //! as in C++.
 
-use tvm_tirx::ir::prim::op::{self, ex, Ex, PrimOps};
+use tvm_tirx::ir::prim::op::{self, ExprResult, PrimOps};
+use tvm_tirx::ir::IntoExpr;
 use tvm_tirx::ir::prim::{self, Add, Broadcast, BroadcastObj, Cast, CastObj, 
Not, Ramp, RampObj, Sub};
 use tvm_tirx::ir::{
     Call, Expr, FloatImm, IntImm, Location, Op, PrimType, Range, SourceLoc, 
SourceName, StringImm,
@@ -79,8 +80,8 @@ fn native1(name: &str, x: &Expr) -> Result<Any> {
     global(name).call_tuple((x.clone(), Location::unknown()))
 }
 
-fn same(built: Ex, expected: &Expr) -> bool {
-    structural_equal(&built.expr().unwrap(), expected)
+fn same(built: ExprResult, expected: &Expr) -> bool {
+    structural_equal(&built.into_expr().unwrap(), expected)
 }
 
 /// The cases where the Rust operator and the native one disagree, and how 
many were compared.
@@ -215,7 +216,7 @@ fn operands() -> Vec<Expr> {
     operands
 }
 
-type Binary = fn(&Expr, &Expr) -> Ex;
+type Binary = fn(&Expr, &Expr) -> ExprResult;
 
 #[test]
 fn binary_operators_agree_with_cpp() {
@@ -257,7 +258,7 @@ fn binary_operators_agree_with_cpp() {
             for b in &operands {
                 let native = native_op.call_tuple((a.clone(), b.clone(), 
Location::unknown()));
                 let what = || format!("{name}({}, {})", repr(a), repr(b));
-                outcome.check(what, operator(a, b).expr(), native);
+                outcome.check(what, operator(a, b).into_expr(), native);
             }
         }
         outcome.assert_agrees(name);
@@ -278,7 +279,7 @@ fn unary_and_ternary_operators_agree_with_cpp() {
     let bitwise_not = global("prim.BitwiseNot");
     for a in &operands {
         let native = bitwise_not.call_tuple((a.clone(), Location::unknown()));
-        match a.bitwise_not().expr() {
+        match a.bitwise_not().into_expr() {
             Ok(rust) => outcome.check(|| format!("~{}", repr(a)), Ok(rust), 
native),
             Err(_) => assert!(native.is_err(), "~{}: only Rust rejects it", 
repr(a)),
         }
@@ -302,7 +303,7 @@ fn unary_and_ternary_operators_agree_with_cpp() {
                 let native =
                     if_then_else.call_tuple((c.clone(), t.clone(), f.clone(), 
Location::unknown()));
                 let what = || format!("if_then_else({}, {}, {})", repr(c), 
repr(t), repr(f));
-                outcome.check(what, c.if_then_else(t, f).expr(), native);
+                outcome.check(what, c.if_then_else(t, f).into_expr(), native);
             }
         }
     }
@@ -318,19 +319,19 @@ fn literals_and_compound_expressions_agree_with_cpp() {
     let mut outcome = Outcome::default();
     outcome.check(
         || "x + 3".into(),
-        (ex(&x) + 3).expr(),
+        (&x + 3).into_expr(),
         add.call_tuple((x.clone(), imm("int64", 3), Location::unknown())),
     );
     outcome.check(
         || "3 + x".into(),
-        (3i32 + &x).expr(),
+        (3i32 + &x).into_expr(),
         add.call_tuple((imm("int64", 3), x.clone(), Location::unknown())),
     );
     let floordiv = global("prim._OpFloorDiv");
-    let rust = ((ex(&x) + 0) * 1).floordiv(1);
+    let rust = ((&x + 0) * 1).floordiv(1);
     outcome.check(
         || "((x + 0) * 1) // 1".into(),
-        rust.expr(),
+        rust.into_expr(),
         floordiv.call_tuple((x.clone(), imm("int64", 1), Location::unknown())),
     );
     // Unary minus folds a constant whose negation fits its type and is `0 - 
x` otherwise.
@@ -340,7 +341,7 @@ fn literals_and_compound_expressions_agree_with_cpp() {
         let ty: PrimType = value.ty.clone().try_cast().unwrap();
         let zero = op::make_const(ty, 0).unwrap();
         let native = sub.call_tuple((zero, value.clone(), 
Location::unknown()));
-        outcome.check(|| format!("-{}", repr(&value)), (-&value).expr(), 
native);
+        outcome.check(|| format!("-{}", repr(&value)), (-&value).into_expr(), 
native);
     }
     outcome.assert_agrees("literals");
 }
@@ -381,9 +382,9 @@ fn literals_are_typed_as_in_cpp() {
     ));
 }
 
-/// An `Ex` passes into the free functions and the constructors, which report 
its error.
+/// An `ExprResult` passes into the free functions and the constructors, which 
report its error.
 #[test]
-fn an_ex_passes_into_functions_and_constructors() {
+fn expression_results_pass_into_functions_and_constructors() {
     let _serial = serial();
     let x = var("x", PrimType::int(32));
     // C++ `floordiv(x + 1, 4)`.
@@ -405,7 +406,7 @@ fn an_ex_passes_into_functions_and_constructors() {
     // The first error of the chain is what the function or constructor 
reports.
     let f = var("f", PrimType::float(32));
     let failed = f.truncdiv(&x) + 1;
-    let expected = failed.clone().expr().err().unwrap();
+    let expected = failed.clone().into_expr().err().unwrap();
     for error in [
         op::floordiv(failed.clone(), 4).err().unwrap(),
         op::floordiv(4, &failed).err().unwrap(),
@@ -448,14 +449,14 @@ fn unary_operators_follow_neg_and_logical_not() {
     let p = var("p", PrimType::bool());
     // `-x` is `MakeConst(x.ty(), 0) - x`; a constant is negated in place when 
the result fits.
     assert!(same(-&x, &native2("prim._OpSub", imm("int32", 0), &x)));
-    assert!(same(-ex(imm("int32", 5)), &imm("int32", -5)));
+    assert!(same(-imm("int32", 5), &imm("int32", -5)));
     let min = imm("int64", i64::MIN);
-    assert!(same(-ex(&min), &native2("prim._OpSub", imm("int64", 0), &min)));
-    assert!(same(-ex(fimm("float32", 0.0)), &fimm("float32", -0.0)));
+    assert!(same(-&min, &native2("prim._OpSub", imm("int64", 0), &min)));
+    assert!(same(-fimm("float32", 0.0), &fimm("float32", -0.0)));
     // `!p` is the raw `Not`; a constant folds.
     let not_p = Not::new(&p, UNKNOWN_LOC).unwrap();
     assert!(same(!&p, &not_p.into()));
-    assert!(same(!ex(imm("bool", 1)), &imm("bool", 0)));
+    assert!(same(!imm("bool", 1), &imm("bool", 0)));
 }
 
 #[test]
@@ -464,7 +465,7 @@ fn select_folding_and_the_first_error() {
     let x = var("x", PrimType::int(32));
     let r = var("r", PrimType::int(32));
     // `select` is the raw constructor, built from operands under construction.
-    let built = r.greater_equal(0).select(&r, &r - 1).expr().unwrap();
+    let built = r.greater_equal(0).select(&r, &r - 1).into_expr().unwrap();
     let cond = native2("prim._OpGE", &r, imm("int32", 0));
     let minus = native2("prim._OpSub", &r, imm("int32", 1));
     let expected: Expr = global("prim.Select")
@@ -476,11 +477,11 @@ fn select_folding_and_the_first_error() {
     // The raw `Sub` does not fold; the operator does.
     let raw = Sub::new(3, 1, UNKNOWN_LOC).unwrap();
     assert!(!structural_equal(&Expr::from(raw), &imm("int32", 2)));
-    assert!(same(ex(imm("int32", 3)) - 1, &imm("int32", 2)));
+    assert!(same(imm("int32", 3) - 1, &imm("int32", 2)));
 
     // Integer division of a float raises natively; later steps pass the error 
through.
     let f = var("f", PrimType::float(32));
-    let Err(error) = (f.truncdiv(&x) + 1).less(&x).expr() else { 
panic!("expected an error") };
+    let Err(error) = (f.truncdiv(&x) + 1).less(&x).into_expr() else { 
panic!("expected an error") };
     let direct = global("prim._OpTruncDiv").call_tuple((
         Expr::from(&f),
         Expr::from(&x),
@@ -491,7 +492,7 @@ fn select_folding_and_the_first_error() {
     assert_eq!(error.message(), direct.message());
     // A literal next to a non-PrimType expression has no dtype to take: an 
error, not a panic.
     let string: Expr = StringImm::new("s", UNKNOWN_LOC).into();
-    assert!((ex(&string) + 1).expr().is_err());
+    assert!((&string + 1).into_expr().is_err());
 }
 
 /// Variables and constants of the types the math functions treat differently.
@@ -533,7 +534,7 @@ fn values() -> Vec<Expr> {
 #[test]
 fn unary_functions_agree_with_cpp() {
     let _serial = serial();
-    type Unary = fn(&Expr) -> Ex;
+    type Unary = fn(&Expr) -> ExprResult;
     let functions: [(&str, Unary); 11] = [
         ("prim.floor", |x| x.floor()),
         ("prim.ceil", |x| x.ceil()),
@@ -549,7 +550,7 @@ fn unary_functions_agree_with_cpp() {
     ];
     for (name, function) in functions {
         for x in values() {
-            agree(&format!("{name}({})", repr(&x)), function(&x).expr(), 
native1(name, &x));
+            agree(&format!("{name}({})", repr(&x)), function(&x).into_expr(), 
native1(name, &x));
         }
     }
     // `round` folds a tie to even, as `std::nearbyint` does.
@@ -616,7 +617,7 @@ fn cast_agrees_with_cpp() {
             let ty = prim(name);
             let native =
                 global("prim._cast").call_tuple((x.clone(), ty.clone(), 
Location::unknown()));
-            agree(&format!("cast({}, {name})", repr(x)), x.cast(ty).expr(), 
native);
+            agree(&format!("cast({}, {name})", repr(x)), 
x.cast(ty).into_expr(), native);
         }
     }
 }
@@ -674,7 +675,7 @@ fn pow_and_fmod_agree_with_cpp() {
     ];
     for (x, y) in &pairs {
         let native = global("prim._OpPow").call_tuple((x.clone(), y.clone(), 
Location::unknown()));
-        agree(&format!("pow({}, {})", repr(x), repr(y)), x.pow(y).expr(), 
native);
+        agree(&format!("pow({}, {})", repr(x), repr(y)), x.pow(y).into_expr(), 
native);
     }
     // `fmod` has no global: the sides are matched and the call is typed as 
them.
     let (h, f) = (prim_var("float16"), prim_var("float32"));
@@ -683,8 +684,8 @@ fn pow_and_fmod_agree_with_cpp() {
     let ty: Type = PrimType::float(32).into();
     let expected =
         Call::new(Some(&ty), fmod, [widened, f.clone()], None, 
Vec::<Type>::new(), UNKNOWN_LOC);
-    assert!(structural_equal(&h.fmod(&f).expr().unwrap(), &expected.unwrap()));
-    let error = prim_var("int32").fmod(imm("int32", 2)).expr().err().unwrap();
+    assert!(structural_equal(&h.fmod(&f).into_expr().unwrap(), 
&expected.unwrap()));
+    let error = prim_var("int32").fmod(imm("int32", 
2)).into_expr().err().unwrap();
     assert!(error.message().contains("fmod only applies to float"), "{}", 
error.message());
 }
 
@@ -709,7 +710,7 @@ fn intrinsics_are_typed_calls() {
     let i = prim_var("int32");
     assert!(same(i.exp(), &call("int32", "prim.exp", vec![i.clone()])));
     assert!(same(i.clz(), &call("int32", "prim.clz", vec![i.clone()])));
-    let error = i.tan().expr().err().unwrap();
+    let error = i.tan().into_expr().err().unwrap();
     assert_eq!(error.kind(), TYPE_ERROR);
     assert!(error
         .message()
@@ -730,9 +731,12 @@ fn intrinsics_are_typed_calls() {
     assert!(same(x.copysign(&h), &call("float32", "prim.copysign", 
vec![x.clone(), h.clone()])));
     let fma = op::fma(&x, &h, &x).unwrap();
     assert!(structural_equal(&fma, &call("float32", "prim.fma", 
vec![x.clone(), h, x.clone()])));
-    assert!(same(ex(op::vscale()), &call("int32", "prim.vscale", vec![])));
+    assert!(same(ExprResult::from(op::vscale()), &call("int32", "prim.vscale", 
vec![])));
     // Two `vscale()` calls are equal: a bool, as every comparison folds to.
-    assert!(same(ex(op::vscale()).equal(ex(op::vscale())), &imm("bool", 1)));
+    assert!(same(
+        ExprResult::from(op::vscale()).equal(ExprResult::from(op::vscale())),
+        &imm("bool", 1)
+    ));
 }
 
 /// C++ `cast(value, t)` through its global.
@@ -755,7 +759,7 @@ fn cast_folds_and_broadcasts_as_cpp() {
     let (constant, overflowing) = (ramp(imm("int32", 0)), ramp(imm("int32", 
i32::MAX.into())));
 
     // A value already of the type is itself.
-    
assert!(x.cast(PrimType::int(32)).expr().unwrap().same_as(&Expr::from(&x)));
+    
assert!(x.cast(PrimType::int(32)).into_expr().unwrap().same_as(&Expr::from(&x)));
 
     let cases: Vec<(Expr, &str)> = vec![
         // A constant folds to the new type when it is the same value there; 
one the type cannot
@@ -783,7 +787,7 @@ fn cast_folds_and_broadcasts_as_cpp() {
         assert!(same(value.cast(prim(name)), &native_cast(value, name)), 
"{name}");
     }
     assert!(same(imm("int32", 5).cast(prim("int64")), &imm("int64", 5)));
-    let node = |value: &Expr, name: &str| 
value.cast(prim(name)).expr().unwrap();
+    let node = |value: &Expr, name: &str| 
value.cast(prim(name)).into_expr().unwrap();
     let broadcast = node(&Expr::from(&f), "int32x4");
     let element = &broadcast.as_node::<BroadcastObj>().unwrap().value;
     assert!(element.as_node::<CastObj>().is_some());
@@ -802,7 +806,9 @@ fn cast_folds_and_broadcasts_as_cpp() {
 
     // A void value cannot be cast, with the C++ error.
     let void = var("v", PrimType::void());
-    let Err(error) = void.cast(PrimType::int(32)).expr() else { panic!("a void 
value was cast") };
+    let Err(error) = void.cast(PrimType::int(32)).into_expr() else {
+        panic!("a void value was cast")
+    };
     assert_eq!(error.kind(), TYPE_ERROR);
     assert!(error.message().contains("void sentinel type"), "{}", 
error.message());
 }

Reply via email to