Subject: bug#78898: Make read/readevalloop reentrant

From: Lynn Winebarger <[email-masked]>
Date: Mon, 28 Jul 2025 00:32:30 -0400

On Sat, Jul 26, 2025 at 12:00 PM Stefan Monnier
<[email-masked]> wrote:
>
> >> BTW memory that's not returned to the OS but that Emacs can reuse
> >> later is generally not considered as a leak.  E.g., most `malloc`
> >> implementations have the property of (virtually) never bothering to
> >> return freed memory to the OS.
> > Terminology aside, the memory used for the stack is never freed for
> > use by other parts of Emacs.
>
> Yup.  Still not considered a leak.  🙁
> It may be a waste of memory, of course, but we use similar approaches in
> other places without worrying very much about it.  For example, we grow
> hashtables on-demand in `puthash`, but we never shrink them back in
> `remhash`.
>
> That doesn't mean I'm opposed to changing the read code to be more
> careful with its use of memory, but I don't consider it a serious
> concern, so code clarity, speed, reentrancy, and things like that are
> more important.

Ok, the attached patch bootstraps with -O0 without native compilation.
I haven't done any benchmarking or tuning, or tested with optimization
turned on, but it shows the technique.  The changes don't seem to have
leaked beyond the obviously required scope.  The patch does eliminate
one unwind_protect (per entry to read0) which is made unnecessary by
using alloca.

If this approach works in the reader, the same technique could be
applied to making tail recursion safe for space in the bytecode VM.

> >> I'd suggest you simply concatenate a bunch of `.elc` files, until you
> >> get something "large enough" to make the timing easy to measure.
> >> Then measure the time to read that file (put the content inside
> >> a buffer, wrap it inside a pair of `(...)`, then (benchmark-run
> >> (read))`).
> >
> > So the circular expression syntax supports creating such nested
> > expressions?
>
> Yes:
>
>     ELISP> '(#1=4 #1# #1=5 #1#)
>     (4 4 5 5)
>
>     ELISP>
>
> > I didn't notice read0 recurring to the internal start (to get
> > fresh hash tables for the object map).  I don't see how such an artificial
> > list of byte code could be guaranteed to be read correctly if those hash
> > tables are not cleared between top level expressions in the file.
>
> The #N# references in a `.elc` file would fail when loaded if they refer
> to a #N= that's not in the same file.  After concatenation, since the
> #N# refers to the "most recent" #N= it should always refer to the
> "right" one.

I need to go back and check some elc files, because it seemed like
some of the byte-code had #N= definitions after #N# uses, which could
happen if the enumeration is performed breadth-first rather than
depth-first.

In any case, for benchmarking purposes:
* the cost of allocating the structure being read should count toward
the cost of read, but the cost of growing those hash tables repeatedly
should not.
* none of the constructed expressions are released, so the cost of
expanding the heap size (from the OS) is included.

One quibble I would have is that there is some difference in the
details of reading a character from a buffer and reading from a file.
It just seems weird because ELC files are never loaded into a buffer,
and buffers are not exactly simple.  An alternative would be to memory
map elc files that fstat indicates are regular.  That might be a
useful change in itself.

Lynn