f8 0.2.0

A no_std, one-byte UNORM with exact rounding, saturating arithmetic, and SIMD conversion
Documentation
# f8 for The Rust Programming Language

[![Crates.io](https://img.shields.io/crates/v/f8.svg)](https://crates.io/crates/f8)
[![Docs.rs](https://docs.rs/f8/badge.svg)](https://docs.rs/f8)
[![LoC](https://tokei.rs/b1/github/attackgoat/f8?category=code)](https://github.com/attackgoat/f8)

One byte. Exact quantization. No allocator. No `std`.

`f8` is an **unsigned normalized 8-bit number**, representing `bits / 255` in
`[0, 1]`. It is suitable for compact colors, coverage, probabilities, masks,
weights, and normalized device data. It is **not** IEEE FP8, E4M3, or E5M2.

```toml
[dependencies]
f8 = "0.2"
```

```rust
use f8::f8;

const HALF: f8 = 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);
assert_eq!((HALF * HALF).to_bits(), 64);

let values = [HALF, f8::ONE];
assert_eq!(values.iter().sum::<f8>(), f8::ONE);
assert_eq!(values.iter().product::<f8>(), HALF);
```

## Numerical Contract

- Raw encoding `b` denotes the exact real number `b / 255`. All 256 encodings
  are valid; zero and one are exact endpoints.
- `from_f32(x)` clamps to `[0, 1]`, then rounds the **exact** product `255*x`
  to nearest, ties to even. All NaNs become zero; infinities saturate.
- `to_f32()` returns correctly rounded `b / 255`. Every byte round-trips.
- Quantization error is at most `1/510` against the clamped input, before
  the final `f32` decoding roundoff. This is a uniform grid, not relative precision.
- Addition/subtraction saturate instead of wrapping or panicking. Multiplication
  and division saturate and round to nearest, ties to even.
- `0 / 0 = 0`; positive `/ 0 = 1`. No arithmetic operation panics, including
  in debug builds. Arithmetic is not IEEE floating-point arithmetic.

The rounding rule is deliberate. `(x * 255.0_f32).round()` is **not** equivalent:
it uses different tie-breaking and can round the multiplication onto a false
midpoint. The scalar and accelerated encoders avoid both problems using integer
significand arithmetic. See [Numerics](docs/NUMERICS.md) for the derivation.

The byte storage interoperates with UNORM8 formats such as `R8_UNORM`. This is
not a claim that every GPU's float-to-UNORM conversion uses identical rounding.

## Trait Guide

| Task | API | Semantics |
| --- | --- | --- |
| Raw storage | `from_bits`, `to_bits`, `From<u8>`, `Into<u8>` | Lossless byte encoding; not a numeric cast. |
| Quantization | `from_f32`, `From<f32>` | Saturating, lossy, exact nearest-even rounding. |
| Decoding | `to_f32`, `Into<f32>` | Correctly rounded normalized value. |
| Initialization | `Default`, `ZERO`, `ONE`, `MIN`, `MAX` | Default is zero. |
| Ordering and maps | `Eq`, `Ord`, `Hash` | Byte equality, total numerical ordering; no NaNs to handle. |
| Arithmetic | `+`, `-`, `*`, `/`, assignment forms | Saturating UNORM arithmetic, owned or borrowed operands. |
| Const arithmetic | `saturating_add/sub/mul/div` | Same semantics as the corresponding operators. |
| Reductions | `Sum`, `Product` | Left folds; empty identities are zero and one. |
| Formatting | `Display`, `Debug` | Display is the normalized `f32`; Debug exposes the newtype and byte. |
| Serialization | Serde `Serialize`, `Deserialize` | Newtype `f8` containing a `u8`, never a serialized float. |

`Product` quantizes **each step**, so grouping or reordering can change results.
Accumulate in a wider representation and quantize once when accuracy matters:

```rust
use f8::f8;

let weights = [f8::from_bits(1), f8::from_bits(128), f8::from_bits(128)];
assert_eq!(weights.iter().product::<f8>().to_bits(), 1);
let result = f8::from_f32(weights.iter().map(|x| x.to_f32()).product());
assert_eq!(result, f8::ZERO);
```

## Buffers and Layout

Bulk conversion accepts caller-owned storage, does not allocate, and checks
lengths before writing. Inputs need only their normal Rust alignment; vector
widths, tails, and dispatch are internal details.

```rust
use f8::f8;

let mut packed = [f8::ZERO; 4];
f8::from_f32_slice(&[0.0, 0.25, 0.5, 1.0], &mut packed);
assert_eq!(f8::as_bytes(&packed), &[0, 64, 128, 255]);

let mut decoded = [0.0; 4];
f8::to_f32_slice(&packed, &mut decoded);

let mut bytes = [0, 128, 255];
f8::from_bytes_mut(&mut bytes)[0] = f8::ONE;
assert_eq!(bytes, [255, 128, 255]);
```

`#[repr(transparent)]` guarantees the layout/ABI of `u8`: one-byte size and
alignment, with no invalid bit patterns or endianness dependence. `as_bytes`,
`as_bytes_mut`, `from_bytes`, and `from_bytes_mut` provide safe, zero-copy slice
views; Rust's ordinary borrowing rules still apply.

## Platforms and Features

Rust **1.85+**, edition 2024. No nightly compiler is required.

| Feature | Default | Effect |
| --- | --- | --- |
| `serde` | Yes | Allocation-free Serde support, with Serde's `std` feature disabled. |
| `simd` | Yes | AVX2 bulk encoding on eligible x86-64 hosts, portable Rust elsewhere. |

Disable both for a dependency-free library. Enabling either feature never
requires `std` or an allocator. Features of Serde selected by other dependencies
are still unified by Cargo in the usual way.

Scalar encoding, decoding, and arithmetic use integers, with no soft-float
runtime calls or lookup tables. The bulk encoder uses eight-lane integer AVX2
assembly for lengths of at least 32 on supported x86-64 hosts. The cached probe
checks CPUID, OSXSAVE, and XCR0; unsupported CPUs never execute AVX2. An explicitly
AVX2-enabled compilation skips detection. Short slices stay in Rust, as do
AVX-512-enabled builds, where LLVM can exploit the wider instruction set.

Bare-metal x86-64, UEFI, and SGX **do not perform runtime detection or compile
the handwritten kernel**. ARM, RISC-V, WebAssembly, other architectures, and
Miri also use the portable encoder. Explicit compiler target features can
still enable auto-vectorization and require the environment's corresponding
state management. No ARM or WASM-specific assembly is claimed or required.

Bulk decoding uses a vectorizable exact-division loop where the target advertises
SSE2, NEON, VFP2, RISC-V `F`, or WebAssembly floating-point operations, and integer
bit construction elsewhere. Hardware division assumes Rust's default
floating-point environment, including round-to-nearest-even. Scalar conversions
and both encoding paths are independent of floating-point control registers.

See [Performance](docs/PERFORMANCE.md) for measurements, methodology, and commands.
Do not ship binaries built with `-C target-cpu=native` to unknown CPUs.

## Verification

```sh
cargo test --all-features
cargo test --no-default-features
cargo test --release --all-features -- --include-ignored
cargo clippy --all-targets --all-features -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo +nightly miri test --all-features
cargo bench --bench convert
```

Tests cover all 65,536 operand pairs for each arithmetic operation and overload,
every byte round trip, every quantization boundary and its neighbors, NaN
payloads, all sign/exponent classes, randomized inputs, unaligned buffers,
tails, panic-before-write behavior, const evaluation, and Serde tokens.
The opt-in release test verifies **75,497,473** positive `f32` bit patterns
covering the entire nontrivial quantization range against an independent `f64`
reference, in both scalar and bulk paths. Miri checks the portable paths and
slice casts; it does not interpret the assembly kernel.