use bytemuck::Pod;
use crate::byte_storage::CpuStorageBytes;
use fuel_ir::{Error, Result};
pub trait BinaryOp<T: Copy> {
fn apply(a: T, b: T) -> T;
}
pub trait BinaryOpCore {
fn f32(a: f32, b: f32) -> f32;
fn f64(a: f64, b: f64) -> f64;
fn bf16(a: half::bf16, b: half::bf16) -> half::bf16 {
half::bf16::from_f32(Self::f32(a.to_f32(), b.to_f32()))
}
fn f16(a: half::f16, b: half::f16) -> half::f16 {
half::f16::from_f32(Self::f32(a.to_f32(), b.to_f32()))
}
}
impl<O: BinaryOpCore> BinaryOp<f32> for O {
fn apply(a: f32, b: f32) -> f32 {
<O as BinaryOpCore>::f32(a, b)
}
}
impl<O: BinaryOpCore> BinaryOp<f64> for O {
fn apply(a: f64, b: f64) -> f64 {
<O as BinaryOpCore>::f64(a, b)
}
}
impl<O: BinaryOpCore> BinaryOp<half::bf16> for O {
fn apply(a: half::bf16, b: half::bf16) -> half::bf16 {
<O as BinaryOpCore>::bf16(a, b)
}
}
impl<O: BinaryOpCore> BinaryOp<half::f16> for O {
fn apply(a: half::f16, b: half::f16) -> half::f16 {
<O as BinaryOpCore>::f16(a, b)
}
}
pub fn binary<T, U>(
name: &str,
lhs: &CpuStorageBytes,
rhs: &CpuStorageBytes,
output: &mut CpuStorageBytes,
) -> Result<()>
where
T: Copy + Pod,
U: BinaryOp<T>,
{
let lhs_bytes = lhs.len_bytes();
let rhs_bytes = rhs.len_bytes();
let out_bytes = output.len_bytes();
if lhs_bytes != rhs_bytes || lhs_bytes != out_bytes {
return Err(Error::Msg(format!(
"{name}: byte length mismatch (lhs={lhs_bytes}, rhs={rhs_bytes}, out={out_bytes})",
))
.bt());
}
let lhs_view: &[T] = lhs.as_slice()?;
let rhs_view: &[T] = rhs.as_slice()?;
let out_view: &mut [T] = output.as_slice_mut()?;
for (i, slot) in out_view.iter_mut().enumerate() {
*slot = U::apply(lhs_view[i], rhs_view[i]);
}
Ok(())
}
pub struct Add;
impl BinaryOpCore for Add {
fn f32(a: f32, b: f32) -> f32 {
a + b
}
fn f64(a: f64, b: f64) -> f64 {
a + b
}
}
pub struct Sub;
impl BinaryOpCore for Sub {
fn f32(a: f32, b: f32) -> f32 {
a - b
}
fn f64(a: f64, b: f64) -> f64 {
a - b
}
}
pub struct Mul;
impl BinaryOpCore for Mul {
fn f32(a: f32, b: f32) -> f32 {
a * b
}
fn f64(a: f64, b: f64) -> f64 {
a * b
}
}
pub struct Div;
impl BinaryOpCore for Div {
fn f32(a: f32, b: f32) -> f32 {
a / b
}
fn f64(a: f64, b: f64) -> f64 {
a / b
}
}
pub struct Maximum;
impl BinaryOpCore for Maximum {
fn f32(a: f32, b: f32) -> f32 {
if a.is_nan() {
a
} else if b.is_nan() {
b
} else if a >= b {
a
} else {
b
}
}
fn bf16(a: half::bf16, b: half::bf16) -> half::bf16 {
if a.is_nan() {
a
} else if b.is_nan() {
b
} else {
half::bf16::from_f32(Self::f32(a.to_f32(), b.to_f32()))
}
}
fn f16(a: half::f16, b: half::f16) -> half::f16 {
if a.is_nan() {
a
} else if b.is_nan() {
b
} else {
half::f16::from_f32(Self::f32(a.to_f32(), b.to_f32()))
}
}
fn f64(a: f64, b: f64) -> f64 {
if a.is_nan() {
a
} else if b.is_nan() {
b
} else if a >= b {
a
} else {
b
}
}
}
pub struct Minimum;
impl BinaryOpCore for Minimum {
fn f32(a: f32, b: f32) -> f32 {
if a.is_nan() {
a
} else if b.is_nan() {
b
} else if a <= b {
a
} else {
b
}
}
fn bf16(a: half::bf16, b: half::bf16) -> half::bf16 {
if a.is_nan() {
a
} else if b.is_nan() {
b
} else {
half::bf16::from_f32(Self::f32(a.to_f32(), b.to_f32()))
}
}
fn f16(a: half::f16, b: half::f16) -> half::f16 {
if a.is_nan() {
a
} else if b.is_nan() {
b
} else {
half::f16::from_f32(Self::f32(a.to_f32(), b.to_f32()))
}
}
fn f64(a: f64, b: f64) -> f64 {
if a.is_nan() {
a
} else if b.is_nan() {
b
} else if a <= b {
a
} else {
b
}
}
}
pub struct Pow;
impl BinaryOpCore for Pow {
fn f32(a: f32, b: f32) -> f32 {
crate::chassis::unary::quiet_f32(a.powf(b))
}
fn f64(a: f64, b: f64) -> f64 {
crate::chassis::unary::quiet_f64(a.powf(b))
}
}
pub struct Rem;
impl BinaryOpCore for Rem {
fn f32(a: f32, b: f32) -> f32 {
a - (a / b).floor() * b
}
fn f64(a: f64, b: f64) -> f64 {
a - (a / b).floor() * b
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pow_returns_a_quiet_nan_for_a_signalling_operand() {
let snan32 = f32::from_bits(0x7F80_0001);
let snan64 = f64::from_bits(0x7FF0_0000_0000_0001);
assert!(snan32.to_bits() & 0x0040_0000 == 0 && snan32.is_nan());
assert!(snan64.to_bits() & 0x0008_0000_0000_0000 == 0 && snan64.is_nan());
for (a, b) in [(snan32, 2.0f32), (2.0f32, snan32)] {
let r = <Pow as BinaryOpCore>::f32(a, b);
assert!(
r.is_nan() && r.to_bits() & 0x0040_0000 != 0,
"pow f32 {a:?} {b:?}"
);
}
for (a, b) in [(snan64, 2.0f64), (2.0f64, snan64)] {
let r = <Pow as BinaryOpCore>::f64(a, b);
assert!(
r.is_nan() && r.to_bits() & 0x0008_0000_0000_0000 != 0,
"pow f64 {a:?} {b:?}"
);
}
assert_eq!(<Pow as BinaryOpCore>::f32(2.0, 3.0), 8.0);
}
#[test]
fn minmax_move_a_nan_operand_without_quieting_it() {
const QUIET: u16 = 0x0040;
let s = half::bf16::from_bits(0x7F81);
let one = half::bf16::from_f32(1.0);
assert_eq!(s.to_bits() & QUIET, 0, "fixture must be SIGNALLING");
let control = <Add as BinaryOpCore>::bf16(s, one);
assert_ne!(
control.to_bits() & QUIET,
0,
"control failed: the promoting path did not quiet, so the assertions below prove nothing"
);
for (name, got) in [
("Maximum lhs", <Maximum as BinaryOpCore>::bf16(s, one)),
("Maximum rhs", <Maximum as BinaryOpCore>::bf16(one, s)),
("Minimum lhs", <Minimum as BinaryOpCore>::bf16(s, one)),
("Minimum rhs", <Minimum as BinaryOpCore>::bf16(one, s)),
] {
assert_eq!(
got.to_bits(),
s.to_bits(),
"{name} did not MOVE the NaN operand's bits (0x{:04X} -> 0x{:04X}); KISS-OPS-6.16-0009 requires payload AND signalling bit preserved",
s.to_bits(),
got.to_bits()
);
}
}
#[test]
fn minmax_non_nan_path_is_unchanged_by_the_nan_override() {
let cases = [
0x3F80u16, 0xC020, 0x0000, 0x8000, 0x7F7F, 0xFF7F, 0x7F80, 0xFF80,
];
for &ab in &cases {
for &bb in &cases {
let (a, b) = (half::bf16::from_bits(ab), half::bf16::from_bits(bb));
let want_max =
half::bf16::from_f32(<Maximum as BinaryOpCore>::f32(a.to_f32(), b.to_f32()));
let want_min =
half::bf16::from_f32(<Minimum as BinaryOpCore>::f32(a.to_f32(), b.to_f32()));
assert_eq!(
<Maximum as BinaryOpCore>::bf16(a, b).to_bits(),
want_max.to_bits(),
"Maximum diverged from the promoting path on (0x{ab:04X}, 0x{bb:04X}) -- the NaN override must not touch the non-NaN result, tie included"
);
assert_eq!(
<Minimum as BinaryOpCore>::bf16(a, b).to_bits(),
want_min.to_bits(),
"Minimum diverged from the promoting path on (0x{ab:04X}, 0x{bb:04X})"
);
}
}
}
#[test]
fn binary_op_add_f32_sums() {
assert_eq!(<Add as BinaryOp<f32>>::apply(2.5, 1.5), 4.0);
assert_eq!(<Add as BinaryOp<f32>>::apply(-3.0, 3.0), 0.0);
}
#[test]
fn binary_op_maximum_f32_picks_larger() {
assert_eq!(<Maximum as BinaryOp<f32>>::apply(2.5, 1.5), 2.5);
assert_eq!(<Maximum as BinaryOp<f32>>::apply(-3.0, 3.0), 3.0);
}
#[test]
fn binary_op_rem_f32_pytorch_sign_follows_divisor() {
let got = <Rem as BinaryOp<f32>>::apply(7.0, -3.0);
assert!((got - (-2.0)).abs() < 1e-6, "got {got}");
let got = <Rem as BinaryOp<f32>>::apply(-7.0, 3.0);
assert!((got - 2.0).abs() < 1e-6, "got {got}");
}
#[test]
fn binary_op_pow_f32() {
assert_eq!(<Pow as BinaryOp<f32>>::apply(2.0, 3.0), 8.0);
assert_eq!(<Pow as BinaryOp<f32>>::apply(4.0, 0.5), 2.0);
}
#[test]
fn binary_op_bf16_blanket_routes_through_f32() {
let a = half::bf16::from_f32(1.5);
let b = half::bf16::from_f32(2.5);
let got = <Mul as BinaryOp<half::bf16>>::apply(a, b).to_f32();
let expect = half::bf16::from_f32(1.5 * 2.5).to_f32();
assert_eq!(got, expect);
}
#[test]
fn binary_chassis_length_mismatch_errors() {
let lhs = CpuStorageBytes::from_slice(&[1.0_f32, 2.0]);
let rhs = CpuStorageBytes::from_slice(&[1.0_f32, 2.0, 3.0]); let mut out = CpuStorageBytes::from_zero_bytes(8);
let r = binary::<f32, Add>("test", &lhs, &rhs, &mut out);
assert!(r.is_err());
}
#[test]
fn binary_chassis_walks_all_elements() {
let lhs = CpuStorageBytes::from_slice(&[1.0_f32, 2.0, 3.0, 4.0]);
let rhs = CpuStorageBytes::from_slice(&[10.0_f32, 20.0, 30.0, 40.0]);
let mut out = CpuStorageBytes::from_zero_bytes(lhs.len_bytes());
binary::<f32, Add>("test", &lhs, &rhs, &mut out).expect("binary add_f32");
let r: &[f32] = out.as_slice().unwrap();
assert_eq!(r, &[11.0, 22.0, 33.0, 44.0]);
}
#[test]
fn maximum_signed_zero_tie_is_a_biased_f32() {
use std::hint::black_box;
let pz = black_box(0.0_f32);
let nz = black_box(-0.0_f32);
assert_eq!(
<Maximum as BinaryOp<f32>>::apply(pz, nz).to_bits(),
pz.to_bits()
);
assert_eq!(
<Maximum as BinaryOp<f32>>::apply(nz, pz).to_bits(),
nz.to_bits()
);
}
#[test]
fn minimum_signed_zero_tie_is_a_biased_f32() {
use std::hint::black_box;
let pz = black_box(0.0_f32);
let nz = black_box(-0.0_f32);
assert_eq!(
<Minimum as BinaryOp<f32>>::apply(pz, nz).to_bits(),
pz.to_bits()
);
assert_eq!(
<Minimum as BinaryOp<f32>>::apply(nz, pz).to_bits(),
nz.to_bits()
);
}
#[test]
fn minmax_signed_zero_tie_is_a_biased_f64() {
use std::hint::black_box;
let pz = black_box(0.0_f64);
let nz = black_box(-0.0_f64);
assert_eq!(
<Maximum as BinaryOp<f64>>::apply(pz, nz).to_bits(),
pz.to_bits()
);
assert_eq!(
<Maximum as BinaryOp<f64>>::apply(nz, pz).to_bits(),
nz.to_bits()
);
assert_eq!(
<Minimum as BinaryOp<f64>>::apply(pz, nz).to_bits(),
pz.to_bits()
);
assert_eq!(
<Minimum as BinaryOp<f64>>::apply(nz, pz).to_bits(),
nz.to_bits()
);
}
#[test]
fn minmax_signed_zero_tie_a_bias_inherits_through_bf16_and_f16() {
use std::hint::black_box;
let pz = half::bf16::from_f32(black_box(0.0));
let nz = half::bf16::from_f32(black_box(-0.0));
assert_eq!(
<Maximum as BinaryOp<half::bf16>>::apply(pz, nz).to_bits(),
pz.to_bits()
);
assert_eq!(
<Maximum as BinaryOp<half::bf16>>::apply(nz, pz).to_bits(),
nz.to_bits()
);
let pz = half::f16::from_f32(black_box(0.0));
let nz = half::f16::from_f32(black_box(-0.0));
assert_eq!(
<Minimum as BinaryOp<half::f16>>::apply(pz, nz).to_bits(),
pz.to_bits()
);
assert_eq!(
<Minimum as BinaryOp<half::f16>>::apply(nz, pz).to_bits(),
nz.to_bits()
);
}
}