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

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

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

-ddump- pass

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

-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.14.6. 書き換え規則の中で何が起こるか制御するを見よ。

-ddump-simpl:

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

-ddump-inlinings:

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

-ddump-cpranal:

CPR解析の出力

-ddump-stranal:

正格性解析器の出力

-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-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-rn-trace

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

-ddump-rn-stats

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

-dverbose-core2core , -dverbose-stg2stg

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

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

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

-dfaststring-stats

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

-dppr-debug

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

-dsuppress-uniques

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

-dsuppress-coercions

Core出力の中で型変換(coercion)を表示するのを抑制し、Core出力が短かくなるようにする。

-dsuppress-module-prefixes

Core出力の中でモジュール修飾接頭辞を表示するのを抑制し、読み易くする。

-dppr-user-length

エラーメッセージ中では、式はある「深さ」までしか印字されず、それよりも深い部分式は省略符号で表される。このフラグはその深さを決定する。デフォルト値は5。

-dno-debug-output

要求されていないデバッグ出力を全て抑制する。GHCは、DEBUGオプション付きでビルドされている場合、ときどき開発者向きのデバッグ出力を吐き出す。この追加の出力は、テストフレームワークを混乱させて誤ったテスト失敗を引き起すことがあるので、それを無効にするためにこのフラグが用意されている。

整合性の検査

-dcore-lint

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

-dstg-lint:

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

-dcmm-lint:

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

コア構文(-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.の非レジスタ化商標です。)

非レジスタ化コンパイル

「非レジスタ化」という用語は実際には「純粋なCを介してコンパイルする」ということを意味している。つまり、通常GHCがプログラムを速くするために使うプラットフォーム固有の技を一部使わないということである。非レジスタ化コンパイルするときは、GHCは単純にgccでコンパイルできるCファイルを生成する。

非レジスタ化コンパイルはGHCを新しい機械に移植するときに便利である。これを使うと、予め必要なツールはgccasldだけになるし、さらに、非レジスタ化コンパイルが動作するようにするために書かなければならないプラットフォーム固有のコードは通常かなり少ないからである。

コンパイルの段階で非レジスタ化コンパイルを選ぶことはできない。GHCをビルドするときに適切なオプションを設定する必要があるのだ。詳細はGHCのビルドの手引きを参照せよ。