6502tinymachines

二つの入口

トランジスタレベルの 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 の仕事なしにこの穴は塞がる。エンジン側に必要なものを参照。