影の中の 3 バグ
解凍器は interpreter でコンパイルして綺麗に動き、それから C backend で 3 通りにコンパイルに失敗した。C キーワードにちなむ param が宣言と参照で食い違い。別の関数が同名の inner helper を持つせいでローカル変数が capture から消え。Vec を返す match アームが存在しない struct の compound literal を emit。3 つの未宣言識別子エラー、どれも過去のテストが届かなかった —— 本物のプログラムがその下の機構に落とす影だ。
解凍器は interpreter で一発正しく動いた。それから C backend に渡され、コンパイルを拒んだ —— 3 通り別々に、どれも生成された C の未宣言識別子エラーで、どれも closure-lifting 機構の、どの テストも押していなかった隅にあった。interpreter はそのどれも見なかった。それこそが生き延びた 理由だ: interpreter はソースを直接評価するが、これらのバグは C への翻訳に住む。ネストし、再帰し、 capture の多い関数で密な 300 行が、それらを見えるほど大きな影を落とした最初のものだった。
宣言と使用で食い違う名前
一つ目は index という param だった。ソース言語では申し分ない名前だが、index は C の文字列
ライブラリの関数なので、backend は常にそういう名前を全ての参照地点で index_ に sanitize して
きた。宣言地点ではしていなかった —— だから生成された関数は param index を宣言し、body は
index_ を参照し、それは未宣言だった。修正は両方を sanitize すること、そしてそれは 1 箇所でなく
2 箇所に適用せねばならなかった: lifted 関数の param と、匿名 closure adapter の param ——
深くカリー化した inner 関数が取る形だ。解凍器の Huffman デコーダは後者になるほどカリー化されて
いて、だから 2 つ目の、より巧妙な経路を見つけた。
同名者に盗まれた変数
二つ目が最も奇妙だった。ただのローカル変数 p —— let で束縛され inner ループに読まれる ——
が、そのループの capture 変数から黙って落とされ、生成された呼び出しがスコープに無い p を
参照した。原因は関数を跨ぐ名前衝突だ。コンパイラが inner 関数を top-level に lift するとき、その
inner 関数の名前を capture 値として扱ってはならない。それを inner 関数名のglobal なマップで
確認していた —— そしてプログラムの別の場所の別の関数が、たまたま p という inner helper を
持っていた。だから global な確認は「p は inner 関数だ、capture から除外せよ」と言い、ローカル
変数 p は、何の関係もない同名者と一緒に投げ捨てられた。修正はこの確認を関数ごとにスコープする:
名前は同じ host の inner 関数を指すときだけ除外される。自由変数解析はずっと正しかった。下流の
filter がその答えを盗んでいたのだ。
存在しない型のゼロ値
三つ目はパターンマッチにあった。結果型が Vec の match は、どのアームも一致しなかった場合に
落ちる値を要する(決して走らないが型検査を通らねばならない abort 経路)。スカラーなら 0、
struct なら {0} compound literal。コードは variant 名の mangler で struct 名を組み
(Vec___heap_int){0} を生んだ —— だが Vec は C backend では値渡しの struct でなくポインタで、
literal を作る Vec___heap_int 型など存在しない。ポインタ型のコンテナは compound literal でなく
null ポインタに落ちねばならない。修正はコンテナと他のポインタ結果型を 0 に回す。どれも小さい。
合わせてこの話の教訓であり、dogfooding が新しい角度から教え続けるものだ: 機能は明るく、テストは
狭く、バグは、本物の要求の重いプログラムだけが落とせるほど重い影の中に住む。3 つとも直り、解凍器
は workaround なしでコンパイルする —— そして即座に、それらを霞ませる問題を露わにする。それが次の
話だ: 1 メガバイトを展開するのに 484 メガバイト。