#![allow(dead_code, unused_imports)]
use crate::ir::dtype::void;
use crate::ir::dtype::with_lanes;
use crate::ir::expr::Call;
use crate::ir::expr::CallObj;
use crate::ir::expr::Expr;
use crate::ir::expr::FloatImm;
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;
use crate::ir::location::UNKNOWN_LOC;
use crate::ir::location::UnknownLoc;
use crate::ir::prim::const_fold;
use crate::ir::prim::expr::Add;
use crate::ir::prim::expr::And;
use crate::ir::prim::expr::BitwiseAnd;
use crate::ir::prim::expr::BitwiseNot;
use crate::ir::prim::expr::BitwiseOr;
use crate::ir::prim::expr::BitwiseXor;
use crate::ir::prim::expr::Cast;
use crate::ir::prim::expr::Div;
use crate::ir::prim::expr::EQ;
use crate::ir::prim::expr::FloorDiv;
use crate::ir::prim::expr::FloorMod;
use crate::ir::prim::expr::GE;
use crate::ir::prim::expr::GT;
use crate::ir::prim::expr::LE;
use crate::ir::prim::expr::LShift;
use crate::ir::prim::expr::LT;
use crate::ir::prim::expr::Let;
use crate::ir::prim::expr::Max;
use crate::ir::prim::expr::Min;
use crate::ir::prim::expr::Mod;
use crate::ir::prim::expr::Mul;
use crate::ir::prim::expr::NE;
use crate::ir::prim::expr::Not;
use crate::ir::prim::expr::Or;
use crate::ir::prim::expr::RShift;
use crate::ir::prim::expr::Select;
use crate::ir::prim::expr::Sub;
use crate::ir::prim::vector_expr::Broadcast;
use crate::ir::prim::vector_expr::BroadcastObj;
use crate::ir::prim::vector_expr::Ramp;
use crate::ir::prim::vector_expr::RampObj;
use crate::ir::prim::vector_expr::Shuffle;
use crate::ir::r#type::PrimType;
use crate::ir::r#type::Type;
use crate::ir::support;
use crate::sym;
use tvm_ffi::Any;
use tvm_ffi::BigInt;
use tvm_ffi::DLDataType;
use tvm_ffi::DLDataTypeCode;
use tvm_ffi::Error;
use tvm_ffi::INTERNAL_ERROR;
use tvm_ffi::ObjectRefCore;
use tvm_ffi::Result;
use tvm_ffi::TYPE_ERROR;
use tvm_ffi::VALUE_ERROR;
const FLOAT4: DLDataTypeCode = DLDataTypeCode::kDLFloat4_e2m1fn;
fn is_float8(code: u8) -> bool {
support::float8_bound(code).is_some()
}
fn is_float6(code: u8) -> bool {
support::float6_bound(code).is_some()
}
fn small_float_bound(code: u8) -> Option<(f64, bool)> {
support::float8_bound(code).or_else(|| support::float6_bound(code).map(|max| (max, false)))
}
fn is_float4(code: u8) -> bool {
FLOAT4 as u8 == code
}
fn is_bfloat16(dtype: DLDataType) -> bool {
dtype.code == support::BFLOAT && dtype.bits == 16
}
fn is_vector(dtype: DLDataType) -> bool {
support::is_scalable_vector(dtype) || support::is_fixed_length_vector(dtype)
}
fn vscale_factor(dtype: DLDataType) -> i32 {
-i32::from(support::encoded_lanes(dtype))
}
fn lanes_of(dtype: DLDataType) -> Result<Expr> {
match support::is_scalable_vector(dtype) {
true => {
let factor = IntImm::int32(vscale_factor(dtype));
Ok(Mul::new(factor, vscale()?, UNKNOWN_LOC)?.into())
}
false => Ok(IntImm::int32(support::lanes(dtype)?).into()),
}
}
fn call(ty: Option<&Type>, op: &'static str, args: Vec<Expr>, loc: Location) -> Result<Expr> {
let op = support::cached_op(op)?;
Ok(Call::new(ty, op, args, None, Vec::<Type>::new(), loc)?.into())
}
fn is_vscale_call(expr: &Expr) -> Result<bool> {
let Some(call) = expr.downcast_ref::<CallObj>() else { return Ok(false) };
Ok(call.op.same_as(&support::cached_op("prim.vscale")?))
}
fn check_code(
operand: &Expr,
side: &str,
accepted: &[u8],
codes: &str,
message: impl FnOnce() -> String,
) -> Result<()> {
if accepted.contains(&support::prim_dtype(operand)?.code) {
return Ok(());
}
let condition = format!("{side}.ty().MatchesCode({codes})");
Err(support::check_failed(INTERNAL_ERROR, &condition, &message()))
}
const INTEGER: [u8; 2] = [support::INT, support::UINT];
const INTEGER_CODES: &str = "DLDataTypeCode::kDLInt, DLDataTypeCode::kDLUInt";
const INTEGER_OR_BOOL: [u8; 3] = [support::INT, support::UINT, support::BOOL];
const INTEGER_OR_BOOL_CODES: &str =
"DLDataTypeCode::kDLInt, DLDataTypeCode::kDLUInt, DLDataTypeCode::kDLBool";
const BOOL_CODES: &str = "DLDataTypeCode::kDLBool";
fn check_integers(a: &Expr, b: &Expr) -> Result<()> {
check_code(a, "a", &INTEGER, INTEGER_CODES, || support::repr(a))?;
check_code(b, "b", &INTEGER, INTEGER_CODES, || support::repr(b))
}
fn check_operands(
a: &Expr,
b: &Expr,
symbol: &str,
what: &str,
kind: &str,
(accepted, codes): (&[u8], &str),
) -> Result<()> {
for (operand, side, which) in [(a, "a", "LHS"), (b, "b", "RHS")] {
check_code(operand, side, accepted, codes, || {
format!(
"Expected {kind} argument as {which} of {symbol} operator ({what}), but received \
{} of type {}",
support::repr(operand),
support::repr(&operand.ty)
)
})?;
}
Ok(())
}
pub trait Operand {
#[doc(hidden)]
fn classify(self) -> OperandKind;
}
#[doc(hidden)]
pub enum OperandKind {
Expr(ExprResult),
Int32(i32),
Int64(i64),
Float32(f32),
Float64(f64),
}
impl<T: Into<ExprResult>> Operand for T {
fn classify(self) -> OperandKind {
OperandKind::Expr(self.into())
}
}
impl Operand for i32 {
fn classify(self) -> OperandKind {
OperandKind::Int32(self)
}
}
impl Operand for i64 {
fn classify(self) -> OperandKind {
OperandKind::Int64(self)
}
}
impl Operand for f32 {
fn classify(self) -> OperandKind {
OperandKind::Float32(self)
}
}
impl Operand for f64 {
fn classify(self) -> OperandKind {
OperandKind::Float64(self)
}
}
fn typed_after(operand: OperandKind, peer: &ExprResult) -> ExprResult {
let constant = |value: i64| {
ExprResult::checked(
peer.dtype().and_then(|dtype| make_const(PrimType::from_dtype(dtype)?, value)),
)
};
match operand {
OperandKind::Expr(expr) => expr,
OperandKind::Int32(value) => constant(i64::from(value)),
OperandKind::Int64(value) => constant(value),
OperandKind::Float32(value) => ExprResult(Ok(FloatImm::float32(value).into())),
OperandKind::Float64(value) => ExprResult(Ok(FloatImm::float64(value).into())),
}
}
fn on_its_own(operand: OperandKind) -> ExprResult {
match operand {
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())),
}
}
fn operands(a: impl Operand, b: impl Operand) -> Result<(Expr, Expr)> {
let (a, b) = match (a.classify(), b.classify()) {
(OperandKind::Expr(a), b) => {
let b = typed_after(b, &a);
(a, b)
}
(a, OperandKind::Expr(b)) => {
let a = typed_after(a, &b);
(a, b)
}
(a, b) => (on_its_own(a), on_its_own(b)),
};
Ok((a.into_expr()?, b.into_expr()?))
}
macro_rules! unlocated {
($($(#[$doc:meta])* $name:ident($($arg:ident: $ty:ty),*) => $at:ident;)+) => {
$(
$(#[$doc])*
pub fn $name($($arg: $ty),*) -> Result<Expr> {
$at($($arg,)* UNKNOWN_LOC)
}
)+
};
}
#[derive(Clone, Debug)]
pub enum ConstValue {
Int(BigInt),
Float(f64),
}
macro_rules! const_value_from_int {
($($int:ty),+) => {
$(
impl From<$int> for ConstValue {
fn from(value: $int) -> Self {
ConstValue::Int(BigInt::from(value))
}
}
)+
};
}
const_value_from_int!(i8, i16, i32, i64, u8, u16, u32, u64, bool, BigInt);
impl From<f64> for ConstValue {
fn from(value: f64) -> Self {
ConstValue::Float(value)
}
}
impl From<f32> for ConstValue {
fn from(value: f32) -> Self {
ConstValue::Float(f64::from(value))
}
}
pub(crate) fn make_const_scalar(
dtype: DLDataType,
value: ConstValue,
loc: &Location,
) -> Result<Expr> {
let code = dtype.code;
if matches!(code, support::INT | support::UINT | support::BOOL) {
let value = match value {
ConstValue::Int(value) => value,
ConstValue::Float(value) => {
if code == support::UINT && value < 0.0 {
let message = format!(
"cannot make uint from negative value {}",
support::CppDouble(value)
);
return Err(support::check_failed(INTERNAL_ERROR, "value >= 0", &message));
}
BigInt::try_from(value)?
}
};
if code == support::UINT && value.is_negative() {
let message = format!("cannot make uint from negative value {value}");
return Err(support::check_failed(INTERNAL_ERROR, "value >= 0", &message));
}
return Ok(IntImm::from_dtype(dtype, value, loc.clone())?.into());
}
if code == support::FLOAT
|| is_float8(code)
|| is_float6(code)
|| is_float4(code)
|| is_bfloat16(dtype)
{
let value = match value {
ConstValue::Int(value) => value.to_f64()?,
ConstValue::Float(value) => value,
};
return Ok(FloatImm::from_dtype(dtype, value, loc.clone())?.into());
}
let ty = PrimType::from_dtype(dtype)?;
let message = format!("cannot make const for type {}", support::repr(&ty));
Err(Error::new(INTERNAL_ERROR, &message, ""))
}
unlocated! {
make_const(ty: impl Into<PrimType>, value: impl Into<ConstValue>) => make_const_at;
}
pub fn make_const_at(
ty: impl Into<PrimType>,
value: impl Into<ConstValue>,
loc: impl Into<Location>,
) -> Result<Expr> {
let (dtype, value, loc) = (ty.into().dtype, value.into(), loc.into());
if !is_vector(dtype) {
return make_const_scalar(dtype, value, &loc);
}
let scalar = make_const_scalar(with_lanes(dtype, 1), value, &loc)?;
let lanes = match support::is_fixed_length_vector(dtype) {
true => IntImm::int32(support::lanes(dtype)?).into(),
false => super::vector_expr::vscale_lanes(vscale_factor(dtype))?,
};
Ok(Broadcast::new(scalar, lanes, loc)?.into())
}
pub fn convert(value: impl Into<Any>) -> Result<Expr> {
tvm_ffi::cached_global_func!("prim.convert").call_tuple((value.into(),))?.try_into()
}
fn frexp_exponent(value: f64) -> i32 {
match ((value.to_bits() >> 52) & 0x7ff) as i32 {
0 if value == 0.0 => 0,
0 => frexp_exponent(value * pow2(64)) - 64,
biased => biased - 1022,
}
}
fn pow2(exponent: i32) -> f64 {
f64::from_bits(((exponent + 1023) as u64) << 52)
}
fn is_rounded_to_16_bits(value: f64, bfloat: bool) -> bool {
let (digits, min_step) = if bfloat { (8, -133) } else { (11, -24) };
let step = (frexp_exponent(value) - digits).max(min_step);
let scaled = value * pow2(-step);
scaled.trunc() == scaled
}
fn fold_cast(value: &Expr, to: DLDataType) -> Result<Option<Expr>> {
let integer = matches!(to.code, support::INT | support::UINT | support::BOOL);
let float32 = to.code == support::FLOAT && to.bits == 32;
let float64 = to.code == support::FLOAT && to.bits == 64;
let limit_16 = match (to.code, to.bits) {
(support::FLOAT, 16) => Some(support::MAX_FLOAT16),
(support::BFLOAT, 16) => Some(support::MAX_BFLOAT16),
_ => None,
};
let int = |value: BigInt, loc: &Location| -> Result<Option<Expr>> {
Ok(Some(IntImm::from_dtype(to, value, loc.clone())?.into()))
};
let float = |value: f64, loc: &Location| -> Result<Option<Expr>> {
Ok(Some(FloatImm::from_dtype(to, value, loc.clone())?.into()))
};
if let Some(imm) = value.downcast_ref::<IntImmObj>() {
if integer {
if const_fold::is_representable(&imm.value, to) {
return int(imm.value.clone(), &imm.loc);
}
} else if float32 {
if let Some(small) = imm.value.to_i64() {
return float(f64::from(small as f32), &imm.loc);
}
} else if float64 {
let limit = BigInt::try_from(f64::MAX)?;
if (-&limit..=limit).contains(&imm.value) {
return float(imm.value.to_f64()?, &imm.loc);
}
} else if let Some(limit) = limit_16 {
let source = imm.value.to_f64()?;
if (-limit..=limit).contains(&source) && BigInt::try_from(source)? == imm.value {
return float(source, &imm.loc);
}
}
} else if let Some(imm) = value.downcast_ref::<FloatImmObj>() {
let from = support::prim_dtype(value)?;
if imm.value.is_finite() && from.bits == 16 {
let bfloat = match from.code {
support::FLOAT => Some(false),
support::BFLOAT => Some(true),
_ => None,
};
if bfloat.is_some_and(|bfloat| !is_rounded_to_16_bits(imm.value, bfloat)) {
return Ok(None);
}
}
let source = match from.code == support::FLOAT && from.bits == 32 {
true => f64::from(imm.value as f32),
false => imm.value,
};
if integer {
let fits_bool = to.code != support::BOOL || source == 0.0 || source == 1.0;
if source.is_finite() && fits_bool {
let result = BigInt::try_from(source)?;
if const_fold::is_representable(&result, to) {
return int(result, &imm.loc);
}
}
} else if float32 {
if const_fold::is_in_float32_range(source) {
return float(f64::from(source as f32), &imm.loc);
}
} else if float64
&& ((from.code == support::FLOAT && matches!(from.bits, 16 | 32)) || is_bfloat16(from))
{
return float(source, &imm.loc);
} else if let Some(limit) = limit_16 {
if !source.is_finite() || (-limit..=limit).contains(&source) {
return float(source, &imm.loc);
}
}
}
Ok(None)
}
fn cast_scalar(value: Expr, ty: PrimType, loc: &Location) -> Result<Expr> {
match fold_cast(&value, ty.dtype)? {
Some(folded) => Ok(folded),
None => Ok(Cast::new(ty, value, loc.clone())?.into()),
}
}
unlocated! {
cast(value: impl IntoExpr, ty: impl Into<PrimType>) => cast_at;
}
pub fn cast_at(
value: impl IntoExpr,
ty: impl Into<PrimType>,
loc: impl Into<Location>,
) -> Result<Expr> {
let (value, ty, loc) = (value.into_expr()?, ty.into(), loc.into());
let (from, to) = (support::prim_dtype(&value)?, ty.dtype);
if from == to {
return Ok(value);
}
if from == void() {
let message = "Cannot cast an expression with the void sentinel type";
return Err(support::check_failed(TYPE_ERROR, "!value.ty().IsVoid()", message));
}
if support::is_scalar(to) {
return cast_scalar(value, ty, &loc);
}
let element = PrimType::from_dtype(with_lanes(to, 1))?;
if !is_vector(from) {
let value = cast_at(value, element, loc.clone())?;
return Ok(Broadcast::new(value, lanes_of(to)?, loc)?.into());
}
if support::is_scalable_vector(from) != support::is_scalable_vector(to) {
let condition = "value.ty().IsScalableVector() == dtype.IsScalableVector()";
return Err(support::check_failed(INTERNAL_ERROR, condition, ""));
}
let lanes_match = match support::is_scalable_vector(from) {
true => vscale_factor(from) == vscale_factor(to),
false => support::lanes(from)? == support::lanes(to)?,
};
if !lanes_match {
return Err(support::check_failed(INTERNAL_ERROR, "lanes_match", ""));
}
if let Some(broadcast) = value.downcast_ref::<BroadcastObj>() {
let value = cast_at(broadcast.value.clone(), element, loc.clone())?;
return Ok(Broadcast::new(value, broadcast.lanes.clone(), loc)?.into());
}
if let Some(ramp) = value.downcast_ref::<RampObj>() {
let integer = matches!(to.code, support::INT | support::UINT) && matches!(to.bits, 32 | 64);
if let (Some(base), Some(stride)) =
(const_fold::int(&ramp.base_), const_fold::int(&ramp.stride))
{
if support::is_fixed_length_vector(to) && integer {
let last = base + stride * i64::from(support::lanes(to)? - 1);
let source = support::prim_dtype(&ramp.base_)?;
let fits = |value: &BigInt| const_fold::is_representable(value, element.dtype);
if fits(base)
&& fits(stride)
&& fits(&last)
&& const_fold::is_representable(&last, source)
{
let base = cast_at(ramp.base_.clone(), element.clone(), loc.clone())?;
let stride = cast_at(ramp.stride.clone(), element, loc.clone())?;
return Ok(Ramp::new(base, stride, ramp.lanes.clone(), loc)?.into());
}
}
}
}
Ok(Cast::new(ty, value, loc)?.into())
}
fn broadcast_to_match_lanes(value: Expr, other: DLDataType) -> Result<Expr> {
if is_vector(support::prim_dtype(&value)?) || !is_vector(other) {
return Ok(value);
}
Ok(Broadcast::new(value, lanes_of(other)?, UNKNOWN_LOC)?.into())
}
fn promote_binary_op_type(lhs: DLDataType, rhs: DLDataType) -> Result<DLDataType> {
if lhs == rhs {
return Ok(lhs);
}
let wider = || if lhs.bits < rhs.bits { rhs } else { lhs };
let float = |dtype: DLDataType| dtype.code == support::FLOAT;
let bfloat = |dtype: DLDataType| dtype.code == support::BFLOAT;
let families: [fn(DLDataType) -> bool; 5] = [
float,
bfloat,
|dtype| is_float8(dtype.code),
|dtype| is_float6(dtype.code),
|dtype| is_float4(dtype.code),
];
if float(lhs) && float(rhs) {
return Ok(wider());
}
for family in families {
match (family(lhs), family(rhs)) {
(false, true) => return Ok(rhs),
(true, false) => return Ok(lhs),
_ => {}
}
}
let integer = |dtype: DLDataType| matches!(dtype.code, support::INT | support::UINT);
match (lhs.code, rhs.code) {
(support::BOOL, _) if integer(rhs) => Ok(rhs),
(_, support::BOOL) if integer(lhs) => Ok(lhs),
(support::INT, support::INT) | (support::UINT, support::UINT) => Ok(wider()),
(support::INT, support::UINT) | (support::UINT, support::INT) => match lhs.bits != rhs.bits
{
true => Ok(wider()),
false => Ok(DLDataType { code: support::UINT, ..lhs }),
},
_ => {
let (lhs, rhs) = (support::dtype_name(lhs), support::dtype_name(rhs));
let message = format!("Cannot match type {lhs} vs {rhs}");
Err(Error::new(INTERNAL_ERROR, &message, ""))
}
}
}
pub fn match_types(a: impl IntoExpr, b: impl IntoExpr) -> Result<(Expr, Expr)> {
match_types_at(a, b, UNKNOWN_LOC)
}
pub fn match_types_at(
a: impl IntoExpr,
b: impl IntoExpr,
loc: impl Into<Location>,
) -> Result<(Expr, Expr)> {
let (a, b, loc) = (a.into_expr()?, b.into_expr()?, loc.into());
let (a_dtype, b_dtype) = (support::prim_dtype(&a)?, support::prim_dtype(&b)?);
if a_dtype == b_dtype {
return Ok((a, b));
}
let a = broadcast_to_match_lanes(a, b_dtype)?;
let b = broadcast_to_match_lanes(b, support::prim_dtype(&a)?)?;
let (a_dtype, b_dtype) = (support::prim_dtype(&a)?, support::prim_dtype(&b)?);
if support::is_scalable_vector(a_dtype) != support::is_scalable_vector(b_dtype) {
let condition = "lhs_ty.IsScalableVector() == rhs_ty.IsScalableVector()";
let message = "Can't match scalable and fixed length vectors";
return Err(support::check_failed(INTERNAL_ERROR, condition, message));
}
let lanes_match = match support::is_scalable_vector(a_dtype) {
true => vscale_factor(a_dtype) == vscale_factor(b_dtype),
false => support::lanes(a_dtype)? == support::lanes(b_dtype)?,
};
if !lanes_match {
let (lhs, rhs) = (support::dtype_name(a_dtype), support::dtype_name(b_dtype));
let message = format!("Cannot match type {lhs} vs {rhs}");
return Err(support::check_failed(INTERNAL_ERROR, "lanes_match", &message));
}
let promoted = promote_binary_op_type(a_dtype, b_dtype)?;
let a = match a_dtype == promoted {
true => a,
false => cast_at(a, PrimType::from_dtype(promoted)?, loc.clone())?,
};
let b = match b_dtype == promoted {
true => b,
false => cast_at(b, PrimType::from_dtype(promoted)?, loc)?,
};
Ok((a, b))
}
macro_rules! folded_binary {
($($(#[$doc:meta])* $name:ident / $at:ident: $fold:expr => $node:ident),+ $(,)?) => {
$(
$(#[$doc])*
pub fn $name(a: impl Operand, b: impl Operand) -> Result<Expr> {
$at(a, b, UNKNOWN_LOC)
}
#[doc = concat!("[`", stringify!($name), "`] at `loc`.")]
pub fn $at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
let (a, b) = match_types_at(a, b, loc.clone())?;
match $fold(&a, &b)? {
Some(folded) => Ok(folded),
None => Ok($node::new(a, b, loc)?.into()),
}
}
)+
};
}
folded_binary!(
add / add_at: const_fold::add => Add,
sub / sub_at: const_fold::sub => Sub,
mul / mul_at: const_fold::mul => Mul,
div / div_at: const_fold::div => Div,
truncmod / truncmod_at: const_fold::truncmod => Mod,
gt / gt_at: fold_gt => GT,
ge / ge_at: fold_ge => GE,
lt / lt_at: fold_lt => LT,
le / le_at: fold_le => LE,
ne / ne_at: fold_ne => NE,
);
fn fold_gt(a: &Expr, b: &Expr) -> const_fold::Folded {
const_fold::compare(a, b, |x, y| x > y, |x, y| x > y)
}
fn fold_ge(a: &Expr, b: &Expr) -> const_fold::Folded {
const_fold::compare(a, b, |x, y| x >= y, |x, y| x >= y)
}
fn fold_lt(a: &Expr, b: &Expr) -> const_fold::Folded {
const_fold::compare(a, b, |x, y| x < y, |x, y| x < y)
}
fn fold_le(a: &Expr, b: &Expr) -> const_fold::Folded {
const_fold::compare(a, b, |x, y| x <= y, |x, y| x <= y)
}
fn fold_ne(a: &Expr, b: &Expr) -> const_fold::Folded {
const_fold::compare(a, b, |x, y| x != y, |x, y| x != y)
}
unlocated! {
eq(a: impl Operand, b: impl Operand) => eq_at;
neg(a: impl IntoExpr) => neg_at;
truncdiv(a: impl Operand, b: impl Operand) => truncdiv_at;
floordiv(a: impl Operand, b: impl Operand) => floordiv_at;
floormod(a: impl Operand, b: impl Operand) => floormod_at;
indexdiv(a: impl Operand, b: impl Operand) => indexdiv_at;
indexmod(a: impl Operand, b: impl Operand) => indexmod_at;
ceildiv(a: impl Operand, b: impl Operand) => ceildiv_at;
min(a: impl Operand, b: impl Operand) => min_at;
max(a: impl Operand, b: impl Operand) => max_at;
}
pub fn eq_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
let (a, b) = match_types_at(a, b, loc.clone())?;
if let Some(folded) = const_fold::compare(&a, &b, |x, y| x == y, |x, y| x == y)? {
return Ok(folded);
}
if is_vscale_call(&a)? && is_vscale_call(&b)? {
return Ok(IntImm::bool(true).into());
}
Ok(EQ::new(a, b, loc)?.into())
}
pub fn neg_at(a: impl IntoExpr, loc: impl Into<Location>) -> Result<Expr> {
let (a, loc) = (a.into_expr()?, loc.into());
let dtype = support::prim_dtype(&a)?;
if let Some(imm) = a.downcast_ref::<IntImmObj>() {
let value = -imm.value.clone();
if const_fold::is_representable(&value, dtype) {
return Ok(IntImm::from_dtype(dtype, value, loc)?.into());
}
}
if let Some(imm) = a.downcast_ref::<FloatImmObj>() {
return Ok(FloatImm::from_dtype(dtype, -imm.value, loc)?.into());
}
sub(make_const_at(PrimType::from_dtype(dtype)?, 0, loc)?, a)
}
pub fn truncdiv_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let (a, b) = operands(a, b)?;
check_integers(&a, &b)?;
div_at(a, b, loc)
}
pub fn floordiv_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
check_integers(&a, &b)?;
let (a, b) = match_types_at(a, b, loc.clone())?;
match const_fold::floordiv(&a, &b)? {
Some(folded) => Ok(folded),
None => Ok(FloorDiv::new(a, b, loc)?.into()),
}
}
pub fn floormod_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
check_integers(&a, &b)?;
let (a, b) = match_types_at(a, b, loc.clone())?;
match const_fold::floormod(&a, &b)? {
Some(folded) => Ok(folded),
None => Ok(FloorMod::new(a, b, loc)?.into()),
}
}
pub fn indexdiv_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
floordiv_at(a, b, loc)
}
pub fn indexmod_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
floormod_at(a, b, loc)
}
pub fn ceildiv_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
check_integers(&a, &b)?;
let (a, b) = match_types_at(a, b, loc.clone())?;
let sum = add(a, b.clone())?;
let one = make_const(support::prim_type(&sum)?, 1)?;
let numerator = sub(sum, one)?;
match const_fold::floordiv(&numerator, &b)? {
Some(folded) => Ok(folded),
None => Ok(FloorDiv::new(numerator, b, loc)?.into()),
}
}
fn symbolic_infinities() -> (Expr, Expr) {
use std::cell::OnceCell;
thread_local! {
static INFINITIES: OnceCell<(Expr, Expr)> = const { OnceCell::new() };
}
support::shared(&INFINITIES, || Ok((sym::pos_inf()?, sym::neg_inf()?)))
}
pub fn min_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
let (pos_inf, neg_inf) = symbolic_infinities();
if a.same_as(&pos_inf) {
return Ok(b);
}
if a.same_as(&neg_inf) {
return Ok(a);
}
if b.same_as(&pos_inf) {
return Ok(a);
}
if b.same_as(&neg_inf) {
return Ok(b);
}
let (a, b) = match_types_at(a, b, loc.clone())?;
match const_fold::min(&a, &b)? {
Some(folded) => Ok(folded),
None => Ok(Min::new(a, b, loc)?.into()),
}
}
pub fn max_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
let (pos_inf, neg_inf) = symbolic_infinities();
if a.same_as(&pos_inf) {
return Ok(a);
}
if a.same_as(&neg_inf) {
return Ok(b);
}
if b.same_as(&pos_inf) {
return Ok(b);
}
if b.same_as(&neg_inf) {
return Ok(a);
}
let (a, b) = match_types_at(a, b, loc.clone())?;
match const_fold::max(&a, &b)? {
Some(folded) => Ok(folded),
None => Ok(Max::new(a, b, loc)?.into()),
}
}
unlocated! {
logical_and(a: impl IntoExpr, b: impl IntoExpr) => logical_and_at;
logical_or(a: impl IntoExpr, b: impl IntoExpr) => logical_or_at;
logical_not(a: impl IntoExpr) => logical_not_at;
all(conds: impl IntoIterator<Item = impl IntoExpr>) => all_at;
any(conds: impl IntoIterator<Item = impl IntoExpr>) => any_at;
if_then_else(cond: impl IntoExpr, true_value: impl IntoExpr, false_value: impl IntoExpr) => if_then_else_at;
}
pub fn logical_and_at(
a: impl IntoExpr,
b: impl IntoExpr,
loc: impl Into<Location>,
) -> Result<Expr> {
let (a, b) = (a.into_expr()?, b.into_expr()?);
check_operands(&a, &b, "&&", "logical AND", "boolean", (&[support::BOOL], BOOL_CODES))?;
match const_fold::and(&a, &b)? {
Some(folded) => Ok(folded),
None => Ok(And::new(a, b, loc)?.into()),
}
}
pub fn logical_or_at(a: impl IntoExpr, b: impl IntoExpr, loc: impl Into<Location>) -> Result<Expr> {
let (a, b) = (a.into_expr()?, b.into_expr()?);
check_operands(&a, &b, "||", "logical OR", "boolean", (&[support::BOOL], BOOL_CODES))?;
match const_fold::or(&a, &b)? {
Some(folded) => Ok(folded),
None => Ok(Or::new(a, b, loc)?.into()),
}
}
pub fn logical_not_at(a: impl IntoExpr, loc: impl Into<Location>) -> Result<Expr> {
let a = a.into_expr()?;
check_code(&a, "a", &[support::BOOL], BOOL_CODES, || {
format!(
"Expected boolean argument for ! operator (logical NOT), but received {} of type {}",
support::repr(&a),
support::repr(&a.ty)
)
})?;
match const_fold::not(&a)? {
Some(folded) => Ok(folded),
None => Ok(Not::new(a, loc)?.into()),
}
}
pub fn all_at(
conds: impl IntoIterator<Item = impl IntoExpr>,
loc: impl Into<Location>,
) -> Result<Expr> {
fold_conditions(conds, loc.into(), "all", logical_and_at)
}
pub fn any_at(
conds: impl IntoIterator<Item = impl IntoExpr>,
loc: impl Into<Location>,
) -> Result<Expr> {
fold_conditions(conds, loc.into(), "any", logical_or_at)
}
fn fold_conditions(
conds: impl IntoIterator<Item = impl IntoExpr>,
loc: Location,
name: &str,
op: fn(Expr, Expr, Location) -> Result<Expr>,
) -> Result<Expr> {
let mut conds = conds.into_iter().map(IntoExpr::into_expr);
let Some(first) = conds.next() else {
let message = format!("{name} must take at least 1 argument");
return Err(Error::new(VALUE_ERROR, &message, ""));
};
conds.try_fold(first?, |acc, cond| op(acc, cond?, loc.clone()))
}
pub fn if_then_else_at(
cond: impl IntoExpr,
true_value: impl IntoExpr,
false_value: impl IntoExpr,
loc: impl Into<Location>,
) -> Result<Expr> {
let (cond, loc) = (cond.into_expr()?, loc.into());
if support::prim_dtype(&cond)?.code != support::BOOL {
let condition = "cond.ty().MatchesCode(DLDataTypeCode::kDLBool)";
let message = "if_then_else only accept the condition to be boolean type.";
return Err(support::check_failed(INTERNAL_ERROR, condition, message));
}
let (true_value, false_value) = match_types_at(true_value, false_value, loc.clone())?;
if let Some(imm) = cond.downcast_ref::<IntImmObj>() {
return Ok(if imm.value.is_zero() { false_value } else { true_value });
}
let ty = true_value.ty.clone();
call(Some(&ty), "prim.if_then_else", vec![cond, true_value, false_value], loc)
}
fn fold_shift(
a: &Expr,
b: &Expr,
shift: impl FnOnce(&BigInt, &BigInt) -> Result<Option<BigInt>>,
loc: &Location,
) -> const_fold::Folded {
let dtype = support::prim_dtype(a)?;
if !const_fold::is_index_type(dtype) || !const_fold::is_index_type(support::prim_dtype(b)?) {
return Ok(None);
}
let (pa, pb) = (const_fold::int(a), const_fold::int(b));
if let Some(y) = pb {
if y.is_negative() || *y >= BigInt::from(i64::from(dtype.bits)) {
let ty = support::prim_type(a)?;
let condition = "pb->value >= 0 && pb->value < result_ty.bits()";
let message = format!(
"Shift amount must be non-negative and less than {} for type {}",
dtype.bits,
support::repr(&ty)
);
return Err(support::check_failed(INTERNAL_ERROR, condition, &message));
}
}
if let (Some(x), Some(y)) = (pa, pb) {
if let Some(value) = shift(x, y)? {
return Ok(Some(IntImm::from_dtype(dtype, value, loc.clone())?.into()));
}
}
if pb.is_some_and(BigInt::is_zero) {
return Ok(Some(a.clone()));
}
Ok(None)
}
unlocated! {
shift_left(a: impl Operand, b: impl Operand) => shift_left_at;
shift_right(a: impl Operand, b: impl Operand) => shift_right_at;
bitwise_not(a: impl IntoExpr) => bitwise_not_at;
}
pub fn shift_left_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
check_operands(&a, &b, "<<", "left shift", "integer", (&INTEGER, INTEGER_CODES))?;
let (a, b) = match_types_at(a, b, loc.clone())?;
let dtype = support::prim_dtype(&a)?;
let shift = |x: &BigInt, y: &BigInt| {
let value = x.try_shl(y.clone())?;
Ok(const_fold::is_representable(&value, dtype).then_some(value))
};
match fold_shift(&a, &b, shift, &loc)? {
Some(folded) => Ok(folded),
None => Ok(LShift::new(a, b, loc)?.into()),
}
}
pub fn shift_right_at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
check_operands(&a, &b, ">>", "right shift", "integer", (&INTEGER, INTEGER_CODES))?;
let (a, b) = match_types_at(a, b, loc.clone())?;
match fold_shift(&a, &b, |x, y| x.try_shr(y.clone()).map(Some), &loc)? {
Some(folded) => Ok(folded),
None => Ok(RShift::new(a, b, loc)?.into()),
}
}
macro_rules! bitwise {
($($(#[$doc:meta])* $name:ident / $at:ident: $symbol:literal, $what:literal, $op:tt => $node:ident),+ $(,)?) => {
$(
$(#[$doc])*
pub fn $name(a: impl Operand, b: impl Operand) -> Result<Expr> {
$at(a, b, UNKNOWN_LOC)
}
#[doc = concat!("[`", stringify!($name), "`] at `loc`.")]
pub fn $at(a: impl Operand, b: impl Operand, loc: impl Into<Location>) -> Result<Expr> {
let ((a, b), loc) = (operands(a, b)?, loc.into());
let accepted = (&INTEGER_OR_BOOL[..], INTEGER_OR_BOOL_CODES);
check_operands(&a, &b, $symbol, $what, "integer", accepted)?;
let (a, b) = match_types_at(a, b, loc.clone())?;
let dtype = support::prim_dtype(&a)?;
let index = const_fold::is_index_type(dtype)
&& const_fold::is_index_type(support::prim_dtype(&b)?);
if let (true, Some(x), Some(y)) =
(index, const_fold::int(&a), const_fold::int(&b))
{
return Ok(IntImm::from_dtype(dtype, x $op y, loc)?.into());
}
Ok($node::new(a, b, loc)?.into())
}
)+
};
}
bitwise!(
bitwise_and / bitwise_and_at: "&", "bitwise AND", & => BitwiseAnd,
bitwise_or / bitwise_or_at: "|", "bitwise OR", | => BitwiseOr,
bitwise_xor / bitwise_xor_at: "^", "bitwise XOR", ^ => BitwiseXor,
);
pub fn bitwise_not_at(a: impl IntoExpr, loc: impl Into<Location>) -> Result<Expr> {
let a = a.into_expr()?;
check_code(&a, "a", &INTEGER_OR_BOOL, INTEGER_OR_BOOL_CODES, || {
format!(
"Expected integer or boolean argument for ~ operator (bitwise NOT), but received {} \
of type {}",
support::repr(&a),
support::repr(&a.ty)
)
})?;
Ok(BitwiseNot::new(a, loc)?.into())
}
fn rounding(x: Expr, op: &'static str, fold: fn(f64) -> f64, loc: Location) -> Result<Expr> {
let dtype = support::prim_dtype(&x)?;
if matches!(dtype.code, support::INT | support::UINT | support::BOOL) {
return Ok(x);
}
if let Some(imm) = x.downcast_ref::<FloatImmObj>() {
return Ok(FloatImm::from_dtype(dtype, fold(imm.value), imm.loc.clone())?.into());
}
let ty = x.ty.clone();
call(Some(&ty), op, vec![x], loc)
}
macro_rules! unary {
($($(#[$doc:meta])* $name:ident / $at:ident => $imp:expr;)+) => {
$(
$(#[$doc])*
pub fn $name(x: impl IntoExpr) -> Result<Expr> {
$at(x, UNKNOWN_LOC)
}
#[doc = concat!("[`", stringify!($name), "`] at `loc`.")]
pub fn $at(x: impl IntoExpr, loc: impl Into<Location>) -> Result<Expr> {
$imp(x.into_expr()?, loc.into())
}
)+
};
}
unary! {
ceil / ceil_at => |x, loc| rounding(x, "prim.ceil", f64::ceil, loc);
floor / floor_at => |x, loc| rounding(x, "prim.floor", f64::floor, loc);
round / round_at => |x, loc| rounding(x, "prim.round", f64::round_ties_even, loc);
nearbyint / nearbyint_at => |x, loc| rounding(x, "prim.nearbyint", f64::round_ties_even, loc);
trunc / trunc_at => |x, loc| rounding(x, "prim.trunc", f64::trunc, loc);
abs / abs_at => absolute;
isnan / isnan_at => |x, loc| call(None, "prim.isnan", vec![x], loc);
isinf / isinf_at => infinite;
isfinite / isfinite_at => finite;
likely / likely_at => likely_hint;
assume / assume_at => |cond, loc| {
let ty: Type = PrimType::bool().into();
call(Some(&ty), "prim.assume", vec![cond], loc)
};
clz / clz_at => |x: Expr, loc| {
let ty = x.ty.clone();
call(Some(&ty), "prim.clz", vec![x], loc)
};
}
fn absolute(x: Expr, loc: Location) -> Result<Expr> {
let dtype = support::prim_dtype(&x)?;
match dtype.code {
support::INT => integer_abs(x, loc),
support::FLOAT | support::BFLOAT => match x.downcast_ref::<FloatImmObj>() {
Some(imm) => Ok(FloatImm::from_dtype(dtype, imm.value.abs(), imm.loc.clone())?.into()),
None => {
let ty = x.ty.clone();
call(Some(&ty), "prim.fabs", vec![x], loc)
}
},
support::UINT => Ok(x),
_ => {
let message = format!(
"Data type {} not supported for absolute op. Skipping absolute op...",
support::repr(&x.ty)
);
Err(Error::new(INTERNAL_ERROR, &message, ""))
}
}
}
fn integer_abs(x: Expr, loc: Location) -> Result<Expr> {
let dtype = support::prim_dtype(&x)?;
if let Some(imm) = x.downcast_ref::<IntImmObj>() {
let value = match imm.value.is_negative() {
true => -imm.value.clone(),
false => imm.value.clone(),
};
if const_fold::is_representable(&value, dtype) {
return Ok(IntImm::from_dtype(dtype, value, imm.loc.clone())?.into());
}
}
let zero = make_const(PrimType::from_dtype(dtype)?, 0)?;
let nonnegative = ge(x.clone(), zero)?;
let negated = neg(x.clone())?;
Ok(Select::new(nonnegative, x, negated, loc)?.into())
}
fn infinite(x: Expr, loc: Location) -> Result<Expr> {
let dtype = support::prim_dtype(&x)?;
let result = PrimType::bool().with_lanes(support::lanes(dtype)? as u16)?;
match dtype.code {
support::INT | support::UINT => make_const_at(result, false, loc),
support::FLOAT => {
let infinity = infinity_at(support::prim_type(&x)?, loc.clone())?;
let at_infinity = eq(abs_at(x.clone(), loc.clone())?, infinity)?;
let not_nan = logical_not(isnan_at(x, loc)?)?;
logical_and(at_infinity, not_nan)
}
_ => {
let message = format!(
"Data type {} not supported for finiteness ops. Skipping it...",
support::repr(&x.ty)
);
Err(Error::new(INTERNAL_ERROR, &message, ""))
}
}
}
fn finite(x: Expr, loc: Location) -> Result<Expr> {
let not_infinite = logical_not(isinf_at(x.clone(), loc.clone())?)?;
let not_nan = logical_not(isnan_at(x, loc)?)?;
logical_and(not_infinite, not_nan)
}
fn likely_hint(cond: Expr, loc: Location) -> Result<Expr> {
if cond.downcast_ref::<IntImmObj>().is_some() {
return Ok(cond);
}
let ty = cond.ty.clone();
call(Some(&ty), "prim.likely", vec![cond], loc)
}
fn check_scalar(dtype: DLDataType) -> Result<()> {
let lanes = support::lanes(dtype)?;
match lanes == 1 {
true => Ok(()),
false => Err(support::compare_failed(INTERNAL_ERROR, "dtype.lanes() == 1", lanes, 1, "")),
}
}
unlocated! {
max_value(ty: impl Into<PrimType>) => max_value_at;
min_value(ty: impl Into<PrimType>) => min_value_at;
infinity(ty: impl Into<PrimType>) => infinity_at;
vscale() => vscale_at;
}
pub fn max_value_at(ty: impl Into<PrimType>, loc: impl Into<Location>) -> Result<Expr> {
let (ty, loc) = (ty.into(), loc.into());
let dtype = ty.dtype;
check_scalar(dtype)?;
let (code, bits) = (dtype.code, dtype.bits);
let int = |value: BigInt| Ok(IntImm::from_dtype(dtype, value, loc.clone())?.into());
let float = |value: f64| Ok(FloatImm::from_dtype(dtype, value, loc.clone())?.into());
match code {
support::INT if bits == 64 => int(BigInt::from(i64::MAX)),
support::INT if bits < 64 => int(BigInt::from((1i64 << (bits - 1)) - 1)),
support::UINT if bits == 64 => int(BigInt::from(u64::MAX)),
support::UINT if bits < 64 => int(BigInt::from((1u64 << bits) - 1)),
support::FLOAT if bits == 64 => float(f64::MAX),
support::FLOAT if bits == 32 => float(f64::from(f32::MAX)),
support::FLOAT if bits == 16 => float(support::MAX_FLOAT16),
support::BFLOAT => float(support::MAX_BFLOAT16),
_ if is_float4(code) => float(6.0),
_ => match small_float_bound(code) {
Some((max, _)) => float(max),
None => {
let message = format!("Cannot decide max_value for type{}", support::repr(&ty));
Err(Error::new(INTERNAL_ERROR, &message, ""))
}
},
}
}
pub fn min_value_at(ty: impl Into<PrimType>, loc: impl Into<Location>) -> Result<Expr> {
let (ty, loc) = (ty.into(), loc.into());
let dtype = ty.dtype;
check_scalar(dtype)?;
let (code, bits) = (dtype.code, dtype.bits);
let int = |value: BigInt| Ok(IntImm::from_dtype(dtype, value, loc.clone())?.into());
let float = |value: f64| Ok(FloatImm::from_dtype(dtype, value, loc.clone())?.into());
match code {
support::INT if bits == 64 => int(BigInt::from(i64::MIN)),
support::INT if bits < 64 => int(BigInt::from(-(1i64 << (bits - 1)))),
support::UINT => int(BigInt::from(0i64)),
support::FLOAT if bits == 64 => float(f64::MIN),
support::FLOAT if bits == 32 => float(f64::from(f32::MIN)),
support::FLOAT if bits == 16 => float(-support::MAX_FLOAT16),
support::BFLOAT => float(-support::MAX_BFLOAT16),
_ if is_float4(code) => float(-6.0),
_ => match small_float_bound(code) {
Some((_, true)) => float(0.0),
Some((max, false)) => float(-max),
None => {
let message = format!("Cannot decide min_value for type{}", support::repr(&ty));
Err(Error::new(INTERNAL_ERROR, &message, ""))
}
},
}
}
pub fn infinity_at(ty: impl Into<PrimType>, loc: impl Into<Location>) -> Result<Expr> {
let (ty, loc) = (ty.into(), loc.into());
let dtype = ty.dtype;
check_scalar(dtype)?;
match (dtype.code, dtype.bits) {
(support::FLOAT, 64) => Ok(FloatImm::from_dtype(dtype, f64::INFINITY, loc)?.into()),
(support::FLOAT, 32 | 16) => {
Ok(FloatImm::from_dtype(dtype, f64::from(f32::INFINITY), loc)?.into())
}
_ => {
let message = format!("Cannot decide infinity for type {}", support::repr(&ty));
Err(Error::new(INTERNAL_ERROR, &message, ""))
}
}
}
pub fn vscale_at(loc: impl Into<Location>) -> Result<Expr> {
let ty: Type = PrimType::int(32).into();
call(Some(&ty), "prim.vscale", vec![], loc.into())
}
fn float_binary(x: Expr, y: Expr, op: &'static str, what: &str, loc: Location) -> Result<Expr> {
let (x, y) = match_types_at(x, y, loc.clone())?;
if support::prim_dtype(&x)?.code != support::FLOAT {
let condition = "x.ty().MatchesCode(DLDataTypeCode::kDLFloat)";
let message = format!("{what} only applies to float");
return Err(support::check_failed(INTERNAL_ERROR, condition, &message));
}
let ty = x.ty.clone();
call(Some(&ty), op, vec![x, y], loc)
}
unlocated! {
pow(x: impl IntoExpr, y: impl IntoExpr) => pow_at;
fmod(x: impl IntoExpr, y: impl IntoExpr) => fmod_at;
fma(x: impl IntoExpr, y: impl IntoExpr, z: impl IntoExpr) => fma_at;
}
pub fn pow_at(x: impl IntoExpr, y: impl IntoExpr, loc: impl Into<Location>) -> Result<Expr> {
float_binary(x.into_expr()?, y.into_expr()?, "prim.pow", "power", loc.into())
}
pub fn fmod_at(x: impl IntoExpr, y: impl IntoExpr, loc: impl Into<Location>) -> Result<Expr> {
float_binary(x.into_expr()?, y.into_expr()?, "prim.fmod", "fmod", loc.into())
}
pub fn fma_at(
x: impl IntoExpr,
y: impl IntoExpr,
z: impl IntoExpr,
loc: impl Into<Location>,
) -> Result<Expr> {
let x = x.into_expr()?;
let ty = x.ty.clone();
call(Some(&ty), "prim.fma", vec![x, y.into_expr()?, z.into_expr()?], loc.into())
}
fn unary_intrinsic(op: &'static str, x: Expr, float_only: bool, loc: Location) -> Result<Expr> {
let dtype = support::prim_dtype(&x)?;
if float_only && dtype.code != support::FLOAT && !is_bfloat16(dtype) {
let condition = "dtype.code() == DLDataTypeCode::kDLFloat || \
dtype.MatchesElementType(DLDataTypeCode::kDLBfloat, 16)";
let message =
format!("{op} only supports floating-point inputs, but got {}", support::repr(&x.ty));
return Err(support::check_failed(TYPE_ERROR, condition, &message));
}
if !is_bfloat16(dtype) {
let ty = x.ty.clone();
return call(Some(&ty), op, vec![x], loc);
}
let float32 = match support::is_scalable_vector(dtype) {
true => support::scalable_vector(PrimType::float(32).dtype, vscale_factor(dtype))?,
false => with_lanes(PrimType::float(32).dtype, dtype.lanes),
};
let float32 = PrimType::from_dtype(float32)?;
let x_float32: Expr = Cast::new(float32.clone(), x, loc.clone())?.into();
let result = call(Some(&float32.into()), op, vec![x_float32], loc.clone())?;
Ok(Cast::new(PrimType::from_dtype(dtype)?, result, loc)?.into())
}
macro_rules! unary_intrinsics {
($($(#[$doc:meta])* $name:ident / $at:ident => $op:literal, $float_only:literal;)+) => {
unary! {
$(
$(#[$doc])*
$name / $at => |x, loc| unary_intrinsic($op, x, $float_only, loc);
)+
}
};
}
unary_intrinsics! {
exp / exp_at => "prim.exp", false;
exp2 / exp2_at => "prim.exp2", false;
exp10 / exp10_at => "prim.exp10", false;
erf / erf_at => "prim.erf", false;
sigmoid / sigmoid_at => "prim.sigmoid", false;
sqrt / sqrt_at => "prim.sqrt", false;
rsqrt / rsqrt_at => "prim.rsqrt", false;
log / log_at => "prim.log", false;
log2 / log2_at => "prim.log2", false;
log10 / log10_at => "prim.log10", false;
log1p / log1p_at => "prim.log1p", false;
popcount / popcount_at => "prim.popcount", false;
tanh / tanh_at => "prim.tanh", true;
tan / tan_at => "prim.tan", true;
cos / cos_at => "prim.cos", true;
cosh / cosh_at => "prim.cosh", true;
sin / sin_at => "prim.sin", true;
sinh / sinh_at => "prim.sinh", true;
asin / asin_at => "prim.asin", true;
acos / acos_at => "prim.acos", true;
atan / atan_at => "prim.atan", true;
acosh / acosh_at => "prim.acosh", true;
asinh / asinh_at => "prim.asinh", true;
atanh / atanh_at => "prim.atanh", true;
}
macro_rules! binary_intrinsics {
($($(#[$doc:meta])* $name:ident / $at:ident => $op:literal;)+) => {
$(
$(#[$doc])*
pub fn $name(x: impl IntoExpr, y: impl IntoExpr) -> Result<Expr> {
$at(x, y, UNKNOWN_LOC)
}
#[doc = concat!("[`", stringify!($name), "`] at `loc`.")]
pub fn $at(
x: impl IntoExpr,
y: impl IntoExpr,
loc: impl Into<Location>,
) -> Result<Expr> {
let x = x.into_expr()?;
let ty = x.ty.clone();
call(Some(&ty), $op, vec![x, y.into_expr()?], loc.into())
}
)+
};
}
binary_intrinsics! {
atan2 / atan2_at => "prim.atan2";
nextafter / nextafter_at => "prim.nextafter";
copysign / copysign_at => "prim.copysign";
hypot / hypot_at => "prim.hypot";
ldexp / ldexp_at => "prim.ldexp";
}
#[derive(Clone)]
#[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;
fn try_from(value: ExprResult) -> Result<Self> {
value.0.map(Into::into)
}
}
impl TryFrom<ExprResult> for PrimExpr {
type Error = Error;
fn try_from(value: ExprResult) -> Result<Self> {
value.0
}
}
impl ExprResult {
fn dtype(&self) -> Result<DLDataType> {
match &self.0 {
Ok(expr) => Ok(expr.dtype()),
Err(error) => Err(error.clone()),
}
}
}
impl IntoExpr for ExprResult {
fn into_expr(self) -> Result<Expr> {
self.0.map(Into::into)
}
}
impl IntoExpr for &ExprResult {
fn into_expr(self) -> Result<Expr> {
self.0.clone().map(Into::into)
}
}
impl<T: Into<Expr>> From<Result<T>> for ExprResult {
fn from(value: Result<T>) -> Self {
ExprResult::checked(value.map(Into::into))
}
}
impl ExprResult {
fn checked(value: Result<Expr>) -> Self {
ExprResult(value.and_then(PrimExpr::try_from))
}
}
impl From<&ExprResult> for ExprResult {
fn from(value: &ExprResult) -> Self {
value.clone()
}
}
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)) => ExprResult::checked(op(a.into(), b.into())),
(Err(error), _) | (_, Err(error)) => ExprResult(Err(error)),
}
}
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 {
use std::ops::{
Add, AddAssign, BitAnd, BitOr, BitXor, Mul, MulAssign, Neg, Not, Shl, Shr, Sub, SubAssign,
};
use super::{binary, binary_literal, ExprResult, Operand};
macro_rules! binary_operators {
($($Trait:ident::$method:ident => $op:ident),+ $(,)?) => {
$(
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; &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 = ExprResult;
fn $method(self, rhs: R) -> ExprResult {
binary(self.into(), rhs, super::$op)
}
}
)+
};
(@literal $Trait:ident::$method:ident => $op:ident; $($lit:ty),+) => {
$(
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 = ExprResult;
fn $method(self, rhs: $rhs) -> ExprResult {
binary_literal(self, rhs.into(), super::$op)
}
}
)+
};
}
binary_operators!(
Add::add => add,
Sub::sub => sub,
Mul::mul => mul,
BitAnd::bitand => bitwise_and,
BitOr::bitor => bitwise_or,
BitXor::bitxor => bitwise_xor,
Shl::shl => shift_left,
Shr::shr => shift_right,
);
macro_rules! assign_operators {
($($Trait:ident::$method:ident => $op:ident),+ $(,)?) => {
$(
impl<R: Operand> $Trait<R> for ExprResult {
fn $method(&mut self, rhs: R) {
*self = binary(self.clone(), rhs, super::$op);
}
}
)+
};
}
assign_operators!(AddAssign::add_assign => add, SubAssign::sub_assign => sub, MulAssign::mul_assign => mul);
macro_rules! unary_operators {
($($ty:ty),+) => {
$(
impl Neg for $ty {
type Output = ExprResult;
fn neg(self) -> ExprResult {
ExprResult::checked(super::neg(ExprResult::from(self)))
}
}
impl Not for $ty {
type Output = ExprResult;
fn not(self) -> ExprResult {
ExprResult::checked(super::logical_not(ExprResult::from(self)))
}
}
)+
};
}
unary_operators!(ExprResult, &ExprResult);
}
macro_rules! unary_methods {
($($(#[$doc:meta])* $name:ident;)+) => {
$(
$(#[$doc])*
fn $name(&self) -> ExprResult {
ExprResult::checked($name(self.to_expr_result()))
}
)+
};
}
macro_rules! binary_methods {
($($(#[$doc:meta])* $name:ident;)+) => {
$(
$(#[$doc])*
fn $name(&self, rhs: impl Operand) -> ExprResult {
binary(self.to_expr_result(), rhs, $name)
}
)+
};
}
pub trait PrimOps {
fn to_expr_result(&self) -> ExprResult;
fn less(&self, rhs: impl Operand) -> ExprResult {
binary(self.to_expr_result(), rhs, lt)
}
fn less_equal(&self, rhs: impl Operand) -> ExprResult {
binary(self.to_expr_result(), rhs, le)
}
fn greater(&self, rhs: impl Operand) -> ExprResult {
binary(self.to_expr_result(), rhs, gt)
}
fn greater_equal(&self, rhs: impl Operand) -> ExprResult {
binary(self.to_expr_result(), rhs, ge)
}
fn equal(&self, rhs: impl Operand) -> ExprResult {
binary(self.to_expr_result(), rhs, eq)
}
fn not_equal(&self, rhs: impl Operand) -> ExprResult {
binary(self.to_expr_result(), rhs, ne)
}
fn logical_and(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(logical_and(self.to_expr_result(), rhs))
}
fn logical_or(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(logical_or(self.to_expr_result(), rhs))
}
binary_methods! {
div;
truncdiv;
truncmod;
floordiv;
floormod;
indexdiv;
indexmod;
ceildiv;
min;
max;
}
fn pow(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(pow(self.to_expr_result(), rhs))
}
fn fmod(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(fmod(self.to_expr_result(), rhs))
}
fn atan2(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(atan2(self.to_expr_result(), rhs))
}
fn nextafter(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(nextafter(self.to_expr_result(), rhs))
}
fn copysign(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(copysign(self.to_expr_result(), rhs))
}
fn hypot(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(hypot(self.to_expr_result(), rhs))
}
fn ldexp(&self, rhs: impl IntoExpr) -> ExprResult {
ExprResult::checked(ldexp(self.to_expr_result(), rhs))
}
unary_methods! {
bitwise_not;
floor;
ceil;
round;
nearbyint;
trunc;
abs;
isnan;
isinf;
isfinite;
likely;
assume;
clz;
exp;
exp2;
exp10;
erf;
sigmoid;
sqrt;
rsqrt;
log;
log2;
log10;
log1p;
popcount;
tanh;
tan;
cos;
cosh;
sin;
sinh;
asin;
acos;
atan;
acosh;
asinh;
atanh;
}
fn cast(&self, ty: impl Into<PrimType>) -> ExprResult {
ExprResult::checked(cast(self.to_expr_result(), ty))
}
fn if_then_else(&self, t: impl IntoExpr, f: impl IntoExpr) -> ExprResult {
ExprResult::checked(if_then_else(self.to_expr_result(), t, f))
}
fn select(&self, t: impl IntoExpr, f: impl IntoExpr) -> ExprResult {
ExprResult::checked(Select::new(self.to_expr_result(), t, f, UNKNOWN_LOC).map(Expr::from))
}
}
impl PrimOps for ExprResult {
fn to_expr_result(&self) -> ExprResult {
self.clone()
}
}