Cコンパイラ作成入門のメモ #8 (Step11)

はじめに

こちらをやってみたときのメモを書いていく。

www.sigbus.info

今回はStep11

Commit

Step11

github.com

調べたこと・理解したこと

トークナイズ結果出力用関数の実装

イメージ図/コード

// トークナイズした結果を出力する
void token_preview(Token *start)
{
  Token *cur = start;
  fprintf(stderr, ".... start preview tokens ....\n");
  for (Token start; cur; cur = cur->next)
  {
    char c[100];
    memset(c, '\0', sizeof(c));
    memcpy(c,cur->str,cur->len);
    fprintf(stderr, " kind=> %d, str=> %s,\n", cur->kind, c);
  }
  fprintf(stderr, ".... finish!! ....\n");
}

メモ

  • デバッグ用にトークナイズ結果を出力する関数を作ってみた。
  • foo = 1; return foo;出力結果は下記の感じ
.... start preview tokens ....
 kind=> 1, str=> foo,
 kind=> 0, str=> =,
 kind=> 2, str=> 1,
 kind=> 0, str=> ;,
 kind=> 3, str=> return,
 kind=> 1, str=> foo,
 kind=> 0, str=> ;,
 kind=> 4, str=> ,
.... finish!! ....
  • 連結リストを辿っているlvar_findとかを参考に実装した。

参考

文字列用配列の初期化について

文字列を空にする | Programming Place Plus C言語編 逆引き

文字列の切り出しについて

C 言語でサブストリングを取得する | Delft スタック

思ったこと

パーサの実装について

returnの右側をlhsとしてノードに格納しているところ

Node *stmt() {
  Node *node;

  if (consume(TK_RETURN)) {
    node = calloc(1, sizeof(Node));
    node->kind = ND_RETURN;
    node->lhs = expr();
  } else {
    node = expr();
  }

  if (!consume(';'))
    error_at(tokens[pos].str, "';'ではないトークンです");
  return node;
}

上記のパース部分returnの右側をlhsとしてノードに格納している。 直感的には右辺なんだけど、左辺としてデータ登録している部分イマイチしっくりこない。

パーサの文法と、コードの実装の対応

パーサの文法と、コードの実装の対応についてイマイチ腹落ちできていない。文法とコードを横に並べて、どことどこが対応しているか一回整理できれば、おおよそ理解できそうなので、今度一回まとめてみた。

書いて見ると、まぁ大体、対応は理解できた。 新しい構文に対応するときに、文法を自分で考えるのは難しそう。。。

まとめ図

https://raw.githubusercontent.com/lvlnaga/9cc/master/docs/step11/%E7%94%9F%E6%88%90%E8%A6%8F%E5%89%87%E3%81%A8%E5%AE%9F%E8%A3%85%E3%81%AE%E9%96%A2%E4%BF%82.drawio.svg

  • 画像にするとズレちゃうのでご容赦ください。

テキストに書いてあった複数ret出力されるが気にしないのところの解釈

イマイチしっくりこなかったので、下図にまとめた。
要は、使われることがないretも出力される実装になっている。 でも実装をシンプルにするためであり、実害が無いならそれでもいいよね。 ということと理解した。

まとめ図

https://raw.githubusercontent.com/lvlnaga/9cc/master/docs/step11/codegen%E3%81%AEret%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E6%95%B4%E7%90%86.drawio.svg

エピローグ部分についておさらい

エピローグでやっていることが、やっぱりよくわからなくなったので、図にしてみた。

retでやっていることがいまいち理解できていない。 プログラムカウンタが存在しているはずだが、そこをテキストでは説明されていないので、そこがいまいちしっくり来ていないことが原因なのかも。 retが実行されるとrspは一個上にずれるのは、勝手にされる仕様なのだろうか?

まとめ図

https://raw.githubusercontent.com/lvlnaga/9cc/master/docs/step11/ret%E3%81%A8%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E9%96%A2%E4%BF%82.drawio.svg

思ったこと

やれば、やるほど、前回のここよくわかってないなぁというのが次々に出てくる。 気づけるだけよいということ?これもブログにoutputしている効能?

同じように9ccテキスト参考に実装してみた系のgithubを見ていると、自分よりもすごい短い期間に実装、コミットされていて自分との距離を感じる。
そんなに長い時間実装集中できないんだよなぁ。
慣れるとできるのかしら?

Cコンパイラ作成入門のメモ #7 (Step10)

はじめに

こちらをやってみたときのメモを書いていく。

www.sigbus.info

