今日のバグ
前書き
今日も今日とて、バグった Common Lisp コードを走らせてしまい、 SBCL を落としてしまいました。 自省のために一筆書きます。
対象のバージョンは以下です。
- SBCL 1.2.4-2ubuntu1 (Ubuntu Linux 15.04 上。 2015-09-30 時点での apt 上の最新)
- SBCL 1.2.15 (MacOS X 上。 2015-09-30 時点での homebrew 上の最新)
落としてしまったコード
処理系を落としてしまったコードと同じ状況を再現するコードが、以下になります。
(defun string-to-char-code-array (str) (declare (optimize (speed 3) (safety 0) (debug 0)) (type string str)) (with-input-from-string (in str) (loop for c of-type character = (read-char in nil :eof) for i of-type fixnum from 0 until (eq c :eof) collect (char-code c) into cc finally (return (make-array i :element-type 'fixnum :initial-contents (the list cc))))))
このコードのやっていることは、引数で受けとった文字列 (string
) を、 char-code
の配列に変換するというだけで、つまり以下と同様です。
(defun string-to-char-code-array-simple (str) (map 'vector #'char-code str))
実際のコードでは、 char-code
を取ったあとに、細かな処理をするのですが、その実コードはちょっと出せないので、そこを省いた疑似コードでご容赦ください。
何が起こったのか
上記の string-to-char-code-array
関数をコンパイルすると、こんなメッセージが出てきます:
; file: /private/var/tmp/tmp.T3teOn ; in: DEFUN STRING-TO-CHAR-CODE-ARRAY ; (MAKE-ARRAY I :ELEMENT-TYPE 'FIXNUM :INITIAL-CONTENTS (THE LIST CC)) ; --> LOCALLY MAKE-ARRAY ; ==> ; I ; ; note: deleting unreachable code ; ; compilation unit finished ; printed 1 note
「 I
は使われてないっぽいから消したからね」 って言われてます。
「えっ。 I
で数を数えてるし、 make-array
も呼んでるじゃない。なんだろう?」と思いながら、走らせてみると…
CL-USER> (string-to-char-code-array "abcd")
いつまで経っても値は返ってこず、それどころか、 Ctrl-C の割り込みさえも効かなくなってしまいました。 処理系の出力を見ると、以下のようなメッセージがありました。
fatal error encountered in SBCL pid 94418(tid 2953408512): Heap exhausted, game over.
「ヒープ使い切っちゃったんで終了です。」
どうしてこうなったのか
結論から言うと、 read-char
の戻り値を受ける変数の型宣言が間違ってました。
今回の呼び出しでは、 read-char
は character
か :eof
というシンボルを返すので、それを受ける変数への型宣言は (or character symbol)
が正しいです。
以下が修正済コードです。
(defun string-to-char-code-array (str) (declare (optimize (speed 3) (safety 0) (debug 0)) (type string str)) (with-input-from-string (in str) (loop for c of-type (or character symbol) ; <== ここ!!! = (read-char in nil :eof) for i of-type fixnum from 0 until (eq c :eof) collect (char-code c) into cc finally (return (make-array i :element-type 'fixnum :initial-contents (the list cc))))))
間違っていたコードでは、以下のような最適化がされてしまっていたのでしょう:
- 変数
C
はcharacter
しか取らない (と、私が書いてしまった。) - ということは、
C
はsymbol
ではないので、(eq c :eof)
は絶対に成立しない。無限ループだ。 - なので、
finally
以下のコードには到達しない。 - じゃあ
I
っていう変数は使わなくても大丈夫だよね - 「
I
は使われてないっぽいから消したからね」 というメッセージ
まとめ
(safety 0)
にしてたので悲惨な結果になりましたが、 (safety 3)
で実行すると、以下のエラーを出してちゃんと止まります。
The value :EOF is not of type CHARACTER. [Condition of type TYPE-ERROR]
(safety 0)
でぶっ込むのは、気をつけようね! というベタな結論に落ち着きました。