4.19. コンパイラをデバッグする

ここより先はハックする者の領域である。繰り返す。ここより先はハックする者の領域である。(警告したよ)

4.19.1. コンパイラの中間構造を出力する

-ddump- pass

過程<pass>(頻繁に使われるものについては短縮形がある)の後にデバッグ情報を出力する。-v5を使うことでこれら全てを一遍に得られる。(大量の出力になる)。あるいは、-v4を使うことで大部分を得られる。-ddump-to-fileを渡すと、標準出力の邪魔をするのを防ぐことができる。特に重要なのは下記の通りである。

-ddump-parsed:

パーサの出力

-ddump-rn:

名前変更器(renamer)の出力

-ddump-tc:

型検査器の出力

-ddump-splices:

接合するTemplate Haskellの式と、その評価結果のHaskellコードを出力する。

-ddump-types:

モジュールのトップレベルで定義されている全ての値について型シグネチャを出力する。この一覧は辞書順にソートされている。-dppr-debugを使うと、インポートされたもの及びシステム定義のものの型シグネチャも出力する。これはコンパイラをデバッグするときに便利である。

-ddump-deriv:

自動導出インスタンス

-ddump-ds:

脱糖器の出力

-ddump-spec:

特殊化過程の出力

-ddump-rules:

このモジュールで指定された書き換え規則を全て出力する。7.21.6. 書き換え規則の中で何が起こるか制御するを見よ。

-ddump-rule-firings:

このモジュールで発動した規則の名前を全て出力する

-ddump-rule-rewrites:

このモジュールで発動した全ての規則に関して詳細な情報を出力する

-ddump-vect:

ベクトル化器の出力を表示する。

-ddump-simpl:

単純化器の出力(「コアからコアへ」の過程)

-ddump-inlinings:

単純化器からのインライン化情報

-ddump-cpranal:

CPR解析の出力

-ddump-stranal:

正格性解析器の出力

-ddump-strsigs:

正格性シグネチャ

-ddump-cse:

CSE過程の出力

-ddump-worker-wraper:

worker/wrapper分割の出力

-ddump-occur-anal:

「出現解析」の出力

-ddump-prep:

コアpreparation過程の出力

-ddump-stg:

「STGからSTGへ」過程の出力

-ddump-flatC:

平坦化された(flattened)抽象C

-ddump-cmm:

C--のコードを表示する。

-ddump-opt-cmm:

C--からC--への最適化過程の結果を表示する。

-ddump-asm:

ネイティブコード生成器が出力するアセンブリ言語

-ddump-llvm:

LLVMコード生成器が出力するLLVMコード

-ddump-bcos:

バイトコードコンパイラの出力

-ddump-foreign:

foreign exportのスタブを出力

-ddump-simpl-phases:

単純化器の毎回の出力を見せる。-dverbose-core2coreでも足りないときに使う。

-ddump-simpl-iterations:

単純化器の毎回の繰り返し毎の出力を見せる。(単純化器の一回の実行に対して繰り返し回数の最大値が決まっている。通常4)。これは-ddump-simpl-phasesよりさらに多くの情報を出力する。

-ddump-simpl-stats

変換の種類毎に、何回実行されたかの統計情報を出力する。-dppr-debugを追加すればより詳細な情報が得られる。

-ddump-if-trace

インタフェースローダが自分のやっていることを*事細かに*報告するようにする。

-ddump-tc-trace

型検査器が自分のやっていることを*事細かに*報告するようにする。

-ddump-vt-trace

ベクトル化器が自分のやっていることを*事細かに*報告するようにする。

-ddump-rn-trace

名前変更器が自分のやっていることを*事細かに*報告するようにする。

-ddump-rn-stats

名前変更器がどんな種類の情報を収集しなければならなかったかについての要約を表示する。

-dverbose-core2core , -dverbose-stg2stg

中間の「コアからコアへ」および「STGからSTGへ」過程の出力をそれぞれ表示する。(たくさんの出力になるよ!)。なので、本当に自暴自棄になったときには、次のように打つ。

% ghc -noC -O -ddump-simpl -dverbose-core2core -dcore-lint Foo.hs
-dshow-passes

それぞれの過程を、発生するごとに表示する。

-ddump-core-stats

最適化パイプラインの最後に、コア言語のプログラムの大きさの要約を一行で表示する。

-dfaststring-stats

コンパイラによるfast stringの使用についての統計情報を表示する。

-dppr-debug

デバッグ出力にはいくつかの「様式」がある。例えば型を表示する場合、「利用者」様式(デフォルト)では、コンパイラが内部的に持っている型についての情報をできる限りHaskellソースファイルの構文に従って表す。「デバッグ」様式で型が表示されるときには、明示的なforallが付き、変数には固有IDが添付される。(このため、一見同じだが実は異なるものを見分けることができる)。このフラグは、デバッグ出力を、より多弁なデバッグ様式で行うようにする。

4.19.2. 出力の整形

-dppr-user-length

エラーメッセージにおいて、式は決まった「深さ」までしか表示されず、それを超えた部分式は省略記号になる。このフラグはその深さを決めるものである。デフォルト値は5。

-dppr-colsNNN

デバッグ出力の幅を設定する。コードが過度に折り返されるときにはこれを使うと良い。例えば、-dppr-cols200

-dppr-case-as-let

選択肢が一つであるcase式を、正格なlet式であるかのように表示する。これは、大量のボックス外しを伴うコードを扱うときに助けになる。

-dno-debug-output

求められた以外のデバッグ出力を全て抑制する。GHCがDEBUGオプション付きでビルドされているとき、開発者にとって興味のあるデバッグ情報を出力することがある。この余分な出力がテストフレームワークを混乱させ、嘘のテスト失敗を引き起すことがあるので、これを無効にするためにこのフラグがある。

