🐬はじめに
buiderscon2019でのDQNEOさんのライブコーディングの発表を見て「コンパイラ作るの夢ある〜面白そ〜」となったのでforkして0からやってみました。
といってGoで書いたのでほとんど写経。ちなみに更にその親玉のchibicc
や8cc
なんかの実際のコードも見てみました。
📝メモ
... case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': intLiteral := readNumber(char) token := &Token{ kind: "intliteral", value: intLiteral, } tokens = append(tokens, token) fmt.Printf(" '%s'", token.value) case ';', '+', '-', '*', '/': token := &Token{ kind: "punct", value: string([]byte{char}), } tokens = append(tokens, token) fmt.Printf(" '%s'", token.value) ...
このあたりの部分はByteReaderさんが処理したものをTokenizerさんがTokenごとにうまく分割しているところ。
readNumber
メソッドは数字がその後も続くようだったら最後まで見に行くやつ。
... switch token.value { case "+", "-", "*", "/": return &Expr{ kind: "binary", operator: token.value, left: expr, right: parseUnaryExpr(), } ...
このあたりはTokenの2つ目を見た後にそれが演算子Tokenだった場合はそれにさらに続くと期待される数値Tokenをパースさせようとするところ。
parseUnaryExpr
の中でparseUnaryExpr
を呼び出して再帰的になっている。
ちなみに余談なんですがアセンブリコードに出てくるmovq
addq
のqってなんなんだろう?と思って調べてみました。
検索して出てくるサンプルコードを見ていると、movq や addq ではなく mov, add と書かれているものもあります。q は quad word の略で、4 語長つまり 64bit データを扱うことを意味します。x86 では命令にビット長を明示指定することができ、b : byte (8bit)、w : word (16bit)、l : long word (32bit)、q : quad word (64bit) が指定できます。x86 はもともと 16bit CPU である 8086 から出発しているので、16bit を基準の語長(ワードサイズ)としています。 多くの場合、movq と書かず単に mov とだけ書けばアセンブラが自動的に適切な長さを判断してくれるのですが、この資料では長さを明示する方を選びました。
ということでqなくても問題なく動作しました。
root@e1219eb59aea:/mnt# echo -n '4*7' | go run main.go | ./asrun ------------ assembly(a.s) ------------ #Tokens: '4' '*' '7' .global main main: mov $4, %rax mov $7, %rcx imul %rcx, %rax ret ------------ result ------------ 28 root@e1219eb59aea:/mnt# echo -n '12/3' | go run main.go | ./asrun ------------ assembly(a.s) ------------ #Tokens: '12' '/' '3' .global main main: mov $12, %rax mov $3, %rcx mov $0, %rdx idiv %rcx ret ------------ result ------------ 4
てな具合で写経しながらではあるものの四則演算のできるアセンブリコードを吐かせることにも無事成功。
型があるとだいぶ楽ですね。
楽しかったのでまた続きをやる…かも?