この教科書の目的は、段階的な作成の説明である言語を徐々に紹介することです。 これにより、言語設計とLLVMの使用に関するかなり広範な問題をカバーし、大量の不必要な詳細なしにコードを同時に表示および説明することができます。
このチュートリアルは、いくつかのコンパイル手法とLLVMの詳細を教えているが、最新のソフトウェア開発の原則は教えていないことをあらかじめ知っておくと便利です。 実際には、これは、プレゼンテーションを簡素化するために最も単純化および縮小化されたソリューションを使用することを意味します。 したがって、たとえば、どこでもメモリを操作するコードは、 Visitorなどの優れたデザインパターンを使用せずにグローバル変数を使用します。これは最良の方法ではありませんが、非常にシンプルで明確です。 コードを理解し、将来のプロジェクトの基礎として使用する場合、そのような欠点を修正することは問題ではありません。
このチュートリアルは、すでに知っている部分や興味のない部分を簡単にスキップできるように作成しようとしました。 教科書の構造は次のとおりです。
- 第1章 :Kaleidoscope言語の紹介とその語彙解析の定義 -どの方向に進んでいるか、および開発する必要がある基本的な機能を示します。 このチュートリアルを理解しやすくし、例をより現実に近づけるために、パーサージェネレーターを使用せずに純粋なC ++のみですべてを実装することにしました。 もちろん、LLVMはこのようなツールでうまく機能するため、いずれかを簡単に使用できます。
- 第2章 :パーサーとASTの実装 -既製の字句アナライザーを使用すると、解析メソッドとASTの構築の基本について既に説明できます。 このガイドでは、再帰降下分析と解析演算子の優先順位について説明します。 第1章または第2章の何もLLVMに適用されません;これらの章のコードはLLVMにもバインドしません。 :)
- 第3章 :LLVM IRコード生成 -ASTの準備ができたら、LLVM IRを実際に生成することがどれほど簡単かを示すことができます。
- 第4章 :JITおよびオプティマイザーサポートの追加 -多くの人がLLVMをJITとして使用することに関心があるため、これに飛び込み、JITサポートを追加するための3つの手順を示します。 LLVMは他の多くの方法でも役立ちますが、あなたの強さを示す最も簡単で最も「性的な」方法の1つです。 :)
- 第5章 :言語の拡張:制御フロー -プログラミング言語が機能するようになったので、制御構造(if / then / elseおよび "for"ループ)で拡張できます。 これにより、SSAの簡単な構築とフロー制御について話す機会が与えられます。
- 第6章 :言語拡張機能:ユーザー定義演算子は、言語拡張機能に関する簡単ですが興味深い章です。これにより、ユーザーは(割り当て可能な優先度で)独自の単項演算子と二項演算子を定義できます。 これにより、ルーチンのライブラリとして「プログラミング言語」のかなりの部分を構築できます。
- 第7章 :言語の拡張:変数変数 -この章では、代入演算子を使用したカスタムローカル変数の追加について説明します。 ここで最も興味深いのは、SSAフォームを構築するのがどれほど簡単で簡単なのかということです。そして、LLVM はフロントエンドでSSAフォームを使用する必要はありません 。
- 第8章 :結論とその他のLLVMの利点 -この章では、言語を拡張する可能な方法について説明することでシリーズを締めくくります。また、ガベージコレクションのサポート、例外、デバッグ、スパゲッティのサポートなど、さまざまなトピックへのリンクの束も含みます-stack”(“スパゲッティスタック”)、およびその他の多くのヒントとコツ。
そしてもう1つ、私たちはあなたが言語を拡張し、私たちの裁量でそれで遊ぶことを期待しています。 コードを取り、必死にプログラミングを開始し、夢中になります。コンパイラはあなたを怖がらせるべきではありません-言語で遊ぶのはとても楽しいです!
舌の万華鏡
このチュートリアルでは、「 カレイドスコープ 」と呼ばれるおもちゃのプログラミング言語を説明します(名前はギリシャ語の3つの単語「美しい」、「見える」、「見える、見る」に由来します)。 カレイドスコープは、関数の定義、条件の使用、数学などを可能にする手続き型言語です。 調査中に、万華鏡を拡張してif / then / elseの構成、ループ、ユーザーステートメント、シンプルなコンソールインターフェイスを使用したJITコンパイルなどをサポートします。簡単にするために、Kaleidoscopeにはデータタイプが1つだけあります。64ビットの浮動小数点数(Cの「double」など)です。 したがって、すべての値は倍精度の実数であり、言語は型宣言を必要としません。 これにより、言語が非常に美しくなり、構文が簡素化されます。 たとえば、次の簡単な例ではフィボナッチ数を計算します 。
# x- def fib(x) if x < 3 then 1 else fib(x-1)+fib(x-2) # 40- . fib(40)
また、Kaleidoscopeが標準ライブラリ関数を呼び出すことを許可しました(LLVM JITはこれを非常に簡単にします)。 これは、externキーワードを使用して関数を使用する前に宣言できることを意味します(これは相互に再帰的な関数にも役立ちます)。 例:
extern sin(arg); extern cos(arg); extern atan2(arg1 arg2); atan2(sin(.4), cos(42))
より興味深い例は第6章にあります。そこでは、マンデルブロ集合をさまざまな近似度で表示する万華鏡の小さなアプリケーションを作成します。
そして今、この言語の実装に飛び込みましょう!
字句解析器
プログラミング言語の実装に関して、最初に必要なことは、テキストファイルを処理し、その中のコードを認識する機能です。 これを行う従来の方法は、「 字句解析器 」(または「スキャナー」)を使用して入力ストリームを「トークン」に分割することです。 字句解析プログラムによって返される各トークンには、コードのユニットと、場合によってはメタデータ(数値など)が含まれます。 まず、可能性を特定します。 // [0-255], , // enum Token { tok_eof = -1, // ( ) tok_def = -2, tok_extern = -3, // (, ) tok_identifier = -4, tok_number = -5, }; static std::string IdentifierStr; // , tok_identifier ( - ) static double NumVal; // , tok_number ( - )
字句解析ツールによって返される各トークンは、トークン列挙値のいずれか、またはASCII値として返される「+」文字のように「不明」になります。 現在のトークンが識別子の場合、グローバル変数IdentifierStrには識別子名が含まれます。 現在のトークンが数値リテラル(1.0など)の場合、NumValにはその値が含まれます。 簡単にするためにグローバル変数を使用していますが、これは実際の使用に最適な選択とはほど遠いことに注意してください。
実際、字句解析器はgettokと呼ばれる単一の関数によって実装されています。 Gettok関数は、呼び出されると、標準入力ストリームから次のトークンを返します。 彼女の定義は次のように始まります。
/// gettok - . static int gettok() { static int LastChar = ' '; // . while (isspace(LastChar)) LastChar = getchar();
gettokはgetchar()C関数を呼び出して、標準入力の1つから文字を読み取ります。 それらを認識し、LastCharに最後に読み取った文字を保存しますが、処理はしません。 彼女が最初にすべきことは、トークン間のスペースを無視することです。 これは上記のサイクルで達成されます。
次のgettokアクションは、識別子と「def」などの特別なキーワードを認識することです。 Kaleidoscopeは、次の単純なループでこれを行います。
if (isalpha(LastChar)) { // : [a-zA-Z][a-zA-Z0-9]* IdentifierStr = LastChar; while (isalnum((LastChar = getchar()))) IdentifierStr += LastChar; if (IdentifierStr == "def") return tok_def; if (IdentifierStr == "extern") return tok_extern; return tok_identifier; }
このコードは、識別子の解析中にグローバル変数「IdentifierStr」を収集することに注意してください。 さらに、言語キーワードは同じサイクルでチェックされます。 数値の場合、すべてが類似しています:
if (isdigit(LastChar) || LastChar == '.') { // : [0-9.]+ std::string NumStr; do { NumStr += LastChar; LastChar = getchar(); } while (isdigit(LastChar) || LastChar == '.'); NumVal = strtod(NumStr.c_str(), 0); return tok_number; }
これは、入力を処理するための非常に単純なコードです。 数値を読み取るときは、strtod C関数を使用して数値に変換し、NumValに保存します。 ここでは十分なエラーチェックが行われていないことに注意してください。値「1.23.45.67」は、「1.23」に入力したかのように読み取られて処理されます。 このようなエラーをチェックすることでコードを簡単に補完できます:)。 それではコメントに対処しましょう。
if (LastChar == '#') { // . do LastChar = getchar(); while (LastChar != EOF && LastChar != '\n' && LastChar != '\r'); if (LastChar != EOF) return gettok(); }
コメントを定義したら、行末までの文字をスキップして、次のトークンを返します。 次に、入力が上記のケースのいずれかと一致しない場合、これは「+」などの演算子またはファイルの終わりです。 これらはこのコードによって処理されます:
// . if (LastChar == EOF) return tok_eof; // ASCII int ThisChar = LastChar; LastChar = getchar(); return ThisChar; }
同時に、Kaleidoscope言語用の完全な字句アナライザーがあります(字句アナライザーコードの完全なリストは 、チュートリアルの次の章にあります)。 次に、抽象構文ツリーの構築に使用する単純なパーサーを開発します。 その後、レキシカルとパーサーを一緒に使用できます。
UPD。
学習に役立つ便利なリンク:
- LLVMの公式ウェブサイト
- ロシア語および英語版ウィキペディアの記事
- 英語ドキュメントの束
- 内部からのLLVM:仕組み[Habrの記事]
- LLVMレビュー[ハブに関する記事]