API
リファレンスは /ja/6502/api にあり、そこで稼働中のサービスと自分を突き合わせ、記述したルートのうちどれがまだ応答しないかを述べる。サービスは 6502.tinymachines.ai/api で応答し、2026-08-24 からはこのサイトの
tinymachines.ai/6502/api/ でも応答する: 一つのプロセスに複数の正面玄関があり、スキーマはリクエストごとに、そのリクエストが入ってきた扉を名乗る。だからどちらの写しを読んだクライアントも、読んだ当のアドレスを呼ぶ。しばらくの間ここへのプロキシは不可能だった。静的なルートパスが一つだけで、どのスキーマも /api を主張し、apex ではそれがこのサイト自身の API だからだ。正準のアドレスは、切り替えがリダイレクトになるまでサブドメインのままだ。
トランジスタレベルの MOS 6502 を HTTP で、半サイクルずつ。ここには 6502 の挙動をモデル化したものは無い: すべてのリクエストが実物の 3510 スイッチの網を整定させ、すべての応答のすべてのレジスタは、それ自身の記憶ノードから読み戻される。
サーバはステートレス
マシン全体が Machine オブジェクトとして毎リクエストに載って移動する:
チップの四つのビット集合 (全ノードのレベル、全プル、導通中の全トランジスタ、
16 進でおよそ 2 KB) に加えて、疎な 64 KiB メモリ。これはフィルバイト一つと、それと異なる 256 バイトページだけでできている。応答はマシン全体を持ち帰る。
あなたが持つそのオブジェクトの写しがセッションだ。それが、任意の数のインスタンスが任意のリクエストに答えられる理由であり、セッションをファイルに保存し、diff を取り、誰かに手渡せるものにしている理由でもある。
この主張は断言ではなくテストに繋がれている。 検証を参照。
学ぶ人がたどる流れ
# 1. アセンブルする (rom を渡して直接ブートしてもよい: ブートもアセンブルする)。
curl -s localhost:6502/v1/assemble -H 'content-type: application/json' -d '{
"source": " .org $0200\nstart: LDA #$2E\n STA $80\n JMP start"
}'
# -> バイト列、行ごとにアドレスとバイトが付くリスティング、ラベル表
# 2. ブートする: rom はその org でメモリに敷かれ、リセットベクタがそこへ
# 向けられ、チップは実物のリセットシーケンスで電源投入される。マシンは
# 最初のオペコードフェッチに立った状態で返ってくる。
curl -s localhost:6502/v1/boot -d '{"rom": {"source": "..."}}' > m0.json
# 3. ステップする: マシンを半サイクル数と一緒に POST し返す。
# until="instruction" なら次のオペコードフェッチまで走る。trace=true は
# 半サイクルごとに Observation を一つ返し、watch=["sync","sb0"] はダイ上の
# 任意の名前付きノードをそのたびに読む。
jq '{machine, half_cycles: 41, trace: true}' m0.json | \
curl -s localhost:6502/v1/step -d @- > m1.json
Observation は、学ぶ人がある瞬間に読むものだ: バス (アドレス、データ、読み書き、sync)、nv-BdIzC のフラグ文字列付きレジスタ、クロック位相、タイミングチェーンの T ステート、直近のオペコードフェッチ、そして見張り中のノード。すべてシリコンから読み取ったもので、モデル化したものは一つも無い。
最初に試すもの
プログラムページの「Add two bytes」($2E + $14): ブートして、trace を点けて 41 半サイクル進め、答えを見張る。半サイクル 37 あたりで加算器は
$42 を保持しているのに、A はまだ $40 と読める。ADC の結果は存在するのにどのレジスタにも無く、次の命令のフェッチが転送するまでそのままだ。
この重なりは実物のシリコンの挙動であり、どの挙動エミュレータでも見えず、トレースの中でそれが見えることこそ、このサービスがオペコードではなくスイッチをシミュレートする理由だ。
部品
target/release/halfwave | エンジン: 常温で常駐する、ステートレスなチップ。ネットリストは一度だけパースし、状態はリクエストごとに注入。行プロトコルが入り、JSON 一行が出る。依存ゼロ。 |
asm-bridge.mjs | アセンブラ: web/asm.js を stdin/stdout 越しに。このプロジェクトのアセンブラは一つで、二つ目を育てる代わりにサービスはこう使う。 |
models.py | 公開される形 (Pydantic): Machine、ChipState、SparseMemory、Rom、Observation。 |
engine.py | 常温エンジンプロセスのプール。 |
app.py | FastAPI: エンドポイント。 |
atlas.py | チップアトラス: ダイの導出コンテナに索引を付けて照会可能に。生成済みファイルを二つ読むだけで、何も走らせない。 |
test_service.py | 端から端まで 26 テスト。 |
test_atlas.py | アトラスに 52 テスト。サイト自身の公表数字と突き合わせる。 |
ローカルで走らせる
# エンジン (一度だけ):
export PATH="$HOME/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
cargo build --release -p v6502-sim --bin halfwave
# サービス:
uvicorn app:app --app-dir service --port 6502
# テスト:
python3 -m pytest service/test_service.py -q
環境変数: HALFWAVE_BIN (エンジンのパス、既定は
target/release/halfwave)、HALFWAVE_POOL (常温インスタンス数、既定 2)、
NODE (アセンブラ用の node、既定 node、16 以降が必要)。
ポート 6502 は本番 API のものだ。すでに使用中のポートで起動したローカルサーバは静かにバインドに失敗し、以後すべてのリクエストは本番へ行く。ローカルサーバを自分のものと信じる前に ss -ltn を確認すること。
この上に何かを建てる人のための数字
どれも見積もりではなく実測で、そう述べる一文が数字と一緒に旅をする。
- 常温インスタンス 1 個あたり毎秒およそ 26,000 半サイクル。 リクエストは
200,000 半サイクル (
/v1/metaのmax_step) で頭打ちなので、長い実行は分割することになる。 - 最初のリクエストの前に 12 個の常温チップが立ち上がっている
(
HALFWAVE_POOL、1 個あたり 3 ms と 2.2 MB)。スループットは 1 個の 12 倍ではなく 5.55 倍だ: これは 6 コア 2 スレッドの石で、ソルバは計算律速なので、コアの二本目のスレッドには挟み込む仕事がほとんど無い。 - 仕事が半サイクル 1 個のとき、毎秒およそ 980 リクエスト。HTTP 層は ボトルネックではなく、それも仮定ではなく実測だ。
- エンジンはトレース付きステップをリクエストあたり 10,000 で打ち切る。
もっともらしい答えではなく、拒絶を
- JAM オペコード (
$02とその仲間) は二度と次のフェッチに達しない。until="instruction"は上限に当たるとcompleted: falseを返す。それがハングではなく正直な答えだ。 - チップ自身の癖は均さずに晒す。
Sは電源投入直後は未定義だ。リセットは それを 3 減らすだけだから。Pのビット 5 は 1 と読める。そのための記憶ノードが存在しないからだ。リセットベクタの無いメモリへのブートは$FFFCが指す先を何であれ実行する。シリコンと正確に同じように。