母言語を選ぶ

新しい言語は既存の言語で書かれる。そしてその選択が、コンパイラのどの部分が快適でどの部分がずっと足を引っ張るかを静かに決めてしまう。なぜ Mere を OCaml で実装するのか —— コンパイラが母言語に本当に求めるもの、「検査可能にする」という原則がコンパイラ自身にも効くこと、そしてその決定を思い込みではなく使い捨ての trial で下した話。

mereocamlcompilerlanguage-designhost-language

言語は無から作られるわけではない。何か別の言語で書かれる。そしてその母言語が、 作業のどの部分が快適で、どの部分が最後まで足を引っ張るかを静かに決めてしまう。 Mere を一行書く前の最初の本当の決定は —— 何で作るか? だった。

コンパイラが母言語に本当に求めるもの

削ぎ落とせば、コンパイラとは、木を読み、何度も歩き回り、別の木か何らかのテキストを 生み出すプログラムだ。具体的な要求ははっきりしている:

  • AST を表現する —— 数十のノード形、いくつかは再帰的。これには代数的データ型が ほしい:Int of intApp of expr * expr、等々。
  • それを網羅的に歩く —— どのパス(評価、型推論、コード生成)も、それらの形に 対する大きな場合分けだ。これにはパターンマッチがほしい。そして —— 決定的に —— ケースを一つ忘れたときに教えてくれるコンパイラがほしい。
  • まずツリーウォーキング インタプリタ —— クロージャ、環境、再帰。これには第一級 関数と GC がほしい。インタプリタが同時にメモリ管理プロジェクトにならないように。
  • 型推論 —— 可変な型変数を使う単一化、一般化、具体化。これには安価な可変参照か、 良い永続データ構造がほしい。
  • 診断 —— どのノードもソース位置を持ち、至る所で引き回される。
  • 後には複数のバックエンド —— C、LLVM IR、WebAssembly テキストを吐く。多くは 規律ある文字列構築だが、場合分けが増えるので、網羅性検査がまた効いてくる。

同じ原則を、一段上で

前回、Mere は 正しさの検証可能性 を最適化する —— プログラムを読んで正しいと分かる こと —— と述べた。この価値は Mere 書かれるプログラムだけに効くのではない。 Mere のための コンパイラにも同じだけ効く。コンパイラはまさに、ケースの見落としや ノードの扱い間違いが、実在するのに静かなバグになる類のプログラムだ。

だから母言語は、そうしたバグを隠しにくいものであるべきだ。代数的データ型と網羅的な パターンマッチを持つ言語は、「コード生成器でレコード型の処理を忘れた」を、実行時の 不意打ちからコンパイルエラーに変えてくれる。新しい AST ノードを足すと、不完全な パスがすべて点灯する。母言語自身の型システムが、コンパイラを正直に保つ道具になる —— Mere がする賭けと同じものを、Mere 自身の実装に適用しているわけだ。

なぜ OCaml か

OCaml はその形にほぼ一点ずつ合致する:

  • 代数的データ型とパターンマッチが第一級で快適 —— AST とそのパスが、紙に描くように 読める。
  • 網羅的でないマッチはエラー扱いにできる警告なので、「全ノードを処理したか?」の検査が 無料かつ継続的に効く。
  • GC があるので、インタプリタは木を歩くだけのものにできる。
  • 成熟したツールチェイン(dune、読みやすい型エラー)が邪魔をしない。
  • そして ML 系だ —— これは二重に効く。なぜなら Mere 自身が ML 系言語 だからだ。 コンパイラを書くのに使うイディオムが、その言語が主題とするものと同じで、直観が 双方向に移る。

もっともな代替もある。Rust のようなシステム言語ならより多くの制御と速度が得られるが、 コンパイラは共有と変更の多い木・グラフ状のコードだらけで、母言語 のプログラムで 所有権チェッカと格闘するのは、間違った場所で払う摩擦だ —— Mere 自身の明示的メモリの 物語こそがこのプロジェクトの眼目であって、それを作る間にもう一度払うものではない。 (母言語が GC を使い、それが作る言語は使わない —— これは矛盾ではなく、関心の分離だ。)

思い込みではなく、trial で決めた

このプロジェクトで繰り返される手法は「思い込まず、まず小さな trial を回す」だ。母言語 選びは、それが最初に適用された場所だった。雰囲気で OCaml に決めるのではなく、決定は 使い捨てのプロトタイプ —— OCaml で端から端まで実装した小さな式言語 —— から来た。 要求のきつい部分がどれも本当に自然かを確かめるために、あえて作ったものだ:

  • ADT の AST と再帰的評価、
  • 小さな手書きパーサ、
  • クロージャと環境、
  • 引き回されるソース位置、
  • そして双方向型検査まで —— 型推論の機構が窮屈にならないことを確かめるために。

そのプロトタイプが各部分の快適さを確認して —— 数十のテストに支えられて —— 初めて、 OCaml を母言語として採用した。trial は意図的に使い捨てだ。その仕事は問いに答える ことであって、コードベースになることではない。

これが、この連載で以降続くほぼすべての決定の形だ。設計上の問いを立て、小さな trial か 紙上のスケッチが答え、その後で初めて実コードになる。母言語は、たまたまその問いの リストの最初だった、というだけだ。

次回:最小コア —— 実際に動く Mere の最小版。

← Back to Mere: 言語を作る