今回はStep10

Commit

Step10

github.com

調べたこと・理解したこと

ローカル変数管理用の連結リスト周りについて

テキスト読んだだけではいまいち理解できなかったり、エラー吐いたりしたところについて、下図の通りにまとめた

イメージ図

https://raw.githubusercontent.com/lvlnaga/9cc/master/docs/step10/%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E5%A4%89%E6%95%B0%E7%94%A8%E9%80%A3%E7%B5%90%E3%83%AA%E3%82%B9%E3%83%88.drawio.svg

思ったこと

  • 連結リストに抵抗感がなくなってきた気がする。
  • ローカル変数の管理方法についてよくわからないなぁと思って、次の日もう一度考えたら理解できたのでコツコツやるのは大事だと思った。

Cコンパイラ作成入門のメモ #6 (Step9)

はじめに

こちらをやってみたときのメモを書いていく。

www.sigbus.info

今回はStep9

Commit

Step9

github.com

調べたこと、理解したこと

データ構造の流れ

イメージ図

https://raw.githubusercontent.com/lvlnaga/9cc/master/docs/step9/%E3%83%87%E3%83%BC%E3%82%BF%E6%A7%8B%E9%80%A0%E3%81%AE%E6%B5%81%E3%82%8C.drawio.svg

メモ

  • ソースコードをどのようなステップでどういうデータ構造に落とし込んで、アセンブリを吐いているのかイマイチ腹落ちしてなかったので、絵を描いて整理

参考

なし

NodeとCodegen実装の対応を理解

イメージ図

https://raw.githubusercontent.com/lvlnaga/9cc/master/docs/step9/Node%E3%81%AE%E6%A7%8B%E9%80%A0%E3%81%A8Codegen%E3%81%AE%E5%AF%BE%E5%BF%9C%E7%90%86%E8%A7%A3.drawio.svg

メモ

  • gen()でどのようにパースした結果をアセンブリに変換しているのかを順を追って理解した。

参考

なし

スタックの伸びる方向について

イメージ図

https://raw.githubusercontent.com/lvlnaga/9cc/master/docs/step9/%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF%E3%81%AE%E4%BC%B8%E3%81%B3%E3%82%8B%E6%96%B9%E5%90%91%E3%81%A8%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%83%9E%E3%83%83%E3%83%97.drawio.svg

メモ

  • 「コラム: スタックの伸びる方向」に書いてあることがいまいちイメージできなかったので図にしてみた。
  • 上位アドレス、下位アドレスで、上、下方向は図のようなイメージだと思う

参考

なし

consume_ident() の実装について

コード

// 次のトークンがidentのときには、トークンを1つ読み進めて
// そのトークンを返す。それ以外の場合はNULLを返す。
static Token *consume_ident()
{
  if (token->kind != TK_IDENT)
    return NULL;// 期待するトークンと不一致(変数でなければ)の場合はNULL

  // 次のトークンがIdentのときはreturnするtokenを退避
  Token *ret = token;
  // tokenを次にconsumeする
  token = token->next;
  return ret;
}

// 新:primary    = num | ident | "(" expr ")"
static Node *primary()
{
  // 次のトークンが"("なら、"(" expr ")"のはず
  if (consume("("))
  {
    Node *node = expr();
    expect(")");
    return node;
  }

  Token *tok = consume_ident(); 
  if (tok)
  {
    Node *node = calloc(1, sizeof(Node));
    node->kind = ND_LVAR;
    //文字コードからaから何文字先かを求めてそれに8byte掛けて、オフセットを計算
    node->offset = (tok->str[0] - 'a'+1) * 8; 
    return node;
  }
  
  // ()でもidentでもなければ数値のはず
  return new_num(expect_number());
}


メモ

  • 変数を読み進めるための関数。
  • テキストには実装書いてないので、こんな感じに実装した。
  • consume()とかを参考に、returnにはtokenを返すように実装

参考

なし

『for (int i = 0; code[i]; i++)』のcode[i]でloop終了条件にしていいの?

コード

// 9cc.c
int main(int argc, char **argv)
{
//中略

  // 先頭の式から順にコード生成
  // program()でcode[i]の最後はNULLになっているはずなのでfor文の終了条件はcode[i]でOK
  for (int i = 0; code[i]; i++)
  {
    gen(code[i]);

    // 式の評価結果としてスタックに1つの値が残っている
    // はずなので、スタックが溢れないようにポップしておく
    printf("  pop rax\n");
  }

//中略
}

//parse.c

