Gauche(Scheme) でデバッグをする4つの方法

Gauche でコードを書いているときにコードが意図どおりに動かないことがあります。そのような場合にデバッグする方法を4つ紹介します。

前提

まず Gauche はリリースされている最新版を使った方が良いでしょう。Linuxディストリビューションによってはパッケージが古い場合あります。
またScheme関数型言語なので、デバッグの単位は関数(手続き)ごとに行うことが多いです。一つ一つの手続きが意図どおり動いているのか?を調べながら進めるのが基本になります。

方法1 print デバッグ

Gauche には今のところデバッガがありませんから基本的には print デバッグがメインとなります。単純な print デバッグから見ていきましょう。
以下のような sum という手続きで print デバッグしてみましょう。

(define (sum n)
  (if (= n 1)
      1
      (+ n (sum (- n 1)))))


例えば n の値を見てみたければ

(define (sum n)
  (print "n=" n) ; ←ここ
  (if (= n 1)
      1
      (+ n (sum (- n 1)))))

とすれば良いでしょう。 print には任意の個数の引数が渡せます。


もう少し整形したい場合は format 手続きを利用して

(define (sum n)
  (format #t "(n, ~a)" n)
  (if (= n 1)
      1
      (+ n (sum (- n 1)))))

のように書くことが出来ます。format の第一引数が #t の場合は現在の出力ポートに出力という意味です。

方法2 標準エラー出力

標準出力に print すると出力が混じってしまって都合が悪いときがあります。
そんなときは標準エラー出力 (current-error-port) に出力しましょう。
毎回 (display "hoge" (current-error-port)) は面倒なので log 手続きを用意します。

(define (log . msg)
  (display msg (current-error-port)))

(define (sum n)
  (log "n=" n)
  (if (= n 1)
      1
      (+ n (sum (- n 1)))))

log は引数の個数を可変にして手を抜いています。

方法3 リーダマクロ #?=

print デバッグはタイプ数が多くて面倒だし、(sum (- n 1)) の結果を print したい場合は一度 let に格納しないといけないので不便です。

(define (sum n)
  (if (= n 1)
      1
      (+ n (sum (- n 1)))))


そんなときはリーダマクロを使いましょう。名前はごついですが全然難しいことはありません。出力したい場所に #?= と書けば OK です。

(define (sum n)
  (if (= n 1)
      1
      (+ n #?=(sum (- n 1))))) ;; ←ここ


これを実行すると

(sum 3)
#?="(stdin)":24:(sum (- n 1))
#?="(stdin)":24:(sum (- n 1))
#?-    1
#?-    3
=> 6

なんと (sum 1) の (sum 2)結果が #?- に引き続いて出力されています。これは便利!。
おそらくこれがきっと一番便利だと思います。

方法4 trace を使う

SLIB という外部ライブラリを Gauche にインストールすると trace という便利な機能が使えます。
SLIB のインストール方法はGauche FAQ - SLIBは使えますかを参照してください。
さっそく使ってみましょう。

;; sum には何も手を入れない
(define (sum n)
  (if (= n 1)
      1
      (+ n (sum (- n 1)))))

(use slib) ;; おまじない
(require 'trace) ;; おまじない
(trace sum) ;; sum を trace してくれという意味

これで sum を実行すると

(sum 3)
CALL sum 3
 CALL sum 2
  CALL sum 1
  RETN sum 1
 RETN sum 3
RETN sum 6
=> 6

sum がどのような引数でどういう順序で呼ばれたかを見ることが出来ます。
これを利用すればどこで引数の値がおかしくなったか?などが分かります。


以上 Gauche における4つのデバッグ方法を紹介しました。
なお Emacs を使っている方は今回紹介した方法と、gca.elを組み合わせれば最強でしょう。

上級者向け番外編

変態上級者向けの情報をいくつか。
Gaucheスタックトレースはインライン展開最適化の影響で、該当個所のコード行番号がおかしい時があるようです。疑わしい時は gosh -f no-inline で起動すると改善するかもしれません。


「俺のコードは100%正しいはずなのに動作がおかしい!」というときは disasm 手続きでディスアセンブルしましょう。
もしかしたら意図しないコードが吐かれていることがあるかもしれません。(僕は一度もありませんが)

(disasm (lambda (x) x))
main_code (name=#f, code=0x8136aa8, size=2, const=0, stack=0):
args: #f
     0 LREF0                    ; x
     1 RET 
=> #<undef>


「俺のコードは完璧なのになぜか遅い!」というときは gosh -p time でプロファイラを動かしてみると良いかもしれません。


「俺の完璧なコードが最適化の前後でどう変わるか見てみないと気が済まない!」という場合はコンパイラに手を入れてください。
具体的には src/compile.scm を書き直して make しなおせばよいです。

;; compile:: Sexpr, Module -> CompiledCode
(define (compile program module)
  (let1 cenv (if (module? module)
               (make-bottom-cenv module)
               (make-bottom-cenv))
    (guard
        (e
         (else
          ;; TODO: check if e is an expected error (such as syntax error) or
          ;; an unexpected error (compiler bug).
          (let1 srcinfo (and (pair? program)
                             (pair-attribute-get program 'source-info #f))
            (if srcinfo
              (errorf "Compile Error: ~a\n~s:~d:~,,,,40:s\n"
                      (slot-ref e 'message) (car srcinfo)
                      (cadr srcinfo) program)
              (errorf "Compile Error: ~a\n" (slot-ref e 'message))))))
      (let1 p1 (pass1 program cenv)
        (pp-iform p1) ;; ←ここで pass1(パース)結果を pretty-print する!
        (pass3 (pass2 p1)
               (make-compiled-code-builder 0 0 '%toplevel #f #f)
               '() 'tail))
      )))

最後になりましたがもっと良いデバッグ方法がある場合はぜひ教えてください。よろしくおねがいします。

追記

id:scinfaxiさんより debug-print のカスタマイズの方法を教えていただきました。ありがとうございます。


id:yuum3さんから d 手続きを教えていただきました。ありがとうございます。
Gauche ユーザリファレンス: 9.8 gauche.interactive - インタラクティブセッション