ClojureScriptのコンパイルパイプライン

この文書について

この文書はMichael Fogusさんの「The ClojureScript Compilation Pipeline」を日本語に翻訳したものです。 原文のライセンスと同じく、この文書はクリエイティブ・コモンズ 表示 - 継承 2.5 一般 ライセンスの下に提供されます。


これは「解説Clojureのコンパイル技術」シリーズの5番目の記事です。

来るべきClojureのGoogle Summer of Codeプロジェクトを記念して、私はClojureScriptのコンパイラパイプラインについてのいくつかの解説を公開します。 私はこれについて私のClojureWestでのトークで話しました。しかし、もう少しの検討は歓迎です。 ここで私が述べることのほとんどは、データ構造と実際のプログラム上のインターフェイスの周りの詳細が異なることを除いてClojureパイプラインにも適用されます。 私は抽象度の高いレベルにとどまろうと試みるつもりです。

ClojureScriptのパイプライン概観

これはClojureScriptのパイプラインです。

f:id:ykomatsu0:20151104165814j:plain

あなたがClojureコードを片端に入れると、もう一方の端からJavaScriptが得られます。

f:id:ykomatsu0:20151104165807j:plain

コンパイラーは非常に多くのフェーズから構成され、その最初のフェーズは文字列の読込みとそれらをClojureのデータ構造に変換することに充てられます。

f:id:ykomatsu0:20151104165759j:plain

次のコードをClojure REPLで観察することで、あなたはどのように読込みフェーズが動くのかが分かるでしょう。

(def E (read-string "(-> 42 (- 6) Math/sqrt)"))

(type E)
;=> clojure.lang.PersistentList

(type (last E))
;=> clojure.lang.Symbol

あなたはEがデータ構造であることが分かるでしょう。

次のコンパイルフェーズはマクロ展開フェーズです。

f:id:ykomatsu0:20151104165740j:plain

このフェーズの間に、以下のとおり、フォームはそれがある不動点に到達するまで展開されるでしょう。

(-> E macroexpand-1)
;=> (clojure.core/-> (clojure.core/-> 42 (- 6)) Math/sqrt)

(-> E macroexpand-1 macroexpand-1)
;=> (Math/sqrt (clojure.core/-> 42 (- 6)))

(-> E macroexpand-1 macroexpand-1 macroexpand-1)
;=> (. Math sqrt (clojure.core/-> 42 (- 6)))

(-> E macroexpand-1 macroexpand-1 macroexpand-1 macroexpand-1)
;=> (. Math sqrt (clojure.core/-> 42 (- 6)))

上の実例では3つのレベルのマクロ展開でフォームは(あるレベルから次のレベルに変わらない)不動点に到達しました。 結局は内部の->マクロも展開するでしょうが、それはASTの生成中にフォームが巡回されるときに起きます。 あなたは私がマクロ展開の箱を少し小さく作ったことに気付くでしょう。 この違いの意味はマクロ展開がASTの生成に割り込むということです。 機械的なマクロ展開の他に、このフェーズは.(ドット)演算子の引数を同じ意味の( . target field/method args)形式に入れることもします。

次のフェーズは解析フェーズであり、その主目的はClojureScriptの抽象構文木(abstract syntax tree、AST)を生成することだと思われます。

f:id:ykomatsu0:20151104165728j:plain

もしあなたが私とASTについて話したことがあれば、不幸なことにあなたはおそらく「Lispの構文はプログラム自体のASTである」という不運な言葉(又はいくつかのそのようなバリエーション)に対する私の先入観の全てを聞いてしまったでしょう。 これはごみくずです。 実際のASTはローカル束縛の情報、アクセス可能な束縛、アリティーの情報、その他多くの便利な小さな情報のような船1杯分の追加情報で装飾されています。

f:id:ykomatsu0:20151104165720j:plain

あなたはおそらくこのような図を(そしておそらくパイプラインに似た画像も)他のブログの記事、教科書、論文で見たことがあるでしょう。 しかし、ClojureScript(そしてある日のClojureコンパイラー)の提供する1つの利点は各コンパイルフェーズ間でインターフェイスが単なるClojureのデータで構成されているということです!  リーダーの生成物はリスト、いくつかの他のClojureのデータ型、又はそれらのネストしたものです。 マクロ展開の生成物も同じです。 解析フェーズの生成物はネストしたClojureのマップから構成されたAST自体です。 Clojure又はClojureScript自体(そして数百のライブラリー)が直接処理するツールそのものによって処理できないコンパイルフェーズの生成物はありません。 これはClojureLisp全般の本当にすばらしい機能です。

最後のステージはASTを受け取りJavaScriptを生成するエミッションフェーズです。

f:id:ykomatsu0:20151104165713j:plain

エミッションはおそらくClojureScriptコンパイラー全体で最も複雑な部分でしょう。それはコード700行くらいです。

ClojureScriptのパイプラインへの接続

