7

I am writing some lisp code and I would like it not to mess with the global variables in the system. I am therefore being very careful to only use variables within the scope of the let special form.

However, as the code gets longer, it becomes harder to make sure this principle is being fully respected, especially since there is the risk of a mispelled variable suddently becoming global in case the incorrect spelling turns the name of a variable into one not bound in any let form.

Ideally it would be great to be able to simply prohibit new global variables to be setq, say with a lisp command inhibit-setting-global-variables-not-already-defined. My question is thus:

Question. Is it possible to automatically verify that a piece of lisp code does not create any new global variables?

Drew
  • 75,699
  • 9
  • 109
  • 225
Ruy
  • 787
  • 4
  • 11

1 Answers1

10

Is it possible to automatically verify that a piece of lisp code does not create any new global variables?

Turn on lexical-binding:

;;; foo.el --- just frobnicating some foo -*- lexical-binding: t -*-

(setq foo-bar nil)

(defun foo-bar ()
  (let (x)
    (setq y nil)))

;;; foo.el ends here

and then the byte-compiler will do the work for you:

emacs -Q -batch -f batch-byte-compile foo.el

In toplevel form:
foo.el:3:7: Warning: assignment to free variable ‘foo-bar’
foo.el:5:1: Warning: Unused lexical variable ‘x’

In foo-bar:
foo.el:7:11: Warning: assignment to free variable ‘y’

If you want to be particularly strict, you can turn warnings into errors:

emacs -Q -batch -eval '(setq byte-compile-error-on-warn t)' -f batch-byte-compile foo.el

In toplevel form:
foo.el:3:7: Error: assignment to free variable ‘foo-bar’

To see these warnings interactively, turn on flymake-mode.

To reduce the chance of typos to begin with, you can use symbol completion (C-M-i - completion-at-point), dynamic abbreviations (M-/ - dabbrev-expand), or similar.

Basil
  • 12,019
  • 43
  • 69
  • Thanks @Basil! I couldn't ask for a better answer! – Ruy Feb 12 '21 at 23:47
  • 1
    @Ruy: Note that *without* lexical binding, all of your `let`-bound variables were global (with dynamic scope). – phils Feb 13 '21 at 05:32
  • Hi @Basil, your suggestion is working wonders but I'm seeing some strange warnings when byte-compiling my file. Here are some examples: ```;;; test.el -*- lexical-binding: t -*- (loop for x in (list 1 2 3) collect (* 2 x)) (let ((animal 'dog)) (case animal ('tiger "Feline") ('dog "Canine") (t "Other") ) ) ;;; test.el ends here ``` – Ruy Feb 14 '21 at 23:05
  • I get lots of errors! In toplevel form: test.el:3:7:Warning: reference to free variable ‘for’ test.el:3:11:Warning: reference to free variable ‘x’ test.el:3:13:Warning: reference to free variable ‘in’ test.el:4:3:Warning: reference to free variable ‘collect’ test.el:6:1:Warning: ‘(quote tiger)’ is a malformed function test.el:6:1:Warning: ‘(quote dog)’ is a malformed function test.el:7:9:Warning: ‘t’ called as a function In end of data: test.el:17:1:Warning: the following functions are not known to be defined: What's wrong? – Ruy Feb 14 '21 at 23:09
  • @Ruy Elisp doesn't predefine a `loop` macro. The [`cl-lib`](https://gnu.org/software/emacs/manual/html_node/cl/Loop-Facility.html) library provides `cl-loop`, so you need `(eval-when-compile (require 'cl-lib))` at the top of your file in order to use it. The `cl.el` library defines `loop` as an alias of `cl-loop`, but this library [is deprecated](https://gnu.org/software/emacs/manual/html_node/cl/Organization.html). – Basil Feb 14 '21 at 23:15
  • 1
    The `loop` issue aside, lexical binding affects the entire library, and you definitely want to do some reading about the difference between lexical binding and dynamic binding, because code written for (only) dynamic binding does not necessarily work as-is if you enable lexical binding for that library. – phils Feb 17 '21 at 08:52