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


214

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

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

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

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

実用的なチュートリアルとリソースをご紹介いただければ幸いです:-)



lex / flexとyacc / bisonを試しましたか?
ムーヴィシエル

15
@mouviciel:それはコンパイラの構築について学ぶ良い方法ではありません。これらのツールはかなりの労力を費やしますので、実際にそれを行うことはありません。
メイソンウィーラー

11
@Mat興味深いことに、最初のリンクは404を返しますが、2番目のリンクはこの質問の複製としてマークされています。
ルスラン

回答:


326

イントロ

典型的なコンパイラは次の手順を実行します。

  • 解析:ソーステキストは抽象構文ツリー(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」を見るのは、努力する価値があるかもしれません。


45
これは私が今まで見た中で最高の答えの一つです。
ガフア

11
質問の一部を見逃したと思います... OPは非常に基本的なコンパイラを作成したかったのです。ここでは非常に基本的なことを超えていると思います。
マルコ・fiset

22
@ marco-fisetは、逆に、OPに非常に基本的なコンパイラーの実行方法を伝えると同時に、より高度なフェーズを回避および定義するためのトラップを指摘する優れた答えだと思います。
SMCI

6
これは、Stack Exchangeの世界全体で私が見た中で最高の答えの1つです。称賛!
アンドレテラ

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

27

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

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

他の人々は、さまざまなドラゴンの本に言及しています。


1
Pascalの素晴らしい点の1つは、使用する前にすべてを定義または宣言する必要があることです。したがって、1回のパスでコンパイルできます。Turbo Pascal 3.0はそのような例の1つであり、内部に関する多くのドキュメントがここにあります
tcrosley

1
PASCALは、ワンパスコンパイルとリンクを念頭に置いて特別に設計されました。Wirthのコンパイラー・ブックはマルチパス・コンパイラーに言及しており、70(はい、70)パスかかったPL / Iコンパイラーを知っていたと付け加えています。
ジョンR.ストローム

使用前の必須宣言はALGOLに遡ります。トニー・ホアは、FORTRANが持っていたのと同様に、デフォルトのタイプルールを追加することを提案しようとしたときに、ALGOL委員会によって耳を固定されました。彼らは、これが引き起こす可能性のある問題をすでに知っていました。名前の誤植やデフォルトのルールが興味深いバグを生み出しています。
ジョンR.ストローム

1
ここでは、本のより多くの更新および完成バージョンは自身がオリジナルの著者である: stack.nl/~marcov/compiler.pdfは あなたの答えを編集して、この:)を追加してください
ソネット

16

私は実際にBrainfuckのコンパイラーを書くことから始めます。プログラムするのはかなり鈍い言語ですが、実装するのは8つの命令しかありません。できる限り簡単で、構文に問題がある場合は、関連するコマンドに対応するCの命令があります。


7
しかし、その後、BFコンパイラの準備ができたら、その中にコードを記述する必要があります。(
内部サーバーエラー

@ 500-InternalServerErrorは、Cサブセットメソッドを使用します
ワールドエンジニア

12

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

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

  • b。COFFおよびPE形式(Windows用)、またはELF形式(Linux用)を理解する

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

言ったよりもずっと難しい。出発点としてC ++のコンパイラと通訳を読むことをお勧めします(Ronald Mak著)。または、Crenshawによる「コンパイラをビルドしましょう」でも構いません。

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

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

幸運を!


7
実際のマシンコードではなくLLVMをターゲットにすることは、今日利用可能な最善の方法についてだと思います。
9000

LLVMをしばらく追跡してきたので、それをターゲットにするのに必要なプログラマーの努力という点で、長年見た中で最高のものの1つだったと言えるでしょう。
アニケットインゲ

2
MIPSについてはどうですか、それを実行するためにスピムを使用しますか?またはミックス

@MichaelT MIPSを使用したことはありませんが、それは良いと確信しています。
アニケットインゲ

@PrototypeStark RISC命令セット、現在もまだ使用されている実世界のプロセッサ(組み込みシステムに変換できることを理解しています)。完全な命令セットはウィキペディアにあります。ネットを見ると多くの例があり、多くのアカデミッククラスで機械語プログラミングのターゲットとして使用されています。SOで少し活動しています。

10

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

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

各ステップを詳細に説明する多くの文献があるはずです。


7番目のポイントは、OPが求めていることです。
フローリアンマーゲイン

7
1-5は無関係であり、そのような細心の注意に値しません。6が最も興味深い部分です。残念ながら、悪名高いドラゴンの本に続いて、ほとんどの本は同じパターンに従っており、コード変換の解析に余りにも注意を払い、コード変換を範囲外にしています。
SKロジック
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.