unsigned-float 0.2.1

Unsigned floating-point formats for non-negative numeric domains.
Documentation

unsigned-float

Crates.io docs.rs GitHub last commit GitHub license GitHub 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

Alias Concrete layout type Layout Storage Native compute path
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

[dependencies]
unsigned-float = "0.2"

Example

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:

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:

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:

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.

Exponents

Use PowUf to raise native floats to unsigned-float exponents:

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:

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:

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

Feature Default Description
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.
nightly No Convenience feature enabling both f16 and f128.
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.

Benchmarks

The benchmark suite uses nightly's built-in test harness and covers conversions, arithmetic, and raw-bit ordering for all exported layouts.

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.

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
  • 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
  • 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
  • SIMD bulk operations
  • more explicit NaN payload policy
  • broader property tests for all finite Uf16 layout edge cases

License

See LICENSE.