y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

今日のバグ

前書き

今日も今日とて、バグった 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-charcharacter: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))))))

間違っていたコードでは、以下のような最適化がされてしまっていたのでしょう:

  1. 変数 Ccharacter しか取らない (と、私が書いてしまった。)
  2. ということは、 Csymbol ではないので、 (eq c :eof) は絶対に成立しない。無限ループだ。
  3. なので、 finally 以下のコードには到達しない。
  4. じゃあ I っていう変数は使わなくても大丈夫だよね
  5. I は使われてないっぽいから消したからね」 というメッセージ

まとめ

(safety 0) にしてたので悲惨な結果になりましたが、 (safety 3) で実行すると、以下のエラーを出してちゃんと止まります。

The value :EOF is not of type CHARACTER.
   [Condition of type TYPE-ERROR]

(safety 0)でぶっ込むのは、気をつけようね! というベタな結論に落ち着きました。