f8 0.2.0

A no_std, one-byte UNORM with exact rounding, saturating arithmetic, and SIMD conversion
Documentation
//! A one-byte unsigned normalized number (UNORM8), with no allocation or `std`.
//!
//! [`f8`] stores an integer `b` and represents the real number `b / 255`.
//! It is **not** an IEEE floating-point format: there is no exponent, negative
//! zero, infinity, or NaN. All 256 bit patterns are valid and ordered.
//!
//! # Quantization
//!
//! Conversion from `f32` rounds the **exact** real product `255 * x` to the
//! nearest integer, ties to even, after clamping to `[0, 1]`. NaN maps to zero.
//! Unlike `(x * 255.0).round()`, this neither rounds ties away from zero nor
//! introduces an intermediate floating-point rounding at bin boundaries.
//! Conversion to `f32` is the correctly rounded value of `b / 255`.
//!
//! ```
//! use f8::f8;
//!
//! let half = f8::from_f32(0.5);
//! assert_eq!(half.to_bits(), 128);
//! assert_eq!(half.to_f32(), 128.0 / 255.0);
//! assert_eq!(half + half, f8::ONE); // Saturates instead of wrapping.
//! assert_eq!(half * f8::ONE, half);
//! ```
//!
//! # Traits
//!
//! | Trait | Contract |
//! | --- | --- |
//! | `From<u8>`, `Into<u8>` | Lossless raw storage, not a numeric cast. |
//! | `From<f32>` | Saturating, nearest-even quantization; NaN becomes zero. |
//! | `Into<f32>`, `Display` | Normalized value, not the raw byte. |
//! | `Eq`, `Ord`, `Hash` | Compare/hash the byte; a total numerical order. |
//! | `Default` | [`f8::ZERO`]. |
//! | `Add`, `Sub` and assignments | Exact UNORM addition/subtraction, saturated. |
//! | `Mul`, `Div` and assignments | Saturated, nearest-even UNORM arithmetic. |
//! | `Sum`, `Product` | Left folds, starting at zero/one; quantize each step. |
//! | Serde (feature `serde`) | A newtype named `f8` containing one `u8`. |
//!
//! Operators and iterator traits accept owned and borrowed values. Division
//! defines `0 / 0 = 0` and positive `/ 0 = 1`; no arithmetic operation panics.
//! Quantized multiplication is not associative. Accumulate in `f32` instead
//! when intermediate quantization is undesirable.
//!
//! # Platforms and Performance
//!
//! Scalar conversion and arithmetic use integer operations, even on soft-float
//! microcontrollers. [`f8::from_f32_slice`] and [`f8::to_f32_slice`] reuse caller
//! storage. With `simd`, x86-64 bulk encoding detects AVX2 **and OS support** at
//! runtime and uses an integer-only assembly kernel. Other targets, short
//! inputs, and Miri use portable Rust. A compile-time AVX2 target needs no probe.
//! AVX-512 targets stay in Rust so LLVM can use the wider instruction set.
//! Both features (`serde`, `simd`) are enabled by default; neither requires
//! `std` or an allocator. Disable default features for a dependency-free build.
//! Bare-metal x86-64, UEFI, and SGX always use Rust: this crate never probes or
//! assumes ownership of extended SIMD state on those targets. Explicit target
//! features may still let the compiler vectorize the portable loops.
//!
//! Layout is transparent over `u8`, with size/alignment one. Slice views are
//! zero-copy and endian-independent. This storage contract corresponds to
//! UNORM8, not any of the E4M3/E5M2 formats sometimes also called "FP8".

#![no_std]
#![deny(missing_docs, unsafe_op_in_unsafe_fn)]

mod ops;
#[cfg(all(
    feature = "simd",
    target_arch = "x86_64",
    not(target_feature = "avx512f"),
    not(any(miri, target_os = "none", target_os = "uefi", target_env = "sgx"))
))]
mod simd;

use core::fmt;

/// A one-byte UNORM representing `bits / 255` in the inclusive range `[0, 1]`.
///
/// Every bit pattern is valid. Conversion and arithmetic round to nearest,
/// ties to even; out-of-range results saturate. See the [crate documentation]
/// for the complete trait and numerical contracts.
///
/// [crate documentation]: crate
#[allow(non_camel_case_types)]
#[repr(transparent)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct f8(u8);

