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に上げればブログにもペタっと貼り付けできるしかなりよい。