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 16e90115a [FEAT][RUST] Add PrimExpr and IntExpr typed expression views 
(#167)
16e90115a is described below

commit 16e90115a605c8ba8767a064e082ed51e17bf853
Author: Linzhang Li <[email protected]>
AuthorDate: Mon Nov 2 18:46:15 2026 -0500

    [FEAT][RUST] Add PrimExpr and IntExpr typed expression views (#167)
    
    ## Summary
    
    C++ `PrimExpr` and `IntExpr` are typed views: an `Expr` whose `ty` is a
    `PrimType`, or a scalar
    integer type for `IntExpr`. They have no node class of their own; the
    type is checked where an
    expression is converted into the view. The Rust crate bound C++
    `PrimExpr` fields as `Expr`,
    checked the operand types inside each operator, and had `sig::PrimExpr`
    and `sig::IntExpr` as
    marker types for operator signatures only.
    
    This PR adds `tvm_tirx::ir::PrimExpr` and `IntExpr` as reference types.
    An expression is checked
    once, where it becomes a view, and the type then holds for every use.
    
    Before:
    
    ```rust
    // `index` may be any expression: the function checks its type, and so does 
every operator.
    fn next_index(index: &Expr) -> Result<(Expr, DLDataType)> {
        let next = Expr::try_from(index + 1)?;
        let Some(ty) = next.ty.as_node::<PrimTypeObj>() else {
            return Err(Error::new(TYPE_ERROR, "expected a PrimExpr", ""));
        };
        let dtype = ty.dtype;
        Ok((next, dtype))
    }
    
    let i = Expr::from(var);
    let load = TensorLoad::new(&a, [&i], UNKNOWN_LOC)?; // load.indices: 
Array<Expr>
    let (next, dtype) = next_index(&i)?;
    ```
    
    After:
    
    ```rust
    // The signature says what `index` is, and the operators build a `PrimExpr`.
    fn next_index(index: &IntExpr) -> Result<(PrimExpr, DLDataType)> {
        let next = PrimExpr::try_from(index + 1)?;
        let dtype = next.dtype();
        Ok((next, dtype))
    }
    
    let i: IntExpr = Expr::from(var).try_into()?; // checked here, once
    let load = TensorLoad::new(&a, [&i], UNKNOWN_LOC)?; // load.indices: 
Array<PrimExpr>
    let (next, dtype) = next_index(&i)?;
    ```
    
    The `?` on `PrimExpr::try_from(index + 1)` reports only the errors of
    the operators, such as
    mismatched vector lanes; the type of the result needs no check.
---
 rust/src/ir/expr.rs    | 466 ++++++++++++++++++++++++++++++++++++++++++++++---
 rust/src/ir/op_def.rs  |  57 +-----
 rust/src/ir/prim/op.rs |  80 ++++++---
 rust/tests/ir/expr.rs  | 126 ++++++++++++-
 4 files changed, 627 insertions(+), 102 deletions(-)

diff --git a/rust/src/ir/expr.rs b/rust/src/ir/expr.rs
index bad821470..4054d68f6 100644
--- a/rust/src/ir/expr.rs
+++ b/rust/src/ir/expr.rs
@@ -38,8 +38,24 @@
 // tvm-ffi-stubgen(custom-new): ir.Var
 // tvm-ffi-stubgen(custom-new): ir.LambdaExpr
 // tvm-ffi-stubgen(custom-new): ir.Range
+// Constants and loads are typed by a `PrimType`: their allocators take one, 
and they convert into
+// a `PrimExpr` without a check.
+// tvm-ffi-stubgen(field): ir.IntImm.ty -> PrimType
+// tvm-ffi-stubgen(field): ir.FloatImm.ty -> PrimType
+// tvm-ffi-stubgen(field): ir.TensorLoad.ty -> PrimType
+// tvm-ffi-stubgen(field): ir.TensorLoad.indices -> Array<PrimExpr>
+// tvm-ffi-stubgen(upcast): ir.IntImm -> PrimExpr
+// tvm-ffi-stubgen(upcast): ir.FloatImm -> PrimExpr
+// tvm-ffi-stubgen(upcast): ir.TensorLoad -> PrimExpr
 // Names the hand-written wrappers use: the import section keeps them even 
when no generated block
 // does.
+// tvm-ffi-stubgen(import-object): crate::ir::prim::op::ExprResult
+// tvm-ffi-stubgen(import-object): crate::ir::r#type::PrimTypeObj
+// tvm-ffi-stubgen(import-object): tvm_ffi::Error
+// tvm-ffi-stubgen(import-object): tvm_ffi::ObjectRefCast
+// tvm-ffi-stubgen(import-object): tvm_ffi::object::is_instance_of
+// tvm-ffi-stubgen(import-object): tvm_ffi::tvm_ffi_sys::TVMFFIAny
+// tvm-ffi-stubgen(import-object): tvm_ffi::type_traits::AnyCompatible
 // tvm-ffi-stubgen(import-object): tvm_ffi::AnyView
 // tvm-ffi-stubgen(import-object): tvm_ffi::ObjectRefCore
 // tvm-ffi-stubgen(import-object): tvm_ffi::Result
@@ -66,12 +82,14 @@ use crate::ir::location::Location;
 use crate::ir::location::UnknownLoc;
 use crate::ir::op::OpObj;
 use crate::ir::prim::AddObj;
+use crate::ir::prim::op::ExprResult;
 use crate::ir::r#type::AnyType;
 use crate::ir::r#type::FuncType;
 use crate::ir::r#type::FuncTypeObj;
 use crate::ir::r#type::MissingType;
 use crate::ir::r#type::MissingTypeObj;
 use crate::ir::r#type::PrimType;
+use crate::ir::r#type::PrimTypeObj;
 use crate::ir::r#type::StringType;
 use crate::ir::r#type::TupleType;
 use crate::ir::r#type::TupleTypeObj;
@@ -84,15 +102,20 @@ use tvm_ffi::AnyView;
 use tvm_ffi::Array;
 use tvm_ffi::BigInt;
 use tvm_ffi::DLDataType;
+use tvm_ffi::Error;
 use tvm_ffi::Object;
 use tvm_ffi::ObjectArc;
+use tvm_ffi::ObjectRefCast;
 use tvm_ffi::ObjectRefCore;
 use tvm_ffi::Result;
 use tvm_ffi::ShallowCopy;
 use tvm_ffi::String;
 use tvm_ffi::UnsafeInit;
+use tvm_ffi::object::is_instance_of;
 use tvm_ffi::reflection::drop_if_defined;
+use tvm_ffi::tvm_ffi_sys::TVMFFIAny;
 use tvm_ffi::tvm_ffi_sys::TVMFFITypeIndex;
+use tvm_ffi::type_traits::AnyCompatible;
 // tvm-ffi-stubgen(end)
 
 // tvm-ffi-stubgen(begin): object/ir.Expr
@@ -330,6 +353,396 @@ unsafe impl UnsafeInit for OpaqueExprObj {
 tvm_ffi::impl_object_upcast!(OpaqueExpr => Expr);
 // tvm-ffi-stubgen(end)
 
+// 
---------------------------------------------------------------------------------------------
+// Typed views: `PrimExpr` and `IntExpr` (C++ `TypedExpr`)
+
+/// Define a view: a reference to an `ExprObj` whose type `$accepts` accepts, 
named `$type_str`
+/// like the C++ type, which an `Any` holding no expression converts into by 
`$fallback`.
+macro_rules! typed_expr_view {
+    (
+        $(#[$meta:meta])*
+        $name:ident, $type_str:literal, accepts = $accepts:path, fallback = 
$fallback:path
+    ) => {
+        $(#[$meta])*
+        #[repr(transparent)]
+        #[derive(Clone)]
+        pub struct $name {
+            base: ObjectArc<ExprObj>,
+        }
+
+        // SAFETY: `base` is the reference; `from_data` requires its caller to 
have checked it.
+        unsafe impl ObjectRefCore for $name {
+            type ContainerType = ExprObj;
+
+            #[inline]
+            fn data(this: &Self) -> &ObjectArc<ExprObj> {
+                &this.base
+            }
+
+            #[inline]
+            fn into_data(this: Self) -> ObjectArc<ExprObj> {
+                this.base
+            }
+
+            /// The view of `data`.
+            ///
+            /// # Safety
+            ///
+            /// The type of `data` is one the view accepts.
+            #[inline]
+            unsafe fn from_data(data: ObjectArc<ExprObj>) -> Self {
+                Self { base: data }
+            }
+        }
+
+        // SAFETY: a view is an `Expr` whose `check_any_strict` accepts fewer 
values; the other
+        // conversions are the `Expr` ones.
+        unsafe impl AnyCompatible for $name {
+            #[inline]
+            unsafe fn is_undefined(field: *const Self) -> bool {
+                unsafe { 
ObjectArc::is_null(::core::ptr::addr_of!((*field).base)) }
+            }
+
+            fn type_str() -> std::string::String {
+                $type_str.into()
+            }
+
+            fn get_mismatch_type_info(data: &TVMFFIAny) -> std::string::String 
{
+                typed_expr_mismatch(data)
+            }
+
+            #[inline(always)]
+            unsafe fn copy_to_any_view(src: &Self, data: &mut TVMFFIAny) {
+                unsafe { <Expr as 
AnyCompatible>::copy_to_any_view(src.as_expr(), data) }
+            }
+
+            #[inline(always)]
+            unsafe fn move_to_any(src: Self, data: &mut TVMFFIAny) {
+                unsafe { <Expr as AnyCompatible>::move_to_any(src.into(), 
data) }
+            }
+
+            #[inline(always)]
+            unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
+                unsafe { prim_dtype(data) }.is_some_and($accepts)
+            }
+
+            unsafe fn copy_from_any_view_after_check(data: &TVMFFIAny) -> Self 
{
+                let expr = unsafe { <Expr as 
AnyCompatible>::copy_from_any_view_after_check(data) };
+                Self { base: ObjectRefCore::into_data(expr) }
+            }
+
+            unsafe fn move_from_any_after_check(data: &mut TVMFFIAny) -> Self {
+                let expr = unsafe { <Expr as 
AnyCompatible>::move_from_any_after_check(data) };
+                Self { base: ObjectRefCore::into_data(expr) }
+            }
+
+            unsafe fn try_cast_from_any_view(data: &TVMFFIAny) -> 
std::result::Result<Self, ()> {
+                unsafe {
+                    if Self::check_any_strict(data) {
+                        Ok(Self::copy_from_any_view_after_check(data))
+                    } else {
+                        $fallback(data).ok_or(())
+                    }
+                }
+            }
+        }
+
+        tvm_ffi::impl_try_from_any!($name);
+        tvm_ffi::impl_arg_into_ref!($name);
+        tvm_ffi::impl_into_arg_holder_default!($name);
+        crate::def_expr_op_over_loads!($name);
+
+        impl From<&$name> for $name {
+            fn from(value: &$name) -> Self {
+                value.clone()
+            }
+        }
+
+        /// The expression object, to read, as [`Expr`] dereferences to it.
+        impl Deref for $name {
+            type Target = ExprObj;
+
+            fn deref(&self) -> &ExprObj {
+                &self.base
+            }
+        }
+
+        impl $name {
+            /// This expression as an [`Expr`], borrowed.
+            #[inline]
+            pub fn as_expr(&self) -> &Expr {
+                // SAFETY: `Expr` is a `repr(C)` struct of the one 
`ObjectArc<ExprObj>` the view
+                // holds, and every value of the view is an `Expr`.
+                unsafe { &*(self as *const Self as *const Expr) }
+            }
+
+            /// The type of this expression (C++ `PrimExpr::ty()`), which the 
view guarantees.
+            pub fn ty(&self) -> PrimType {
+                self.base.ty.clone().try_cast().expect("a view is typed by a 
`PrimType`")
+            }
+
+            /// The dtype of this expression's type.
+            pub fn dtype(&self) -> DLDataType {
+                let ty = self.base.ty.downcast_ref::<PrimTypeObj>();
+                ty.expect("a view is typed by a `PrimType`").dtype
+            }
+        }
+
+        /// The view of an expression whose type it accepts; a TypeError names 
any other.
+        impl TryFrom<Expr> for $name {
+            type Error = Error;
+
+            fn try_from(value: Expr) -> Result<Self> {
+                value.try_cast()
+            }
+        }
+
+        impl TryFrom<&Expr> for $name {
+            type Error = Error;
+
+            fn try_from(value: &Expr) -> Result<Self> {
+                value.clone().try_cast()
+            }
+        }
+    };
+}
+
+typed_expr_view! {
+    /// An expression typed by a `PrimType` (C++ `PrimExpr`): a constant, a 
variable, an operator
+    /// on scalars or vectors, or a call that returns one.
+    ///
+    /// `PrimExpr` and [`IntExpr`] are typed views (C++ `TypedExpr`). A view 
holds an `ir.Expr` as
+    /// [`Expr`] does, and adds what the object class does not say: the type 
of the expression. An
+    /// `ir.Call` is a `PrimExpr` when its `ty` is a `PrimType`, and an 
`IntExpr` when that type is
+    /// a scalar integer. The views are checked when they are made, so a 
function that takes one
+    /// needs no check of its own:
+    ///
+    /// - A view converts into the views and the [`Expr`] it refines for free 
(`From`): an
+    ///   `IntExpr` into a `PrimExpr` and both into an `Expr`. Borrowed, an 
`IntExpr` is a
+    ///   `PrimExpr` through `AsRef`, and both views read their object through 
`Deref`, as an
+    ///   `Expr` does.
+    /// - An `Expr` converts into a view only through a check (`TryFrom`,
+    ///   `ObjectRefCast::try_cast`), as an `Any` does. A failed check names 
the expression and its
+    ///   type, `ir.StringImm[ty=ir.StringType]`.
+    /// - [`IntImm`], [`FloatImm`] and [`TensorLoad`] are typed by a 
`PrimType` when they are made,
+    ///   so they convert into a `PrimExpr` for free. Rust literals convert as 
they convert into an
+    ///   `Expr`: an `i32` and an `f32` as C++ `PrimExpr(int32_t)` and 
`PrimExpr(float)` convert
+    ///   them.
+    /// - The operators build a `PrimExpr`: each step of an [`ExprResult`] is 
checked, so
+    ///   `PrimExpr::try_from(a + b)?` reports only the errors of the 
operators.
+    /// - The view holds its object privately and gives it out only to read, 
so its type cannot be
+    ///   changed; the one way past the check is the `unsafe` 
[`ObjectRefCore::from_data`].
+    ///
+    /// ```compile_fail
+    /// # use tvm_tirx::ir::{Expr, PrimExpr};
+    /// fn widen(expr: Expr) -> PrimExpr {
+    ///     expr // An `Expr` is not a `PrimExpr` until it is checked.
+    /// }
+    /// ```
+    ///
+    /// ```compile_fail
+    /// # use tvm_tirx::ir::{IntExpr, PrimExpr};
+    /// fn narrow(expr: PrimExpr) -> IntExpr {
+    ///     expr.into() // Nor is a `PrimExpr` an `IntExpr`.
+    /// }
+    /// ```
+    PrimExpr, "ir.PrimExpr", accepts = any_prim, fallback = prim_literal
+}
+
+typed_expr_view! {
+    /// A [`PrimExpr`] of a scalar signed or unsigned integer type of any 
width (C++ `IntExpr`),
+    /// such as an index or an extent.
+    IntExpr, "ir.IntExpr", accepts = scalar_int, fallback = int_literal
+}
+
+tvm_ffi::impl_object_upcast!(PrimExpr => Expr, IntExpr => Expr, IntExpr => 
PrimExpr);
+
+/// An `IntExpr` borrowed as the `PrimExpr` it is, as C++ `IntExpr` derives 
from `PrimExpr`.
+impl AsRef<PrimExpr> for IntExpr {
+    fn as_ref(&self) -> &PrimExpr {
+        // SAFETY: both views are `repr(transparent)` over an 
`ObjectArc<ExprObj>`, and every
+        // scalar integer type is a `PrimType`.
+        unsafe { &*(self as *const IntExpr as *const PrimExpr) }
+    }
+}
+
+impl TryFrom<PrimExpr> for IntExpr {
+    type Error = Error;
+
+    fn try_from(value: PrimExpr) -> Result<Self> {
+        value.try_cast()
+    }
+}
+
+impl TryFrom<&PrimExpr> for IntExpr {
+    type Error = Error;
+
+    fn try_from(value: &PrimExpr) -> Result<Self> {
+        value.clone().try_cast()
+    }
+}
+
+/// The built expression, checked to be an integer, or the first error raised 
while building it.
+impl TryFrom<ExprResult> for IntExpr {
+    type Error = Error;
+
+    fn try_from(value: ExprResult) -> Result<Self> {
+        PrimExpr::try_from(value)?.try_cast()
+    }
+}
+
+// Rust literals, as they convert into an `Expr`. C++ has the constructors 
`PrimExpr(int32_t)` and
+// `PrimExpr(float)`; the other literals are constants of their own type.
+
+/// An `i32` is an `int32` constant (C++ `PrimExpr(int32_t)`).
+impl From<i32> for PrimExpr {
+    fn from(value: i32) -> Self {
+        IntImm::int32(value).into()
+    }
+}
+
+/// An `i64` is an `int64` constant. (C++ converts an `int64_t` held by an 
`Any` to `int32` when
+/// the value fits.)
+impl From<i64> for PrimExpr {
+    fn from(value: i64) -> Self {
+        IntImm::int64(value).into()
+    }
+}
+
+/// A `bool` is a boolean constant, as C++ converts a `bool` held by an `Any`. 
(C++ promotes a
+/// `bool` argument to `PrimExpr(int32_t)`, an `int32` constant.)
+impl From<bool> for PrimExpr {
+    fn from(value: bool) -> Self {
+        IntImm::bool(value).into()
+    }
+}
+
+/// An `f32` is a `float32` constant (C++ `PrimExpr(float)`).
+impl From<f32> for PrimExpr {
+    fn from(value: f32) -> Self {
+        FloatImm::float32(value).into()
+    }
+}
+
+/// An `f64` is a `float64` constant, as C++ types a `double` next to an 
expression. (C++ converts
+/// a `double` held by an `Any` to `float32`.)
+impl From<f64> for PrimExpr {
+    fn from(value: f64) -> Self {
+        FloatImm::float64(value).into()
+    }
+}
+
+/// An `i32` is an `int32` constant. (C++ `IntExpr` converts only an int held 
by an `Any`, as
+/// `PrimExpr` does.)
+impl From<i32> for IntExpr {
+    fn from(value: i32) -> Self {
+        int_constant(IntImm::int32(value))
+    }
+}
+
+/// An `i64` is an `int64` constant.
+impl From<i64> for IntExpr {
+    fn from(value: i64) -> Self {
+        int_constant(IntImm::int64(value))
+    }
+}
+
+/// The `IntExpr` of an integer constant, which `int32` and `int64` constants 
are.
+fn int_constant(constant: IntImm) -> IntExpr {
+    debug_assert!(scalar_int(PrimExpr::from(&constant).dtype()));
+    // SAFETY: the constant is typed by a scalar integer type.
+    unsafe { 
IntExpr::from_data(ObjectRefCore::into_data(Expr::from(constant))) }
+}
+
+// The checks and conversions of the views.
+
+/// Every `PrimType`.
+fn any_prim(_: DLDataType) -> bool {
+    true
+}
+
+/// A scalar signed or unsigned integer type.
+fn scalar_int(dtype: DLDataType) -> bool {
+    dtype.lanes == 1 && matches!(dtype.code, support::INT | support::UINT)
+}
+
+/// The expression `data` holds, if it holds one.
+///
+/// # Safety
+///
+/// `data` is a live FFI value.
+unsafe fn expr_of(data: &TVMFFIAny) -> Option<&ExprObj> {
+    let is_object = data.type_index >= 
TVMFFITypeIndex::kTVMFFIStaticObjectBegin as i32;
+    // SAFETY: an object of an `ir.Expr` type is an `ExprObj`.
+    (is_object && is_instance_of::<ExprObj>(data.type_index))
+        .then(|| unsafe { &*(data.data_union.v_obj as *const ExprObj) })
+}
+
+/// The dtype of the expression `data` holds, if it holds one typed by a 
`PrimType`.
+///
+/// # Safety
+///
+/// `data` is a live FFI value.
+unsafe fn prim_dtype(data: &TVMFFIAny) -> Option<DLDataType> {
+    Some(unsafe { expr_of(data) }?.ty.downcast_ref::<PrimTypeObj>()?.dtype)
+}
+
+/// What a view names a value it rejects (C++ 
`TypeTraits<TypedExpr<T>>::GetMismatchTypeInfo`):
+/// an expression with its type, `ir.StringImm[ty=ir.StringType]`, or the type 
key of another value.
+fn typed_expr_mismatch(data: &TVMFFIAny) -> std::string::String {
+    // SAFETY: a mismatch is reported for a live value.
+    let Some(expr) = (unsafe { expr_of(data) }) else { return 
support::type_key(data.type_index) };
+    let ty = match expr.ty.downcast_ref::<PrimTypeObj>() {
+        Some(prim) => support::dtype_name(prim.dtype),
+        None => support::type_key(AnyView::from(&expr.ty).type_index()),
+    };
+    format!("{}[ty={ty}]", support::type_key(data.type_index))
+}
+
+/// The constant of the int, bool or float `data` holds, as C++ converts an 
`Any` into a
+/// `PrimExpr`: a bool is a boolean constant, an int an `int32` constant when 
it fits and an
+/// `int64` one otherwise, and a float a `float32` constant.
+///
+/// # Safety
+///
+/// `data` is a live FFI value.
+unsafe fn prim_literal(data: &TVMFFIAny) -> Option<PrimExpr> {
+    let index = data.type_index;
+    // SAFETY: the type index says which member of the union holds the value.
+    unsafe {
+        if index == TVMFFITypeIndex::kTVMFFIBool as i32 {
+            Some(IntImm::bool(data.data_union.v_int64 != 0).into())
+        } else if index == TVMFFITypeIndex::kTVMFFIInt as i32 {
+            Some(int_imm(data.data_union.v_int64).into())
+        } else if index == TVMFFITypeIndex::kTVMFFIFloat as i32 {
+            let value = data.data_union.v_float64;
+            FloatImm::from_dtype(dtype::float(32), value, 
UnknownLoc::new()).ok().map(Into::into)
+        } else {
+            None
+        }
+    }
+}
+
+/// The constant of the int `data` holds, as [`prim_literal`] makes it.
+///
+/// # Safety
+///
+/// `data` is a live FFI value.
+unsafe fn int_literal(data: &TVMFFIAny) -> Option<IntExpr> {
+    // SAFETY: an int is held in `v_int64`.
+    (data.type_index == TVMFFITypeIndex::kTVMFFIInt as i32)
+        .then(|| int_constant(int_imm(unsafe { data.data_union.v_int64 })))
+}
+
+/// An `int32` constant when `value` fits, and an `int64` one otherwise (C++
+/// `TypeTraits<IntImm>::ConvertFallbackValue`).
+fn int_imm(value: i64) -> IntImm {
+    match i32::try_from(value) {
+        Ok(value) => IntImm::int32(value),
+        Err(_) => IntImm::int64(value),
+    }
+}
+
 // tvm-ffi-stubgen(begin): object/ir.GlobalVar
 /// Complete: reflected fields fill [40, 56) exactly.
 #[repr(C)]
@@ -592,7 +1005,7 @@ impl TupleGetItem {
 pub struct TensorLoadObj {
     base: ExprObj,
     pub source: Expr,
-    pub indices: Array<Expr>,
+    pub indices: Array<PrimExpr>,
 }
 
 const _: () = {
@@ -621,8 +1034,8 @@ impl Deref for TensorLoadObj {
 }
 
 impl TensorLoadObj {
-    pub(crate) fn new(loc: Location, ty: Type, source: Expr, indices: 
Array<Expr>) -> Self {
-        let base = ExprObj::new(loc, ty);
+    pub(crate) fn new(loc: Location, ty: PrimType, source: Expr, indices: 
Array<PrimExpr>) -> Self {
+        let base = ExprObj::new(loc, ty.into());
         Self { base, source, indices }
     }
 }
@@ -631,16 +1044,16 @@ impl TensorLoad {
     /// Lossless complete-field allocation.
     pub fn from_fields(
         loc: impl Into<Location>,
-        ty: impl Into<Type>,
+        ty: impl Into<PrimType>,
         source: impl Into<Expr>,
-        indices: impl Into<Array<Expr>>,
+        indices: impl Into<Array<PrimExpr>>,
     ) -> Self {
         let obj = TensorLoadObj::new(loc.into(), ty.into(), source.into(), 
indices.into());
         Self { base: ObjectArc::new(obj) }
     }
 }
 
-tvm_ffi::impl_object_upcast!(TensorLoad => Expr);
+tvm_ffi::impl_object_upcast!(TensorLoad => Expr, TensorLoad => PrimExpr);
 // tvm-ffi-stubgen(end)
 
 impl TensorLoad {
@@ -654,20 +1067,23 @@ impl TensorLoad {
         static INFER_TYPE: support::TypeAttr =
             support::TypeAttr::new("__tvm_tensor_load_infer_type__");
         let (source, loc) = (source.into_expr()?, loc.into());
-        let indices =
-            
indices.into_iter().map(IntoExpr::into_expr).collect::<Result<Array<Expr>>>()?;
-        for index in indices.iter() {
-            support::prim_dtype(&index)?;
-        }
+        let indices = indices
+            .into_iter()
+            .map(|index| PrimExpr::try_from(index.into_expr()?))
+            .collect::<Result<Array<PrimExpr>>>()?;
         let ty_index = AnyView::from(&source.ty).type_index();
         let Some(infer) = INFER_TYPE.get(ty_index) else {
             let message =
                 format!("Type {} does not support TensorLoad", 
support::type_key(ty_index));
             return Err(support::check_failed(tvm_ffi::TYPE_ERROR, 
"infer.has_value()", &message));
         };
-        let load = Self::from_fields(loc, MissingType::new(), source, indices);
-        let ty: PrimType = 
support::NativeHook::from_any(infer)?.call_as(&load)?;
-        Ok(Self::from_fields(load.loc.clone(), ty, load.source.clone(), 
load.indices.clone()))
+        // The hook types a load that has no type yet. Such a load is no 
`TensorLoad`, which is
+        // typed by a `PrimType`, so it is made as an `Expr`.
+        let base = ExprObj::new(loc.clone(), MissingType::new().into());
+        let untyped = TensorLoadObj { base, source: source.clone(), indices: 
indices.clone() };
+        let untyped = Expr::from(Self { base: ObjectArc::new(untyped) });
+        let ty: PrimType = 
support::NativeHook::from_any(infer)?.call_as(&untyped)?;
+        Ok(Self::from_fields(loc, ty, source, indices))
     }
 }
 
@@ -774,21 +1190,21 @@ impl Deref for FloatImmObj {
 }
 
 impl FloatImmObj {
-    pub(crate) fn new(loc: Location, ty: Type, value: f64) -> Self {
-        let base = ConstantObj::new(loc, ty);
+    pub(crate) fn new(loc: Location, ty: PrimType, value: f64) -> Self {
+        let base = ConstantObj::new(loc, ty.into());
         Self { base, value }
     }
 }
 
 impl FloatImm {
     /// Lossless complete-field allocation.
-    pub fn new(loc: impl Into<Location>, ty: impl Into<Type>, value: f64) -> 
Self {
+    pub fn new(loc: impl Into<Location>, ty: impl Into<PrimType>, value: f64) 
-> Self {
         let obj = FloatImmObj::new(loc.into(), ty.into(), value);
         Self { base: ObjectArc::new(obj) }
     }
 }
 
-tvm_ffi::impl_object_upcast!(FloatImm => Expr, FloatImm => Constant);
+tvm_ffi::impl_object_upcast!(FloatImm => Expr, FloatImm => Constant, FloatImm 
=> PrimExpr);
 // tvm-ffi-stubgen(end)
 
 impl FloatImm {
@@ -968,21 +1384,25 @@ impl Deref for IntImmObj {
 }
 
 impl IntImmObj {
-    pub(crate) fn new(loc: Location, ty: Type, value: BigInt) -> Self {
-        let base = ConstantObj::new(loc, ty);
+    pub(crate) fn new(loc: Location, ty: PrimType, value: BigInt) -> Self {
+        let base = ConstantObj::new(loc, ty.into());
         Self { base, value }
     }
 }
 
 impl IntImm {
     /// Lossless complete-field allocation.
-    pub fn new(loc: impl Into<Location>, ty: impl Into<Type>, value: impl 
Into<BigInt>) -> Self {
+    pub fn new(
+        loc: impl Into<Location>,
+        ty: impl Into<PrimType>,
+        value: impl Into<BigInt>,
+    ) -> Self {
         let obj = IntImmObj::new(loc.into(), ty.into(), value.into());
         Self { base: ObjectArc::new(obj) }
     }
 }
 
-tvm_ffi::impl_object_upcast!(IntImm => Expr, IntImm => Constant);
+tvm_ffi::impl_object_upcast!(IntImm => Expr, IntImm => Constant, IntImm => 
PrimExpr);
 // tvm-ffi-stubgen(end)
 
 impl IntImm {
@@ -1045,7 +1465,7 @@ 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 
[`ExprResult`](crate::ir::prim::ExprResult), an
+/// node, a `Var`, a Rust `i32`, `f32` and the like), and an [`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 `ExprResult` passes straight in and `?` on their result covers it.
diff --git a/rust/src/ir/op_def.rs b/rust/src/ir/op_def.rs
index dcd963940..1620956f3 100644
--- a/rust/src/ir/op_def.rs
+++ b/rust/src/ir/op_def.rs
@@ -423,10 +423,10 @@ fn call_hook<N, R: Into<Any>>(node: *const N, hook: impl 
Fn(&N) -> Result<R>) ->
 /// order, [`var_args`](sig::var_args) and [`var_ty_args`](sig::var_ty_args) a 
variadic tail of
 /// each, and [`call_attrs`](sig::call_attrs) the type of `Call.attrs`. As an 
argument type, a node
 /// type, such as [`StringImm`](super::StringImm) or [`Var`], accepts an 
instance of it, as C++
-/// `TypeTraits<T>::CheckAnyStrict` does, and [`Expr`] or [`Type`] accepts any.
-/// [`PrimExpr`](sig::PrimExpr), [`IntExpr`](sig::IntExpr) and 
[`PrimVar`](sig::PrimVar) are the
-/// C++ view types, which also check the expression's type, as is
-/// [`TensorVar`](crate::tirx::TensorVar). Implement [`ArgType`](sig::ArgType) 
for another check.
+/// `TypeTraits<T>::CheckAnyStrict` does, and [`Expr`] or [`Type`] accepts 
any. The typed views
+/// [`PrimExpr`](super::PrimExpr) and [`IntExpr`](super::IntExpr) also check 
the expression's type;
+/// [`PrimVar`](sig::PrimVar) and [`TensorVar`](crate::tirx::TensorVar) check 
a variable's type, as
+/// the C++ view types of those names do. Implement [`ArgType`](sig::ArgType) 
for another check.
 pub mod sig {
     use super::*;
     use crate::ir::expr::ExprObj;
@@ -455,43 +455,14 @@ pub mod sig {
         fn type_str() -> std::string::String {
             T::type_str()
         }
-    }
-
-    /// C++ `PrimExpr`: an expression whose type is a `PrimType`.
-    pub struct PrimExpr;
-
-    impl ArgType for PrimExpr {
-        fn check(value: &TVMFFIAny) -> bool {
-            prim_dtype(value).is_some()
-        }
-
-        fn type_str() -> std::string::String {
-            "ir.PrimExpr".into()
-        }
 
         fn mismatch(value: &TVMFFIAny) -> std::string::String {
-            typed_expr_mismatch(value)
+            T::get_mismatch_type_info(value)
         }
     }
 
-    /// C++ `IntExpr`: a `PrimExpr` of a scalar signed or unsigned integer 
type.
-    pub struct IntExpr;
-
-    impl ArgType for IntExpr {
-        fn check(value: &TVMFFIAny) -> bool {
-            prim_dtype(value).is_some_and(|dtype| {
-                dtype.lanes == 1 && matches!(dtype.code, support::INT | 
support::UINT)
-            })
-        }
-
-        fn type_str() -> std::string::String {
-            "ir.IntExpr".into()
-        }
-
-        fn mismatch(value: &TVMFFIAny) -> std::string::String {
-            typed_expr_mismatch(value)
-        }
-    }
+    /// The typed views, which are argument types as they are.
+    pub use crate::ir::expr::{IntExpr, PrimExpr};
 
     /// C++ `PrimVar`: a `Var`, not a subclass, whose type is a `PrimType`.
     pub struct PrimVar;
@@ -522,20 +493,6 @@ pub mod sig {
         }
     }
 
-    fn prim_dtype(value: &TVMFFIAny) -> Option<tvm_ffi::DLDataType> {
-        Some(expr(value)?.ty.downcast_ref::<PrimTypeObj>()?.dtype)
-    }
-
-    /// C++ `TypeTraits<TypedExpr<T>>::GetMismatchTypeInfo`: an expression 
with its type.
-    fn typed_expr_mismatch(value: &TVMFFIAny) -> std::string::String {
-        let Some(expr) = expr(value) else { return 
support::type_key(value.type_index) };
-        let ty = match expr.ty.downcast_ref::<PrimTypeObj>() {
-            Some(prim) => support::dtype_name(prim.dtype),
-            None => support::type_key(AnyView::from(&expr.ty).type_index()),
-        };
-        format!("{}[ty={ty}]", support::type_key(value.type_index))
-    }
-
     /// A required value argument of type `T` (C++ `sig::arg<T>`); [`Expr`] 
accepts any.
     pub fn arg<T: ArgType>(name: &str, doc: &str) -> Arg<T> {
         Arg { name: String::from(name), doc: String::from(doc), ty: 
PhantomData }
diff --git a/rust/src/ir/prim/op.rs b/rust/src/ir/prim/op.rs
index 869ab9f39..4903cd3c2 100644
--- a/rust/src/ir/prim/op.rs
+++ b/rust/src/ir/prim/op.rs
@@ -73,6 +73,7 @@
 // tvm-ffi-stubgen(import-object): crate::ir::expr::IntImm
 // tvm-ffi-stubgen(import-object): crate::ir::expr::IntImmObj
 // tvm-ffi-stubgen(import-object): crate::ir::expr::IntoExpr
+// tvm-ffi-stubgen(import-object): crate::ir::expr::PrimExpr
 // tvm-ffi-stubgen(import-object): crate::ir::expr::TensorLoad
 // tvm-ffi-stubgen(import-object): crate::ir::expr::Var
 // tvm-ffi-stubgen(import-object): crate::ir::location::Location
@@ -127,6 +128,7 @@ use crate::ir::expr::FloatImmObj;
 use crate::ir::expr::IntImm;
 use crate::ir::expr::IntImmObj;
 use crate::ir::expr::IntoExpr;
+use crate::ir::expr::PrimExpr;
 use crate::ir::expr::TensorLoad;
 use crate::ir::expr::Var;
 use crate::ir::location::Location;
@@ -358,7 +360,9 @@ impl Operand for f64 {
 /// An operand next to `peer`: a literal is typed after it.
 fn typed_after(operand: OperandKind, peer: &ExprResult) -> ExprResult {
     let constant = |value: i64| {
-        ExprResult(peer.dtype().and_then(|dtype| 
make_const(PrimType::from_dtype(dtype)?, value)))
+        ExprResult::checked(
+            peer.dtype().and_then(|dtype| 
make_const(PrimType::from_dtype(dtype)?, value)),
+        )
     };
     match operand {
         OperandKind::Expr(expr) => expr,
@@ -1727,12 +1731,14 @@ binary_intrinsics! {
 // 
---------------------------------------------------------------------------------------------
 // `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 `ExprResult` is an 
[`IntoExpr`], so it passes
-/// into any function or constructor that takes one, whose `?` then reports 
the error;
-/// `Expr::try_from` takes the expression out.
+/// An expression being built by operators: either the expression, a 
[`PrimExpr`], or the first
+/// error any step 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; `PrimExpr::try_from` takes the expression out without a 
further check, and
+/// `Expr::try_from` takes it out as an `Expr`.
 ///
-/// Registered expression wrappers convert into an `ExprResult`. The operators
+/// Registered expression wrappers convert into an `ExprResult`; one whose 
type is not a
+/// `PrimType` is a TypeError there. 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,13 +1747,23 @@ binary_intrinsics! {
 /// Comparisons, `&&` and `||` are [`PrimOps`] methods too, because Rust's 
comparison traits
 /// return `bool`.
 #[derive(Clone)]
-#[must_use = "an `ExprResult` holds the result; pass it on, or convert it with 
`Expr::try_from`"]
-pub struct ExprResult(Result<Expr>);
+#[must_use = "an `ExprResult` holds the result; pass it on, or convert it with 
`PrimExpr::try_from`"]
+pub struct ExprResult(Result<PrimExpr>);
 
 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.map(Into::into)
+    }
+}
+
+impl TryFrom<ExprResult> for PrimExpr {
+    type Error = Error;
+
+    /// The built expression, which every step checks is a `PrimExpr`, or the 
first error raised
+    /// while building it.
     fn try_from(value: ExprResult) -> Result<Self> {
         value.0
     }
@@ -1757,7 +1773,7 @@ impl ExprResult {
     /// The dtype a literal next to this expression takes 
(`MakeConst(this.ty(), v)`).
     fn dtype(&self) -> Result<DLDataType> {
         match &self.0 {
-            Ok(expr) => support::prim_dtype(expr),
+            Ok(expr) => Ok(expr.dtype()),
             Err(error) => Err(error.clone()),
         }
     }
@@ -1765,19 +1781,27 @@ impl ExprResult {
 
 impl IntoExpr for ExprResult {
     fn into_expr(self) -> Result<Expr> {
-        self.0
+        self.0.map(Into::into)
     }
 }
 
 impl IntoExpr for &ExprResult {
     fn into_expr(self) -> Result<Expr> {
-        self.0.clone()
+        self.0.clone().map(Into::into)
     }
 }
 
 impl<T: Into<Expr>> From<Result<T>> for ExprResult {
     fn from(value: Result<T>) -> Self {
-        ExprResult(value.map(Into::into))
+        ExprResult::checked(value.map(Into::into))
+    }
+}
+
+impl ExprResult {
+    /// A step's result, checked to be a `PrimExpr`; an expression of another 
type is a TypeError,
+    /// raised where it enters the chain.
+    fn checked(value: Result<Expr>) -> Self {
+        ExprResult(value.and_then(PrimExpr::try_from))
     }
 }
 
@@ -1795,7 +1819,7 @@ fn binary(
 ) -> ExprResult {
     let rhs = typed_after(rhs.classify(), &lhs);
     match (lhs.0, rhs.0) {
-        (Ok(a), Ok(b)) => ExprResult(op(a, b)),
+        (Ok(a), Ok(b)) => ExprResult::checked(op(a.into(), b.into())),
         (Err(error), _) | (_, Err(error)) => ExprResult(Err(error)),
     }
 }
@@ -1891,14 +1915,14 @@ mod operators {
                 impl Neg for $ty {
                     type Output = ExprResult;
                     fn neg(self) -> ExprResult {
-                        ExprResult(super::neg(ExprResult::from(self)))
+                        ExprResult::checked(super::neg(ExprResult::from(self)))
                     }
                 }
                 /// C++ `!` (`logical_not`).
                 impl Not for $ty {
                     type Output = ExprResult;
                     fn not(self) -> ExprResult {
-                        ExprResult(super::logical_not(ExprResult::from(self)))
+                        
ExprResult::checked(super::logical_not(ExprResult::from(self)))
                     }
                 }
             )+
@@ -1914,7 +1938,7 @@ macro_rules! unary_methods {
         $(
             $(#[$doc])*
             fn $name(&self) -> ExprResult {
-                ExprResult($name(self.to_expr_result()))
+                ExprResult::checked($name(self.to_expr_result()))
             }
         )+
     };
@@ -1967,11 +1991,11 @@ pub trait PrimOps {
     }
     /// C++ `a && b` ([`logical_and`]).
     fn logical_and(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(logical_and(self.to_expr_result(), rhs))
+        ExprResult::checked(logical_and(self.to_expr_result(), rhs))
     }
     /// C++ `a || b` ([`logical_or`]).
     fn logical_or(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(logical_or(self.to_expr_result(), rhs))
+        ExprResult::checked(logical_or(self.to_expr_result(), rhs))
     }
 
     binary_methods! {
@@ -1999,31 +2023,31 @@ pub trait PrimOps {
 
     /// C++ `pow(a, b)` ([`pow`]).
     fn pow(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(pow(self.to_expr_result(), rhs))
+        ExprResult::checked(pow(self.to_expr_result(), rhs))
     }
     /// C++ `fmod(a, b)` ([`fmod`]).
     fn fmod(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(fmod(self.to_expr_result(), rhs))
+        ExprResult::checked(fmod(self.to_expr_result(), rhs))
     }
     /// C++ `atan2(a, b)` ([`atan2`]).
     fn atan2(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(atan2(self.to_expr_result(), rhs))
+        ExprResult::checked(atan2(self.to_expr_result(), rhs))
     }
     /// C++ `nextafter(a, b)` ([`nextafter`]).
     fn nextafter(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(nextafter(self.to_expr_result(), rhs))
+        ExprResult::checked(nextafter(self.to_expr_result(), rhs))
     }
     /// C++ `copysign(a, b)` ([`copysign`]).
     fn copysign(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(copysign(self.to_expr_result(), rhs))
+        ExprResult::checked(copysign(self.to_expr_result(), rhs))
     }
     /// C++ `hypot(a, b)` ([`hypot`]).
     fn hypot(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(hypot(self.to_expr_result(), rhs))
+        ExprResult::checked(hypot(self.to_expr_result(), rhs))
     }
     /// C++ `ldexp(a, b)` ([`ldexp`]).
     fn ldexp(&self, rhs: impl IntoExpr) -> ExprResult {
-        ExprResult(ldexp(self.to_expr_result(), rhs))
+        ExprResult::checked(ldexp(self.to_expr_result(), rhs))
     }
 
     unary_methods! {
@@ -2105,17 +2129,17 @@ pub trait PrimOps {
 
     /// C++ `cast(a, t)` ([`cast`]).
     fn cast(&self, ty: impl Into<PrimType>) -> ExprResult {
-        ExprResult(cast(self.to_expr_result(), ty))
+        ExprResult::checked(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) -> ExprResult {
-        ExprResult(if_then_else(self.to_expr_result(), t, f))
+        ExprResult::checked(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) -> ExprResult {
-        ExprResult(Select::new(self.to_expr_result(), t, f, 
UNKNOWN_LOC).map(Into::into))
+        ExprResult::checked(Select::new(self.to_expr_result(), t, f, 
UNKNOWN_LOC).map(Expr::from))
     }
 }
 
diff --git a/rust/tests/ir/expr.rs b/rust/tests/ir/expr.rs
index 6c47e2c9d..0db3b8680 100644
--- a/rust/tests/ir/expr.rs
+++ b/rust/tests/ir/expr.rs
@@ -19,10 +19,11 @@
 
 //! Tests of `ir::expr`.
 
+use tvm_tirx::ir::prim::Broadcast;
 use tvm_tirx::ir::*;
 use tvm_tirx::tvm_ffi::{
     Any, AnyView, Array, BigInt, DLDataType, Function, ObjectRefCast, 
ObjectRefCore, Result,
-    String as FfiString,
+    String as FfiString, TYPE_ERROR,
 };
 
 use super::{int32, int32_var, op, parse_dtype, tensor_var};
@@ -421,3 +422,126 @@ fn calls_agree_with_cpp() {
         }
     }
 }
+
+// Typed views: `PrimExpr` and `IntExpr`.
+
+#[test]
+fn views_check_the_type() {
+    let _serial = serial();
+    let i = Expr::from(int32_var("i"));
+    let x = Expr::from(Var::new("x", PrimType::float(32), UNKNOWN_LOC));
+    let a = Expr::from(tensor_var("A"));
+    let text = Expr::from(StringImm::new("s", UNKNOWN_LOC));
+
+    // An expression is a view only when its type is one the view accepts.
+    let prim = PrimExpr::try_from(&i).unwrap();
+    assert_eq!(prim.dtype(), dtype::int(32));
+    assert_eq!(prim.ty().dtype, dtype::int(32));
+    assert!(prim.same_as(&i));
+    assert_eq!(PrimExpr::try_from(&x).unwrap().dtype(), dtype::float(32));
+    let error = PrimExpr::try_from(&a).err().unwrap();
+    assert_eq!(error.kind(), TYPE_ERROR);
+    assert!(error.message().contains("`ir.Var[ty=tirx.TensorType]` to 
`ir.PrimExpr`"), "{error}");
+    let error = PrimExpr::try_from(&text).err().unwrap();
+    assert!(error.message().contains("ir.StringImm[ty=ir.StringType]"), 
"{error}");
+
+    let index = IntExpr::try_from(&i).unwrap();
+    assert!(IntExpr::try_from(&x).is_err());
+    assert!(IntExpr::try_from(PrimExpr::from(true)).is_err());
+    // A node type whose constructor takes any type converts only through a 
check.
+    let vector = Broadcast::new(&i, int32_lanes(4), UNKNOWN_LOC).unwrap();
+    let vector: PrimExpr = vector.try_cast().unwrap();
+    assert!(IntExpr::try_from(vector).is_err());
+
+    // A view widens for free and reads as the views it refines.
+    let widened: PrimExpr = index.clone().into();
+    let expr: Expr = index.clone().into();
+    assert!(widened.same_as(&expr));
+    assert_eq!(index.dtype(), dtype::int(32));
+    assert!(index.as_expr().same_as(&i));
+    let borrowed: &PrimExpr = index.as_ref();
+    assert!(borrowed.same_as(&i));
+    // A view reads the fields of its object.
+    let ty: &Type = &index.ty;
+    assert_eq!(ty.downcast_ref::<PrimTypeObj>().unwrap().dtype, 
dtype::int(32));
+}
+
+#[test]
+fn constants_and_loads_are_views() {
+    let _serial = serial();
+    // Constants and loads are typed by a `PrimType`, so they convert without 
a check.
+    assert_eq!(PrimExpr::from(IntImm::int64(1)).dtype(), dtype::int(64));
+    assert_eq!(PrimExpr::from(FloatImm::float32(0.5)).dtype(), 
dtype::float(32));
+    let load = TensorLoad::new(tensor_var("A"), [int32_var("i")], 
UNKNOWN_LOC).unwrap();
+    let indices: &Array<PrimExpr> = &load.indices;
+    assert_eq!(indices.get(0).unwrap().dtype(), dtype::int(32));
+    assert_eq!(PrimExpr::from(&load).dtype(), dtype::float(32));
+
+    // A load whose index is not a `PrimExpr` is not made.
+    let text = StringImm::new("s", UNKNOWN_LOC);
+    let error = TensorLoad::new(tensor_var("A"), [text], 
UNKNOWN_LOC).err().unwrap();
+    assert_eq!(error.kind(), TYPE_ERROR);
+
+    // Rust literals.
+    assert_eq!(PrimExpr::from(1).dtype(), dtype::int(32));
+    assert_eq!(PrimExpr::from(1i64).dtype(), dtype::int(64));
+    assert_eq!(PrimExpr::from(true).dtype(), dtype::bool());
+    assert_eq!(PrimExpr::from(1.5f32).dtype(), dtype::float(32));
+    assert_eq!(PrimExpr::from(1.5f64).dtype(), dtype::float(64));
+    assert_eq!(IntExpr::from(7).dtype(), dtype::int(32));
+    assert_eq!(IntExpr::from(7i64).dtype(), dtype::int(64));
+}
+
+#[test]
+fn any_converts_as_in_cpp() {
+    let _serial = serial();
+    // An expression is checked; a bool, an int or a float becomes a constant, 
as C++
+    // `TypeTraits<PrimExpr>` converts it: an int that fits `int32` is one, a 
float is `float32`.
+    let i = int32_var("i");
+    assert!(PrimExpr::try_from(Any::from(i.clone())).unwrap().same_as(&i));
+    assert!(PrimExpr::try_from(Any::from(tensor_var("A"))).is_err());
+    assert_eq!(PrimExpr::try_from(Any::from(true)).unwrap().dtype(), 
dtype::bool());
+    assert_eq!(PrimExpr::try_from(Any::from(3i64)).unwrap().dtype(), 
dtype::int(32));
+    assert_eq!(PrimExpr::try_from(Any::from(1i64 << 40)).unwrap().dtype(), 
dtype::int(64));
+    assert_eq!(PrimExpr::try_from(Any::from(0.5f64)).unwrap().dtype(), 
dtype::float(32));
+    
assert!(PrimExpr::try_from(Any::from(tvm_tirx::tvm_ffi::String::from("s"))).is_err());
+    assert_eq!(IntExpr::try_from(Any::from(3i64)).unwrap().dtype(), 
dtype::int(32));
+    assert!(IntExpr::try_from(Any::from(true)).is_err());
+    assert!(IntExpr::try_from(Any::from(0.5f64)).is_err());
+
+    // An array of views checks each element, and converts one of literals 
element by element.
+    let mixed: Array<Any> = [Any::from(i.clone()), 
Any::from(2i64)].into_iter().collect();
+    let views: Array<PrimExpr> = Any::from(mixed).try_into().unwrap();
+    assert!(views.get(0).unwrap().same_as(&i));
+    assert_eq!(views.get(1).unwrap().dtype(), dtype::int(32));
+    let exprs: Array<Expr> =
+        [Expr::from(i), StringImm::new("s", 
UNKNOWN_LOC).into()].into_iter().collect();
+    assert!(Array::<PrimExpr>::try_from(Any::from(exprs)).is_err());
+}
+
+#[test]
+fn operators_take_views() {
+    let _serial = serial();
+    let i = IntExpr::try_from(Expr::from(int32_var("i"))).unwrap();
+    let j = int32_var_expr("j");
+    let sum = PrimExpr::try_from(&i + &j + 1).unwrap();
+    assert_eq!(sum.dtype(), dtype::int(32));
+    assert_eq!(IntExpr::try_from(i.clone() * 2).unwrap().dtype(), 
dtype::int(32));
+    // An operand that is not a `PrimExpr` is a TypeError where it enters the 
chain.
+    let error = PrimExpr::try_from(Expr::from(tensor_var("A")) + 
1).err().unwrap();
+    assert_eq!(error.kind(), TYPE_ERROR);
+    assert!(error.message().contains("ir.Var[ty=tirx.TensorType]"), "{error}");
+    // `ObjectRefCast` downcasts between views as it does between node types.
+    let back: IntExpr = sum.try_cast().unwrap();
+    assert_eq!(back.dtype(), dtype::int(32));
+}
+
+/// `int32` lanes of a vector.
+fn int32_lanes(lanes: i32) -> Expr {
+    IntImm::int32(lanes).into()
+}
+
+/// An `int32` variable as a `PrimExpr`.
+fn int32_var_expr(name: &str) -> PrimExpr {
+    PrimExpr::try_from(Expr::from(int32_var(name))).unwrap()
+}

Reply via email to