impl f8 {
    /// The additive identity, encoded as `0`.
    pub const ZERO: Self = Self(0);
    /// The multiplicative identity, encoded as `255`.
    pub const ONE: Self = Self(255);
    /// The smallest representable value, zero.
    pub const MIN: Self = Self::ZERO;
    /// The largest representable value, one.
    pub const MAX: Self = Self::ONE;

    /// Constructs a value from its raw UNORM8 encoding without quantization.
    #[inline]
    pub const fn from_bits(bits: u8) -> Self {
        Self(bits)
    }

    /// Returns the raw UNORM8 encoding, not a numeric cast of the value.
    #[inline]
    pub const fn to_bits(self) -> u8 {
        self.0
    }

    /// Quantizes the exact value of `value` to nearest UNORM8, ties to even.
    ///
    /// Values below zero (including negative infinity) and all NaNs become
    /// [`ZERO`](Self::ZERO). Values above one become [`ONE`](Self::ONE).
    /// This integer-only conversion does not depend on floating-point rounding
    /// modes or flush-to-zero settings and is available in const contexts.
    ///
    /// ```
    /// use f8::f8;
    /// const HALF: f8 = f8::from_f32(0.5);
    /// assert_eq!(HALF.to_bits(), 128);
    /// assert_eq!(f8::from_f32(f32::NAN), f8::ZERO);
    /// assert_eq!(f8::from_f32(f32::INFINITY), f8::ONE);
    /// ```
    #[inline]
    pub const fn from_f32(value: f32) -> Self {
        let bits = value.to_bits();
        // All negative values, NaNs, and values below 2^-9 quantize to zero.
        if bits > 0x7f80_0000 || bits < 0x3b00_0000 {
            return Self::ZERO;
        }
        if bits >= 0x3f80_0000 {
            return Self::ONE;
        }

        // For 2^-9 <= x < 1, 255*x = product / 2^(shift + 1), exactly.
        // The 24-bit significand times 255 fits in u32. Retaining the guard
        // bit separately avoids both 32-bit shifts and rounding overflow.
        let product = ((bits & 0x007f_ffff) | 0x0080_0000) * 255;
        let shift = 149 - (bits >> 23); // 23..=31: one less than the divisor shift.
        let guard = product >> shift;
        let sticky = product & ((1 << shift) - 1);
        let lower = guard >> 1;
        Self((lower + ((guard & 1) & ((sticky != 0 || lower & 1 != 0) as u32))) as u8)
    }

    /// Returns the correctly rounded `f32` representation of `self.to_bits() / 255`.
    ///
    /// Every byte survives a round trip through `f32`. No division, lookup
    /// table, or floating-point arithmetic is needed, including on soft-float
    /// targets.
    #[inline]
    pub const fn to_f32(self) -> f32 {
        if self.0 == 0 {
            return 0.0;
        }
        // b/255 is the repeating binary fraction 0.bbbbbbbb bbbbbbbb ... .
        // Normalize four repetitions and round using the next bit. Nonzero
        // repetitions cannot be an exact halfway case; 255 carries to 1.0.
        let repeated = self.0 as u32 * 0x0101_0101;
        let zeros = repeated.leading_zeros();
        let normalized = repeated << zeros;
        let significand = (normalized >> 8) + ((normalized >> 7) & 1);
        f32::from_bits(((125 - zeros) << 23) + significand)
    }

