Subject: Re: bug#55645: src/print.c; print_object changes make it impossible to compare elisp code across versions

From: Tom Gillespie <[email-masked]>
Date: Sun, 29 May 2022 14:03:11 -0700

Hi Eli,
  I have included an example below, with some additional context. Best!
Tom

I'm using my own use case as an example, but I suspect that there are
other users out there who may have similar use cases where stability
of the printed representation of read code is important.

Some examples from a quick search on github that will or could be
affected by this (beyond my own). I only searched for cases matching
my own involving secure-hash and prin1-to-string, but there are surely
other cases that would be affected that I have not imagined.
https://github.com/skeeto/elfeed/blob/162d7d545ed41c27967d108c04aa31f5a61c8e16/web/elfeed-web.el#L73-L75
https://github.com/mrmekon/snitch-el/blob/3b3e7f1bf612c4624764d1ec4b1a96e4d2850b05/snitch-timer.el#L164-L181
https://github.com/radian-software/straight.el/blob/af5437f2afd00936c883124d6d3098721c2d306c/straight.el#L5627

https://github.com/search?q=prin1-to-string+secure-hash&type=Code
You can filter out some of the noise by adding the follow to the search
-elfeed -orgstrap -quelpa -litable -xah-elisp-mode -subr.el

The use case that I have is to store the checksum of a code block to
make sure that it has not changed. The checksum needs to be invariant
to changes in formatting e.g. whitespace and needs to be backward and
forward compatible across Emacs versions. In order to compute the
checksum I need a serialized representation of the code. Note when I
say "compare" two pieces of elisp code, one of them may no longer be
available to be read, because only a checksum was retained, so direct
comparison of the two structures in memory is not possible and defeats
the point of having something that is simple to audit and store.

This is discussed in more detail in the orgstrap readme.
https://github.com/tgbugs/orgstrap/blob/master/README.org#normalization-functions
https://github.com/tgbugs/orgstrap/blob/master/README.org#specification

>From Emacs 18 through 28 prin1-to-string and the existing print
variables have been able to provide the needed stability.

There is currently no way to compensate for the change introduced in
637dde4aba921435f78d0de769ad74c4f3230aa6 short of reimplementing the
old behavior of prin1-to-string from scratch, which would ultimately
increase the maintenance load across the whole community.

The example below works on emacs-18 (had to remove the number 1.0
example because emacs 18 does not have support for reading floats).

The output of this code is unchanged from emacs-18 through emacs-28,
however it is now different in emacs-29.
Emacs 18-28:
"(progn (+ 1 2) (a b\\.c d) (defun hello nil world)) (progn (some
elisp code I want to normalize\\. That has strings \"1.0\" and symbols
a\\.b))"
Emacs 29:
"(progn (+ 1 2) (a b.c d) (defun hello nil world)) (progn (some elisp
code I want to normalize. That has strings \"1.0\" and symbols a.b))"

#+begin_src elisp :tangle /tmp/example.el
(defun normalize-elisp-code (body)
  (let (print-quoted print-length print-level) ; proposed variable
would be added here
    (prin1-to-string (read (concat "(progn\n" body "\n)")))))

(defvar example-body-1 "(+ 1 2) (a b\\.c d) (defun hello () world)")
(defvar example-body-2 (prin1-to-string '(some elisp code I want to
normalize\. That has strings "1.0" and symbols a\.b)))
(message "%s %s"
         (normalize-elisp-code example-body-1)
         (normalize-elisp-code example-body-2))
#+end_src

The additional step in my use case is the checksum, which cannot be
read back in, and changes from 28 -> 29 due to the differences in the
output of prin1-to-string seen above.

#+begin_src elisp
(defun checksum-elisp-code (body)
  (secure-hash 'sha256 (normalize-elisp-code body)))

(message "%s %s"
         (checksum-elisp-code example-body-1)
         (checksum-elisp-code example-body-2))
#+end_src