From 5b04d7ec3e1d131fbaafe9e1e49908dd57b465fa Mon Sep 17 00:00:00 2001 From: Demetrios Agourakis Date: Tue, 27 Oct 2026 06:24:07 -0300 Subject: [PATCH] Avoid NaN real components in complex tanh after overflow When the denominator cosh(2*real(z)) + cos(2*imag(z)) overflows, return the correctly signed saturated real component instead of the inf/inf quotient. Keep the ordinary finite path, imaginary formula and fast-math path unchanged. Add mixed-lane float/double regression tests. Validate unchanged-base failure and corrected results in C++23 on arm64, SSE2 and AVX2, focused ASan/UBSan and the complete arm64 CMake suite. Signed-zero discrepancies and floating-point exception behavior remain outside this fix. Assisted-by: OpenAI Codex --- .../xsimd/arch/common/xsimd_common_trigo.hpp | 6 ++- test/test_complex_hyperbolic.cpp | 52 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/include/xsimd/arch/common/xsimd_common_trigo.hpp b/include/xsimd/arch/common/xsimd_common_trigo.hpp index 00ad75076..3c12982e6 100644 --- a/include/xsimd/arch/common/xsimd_common_trigo.hpp +++ b/include/xsimd/arch/common/xsimd_common_trigo.hpp @@ -986,7 +986,11 @@ namespace xsimd auto y = z.imag(); real_batch two(2); auto d = cosh(two * x) + cos(two * y); - return { sinh(two * x) / d, sin(two * y) / d }; + auto real_part = sinh(two * x) / d; +#ifndef __FAST_MATH__ + real_part = select(isinf(d), copysign(real_batch(1), x), real_part); +#endif + return { real_part, sin(two * y) / d }; } } diff --git a/test/test_complex_hyperbolic.cpp b/test/test_complex_hyperbolic.cpp index 40e97ca7c..ebaf0a95b 100644 --- a/test/test_complex_hyperbolic.cpp +++ b/test/test_complex_hyperbolic.cpp @@ -172,4 +172,56 @@ TEST_CASE_TEMPLATE("[complex hyperbolic]", B, BATCH_COMPLEX_TYPES) Test.test_atanh(); } } + +#ifndef __FAST_MATH__ +TEST_CASE_TEMPLATE("[complex tanh real overflow]", B, BATCH_COMPLEX_TYPES) +{ + using value_type = typename B::value_type; + using real_type = typename value_type::value_type; + const real_type large = std::is_same::value ? real_type(45) : real_type(356); + const real_type largest = std::numeric_limits::max(); + const std::array samples = { { + { -large, real_type(1) }, + { large, real_type(-1) }, + { -large, real_type(0) }, + { large, -real_type(0) }, + { -largest, real_type(1) }, + { largest, real_type(-1) }, + { -largest, -real_type(0) }, + { largest, real_type(0) }, + { real_type(1), real_type(-1) }, + { real_type(-1), real_type(1) }, + { large, real_type(1) }, + { -large, real_type(-1) }, + } }; + + for (size_t offset = 0; offset < samples.size(); offset += B::size) + { + std::array input, output; + for (size_t lane = 0; lane < B::size; ++lane) + { + input[lane] = samples[(offset + lane) % samples.size()]; + } + xsimd::tanh(B::load_unaligned(input.data())).store_unaligned(output.data()); + for (size_t lane = 0; lane < B::size; ++lane) + { + const value_type expected = std::tanh(input[lane]); + const real_type actual_parts[] = { output[lane].real(), output[lane].imag() }; + const real_type expected_parts[] = { expected.real(), expected.imag() }; + for (size_t part = 0; part < 2; ++part) + { + CHECK(std::isfinite(actual_parts[part])); + if (expected_parts[part] == real_type(0)) + { + CHECK(actual_parts[part] == real_type(0)); + } + else + { + CHECK(actual_parts[part] == doctest::Approx(expected_parts[part]).epsilon(64 * std::numeric_limits::epsilon())); + } + } + } + } +} +#endif #endif