On Buglet-Bindings
Highlighting a pervasive Emacs Lisp bug
This post is about a class of bugs that you run into left and right in Emacs Lisp code, a sneaky pitfall that developers keep falling into, n00bs and greybeards alike: unsafe let-bindings of special variables.
Say you’re writing some code that lets you (or other users) input some
text interactively. Say you’re using the minibuffer for reading that
input. Say you’re going the extra mile to provide text completion.
In short, say you’re using completing-read
:
(completing-read "Frobnicate: " '("Foo" "Bar" "Spam"))
So far so good, but when you try it out you realize that completion is
case-sensitive by default, so typing s TAB
doesn’t complete to
Spam
and you have to type S TAB
with an uppercase S
instead.
That’s a nuance, so you venture to make completion case-insensitive.
You could just configure you’re Emacs with completion-ignore-case
set to t
, but that affects all kinds of completion, and you only
want “this” completion to be case-insensitive. That’s when you fall
head first into the buglet-binding pitfall, and do this:
;; BAD! (let ((completion-ignore-case t)) (completing-read "Frobnicate: " '("Foo" "Bar" "Spam")))
You’ve let-bound completion-ignore-case
around the call to
completing-read
. You do this because that’s what you see others do.
It makes sense. But it’s wrong. The others you mimic are staring at
you from the bottom of the pitfall, and you’ve jumped in to join them.
It is wrong because this binding affects all recursive minibuffers, while you only wanted to influence the completion you’re coding up. Reading input from the minibuffer puts Emacs in a recursive edit. Many things can happen before the minibuffer is terminated—you can switch away to another buffer and do some editing, get a coffee, and even enter more, recursive, minibuffers. And your let-binding takes effect over all of that.
To see this nasty effect, execute the above code, and while in the
minibuffer type C-h f
to invoke describe-function
. Now, in the
new minibuffer, try to complete some function name. For example, type
info
, all lower case, followed by ?
to show the completions
list—you get completion candidates that start with capitalized
Info
, because your let-binding of completion-ignore-case
is still
in effect. Ouch!
Instead, the correct way to implement case-insensitive minibuffer completion, and more generally, to change some minibuffer settings without affecting recursive minibuffers, is to use buffer-local bindings. For example:
;; OK. (minibuffer-with-setup-hook (lambda () (setq-local completion-ignore-case t)) (completing-read "Frobnicate: " '("Foo" "Bar" "Spam")))
Actually, since Emacs version 29, completion-ignore-case
in
particular gets a special treatment that requires some additional work
to handle correctly. This is fine for other variables.
While let-bindings are local to a certain piece of code, that piece may become boundless when recursive edits or other forms of arbitrary code execution are involved. On the other hand, buffer-local variable values only affect the one minibuffer in which you set them. Recursive minibuffers are different buffers, so they have their own buffer-local values and thus maintain their usual behavior.
A bug-let, or a buglet-binding, is a coding error in which a
let-binding has unintended influences on some code in the scope of
that let-binding, like in the example we saw earlier. Let-binding
special variables (those defined with defvar
, defcustom
, etc.)
around functions that read input in the minibuffer often brings about
such bug-lets. Variables that are often buglet-bound include
enable-recursive-minibuffers
, completion-extra-properties
,
completion-ignore-case
, minibuffer-allow-text-properties
,
completion-ignored-extensions
, and minibuffer-completing-file-name
among others. But it’s not all minibuffers, buglet-bindings are a
more general class of bugs that can occur even when no minibuffers are
in play. When you let-bind special variables, you need to ensure that
everything in the binding’s scope cooperates with it correctly.
That’s easy to do for your own special variables that only your code
uses: if all other code is indifferent to the value of your variable,
then all you need to check is that your code is not called
recursively, or that it’s prepared for being called recursively with
the let-binding in effect. For special variables defined elsewhere,
keep the scope of let-bindings as small as possible and check that all
functions in scope are known and that the let-binding doesn’t change
their behavior in unindented ways. If you’re invoking a user-supplied
function in the let-binding scope, document that clearly so users can
prepare their code accordingly.
Other than that, let-bind away, and stay safe!