イントロ
典型的なコンパイラは次の手順を実行します。
- 解析:ソーステキストは抽象構文ツリー(AST)に変換されます。
- 他のモジュールへの参照の解決(Cはこのステップをリンクまで延期します)。
- セマンティック検証:到達不能なコードや重複した宣言など、意味をなさない構文的に正しいステートメントを取り除く。
- 同等の変換と高度な最適化:ASTは、同じセマンティクスでより効率的な計算を表すように変換されます。これには、たとえば、共通の部分式と定数式の早期計算、過剰なローカル割り当ての排除(SSAも参照)などが含まれます。
- コード生成:ASTは、ジャンプ、レジスタ割り当てなどを使用して、線形の低レベルコードに変換されます。この段階でいくつかの関数呼び出しをインライン化したり、いくつかのループを展開したりできます。
- のぞき穴の最適化:低レベルのコードがスキャンされ、単純なローカルの非効率性が排除されます。
最新のコンパイラ(gccやclangなど)は、最後の2つのステップをもう一度繰り返します。初期のコード生成には、低レベルではあるがプラットフォームに依存しない中間言語が使用されます。次に、その言語はプラットフォーム固有のコード(x86、ARMなど)に変換され、プラットフォームに最適化された方法でほぼ同じことを行います。これには、可能な場合のベクトル命令の使用、分岐予測の効率を高めるための命令の並べ替えなどが含まれます。
その後、オブジェクトコードをリンクする準備ができました。ほとんどのネイティブコードコンパイラは、リンカを呼び出して実行可能ファイルを生成する方法を知っていますが、それ自体はコンパイル手順ではありません。JavaやC#などの言語では、リンクは完全に動的であり、ロード時にVMによって行われます。
基本を覚えておいてください
この古典的なシーケンスは、すべてのソフトウェア開発に適用されますが、繰り返しが発生します。
シーケンスの最初のステップに集中してください。おそらく動作する可能性のある最も単純なものを作成します。
本を読んでください!
アホとウルマンのドラゴンブックを読んでください。これは古典的であり、今日でも非常に適切です。
現代のコンパイラ設計も賞賛されています。
このようなものが今あなたにとってあまりにも難しい場合は、最初に解析に関するイントロを読んでください。通常、解析ライブラリにはイントロと例が含まれています。
グラフ、特にツリーを快適に操作できることを確認してください。これらは、プログラムが論理レベルで作成されているものです。
言語を適切に定義する
必要な表記を使用しますが、言語の完全で一貫した説明を用意してください。これには、構文とセマンティクスの両方が含まれます。
将来のコンパイラのテストケースとして、新しい言語でコードのスニペットを書く時が来ました。
お気に入りの言語を使用する
PythonやRuby、またはあなたにとって使いやすい言語でコンパイラを書くことはまったく問題ありません。よく理解しているシンプルなアルゴリズムを使用してください。最初のバージョンは、高速、効率的、または機能完全である必要はありません。それは、十分に正しく、修正が簡単である必要があるだけです。
必要に応じて、異なる言語でコンパイラのさまざまな段階を記述してもかまいません。
多くのテストを書く準備をする
言語全体をテストケースでカバーする必要があります。事実上、それらはそれらによって定義されます。好みのテストフレームワークに精通してください。初日からテストを書きます。誤ったコードの検出とは対照的に、正しいコードを受け入れる「ポジティブ」テストに集中します。
すべてのテストを定期的に実行します。続行する前に壊れたテストを修正します。有効なコードを受け入れられない不明確な言語になってしまうのは残念です。
優れたパーサーを作成する
パーサージェネレーターは多数あります。好きなものを選んでください。独自のパーサーを最初から作成することもできますが、言語の構文が非常に単純な場合にのみ価値があります。
パーサーは構文エラーを検出して報告する必要があります。正と負の両方の多くのテストケースを作成します。言語の定義中に作成したコードを再利用します。
パーサーの出力は、抽象構文ツリーです。
言語にモジュールがある場合、パーサーの出力は、生成する「オブジェクトコード」の最も単純な表現になる場合があります。ツリーをファイルにダンプし、それをすばやくロードする簡単な方法はたくさんあります。
セマンティックバリデーターを作成する
ほとんどの場合、特定のコンテキストでは意味をなさない構文的に正しい構文が言語で許可されています。例は、同じ変数の重複した宣言または間違った型のパラメーターを渡すことです。バリデーターは、ツリーを見てそのようなエラーを検出します。
バリデーターは、言語で記述された他のモジュールへの参照も解決し、これらの他のモジュールをロードして、検証プロセスで使用します。たとえば、このステップでは、別のモジュールから関数に渡されるパラメーターの数が正しいことを確認します。
繰り返しますが、多くのテストケースを作成して実行します。些細なケースは、トラブルシューティングにおいてスマートで複雑なものとして不可欠です。
コードを生成する
知っている最も簡単なテクニックを使用してください。多くの場合if
、HTMLテンプレートとは異なり、言語構造(ステートメントなど)を簡単にパラメーター化されたコードテンプレートに直接変換してもかまいません。
繰り返しますが、効率を無視し、正確さに集中してください。
プラットフォームに依存しない低レベルVMを対象とする
ハードウェア固有の詳細に興味があるのでない限り、低レベルのものは無視すると思います。これらの詳細は面倒で複雑です。
あなたのオプション:
- LLVM:通常x86およびARM向けの効率的なマシンコード生成を可能にします。
- CLR:ほとんどがx86 / Windowsベースの.NETをターゲットとしています。JITが優れています。
- JVM:かなりマルチプラットフォームで、優れたJITを持つJavaの世界をターゲットとしています。
最適化を無視
最適化は難しいです。ほとんどの場合、最適化は時期尚早です。効率的ではないが正しいコードを生成します。結果のコードを最適化する前に、言語全体を実装します。
もちろん、簡単な最適化を導入しても構いません。ただし、コンパイラが安定する前に、cな毛深いものは避けてください。
だから何?
これらすべてがあなたにとってあまりにも恐ろしくない場合は、続行してください!単純な言語の場合、各ステップは思っているよりも簡単かもしれません。
コンパイラが作成したプログラムから「Hello world」を見るのは、努力する価値があるかもしれません。