web-dev-qa-db-ja.com

非常に基本的なコンパイラの書き方

gccのような高度なコンパイラは、コードが記述されている言語(C、C++など)に従って、コードを機械可読ファイルにコンパイルします。実際、それらは対応する言語のライブラリと機能に従って各コードの意味を解釈します。私が間違っていたら訂正してください。

静的ファイル(テキストファイルのHello Worldなど)をコンパイルするための非常に基本的なコンパイラ(おそらくCで)を記述して、コンパイラをよりよく理解したいと思います。私はいくつかのチュートリアルと本を試しましたが、それらはすべて実用的なケースのためのものです。それらは、対応する言語に関連付けられた意味を持つ動的コードのコンパイルを扱います。

静的テキストを機械可読ファイルに変換する基本的なコンパイラーをどのように作成できますか?

次のステップは、コンパイラーに変数を導入することです。言語の一部の関数のみをコンパイルするコンパイラを書きたいと想像してください。

実用的なチュートリアルとリソースの導入は高く評価されています:-)

229
Googlebot

はじめに

典型的なコンパイラーは、以下のステップを実行します。

  • 解析:ソーステキストは抽象構文木(AST)に変換されます。
  • 他のモジュールへの参照の解決(Cはリンクまでこのステップを延期します)。
  • 意味の検証:意味をなさない構文的に正しいステートメントを取り除きます。到達できないコードまたは重複した宣言。
  • 同等の変換と高レベルの最適化:ASTは、同じセマンティクスでより効率的な計算を表すように変換されます。これには、一般的な部分式と定数式の初期計算などが含まれ、過剰なローカル割り当てを排除します(また [〜#〜] ssa [〜#〜] )など.
  • コード生成:ASTは、ジャンプ、レジスタ割り当てなどを使用して、線形低レベルコードに変換されます。一部の関数呼び出しはこの段階でインライン化でき、一部のループは展開されます。
  • のぞき穴の最適化:低レベルのコードがスキャンされ、単純なローカルの非効率性が排除されます。

最新のコンパイラ(gccやclangなど)は、最後の2つのステップをもう一度繰り返します。彼らは、初期のコード生成に中間の低レベルだがプラットフォームに依存しない言語を使用します。次に、その言語がプラットフォーム固有のコード(x86、ARMなど)に変換され、プラットフォームに最適化された方法でほぼ同じことが行われます。これには、可能な場合はベクトル命令を使用し、分岐予測効率を高めるために命令を並べ替えるなど。

その後、オブジェクトコードをリンクする準備が整います。ほとんどのネイティブコードコンパイラは、リンカーを呼び出して実行可能ファイルを生成する方法を知っていますが、それ自体はコンパイル手順ではありません。 JavaおよびC#リンクのような言語では、ロード時にVMによって実行されるため、C#リンクは完全に動的になる可能性があります。

基本を覚える

  • 機能させる
  • 美しくする
  • 効率的にする

この古典的なシーケンスはすべてのソフトウェア開発に適用されますが、繰り返しが必要です。

シーケンスの最初のステップに集中します。機能する可能性のある最も単純なものを作成します。

本を読んでください!

AhoとUllmanによる Dragon Book を読んでください。これは古典的であり、今日でもまだかなり当てはまります。

Modern Compiler Design も称賛されています。

今のところ、これが難しすぎる場合は、まず構文解析の概要を読んでください。通常、ライブラリの解析にはイントロと例が含まれます。

グラフ、特にツリーの操作に慣れていることを確認してください。これらは、プログラムが論理レベルで構成されているものです。

言語を明確に定義する

希望する表記を使用しますが、言語の完全で一貫した説明があることを確認してください。これには、構文とセマンティクスの両方が含まれます。

将来のコンパイラーのテストケースとして、新しい言語でコードのスニペットを書くときがきました。

好きな言語を使う

コンパイラをPythonまたはRubyまたは簡単な言語で記述します。よく理解している単純なアルゴリズムを使用してください。最初のバージョンには高速、または効率的、または機能的に完全なものである必要があります。

必要に応じて、コンパイラのさまざまな段階をさまざまな言語で作成することもできます。

たくさんのテストを書く準備をする

言語全体がテストケースでカバーされている必要があります。事実上、それは定義されます。お好みのテストフレームワークをよく理解してください。初日からテストを作成します。誤ったコードの検出ではなく、正しいコードを受け入れる「ポジティブ」テストに集中してください。

すべてのテストを定期的に実行します。続行する前に壊れたテストを修正します。有効なコードを受け入れることができない不適切に定義された言語で終わるのは残念です。

優れたパーサーを作成する

パーサージェネレーターはたくさんあります 。好きなものを選んでください。独自のパーサーを最初から作成することもできますが、それは、言語の構文がdead単純である場合にのみ価値があります。

パーサーは構文エラーを検出して報告する必要があります。ポジティブとネガティブの両方の多くのテストケースを記述します。言語を定義しながら、記述したコードを再利用します。

パーサーの出力は、抽象構文ツリーです。

言語にモジュールがある場合、パーサーの出力は、生成した「オブジェクトコード」の最も単純な表現になる可能性があります。ツリーをファイルにダンプし、すばやくロードする簡単な方法はたくさんあります。

セマンティックバリデーターを作成する

おそらくあなたの言語では、特定のコンテキストでは意味をなさない可能性がある構文的に正しい構文を許可しています。例としては、同じ変数の重複した宣言、または間違った型のパラメーターを渡した場合があります。バリデーターは、ツリーを見てこのようなエラーを検出します。

バリデーターは、言語で記述された他のモジュールへの参照を解決し、これらの他のモジュールをロードして、検証プロセスで使用します。たとえば、この手順では、別のモジュールから関数に渡されるパラメーターの数が正しいことを確認します。

繰り返しますが、たくさんのテストケースを書いて実行してください。些細なケースは、スマートで複雑なのと同じくらいトラブルシューティングに不可欠です。

コードを生成する

あなたが知っている最も簡単なテクニックを使用してください。 HTMLテンプレートとは異なり、言語構成体(ifステートメントなど)を、パラメータが少ないコードテンプレートに直接変換することはよくあります。

繰り返しになりますが、効率を無視して正確さに集中してください。

プラットフォームに依存しない低レベルVMをターゲットにする

ハードウェア固有の詳細に強く関心がない限り、低レベルのものは無視すると思います。これらの詳細は悲惨で複雑です。

あなたのオプション:

  • LLVM:効率的なマシンコード生成を可能にします。通常はx86とARMの場合です。
  • CLR:.NETをターゲットとし、主にx86/Windowsベース。良いJITがあります。
  • JVM:ターゲットJavaワールド、かなりマルチプラットフォームで、良いJITがあります。

最適化を無視

最適化は難しいです。ほとんどの場合、最適化は時期尚早です。非効率的で正しいコードを生成します。結果のコードを最適化する前に、言語全体を実装してください。

もちろん、ささいな最適化を導入してもかまいません。ただし、コンパイラーが安定する前に、狡猾で毛深いものは避けてください。

だから何?

このすべてがあなたにとってあまりにも怖がらない場合は、続行してください!単純な言語の場合、各ステップは思ったよりも簡単かもしれません。

コンパイラーが作成したプログラムから「Hello world」を見るのは、努力する価値があるかもしれません。

335
9000

Jack Crenshawの Let's Build a Compiler は未完成ですが、非常に読みやすい導入とチュートリアルです。

Nicklaus Wirthの コンパイラ構築 は、単純なコンパイラ構築の基本に関する非常に優れた教科書です。彼はトップダウンの再帰的降下に焦点を当てていますが、それに直面すると、Lex/yaccやflex/bisonよりもはるかに簡単です。彼のグループが書いたオリジナルのPascalコンパイラはこの方法で作成されました。

他の人々はさまざまなドラゴンの本に言及しました。

29
John R. Strohm

Brainfuck のコンパイラを作成することから始めます。プログラムするのはかなり鈍い言語ですが、実装する命令は8つしかありません。それはあなたが得ることができるほど簡単であり、構文がおかしいとわかった場合、関係するコマンドのための同等のC命令があります。

15
World Engineer

本当に機械で読み取り可能なコードのみを記述し、仮想マシンをターゲットにしない場合は、Intelのマニュアルを読んで理解する必要があります。

  • a。実行可能コードのリンクとロード

  • b。 COFFおよびPEフォーマット(Windows用)、またはELFフォーマット(Linux用)を理解

  • c。 .COMファイル形式を理解する(PEよりも簡単)
  • d。アセンブラを理解する
  • e。コンパイラーとコンパイラーのコード生成エンジンを理解する。

言うよりもはるかに難しい。開始点としてC++のコンパイラとインタープリタを読むことをお勧めします(Ronald Mak著)。あるいは、Crenshawによる「コンパイラーをビルドしてみましょう」でもかまいません。

そうしたくない場合は、独自のVMを作成し、そのVMをターゲットとするコードジェネレーターを作成することもできます。

ヒント:FlexとBisonを最初に学びます。次に、独自のコンパイラ/ VMを構築します。

幸運を!

12
Aniket Inge

単純なコンパイラのDIYアプローチは次のようになります(少なくとも、私のuniプロジェクトはこのようになりました)。

  1. 言語の文法を定義します。コンテキストフリー。
  2. 文法がまだLL(1)でない場合は、ここで実行してください。単純なCF文法で問題ないように見えたいくつかのルールは、醜いものになるかもしれないことに注意してください。おそらくあなたの言語は複雑すぎる...
  3. テキストのストリームをトークン(単語、数字、リテラル)にカットするレクサーを記述します。
  4. 入力を受け入れるか拒否する文法のトップダウンの再帰降下パーサーを記述します。
  5. 構文木生成をパーサーに追加します。
  6. 構文ツリーからマシンコードジェネレーターを記述します。
  7. Profit&Beerの代わりに、よりスマートなパーサーを実行する方法や、より優れたコードを生成する方法を考え始めることができます。

各ステップを詳細に説明している文献はたくさんあるはずです。

10
MaR