const BIAS: i32 = 127; // <1> Similar constants are accessible via the `std::f32` module const RADIX: f32 = 2.0; // <1> fn main() { // <2> main() can live happily at the beginning of a file let n: f32 = 42.42; let (signbit, exponent, fraction) = deconstruct_f32(n); // <3> Here the three components of `n` are extracted, with each one being an uninterpreted sequence of bits println!("{} -> [sign:{:01b}, exponent:{:08b}, mantissa:{:023b}] -> tbc", n, signbit, exponent, fraction); let (sign, exponent, mantissa) = decode_f32_parts(signbit, exponent, fraction); // <4> Each component is interpreted according to the standard let reconstituted_n = f32_from_parts(sign, exponent, mantissa); // <5> The original value is produced from those three components println!("{} -> [sign:{}, exponent:{}, mantissa:{:?}] -> {}", n, signbit, exponent, mantissa, reconstituted_n); } fn deconstruct_f32(n: f32) -> (u32, u32, u32) { let n_: u32 = unsafe { std::mem::transmute(n) }; let sign = (n_ >> 31) & 1; // <2> strip 31 unwanted bits away by shifting them into nowhere, leaving only the sign bit let exponent = (n_ >> 23) & 0xff; // <3> filter out the top bit with a logical AND mask, then strip 23 unwanted bits away let fraction = 0b00000000_01111111_11111111_11111111 & n_; // <4> only retain the 23 "`least significant`" bits via a mask (sign, exponent, fraction) // <5> The mantissa part is called a fraction here, as it becomes the mantissa once it's decoded } fn decode_f32_parts(sign: u32, exponent: u32, fraction: u32) -> (f32, f32, f32) { let signed_1 = (-1.0_f32).powf(sign as f32); // <6> Convert the sign bit to 1.0 or -1.0. Parentheses are required around `-1.0_f32` to clarify operator precedence as method calls rank higher than unary minus. let exponent = (exponent as i32) - BIAS; // <7> We need to do a bit of a type dance here. `exponent` must become an `i32` in case subtracting the `BIAS` results in a negative number. Then it needs to be cast as a `f32`, so that it can be used for exponentiation. let exponent = RADIX.powf(exponent as f32); // <7> let mut mantissa: f32 = 1.0; // <8> We start by assuming that the implicit 24th bit is set. That has the upshot of defaulting the mantissa's value as 1. for i in 0..23_u32 { // <9> We provide a concrete type here to ensure that the bit patterns that are generated by the mask are defined let one_at_bit_i = 1 << i; // <10> At each iteration, create an AND mask of a single bit in the position that we're currently interested in. if (one_at_bit_i & fraction) != 0 { // <11> Any non-zero result means that the bit is present within `fraction` mantissa += 2_f32.powf((i as f32) - 23.0); // <11> To arrive at the decimal value of the bit at `i`, we find 2^i-23^. -23 means that the result gets smaller when `i` is close to 0, as desired. } } (signed_1, exponent, mantissa) } fn f32_from_parts(sign: f32, exponent: f32, mantissa: f32) -> f32 { // <12> This code cheats a bit by using `f32` values in intermediate steps. Hopefully it is a forgivable offense. sign * exponent * mantissa }