違う痛み方をするよう選ばれた二つ目の dogfood

Mere で建てた最初のアプリケーションは Wasm-and-Node ホスト上の web アプリで、あらゆるシステムコールがそれに供給された。二つ目は、その快適さを取り除くよう意図して選ばれた:native バイナリにコンパイルされる jq 風のコマンドラインツールで、web ホストが常にそれに代わって答えてきた問いに C backend が答えるよう強いた —— 引数をどう読むか、標準入力をどう読むか、exit コードをどう立てるか、そして最も鋭く、二つのライブラリを内部を衝突させずにどう合成するか。ツールは実だったが、friction log がこそ狙いだった。

meredogfoodclic-backendlanguage-design

言語に何が欠けているかを見つけるやり方は、それで何かを建て、どこで抵抗するかを見ることだ。Mere で 建てた最初の実アプリケーションは小さな共同編集ノートサーバで、Node ホストの下で WebAssembly として 走り、多くを行使した —— だがホストは寛大だった。プログラムが必要とするあらゆるシステムコールを、 Node runtime が供給した:ファイル、ソケット、タイマー、標準出力。まるごと一クラスの問いが決して 現れなかった、何か別のものがそれに答えていたから。

だから二つ目のアプリケーションは、何を生むかでなく、どこで痛むかで選ばれた。JSON と CSV を コマンドラインで問い合わせる jq 風のツール、C backend を通して native バイナリ にコンパイルされる それは、web ホストが隠してきた領域にまさに着地する:環境的な runtime なし、引数と標準入力と exit コードを持つ実のプロセス、そして寛大なホストにロードされるのでなく共にコンパイルされねばならない ライブラリ。ツール自身は有用だ。だがその friction log —— 言語が押し返したあらゆる場所の走る記録 —— がバイナリと同じくらい成果物だった。

プロセスの縁の壁

最初の抵抗は即座で全面的だった。コマンドラインツールの目的そのものは引数と入力を読むことで、 interpreter ではこのすべてが動いた:引数ベクタを得る、標準入力から一行読む、ステータスコードで exit する、という builtin があった。native バイナリにコンパイルすると、そのどれも存在しなかった。C backend は argsread_line をあたかも関数であるかのように参照するコードを emit したが、決して定義せず、 C コンパイラは未定義識別子で止まった。native な Mere プログラムは、その瞬間、一つのハードコードされた ファイルパスを読めるだけで、それ以外は何もできなかった —— 引数も、パイプも、exit ステータスもなく。

これは寛大なホストが隠す gap のまさにその形だ。web ホストでは、引数渡しと入力はホストの仕事だった; 言語はそれらを実のオペレーティングシステムプロセスに lower するよう求められたことがなく、だから 決してしなかった。gap を閉じるのは、いったん見えれば劇的でなかった:引数ベクタが最初に来たので、 main はプロセスの argcargv を取り残りをプログラムに渡した;次に標準入力すべてを読むこと、 だからツールがパイプの末尾に座れる;次に実のプロセス exit、到達不能なエラーがすでにそうであったのと 同じやり方で C ライブラリの exit に lower される。それぞれが自分のリリースに着地した。どれも難しく なかった。要点は、プログラムがありふれた Unix 市民であろうとするまで、どれも存在する理由がなかった —— そしてそのとき、三つとも便利機能でなく前提条件だった、ということだ。

シャドウイングを忘れた backend

次の frictions の一家はみな一つの形を持っていた:C backend が、プログラムがその名前を rebind したか どうかをまず確かめずに、名前を builtin として扱う。並行処理の仕事が join builtin を足していた —— spawn されたスレッドを待つやつ —— そして CSV ライブラリは、まったく理にかなって、文字列を貼り合わせる 自分自身のローカルな join を持っていた。コンパイルすると、文字列 joiner はスレッド primitive への 呼び出しに変わり、C コンパイラは文字列には待つべきスレッドハンドルがないと文句を言った。dispatch は、 この特定の join が builtin なのかその前に立つローカル定義なのかを問う前に、名前 join に match していた。

