Subject: bug#54785: for floating point, printf should use double like in C instead of long double

From: Paul Eggert <[email-masked]>
Date: Sat, 9 Apr 2022 12:31:24 -0700

Vincent Lefevre wrote in <https://bugs.gnu.org/54785>:

> $ zsh -fc '/usr/bin/printf "%a\n" $((43./2**22))'
> 0xa.c0000000000025cp-20
> 
> instead of
> 
> 0xa.cp-20

To summarize, this test case is:

printf '%a\n' 1.0251998901367188e-05

and the problem is that converting 1.0251998901367188e-05 to long double 
prints the too-precise "0xa.c0000000000025cp-20", whereas you want it to 
convert to double (which matches what most other programs do) and to 
print "0xa.cp-20" or equivalent.

> (Note that ksh uses long double internally, but does not ensure the
> round trip back to long double

Yes, ksh messes up here. However, it's more important for Coreutils 
printf to be compatible with the GNU shell, and Bash uses long double:

$ echo $BASH_VERSION
5.1.8(1)-release
$ /usr/bin/printf --version | head -n1
printf (GNU coreutils) 8.32
$ printf '%a\n' 1.0251998901367188e-05
0xa.c0000000000025cp-20
$ /usr/bin/printf '%a\n' 1.0251998901367188e-05
0xa.c0000000000025cp-20

> I suggest to parse the argument as a "long double" only if the "L"
> length modifier is provided, like in C.
Thanks, good idea.

I checked, and this also appears to be a POSIX conformance issue. POSIX 
  says that floating point operands "shall be evaluated as if by the 
strtod() function". This means double, not long double.

Whatever decision we make here, we should be consistent with Bash so 
I'll cc this email to bug-bash.

I propose that we change both coreutils and Bash to use 'double' rather 
than 'long double' here, unless the user specifies the L modifier (e.g., 
"printf '%La\n' ...". I've written up a patch (attached) to Bash 5.2 
alpha to do that. Assuming the Bash maintainer likes this proposal, I 
plan to implement something similar for Coreutils printf.

diff '-x*~' -pru bash-5.2-alpha/builtins/printf.def bash-5.2-alpha-double/builtins/printf.def
--- bash-5.2-alpha/builtins/printf.def	2021-12-29 13:09:20.000000000 -0800
+++ bash-5.2-alpha-double/builtins/printf.def	2022-04-09 12:02:35.330476097 -0700
@@ -215,13 +215,14 @@ static uintmax_t getuintmax PARAMS((void
 
 #if defined (HAVE_LONG_DOUBLE) && HAVE_DECL_STRTOLD && !defined(STRTOLD_BROKEN)
 typedef long double floatmax_t;
-#  define FLOATMAX_CONV	"L"
+#  define USE_LONG_DOUBLE 1
 #  define strtofltmax	strtold
 #else
 typedef double floatmax_t;
-#  define FLOATMAX_CONV	""
+#  define USE_LONG_DOUBLE 0
 #  define strtofltmax	strtod
 #endif
+static double getdouble PARAMS((void));
 static floatmax_t getfloatmax PARAMS((void));
 
 static intmax_t asciicode PARAMS((void));
@@ -247,7 +248,7 @@ printf_builtin (list)
      WORD_LIST *list;
 {
   int ch, fieldwidth, precision;
-  int have_fieldwidth, have_precision;
+  int have_fieldwidth, have_precision, use_Lmod;
   char convch, thisch, nextch, *format, *modstart, *precstart, *fmt, *start;
 #if defined (HANDLE_MULTIBYTE)
   char mbch[25];		/* 25 > MB_LEN_MAX, plus can handle 4-byte UTF-8 and large Unicode characters*/
@@ -422,8 +423,12 @@ printf_builtin (list)
 
 	  /* skip possible format modifiers */
 	  modstart = fmt;
+	  use_Lmod = 0;
 	  while (*fmt && strchr (LENMODS, *fmt))
-	    fmt++;
+	    {
+	      use_Lmod |= USE_LONG_DOUBLE && *fmt == 'L';
+	      fmt++;
+	    }
 	    
 	  if (*fmt == 0)
 	    {
@@ -694,11 +699,24 @@ printf_builtin (list)
 #endif
 	      {
 		char *f;
-		floatmax_t p;
 
-		p = getfloatmax ();
-		f = mklong (start, FLOATMAX_CONV, sizeof(FLOATMAX_CONV) - 1);
-		PF (f, p);
+		if (use_Lmod)
+		  {
+		    floatmax_t p;
+
+		    p = getfloatmax ();
+		    f = mklong (start, "L", 1);
+		    PF (f, p);
+		  }
+		else
+		  {
+		    double p;
+
+		    p = getdouble ();
+		    f = mklong (start, "", 0);
+		    PF (f, p);
+		  }
+
 		break;
 	      }
 
@@ -1248,35 +1266,40 @@ getuintmax ()
   return (ret);
 }
 
+#define getfloat(ret, convert) \
+  char *ep; \
+  if (garglist == 0) \
+    return 0; \
+  if (garglist->word->word[0] == '\'' || garglist->word->word[0] == '"') \
+    return asciicode (); \
+  errno = 0; \
+  (ret) = (convert) (garglist->word->word, &ep); \
+  if (*ep) \
+    { \
+      sh_invalidnum (garglist->word->word); \
+      /* Same thing about POSIX.2 conversion error requirements. */ \
+      if (0) \
+        (ret) = 0; \
+      conversion_error = 1; \
+    } \
+  else if (errno == ERANGE) \
+    printf_erange (garglist->word->word); \
+  garglist = garglist->next
+
+static double
+getdouble ()
+{
+  double ret;
+  getfloat (ret, strtod);
+  return ret;
+}
+
 static floatmax_t
 getfloatmax ()
 {
   floatmax_t ret;
-  char *ep;
-
-  if (garglist == 0)
-    return (0);
-
-  if (garglist->word->word[0] == '\'' || garglist->word->word[0] == '"')
-    return asciicode ();
-
-  errno = 0;
-  ret = strtofltmax (garglist->word->word, &ep);
-
-  if (*ep)
-    {
-      sh_invalidnum (garglist->word->word);
-#if 0
-      /* Same thing about POSIX.2 conversion error requirements. */
-      ret = 0;
-#endif
-      conversion_error = 1;
-    }
-  else if (errno == ERANGE)
-    printf_erange (garglist->word->word);
-
-  garglist = garglist->next;
-  return (ret);
+  getfloat (ret, strtofltmax);
+  return ret;
 }
 
 /* NO check is needed for garglist here. */
diff '-x*~' -pru bash-5.2-alpha/CHANGES bash-5.2-alpha-double/CHANGES
--- bash-5.2-alpha/CHANGES	2022-01-10 14:22:14.000000000 -0800
+++ bash-5.2-alpha-double/CHANGES	2022-04-09 11:58:36.473880401 -0700
@@ -142,6 +142,9 @@ uu. Fixed a problem with quoting shell e
     they appear in a tab-completed word along with characters that do need
     quoting (e.g.. $HOME/VirtualBox VMs).
 
+vv. The printf builtin now ordinarily uses 'double' for floating point
+    formats like %g.  Use formats like %Lg for 'long double'.
+
 2. Changes to Readline
 
 a. Fixed a problem with cleaning up active marks when using callback mode.