Skip to content

floating-point NaN payload functions #779

@phayes

Description

@phayes

Proposal

Some background reading on NaN payloads for those unfamiliar: https://www.codegenes.net/blog/why-does-ieee-754-reserve-so-many-nan-values

Problem statement

Rust exposes floating-point bit patterns through to_bits() and from_bits(), but it does not provide a direct hard-to-misuse way to get or set the payload bits of a NaN. This currently leaves every user who wants to use NaN payloads to implenent their own solution using to_bits(), from_bits(), and low-level bit-twiddling. This current approach is error prone.

Given that NaN payloads are an official part of the standard ( See IEEE 754-2019 3.5.2 and 9.7.0), they should be properly supported by the rust standard lilbrary.

Motivating examples or use cases

Some software uses NaN payloads as a compact metadata channel for values that are already represented as floating-point numbers. A common pattern is NaN-boxing or tagging schemes, where certain non-number cases are represented by quiet NaNs carrying payload bits.

Some possible modivating examples:

  1. A NaN payload might contain an application specific error code that encodes the reason why that value is a NaN.
  2. A NaN payload might contain the ID of the sensor that produced the NaN value
  3. My specific use-case is in real-time audio processing, where I wish to use it to communicate signalling metadata (num channels, sample rate etc) within an f32 audio stream. I am in a very contrained environment and wish to avoid the overhead of using an enum.

This proposal is not motivated by arithmetic semantics. Rust explicitly documents that arithmetic operations may choose NaN payloads non-deterministically from a constrained set, and that results do not generally preserve a chosen payload. NaN payoads should not be expected to survive any arithmetic operation.

Solution sketch

/// Errors that can occur when setting a NaN payload.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetNanPayloadError {
    /// The value is not a NaN.
    NotNan,
    /// The provided payload does not fit within the available payload bits.
    PayloadOutOfRange,
}

impl f32 {
    /// Returns the payload bits of this NaN, if the value is NaN.
    ///
    /// The payload does not include the sign bit or the quiet/signaling bit.
    /// For `f32`, the payload is 22 bits wide.
    ///
    /// Returns `None` if this value is not NaN.
    ///
    /// # Examples
    ///
    /// ```
    /// let x = f32::NAN.set_nan_payload(5).unwrap();
    /// assert_eq!(x.get_nan_payload(), Some(5));
    ///
    /// let x = 1.0f32;
    /// assert_eq!(x.get_nan_payload(), None);
    /// ```
    pub fn get_nan_payload(&self) -> Option<u32>;

    /// Returns a new NaN with the payload bits set to `payload`.
    ///
    /// This function only succeeds if `self` is NaN and `payload` fits within
    /// the available payload bits (22 bits for `f32`).
    ///
    /// The sign bit and the quiet/signaling state of the NaN are preserved.
    /// Only the payload bits are modified.
    ///
    /// # Errors
    ///
    /// - Returns [`SetNanPayloadError::NotNan`] if `self` is not NaN.
    /// - Returns [`SetNanPayloadError::PayloadOutOfRange`] if `payload`
    ///   does not fit within 22 bits.
    ///
    /// # Examples
    ///
    /// ```
    /// let x = f32::NAN.set_nan_payload(7).unwrap();
    /// assert_eq!(x.get_nan_payload(), Some(7));
    /// ```
    ///
    /// # Notes
    ///
    /// This method operates purely on the bit-level representation of the float.
    /// No guarantees are made about preservation of payload bits through
    /// arithmetic operations.
    pub fn set_nan_payload(self, payload: u32) -> Result<f32, SetNanPayloadError>;
}

impl f64 {
    /// Returns the payload bits of this NaN, if the value is NaN.
    ///
    /// The payload does not include the sign bit or the quiet/signaling bit.
    /// For `f64`, the payload is 51 bits wide.
    ///
    /// Returns `None` if this value is not NaN.
    ///
    /// # Examples
    ///
    /// ```
    /// let x = f64::NAN.set_nan_payload(5).unwrap();
    /// assert_eq!(x.get_nan_payload(), Some(5));
    ///
    /// let x = 1.0f64;
    /// assert_eq!(x.get_nan_payload(), None);
    /// ```
    pub fn get_nan_payload(&self) -> Option<u64>;

