発端——8 月 26 日の報告
1.0 宣言の週の締めくくりに、開発の舞台裏をひとつ書く。第 18 弾の結びで「黙りは実装にも忍び込む」と一段だけ書いた、あのバグの一部始終である。
8 月 26 日、この言語を本番で使っている外部の実装者から、詳細な実測つきの報告が届いた。要旨はこうである——束縛に名前を付けた瞬間に、答えが変わる。
T = ordinalIn(day, month, d) everyDay |> filter(d => T == 3) # 「毎月 3 日」のつもり #=> (出力 0 行。エラーも警告も出ない)
同じ式をインライン展開すると(束縛 T をやめて右辺を直接書くと)、正しく毎月 3 日が出る。名前を付けただけで、点集合が黙って空になる。形によっては逆に評価窓の全日が返る——正解2 点に対して 59 点、である。
機序——最初に評価された点が「焼き付く」
原因は、直前に入れたばかりの最適化(束縛のメモ化)だった。右辺 ordinalIn(day, month, d) のd は、本来その束縛の中では意味を持たない名前である。ところが名前解決が呼び出し側のfilter(d => …) のラムダ変数まで素通しで落ちるため、最初に述語が評価された点の値が計算され、メモ化キャッシュに入り、以降の全点がその固定値を読む。
答え合わせが恐ろしいのはここからで、焼き付くのは「評価窓の 1 点目」ですらない。ストリームの実体化はエポック(1970-01-01)から始まるので、T には ordinalIn(day, month, 1970-01-01) = 1が固定される——窓を動かしても答えが変わらないという、デバッグ泣かせの性質まで持っていた。
そして、これが最悪の点だが——エラーも警告も出ない。第 18 弾で書いた「黙りの既定」の分類でいえば、最も深い黙りが、よりによって言語の実装自身に入っていた。
なぜ 1,583 本のテストが素通りしたか
報告時点で、参照実装のテストは 607 本、報告者側のテストは 976 本(写経実装に限らない全体の数)あった。合計 1,583 本が、このバグを踏まなかった。「右辺が呼び出し側のラムダ変数を掴む束縛」という形を、どちらの陣営もテストに書いていなかったからである——正しいと思っている形しかテストは書かれない。
見つけたのは、報告者が自分の追随作業に掛けた敵対的レビューだった。ここで特筆すべきは、報告者の写経は 1 対 1 で正確だったことである。写経が正確だったからこそ、欠陥ごと正確に写っていた——「写経のズレ」ではなく「写経した意味そのものが退行を生む」型。実装が 2 つ(参照実装と、その写経)あり、双方の実測を突き合わせる往復書簡があったから、この型のバグが言語仕様の問題として表面化した。
裁定——マクロ捕獲を合法にしない
修正の設計には分岐があった。この「呼び出し側の変数を掴む」挙動を仕様として認める道(マクロ的な捕獲)もないではない。だがそれを認めると、定義の意味が使用箇所の変数名に依存する——filter(d => …) の中では動き、filter(x => …) に改名すると壊れる。「定義はテキストだけで意味が閉じる」という言語の根本思想への正面の反である。
裁定は遮断——この形を誘導つきの静的エラーにした(ADR-53)。今の実装で同じ定義を走らせると:
束縛の右辺から呼び出し側のラムダ変数は見えない: d——束縛の意味は定義単体で閉じる(点に依存する値は引数付き束縛のパラメータで受ける: T(x) = … の右辺では x を使い、呼び出し側で T(d) と渡す。ADR-53)
エラーには直し方が書いてあるの流儀どおり、正道は文言の中にある:
T(x) = ordinalIn(day, month, x) everyDay |> filter(d => T(d) == 3) #=> 2026-01-03 2026-02-03
事件は一度で終わらなかった
ここからが事件簿の本題かもしれない。修正を公開するたび、報告者は追随し、追随のたびに敵対的レビューを掛けた。その網が、修正の穴を三度立て続けに見つけた。
第二報: 遮断が一経路(引数付き束縛の呼び出し)で漏れていた。あわせて、同時に入れた検査の記録条件に精密化が入った。
第三報: その精密化が、別の退行を新たに作っていた。修正前は暗黙に成立していた性質(検査の伝播)を、精密化が知らずに落としていた——「直しが次の穴を開ける」の教科書事例である。ここで得た教訓が最小修正の原則: 綺麗にしたくなっても、退行を封じる最小の変更に留める。
第四報: 退行を封じるために入れた再発防止テスト(witness)自体への監査。3 本のうち 1 本はその退行を検出しないこと、期待値の正規表現が緩く診断文言の取り違えを素通しにすること、3 経路ある修正の 1 経路にテストが無いことが、変異試験(実装をわざと壊してテストが赤くなるか確かめる)で示された。全部そのとおりだった。ここの教訓は一行にできる——再発防止テストは、赤くなることを確かめてから数えよ。
事件簿の教訓
- 黙るバグは、既存のテストを必ず素通りする。1,583 本の網の外にいたのだから。対抗手段は 網の内側を増やすことではなく、敵対的レビューと変異試験——「壊しに行く」工程を定常に置くこと
- 修正は最小に。精密化・一般化の誘惑は、退行の最大の供給源だった
- 参照実装と写経の往復に勝る検証はない。この夏、往復書簡は 19 便。仕様の隙間の大半は、 この往復が見つけた
1.0 が凍結したのは、こうして塞がれたあとの意味論である。バグゼロの主張ではない——見つかったものが黙って直されず、記録され、再発防止ごと仕様の一部になっているという主張である。設計判断の記録(ADR)53 本はすべて公開している。事件の顛末も ADR-53 とその追記に、この記事より詳しく書いてある。
ローンチ週はこれで締めである。来週からは通常の連載に戻る。
Kairos はスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント