1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! 0.5.0 item 2 — fallible composed transcendentals bypass FASC (ROADMAP).
//!
//! The `try_*` variants of the composed transcendentals (tan, asin, acos,
//! sinh, cosh, tanh, asinh, acosh, atanh, plus atan) previously routed
//! through the FASC pipeline (`try_apply_unary(LazyExpr)`); they are now
//! direct compute-tier compositions mirroring the infallible methods.
//!
//! Contract pinned here:
//! 1. On in-domain inputs, `try_x(v)` is BIT-IDENTICAL to the infallible
//! `x(v)` (which is 0-ULP gated in fasc_ulp_validation) — same engines,
//! same composition, same single downscale.
//! 2. Domain violations return `Err(DomainError)` (0.4.27 contract), never
//! panic: asin/acos beyond |1|, acosh below 1, atanh at/beyond |1|.
//! 3. Storage overflow returns `Err(TierOverflow)`, never a panic and never
//! a silent wrap (0.5.0 item 1 contract).
//! 4. Boundary values: asin(±1) = ±π/2, acos(1) = 0 — no division by
//! sqrt(0) on the way.
use g_math::fixed_point::{FixedPoint, OverflowDetected};
fn fp(s: &str) -> FixedPoint {
FixedPoint::from_str(s)
}
/// In-domain inputs valid for EVERY function under test on EVERY profile
/// (|x| < 1 for the asin/acos/atanh subset; small enough for Q8.24).
const UNIT_INTERVAL: &[&str] = &["0.5", "-0.5", "0.25", "-0.75", "0"];
/// Inputs for the unrestricted functions (tan/atan/sinh/cosh/tanh/asinh).
const GENERAL: &[&str] = &["0.5", "-0.5", "1.5", "-1.25", "2", "0"];
#[test]
fn try_variants_bit_identical_to_infallible() {
for s in GENERAL {
let v = fp(s);
assert_eq!(v.try_tan().unwrap(), v.tan(), "tan({s})");
assert_eq!(v.try_atan().unwrap(), v.atan(), "atan({s})");
assert_eq!(v.try_sinh().unwrap(), v.sinh(), "sinh({s})");
assert_eq!(v.try_cosh().unwrap(), v.cosh(), "cosh({s})");
assert_eq!(v.try_tanh().unwrap(), v.tanh(), "tanh({s})");
assert_eq!(v.try_asinh().unwrap(), v.asinh(), "asinh({s})");
}
for s in UNIT_INTERVAL {
let v = fp(s);
assert_eq!(v.try_asin().unwrap(), v.asin(), "asin({s})");
assert_eq!(v.try_acos().unwrap(), v.acos(), "acos({s})");
assert_eq!(v.try_atanh().unwrap(), v.atanh(), "atanh({s})");
}
for s in &["1", "1.5", "2", "10"] {
let v = fp(s);
assert_eq!(v.try_acosh().unwrap(), v.acosh(), "acosh({s})");
}
// tanh deep in saturation territory still matches the infallible twin
// (wide profiles resolve 1 - 2e-26; narrow ones round to exactly 1).
let v = fp("30");
assert_eq!(v.try_tanh().unwrap(), v.tanh(), "tanh(30)");
}
#[test]
fn domain_violations_are_domain_errors() {
for (name, r) in [
("asin(1.5)", fp("1.5").try_asin()),
("asin(-1.5)", fp("-1.5").try_asin()),
("acos(1.5)", fp("1.5").try_acos()),
("acos(-1.5)", fp("-1.5").try_acos()),
("acosh(0.5)", fp("0.5").try_acosh()),
("acosh(-2)", fp("-2").try_acosh()),
("atanh(1)", fp("1").try_atanh()),
("atanh(-1)", fp("-1").try_atanh()),
("atanh(2)", fp("2").try_atanh()),
] {
match r {
Err(OverflowDetected::DomainError) => {}
other => panic!("{name}: expected Err(DomainError), got {other:?}"),
}
}
}
#[test]
fn asin_acos_boundaries_are_exact() {
// asin(±1) = ±π/2 via the boundary shortcut (no division by sqrt(0)).
//
// No decimal literal can serve as a sub-ulp π/2 reference on every
// profile (the parser caps at 76 fractional digits; scientific needs
// 77+), so the numeric grounding is structural: acos(0) computes
// π/2 − atan(0) through the GENERAL branch — the same pi-half constant
// and the same single downscale — and must agree with the asin(1)
// shortcut bit-for-bit. The constant itself is 0-ULP mpmath-gated in
// fasc_ulp_validation via the infallible asin/acos.
let asin_one = fp("1").try_asin().unwrap();
assert_eq!(
asin_one,
fp("0").try_acos().unwrap(),
"asin(1) shortcut must equal acos(0) general path"
);
assert!(asin_one > fp("1.57"), "asin(1) magnitude sanity");
let asin_neg_one = fp("-1").try_asin().unwrap();
assert_eq!(
asin_one + asin_neg_one,
FixedPoint::ZERO,
"asin(1) + asin(-1) must cancel exactly"
);
// acos(1) = π/2 − π/2 = exactly 0.
assert_eq!(fp("1").try_acos().unwrap(), FixedPoint::ZERO, "acos(1)");
}
#[test]
fn overflow_is_loud_never_wrapped() {
// cosh(180) ≈ 3.7e77 exceeds even scientific Q256.256 (max ~5.8e76);
// every narrower profile overflows earlier still. The contract is a
// typed error — never a panic, never a wrapped value.
match fp("180").try_cosh() {
Err(OverflowDetected::TierOverflow) => {}
other => panic!("try_cosh(180): expected Err(TierOverflow), got {other:?}"),
}
match fp("180").try_sinh() {
Err(OverflowDetected::TierOverflow) => {}
other => panic!("try_sinh(180): expected Err(TierOverflow), got {other:?}"),
}
}