32ビットのままの整数

機能を足すのは目に見える仕事だ。足さないと決め、その理由を示せること — それが言語設計の、より難しい方の半分だ。

merelanguage-designessaywasm

Mere は 4 つのバックエンドにコンパイルする。そのうち 3 つ — インタプリタ、C バックエンド、 LLVM バックエンド — は整数を 64 ビットとして扱う。4 つ目の WebAssembly は 32 ビットだ。 次にやるべきことは自明に見える。Wasm の整数を 64 ビットに広げて、4 つを揃えればいい。 私はこれに 1 セッションを費やし、何も出荷しなかった。面白い仕事はむしろ「やらないと決める」 ことの方にあったからだ。これは、その決定がどう下されるかについてのノートだ。結論よりも、 それを生んだ方法についての。

ミスマッチではなく、契約から始める

「3 つが 64、1 つが 32」を見た最初の直感は、32 の方をバグと呼ぶことだ。だが違う。Mere は 整数幅の統一を約束したことが一度もない。言語リファレンスは最初からはっきり書いている — C と LLVM は 64 ビット整数、インタプリタはホストのネイティブ int(通常 63 ビット)、Wasm は 32 ビット。32 ビットに収まらない整数リテラルは、Wasm ではコンパイル時エラーになる。 範囲を示し、より広いバックエンドを指し、ドキュメントへリンクするメッセージ付きで — 黙って 切り捨てるのではなく。

だから「Wasm は 32 ビット」は破られた約束ではない。それが約束なのだ。機能を却下する最初の 一手は、自分が実際に何を約束したのかを読み直すことだ。「直すべき」と感じたものの驚くほど多くが、 「これは文書化された想定範囲が意図どおり働いているだけ」に溶けていく。本当の隙間は、黙って オーバーフローする計算だけ — そしてそれはどのバックエンドも自分の幅でやることで、C と その子孫が 50 年やってきたのと同じだ。

矛盾に見える非対称

正直に答えなければならない反論はこれだ。Wasm の float は既に 64 ビットになっている。float は ボックス化されている — 値は線形メモリ上の 8 バイトの f64 を指すポインタで、演算は unbox し、 ネイティブの f64 命令で計算し、再び box する。float にその手間をかけたなら、なぜ int には かけないのか。

両者が対称ではないからだ。そしてそれを見抜く方法は、それぞれを「box しなかったら」 何が起きるかを問うことだ。

int を 32 ビットのままにすると、同じ意味狭い範囲で得られる。ほとんどのプログラムは 無影響で、境界は稀、しかもリテラルなら loud(明示的なエラー)だ。float を 32 ビットのままに すると、ほぼ全ての計算で丸めが変わる — 0.1 すら同じようには表現できない — ので、 バックエンド間の結果が全域で silent に食い違う。float には parity を保つ「box しない」選択肢が 無かった。int にはある。同じ機構、逆の判定。壊れ方が違うからだ。ここに一般則が隠れている — 同じ仕掛けが、ある型には正しく別の型には誤りであり得る。決め手の問いは「一貫しているか」では なく「放っておいたら何が壊れるか」だ。

代替案を最後まで設計する

「float と同じく int も box する — それが唯一の整合方式だ」で止めてしまうのは簡単だった。私は その一文をあやうく書きかけた。それは誤りで、誤りだと示したのは、最初のもっともらしい設計で 止めずに設計を最後までやり切ったことだった。

Wasm に 64 ビット整数を与える方法は 2 つある。float のように box する。あるいはオブジェクト モデルの全ての値スロットを 4 バイトから 8 バイトに広げ、int を unboxed で格納する。box-int は 小さく、float と気持ちよく対称だ — そして、このバックエンドが存在する理由そのものを静かに 台無しにする。Wasm のアロケータは実行中に解放しない bump ポインタで、このバックエンドが 生きる理由はブラウザだ。長時間動くページ、プレイグラウンド、キー入力のたびに整数演算をする ゲーム。全ての int を box すれば、そのそれぞれが二度と戻らない 8 バイトを確保する。ニッチ — ブラウザタブの中の bignum 演算 — のための correctness 修正が、主用途をリークさせる。これは 差し引きマイナスだ。もう一方、8 バイト統一スロットは本当に正しいが、コードジェネレータの およそ 1000 箇所に加え、JavaScript ホスト全体 — チャンネルも含めて全てを BigInt に移す — に触れる。現在の需要ゼロに対する巨大な書き換えだ。

私が「唯一の方式」と呼びかけた代替案は、2 つのうち悪い方だった。それを最後まで設計したことが、 この決定を肩すくめから判断へと変えた。

領収書を書く

機能を却下することは、いつなら受け入れるかも記録して初めて誠実になる。この決定はトリガと 共に書き留められている — ブラウザの中で 64 ビット整数を本当に必要とする最初の実 dogfood が 現れた時 — そしてその日のために手を伸ばすべき設計と共に。動機が一発実行の correctness なら box-int、長時間実行や性能なら uniform-i64、それぞれに前提条件を名指しして。閾値が文書化された 「ノー」は、成長の拒否ではない。急いだ「イエス」よりも、小さく鋭いコミットメントだ。

規律

機能を出荷するのは可読だ。diff があり、バージョンが上がり、変更履歴に 1 行載る。却下することは、 外から見れば何も起きなかった一日のように見える。だが言語は、吸収したものと少なくとも同じくらい、 拒んだもので定義される — 受け入れた機能はどれも、次の読み手が永遠に背負う重さだ。32 ビットの ままの整数は、埋められるのを待つ隙間ではない。それは決定だ。背後に契約があり、それを正当化する 非対称があり、バックエンドの本当の目的に照らして測られた却下済みの代替案があり、変わり得る日を 名指しした領収書がある。それが、良いノーの形だ。

追記:領収書の期日が来た

これを書いた翌日、トリガが現れた — 偶然つまずいたというより半ば迎えに行った形だが、いずれに せよ普通のプログラムからだった。小さな日時ユーティリティ mdate:Unix エポックをミリ秒で渡すと UTC のタイムスタンプが返る、すべて整数演算だ。インタプリタとネイティブバックエンドでは参照実装と バイト単位で一致する。それをブラウザの中で時計にすると、間違った時刻を表示するのではなく — トラップする:今日のエポックミリ秒、約 1.75×10¹² は 32 ビットに収まらず、float から int への 変換がそれを拒む。

これは領収書の条件そのものだ:本当の dogfood — どのみち書くたぐいのもの — が、ブラウザの中で 64 ビット整数を必要とし、しかも長時間実行(一発ではなく時計)だ。だから設計はすでに名指しした 方 — box ではなく uniform な 64 ビットスロット。

以前の決定は間違っていなかった。需要ゼロに対してその重さを背負わずに済む一ヶ月を買い、そして今 この瞬間を、新たな議論ではなく二行の参照で済ませてくれた。それこそが領収書の要点だ:良いノーは 永久ではなく、期限条件つきの決定であり — その規律は、条件が発火した日にそれを守って初めて完成 する。

← Back to Notes