use g_math::fixed_point::domains::decimal_fixed::transcendental::decimal_compute::BinaryStorage;
use g_math::fixed_point::domains::decimal_fixed::transcendental::{
decimal_compute_from_int, decimal_compute_is_zero, decimal_downscale_to_storage, decimal_exp,
decimal_upscale_to_compute, DECIMAL_STORAGE_MAX_DP,
};
use g_math::fixed_point::{DecimalFixed, OverflowDetected};
use std::panic::{catch_unwind, AssertUnwindSafe};
include!("data/decimal_exp_range_refs.rs");
#[cfg(table_format = "q16_16")]
const COMPUTE_BITS: i64 = 64;
#[cfg(table_format = "q32_32")]
const COMPUTE_BITS: i64 = 128;
#[cfg(table_format = "q64_64")]
const COMPUTE_BITS: i64 = 256;
#[cfg(table_format = "q128_128")]
const COMPUTE_BITS: i64 = 512;
#[cfg(table_format = "q256_256")]
const COMPUTE_BITS: i64 = 1024;
fn eval<const D: u8>(f: &str, x: i128) -> i128 {
let v = DecimalFixed::<D>::from_raw(x);
let r = match f {
"exp" => v.exp(),
"sinh" => v.sinh(),
"cosh" => v.cosh(),
"tanh" => v.tanh(),
"asinh" => v.asinh(),
"acosh" => v.acosh(),
"sin" => v.sin(),
"cos" => v.cos(),
other => panic!("unknown function {other}"),
};
r.raw_value()
}
fn eval_at(f: &str, d: u8, x: i128) -> i128 {
match d {
0 => eval::<0>(f, x),
2 => eval::<2>(f, x),
4 => eval::<4>(f, x),
9 => eval::<9>(f, x),
19 => eval::<19>(f, x),
28 => eval::<28>(f, x),
38 => eval::<38>(f, x),
other => panic!("no dispatch for D = {other}"),
}
}
thread_local! {
static QUIET: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn quiet<T>(f: impl FnOnce() -> T) -> Option<T> {
static HOOK: std::sync::Once = std::sync::Once::new();
HOOK.call_once(|| {
let default = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if !QUIET.with(|q| q.get()) {
default(info);
}
}));
});
QUIET.with(|q| q.set(true));
let r = catch_unwind(AssertUnwindSafe(f)).ok();
QUIET.with(|q| q.set(false));
r
}
fn storage(s: &str) -> BinaryStorage {
let (neg, digits) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s),
};
#[cfg(any(table_format = "q16_16", table_format = "q32_32", table_format = "q64_64"))]
{
let v: BinaryStorage = digits.parse().expect("storage literal");
if neg { -v } else { v }
}
#[cfg(any(table_format = "q128_128", table_format = "q256_256"))]
{
let ten = BinaryStorage::from_i128(10);
let mut v = BinaryStorage::from_i128(0);
for ch in digits.chars() {
v = v * ten + BinaryStorage::from_i128(ch.to_digit(10).expect("digit") as i128);
}
if neg { -v } else { v }
}
}
fn one_unit() -> BinaryStorage {
storage("1")
}
fn units(a: BinaryStorage, b: BinaryStorage) -> u128 {
#[cfg(any(table_format = "q16_16", table_format = "q32_32", table_format = "q64_64"))]
{
(a as i128).checked_sub(b as i128).map_or(u128::MAX, |d| d.unsigned_abs())
}
#[cfg(any(table_format = "q128_128", table_format = "q256_256"))]
{
let d = a - b;
if d.fits_in_i128() { d.as_i128().unsigned_abs() } else { u128::MAX }
}
}
#[test]
fn decimal_fixed_exp_family_is_correctly_rounded() {
let mut failures = Vec::new();
let mut stats: Vec<((&str, u8), (usize, u128, usize, usize))> = Vec::new();
for &(f, d, x, expected) in FIXED_REFS {
let got = quiet(|| eval_at(f, d, x));
let idx = match stats.iter().position(|(k, _)| *k == (f, d)) {
Some(i) => i,
None => {
stats.push(((f, d), (0, 0, 0, 0)));
stats.len() - 1
}
};
let s = &mut stats[idx].1;
s.0 += 1;
match got {
Some(v) => {
let units = (v - expected).unsigned_abs();
if units > 0 {
s.2 += 1;
if failures.len() < 20 {
failures.push(format!("{f}<{d}>({x}): got {v}, expected {expected} ({units} units)"));
}
}
s.1 = s.1.max(units);
}
None => {
s.3 += 1;
if failures.len() < 20 {
failures.push(format!("{f}<{d}>({x}): panicked, expected {expected}"));
}
}
}
}
for ((f, d), (n, max, bad, panics)) in &stats {
eprintln!("DecimalFixed<{d:2}>::{f:5}: points={n:4} max_units={max} mismatches={bad} panics={panics}");
}
assert!(failures.is_empty(), "{} failures, first:\n{}", failures.len(), failures.join("\n"));
}
#[test]
fn decimal_fixed_exp_beyond_range_panics() {
for f in ["exp", "sinh", "cosh"] {
let mut tops: Vec<(u8, i128)> = Vec::new();
for &(g, d, x, _) in FIXED_REFS {
if g != f {
continue;
}
match tops.iter_mut().find(|(e, _)| *e == d) {
Some(t) => t.1 = t.1.max(x),
None => tops.push((d, x)),
}
}
for (d, top) in tops {
let beyond = quiet(|| eval_at(f, d, top + 1));
assert!(beyond.is_none(), "{f}<{d}>({}) should panic, got {:?}", top + 1, beyond);
}
}
}
#[test]
fn engine_exp_at_canonical_storage_is_correctly_rounded() {
let dp = DECIMAL_STORAGE_MAX_DP;
let mut failures = Vec::new();
let (mut errors, mut mismatches, mut max_units) = (0usize, 0usize, 0u128);
let start = std::time::Instant::now();
for &(x, expected) in ENGINE_EXP_REFS {
let xc = decimal_upscale_to_compute(storage(x), dp).expect("input upscale");
match decimal_exp(xc).and_then(|r| decimal_downscale_to_storage(r, dp)) {
Ok(v) => {
let u = units(v, storage(expected));
max_units = max_units.max(u);
if u > 0 {
mismatches += 1;
if failures.len() < 20 {
failures.push(format!("exp({x} at dp {dp}): {u} units off, expected {expected}"));
}
}
}
Err(e) => {
errors += 1;
if failures.len() < 20 {
failures.push(format!("exp({x} at dp {dp}): {e:?}, expected {expected}"));
}
}
}
}
let per_call = start.elapsed().as_nanos() / ENGINE_EXP_REFS.len().max(1) as u128;
eprintln!(
"decimal_exp at dp {dp}: points={} max_units={max_units} mismatches={mismatches} errors={errors} ({per_call} ns/call incl. conversions)",
ENGINE_EXP_REFS.len()
);
assert!(failures.is_empty(), "first failures:\n{}", failures.join("\n"));
}
#[test]
fn engine_exp_reports_overflow() {
let dp = DECIMAL_STORAGE_MAX_DP;
let past = storage(ENGINE_EXP_TOP) + one_unit();
let xc = decimal_upscale_to_compute(past, dp).expect("input upscale");
let r = decimal_exp(xc).and_then(|r| decimal_downscale_to_storage(r, dp));
assert_eq!(r, Err(OverflowDetected::TierOverflow));
for k in [COMPUTE_BITS, COMPUTE_BITS + 1, 2 * COMPUTE_BITS] {
assert_eq!(decimal_exp(decimal_compute_from_int(k)), Err(OverflowDetected::TierOverflow), "exp({k})");
}
for k in [COMPUTE_BITS, COMPUTE_BITS + 1, 2 * COMPUTE_BITS] {
let r = decimal_exp(decimal_compute_from_int(-k)).expect("exp of a large negative");
assert!(decimal_compute_is_zero(&r), "exp(-{k}) should round to 0");
}
}
#[test]
fn reported_defects() {
#[cfg(table_format = "q16_16")]
{
assert_eq!(DecimalFixed::<4>::from_integer(22).exp().raw_value(), 35849128461316);
assert!(quiet(|| DecimalFixed::<4>::from_integer(23).exp()).is_none());
let big = DecimalFixed::<4>::from_raw(90_000_000_000_000);
assert_eq!(big.tanh().raw_value(), 10_000);
assert_eq!((-big).tanh().raw_value(), -10_000);
}
#[cfg(table_format = "q64_64")]
{
assert_eq!(
DecimalFixed::<19>::from_integer(40).exp().raw_value(),
2353852668370199854078999107490348045
);
assert_eq!(DecimalFixed::<0>::from_raw(1_000_000_000_000_000_000_000_000_000_000).asinh().raw_value(), 70);
}
}