rui312/9ccを写経する-その3

https://github.com/rui314/9cc/commit/9d4e20421f140f9ad7a1d161daab088008aa5760

Fix warnings.

とのことなので大した変更ではなさそうだが、stdnoreturn.hnoreturnは知らなかった。

返り値のないvoidな関数にreturn;があってもコンパイラは特に何も言わないが、noreturnをつけていると、return;があったら

9cc.c:87:3: warning: function declared 'noreturn' has a 'return' statement
   return;
   ^~~~~~

みたいな感じで教えてくれるようだ。

https://github.com/rui314/9cc/commit/52dc210c9d22171de630eb78a3946d23b0dc727d

ぬー、またデータ構造を追加する割にはテストに変化がないのでメリットが見えないパターンっぽい。 がんばっていこう。

typedef struct {
  void **data;
  int capacity;
  int len;
} Vector;
Vector *new_vec()
void vec_push(Vector *v, void *elem)

どうやら自動拡張する配列のようだ。

なぜこう判断できたかというと、mrubyで見たことあるから、という他なくうまく説明できない……。

(void *)としているのは任意の型のポインタを使えるパターンで、pushしてlenが1増えて、capacityに到達したらcapacityを2倍にしてreallocというのが自動拡張配列の典型パターンというイメージ。

こういうデータ構造はあると何かと便利なんだろう。Rubyでも最終的なサイズとか意識せずに、Arrayに適当にオブジェクトを突っ込んだりできて便利だ。

Token *add_token(Vector *v, int ty, char *input)

なるほど、察しはついた。これまでtokens[100]と適当に100 tokenまでとしていたものを、先程の自動拡張配列Vectorを使ってtokenの拡張を自動化するようだ。

Vector *tokenize(char *p)

ここまでくれば変更は予測できる。

新たにVectorを作って、tokensを使っている部分をadd_tokenに置き換え、最後にVectorを返せば、 任意の長さのプログラムに合わせていい感じにメモリを確保した配列が手に入る。

Vector *tokens;
int pos;

Token tokens[100]は廃止されVector *tokensとして生まれ変わった。

posは0初期化しなくていいのかな?と思ったけど、グローバル変数は勝手に0初期化された気がしなくもない。

Node *number()

tokensの構造は変わったので、使っている部分を書き直す。

ついでにパースエラーだったときはposを変更しないように修正しているっぽい。

int gen_ir_sub(Vector *v, Node *node)

引数にVectorを追加。ins[1000]を廃止して自動拡張配列に置き換えるんだろう。

tokensはグローバル変数のままなのに、insはグローバル変数ではなくするようだ。

regnogen_ir_subでしか使ってないので、グローバル変数からstatic変数に変更して影響範囲を減らしている。

あとはins[inp++]となっている部分をvec_pushに置き換えるだけ。

Vector *gen_ir(Node *node)

ここでVectorを作ってgen_ir_subに渡してIR_RETURNを最後に追加してVectorを返しておしまい。

- bool used[8];
+ bool used[sizeof(regs) / sizeof(*regs)];

決め打ちだった8をregsの長さに合わせている。

- int reg_map[1000];
+ int *reg_map;

1000個決め打ちを止めている。

このサイズは元はinsの1000と合わせていた。

insの長さが可変になったので、insの代わりになったVectorからlenを引いてくればサイズは決めれそうだ。

void alloc_regs(Vector *irv)

引数が増えた。

insが廃止されたので、gen_irで作ったinsの代わりになるVectorを渡しているのだろう。

ならばinp => vector->len, ins[i] => vector->data[i]と置き換えればいいだけだ。

……、と思ったらreg_mapも決め打ちじゃなくなったんだった。

alloc_regsの外で初期化してもいいが、-1allloc_regsでしか使ってないので、alloc_regsの中で初期化したほうがわかりやすくなりそうだ。

reg_mapグローバル変数を辞めて関数間で渡したほうが更に良くなりそうだが、いずれやるんだろう。

void gen_x86(Vector *irv)

gen_x86でもinsを使っていたので、代わりになるirvを与えている。

新たに構造体が増えたが、総じてリファクタリング的な内容だった。

https://github.com/rui314/9cc/commit/ce9a6bbfbd430332666c64cd8b433043c45a6044

概念毎にファイルを分けているだけ。と思ったら少し変更がある。

tokensグローバル変数をやめてtokenize関数の返り値にしている。

かと思いきやtoken.cのファイルスコープのグローバル変数tokensは存在していて、新関数parseグローバル変数tokensにtokenizeで作った変数をそのまま割り当てている。どういうこっちゃ。

んーオブジェクト指向で考えてはいけなくて、ファイル毎にスコープ切れてるからいいじゃん的な考え方ということか。

まあこれはこれで簡素で便利と言える。

Vector系関数はutil.cなので、やはり便利構造体ということなんだろう。

https://github.com/rui314/9cc/commit/e3cda6b3777ad6937dcf51faddbbbeea733a1048

掛け算の導入のようだ。 これまで上から見ていったが、ファイル分割によってどこから見ていけばわからなくなった。

とりあえずtest.shからみて、token -> node -> irと処理順に見ていこう。

test.sh

ようやく新しいことができる。まずは掛け算っぽい。

token.c

if文の||で一つずつ書いていた部分をstrchrで纏めてしまっている。

parse.c

exprnumberの間にmul関数の層を増やしている。

これによって、mulでは掛け算があれば掛け算のNode構造を作り、掛け算がなく加減算ならばそのまま数字を返す。

つまり掛け算を優先して計算するようなNodeの木構造を作っている。んーこのへんはむずい。

難しいがとりあえず計算の優先順位は関数の層を追加して、Nodeの構造を作ってやれば、あとはir層で再帰的に処理してくれる。

なるほど、ここでようやくNode層を作ったメリットが現れた。

codegen.c

    case '*':
      printf("  mov rax, %s\n", regs[ir->rhs]);
      printf("  mul %s\n", regs[ir->lhs]);
      printf("  mov %s, rax\n", regs[ir->lhs]);
      break;

アセンブラでの掛け算はaddsubと違って、レジスタが固定されているっぽい。

参考文献としてはhttps://qiita.com/MoriokaReimen/items/4853587dcb9eb96fab62http://bttb.s1.valueserver.jp/wordpress/blog/2017/10/22/assembler_mul/がみつかるが、一次ソースはどうすれば見つかるんだろう……。

https://github.com/rui314/9cc/commit/4cd8eedb7c352380828e664950dff97a83eaeef8

割り算の追加。殆どは問題なく*のときと同じだ。 mul関数が名前がそのままなのが気になる。

    case '/':
      printf("  mov rax, %s\n", regs[ir->lhs]);
      printf("  cqo\n");
      printf("  div %s\n", regs[ir->rhs]);
      printf("  mov %s, rax\n", regs[ir->lhs]);
      break;

divはともかくcqoがググっても全然わからん……。

符号を云々なもののようだが、これがないとコンパイラFloating point exceptionと表示されてしまう。うーむ。

ともかくこのコンパイラは割り算までできるようになった。