なぜ一つ以上の backend か、そしてそれらを同一に保つとは

Mere は四つの走り方を持つ:ツリーウォーキングのインタプリタ、そして C・LLVM IR・WebAssembly へのコード生成器。要点は到達範囲だけではない —— 四つすべてが、同じプログラムで byte 単位で同一の出力を出さねばならない。その一致が、backend を互いのテストオラクルに変える。そしてそれは、言語の第一原則を実装へ持ち込んだものだ:どの backend を選んだかが、プログラムの振る舞いを変える隠れた変数であってはならない。

merebackendscodegendifferential-testinglanguage-design

二つの Part が Mere が何であるかを描いた —— メモリモデルとエフェクトシステム、どちらも 明示的で、どちらも検査できる。だが言語は、何かがそれを実行するまでは約束にすぎない。Part IV は実行の話で、それは、何が買えるかを見るまでは過剰設計に見える選択から開く:Mere は一つの やり方で走らず、四つで走り、その四つを厳密な一致に保つ。

同じ言語を走らせる四つの道

四つのターゲットが存在するのは、「このプログラムを走らせる」が場所によって違うことを意味する からだ:

  • ツリーウォーキングのインタプリタは構文木を歩いて直接評価する。それは速い内側のループ —— REPL、素早いテスト、コードを書きながら手を伸ばすもの —— であり、同時に基準を兼ねる: プログラムが何を意味するかの、素朴で明らかに正しい読みだ。
  • C backend は、任意の C コンパイラでコンパイルする C ソースを吐く。つまりどこでも走る —— あらゆるプラットフォームに C ツールチェインがある。
  • LLVM IR backend は LLVM の中間表現を吐き、そのエコシステムの最適化器とネイティブコード 生成にプログラムを託す。
  • WebAssembly backend は Wasm を吐く。だから同じプログラムがブラウザ、あるいは任意の サンドボックス化された Wasm ホストで走る。

一つの言語、四つの実行環境、「この行を今すぐ評価する」から「これをブラウザに届ける」までの 幅を覆うために選ばれた。それだけでも相応の工学だ。だが四つのターゲットへコンパイルすることは、 簡単な方の主張だ。難しいのは次の一文だ。

難しい部分:それらは厳密に一致せねばならない

プログラムが四つの backend すべてでコンパイルできるだけでは足りない。Mere が自らに課す 要件は、四つすべてが同じプログラムで byte 単位で同一の出力を出すことだ —— 「等価」でも、 「整形を除けば十分近い」でもなく、同じバイトだ。十を超える現実的なプログラム —— JSON パーサ、 小さな計算機、S 式リーダ、そして千行を超える SQL 風のエンジンに至るまで —— が、インタプリタと 三つのコード生成器すべてで走り、何一つ違わない出力を出す。全面で diff がゼロだ。

それは厳しい基準で、それを満たすことが Part IV の仕事の大半だ。だが、なぜ基準がそこに、もっと 寛容などこかではなく置かれるのかをはっきりさせる価値がある。「四つの backend で byte 単位同一」 は完璧主義に聞こえて、実は言語の第一原則を一段下に適用したものだからだ。

parity は第一原則を codegen に適用したもの

Mere は、振る舞いをプログラムから読み取って確信できるものにするために存在する。では、言語が 四つのやり方で走りながら、それらが一致することを主張しないとしたら何を意味するか考えてみよう。 それは、backend がプログラムの振る舞いへの隠れた入力になることを意味する:同じソースが、 ブラウザ向けにコンパイルされると、インタプリタの下とは微妙に違う何かを表示するかもしれない —— 丸め、順序、エッジケース。たまたまどの backend を使ったかが、コードのすることを黙って変える 不可視な変数になる。それこそ、言語全体が廃するために建てられた、暗黙で環境的な影響だ。メモリと エフェクトを明示にしようと戦った言語が、最後の一歩で「どのコンパイラか」に結果を静かに変えさせる なら、それは自らと矛盾している。

だから byte 単位の parity は、最後に取り付ける品質保証の贅沢ではない。明示的な型・明示的な エフェクトと同じ約束を、実装へ持ち込んだものだ:backend の選択は意味論的変数であってはなら ない。 インタプリタがプログラムの意味を定め、そして各コード生成器は、その意味をバイトまで 再現する義務を負う。いかなる乖離も、許された方言ではなく、backend のバグだ。

予想外の配当:backend が互いをテストする

四つの backend を同一の出力に保つことには、コストを十分に上回る見返りがある。プログラムを四つの やり方で走らせ、出力を比べる:どの二つかが食い違えば、少なくとも一つの backend が間違っていて、 正解が何だったかを書き下すことなしにバグを見つけたことになる。冗長性がそれ自身のテスト オラクルだ。

これは差分テストで、ここでは backend が同一であるはずだからこそ、異例に安い。ふつう、コンパイラを テストするとは期待出力を手で著すことを意味する —— 退屈で、エッジケースへの想像力の分だけしか 良くならない。一致せねばならない四つの実装があれば、あらゆるプログラムが、四つを自動的に互いに 照らすテストになる。文字列比較の、variant タグのレイアウトの、評価順序の微妙なバグ —— 目視で 見つけるのが苦行の類 —— が、同じ入力に対して二つの backend が違うバイトを表示した瞬間に現れる。 インタプリタは、最も単純ゆえ、たいてい「正しい方」の役を演じ、コード生成器はそれに合わせられる。

台帳は正直なままに

四方向 parity は大きな表面で、プロジェクトはそれをカバレッジ行列で追う:あらゆる言語 feature、 あらゆる builtin について、四つの backend それぞれに印を —— 済み、部分的、まだ。その台帳の 正直さが規律の一部だ。C backend の下でしか動かない feature は済みと印されない;「C のみ」と 印され、目に見えて未完で、LLVM と Wasm が追いつくときに払うべき負債だ。ひと握りのもの —— 浮動小数点、より風変わりな標準ライブラリ関数のいくつか —— はインタプリタのみのままで、行列は 「どこかで動く」を「動く」に成りすまさせるのではなく、そうと平然と述べる。プロジェクトが自らに 課す基準は、feature が完成するのは四つの backend すべてがそれに一致したときであって、一つが それを走らせたときではない、ということだ。

その原則は一見よりさらにスケールする —— 後の Part が、セルフホストされたコンパイラの出力が backend 間で同じ byte 単位の一致を保つのを見せる —— が、形はすでにここに見える:ものを一度 より多く作り、写しが一致すると主張し、その一致に検証をさせる。

なぜが据わったので、Part IV の残りはその方法だ。次回:インタプリタと C backend —— 基準としてのツリーウォーキング評価と、コンパイルされたコードへの最初の翻訳。そこでの面白い 問題は、クロージャを持たない言語でクロージャをどう表現するかだ。

← Back to Mere: 言語を作る