バイトが入口を通る

30 行の CRC-32 ツールは新しい bitwise 演算子で自明だった。発見は入力側にあった: ファイルを読むと文字列が返り、C backend では文字列は NUL 終端なので、ゼロバイトを含むバイナリファイルはそこで黙って止まる。25 バイトのファイルが 2 バイトとして読まれ、自信満々の誤ったチェックサムを吐いた —— 一方 NUL を保持する文字列を持つ interpreter は正しく読んだ。「文字列はただのバイト列」という約束は、一つの backend でしか真ではなかった。

merebytesbackendsiolanguage-design

bitwise 演算子はまるまる一群のプログラムを急に安く書けるようにしたので、次の probe は CRC-32 チェックサムツールだった —— 30 行、アルゴリズムはその多項式からほぼ書き写せる。標準テスト文字列 の正しいチェックサムを一発で計算した。面白い失敗は算術の中には無かった。ファイルをそもそも プログラムに入れる所にあった。

一つの backend でだけ守られた約束

Mere の文字列は常にバイト列として文書化されてきた —— メモリモデルのノートがそう言い、Unicode の 話がそれに寄りかかった。だが「文字列はバイト列」は、意味を持つには全 backend で真でなければ ならない主張であり、そうではなかった。ファイルを読むと文字列が返り、C backend では文字列は NUL 終端の char* だ。真ん中あたりにゼロバイトのあるバイナリファイルを渡すと、そこで黙って読むのを やめる。probe のテストファイルは先頭近くに NUL のある 25 バイトで、C ビルドはそのうち 2 バイトを 読み、内部整合的で、もっともらしく、完全に誤ったチェックサムを吐いた。文句を言わずに NUL を運ぶ 本物の OCaml 文字列を持つ interpreter は、25 バイト全部を読んで正しい答えを得た。つまり 2 つの backend が食い違った —— エラーではなく、2 つの異なる自信満々の数で —— そしてテストが走る方が、 たまたま正しい方だった。これは整数幅の話とまったく同じ形だ: 言語がした約束はテストされた場所で だけ守られ、そしてバイナリファイルが、それが壊れる場所の先に届いた最初の入力だった。

バイト型でなく、バイトの vector

修正には設計の分岐があった。正直なバイナリ入力の経路は、どこでも同じ意味を持つ値であり、誘惑は 専用のバイト型を導入することだ。より安い手 —— そして採った手 —— は、ファイルを整数の vector に 読むことだった。1 バイトにつき 1 要素、各々 0 から 255 の範囲、言語が既に持っていた vector 機構の 再利用。ファイル 1 バイトあたり 8 バイトの記憶はぱっとしない比率で、正直なコストとして書き留めて ある。プログラムがその比率を痛くする日が、本物のバイト型が forcing program を得る日だ。 interpreter と C backend は reader を得た。wasm と LLVM は位置情報つきの compile エラーで拒否する。 それらの host ファイルインターフェースがまだ NUL 終端の文字列を扱うからだ。一つの微妙さが専用の デバッグセッションを稼いだ: 新しい reader は vector を返し、その vector の region を名指す型変数は reader が呼ばれる地点で固定されねばならない —— vector コンストラクタがやるのとまったく同じに —— さもなくば region は未解決のまま残り、C backend は返された vector を取る関数を黙って emit しなく なる。probe はそれも踏んだ。ふさわしいことだ: 値が黙って到着しないことについての話が、コンパイラ の一段下で、黙って到着しない 2 つ目の値を見つけたのだから。チェックサムツールはいまテストと NUL 入りの両方のファイルでシステムの zlib と一致する。次の話はまったく別のファイル形式を取り上げ —— そして実在する言語より貧しい言語を記述している文書を見つける。

← Back to Mere: 言語を作る