自作仮想マシンの自作アセンブラに向けた自作コンパイラで遊んだ話

https://github.com/KajizukaTaichi/ruka

# はじめに
x86_64コンパイラを作ろうとしたのですが、もう複雑で複雑で挫折しました。
そこで、自分で理想のISAを持つ仮想マシンを作ろうとしました。

# 仮想マシン

こんな感じに実行の様子が見えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952107/bb627f00-5876-45fb-b86a-38866f4f1f0c.png)

<details><summary>仕様はこちら。</summary>

## Ruka VM
手軽な命令セットアーキテクチャを採用した軽量仮想マシン

### システム構成
|名前|説明|サイズ|読み込み|書き込み|
|---|---|---|---|---|
|プログラム領域|実行されるバイトコードを格納|可変長|不可|不可|
|メモリ領域|データやオブジェクトを格納|512バイト固定|可能(アドレス指定)|可能(同左)|
|スタック領域|関数などとのデータ受け渡し用|可変長|可能(ポップのみ)|可能(プッシュのみ)|
|関数スタック|呼び出し元アドレスが積まれる|可変長|不可|不可|
|レジスタ群|演算時の一時的なデータ格納用|64ビット固定|可能(mov命令)|可能(同左)|

※ 全ての値は64ビット浮動小数点数(f64)です。

### ISA
|ニーモニック|意味|
|---|---|
|MOV レジスタ, 値|データ転送|
|ADD レジスタ, 値|足し算|
|MUL レジスタ, 値|掛け算|
|NEG レジスタ|符号反転|
|INV レジスタ|逆数|
|EQL レジスタ, 値|等価比較|
|LES レジスタ, 値|大小比較|
|NOR レジスタ, 値|論理ORして否定|
|JMP 条件, アドレス|条件つきジャンプ|
|CAL アドレス|関数呼び出し|
|RET|呼び出し元に戻る|
|LDA レジスタ, アドレス|データ読み込み|
|STA アドレス, 値|データ書き込み|
|PSH 値|スタックに追加|
|POP レジスタ|スタックから取り出し|
|NOP|何もしない|
|HLT|プログラム終了|

### レジスタ
|名前|意味|役割|
|---|---|---|
|PC|Program Counter|次の命令のアドレス|
|AR|Accumulator Register|演算結果の格納|
|DR|Data Register|データの格納|
|CR|Conditional Register|条件の格納|
|BA|Base Address|メモリの基底アドレス|
|SP|Stack Pointer|スタックに長さ|

</details>

# コンパイラ群
コンパイラを作りました。現在対応してるのはBASICとForthです。他の言語もこれから実装できたらなと思います。

## BASIC
https://github.com/KajizukaTaichi/ruka/tree/main/basic

```vb
Let bit = (1 + 2) * 3 - 7
Let max_1byte = pow(bit, 8) - 1
Exit Program

Sub pow(base, exponent)
    Let number = 1
    Let index = 0
    Let flag = true
    While flag
        If index = exponent
            Let flag = false
        Else
            Let index = index + 1
            Let number = number * base
        End If
    End While
    Return number
End Sub
```

<details><summary>コンパイルで生成されたアセンブリ</summary>

```nasm
	mov ar, 1
	psh ar
	mov ar, 2
	mov dr, ar
	pop ar
	add ar, dr
	psh ar
	mov ar, 3
	mov dr, ar
	pop ar
	mul ar, dr
	psh ar
	mov ar, 7
	mov dr, ar
	pop ar
	neg dr
	add ar, dr
	sta 0, ar	; bit
	lda ar, 0	; bit
	psh ar
	mov ar, 8
	psh ar
	cal subroutine_pow
	pop ar
	psh ar
	mov ar, 1
	mov dr, ar
	pop ar
	neg dr
	add ar, dr
	sta 1, ar	; max_1byte
	hlt
subroutine_pow:
	pop ar
	sta 2, ar	; exponent
	pop ar
	sta 3, ar	; base
	mov ar, 1
	sta 4, ar	; number
	mov ar, 0
	sta 5, ar	; index
	mov ar, 1
	sta 6, ar	; flag
while_start_1:
	lda ar, 6	; flag
	mov cr, ar
	nor cr, cr
	jmp cr, while_end_1
	lda ar, 5	; index
	psh ar
	lda ar, 2	; exponent
	mov dr, ar
	pop ar
	eql ar, dr
	mov cr, ar
	jmp cr, if_then_0
	jmp 1, if_else_0
if_then_0:
	mov ar, 0
	sta 6, ar	; flag
	jmp 1, if_end_0
if_else_0:
	lda ar, 5	; index
	psh ar
	mov ar, 1
	mov dr, ar
	pop ar
	add ar, dr
	sta 5, ar	; index
	lda ar, 4	; number
	psh ar
	lda ar, 3	; base
	mov dr, ar
	pop ar
	mul ar, dr
	sta 4, ar	; number
if_end_0:
	jmp 1, while_start_1
while_end_1:
	lda ar, 4	; number
	psh ar
	ret
```

</details>



## Forth
https://github.com/KajizukaTaichi/ruka/tree/main/forth

思ったよりパーサーの実装が難しかったです。（まだ不完全です）

```forth
twice: 2 * ;
half: 2 / ;

main:
    5 twice twice half
    10 =
        ? 1 2
        ¥ 3 4
    # + half
    8 ! ;
```

<details><summary>コンパイルで生成されたアセンブリ</summary>

```nasm
	cal word_main
	hlt
word_twice:
	psh 2
	pop dr
	pop ar
	mul ar, dr
	psh ar
	ret
word_half:
	psh 2
	pop dr
	pop ar
	inv dr
	mul ar, dr
	psh ar
	ret
word_main:
	psh 5
	cal word_twice
	cal word_twice
	cal word_half
	psh 10
	pop dr
	pop ar
	eql ar, dr
	psh ar
	pop cr
	jmp cr, then_0
	jmp 1, else_0
then_0:
	psh 1
	psh 2
	jmp 1, end_0
else_0:
	psh 3
	psh 4
end_0:
	pop dr
	pop ar
	add ar, dr
	psh ar
	cal word_half
	psh 8
	pop ba
	pop ar
	sta ba, ar
	ret
```

</details>

#### コロンの使われ方が本来のForthとは違うのではないか？

はい、違いますね。これは意図した独自の拡張です。
`: twice 2 * ;`より`twice: 2 * ;`の方が、単語名と本体の分離が明確になるので。

### 日本語プログラミング言語Mind風の文法
```
倍 とは 2 を 掛 ける こと。
半分 とは 2 で 割 る こと。

始まり とは
    5 の 倍 の 倍 の 半分 が
    10 と 等 しい
        ならば 1 と 2 を
        でなければ 3 と 4 を
    つぎに
    足 しあわせて 半分 にして
    メモリの 8 番地に 書 きこむ こと。
```

# 感想
ちょうど子供の日なので良いおもちゃになったと思います。