2026-11-06
引き続き、『コンピュータシステムの理論と実装』(通称 nand2tetris)を読み進めています。
今回は第6章「アセンブラ」で、第4章で書いた .asm のコードを、第5章で作ったCPUが読める0と1の機械語(.hack)に変換するアセンブラをTypeScriptで作ってみたので、学んだことをまとめてみました。
アセンブラは、@sum や D=D+M のような人間が読める アセンブリ言語 を、CPUが直接理解できる16ビットの0と1の並びに変換するプログラムです。
たとえば上のコードは、次のような .hack ファイルに変換されます。
アセンブリと、JavaScriptのような高水準言語の大きな違いは次の2つです。
2つ目の理由は、たとえばIntelのCPUには A・D レジスタがなく、命令の形もまったく違うからです。アセンブリは「そのCPUの機械語に名前を付けただけのもの」に近いので、CPUが変われば書き方も変わってしまうんですね。
全体のコードは次のとおりです。node で実行すると、引数に渡した .asm ファイルと同じ場所に .hack ファイルを出力します。
処理の流れは大きく次の3ステップです。
R0〜R15、SCREEN、KBD など)をシンボルテーブルに登録する(LOOP) など)が何行目を指すかをテーブルに記録するここからは、それぞれのポイントを見ていきます。
// より後ろのコメントを捨てて、空白をすべて取り除いています。空になった行は命令ではないので、ループの中で continue して飛ばします。
こうしておくと、D = D + M // 足す のように書かれていても D=D+M という形に揃うので、後の処理がシンプルになります。
ラベルは ROMの何行目にジャンプするか をテーブルに記録しておくためのものです。
A命令とC命令は機械語1行になるので rom を1つ進めますが、ラベル自体は機械語にならないので行数を進めません。そのため、ラベルには その次に来る命令の行番号 が記録されます。
わざわざ2回ループするのは、@LOOP がラベルの定義より前に出てくることがあるからです。1回目で全部のラベルの行番号を先に調べておくことで、2回目でどこに出てきても番地に変換できます。
なお、(R0) のように定義済みシンボルと同じ名前のラベルが来ると、テーブルの値が上書きされてしまいます。ただ、これはそういう .asm を書くほうが悪いとして、今回は考慮していません。
@ の後ろの値によって、番地の決め方が3通りに分かれます。
@2 のような 数字 → そのまま番地にする@LOOP や @R0 のような テーブルにある名前 → テーブルの番地を使う@sum のような テーブルにない名前 → 変数とみなして、16番地から順に割り当てる最後に先頭を 0(A命令の印)にして、番地を15ビットの2進数にしています。
第5章のCPUの記事で、変数が16番地から順に割り当てられると書きましたが、それはこの nextVariable = 16 のことだったんですね。
C命令は dest=comp;jump の形なので、= と ; で分解して、それぞれを表(DEST・COMP・JUMP)で0と1に置き換えています。
dest: = の左側。なければ ""jump: ; の右側。なければ ""comp: = の右側かつ ; の左側たとえば D=D+M なら、dest が D、comp が D+M、jump が "" になり、次のように組み立てられます。
第5章で見た「先頭の 111、ALUの計算を決める a cccccc、書き込み先の ddd、ジャンプ条件の jjj」の並びが、そのまま文字列の連結になっているのが分かりやすいなと感じました。
ちなみに DEST では DM と MD のように順番違いも同じ値にしているので、MD=D+1 と書いても DM=D+1 と書いても変換できます。
作っていて少し気になったのが、ラベルと変数を同じ table に入れていることです。
ラベルは「ROMの何行目にジャンプするか」、変数は「RAMの何番地を読み書きするか」を指していて、指している場所はまったく別です。
それでも1つのテーブルで管理できるのは、どちらも 名前 → 数 という変換でしかないからです。アセンブラからすると、@LOOP も @sum も「名前を数に置き換えてA命令にする」だけで、その数がROMの行なのかRAMの番地なのかは、実行時にCPUがその値をどう使うか(ジャンプ先にするか、M として読み書きするか)で決まります。
第6章では、アセンブリ言語を機械語に変換するアセンブラをTypeScriptで作りました。
これまでは「@sum と書けばいい感じに番地になる」と思っていた部分が、実際には ラベルを先に集める1回目のループ と 変数を16番地から割り当てる2回目のループ という、思ったよりシンプルな仕組みで動いていると分かったのが面白かったです。
第1章から作ってきたハードウェアの上で、自分の書いたソフトウェアが初めてつながった感じがしました。