Skip to main content

sema_core/
num.rs

1//! Exact numeric comparison across the int/float boundary.
2//!
3//! The naive `a as f64` cast used by mixed-type `=`/`<`/`>` is lossy above
4//! 2^53: it collapses adjacent integers onto the same float, so distinct
5//! numbers compare equal (`(= 9007199254740993 9007199254740992.0)` → `#t`).
6//! [`cmp_int_float`] compares an `i64` and an `f64` exactly instead, and is the
7//! single source of truth shared by the VM (`vm_eq`/`vm_lt`) and the stdlib
8//! first-class `=`/`<`/`>` functions.
9
10use std::cmp::Ordering;
11
12/// Compare an `i64` against an `f64` *exactly*, without any lossy cast. Returns
13/// `None` only when `b` is NaN, matching IEEE's unordered semantics (callers
14/// treat `None` as "comparison false").
15pub fn cmp_int_float(a: i64, b: f64) -> Option<Ordering> {
16    if b.is_nan() {
17        return None;
18    }
19    // Floats outside the i64 range: the sign decides without inspecting `a`.
20    // 2^63 and -2^63 are exactly representable in f64.
21    if b >= 9_223_372_036_854_775_808.0 {
22        return Some(Ordering::Less); // a < 2^63 ≤ b
23    }
24    if b < -9_223_372_036_854_775_808.0 {
25        return Some(Ordering::Greater); // a ≥ -2^63 > b
26    }
27    // b is now within [-2^63, 2^63): truncation toward zero is exact and in range.
28    let bt = b.trunc();
29    let bi = bt as i64;
30    match a.cmp(&bi) {
31        Ordering::Equal if b > bt => Some(Ordering::Less), // a == bt, b has a positive fraction
32        Ordering::Equal if b < bt => Some(Ordering::Greater), // b has a negative fraction
33        ord => Some(ord),                                  // integral b, or a ≠ bt
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn exact_above_2_pow_53() {
43        // 2^53 + 1 (int) vs 2^53 (float): distinct, int is larger.
44        assert_eq!(
45            cmp_int_float(9_007_199_254_740_993, 9_007_199_254_740_992.0),
46            Some(Ordering::Greater)
47        );
48        // Exactly equal at 2^53.
49        assert_eq!(
50            cmp_int_float(9_007_199_254_740_992, 9_007_199_254_740_992.0),
51            Some(Ordering::Equal)
52        );
53    }
54
55    #[test]
56    fn fractions_and_small_values() {
57        assert_eq!(cmp_int_float(1, 1.5), Some(Ordering::Less));
58        assert_eq!(cmp_int_float(2, 1.5), Some(Ordering::Greater));
59        assert_eq!(cmp_int_float(-2, -1.5), Some(Ordering::Less));
60        assert_eq!(cmp_int_float(-2, -2.5), Some(Ordering::Greater));
61        assert_eq!(cmp_int_float(0, 0.0), Some(Ordering::Equal));
62    }
63
64    #[test]
65    fn out_of_range_and_nan() {
66        assert_eq!(cmp_int_float(5, 1e300), Some(Ordering::Less));
67        assert_eq!(cmp_int_float(5, -1e300), Some(Ordering::Greater));
68        assert_eq!(
69            cmp_int_float(i64::MAX, 9_223_372_036_854_775_808.0),
70            Some(Ordering::Less)
71        );
72        assert_eq!(
73            cmp_int_float(i64::MIN, -9_223_372_036_854_775_808.0),
74            Some(Ordering::Equal)
75        );
76        assert_eq!(cmp_int_float(1, f64::NAN), None);
77    }
78}