rvsim-core 2.0.0

A cycle-level RISC-V 64-bit system simulator.
//! NaN boxing, unboxing, and canonical NaN propagation for the FPU.
//!
//! RISC-V stores single-precision (f32) values in 64-bit floating-point
//! registers using "NaN boxing": the upper 32 bits must be all 1s.
//!
//! - **Boxing** ([`box_f32`]): Sets upper 32 bits to 1s when writing an f32
//!   result into a 64-bit register.
//! - **Unboxing** ([`unbox_f32`]): Checks that the upper 32 bits are all 1s.
//!   If not, the value is treated as canonical NaN (RISC-V spec §12.2).
//! - **Canonicalization** ([`canonicalize_f32`], [`canonicalize_f64`]): Any NaN
//!   result from an arithmetic operation is replaced with the canonical quiet
//!   NaN, discarding payload bits (RISC-V spec §11.3).

/// Canonical quiet NaN for IEEE 754 single-precision (positive, quiet, zero payload).
const CANONICAL_NAN_F32: u32 = 0x7fc0_0000;

/// Canonical quiet NaN for IEEE 754 double-precision (positive, quiet, zero payload).
const CANONICAL_NAN_F64: u64 = 0x7ff8_0000_0000_0000;

/// Upper-32-bit mask used for NaN boxing validation.
const NAN_BOX_MASK: u64 = 0xFFFF_FFFF_0000_0000;

/// Boxes an f32 value into a 64-bit NaN-boxed representation.
///
/// Sets the upper 32 bits to all 1s, per RISC-V spec §12.2.
///
/// # Arguments
///
/// * `f` - The 32-bit floating-point value to box.
///
/// # Returns
///
/// A 64-bit value with the f32 in the lower 32 bits and all 1s in the upper 32 bits.
#[inline]
pub const fn box_f32(f: f32) -> u64 {
    (f.to_bits() as u64) | NAN_BOX_MASK
}

/// Canonicalizes an f32 result and boxes it into a 64-bit NaN-boxed value.
///
/// Combines canonicalization and boxing in a single function that operates
/// at the bit level. This prevents the optimizer from altering NaN payloads
/// when a canonical NaN f32 flows from `canonicalize_f32` into `box_f32`.
///
/// Use this for arithmetic results that require NaN canonicalization.
/// For sign injection and other non-canonicalizing operations, use plain
/// [`box_f32`] instead.
#[inline]
pub const fn box_f32_canon(f: f32) -> u64 {
    let mut bits = f.to_bits();
    // NaN: exponent all 1s AND mantissa nonzero → replace with canonical
    if (bits & 0x7F80_0000) == 0x7F80_0000 && (bits & 0x007F_FFFF) != 0 {
        bits = CANONICAL_NAN_F32;
    }
    (bits as u64) | NAN_BOX_MASK
}

/// Canonicalizes an f64 result and returns its bits.
///
/// Like [`box_f32_canon`] but for double-precision. Replaces any NaN with
/// the canonical quiet NaN at the bit level.
#[inline]
pub const fn canonicalize_f64_bits(f: f64) -> u64 {
    let mut bits = f.to_bits();
    if (bits & 0x7FF0_0000_0000_0000) == 0x7FF0_0000_0000_0000
        && (bits & 0x000F_FFFF_FFFF_FFFF) != 0
    {
        bits = CANONICAL_NAN_F64;
    }
    bits
}

/// Unboxes a 64-bit register value to obtain an f32.
///
/// Validates that the upper 32 bits are all 1s. If valid, returns the
/// lower 32 bits interpreted as f32. If invalid (not properly NaN-boxed),
/// returns the canonical NaN per RISC-V spec §12.2.
///
/// # Arguments
///
/// * `val` - The 64-bit register value to unbox.
///
/// # Returns
///
/// The unboxed f32, or canonical NaN if the value was not properly NaN-boxed.
#[inline]
pub const fn unbox_f32(val: u64) -> f32 {
    if (val & NAN_BOX_MASK) == NAN_BOX_MASK {
        f32::from_bits(val as u32)
    } else {
        // Not properly NaN-boxed: treat as canonical NaN (RISC-V spec §12.2).
        f32::from_bits(CANONICAL_NAN_F32)
    }
}

