From d194d5eddea3377d5a8976f48e7a65c23e471f43 Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Sun, 1 Dec 2024 13:43:29 -0800 Subject: [PATCH 1/7] cleanup of FPE on macOS --- src/system.cpp | 90 +++++++++++++++++++---- src/system.hpp | 3 + unitTests/testFloatingPointExceptions.cpp | 4 - 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index a6532ac5..e1a76d9a 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -530,7 +530,48 @@ void resetSignalHandling() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int getDefaultFloatingPointExceptions() { - return ( FE_DIVBYZERO | FE_OVERFLOW | FE_INVALID ); + return ( FE_DIVBYZERO | FE_UNDERFLOW | FE_OVERFLOW | FE_INVALID ); +} + +unsigned long long int translateFloatingPointException( unsigned long long int const exception ) +{ + unsigned long long int result = 0; +#if defined(__APPLE__) && defined(__MACH__) // if apple + if( exception & FE_INEXACT ) + { + result |= __fpcr_trap_inexact; + } + if( exception & FE_UNDERFLOW ) + { + result |= __fpcr_trap_underflow; + } + if( exception & FE_OVERFLOW ) + { + result |= __fpcr_trap_overflow; + } + if( exception & FE_DIVBYZERO ) + { + result |= __fpcr_trap_divbyzero; + } + if( exception & FE_INVALID ) + { + result |= __fpcr_trap_invalid; + } + +#if defined(__arm__) || defined(__arm64__) // if apple arm +#elif defined(__x86_64__) // if apple x86_64 +#else // if apple but not arm or x86_64 + std::cerr<< "LvArray::system::translateFloatingPointException() not implemented for this architecture" << std::endl; +#endif + + +#else // if not apple +#if defined(__x86_64__) + result = exception; +#endif +#endif + +return result; } #if defined(__APPLE__) && defined(__MACH__)&& !defined(__x86_64__) @@ -542,10 +583,8 @@ fpe_signal_handler( int sig, siginfo_t *sip, void *scp ) int fe_code = sip->si_code; - printf( "In signal handler : " ); - if( fe_code == ILL_ILLTRP ) - printf( "Illegal trap detected\n" ); + printf( "Illegal trap detected. If you see this you have a FPE, but Apple Silicon doesn't provide data on which FPE has occured.\n" ); else printf( "Code detected : %d\n", fe_code ); @@ -559,19 +598,22 @@ int enableFloatingPointExceptions( int const exceptions ) #if defined(__APPLE__) && defined(__MACH__) #if !defined(__x86_64__) - LVARRAY_UNUSED_VARIABLE( exceptions ); + unsigned long long int const exceptionMasks = translateFloatingPointException( exceptions ); fenv_t env; fegetenv( &env ); - env.__fpcr = env.__fpcr | __fpcr_trap_invalid; +// std::cout< #include - -#if defined(__x86_64__) - using namespace testFloatingPointExceptionsHelpers; const char IGNORE_OUTPUT[] = ".*"; @@ -74,7 +71,6 @@ TEST( TestFloatingPointEnvironment, FloatingPointExceptionGuard ) } // namespace testing } // namespace LvArray -#endif // This is the default gtest main method. It is included for ease of debugging. int main( int argc, char * * argv ) From 031918d0f8556bd0f5c290086cd21e1f4adc22c4 Mon Sep 17 00:00:00 2001 From: "Randolph R. Settgast" Date: Wed, 4 Feb 2026 14:39:57 -0800 Subject: [PATCH 2/7] some general cleanup --- src/system.cpp | 180 ++++++++++------------ src/system.hpp | 11 +- unitTests/testFloatingPointExceptions.cpp | 27 +--- 3 files changed, 93 insertions(+), 125 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index e1a76d9a..21d8279c 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -18,7 +18,8 @@ #include #include -#if defined(__x86_64__) +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) + #include // or #include #endif @@ -311,41 +312,6 @@ static std::string getSourceLocationFromFrame( void const * const address ) return ""; } -/** - * @brief Return a string representing the current floating point exception. - * @return A string representing the current floating point exception. - */ -static std::string getFpeDetails() -{ - std::ostringstream oss; - int const fpe = fetestexcept( FE_ALL_EXCEPT ); - - oss << "Floating point exception:"; - - if( fpe & FE_DIVBYZERO ) - { - oss << " Division by zero;"; - } - if( fpe & FE_INEXACT ) - { - oss << " Inexact result;"; - } - if( fpe & FE_INVALID ) - { - oss << " Invalid argument;"; - } - if( fpe & FE_OVERFLOW ) - { - oss << " Overflow;"; - } - if( fpe & FE_UNDERFLOW ) - { - oss << " Underflow;"; - } - - return oss.str(); -} - namespace LvArray { namespace system @@ -465,72 +431,78 @@ void callErrorHandler() } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void stackTraceHandler( int const sig, bool const exit ) + +void signalHandler( int sig, siginfo_t * info, void * /*ucontext*/ ) { std::ostringstream oss; if( sig >= 0 && sig < NSIG ) { - // sys_signame not available on linux, so just print the code; strsignal is POSIX oss << "Received signal " << sig << ": " << strsignal( sig ) << "\n"; if( sig == SIGFPE ) { - oss << getFpeDetails() << "\n"; + if( info ) + { + oss << " SIGFPE si_code = " << info->si_code << " "; + + switch( info->si_code ) + { + case FPE_FLTDIV: oss << "(floating divide by zero)\n"; break; + case FPE_FLTOVF: oss << "(floating overflow)\n"; break; + case FPE_FLTUND: oss << "(floating underflow)\n"; break; + case FPE_FLTINV: oss << "(floating invalid operation)\n"; break; + case FPE_FLTRES: oss << "(floating inexact)\n"; break; + case FPE_INTDIV: oss << "(integer divide by zero)\n"; break; + case FPE_INTOVF: oss << "(integer overflow)\n"; break; + default: oss << "(other)\n"; break; + } + } } } oss << stackTrace( true ) << std::endl; - std::cout << oss.str(); + std::cerr << oss.str(); - if( exit ) - { - // An infinite loop was encountered when an FPE was received. Resetting the handlers didn't - // fix it because they would just recurse. This does. - setSignalHandling( nullptr ); - callErrorHandler(); - } + std::_Exit( 1 ); } -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void setSignalHandling( void (* handler)( int ) ) -{ - initialHandler[SIGHUP] = signal( SIGHUP, handler ); - initialHandler[SIGINT] = signal( SIGINT, handler ); - initialHandler[SIGQUIT] = signal( SIGQUIT, handler ); - initialHandler[SIGILL] = signal( SIGILL, handler ); - initialHandler[SIGTRAP] = signal( SIGTRAP, handler ); - initialHandler[SIGABRT] = signal( SIGABRT, handler ); -#if (defined(_POSIX_C_SOURCE) && !defined(_DARWIN_C_SOURCE)) - initialHandler[SIGPOLL] = signal( SIGPOLL, handler ); -#else - initialHandler[SIGIOT] = signal( SIGIOT, handler ); - initialHandler[SIGEMT] = signal( SIGEMT, handler ); -#endif - initialHandler[SIGFPE] = signal( SIGFPE, handler ); - initialHandler[SIGKILL] = signal( SIGKILL, handler ); - initialHandler[SIGBUS] = signal( SIGBUS, handler ); - initialHandler[SIGSEGV] = signal( SIGSEGV, handler ); - initialHandler[SIGSYS] = signal( SIGSYS, handler ); - initialHandler[SIGPIPE] = signal( SIGPIPE, handler ); - initialHandler[SIGTERM] = signal( SIGTERM, handler ); - - return; -} -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void resetSignalHandling() +static struct sigaction g_oldAction[NSIG]; + +void setSignalHandling( void (* handler)( int, siginfo_t * info, void * ) ) { - for( auto a : initialHandler ) - { - signal( a.first, a.second ); - } + struct sigaction sa; + sigemptyset( &sa.sa_mask ); + sa.sa_sigaction = handler; + sa.sa_flags = SA_SIGINFO; + + auto install = [&]( int sig ) + { + sigaction( sig, &sa, &g_oldAction[sig] ); + }; + + install( SIGHUP ); + install( SIGINT ); + install( SIGQUIT ); + install( SIGILL ); + install( SIGTRAP ); + install( SIGABRT ); + install( SIGFPE ); + install( SIGBUS ); + install( SIGSEGV ); + install( SIGSYS ); + install( SIGPIPE ); + install( SIGTERM ); + // Do NOT try SIGKILL/SIGSTOP: they can’t be caught. } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int getDefaultFloatingPointExceptions() { - return ( FE_DIVBYZERO | FE_UNDERFLOW | FE_OVERFLOW | FE_INVALID ); + return ( FE_DIVBYZERO | FE_OVERFLOW | FE_INVALID ); } unsigned long long int translateFloatingPointException( unsigned long long int const exception ) @@ -674,40 +646,48 @@ int disableFloatingPointExceptions( int const exceptions ) #endif } - -void setFlushToZero() +static void enableFlushDenormalsToZero() { +#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) -#if defined(__APPLE__) && defined(__MACH__) // if apple + // x86/x86-64: MXCSR control, via SSE intrinsics + _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); + #ifdef _MM_DENORMALS_ZERO_ON + _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); + #endif -#if defined(__arm__) || defined(__arm64__) // if apple arm - fenv_t env; - fegetenv( &env ); - env.__fpcr = env.__fpcr | __fpcr_flush_to_zero ; - // std::cout< #include #include +#include + + namespace LvArray { @@ -77,13 +80,14 @@ void callErrorHandler(); * @param sig The signal received. * @param exit If true abort execution. */ -void stackTraceHandler( int const sig, bool const exit ); +void signalHandler( int sig, siginfo_t * info, void * /*ucontext*/ ); + /** * @brief Set the signal handler for common signals. * @param handler The signal handler. */ -void setSignalHandling( void (* handler)( int ) ); +void setSignalHandling( void (* handler)( int, siginfo_t * info, void * ) = signalHandler ); /** * @brief Rest the signal handling back to the original state. @@ -117,9 +121,6 @@ int disableFloatingPointExceptions( int const exceptions = getDefaultFloatingPoi */ void setFPE(); - -void setFlushToZero(); - /** * @class FloatingPointExceptionGuard * @brief Changes the floating point environment and reverts it when destoyed. diff --git a/unitTests/testFloatingPointExceptions.cpp b/unitTests/testFloatingPointExceptions.cpp index 3ceed03a..1ae5ee80 100644 --- a/unitTests/testFloatingPointExceptions.cpp +++ b/unitTests/testFloatingPointExceptions.cpp @@ -29,43 +29,27 @@ namespace testing TEST( TestFloatingPointEnvironment, Underflow ) { - system::enableFloatingPointExceptions( FE_UNDERFLOW ); - EXPECT_DEATH_IF_SUPPORTED( divide( DBL_MIN, 2 ), IGNORE_OUTPUT ); - system::disableFloatingPointExceptions( FE_UNDERFLOW ); - system::setFPE(); double fpnum = divide( DBL_MIN, 2 ); - int fpclassification = std::fpclassify( fpnum ); - EXPECT_NE( fpclassification, FP_SUBNORMAL ); + EXPECT_DOUBLE_EQ( fpnum, 0.0 ); } TEST( TestFloatingPointEnvironment, DivideByZero ) { system::setFPE(); - EXPECT_DEATH_IF_SUPPORTED( divide( 1, 0 ), IGNORE_OUTPUT ); + EXPECT_DEATH_IF_SUPPORTED( divide( 1, 0 ), R"((floating divide by zero)(.|\n)*StackTrace)" ); } TEST( TestFloatingPointEnvironment, Overlow ) { system::setFPE(); - EXPECT_DEATH_IF_SUPPORTED( multiply( DBL_MAX, 2 ), IGNORE_OUTPUT ); + EXPECT_DEATH_IF_SUPPORTED( multiply( DBL_MAX, 2 ), R"((floating overflow)(.|\n)*StackTrace)" ); } TEST( TestFloatingPointEnvironment, Invalid ) { system::setFPE(); - EXPECT_DEATH_IF_SUPPORTED( invalid(), IGNORE_OUTPUT ); -} - -TEST( TestFloatingPointEnvironment, FloatingPointExceptionGuard ) -{ - system::setFPE(); - - { - system::FloatingPointExceptionGuard guard( FE_UNDERFLOW ); - divide( DBL_MIN, 2 ); - EXPECT_DEATH_IF_SUPPORTED( multiply( DBL_MAX, 2 ), IGNORE_OUTPUT ); - } + EXPECT_DEATH_IF_SUPPORTED( invalid(), R"((floating invalid operation)(.|\n)*StackTrace)" ); } } // namespace testing @@ -75,6 +59,9 @@ TEST( TestFloatingPointEnvironment, FloatingPointExceptionGuard ) // This is the default gtest main method. It is included for ease of debugging. int main( int argc, char * * argv ) { + + LvArray::system::setSignalHandling(); + ::testing::InitGoogleTest( &argc, argv ); int const result = RUN_ALL_TESTS(); return result; From f173d1c7772574e8954912941c7266d3545c19e0 Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Mon, 2 Mar 2026 20:17:48 -0800 Subject: [PATCH 3/7] some cleanup and documentation --- src/system.cpp | 274 +++++++++++++++------- src/system.hpp | 7 +- unitTests/testFloatingPointExceptions.cpp | 33 ++- 3 files changed, 225 insertions(+), 89 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index 21d8279c..1dd013d4 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -10,7 +10,6 @@ #include "Macros.hpp" // System includes -#include #include #include #include @@ -31,6 +30,17 @@ #include #endif +/* + * Implementation map: + * 1) Low-level stack frame collection and symbol/source resolution. + * 2) Public stack trace and type demangling helpers. + * 3) Error and signal handling (including FPE signal decoding). + * 4) Cross-platform floating-point environment control: + * - enable/disable traps, + * - translate FE_* masks where platform internals differ, + * - flush denormals/subnormals to zero. + */ + /** * @struct UnwindState * @brief Holds info used in unwindCallback. @@ -300,6 +310,7 @@ static std::string addr2line( const char * flag, const void * addr ) static std::string getSourceLocationFromFrame( void const * const address ) { #if defined( LVARRAY_ADDR2LINE_EXEC ) + // "-Cpe" asks addr2line for demangled symbol + file:line + inlined call info. std::string const source_line = addr2line( "-Cpe", address ); if( !source_line.empty() && source_line[0] != '?' ) { @@ -317,11 +328,10 @@ namespace LvArray namespace system { -/// An alias for a function that takes an int and returns nothing. -using handle_type = void ( * )( int ); - -/// A map containing the initial signal handlers. -static std::map< int, handle_type > initialHandler; +// Snapshot of handlers that were active before LvArray installs its own handlers. +// We keep these so resetSignalHandling() can restore previous process behavior. +static struct sigaction g_oldAction[NSIG]; +static bool g_oldActionSet[NSIG] = {}; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// std::string stackTrace( bool const location ) @@ -329,6 +339,7 @@ std::string stackTrace( bool const location ) constexpr int MAX_FRAMES = 25; void * array[ MAX_FRAMES ]; + // Skip this helper frame so frame 0 is the caller of stackTrace(). std::size_t const size = collect( array, MAX_FRAMES, 1 ); std::ostringstream oss; @@ -432,8 +443,9 @@ void callErrorHandler() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void signalHandler( int sig, siginfo_t * info, void * /*ucontext*/ ) +void signalHandler( int sig, siginfo_t * info, void * ucontext ) { + LVARRAY_UNUSED_VARIABLE( ucontext ); std::ostringstream oss; if( sig >= 0 && sig < NSIG ) @@ -448,6 +460,7 @@ void signalHandler( int sig, siginfo_t * info, void * /*ucontext*/ ) switch( info->si_code ) { + // POSIX SIGFPE subcodes let us report the concrete floating-point fault. case FPE_FLTDIV: oss << "(floating divide by zero)\n"; break; case FPE_FLTOVF: oss << "(floating overflow)\n"; break; case FPE_FLTUND: oss << "(floating underflow)\n"; break; @@ -459,6 +472,22 @@ void signalHandler( int sig, siginfo_t * info, void * /*ucontext*/ ) } } } + else if( sig == SIGILL && info ) + { +#if defined(__APPLE__) && defined(__MACH__) && defined(__aarch64__) + if( info->si_code == ILL_ILLTRP ) + { + // Apple arm64 may report FP traps as SIGILL/ILL_ILLTRP instead of SIGFPE. + // In that mode the subtype is not exposed, so we emit the best available text. + oss << " SIGILL si_code = " << info->si_code + << " (floating-point trap, subtype unavailable on this platform)\n"; + } + else +#endif + { + oss << " SIGILL si_code = " << info->si_code << "\n"; + } + } } oss << stackTrace( true ) << std::endl; @@ -468,47 +497,146 @@ void signalHandler( int sig, siginfo_t * info, void * /*ucontext*/ ) } -static struct sigaction g_oldAction[NSIG]; - void setSignalHandling( void (* handler)( int, siginfo_t * info, void * ) ) { struct sigaction sa; + memset( &sa, 0, sizeof( sa ) ); sigemptyset( &sa.sa_mask ); - sa.sa_sigaction = handler; - sa.sa_flags = SA_SIGINFO; + if( handler == nullptr ) + { + sa.sa_handler = SIG_DFL; + sa.sa_flags = 0; + } + else + { + sa.sa_sigaction = handler; + sa.sa_flags = SA_SIGINFO; + } auto install = [&]( int sig ) { - sigaction( sig, &sa, &g_oldAction[sig] ); + if( sig <= 0 || sig >= NSIG ) + { + return; + } + + if( g_oldActionSet[sig] ) + { + // We already cached the original action for this signal, so do not overwrite it. + sigaction( sig, &sa, nullptr ); + } + else if( sigaction( sig, &sa, &g_oldAction[sig] ) == 0 ) + { + // First install: capture previous action for future resetSignalHandling(). + g_oldActionSet[sig] = true; + } }; +#ifdef SIGHUP install( SIGHUP ); +#endif +#ifdef SIGINT install( SIGINT ); +#endif +#ifdef SIGQUIT install( SIGQUIT ); +#endif +#ifdef SIGILL install( SIGILL ); +#endif +#ifdef SIGTRAP install( SIGTRAP ); +#endif +#ifdef SIGABRT install( SIGABRT ); +#endif +#ifdef SIGFPE install( SIGFPE ); +#endif +#ifdef SIGBUS install( SIGBUS ); +#endif +#ifdef SIGSEGV install( SIGSEGV ); +#endif +#ifdef SIGSYS install( SIGSYS ); +#endif +#ifdef SIGPIPE install( SIGPIPE ); +#endif +#ifdef SIGTERM install( SIGTERM ); +#endif // Do NOT try SIGKILL/SIGSTOP: they can’t be caught. } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +void resetSignalHandling() +{ + auto restore = []( int sig ) + { + if( sig <= 0 || sig >= NSIG || !g_oldActionSet[sig] ) + { + return; + } + + sigaction( sig, &g_oldAction[sig], nullptr ); + }; + +#ifdef SIGHUP + restore( SIGHUP ); +#endif +#ifdef SIGINT + restore( SIGINT ); +#endif +#ifdef SIGQUIT + restore( SIGQUIT ); +#endif +#ifdef SIGILL + restore( SIGILL ); +#endif +#ifdef SIGTRAP + restore( SIGTRAP ); +#endif +#ifdef SIGABRT + restore( SIGABRT ); +#endif +#ifdef SIGFPE + restore( SIGFPE ); +#endif +#ifdef SIGBUS + restore( SIGBUS ); +#endif +#ifdef SIGSEGV + restore( SIGSEGV ); +#endif +#ifdef SIGSYS + restore( SIGSYS ); +#endif +#ifdef SIGPIPE + restore( SIGPIPE ); +#endif +#ifdef SIGTERM + restore( SIGTERM ); +#endif +} + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int getDefaultFloatingPointExceptions() { + // These are the "hard" numerical faults that should typically stop execution. return ( FE_DIVBYZERO | FE_OVERFLOW | FE_INVALID ); } -unsigned long long int translateFloatingPointException( unsigned long long int const exception ) +static unsigned long long int translateFloatingPointException( unsigned long long int const exception ) { unsigned long long int result = 0; #if defined(__APPLE__) && defined(__MACH__) // if apple + // Darwin arm64 stores trap masks in fpcr-specific bits (__fpcr_trap_*), + // so FE_* must be translated before writing env.__fpcr. if( exception & FE_INEXACT ) { result |= __fpcr_trap_inexact; @@ -529,40 +657,15 @@ unsigned long long int translateFloatingPointException( unsigned long long int c { result |= __fpcr_trap_invalid; } - -#if defined(__arm__) || defined(__arm64__) // if apple arm -#elif defined(__x86_64__) // if apple x86_64 -#else // if apple but not arm or x86_64 - std::cerr<< "LvArray::system::translateFloatingPointException() not implemented for this architecture" << std::endl; -#endif - - #else // if not apple -#if defined(__x86_64__) +#if defined(__x86_64__) || defined(__i386__) + // Linux x86 feenableexcept/fedisableexcept already use FE_* bit positions. result = exception; #endif #endif -return result; -} - -#if defined(__APPLE__) && defined(__MACH__)&& !defined(__x86_64__) -static void -fpe_signal_handler( int sig, siginfo_t *sip, void *scp ) -{ - LVARRAY_UNUSED_VARIABLE( sig ); - LVARRAY_UNUSED_VARIABLE( scp ); - - int fe_code = sip->si_code; - - if( fe_code == ILL_ILLTRP ) - printf( "Illegal trap detected. If you see this you have a FPE, but Apple Silicon doesn't provide data on which FPE has occured.\n" ); - else - printf( "Code detected : %d\n", fe_code ); - - abort(); + return result; } -#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int enableFloatingPointExceptions( int const exceptions ) @@ -570,23 +673,17 @@ int enableFloatingPointExceptions( int const exceptions ) #if defined(__APPLE__) && defined(__MACH__) #if !defined(__x86_64__) - unsigned long long int const exceptionMasks = translateFloatingPointException( exceptions ); + // Apple arm64 path: manipulate fpcr trap bits via fenv_t. + unsigned long long int const exceptionMasks = translateFloatingPointException( exceptions ); fenv_t env; - fegetenv( &env ); - -// std::cout< const & handler ); void callErrorHandler(); /** - * @brief Print signal information and a stack trace to standard out, optionally aborting. + * @brief Print signal information and a stack trace, then terminate. * @param sig The signal received. - * @param exit If true abort execution. + * @param info Additional signal information. + * @param ucontext Platform-specific user context (unused). */ -void signalHandler( int sig, siginfo_t * info, void * /*ucontext*/ ); +void signalHandler( int sig, siginfo_t * info, void * ucontext ); /** diff --git a/unitTests/testFloatingPointExceptions.cpp b/unitTests/testFloatingPointExceptions.cpp index 1ae5ee80..4af78478 100644 --- a/unitTests/testFloatingPointExceptions.cpp +++ b/unitTests/testFloatingPointExceptions.cpp @@ -8,7 +8,6 @@ // Source includes #include "testFloatingPointExceptionsHelpers.hpp" #include "system.hpp" -#include "system.hpp" // TPL includes #include @@ -20,7 +19,18 @@ using namespace testFloatingPointExceptionsHelpers; -const char IGNORE_OUTPUT[] = ".*"; +#if defined(__APPLE__) && defined(__MACH__) && defined(__aarch64__) +char const DIVIDE_BY_ZERO_REGEX[] = + R"(((floating divide by zero)|(floating-point trap, subtype unavailable on this platform))(.|\n)*StackTrace)"; +char const OVERFLOW_REGEX[] = + R"(((floating overflow)|(floating-point trap, subtype unavailable on this platform))(.|\n)*StackTrace)"; +char const INVALID_REGEX[] = + R"(((floating invalid operation)|(floating-point trap, subtype unavailable on this platform))(.|\n)*StackTrace)"; +#else +char const DIVIDE_BY_ZERO_REGEX[] = R"((floating divide by zero)(.|\n)*StackTrace)"; +char const OVERFLOW_REGEX[] = R"((floating overflow)(.|\n)*StackTrace)"; +char const INVALID_REGEX[] = R"((floating invalid operation)(.|\n)*StackTrace)"; +#endif namespace LvArray { @@ -37,19 +47,30 @@ TEST( TestFloatingPointEnvironment, Underflow ) TEST( TestFloatingPointEnvironment, DivideByZero ) { system::setFPE(); - EXPECT_DEATH_IF_SUPPORTED( divide( 1, 0 ), R"((floating divide by zero)(.|\n)*StackTrace)" ); + EXPECT_DEATH_IF_SUPPORTED( divide( 1, 0 ), DIVIDE_BY_ZERO_REGEX ); } -TEST( TestFloatingPointEnvironment, Overlow ) +TEST( TestFloatingPointEnvironment, Overflow ) { system::setFPE(); - EXPECT_DEATH_IF_SUPPORTED( multiply( DBL_MAX, 2 ), R"((floating overflow)(.|\n)*StackTrace)" ); + EXPECT_DEATH_IF_SUPPORTED( multiply( DBL_MAX, 2 ), OVERFLOW_REGEX ); } TEST( TestFloatingPointEnvironment, Invalid ) { system::setFPE(); - EXPECT_DEATH_IF_SUPPORTED( invalid(), R"((floating invalid operation)(.|\n)*StackTrace)" ); + EXPECT_DEATH_IF_SUPPORTED( invalid(), INVALID_REGEX ); +} + +TEST( TestFloatingPointEnvironment, FloatingPointExceptionGuard ) +{ + system::setFPE(); + + { + system::FloatingPointExceptionGuard guard( FE_UNDERFLOW ); + divide( DBL_MIN, 2 ); + EXPECT_DEATH_IF_SUPPORTED( multiply( DBL_MAX, 2 ), OVERFLOW_REGEX ); + } } } // namespace testing From 7c71c947dac404aeab8e855504caef97b5fc99d2 Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Mon, 16 Mar 2026 13:44:55 -0700 Subject: [PATCH 4/7] cleanup --- src/system.cpp | 155 ++++++++++++++++++++----------------------------- src/system.hpp | 16 +++-- 2 files changed, 73 insertions(+), 98 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index 1dd013d4..070e4f24 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -443,6 +443,16 @@ void callErrorHandler() /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * @brief Default signal handler: print diagnostics and exit. + * @param sig The signal received. + * @param info Additional signal information (si_code identifies the fault). + * @param ucontext Platform-specific user context (unused). + * @note Uses non-async-signal-safe functions (ostringstream, strsignal, cerr) + * for actionable diagnostics. This is a deliberate best-effort tradeoff; + * if the process is in a state where these fail, the _Exit below will + * still terminate cleanly. + */ void signalHandler( int sig, siginfo_t * info, void * ucontext ) { LVARRAY_UNUSED_VARIABLE( ucontext ); @@ -493,9 +503,49 @@ void signalHandler( int sig, siginfo_t * info, void * ucontext ) oss << stackTrace( true ) << std::endl; std::cerr << oss.str(); - std::_Exit( 1 ); + std::_Exit( ( sig > 0 ) ? ( 128 + sig ) : EXIT_FAILURE ); } +/// Signals that LvArray installs handlers for. +/// Do NOT include SIGKILL or SIGSTOP — they cannot be caught. +static constexpr int handledSignals[] = { +#ifdef SIGHUP + SIGHUP, +#endif +#ifdef SIGINT + SIGINT, +#endif +#ifdef SIGQUIT + SIGQUIT, +#endif +#ifdef SIGILL + SIGILL, +#endif +#ifdef SIGTRAP + SIGTRAP, +#endif +#ifdef SIGABRT + SIGABRT, +#endif +#ifdef SIGFPE + SIGFPE, +#endif +#ifdef SIGBUS + SIGBUS, +#endif +#ifdef SIGSEGV + SIGSEGV, +#endif +#ifdef SIGSYS + SIGSYS, +#endif +#ifdef SIGPIPE + SIGPIPE, +#endif +#ifdef SIGTERM + SIGTERM, +#endif +}; void setSignalHandling( void (* handler)( int, siginfo_t * info, void * ) ) { @@ -513,11 +563,11 @@ void setSignalHandling( void (* handler)( int, siginfo_t * info, void * ) ) sa.sa_flags = SA_SIGINFO; } - auto install = [&]( int sig ) + for( int sig : handledSignals ) { if( sig <= 0 || sig >= NSIG ) { - return; + continue; } if( g_oldActionSet[sig] ) @@ -530,100 +580,24 @@ void setSignalHandling( void (* handler)( int, siginfo_t * info, void * ) ) // First install: capture previous action for future resetSignalHandling(). g_oldActionSet[sig] = true; } - }; - -#ifdef SIGHUP - install( SIGHUP ); -#endif -#ifdef SIGINT - install( SIGINT ); -#endif -#ifdef SIGQUIT - install( SIGQUIT ); -#endif -#ifdef SIGILL - install( SIGILL ); -#endif -#ifdef SIGTRAP - install( SIGTRAP ); -#endif -#ifdef SIGABRT - install( SIGABRT ); -#endif -#ifdef SIGFPE - install( SIGFPE ); -#endif -#ifdef SIGBUS - install( SIGBUS ); -#endif -#ifdef SIGSEGV - install( SIGSEGV ); -#endif -#ifdef SIGSYS - install( SIGSYS ); -#endif -#ifdef SIGPIPE - install( SIGPIPE ); -#endif -#ifdef SIGTERM - install( SIGTERM ); -#endif - // Do NOT try SIGKILL/SIGSTOP: they can’t be caught. + } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void resetSignalHandling() { - auto restore = []( int sig ) + for( int sig : handledSignals ) { if( sig <= 0 || sig >= NSIG || !g_oldActionSet[sig] ) { - return; + continue; } sigaction( sig, &g_oldAction[sig], nullptr ); - }; - -#ifdef SIGHUP - restore( SIGHUP ); -#endif -#ifdef SIGINT - restore( SIGINT ); -#endif -#ifdef SIGQUIT - restore( SIGQUIT ); -#endif -#ifdef SIGILL - restore( SIGILL ); -#endif -#ifdef SIGTRAP - restore( SIGTRAP ); -#endif -#ifdef SIGABRT - restore( SIGABRT ); -#endif -#ifdef SIGFPE - restore( SIGFPE ); -#endif -#ifdef SIGBUS - restore( SIGBUS ); -#endif -#ifdef SIGSEGV - restore( SIGSEGV ); -#endif -#ifdef SIGSYS - restore( SIGSYS ); -#endif -#ifdef SIGPIPE - restore( SIGPIPE ); -#endif -#ifdef SIGTERM - restore( SIGTERM ); -#endif + } } - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int getDefaultFloatingPointExceptions() { @@ -631,12 +605,15 @@ int getDefaultFloatingPointExceptions() return ( FE_DIVBYZERO | FE_OVERFLOW | FE_INVALID ); } +/// Translate standard FE_* masks to platform-specific fpcr trap bits. +/// This is only needed on Apple arm64 where fpcr uses a different bit layout. +/// Linux (x86 and aarch64) uses feenableexcept/fedisableexcept directly. +#if defined(__APPLE__) && defined(__MACH__) && !defined(__x86_64__) static unsigned long long int translateFloatingPointException( unsigned long long int const exception ) { - unsigned long long int result = 0; -#if defined(__APPLE__) && defined(__MACH__) // if apple // Darwin arm64 stores trap masks in fpcr-specific bits (__fpcr_trap_*), // so FE_* must be translated before writing env.__fpcr. + unsigned long long int result = 0; if( exception & FE_INEXACT ) { result |= __fpcr_trap_inexact; @@ -657,15 +634,9 @@ static unsigned long long int translateFloatingPointException( unsigned long lon { result |= __fpcr_trap_invalid; } -#else // if not apple -#if defined(__x86_64__) || defined(__i386__) - // Linux x86 feenableexcept/fedisableexcept already use FE_* bit positions. - result = exception; -#endif -#endif - return result; } +#endif /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// int enableFloatingPointExceptions( int const exceptions ) diff --git a/src/system.hpp b/src/system.hpp index 6448819c..a407b987 100644 --- a/src/system.hpp +++ b/src/system.hpp @@ -18,8 +18,7 @@ #include #include #include - - +#include namespace LvArray { @@ -83,7 +82,6 @@ void callErrorHandler(); */ void signalHandler( int sig, siginfo_t * info, void * ucontext ); - /** * @brief Set the signal handler for common signals. * @param handler The signal handler. @@ -124,7 +122,7 @@ void setFPE(); /** * @class FloatingPointExceptionGuard - * @brief Changes the floating point environment and reverts it when destoyed. + * @brief Changes the floating point environment and reverts it when destroyed. */ class FloatingPointExceptionGuard { @@ -138,10 +136,16 @@ class FloatingPointExceptionGuard {} /** - * @brief Re-enable the floating point exceptions that were active on construction. + * @brief Clear stale FE status flags and re-enable the floating point exceptions + * that were active on construction. + * @details Clearing flags before re-enabling traps prevents spurious SIGFPE + * from FE flags accumulated by third-party libraries during the guarded scope. */ ~FloatingPointExceptionGuard() - { enableFloatingPointExceptions( m_previousExceptions ); } + { + std::feclearexcept( FE_ALL_EXCEPT ); + enableFloatingPointExceptions( m_previousExceptions ); + } private: /// The floating point exceptions that were active on construction. From c0651c6a9c9ab01215149ebff1106e61c0edb683 Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Sat, 2 May 2026 15:48:13 -0700 Subject: [PATCH 5/7] fix python on macos --- CMakeLists.txt | 11 ++++++++++- src/python/CMakeLists.txt | 2 ++ src/python/PyCRSMatrix.cpp | 9 +++++++++ unitTests/python/CMakeLists.txt | 2 ++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 869aeed0..d96c16b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,16 @@ include( cmake/Config.cmake ) set( lvarray_dependencies dl ) +if( ENABLE_PYLVARRAY ) + execute_process( COMMAND ${Python3_EXECUTABLE} -c "import scipy.sparse" + RESULT_VARIABLE scipy_sparse_result + ERROR_VARIABLE scipy_sparse_error + OUTPUT_QUIET ) + if( NOT scipy_sparse_result EQUAL 0 ) + message( FATAL_ERROR "ENABLE_PYLVARRAY requires scipy.sparse in ${Python3_EXECUTABLE}: ${scipy_sparse_error}" ) + endif() +endif() + blt_list_append( TO lvarray_dependencies ELEMENTS chai IF ENABLE_CHAI ) blt_list_append( TO lvarray_dependencies ELEMENTS RAJA ) @@ -117,4 +127,3 @@ if( ENABLE_DOCS ) add_subdirectory( docs ) endif() - diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 0528e703..817cdc63 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -31,6 +31,8 @@ blt_add_library( NAME pylvarray CLEAR_PREFIX TRUE ) +set_target_properties( pylvarray PROPERTIES SUFFIX "${CMAKE_SHARED_MODULE_SUFFIX}" ) + target_include_directories( pylvarray PUBLIC $ diff --git a/src/python/PyCRSMatrix.cpp b/src/python/PyCRSMatrix.cpp index 9b81a4b7..052294e7 100644 --- a/src/python/PyCRSMatrix.cpp +++ b/src/python/PyCRSMatrix.cpp @@ -177,7 +177,16 @@ static PyObject * PyCRSMatrix_toSciPy( PyCRSMatrix * const self, PyObject * cons "Error constructing the offsets NumPy array", nullptr ); PyObjectRef<> sciPySparse = PyImport_ImportModule( "scipy.sparse" ); + if( sciPySparse == nullptr ) + { + return nullptr; + } + PyObjectRef<> constructor = PyObject_GetAttrString( sciPySparse, "csr_matrix" ); + if( constructor == nullptr ) + { + return nullptr; + } return PyObject_CallFunction( constructor, "(OOO)(ll)", diff --git a/unitTests/python/CMakeLists.txt b/unitTests/python/CMakeLists.txt index f14913c6..d283db21 100644 --- a/unitTests/python/CMakeLists.txt +++ b/unitTests/python/CMakeLists.txt @@ -18,6 +18,8 @@ foreach(test ${pythonTests}) SHARED TRUE CLEAR_PREFIX TRUE ) + set_target_properties( ${test_name} PROPERTIES SUFFIX "${CMAKE_SHARED_MODULE_SUFFIX}" ) + target_include_directories( ${test_name} PUBLIC ${CMAKE_CURRENT_LIST_DIR}/../../src ) From 464fa08567c356ed4979109931878816bc521c77 Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Thu, 21 May 2026 23:49:33 -0700 Subject: [PATCH 6/7] better messaging --- src/system.cpp | 2 +- unitTests/testFloatingPointExceptions.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index 070e4f24..92183e5c 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -490,7 +490,7 @@ void signalHandler( int sig, siginfo_t * info, void * ucontext ) // Apple arm64 may report FP traps as SIGILL/ILL_ILLTRP instead of SIGFPE. // In that mode the subtype is not exposed, so we emit the best available text. oss << " SIGILL si_code = " << info->si_code - << " (floating-point trap, subtype unavailable on this platform)\n"; + << " (possible floating-point trap; subtype unavailable on this platform)\n"; } else #endif diff --git a/unitTests/testFloatingPointExceptions.cpp b/unitTests/testFloatingPointExceptions.cpp index 4af78478..6a4def09 100644 --- a/unitTests/testFloatingPointExceptions.cpp +++ b/unitTests/testFloatingPointExceptions.cpp @@ -21,11 +21,11 @@ using namespace testFloatingPointExceptionsHelpers; #if defined(__APPLE__) && defined(__MACH__) && defined(__aarch64__) char const DIVIDE_BY_ZERO_REGEX[] = - R"(((floating divide by zero)|(floating-point trap, subtype unavailable on this platform))(.|\n)*StackTrace)"; + R"(((floating divide by zero)|(possible floating-point trap; subtype unavailable on this platform))(.|\n)*StackTrace)"; char const OVERFLOW_REGEX[] = - R"(((floating overflow)|(floating-point trap, subtype unavailable on this platform))(.|\n)*StackTrace)"; + R"(((floating overflow)|(possible floating-point trap; subtype unavailable on this platform))(.|\n)*StackTrace)"; char const INVALID_REGEX[] = - R"(((floating invalid operation)|(floating-point trap, subtype unavailable on this platform))(.|\n)*StackTrace)"; + R"(((floating invalid operation)|(possible floating-point trap; subtype unavailable on this platform))(.|\n)*StackTrace)"; #else char const DIVIDE_BY_ZERO_REGEX[] = R"((floating divide by zero)(.|\n)*StackTrace)"; char const OVERFLOW_REGEX[] = R"((floating overflow)(.|\n)*StackTrace)"; From ea57a2671b88caa0f544fb24637faadfef1bb890 Mon Sep 17 00:00:00 2001 From: Randolph Settgast Date: Sat, 23 May 2026 11:21:37 -0700 Subject: [PATCH 7/7] Add query helper for enabled floating-point traps Expose system::queryEnabledFloatingPointExceptions() so callers can verify which FE_* exceptions are actually trap-enabled after changing the floating-point environment. Implement platform-specific handling for glibc, Apple arm64 FPCR, and Apple x86 fenv control words, allowing trapless FPUs to be detected when enable calls appear to succeed. Extend FPE tests to cover the query helper and disable optimization for FPE helper operations across all builds. --- src/system.cpp | 39 +++++++++++++++++++++++ src/system.hpp | 14 ++++++++ unitTests/CMakeLists.txt | 7 ++-- unitTests/testFloatingPointExceptions.cpp | 29 +++++++++++++++++ 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/system.cpp b/src/system.cpp index 92183e5c..b9ccd792 100644 --- a/src/system.cpp +++ b/src/system.cpp @@ -733,6 +733,45 @@ int disableFloatingPointExceptions( int const exceptions ) #endif } +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +int queryEnabledFloatingPointExceptions() +{ +#if defined(__APPLE__) && defined(__MACH__) +#if !defined(__x86_64__) + // Apple arm64: read FPCR trap-enable bits and translate back to FE_*. + // If the underlying FPU is trapless, the bits we previously wrote will read + // back as 0 here and the caller can warn the user. + fenv_t env; + if( fegetenv( &env ) ) + { + return 0; + } + + int enabled = 0; + if( env.__fpcr & __fpcr_trap_inexact ) enabled |= FE_INEXACT; + if( env.__fpcr & __fpcr_trap_underflow ) enabled |= FE_UNDERFLOW; + if( env.__fpcr & __fpcr_trap_overflow ) enabled |= FE_OVERFLOW; + if( env.__fpcr & __fpcr_trap_divbyzero ) enabled |= FE_DIVBYZERO; + if( env.__fpcr & __fpcr_trap_invalid ) enabled |= FE_INVALID; + return enabled; +#else + // Apple x86: a bit SET in fenv.__control means the exception is masked + // (disabled). The enabled set is the bitwise inverse, restricted to + // FE_ALL_EXCEPT. + fenv_t env; + if( fegetenv( &env ) ) + { + return 0; + } + return ( ~env.__control ) & FE_ALL_EXCEPT; +#endif +#else + // Linux/glibc: native API. fegetexcept returns -1 on error; treat as none. + int const enabled = fegetexcept(); + return ( enabled < 0 ) ? 0 : enabled; +#endif +} + static void enableFlushDenormalsToZero() { // Flushing denormals prevents very slow subnormal arithmetic paths on many CPUs. diff --git a/src/system.hpp b/src/system.hpp index a407b987..86738136 100644 --- a/src/system.hpp +++ b/src/system.hpp @@ -113,6 +113,20 @@ int enableFloatingPointExceptions( int const exceptions = getDefaultFloatingPoin */ int disableFloatingPointExceptions( int const exceptions = getDefaultFloatingPointExceptions() ); +/** + * @brief Query which floating-point exceptions are currently set to trap. + * @return A bitmask of @c FE_* values that are currently trap-enabled. A return + * of 0 means no exceptions will trap on this thread — including the + * case of a trapless hardware FPU (some aarch64 implementations) where + * enableFloatingPointExceptions() appeared to succeed but the hardware + * did not honor the request. + * @note On glibc this calls @c fegetexcept(). On Apple arm64 it reads FPCR + * trap-enable bits and translates back to @c FE_* values. On Apple x86 + * it reads the SSE/x87 control word via @c fenv_t (where SET bits mean + * masked/disabled, so the enabled set is the bitwise inverse). + */ +int queryEnabledFloatingPointExceptions(); + /** * @brief Sets the floating point environment. * @details Sets the floating point environment such that FE_DIVBYZERO, FE_OVERFLOW diff --git a/unitTests/CMakeLists.txt b/unitTests/CMakeLists.txt index 3f5bb961..88f59d34 100644 --- a/unitTests/CMakeLists.txt +++ b/unitTests/CMakeLists.txt @@ -106,10 +106,9 @@ blt_add_executable( NAME testFloatingPointExceptions OUTPUT_DIR ${TEST_OUTPUT_DIRECTORY} DEPENDS_ON gtest lvarray ${lvarray_dependencies} ) -# Need to avoid optimization to catch invalid operations -if( APPLE AND ${CMAKE_CXX_COMPILER} MATCHES "clang" ) - set_source_files_properties( testFloatingPointExceptionsHelpers.cpp PROPERTIES COMPILE_FLAGS "-O0" ) -endif() +# Disable optimization on the helpers so cross-TU folding (especially under LTO) +# cannot turn the trapping operations into compile-time constants. +set_source_files_properties( testFloatingPointExceptionsHelpers.cpp PROPERTIES COMPILE_FLAGS "-O0" ) target_include_directories( testFloatingPointExceptions PUBLIC ${CMAKE_CURRENT_LIST_DIR}/../src ) diff --git a/unitTests/testFloatingPointExceptions.cpp b/unitTests/testFloatingPointExceptions.cpp index 6a4def09..a092f3b9 100644 --- a/unitTests/testFloatingPointExceptions.cpp +++ b/unitTests/testFloatingPointExceptions.cpp @@ -62,6 +62,35 @@ TEST( TestFloatingPointEnvironment, Invalid ) EXPECT_DEATH_IF_SUPPORTED( invalid(), INVALID_REGEX ); } +TEST( TestFloatingPointEnvironment, QueryReflectsSetFPE ) +{ + // setFPE() enables the default FE_* set. queryEnabledFloatingPointExceptions() + // should report at least those bits back. If the hardware is trapless, the + // bits we wrote will read back as zero — we surface that as a skip rather + // than a failure. + system::setFPE(); + int const wanted = system::getDefaultFloatingPointExceptions(); + int const active = system::queryEnabledFloatingPointExceptions(); + + if( ( active & wanted ) != wanted ) + { + GTEST_SKIP() << "Hardware FPU appears trapless (wanted mask=0x" << std::hex << wanted + << ", active mask=0x" << active << ")."; + } + + EXPECT_EQ( active & wanted, wanted ); +} + +TEST( TestFloatingPointEnvironment, QueryAfterDisableIsEmpty ) +{ + system::setFPE(); + system::disableFloatingPointExceptions( system::getDefaultFloatingPointExceptions() ); + int const active = system::queryEnabledFloatingPointExceptions(); + EXPECT_EQ( active & system::getDefaultFloatingPointExceptions(), 0 ); + // Restore for subsequent tests in the same process. + system::setFPE(); +} + TEST( TestFloatingPointEnvironment, FloatingPointExceptionGuard ) { system::setFPE();