Skip to main content

cubecl_ir/dialect/
math.rs

1use core::ops::Neg;
2
3use cubecl_macros_internal::{const_eval, cube_op, simplify};
4use half::{bf16, f16};
5use num_integer::Integer;
6use num_traits::Float;
7use pliron::{
8    attribute::AttrObj,
9    builtin::{attributes::IntegerAttr, types::IntegerType},
10    utils::apint::{APInt, bw},
11};
12
13use crate::{
14    CanMaterialize, Commutative, ConstantValue, NoMemoryEffect, NoSideEffects,
15    PropagatesUniformity, Pure,
16    attributes::{BoolAttr, FloatAttr, IndexAttr, IntAttrExt},
17    dialect::{pure_binop, pure_unop},
18    interfaces::{
19        Expression, ExpressionCanonicalize, ExpressionValue, TriviallyUnrollable, TypedExt,
20        side_effects::{ConditionallySpeculatable, Speculatability},
21    },
22    prelude::*,
23    types::{VectorType, scalar::BoolType},
24};
25
26pure_unop!("math.s_abs", SAbsOp);
27const_eval!(SAbsOp, {
28    [IntegerAttr(i8, i16, i32, i64)]: |inp| inp.abs(),
29});
30
31pure_unop!("math.f_abs", FAbsOp);
32const_eval!(FAbsOp, {
33    [FloatAttr(f16, bf16, f32, f64)]: |inp| inp.abs(),
34});
35
36#[cube_op(name = "math.c_abs")]
37#[result_ty(argument)]
38#[op_interfaces(TriviallyUnrollable)]
39#[op_traits(Pure, CanMaterialize)]
40pub struct CAbsOp {
41    pub input: Value,
42}
43
44pure_unop!("math.c_conj", CConjOp);
45
46#[cube_op(name = "math.c_real")]
47#[result_ty(argument)]
48#[op_interfaces(TriviallyUnrollable)]
49#[op_traits(Pure, CanMaterialize)]
50pub struct CRealOp {
51    pub input: Value,
52}
53
54#[cube_op(name = "math.c_imag")]
55#[result_ty(argument)]
56#[op_interfaces(TriviallyUnrollable)]
57#[op_traits(Pure, CanMaterialize)]
58pub struct CImagOp {
59    pub input: Value,
60}
61
62pure_unop!("math.exp", ExpOp);
63const_eval!(ExpOp, {
64    FloatAttr(f16, bf16, f32, f64): |inp| inp.exp(),
65});
66
67pure_unop!("math.log", LogOp);
68const_eval!(LogOp, {
69    FloatAttr(f16, bf16, f32, f64): |inp| inp.ln(),
70});
71
72pure_unop!("math.log1p", Log1pOp);
73const_eval!(Log1pOp, {
74    FloatAttr(f16, bf16, f32, f64): |inp| inp.ln_1p(),
75});
76
77pure_unop!("math.expm1", Expm1Op);
78const_eval!(Expm1Op, {
79    FloatAttr(f16, bf16, f32, f64): |inp| inp.exp_m1(),
80});
81
82pure_unop!("math.sin", SinOp);
83const_eval!(SinOp, {
84    FloatAttr(f16, bf16, f32, f64): |inp| inp.sin(),
85});
86
87pure_unop!("math.cos", CosOp);
88const_eval!(CosOp, {
89    FloatAttr(f16, bf16, f32, f64): |inp| inp.cos(),
90});
91
92pure_unop!("math.tan", TanOp);
93const_eval!(TanOp, {
94    FloatAttr(f16, bf16, f32, f64): |inp| inp.tan(),
95});
96
97pure_unop!("math.sinh", SinhOp);
98const_eval!(SinhOp, {
99    FloatAttr(f16, bf16, f32, f64): |inp| inp.sinh(),
100});
101
102pure_unop!("math.cosh", CoshOp);
103const_eval!(CoshOp, {
104    FloatAttr(f16, bf16, f32, f64): |inp| inp.cosh(),
105});
106
107pure_unop!("math.tanh", TanhOp);
108const_eval!(TanhOp, {
109    FloatAttr(f16, bf16, f32, f64): |inp| inp.tanh(),
110});
111
112pure_unop!("math.arcsin", ArcSinOp);
113const_eval!(ArcSinOp, {
114    FloatAttr(f16, bf16, f32, f64): |inp| inp.asin(),
115});
116
117pure_unop!("math.arccos", ArcCosOp);
118const_eval!(ArcCosOp, {
119    FloatAttr(f16, bf16, f32, f64): |inp| inp.acos(),
120});
121
122pure_unop!("math.arctan", ArcTanOp);
123const_eval!(ArcTanOp, {
124    FloatAttr(f16, bf16, f32, f64): |inp| inp.atan(),
125});
126
127pure_unop!("math.arcsinh", ArcSinhOp);
128const_eval!(ArcSinhOp, {
129    FloatAttr(f16, bf16, f32, f64): |inp| inp.asinh(),
130});
131
132pure_unop!("math.arccosh", ArcCoshOp);
133const_eval!(ArcCoshOp, {
134    FloatAttr(f16, bf16, f32, f64): |inp| inp.acosh(),
135});
136
137pure_unop!("math.arctanh", ArcTanhOp);
138const_eval!(ArcTanhOp, {
139    FloatAttr(f16, bf16, f32, f64): |inp| inp.atanh(),
140});
141
142pure_unop!("math.degrees", DegreesOp);
143const_eval!(DegreesOp, {
144    FloatAttr(f16, bf16, f32, f64): |inp| inp.to_degrees(),
145});
146
147pure_unop!("math.radians", RadiansOp);
148const_eval!(RadiansOp, {
149    FloatAttr(f16, bf16, f32, f64): |inp| inp.to_radians(),
150});
151
152pure_unop!("math.sqrt", SqrtOp);
153const_eval!(SqrtOp, {
154    FloatAttr(f16, bf16, f32, f64): |inp| inp.sqrt(),
155});
156
157pure_unop!("math.rsqrt", RsqrtOp);
158const_eval!(RsqrtOp, {
159    FloatAttr(f16, bf16, f32, f64): |inp| inp.sqrt().recip(),
160});
161
162pure_unop!("math.round", RoundOp);
163const_eval!(RoundOp, {
164    FloatAttr(f16, bf16, f32, f64): |inp| inp.round(),
165});
166
167pure_unop!("math.floor", FloorOp);
168const_eval!(FloorOp, {
169    FloatAttr(f16, bf16, f32, f64): |inp| inp.floor(),
170});
171
172pure_unop!("math.ceil", CeilOp);
173const_eval!(CeilOp, {
174    FloatAttr(f16, bf16, f32, f64): |inp| inp.ceil(),
175});
176
177pure_unop!("math.trunc", TruncOp);
178const_eval!(TruncOp, {
179    FloatAttr(f16, bf16, f32, f64): |inp| inp.trunc(),
180});
181
182pure_unop!("math.erf", ErfOp);
183// Unstable as of now, don't want to make a buggy version myself
184// const_eval!(ErfOp, {
185//     FloatAttr(f16, bf16, f32, f64): |inp| inp.erf(),
186// });
187
188pure_unop!("math.recip", RecipOp);
189const_eval!(RecipOp, {
190    FloatAttr(f16, bf16, f32, f64): |inp| inp.recip(),
191});
192
193pure_unop!("math.s_neg", SNegOp);
194const_eval!(SNegOp, {
195    [IntegerAttr(i8, i16, i32, i64)]: |inp| inp.neg(),
196});
197
198pure_unop!("math.f_neg", FNegOp);
199const_eval!(FNegOp, {
200    [FloatAttr(f16, bf16, f32, f64)]: |inp| inp.neg(),
201});
202
203#[cube_op(name = "math.is_nan")]
204#[result_ty(from_inputs = pred_result_ty)]
205#[op_interfaces(TriviallyUnrollable)]
206#[op_traits(Pure, CanMaterialize, PropagatesUniformity)]
207pub struct IsNanOp {
208    pub input: Value,
209}
210const_eval!(IsNanOp, {
211    FloatAttr(f16, bf16, f32, f64): |inp| -> BoolAttr { inp.is_nan().into() }
212});
213
214#[cube_op(name = "math.is_inf")]
215#[result_ty(from_inputs = pred_result_ty)]
216#[op_interfaces(TriviallyUnrollable)]
217#[op_traits(Pure, CanMaterialize, PropagatesUniformity)]
218pub struct IsInfOp {
219    pub input: Value,
220}
221const_eval!(IsInfOp, {
222    FloatAttr(f16, bf16, f32, f64): |inp| -> BoolAttr { inp.is_infinite().into() }
223});
224
225fn pred_result_ty(ctx: &Context, input: &Value) -> TypeHandle {
226    let vectorization = input.vector_size(ctx);
227    let bool = BoolType::get(ctx).into();
228    if vectorization == 1 {
229        bool
230    } else {
231        VectorType::get(ctx, bool, vectorization).into()
232    }
233}
234
235pure_binop!("math.i_add", IAddOp);
236Commutative!(IAddOp);
237const_eval!(IAddOp, {
238    [IndexAttr, IntegerAttr(u8, u16, u32, u64)]: |lhs, rhs| lhs.wrapping_add(rhs),
239});
240simplify!(IAddOp, {
241    |lhs, _| match lhs?.as_int(ctx)?.is_zero() {
242        true => Some(self.rhs(ctx)),
243        _ => None,
244    },
245    |_, rhs| match rhs?.as_int(ctx)?.is_zero() {
246        true => Some(self.lhs(ctx)),
247        _ => None,
248    }
249});
250
251pure_binop!("math.f_add", FAddOp);
252Commutative!(FAddOp);
253const_eval!(FAddOp, {
254    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs + rhs
255});
256simplify!(FAddOp, {
257    |lhs, _| match lhs?.float_as_f64(ctx) {
258        Some(0.0) => Some(self.rhs(ctx)),
259        _ => None?,
260    },
261    |_, rhs| match rhs?.float_as_f64(ctx) {
262        Some(0.0) => Some(self.lhs(ctx)),
263        _ => None?,
264    }
265});
266
267pure_binop!("math.saturating_s_add", SaturatingSAddOp);
268Commutative!(SaturatingSAddOp);
269const_eval!(SaturatingSAddOp, {
270    [IntegerAttr(i8, i16, i32, i64)]: |lhs, rhs| lhs.saturating_add(rhs)
271});
272simplify!(SaturatingSAddOp, {
273    |lhs, _| match lhs?.as_int(ctx)?.is_zero() {
274        true => Some(self.rhs(ctx)),
275        false => None,
276    },
277    |_, rhs| match rhs?.as_int(ctx)?.is_zero() {
278        true => Some(self.lhs(ctx)),
279        false => None,
280    }
281});
282
283pure_binop!("math.saturating_u_add", SaturatingUAddOp);
284Commutative!(SaturatingUAddOp);
285const_eval!(SaturatingUAddOp, {
286    [IndexAttr, IntegerAttr(u8, u16, u32, u64)]: |lhs, rhs| lhs.saturating_add(rhs)
287});
288simplify!(SaturatingUAddOp, {
289    |lhs, _| match lhs?.as_int(ctx)?.is_zero() {
290        true => Some(self.rhs(ctx)),
291        false => None,
292    },
293    |_, rhs| match rhs?.as_int(ctx)?.is_zero() {
294        true => Some(self.lhs(ctx)),
295        false => None,
296    }
297});
298
299pure_binop!("math.i_sub", ISubOp);
300const_eval!(ISubOp, {
301    [IndexAttr, IntegerAttr(i8, i16, i32, i64), IntegerAttr(u8, u16, u32, u64)]: |lhs, rhs| lhs.wrapping_sub(rhs),
302    // x - x -> 0. Only for one lane: `int_attr` carries no vectorization.
303    custom: |_, _| {
304        let result = self.get_result(ctx);
305        if self.lhs(ctx) == self.rhs(ctx) && result.vector_size(ctx) == 1 {
306            Some(int_attr(ctx, result.get_type(ctx), 0))
307        } else {
308            None
309        }
310    }
311});
312simplify!(ISubOp, {
313    |_, rhs| match rhs?.as_int(ctx)?.is_zero() {
314        true => Some(self.lhs(ctx)),
315        false => None,
316    }
317});
318
319pure_binop!("math.f_sub", FSubOp);
320const_eval!(FSubOp, {
321    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs - rhs,
322    // x - x -> 0. Only for one lane: `float_attr` carries no vectorization.
323    custom: |_, _| {
324        let result = self.get_result(ctx);
325        if self.lhs(ctx) == self.rhs(ctx) && result.vector_size(ctx) == 1 {
326            Some(float_attr(ctx, result.get_type(ctx), 0.0))
327        } else {
328            None
329        }
330    }
331});
332simplify!(FSubOp, {
333    |_, rhs| match rhs?.as_const_val(ctx) {
334        ConstantValue::Float(val) if val.to_bits() == 0 => Some(self.lhs(ctx)),
335        _ => None?,
336    }
337});
338
339pure_binop!("math.saturating_s_sub", SaturatingSSubOp);
340const_eval!(SaturatingSSubOp, {
341    [IntegerAttr(i8, i16, i32, i64)]: |lhs, rhs| lhs.saturating_sub(rhs)
342});
343simplify!(SaturatingSSubOp, {
344    |_, rhs| match rhs?.as_int(ctx)?.is_zero() {
345        true => Some(self.lhs(ctx)),
346        false => None,
347    }
348});
349
350pure_binop!("math.saturating_u_sub", SaturatingUSubOp);
351const_eval!(SaturatingUSubOp, {
352    [IndexAttr, IntegerAttr(u8, u16, u32, u64)]: |lhs, rhs| lhs.saturating_sub(rhs)
353});
354simplify!(SaturatingUSubOp, {
355    |_, rhs| match rhs?.as_int(ctx)?.is_zero() {
356        true => Some(self.lhs(ctx)),
357        false => None,
358    }
359});
360
361pure_binop!("math.i_mul", IMulOp);
362Commutative!(IMulOp);
363const_eval!(IMulOp, {
364    [IndexAttr, IntegerAttr(i8, i16, i32, i64), IntegerAttr(u8, u16, u32, u64)]: |lhs, rhs| lhs.wrapping_mul(rhs),
365    // 0 * x -> 0; x * 0 -> 0
366    custom: |lhs, rhs| match lhs.or(rhs)?.as_int(ctx)?.is_zero() {
367        true => Some(int_attr(ctx, self.result_type(ctx), 0)),
368        false => None?
369    }
370});
371simplify!(IMulOp, {
372    |lhs, _| match lhs?.as_const_val(ctx) {
373        ConstantValue::Int(1) | ConstantValue::UInt(1) => {
374            Some(self.rhs(ctx))
375        }
376        _ => None?,
377    },
378    |_, rhs| match rhs?.as_const_val(ctx) {
379        ConstantValue::Int(1) | ConstantValue::UInt(1) => {
380            Some(self.lhs(ctx))
381        }
382        _ => None?,
383    }
384});
385
386pure_binop!("math.f_mul", FMulOp);
387Commutative!(FMulOp);
388const_eval!(FMulOp, {
389    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs * rhs,
390    // 0 * x -> 0; x * 0 -> 0
391    custom: |lhs, rhs| {
392        let const_val = lhs.or(rhs)?;
393        Some(match const_val.float_as_f64(ctx) {
394            Some(0.0) => float_attr(ctx, const_val.get_type(ctx), 0.0),
395            _ => None?
396        })
397    }
398});
399simplify!(FMulOp, {
400    |lhs, _| match lhs?.float_as_f64(ctx) {
401        Some(1.0) => Some(self.rhs(ctx)),
402        _ => None?,
403    },
404    |_, rhs| match rhs?.float_as_f64(ctx) {
405        Some(1.0) => Some(self.lhs(ctx)),
406        _ => None?,
407    }
408});
409
410#[cube_op(name = "math.s_div")]
411#[result_ty(same_as = lhs)]
412#[op_interfaces(SameOperandsType, SameOperandsAndResultType, TriviallyUnrollable)]
413#[op_traits(CanMaterialize, NoSideEffects, NoMemoryEffect, PropagatesUniformity)] // Not pure because divide by zero
414pub struct SDivOp {
415    pub lhs: Value,
416    pub rhs: Value,
417}
418const_eval!(SDivOp, {
419    [IntegerAttr(i8, i16, i32, i64)]: |lhs, rhs| lhs.wrapping_div(rhs),
420    // 0 / x -> 0
421    custom: |lhs, _| match lhs?.as_int(ctx)?.is_zero() {
422        true => Some(int_attr(ctx, self.result_type(ctx), 0)),
423        false => None
424    },
425    // x / x -> 1. Only for one lane: `int_attr` carries no vectorization either.
426    custom: |_, _| {
427        let result = self.get_result(ctx);
428        if self.lhs(ctx) == self.rhs(ctx) && result.vector_size(ctx) == 1 {
429            Some(int_attr(ctx, result.get_type(ctx), 1))
430        } else {
431            None
432        }
433    }
434});
435simplify!(SDivOp, {
436    |_, rhs| match rhs?.as_const_val(ctx) {
437        ConstantValue::Int(1) => Some(self.lhs(ctx)),
438        _ => None?,
439    }
440});
441
442#[op_interface_impl]
443impl ConditionallySpeculatable for SDivOp {
444    fn speculatability(&self, ctx: &Context) -> Speculatability {
445        let Some(const_val) = const_operand::<IntegerAttr>(ctx, self.get_operation(), 1) else {
446            return Speculatability::NotSpeculatable;
447        };
448        if const_val.value().is_zero() || const_val.value().to_i128() == -1 {
449            Speculatability::NotSpeculatable
450        } else {
451            Speculatability::Speculatable
452        }
453    }
454}
455
456#[cube_op(name = "math.u_div")]
457#[result_ty(same_as = lhs)]
458#[op_interfaces(SameOperandsType, SameOperandsAndResultType, TriviallyUnrollable)]
459#[op_traits(CanMaterialize, NoSideEffects, NoMemoryEffect, PropagatesUniformity)] // Not pure because divide by zero
460pub struct UDivOp {
461    pub lhs: Value,
462    pub rhs: Value,
463}
464const_eval!(UDivOp, {
465    [IndexAttr, IntegerAttr(u8, u16, u32, u64)]: |lhs, rhs| lhs.wrapping_div(rhs),
466    // 0 / x -> 0
467    custom: |lhs, _| match lhs?.as_int(ctx)?.is_zero() {
468        true => Some(int_attr(ctx, lhs?.get_type(ctx), 0)),
469        false => None
470    },
471    // x / x -> 1. Only for one lane: `int_attr` carries no vectorization either.
472    custom: |_, _| {
473        let result = self.get_result(ctx);
474        if self.lhs(ctx) == self.rhs(ctx) && result.vector_size(ctx) == 1 {
475            Some(int_attr(ctx, result.get_type(ctx), 1))
476        } else {
477            None
478        }
479    }
480});
481simplify!(UDivOp, {
482    |_, rhs| match rhs?.as_const_val(ctx) {
483        ConstantValue::UInt(1) => Some(self.lhs(ctx)),
484        _ => None?,
485    }
486});
487
488#[op_interface_impl]
489impl ConditionallySpeculatable for UDivOp {
490    fn speculatability(&self, ctx: &Context) -> Speculatability {
491        let Some(const_val) = const_operand::<IntegerAttr>(ctx, self.get_operation(), 1) else {
492            return Speculatability::NotSpeculatable;
493        };
494        if const_val.value().is_zero() {
495            Speculatability::NotSpeculatable
496        } else {
497            Speculatability::Speculatable
498        }
499    }
500}
501
502pure_binop!("math.f_div", FDivOp);
503const_eval!(FDivOp, {
504    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs / rhs,
505    // 0 / x -> 0
506    custom: |lhs, _| {
507        Some(match lhs?.float_as_f64(ctx) {
508            Some(0.0) => float_attr(ctx, lhs?.get_type(ctx), 0.0),
509            _ => None?
510        })
511    },
512});
513simplify!(FDivOp, {
514    |_, rhs| match rhs?.float_as_f64(ctx) {
515        Some(1.0) => Some(self.lhs(ctx)),
516        _ => None?,
517    }
518});
519
520pure_binop!("math.arc_tan2", ArcTan2Op);
521const_eval!(ArcTan2Op, {
522    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs.atan2(rhs),
523});
524
525pure_binop!("math.powf", PowfOp);
526const_eval!(PowfOp, {
527    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs.powf(rhs),
528});
529
530#[cube_op(name = "math.powi")]
531#[result_ty(same_as = lhs)]
532#[op_traits(Pure, CanMaterialize, PropagatesUniformity)]
533pub struct PowiOp {
534    pub lhs: Value,
535    pub rhs: Value,
536}
537
538// TODO const_eval
539
540pure_binop!("math.hypot", HypotOp);
541Commutative!(HypotOp);
542const_eval!(HypotOp, {
543    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs.hypot(rhs),
544});
545
546pure_binop!("math.rhypot", RhypotOp);
547Commutative!(RhypotOp);
548const_eval!(RhypotOp, {
549    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs.hypot(rhs).recip(),
550});
551
552#[cube_op(name = "math.s_rem")]
553#[result_ty(same_as = lhs)]
554#[op_interfaces(SameOperandsType, SameOperandsAndResultType, TriviallyUnrollable)]
555#[op_traits(CanMaterialize, NoSideEffects, NoMemoryEffect, PropagatesUniformity)] // Not pure because divide by zero
556pub struct SRemOp {
557    pub lhs: Value,
558    pub rhs: Value,
559}
560const_eval!(SRemOp, {
561    [IntegerAttr(i8, i16, i32, i64)]: |lhs, rhs| lhs % rhs,
562    // 0 % x -> 0
563    custom: |lhs, _| match lhs?.as_const_val(ctx) {
564        ConstantValue::Int(0) => Some(int_attr(ctx, lhs?.get_type(ctx), 0)),
565        _ => None
566    },
567    // x % 1 -> 0
568    custom: |_, rhs| match rhs?.as_const_val(ctx) {
569        ConstantValue::Int(1) => Some(int_attr(ctx, rhs?.get_type(ctx), 0)),
570        _ => None
571    }
572});
573simplify!(SRemOp, {
574    |_, rhs| match rhs?.as_const_val(ctx) {
575        ConstantValue::Int(1) => Some(self.lhs(ctx)),
576        _ => None?,
577    }
578});
579
580#[op_interface_impl]
581impl ConditionallySpeculatable for SRemOp {
582    fn speculatability(&self, ctx: &Context) -> Speculatability {
583        let Some(const_val) = const_operand::<IntegerAttr>(ctx, self.get_operation(), 1) else {
584            return Speculatability::NotSpeculatable;
585        };
586        if const_val.value().is_zero() || const_val.value().to_i128() == -1 {
587            Speculatability::NotSpeculatable
588        } else {
589            Speculatability::Speculatable
590        }
591    }
592}
593
594#[cube_op(name = "math.u_rem")]
595#[result_ty(same_as = lhs)]
596#[op_interfaces(SameOperandsType, SameOperandsAndResultType, TriviallyUnrollable)]
597#[op_traits(CanMaterialize, NoSideEffects, NoMemoryEffect, PropagatesUniformity)] // Not pure because divide by zero
598pub struct URemOp {
599    pub lhs: Value,
600    pub rhs: Value,
601}
602const_eval!(URemOp, {
603    [IndexAttr, IntegerAttr(u8, u16, u32, u64)]: |lhs, rhs| lhs % rhs,
604    // 0 % x -> 0
605    custom: |lhs, _| match lhs?.as_const_val(ctx) {
606        ConstantValue::UInt(0) => Some(int_attr(ctx, lhs?.get_type(ctx), 0)),
607        _ => None
608    },
609    // x % 1 -> 0
610    custom: |_, rhs| match rhs?.as_const_val(ctx) {
611        ConstantValue::UInt(1) => Some(int_attr(ctx, rhs?.get_type(ctx), 0)),
612        _ => None
613    }
614});
615simplify!(URemOp, {
616    |_, rhs| match rhs?.as_const_val(ctx) {
617        ConstantValue::UInt(1) => Some(self.lhs(ctx)),
618        _ => None?,
619    }
620});
621
622#[op_interface_impl]
623impl ConditionallySpeculatable for URemOp {
624    fn speculatability(&self, ctx: &Context) -> Speculatability {
625        let Some(const_val) = const_operand::<IntegerAttr>(ctx, self.get_operation(), 1) else {
626            return Speculatability::NotSpeculatable;
627        };
628        if const_val.value().is_zero() {
629            Speculatability::NotSpeculatable
630        } else {
631            Speculatability::Speculatable
632        }
633    }
634}
635
636pure_binop!("math.f_rem", FRemOp);
637const_eval!(FRemOp, {
638    [FloatAttr(f16, bf16, f32, f64)]: |lhs, rhs| lhs % rhs,
639    // 0 % x -> 0
640    custom: |lhs, _| match lhs?.float_as_f64(ctx) {
641        Some(0.0) => Some(float_attr(ctx, lhs?.get_type(ctx), 0.0)),
642        _ => None
643    },
644});
645simplify!(FRemOp, {
646    |_, rhs| match rhs?.float_as_f64(ctx) {
647        Some(1.0) => Some(self.lhs(ctx)),
648        _ => None?,
649    }
650});
651
652/// Signed floor modulo. A nonzero result has the same sign as the divisor.
653/// Division by zero and `MIN mod_floor -1` have no portable result or error guarantee.
654#[cube_op(name = "math.s_mod_floor")]
655#[result_ty(same_as = lhs)]
656#[op_interfaces(SameOperandsType, SameOperandsAndResultType, TriviallyUnrollable)]
657#[op_traits(CanMaterialize, NoSideEffects, NoMemoryEffect, PropagatesUniformity)] // Not pure because divide by zero
658pub struct SModFloorOp {
659    pub lhs: Value,
660    pub rhs: Value,
661}
662const_eval!(SModFloorOp, {
663    IntegerAttr(i8, i16, i32, i64): |lhs, rhs| lhs.mod_floor(&rhs),
664    // 0 % x -> 0
665    custom: |lhs, _| match lhs?.as_const_val(ctx) {
666        ConstantValue::Int(0) => Some(int_attr(ctx, lhs?.get_type(ctx), 0)),
667        _ => None?
668    },
669    // x % 1 -> 0
670    custom: |_, rhs| match rhs?.as_const_val(ctx) {
671        ConstantValue::Int(1) => Some(int_attr(ctx, rhs?.get_type(ctx), 0)),
672        _ => None?
673    }
674});
675
676#[op_interface_impl]
677impl ConditionallySpeculatable for SModFloorOp {
678    fn speculatability(&self, ctx: &Context) -> Speculatability {
679        let Some(const_val) = const_operand::<IntegerAttr>(ctx, self.get_operation(), 1) else {
680            return Speculatability::NotSpeculatable;
681        };
682        if const_val.value().is_zero() {
683            Speculatability::NotSpeculatable
684        } else {
685            Speculatability::Speculatable
686        }
687    }
688}
689
690pure_binop!("math.f_mod_floor", FModFloorOp);
691const_eval!(FModFloorOp, {
692    FloatAttr(f16, bf16, f32, f64): |lhs, rhs| lhs - (lhs / rhs).floor() * rhs,
693    // 0 % x -> 0
694    custom: |lhs, _| match lhs?.float_as_f64(ctx) {
695        Some(0.0) => Some(float_attr(ctx, lhs?.get_type(ctx), 0.0)),
696        _ => None?
697    },
698});
699
700pure_binop!("math.s_mul_hi", SMulHiOp);
701Commutative!(SMulHiOp);
702const_eval!(SMulHiOp, {
703    IntegerAttr(i64): |lhs, rhs| ((lhs as i128 * rhs as i128) >> 64) as i64,
704    IntegerAttr(i32): |lhs, rhs| ((lhs as i64 * rhs as i64) >> 32) as i32,
705    // 0 * x -> 0; x * 0 -> 0
706    custom: |lhs, rhs| match lhs.or(rhs)?.as_const_val(ctx) {
707        ConstantValue::Int(0) => Some(int_attr(ctx, self.result_type(ctx), 0)),
708        _ => None?
709    }
710});
711simplify!(SMulHiOp, {
712    |lhs, _| match lhs?.as_const_val(ctx) {
713        ConstantValue::Int(1) => Some(self.rhs(ctx)),
714        _ => None?,
715    },
716    |_, rhs| match rhs?.as_const_val(ctx) {
717        ConstantValue::Int(1) => Some(self.lhs(ctx)),
718        _ => None?,
719    }
720});
721
722pure_binop!("math.u_mul_hi", UMulHiOp);
723Commutative!(UMulHiOp);
724const_eval!(UMulHiOp, {
725    IndexAttr: |lhs, rhs| ((lhs as u128 * rhs as u128) >> 64) as usize,
726    IntegerAttr(u64): |lhs, rhs| ((lhs as u128 * rhs as u128) >> 64) as u64,
727    IntegerAttr(u32): |lhs, rhs| ((lhs as u64 * rhs as u64) >> 32) as u32,
728    // 0 * x -> 0; x * 0 -> 0
729    custom: |lhs, rhs| match lhs.or(rhs)?.as_const_val(ctx) {
730        ConstantValue::UInt(0) => Some(int_attr(ctx, self.result_type(ctx), 0)),
731        _ => None?
732    }
733});
734simplify!(UMulHiOp, {
735    |lhs, _| match lhs?.as_const_val(ctx) {
736        ConstantValue::UInt(1) => Some(self.rhs(ctx)),
737        _ => None?,
738    },
739    |_, rhs| match rhs?.as_const_val(ctx) {
740        ConstantValue::UInt(1) => Some(self.lhs(ctx)),
741        _ => None?,
742    }
743});
744
745pub(super) fn index_attr(val: usize) -> AttrObj {
746    AttrObj::from(IndexAttr::new(val))
747}
748
749pub(super) fn int_attr(ctx: &Context, ty: TypeHandle, val: i128) -> AttrObj {
750    if ty.is_index(ctx) {
751        IndexAttr::new(val as usize).into()
752    } else {
753        let ty = TypedHandle::<IntegerType>::from_handle(ty, ctx).unwrap();
754        let width = bw(ty.deref(ctx).width() as usize);
755        let val = APInt::from_i128(val, width);
756        AttrObj::from(IntegerAttr::new(ty, val))
757    }
758}
759
760pub(super) fn float_attr(ctx: &Context, ty: TypeHandle, val: f64) -> AttrObj {
761    AttrObj::from(FloatAttr::from_f64(ctx, ty, val))
762}
763
764#[cube_op(name = "math.fma")]
765#[result_ty(same_as = a)]
766#[op_interfaces(SameOperandsType, SameOperandsAndResultType, TriviallyUnrollable)]
767#[op_traits(Pure, CanMaterialize, PropagatesUniformity)]
768pub struct FmaOp {
769    pub a: Value,
770    pub b: Value,
771    pub c: Value,
772}
773const_eval!(FmaOp, {
774    FloatAttr(f16, bf16, f32, f64): |a, b, c| a * b + c,
775});
776
777#[op_interface_impl]
778impl ExpressionCanonicalize for FmaOp {
779    fn canonical_expression(
780        &self,
781        ctx: &Context,
782        mut operands: Vec<ExpressionValue>,
783    ) -> Expression {
784        if operands[0] > operands[1] {
785            operands.swap(0, 1)
786        }
787        Expression::new(
788            self.result_type(ctx),
789            Self::get_opid_static(),
790            operands,
791            Default::default(),
792        )
793    }
794}
795
796/// Dot product of four packed signed 8-bit integers, plus an `i32` accumulator.
797#[cube_op(name = "math.dp4a")]
798#[result_ty(same_as = a)]
799#[op_interfaces(SameOperandsType, SameOperandsAndResultType, TriviallyUnrollable)]
800#[op_traits(Pure, CanMaterialize)]
801pub struct Dp4aOp {
802    pub a: Value,
803    pub b: Value,
804    pub c: Value,
805}
806const_eval!(Dp4aOp, {
807    IntegerAttr(i32): |a, b, c| dp4a_const(a, b, c),
808});
809
810#[op_interface_impl]
811impl ExpressionCanonicalize for Dp4aOp {
812    fn canonical_expression(
813        &self,
814        ctx: &Context,
815        mut operands: Vec<ExpressionValue>,
816    ) -> Expression {
817        if operands[0] > operands[1] {
818            operands.swap(0, 1)
819        }
820        Expression::new(
821            self.result_type(ctx),
822            Self::get_opid_static(),
823            operands,
824            Default::default(),
825        )
826    }
827}
828
829fn dp4a_const(a: i32, b: i32, c: i32) -> i32 {
830    let byte = |value: i32, shift: u32| ((value as u32 >> shift) as u8) as i8 as i32;
831    [0, 8, 16, 24].into_iter().fold(c, |acc, shift| {
832        acc.wrapping_add(byte(a, shift).wrapping_mul(byte(b, shift)))
833    })
834}
835
836#[cfg(test)]
837mod dp4a_tests {
838    use super::dp4a_const;
839
840    #[test]
841    fn evaluates_signed_bytes_and_wraps_accumulator() {
842        let a = i32::from_le_bytes([1, -2i8 as u8, 127, -128i8 as u8]);
843        let b = i32::from_le_bytes([-3i8 as u8, 4, -5i8 as u8, 6]);
844        let dot = -3 - 8 - 635 - 768;
845
846        assert_eq!(dp4a_const(a, b, 10), dot + 10);
847        assert_eq!(dp4a_const(a, b, i32::MIN), i32::MIN.wrapping_add(dot));
848    }
849}