その発想はなかった! 新しい自動バイトコンパイルでEmacsを高速化する

Emacs Lispをバイトコンパイルすると動作が高速化するのは常識である。しかし、バイトコンパイルには致命的な欠点があって、Lispファイルの方がバイトコンパイルファイルよりも新しい場合は、古いバイトコンパイルファイルが読み込まれてしまうのだ!!そのため、Lispファイルを更新したらバイトコンパイルしておかないといけない。
このどうしようもない仕様をなんとかするべく、自動バイトコンパイルで自衛をしている人はけっこういると思う。というか、自動バイトコンパイルがないと絶対に泥沼にはまってしまう。

自動バイトコンパイルとは、Lispファイルを保存したときに after-save-hook をつかって自動でバイトコンパイルをするというもの。しかし、これだとバイトコンパイル中は待たされてしまう。シングルスレッドの悲劇。
Emacsで同時に複数の処理を実行するには、タイマーを使うか、外部プロセスに任せるか、しかない。
バイトコンパイルコマンドラインから行うことができる。 emacs -Q -batch -f batch-byte-compile hogehoge.el って感じでbatch modeを使ってバイトコンパイルする。これを使えば、待たされることなくバイトコンパイルすることができる!!

バイトコンパイルスクリプトを作成する

しかし、話はそう簡単ではない。ファイルをバイトコンパイルするときには、ファイルのトップレベルのrequireを読むのである。そのため、バイトコンパイル時に依存Emacs Lispが読み込めないといけない。load-pathを適宜追加しないといけないのだ。

では、そのload-pathをどう追加するか?
load-pathは通常 (add-to-list 'load-path "/path/to/elisp") というS式で追加する。*1ならば、.emacsの中からこの式を正規表現で切り取れば新たに追加されたload-pathを得ることができる。

そして、batch-byte-compileのコマンドラインに -l load-path1 -l load-path2 という感じで追加されたload-pathを加える。
で、完成したのが以下のRubyスクリプト。byte-compile という名前で保存して実行属性をつけよう。Ruby 1.9でないと動かないからね!
EMACSEmacs実行ファイル名、GLOBSが初期化ファイル(.emacs)が読み込むファイルのワイルドカードの配列、DEFAULT_OPTSが最初につけるオプション。俺はclマクロ常用しているので予め読み込ませている。~/emacs/initfuncs.elは自作マクロなどが入っている。これもないとバイトコンパイルができない。

http://www.rubyist.net/~rubikitch/archive/byte-compile にも置いている。

#!/usr/local/bin/ruby191
# byte-compile from command line
# Needs Ruby 1.9.x
EMACS = "emacs-snapshot"
GLOBS = ["~/.emacs.el", "~/emacs/init.d/*.el"]
DEFAULT_OPTS = ["-l", "cl", "-L", ".", "-l", File.expand_path("~/emacs/initfuncs.el"),]
def extra_load_path(files)  # extract load-path from init files
  files.map{|file| File.read(file).scan(/add-to.+load-path.+"(.+?)"|push "(.+?)" load-path/) }.flatten.compact
end
load_path_opts = extra_load_path(GLOBS.map{|g| Dir[File.expand_path(g)]}.flatten).map{|f| ["-L", f]}.flatten
exec EMACS, "-Q", "-batch", *load_path_opts, *DEFAULT_OPTS, "-f", "batch-byte-compile", *ARGV

自動非同期バイトコンパイルの設定

今度はEmacsサイド。auto-async-byte-compile-modeというマイナーモードを定義している。これをemacs-lisp-mode-hookに加えておく。auto-async-byte-compileコマンドは外部コマンドとしてバイトコンパイルを起動させる。

(define-minor-mode auto-async-byte-compile-mode
"With no argument, toggles the mode.
With a numeric argument, turn mode on iff ARG is positive."
  nil "" nil
  (if auto-async-byte-compile-mode
      (add-hook 'after-save-hook 'auto-async-byte-compile nil 'local)
    (remove-hook 'after-save-hook 'auto-async-byte-compile 'local)))

(defun auto-async-byte-compile ()
  (interactive)
  (and buffer-file-name
       (string-match "\\.el$" buffer-file-name)
       (executable-interpret (format "byte-compile %s" buffer-file-name))))
(add-hook 'emacs-lisp-mode-hook 'auto-async-byte-compile)

追記[2010/01/08]

コメントありがとうございます。pushにも対応させました。

*1:consとsetqで追加するのはダサいので書き換えようねw