GHCは、コンパイル時にコンパイラプラグインをロードする能力を持っている。この機能はGCCが提供するものに似ており、利用者がプラグインを書いてコンパイルのパイプラインを確認/変更したり、GHCの中間言語であるCoreを変換したりできる。プラグインは実験的な分析や最適化に適しており、利用に際してGHCのソースコードを必要としない。
プラグインはC--を読んだり最適化したりできず、GCCと違ってパーサ/フロントエンドへの変更を実装することができない。これらの制限のうちどれかがあまりに重荷だと強く思うなら、GHCチームに連絡を寄越して欲しい。
プラグインは、コマンド行中で-fplugin=
を使うことて指定できる。ここでmodule
module
は、プラグインをエクスポートする登録モジュールである。プラグインへの引数は、-fplugin-opt=
というコマンド行オプションで指定できる。ここでmodule
:args
args
はmodule
で提供されるプラグインに解釈される引数列である。
例として、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
フィールドでプラグインの引数を指定する、ということをしても問題ない。
プラグインとは、plugin
という名前のGhcPlugins.Plugin
型の識別子をエクスポートしているモジュールのことである。全てのプラグインはimport GhcPlugins
すべきである。コンパイルパイプラインとのインタフェースがそこで定義されているからである。
Plugin
は、実質的に、コンパイルパイプラインの中にコンパイル過程を設置する関数を保持している。デフォルトで、GhcPlugins.defaultPlugin
という何もしない空プラグインが存在する。自分の設置関数を指定するには、レコード構文を使ってこの空プラグイン(訳注: のフィールド)を再定義するべきである。Plugin
型が正確にどんなフィールドを持つかは変更の可能性があるので、プラグインが将来にわたってインタフェースへの影響を最小限にしつつ動作しつづけるようにするために、これが最良の方法である。
Plugin
はinstallCoreToDos
というフィールドをエクスポートする。これは[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
は非推奨として警告対象になり、何もしないように変更されるだろう。
CoreToDo
についてより詳しくCoreToDo
は実質的に、GHCがCoreに対して行うあらゆる種類の最適化過程を記述するデータ型である。過程には単純化、CSE、ベクトル化などがある。プラグインのためには専用の構築子、CoreDoPluginPass :: String -> PluginPass -> CoreToDo
があり、自分で用意した過程をパイプラインに挿入する場合には常にこれを使うべきである。最初のパラメタはプラグインの名前であり、二番目は挿入したい過程である。
CoreM
はモナドであり、全てのCore最適化はその中に生息し、その中で動作する。
プラグインの設置関数(上の例ではinstall
)は、CoreToDo
のリストを取り、CoreToDo
のリストを返す。GHCは、モジュールのコンパイルを開始する前に、ロードするように指示された全てのプラグインを列挙し、全ての設置関数を走らせる。最初のリストはGHCが自分で指定したものである。全てのプラグインについてこれを行なった後、最終的なリストが最適化器に与えられ、単にリストを順番に辿ることで実行される。
設置関数を書く際には注意深くなければならない。なぜなら、執筆時点では、あなたが返したリストは疑われたり再確認されたりしないからである。以下のような設置関数を考える。
install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo] install _ _ = return []
これが正しいのは間違いないが、やりたいことと違うのもまた間違いない。
前の節では、CoreDoPluginPass
が名前の他にPluginPass
型の過程を取ることを見た。PluginPass
は(ModGuts -> CoreM ModGuts)
の別名である。ModGuts
は、GHCが特定の時点でコンパイルしようとしている一つのモジュールを表現する型である。
ModGuts
は、モジュールの最上位の束縛のうち見て確かめられるものを全て保持する。これらの束縛の型はCoreBind
であり、実質的に、名前が本体のコードに束縛されるのを表す。最上位の束縛はModGuts
のmg_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 = do dflags <- getDynFlags bindsOnlyPass (mapM (printBind dflags)) where printBind :: DynFlags -> CoreBind -> CoreM CoreBind printBind dflags bndr@(NonRec b _) = do putMsgS $ "Non-recursive binding named " ++ showSDoc dflags (ppr b) return bndr printBind _ bndr = return bndr
先に注釈プラグマ(9.1. ソース注釈)について議論した際、コンパイラプラグインに追加の手掛かりや情報を与えるのにこれを使えることに言及した。モジュールの注釈はプラグインから取得できるが、そのためにはモジュールのModGuts
を通らなければならない。注釈はData
とTypeable
の任意のインスタンスであり得るので、インタフェースから取得するデータの正しい型を指定する型注釈を与える必要がある。また、あなたのユーザが使う注釈型と、あなたのプラグインが使うものが同じであることを保証しなければならない。このため、可能ならば、コンパイラプラグインを提供するパッケージの一部として注釈も配布することを勧める。
あるbinderの注釈を取得するには、`getAnnotations`を使い、適切な型を指定すればよい。SomeAnn
注釈の付いた全ての最上位・非再帰的な束縛の名前を表示する例を示す。
{-# LANGUAGE DeriveDataTypeable #-} module SayAnnNames.Plugin (plugin, SomeAnn(..)) where import GhcPlugins import Control.Monad (unless) import Data.Data 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 = do dflags <- getDynFlags mapM_ (printAnn dflags g) (mg_binds g) >> return g where printAnn :: DynFlags -> ModGuts -> CoreBind -> CoreM CoreBind printAnn dflags guts bndr@(NonRec b _) = do anns <- annotationsOn guts b :: CoreM [SomeAnn] unless (null anns) $ putMsgS $ "Annotated binding found: " ++ showSDoc dflags (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説明書を参照のこと。