9.3. コンパイラプラグイン

GHCは、コンパイル時にコンパイラプラグインをロードする能力を持っている。この機能はGCCが提供するものに似ており、利用者がプラグインを書いてコンパイルのパイプラインを確認/変更したり、GHCの中間言語であるCoreを変換したりできる。プラグインは実験的な分析や最適化に適しており、利用に際してGHCのソースコードを必要としない。

プラグインはC--を読んだり最適化したりできず、GCCと違ってパーサ/フロントエンドへの変更を実装することができない。これらの制限のうちどれかがあまりに重荷だと強く思うなら、GHCチームに連絡を寄越して欲しい

9.3.1. コンパイラプラグインを使う

プラグインは、コマンド行中で-fplugin=moduleを使うことて指定できる。ここでmoduleは、プラグインをエクスポートする登録モジュールである。プラグインへの引数は、-fplugin-opt=module:argsというコマンド行オプションで指定できる。ここでargsmoduleで提供されるプラグインに解釈される引数列である。

例として、foo-ghc-pluginパッケージのFoo.Pluginモジュールからエクスポートされているプラグインをロードして、それに引数"baz"を与えるには、このようにGHCを起動する。

$ ghc -fplugin Foo.Plugin -fplugin-opt Foo.Plugin:baz Test.hs
[1 of 1] Compiling Main             ( Test.hs, Test.o )
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Loading package ffi-1.0 ... linking ... done.
Loading package foo-ghc-plugin-0.1 ... linking ... done.
...
Linking Test ...
$ 

プラグインは登録済みパッケージからエクスポートされるものなので、例えばcabalファイルに依存関係としてプラグインを書き、ghc-optionsフィールドでプラグインの引数を指定する、ということをしても問題ない。

9.3.2. コンパイラプラグインを書く

プラグインとは、pluginという名前のGhcPlugins.Plugin型の識別子をエクスポートしているモジュールのことである。全てのプラグインはimport GhcPluginsすべきである。コンパイルパイプラインとのインタフェースがそこで定義されているからである。

Pluginは、実質的に、コンパイルパイプラインの中にコンパイル過程を設置する関数を保持している。デフォルトで、GhcPlugins.defaultPluginという何もしない空プラグインが存在する。自分の設置関数を指定するには、レコード構文を使ってこの空プラグイン(訳注: のフィールド)を再定義するべきである。Plugin型が正確にどんなフィールドを持つかは変更の可能性があるので、プラグインが将来にわたってインタフェースへの影響を最小限にしつつ動作しつづけるようにするために、これが最良の方法である。

PlugininstallCoreToDosというフィールドをエクスポートする。これは[CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]という型の関数である。CommandLineOptionは実質的にただのStringであり、CoreToDoは要するにCore -> Core型の関数である。CoreToDoは、あなたのパスに名前を与え、GHCを起動したときに全てのモジュールに対してそれを走らせる。

例として、何もせずに元のコンパイルパイプラインを返し、そして'Hello'と言うプラグインを掲げる。

module DoNothing.Plugin (plugin) where
import GhcPlugins

plugin :: Plugin
plugin = defaultPlugin {
  installCoreToDos = install
  }

install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ todo = do
  reinitializeGlobals
  putMsgS "Hello!"
  return todo

このプラグインをコンパイルしてパッケージとして登録(例えばcabalで)したなら、コマンド行で-fplugin=DoNothing.Pluginと指定するだけでこれを使うことができ、コンパイル中にGHCが'Hello'と言うのを確認できるはずである。

設置関数の先頭でreinitializeGlobalsを呼んでいるのに十分注意すること。windowsのリンカがlibghcを扱う際のバグにより、コンパイラプラグインが呼び出しの時点でGHCと同じ大域状態を持つことを正しく保証するのに、この呼び出しが必要である。reinitializeGlobalsを使わないと、コンパイラプラグインが未初期化の状態を要求し、実行時にクラッシュすることがある。

将来的に、このリンクのバグが修正されれば、reinitializeGlobalsは非推奨として警告対象になり、何もしないように変更されるだろう。

9.3.2.1. CoreToDoについてより詳しく

CoreToDoは実質的に、GHCがCoreに対して行うあらゆる種類の最適化過程を記述するデータ型である。過程には単純化、CSE、ベクトル化などがある。プラグインのためには専用の構築子、CoreDoPluginPass :: String -> PluginPass -> CoreToDoがあり、自分で用意した過程をパイプラインに挿入する場合には常にこれを使うべきである。最初のパラメタはプラグインの名前であり、二番目は挿入したい過程である。

CoreMはモナドであり、全てのCore最適化はその中に生息し、その中で動作する。

