#![cfg(sidereon_repo_tests)]
use std::path::PathBuf;
use serde_json::Value;
fn parse_hex_float(s: &str) -> f64 {
let s = s.trim();
let (neg, rest) = match s.strip_prefix('-') {
Some(r) => (true, r),
None => (false, s),
};
let rest = rest
.strip_prefix("0x")
.or_else(|| rest.strip_prefix("0X"))
.unwrap_or_else(|| panic!("not a hex float (missing 0x): {s:?}"));
let (mantissa, exp_str) = rest
.split_once(['p', 'P'])
.unwrap_or_else(|| panic!("not a hex float (missing p exponent): {s:?}"));
let exp2: i32 = exp_str
.parse()
.unwrap_or_else(|_| panic!("bad binary exponent in {s:?}"));
let (int_part, frac_part) = match mantissa.split_once('.') {
Some((i, f)) => (i, f),
None => (mantissa, ""),
};
let int_val: f64 = i64::from_str_radix(int_part, 16)
.unwrap_or_else(|_| panic!("bad integer hex digits in {s:?}"))
as f64;
let mut frac_val = 0.0f64;
let mut scale = 1.0f64 / 16.0;
for c in frac_part.chars() {
let d = c
.to_digit(16)
.unwrap_or_else(|| panic!("bad hex frac digit {c:?} in {s:?}"));
frac_val += (d as f64) * scale;
scale /= 16.0;
}
let significand = int_val + frac_val;
let val = significand * exp2_pow(exp2);
if neg {
-val
} else {
val
}
}
fn exp2_pow(n: i32) -> f64 {
2.0f64.powi(n)
}
fn ulp_distance(a: f64, b: f64) -> u64 {
if a.is_nan() || b.is_nan() {
return u64::MAX;
}
let ia = ordered_i64(a);
let ib = ordered_i64(b);
ia.abs_diff(ib)
}
fn ordered_i64(x: f64) -> i64 {
let bits = x.to_bits() as i64;
if bits < 0 {
i64::MIN - bits
} else {
bits
}
}
fn float_hex(x: f64) -> String {
if x == 0.0 {
return if x.is_sign_negative() {
"-0x0.0p+0".into()
} else {
"0x0.0p+0".into()
};
}
let bits = x.to_bits();
let sign = if (bits >> 63) & 1 == 1 { "-" } else { "" };
let exp = ((bits >> 52) & 0x7ff) as i64;
let mantissa = bits & 0x000f_ffff_ffff_ffff;
let unbiased = exp - 1023;
let s = if unbiased >= 0 {
format!("{sign}0x1.{mantissa:013x}p+{unbiased}")
} else {
format!("{sign}0x1.{mantissa:013x}p{unbiased}")
};
s
}
fn fixture_path() -> PathBuf {
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
crate_dir
.join("tests/fixtures/libm_tiny.json")
.canonicalize()
.unwrap_or_else(|e| {
panic!(
"cannot locate tests/fixtures/libm_tiny.json from {}: {e}",
crate_dir.display()
)
})
}
#[test]
fn libm_tiny_zero_ulp() {
let raw = std::fs::read_to_string(fixture_path()).expect("read libm_tiny.json");
let doc: Value = serde_json::from_str(&raw).expect("parse libm_tiny.json");
let mut failures: Vec<String> = Vec::new();
let mut checks = 0usize;
let probe = "0x1.921fb54442d18p-1"; let pv = parse_hex_float(probe);
assert_eq!(
float_hex(pv),
probe,
"hex-float parser/serializer round-trip is broken"
);
let cases = &doc["cases"];
for case in cases["unary"].as_array().expect("unary array") {
let name = case["name"].as_str().unwrap();
let x = parse_hex_float(case["x"].as_str().unwrap());
let expect = &case["expect"];
let got = [
("sin", x.sin()),
("cos", x.cos()),
("sqrt", x.abs().sqrt()),
("exp", (x / 256.0).exp()),
("log", (x.abs() + 1.0).ln()),
];
for (fname, actual) in got {
let exp_hex = expect[fname].as_str().unwrap();
let expected = parse_hex_float(exp_hex);
let ulp = ulp_distance(actual, expected);
checks += 1;
if ulp != 0 {
failures.push(format!(
"unary {name}.{fname}: {ulp} ULP (rust={} numpy={})",
float_hex(actual),
exp_hex
));
}
}
}
for case in cases["binary"].as_array().expect("binary array") {
let name = case["name"].as_str().unwrap();
let y = parse_hex_float(case["y"].as_str().unwrap());
let x = parse_hex_float(case["x"].as_str().unwrap());
let expect = &case["expect"];
let got = [
("atan2", y.atan2(x)),
("norm2", (x * x + y * y).sqrt()),
];
for (fname, actual) in got {
let exp_hex = expect[fname].as_str().unwrap();
let expected = parse_hex_float(exp_hex);
let ulp = ulp_distance(actual, expected);
checks += 1;
if ulp != 0 {
failures.push(format!(
"binary {name}.{fname}: {ulp} ULP (rust={} numpy={})",
float_hex(actual),
exp_hex
));
}
}
}
assert!(checks > 0, "no components were checked - fixture empty?");
assert!(
failures.is_empty(),
"Rust std libm diverged from the numpy reference on {} of {checks} components:\n {}",
failures.len(),
failures.join("\n ")
);
}