原来有人在前几天已经提了一个 issue 了
我在 github actions 里的机器上使用 emacs 29.0.50 测试了一下,发现只有在第 1 次运行的时候会出现 bar is not a defined segment
错误,在第 2 次进行时就没问题了。
如下代码即可精准复现。
;; 假设本文件名 a.el, 则通过 emacs -Q -l a.el 即可复现
(require 'package)
(setq package-archives
'(("melpa" . "https://melpa.org/packages/")
("gnu" . "https://elpa.gnu.org/packages/")))
(package-initialize)
(unless (package-installed-p 'doom-modeline)
(package-refresh-contents)
(package-install 'doom-modeline))
;; 删除所有的 .elc 文件,再模拟安装时的编译过程
(cl-loop for path in load-path
when (string-match-p "doom-modeline" path)
do (let ((default-directory path))
(shell-command "find . -iname '*.elc' -delete")
(byte-recompile-directory "." 0 t)))
(require 'doom-modeline) ;; 这里会挂
(load-theme 'doom-one t)
下面探索了一下为什么会出现这种情况。
xxx is not a defined segment
错误是在 doom-modeline--prepare-segments
里抛出的。
(defun doom-modeline--prepare-segments (segments)
"Prepare mode-line `SEGMENTS'."
(let (forms it)
(dolist (seg segments)
(cond ((stringp seg)
(push seg forms))
((symbolp seg)
(cond ((setq it (cdr (assq seg doom-modeline-fn-alist)))
(push (list :eval (list it)) forms))
((setq it (cdr (assq seg doom-modeline-var-alist)))
(push it forms))
((error "%s is not a defined segment" seg))))
((error "%s is not a valid segment" seg))))
(nreverse forms)))
例如 bar
这个 segment 是这样定义的:
(doom-modeline-def-segment bar
"The bar regulates the height of the mode-line in GUI."
(if doom-modeline-hud
(doom-modeline--hud)
(doom-modeline--bar)))
再继续深入下去看一下 doom-modeline-def-segment
的实现,
(defmacro doom-modeline-def-segment (name &rest body)
"Defines a modeline segment NAME with BODY and byte compiles it."
(declare (indent defun) (doc-string 2))
(let ((sym (intern (format "doom-modeline-segment--%s" name)))
(docstring (if (stringp (car body))
(pop body)
(format "%s modeline segment" name))))
(cond ((and (symbolp (car body))
(not (cdr body)))
(add-to-list 'doom-modeline-var-alist (cons name (car body)))
`(add-to-list 'doom-modeline-var-alist (cons ',name ',(car body))))
(t
(add-to-list 'doom-modeline-fn-alist (cons name sym))
`(progn
(fset ',sym (lambda () ,docstring ,@body))
(add-to-list 'doom-modeline-fn-alist (cons ',name ',sym))
,(unless (bound-and-true-p byte-compile-current-file)
`(let (byte-compile-warnings)
(byte-compile #',sym))))))))
由于 bar
segment 不满足「在去掉 docstring 之后至少还有 2 个元素,且满足第一个元素为 symbol」,说明 bar
segment 会被放在 doom-modeline-fn-alist
中。在 package-install
时默认就会编译所有的 .el
文件,所以后面的 byte-compile
不会被执行。
再回过头去看错误抛出的函数 doom-modeline--prepare-segments
, 由于已经已知对应的的元素必定会在 doom-modeline-fn-alist
中,所以只需要知道什么原因使得
(setq it (cdr (assq seg doom-modeline-fn-alist)))
返回了 nil
就行了.
打开 emacs 查看 doom-modeline-fn-alist
的值,发现是这样的:
...
(#<symbol hud at 64346> . doom-modeline-segment--hud)
(#<symbol bar at 64178> . doom-modeline-segment--bar)
...
然后在 scratch 里观察 (assq 'bar doom-modeline-fn-alist)
的值,发现表达式的值为 nil
…
查阅 emacs lisp reference 得知 #<symbol bar at 64178>
叫作 “bare symbol”,通常由 byte compiler 产生。
于是开始猜想是不是 byte compiler 最近有变动导致生成的字节码不同?
于是就开始了漫长的 git bisect
, 附 git bisect log
:
git bisect start
# good: [2dad332a1439b59a62cd5ed0d8e3626d9e91e3e5] (hack-local-variables--find-variables): Use `user-error`
git bisect good 2dad332a1439b59a62cd5ed0d8e3626d9e91e3e5
# bad: [82aa5be7ce1d5f508d42a4bb394760198a1c6e62] ; Merge from origin/emacs-28
git bisect bad 82aa5be7ce1d5f508d42a4bb394760198a1c6e62
# good: [41846901e22e824f02796012164c51df0297c6ec] Improve dired-do-create-files slightly
git bisect good 41846901e22e824f02796012164c51df0297c6ec
# bad: [067e84116dde36a2e058e3915fe81c818a21e40a] ; * src/bytecode.c (exec_byte_code): Silence GCC warning
git bisect bad 067e84116dde36a2e058e3915fe81c818a21e40a
# bad: [d0f3de72b678608677e1021f3e3c4dd42935b537] Allow using outline minor mode in `M-x apropos-value'
git bisect bad d0f3de72b678608677e1021f3e3c4dd42935b537
# bad: [df49e3a3ab4cddf1e3c0f5482c7fdd809d8a8884] Merge branch 'master' of /home/acm/emacs/emacs.git/master
git bisect bad df49e3a3ab4cddf1e3c0f5482c7fdd809d8a8884
# bad: [bdd9b5b8a0d37dd09ee530c1dab3a44bee09e0f8] Miscellaneous amendments to the scratch/correct-warning-pos branch
git bisect bad bdd9b5b8a0d37dd09ee530c1dab3a44bee09e0f8
# bad: [4e77177b063f9da8a48709aa3ef416d0ac21837b] Try to make scratch/correct-warning-pos build on Windows and not segfault
git bisect bad 4e77177b063f9da8a48709aa3ef416d0ac21837b
# bad: [8f1106ddf2a3861e9c1ebb9d8fa3d4087899de81] Several amendments to scratch/correct-warning-pos.
git bisect bad 8f1106ddf2a3861e9c1ebb9d8fa3d4087899de81
# bad: [368570b3fd09d03ac5b9276d1ca85ae813c3f385] First commit of scratch/correct-warning-pos.
git bisect bad 368570b3fd09d03ac5b9276d1ca85ae813c3f385
# first bad commit: [368570b3fd09d03ac5b9276d1ca85ae813c3f385] First commit of scratch/correct-warning-pos.
一看就知道,368570b3fd09d03ac5b9276d1ca85ae813c3f385
是跟 bare symbol 相关的。尝试着 git revert
但是有太多冲突了,只能仔细看这个 commit 的改动是啥了。
由于这个提交太大了,源码理解在最后再进行,这里先从库的角度看是否可以规避。
再次观察 doom-modeline-def-segment
的定义
(defmacro doom-modeline-def-segment (name &rest body)
"Defines a modeline segment NAME with BODY and byte compiles it."
(declare (indent defun) (doc-string 2))
(let ((sym (intern (format "doom-modeline-segment--%s" name)))
(docstring (if (stringp (car body))
(pop body)
(format "%s modeline segment" name))))
(cond ((and (symbolp (car body))
(not (cdr body)))
(add-to-list 'doom-modeline-var-alist (cons name (car body)))
`(add-to-list 'doom-modeline-var-alist (cons ',name ',(car body))))
(t
(add-to-list 'doom-modeline-fn-alist (cons name sym))
`(progn
(fset ',sym (lambda () ,docstring ,@body))
(add-to-list 'doom-modeline-fn-alist (cons ',name ',sym))
,(unless (bound-and-true-p byte-compile-current-file)
`(let (byte-compile-warnings)
(byte-compile #',sym))))))))
发现出现了两行 add-to-list
调用。根据这里的说明
没有用 backquote 包起来的 add-to-list
在编译时会执行一次。之后再运行 elc 文件时这行 add-to-list
已经不存在了。实际上去掉这一行也是没问题的,因为下面的另一个 add-to-list
总会被调用,无论是在解释执行 el 文件或是编译成了 elc 再执行。
所以这里可以安全地将不正确的 add-to-list
调用给注释掉。经验证, bar is not a defined segment
错误不再出现。
容易得到以下结论:
在 首次 安装 doom-modeline
进行 byte-compile 的时候
(add-to-list 'doom-modeline-fn-alist (cons name sym))
此处的 name
会是一个 bare symbol #<symbol bar at 64178>
,所以此时 doom-modeline-fn-alist
里的符号都是 #<symbol xxx at yyy>
这种形式。又因为 'bar
与 #<symbol bar at 64178>
满足 equal
,add-to-list
通过 equal
来判断元素是否相等,所以第二个 add-to-list
相当于没做什么。
但是在第二次运行时,由于已经编译完成,不会将 bare symbol 给添加到 doom-modeline-fn-alist
中,所以在执行第二个 add-to-list
时会把普通的 'bar
给插入至链表。
而这里真正的问题是 assq
里使用的 eq
来判断符号是否相等,但是
(car (car doom-modeline-fn-alist))
;;=> #<symbol follow at 127713>
(eq (car (car doom-modeline-fn-alist)) 'follow)
;;=> nil
(equal (car (car doom-modeline-fn-alist)) 'follow)
;;=> t
显示带有 position 信息的符号与普通的符号不满足 eq
.
所以目前的解决方案有 2 个:
- 将
这一行和上面的那行 add-to-list
给删掉。
- 将
doom-modeline--prepare-segments
函数内的assq
换成assoc
,不过倒是有一个奇怪的现象,明明assoc
默认就是用的equal
,但是在不显式指明equal
的时候居然找不到
(car (car doom-modeline-fn-alist))
;;=> #<symbol follow at 127713>
(equal (car (car doom-modeline-fn-alist)) 'follow)
;;=> t
(assoc 'follow doom-modeline-fn-alist)
;;=> nil
(assoc 'follow doom-modeline-fn-alist 'equal)
;;=> (#<symbol follow at 127713> . doom-modeline-segment--follow)