WebAssembly backend:サンドボックスへコンパイルする
WebAssembly は四つのターゲットの中で最も制約が強い:一つの平らなメモリ配列を持つスタックマシン、malloc なし、関数ポインタなし、そして自分だけでは外の世界に触れる術がない。それが欠くもののそれぞれを、コンパイラが供給する —— バンプアロケータ、クロージャのための関数テーブル、I/O のための host import。そしてその最後の制約は、言語自身のケイパビリティの考えと韻を踏むことになる:サンドボックスでは、世界に影響する権限は手渡されねばならない。
三つの backend を終え、すべて同じ出力に保たれた。四つ目は、あらゆる比較に居合わせてきたが、それ 単独で調べたことはなく、そしてこの組の中で最も奇妙なターゲットだ:WebAssembly。C が人のための 言語で、LLVM IR がコンパイラのための形式であるのに対し、Wasm はサンドボックスのための命令セット だ —— 信頼できないコードをウェブページの中で安全に走らせられるよう、ほぼすべてを意図的に剥ぎ取られた マシン。そこへコンパイルすることは、この Part を貫くテーマの最も明瞭な例だ:ターゲットがくれない ものは何であれ、コンパイラが作らねばならない。
ほぼすべてを欠くマシン
四つの不在が Wasm backend の形を定め、そのそれぞれが、C と LLVM のターゲットが静かに提供していた ものだ。
式もレジスタもない —— Wasm はスタックマシンだ。200 を push し、100 を push し、加算命令が
両方を pop して結果を push する。コードは、入れ子の式や SSA レジスタではなく、スタック操作の列と
して吐かれる。
アロケータがない。Wasm がくれるのは一つの平らなバイト配列 —— リニアメモリ、始まりは 64KB の 1 ページ、必要に応じて成長する —— それだけだ。だからあらゆる構造化された値は、その配列への offset に 住み、グローバルポインタをバンプして配られる。タプルは連続する offset のその要素;レコードは宣言順の そのフィールド;variant はタグと、もし持つなら、その直後のペイロードだ。region はただ、バンプ ポインタの save と restore だ。Part II のメモリモデル丸ごとが、一つの成長するバイト配列の上に 建て直される。
関数ポインタがない。Wasm は関数のアドレスを取らせない。代わりに、関数は関数テーブルに住み、
その中のインデックスで参照される。これがクロージャの正体を変える。C と LLVM ではクロージャは
関数ポインタと環境ポインタの二語の対だった;Wasm ではそれが、リニアメモリへの offset(環境の
ため)と関数テーブルへのインデックス(コードのため)の対になる —— 同じアイデアだが、Wasm が
コードとデータを別々の世界に保つから「ポインタ」が二つに割れる。それを呼ぶのはテーブル越しの
call_indirect だ。素のトップレベル関数は、テーブルに登録される小さなアダプタをもらい、それも
インデックスで名指され値として渡せる。
そして I/O がない —— 表示なし、ファイルなし、サンドボックスの外へ届くものは何もない。その最後の 不在が最も重要で、それ自身の節を得る。
Wasm が易しくする一つのこと
引き算ばかりではない。Wasm は、他の二つの backend が決して持たなかった簡略化を返す:あらゆる値が
単一の 4 バイト語だ。 スカラーはその語を直接;構造化された値はその語をメモリ offset として使う。
すべてが一様に一語幅だから、backend は型ごとのレイアウトを要さない —— C と LLVM がやらねばならなかった
単相化、多相型の各具体インスタンスに別々の構造体を吐くこと、が単に要らない。一つの Vec 実装が
どの要素型のベクタも扱う;多相 variant は特殊化を要さない。最も多くを奪うターゲットが、この一点では、
最も少なくしか求めない。「より制約が強い」が単一の軸ではないことの有用な注意書きだ —— 各ターゲットは、
それ自身の場所で難しく、他の場所で易しい。
サンドボックス、そして必要によるケイパビリティ
最も重要な不在は I/O だ。Wasm モジュールは、それ単独では、純粋な計算だ:計算はできるが、行を表示
することも、ファイルを読むことも、ネットワーク呼び出しをすることもできない。そのどれの命令も持たない
からだ。外の世界に触れる何もかもが、host import —— 周囲の環境が提供する関数 —— でなければならない。
Mere の print は import された puts への呼び出しにコンパイルされ、host —— テストでは Node.js、
他の場所ではブラウザ —— がその呼び出しを受け、モジュールのリニアメモリから文字列を読み出し、実際の
出力を行う。ファイルの読み書きも、host が提供すると選べば、同じように動く。
これは立ち止まる価値がある。言語がいくつもの Part 前に、まったく違う理由で自らに決めた何かと韻を 踏むからだ。Part III はケイパビリティ渡しの上にエフェクトシステムを建てた:関数は、値として明示的に 手渡された権限を通してしか世界に影響できず、手を伸ばすべき環境グローバルはない。WebAssembly はまさに その規律を、マシンのレベルで強制する —— モジュールは環境的な力をまったく持たず、それが起こしうる あらゆるエフェクトは、host が import テーブルを通して与えねばならなかった何かだ。言語は設計によって 権限を明示にする;サンドボックスは必要によってそれを明示にする。世界に触れたいプログラムは、その手段を 与えられねばならない、門が Mere の型システムであれ Wasm の import リストであれ。まったく別々の二筋の 推論 —— 一つは検証可能なプログラムについて、一つは信頼できないコードを安全に走らせることについて —— が同じ形に到達し、Mere を Wasm へコンパイルすることが、それらの出会うところだ。
それが解き放つもの
Wasm backend が据わって、C と LLVM を通してネイティブに走ったのと同じプログラムが、いまやブラウザの 中でも、あるいは任意のサンドボックス化された Wasm host でも走り、そして —— 他と同じやり方で、wat2wasm と host ランナーと byte 単位の diff で検証されて —— 同一の出力を生む。その可搬性はそれ自身が目的ではない; それは、後に言語自身のツールがウェブページに住めるようにするものであり、続く Part がそれを直接土台に する。
一つの言語に四つの実行経路がいま存在し、feature ごとに一致し、同一のバイトに保たれる:基準のための インタプリタ、到達範囲のための C、最適化のための LLVM、サンドボックスのための Wasm。コード生成は、 この時点で、普通のプログラムについては解決済みの問題だ。まだできないのは、他の言語で書かれたコードと 話すこと —— C 関数を呼ぶこと、あるいは JavaScript がブラウザで走る Mere のクロージャへ呼び戻すこと —— だ。次回:外部関数インターフェース、そして、ネイティブライブラリへ呼び出すことと、host から呼び 込まれることという、まったく違う二つの問題。