仕様書がない巨大言語に、証明をぶつける意味
Python は「この挙動が正しい」と書かれた一枚の仕様書だけで動いているわけではない。現場では、実装である CPython が事実上の基準になり、参考になるのは公式マニュアルや周辺資料だが、そこには普通の使い方しか丁寧には書かれていない。端の条件、例外の途中発生、巨大な整数、辞書探索中の自己変更のような話になると、話が途切れる。
その空白を埋めるために、Lean のような定理証明系を使う。Lean は「たぶんこうなる」では通らない。関数が何を返すか、失敗するならどう失敗するか、すべてを型と証明で閉じ込める。曖昧さを残せば証明器が受け付けないので、実装の暗黙知を文章にし直す作業になる。人間の頭の中では当たり前だった前提が、そこで初めて露出する。
今回の試みで面白いのは、対象が小さなアルゴリズムではなく、CPython そのものだという点だ。公式ページでは、AI エージェントが 61万3318 行の Lean を書き、1539 ファイルにまたがる 18,898 個の定理・補題を機械に確認させたとされる。単純な翻訳ではない。Python の内部を、行ごと、枝ごと、例外の分岐ごとに写し取っている。
この種の作業がなぜ効くかは、証明より先に「本当に何を仕様とみなすのか」を問うからだ。CPython は古く、広く、そして例外的な挙動が多い。そこに証明を当てると、昔から“そういうもの”として流されてきた振る舞いが、急に説明を要する対象になる。ここで出てくるのが、誰も明文化していなかった挙動の正体である。
まず写し、次に突き合わせる。四段階で回す検査機
やり方は意外と素朴だ。最初に、CPython の関数を Lean に置き換える。しかも大づかみに要約するのではなく、条件分岐の順番も、エラーメッセージも、変な境界条件もできるだけ保つ。次に、その写しを実 Python と並べて動かし、少なくとも 5万件の厄介な入力で差分を見る。ここでズレたら、Lean 側のモデルがまずい。
そのうえで、性質を証明する。たとえば「こういう入力では必ずこの値になる」「この条件の範囲では結果が変わらない」といった命題を Lean に渡し、証明器に通す。だが本当に重要なのは最後の確認だ。別のエージェントが、自分では書いていないモデルをレビューし、元コードと食い違わないか、壊し方が残っていないかを探す。通ったものだけが採用される。
公式ページの数字をそのまま眺めると、かなりの規模だ。全体で 4071 個の関数がこのチェックを通過し、進捗は CPython 全体で 14% とされている。内訳を見ると、グラフや数字はかなり偏っていて、たとえば grammar rules は 100% だが、標準ライブラリはまだ 8% にすぎない。巨大な塊を一気に片づけたのではなく、検査しやすい場所から積み上げている。
この時点では、まだ「証明できた」だけでは安心できない。証明はあくまで写した Lean モデルについて成立する。元の CPython が同じとは限らないし、テストは試した入力に限られる。だからこそ、写経・差分・証明・再レビューの四段階が必要になる。ここで初めて、証明が発見器として働き始める。
“正しい証明”なのに外れる。そこで露見する落とし穴
厄介なのは、証明器が正しくても、現実の Python と合っているとは限らないことだ。単に「返るなら非負」といった弱い命題なら、壊れた実装でも通ってしまう。実際、最初の試みでは、レビューで見直した関数の半分にも満たないものしか生き残らず、あるファイルでは 21% しか有用な内容になっていなかったとされる。証明自体は正しかったが、意味が薄かった。
そこで、レビュー側は一つだけ厳しい問いを立てる。「壊れた版でもこの定理をすり抜けないか」。この問いに耐えられない命題は、証明できても採用しない。写したモデルが現実をどれだけ反映しているかを、定理の強さで測り直すわけだ。弱い命題を捨てると、今度はモデル側を直す必要が出てくる。途中でユーザーコードが例外を投げる可能性まで入れ直す、という具合に。
公式ページでは、こうした見直しの積み重ねが数値で見える。たとえば Objects/typeobject.c は 142 個中 135 個、Objects/genobject.c は 56 個中 56 個、Objects/setobject.c は 46 個中 46 個まで進んだとされる。一方で、Lib/enum.py は 9 回のレビューを経ても 39 個中 37 個、Modules/_json.c は 48 個中 45 個にとどまる。完了率のグラフが一様でないのが、むしろ実際らしい。
このばらつきは、単なる作業量の差ではない。証明しやすい関数と、証明すると現実の曖昧さが噴き出す関数がある。前者は写せば終わるが、後者は途中の失敗や例外伝播まで面倒を見る必要がある。日本の現場で言えば、ライブラリの表面的なユーティリティより、データ構造の更新や境界値処理のほうが問題を起こしやすいのと似ている。
418 件という数字が示すもの
この形式化の価値は、単に「バグを見つけました」で終わらないところにある。公式ページでは 418 件のバグ報告が出たとされ、そのうち約 55 件は通常の Python コードからでもインタプリタを落としたり、メモリを壊したりしうるものだという。残りは細かい仕様の食い違いが中心だが、細かいからこそ、放置すると利用者が気づかない。
わかりやすい例として、JSON エンコーダの話が挙がっている。コピー側はメモリ要求が必ず成功すると仮定していたため、入れ子の深さ次第では 16 テラバイトもの文字列を構築できる、と“証明”してしまった。現実の Python はそんな要求に耐えられず、メモリ不足になる。ここでは証明が間違ったのではなく、前提が実装の現実を見落としていた。
別の例では、pickle モジュールの性質を巡って、ほんの少し余分な手順が紛れ込むだけで挙動が変わることが示されている。こうした話は、単純なアルゴリズムの誤りというより、「誰も明示していなかった前提」を証明の側があぶり出したものだ。だから、418 という数字は単なる不具合件数ではなく、暗黙仕様の地層を掘り当てた回数だと見たほうが近い。
ここで気になるのは、どこまでが本当に CPython の問題で、どこからが形式化の副産物なのかだ。61万行の Lean、18,898 個の定理、4071 関数、そして 418 件。数字は十分に大きいが、内訳を見ないと意味が変わる。何が壊れていて、何が“まだ決めていない”のか。その境界の描き方こそが、この試みの核心に触れるはずで——
👉 続き(核心はここから)は PaPoo で読めます: https://papoo.work/doc/2c2980246d228d82