    /// Quantizes a slice into caller-owned storage, with scalar-equivalent results.
    ///
    /// With `simd`, sufficiently long slices use AVX2 on eligible x86-64
    /// hosts; see [platform dispatch](crate#platforms-and-performance).
    /// No alignment beyond normal Rust slice alignment is required.
    ///
    /// # Panics
    ///
    /// Panics if the lengths differ, before writing any output.
    ///
    /// ```
    /// use f8::f8;
    /// let mut packed = [f8::ZERO; 3];
    /// f8::from_f32_slice(&[0.0, 0.5, 1.0], &mut packed);
    /// assert_eq!(f8::as_bytes(&packed), &[0, 128, 255]);
    /// ```
    #[inline]
    pub fn from_f32_slice(src: &[f32], dst: &mut [Self]) {
        assert_eq!(
            src.len(),
            dst.len(),
            "source and destination lengths differ"
        );
        #[cfg(all(
            feature = "simd",
            target_arch = "x86_64",
            not(target_feature = "avx512f"),
            not(any(miri, target_os = "none", target_os = "uefi", target_env = "sgx"))
        ))]
        if src.len() >= 32 && simd::available() {
            // SAFETY: the probe verifies CPU and OS AVX2 support. Lengths match.
            unsafe { simd::encode(src, dst) };
            return;
        }
        for (&value, out) in src.iter().zip(dst) {
            *out = Self::from_f32(value);
        }
    }

    /// Decodes a slice into caller-owned storage, with scalar-equivalent results.
    ///
    /// Uses a vectorizable division loop on hardware-floating-point targets,
    /// and integer bit construction elsewhere. As with Rust floating-point
    /// arithmetic generally, hardware decoding requires the default floating-
    /// point environment (round to nearest, ties to even).
    ///
    /// # Panics
    ///
    /// Panics if the lengths differ, before writing any output.
    #[inline]
    pub fn to_f32_slice(src: &[Self], dst: &mut [f32]) {
        assert_eq!(
            src.len(),
            dst.len(),
            "source and destination lengths differ"
        );
        for (&value, out) in src.iter().zip(dst) {
            // Exact division vectorizes better than bit normalization on these
            // targets; avoid introducing soft-float division on microcontrollers.
            #[cfg(any(
                target_feature = "sse2",
                target_feature = "neon",
                target_feature = "vfp2",
                target_feature = "f",
                target_family = "wasm"
            ))]
            {
                *out = value.0 as f32 / 255.0;
            }
            #[cfg(not(any(
                target_feature = "sse2",
                target_feature = "neon",
                target_feature = "vfp2",
                target_feature = "f",
                target_family = "wasm"
            )))]
            {
                *out = value.to_f32();
            }
        }
    }

    /// Views UNORM8 storage as bytes without copying.
    #[inline]
    pub fn as_bytes(values: &[Self]) -> &[u8] {
        // SAFETY: Self is transparent over u8; the lifetime and length are unchanged.
        unsafe { core::slice::from_raw_parts(values.as_ptr().cast(), values.len()) }
    }

    /// Mutably views UNORM8 storage as bytes without copying.
    ///
    /// Every byte remains a valid `f8` after mutation.
    #[inline]
    pub fn as_bytes_mut(values: &mut [Self]) -> &mut [u8] {
        // SAFETY: identical layout and all bit patterns valid; the borrow stays exclusive.
        unsafe { core::slice::from_raw_parts_mut(values.as_mut_ptr().cast(), values.len()) }
    }

    /// Views bytes as UNORM8 values without copying, alignment checks, or quantization.
    #[inline]
    pub fn from_bytes(bytes: &[u8]) -> &[Self] {
        // SAFETY: Self has u8's layout and validity; the lifetime and length are unchanged.
        unsafe { core::slice::from_raw_parts(bytes.as_ptr().cast(), bytes.len()) }
    }

    /// Mutably views bytes as UNORM8 values without copying or quantization.
    ///
    /// ```
    /// use f8::f8;
    /// let mut bytes = [0, 128, 255];
    /// f8::from_bytes_mut(&mut bytes)[0] = f8::ONE;
    /// assert_eq!(bytes, [255, 128, 255]);
    /// ```
    #[inline]
    pub fn from_bytes_mut(bytes: &mut [u8]) -> &mut [Self] {
        // SAFETY: identical layout and all bit patterns valid; the borrow stays exclusive.
        unsafe { core::slice::from_raw_parts_mut(bytes.as_mut_ptr().cast(), bytes.len()) }
    }
}

impl From<u8> for f8 {
    #[inline]
    fn from(value: u8) -> Self {
        Self::from_bits(value)
    }
}

impl From<f32> for f8 {
    #[inline]
    fn from(value: f32) -> Self {
        Self::from_f32(value)
    }
}

impl From<f8> for u8 {
    #[inline]
    fn from(value: f8) -> Self {
        value.to_bits()
    }
}

impl From<f8> for f32 {
    #[inline]
    fn from(value: f8) -> Self {
        value.to_f32()
    }
}

impl fmt::Display for f8 {
    #[inline]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.to_f32().fmt(f)
    }
}