f8 0.2.0

A no_std, one-byte UNORM with exact rounding, saturating arithmetic, and SIMD conversion
Documentation
use f8::f8;

fn reference_bits(value: f64) -> u8 {
    value.clamp(0.0, 255.0).round_ties_even() as u8
}

#[test]
#[cfg_attr(miri, ignore = "pure-integer exhaustive coverage is run natively")]
#[allow(clippy::op_ref)]
fn exhaustive_arithmetic_and_overloads_match_f64_reference() {
    macro_rules! check {
        ($lhs:ident, $rhs:ident, $expected:expr, $method:ident, $op:tt, $assign:tt) => {{
            let mut owned_assignment = $lhs;
            owned_assignment $assign $rhs;
            let mut borrowed_assignment = $lhs;
            borrowed_assignment $assign &$rhs;

            assert_eq!(
                [
                    $lhs.$method($rhs),
                    $lhs $op $rhs,
                    $lhs $op &$rhs,
                    &$lhs $op $rhs,
                    &$lhs $op &$rhs,
                    owned_assignment,
                    borrowed_assignment,
                ],
                [f8::from_bits($expected); 7],
                "{}({}, {})",
                stringify!($method),
                $lhs.to_bits(),
                $rhs.to_bits(),
            );
        }};
    }

    for a in 0..=u8::MAX {
        for b in 0..=u8::MAX {
            let lhs = f8::from_bits(a);
            let rhs = f8::from_bits(b);
            // Byte-space calculations preserve exact division midpoints in f64.
            let left = f64::from(a);
            let right = f64::from(b);

            check!(lhs, rhs, reference_bits(left + right), saturating_add, +, +=);
            check!(lhs, rhs, reference_bits(left - right), saturating_sub, -, -=);
            check!(lhs, rhs, reference_bits(left * right / 255.0), saturating_mul, *, *=);

            let quotient = if b == 0 {
                if a == 0 { 0 } else { 255 }
            } else {
                reference_bits(left * 255.0 / right)
            };
            check!(lhs, rhs, quotient, saturating_div, /, /=);
        }
    }
}

#[test]
#[allow(clippy::eq_op)]
fn normalized_identities_hold_for_every_value() {
    for bits in 0..=u8::MAX {
        let value = f8::from_bits(bits);
        assert_eq!(value + f8::ZERO, value);
        assert_eq!(f8::ZERO + value, value);
        assert_eq!(value + f8::ONE, f8::ONE);
        assert_eq!(value - f8::ZERO, value);
        assert_eq!(value - value, f8::ZERO);
        assert_eq!(f8::ZERO - value, f8::ZERO);
        assert_eq!(value * f8::ONE, value);
        assert_eq!(f8::ONE * value, value);
        assert_eq!(value * f8::ZERO, f8::ZERO);
        assert_eq!(f8::ZERO * value, f8::ZERO);
        assert_eq!(value / f8::ONE, value);
    }
}

#[test]
#[allow(clippy::eq_op)]
fn zero_division_is_defined_and_positive_self_division_is_one() {
    assert_eq!(f8::ZERO / f8::ZERO, f8::ZERO);
    for bits in 1..=u8::MAX {
        let value = f8::from_bits(bits);
        assert_eq!(value / f8::ZERO, f8::ONE);
        assert_eq!(f8::ZERO / value, f8::ZERO);
        assert_eq!(value / value, f8::ONE);
        assert_eq!(f8::ONE / value, f8::ONE);
    }
}

#[test]
fn division_rounds_midpoints_to_even_in_both_directions() {
    for (a, b, expected) in [(1, 2, 128), (1, 6, 42), (1, 10, 26), (3, 10, 76)] {
        assert_eq!(
            (f8::from_bits(a) / f8::from_bits(b)).to_bits(),
            expected,
            "{a} / {b}",
        );
    }
}

#[test]
fn empty_and_singleton_iterators_use_normalized_identities() {
    assert_eq!(std::iter::empty::<f8>().sum::<f8>(), f8::ZERO);
    assert_eq!(std::iter::empty::<&f8>().sum::<f8>(), f8::ZERO);
    assert_eq!(std::iter::empty::<f8>().product::<f8>(), f8::ONE);
    assert_eq!(std::iter::empty::<&f8>().product::<f8>(), f8::ONE);

    for bits in 0..=u8::MAX {
        let value = f8::from_bits(bits);
        assert_eq!(std::iter::once(value).sum::<f8>(), value);
        assert_eq!(std::iter::once(&value).sum::<f8>(), value);
        assert_eq!(std::iter::once(value).product::<f8>(), value);
        assert_eq!(std::iter::once(&value).product::<f8>(), value);
    }
}

#[test]
fn iterator_sums_saturate() {
    for (bits, expected) in [([20, 30, 40], 90), ([100, 100, 100], 255)] {
        let values = bits.map(f8::from_bits);
        assert_eq!(values.iter().copied().sum::<f8>().to_bits(), expected);
        assert_eq!(values.iter().sum::<f8>().to_bits(), expected);
        assert_eq!(values.iter().rev().sum::<f8>().to_bits(), expected);
    }
}

#[test]
fn iterator_products_quantize_each_step_and_depend_on_order() {
    let values = [1, 128, 128].map(f8::from_bits);
    let forward = values.iter().copied().product::<f8>();
    let reverse = values.iter().rev().copied().product::<f8>();

    assert_eq!(forward.to_bits(), 1);
    assert_eq!(reverse, f8::ZERO);
    assert_eq!(values.iter().product::<f8>(), forward);
    assert_eq!(values.iter().rev().product::<f8>(), reverse);
    assert_eq!((values[0] * values[1]) * values[2], forward);
    assert_eq!(values[0] * (values[1] * values[2]), reverse);

    let unquantized_product = values
        .iter()
        .map(|value| f64::from(value.to_bits()) / 255.0)
        .product::<f64>();
    assert_eq!(reference_bits(unquantized_product * 255.0), 0);
    assert_ne!(
        forward.to_bits(),
        reference_bits(unquantized_product * 255.0)
    );

    let with_zero = [f8::ONE, f8::from_bits(128), f8::ZERO, f8::ONE];
    assert_eq!(with_zero.iter().copied().product::<f8>(), f8::ZERO);
    assert_eq!(with_zero.iter().product::<f8>(), f8::ZERO);
}

#[test]
fn saturating_methods_are_const() {
    const RESULTS: [f8; 9] = [
        f8::from_bits(200).saturating_add(f8::from_bits(100)),
        f8::from_bits(100).saturating_sub(f8::from_bits(200)),
        f8::from_bits(128).saturating_mul(f8::from_bits(128)),
        f8::ONE.saturating_mul(f8::ONE),
        f8::from_bits(1).saturating_div(f8::from_bits(2)),
        f8::from_bits(1).saturating_div(f8::from_bits(6)),
        f8::ONE.saturating_div(f8::from_bits(128)),
        f8::ZERO.saturating_div(f8::ZERO),
        f8::ONE.saturating_div(f8::ZERO),
    ];

    assert_eq!(
        RESULTS.map(f8::to_bits),
        [255, 0, 64, 255, 128, 42, 255, 0, 255]
    );
}