// program    = stmt*
void *program()
{
  int i = 0;
  while (!at_eof())
    code[i++] = stmt();
  code[i] = NULL; 
//ここで末尾にNULLを入れているから, mainのコード生成のfor文の終了条件判定がcode[i]でOKなのか
}

メモ

  • なんで、for文の終了条件がcode[i]でいいんだろう?と思った。
  • ただ単に program()でperse結果をcode[]に格納するときに、末尾にNULLを格納しているだけ。
  • NULLポインタは評価するとfalseになるので、code[]終端でループ抜ける。

参考

標準エラー出力デバッグ

イメージ図/コード

// 新しいトークンを作成してcurに繋げる
static Token *new_token(TokenKind kind, Token *cur, char *str, int len)
{
  Token *tok = calloc(1, sizeof(Token));
  tok->kind = kind;
  tok->str = str;
  tok->len = len;
  cur->next = tok;

  #ifdef DEBUG
    fprintf(stderr, "add new token. kind=> %d, str=> %s \n", kind ,str);
  #endif

  return tok;
}

メモ

ビルドしたら、テスト通らなかったので、printfデバッグを試みた。
が、このコードは標準出力結果をアセンブラとして保存しているので、だめじゃんとなった。 標準エラー出力デバッグすればよい。ということで、出力方法を調べた。

ちゃんとトークナイズはできてそう。ということはわかった。

ただ結局printfデバッグはあまり活躍せず、吐かれたアセンブリを読んでいって、pushとpopが逆に書いてあったり、いろいろケアレスミスを発見した。という感じで解決。

参考

標準入力・標準出力・標準エラー出力

参考にしたサイト

実装の参考

どう実装したらわからない部分参考にさせてもらった
ローカル変数導入 · pocari/compilerbook-9cc@9dbdc68 · GitHub

drawioファイルをブログに貼り付け

drawioをvscodeで描いて、githubのリンクを使ってブログで使う
Draw.ioをGitHub管理して画像を埋め込む - システム開発で思うところ

Draw.io Integrationの背景が暗いの嫌だなぁ

白背景にする方法
VScodeの拡張機能「Draw.io Integration」で背景色を白色に変更する方法

思ったこと

  • 学習のススメ方について
    • このステップは平日の仕事後の時間に15min.とかをコツコツ積み重ねながら進められたところがかなりよかった。
    • まとまった時間はなくても進められる。と思えた。
    • むしろ、よくわからないなぁと思いながら、中断して、次の日、仕事しながらバックグランドでそのことをぼんやり考えるので、ふとしたときに腹に落ちることが何度かあって、良かった。
  • 細切れにやった方がよくわからないことを理解するときは、時間区切って、わからない状態で中断することがある意味効率的とも感じた
  • draw.ioで図にかきながら理解したことが良かった
    • VSCodeで描けるし、それをgithubに上げればブログにもペタっと貼り付けできるしかなりよい。

Cコンパイラ作成入門のメモ #5 (Step8)

はじめに

こちらをやってみたときのメモを書いていく。

www.sigbus.info

今回は分割コンパイルとリンク、Step8

Commit

Step8

github.com

調べたこと

make

コード

CFLAGS=-std=c11 -g -static
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)

9cc: $(OBJS)
        $(CC) -o 9cc $(OBJS) $(LDFLAGS)

$(OBJS): 9cc.h

test: 9cc
        ./test.sh

clean:
        rm -f 9cc *.o *~ tmp*

.PHONY: test clean
}

実行されるコマンド

$ make
cc -std=c11 -g -static   -c -o tokenize.o tokenize.c
cc -std=c11 -g -static   -c -o 9cc.o 9cc.c
cc -std=c11 -g -static   -c -o parse.o parse.c
cc -std=c11 -g -static   -c -o codegen.o codegen.c
cc -o 9cc tokenize.o 9cc.o parse.o codegen.o 

理解したこと

Makefileの前提知識

  • コロン(:)で区切られた行と、タブでインデントされた0行以上のコマンドの行が1つのルールを構成する
    • コロンの前の名前のことを「ターゲット」という -コロンの後の0個以上のファイル名のことを依存ファイルという
  • .PHONY
    • ダミーのターゲットを表している
    • make test や make cleanのtest,cleanというのは、そういう名前のファイルを作成するために指定しているわけでは無いが、makeにはそれがわからない。
    • .PHONYで指定してあげることで、本当にそういう名前のファイルを作りたいわけではなく、指定されたターゲットのファイルが存在しているかどうかに関わらずルールのコマンドを実行するべき。ということをmakeに伝えることができる。