プラグインの設置関数(上の例ではinstall)は、CoreToDoのリストを取り、CoreToDoのリストを返す。GHCは、モジュールのコンパイルを開始する前に、ロードするように指示された全てのプラグインを列挙し、全ての設置関数を走らせる。最初のリストはGHCが自分で指定したものである。全てのプラグインについてこれを行なった後、最終的なリストが最適化器に与えられ、単にリストを順番に辿ることで実行される。

設置関数を書く際には注意深くなければならない。なぜなら、執筆時点では、あなたが返したリストは疑われたり再確認されたりしないからである。以下のような設置関数を考える。

install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ _ = return []

これが正しいのは間違いないが、やりたいことと違うのもまた間違いない。

9.3.2.2. 束縛を操作する

前の節では、CoreDoPluginPassが名前の他にPluginPass型の過程を取ることを見た。PluginPass(ModGuts -> CoreM ModGuts)の別名である。ModGutsは、GHCが特定の時点でコンパイルしようとしている一つのモジュールを表現する型である。

ModGutsは、モジュールの最上位の束縛のうち見て確かめられるものを全て保持する。これらの束縛の型はCoreBindであり、実質的に、名前が本体のコードに束縛されるのを表す。最上位の束縛はModGutsmg_bindsフィールドにある。最上位の束縛を操作する過程は、このフィールドに沿って繰り返しを行い、更新したmg_bindsフィールドを含む新しいModGutsを返すだけで実装できる。これは非常によくあるケースなので、([CoreBind] -> CoreM [CoreBind])型の関数を(ModGuts -> CoreM ModGuts)型に持ち上げるbindsOnlyPassという関数が用意されている。

前節の例を続けて、コンパイル中モジュールの全ての非再帰的束縛の名前を表示するだけの単純なプラグインを書くことができる。

module SayNames.Plugin (plugin) where
import GhcPlugins

plugin :: Plugin
plugin = defaultPlugin {
  installCoreToDos = install
  }

install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ todo = do
  reinitializeGlobals
  return (CoreDoPluginPass "Say name" pass : todo)

pass :: ModGuts -> CoreM ModGuts
pass = bindsOnlyPass (mapM printBind)
  where printBind :: CoreBind -> CoreM CoreBind
        printBind bndr@(NonRec b _) = do
          putMsgS $ "Non-recursive binding named " ++ showSDoc (ppr b)
          return bndr 
        printBind bndr = return bndr

9.3.2.3. 注釈を使う

先に注釈プラグマ(9.1. ソース注釈)について議論した際、コンパイラプラグインに追加の手掛かりや情報を与えるのにこれを使えることに言及した。モジュールの注釈はプラグインから取得できるが、そのためにはモジュールのModGutsを通らなければならない。注釈はDataTypeableの任意のインスタンスであり得るので、インタフェースから取得するデータの正しい型を指定する型注釈を与える必要がある。また、あなたのユーザが使う注釈型と、あなたのプラグインが使うものが同じであることを保証しなければならない。このため、可能ならば、コンパイラプラグインを提供するパッケージの一部として注釈も配布することを勧める。

あるbinderの注釈を取得するには、`getAnnotations`を使い、適切な型を指定すればよい。SomeAnn注釈の付いた全ての最上位・非再帰的な束縛の名前を表示する例を示す。

{-# LANGUAGE DeriveDataTypeable #-}
module SayAnnNames.Plugin (plugin, SomeAnn) where
import GhcPlugins
import Control.Monad (when)
import Data.Data
import Data.Typeable

data SomeAnn = SomeAnn deriving (Data, Typeable)

plugin :: Plugin
plugin = defaultPlugin {
  installCoreToDos = install
  }

install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
install _ todo = do
  reinitializeGlobals
  return (CoreDoPluginPass "Say name" pass : todo)

pass :: ModGuts -> CoreM ModGuts
pass g = mapM_ (printAnn g) (mg_binds g) >> return g
  where printAnn :: ModGuts -> CoreBind -> CoreM CoreBind
        printAnn guts bndr@(NonRec b _) = do
          anns <- annotationsOn guts b :: CoreM [SomeAnn]
          when (not $ null anns) $ putMsgS $ "Annotated binding found: " ++  showSDoc (ppr b)
          return bndr
        printAnn _ bndr = return bndr

annotationsOn :: Data a => ModGuts -> CoreBndr -> CoreM [a]
annotationsOn guts bndr = do
  anns <- getAnnotations deserializeWithData guts
  return $ lookupWithDefaultUFM anns [] (varUnique bndr)

これらの内部APIなどの使いかたについて、より詳しくはGHC API説明書を参照のこと。