compilerbook作成日記
compilerbook作成日記
ステップ9
-
コンパイルは通るが、上手くtokenizeが動いていないっぽい
-
tokenizeする時のログ:
[2023/07/19/23 05:08:13] start tokenizing... [2023/07/19/23 05:08:13] tokenizing: 0; [2023/07/19/23 05:08:13] tokenizing number(0;): [2023/07/19/23 05:08:13] tokenizing: kind=TK_NUM, str=0;, len=1, val=0 [2023/07/19/23 05:08:13] tokenizing: ; [2023/07/19/23 05:08:13] tokenizing reserved(;): [2023/07/19/23 05:08:13] token: kind=TK_NUM, str=0;, len=1, val=0 <-おかしい [2023/07/19/23 05:08:13] token: kind=TK_NUM, str=0;, len=1, val=0 [2023/07/19/23 05:08:13] token: kind=TK_NUM, str=0;, len=1, val=0 [2023/07/19/23 05:08:13] end tokenizing...
-
token→len
と実際の文字列の長さが一致してなくてエラーになってることがあった -
‘=’
を許容する文字列に加えるのを忘れてた
- ここまでの実装。
- 本と違うところとしては、
- logger、log用のヘルパー関数を実装した。
printf
と同じ構文で使えるLOGGER()
という関数を定義した。log/log.txt
に書き込まれるので、tail +1f ./log/log.txt
で末尾だけ表示させ続けるのがやりやすい。 -
consume_ident()
の実装が書いてなかったので自分で実装した。consume()
を参考に実装した。
- logger、log用のヘルパー関数を実装した。
ステップ10, 11
- グローバル変数のlocalsを最初にcallocしとかないといけない。
LVar *locals;
を宣言するだけでは、メモリに領域が確保されるわけではない。 -
bool consume(Tokenkind kind || char op);
を定義したかったが、こんなことができるか分からなかったので、bool consume(char op)
とbool consume_token(Tokenkind kind)
に分けた。 -
user_input
(p
)をトークンにパースする段階で、複数文字のローカル変数のトークンにtoken→len
を文字数の長さ分設定する必要がある。
ステップ12
この章から詳細な解説がなくなるので、今までコードのコピペで済ませて理解していなかった部分が浮き彫りになる。
わからないところは面倒でも前の方に戻ってもう一度読み直した方が良い。compilerbookに書いてある情報以外には基本的に必要ないはず。
わかってなかったこと
- ツリーとコードの対応
どうやら今までこのツリーとコードの対応を理解していなかったらしい。白丸は関数を表していて、最終的に出来上がるノードに含まれない、ということを理解していなかった。
(左側の図はcompilerbookから拝借した)
if文の実装
次のEBNFを実装する
program = stmt*
stmt = expr ";" | "return" expr ";"
+ | "if" "(" expr ")" stmt ("else" stmt)?
※終端記号を◯、関数を□で表現する
- elseがないとき
- elseがあるとき
Nodeがlhs(左手)、rhs(右手)以外にelse節を持てるようにする
↓こちらのコードを参考にした。
nodeは完全2分木じゃないといけないと思っていたけど、このコード見てelse用のノードを生やすアイデアを真似させてもらった。注意
普通のCとはまだ違っていて、if文の中にかけるstmtは一つだけ。なので、
if(1<2) a = 1; b = 2; return a + b; a = 0; b = 1; else return b - a;
みたいに、elseより前に2つ以上stmtがある文はまだコンパイルしなくて良い。
stmtをprogramに置き換えれば良いのかもしれないけど…
アセンブリの見方
命令 | 意味 |
---|---|
push x | スタックのトップにxをpush |
pop x | スタックのトップからxをpop |
rax | 関数がリターンする値が入っているレジスタ |
rdi | 第一引数が入っているレジスタ |
rsi | 第二引数が入っているレジスタ |
ret | スタックからアドレスを一つポップし、そのアドレスにジャンプする |
よく見たらチートシートついてた。神か??
./assembly/tmp.s:38: Error: unknown pseudo-op: '.lend1'
というエラーが出る
フラグに:
をつけ忘れるとこのエラーが出るらしい
.Lend1:
がうっかり.Lend1
になっているとエラーが出る
if文だけ追加
if-else文の実装
次のEBNFを実装する
program = stmt*
stmt = expr ";" | "return" expr ";"
| "if" "(" expr ")" stmt (else stmt ";")?
ifとelseの中のstmtは一つだけなのに注意。stmt*
ではない…(ここを間違えてて詰んでた)
ここまでの実装
while文の実装
次のEBNFを実装する
stmt = expr "; | "return" expr ";"
| "if" "(" expr ")" stmt ("else" stmt)?
| "while" "(" expr ")" stmt
for文の実装
次のEBNFを実装する
stmt = expr "; | "return" expr ";"
| "if" "(" expr ")" stmt ("else" stmt)?
| "while" "(" expr ")" stmt
| "for" "(" expr? ";" expr? ";" expr? ")" stmt
同時に、if, whileの条件式を格納する場所も変更する
- if文を次のように変更してしまう。条件式などをlhs, rhsに入れたままでも動くとは思うが、意味が異なってしまうため。
- while文を次のように変更する
- for文を次のように実装する
無限ループのテストケースが通らない
for文では、for(expr? ; ; expr?)
と条件式をなしにすることで無限ループになるはずである。
しかし、
assert 10 'for(i=1;;) a = i + 1; return a;'
このケースでは無限ループになるのに、
assert 10 'for(i=0;;) a = i + 1; return a;'
とすると無限ループにならない。
for(i=0;;i = i + 1) a = i + 1; return 123456; => 139 expected, but got 64
→いやなんで?????
↑ここまでの実装
後でバグが出てきそうだけどとりあえずこのままにしておくか…
ステップ13 ブロック
実装するEBNF
program = stmt*
stmt = expr ";"
| "{" stmt* "}"
| ...
...
↑こういうふうに実装しようとすると、Nodeにnext
みたいな単方向リスト用の要素が必要になる?
他の人の実装方法
-
Vector型を作っている
-
Node->next
を持たせて単方向リストにしてる
https://github.com/pocari/compilerbook-9cc/blob/master/ynicc.h
意外とすんなり実装できてしまった…こんなにうまくいくもん?
ステップ14: 関数の呼び出しに対応する
実装するEBNF
...
primary = num
| ident ("(" ")")?
| "(" expr ")"
ん?また一段難しくなった…???
リンクとは
別々の2つ以上の.c
ファイルを、他のファイルに依存しているアドレスが虫食い状態になっているオブジェクトファイルにコンパイルして、後からそれらのオブジェクトファイルをガッチャンコするやつ(合ってる…?)
あと、アセンブラだとどの命令をどのアドレスに配置すればいいかは決められないけど、その配置もリンカがやってくれる。
↓パタヘネに載っていたC言語の翻訳階層の図を見て書いた図
引数のない関数を使えるようにする
int foo() { printf("OK\n"); }
をオブジェクトファイルにコンパイルして、自分のコンパイラの出力とリンクできるようにする。
まずは、この構文をパースできるようにする
実装するEBNF
...
primary = num
| ident ("(" ")")?
| "(" expr ")"
x86_64では、call 関数のラベル
で関数にジャンプすることができる。
gcc -S test.c
でtest.c
のアセンブリを確認したところ、test()
関数のラベルは.test
になっていた。
test()
をcallするには、call test
を使う。(.test
ではないことに注意)
- C言語で詰まった点:
strncpy(node->fname, tok->str, tok->len);
だけだとセグフォが出るので、一度node->fname
というchar *
型のポインタが指す先にchar用の領域を次のように確保してあげます。
node->fname = calloc(1, sizeof(char));
strncpy(node->fname, tok->str, tok->len);
6つまでの引数のある関数を呼び出せるようにする
↓compilerbookにあるありがたい表によると、引数は以下のレジスタに収納すればいいらしい。
レジスタ名 | 引数 |
---|---|
RDI | 第1引数 |
RSI | 第2引数 |
RDX | 第3引数 |
RCX | 第4引数 |
R8 | 第5引数 |
R9 | 第6引数 |