# unsigned-float
[](https://crates.io/crates/unsigned-float)
[](https://docs.rs/unsigned-float)
[](https://github.com/MicroPerceptron/ufloat/actions/workflows/ci.yml)
[](https://github.com/MicroPerceptron/ufloat/commits/main)
[](https://github.com/MicroPerceptron/ufloat/blob/main/LICENSE)
[](https://github.com/MicroPerceptron/ufloat/issues)
[](#no_std)
Unsigned floating-point types for values that can never be negative.
`unsigned-float` provides compact unsigned float newtypes with IEEE-like
exponent and mantissa fields, but no sign bit. The missing sign bit buys either
extra precision or range, removes negative zero, and makes total ordering as
simple as comparing the raw integer representation.
This crate is young and experimental. The current behavior prioritizes a small,
clear API and predictable bit layouts over exhaustive IEEE 754 compatibility.
## Formats
| `Uf8` | `Uf8E4M4` | E4M4 | `u8` | LUT by default, nightly `f16` optional |
| | `Uf8E5M3` | E5M3 | `u8` | LUT by default, nightly `f16` optional |
| `Uf16` | `Uf16E5M11` | E5M11 | `u16` | promote to `f32` |
| | `Uf16E6M10` | E6M10 | `u16` | promote to `f32` |
| `Uf32` | `Uf32E8M24` | E8M24 | `u32` | promote to `f64` |
| `Uf64` | `Uf64E11M52` | E11M52 | `u64` | promote to nightly `f128` |
The all-ones exponent is reserved for infinity and NaN, following the usual
IEEE-style convention. Negative native inputs encode to NaN. Operations whose
mathematical result is negative also produce NaN.
`Uf8`, `Uf16`, and `Uf32` are ergonomic aliases for the default concrete layout
types. Layout-specific names are exported for every format because changing the
exponent/mantissa split changes the numeric contract. Use E4M4/E5M11 when
precision near 1.0 matters more; use E5M3/E6M10 when range matters more.
`Uf64` is available only with the `f128` feature.
## Install
```toml
[dependencies]
unsigned-float = "0.2"
```
## Example
```rust
use unsigned_float::{Uf8, Uf8E5M3, Uf16, Uf32};
let a = Uf16::from_f32(1.5);
let b = Uf16::from_f32(2.0);
let product = a * b;
assert_eq!(product.to_f32(), 3.0);
let small = Uf8::from_f32(0.5);
let large = Uf8::from_f32(4.0);
assert!(small < large);
assert_eq!(Uf8::ONE.to_bits(), 0x70);
let wide = Uf32::from_f64(1024.0);
assert_eq!(wide.to_f64(), 1024.0);
let wider_range = Uf8E5M3::from_f32(1024.0);
assert_eq!(wider_range.to_f32(), 1024.0);
```
## Raw Ordering
Unsigned floats are ordered by their raw bits:
```rust
use unsigned_float::Uf16;
let values = [Uf16::from_f32(4.0), Uf16::ZERO, Uf16::from_f32(1.0)];
let mut sorted = values;
sorted.sort();
assert_eq!(sorted[0], Uf16::ZERO);
assert_eq!(sorted[1], Uf16::ONE);
```
Because there is no sign bit and no negative zero, `Ord` and `PartialOrd` are
implemented as simple unsigned integer comparisons of the underlying bits. NaN
payloads therefore sort after infinity rather than behaving like IEEE
`f32::partial_cmp`.
## Conversions
Use the explicit constructors when you want IEEE-like encoding behavior:
```rust
use unsigned_float::Uf8;
assert!(Uf8::from_f32(-1.0).is_nan());
assert!(Uf8::from_f32(f32::INFINITY).is_infinite());
```
Use `TryFrom`/`TryInto` when you want to reject values outside the finite,
non-negative domain:
```rust
use unsigned_float::{ConversionError, Uf16};
assert_eq!(Uf16::try_from(42_u32), Ok(Uf16::from_f32(42.0)));
assert_eq!(Uf16::try_from(-1_i32), Err(ConversionError::Negative));
assert_eq!(Uf16::try_from(f64::INFINITY), Err(ConversionError::Infinite));
```
The crate implements fallible conversions from `f64` and primitive integer
types. With the `f16` feature enabled, `from_f16`, `to_f16`,
`From<f16>`, and `Into<f16>` are also available.
Unsigned-float values also forward common float formatting to their promoted
native value:
```rust
use unsigned_float::Uf16;
let value = Uf16::from_f32(1.5);
assert_eq!(format!("{value:.2}"), "1.50");
assert_eq!(format!("{value:e}"), "1.5e0");
```
## Exponents
Use `PowUf` to raise native floats to unsigned-float exponents:
```rust
use unsigned_float::{PowUf, Uf16};
let exponent = Uf16::from_f32(0.5);
let root = 9.0_f32.powuf(exponent);
assert_eq!(root, 3.0);
```
`PowUf` is dependency-free from the caller's perspective and works in `no_std`
builds by using `libm` internally. It is implemented for `f32` and `f64` bases
over all exported unsigned-float exponent layouts. With `f128`, `Uf64` can also
be used as an exponent for `f32` and `f64` bases.
The trait dispatches through small kernels before falling back to general
`libm` exponentiation. Exact exponents `0`, `1`, and `0.5` map to identity,
base, and square-root paths, while exact small integer exponents use
exponentiation by squaring.
UF8 exponentiation is also available and uses generated lookup tables. The
output follows the receiver's layout, including when the exponent uses the
other UF8 layout:
```rust
use unsigned_float::{PowUf, Uf8, Uf8E5M3};
let root = Uf8::from_f32(9.0).powuf(Uf8::from_f32(0.5));
let cross = Uf8::from_f32(9.0).powuf(Uf8E5M3::from_f32(0.5));
assert_eq!(root.to_f32(), 3.0);
assert_eq!(cross.to_f32(), 3.0);
```
For complement powers, use `Pow1mUf` to evaluate `(1 - u)^a` directly:
```rust
use unsigned_float::{Pow1mUf, Uf8};
let root = Uf8::from_f32(0.75).pow1muf(Uf8::from_f32(0.5));
assert_eq!(root.to_f32(), 0.5);
```
Native `f32`/`f64` complement powers use `log1p`/`expm1` for the general
`0 <= u <= 1` case. Same-layout and cross-layout UF8 complement powers use
generated lookup tables, so `u` and `a` can stay compact from input through
output.
## Features
| `f16` | No | Uses nightly primitive `f16` for `Uf8` arithmetic dispatch. Requires nightly Rust. |
| `f128` | No | Enables `Uf64`/`Uf64E11M52` and promotes its arithmetic through nightly primitive `f128`. |
| `simd` | No | Enables nightly portable-SIMD bulk conversion and elementwise APIs for UF16/UF32. |
| `nightly` | No | Convenience feature enabling `f16`, `f128`, and `simd`. |
| `soft-float` | No | Forces the software/LUT dispatch path where available. If combined with `f16`, `soft-float` wins for `Uf8`. |
The default `Uf8` arithmetic path uses generated 256x256 lookup tables for
`Add`, `Sub`, `Mul`, `Div`, UF8 `PowUf`, and UF8 `Pow1mUf`. These tables are
generated by `build.rs` into Cargo's `OUT_DIR` and embedded with
`include_bytes!`, so they do not need to be checked in.
## no_std
The library is `#![no_std]`. The build script and benchmarks use `std`, but the
crate API itself does not require allocation or the standard library.
## Development and release checks
The supported stable baseline is Rust 1.85. CI tests that baseline and current
stable on Linux, macOS, and Windows; compiles the default `no_std` API for
AArch64, RISC-V, and WebAssembly; and runs the nightly-only feature set
separately. It also packages the crate, extracts the archive, and reruns the
library, documentation, and documentation tests from the extracted source.
Before submitting a change, run the stable baseline checks:
```sh
cargo fmt --check
cargo clippy --lib --tests -- -D warnings
cargo test --lib
cargo test --doc
RUSTDOCFLAGS='-D warnings' cargo doc --no-deps
```
Changes to an opt-in nightly implementation should additionally run:
```sh
cargo +nightly test --all-features
RUSTDOCFLAGS='-D warnings' cargo +nightly doc --all-features --no-deps
```
Releases are tag-driven: GitHub release tag `vX.Y.Z` must name the exact
`Cargo.toml` version, and its current target must still match the immutable
commit SHA captured when that release was published; that commit must be
reachable from `main`. The release workflow repeats the extracted-package
verification before publishing through crates.io Trusted Publishing: it
exchanges the workflow's GitHub OIDC identity for a short-lived token rather
than storing a long-lived crates.io credential in the repository. Before the
first automated release, configure the crate's trusted publisher as GitHub
owner `MicroPerceptron`, repository `ufloat`, and workflow filename
`publish.yml`.
## Benchmarks
The benchmark suite uses nightly's built-in `test` harness and covers
conversions, arithmetic, and raw-bit ordering for all exported layouts.
```sh
cargo bench --bench arithmetic
cargo bench --features f16 --bench arithmetic
cargo bench --all-features --bench arithmetic
```
Each command benchmarks the dispatch path selected by that feature set. For
example, `--features f16` measures the `Uf8` primitive-`f16` path, while the
default command measures the generated LUT path.
With nightly, `simd` adds allocation-free slice APIs that widen normal finite
UF16 lanes to native F32 and UF32 lanes to native F64. The bulk operators keep
the exact scalar result for every lane: blocks containing subnormals, NaNs,
infinities, negative results, or an encoding round-carry fall back to the
scalar conversion rather than changing the numeric contract.
```rust
use unsigned_float::{Uf16, simd};
let packed = [Uf16::from_f32(0.75); 256];
let mut linear = [0.0_f32; 256];
simd::decode_uf16_to_f32(&packed, &mut linear).unwrap();
let mut doubled = [Uf16::ZERO; 256];
simd::add_uf16(&packed, &packed, &mut doubled).unwrap();
assert_eq!(doubled[0].to_f32(), 1.5);
```
## Status
Implemented:
- `Uf8`, `Uf16`, and `Uf32` transparent newtypes
- explicit concrete layout names: `Uf8E4M4`, `Uf8E5M3`, `Uf16E5M11`,
`Uf16E6M10`, `Uf32E8M24`, and feature-gated `Uf64E11M52`
- raw bit constructors and extractors
- native float conversions
- float-style `Display`, `LowerExp`, and `UpperExp` formatting
- `PowUf` for native float bases and unsigned-float exponents
- `Pow1mUf` for native and UF8 complement powers
- fallible conversions from `f64` and primitive integers
- `Add`, `Sub`, `Mul`, and `Div`
- raw-bit `Ord`/`PartialOrd`
- generated UF8 arithmetic and exponentiation lookup tables
- opt-in SIMD bulk conversion and arithmetic for UF16/UF32, with scalar-exact
edge-lane fallback
- benchmarks across conversions, arithmetic, and ordering
Still worth exploring:
- native baseline benchmarks against `f32` and `f64`
- configurable UF8 dispatch for direct LUT versus promote-to-native comparisons
- more explicit NaN payload policy
- broader property tests for all finite `Uf16` layout edge cases
## License
See [LICENSE](LICENSE).