Subject: bug#78737: sit-for behavior changes when byte-compiled

From: Daniel Colascione <[email-masked]>
Date: Thu, 12 Jun 2025 12:07:32 -0700

Eli Zaretskii <[email-masked]> writes:

>> Date: Thu, 12 Jun 2025 11:04:06 -0700
>> From: Daniel Colascione <[email-masked]>
>> CC: [email-masked], [email-masked], [email-masked]
>> 
>> 
>> 
>> On June 12, 2025 9:56:26 AM PDT, Eli Zaretskii <[email-masked]> wrote:
>> >> Date: Thu, 12 Jun 2025 09:52:22 -0700
>> >> From: Daniel Colascione <[email-masked]>
>> >> CC: [email-masked], [email-masked]
>> >> 
>> >> 
>> >> 
>> >> On June 12, 2025 9:32:26 AM PDT, Eli Zaretskii <[email-masked]> wrote:
>> >> >> Date: Thu, 12 Jun 2025 13:58:51 +0000
>> >> >> From: Pip Cet <[email-masked]>
>> >> >> Cc: Eli Zaretskii <[email-masked]>, [email-masked], [email-masked]
>> >> >> 
>> >> >> I'd say breaking (read-event) called in a loop is bad enough, because
>> >> >> how else are you supposed to start developing code which uses it?
>> >> >
>> >> >Maybe this regression should be fixed, then.
>> >> 
>> >> It's not a regression. It's a bug fix. Not every behavior change is a problem. Who starts coding something by calling it in a loop? That's like learning to drive by crashing into a wall.
>> >
>> >Did it never happen to you that you wrote a loop and forgot the part
>> >that advances the counter or some other thing that will prevent an
>> >infloop?  Stuff happens when developing code.
>> 
>> And the mechanism I described just now addresses the problem of recovering from programmer error.
>
> Sorry, I must have missed it (assuming that was in your previous
> message).  Could you please point me to that description, or repeat
> it?

It's near the bottom of 
https://lists.gnu.org/archive/html/bug-gnu-emacs/2025-06/msg00629.html

> Specifically, what I'm interested in is how come
>
>  (while t
>    (read-event))
>
> cannot be interrupted by C-g, but you seem to be saying that
>
>  (while t
>    (let (evt (read-event))
>      (do-something-with evt)))
>
> _can_ be interrupted?

(read-event) returns \?C-g.  If the C-g arrives when Lisp code is
running (e.g. inside (do-something-with evt), then we quit --- unless
that code is also reading an event, in which case we return it.

> Let's say when I type C-g, the loop is stuck
> inside read-event: could you then describe how this latter loop could
> be interrupted in that case?

As I mentioned just now in yet another message, we impose a threshold.

N quits in T milliseconds -> upgrade quit to immediate-quit when we
Fsignal, with quit on immediate-quit's condition list, just like we do
for minibuffer-quit.  immediate-quit means "I definitely meant to exit
whatever I'm doing, even if the code says to read an event or key
sequence".

N_emergency quits in T_emergency milliseconds -> do same as above,
except that we ignore inhibit-quit when deciding whether to signal:

/* Have we gotten at least N quits in last T milliseconds?  */
static bool
check_recent_quits (int n, int t)
{
  return <whether we've received N_emergency quits
  in last T_emergency milliseconds>;
}

void
probably_quit (void)
{
  specpdl_ref gc_count = inhibit_garbage_collection ();

  /* Quit promptly if processing pending signals makes us want to
     quit.  */
  if (!QUITP && pending_signals)
    process_pending_signals ();
  if (QUITP || check_recent_quit(quit_emergency_count, quit_emergency_window))
    process_quit_flag ();

  unbind_to (gc_count, Qnil);
}

and then,

Lisp_Object
quit (void)
{
  bool upgrade =
    check_recent_quit (quit_upgrade_count, quit_upgrade_count);
  /* Only non-upgraded quits count as continuable.  */
  return signal_or_quit (upgrade ? Qquit_immediate : Qquit,
    Qnil, !upgrade);
}

and then in read_char, we change from

  if (convert_quits && !NILP (Vquit_flag))
    {
      Vquit_flag = Qnil;
      result = make_fixnum (quit_char);
    }

to

  if (convert_quits && !NILP (Vquit_flag)
     && check_recent_quit(quit_upgrade_count, quit_upgrade_count))
  {
    Vquit_flag = Qnil;
    result = make_fixnum (quit_char);
  }

and then...

  DEFVAR_INT ("quit-upgrade-count", quit_upgrade_count,
	      doc: /* Number of quits received in quit-upgrade-period
  to count as an urgent quit that quits out of functions meant to read
  input events.  */)
  quit_upgrade_count = 3;

  DEFVAR_INT ("quit-upgrade-period", quit_upgrade_period,
	      doc: /* Time in which if we see at least quit-upgrade-count
    quits we quit out of functions meant to read input events.  */)
  quit_upgrade_period = 500;

  DEFVAR_INT ("quit-emergency-count", quit_emergency_count,
	      doc: /* Number of quits received in quit-emergency-period
    after which we disable safeguards that normally prevent quitting
    from protected code paths.  */);
  quit_emergency_count = 6;
 a
  DEFVAR_INT ("quit-emergency-period", quit_emergency_period,
	      doc: {you get the idea});
  quit_emergency_period = 1000;

At the top of command_loop_1, we reset the quit ring.  The above
thresholds count only between command invocations.

TIMTOWTDI of course.  This is a sketch.  But something like this should
make us both happy, yes?