LoginSignup
4
0

More than 3 years have passed since last update.

Kotlinで セルフホストを したかった

Posted at

(575)

はじめに

こんにちは、unifarといいます。
Androidアプリを七個ぐらい作った。
随分前に、ruiさんの低レイヤを知りたい人のためのcコンパイラ作成入門
を読んで「これなら作れそう!」と思い、大胆にも、Cコンパイラ作成入門を読んでKotlinコンパイラを作成し始めてしまった。

それから、
195日が過ぎ、
112コミットが積まれ、
本体は1827行になり、
私の受験勉強はいよいよ終盤に差し掛かかった。

そして、現状がこちら(github)

今現在動かせるのはこんな感じ

//まあまあ複雑なテストコード
    class TestClass(n) {
        val a = 4
        fun get42(m): Int {
            return n + m + a
        }
    }

    fun func(x): TestClass {
        val instance = TestClass(x)
        return instance
    }

    fun main(): Int {
        val instance = func(26)
        return instance.get42(12)
    } //->42

//フィボナッチ数列
fun fib(n): Int {
    if (n == 1) {
        return 1
    }
    if (n == 2) {
        return 1
    }
    return fib(n - 1) + fib(n - 2)
}

fun main(): Int {
    return fib(5) //->5
}

最初の内は「セルフホストしてやんよ!」と思っていたが、次第に雲行きが怪しくなり、他に作りたいものも出来て、一旦開発を止めることにした。しかし、ここまでの開発でも多くのことを学べた。例えば、再帰下降分析、アセンブラでのmain、加算、関数、文字の実装、など。ここからは、具体的な作成方法をコミットを追って解説していく。

1コミット目

fun main() {
    val input = readLine()
    println(input?.let { parse(it) })
}

fun parse(likScript: String): String {
    val tokens = tokenize(likScript)
    return "default"

}

fun tokenize(str: String): List<Token> {
    val spaceRemoved = str.split(space)
    var temporaryNumberList = mutableListOf<String>()
    val resultList = mutableListOf<Token>()

    spaceRemoved.forEach { char: String ->
        if (!(numbers.contains(char))) {
            resultList.add(Token(TokenType.NUMBER, numberList2number(temporaryNumberList)))
            temporaryNumberList = mutableListOf()
        }
        if (operators.contains(char)) {
            resultList.add(
                when (char) {
                    plus -> Token(TokenType.PLUS)
                    minus -> Token(TokenType.MINUS)
                    multiply -> Token(TokenType.MULTIPLY)
                    divide -> Token(TokenType.DIVIDE)
                    else -> Token(TokenType.NULL)//絶対来ない
                }
            )
            return@forEach
        }
        if (numbers.contains(char)) {
            temporaryNumberList.add(char)
            return@forEach
        }
    }
    return resultList
}

fun numberList2number(list: List<String>): Int {
    val buffer = StringBuilder()
    list.forEach {
        buffer.append(it)
    }
    return Integer.parseInt(buffer.toString())
}

これは主に入力を分解する関数(1+3 ->1,+,3)。
書き忘れていたけど、このKotlinもどき言語は、KotlinのKとLとIを並び替えて、Likという名前にしてある(どう発音するんだろうね?)。
本家のファーストコミットとはだいぶ違い、はなからCコンパイラ作成入門に従っていないのが分かる。
(最初はインタープリタを作るつもりだった)

4コミット目

fun parseAdd(innerList: List<Token>): Int? {
    var cursor = 0
    innerList[cursor].value ?: run {
        throw IllegalArgumentException("first element must be number")
    }

    var result = innerList[cursor].value!!

    cursor++
    while (innerList.size - 1 > cursor)
        if (innerList[cursor].type == TokenType.PLUS) {
            cursor++
            innerList[cursor].value?.let {
                result += it
                cursor++
            }
        } else if (innerList[cursor].type == TokenType.MINUS) {
            cursor++
            innerList[cursor].value?.let {
                result -= it
                cursor++
            }
        }
    return result
}

これは加算のトークン列を受け取って結果をかえす関数(21 ,+ ,4 ,- ,2 -> 23 )
久々にこのコードを見たが、把握するのに結構時間がかかった。
おそらくこれはあまり良くないコードなのだろう。
しかし、コンパイラになる気配がない。

20コミット目

fun main() {
    val input = mutableListOf<String>()
    var next = readLine()
    while (next != "***") {
        input.add(next!!)
        next = readLine()
    }
    println(".intel_syntax noprefix")
    println(".global main")
    println("main:")
    println("  mov rax, ${input[0]}")
    println("  ret")
}

20コミット目にしてやっとアセンブリが出てきた。今まで何をやっていたんだ。(本当はインタープリターとしてif文くらいまで実装していた)。

これは「標準入力から受け取った数字を返却するアセンブリ」を出力するプログラム。
Kotlinプログラムは内部でアセンブラに変換され実行される。
解説すると、

println(".intel_syntax noprefix")

訳↑「これからintel記法を使うよ~」

  • アセンブラにはintel記法とAT&T記法がある
println(".global main")

訳↑「main関数を公開するよ~」

  • アセンブラもmainから実行される
println("main:")

訳↑「main関数 はっじまっるよ~~」

println("  mov rax, ${input[0]}")

訳↑「raxレジスタにinput[0]を入れるよ~」

  • アセンブラではレジスタという場所に値を保管する
  • raxはレジスタの名前
  • ${input[0]}はテンプレート記法
  • mov x, y はyをxに入れる、というアセンブラの命令
  • 行頭を開けないと特殊な文として解釈される
  • 開けると普通の命令
println("  ret")

訳↑「撤退するよ~」
(ret命令で関数から抜ける際raxの値が戻り値となる)
という感じ。

コンパイラと聞くと難しそうな感じがするが、これもコンパイラの一種だと(私は)思う。

34コミット目

fun printAssembly() {
        when (token.type) {
            PLUS -> {
                println("  push ${leftNode!!.token.value}")
                println("  push ${rightNode!!.token.value}")

                println("  pop rdi")
                println("  pop rax")
                println("  add rax, rdi")
            }
            NUMBER -> {
                println("  mov rax, ${token.value}")
            }
        }
    }

このprintAssebmly関数はNodeクラスの関数である。Nodeはtokenを本体として持っている。
これはアセンブラのmain関数の中身を出力する部分。3+4とかのコードがコンパイルできるようになった。
そろそろ疲れたので、解説はこれでおしまいにする。

前提としては、

  • アセンブラでもスタックが利用できる
  • push x はxの値をpushする
  • pop x はスタックトップの値をxに入れる
  • add x, yはx+yの結果をxのレジスタに代入する
  • x + yは内部で↓のような構造になっている

.......x
.../
+
...\
.......y (わかりずらいね)
要するに、+ノードを親として左右にx,yという値を子として持っているということである。
(実は、1コミット目のトークン化する関数は、このような木構造を作るための下準備でもあった。)

ここでは、左右のノードの値をpushして、raxとrdiレジスタにそれぞれ保存(pop)、そして両者を加算し(add)、raxに値を保管する(自動的に保存される)、ということをやっている。この後ret命令を呼べば、めでたく戻り値もセットされた状態で関数から帰還できる。

最後に

コンパイラを作るのは難しいことではない―――コンパイラの定義によっては。

関係ないけど、下に好きな曲を貼っておくので聞いてほしい。鳥肌立った。

マグ・メル

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0