float を走らせたことのなかった backend
raytracer —— 球が三つ、鏡面バウンス、硬い影、ベクトル演算はすべて float の tuple —— は interpreter と C backend で同一にレンダリングされ、WebAssembly のアセンブラで死んだ。local.set は i32 を期待したが f64 が来た。原因は機能の欠落より奇妙だった。Wasm の float は box され、boxing には raw f64 の一時変数が要り、その一時変数を正しい型で宣言する機構は存在して動いていた —— だが読んでいたのは top-level の emitter だけ。関数の emitter 三つは、すべての local を無条件に i32 と宣言していた。top-level の float 式は常に動いた。名前付き関数の中の float 一時変数は、一度もアセンブラを通ったことがなかった。テストスイートは emit されたテキストを assert し、アセンブルするのは整数プログラムだけだったので、この穴はプログラムが気にするまで見えなかった。
probe リストの最後の未測定軸は、WebAssembly backend の float だった。C backend は float の probe を済ませていた —— Mandelbrot、見つかったのはドキュメントの穴で、churn は無かった —— だが Wasm は float を違う形で表現する。box されるのだ。heap に確保した f64 への i32 ポインタで、 演算のたびに unbox し、終われば re-box する。backend の「すべては i32」という一様な値モデルを 保つために。raytracer は自然な forcing program だ。float 演算がほぼすべて、1 ピクセルに数千の 演算、そのひとつひとつが box。巨大な地面球の上に球が三つ、平行光源、Lambert と鏡面反射の シェーディング、硬い影、鏡面バウンス一回 —— そして、すべての backend が同じ checksum を print し 同じ画像ファイルを書かねばならないという設計上の制約。どこかで食い違えば、隠れる場所はない。
二つの backend で同一、三つ目で死ぬ
interpreter はレンダリングした。C backend もレンダリングした。バイト単位で同じで、画像は正しく
見えた —— 鏡面球に映る反射、球の下に溜まる影。Wasm ビルドは一命令も走る前に失敗した。アセンブラが
local.set expected [i32] but got [f64] でモジュールを拒否したのだ。実行時の誤答ではない ——
型として不正なモジュール。コンパイラは、WebAssembly が「それは WebAssembly ではない」と言う
WebAssembly を emit していた。
ひとつの emitter だけが読んでいた機構
失敗していた関数はピクセルの量子化器で、失敗していた命令はリテラル 255.999 の boxing だった。
float の boxing は、アロケータが 8 バイトを切り出す間、raw f64 を一時 local に保持することを
意味する —— そして Wasm の local は型付きなので、その一時変数は f64 と宣言されねばならない。
奇妙なのはここだ。このための機構は存在し、正しかった。mint されたすべての一時変数の型を追跡する
リストがあり、そこへ記録する f64 用のヘルパがあり、top-level のプログラム emitter は local を
宣言するとき忠実にそのリストを読んでいた。だが「関数」の emitter 三つ —— top-level 関数、持ち上げ
られた内部関数、closure adapter —— は一度も相談しなかった。各々が余分な local をハードコードの
i32 で、カウンタの言う個数だけ、型を無視して宣言していた。
つまり分業がバグを完璧に隠していた。プログラムの top-level にある float 式は main-body emitter に
落ちて動く。同じ式が名前付き関数の中にあると関数 emitter に落ちて不正なモジュールを生む。テスト
スイートの float テストは emit されたテキストを assert していた —— 出力に f64.add はあるか ——
アセンブルはせずに。そしてアセンブルして走らせるテストはセルフホスト系で、それはコンパイラで、
端から端まで整数と文字列だ。すべての float テストは通り、float の関数は一度もアセンブラを通った
ことがなかった。この backend は、正確な意味で、実際のコードの形をした float プログラムを一度も
走らせたことがなかった。
三つの emitter、ひとつのリスト
修正は、関数 emitter 三つに main emitter が昔からやっていたことをさせるだけだった。型リストを save し、関数ごとに reset し、余分な local を記録された型で宣言する —— closure adapter にはひとつ ひねりがあり、その先頭数個の local は capture の展開で常に i32、追跡された一時変数の前に置く。 整数だけのプログラムは i32 の一時変数しか mint しないので、出力は以前とバイト単位で同一だ —— これが重要なのは、セルフホストの fixpoint が emit された WAT を文字列として比較し、セルフホスト されたコンパイラが整数プログラムだからだ。fixpoint は微動だにしなかった。宣言が直ると、raytracer は アセンブルされ、走り、interpreter や C と同じ checksum を print し、バイトまで同じ画像ファイルを 書いた。三つの backend、一枚の絵。それをレンダリングするのに何が支払われたか —— その測定は独立した 一話に値する。