Hi Mohammad, I wrote: > I'll work on a strtof, strtod, strtold override...
Done: 2026-05-02 Bruno Haible <[email protected]> strtof, strtod, strtold: Return NaNs with correct sign. Reported by Mohammad-Reza Nabipoor <[email protected]> in <https://lists.gnu.org/archive/html/bug-gnulib/2026-04/msg00136.html>. * m4/strtof.m4 (gl_FUNC_STRTOF): Add test whether strtod works on signed NaNs. * m4/strtod.m4 (gl_FUNC_STRTOD): Likewise. * m4/strtold.m4 (gl_FUNC_STRTOLD): Likewise. * lib/strtod.c: Include isnan?-nolibm.h. (HAS_MINUS_NAN_BUG): New macro. (STRTOD): Add separate implementation for HAS_MINUS_NAN_BUG. * modules/strtof (Files): Add m4/signbit.m4. (Depends-on): Add isnanf-nolibm, signbit-no-c++. * modules/strtod (Files): Add m4/signbit.m4. (Depends-on): Add isnand-nolibm, signbit-no-c++. * modules/strtold (Files): Add m4/signbit.m4. (Depends-on): Add isnanl-nolibm, signbit-no-c++. * tests/test-strtof.h (test_function): Enable the test of sign bits of NaN. * tests/test-strtod.h (test_function): Likewise. * tests/test-strtold.h (test_function): Likewise. * doc/posix-functions/strtof.texi: Document the bug regarding "-nan". * doc/posix-functions/strtod.texi: Likewise. * doc/posix-functions/strtold.texi: Likewise. diff --git a/doc/posix-functions/strtod.texi b/doc/posix-functions/strtod.texi index c929fa8639..03b4d0e125 100644 --- a/doc/posix-functions/strtod.texi +++ b/doc/posix-functions/strtod.texi @@ -49,6 +49,10 @@ This function misparses @samp{nan(} on some platforms: macOS 10.6.6. +@item +This function returns @samp{NaN} for @samp{-NaN} on some platforms: +glibc 2.27, musl libc. + @item This function fails to parse C99 hexadecimal floating point on some platforms: diff --git a/doc/posix-functions/strtof.texi b/doc/posix-functions/strtof.texi index 9caa74d58d..e2405883fe 100644 --- a/doc/posix-functions/strtof.texi +++ b/doc/posix-functions/strtof.texi @@ -50,6 +50,10 @@ platforms: glibc 2.7, mingw, MSVC 14. +@item +This function returns @samp{NaN} for @samp{-NaN} on some platforms: +glibc 2.27, musl libc. + @item This function fails to correctly parse very long strings on some platforms: diff --git a/doc/posix-functions/strtold.texi b/doc/posix-functions/strtold.texi index 555c78e955..fd481748c0 100644 --- a/doc/posix-functions/strtold.texi +++ b/doc/posix-functions/strtold.texi @@ -44,6 +44,10 @@ the wrong end pointer on some platforms: glibc-2.3.2, mingw, Haiku. +@item +This function returns @samp{NaN} for @samp{-NaN} on some platforms: +glibc 2.27, musl libc. + @item This function fails to parse C99 hexadecimal floating point on some platforms: diff --git a/lib/strtod.c b/lib/strtod.c index 438b65c1a8..3bdf2c5e9a 100644 --- a/lib/strtod.c +++ b/lib/strtod.c @@ -35,6 +35,14 @@ #include "c-ctype.h" +#if defined USE_FLOAT +# include "isnanf-nolibm.h" +#elif defined USE_LONG_DOUBLE +# include "isnanl-nolibm.h" +#else +# include "isnand-nolibm.h" +#endif + #undef MIN #undef MAX #if defined USE_FLOAT @@ -47,6 +55,7 @@ # define HAVE_UNDERLYING_STRTOD HAVE_STRTOF # endif # define HAS_GRADUAL_UNDERFLOW_PROBLEM STRTOF_HAS_GRADUAL_UNDERFLOW_PROBLEM +# define HAS_MINUS_NAN_BUG STRTOF_HAS_MINUS_NAN_BUG # define DOUBLE float # define MIN FLT_MIN # define MAX FLT_MAX @@ -81,6 +90,7 @@ # define HAVE_UNDERLYING_STRTOD HAVE_STRTOLD # endif # define HAS_GRADUAL_UNDERFLOW_PROBLEM STRTOLD_HAS_GRADUAL_UNDERFLOW_PROBLEM +# define HAS_MINUS_NAN_BUG STRTOLD_HAS_MINUS_NAN_BUG # define DOUBLE long double # define MIN LDBL_MIN # define MAX LDBL_MAX @@ -100,6 +110,7 @@ # define HAVE_UNDERLYING_STRTOD 1 # endif # define HAS_GRADUAL_UNDERFLOW_PROBLEM STRTOD_HAS_GRADUAL_UNDERFLOW_PROBLEM +# define HAS_MINUS_NAN_BUG STRTOD_HAS_MINUS_NAN_BUG # define DOUBLE double # define MIN DBL_MIN # define MAX DBL_MAX @@ -120,6 +131,40 @@ locale_isspace (char c) return isspace (uc) != 0; } +#if HAS_MINUS_NAN_BUG + +/* The underlying implementation works fine, except for signed NaNs. */ + +DOUBLE +STRTOD (const char *nptr, char **endptr) +# undef STRTOD +# if defined USE_FLOAT +# undef strtof +# define STRTOD strtof +# define ISNAN isnanf +# elif defined USE_LONG_DOUBLE +# undef strtold +# define STRTOD strtold +# define ISNAN isnanl +# else +# undef strtod +# define STRTOD strtod +# define ISNAN isnand +# endif +{ + DOUBLE value = STRTOD (nptr, endptr); + if (ISNAN (value)) + { + while (locale_isspace (*nptr)) + nptr++; + if (*nptr == '-' && !signbit (value)) + value = - value; + } + return value; +} + +#else + /* Determine the decimal-point character according to the current locale. */ static char decimal_point_char (void) @@ -129,20 +174,20 @@ decimal_point_char (void) thread-safe on glibc systems and Mac OS X systems, but is not required to be thread-safe by POSIX. sprintf(), however, is thread-safe. localeconv() is rarely thread-safe. */ -#if HAVE_NL_LANGINFO && (__GLIBC__ || defined __UCLIBC__ || (defined __APPLE__ && defined __MACH__)) +# if HAVE_NL_LANGINFO && (__GLIBC__ || defined __UCLIBC__ || (defined __APPLE__ && defined __MACH__)) point = nl_langinfo (RADIXCHAR); -#elif 1 +# elif 1 char pointbuf[5]; sprintf (pointbuf, "%#.0f", 1.0); point = &pointbuf[1]; -#else +# else point = localeconv () -> decimal_point; -#endif +# endif /* The decimal point is always a single byte: either '.' or ','. */ return (point[0] != '\0' ? point[0] : '.'); } -#if !USE_LDEXP +# if !USE_LDEXP #undef LDEXP #define LDEXP dummy_ldexp /* A dummy definition that will never be invoked. */ @@ -151,7 +196,7 @@ decimal_point_char (void) abort (); return L_(0.0); } -#endif +# endif /* Return X * BASE**EXPONENT. Return an extreme value and set errno to ERANGE if underflow or overflow occurs. */ @@ -344,42 +389,42 @@ parse_number (const char *nptr, static DOUBLE minus_zero (void) { -#if defined __hpux || defined __ICC +# if defined __hpux || defined __ICC return -MIN * MIN; -#else +# else return -0.0; -#endif +# endif } /* Convert NPTR to a DOUBLE. If ENDPTR is not NULL, a pointer to the character after the last one used in the number is put in *ENDPTR. */ DOUBLE STRTOD (const char *nptr, char **endptr) -#if HAVE_UNDERLYING_STRTOD -# if defined USE_FLOAT -# undef strtof -# elif defined USE_LONG_DOUBLE -# undef strtold -# else -# undef strtod -# endif -# if HAS_GRADUAL_UNDERFLOW_PROBLEM -# define SET_ERRNO_UPON_GRADUAL_UNDERFLOW(RESULT) \ - do \ - { \ - if ((RESULT) != 0 && (RESULT) < MIN && (RESULT) > -MIN) \ - errno = ERANGE; \ - } \ - while (0) +# if HAVE_UNDERLYING_STRTOD +# if defined USE_FLOAT +# undef strtof +# elif defined USE_LONG_DOUBLE +# undef strtold +# else +# undef strtod +# endif +# if HAS_GRADUAL_UNDERFLOW_PROBLEM +# define SET_ERRNO_UPON_GRADUAL_UNDERFLOW(RESULT) \ + do \ + { \ + if ((RESULT) != 0 && (RESULT) < MIN && (RESULT) > -MIN) \ + errno = ERANGE; \ + } \ + while (0) +# else +# define SET_ERRNO_UPON_GRADUAL_UNDERFLOW(RESULT) (void)0 +# endif # else +# undef STRTOD +# define STRTOD(NPTR,ENDPTR) \ + parse_number (NPTR, 10, 10, 1, radixchar, 'e', ENDPTR) # define SET_ERRNO_UPON_GRADUAL_UNDERFLOW(RESULT) (void)0 # endif -#else -# undef STRTOD -# define STRTOD(NPTR,ENDPTR) \ - parse_number (NPTR, 10, 10, 1, radixchar, 'e', ENDPTR) -# define SET_ERRNO_UPON_GRADUAL_UNDERFLOW(RESULT) (void)0 -#endif /* From here on, STRTOD refers to the underlying implementation. It needs to handle only finite unsigned decimal numbers with non-null ENDPTR. */ { @@ -544,3 +589,5 @@ STRTOD (const char *nptr, char **endptr) return minus_zero (); return negative ? -num : num; } + +#endif diff --git a/m4/strtod.m4 b/m4/strtod.m4 index 520018d9f1..629f48822d 100644 --- a/m4/strtod.m4 +++ b/m4/strtod.m4 @@ -1,5 +1,5 @@ # strtod.m4 -# serial 32 +# serial 33 dnl Copyright (C) 2002-2003, 2006-2026 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -179,6 +179,48 @@ AC_DEFUN([gl_FUNC_STRTOD] esac ;; esac + if test $REPLACE_STRTOD = 0; then + gl_DOUBLE_SIGN_LOCATION + AC_CACHE_CHECK([whether strtod works on signed NaNs], + [gl_cv_func_strtod_nan_works], + [AC_RUN_IFELSE([AC_LANG_PROGRAM([[ +#include <stdlib.h> +]], [[ + int result = 0; + # define NWORDS \ + ((sizeof (double) + sizeof (unsigned int) - 1) / sizeof (unsigned int)) + union { double value; unsigned int word[NWORDS]; } m; + { + /* On glibc <= 2.27 and Alpine Linux, strtod("-nan") returns a NaN with a + wrong sign bit. */ + const char *string = "-nan"; + char *term; + m.value = strtod (string, &term); + if (((m.word[DBL_SIGNBIT_WORD] >> DBL_SIGNBIT_BIT) & 1) == 0) + result |= 1; + } + return result; +]])], + [gl_cv_func_strtod_nan_works=yes], + [gl_cv_func_strtod_nan_works=no], + [case "$host_os" in + # Guess no on glibc systems. + *-gnu* | gnu*) gl_cv_func_strtod_nan_works="guessing no" ;; + # Guess no on musl systems. + *-musl* | midipix*) gl_cv_func_strtod_nan_works="guessing no" ;; + *) gl_cv_func_strtod_nan_works="$gl_cross_guess_normal" ;; + esac + ]) + ]) + case "$gl_cv_func_strtod_nan_works" in + *yes) ;; + *) + REPLACE_STRTOD=1 + AC_DEFINE([STRTOD_HAS_MINUS_NAN_BUG], [1], + [Define to 1 if strtod may return a NaN with a wrong sign bit.]) + ;; + esac + fi fi ]) diff --git a/m4/strtof.m4 b/m4/strtof.m4 index 284900a983..da2df20df2 100644 --- a/m4/strtof.m4 +++ b/m4/strtof.m4 @@ -1,5 +1,5 @@ # strtof.m4 -# serial 5 +# serial 6 dnl Copyright (C) 2002-2003, 2006-2026 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -192,6 +192,48 @@ AC_DEFUN([gl_FUNC_STRTOF] esac ;; esac + if test $REPLACE_STRTOF = 0; then + gl_FLOAT_SIGN_LOCATION + AC_CACHE_CHECK([whether strtof works on signed NaNs], + [gl_cv_func_strtof_nan_works], + [AC_RUN_IFELSE([AC_LANG_PROGRAM([[ +#include <stdlib.h> +]], [[ + int result = 0; + # define NWORDS \ + ((sizeof (float) + sizeof (unsigned int) - 1) / sizeof (unsigned int)) + union { float value; unsigned int word[NWORDS]; } m; + { + /* On glibc <= 2.27 and Alpine Linux, strtof("-nan") returns a NaN with a + wrong sign bit. */ + const char *string = "-nan"; + char *term; + m.value = strtof (string, &term); + if (((m.word[FLT_SIGNBIT_WORD] >> FLT_SIGNBIT_BIT) & 1) == 0) + result |= 1; + } + return result; +]])], + [gl_cv_func_strtof_nan_works=yes], + [gl_cv_func_strtof_nan_works=no], + [case "$host_os" in + # Guess no on glibc systems. + *-gnu* | gnu*) gl_cv_func_strtof_nan_works="guessing no" ;; + # Guess no on musl systems. + *-musl* | midipix*) gl_cv_func_strtof_nan_works="guessing no" ;; + *) gl_cv_func_strtof_nan_works="$gl_cross_guess_normal" ;; + esac + ]) + ]) + case "$gl_cv_func_strtof_nan_works" in + *yes) ;; + *) + REPLACE_STRTOF=1 + AC_DEFINE([STRTOF_HAS_MINUS_NAN_BUG], [1], + [Define to 1 if strtof may return a NaN with a wrong sign bit.]) + ;; + esac + fi fi ]) diff --git a/m4/strtold.m4 b/m4/strtold.m4 index b21a186cbd..f74fe4c5c1 100644 --- a/m4/strtold.m4 +++ b/m4/strtold.m4 @@ -1,5 +1,5 @@ # strtold.m4 -# serial 11 +# serial 12 dnl Copyright (C) 2002-2003, 2006-2026 Free Software Foundation, Inc. dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, @@ -169,6 +169,48 @@ AC_DEFUN([gl_FUNC_STRTOLD] esac ;; esac + if test $REPLACE_STRTOLD = 0; then + gl_LONG_DOUBLE_SIGN_LOCATION + AC_CACHE_CHECK([whether strtold works on signed NaNs], + [gl_cv_func_strtold_nan_works], + [AC_RUN_IFELSE([AC_LANG_PROGRAM([[ +#include <stdlib.h> +]], [[ + int result = 0; + # define NWORDS \ + ((sizeof (long double) + sizeof (unsigned int) - 1) / sizeof (unsigned int)) + union { long double value; unsigned int word[NWORDS]; } m; + { + /* On glibc <= 2.27 and Alpine Linux, strtold("-nan") returns a NaN with a + wrong sign bit. */ + const char *string = "-nan"; + char *term; + m.value = strtold (string, &term); + if (((m.word[LDBL_SIGNBIT_WORD] >> LDBL_SIGNBIT_BIT) & 1) == 0) + result |= 1; + } + return result; +]])], + [gl_cv_func_strtold_nan_works=yes], + [gl_cv_func_strtold_nan_works=no], + [case "$host_os" in + # Guess no on glibc systems. + *-gnu* | gnu*) gl_cv_func_strtold_nan_works="guessing no" ;; + # Guess no on musl systems. + *-musl* | midipix*) gl_cv_func_strtold_nan_works="guessing no" ;; + *) gl_cv_func_strtold_nan_works="$gl_cross_guess_normal" ;; + esac + ]) + ]) + case "$gl_cv_func_strtold_nan_works" in + *yes) ;; + *) + REPLACE_STRTOLD=1 + AC_DEFINE([STRTOLD_HAS_MINUS_NAN_BUG], [1], + [Define to 1 if strtold may return a NaN with a wrong sign bit.]) + ;; + esac + fi fi ]) diff --git a/modules/strtod b/modules/strtod index e1353bc0ad..9d184c6465 100644 --- a/modules/strtod +++ b/modules/strtod @@ -4,6 +4,7 @@ strtod() function: convert string to 'double'. Files: lib/strtod.c m4/strtod.m4 +m4/signbit.m4 m4/ldexp.m4 Depends-on: @@ -13,6 +14,8 @@ c-ctype [test $HAVE_STRTOD = 0 || test $REPLACE_STRTOD = 1] math-h [test $HAVE_STRTOD = 0 || test $REPLACE_STRTOD = 1] bool [test $HAVE_STRTOD = 0 || test $REPLACE_STRTOD = 1] isinf-no-c++ [test $HAVE_STRTOD = 0 || test $REPLACE_STRTOD = 1] +isnand-nolibm [test $REPLACE_STRTOD = 1] +signbit-no-c++ [test $REPLACE_STRTOD = 1] configure.ac: gl_FUNC_STRTOD diff --git a/modules/strtof b/modules/strtof index 4850dc30ae..460e799e48 100644 --- a/modules/strtof +++ b/modules/strtof @@ -5,6 +5,7 @@ Files: lib/strtof.c lib/strtod.c m4/strtof.m4 +m4/signbit.m4 m4/ldexpf.m4 Depends-on: @@ -13,6 +14,8 @@ c-ctype [test $HAVE_STRTOF = 0 || test $REPLACE_STRTOF = 1] math-h [test $HAVE_STRTOF = 0 || test $REPLACE_STRTOF = 1] bool [test $HAVE_STRTOF = 0 || test $REPLACE_STRTOF = 1] isinf-no-c++ [test $HAVE_STRTOF = 0 || test $REPLACE_STRTOF = 1] +isnanf-nolibm [test $REPLACE_STRTOF = 1] +signbit-no-c++ [test $REPLACE_STRTOF = 1] configure.ac: gl_FUNC_STRTOF diff --git a/modules/strtold b/modules/strtold index 50b9ee626b..c9468d269e 100644 --- a/modules/strtold +++ b/modules/strtold @@ -6,6 +6,7 @@ lib/strtold.c lib/strtod.c m4/strtold.m4 m4/math_h.m4 +m4/signbit.m4 m4/ldexpl.m4 Depends-on: @@ -14,6 +15,8 @@ c-ctype [test $HAVE_STRTOLD = 0 || test $REPLACE_STRTOLD = 1] math-h [test $HAVE_STRTOLD = 0 || test $REPLACE_STRTOLD = 1] bool [test $HAVE_STRTOLD = 0 || test $REPLACE_STRTOLD = 1] isinf-no-c++ [test $HAVE_STRTOLD = 0 || test $REPLACE_STRTOLD = 1] +isnanl-nolibm [test $REPLACE_STRTOLD = 1] +signbit-no-c++ [test $REPLACE_STRTOLD = 1] strtod [{ test $HAVE_STRTOLD = 0 || test $REPLACE_STRTOLD = 1; } && test $HAVE_SAME_LONG_DOUBLE_AS_DOUBLE = 1] configure.ac: diff --git a/tests/test-strtod.h b/tests/test-strtod.h index d206fce2e0..ac11f280b5 100644 --- a/tests/test-strtod.h +++ b/tests/test-strtod.h @@ -661,11 +661,8 @@ test_function (double (*my_strtod) (const char *, char **)) #if 1 /* All known CPUs support NaNs. */ ASSERT (isnand (result1)); /* OpenBSD 4.0, mingw */ ASSERT (isnand (result2)); /* OpenBSD 4.0, mingw */ -# if 0 - /* Sign bits of NaN is a portability sticking point, not worth - worrying about. */ - ASSERT (!!signbit (result1) != !!signbit (result2)); /* glibc-2.3.6, mingw */ -# endif + /* Sign bits of NaN are particularly hairy. */ + ASSERT (!!signbit (result1) != !!signbit (result2)); /* glibc-2.27, mingw, musl libc */ ASSERT (ptr1 == input + 4); /* OpenBSD 4.0, Solaris 2.5.1, mingw */ ASSERT (ptr2 == input + 4); /* OpenBSD 4.0, Solaris 2.5.1, mingw */ ASSERT (errno == 0); /* HP-UX 11.11 */ diff --git a/tests/test-strtof.h b/tests/test-strtof.h index 2adc03e970..6fd95478ad 100644 --- a/tests/test-strtof.h +++ b/tests/test-strtof.h @@ -661,11 +661,8 @@ test_function (float (*my_strtof) (const char *, char **)) #if 1 /* All known CPUs support NaNs. */ ASSERT (isnanf (result1)); /* OpenBSD 4.0, mingw */ ASSERT (isnanf (result2)); /* OpenBSD 4.0, mingw */ -# if 0 - /* Sign bits of NaN is a portability sticking point, not worth - worrying about. */ - ASSERT (!!signbit (result1) != !!signbit (result2)); /* glibc-2.3.6, mingw */ -# endif + /* Sign bits of NaN are particularly hairy. */ + ASSERT (!!signbit (result1) != !!signbit (result2)); /* glibc-2.27, mingw, musl libc */ ASSERT (ptr1 == input + 4); /* OpenBSD 4.0, Solaris 2.5.1, mingw */ ASSERT (ptr2 == input + 4); /* OpenBSD 4.0, Solaris 2.5.1, mingw */ ASSERT (errno == 0); /* HP-UX 11.11 */ diff --git a/tests/test-strtold.h b/tests/test-strtold.h index 5c8d88c8a4..9fa4bc965f 100644 --- a/tests/test-strtold.h +++ b/tests/test-strtold.h @@ -669,11 +669,8 @@ test_function (long double (*my_strtold) (const char *, char **)) #if 1 /* All known CPUs support NaNs. */ ASSERT (isnanl (result1)); ASSERT (isnanl (result2)); -# if 0 - /* Sign bits of NaN is a portability sticking point, not worth - worrying about. */ - ASSERT (!!signbit (result1) != !!signbit (result2)); -# endif + /* Sign bits of NaN are particularly hairy. */ + ASSERT (!!signbit (result1) != !!signbit (result2)); /* glibc-2.27, mingw, musl libc */ ASSERT (ptr1 == input + 4); ASSERT (ptr2 == input + 4); ASSERT (errno == 0); /* HP-UX 11.31/ia64 */