なぜこのMakefileでこのコマンドが実行されるのか

コンパイルオプション

ルールを分解して考える

9cc: $(OBJS)
        $(CC) -o 9cc $(OBJS) $(LDFLAGS)

のルールでmakeがやることは?

変数

SRCSに含まれるのは?⇒tokenize.c,9cc.c, parse.c, codegen.c
OBJSに含まれるのは?⇒tokenize.o,9cc.o, parse.o, codegen.o

流れ
  1. 9ccを作りたいからOBJSを見に行こう
  2. OBJSにはtokenize.oが含まれるぞ
  3. .oには.cが必要だからtokenize.cを見に行こう
  4. tokenize.cが更新されているからコンパイルしよう
  5. cc -std=c11 -g -static -c -o tokenize.o tokenize.c コマンドが叩かれる
    CFLAGSで指定されている引数をつかってコマンドが叩かれる
  6. 上記2. - 5. が全OBJS指定ファイル分繰り返される
  7. OBJSが揃ったから、いよいよコマンド行の実行をするぞ。
    cc -o 9cc tokenize.o 9cc.o parse.o codegen.o
  8. おわり

ちなみに、$(OBJS): 9cc.h の意味は
ちなみにOBJSは9cc.hに依存すると書いてあるので、もし9cc.hが更新されてたら、全.oは再度作り直しの意味

その他

思ったこと

  • tokenizerについてはファイル分けると書いてなかったが、Referenceも分けているので分けといた
  • makeにもccにもユーザが指定せずとも勝手にやってくれていることがたくさんあって、それを理解するのが大変。
    • 理解しなくてもいいのかもだけど。

Cコンパイラ作成入門のメモ #4 (Step6, 7)

はじめに

こちらをやってみたときのメモを書いていく。

www.sigbus.info

今回はStep6, 7

Commit

Step6

https://github.com/lvlnaga/9cc/commit/0dd18e522522b32fd049d74d5fdd023eddced843

Step7

https://github.com/lvlnaga/9cc/commit/e89c0bbd2f42a5a409211f38822f7d1b8bc6c45b

調べたこと

startswith

コード

bool startswith(char *p, char *q)
{
  return memcmp(p, q, strlen(q)) == 0;
}

詳細

  • 文字列が特定のフラグメントで開始しているどうかを確認
    • トークナイズするために、期待する文字列一致すれば true, 一致してなければfalseを返す関数を用意している
  • Javaとかでは用意されている関数の模様。

参考

JavaのstartsWith、endsWithメソッドの使い方を現役エンジニアが解説【初心者向け】 | TechAcademyマガジン

【Python】startswith・endswith(指定文字で始まる?終わる?) | 鎖プログラム (pg-chain.com)

思ったこと

  • 変更は大きいが変更している内容はおおよそ理解できた。
  • ただ、これを自分で考えて開発できるかというとそんな気はしない。
    • 引き出しが増えればこのパターンとかを引き出せる様になるのかな。

Cコンパイラ作成入門のメモ #3 (Step4, 5)

はじめに

こちらをやってみたときのメモを書いていく。

www.sigbus.info

今回はStep4, 5 commitはこちら Add *, / and () · lvlnaga/9cc@f1b1a96 (github.com)

調べたこと

関数のプロトタイプ宣言について

  • プロトタイプ宣言とは
    • 「関数の名前」と「引数と返り値の型」だけを先に宣言すること。
  • なぜ必要か
    • コンパイラは上から順にコードを読んでいく。
    • 関数が定義されてないのに、関数をcallされると、それが正しいのかわからない。
    • 書き手からすると、関数の中身をまず書かないとcallできないのは面倒。
    • プロトタイプ宣言という構文を用意すれば、さきに関数宣言しておいて、中身は後で書く。ということができるようになる。
  • どういう嬉しさがあるか
    • コンパイラが関数callのミスを検出できる
      • 例えば、引数の数、引数のデータ型、戻り値のデータ型のチェック

参考

C言語入門 - 関数のプロトタイプ宣言 - Webkaru

C言語 プロトタイプ宣言の効果【関数を安全に呼び出す仕組み】

P - 1.15.関数

空returnについて

ここ

