二つの入口
トランジスタレベルの 6502 は二つのやり方で動かせて、その二つは似ても似つかない。
ページの中では、wasm ビルドを通して、チップは手に持つオブジェクトだ。五十ほどのメソッド、halfStep()、pc()、peek()。HTTP 越しには、API を通して、チップは渡す値だ。機械まるごとが出て行って戻ってきて、サーバーはリクエストの間に何も持たない。
その違いは二つのチームの偶然ではない。状態がどこに住むかから従うもので、どちらの形もそれぞれの側で正しい。足りなかったのは、機械を一方から他方へ運ぶ道だった。
その隙間は閉じた
wasm のクレートは exportMachine() と importState() を公開し、どちらもサービスが使うのと同じコーデック、crates/v6502-sim/src/state.rs の state::snapshot と MachineState::from_hex の上に組まれている。exportMachine() は API 自身の JSON を出す。
だから二つの面はすでに機械をやり取りしている。これはソースから読み取ったのではなく、生きている API に対して検証した。wasm が出すままの形の機械、欠けている version フィールドも含めて、を POST /v1/step は受け取り、正しく進める。version には既定値があるので、無くても誤りではない。
| wasm の面 | HTTP の面 | |
|---|---|---|
| 機械とは | 動かすオブジェクト | 渡す値 |
| 粒度 | 細かい: halfStep()、pc()、peek() | 粗い: 機械を送り、機械を受け取る |
| n 半サイクル走らせる | runHalfCycles(n) | {"half_cycles": n} |
| 命令をひとつ走らせる | stepInstruction(max) | {"until": "instruction"} |
| レジスタ | オブジェクトのメソッド | 応答の observe ブロック |
| アセンブル | 無い | POST /v1/assemble |
| 機械を取り出す | exportMachine() | 応答そのもの |
| 機械を入れる | importState(...) とメモリ | リクエストそのもの |
両方の上にひとつのインターフェース
tm6502.mjs はどちらのバックエンドの上でも同じセッションを見せる。ビルド手順も依存も無い:
import { remote, local } from "https://tinymachines.ai/engine/tm6502.mjs";
const cpu = remote(); // or local({ engine })
await cpu.boot({ source: " LDA #$2E\n BRK\n" });
await cpu.runUntil("instruction");
cpu.registers(); // { pc, a, x, y, s, p, ir }
これを単に整理整頓ではなく作る価値のあるものにしている性質: API が状態を持たず機械まるごとを運ぶので、二つのバックエンドは作りからして取り替えがきく。 ブラウザで走らせ始め、機械をサーバーに渡してそこで終える。あるいはその逆。
const snapshot = cpu.export(); // { state, memory }
const second = remote();
second.import(snapshot); // continues exactly where the first stopped
await second.step(10);
それが動くために新しく真でなければならないことは何も無い。どちらでも同じ四つの十六進ビット集合で、ノード集合はそれぞれ 432 文字、トランジスタは 878 文字だ。
和は二半サイクル遅れて着く
走らせてみよう。これは録画ではない:
これはあなたのブラウザの中で、このサイトの /6502/api の背後にあるチップに対して、下のモジュールを通して走る。前もって計算したものは何も無い。
命令は半サイクル 12 で終わり、A はまだ $2E で、A が $42 になるのは半サイクル 14 だ。読み取りのバグではない。書き込みは次のフェッチがもう始まってから着く。命令セットをモデル化するのではなくスイッチをシミュレートすると、そういうことが自然に出てくる。ここでは何もレジスタがいつ更新されるかを決めていない。
ライセンスが梱包を分ける
ここは間違えやすく、間違えると他人の仕事をライセンスし直すことになる。
wasm のバンドルはダイのデータを埋め込む。v6502-wasm は v6502-sim に依存し、それは v6502-netlist に依存し、それが netlist.bin を include_bytes! する。だからそのバンドルは、周りのコードについてライセンスファイルが何と言おうと、CC BY-NC-SA 3.0 を帯びる。
だから tm6502.mjs は MIT で、チップを含まない。取ってくることも、束ねることも、組むことも決してしない。local({ engine }) は呼び手がすでに持っているエンジンを手渡され、そうでなければ理由をつけて拒む。それは Rust の側がすでに引いている線、チップを名指さない halfphi と、ダイのデータである v6502-netlist とのあいだの線と同じだ。NOTICE.md を参照。
ここに無いもの
local バックエンドは書かれているが、このページでは使われていない。動かすには読者自身のページに wasm ビルドが要り、このサイトはダイのデータを配布しないので、ここにはそれを示す相手が無い。コードの道筋は本物で形は正しい。このリポジトリから生きた wasm エンジンに対して動かしたことはまだ無い。
local() はアセンブルもできない。wasm ビルドにアセンブラが無いからだ。空のバイト列を返す代わりにそう言い、リモートのバックエンドを指す。
その隙間は見た目より小さい。アセンブラはすでに JavaScript だ。6502 リポジトリの web/asm.js は assemble() を公開する ES モジュールで、オペコード表ひとつだけを import し、ダイのデータには触れない。あのプロジェクトのただ一つのアセンブラで、だから Python のサービスは二つ目を持つ代わりにそれを呼び出す。これを公開すれば、Rust の仕事なしにこの穴は塞がる。エンジン側に必要なものを参照。