Commit 4bf71d0d authored by morifuji.koudai's avatar morifuji.koudai

[add] 7/simpleAdd完成

parent 6b8db361
こんばんは。10日ぶりの更新です。
時間が取れなくて前回からだいぶ遅くなった :sweat_drops:
[https://www.oreilly.co.jp/books/9784873117126/:embed:cite]
[記録用git](https://gitlab.com/morifuji/nand2tetris)
[vol.1](https://morimori-kochan.hatenablog.com/entry/2019/01/12/203225)
[vol.2](https://morimori-kochan.hatenablog.com/entry/2019/01/13/122457)
[vol.3](https://morimori-kochan.hatenablog.com/entry/2019/01/13/154122)
[vol.4](https://morimori-kochan.hatenablog.com/entry/2019/01/14/002915)
[vol.5](https://morimori-kochan.hatenablog.com/entry/2019/01/14/002915)
vol.6 **いまここ**
## 6章 アセンブラ
### 理論
この章ではアセンブリ言語をアセンブラによって機械語に変換できるようにする。
#### アセンブリ言語
`LOAD R3 7`などの人間が読みやすい言語。
#### 機械語
バイナリである。先ほどの`LOAD R3 7`は、ハードウェアの論理設計上で、
LOADは最初の8ビット。R3(レジスタ)は次の8ビット。7(アドレス)を残りの16ビットで表現するというようなルールが決められており、それに従って出力されたバイナリ
#### シンボル
アセンブリ言語上でメモリアドレスを間接的に指定できる変数。
任意のシンボルは、どこかのアドレスの位置を参照している
利用方法としては一般的に以下の2種類がある
- ラベル
- `LOOP`など。gotoやjmpで利用するためのラベル、タグ。この行の次の行のアドレスを指し示す。この行自体は機械語に出力されない。
- 変数
- `count=0``i=1`のように利用できる。変数はRAMと混合しないようにメモリのある地点から格納される。
#### シンボル解決
アセンブラは、シンボルを使わない場合だとルールに従ってバイナリに変換するだけで良い。しかし、シンボルを利用する場合はシンボルが表すアドレスを全て保存しシンボルが利用されている箇所全てを置換する必要がある。
### 理論(Hack)
#### アセンブリ言語
各行は、以下のうちのどれかで構成される
- A命令またはC命令の命令
- シンボル
- 意味のない行
また、コメントは `//`で書き、その行のそれ以降の文字は全てアセンブラに無視される
#### 定義済みシンボル
`R0...R15``THIS``ARGS`は定義済みシンボル。
| ラベル | RAMアドレス |
| ------ | ----------- |
| R0 | 0 |
| R1 | 1 |
| ... | ... |
| R15 | 15 |
| SP | 0 |
| LCL | 1 |
| ARGS | 2 |
| THIS | 3 |
| THAT | 4 |
### 実装
アセンブリを実装しなければならない。
2段階で実装する。
1. シンボルがないアセンブリの場合のアセンブラ
2. シンボルがあるアセンブリの場合のアセンブラ
1の場合は一度アセンブリを舐めるだけで良い。というのも、上から順に変換することが可能であるため。
2の場合、ファイルを舐める作業は2回必要である。というのも、シンボルは先に利用して後から宣言できるため。
こんな場合👇
```vhdl
@LOOP_END
0;JMP
...
...
...
(LOOP_END)
```
なので、1度目は、L命令を全てシンボルテーブルに保存して、2度目はそれを元に、出力していく。
特にこだわりがないので言語はkotlinでやってみた。
[`assembler`ディレクトリにkotlinのプロジェクトファイルを保存してあります](https://gitlab.com/morifuji/nand2tetris/tree/master/assembler)
#### Parserモジュール
```kotlin
import java.io.File
import java.io.InputStream
class Parser(filePath: String) {
enum class COMMAND_TYPE {
A_COMMAND,
C_COMMAND,
L_COMMAND,
NONE
}
var inputLines = mutableListOf<String>()
var commandIndex: Int = -1
var binaryIndex: Int = -1
init {
val inputStream: InputStream = File(filePath).inputStream()
val lineList = mutableListOf<String>()
inputStream.bufferedReader().useLines { lines -> lines.forEach { lineList.add(it) } }
lineList.forEach {
// コメントは除去
val formattedLine = it.split("//")[0]
if (formattedLine.isEmpty()) {
return@forEach
}
this.inputLines.add(formattedLine)
}
}
// 入力にまだコマンドが存在するか
fun hasMoreCommands(): Boolean {
val nextLine = inputLines.getOrNull(this.commandIndex + 1)
return nextLine !== null
}
// 次のコマンドを読み込む
fun advance() {
this.commandIndex++
}
// 現コマンドの種類を表す
fun commandType(): COMMAND_TYPE {
val command = getCurrentCommand()
return when {
command.startsWith("@") -> COMMAND_TYPE.A_COMMAND
command.startsWith("(") && command.endsWith(")") -> COMMAND_TYPE.L_COMMAND
else -> COMMAND_TYPE.C_COMMAND
}
}
fun symbol(): String {
val command = this.getCurrentCommand()
val commandType = this.commandType()
if (commandType !== COMMAND_TYPE.L_COMMAND && commandType !== COMMAND_TYPE.A_COMMAND) {
return ""
}
if (commandType === COMMAND_TYPE.L_COMMAND) {
return command.replace("(", "").replace(")", "")
}
return command.replace("@", "").trim()
}
fun dest(): String {
val command = this.getCurrentCommand()
if (!command.contains("=")) {
return ""
}
return command.split("=")[0].trim()
}
fun comp(): String {
val command = this.getCurrentCommand()
var comp = command
if (command.contains(";")) {
comp = command.split(";")[0].trim()
}
if (command.contains("=")) {
comp = command.split("=")[1].trim()
}
return comp
}
fun jump(): String {
val command = this.getCurrentCommand()
if (!command.contains(";")) {
return ""
}
return command.split(";")[1].trim()
}
private final fun getCurrentCommand(): String {
return inputLines.get(this.commandIndex).trim()
}
fun reset() {
commandIndex = -1
binaryIndex = -1
}
}
```
愚直に実装。
ただし以下の部分に注意
```kotlin
// コメントは除去
val formattedLine = it.split("//")[0]
```
#### Codeモジュール
`dest`はロジックに落とし込めた。`jmp`はロジック無理と割り切ってmapで行った。`comp`はロジックに落とそうとすると非常に難しかった。
もしかしてと思って他の人の回答を見ると、そのまさかMapで持たせてた。まあそれが早いよね。
```kotlin
class Code() {
companion object {
fun dest(nimonic: String): String {
var d = ""
d += if (nimonic.contains("A")) {
"1"
} else {
"0"
}
d += if (nimonic.contains("D")) {
"1"
} else {
"0"
}
d += if (nimonic.contains("M")) {
"1"
} else {
"0"
}
return d
}
fun comp(nimonic: String): String {
val map = mapOf(
"0" to "0101010",
"1" to "0111111",
"-1" to "0111010",
"D" to "0001100",
"A" to "0110000",
"!D" to "0001101",
"!A" to "0110001",
"-D" to "0001111",
"-A" to "0110011",
"D+1" to "0011111",
"A+1" to "0110111",
"D-1" to "0001110",
"A-1" to "0110010",
"D+A" to "0000010",
"D-A" to "0010011",
"A-D" to "0000111",
"D&A" to "0000000",
"D|A" to "0010101",
"M" to "1110000",
"!M" to "1110001",
"-M" to "1110011",
"M+1" to "1110111",
"M-1" to "1110010",
"D+M" to "1000010",
"D-M" to "1010011",
"M-D" to "1000111",
"D&M" to "1000000",
"D|M" to "1010101"
)
return map.get(nimonic)!!
}
fun jump(nimonic: String): String {
return when (nimonic) {
"JGT" -> "001"
"JEQ" -> "010"
"JGE" -> "011"
"JLT" -> "100"
"JNE" -> "101"
"JLE" -> "110"
"JMP" -> "111"
else -> "000"
}
}
}
}
```
#### Main.kt
```kotlin
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.PrintWriter
fun main(args: Array<String>) {
// 入力
val parser = Parser("/nand2tetris/projects/06/rect/Rect.asm")
// 出力
val fil = FileWriter("/nand2tetris/projects/06/rect/Rect.hack")
// シンボルテーブル
val symbolTable = SymbolTable()
// 1. 変数保存
while (parser.hasMoreCommands()) {
parser.advance()
if (parser.commandType() == Parser.COMMAND_TYPE.L_COMMAND) {
// 次の行のbinaryIndexを保存
symbolTable.addEntry(parser.symbol(), parser.binaryIndex + 1)
} else {
// L命令ではないので、機械語として出力される=>機械語の行数をカウントアップ
parser.binaryIndex++
}
}
parser.reset()
// 2. 変換出力
val pw = PrintWriter(BufferedWriter(fil))
while (parser.hasMoreCommands()) {
parser.advance()
if (parser.commandType() === Parser.COMMAND_TYPE.NONE) {
continue
}
if (parser.commandType() === Parser.COMMAND_TYPE.L_COMMAND) {
continue
}
if (parser.commandType() === Parser.COMMAND_TYPE.A_COMMAND) {
val symbol = parser.symbol()
// シンボルに文字が含まれる場合
val address = if (symbol.contains(Regex("""\D+"""))) {
// シンボルテーブルに含まれていない場合
if (!symbolTable.contains(symbol)) {
// 新たに追加
symbolTable.addEntry(symbol)
}
// シンボルテーブルからアドレスを解決
symbolTable.getAddress(symbol)
} else {
// シンボルが数値の場合
Integer.parseInt(symbol)
}
pw.println("%016d".format(Integer.toBinaryString(address).toLong()))
continue
}
// C命令
val binaryString = "111" + Code.comp(parser.comp()) + Code.dest(parser.dest()) + Code.jump(parser.jump())
pw.println(binaryString)
}
pw.close()
}
```
エントリーポイント。ファイルを二回なめている。
`commandIndex`で、アセンブリ言語の行数をカウントアップしているが、機械語の行数とは一致しないので注意。途中で気づいて、 `binaryIndex`を生やしてカバーリング。👇
```kotlin
// 1. 変数保存
while (parser.hasMoreCommands()) {
parser.advance()
if (parser.commandType() == Parser.COMMAND_TYPE.L_COMMAND) {
// 次の行のbinaryIndexを保存
symbolTable.addEntry(parser.symbol(), parser.binaryIndex + 1)
} else {
// L命令ではないので、機械語として出力される=>機械語の行数をカウントアップ
parser.binaryIndex++
}
}
```
#### SymbolTableモジュール
```kotlin
class SymbolTable() {
var table = mutableMapOf<String, Int>()
init {
table.putAll(mapOf(
"R0" to 0,
"R1" to 1,
"R2" to 2,
"R3" to 3,
"R4" to 4,
"R5" to 5,
"R6" to 6,
"R7" to 7,
"R8" to 8,
"R9" to 9,
"R10" to 10,
"R11" to 11,
"R12" to 12,
"R13" to 13,
"R14" to 14,
"R15" to 15,
"SP" to 0,
"LCL" to 1,
"ARG" to 2,
"THIS" to 3,
"THAT" to 4,
"SCREEN" to 16384,
"KBD" to 24576
))
}
// 保存開始アドレス
var index = 16
fun addEntry(symbol: String, address: Int) {
table[symbol] = address
}
fun addEntry(symbol: String) {
table[symbol] = this.index++
}
fun contains(symbol: String): Boolean {
return table.contains(symbol)
}
fun getAddress(symbol: String): Int {
return table.get(symbol)!!
}
}
```
愚直に実装。特に面白い点なし。
#### Pong.hack
30000行近くのアセンブリを機械語に変換して実行。
こんばんは。フェリーハッカソンがめっちゃたのしかったmorifujiです。
また時間が空いてしまったけど頑張ります :muscle:
[https://www.oreilly.co.jp/books/9784873117126/:embed:cite]
[記録用git](https://gitlab.com/morifuji/nand2tetris)
[vol.1](https://morimori-kochan.hatenablog.com/entry/2019/01/12/203225)
[vol.2](https://morimori-kochan.hatenablog.com/entry/2019/01/13/122457)
[vol.3](https://morimori-kochan.hatenablog.com/entry/2019/01/13/154122)
[vol.4](https://morimori-kochan.hatenablog.com/entry/2019/01/14/002915)
[vol.5](https://morimori-kochan.hatenablog.com/entry/2019/01/14/002915)
[vol.6](https://morimori-kochan.hatenablog.com/entry/2019/01/23/101650)
vol.7 **いまここ**
## 7章 バーチャルマシン
VM(バーチャルマシン)言語から機械語への変換作業を本章・次章で扱う。
### 理論
#### VM(バーチャルマシン)
抽象化されたコンピュータ。VMの実装を備えていれば、VMの実行が可能になる。すなはちVMはただのインターフェース(規約)でしかない
たとえば、Pascalなら**pコード**。Javaなら**バイトコード**
VMがある利点は下の2つ。
1. VMで記述するだけで複数のプラットフォームに同時に対応することができる
2. VMを通して多言語の相互利用が可能になる
3. モジュール性が高まる
#### スタックマシン
`オペランドとVM命令の結果をどう保存するか`がキモになる。そこで出てくるのがスタック。データ構造の一つ。
スタック=LIFOである。算術に便利。例えば以下のような算術の場合。
```
// d=(2-x)*(y+5)
// push ==> スタックに値を格納する
// pop ===> スタックから値を取り出す
push 2
push x
sub // 減算
push y
push 5
add // 加算
mult // 乗算
pop d
```
スタック構造だからこそ、前方から読み取っていけば計算ができることがわかる。キュー構造などではうまくいかない。
### 理論(PJ)
スタックベースのVMを実装する。16bitのデータ型を持ち整数・ブール値・ポインタとして使われる前提
### 実装
// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/06/add/Add.asm
// Computes R0 = 2 + 3 (R0 refers to RAM[0])
@2
D=A
@3
D=D+A
@0
M=D
// This file is part of www.nand2tetris.org
// and the book "The Elements of Computing Systems"
// by Nisan and Schocken, MIT Press.
// File name: projects/06/add/Add.asm
// Computes R0 = 2 + 3 (R0 refers to RAM[0])
@2
D=A
@3
D=D+A
@0
M=D
@256
D=A
@SP
M=D
// push start constant,7
@7
D=A
@SP
A=M
M=D
// push end constant,7
// increment start
@SP
M=M+1
// increment end
// increment end
// push start constant,8
@8
D=A
@SP
A=M
M=D
// push end constant,8
// increment start
@SP
M=M+1
// increment end
// increment end
// decrement start
@SP
M=M-1
// decrement end
@SP
A=M
D=M
// 一つ目
// decrement start
@SP
M=M-1
// decrement end
@SP
A=M
M=M+D
// 計算
// increment start
@SP
M=M+1
// increment end
(END)
@END
0;JMP
<
import java.io.*
class CodeWriter(filePath: String) {
private var pw: PrintWriter? = null
private var fileDir: String? = null
init {
fileDir = filePath.split("/").subList(0, filePath.split("/").size - 1).joinToString("/")
// 出力
val fil = FileWriter(File(fileDir, filePath.split("/").last()))
pw = PrintWriter(BufferedWriter(fil))
pw!!.println("@256")
pw!!.println("D=A")
pw!!.println("@SP")
pw!!.println("M=D")
}
fun setFileName(fileName: String) {
// 一旦閉じて
pw!!.close()
// また再開
val fil = FileWriter(File(fileDir, fileName))
pw = PrintWriter(BufferedWriter(fil))
pw!!.println("@256")
pw!!.println("D=A")
pw!!.println("@SP")
pw!!.println("M=D")
}
/**
* 算術コードを書き込む
*/
fun writeArithmetic(command: String) {
when (command) {
"add" -> {
decrementPointer()
// 一つ目のスタック取得
pw!!.println("@SP")
pw!!.println("A=M")
pw!!.println("D=M")
pw!!.println("// 一つ目")
decrementPointer()
// 二つ目のスタック取得
pw!!.println("@SP")
pw!!.println("A=M")
pw!!.println("M=M+D")
pw!!.println("// 計算")
incrementPointer()
}
"sub" -> {
// 一つ目のスタック取得
pw!!.println("@SP")
pw!!.println("D=M")
decrementPointer()
// 二つ目のスタック取得
pw!!.println("@SP")
// 計算
pw!!.println("M=M+D")
}
"neg" -> {
}
"eq" -> {
}
"gt" -> {
}
"lt" -> {
}
"and" -> {
}
"or" -> {
}
"not" -> {
}
}
}
/**
* pushまたはpopコマンドを書き込む
*/
fun writePushPop(command: Parser.COMMAND_TYPE, segment: String, index: Int) {
if (command == Parser.COMMAND_TYPE.C_PUSH) {
if (segment == "constant") {
pw!!.println("// push start " + segment + "," + index.toString())
pw!!.println("@" + index.toString())
pw!!.println("D=A")
pw!!.println("@SP")
pw!!.println("A=M")
pw!!.println("M=D")