Hackコンピューター(nand2tetris)のアセンブラを作る

# 1. 概要
「[VHDLの練習としてnand2tetrisのHackコンピューターをMAX10 FPGAで作る](https://qiita.com/cyq04000/items/879755a83834d82c92b1)」の続き。今度はアセンブラを作る。テキストどおりに作業を進める。Pythonを使う。   

# 2. Parserクラスを作る

<details><summary>コードを見る/隠す</summary><div>

```Parser.py
class Parser:
    def __init__(self, file_path):
        self.file = open(file_path, "r")
        self.EOF_POS = self.__get_EOF_position(self.file)
        self.current_inst = ""

    def has_more_lines(self):
        return self.file.tell() != self.EOF_POS

    def advance(self):
        self.current_inst = self.__readline_then_strip()
        return self

    def inst_type(self):
        if not self.current_inst:
            return ""
        elif "@" in self.current_inst:
            return "A"
        elif "(" in self.current_inst and ")" in self.current_inst:
            return "L"
        else:
            return "C"

    def symbol(self):
        if self.inst_type() == "A":
            return self.__get_right_hand(self.current_inst, "@")
        elif self.inst_type() == "L":
            return self.current_inst[1:-1] # (LABEL)のLABELだけを抜いて返す。
        else:
            return ""

    def dest(self):
        # dest=comp;jump
        if self.inst_type() == "C" and self.__has_destination():
            return self.__get_left_hand(self.current_inst, "=") # destだけを抜いて返す。
        else:
            return ""

    def comp(self):
        # dest=comp;jump
        if self.inst_type() == "C":
            comp_part = self.__get_right_hand(self.current_inst, "=") # dest=を切り落として、
            comp_part = self.__get_left_hand(comp_part, ";") # さらに;jumpを切り落としてcompだけを抜いて返す。
            return comp_part
        else:
            return ""

    def jump(self):
        # dest=comp;jump
        if self.inst_type() == "C" and self.__has_jump_to():
            return self.__get_right_hand(self.current_inst, ";") # jumpだけを抜いて返す。
        else:
            return ""

    def close(self):
        self.file.close()
        return self


    def __readline_then_strip(self):
        line = self.file.readline()
        line = line.strip() # 先頭の空白と末尾の改行記号とを削除する。
        line = self.__get_left_hand(line, "//") # コメントを削除する
        line = line.replace(" ", "") # 文字列中の半角スペースを削除する。
        line = line.replace("　", "") # 文字列中の全角スペースを削除する。
        line = line.replace("\t", "") # 文字列中のタブ文字を削除する。
        return line if line else ""
    def __has_jump_to(self):
        return (";" in self.current_inst)
    def __has_destination(self):
        return ("=" in self.current_inst)
    def __get_left_hand(self, strings, delimiter):
        return strings.split(delimiter)[0]
    def __get_right_hand(self, strings, delimiter):
        return strings.split(delimiter)[-1]
    def __get_EOF_position(self, file):
        file.seek(0, 2) # ファイル末尾に移動して、
        pos = file.tell() # その位置を取得して、
        self.file.seek(0) # 再度ファイル先頭に戻って、
        return pos # 取得したファイル末尾位置を返す。


if __name__ == "__main__":
    s = Parser("test.asm")
    while (s.has_more_lines()):
        s.advance()
        print("--------------------------")
        print("命令        :", s.current_inst)
        print("命令タイプ  :", s.inst_type())
        print("シンボル    :", s.symbol())
        print("格納先      :", s.dest())
        print("ALU出力     :", s.comp())
        print("ジャンプ命令:", s.jump())
    s.close()
```

</div></details>


# 3. Codeクラスを作る

<details><summary>コードを見る/隠す</summary><div>

```Code.py
class Code:
    def __init__(self):
        pass
        
    # dest=comp;jumpのdestをバイナリコードに変える。
    def dest(self, mnemonic):
        adm = 0
        if "A" in mnemonic: adm |= 1 << 2
        if "D" in mnemonic: adm |= 1 << 1
        if "M" in mnemonic: adm |= 1

        return bin(adm)[2:].zfill(3)

    # dest=comp;jumpのcompをバイナリコードに変える。
    def comp(self, mnemonic):
        cccccc = ""
        if mnemonic == "0"                       : cccccc = "101010"
        if mnemonic == "1"                       : cccccc = "111111"
        if mnemonic == "-1"                      : cccccc = "111010"
        if mnemonic == "D"                       : cccccc = "001100"
        if mnemonic == "A"   or mnemonic == "M"  : cccccc = "110000"
        if mnemonic == "!D"                      : cccccc = "001101"
        if mnemonic == "!A"  or mnemonic == "!M" : cccccc = "110001"
        if mnemonic == "-D"                      : cccccc = "001111"
        if mnemonic == "-A"  or mnemonic == "-M" : cccccc = "110011"
        if mnemonic == "D+1"                     : cccccc = "011111"
        if mnemonic == "A+1" or mnemonic == "M+1": cccccc = "110111"
        if mnemonic == "D-1"                     : cccccc = "001110"
        if mnemonic == "A-1" or mnemonic == "M-1": cccccc = "110010"
        if mnemonic == "D+A" or mnemonic == "D+M": cccccc = "000010"
        if mnemonic == "D-A" or mnemonic == "D-M": cccccc = "010011"
        if mnemonic == "A-D" or mnemonic == "M-D": cccccc = "000111"
        if mnemonic == "D&A" or mnemonic == "D&M": cccccc = "000000"
        if mnemonic == "D|A" or mnemonic == "D|M": cccccc = "010101"

        acccccc = ""
        if "M" in mnemonic: acccccc = "1" + cccccc
        else              : acccccc = "0" + cccccc
        return acccccc

    # dest=comp;jumpのjumpをバイナリコードに変える。
    def jump(self, mnemonic):
        jjj = [
            "",
            "JGT",
            "JEQ",
            "JGE",
            "JLT",
            "JNE",
            "JLE",
            "JMP",
            ]
        jjj_index = jjj.index(mnemonic)
        return bin(jjj_index)[2:].zfill(3)
        
#########################################################################
if __name__ == "__main__":
    inst = Code()
    
    print(inst.dest(""))
    print(inst.dest("M"))
    print(inst.dest("D"))
    print(inst.dest("DM"))
    print(inst.dest("A"))
    print(inst.dest("AM"))
    print(inst.dest("AD"))
    print(inst.dest("ADM"))
    print("")

    print(inst.comp("0"))
    print(inst.comp("A"))
    print(inst.comp("!A"))
    print(inst.comp("!M"))
    print(inst.comp("D|A"))
    print(inst.comp("D|M"))
    print("")

    print(inst.jump(""))
    print(inst.jump("JGT"))
    print(inst.jump("JEQ"))
    print(inst.jump("JGE"))
    print(inst.jump("JLT"))
    print(inst.jump("JNE"))
    print(inst.jump("JLE"))
    print(inst.jump("JMP"))
    print("")
```

</div></details>



# 4. Symbol_Tableクラスを作る
ここではPythonの辞書を使った。     

<details><summary>コードを見る/隠す</summary><div>

```Symbol_Table.py
class Symbol_Table:
    def __init__(self, screen_address=8192, keyboard_address=8576, out_port_address=8577):
        self.symbol_dict = {
            "SP":0,
            "LCL":1,
            "ARG":2,
            "THIS":3,
            "THAT":4,
            "SCREEN":screen_address,
            "KBD":keyboard_address,
            "OUT_PORT":out_port_address}
        
        self.ram_last_address = 15

        # 疑似汎用レジスタとして使うR0～R15とアドレスとのペアをシンボルテーブルに追加する。
        for i in range(self.ram_last_address+1):
            self.symbol_dict["R" + str(i)] = i

    def add_entry(self, symbol, address):
        if not self.contains(symbol):
            self.symbol_dict[symbol] = address
        else:
            print("symbol already exists")
        return self

    def contains(self, symbol):
        return symbol in self.symbol_dict
    
    def get_address(self, symbol):
        return self.symbol_dict[symbol]





    def get_symbol_dict(self):
        return self.symbol_dict
    def get_ram_last_address(self):
        return self.ram_last_address
    def increment_ram_last_address(self):
        self.ram_last_address += 1
        return self


##################################################
if __name__ == "__main__":
    s = Symbol_Table(screen_address=8192, keyboard_address=8576, out_port_address=8577)
    print("初期化直後:", s.get_symbol_dict())

    s.add_entry("LOOP",10)

    s.increment_ram_last_address()
    s.add_entry("sum", s.get_ram_last_address())

    s.increment_ram_last_address()
    s.add_entry("i", s.get_ram_last_address())

    s.add_entry("LOOP2",20)
    print("エントリ追加後: ", s.get_symbol_dict())

```

</div></details>

# 5. Hack_Assemblerを作る
これまでに作った3つのクラスをアセンブラにまとめる。これで`.hack`ファイルが生成できるようになった。以上でテキストの内容は終わりである。      

<details><summary>コードを見る/隠す</summary><div>

```Hack_Assembler.py
from Parser import *
from Code import *
from Symbol_Table import *

class Hack_Assembler:
    def __init__(self,
                 asm_file_path,
                 screen_address=8192,
                 keyboard_address=8576,
                 out_port_address=8577):
        
        # 出力ファイル名は入力ファイル名の拡張子を.hackに変えたものにする。
        self.output_file_path = self.__replace_file_extension(asm_file_path, "hack")
        self.output_file = open(self.output_file_path, "w")
        
        self.parser = Parser(asm_file_path)
        self.code = Code()
        self.symbol_table = Symbol_Table(
            screen_address=screen_address,
            keyboard_address=keyboard_address,
            out_port_address=out_port_address)

        self.hack_file = [] # .hackファイルを出力するだけでなくリストとしても持っておく。
        
        self.__first_pass()
        self.__second_pass()

    def __first_pass(self):
        self.parser.file.seek(0) # ファイルの先頭に移動して、
        line_number = -1 # 行番号の初期値を設定して、
        
        while (self.parser.has_more_lines()):
            self.parser.advance() # 次の行に移動して、

            if (self.parser.inst_type() == "C") or (self.parser.inst_type() == "A"):
                line_number += 1 # A命令なら何もせずに行番号を先へ進めるだけにして、

            elif self.parser.inst_type() == "L":
                self.symbol_table.add_entry(self.parser.symbol(), line_number + 1) # L命令ならラベルをシンボルテーブルに追加する。

    def __second_pass(self):
        self.parser.file.seek(0) # ファイルの先頭に移動して、
        
        while (self.parser.has_more_lines()):
            self.parser.advance() # 次の行に移動して、

            if self.parser.inst_type() == "C":
                self.__second_pass_for_C_inst() # C命令ならそれに応じた処理をして、

            elif self.parser.inst_type() == "A":
                self.__second_pass_for_A_inst() # A命令ならそれに応じた処理をして、

        self.parser.close()
        self.close()

    def __second_pass_for_C_inst(self):
        # dest=comp;jumpの各部をバイナリコードに変えて、
        comp_part = self.code.comp(self.parser.comp())
        dest_part = self.code.dest(self.parser.dest())
        jump_part = self.code.jump(self.parser.jump())

        # 16ビットのC命令に変えてファイルに書き込む。
        processed_string = "111" + comp_part + dest_part + jump_part
        self.output_file.write(processed_string + "\n") 
        
        self.hack_file.append(processed_string)

    def __second_pass_for_A_inst(self):
        address_part = self.parser.symbol()
        try:
            int(address_part)
        except:
            # シンボルがシンボルテーブルに登録済みであったら
            if self.symbol_table.contains(address_part):
                # それを取り出すが、
                address_part = self.symbol_table.get_address(address_part)
            # シンボルテーブルに登録していなかったら、
            else:
                # そのシンボルと、空いているRAMのうち一番若いRAMの番地とをペアにして
                # シンボルテーブルに登録して、
                self.symbol_table.increment_ram_last_address()
                self.symbol_table.add_entry(address_part, self.symbol_table.get_ram_last_address())
                #address_part = self.symbol_table.ram_last_address
                address_part = self.symbol_table.get_ram_last_address()
                        
        # 16ビットのA命令に変えてファイルに書き込む。
        processed_string = "0" + bin(int(address_part))[2:].zfill(15)
        self.output_file.write(processed_string + "\n")
        
        self.hack_file.append(processed_string)


    def get_hack_file(self):
        return self.hack_file

    def close(self):
        self.output_file.close()
        return self

    def __replace_file_extension(self, file_name, new_extension):
        return file_name.split(".")[0] + "." + new_extension
    
             

####################################################################
if __name__ == "__main__":
    Hack_Assembler("test.asm")
```

</div></details>


# 6. .asmファイルから一気に.mifファイルを生成する
`.asm`ファイルから`.mif`ファイルが一気に生成できるようにする。`.mif`ファイルというのは、MAX10のメモリーブロックに作ったROMを初期化するためのファイルのこと。`.asm`ファイルを`drop_here_asm.bat`にドラッグ&ドロップすれば`rom_int.mif`ファイルが生成される。     

<details><summary>コード(asm2mif.py)を見る/隠す</summary><div>

```asm2mif.py
import sys
from Hack_Assembler import *

class asm2mif:
    def __init__(
        self,
        asm_file_path,
        mif_name = "rom_init.mif",
        rom_words = 4096,
        screen_address = 8192,
        keyboard_address = 8576,
        out_port_address = 8577):

        file_path = ""
        try:
            file_path = sys.argv[1]
        except:
            file_path = asm_file_path
        hack_assembler = Hack_Assembler(
            asm_file_path = file_path,
            screen_address = screen_address,
            keyboard_address = keyboard_address,
            out_port_address = out_port_address)
        
        self.hack_file = hack_assembler.get_hack_file()

        self.__generate_mif(self.hack_file,
                            mif_name,
                            rom_words = rom_words)

    def __generate_mif(self, bit_strings, mif_path, rom_words):
        f = open(mif_path, "w")
        f.write("WIDTH=%d;\n" % 16)
        f.write("DEPTH=%d;\n" % rom_words)
        f.write("ADDRESS_RADIX=UNS;\n")
        f.write("DATA_RADIX=BIN;\n")
        f.write("CONTENT BEGIN\n")
        for i in range(len(bit_strings)):
            f.write("%10d: %s;\n" % (i, bit_strings[i]))
        f.write("END;\n")
        f.close()

if __name__ == "__main__":
    asm2mif(
        asm_file_path = "test.asm",
        mif_name = "rom_init.mif",
        rom_words = 4096,
        screen_address = 8192,
        keyboard_address = 8576,
        out_port_address = 8577)
```

</div></details>


<details><summary>バッチファイル(drop_here_asm.bat)を見る/隠す</summary><div>

```drop_here_asm.bat
for %%i in (%*) do (
    python.exe asm2mif.py %%i
    )
```

</div></details>


<details><summary>テストした.asmファイル(test.asm)を見る/隠す</summary><div>

```test.asm
////////////////////////////////////
// 1からRAM[0]までの総和を求める。//
////////////////////////////////////

// ディップスイッチで設定した任意の値をRAM[0]に格納する。
@KBD
D=M
@R0
M=D

// 加算する値の初期値i=1
@i
M=1

// 総和の初期値sum=0
@sum
M=0

(LOOP)
@i
D=M

// 途中の何かをOUT_PORTに出力する。
@OUT_PORT
M=D

@R0
D=D-M

// 途中の何かをOUT_PORTに出力する。
@OUT_PORT
M=D

// if i-RAM[0] > 0: goto (STOP)
// すなわち計算し終わったらループを抜けて(STOP)へ飛ぶ。
@STOP
D;JGT

// sum+=i
@sum
D=M

// 途中の何かをOUT_PORTに出力する。
@OUT_PORT
M=D

@i
D=D+M

// 途中の何かをOUT_PORTに出力する。
@OUT_PORT
M=D

@sum
M=D

// i++
@i
M=M+1
@LOOP
0;JMP

(STOP)
// R1=sum
@sum
D=M

// 最終結果をOUT_PORTに出力する。
@OUT_PORT
M=D

(END)
@END
0;JMP
```

</div></details>



<details><summary>生成された.mifファイル(rom_init.mif)を見る/隠す</summary><div>

```rom_init_mif
WIDTH=16;
DEPTH=4096;
ADDRESS_RADIX=UNS;
DATA_RADIX=BIN;
CONTENT BEGIN
         0: 0010000110000000;
         1: 1111110000010000;
         2: 0000000000000000;
         3: 1110001100001000;
         4: 0000000000010000;
         5: 1110111111001000;
         6: 0000000000010001;
         7: 1110101010001000;
         8: 0000000000010000;
         9: 1111110000010000;
        10: 0010000110000001;
        11: 1110001100001000;
        12: 0000000000000000;
        13: 1111010011010000;
        14: 0010000110000001;
        15: 1110001100001000;
        16: 0000000000100000;
        17: 1110001100000001;
        18: 0000000000010001;
        19: 1111110000010000;
        20: 0010000110000001;
        21: 1110001100001000;
        22: 0000000000010000;
        23: 1111000010010000;
        24: 0010000110000001;
        25: 1110001100001000;
        26: 0000000000010001;
        27: 1110001100001000;
        28: 0000000000010000;
        29: 1111110111001000;
        30: 0000000000001000;
        31: 1110101010000111;
        32: 0000000000010001;
        33: 1111110000010000;
        34: 0010000110000001;
        35: 1110001100001000;
        36: 0000000000100100;
        37: 1110101010000111;
END;
```

</div></details>

# 7. 実行結果
https://qiita.com/cyq04000/items/879755a83834d82c92b1#61-%E5%AE%9F%E8%A1%8C%E7%B5%90%E6%9E%9C