Add conversions of floating point to / from exponent+mantissa

There doesn't seem to exist an easy way to get the exponent and mantissa from a f32. The only way seems to be to use f32::to_bits and parse the bits.

And vice-versa, there doesn't seem to exist a direct way to convert exponent+mantissa to f32, except messing with f32::from_bits, or using mantissa * 2.0f32.powi(exponent) (but multiplication and powi seem like an overkill for this, and the formula risks underflow in the power of 2).

I'd like to see something like this:

pub enum F32Representation {
    NegInfinity,
    // Number = 2^exponent * mantissa
    Finite { exponent: i32, mantissa: i32 },
    Infinity,
    Nan, // with payload?
}

impl f32 {
    pub fn to_repr(self) -> F32Representation {
        todo!()
    }
    
    pub fn from_repr(repr: F32Representation) -> Self {
        todo!()
    }    

Does this make sense to add?

These would have been useful to me when I was implementing my bignum <-> floating point conversions, and now they'd be useful to me to fix the div_euclid bug in std.

9 Likes

This could be made generic over all float types, with exponent and mantissa being the signed integer of the same size.

3 Likes

We might define type of mantissa manually, since f128 needs at least i128, but using i128 for f64 is a overkill.

I’d be in favor of giving the Nan variant a NonZero<u32> payload argument so that this type has the capability to losslessly round-trip with any f32 value.

Also, it probably makes sense to provide a fallible version of from_repr which checks for overflow in the various fields.

———

Edit: For round-tripping, it needs to differentiate between +0 and -0 as well, which the current version isn’t equipped for.

2 Likes

To get lossless round-trip it would also need a way to distinguish -0.0 from +0.0. But I don't see a neat way to do that. One could do:

pub struct F32Representation {
    sign: Sign,
    unsigned: F32RepresentationUnsigned,
}

pub enum Sign {
    Negative,
    Positive,
}

pub enum F32RepresentationUnsigned {
    Finite { exponent: i32, mantissa: u32 },
    Infinite,
    Nan { payload: u32 },
}

But that's less pleasant.

Edit: Another option would be to keep mantissa as i32 in this version also, so that from_repr can still be used to convert from a signed mantissa by just using the Positive sign even if the number is negative...

1 Like

I suppose you could do something like this, but I’m not convinced it’s an improvement:

struct F32Representation {
    sign: Sign,
    exponent: Option<i8>, // None means mantissa must be interpreted as IEEE 754 special value
    mantissa: u32
}

So maybe from_repr which returns f32 and rounds to the nearest value if necessary, and from_repr_exact that returns Option<f32> and never rounds.

1 Like

similar discussion on zulip, started with hex float literals and arrived at constructor methods.

Separating the sign from the mantissa is inconvenient because it requires two cases to deal with the mantissa where we really usually just want a signed mantissa.

How about this?

pub enum F32Representation {
    // number = 2^exponent * mantissa
    // sign is redundant except for -0.0
    // `from_repr` ignores `sign` for non-zero values
    Finite { exponent: i32, mantissa: i32, sign: Sign },
    Infinity { sign: Sign },
    Nan { sign: Sign, nan_type: NanType, payload: u32 },
}

pub enum Sign {
    Positive,
    Negative,
}

pub enum NanType {
    Signaling,
    Quiet,
}
2 Likes

That looks reasonable; my biggest concern would be confusion about how conversions handle a sign bit that disagrees with the given mantissa-- Does a Negative sign bit:

  • Do nothing unless the mantissa is 0,
  • Always invert the mantissa, or
  • Invert only non-negative mantissas?

Another option would be:

// T is an integer type; represents 2^exponent * mantissa
pub struct FiniteFloat<T> {
    pub exponent: T,
    pub mantissa: T
}

#[derive(Copy,Clone)]
pub enum F32Representation {
    // The usual case: number = 2^exponent * mantissa
    Normal(FiniteFloat<i32>),
    // Weird finite representations, incl. negative zero
    Subnormal { exponent: i32, mantissa: u32, sign: Sign }
    Infinity { sign: Sign },
    Nan { sign: Sign, nan_type: NanType, payload: u32 },
}

impl F32Representation {
    pub fn normalize(self)->Option<FiniteFloat<i32>> {
        match self {
            Self::Normal(f) => Some(f),
            Self::Subnormal { .. } => { /* convert to normalized form... */ },
            Self::Infinity { .. } => None,
            Self::Nan { .. } => None
        }
    }
}

pub enum Sign {
    Positive,
    Negative,
}

pub enum NanType {
    Signaling,
    Quiet,
}

I am thinking the first option: do nothing unless the mantissa is 0. I agree it's a bit confusing. The docs would have to have a warning about this.

This means that a user of to_repr can ignore sign if they don't care about -0.0, but it round-trips correctly.

1 Like

Prior art: num-traits already has integer_decode() which provides some of the functionality discussed here.

3 Likes

I doodled some code to work on float parts a while ago, cleaned it up and pushed to GitHub thanks to this thread: flotsam. The extension trait might be too invasive as it is now, adding too much clutter to the float types when it's in scope, but YMMV.

How much do you need i32 mantissa, vs just splitting it to normalized-f32+exponent?

Having std::frexp, std::frexpf, std::frexpl - cppreference.com and std::ldexp, std::ldexpf, std::ldexpl - cppreference.com seems like easy initial uplifts to me, and avoid the sign question by being able to still give a floating point mantissa on which one can use is_sign_positive if needed.

2 Likes

frexp and ldexp would go some way towards it, but for my two practical use cases (one of which is fixing f32::div_euclid), I'd want a i32, so I'd have to convert it anyway.

I've used ldexp a few times before, and every time I used it, I converted an integer to floating point first in order to pass it into ldexp.

It'd also be nice to have the special cases (infinities and NaN) as enum variants, rather than having to deal with them by if checks for special values. The frexp interface is very C-style because C lacks enums, so it returns some arbitrary values for these.

3 Likes

This could also be useful as a teaching tool and for studying behavior of floating point functions. Whenever people ask questions about floating point, one could use this to see exactly what's going on inside rather than using some external calculators or converters. For this, it would be nice to see an integer mantissa, so that you know (mantissa + 1) is the next representable number.

7 Likes

Isn't implementing the From/Into trait on a custom type the Rust way?

Granted it should already exist.

enum F32Representation {...}

impl From<f32> for F32Representation {...}

impl Into<f32> for F32Representation {...}

impl Debug for F32Representation {...}
1 Like