void gen(Node *node)
{
  if (node->kind == ND_NUM )
  {
    printf("  push %d\n", node->val);
    return; // ★
  }
...
  • 戻り値を持たないということ。NULLを返すのとは違う。

参考

【C言語入門】returnで関数の戻り値を返す方法 | 侍エンジニアブログ

strchr を使って特定文字有無のif文

ここ

    if (strchr("+-*/()",*p)) // ★
    {
      cur = new_token(TK_RESERVED, cur, p++);
      continue;
    }
...
  • strchr関数は文字列の先頭から「文字」を検索して見つかった場所をポインタで返す
  • これをif文のconditionとして使うことで、抽出したい文字が含まれるかどうかを確認している
  • 賢い使い方というか、テクニック

参考

C言語 strchr 使い方

思ったこと

マインドマップに書きながら理解

読んでるだけじゃ全然いみ分からなかったのでマインドマップにまとめながら読み進めて言ったら、腹落ちできた。

マインドマップ

再帰的な処理について

  • 再帰的な関数は自分で思いつく気がしない。
  • 「慣れてもこれでできるのが、不思議な感じがするものだよ。」という文章があって、そういうもんか。と思った
  • 再帰的な処理って潜っていくところと、もどってくるところの2フェーズに分けられると思った。

説明の流れについて

  • いきなり具体的な環境の話をするのではなく、自分の伝えたいことを伝えるために、まずはシンプルな仮想的な世界における振る舞いを説明する。という説明の流れでわかりやすい。

Cコンパイラ作成入門のメモ #2 (Step2,3)

はじめに

こちらをやってみたときのメモを書いていく。

www.sigbus.info

今回はStep3まで commitはこちら tokenizerにより空白文字のスキップが可能になった · lvlnaga/9cc@056147d (github.com)

調べたこと

連結リスト

  • リストは「要素」と「次のデータを指し示すポインタ」の2つからなるデータ数珠のようにつながっているデータ構造
  • リストの場合は要素を探すために先頭からポイントをたどる必要あり
  • 配列と比べていい点
    • データの追加、削除がデータ数に関係なく数ステップでできる
      • nextポインタを書き換えればOK

参考

うさぎでもわかる配列と連結リスト | 工業大学生ももやまのうさぎ塾 (momoyama-usagi.com)

tokenizeのところ詳細

書いたのはこれ

// 新しいトークンを作成してcurに繋げる
Token *new_token(TokenKind kind, Token *cur, char *str){
  Token *tok = calloc(1, sizeof(Token)); //? callocの仕様を知りたい
  tok->kind = kind;
  tok->str = str;
  cur->next = tok;
  return tok;
}

// 入力文字列pをトークナイズしてそれを返す
Token *tokenize(char *p){
  Token head; //dummyのhead要素を作る
  head.next = NULL;
  Token *cur = &head; //現在のtokenへのポインタにdummyのheadを設定

  while (*p)
  {
    // 空白文字をスキップ
    if (isspace(*p))
    {
      p++;
      continue;
    }
    
    if (*p == '+' || *p == '-')
    {
      cur = new_token(TK_RESERVED, cur, p++); //(1)
      continue;
    }
    
    if (isdigit(*p))
    {
      cur = new_token(TK_NUM, cur, p); //(2)
      cur->val = strtol(p, &p, 10); 
      continue;
    }

    error("トークナイズできません");
  }

  new_token(TK_EOF, cur, p);
  return head.next; //先頭のトークンを返す
}

(1)について

p++は後置演算子なので現在のpが指し示す位置でtokenを作成した後、 pが次の位置へと進められる。ということ

(2)について

strtol(p,&p,10); で#2引数でpのポインタが次(数字の次の位置)に進められる。
strtolの#2引数には変換が失敗する文字がある位置(数字じゃない文字がある位置)のポインタが入る。
例えば 12 +トークナイズする場合は、12の次の変換失敗する’ ‘ (スペース)の位置が&pに格納される。
すなわちポインタが次に進められる。

参考

文字列を数値に変換(C言語) - 超初心者向けプログラミング入門 (pc-note.net)`

可変個引数、動的引数 (...)について

// エラーを報告するための関数
// printfと同じ引数をとる
void error(char *fmt, ...){ 
  va_list ap;
  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap);
  fprintf(stderr, "\n");
  exit(1);
}

引数の数を可変に受け取れる関数のこと。

stdarg.hで定義されているマクロとセットで基本的に使用する模様。

詳しくは下記。

可変個引数 | Programming Place Plus C言語編 第52章 (programming-place.net)

vprintfについて、詳しくは下記。自前でエラー関数作ったりするときはよくある常套手段のような雰囲気。

vprintf | Programming Place Plus C言語編 標準ライブラリのリファレンス (programming-place.net)