6502tinymachines

エンジン側に必要なもの

tinymachines/public から書かれている。あちら側には手を伸ばさないリポジトリだ。以下はすべて依頼か調査結果であって、変更では決してない。すべての主張は実測されていて、その測定は信じる代わりに再実行できるように書いてある。

作業ツリーではなく origin/main に対して読むこと。 push されていない ものはここに書かれていない。

現在地

1メモリは importState と一緒に旅すべきだd5122f2着地
2ダイデータを載せないビルド06eb9fb着地
3すでに存在するアセンブラを公開する7d280c0着地
4揃えられるのに揃っていない二つの名前取り下げ。理由は下に

マージの 3 コミット後、2026-08-23 に origin/main に対して確認した。

ここに屋根を塞ぐものは無い。/engine/tm6502.mjs二つの入口はビルドされ、提供中だ。

概要が語るブロッカーはもう無い

START-HERE.md のステップ 7 は、wasm クレートはマシンをエクスポートもインポートもできず、状態関数はゼロだと言う。それはしばらく前から真実ではない。

exportMachine()importState() はどちらも存在し、サービスがすでに使うコーデック (state::snapshotMachineState::from_hex) の上に建っている。 exportMachine() は API 自身の {state, memory} JSON を出力する。

読んだのではなく測った: wasm が出力する形そのまま (欠けている version フィールド込み) のマシンを本番の POST /v1/step に投げ、半サイクル 0 から 8 まで進めた。version は既定値付きの const 1 で、スキーマの required に入っていないので、欠けていても検証を通る。

書き直しが必要なのは START-HERE.md の「The blocker is concrete」で始まる 段落だ。


1. メモリは importState と一緒に旅すべきだ

importMachine として d5122f2着地

importState はチップの半分だけを復元する。メモリは別に fillMemoryload を通る。doc コメントは何を呼べばよいかは教えるという意味でそう言っているが、それは警告ではなく配置の説明だ。

ここは二つの表面が、名前の違いではなく本当に違う唯一の場所だ。HTTP 越しのマシンは一つの値で、丸ごと届く。ブラウザでは呼び出し二つで、最初の一つしかしなかった呼び出し手は、そこに無いプログラムを走らせる正しいチップを手にする。その失敗はシミュレーションのバグに見える。欠けた呼び出しには見えない。

どちらでもよかった: 両方やる importMachine(json) か、メモリも復元されるまでマシンは復元されていない、という一文か。

いまは両方ある。 importMachine はデコード済みバイトを受けて対を一度に やり、importState 自身の doc コメントはメモリに触れないと警告する。 tools/check-wasm-import.py がそれを縛る: importMachineimportState プラス writeMemory とバイト単位で一致すること、レジスタが一致すること、丸ごと復元したプログラムが自分の和を計算すること、そして importState 単体は古いメモリをその場に残すこと。それがこの依頼の主題だった失敗だ。

マージ済みツリーからビルドした wasm に対して走らせると、六つの表明はすべて通る。/engine/tm6502.mjs は今も restoreInto() で自分で対をやっていて、これからは一度の呼び出しでもできる。

2. ダイデータを載せないビルド

06eb9fb、「Two builds of the wasm crate: one with the die data, one without」で着地。

ライセンス上の帰結を持つのはこれだけで、何を解いたかは記録に値する。 v6502-wasmv6502-sim に依存し、それは v6502-netlist に依存し、 crates/v6502-netlist/src/lib.rsinclude_bytes!netlist.bin を埋め込む。そのブロブは 32,628 バイトで、ビルドされたバンドルは 108,956 バイト。だからあのバンドルは、周りのコードのライセンスファイルが何と言おうと CC BY-NC-SA 3.0 を運んでいる。

着地したのは mos6502 フィーチャで、既定で有効なので既存の呼び出し手は無傷だ。切ると v6502-netlist が落ち、それと一緒に、ワークスペースでダイデータを埋め込む唯一のものが落ちる。代わりに Machine.fromNetlist(bytes)netlistInfoOf(bytes) が実行時に一つ受け取る。漂う二つのクレートではなく、一つのクレートの二つのビルドだ。

