use g_math::fixed_point::{FixedPoint, OverflowDetected};
#[allow(dead_code)]
mod refs {
include!("data/float_boundary_refs.rs");
}
use refs::{FromFloat, ToFloat};
#[cfg(table_format = "q128_128")]
use g_math::fixed_point::domains::binary_fixed::i256::I256;
#[cfg(table_format = "q256_256")]
use g_math::fixed_point::domains::binary_fixed::i512::I512;
#[cfg(any(table_format = "q128_128", table_format = "q256_256"))]
fn hex_words(hex: &str, count: usize) -> Vec<u64> {
let mut words = vec![0u64; count];
let digits = hex.as_bytes();
for (i, chunk) in digits.rchunks(16).enumerate() {
words[i] = u64::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16).unwrap();
}
words
}
#[cfg(any(table_format = "q16_16", table_format = "q32_32", table_format = "q64_64"))]
fn raw_from(negative: bool, hex: &str) -> FixedPoint {
let magnitude = u128::from_str_radix(hex, 16).unwrap();
let value = if negative { (magnitude as i128).wrapping_neg() } else { magnitude as i128 };
FixedPoint::from_raw(value as _)
}
#[cfg(table_format = "q128_128")]
fn raw_from(negative: bool, hex: &str) -> FixedPoint {
let words = hex_words(hex, 4);
let magnitude = I256 { words: [words[0], words[1], words[2], words[3]] };
FixedPoint::from_raw(if negative { -magnitude } else { magnitude })
}
#[cfg(table_format = "q256_256")]
fn raw_from(negative: bool, hex: &str) -> FixedPoint {
let words = hex_words(hex, 8);
let mut array = [0u64; 8];
array.copy_from_slice(&words);
let magnitude = I512 { words: array };
FixedPoint::from_raw(if negative { -magnitude } else { magnitude })
}
fn small_raw(value: i64) -> FixedPoint {
#[cfg(any(table_format = "q16_16", table_format = "q32_32", table_format = "q64_64"))]
{
FixedPoint::from_raw(value as _)
}
#[cfg(any(table_format = "q128_128", table_format = "q256_256"))]
{
raw_from(value < 0, &format!("{:x}", value.unsigned_abs()))
}
}
fn frac_bits() -> u32 {
g_math::fixed_point::frac_config::FRAC_BITS
}
fn tables() -> Option<(&'static [ToFloat], &'static [FromFloat], &'static [FromFloat])> {
#[cfg(table_format = "q16_16")]
{
match frac_bits() {
16 => Some((refs::REALTIME_F16_TO_FLOAT, refs::REALTIME_F16_FROM_F64, refs::REALTIME_F16_FROM_F32)),
10 => Some((refs::REALTIME_F10_TO_FLOAT, refs::REALTIME_F10_FROM_F64, refs::REALTIME_F10_FROM_F32)),
other => {
println!("no references for GMATH_FRAC_BITS={other}: add it to the generator");
None
}
}
}
#[cfg(table_format = "q32_32")]
{
Some((refs::COMPACT_TO_FLOAT, refs::COMPACT_FROM_F64, refs::COMPACT_FROM_F32))
}
#[cfg(table_format = "q64_64")]
{
Some((refs::EMBEDDED_TO_FLOAT, refs::EMBEDDED_FROM_F64, refs::EMBEDDED_FROM_F32))
}
#[cfg(table_format = "q128_128")]
{
Some((refs::BALANCED_TO_FLOAT, refs::BALANCED_FROM_F64, refs::BALANCED_FROM_F32))
}
#[cfg(table_format = "q256_256")]
{
Some((refs::SCIENTIFIC_TO_FLOAT, refs::SCIENTIFIC_FROM_F64, refs::SCIENTIFIC_FROM_F32))
}
}
#[test]
fn to_float_matches_the_exact_rational_references() {
let Some((to_float, _, _)) = tables() else { return };
for r in to_float {
let x = raw_from(r.negative, r.magnitude);
let sign = if r.negative { "-" } else { "" };
assert_eq!(x.to_f64().to_bits(), r.f64_bits, "to_f64 of raw {sign}0x{}", r.magnitude);
assert_eq!(x.to_f32().to_bits(), r.f32_bits, "to_f32 of raw {sign}0x{}", r.magnitude);
}
println!("{} raws: to_f64 and to_f32 bit-identical to the references", to_float.len());
}
fn check_from(kind: &str, vectors: &[FromFloat], convert: impl Fn(u64) -> Result<FixedPoint, OverflowDetected>) {
for v in vectors {
match (convert(v.bits), v.raw) {
(Ok(x), Some((negative, hex))) => {
assert!(x == raw_from(negative, hex), "{kind} of bits 0x{:x}: raw differs from the reference", v.bits)
}
(Err(OverflowDetected::TierOverflow), None) => {}
(got, expected) => panic!(
"{kind} of bits 0x{:x}: got {:?}, reference {}",
v.bits,
got.map(|x| x.raw()),
if expected.is_some() { "a raw value" } else { "TierOverflow" }
),
}
}
println!("{kind}: {} inputs match the truncation references", vectors.len());
}
#[test]
fn from_float_truncates_toward_zero_and_rejects_out_of_range() {
let Some((_, from_f64, from_f32)) = tables() else { return };
check_from("try_from_f64", from_f64, |bits| FixedPoint::try_from_f64(f64::from_bits(bits)));
check_from("try_from_f32", from_f32, |bits| FixedPoint::try_from_f32(f32::from_bits(bits as u32)));
for v in from_f64.iter().filter(|v| v.raw.is_some()) {
let x = f64::from_bits(v.bits);
assert!(FixedPoint::from_f64(x) == FixedPoint::try_from_f64(x).unwrap());
}
}
fn f64_is_exactly(bits: u64, raw: i64, frac: u32) -> bool {
let negative = bits >> 63 == 1;
let biased = ((bits >> 52) & 0x7FF) as i32;
if raw == 0 {
return bits == 0;
}
if negative != (raw < 0) || biased == 0 || biased == 0x7FF {
return false;
}
let significand = (bits & ((1u64 << 52) - 1)) | (1u64 << 52);
let shift = biased - 1075 + frac as i32;
let magnitude = raw.unsigned_abs() as u128;
if shift >= 0 {
(significand as u128).checked_shl(shift as u32) == Some(magnitude)
} else {
magnitude.checked_shl((-shift) as u32) == Some(significand as u128)
}
}
fn sample_raws() -> Vec<i64> {
#[cfg(table_format = "q16_16")]
let bound: i64 = 1 << 31;
#[cfg(not(table_format = "q16_16"))]
let bound: i64 = 1 << 53;
let mut raws: Vec<i64> = (-65_536..=65_536).collect();
let mut state = 0x2026_0919_u64;
for _ in 0..100_000 {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let bits = 1 + (state >> 58) as u32 % 53; state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let magnitude = ((state >> 11) as i64) & ((1i64 << bits) - 1);
let value = if state & 1 == 1 { -magnitude } else { magnitude };
if value > -bound && value < bound {
raws.push(value);
}
}
raws
}
#[test]
fn exact_conversions_are_exact_and_round_trip() {
let frac = frac_bits();
let mut f32_checked = 0;
for raw in sample_raws() {
let x = small_raw(raw);
let y = x.to_f64();
assert!(f64_is_exactly(y.to_bits(), raw, frac), "to_f64 of raw {raw} is not exact");
assert!(FixedPoint::from_f64(y) == x, "from_f64(to_f64(x)) != x for raw {raw}");
if raw.unsigned_abs() < 1 << 24 && frac <= 149 {
assert!(FixedPoint::from_f32(x.to_f32()) == x, "from_f32(to_f32(x)) != x for raw {raw}");
f32_checked += 1;
}
}
println!("round trips exact: {} f64, {f32_checked} f32", sample_raws().len());
}
#[test]
fn round_trips_do_not_erode() {
for raw in [99i64, -99, 1, -1, 12_345, -12_345] {
let x = small_raw(raw);
let mut y = FixedPoint::from_f64(x.to_f64());
for _ in 0..5 {
y = FixedPoint::from_f64(y.to_f64());
}
assert!(y == x, "raw {raw} moved after repeated round trips");
}
}
#[test]
fn to_f64_is_sign_symmetric_and_monotone() {
let mut raws: Vec<i64> = sample_raws().into_iter().filter(|r| *r > 0).collect();
raws.sort_unstable();
raws.dedup();
let mut previous = 0u64;
for raw in raws {
let positive = small_raw(raw).to_f64().to_bits();
let negative = small_raw(-raw).to_f64().to_bits();
assert_eq!(negative, positive | (1 << 63), "to_f64(-x) != -to_f64(x) for raw {raw}");
assert!(positive > previous, "to_f64 not strictly increasing at raw {raw}");
previous = positive;
}
}
#[test]
fn non_finite_inputs_are_errors() {
assert_eq!(FixedPoint::try_from_f64(f64::NAN).unwrap_err(), OverflowDetected::InvalidInput);
assert_eq!(FixedPoint::try_from_f32(f32::NAN).unwrap_err(), OverflowDetected::InvalidInput);
for v in [f64::INFINITY, f64::NEG_INFINITY] {
assert_eq!(FixedPoint::try_from_f64(v).unwrap_err(), OverflowDetected::TierOverflow);
}
for v in [f32::INFINITY, f32::NEG_INFINITY] {
assert_eq!(FixedPoint::try_from_f32(v).unwrap_err(), OverflowDetected::TierOverflow);
}
assert!(FixedPoint::try_from_f64(-0.0).unwrap() == FixedPoint::ZERO);
}
fn power_of_two(e: i32) -> f64 {
f64::from_bits(((e + 1023) as u64) << 52)
}
fn range_top() -> i32 {
#[cfg(table_format = "q16_16")]
let width = 32;
#[cfg(table_format = "q32_32")]
let width = 64;
#[cfg(table_format = "q64_64")]
let width = 128;
#[cfg(table_format = "q128_128")]
let width = 256;
#[cfg(table_format = "q256_256")]
let width = 512;
width - 1 - frac_bits() as i32
}
#[test]
fn the_range_ends_exactly_at_its_bounds() {
let top = range_top();
let minimum = FixedPoint::try_from_f64(-power_of_two(top)).expect("-2^top is representable");
assert!(minimum < FixedPoint::ZERO);
assert_eq!(FixedPoint::try_from_f64(power_of_two(top)).unwrap_err(), OverflowDetected::TierOverflow);
assert_eq!(FixedPoint::try_from_f64(-power_of_two(top + 1)).unwrap_err(), OverflowDetected::TierOverflow);
}
#[test]
#[should_panic(expected = "infinity or value outside the profile's range")]
fn from_f64_panics_beyond_the_range_instead_of_wrapping() {
FixedPoint::from_f64(power_of_two(range_top()));
}
#[test]
#[should_panic(expected = "NaN")]
fn from_f64_panics_on_nan() {
FixedPoint::from_f64(f64::NAN);
}
#[test]
#[should_panic(expected = "infinity or value outside the profile's range")]
fn from_f32_panics_on_infinity() {
FixedPoint::from_f32(f32::INFINITY);
}