This answer is an extension of Phils answer.
Your question shows the potential danger of locally binding special-declared variables.
The convention of prefixing symbol names with library names normally protects us from accidentally using special-declared symbols for local bindings that should have lexical scope.
But there are exceptions like displayed-month and displayed-year from calendar.el.
One can ensure lexical binding with the help of uninterned symbols.
The principle is described through a simple documented example:
;; -*- lexical-binding: t -*-
(require 'cl)
;; reset:
(unintern 'wtf)
(unintern 'foo)
;; declare `wtf' as special:
(defvar wtf)
(setq wtf 0)
(defmacro f ()
(let ((sym (make-symbol "wtf"))) ;; make-symbol' returns an uninterned symbol with namewtf'
(let ((,sym 1)) ;; We substitute here the uninterned symbolwtf'.
(lambda ()
,sym))))
(setq ans (funcall (f)))
;; => 1
It is clear that the definition of the macro f in the above example is cumbersome.
But a special lexlet macro as defined in the following can relieve us from that work.
First comes the library code defining the lexlet macro and below that the user code resembling the simplified introductory example.
The macro lexlet generates uninterned duplicates of all locally bound symbols, locally binds the uninterned symbols to their given values and replaces the locally bound interned symbols in the body with their uninterned counterparts.
The definition of the macro is kept simple. But there is a small caveat. One cannot use the function definition of the locally bound symbols.
;; -*- lexical-binding: t; -*-
;; Here starts the library code.
(defun lexlet--search (data symbol-alist)
"Replace symbols according to SYMBOL-ALIST in DATA."
(cond
((symbolp data)
(or
(alist-get data symbol-alist)
data))
((vectorp data)
(apply #'vector (seq-map #'lexlet--search data symbol-alist)))
((consp data)
(cons (lexlet--search (car data) symbol-alist)
(lexlet--search (cdr data) symbol-alist)))
(t data)))
;; Test:
;; (lexlet--search '(lambda () wtf) '((wtf . var)))
(defmacro lexlet (bindings &rest body)
"Like `let' but with lexical binding for all symbols in BINDINGS.
If a symbol has a local binding in BINDINGS its binding during
the execution of BODY is lexical even if the symbol is declared as special.
This macro requires `lexical-binding' set to t."
(declare (indent 1)
(debug ((&rest (symbol sexp)) body)))
(let* ((symbol-alist (mapcar
(lambda (binding)
(let ((symbol (car binding)))
(cons
symbol
(make-symbol (symbol-name symbol)))))
bindings))
(new-bindings (mapcar
(lambda (binding)
(let* ((old-symbol (car binding))
(new-symbol (alist-get old-symbol symbol-alist)))
(cons new-symbol (cdr binding))))
bindings)))
(append
(list
#'let
new-bindings)
(lexlet--search body symbol-alist))))
(provide 'lexlet)
With lexlet the user code of the introductory example looks friendlier:
(require 'lexlet)
(defvar wtf)
(setq wtf 0)
(defun f ()
(lexlet ((wtf 1))
(lambda ()
wtf)))
(setq ans (funcall (f)))
;; => 1
It is important that the macro extends the lexical environment created by an outer let.
For an example eval with an environment given as argument lexical does not extend an outer lexical environment and is therefore only of limited use for our purpose.
But, lexlet is okay in that regard:
;; -*- lexical-binding: t -*-
(require 'lexlet)
;; reset:
(unintern 'wtf)
(unintern 'lexbound)
(defvar wtf)
(setq wft 0)
(defun f ()
(let ((lexbound 1))
(lexlet ((wtf 2))
(lambda ()
(list
lexbound wtf)))))
(setq ans (funcall (f)))
;; => (1 2)
fset. The enclosing lambda can be defined andfsetby adefunas in your case. But you are right, the behavior is wired. – Tobias Mar 16 '23 at 08:46lambdais self-quoting. Furthermore, for repeatability it is better to separate the marking of the symbol as special and the binding of the symbol to a value:(defvar wtf) (setq wtf 10). – Tobias Mar 16 '23 at 08:48#'or not. I used to write some CL code and I used#'there. It seems that Elisp doesn't refuse this notation, so I continue to do this way because it makes lambda expressions more prominent. Thanks for your suggestion; without#', the code may look cleaner. – shynur Mar 16 '23 at 09:32