nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

ついにeval-whenにたどり着く

compile-file関数を実装して行ったら、 最後の最後にeval-whenにたどり着きました。
eval-whenは難しいですね。
この機能を調べても全然意味が分かりません。

何に使うものなのかを簡潔に書くことすら難しいです。
たぶん「compile-fileで、コンパイルだけではなく同時に実行もしたいときに使うもの」です。

知らなかったのですがcompile-file関数はコンパイルだけじゃなく、 場合によっては実行もするんだそうです。
じゃあ何をいつ実行するんだというのを制御するのがeval-whenです。

典型的な例文を示します。
下記のファイルをコンパイルしてみましょう。

;; aaa.lisp
(defun aaa ()
  :hello)

(defmacro bbb ()
  `(format t "~A~%" ,(aaa)))

(bbb)

コンパイルします。

* (compile-file #p"aaa.lisp")
★エラー、undefined function AAA

どういうことかというと、compile-file(defun aaa ...)という文を コンパイルはするものの実行はしないため、 マクロbbbが関数aaaを呼び出せなかったというエラーです。
sbclでは何故かwarningだけで済むのですが、 何をどうしたからそうなったのか良くわかりません。 (faslを実行するとエラーになります)

解決策は、eval-whenを用いて コンパイル時に(defun aaa ...)を実行させることです。

;; aaa.lisp
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun aaa ()
    :hello))

(defmacro bbb ()
  `(format t "~A~%" ,(aaa)))

(bbb)

コンパイルします。

