コンパイラの開発を通してプログラムの書き方について学ぶ取り組み

記録を取ることもなく黙々とやっていたので、日記はステップ11から。

  • 2020/6/11
    • 無引数の定義を実装した。
  • 2020/5/28
    • 忙しかったのと、ほかにやってみたいことがあったのでブランクが空いてしまった。
    • 関数呼び出しの実装をした。
    • strndup関数がimplicit declaration errorになって呼べないのでおかしいなと思って、manを見たりStackoverflowを見た。原因は、プログラム内で_POSIX_C_SOURCEを定義してないせいで関数原型宣言が公開されていなかったのだった。
  • 2020/5/12
    • for文を追加した。
    • ブランク空けちゃったのはもったいない。
    • リファレンス実装では補助的なファクトリ関数を用意して読みやすさと書きやすさを高めている。
      • new_unaryやread_expr_stmtなど。
      • リファレンンス実装ではlhsを使いまわしているのだから、自分は二項演算子用のファクトリ関数で書いているが、本来はunary用専用のノードを用意したほうがいいのだろうなと思った。
        • これはTODOとしたい。
  • 2020/5/7
    • if文とwhile文を追加した。
    • リファレンス実装の中でシリアルナンバーを一時的な変数に保存しているのを見て、どうしてこう書いているのか不思議に思い、そうしないで直接シリアルナンバーを出力するよう書いてみたところ、このテストケースGNUアセンブラからラベルが見つからないエラーが出た。
      • 理由は単純で、子のノードをコンパイル中にシリアルナンバーがインクリメントされてしまい、戻ってきたら対応が取れなくなってしまうからだった。
  • 2020/5/5
    • リファレンス実装に倣ってFunction構造体を足してみた。
    • 9ccはchibiccを参考してCでやっているが、zinccでのやりかたも気になりCommon Lispで真似てみることにした。CLだとテストを整備するのが少々煩雑で、Cとシェルスクリプトの組みあわせのやりかたはとても単純で良いと思った。
  • 2020/5/4
    • if文を追加しようとしたがバグがあり、その解決の過程で、それまでに書いたコードにはその場しのぎ のコードが目立つと感じて一旦取りやめた。
      • コード生成においてpop raxが入力プログラムの各式のコード生成終了後に挿入されてしまい、if文がraxの値に依存していたせいで、必要な値が潰されてしまう現象が起きていた。
    • リファレンス実装を見ながらリファクタリングや、制約の撤廃を行なった。
      • main関数のコード生成の責任をコード生成部に移した。
      • ファイル名や関数名をより良いものにした。
      • 各モジュールが公開すべき関数やグローバル変数と非公開とすべき関数とグローバル変数を分けた。
      • オブジェクト生成を専用の関数で行なうように統一した。
      • スタックフレームのサイズを使った変数の個数から計算したり、受け入れ可能なプログラム行をメモリの許す限り無制限とした。
  • 2020/4/28
    • ステップ9からステップ11まで進んだ。
    • 可変長の変数を定義できるようになった。
      • 本では、識別子に使用可能な文字を判定するのにctype.hのisalnum(3)関数の代わりにis_alnum関数を定義して使っていた。isalnum(3)はアンダースコアを認識せず、ロケールに左右されるためだろうか?
    • returnキーワードを導入した。
      • 本ではreturnキーワードのために、トークン用の新たな列挙定数TK_RETURNを定義するように書いていたが、予約語を表すTK_RESERVEDで十分と考えた。演算子もキーワードも言語の予約語と考えたほうがすっきりするし、導入もしやすかった。
      • returnキーワードに対してコード生成するとき、return用のエピローグの中で誤ってmov rsp, rspと書いていて、SEGVを起こした。正しくはmov rsp, rbp
    • gdbのTUIを起動する方法を知った。

Tips/気をつけること

連結リストを構築するときは、ダミーのhead要素を作ってそこに新しい要素を繋げていって、最後にhead->nextを返すようにするとコードが簡単になります。このような方法ではhead要素に割り当てられたメモリはほとんど無駄になりますが、ローカル変数をアロケートするコストはほぼゼロなので、特に気にする必要はありません。

/* こういうこと? */

#include <stdio.h>
#include <stdlib.h>

typedef struct Cell Cell;
struct Cell {
    struct Cell *next;
    char *word;
};

/* producer */
Cell *create_list(void) {
    Cell dummy = { .next = NULL, .word = NULL };
    Cell *current = &dummy;

    char *words[] = {"akizuki", "teruzuki", "suzutsuki", "hatsuzuki", NULL};

    for (char **place = words; *place != NULL; place++) {
        Cell *cell = calloc(1, sizeof(Cell));
        cell->word = *place;
        cell->next = NULL;
        current->next = cell;
        current = cell;
    }
    return dummy.next;
}

/* consumer */
int main(void) {
    Cell *current = create_list();

    for (; current != NULL; current = current->next) {
        printf("%s\n", current->word);
    }
    return 0;
}
  • 前章までに実装した機能では、関数の最後に必ず1つのret命令を出力していました。この章で説明した方法でreturn文を実装すると、return文ごとにさらに余分なret命令が出力されることになります。それらの命令はまとめることも可能ですが、ここでは簡単に実装するために、ret命令を複数出力してもかまわないということにしました。こういう細かいところは今の時点では気にしても仕方がないので、実装のシンプルさを優先することが大切です。難しいコードを書けるというのは役立つスキルですが、そもそもコードを難しくしすぎないというのは、時にはさらに役立つスキルなのです。

    • 無駄が気になることは多い。つい、もっと賢い処理にしようとしてしまうこともあるが、その努力に見合わない結果になるどころか、複雑な書き方になりやすいってことだろうか?

Undefined Behavior(未定義の動作)