C言語のコンパイラを自作に関する今日の日記。

これまでの記事


今日実装したのは次の2つ。

  • 構造体のアライメント
  • for文バグの修正

ちなみに昨日はastをjsonで吐くコード(git反映前に間違えて切り取って飛ばしてしまい頑張って再実装したが、翌日セルフホスト向けにはバグっていることがわかりお蔵入り)、セルフホスト用のシェルスクリプトなどを実装していた。

構造体のアライメント

willani での今日行ったアライメントの実装はこのあたり。実装内容は前回の記事(C言語の構造体メンバのアライメント (x86_64, Linux (System V ABI)))で説明している。

セルフホストをしようとしているがうまくいかないファイルが結構ある。 構造体のアライメントを実装するきっかけは色々なファイルを第一世代コンパイラに流しているときに出てきた問題からである。

トークナイズ結果をファイルに出力する src/tokenize_log.c というファイルを willani でビルドしてリンクすると、コンパイラは動くがトークナイズ結果のうち構造体アクセスしている部分だけ出力がなかった。 ここから構造体のアライメントが必要なことと実装していないことに気づいた。

本当は配列も 16byte 境界でアライメントしなければならなかった気がするが、こちらはまだ未実装。 Union もアライメントしなきゃいけないが、こちらはそもそも Union を未実装。 グローバル変数もアライメントしなきゃいけないが、これもまだ実装していない。

for文バグの修正

今日はこのアライメントと、いわゆるfor文バグ (と私が呼んでいるもの) の修正をやった。

ここ最近セルフホストに向けて、gccでコンパイルしたアセンブリに一部 willani でコンパイラしたアセンブリを混ぜてリンクして第1.5世代コンパイラを作っている。 その中で src/type.c というファイルを willani でビルドしてリンクしたときの話。

バグの詳細は記事最後に記す。 時系列に説明しているので長くなってしまった。

どうもセルフホストに近づいてコンパイラ自身のコードをビルドし始めるとどこで何がバグっているのかわからなくなってくる。

普通のプログラムでは、入力が誤っているか、入力を受け取るコードが誤っているかを考えればいいが、セルフホストを目指すコンパイラではさらに、入力を受けとるコードを生成するコードが誤っている可能性も考慮しなければならない。

例えばあとで記す「バグの詳細」に出てくる構造体は、入力文字列で表記されている構造体と、入力文字列を処理するプログラムのデータ構造としての構造体、そしてそのプログラムを生成するときのデータ構造としての構造体があって、いま考えている構造体はなんなんだ?という気持ちになる。

とにかくバグトラックが大変で、些細なことで1日が溶けた。

今回のsrc/type.cではあまりなかったが、segmentation falut で終了することもよくあるし、デバッグが結構大変。

パース結果のログは、パースが完全に終わってからASTをたどって出力しているので、パース中に死ぬとどんな状況かつかめないことが多々ある。なのでパース中のデバッグを楽にするためのログ出力などは強化していきたい。

大変とは書いたが、ゆっくりだが着実にセルフホストに近づいているし、なにより自分の書いたコードで自分の書いたコードをコンパイルする状況はなんともいえないワクワク感があるので楽しい。

明日以降も楽しみながら進めていきたい。


補足: タイトルについて

今日見つけたバグは、私がfor文バグと呼んでいて、実際for文の実装にバグがあったが、内容はささいな話で、結構実装依存な内容である。

なのでfor文の実装に他の人もハマりがちな落とし穴があるという意味はなく、タイトルはあくまで自分の記憶のためのもの。


バグの詳細

どうもこの第1.5世代コンパイラは、構造体のメンバを読む処理がバグっていて、定義済みのメンバを呼び出すコードを解釈する際に、同名のメンバが定義されていないとしてエラーを出力していた。

構造体を含まないコードは問題なくコンパイルできるので、構造体のメンバを読むあたりが明らかに怪しい。 しかしどうやってバグっているのか全く検討がつかなかった。

まず疑ったのは、入力のコードの構造体を読み込むときに構造体のアライメントが狂っていて読み込みに失敗しているのではないかということ。 実は多分ここでもバグっていて、まずは冒頭のような構造体のアライメントを実装した。 これにより、構造体の情報を willani 内部で保持する構造体 (Member 構造体) の各メンバに、コンパイラ内で正しくアクセスできるようになった。

しかしエラーは消えず、ほかにどこがバグっているのかよくわからない。 (自作コンパイラでは、エラーメッセージは往々にして自分が書いたものが出力されるので、なんともいえない気持ちになる。)

ひとまず関係のある Member 構造体の生成・格納・検索などのコードに片っ端に fprintf を挿入し、変数の値やポインタの指すメモリ番地などをダンプすることとした。

ダンプしたことでエラー発生の直前までうまく値がわたっていることはわかった。 何故か (for 文をつかった) メンバ名の検索だけがうまくいっていない。

色々試したうえで、たまたま for 文を while 文に書き換えると嘘のように正しく動作して解決した。

あとで調べると、for 文の初期化処理がうまくいってなかったようだ。 for 文を表す node は、初期化文を表す node を init メンバとして持っている。 init メンバはふつう1文を表す1つの node で、next メンバに値が入ることはないとして実装されていた。

しかし、init メンバが変数の宣言と初期化を行う文の node で、かつ初期化子が実行時に定まるとき(例えば int a=p;)バグる。 int a=1; のような文は、willani では1つの Node 構造体 (Node.kind = ND_STMT_VAR_INIT) で表される。 この構造体には初期化の値も含まれており、初期化の値が即値(コンパイル時に定まる値(数字か文字列))なら1つの構造体で完結する。 一方初期化の値は実行時に定まる場合もある。 このときは Node 構造体の next メンバで別の Node 構造体を数珠つなぎに持ち、これらが初期化用の式文をそれぞれあらわすようになっている。

src/codegen.c の実装バグで、for 文の init メンバの next メンバが指すノードを出力していなかったことで、構造体のメンバを表す変数のアドレスを初期価値として渡すコードがコンパイル時に含まれていなかったらしい。

わかってしまえば数行で解決できるバグだった。 (コミット)