/// IEEE 754-2008 `minNum` for single-precision (RISC-V FMIN.S).
///
/// If exactly one operand is NaN, returns the non-NaN operand.
/// If both are NaN, returns canonical NaN.
/// Otherwise returns the arithmetic minimum.
///
/// Note: sNaN inputs set the Invalid flag (handled at a higher level
/// once exception flags are implemented).
///
/// # Arguments
///
/// * `a` - First f32 operand.
/// * `b` - Second f32 operand.
///
/// # Returns
///
/// The IEEE 754-2008 minimum, with canonical NaN propagation.
#[inline]
pub const fn fmin_f32(a: f32, b: f32) -> f32 {
    match (a.is_nan(), b.is_nan()) {
        (true, true) => f32::from_bits(CANONICAL_NAN_F32),
        (true, false) => b,
        (false, true) => a,
        (false, false) => {
            // IEEE 754-2008: -0.0 < +0.0
            if a.to_bits() == 0x8000_0000 && b.to_bits() == 0x0000_0000 {
                a // -0.0
            } else if b.to_bits() == 0x8000_0000 && a.to_bits() == 0x0000_0000 {
                b // -0.0
            } else {
                a.min(b)
            }
        }
    }
}

/// IEEE 754-2008 `maxNum` for single-precision (RISC-V FMAX.S).
///
/// If exactly one operand is NaN, returns the non-NaN operand.
/// If both are NaN, returns canonical NaN.
/// Otherwise returns the arithmetic maximum.
///
/// # Arguments
///
/// * `a` - First f32 operand.
/// * `b` - Second f32 operand.
///
/// # Returns
///
/// The IEEE 754-2008 maximum, with canonical NaN propagation.
#[inline]
pub const fn fmax_f32(a: f32, b: f32) -> f32 {
    match (a.is_nan(), b.is_nan()) {
        (true, true) => f32::from_bits(CANONICAL_NAN_F32),
        (true, false) => b,
        (false, true) => a,
        (false, false) => {
            // IEEE 754-2008: +0.0 > -0.0
            if a.to_bits() == 0x0000_0000 && b.to_bits() == 0x8000_0000 {
                a // +0.0
            } else if b.to_bits() == 0x0000_0000 && a.to_bits() == 0x8000_0000 {
                b // +0.0
            } else {
                a.max(b)
            }
        }
    }
}

/// IEEE 754-2008 `minNum` for double-precision (RISC-V FMIN.D).
///
/// Same semantics as [`fmin_f32`] but for f64 operands.
#[inline]
pub const fn fmin_f64(a: f64, b: f64) -> f64 {
    match (a.is_nan(), b.is_nan()) {
        (true, true) => f64::from_bits(CANONICAL_NAN_F64),
        (true, false) => b,
        (false, true) => a,
        (false, false) => {
            if a.to_bits() == 0x8000_0000_0000_0000 && b.to_bits() == 0x0000_0000_0000_0000 {
                a
            } else if b.to_bits() == 0x8000_0000_0000_0000 && a.to_bits() == 0x0000_0000_0000_0000 {
                b
            } else {
                a.min(b)
            }
        }
    }
}

/// IEEE 754-2008 `maxNum` for double-precision (RISC-V FMAX.D).
///
/// Same semantics as [`fmax_f32`] but for f64 operands.
#[inline]
pub const fn fmax_f64(a: f64, b: f64) -> f64 {
    match (a.is_nan(), b.is_nan()) {
        (true, true) => f64::from_bits(CANONICAL_NAN_F64),
        (true, false) => b,
        (false, true) => a,
        (false, false) => {
            if a.to_bits() == 0x0000_0000_0000_0000 && b.to_bits() == 0x8000_0000_0000_0000 {
                a
            } else if b.to_bits() == 0x0000_0000_0000_0000 && a.to_bits() == 0x8000_0000_0000_0000 {
                b
            } else {
                a.max(b)
            }
        }
    }
}

/// Checks if an f32 value is a signaling NaN.
///
/// A signaling NaN has the exponent field all 1s, the quiet bit (bit 22) = 0,
/// and a non-zero mantissa payload.
pub const fn is_snan_f32(f: f32) -> bool {
    let bits = f.to_bits();
    let exp = (bits >> 23) & 0xFF;
    let mantissa = bits & 0x007F_FFFF;
    let quiet_bit = bits & 0x0040_0000;
    exp == 0xFF && mantissa != 0 && quiet_bit == 0
}

/// Checks if an f64 value is a signaling NaN.
pub const fn is_snan_f64(f: f64) -> bool {
    let bits = f.to_bits();
    let exp = (bits >> 52) & 0x7FF;
    let mantissa = bits & 0x000F_FFFF_FFFF_FFFF;
    let quiet_bit = bits & 0x0008_0000_0000_0000;
    exp == 0x7FF && mantissa != 0 && quiet_bit == 0
}