Subject: [PATCH] %o and %x now format signed numbers

From: Paul Eggert <[email-masked]>
Date: Mon, 23 Jul 2018 12:12:50 -0700

Treat integers as signed numbers when using the %o
and %x format specifiers, instead of treating them as
a machine-dependent two’s complement representation.
This should make Emacs more machine-independent, and
better-insulated for future changes involving bignums.
The new compatibility variable ‘binary-as-unsigned’
enables the old machine-dependent behavior.
This is a simplified version of the change proposed in:
https://lists.gnu.org/r/emacs-devel/2018-07/msg00763.html
I simplified that proposal by omitting bitwidth modifiers, as
I could not find an any example uses in the Emacs source code
that needed them and doing them correctly would have been
quite a bit more work for apparently little benefit.
* doc/lispref/strings.texi (Formatting Strings):
* etc/NEWS: Document change.
* src/editfns.c (styled_format): Treat integers as signed
numbers even with %o and %x.  Support the + and space
flags with %o and %x, since they’re about signs.
(syms_of_editfns): New compatibility variable binary-as-signed.
* test/src/editfns-tests.el (read-large-integer):
Test that maximal integers can be read after printing
with all integer formats.
(format-%o-invalid-float): Adjust test to match new
behavior, which allows negative octal.
---
 doc/lispref/strings.texi  |  6 +++---
 etc/NEWS                  |  8 ++++++++
 src/editfns.c             | 42 ++++++++++++++++++++++++++++++++++-----
 test/src/editfns-tests.el | 11 +++++-----
 4 files changed, 54 insertions(+), 13 deletions(-)

diff --git a/doc/lispref/strings.texi b/doc/lispref/strings.texi
index 2fff3c7c75..582561fe42 100644
--- a/doc/lispref/strings.texi
+++ b/doc/lispref/strings.texi
@@ -922,7 +922,7 @@ Formatting Strings
 @item %o
 @cindex integer to octal
 Replace the specification with the base-eight representation of an
-unsigned integer.  The object can also be a nonnegative floating-point
+integer.  The object can also be a floating-point
 number that is formatted as an integer, dropping any fraction, if the
 integer does not exceed machine limits.
 
@@ -935,8 +935,8 @@ Formatting Strings
 @itemx %X
 @cindex integer to hexadecimal
 Replace the specification with the base-sixteen representation of an
-unsigned integer.  @samp{%x} uses lower case and @samp{%X} uses upper
-case.  The object can also be a nonnegative floating-point number that
+integer.  @samp{%x} uses lower case and @samp{%X} uses upper
+case.  The object can also be a floating-point number that
 is formatted as an integer, dropping any fraction, if the integer does
 not exceed machine limits.
 
