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

你没看到 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 看着也麻烦, 但忍不住还是会过来再问一下.

我研究了下,在 emacs SE 的问题上更新回答了。结论是 Emacs 在这里报 warning 是符合预期的,29.1 引入的 closure 优化在这里没有改变应有的语义,而题目里的代码的确也是不符合规范的。

包括题主和可能 @include-yy 在內都犯了个错,

(let (_) ...) 的写法并不是一个没有记载的特殊用法,它的行为是和 (let (_a) ...) 甚至 (let (a) ...) (... 中没有引用 _a / a) 的行为是一样的。

(equal
 (byte-compile
  (lambda ()
    (let (_)
      (defvar abc-foo)
      (let (abc-foo)
        (setq abc-foo 1)))))
 (byte-compile
  (lambda ()
    (let (_a)
      (defvar abc-foo)
      (let (abc-foo)
        (setq abc-foo 1))))))
;; t

反而是 (let () ...) 有会被编译器优化成 (progn ...) 的特殊性,所以解釋器版本的 (let () ...) 为了统一也做成了 nop。

3 个赞

大神都用这么可爱的头像吗

2 个赞

是这样的,我在想这个问题的时候看了 let 的代码,如果 binding 什么也没有是不会有新的作用域的。只要有一个变量就行,_ 并不是什么关键字

我看了下 byte-compile-file 如下的文件

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

是不会报 warning 的, 此处的 (let (_) ... 也没有

ldb: nullify the (defvar ... declaration in global scope.

另外这代码看上去挺常规吧, 我把题目的代码改一下.


只有在 C-x C-e 下列表达式时, 会有 warning:

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

似乎 解释器 和 被 byte-compile 调用的 byte-compiler 之间有信息差. 不知道 @include-yy 怎么看.

字节编译文件时编译器会收集信息的, defvar 相当于声明有这个变量

一般来说用户用不到 byte-compile , byte-compile 主要也是对文件来进行

因为 defvar 在没有绑定变量时是不会产生任何副作用的,而是单纯作为一个声明表达式由 eval 或者编译器进行魔法处理。

整个文件进行编译时,编译器会搜索到 toplevel 的 defvar 声明。

手动执行 byte compile 时,编译函数看到的是已经由解释器处理过的 closure,报错不是因为搜索不到 defvar 声明,而是因为这个 closure 本身是有引用自由变量。

报错来源是执行

(byte-compile '(closure (t) nil foo-fpp))

而在给编译器足夠信息时是不会报错的。

(progn (defvar foo-fpp)
       (let (_)
         (lambda () foo-fpp)))
(closure (t) nil foo-fpp)

(byte-compile '(progn (defvar foo-fpp)
                      (let (_)
                        (lambda () foo-fpp))))
;;; no warning, produce same byte code

会有这种疑问就是对编程语言的实现细节没有概念,写代码就是纯凭 guts feeling 瞎湊的体现。


在 scratch buffer 里,

(defvar foo-fpp)

(lambda () foo-fpp)
(closure (foo-fpp t) nil foo-fpp)

(lambda () foo-fpp) ;; 把 defvar 注释后
(closure (t) nil foo-fpp)

本来还以为 29.1 的 closure 优化有 bug,结果只是虛惊一场忘删 defvar 了。

2 个赞