他のプログラムを走らせる

四つのツールの一つ目は task runner —— それが建てを助けるまさにその言語で書かれた小さな make だった。選ばれたのは、task runner が runtime に証明的に欠けていた唯一の力なしには存在できないから:別プロセスを起動すること。その力を足すのは一午後だった。驚きはその後、計画が指さなかった場所に来た ——欠けたエラーチャネル、欠けたファイル検査、そして OS 自身の C ライブラリに埋まったグローバルロックだと判明した性能バグ。

meredogfoodsubprocessconcurrencylanguage-design

一つ目のツールは task runner だった:名前付きタスクの小さなファイルを読み、各々が任意の依存を 伴うコマンドで、それを正しい順に走らせる。名は mk ——縮小版の make。選ばれたのは世界がもう一つ make を要るからでなく、task runner が他のプログラムを走らせる力なしに存在できず、そしてそれこそが grep が「無し」と報じた力だったから。心地よい円環もあった:言語はその瞬間、コンパイラを自分自身に かける shell script によって建てられていた。言語で書かれた task runner は、言語を建てているまさに その種のコマンドを走らせることになる。ツールはまっすぐ自分の土台に向けられていた。

予測された gap と、それがいかに小さかったか

計画は痛みが subprocess 制御だと言い、計画は正しかった ——ちょうど一午後の間は。最初の run "echo hi" を動かすのに要ったのは builtin を一つ、run、最も単純で正直な型で足すことだった: コマンド文字列が入り、exit code が出る。interpreter ではそれは標準ライブラリのコマンド実行子への 呼び出しになり;C backend では exit status を取り出した system になった。それで全部だった。ツール 全体がそれを存在へ強制するために建てられたその能力は、数十行だと判明し、いったんそれが在れば マイルストーンは速く落ちた:出力を捕捉、タスクファイルを parse、依存をトポロジカル順に解決、最初の 失敗で short-circuit。それらの各々が、今やもう心地よい言語での平凡な作業だった ——record、「この タスクを走らせる」と「その依存を走らせる」の相互再帰、exit code と done フラグを運ぶために threading された tuple。痛みは皆無。その不在自体が一つの発見だ:コアは堅牢で、鋭い縁は全て OS との境界に 外在していた。

計画が名づけなかった gap

面白い失敗は計画が予測しなかったもので、それらは interpreter で走らせるでなくツールを native バイナリにコンパイルしたときにだけ現れた。mk が失敗したコマンドを標準エラーに報せる必要が最初に 生じたとき、C backend は単に stderr に印字する術を持たなかった ——interpreter は持っていたので、 プログラムは型検査を通り interpreted で走り、native build だけがその穴を露わにした。一関数の修正 だったが、どれだけ interpreter でテストしても surface しない実の gap だった。同じ話が、incremental build が出力ファイルが既に在るかを問う必要が生じたときに繰り返した:backend はファイルの更新時刻を 読めても、「これは在るか?」という平明な問いに答えられなかった。二つの小さな穴、共に同じ縫い目の 上 ——interpreter に在り、コンパイルされた backend に無い振る舞い ——そして共に同じ仕方で、正しい形 の実プログラムがそれを踏み越えることで見つかった。これは backend を一致に保つ論拠を否定形で述べた ものだ:それらが食い違うあらゆる場所は、正しい形のプログラムを待つ潜在バグである。

牙は OS の中にあった

最後のマイルストーンが最も強く噛み、それは誰も地図に印していない場所で噛んだ。独立したタスクを 並列に走らせるのはただ同然のはずだった ——言語には既に thread と parallel-map があり、タスクは構成 上独立だった。だが各々が 3 分の 1 秒眠る三つのタスクを並列に走らせると、3 分の 1 でなく丸 1 秒 かかった:それらは一緒でなく次々に走っていた。thread は実で、並列性は実でなかった。小さな C probe で切り分けると判定が出た:この OS では C ライブラリの system 呼び出しがグローバルロックを取るので、 それを呼ぶ幾つの thread も単一の扉の後ろに列をなす。並列性を絞め殺していたのは言語でなく、プラット フォーム自身の runtime に隠れた直列化だった。修正はその扉を通るのを完全にやめること ——より低水準の spawn 呼び出しでプロセスを起動し、個別に待つこと ——であり、その後は 3 分の 1 秒のタスク三つが 3 分の 1 秒で終わった、常にそうあるべきだったように。ツールが露わにするために建てられたバグは subprocess 制御で;それが実際に教えたのは、「コマンドを走らせる」と「他の皆を密かに直列化せずにコマンドを 走らせる」は二つの別の能力で、値するのは後者だけだ、ということだった。

終わりまでにこの runner は小さな make がすべきことを全てした ——parse、依存、トポロジカル順、 incremental な skip、並列グループ、exit code の伝播 ——そしてそれを native バイナリとして行った。 言語の五つのリリースを駆動し、それらのリリースの一つ一つが、今やこの一本だけでなく全てのプログラム に属する能力だった。予測された gap は実だが浅く;価値ある発見はそのすぐ後ろに立っていたもので、それが フロンティアで建てることの繰り返される教訓だ:見える gap に狙いをつけ、学ぶのはその影に隠れていた 何かだ。次のツールはまったく別の場所に狙いをつけた ——他のプログラムでなく、端末そのものに。

← Back to Mere: 言語を作る