コード生成器を Mere で:自分自身を吐くコンパイラ

言語を解釈することと、コンパイルすることは同じではない。最後のセルフホスト部品はコード生成器 —— 木を歩く代わりに WebAssembly を吐く部分 —— で、それを仕上げるとパイプラインが閉じる:Mere ソースから Wasm モジュールへ、完全に Mere で。最も鋭い瞬間は、コード生成器を自身のソースに向けて走らせると、自分自身の valid な Wasm を吐くときだ —— ブートストラップ可能なコンパイラの定義。加えて、バグでなかったバグの寓話:不気味な境界が、欠陥ではなくメモリの天井だったという。

mereself-hostingcodegenbootstrapwebassemblylanguage-design

セルフホストパイプラインはいま Mere を読み、その型を検査し、解釈できる。だがプログラムを解釈することは、 それをコンパイルすることと同じではない —— インタプリタは木を直接走らせ、コンパイラはインタプリタが 居なくても走るコードを吐く。セルフホストコンパイラの最後の一片は、その違いを生むもの:コード 生成器、Mere で書かれ、検査済みプログラムを WebAssembly に変える。それを仕上げると、この Part 全体が 回ってきたループが閉じる。

三つのうち、なぜ WebAssembly か

OCaml コンパイラは三つのコード生成器 —— C へ、LLVM へ、Wasm へ —— を持ち、セルフホストは一つを選んだ: Wasm 生成器、WAT emitter だ。選択は恣意的ではない。Wasm はブラウザのターゲットで、ブラウザは セルフホストパイプラインがすでに住むところだ —— フォーマッタ、REPL、型検査器はみなウェブページとして 走っていた。セルフホスト Wasm 生成器は、セルフホストの取り組み全体が向かってきたデモを閉じる:Mere ソースをページに貼ると、Wasm モジュールが返る、サーバなしで、ループに OCaml なしで。それはまた元の 三つのうち最も小さく、それがそれを、セルフホストする自然な最初の —— そして判明したとおり、唯一必要な —— ターゲットにする。C と LLVM の生成器は OCaml に留まる;セルフホストコンパイラは Wasm を吐く。

Mere から Wasm を吐くことは、Wasm backend の回が記述したのと同じ作業が、いまそれがターゲットとする 言語で書かれたものだ:値をリニアメモリにレイアウトし、クロージャを関数テーブルのインデックスと環境 offset の対として表現し、match をロードと分岐に下ろし、すべてをサンドボックスの import 機構に通す。 そのすべてが、今回は、フロントエンドが生みタイプ検査器が祝福する共有された構文木を消費する Mere で。

コンパイラを吐くコンパイラ

ここで自己参照が最も深い点に達する。コード生成器はそれ自身が Mere プログラム —— 実質的なものだ。だから、 コンパイラを真にセルフホストと名づけることを、できる:コード生成器を自分自身のソースに向けて走らせる。 Mere で書かれた Wasm 生成器に、それ自身のソースファイルを与えると、それは自分自身の valid な WebAssembly を吐く —— 百万文字を優に超える WAT テキストで、標準の Wasm アセンブラがそれを受け入れ Wasm モジュールに 変える。コンパイラがコンパイラをコンパイルした。

それがブートストラップ可能なコンパイラの定義であり、飾りではなく真のマイルストーンだ。自分自身の ソースを吐けるコンパイラは、原理的に、最初に書かれた言語から自由だ:OCaml は Mere を離陸させた足場 だったが、Mere で書かれた生成器が Mere で書かれた生成器を生めるようになれば、OCaml の元版は言語を生かし 続けるのに論理的にはもう要らない。実際には留まる —— セルフホスト部品が検証される対象のリファレンスと して、そして extern primitive のホストとして —— が、依存は原理的に断たれ、それがブートストラップの要点 そのものだ。

ループがどこで止まるか、そして red herring について正直に

達成されたものを正確に述べる価値がある、プロジェクトが保つ正直な台帳の精神で。コンパイル時の セルフコンパイルは動く:Mere コード生成器は、既存のツールチェーンの下で走らせると、自分自身のソースの valid な Wasm を吐き、アセンブラがそれを受け入れる。そのマイルストーンは継続的インテグレーションの スイートに釘付けにされている —— self-emit がサイズ閾値以上の WAT 出力を生み、アセンブラがそれを検証 するという立ち続けるテストで。

完全な実行時ブートストラップループ —— その self-emit された Wasm を取り、走らせ、それが今度は Mere をコンパイルする —— は閉じるのがより難しく、そして難しかった理由は、誤った原因を追うことについての 小さな寓話だ。コンパイル済みコンパイラを Wasm の中で走らせると trap し、その trap は不気味な特徴を持って いた:名前が五文字の関数を参照するプログラムは問題なくコンパイルされ、六文字では trap する —— 正確で、 内容とは無関係な境界で、名前やメモリのレイアウトの深いバグに見えた。それは red herring だった。本当の 原因は平凡だった:セルフコンパイルは、小さなプログラムをコンパイルするよりはるかに多くの作業メモリを 要する —— 数十メガバイトのスクラッチ arena —— そして生成された Wasm モジュールのリニアメモリは、それを はるかに下回る数メガバイトに上限づけられていた。「五文字か六文字かの境界」は、ただ arena が天井を越える 入力にすぎず、名前の長さがたまたま総確保量と相関して bisection を欺いた。メモリ上限を上げるとループは 閉じ、いまや runtime-bootstrap のチェックが CI に立つ:コンパイル済みコンパイラが走り、プログラムを端から 端までコンパイルする。まだ先送りされている一片は、その上げた天井より大きなワークロードのための成長 できるアロケータだけだ。

教訓は、正直な台帳が両方向に切ることだ。それは過剰な主張を止めた —— しばらくループは本当に閉じて いなかった —— そしてそれはまた、メモリの天井を正しさのバグと取り違えないこと、コンパイラについて理論 づける代わりに不気味な境界を退屈な真実まで辿ることを意味した。「十分近い」は両方向で拒まれる:持って いないブートストラップを主張せず、持ってもいない深い欠陥を診断もしない。

パイプライン、丸ごと

コード生成がセルフホストされて、コンパイラのあらゆる段が Mere に存在する —— レキサ、パーサ、フォー マッタ、評価器、型検査器、そしてコード生成器、数千行の言語が自分自身を記述し、すべてが一つの構文木を 共有し、それぞれが OCaml の双子に対して cross-validate される。OCaml コンパイラの役割は、この Part の 過程で静かに反転した:それは実装の全部として始まり、足場として終わる —— 照合する対象のリファレンスと、 primitive のホスト —— 一方、動くコンパイラはいま、それがコンパイルする言語に存在する。その反転は跳躍で 起きなかった;一度に一つの cross-validate された部品ずつ、そのそれぞれが先に来たものに保たれて、起きた。

コンパイラは Mere に存在し、自分自身を吐ける。物語に残るのは、もう一つの部品ではなく、場所だ:ここで 建てられたすべては、人が届けるどこかで走るためのものだった。フォーマッタ、REPL、型検査器、コンパイラは、 落ちるたびにウェブページに配線された。次回、Part V の締め:ブラウザで走る Mere —— 四つの live page、 そこでセルフホストパイプラインが、それがターゲットとするサンドボックスにコンパイルされて、あなたの目の 前で仕事をする。

← Back to Mere: 言語を作る