From a65bb9609284d273f0aa232dbaf69597c87f5a12 Mon Sep 17 00:00:00 2001
From: nsfisis
- C コンパイラと言えば、世界三大自作したいソフトウェアの一角である。 というわけで 『低レイヤを知りたい人のためのCコンパイラ作成入門』 (以下 compilerbook) 片手に作ることにした。
+ C コンパイラと言えば、世界三大自作したいソフトウェアの一角である。というわけで 『低レイヤを知りたい人のためのCコンパイラ作成入門』 (以下 compilerbook) 片手に作ることにした。
実装する機能を適切に絞ってやればゴールデンウィークの間 (2025-05-03 から 2025-05-06) にセルフホストまで持っていけるのではないか?という仮説を立て、ISO 8601 の表記で 4日間を表す “P4D” を冠して P4Dcc と名付けた。
@@ -92,7 +92,7 @@
- ゴールデンウィークの4日間で終わらせたいので、実装する言語機能は最低限に絞ることが必要になる。 今回は次のような設計とした (compilerbook の設計を踏襲しているものは除く)。
+ ゴールデンウィークの4日間で終わらせたいので、実装する言語機能は最低限に絞ることが必要になる。今回は次のような設計とした (compilerbook の設計を踏襲しているものは除く)。
- それは、どのくらいの言語機能があればコンパイラを作るのに十分かをこの時点で見積もるためである。 開発を開始する前にも必要な言語機能にはあたりを付けていたが、実際にプロトタイプを作ってみて、これだけの機能セットがあれば足りるだろうという正確な TODO リストを作りたかった。 実際、このとき作ったチェックリストはこのあともほとんど変わっていない (大きな変化点は、配列型をサポートしないと決めたことくらいか)。
+ それは、どのくらいの言語機能があればコンパイラを作るのに十分かをこの時点で見積もるためである。開発を開始する前にも必要な言語機能にはあたりを付けていたが、実際にプロトタイプを作ってみて、これだけの機能セットがあれば足りるだろうという正確な TODO リストを作りたかった。実際、このとき作ったチェックリストはこのあともほとんど変わっていない (大きな変化点は、配列型をサポートしないと決めたことくらいか)。
このあとは、おおむね compilerbook に従って以下のように機能追加を続けた。
@@ -455,13 +455,13 @@
- このあたりから、セルフホストに向けて逆方向からのアプローチも並行しておこなっている。 セルフホストするためには処理系のソースコードで使っている言語機能をすべて実装する必要があるわけだが、これまでは処理系が扱える機能を拡充していくという方向だった。この逆、つまり処理系のソースコードで使っている機能を減らすことでもセルフホストに近付いていく。
+ このあたりから、セルフホストに向けて逆方向からのアプローチも並行しておこなっている。セルフホストするためには処理系のソースコードで使っている言語機能をすべて実装する必要があるわけだが、これまでは処理系が扱える機能を拡充していくという方向だった。この逆、つまり処理系のソースコードで使っている機能を減らすことでもセルフホストに近付いていく。
- 例えば、このコンパイラは
- これらの作業をおこなうことで、処理系自身のソースコード はじめに
設計
&、*、sizeof あたりの実装が終わるとかなり C 言語らしくなっていき楽しい。
typedef をサポートしていないが、開発中ずっと typedef を使わないというのは面倒だ。 そこで、セルフホストがある程度現実的になるまでは構造体を typedef しておいて、途中のどこかで typedef を手で脱糖する。
+ 例えば、このコンパイラは typedef をサポートしていないが、開発中ずっと typedef を使わないというのは面倒だ。そこで、セルフホストがある程度現実的になるまでは構造体を typedef しておいて、途中のどこかで typedef を手で脱糖する。
main.c をパースしてバイナリを出力することができるようになった。 いわゆる第2世代のコンパイラである。この現時点ではまだ第2世代コンパイラは何もできない (何を与えてもクラッシュする)。
+ これらの作業をおこなうことで、処理系自身のソースコード main.c をパースしてバイナリを出力することができるようになった。いわゆる第2世代のコンパイラである。この現時点ではまだ第2世代コンパイラは何もできない (何を与えてもクラッシュする)。
- ……と考えていたのだが、実際のところデバッグは1時間ほどで終わってしまった。 修正したのは1点のみ。 なんのことはない、2日目終了時点でほとんど完成していたわけだ。 + ……と考えていたのだが、実際のところデバッグは1時間ほどで終わってしまった。修正したのは1点のみ。なんのことはない、2日目終了時点でほとんど完成していたわけだ。
記念すべき (?) 最後のバグはこちら。 @@ -485,13 +485,13 @@ }
- メモリアドレスから参照先の値を得る際、その型によってロードする命令の種類を変える必要があるのだが、その切替をポインタ型でおこなっていた。 正しくは、そのポインタ型が指す型を元にして切り替えなければならない。 + メモリアドレスから参照先の値を得る際、その型によってロードする命令の種類を変える必要があるのだが、その切替をポインタ型でおこなっていた。正しくは、そのポインタ型が指す型を元にして切り替えなければならない。
これを修正すると、第2世代コンパイラが第3世代コンパイラを出力できるようになり、その後も第N世代が第N+1世代を生成できるようになった。
- あとは、第2世代のコンパイラがそれ以降のコンパイラとバイナリレベルで一致するかどうかを確かめればよい。 実際に調べてみると、ほとんどの場所が一致したもののどの世代も 6バイトだけ異なることがわかった。 + あとは、第2世代のコンパイラがそれ以降のコンパイラとバイナリレベルで一致するかどうかを確かめればよい。実際に調べてみると、ほとんどの場所が一致したもののどの世代も 6バイトだけ異なることがわかった。
一体どこが異なるのか。hexdump の差分がこちら。
@@ -509,7 +509,7 @@
00015e10 79 70 65 5f 6e 65 77 00 74 79 70 65 5f 6e 65 77 |ype_new.type_new|
- fatal_error、read_all、tokenize type_new はいずれも main.c で定義された関数の名前である。 このことから考えると、これは GCC が埋め込んだシンボルテーブルである可能性が高い。 わずかに異なっている 6バイトは、ランダム生成された何かのように見える。
+ fatal_error、read_all、tokenize type_new はいずれも main.c で定義された関数の名前である。このことから考えると、これは GCC が埋め込んだシンボルテーブルである可能性が高い。わずかに異なっている 6バイトは、ランダム生成された何かのように見える。
そこで gcc に -s (シンボルテーブルを削除するフラグ) を渡してみると、めでたく2世代目以降のコンパイラのバイナリが完全に一致するようになった。
@@ -525,10 +525,10 @@
最終的な実装は1900行ほど、所要時間は20時間弱となった。
- 正直なところ、思ったより早く終わって拍子抜けしている。 これは compilerbook がうまく実装順を整理しているのと、アセンブリの細かい落とし穴を事前に解説して潰していることが大きいと思われる。 + 正直なところ、思ったより早く終わって拍子抜けしている。これは compilerbook がうまく実装順を整理しているのと、アセンブリの細かい落とし穴を事前に解説して潰していることが大きいと思われる。
- 当初の仮説どおり、サポートする機能を慎重に選ぶことにより短期間でセルフホストまで持っていくことができた。 案外簡単に作れてしまうので、まとまった休みに是非いかがだろうか。 + 当初の仮説どおり、サポートする機能を慎重に選ぶことにより短期間でセルフホストまで持っていくことができた。案外簡単に作れてしまうので、まとまった休みに是非いかがだろうか。
-- cgit v1.2.3-70-g09d2