interpreter と型検査器は常にそのシャドウイングを尊重していた;C backend はそうしない別の dispatch 経路を持っていた。同じ話がより小さな形で繰り返された —— interpreter と Wasm backend には存在するが C にはない文字列等価関数、数値では型検査が通るが文字列では通らない順序比較。それぞれが、一つの backend の coverage が他から静かにずれた場所で、それぞれが native ビルドがそこに歩み込むまで見えなかった。 四 backend の言語は、その parity をまさにこの通貨で支払う:gap は実で、個々には小さく、一度に一つの dogfood で表面化する。

最も鋭い発見:衝突した二つのライブラリ

演習全体の最良の発見は二つのライブラリを同時に要した。JSON 配列のパースは動いた。その傍らに CSV ライブラリを import して同じ配列をパースするとコンパイルに失敗した、使われるが決して宣言されない 変数で —— 消えてしまった、捕捉された文字列長。どちらのライブラリも単独では間違っていなかった; 組み合わせだけが壊れ、それはどちらの部品でもなくそれらを合成する機構の問題の署名だ。

原因は狩る価値があった。backend が内側の再帰関数をトップレベルに lift するとき、その関数がどの外側の 変数を捕捉したかを割り出さねばならず、それは lift された関数の名前を単一のグローバルなテーブルを 通して解決していた。両ライブラリがたまたま loop という名の内側ヘルパを含んでいた。JSON パーサの loop は自分自身を再帰的に呼んだ —— そしてその自己呼び出しが、グローバルテーブルを通して解決され、 CSV ライブラリの lift された loop に代わりに着地し、だから JSON の loop は CSV の loop が捕捉した 変数を継承し、それ自身の呼び出し元は聞いたこともなかった。二つの異なるファイルの二つの同名ローカルが、 ソースでは互いに見えないのに、コンパイラに静かに merge されていた。修正は、それらの名前をグローバル でなく host 関数ごとに解決することで、だから loop という名の二つの内側関数は別々のまま —— そして それは最小再現、それぞれ loop を持つ二つの関数、を回帰テストとして携えて来た。native ライブラリの 合成の実の限界、二つがついに一つのバイナリを共有するよう求められたからこそ見つかった。

言語が抵抗しなかったところ

friction だけを提示するのは歪曲だろう、log の最も有用なエントリの一つがその不在を記録するからだ。 途中で、クエリエンジンが一つの JSON 値を一つに map する関数から、セレクタが値を多くに爆発させ セレクタが sequence できる streaming モデルに再設計された —— ツールのコアへの非自明な変更だ。それは Mere でまったく抵抗なく書かれた:セレクタ型の新しい case、そして残りは普通の再帰。これは今までの あらゆる dogfood が言ってきたことに合う。言語のコア —— 代数的データ型、パターンマッチ、木と ストリームの上の再帰 —— は仕事が快適なところだ。痛みは縁に群がる:native な入力と出力、ライブラリの パッケージング、文字列と Unicode の隅。真ん中は保つ。

log がループだった

そのすべてを貫く糸はループで、ループが方法だ。実のプログラムが抵抗するまで建てられる;抵抗が精密な signal として書き留められる;鋭いものが言語自身の中の修正になる;そしてプログラムがその修正を採用し、 携えてきた workaround を落とす。ツールは、まだコンパイルできない CSV ライブラリの代わりに立つ手製の 文字列スプリッタで始まった;合成バグが修正されると、その workaround は削除されツールは実の ライブラリを import し、まっとうな引用符フィールドのパースをただで得た。バイト値で書かれた文字分類 ルーチンは、それらが型検査を通るようになると、なりたかった直接の文字列比較に書き直された。 workaround は傷跡でない;閉じたループの領収書だ。違う痛み方をするドメインを選ぶことは悲観でなかった。 言語が次に必要とする真のものを見つけ、それから建てる、最速のやり方だった。

← Back to Mere: 言語を作る