取られていた名前
Scheme インタプリタは関数を `run` と名づけ、C backend ではその呼び出しが代わりにシェルコマンドにコンパイルされた —— `run` も組み込みで、コンパイラがユーザの定義を考える前に組み込みをマッチしたからだ。インタプリタは常に正しくやっていたので、コンパイル backend だけが間違っていた。同じバグの遅い行進の五つ目の名前だった —— index、remove、acct、dup、そして今度は run —— そしてパターンは、一例ずつ扱うのをやめて種類ごと終わらせるのに十分明らかだった。一つのヘルパが今、どの組み込みが名前を主張するより先に、その名前がユーザに束縛されているかを問い、衝突しやすい約 30 の組み込みをその呼び出し地点で守る。ガードは証明可能に安全 —— ユーザが実際に組み込みを shadow したときだけ振る舞いを変えるので、そうしなかったすべてのプログラムはバイト単位で不変だ。ユーザの定義は組み込みに勝つべきで、今そうなる。
Scheme インタプリタには run という名の関数があった —— 各トップレベル形を読んで評価するループだ。
インタプリタでは動いた。C backend ではプログラムは奇妙なことをした。自分のソーステキストをシェル
コマンドとして実行しようとしたのだ。原因は、run も組み込みで —— 外部コマンドラインを実行する手段 ——
コンパイラのコード生成器が、ユーザが同じ名前の関数を定義したことを考える前に、呼び出し地点でその
組み込みをマッチしたことだった。インタプリタは名前を普通のやり方で解決し、後の束縛が前を shadow する
ので、常に正しかった。コンパイル backend だけが先に組み込みへ手を伸ばした。
五度目はパターン
これは衝突した最初の名前ではなかった。五つ目だった。以前の probe は index と remove が C ライブラリ
シンボルと衝突して躓き、台帳は関数を acct と名づけ、それは POSIX 呼び出しだ。各々その場で修正され ——
これはサニタイズ、あれは改名 —— そして各修正は、危険な名前のリストが本質的に不完全だと静かに認めていた。
同じ形の五つの事例は、事例を直すのが正しい手でなくなる地点だ。バグは本当は run や index や acct
についてではなかった。ユーザがすでに束縛した名前を組み込みに主張させる解決順についてだった。修正は種類を
終わらせねばならず、六つ目の名前をリストに足すのではなかった。
ユーザを勝たせる
コードベースにはすでに前例が埋まっていた。よくある名前を持ついくつかの組み込み —— スレッドの join、
文字の述語 —— は、名前がユーザに束縛されていれば脇へどくガードを持ち、各々が誰かを噛むたびに一つずつ
足されていた。修正はその前例を一つのヘルパに一般化する。一つの問いを問う —— この名前は local に束縛
されているか、囲むスコープから捕捉されたか、内側の関数から持ち上げられたか、トップレベルで定義されたか
—— そしてそのガードを、ユーザがもっともらしく選びうる約 30 の組み込みに置く。run、spawn、even、
odd、show、abs、その他。答えが yes なら呼び出し地点は普通のユーザ呼び出しの道へ落ち、no なら
組み込みが以前とまったく同じに出る。
証明可能に安全、そして問題の半分が閉じる
変更を広く、一挙に入れられたのは、それが構成上安全だからだ。ガードはユーザが実際に名前を束縛したときだけ
発火する —— そして run を関数として束縛するプログラムは、コンパイラがすでに誤コンパイルしていた
プログラムだ。組み込みを shadow しないすべてのプログラムは以前とバイト単位で同一の出力を出す。テスト
スイート全体がそれを確認し、変わらず通った。これが閉じるのは予約名問題の組み込み半分だ —— ユーザの
定義が呼び出し地点で Mere 自身の組み込みに勝つ。もう半分、生成コードでの C キーワードや POSIX シンボルとの
衝突は依然名前サニタイザが扱い、完全に一般的な修正 —— すべてのユーザ名を名前空間化して何も衝突できなく
する —— は据え置かれたままだ。組み込み半分が閉じた今、それはより小さな残りの利得のためのより大きな変更
だからだ。五度再発したバグの種類が今や構造的に消えた。それはその一例への修正よりも価値がある。