tools/check-wasm-nodata.py が一緒に来た。出力ではなく依存ツリーを見張る。その理由は引用に値する: 誰かが便利機能を足し、その実装に mos6502() に手を伸ばし、依存が戻ってきて、何も落ちない。ビルドは通り、テストは通り、パッケージは静かに MIT であることをやめる。

3. すでに存在するアセンブラを公開する

7d280c0着地

これには Rust の作業がまったく要らなかった。そしてこのページの以前の版は間違ったものを求めていた。

web/asm.jsassemble()AsmError をエクスポートする 376 行の ES モジュールだ。インポートは一つ、./disasm.jsOPCODES だけで、そちらは 96 行、インポート無し。どちらのファイルもネットリストにもダイデータにも触れず、ネットワーク呼び出しもしない。ツリーから node で走らせると、作例を a9 2e 18 69 14 00 にアセンブルする。同じソースに API が返すものとバイト単位で同じだ。

これはプロジェクト唯一のアセンブラでもある: service/asm-bridge.mjs がそう述べていて、Python のサービスは二つ目を飼う代わりにこれをシェルで呼ぶ。

@tinymachines/6502-asm としてパッケージされている。MIT、依存ゼロ、8.7 kB。 dist/ はビルド時に web/ から生成され、gitignore されている。公開された写しは、単一アセンブラの配置が防いでいる当のものを再導入するからだ。ビルドは、どちらかのファイルがダイデータに触れたり、パッケージの外に届くインポートを生やしたりしたら、何も作らずに拒む。

4. 揃えられるのに揃っていない二つの名前

取り下げ。 ここでは安いと書かれていたが、測ったら安くなかった。

wasmHTTP
runHalfCycles(n)half_cycles
stepInstruction(max)until: "instruction"max_half_cycles

ラッパーが両者を対応付けていて、その対応は 2 行だ。揃える改名はその 2 行を消す代わりに、呼び出し手 6 箇所と公開済みの API 表面を壊す: この名前は v6502-wasm がエクスポートするものの一部なので、変更はすでに使っているすべてにとって破壊的変更だ。維持費ゼロの対応表を消すための対価としては釣り合わない。

意図して放置し、ここに書き残した。次に不揃いに気づいた人が、測定を繰り返す代わりに測定結果を見つけられるように。改名は、あのクレートが別の理由で破壊的リリースをする日には、やる価値がある。破壊的リリースを引き起こす価値は無い。


すでに正しくて、乱すべきでないもの

上の一覧はほとんど不足の話で、その釣り合いは誤解を招く。

  • コーデックは両側で同じで、書き下ろされている。 小文字の 16 進、集合の ビット i はバイト i/8 の LSB から。生きたマシンで実測: ノード集合は 216 バイトで 16 進 432 文字、トランジスタ集合は 439 バイトで 878 文字。概要が述べる数字そのままだ。
  • exportMachine は二つ目の形ではなく、サービスの形を出力する。 その 決定こそ、ステップ 7 がプロトコルではなくラッパーである理由だ。
  • API はステートレスでマシン全体を運ぶ。 それが二つのバックエンドを、 努力によってではなく構成によって交換可能にしている。こちら側から実証済み: 40 半サイクル後にエクスポートしたマシンを別セッションにインポートし、50 まで続けた。
  • tools/check-wasm-parity.py がいまはエンジン側からもそれを証明する。 実行を半分に割り、両方向に受け渡し、答えが中断なしの実行と一致することを要求する。このドキュメントがする主張を、消費される側だけでなくエンジンの住む側で検査したものだ。
  • importState は巻き戻し履歴をクリアする。 理由のコメント付きで: 残せば stepBack が、このマシンが一度もそうであったことの無いマシンへ歩いて入れてしまう。

道すがら見つかった、ステップ 7 ではない一つのこと

POST /v1/stephalf_cycles を取る。ここからの試みは n を送って 422 を受け取った。バグではない: FastAPI が応答の中でフィールドを名指ししていて、それを読むほうが推測より速かった。次の人が同じ推測をするだろうから、記録しておく。