Emacs: Check Interactive Call For Emacspeak

1 Background

Emacspeak uses advice as the means to speech-enable Emacs. Emacspeak's advice forms need to check if the function being speech-enabled is being called interactively — otherwise one would get a lot of chatter as these functions get called from within elisp programs, e.g. functions like forward-sexp or kill-sexp that play the role of both an interactive command, as well as being a convenient elisp function.

Until Emacs 24, the solution used was to write code that did the following check:

(when (interactive-p) ...)

In Emacs-24, interactive-p was made obsolete and replaced with

(called-interactively-p 'interactive)

Emacspeak initially used the above form to perform the equivalent check. However, around the same time, Emacs' advice implementation went through some changes, and there was an attempt to replace advice.el with nadvice.el.

At the end of that round of changes, some problems emerged with the new called-interactively-p implementation; specifically, calling called-interactively-p within around advice forms resulted in hard to debug errors, including one case of infinite recursion involving library smie.el when invoked from within ruby-mode.

After studying the problem in depth in 2014, I decided to create an Emacspeak-specific implementation of the is-interactive check.

The resulting implementation has worked well for the last 30 months; this article is here mostly to document how it works, and the reason for its existence. Note that Emacspeak uses this custom predicate only within advice forms. Further, this predicate has been coded to only work within advice forms created by emacspeak. This constraint can likely be relaxed, but the tighter implementation is less risky.

2 Implementation — ems-interactive-p

2.1 Overview

Within an advice form defined by Emacspeak, detect if the enclosing function call is the result of explicit user interaction. Emacspeak produces auditory feedback only if this predicate returns t.

We first introduce a flag that will be used to record if the enclosing (containing) function has an Emacspeak-defined advice on it — these are the only cases that our predicate needs to test.

(defvar ems-called-interactively-p nil
  "Flag that records if containing function was called interactively."

Next, we define a function that checks if interactive calls to a function should be recorded. We're only interested in functions that have an advice form defined by Emacspeak — all Emacspeak-defined advice forms have the name emacspeak and can therefore be easily idnetified.

(defun ems-record-interactive-p (f)
  "Predicate to test if we need to record interactive calls of
this function. Memoizes result for future use by placing a
property 'emacspeak on the function symbol."
   ((not (symbolp f)) nil)
   ((get f 'emacspeak) t) ; already memoized
   ((ad-find-some-advice f 'any  "emacspeak") ; there is an emacspeak advice
    (put f 'emacspeak t)) ; memoize for future and return true
   (t nil)))

This is a memoized function that remembers earlier invocations by setting property emacspeak on the function symbol.

All advice forms created by Emacspeak are named emacspeak, so we can test for the presence of such advice forms using the test:

(ad-find-some-advice f 'any  "emacspeak")

If this test returns T, we memoize the result and return it.

Next, we advice function call-interactively to check if the function being called interactively is one of the functions that has been adviced by Emacspeak. If so, we record the fact in the previously declared global flag ems-called-interactively-p.

(defadvice call-interactively (around emacspeak  pre act comp)
  "Set emacspeak  interactive flag if there is an Emacspeak advice 
on the function being called."
  (let ((ems-called-interactively-p ems-called-interactively-p)) ; preserve enclosing state
    (when (ems-record-interactive-p (ad-get-arg 0))
      (setq ems-called-interactively-p (ad-get-arg 0)))

We define an equivalent advice form on function funcall-interactively as well. Now, whenever any function that has been adviced by Emacspeak is called interactively, that interactive call gets recorded in the global flag. In the custom Emacspeak predicate we define, we check the value of this flag, and if set, consume it, i.e. unset the flag and return T.

(defsubst ems-interactive-p ()
  "Check our interactive flag.
Return T if set and we are called from the advice for the current
interactive command. Turn off the flag once used."
  (when ems-called-interactively-p      ; interactive call
    (let ((caller (cl-second (backtrace-frame 1))) ; containing function name
          (caller-advice ; advice wrapper of called interactive function
           (ad-get-advice-info-field ems-called-interactively-p  'advicefunname))
          (result nil))
       ; T if called from our advice
      (setq result (eq caller caller-advice))
      (when result
        (setq ems-called-interactively-p nil) ; turn off now that we used  it

The only fragile part of the above predicate is the call to backtrace-frame which we use to discover the name of the enclosing function. Notice however that this is no more fragile than the current implementation of called-interactively-p — which also uses backtrace-frame; If there are changes in the byte-compiler, this form may need to be updated. The implementation above has the advantage of working correctly for Emacspeak's specific use-case.

2.2 Illustrative Example:Interactive Call To next-line

User presses C-n or [down] to move to the next line — this is an interactive call to function next-line. Function next-line is adviced by Emacspeak, that advice form contains:

(when (ems-interactive-p) ...)

Here is the exact sequence of steps that causes the above predicate to return t in this case.

  1. Pressing C-n first calls call-interactively with next-line as the function to call.
  2. The advice on call-interactively first checks if next-line has been adviced by Emacspeak — (ems-record-interactive-p (ad-get-arg 0)) — in this example, (ad-get-arg 0) returns next-line.
  3. Predicate ems-record-interactive-p returns t after setting ems-called-interactively-p to the name of the function being called — next-line.
  4. The advice mechanism now takes over and invokes the various parts of the advice-onion, this starts with calling the advice-generated wrapper ad-Advice-next-line.
  5. Within the defadvice form, we evaluate (when (ems-interactive-p) …)
  6. In the call to predicate ems-interactive-p, we first check that the global flag ems-record-interactive-p is set.
  7. First, we bind caller to the name of the containing function by evaluating (backtrace-frame 1) — this evaluates to the advice-generated wrapper function.
  8. Next, we bind caller-advice to the name of the generated advice wrapper for the function recorded in ems-record-interactive-p using the call (ad-get-advice-info-field ems-called-interactively-p 'advicefunname).
  9. For the present example, caller binds to ad-Advice-next-line — since that is the function that the advice system calls. This matches the value bound to caller-advice which is the precise test we need to verify that the advice form is being evaluated in the context of an interactive call to our adviced function.

Date: 2017-03-01 Wed 00:00

Author: raman

Created: 2017-03-01 Wed 16:58