まず明らかな拡張場所は解析フェーズのバックエンドです。

f:id:ykomatsu0:20151104165728j:plain

これは実際にAmbrose Bonnaire-Sergeantさんが彼のtyped-clojureで採用したアプローチです。 大きな違いは彼の解析プロジェクトがClojureの解析エンジンを使ってClojureScriptに似たASTを提供していることです。 これは非常にクールです。 図で示せば、あなたは次のように置かれたtyped-clojureを想像できるでしょう。

f:id:ykomatsu0:20151104165707j:plain

解析フェーズで生成された木を型チェッカーを使って装飾し、検査します。 ここで質問です。Haskellの型システムの最大の制限は何でしょうか。 それについて考えましょう。 私は私のトークの中ではこれに答えますが、それはこの記事の中心ではありません。

f:id:ykomatsu0:20151104165700j:plain

ただし、概念的にはAST上で順番に起きる追加のチェックを想像することができるので、そこで止める理由はありません。

f:id:ykomatsu0:20151104165653j:plain

それゆえ、派生モデルではパイプラインを通じてASTが徐々に装飾されます。 これがプラガブル *1 コンパイルシステムです。それはClojureの理想である開かれた拡張に完全に従ったものです。

f:id:ykomatsu0:20151104165648j:plain

これは非常に強力なモデルですが、潜在的に複雑さに満ちています

しかし、強力な一方、シーケンシャルモデルはとても便利なものでも望まれたものでもありません。 むしろ、よりよいモデルは分岐やパイプラインへのロジックの入力を許すものでしょう。 例えば、プログラムが独立していて完全に型付けされていれば、分岐(beta)が適切な動作かもしれません。 しかし、プログラムが型付けされていないライブラリーの使用を含んでいれば、おそらく実行時制約チェックで装飾された部分的な静的型付け *2 (alpha)がより適切な分岐先かもしれません *3

f:id:ykomatsu0:20151104165641j:plain

解析フェーズへのインターフェイスとその構造を処理するための一連のツールの設計は比較的単純です。 難しい部分はそれらの使い方、パイプラインへの入力、分岐ロジックです。 ここでは注意深い設計が要求されます。

次の拡張場所はエミッションフェーズの入力部分です。

f:id:ykomatsu0:20151104165713j:plain

もちろん、拡張によって私はそれら独自のエミッターをプラグインできるものを意図しています。

f:id:ykomatsu0:20151104165635j:plain

これはおそらく新しいプラットフォームをClojureScriptのターゲットにするための最も単純な方法であり、実際にclojure-schemeによって採用されています。 つまり、clojure-schemeを使ってNathan SorensonさんはバックエンドにGambit Schemeを生成したということです。 生成されたSchemeはさらにCにコンパイルすること、最後には機械語コンパイルすることができます。

f:id:ykomatsu0:20151104165619j:plain

ただし、このアプローチには関門があります。 ClojureScriptのコンパイラーは非常に単純で、その中のコードの刈込みではほんの少ししか作業をしません。しかし、代わりにその作業をGoogle Closureコンパイラーに委託します。 もしあなたのターゲット言語がその独自の処理(例えばツリーシェイキング)を持たなければ、あなたはあなた独自のものを作る必要があるでしょう。いっそのこと、ClojureScriptをそうするように拡張するという手もあります。

最後の明らかな拡張場所はエミッションフェーズまでのパイプラインを全てあなた独自のパーサーに置き換えることです。

f:id:ykomatsu0:20151104165613j:plain

これは非常に大きな作業(言語のパーサーを1から書く)又は適度に大きな作業(既存のパーサーを修正する)のどちらかでしょう。

f:id:ykomatsu0:20151104165602j:plain

ClojureScriptのパイプラインは様々な次元に沿って拡張することができますが、まだツールインターフェイスの周りでいくつかの仕事を必要としています。 Clojureコンパイラーの拡張性はもう少し低いですが、絶対不可能ということではありません。 パイプラインへの入力という要望と同じ体験を提供するためにはもっと仕事が必要です。 私はclojure-pyによって採用された他のアプローチには言及していません。 つまり、あなたはClojure実行環境全体をあなたの選択したターゲット言語に実装することを選択するかもしれないということです。

あなたが採用を決めたアプローチ(もしあれば)にかかわらず、楽しみ、ロックし続け、何かすばらしいものを作りましょう。

:F

この記事の草稿を読んでくれたことについてCraig AnderaさんBobby CalderwoodさんRich Hickeyさんに感謝します。

*1:Gilad Brachaさんはプラガブル型システムについてhttp://bracha.org/pluggableTypesPosition.pdfにある彼の論文「プラガブル型システム」で述べています

*2:再びGilad Brachaさんで「型は反モジュラーである」

*3:私はdocsフェーズを切り離された分岐に沿って置きましたが、全ての分岐への入力として提供する方がより適切かもしれません