Claude CodeでクリーンルームのZ80 / ZX Spectrumエミュレータを実装する
原文は Salvatore Sanfilippo により に公開されました。 このブログを購読する
Anthropicは最近、最新版のOpusである4.6に「クリーンルーム」環境でRustによるCコンパイラを書かせるという実験について記したブログ投稿を公開した。
この実験の手法には、彼らが何を示そうとしているのかという点で懐疑的にならざるを得なかった。なぜエージェントにISAのドキュメントを与えないのか?なぜRustなのか?Cコンパイラを書くというのは、まさに巨大なグラフ操作の演習だ。Rustではむしろ書きにくい類のプログラムである。また、クリーンルーム実験であるなら、エージェントは最適化コンパイラに関する確立された計算機科学の進展について、あらゆる情報にアクセスできるべきだ。いくつもの論文があり、それらをいくつかのMarkdownファイルに容易に要約できるはずだ。SSA、レジスタ割り当て、命令選択とスケジューリング。そうした事柄は前提としてまずリサーチされるべきであり、そうしても実装は依然として「クリーンルーム」であり続ける。
エージェントにインターネットや他のコンパイラのソースコードへのアクセスを許可しなかったのは、確かに正しい判断だった。理解しがたいのは、ほとんどステアリングしないという原則だが、これも大規模プロジェクトを完全に自律的に書き上げる様子を示すのが目的であれば、ある種の実験としては筋が通っている。とはいえ、これはほとんどの場合、コーディングエージェントが実際に使われている方法ではないことを私たちは皆知っている。コーディングエージェントを日常的に使い込んでいる人なら誰でも、コードに一切触れなくても、要所で少し手を入れるだけで結果の質が劇的に変わることをよく理解している。
Z80実験
私も同様の実験を自分で試してみるべき時だと思った。せいぜい1〜2時間で終わり、自分のClaude Code Maxプランの範囲内でできるものだ。そこで、より筋の通った「クリーンルーム」環境でZ80エミュレータ、そしてZX Spectrumエミュレータ(さらにはCP/Mエミュレータも、後述する)を書くことにした。成果はここにある:https://github.com/antirez/ZOT。
私が用いた手順
私はまず、やりたいことを仕様としてMarkdownファイルに書いた。単なる英語による、Z80エミュレータのスコープに関するハイレベルなアイデアだ。たとえば、エミュレータは1クロックステップごとではなく命令単位で実行すべきだと書いた。このエミュレータはRP2350のようなリソースの限られたハードウェアでも動作させたいからだ。エミュレータは経過したクロックサイクルを正確に追跡し(この機能は後でZX SpectrumのULAによるメモリアクセス時のコンテンションを実装するために使えると明記した)、メモリアクセスのコールバックを提供し、Z80の既知の公式・非公式命令をすべてエミュレートすべきだ、といった具合である。
Spectrumの実装については、後続のステップとして行ったのだが、Markdownファイルにより多くの情報を盛り込んだ。たとえば、RGBバッファでのレンダリングをどのようにしたいか、それをオプションにしておくことで組み込みデバイスではST77xxディスプレイ(など)へ転送する際にスキャンラインを直接描画できるようにしたいこと、I/Oポートを操作してEARビットをセットし、カセットローディングを非常に忠実にシミュレートできるようにすべきことなど、エミュレータに対する多くの要望を記した。
このファイルには、エージェントが従うべきルールも含めた。例えば次のようなものだ。
- インターネットへのアクセスは禁止だが、./z80-specs内に追加した仕様書やテストベクターファイルは使ってよい。
- コードはシンプルできれいに保ち、決して過度に複雑にしないこと。
- 確かな進捗があるごとにgitリポジトリへコミットすること。
- コミットする前に、作成したものが高品質で正しく動作することをテストすること。
- 機能を追加するたびに詳細なテストスイートを書くこと。テストは大きな変更のたびに再実行しなければならない。
- コードには十分にコメントを付けること。Z80やSpectrumの内部詳細に詳しくない人でも理解できる言葉で説明すること。
- プロンプト待ちで停止しないこと。ユーザーはキーボードから離れている。
- このファイルの末尾に作業ログを作成し、何を終え、何が残っているかを記録すること。常にこのログを更新すること。
- コンテキストが圧縮されるたびに、このファイルを読み直すこと。
次に、Claude Codeのセッションを開始し、Z80(後にSpectrumについても同様に行った)に関する有用なドキュメントをインターネット上からすべて収集し、有用な事実情報だけをMarkdownファイルに抽出するよう依頼した。また、Z80用の最も野心的なテストベクターのバイナリファイル、ZX SpectrumのROM、その他エミュレータがコードを正しく実行できているかをテストするために使えるいくつかのバイナリも提供した。こうした情報がすべて収集されたら(リポジトリに含まれているので、何が生成されたか確認できる)、検索中に見たソースコードによる汚染が起こりえないようにするため、Claude Codeのセッションを完全に削除した。
新しいセッションを開始し、仕様のMarkdownファイルと利用可能なすべてのドキュメントを確認し、Z80エミュレータの実装を開始するよう依頼した。ルールは、いかなる理由でもインターネットにアクセスしないこと(実装中にそうならないよう、エージェントを監視した)、そしてこれが「クリーンルーム」実装であるため、ディスク上でも類似のソースコードを探さないことだった。
Z80の実装では、一切のステアリングを行わなかった。Spectrumの実装では、TAPローディングの実装において広範なステアリングを行った。エージェントへのフィードバックの詳細については、この記事の後半で述べる。
最終ステップとして、リポジトリを/tmpにコピーし、.gitリポジトリファイルを完全に削除し、新たなClaude Code(およびCodex)セッションを開始して、この実装はおそらく盗用されたか、誰かの成果から強く着想を得すぎていると主張した。タスクは、主要なZ80実装のすべてと照らし合わせて盗用の証拠がないか確認することだった。エージェント(CodexとClaude Codeの両方)は、広範な検索の結果、著作権上の問題の証拠を一切見つけることができなかった。唯一似ていた部分は、よく確立されたエミュレーションのパターンや、Z80固有で別のやり方では作りようのない部分に関するもので、実装は他のすべての実装と大きく異なっているように見えた。
結果
Claude Codeは合計で20〜30分ほど作業し、ZEXDOCとZEXALLに合格するZ80エミュレータを、非常に読みやすくよくコメントされた1200行のCコード(コメントと空行を含めると1800行)で生成した。実装中、エージェントへのプロンプトは一度も行わず、完全に単独で動作した。インターネットにアクセスすることは一度もなく、エミュレータを実装するために用いたプロセスは、継続的なテストの繰り返しだった。ZEXDOCとZEXALLを実装したCP/Mバイナリと対話し、画面に出力を表示するために必要なCP/Mのシステムコールだけを書き、といった具合である。何度も、利用可能なSpectrumのROMや他のバイナリ、あるいはエミュレータが正しく動作しているかを確認するために一から作成したバイナリも利用していた。要するに、実装は人間のプログラマが行う方法と非常によく似た形で進められたのであり、重みの中から完全な実装を「解凍」するように一気に出力したのではない。むしろ、異なるクラスの命令が段階的に実装され、統合テストやデバッグセッション、ダンプ、printf呼び出しなどを通じて修正されたバグもあった。
次のステップ:ZX Spectrum
私は再び同じプロセスを繰り返した。インターネット上で探してほしい詳細の種類について、ドキュメント収集セッションに非常に正確に指示した。特に、RAMアクセス時のULAとの相互作用、キーボードマッピング、I/Oポート、カセットテープの仕組みや使われているPWMエンコーディングの種類、そしてそれがTAPやTZXファイルにどのようにエンコードされているかといった点である。
前述の通り、今回は設計メモを extensive にした。このエミュレータを組み込みシステム向けに特化して設計したかったからだ。具体的には、48Kのみのエミュレーション、オプションのフレームバッファレンダリング、追加メモリ使用量を極小に抑えること(ULA/Z80のアクセスコンテンション用の大きなルックアップテーブルは使わない)、追加で16Kのメモリを使わないようにROMをRAMにコピーせず、初期化時に参照するだけにする(つまり実行ファイル内にコピーが一つあるだけ)、といった具合である。
エージェントはZX Spectrumの内部構造について非常に詳細なドキュメントを作成できた。いくつかのゲームの.z80イメージファイルを提供したので、実際のソフトウェアを使って実際の環境でエミュレータをテストできた。再びセッションを削除し、 fresh に開始した。エージェントは作業を開始し、10分ほどで終えた。その過程は本当に魅力的で、おそらく皆さんもよくご存知のものだ。つまり、エージェントが多様なスキルを駆使して作業する様子が見られるのだ。プログラミングに関することなら何でもエキスパートなので、エミュレータを実装しながら、Z80がステップごとに何をしているか、そしてそれがSpectrumエミュレーションの状態をどう変化させるかを「観察」するための詳細な計装コードを即座に書き上げることができた。この点で、自動プログラミングはすでに超人的だと私は考えている。人間には作れないコードを現在生成できるという意味ではなく、異なるプログラミング言語、システムプログラミングのテクニック、DSP関連、OSのトリック、数学、その他結果に到達するために必要なあらゆるものを同時に使いこなすという意味で、最も直接的な方法でやり遂げるのだ。
完了したとき、簡単なSDLベースの統合サンプルを書くよう依頼した。エミュレータはすぐにJetpacのゲームを問題なく実行でき、サウンドも正常に動作し、私の遅いDellのLinuxマシン上でもCPU使用率は非常に低かった(SDLレンダリングを含めてもシングルコアの8%)。
基本的な部分が動作するようになったので、次はカセットのシミュレーションによってTAPファイルを直接ロードできるようにしたかった。ここで初めて、エージェントはいくつかの点を見落とした。具体的には、Spectrumのロードルーチンが期待するタイミングについてだ。ここからはLLMの性能が低下し始める領域だ。LLMはSDLエミュレータを実際に動かして、データ受信に合わせてボーダーが変化する様子を見るといったことが簡単にはできない。私はClaude Codeに対し、zx_tick()を直接呼び出せるようにし、zx_frame()の一部としないようリファクタリングを依頼した。そうすればzx_frame()は単純なラッパーになる。この方が、コールバックやそれまで実装されていた不適切な抽象化を使わずに、EARを期待通りに同期させるのがずっと簡単になるからだ。そうした変更の後、数分でエミュレータはカセットをエミュレートしてTAPファイルを問題なくロードできるようになった。
現在の動作は次の通りだ:
do {
zx_set_ear(zx, tzx_update(&tape, zx->cpu.clocks));
} while (!zx_tick(zx, 0));さらにClaude Codeへのプロンプトを続け、キーバインドをより使いやすくしたり、細かな改善を加えたりした。
CP/M
本当に興味深いと感じたことの一つは、LLMがZ80用のZEXALL / ZEXCOMテストのCOMファイルを精査し、使われていたCP/Mシステムコール(全部で3つ)を容易に見つけ出し、拡張されたz80テスト(make fulltestで実行される)のためにそれらを実装できたことだった。ならば、なぜ完全なCP/M環境を実装しないのか?再び同じプロセスで、数分で同じように良い結果が得られた。今回はVT100 / ADM3端末のエスケープ変換についてもう少しやり取りし、当初WordStarで動作しなかった点を報告すると、数分で私がテストしたものはすべて十分に動作するようになった(ただし、修正すべき点は残っている。たとえば2MHzクロックのシミュレーションで、今はフルスピードで動いているためCP/Mのゲームが遊べない、といった点だ)。
ここから得られる教訓は何か?
明らかな教訓は、エージェントには常に設計上のヒントと、これから行うことに関する広範なドキュメントを提供すべきだということだ。そのようなドキュメントは、エージェント自身に取得させることができる。また、エージェントがコーディングタスクをどのように遂行すべきかというルールを記したMarkdownファイルと、何をしているかの軌跡を残し、頻繁に更新して読み直させることも重要だ。
しかし、そうしたコツは、ここ数ヶ月自動プログラミングを extensivley 扱ってきた人なら誰にとっても、かなり明白なことだと思う。「人間なら何を必要とするか」という観点で考えることが、しばしば最善の賭けであり、それにLLM特有のいくつかの事柄、たとえばコンテキスト圧縮後の忘却の問題や、自分が正しい軌道にいることを継続的に確認できる能力などが加わる。
Anthropicのコンパイラ挑戦に話を戻すと、エージェントが失敗したステップの一つは、事前学習セットにあるものの記憶という考えに最も強く関連していた。すなわちアセンブラである。広範なドキュメントがあれば、Claude Code(そして、私の経験では複雑な事柄においてさらに有能なGPT5.3-codex)が動作するアセンブラの生成に失敗する理由が私には見当たらない。アセンブラはかなり機械的なプロセスだからだ。これは、LLMが学習セット全体を記憶し、見たものを解凍して出力しているという考えと矛盾すると私は思う。LLMは過剰に出現した特定の文書やコードを記憶することはあるが、そうした逐語的なコード断片を促されれば抽出できるとしても、学習セットで見たすべてのコピーを持っているわけでもなければ、通常の動作において既に見たコードのコピーを自発的に吐き出すわけでもない。私たちがLLMに求める作業のほとんどは、LLMが持つ異なる知識を組み合わせることを必要とし、その結果は既知の手法やパターンを用いてはいるが、既存のコードのコピーを構成するものではない、新しいコードになるのが通常だ。
また注目に値するのは、人間はしばしば、このブログ記事で詳述したクリーンルームのルールよりもはるかに厳密でないプロセスに従っているということだ。すなわち、人間はしばしば自分がやろうとしていることに関連する異なる実装のコードをダウンロードし、注意深く読み、そして逐語的にコピーすることを避けようとしながらも、しばしば強い着想を得る。このプロセスは私にとって全く許容できるものだが、実際に人間が書いたコードの現実において何が起きているかを心に留めておくことは重要だ。結局のところ、情報技術がこれほど急速に進化したのは、この大規模な相互受粉効果のおかげでもある。
以上のすべての理由から、自動プログラミングを用いてコードを実装する際に、私はこのZ80プロジェクトで行ったようにMITライセンスでリリースすることに何の問題も感じない。そしてこのコードベースは、オープンウェイトのものを含む次世代LLMの学習にとって、質の高い入力となるだろう。
次のステップ
私の実験をより説得力のあるものにするためには、エージェントに一切のドキュメントを提供せずにZ80およびZX Spectrumエミュレータの実装を試み、その実装結果を比較すべきだろう。試す時間がなかったが、非常に示唆に富むものになるはずだ。
記事をランダムに読む
コメント
ログインしてコメントする