4.19.3. 不要な情報を抑制する

コア出力は大量の情報を含んでいる。用途によっては不要なものも含まれる。興味のない部分を抑制するには以下のフラグを使えば良い。
-dsuppress-all

抑制可能なものを全て抑制する。ただし、出力を曖昧にすることが多いので、固有識別子だけは抑制しない。コードの全体的な構造を知りたいだけなら、ここから始めると良い。

-dsuppress-uniques

固有子の印字を抑制する。これによって、表示結果が曖昧になることがある(例えば、ある「x」の出現がどこで束縛されているのか不明瞭になる)が、コンパイラを二回実行したときに出力に無駄な違いが出ることを減らせるので、diffをまともに適用できるようになる。diffによってどこを見るべきか分かったら、-dsuppress-uniquesなしでもう一回試せばよい。

-dsuppress-idinfo

識別子の束縛地点に表示される、その識別子の拡張情報を抑制する。これには正格性情報とインライン化器のテンプレートが含まれる。インライン化器のテンプレートを省くことで、このフラグを使うとコア出力の大きさを半減させることができる。

-dsuppress-module-prefixes

Core出力の中でモジュール修飾接頭辞を表示するのを抑制する。Data.List.lengthならData.Listの部分である。

-dsuppress-type-signatures

型シグネチャの印字を抑制する。

-dsuppress-type-applications

型適用の印字を抑制する。

-dsuppress-coercions

型適用の印字を抑制する。

4.19.4. 整合性の検査

-dcore-lint

GHC中の重量級パス内正気度チェックをコア水準で有効にする。(これがチェックするのはGHCの正気であって、あなたのではない)

-dstg-lint:

同じことをSTG水準で行う。(注意: 現在機能していない)

-dcmm-lint:

同じことをC--水準で行う。

4.19.5. コア構文(-ddump系フラグ由来の)の読み方

例を通して見ていくことにしよう。以下のコードに対して-ddump-dsすることを考える。

skip2 m = m : skip2 (m+2)

始める前に、名前について少々。GHC内部では、変数や型構築子などは「固有子(unique)」によって識別される。固有子は「文字」と「数値」(両方とも広く解釈する)をつなげた形をしている。「文字」部分はその固有子がどういう由来を持つのかを示す。例えば、_は「組込みの型変数」を意味し、tは「型検査器由来」、sは「単純化器由来」という具合である。「数値」部分は「base-62」形式でとてもコンパクトに表示されるが、私(WDP)以外の皆はこれが嫌いなようだ。

もう一度言うが、あらゆるものは「固有子」を持っていて、これは通常デバッグ時になんらかの形で出力される。ではいってみよう…

Desugared:
Main.skip2{-r1L6-} :: _forall_ a$_4 =>{{Num a$_4}} -> a$_4 -> [a$_4]

--# 「r1L6」はMain.skip2の固有子
--# 「_4」は型変数(テンプレート)「a」の固有子
--# 「{{Num a$_4}}」は辞書引数

_NI_

--# 「_NI_」は「まだ(実質的に)情報がない」を意味する。これは後に
--# GHC_PRAGMA情報になり、インタフェースファイルに行く。

Main.skip2{-r1L6-} =
    /\ _4 -> \ d.Num.t4Gt ->
        let {
          {- CoRec -}
          +.t4Hg :: _4 -> _4 -> _4
          _NI_
          +.t4Hg = (+{-r3JH-} _4) d.Num.t4Gt

          fromInt.t4GS :: Int{-2i-} -> _4
          _NI_
          fromInt.t4GS = (fromInt{-r3JX-} _4) d.Num.t4Gt

--# クラスメソッド「+」(固有子: r3JH)は辞書「Num」(今や明示的にラ
--# ムダで導入される引数である)から加算のコードを選択する。コアは
--# 二階のラムダ計算なので、型適用と型ラムダ(/\)は明示的である。
--# そのため、「+」は、まず型(「_4」)に適用され、次に辞書に適用さ
--# れている。この適用の結果が実際の加法関数であり、後で使われる。

--# (非標準の)クラスメソッド「fromInt」についても全く同じことを行
--# う。驚くに値しないが、型「Int」はコンパイラに結わえつけられて
--# いる。

          lit.t4Hb :: _4
          _NI_
          lit.t4Hb =
              let {
                ds.d4Qz :: Int{-2i-}
                _NI_
                ds.d4Qz = I#! 2#
              } in  fromInt.t4GS ds.d4Qz

--# 「I# 2#」というのは単なるリテラルのInt「2」のことである。これ
--# は、GHCが「data Int = I# Int#」と定義していることを反映してい
--# る。ここで、Int#はプリミティブの非ボックス化型である。(非ボッ
--# クス化型の関連情報についてはどこか別のところを見よ)

--# 「I#」の後の「!」は、データ構築子「I#」の適用が*飽和*している
--# (つまり、部分適用ではない)ことを示す。

          skip2.t3Ja :: _4 -> [_4]
          _NI_
          skip2.t3Ja =
              \ m.r1H4 ->
                  let { ds.d4QQ :: [_4]
                        _NI_
                        ds.d4QQ =
                    let {
                      ds.d4QY :: _4
                      _NI_
                      ds.d4QY = +.t4Hg m.r1H4 lit.t4Hb
                    } in  skip2.t3Ja ds.d4QY
                  } in
                  :! _4 m.r1H4 ds.d4QQ

          {- end CoRec -}
        } in  skip2.t3Ja

(「これは一つの単純な関数型言語に過ぎない」はPeyton Jones Enterprises, plc.の非レジスタ化商標です。)