* (compile-file #p"aaa.lisp")

実行します。

* (load #p"aaa.fasl")
HELLO

うまく行きました。

では、ちゃんと解説していきます。
eval-whenとはspecial formであり、 下記の条件を色々と判断してコードを処理します。

  • toplevelかどうか
  • compile-timeかどうか
  • eval-whenの引数はどうか

順に説明します。

toplevelかどうか

toplevelとはLispファイルの一番左側に記載されているものです。 下記の例の場合、

(aaa)

(let ((x 10))
  (bbb))

(aaa)がtoplevel、 (let ...)がtoplevelであり、 (bbb)はtoplevelではありません。

例外が規約で決まっており、 toplevelのprogn, locally, macrolet, symbol-macroletimplicit progn部もtoplevelになります。
あとeval-whenimplicit prognも同じですね。

例えばこんな感じ。

(progn
  (defun aaa ()
    :hello)
  (bbb))

(progn ...)はtoplevel、 (defun ...)はtoplevel、 (bbb)はtoplevelであり、 :helloはtoplevelではありません。

compile-timeかどうか

規約で正式に次のような名称が決まっています。

  • not-compile-timeモード
  • compile-time-tooモード

コンパイル時は実行しない」モードと「コンパイル時も実行する」モードです。
名前の通り、not-compile-timeの時はコンパイル時には実行しません。
コンパイルだけして終わりです。
しかしcompile-time-tooの時はコンパイルをしたあと実行されます。
compile-file関数で「実行も」するのです。

コンパイルを開始した瞬間はnot-compile-timeモードです。
どうしたらcompile-time-tooモードになるのかというと、 eval-whenの引数が切り替えのきっかけを与えます。

eval-whenの引数はどうか

まずは話の流れから、compile-time-tooモードになる場合を説明します。
not-compile-time->compile-time-tooモードに変更するには、 eval-when実行時に

  • toplevelである
  • :compile-toplevelを引数に指定する
  • :load-toplevelを引数に指定する

が同時に満たされている必要があります。
例えばこんな感じ。

(eval-when (:compile-toplevel :load-toplevel :execute)
  ...)

逆に、compile-time-too->not-compile-timeモードに変更するには、 eval-when実行時に

  • toplevelである
  • :load-toplevelだけを引数に指定する

が同時に満たされている必要があります。
つまりこんな感じ。

(eval-when (:load-toplevel)
  ...)

ANSI Common Lispの規約には次のような便利な表があります。

http://www.lispworks.com/documentation/HyperSpec/Body/03_bca.htm

CT   LT   E    Mode  Action    New Mode
----------
Yes  Yes  ---  ---   Process   compile-time-too
No   Yes  Yes  CTT   Process   compile-time-too
No   Yes  Yes  NCT   Process   not-compile-time
No   Yes  No   ---   Process   not-compile-time
Yes  No   ---  ---   Evaluate  ---
No   No   Yes  CTT   Evaluate  ---
No   No   Yes  NCT   Discard   ---
No   No   No   ---   Discard   ---

cltl2の方はちょっとわかりづらいので、 ANSI Common Lispの方を見た方がいいと思います。

ActionのProcessは、モードを変更してから処理することを意味します。
もしnot-compile-timeモードの時にtoplevelのeval-when:compile-toplevel:load-toplevel:executeを指定してコンパイルした場合、

  • New Modeのcompile-time-tooモードに移行
  • コードをコンパイルする
  • :compile-toplevel指定なので実行する

という感じになります。

Evaluateは実行を意味します。
eval-whenの引数か現在のcompile-timeモードに応じて、 コンパイル時、ロード時、その他の時に実行するかどうかを決定します。
たぶんどれかは実行されるでしょう。Evaluateなので。
ひとつも実行されない状況もありますが、 それは次に説明するDiscardに該当します。

Discardは破棄です。
コンパイル時、ロード時、その他の時に実行されず、内容は捨てられてしまいます。
では、この破棄というのは一体どのフェーズからなんでしょうか?
どういうことかというと、

(eval-when ()
  (quote aaa bbb ccc))  ;; どうみても構文エラー

みたいな文は正しく受理されるのか?
実験では受理されました。
sbcl, clisp, cclで確認しました。
つまりeval-whenimplicit progn部は、 readはされますがパース処理はされないということです。

ものすごく最初の段階から削除されることが分かります。
nptではevalを行う際に、一般的なコンパイラフェーズのように 字句解析、構文解析、意味解析、コード生成みたいな段取りを経ているのですが、 evalの初段である構文解析の段階で削除されます。
一応断っておくと、字句解析はevalではなくread関数です。

モードの切り替えについてもう少し

compile-timeモードの切り替えは、 toplevelじゃないと動作しないのでしょうか?
これはyesです。
下記の文を考えます。

(eval-when (:compile-toplevel :load-toplevel :execute)
  (eval-when (:execute)
    (format t "compile-time-too~%")))

外側のeval-whenにてcompile-time-tooモードに変更しています。
内側のeval-whenは、compile-time-tooモードの時だけ出力を行うというものです。
このソースをcomple-fileで実行すると、 format文が実行されてcompile-time-tooという文字が出力されます。
では、toplevelではない場合はどうなるでしょうか。

(let ()
  (eval-when (:compile-toplevel :load-toplevel :execute)
    (eval-when (:execute)
      (format t "compile-time-too~%"))))

comple-fileを実行すると出力はされませんでした。
つまり、toplevelじゃないのでcompile-time-tooモードに移行しなかったということです。

ではモードを切り替えた後、:executeの判定がtoplevelに従うかどうかを見て行きます。
具体的には下記の動作がtoplevelに関係するかどうかです。

No   No   Yes  CTT   Evaluate  ---
No   No   Yes  NCT   Discard   ---

結果は、toplevelに関係します。
これは意外でしたが、sbcl, clisp, cclともに同じ動作になりました。

下記の文をコンパイルしてみます。

(eval-when (:load-toplevel)
  (eval-when (:execute)
    (format t "compile-time-too~%")))

not-compile-mode移行後に:executeのみ指定なので、 Discardとなり実行されません。
しかし、format文がtoplevelではない場合はどうなるでしょうか。

(eval-when (:load-toplevel)
  (let ()
    (eval-when (:execute)
      (format t "compile-time-too~%"))))

コンパイル結果は下記の通り。

compile-time-too

実行されました。
モードによる処理の判定が、toplevelを前提にしていることが分かります。

eval-whenの引数の意味

eval-whenの構文は下記の通り。

(eval-when (stuation*)
  ...)

stuationは、:compile-toplevel, :load-toplevel, :executeのどれかです。

:compile-toplevelは、toplevelのフォームを コンパイル時に実行するようにします。
:load-toplevelは、toplevelのフォームを load関数で読み込むときに実行するようにします。
:executeは、toplevelに限らずフォームを実行するようにします。

説明してきたように、 組み合わせによってコンパイル時、ロード時に実行するようになり、 compile-timeというモードも変更されたりします。
もしフォームを破棄する場合はnilが返却されます。

eval-whenが全く現れなかった場合、 デフォルトの状態はどうなっているのでしょうか?
標準では、コンパイル時に実行されないということなので、 not-compile-timeモードのときの、

(eval-when (:load-toplevel :execute)
  ...)

で実行されているのだと思います。

compile-file以外の関数について

今まではcompile-file関数で実験してきましたが、 式の実行とコンパイルは別の関数でも行われます。
列挙すると下記の通り。

  • eval-loop
  • eval
  • compile
  • load(テキストファイルの読み込み)

これらの機能では、eval-whenはどう動くのでしょうか?
上記4例は全て、:executeしか見ません。
さらにtoplevelとはみなされません。
よって、:compile-toplevel:load-toplevelが意味をなさなくなるのです。
toplevel扱いされないため、モード切替もありません。
:executeだけを見て実行するか破棄するかを判断します。
eval-whenはほぼcompile-file関数のためのモノなんですね。

ただloadに関しては、toplevelを完全に無視しているわけではないと思います。
toplevel formは、readで読み込まれた順番に評価して実行するという決まりがあり、 load関数でもそれを忠実に守らないとcompile-fileとの実行に差異が生じてしまいます。
実際は機能しているのかもしれませんが、 それを判断できる方法が見つかりませんでした。

定義の文はどうなるのか

toplevelに記載された定義については、 何の指定をしなくてもコンパイル時にも実行される場合があります。
例えばdeftype
こういうのはコンパイル時にも実行しておかないと、 あとに続く文を正常に解釈できない可能性があるためです。

ではどの定義がそうなんだというと、 ちょっと簡単には説明できないので話題にはしませんが、 基本的な考えは勝手に:compile-toplevelが付与されるということでいいと思います。