diff --git a/etc/NEWS b/etc/NEWS
index fc2a5d4c03..b735cf49b3 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -697,6 +697,14 @@ To get the old behavior, set the new, experimental variable
 read-integer-overflow-as-float to t and please email
 [email-masked] if you need that.  (Bug#30408).
 
++++
+** Numbers formatted via %o or %x are now formatted as signed integers.
+Formerly they were formatted using machine-dependent two's complement
+representations.  To get the old behavior, set the new, experimental
+variable binary-as-unsigned to t and please email [email-masked]
+if you need that.  Because %o and %x now format signed integers, they
+now support the + and space flags.
+
 ---
 ** Some functions and variables obsolete since Emacs 22 have been removed:
 archive-mouse-extract, assoc-ignore-case, assoc-ignore-representation,
diff --git a/src/editfns.c b/src/editfns.c
index 09f836c3eb..a8fc499d63 100644
--- a/src/editfns.c
+++ b/src/editfns.c
@@ -4196,8 +4196,8 @@ contain either numbered or unnumbered %-sequences but not both, except
 that %% can be mixed with numbered %-sequences.
 
 The + flag character inserts a + before any nonnegative number, while a
-space inserts a space before any nonnegative number; these flags only
-affect %d, %e, %f, and %g sequences, and the + flag takes precedence.
+space inserts a space before any nonnegative number; these flags
+affect only numeric %-sequences, and the + flag takes precedence.
 The - and 0 flags affect the width specifier, as described below.
 
 The # flag means to use an alternate display form for %o, %x, %X, %e,
@@ -4736,19 +4736,42 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message)
 		}
 	      else
 		{
-		  /* Don't sign-extend for octal or hex printing.  */
 		  uprintmax_t x;
+		  bool negative;
 		  if (INTEGERP (arg))
-		    x = XUINT (arg);
+		    {
+		      EMACS_INT i;
+		      if (binary_as_unsigned)
+			{
+			  i = XUINT (arg);
+			  negative = false;
+			}
+		      else
+			{
+			  i = XINT (arg);
+			  negative = i < 0;
+			  if (negative)
+			    i = -i;
+			}
+		      x = i;
+		    }
 		  else
 		    {
 		      double d = XFLOAT_DATA (arg);
 		      double uprintmax = TYPE_MAXIMUM (uprintmax_t);
+		      negative = d < 0;
+		      if (negative)
+			d = -d;
 		      if (! (0 <= d && d < uprintmax + 1))
 			xsignal1 (Qoverflow_error, arg);
 		      x = d;
+		      negative &= x != 0;
 		    }
-		  sprintf_bytes = sprintf (sprintf_buf, convspec, prec, x);
+		  sprintf_buf[0] = negative ? '-' : plus_flag ? '+' : ' ';
+		  bool signedp = negative | plus_flag | space_flag;
+		  sprintf_bytes = sprintf (sprintf_buf + signedp,
+					   convspec, prec, x);
+		  sprintf_bytes += signedp;
 		}
 
 	      /* Now the length of the formatted item is known, except it omits
@@ -5558,6 +5581,15 @@ functions if all the text being accessed has this property.  */);
   DEFVAR_LISP ("operating-system-release", Voperating_system_release,
 	       doc: /* The release of the operating system Emacs is running on.  */);
 
+  DEFVAR_BOOL ("binary-as-unsigned",
+	       binary_as_unsigned,
+	       doc: /* Non-nil means `format' %x and %o treat numbers as unsigned.
+Nil (the default) means to treat them as signed.
+Treating them as unsigned has machine-dependent results.
+
+This variable is experimental; email [email-masked] if you need it.  */);
+  binary_as_unsigned = false;
+
   defsubr (&Spropertize);
   defsubr (&Schar_equal);
   defsubr (&Sgoto_char);
diff --git a/test/src/editfns-tests.el b/test/src/editfns-tests.el
index c828000bb4..57a2081fc5 100644
--- a/test/src/editfns-tests.el
+++ b/test/src/editfns-tests.el
@@ -165,15 +165,16 @@ transpose-test-get-byte-positions
                 :type 'overflow-error)
   (should-error (read (substring (format "%d" most-negative-fixnum) 1))
                 :type 'overflow-error)
-  (should-error (read (format "#x%x" most-negative-fixnum))
-                :type 'overflow-error)
-  (should-error (read (format "#o%o" most-negative-fixnum))
-                :type 'overflow-error)
+  (dolist (fmt '("%d" "%s" "#o%o" "#x%x"))
+    (dolist (val (list most-negative-fixnum (1+ most-negative-fixnum)
+                       -1 0 1
+                       (1- most-positive-fixnum) most-positive-fixnum))
+      (should (eq val (read (format fmt val))))))
   (should-error (read (format "#32rG%x" most-positive-fixnum))
                 :type 'overflow-error))
 
 (ert-deftest format-%o-invalid-float ()
-  (should-error (format "%o" -1e-37)
+  (should-error (format "%o" -1e+INF)
                 :type 'overflow-error))
 
 ;; Bug#31938
-- 
2.17.1