1

The compiler will issue a warning message

Warning: assignment to free variable -/var

if you evaluate the following forms:

;;; -*- lexical-binding: t; -*-
(defvar -/var)
(byte-compile (let (_)
                (lambda ()
                  -/var)))

But if you

  • remove the local binding of _, then evaluate the following forms:

    ;;; -*- lexical-binding: t; -*-
    (defvar -/var)
    (byte-compile (let ()
                    (lambda ()
                      -/var)))
    

or

  • byte-compile-file the entire file stead of using byte-compile:

    ;;; -*- lexical-binding: t; -*-
    (defvar -/var)
    (let (_)
      (lambda ()
        -/var))
    

Everything goes well.

Why would this happen? I see no difference among the above snippets.


emacs-version: 29.1
system-configuration: x86_64-w64-mingw32

6
  • @phils: Did you notice the source code comment for dlet? Perhaps they fix this bug but forgot to update the comment.
    – shynur
    Commented Aug 6, 2023 at 2:39
  • @phils: I tested it in v29.1 and v30.0.50; both behaved as the question description mentioned. How did you test it? by -Q?
    – shynur
    Commented Aug 6, 2023 at 4:58
  • @phils: It’s my fault - I didn't state how to test it clearly. Just evaluate that form, instead of byte-compiling the file. Sorry again.
    – shynur
    Commented Aug 6, 2023 at 5:15
  • @phils: In this example, yes, I could separate these two forms. However, this is a simplified version of the actual code. In the context of the original code, I prefer using backquote-comma. ||| I just cannot figure out why the compiler issues a warning. "defvar a SYMBOL without VALUE to make that symbol special locally." I already defvared it and seems that the defvar form and the lambda form are in the same lexical scope.
    – shynur
    Commented Aug 6, 2023 at 7:25
  • It seems like either a false-positive or a false-negative warning. Or at least (as I found in my initial test), both versions of the file byte-compile to the exact same code, so we'd need the byte-code interpreter to execute this differently to the plain lisp interpreter for there to be any difference. That might be the case, but potentially the issue only exists in the code which generates the warning.
    – phils
    Commented Aug 6, 2023 at 7:39

1 Answer 1

1

Before you read the answer, the problem and the answer are only helpful for those want to explicitly call the byte compiler and want to learn the detail on how lexical binding and dynamic binding interact with each other in Emacs. Simply adhering to the usually coding convention can completely avoid the situation.


On 29.1, the minimal reproduced example should be like

(defvar foo) ;; foo must be fresh and unbound to reproduce the problem.

(let ()
  (lambda () (setq foo t)))
(closure (foo t) nil (setq foo t))

(let (_)
  (lambda () (setq foo t)))
(closure (t) nil (setq foo t)) ;; Warn about free variable if compiled.

They produce different closure object thus byte compiler produce different messages.

The difference of results in 29.1 is likely due to

(let (_) ... )

has the semantic of introducing a new scope (mentioned in the manual but didn't into any of the detail of this), thus it would nullify the (defvar ... declaration in global scope. The closure produced reflected this.

It can thus conclude that it is expected for the byte compiler to produce a warning here.

There are a few edge case behavior (tested with 29.1) worth to be mentioned: defvar does not need to be evaluated to take effect, and does not seem to produced any side effect when evaluated if no binding is provided. The interpreter and compiler would search the defvar declarations in lexical binding mode to handle their effect. So simply comment/uncomment defvar declarations in top level of the scratch buffer without reevaluate it can affect the byte code produced. And there are too many ways fool the interpreter/compiler by hide the defvar declarations from them.

defconst works in a similar way to defvar.

However, once used defvar/setq/defconst to binding the variable, the variable itself does become "permanently" special and makunbound can not revert the effect, but unintern can.

Remember that, (defvar foo) and (defvar foo nil) has different meaning and the behavior can be different.


On 28.2 the result is also different, although the byte compiler would not generate a complain for the free variable. Whether foo is bound or undeclared would not make a difference to the result.

enter image description here


Op mentioned the FIXME comment in dlet. However, that is not a bug at all. The problem mentioned in dlet is because the byte compiler would optimize

(let () body) => (progn body)

which is a behavior decided before lexical scoping was introduced. Thus the verbose (let (_) ...) is used.

The (let (_) body) expression itself is not a magic, it is from (let (a) body) where a is not used in body, the only difference is byte compiler would warn if a is an unused variable, and might produce more efficient byte code for _ version. Actually you can use _a to nullify the unused variable warning and it would produce the same bytecode.

(lambda ()
  (let (_)
    (defvar abc-foo)
    (let (abc-foo)
      (setq abc-foo 1))))

(lambda ()
  (let (a)
    (defvar abc-foo)
    (let (abc-foo)
      (setq abc-foo 1))))

(lambda ()
  (let (_a)
    (defvar abc-foo)
    (let (abc-foo)
      (setq abc-foo 1))))

The only magical thing is actually defvar:

(equal
(byte-compile
 (lambda ()
  (progn
    (defvar abc-foo)
    (let (abc-foo)
      (setq abc-foo 1)))))

(byte-compile
 (lambda ()
  (let (_) ;; with or without _ produced same result
    (defvar abc-foo)
    (let (abc-foo)
      (setq abc-foo 1))))))
;; t

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.