Subject: bug#70221: [PATCH] New function `funcall-later`

From: Stefan Monnier <[email-masked]>
Date: Sat, 06 Apr 2024 10:33:05 -0400

>> +DEFUN ("internal--run-pending-funcalls", Frun_pending_funcalls, Srun_pending_funcalls, 0, 0, 0,
>> +       doc: /* Run the still pending `funcall-later'.  */)
>> + (void)
>> +{
>> +  while (CONSP (pending_funcalls) || CONSP (pending_funcalls_r))
>> +    if (CONSP (pending_funcalls))
>> +      {
>> +        Lisp_Object funcall = XCAR (pending_funcalls);
>> +        pending_funcalls = XCDR (pending_funcalls);
>> +        CALLN (Fapply, funcall);
>> +      }
>> +    else
>
> You are using CALLN here, whereas the previous implementation used
> safe_calln.  Is that intended?  Calling Lisp in unsafe ways in that
> place might not be a good idea.

I found it annoying that the debugger isn't brought up when a bug
occurs in such delayed evaluations.  Note also the subsequent call to
`timer-event-handler` uses `calln` rather than `safe_calln`, so I think
my original use of `safe_call2` in commit 58555d8187f3425 was a mistake.
I can't see any good reason why we'd need to protect the
C code from non-local exits in `timer_check_2`.

> You didn't even inhibit QUIT.

I'm torn on this one:

OT1H while the time at which the code is run is loosely specified, it's
not really asynchronous: it's run the next time Emacs "waits", which is
usually about the same time as `post-command-hook` (which does use
`inhibit-quit`), so there doesn't seem to be a good justification for
`inhibit-quit`.

OTOH when the `funcall-later` is performed from asynchronous code
(e.g. from a timer), then running the delayed function call without
`inhibit-quit` ends up "hoisting" that code outside of its original
`inhibit-quit` context.

For users such as `track-changes.el`, `inhibit-quit` is not
needed/desired.  But indeed when I use it in `futur.el` it's common to
call `funcall-later` from process filters and timers.

Usually part of the purpose of `funcall-later` is to run the code in
a different (dynamic) context, but maybe `funcall-later` should
preserve `inhibit-quit`?

> As another difference between run-with-time and this mechanism, the
> former took care of preserving deactivate-mark around the call, wheres
> funcall-later doesn't

This is again because `run-with-time` is designed for the general case
where the delay is not 0s, so the code will be run asynchronously,
wheres `funcall-later` is designed for "different time but not async".

> -- this is at least one difference that we
> should document (assuming we want it).

AFAICT we don't document anywhere that `deactive-mark` is bound
around timers, so I guess fixing that would be the best way to document
the difference.

>> No: beside the fact that they are run at slightly different times
>> (which might be OK), `funcall-later` doesn't return a handle you can
>> then use to cancel the call.
> This should be mentioned in the documentation.

How/where?  Maybe the doc can just say that it returns nil?
Other than that, it would seem odd to write something like
"There is no `cancel-funcall-later`".

> Thanks.  We should document this in the ELisp manual, and should
> explain there the meaning of "at the next convenient time".  (I think
> "convenient" here is very much misleading and thus inappropriate.)

That's the big question, really.  E.g. I currently use `funcall-later`
in `track-changes.el` and in `futur.el`.  I use it because it's handy.
But fundamentally those two need different things:

- `track-changes.el` uses `funcall-later` because it wants to delay the
  execution in order to combine the current buffer modification with
  potential soon-to-come buffer modifications.
- `futur.el` uses `funcall-later` to "wake up" a piece of code that was
  waiting for the current event.  In this case it uses `funcall-later`
  not because it wants to delay execution but because it wants that
  execution to be unaffected by the current dynamic scoping and it
  doesn't want that execution to block the current execution.

So, if we were to change `funcall-later` so that the code gets run more
promptly (e.g. via true concurrency [ let's pretend that this was
actually possible and that the race-condition problems were magically
solved somehow ]), it would be great for `futur.el` but would defeat the
purpose for `track-changes.el`.

Current users of `funcall-later` (well: of its underlying mechanism):
- We use it in `comp.c` to launch the "jit" compilation of a package.
- We use it in `frame.c` and `terminal.c` to run
  `(after-)delete-frame-functions` and `delete-terminal-functions` when
  the current context does not allow running ELisp.

These are more like `futur.el` in the sense that the main purpose is
to run the code in a different context, without blocking the
current execution.

        Stefan