为啥一个无关的局部绑定会引起编译警告?

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


从 Stack Exchange 上搬来的:

(defmacro dlet (binders &rest body)
  "Like `let' but using dynamic scoping."
  (declare (indent 1) (debug let))
  ;; (defvar FOO) only affects the current scope, but in order for
  ;; this not to affect code after the main `let' we need to create a new scope,
  ;; which is what the surrounding `let' is for.
  ;; FIXME: (let () ...) currently doesn't actually create a new scope,
  ;; which is why we use (let (_) ...).
  `(let (_)
     ,@(mapcar (lambda (binder)
                 `(defvar ,(if (consp binder) (car binder) binder)))
               binders)
     (let ,binders ,@body)))

C-h f dlet

1 个赞

卧槽,藏这么深都能找到。收藏了,明天细看

我看了手册上关于 defvar without VALUE 的示例. 似乎是

(defvar -/var)
(let (-/var)
  ;; 此时 -/var 仍是 special 的.
  )

为啥到我那个例子里就会报 warning?

草,这个问题没我想的那么简单,我对 defvar 的理解还不够。

研究一下去

;;no warning
(let ()
  (defvar -//woc)
  (byte-compile
   (lambda () (setq -//woc t))))
;;warning
(let (_)
  (defvar -//woc)
  (byte-compile
   (lambda () (setq -//woc t))))

稍微变形一下。

找到一个类似的贴子: let修改临时变量能否和autoload搭配使用 - Emacs-Lisp - Emacs China (emacs-china.org)

你没看到 dlet 用了两层 let 么,你这少用了一层。

题目的代码属于典型的货物崇拜,连代码是干啥的都不清楚就写上去,还“看不出有什么区别”

defvar 就不该用在 quasiquote 里,不在 toplevel 编译器根本看不到,当然会报警告

没看懂你想指责什么. 你想说我不明白这个代码的含义所以不该把它写下来, 不该问问题吗?

说这话你不觉得很??? 就是没看出来才问问题的好吗


另外你在 stack exchange 上的代码复现不出来

Ldbeth 用的应该不是 29 正式版,我猜

我大概知道原因了,不过得整理一下

* Lisp Changes in Emacs 29.1

** Interpreted closures are “safe for space”. As was already the case for byte-compiled closures, instead of capturing the whole current lexical environment, interpreted closures now only capture the part of the environment that they need.

The previous behavior could occasionally lead to memory leaks or to problems where a printed closure would not be 'read’able because of an un’read’able value in an unrelated lexical variable.

如果没问题的话应该就是这个,但是细节还要点时间分析

1 个赞

我大概知道为啥了…

上面,我们使用的都是 Emacs 29.1,在 Emacs 28.2 上,你给出的两段代码都是可以正常运行的。想来想去只能是 Emacs 29.1 引入的一个东西导致的: gnu.org/software/emacs/news/NEWS.29.1

* Lisp Changes in Emacs 29.1

** Interpreted closures are “safe for space”. As was already the case for byte-compiled closures, instead of capturing the whole current lexical environment, interpreted closures now only capture the part of the environment that they need.

The previous behavior could occasionally lead to memory leaks or to problems where a printed closure would not be 'read’able because of an un’read’able value in an unrelated lexical variable.

在 Emacs 28.2 中,位于 (let (_) 中的 (defvar sym)sym 会和 _ 一起添加到词法环境中,下面的例子可以说明这一点:

(let (_)
  (defvar yyy)
  (lambda ()
    (+ yyy 1)))
=> (closure (yyy (_) t) nil (+ yyy 1))

而在 Emacs 29 中,我们得到的是如下结果:

(let (_)
  (defvar yyy)
  (lambda () (+ yyy 1)))
=> (closure (t) nil (+ yyy 1))

我简单对比了一下 28 和 29 的 function 实现,发现 29 多了以下内容:

if (NILP (Vinternal_make_interpreted_closure_function))
        return Fcons (Qclosure, Fcons (Vinternal_interpreter_environment, cdr));
else
        return call2 (Vinternal_make_interpreted_closure_function,
                      Fcons (Qlambda, cdr),
                      Vinternal_interpreter_environment);

在 28 中没有 if,只有 Fcons 分支部分的代码。这个多出来的 Vinternal_make_interpreted_closure_function 应该做一些清理工作,去掉无用的词法环境变量。它绑定的函数位于 cconv.el 中,为 cconv-make-interpreted-closure 。它主要有这几个特点: (下面使用 trace-function 'cconv-make-interpreted-closure) 观察其行为:

  • 如果词法环境排除动态变量后为空,那么不进行修改
 (let ((lexvars (delq nil (mapcar #'car-safe env))))
    (if (null lexvars)
        ;; The lexical environment is empty, so there's no need to
        ;; look for free variables.
        `(closure ,env . ,(cdr fun))

这也对应 (let ()... 的情况:

(let ()
  (defvar yyyy)
  (lambda ()
    (+ 1 yyyy)))
======================================================================
1 -> (cconv-make-interpreted-closure (lambda nil (+ 1 yyyy)) (yyyy t))
1 <- cconv-make-interpreted-closure: (closure (yyyy t) nil (+ 1 yyyy))
  • 如果环境中存在词法绑定则继续,具体过程大概是寻找函数中 所需 的词法变量和动态变量,然后重新组合得到环境,寻找所需词法和动态变量的过程是通过调用 cconv-fv 完成的:
(fvs (cconv-fv expanded-form lexvars dynvars))

比较有意思的是它返回的动态变量列表似乎 总为空 ,它的返回值结构为 (LEX_LIST . DYN_LIST) ,例如如下调用:

(cconv-fv '((x) (+ x y a b)) '(y) '(a b))
=> ((y))

你可以在 Emacs 29 中多试几个例子。

动态变量列表总为空就导致生成闭包的 env 中没有动态变量,从而导致 byte-compile 时出现 warning

看上去很吓人是吧,不过整个 Emacs 29 既然都能正常使用那就说明这是没问题的。维护者能知道能这样做就行。老实说 LZ 的样例代码不是什么常规用法,还是像 LdBeth 所说遵守约定正常使用就行,不用在边缘情况下耍。虽然会报 Warning 但是生成的字节码是没有问题的,可以 disassemble 对比一下。

不过也很好玩就是了。

1 个赞

是的,你因为不知道代码作用胡乱写出来的不遵守代码规范东西触发了编程语言实现者完全没有预料到的边界条件,只能通过分析具体实现才能解释这个现象,这样的问题对“正常写代码”的别人来说没有实际帮助。

就是没看出来才问问题的好吗

defvar 和 let 的组合用法官方文档都有,显然你没认真查过。

至于复现问题,我手上没有电脑,手机上只有 28.2。

题目里面 defvar 是在 let 外面的,你这在里面,效果不一定一样。

是这样的, 但不妨碍问题被问出来, 只是你不喜欢而已.

我看过了, 不然为什么要这么回复 include-yy

我这里(macOS 10.13, Emacs 26.3 & 27.2 & 28.3 & 29.1)无法复现。

你应该给出测试环境、完整代码和测试步骤,你没有交代的部分有可能是关键信息。

那也是,不过原理就是我上面贴子解释的那样了

偷懒了, 版本信息我写在原帖上面, 这边 没搬运过来. 下次注意

问问题当然没关系,但是你这个代码来自另外一个主题,而且是一个功能上完全不必要的写法,为了不误导别人我建议你把原来的主题里代码修改了,如果你不知道怎么修改,我回去可以给你写如何修改的指导。

1 个赞

你发在 stackexchange 的帖子也没有交代清楚,这就是为什么评论那位也不能复现。

已改. 你要说把代码写得规范一点, 那当然合情合理. 但是遇到这种两种写法前后行为不一致的情形, 还是会纠结一下. 其实我本地的代码早上就改了, 因为这个报 warning 看着也麻烦, 但忍不住还是会过来再问一下.