gcc
のような高度なコンパイラは、コードが記述されている言語(C、C++など)に従って、コードを機械可読ファイルにコンパイルします。実際、それらは対応する言語のライブラリと機能に従って各コードの意味を解釈します。私が間違っていたら訂正してください。
静的ファイル(テキストファイルのHello Worldなど)をコンパイルするための非常に基本的なコンパイラ(おそらくCで)を記述して、コンパイラをよりよく理解したいと思います。私はいくつかのチュートリアルと本を試しましたが、それらはすべて実用的なケースのためのものです。それらは、対応する言語に関連付けられた意味を持つ動的コードのコンパイルを扱います。
静的テキストを機械可読ファイルに変換する基本的なコンパイラーをどのように作成できますか?
次のステップは、コンパイラーに変数を導入することです。言語の一部の関数のみをコンパイルするコンパイラを書きたいと想像してください。
実用的なチュートリアルとリソースの導入は高く評価されています:-)
典型的なコンパイラーは、以下のステップを実行します。
最新のコンパイラ(gccやclangなど)は、最後の2つのステップをもう一度繰り返します。彼らは、初期のコード生成に中間の低レベルだがプラットフォームに依存しない言語を使用します。次に、その言語がプラットフォーム固有のコード(x86、ARMなど)に変換され、プラットフォームに最適化された方法でほぼ同じことが行われます。これには、可能な場合はベクトル命令を使用し、分岐予測効率を高めるために命令を並べ替えるなど。
その後、オブジェクトコードをリンクする準備が整います。ほとんどのネイティブコードコンパイラは、リンカーを呼び出して実行可能ファイルを生成する方法を知っていますが、それ自体はコンパイル手順ではありません。 JavaおよびC#リンクのような言語では、ロード時にVMによって実行されるため、C#リンクは完全に動的になる可能性があります。
この古典的なシーケンスはすべてのソフトウェア開発に適用されますが、繰り返しが必要です。
シーケンスの最初のステップに集中します。機能する可能性のある最も単純なものを作成します。
AhoとUllmanによる Dragon Book を読んでください。これは古典的であり、今日でもまだかなり当てはまります。
Modern Compiler Design も称賛されています。
今のところ、これが難しすぎる場合は、まず構文解析の概要を読んでください。通常、ライブラリの解析にはイントロと例が含まれます。
グラフ、特にツリーの操作に慣れていることを確認してください。これらは、プログラムが論理レベルで構成されているものです。
希望する表記を使用しますが、言語の完全で一貫した説明があることを確認してください。これには、構文とセマンティクスの両方が含まれます。
将来のコンパイラーのテストケースとして、新しい言語でコードのスニペットを書くときがきました。
コンパイラをPythonまたはRubyまたは簡単な言語で記述します。よく理解している単純なアルゴリズムを使用してください。最初のバージョンには高速、または効率的、または機能的に完全なものである必要があります。
必要に応じて、コンパイラのさまざまな段階をさまざまな言語で作成することもできます。
言語全体がテストケースでカバーされている必要があります。事実上、それは定義されます。お好みのテストフレームワークをよく理解してください。初日からテストを作成します。誤ったコードの検出ではなく、正しいコードを受け入れる「ポジティブ」テストに集中してください。
すべてのテストを定期的に実行します。続行する前に壊れたテストを修正します。有効なコードを受け入れることができない不適切に定義された言語で終わるのは残念です。
パーサージェネレーターはたくさんあります 。好きなものを選んでください。独自のパーサーを最初から作成することもできますが、それは、言語の構文がdead単純である場合にのみ価値があります。
パーサーは構文エラーを検出して報告する必要があります。ポジティブとネガティブの両方の多くのテストケースを記述します。言語を定義しながら、記述したコードを再利用します。
パーサーの出力は、抽象構文ツリーです。
言語にモジュールがある場合、パーサーの出力は、生成した「オブジェクトコード」の最も単純な表現になる可能性があります。ツリーをファイルにダンプし、すばやくロードする簡単な方法はたくさんあります。
おそらくあなたの言語では、特定のコンテキストでは意味をなさない可能性がある構文的に正しい構文を許可しています。例としては、同じ変数の重複した宣言、または間違った型のパラメーターを渡した場合があります。バリデーターは、ツリーを見てこのようなエラーを検出します。
バリデーターは、言語で記述された他のモジュールへの参照を解決し、これらの他のモジュールをロードして、検証プロセスで使用します。たとえば、この手順では、別のモジュールから関数に渡されるパラメーターの数が正しいことを確認します。
繰り返しますが、たくさんのテストケースを書いて実行してください。些細なケースは、スマートで複雑なのと同じくらいトラブルシューティングに不可欠です。
あなたが知っている最も簡単なテクニックを使用してください。 HTMLテンプレートとは異なり、言語構成体(if
ステートメントなど)を、パラメータが少ないコードテンプレートに直接変換することはよくあります。
繰り返しになりますが、効率を無視して正確さに集中してください。
ハードウェア固有の詳細に強く関心がない限り、低レベルのものは無視すると思います。これらの詳細は悲惨で複雑です。
あなたのオプション:
最適化は難しいです。ほとんどの場合、最適化は時期尚早です。非効率的で正しいコードを生成します。結果のコードを最適化する前に、言語全体を実装してください。
もちろん、ささいな最適化を導入してもかまいません。ただし、コンパイラーが安定する前に、狡猾で毛深いものは避けてください。
このすべてがあなたにとってあまりにも怖がらない場合は、続行してください!単純な言語の場合、各ステップは思ったよりも簡単かもしれません。
コンパイラーが作成したプログラムから「Hello world」を見るのは、努力する価値があるかもしれません。
Jack Crenshawの Let's Build a Compiler は未完成ですが、非常に読みやすい導入とチュートリアルです。
Nicklaus Wirthの コンパイラ構築 は、単純なコンパイラ構築の基本に関する非常に優れた教科書です。彼はトップダウンの再帰的降下に焦点を当てていますが、それに直面すると、Lex/yaccやflex/bisonよりもはるかに簡単です。彼のグループが書いたオリジナルのPascalコンパイラはこの方法で作成されました。
他の人々はさまざまなドラゴンの本に言及しました。
Brainfuck のコンパイラを作成することから始めます。プログラムするのはかなり鈍い言語ですが、実装する命令は8つしかありません。それはあなたが得ることができるほど簡単であり、構文がおかしいとわかった場合、関係するコマンドのための同等のC命令があります。
本当に機械で読み取り可能なコードのみを記述し、仮想マシンをターゲットにしない場合は、Intelのマニュアルを読んで理解する必要があります。
a。実行可能コードのリンクとロード
b。 COFFおよびPEフォーマット(Windows用)、またはELFフォーマット(Linux用)を理解
言うよりもはるかに難しい。開始点としてC++のコンパイラとインタープリタを読むことをお勧めします(Ronald Mak著)。あるいは、Crenshawによる「コンパイラーをビルドしてみましょう」でもかまいません。
そうしたくない場合は、独自のVMを作成し、そのVMをターゲットとするコードジェネレーターを作成することもできます。
ヒント:FlexとBisonを最初に学びます。次に、独自のコンパイラ/ VMを構築します。
幸運を!
単純なコンパイラのDIYアプローチは次のようになります(少なくとも、私のuniプロジェクトはこのようになりました)。
各ステップを詳細に説明している文献はたくさんあるはずです。