On Buglet-Bindings

Highlighting a pervasive Emacs Lisp bug

Created on [2024-07-15], last updated [2024-07-18]

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!