    /// Returns a new NaN with the payload bits set to `payload`.
    ///
    /// This function only succeeds if `self` is NaN and `payload` fits within
    /// the available payload bits (51 bits for `f64`).
    ///
    /// The sign bit and the quiet/signaling state of the NaN are preserved.
    /// Only the payload bits are modified.
    ///
    /// # Errors
    ///
    /// - Returns [`SetNanPayloadError::NotNan`] if `self` is not NaN.
    /// - Returns [`SetNanPayloadError::PayloadOutOfRange`] if `payload`
    ///   does not fit within 51 bits.
    ///
    /// # Examples
    ///
    /// ```
    /// let x = f64::NAN.set_nan_payload(7).unwrap();
    /// assert_eq!(x.get_nan_payload(), Some(7));
    /// ```
    ///
    /// # Notes
    ///
    /// This method operates purely on the bit-level representation of the float.
    /// No guarantees are made about preservation of payload bits through
    /// arithmetic operations.
    pub fn set_nan_payload(self, payload: u64) -> Result<f64, SetNanPayloadError>;
}

Alternatives

Alternative 1. Do nothing.

Currently users need to do something like this:

fn nan_payload_f32(x: f32) -> Option<u32> {
    let bits = x.to_bits();
    let exp_mask = 0x7f80_0000;
    let frac_mask = 0x007f_ffff;
    let quiet_mask = 0x0040_0000;

    if bits & exp_mask != exp_mask || bits & frac_mask == 0 {
        return None;
    }

    Some(bits & (frac_mask ^ quiet_mask))
}

fn with_nan_payload_f32(x: f32, payload: u32) -> f32 {
    let sign = x.to_bits() & 0x8000_0000;
    let exp  = 0x7f80_0000;
    let quiet = 0x0040_0000;
    let payload = payload & 0x003f_ffff;
    f32::from_bits(sign | exp | quiet | payload)
}

Alternative 2. Implement exactly as the standard suggests.

The IEEE Std 754-2019 says the following:

9.7 NaN payload operations 9.7.0
Language standards should define the following homogeneous quiet-computational operations to provide generic
access to payloads. These operations signal no exceptions.
  ― sourceFormat getPayload(source)
       If the source operand is a NaN, the result is the payload as a non-negative floating-point integer.
       If the source operand is not a NaN, the result is −1.
       The preferred exponent is 0.
  ― sourceFormat setPayload(source)
      If the source operand is a non-negative floating-point integer whose value is one of an
      implementation-defined set of admissible payloads for the operation, 
      the result is a quiet NaN with that payload. Otherwise, the result is +0, with a preferred exponent of 0.
  ― sourceFormat setPayloadSignaling(source)
      If the source operand is a non-negative floating-point integer whose value is one of an 
      implementation-defined set of admissible payloads for the operation, the result is a 
      signaling NaN with that payload. Otherwise, the result is +0, with a preferred exponent of 0.

NOTE — An implementation may restrict the payloads that can be set. Thus getPayload might return a
value that is not an admissible operand for setPayload or setPayloadSignaling. (A program can check by
applying the isNaN operation to the result of setPayload or setPayloadSignaling.)

**should** indicates that among several possibilities, one is recommended as particularly suitable, 
without mentioning or excluding others; or that a certain course of action is preferred
but not necessarily required; 

We could adhere exactly as the standard suggests, so the error type would not include NotNan and instead signal this error by returning −1. I think this is undesirable since we are returning a Result anyways for an invalid payload (which is allowed as per the standard), so we might as well make use of it for all error conditions.

Alternative 3. Implement as a crate.

This could be implemented as a crate, but I do not see any "float_util" crates or similar where it would naturally fit. On it's own it's a bit small to justfiy a full standalone crate.

Links and related work

  1. https://www.codegenes.net/blog/why-does-ieee-754-reserve-so-many-nan-values
  2. EEE Std 754-2019
  3. D programming language GetNanPayload, NaN with payload
  4. glibc NAN Payload operations

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions