本章では,LLVMのフロントエンドの実装を説明します.サンプルとしてC言語のサブセットである簡易言語,DummyCを規定してみました.本章ではDummyCのフロントエンドを作成することで,LLVMのフロントエンドの実装方法を確認していきます.実装に使用した言語はC++です.
本章ではフロントエンドの実装を字句解析,構文解析,意味解析,コード生成の順で解説していきますが,実際のところ字句解析〜意味解析まではLLVMとほとんど関係ありませんので,単純にLLVMの命令をどうやって生成するかという点を知りたい場合はコード生成から読んでいただくのが良いと思います*1.
本章での説明では,フロントエンド実装例として,DummyCフロントエンドのソースコードを適宜載せながら解説をおこないます.しかし,ページ数の削減や本文の可読性を考え,本書に記載する部分は筆者が必要だと考える部分に絞っています.全ソースコードを見たいという方につきましては,巻末の執筆者情報に記載した筆者その1のgithubにてDummyCCompilerリポジトリとして公開していますので各自取得をお願いします.なお,今回のフロントエンド実装は“正しいソースコードを正しく処理できること”ことを目標とします.エラー処理を全くしないわけではありませんが,誤ったソースコードを受け取った時の動作については保証しません.
ちなみにLLVMのドキュメント,LLVM Tutorial*2ではKaleidoScopeという簡単な関数型言語をターゲットにしたフロントエンドの実装を解説しています.興味がある方はそちらにも目を通してみるとよいかもしれません.
最初に今回作成するフロントエンドの対象となる言語の構文規則を規定します.C言語のサブセットですので基本的にC言語が読めればすぐ理解できると思います.なお,今回規定したDummyCがC言語と異なる点は以下の通りです.
要約すると,基本的には何も出来ないC言語もどきですが四則演算と関数呼び出しをサポートする,という感じです.また,構文規則に記載されていない制限として,エントリとなる関数名は“main”とし,引数はとらないことにします.
DummyCのEBNFを図5.1に示します*3.
リスト5.1: dummyC-ebnf
1: (* C言語のサブセット *)
2: (* 名前はDummyCとしておく *)
3: (* 機能はかなりミニマル *)
4: (* 一応EBNFで書いているつもり *)
5: (* {}は0回以上の繰り返し *)
6: (* []はオプション *)
7:
8:
9: translation_unit
10: : { external_declaration }
11: ;
12:
13: external_declaration
14: : function_declaration
15: | function_definition
16: ;
17:
18: function_declaration
19: : prototype , ";"
20: ;
21:
22: function_definition
23: : prototype , function_statement
24: ;
25:
26: prototype
27: : "int" , identifier , "(" , [ parameter , { "," , parameter} ] , ")"
28: ;
29:
30: parameter
31: : "int" , identifier
32: ;
33:
34: function_statement
35: : "{" , [ variable_declaration_list ] , statement_list , "}"
36: ;
37:
38: variable_declaration_list
39: : { variable_declaration }
40: ;
41:
42: variable_declaration
43: : "int" , identifier , ";"
44: ;
45:
46: statement_list
47: : { statement }
48: ;
49:
50: statement
51: : expression_statement
52: | jump_statement
53: ;
54:
55: expression_statement
56: : ";"
57: | assignment_expression , ";"
58: ;
59:
60: jump_statement
61: : "return" , assignment_expression , ";"
62: ;
63:
64: assignment_expression
65: : identifier , "=" , additive_expression
66: | additive_expression
67: ;
68:
69: additive_expression
70: : multiplicative_expression , [ { "+" , multiplicative_expression | "-" , multiplicative_expression } ]
71: ;
72:
73: multiplicative_expression
74: : postfix_expression , [ { "*" , postfix_expression | "/" , postfix_expression } ]
75: ;
76:
77: postfix_expression
78: : primary_expression
79: | identifier , "(" , [ assignment_expression , { "," , assignment_expresstion } ] , ")"
80: ;
81:
82: primary_expression
83: : identifier
84: | integer
85: | "(" , additive_expression , ")"
86: ;
87:
88: identifier
89: : character , [ { character | digit } ]
90: ;
91:
92: integer
93: : "0" | [ "-" ] digit_excluding_zero , [ { digit } ]
94: ;
95:
96: character
97: : "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J"
98: | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T"
99: | "U" | "V" | "W" | "X" | "Y" | "Z"
100: | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j"
101: | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
102: | "u" | "v" | "w" | "x" | "y" | "z"
103: ;
104:
105: digit_excluding_zero
106: : "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
107: ;
108:
109: digit
110: : "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
111: ;
なお,EBNFだけでは構文がわかりにくいという部分もあるかと思いますので,実際に構文解析クラスを実装を説明する5.5.3節では構文図を記載しています.DummyCの構文についてわからない部分がある場合は5.5.3節の構文図とEBNFを照らし合わせて確認してみてください.
実装を始める前に,作成するフロントエンドのゴールを決定しておきましょう.本章で作成するDummyCフロントエンドでは,リスト5.2に示すコードをコンパイルし,正常にLLVM IRへと変換できることを目標としたいと思います.
リスト5.2: 目標とするDummyCのサンプルコード
1: int test(int j) {
2: int i;
3: i = j * 10;
4: return i;
5: }
6:
7: int main() {
8: int i;
9: i = 10;
10: test(i);
11: return 0;
12: }
非常に簡単なソースコードですが,初めの第一歩としてはこのぐらいが適切でしょう.
それでは,目標も決まったことですので次節から早速実装を始めます.
それではまず字句解析の実装から開始しましょう.字句解析の役割は,入力ソースコードをプログラムの最小構成単位であるトークンに分割することです.なお,字句解析には特にLLVMは関係してこないので,ここは自由に実装してしまって問題ありません.今回筆者が作成した字句解析の実装方針は下記の通りです.
なお,後述する構文解析ではTokenStreamからトークン情報を取得するものとします.
まずフロントエンドプログラムを構成する各ソースコード内で共通で使用する以下のマクロをAPP.hppというファイルに定義しておきます.内容は簡単で,与えられたポインタの領域をdeleteし,delete後NULLを設定しているだけです.本書で作成するプログラムではメモリ開放時はこのマクロを呼ぶことで二重開放を防ぎます.
リスト5.3: 共通マクロの定義
1: #ifndef _APP_H_
2:
3: #define SAFE_DELETE(x) {delete x;x=NULL;}
4: #define SAFE_DELETEA(x) {delete[] x;x=NULL;}
5:
6: #endif
スマートポインタで管理してしまえば良いという話もあるとは思いますが,とりあえず本書ではこの方法で管理していきます.
まず切り出したトークンを格納するTokenクラスを作成します.Tokenクラスでは後々構文解析で必要となる情報を格納しておく必要があります.今回筆者が作成したTokenクラスは以下の情報と各情報へのsetter/getterを持つことにしました.
ここで,トークンの種類というのはトークンが識別子,予約語,記号,数値の何れに該当するかという情報です.実際のTokenクラスの定義例をリスト5.4に示します
リスト5.4: Token classの定義
1: /**
2: * トークン種別
3: */
4: enum TokenType {
5: TOK_IDENTIFIER, // 識別子
6: TOK_DIGIT, // 数字
7: TOK_SYMBOL, // 記号
8: TOK_INT, // INT
9: TOK_RETURN, // RETURN
10: TOK_EOF // EOF
11: };
12:
13: /**
14: * 個別トークン格納クラス
15: */
16: class Token {
17: private:
18: TokenType Type;
19: std::string TokenString;
20: int Number;
21: int Line;
22:
23: public:
24: Token(std::string string, TokenType type, int line)
25: : TokenString(string), Type(type), Line(line) {
26: if (type == TOK_DIGIT)
27: Number = atoi(string.c_str());
28: else
29: Number = 0x7fffffff;
30: };
31: ~Token() {};
32:
33: // トークンの種別を取得
34: TokenType getTokenType() {
35: return Type;
36: };
37:
38: // トークンの文字列表現を取得
39: std::string getTokenString() {
40: return TokenString;
41: };
42:
43: // トークンの数値を取得(種別が数字である場合に使用)
44: int getNumberValue() {
45: return Number;
46: };
47:
48: // トークンの出現した行数を取得
49: int getLine() { return Line; }
50: };
先ほどのTokenクラスを格納するTokenStreamクラスを作成します.TokenStreamは切り出した全てのTokenの格納と,格納したTokenの読み出しの機能を提供するものです.TokenStreamクラスはTokenを格納するためのクラスなので,基本的にはTokenを保存するリストや配列を用意してsetter/getterを作成すればよいです.しかし構文解析では“○個分前のトークンまで戻る”という操作や,“○番目のトークンまで戻る”という操作を行うためTokenStreamにはこれらの機能を持たせる必要があります.
このことから,まずTokenStreamクラスでは以下のメンバ変数を持つものとします.
表5.1: TokenStreamクラスのメンバ変数
| クラス名 | メンバ変数 | 補足 |
|---|---|---|
| TokenStream | std::vector<Token*> Tokens | Tokenを格納するベクタ |
| int CurIndex | 呼び出し対象のTokenのインデックス |
切出したTokenはTokensベクタに格納しておき,Tokenを取得するメソッドが呼び出された際はCurIndex番目のトークンを返却することにします.また,インデックスをインクリメントするメソッド,および指定した値にインデックスの値を設定するメソッドを用意することで次のTokenの読み出しとインデックスの巻き戻しに対応します.
以上をまとめ,TokenStreamクラスには以下のメソッドを実装します.
5,6,7は構文解析部からのアクセスを容易にするためにTokenクラスのgetterをラップします.また8はデバッグ用です.以上を実装したTokenStreamクラスの宣言と定義をリスト5.5,リスト5.6に示します.
リスト5.5: TokenStream classの宣言
1: /**
2: * 切り出したToken格納用クラス
3: */
4: class TokenStream {
5: private:
6: std::vector<Token *> Tokens;
7: int CurIndex;
8:
9: public:
10: TokenStream() : CurIndex(0) {}
11: ~TokenStream();
12:
13: bool ungetToken(int Times = 1);
14: bool nextToken();
15: bool pushToken(Token *token) {
16: Tokens.push_back(token);
17: return true;
18: }
19: Token getToken();
20:
21: // トークンの種類を取得
22: TokenType getCurType() { return Tokens[CurIndex]->getTokenType(); }
23:
24: // トークンの文字列表現を取得
25: std::string getCurString() { return Tokens[CurIndex]->getTokenString(); }
26:
27: // トークンの数値を取得
28: int getCurNumVal() { return Tokens[CurIndex]->getNumberValue(); }
29:
30: // 現在のインデックスを取得
31: int getCurIndex() { return CurIndex; }
32:
33: // インデックスを指定した値に設定
34: bool applyTokenIndex(int index) {
35: CurIndex = index;
36: return true;
37: }
38:
39: bool printTokens();
40: };
リスト5.6: TokenStream classの定義
1: /**
2: * デストラクタ
3: */
4: TokenStream::~TokenStream() {
5: for (int i = 0; i < Tokens.size(); i++) {
6: SAFE_DELETE(Tokens[i]);
7: }
8: Tokens.clear();
9: }
10:
11: /**
12: * トークン取得
13: * @return CureIndex番目のToken
14: */
15: Token TokenStream::getToken() { return *Tokens[CurIndex]; }
16:
17: /**
18: * インデックスを一つ増やして次のトークンに進める
19: * @return 成功時:true 失敗時:false
20: */
21: bool TokenStream::nextToken() {
22: int size = Tokens.size();
23: if (--size <= CurIndex) {
24: return false;
25: } else {
26: CurIndex++;
27: return true;
28: }
29: }
30:
31: /**
32: * インデックスをtimes回戻す
33: */
34: bool TokenStream::ungetToken(int times) {
35: for (int i = 0; i < times; i++) {
36: if (CurIndex == 0)
37: return false;
38: else
39: CurIndex--;
40: }
41: return true;
42: }
43:
44: /**
45: * 格納されたトークン一覧を表示する
46: */
47: bool TokenStream::printTokens() {
48: std::vector<Token *>::iterator titer = Tokens.begin();
49: while (titer != Tokens.end()) {
50: fprintf(stdout, "%d:", (*titer)->getTokenType());
51: if ((*titer)->getTokenType() != TOK_EOF)
52: fprintf(stdout, "%s\n", (*titer)->getTokenString().c_str());
53: ++titer;
54: }
55: return true;
56: }
ここまででトークン情報を入れるTokenとTokenを格納するためのTokenStreamを用意しました.しかしTokenとTokenStreamはただの箱ですので,字句解析を実行するにはソースコードからトークンを切り出す解析部が必要です.本節ではこのトークンを切り出す字句解析の実装を行います.
トークン切り出し関数の入出力は以下の通りとします.
また,トークンの切り出しは以下の手順で行います.
なお,DummyCの構文規則より,各トークンは識別子,予約語,数値,その他構文中に表れる記号の何れかになるはずです.
実際のコードをリスト5.7に示します.それほど難しいことはしていないので,詳しい説明は省略します.
リスト5.7: lexer
1: /**
2: * トークン切り出し関数
3: * @param 字句解析対象ファイル名
4: * @return 切り出したトークンを格納したTokenStream
5: */
6: TokenStream *LexicalAnalysis(std::string input_filename) {
7: TokenStream *tokens = new TokenStream();
8: std::ifstream ifs;
9: std::string cur_line;
10: std::string token_str;
11: int line_num = 0;
12: bool iscomment = false;
13:
14: ifs.open(input_filename.c_str(), std::ios::in);
15: if (!ifs)
16: return NULL;
17: while (ifs && getline(ifs, cur_line)) {
18: char next_char;
19: std::string line;
20: Token *next_token;
21: int index = 0;
22: int length = cur_line.length();
23:
24: while (index < length) {
25: next_char = cur_line.at(index++);
26:
27: // コメントアウト読み飛ばし
28: if (iscomment) {
29: if (((length - index) >= 1) && (next_char == '*') &&
30: (cur_line.at(index) == '/')) {
31: iscomment = false;
32: index++;
33: }
34: continue;
35: }
36:
37: // EOF
38: if (next_char == EOF) {
39: token_str = EOF;
40: next_token = new Token(token_str, TOK_EOF, line_num);
41:
42: } else if (isspace(next_char)) {
43: continue;
44:
45: // IDENTIFIER
46: } else if (isalpha(next_char)) {
47: while (isalnum(next_char)) {
48: token_str += next_char;
49: if (index == length)
50: break;
51: next_char = cur_line.at(index++);
52: }
53: if (!isalnum(next_char))
54: index--;
55:
56: if (token_str == "int") {
57: next_token = new Token(token_str, TOK_INT, line_num);
58: } else if (token_str == "return") {
59: next_token = new Token(token_str, TOK_RETURN, line_num);
60: } else {
61: next_token = new Token(token_str, TOK_IDENTIFIER, line_num);
62: }
63:
64: // 数字
65: } else if (isdigit(next_char)) {
66: if (next_char == '0') {
67: token_str += next_char;
68: next_token = new Token(token_str, TOK_DIGIT, line_num);
69: } else {
70: while (isdigit(next_char)) {
71: token_str += next_char;
72: if (index == length)
73: break;
74: next_char = cur_line.at(index++);
75: }
76: next_token = new Token(token_str, TOK_DIGIT, line_num);
77: if (!isdigit(next_char))
78: index--;
79: }
80:
81: // コメント or '/'
82: } else if (next_char == '/') {
83: token_str += next_char;
84:
85: // コメントの場合
86: if (((length - index) >= 1) && (cur_line.at(index) == '/')) {
87: break;
88:
89: // コメントの場合
90: } else if (((length - index) >= 1) && (cur_line.at(index) == '*')) {
91: index++;
92: iscomment = true;
93: continue;
94:
95: // DIVIDER('/')
96: } else {
97: next_token = new Token(token_str, TOK_SYMBOL, line_num);
98: }
99:
100: // それ以外(記号)
101: } else {
102: if (next_char == '*' || next_char == '+' || next_char == '-' ||
103: next_char == '=' || next_char == ';' || next_char == ',' ||
104: next_char == '(' || next_char == ')' || next_char == '{' ||
105: next_char == '}') {
106: token_str += next_char;
107: next_token = new Token(token_str, TOK_SYMBOL, line_num);
108:
109: // 解析不能字句
110: } else {
111: fprintf(stderr, "unclear token : %c", next_char);
112: SAFE_DELETE(tokens);
113: return NULL;
114: }
115: }
116:
117: // Tokensに追加
118: tokens->pushToken(next_token);
119: token_str.clear();
120: }
121:
122: token_str.clear();
123: line_num++;
124: }
125:
126: // EOFの確認
127: if (ifs.eof()) {
128: tokens->pushToken(new Token(token_str, TOK_EOF, line_num));
129: }
130:
131: // クローズ
132: ifs.close();
133: return tokens;
134: }
次に構文解析を実装します.ここでは先ほど作成したTokenStreamを読み込んで,Abstract Syntax Tree(以降,AST)を作成します.
構文解析を実装する前にASTの定義を行います.ASTとは抽象構文木のことで,プログラムの意味的に不要な部分を取り除き,木構造にしたものです.例えばASTでは,木の節に演算子が,葉には変数や数値があてはまります.構文解析の目的は切り出したトークンが構文規則に則っているか確認し,ASTを作成することです.ここではDummyCの構文規則に基づいてASTを考えます.
DummyCの構文規則ではtranslation_unitがモジュールを現し,translation_unitはfunction_declaratinとfunction_definitionによって構成されます.更にfunction_definitionの下にはvariable_declarationとstatementが並び,statementにはexpression_statementとjump_statementに分類されます.expression_statementはさらに“;”もしくはassignment_expressionに分解され,assignment_expressionにはadditive_expressionが含まれ・・・と続きます.これらの構文をASTにすることを考え,構成要素をC++のクラスとして定義していきます.なお,今回記載するASTはあくまで筆者の考えや好みで設計している部分も大いにあることをご了承ください.
DummyCでは一つのモジュールには複数の関数が存在し,関数はステートメントの集合として表現されます.ここでいうステートメントとは“;”で終わる一連の処理のことを指します.この段落では,まずステートメントを表すASTをクラスとして定義することを目標とします.ちなみに,DummyCでいうステートメントとは非終端記号のvariable_declarationやexpression_statement,jump_statementを指します.以降本段落では各種ステートメントのASTやそれらASTを構成するASTのクラス定義を解説していきます.
まず最初に方針を決定しておきます.今回のASTクラス定義の方針は下記の通りです.
これは実は後々構文解析の実装を簡単にするための対策だったりするのですが,とりあえずは“プログラムのステートメントを構成する要素を共通化するために基底クラスを用意しておく”,ぐらいに捕らえてもらえると幸いです.ちなみに2のclassofメソッドはLLVMのisa<>テンプレートやdyn_cast<>テンプレートを使用するために必要なメソッドです.isa<>やdyn_cast<>はC++のRTTIの代わりとしてLLVMが用意しているテンプレートで,llvm/Support/Casting.hに宣言されています.これらテンプレートにより,クラスの型判別やキャストが可能になります.LLVMではRTTIの使用を控えisa<> ,dyn_cast<>,cast<>といったLLVMのテンプレートを使用することを推奨しています*4.なお,classofの実装方法については後ほど記載するASTのソースコードにて確認をお願いします.話が少しそれましたが,下記のBaseASTクラスを基底クラスとして宣言します.
リスト5.8: ASTの基底クラス
1: /**
2: * ASTの種類
3: */
4: enum AstID {
5: BaseID
6: };
7:
8: /**
9: * ASTの基底クラス
10: */
11: class BaseAST {
12: AstID ID;
13:
14: public:
15: BaseAST(AstID id) : ID(id) {}
16: virtual ~BaseAST() {}
17: AstID getValueID() const { return ID; }
18: };
ASTの種類を表すAstIDはこの時点ではBaseASTをあらわすBaseIDのみですが,今後各種ASTのクラスを定義するごとに追加されていきます.
それでは,引き続きステートメントのASTについて考えましょう.まずは,ステートメントの構成要素から考えていきます.プログラムの最小構成要素はトークンであり,それは予約語(intとreturn)とidentifier,integerおよび記号です.これは同時にステートメントの最小構成要素とも言えます.ちなみにASTとして考えた時には,“;”や“{”,“}”等の記号は取り除くので記号として残るのは演算子のみです.とりあえず予約語はおいておくとして,演算子はASTの節になるのでidentifierとinteger,つまり識別子と数値が葉ノードになると考えられます.識別子は変数名と関数名の二つの可能性が考えられますが,識別子だけでは関数を表現できないのでここではまず変数を考えます.というわけで,まず変数名情報をもつVariableASTクラス,数値情報をもつNumberASTクラスを定義することにしましょう.VariableASTクラスとNumberASTクラスは以下の情報をメンバ変数として持つことにします.
表5.2: VariableASTとNumberASTのメンバ変数
| クラス名 | メンバ変数 | 補足 |
|---|---|---|
| VariableAST | std::string Name | 変数名 |
| NumberAST | int Val | 数値情報 |
もちろんクラスとして定義する際にはこれらの変数に対するsetter/getter等のメソッドを定義する必要がありますが,ここではとりあえず変数のみを載せています.
変数名と数値,および演算子がそろえば二項演算および関数呼び出しが成立します.DummyCでの二項演算としてはassignment_expression,additive_expression,multiplicative_expressionがあります.これらを別々のクラスとして定義することも出来ると思いますが,あまり意味はなさそうなので全てまとめて一つのクラス,BinaryExprASTで表現することにします.関数呼び出しは構文規則上はpostfix_expressionに記載されていますが,CallExprASTクラスとして個別に定義することにします.BinaryExprASTクラスとCallExprASTに持たせるメンバ変数を表5.3に示します.
表5.3: BinaryExprASTとCallExprASTのメンバ変数
| クラス名 | メンバ変数 | 補足 |
|---|---|---|
| BinaryExprAST | std::string Op | 演算子の文字列表現 |
| BaseAST *LHS | 二項演算の左辺となるAST | |
| BaseAST *RHS | 二項演算の右辺となるAST | |
| CallExprAST | std::string Callee std::vector<BaseAST*> Args | 関数名 関数呼び出しの引数 |
さて,ここで先ほどおいておいた予約語(intとreturn)の登場です.予約語intと変数名でvariable_declaration(変数宣言)が定義でき,また,jump_statement(return文)は予約語returnと戻り値で構成されます.この二つはVariableDeclASTクラス,JumpStmtASTクラスとし,メンバ変数を表5.4に示します.
表5.4: VariableDeclASTとJumpStmtASTのメンバ変数
| クラス名 | メンバ変数 | 補足 |
|---|---|---|
| VariableDeclAST | std::string Name | 変数名 |
| enum DeclType Type | 変数宣言の種類 | |
| JumpStmtAST | BaseAST *Expr | 戻り値となるAST |
表5.4において,VariableDeclASTに変数宣言の種類を示すDeclTypeという列挙型があると思います.これは変数が引数であるか関数内で宣言されたかを区別するために用意しています.どちらもローカル変数なのだから素直に考えると必要は無いのですが,引数の場合は後述するコード生成の際に関数内でalloca命令で領域確保して値を代入という処理をしてやりたいので用意しています.つまりはClangの最適化オプション無しでコード生成した場合と同じようなコードを生成してやりたいのです.また,JumpStmtASTの戻り値はassignment_expressionであり,assignment_expressionは二項演算,変数,数値,関数呼び出しの何れかになるため,戻り値を格納するためのBaseASTをメンバ変数として用意します.
これまで説明した各ASTクラスの宣言を以下に示します.
リスト5.9: 各種ステートメントのAST
1: /**
2: * ASTの種類
3: */
4: enum AstID {
5: BaseID,
6: VariableDeclID,
7: BinaryExprID,
8: CallExprID,
9: JumpStmtID,
10: VariableID,
11: NumberID
12: };
13:
14: /**
15: * ASTの基底クラス
16: */
17: class BaseAST {
18: AstID ID;
19:
20: public:
21: BaseAST(AstID id) : ID(id) {}
22: virtual ~BaseAST() {}
23: AstID getValueID() const { return ID; }
24: };
25:
26: /**
27: * 変数宣言を表すAST
28: */
29: class VariableDeclAST : public BaseAST {
30: public:
31: typedef enum {
32: param,
33: local
34: } DeclType;
35:
36: private:
37: std::string Name;
38: DeclType Type;
39:
40: public:
41: VariableDeclAST(const std::string &name)
42: : BaseAST(VariableDeclID), Name(name) {}
43:
44: // VariableDeclASTなのでtrueを返却
45: static inline bool classof(VariableDeclAST const *) { return true; }
46:
47: // 渡されたBaseASTクラスがVariableDeclASTか判定する
48: static inline bool classof(BaseAST const *base) {
49: return base->getValueID() == VariableDeclID;
50: }
51: ~VariableDeclAST() {}
52:
53: // 変数名を取得する
54: std::string getName() { return Name; }
55:
56: // 変数の宣言種別を設定する
57: bool setDeclType(DeclType type) {
58: Type = type;
59: return true;
60: };
61:
62: // 変数の宣下種別を設定する
63: DeclType getType() { return Type; }
64: };
65:
66: /**
67: * 二項演算を表すAST
68: */
69: class BinaryExprAST : public BaseAST {
70: std::string Op;
71: BaseAST *LHS, *RHS;
72:
73: public:
74: BinaryExprAST(std::string op, BaseAST *lhs, BaseAST *rhs)
75: : BaseAST(BinaryExprID), Op(op), LHS(lhs), RHS(rhs) {}
76: ~BinaryExprAST() {
77: SAFE_DELETE(LHS);
78: SAFE_DELETE(RHS);
79: }
80:
81: // BinaryExprASTなのでtrueを返却
82: static inline bool classof(BinaryExprAST const *) { return true; }
83:
84: // 渡されたBaseASTがBinaryExprASTか判定する
85: static inline bool classof(BaseAST const *base) {
86: return base->getValueID() == BinaryExprID;
87: }
88:
89: // 演算子を取得する
90: std::string getOp() { return Op; }
91:
92: // 左辺値を取得する
93: BaseAST *getLHS() { return LHS; }
94:
95: // 右辺値を取得する
96: BaseAST *getRHS() { return RHS; }
97: };
98:
99: /**
100: * 関数呼び出しを表すAST
101: */
102: class CallExprAST : public BaseAST {
103: std::string Callee;
104: std::vector<BaseAST *> Args;
105:
106: public:
107: CallExprAST(const std::string &callee, std::vector<BaseAST *> &args)
108: : BaseAST(CallExprID), Callee(callee), Args(args) {}
109: ~CallExprAST();
110:
111: // CallExprASTなのでtrueを返却
112: static inline bool classof(CallExprAST const *) { return true; }
113:
114: // 渡されたBaseASTがCallExprASTか判定する
115: static inline bool classof(BaseAST const *base) {
116: return base->getValueID() == CallExprID;
117: }
118:
119: // 呼び出す関数名を取得する
120: std::string getCallee() { return Callee; }
121:
122: // i番目の引数を取得する
123: BaseAST *getArgs(int i) {
124: if (i < Args.size())
125: return Args.at(i);
126: else
127: return NULL;
128: }
129: };
130:
131: /**
132: * ジャンプ(今回はreturn)を表すAST
133: */
134: class JumpStmtAST : public BaseAST {
135: BaseAST *Expr;
136:
137: public:
138: JumpStmtAST(BaseAST *expr) : BaseAST(JumpStmtID), Expr(expr) {}
139: ~JumpStmtAST() { SAFE_DELETE(Expr); }
140:
141: // JumpStmtASTなのでtrueを返却
142: static inline bool classof(JumpStmtAST const *) { return true; }
143:
144: // 渡されたBaseASTがJumpStmtASTか判定する
145: static inline bool classof(BaseAST const *base) {
146: return base->getValueID() == JumpStmtID;
147: }
148:
149: // returnで返却するExpressionを取得する
150: BaseAST *getExpr() { return Expr; }
151: };
152:
153: /**
154: * 変数参照を表すAST
155: */
156: class VariableAST : public BaseAST {
157: // Name
158: std::string Name;
159:
160: public:
161: VariableAST(const std::string &name) : BaseAST(VariableID), Name(name) {}
162: virtual ~VariableAST() {}
163:
164: // VariableASTなのでtrueを返却
165: static inline bool classof(VariableAST const *) { return true; }
166:
167: // 渡されたBaseASTがVariableASTか判定する
168: static inline bool classof(BaseAST const *base) {
169: return base->getValueID() == VariableID;
170: }
171:
172: // 変数名を取得する
173: std::string getName() { return Name; }
174: };
175:
176: /**
177: * 整数を表すAST
178: */
179: class NumberAST : public BaseAST {
180: int Val;
181:
182: public:
183: NumberAST(int val) : BaseAST(NumberID), Val(val) {}
184: ~NumberAST() {}
185:
186: // NumberASTなのでtrueを返却
187: static inline bool classof(NumberAST const *) { return true; }
188:
189: // 渡されたBaseASTがNumberASTか判定する
190: static inline bool classof(BaseAST const *base) {
191: return base->getValueID() == NumberID;
192: }
193:
194: // このASTが表現する数値を取得する
195: int getNumberValue() { return Val; }
196: };
先ほどまでの内容でDummyCのステートメントであるstatementおよびvariable_declarationの内容は定義しました.本段落ではステートメントの集合である関数,および関数によって構成されるモジュールのASTを定義していきます.
statementおよびvariable_declarationがリストになることでstatement_listおよびvariable_declaration_listになります.この二つのリストでfunction_statementが構成されますので,内部にstatementとvariable_declarationのリストをもつクラスとしてFunctionStmtASTクラスを定義します.なお,function_statementはfunction_definition(関数)のボディにあたる部分です.FunctionStmtASTのメンバ変数は表5.5の通りです.
表5.5: FunctionStmtASTのメンバ変数
| クラス名 | メンバ変数 | 補足 |
|---|---|---|
| FunctionStmtAST | std::vector<VariableDeclAST*> VariableDecls | 関数内の変数宣言 |
| std::vector<BaseAST*> StmtLists | 関数内のステートメント |
ここでfunction_definitionを定義したいわけですが,そのためにはprototype(プロトタイプ宣言)の情報をどう保持するかを先に決定してやりたいですね.prototypeは変数名(引数)リストをと関数を持つクラスとして簡単に定義できるので,変数名のリストと関数名をメンバとして持つPrototypeASTクラスを用意することにします.なお,prototypeはfunction_declarationに相当します.表5.6にPrototypeASTのメンバ変数を記載しておきます.
表5.6: PrototypeASTのメンバ変数
| クラス名 | メンバ変数 | 補足 |
|---|---|---|
| PrototypeAST | std::string Name | 変数名 |
| std::vector<std::string> Params | 引数の変数名 |
これでようやくfunction_definitionを定義できます.function_definitionはFunctionASTクラスとし,メンバにPrototypeASTクラスとFunctionStmtASTクラスを持つことにしましょう.translation_unitはfunction_declarationとfunction_definitionの二つの集合ですので,最後にtranslation_unitを表すクラスを定義します.translation_unitは内部にPrototypeASTクラスとFunctionASTクラスのリストをもつTranslationUnitASTクラスとします.FunctionASTクラスとTranslationUnitASTのメンバ変数は表5.7の様になります.
表5.7: FunctionASTクラスとTranslationUnitASTのメンバ変数
| クラス名 | メンバ変数 | 補足 |
|---|---|---|
| FunctionAST | PrototypeAST *Proto | 関数のプロトタイプ宣言 |
| FunctionStmtAST *Body | 関数のボディ | |
| TranslationUnitAST | std::vector<PrototypeAST*> Prototypes | プロトタイプ宣言のリスト |
| std::vector<FunctionAST*> Functions | 関数定義のリスト |
以上の内容より,追加するASTのクラス宣言をリスト5.10に示します.各クラスは基本的に既に説明したしたメンバ変数とメンバ変数に対するsetter/getterを持ちます.ただし,TranslationUnitASTにはsetter/getter以外にemptyというメソッドを定義しています.これはPtototypesとFunctionsのサイズが共に0,つまり空のASTの時にtrueを返し,それ以外のときにfalseを返すメソッドで,後にエラー処理に使用するために定義しています.
リスト5.10: モジュールと関数
1: /**
2: * ソースコードを表すAST
3: */
4: class TranslationUnitAST {
5: std::vector<PrototypeAST *> Prototypes;
6: std::vector<FunctionAST *> Functions;
7:
8: public:
9: TranslationUnitAST() {}
10: ~TranslationUnitAST();
11:
12: // モジュールにプロトタイプ宣言を追加する
13: bool addPrototype(PrototypeAST *proto);
14:
15: // モジュールに関数を追加する
16: bool addFunction(FunctionAST *func);
17:
18: // モジュールが空か判定する
19: bool empty();
20:
21: // i番目のプロトタイプ宣言を取得する
22: PrototypeAST *getPrototype(int i) {
23: if (i < Prototypes.size())
24: return Prototypes.at(i);
25: else
26: return NULL;
27: }
28:
29: // i番目の関数を取得する
30: FunctionAST *getFunction(int i) {
31: if (i < Functions.size())
32: return Functions.at(i);
33: else
34: return NULL;
35: }
36: };
37:
38: /**
39: * 関数宣言を表すAST
40: */
41: class PrototypeAST {
42: std::string Name;
43: std::vector<std::string> Params;
44:
45: public:
46: PrototypeAST(const std::string &name, const std::vector<std::string> ¶ms)
47: : Name(name), Params(params) {}
48:
49: // 関数名を取得する
50: std::string getName() { return Name; }
51:
52: // i番目の引数名を取得する
53: std::string getParamName(int i) {
54: if (i < Params.size())
55: return Params.at(i);
56: return NULL;
57: }
58:
59: // 引数の数を取得する
60: int getParamNum() { return Params.size(); }
61: };
62:
63: /**
64: * 関数定義を表すAST
65: */
66: class FunctionAST {
67: PrototypeAST *Proto;
68: FunctionStmtAST *Body;
69:
70: public:
71: FunctionAST(PrototypeAST *proto, FunctionStmtAST *body)
72: : Proto(proto), Body(body) {}
73: ~FunctionAST();
74:
75: // 関数名を取得する
76: std::string getName() { return Proto->getName(); }
77:
78: // この関数のプロトタイプ宣言を取得する
79: PrototypeAST *getPrototype() { return Proto; }
80:
81: // この関数のボディを取得する
82: FunctionStmtAST *getBody() { return Body; }
83: };
84:
85: /**
86: * 関数定義(ボディ)を表すAST
87: */
88: class FunctionStmtAST {
89: std::vector<VariableDeclAST *> VariableDecls;
90: std::vector<BaseAST *> StmtLists;
91:
92: public:
93: FunctionStmtAST() {}
94: ~FunctionStmtAST();
95:
96: // 関数に変数を追加する
97: bool addVariableDeclaration(VariableDeclAST *vdecl);
98:
99: // 関数にステートメントを追加する
100: bool addStatement(BaseAST *stmt) { StmtLists.push_back(stmt); }
101:
102: // i番目の変数を取得する
103: VariableDeclAST *getVariableDecl(int i) {
104: if (i < VariableDecls.size())
105: return VariableDecls.at(i);
106: else
107: return NULL;
108: }
109:
110: // i番目のステートメントを取得する
111: BaseAST *getStatement(int i) {
112: if (i < StmtLists.size())
113: return StmtLists.at(i);
114: else
115: return NULL;
116: }
117: };
前節でASTの構造を定義したので,本節では入力ソースコードからASTを作成する構文解析クラスを作成します.構文解析クラスのクラス名はParserとします.
構文解析の実装方法は大きく分けてトップダウン方式とボトムアップ方式がありますし,それぞれの方式にも細かく複数の方式が存在します.どれを採用するかはユーザが自由に考えればいいと思いますが,本書では以下の方針でParserクラスの実装を行います.
以上の内容をまとめたParserクラスの宣言をリスト5.11に示します.
リスト5.11: parserクラス宣言
1: /**
2: * 構文解析・意味解析クラス
3: */
4: class Parser {
5: private:
6: TokenStream *Tokens;
7: TranslationUnitAST *TU;
8:
9: public:
10: Parser(std::string filename);
11: ~Parser() {
12: SAFE_DELETE(TU);
13: SAFE_DELETE(Tokens);
14: }
15: bool doParse();
16: TranslationUnitAST &getAST();
17:
18: private:
19: /**
20: * 各種構文解析メソッド
21: */
22: bool visitTranslationUnit();
23: bool visitExternalDeclaration(TranslationUnitAST *tunit);
24: PrototypeAST *visitFunctionDeclaration();
25: FunctionAST *visitFunctionDefinition();
26: PrototypeAST *visitPrototype();
27: FunctionStmtAST *visitFunctionStatement(PrototypeAST *proto);
28: VariableDeclAST *visitVariableDeclaration();
29: BaseAST *visitStatement();
30: BaseAST *visitExpressionStatement();
31: BaseAST *visitJumpStatement();
32: BaseAST *visitAssignmentExpression();
33: BaseAST *visitAdditiveExpression(BaseAST *lhs);
34: BaseAST *visitMultiplicativeExpression(BaseAST *lhs);
35: BaseAST *visitPostfixExpression();
36: BaseAST *visitPrimaryExpression();
37: };
先ほど宣言したクラスの各メソッドの実装について説明します.全てのメソッドをのせると冗長な上に読むのも疲れるので幾つかのポイントを記載していきます.
まず,外部から参照されるコンストラクタ,doParse,getASTから解説します.最初にコードを載せて解説という流れでいきましょう.リスト5.12にParserクラスのpublicなメソッドの実装例を示します.
リスト5.12: Parserクラスのpublicメソッド
1: /**
2: * コンストラクタ
3: */
4: Parser::Parser(std::string filename) { Tokens = LexicalAnalysis(filename); }
5:
6: /**
7: * 構文解析実行
8: * @return 解析成功:true 解析失敗:false
9: */
10: bool Parser::doParse() {
11: if (!Tokens) {
12: fprintf(stderr, "error at lexer\n");
13: return false;
14: } else
15: return visitTranslationUnit();
16: }
17:
18: /**
19: * AST取得
20: * @return TranslationUnitへの参照
21: */
22: TranslationUnitAST &Parser::getAST() {
23: if (TU)
24: return *TU;
25: else
26: return *(new TranslationUnitAST());
27: }
実装方針でも触れましたが,コンストラクタは引数として入力ソースコード名を受け取り,内部でLexicalAnalaysis関数を呼び出します.これによって取得したTokenStreamクラスのインスタンスをメンバ変数Tokensに保存しておき,以降の解析メソッドではこのTokensからソースコードのトークン情報を取得します.doParseは内部でvisitTranslationUnitを呼び出し構文解析を開始し,visitTranslationUnitが返却した解析の成否をそのまま戻り値とします.このコード上からは見えませんがvisitTranslationUnit内では構文解析に成功した場合,TranslationUnitAST *型のメンバ変数TUに解析結果のASTの頂点となるTranslationUnitASTが代入されます.なお,doParseはコンストラクタで呼び出したLexicalAnalaysisに失敗し,TokensがNULLの場合には構文解析を行わずにfalseを返して終了します.getASTでは既にdoParseに成功していた時,つまりTUがNULL値でない時にTUへの参照を返します.TUがNULL値の場合は新しく空のTranslationUnitASTを生成して返却します.
次にdoParseから直接呼び出されるvisitTranslationUnit,およびvisitExternalDeclarationを確認します.visitTranslationUnitとvisitExternalDeclarationはそれぞれtranslation_unitとexternal_declarationを解析するメソッドですが,最初に示したEBNFだけではどのような構文を受け入れるのかわかりにくいかと思いますので,ここでtranslation_unitとexternal_declarationの構文図を図5.1に記載します.なお,図中の四角い箱は非終端記号を示しています.
図5.1: translation_unitとexternal_declarationの構文図
上記の構文を受け入れる構文解析メソッドがvisitTranslationUnitとvisitExternalDeclarationになります.ソースコード例をリスト5.13に示しますので確認してください.なお,以降も各種構文解析メソッドの実装を説明する際には構文図とソースコードをセットで記載していきたいと思います.
リスト5.13: visitTranslationUnitとvisitExternalDeclaration
1: /**
2: * TranslationUnit用構文解析メソッド
3: * @return 解析成功:true 解析失敗:false
4: */
5: bool Parser::visitTranslationUnit() {
6: TU = new TranslationUnitAST();
7:
8: // ExternalDecl
9: while (true) {
10: if (!visitExternalDeclaration(TU)) {
11: SAFE_DELETE(TU);
12: return false;
13: }
14: if (Tokens->getCurType() == TOK_EOF)
15: break;
16: }
17: return true;
18: }
19:
20: /**
21: * ExternalDeclaration用構文解析クラス
22: * 解析したPrototyupeとFunctionASTをTranslationUitに追加
23: * @param TranslationUnitAST
24: * @return true
25: */
26: bool Parser::visitExternalDeclaration(TranslationUnitAST *tunit) {
27: // FunctionDeclaration
28: PrototypeAST *proto = visitFunctionDeclaration();
29: if (proto) {
30: tunit->addPrototype(proto);
31: return true;
32: }
33:
34: // FunctionDefinition
35: FunctionAST *func_def = visitFunctionDefinition();
36: if (func_def) {
37: tunit->addFunction(func_def);
38: return true;
39: }
40:
41: return false;
42: }
visitTranslationUnitの内部ではEOFがくるまでvisitExternalDeclarationによるexternal_declarationの解析を繰り返します.visitTranslationUnitメソッドは解析成功の場合はメンバ変数TUに解析結果としてASTの頂点であるTranslationUnitASTのインスタンスを代入し,trueを返却します.失敗時には解析中のASTを破棄しTUにNULL値を設定した後,falseを返却します.なお,visitExternalDeclarationは引数としてTranslationUnitASTのポインタをとり,解析したFunctionDeclarationASTとFunctionDefinitionASTをTranslationUnitASTに追加するようにしました.他のメソッドと整合性をとるためにはTranslationUnitへのASTの追加はvisitTranslationUnit内で行った方が良い気もしますが,今回は楽に実装したかったので上記のようにしています.以降,同様に構文規則に基づいて解析メソッドを実装しますが,ここからは実装上注意すべきポイントを絞って説明を進めます.
まず関数宣言と関数定義の解析,つまりvisitFunctionDeclarationとvisitFunctionDefinitionでの処理について説明します.今回も先に構文図を図5.2に記載します.(図中に表れるprototypeの解析は後述するvisitPrototypeの部分で記載します.)
図5.2: function_declarationとfunction_definitionの構文図
上記の構文図からもわかると思いますが,function_declarationとfunction_definitionは関数宣言と関数定義を示す非終端記号です.コンパイラとしては関数宣言と定義の解析において,同名の関数が再定義されていないかのチェックを行うべきです.しかし今回はひとまず構文解析までを目的とするので,これらの処理を行いません.これらは後述する意味解析で実装します.ですので構文解析の段階でのvisitFunctionDeclarationとvisitFunctionDefinitionの処理はリスト5.14の様な形になります.
リスト5.14: visitFunctionDeclaration
1: /**
2: * FunctionDclaration用構文解析メソッド
3: * @return 解析成功:PrototypeAST 解析失敗:NULL
4: */
5: PrototypeAST *Parser::visitFunctionDeclaration() {
6: int bkup = Tokens->getCurIndex();
7: PrototypeAST *proto = visitPrototype();
8: if (!proto) {
9: return NULL;
10: }
11:
12: // prototype;
13: if (Tokens->getCurString() == ";") {
14:
15: ・・・本来であればここで再定義されていないか確認・・・
16:
17: Tokens->nextToken();
18: return proto;
19: } else {
20: SAFE_DELETE(proto);
21: Tokens->applyTokenIndex(bkup);
22: return NULL;
23: }
24: }
リスト5.15: visitFunctionDefinition
1: /**
2: * FunctionDefinition用構文解析メソッド
3: * @return 解析成功:FunctionAST 解析失敗:NULL
4: */
5: FunctionAST *Parser::visitFunctionDefinition() {
6: int bkup = Tokens->getCurIndex();
7:
8: PrototypeAST *proto = visitPrototype();
9: if (!proto) {
10: return NULL;
11: }
12:
13: ・・・本来であればここで再定義されていないか確認・・・
14:
15: FunctionStmtAST *func_stmt = visitFunctionStatement(proto);
16:
17: ・・・省略・・・
18: }
関数の再定義確認と同様に,変数の二重宣言や変数参照時の変数宣言の確認も構文解析では省きます.上記の処理を含むのはvisitPrototype,visitFunctionStatement,visitAssignmentExpression,visitPostfixExpression,visitPrimaryExpressionですので,構文解析時点でのこれらのコードを載せておきます.
まず,visitPrototypeから説明をはじめます.visitPrototypeはprototypeを受け入れる構文解析メソッドです.今回も先にprototypeの構文図を図5.3に記載します.
図5.3: prototypeの構文図
非終端記号prototypeはその名前の通り関数のプロトタイプ宣言を表します.prototypeを受け入れる構文解析メソッドvisitPrototypeの構文解析時点でのソースコード例をリスト5.16に示します.
リスト5.16: 変数宣言のチェック
1: /**
2: * Prototype用構文解析メソッド
3: * @return 解析成功:PrototypeAST 解析失敗:NULL
4: */
5: PrototypeAST *Parser::visitPrototype() {
6: // indexを保存
7: int bkup = Tokens->getCurIndex();
8: ・・・省略・・・
9:
10: // parameter_list
11: bool is_first_param = true;
12: std::vector<std::string> param_list;
13: while (true) {
14: //,
15: if (!is_first_param && Tokens->getCurType() == TOK_SYMBOL &&
16: Tokens->getCurString() == ",") {
17: Tokens->nextToken();
18: }
19: if (Tokens->getCurType() == TOK_INT) {
20: Tokens->nextToken();
21: } else {
22: break;
23: }
24:
25: if (Tokens->getCurType() == TOK_IDENTIFIER) {
26:
27: ・・・本来ならここで引数名に被りがないか確認・・・
28:
29: param_list.push_back(Tokens->getCurString());
30: Tokens->nextToken();
31: } else {
32: Tokens->applyTokenIndex(bkup);
33: return NULL;
34: }
35: }
36: ・・・省略・・・
37: }
visitPrototypeでは非終端記号prototypeの規則に従い,関数のプロトタイプ宣言を解析します.19行目からの処理で,トークンが識別子であることを確認し,引数名としてリストに追加していきます.識別子であることを確認した後,各引数名に被りがないかを確認をしておくところですが,今回は構文解析ですのでこの処理は未実装とします.
次に,visitFunctionStatementメソッドの説明にうつります.visitFunctionStatementで解析するfunction_statementに関連する構文図は図5.4の様になります.
図5.4: function_statementの構文図
function_statementは変数宣言のリストと式のリストを含む関数のブロックを表す非終端記号です.function_statementを受け入れるためのvisitFunctionStatementメソッドの構文解析時点でのコード例をリスト5.17に示します.
リスト5.17: visitFunctionStatement
1: /**
2: * FunctionStatement用構文解析メソッド
3: * @param 関数名や引数を格納したPrototypeクラスのインスタンス
4: * @return 解析成功:FunctionStmtAST 解析失敗:NULL
5: */
6: FunctionStmtAST *Parser::visitFunctionStatement(PrototypeAST *proto) {
7: int bkup = Tokens->getCurIndex();
8:
9: if (Tokens->getCurString() == "{") {
10: Tokens->nextToken();
11: } else {
12: return NULL;
13: }
14:
15: FunctionStmtAST *func_stmt = new FunctionStmtAST();
16:
17: // 引数をfunc_stmtの変数宣言リストに追加
18: for (int i = 0; i < proto->getParamNum(); i++) {
19: VariableDeclAST *vdecl = new VariableDeclAST(proto->getParamName(i));
20: vdecl->setDeclType(VariableDeclAST::param);
21: func_stmt->addVariableDeclaration(vdecl);
22: VariableTable.push_back(vdecl->getName());
23: }
24:
25: ・・・省略・・・
26:
27: } else if (var_decl = visitVariableDeclaration()) {
28: while (var_decl) {
29: var_decl->setDeclType(VariableDeclAST::local);
30:
31: ・・・本来ならここで変数が二重宣言されていないことを確認・・・
32:
33: func_stmt->addVariableDeclaration(var_decl);
34: VariableTable.push_back(var_decl->getName());
35: var_decl = visitVariableDeclaration();
36: }
37:
38: if (stmt = visitStatement()) {
39: while (stmt) {
40: last_stmt = stmt;
41: func_stmt->addStatement(stmt);
42: stmt = visitStatement();
43: }
44: }
45:
46: // other
47: } else {
48: SAFE_DELETE(func_stmt);
49: Tokens->applyTokenIndex(bkup);
50: return NULL;
51: }
52:
53: ・・・本来ならここで戻り値の確認・・・
54:
55: if (Tokens->getCurString() == "}") {
56: Tokens->nextToken();
57: return func_stmt;
58: } else {
59: SAFE_DELETE(func_stmt);
60: Tokens->applyTokenIndex(bkup);
61: return NULL;
62: }
63: }
visitVariableDeclarationで変数宣言を解析した後,本来は変数が二重宣言がされていないことを確認する部分ですが構文解析では行いません.また,Statementがreturn文で終了し,関数が値を返すことを確認すべきですが,これも構文解析では未実装とします.
次は,visitAssignmentExpressionです.visitAssignmentExpressionで解析する非終端記号,assignment_expressionの構文図を図5.5に示します.
図5.5: assignment_expressionの構文図
assignment_expressionは代入文に関する生成規則です.assignment_expressionを受け入れる構文解析メソッド,visitAssignmentExpressionの構文解析時点での実装例をリスト5.18に示します.
リスト5.18: visitAssignmentExpression
1: /**
2: * AssignmentExpression用構文解析メソッド
3: * @return 解析成功:AST 解析失敗:NULL
4: */
5: BaseAST *Parser::visitAssignmentExpression() {
6: int bkup = Tokens->getCurIndex();
7:
8: BaseAST *lhs;
9: if (Tokens->getCurType() == TOK_IDENTIFIER) {
10: ・・・本来はここで変数の宣言確認・・・
11: lhs = new VariableAST(Tokens->getCurString());
12: Tokens->nextToken();
13: BaseAST *rhs;
14: if (Tokens->getCurType() == TOK_SYMBOL && Tokens->getCurString() == "=") {
15: Tokens->nextToken();
16: if (rhs = visitAdditiveExpression(NULL)) {
17: return new BinaryExprAST("=", lhs, rhs);
18: } else {
19: SAFE_DELETE(lhs);
20: Tokens->applyTokenIndex(bkup);
21: }
22: } else {
23: SAFE_DELETE(lhs);
24: Tokens->applyTokenIndex(bkup);
25: }
26: }
27: BaseAST *add_expr = visitAdditiveExpression(NULL);
28: if (add_expr) {
29: return add_expr;
30: }
31:
32: return NULL;
33: }
assignment_expressionは代入文の解析ですので,左辺値となる識別子(変数名)が現れます.構文解析では意味的な部分はチェックしないので,正しい位置に識別子が出現することの確認のみを行い,それが変数として宣言されているかの確認は行いません.
最後はprimary_expressionを解析するvisitPrimaryExpressionです.primary_expressionの構文図は下記の様になります.
図5.6: primary_expressionの構文図
primary_expressionは式の基本構成要素に関する規則で,識別子や数値,()で囲まれた式を受け入れます.構文解析時点でのvisitPrimaryExpressionの実装例をリスト5.19に示します.
リスト5.19: visitPrimaryExpression
1: /**
2: * PrimaryExpression用構文解析メソッド
3: * @return 解析成功時:AST失敗時:NULL
4: */
5: BaseAST *Parser::visitPrimaryExpression() {
6: // indexを保存
7: int bkup = Tokens->getCurIndex();
8:
9: // VARIABLE_IDENTIFIER
10: if (Tokens->getCurType() == TOK_IDENTIFIER) {
11:
12: ・・・本来ならここで変数の宣言確認・・・
13:
14: std::string var_name = Tokens->getCurString();
15: Tokens->nextToken();
16: return new VariableAST(var_name);
17:
18: // integer
19: } else if (Tokens->getCurType() == TOK_DIGIT) {
20: int val = Tokens->getCurNumVal();
21: Tokens->nextToken();
22: return new NumberAST(val);
23:
24: // integer(-)
25: } else if (Tokens->getCurType() == TOK_SYMBOL &&
26: Tokens->getCurString() == "-") {
27:
28: ・・・省略・・・
29: }
30:
31: return NULL;
32: }
visitPrimaryExpressionもassignment_expressionと同様で,識別子が正しい場所で出現することの確認のみを行い,定義済みであるかの確認は行いません.
構文解析では関数呼び出し時の関数宣言の確認についても同じく実装しません.関数呼び出しの構文解析が含まれるvisitPostfixExpressionの構文図と構文解析時点でのソースコード例を図5.7とリスト5.20に示します.
図5.7: postfix_expressionの構文図
リスト5.20: visitPostfixExpression
1: /**
2: * PostfixExpression用構文解析メソッド
3: * @return 解析成功:AST 解析失敗:NULL
4: */
5: BaseAST *Parser::visitPostfixExpression() {
6: // get index
7: int bkup = Tokens->getCurIndex();
8:
9: ・・・省略・・・
10:
11: // FUNCTION_IDENTIFIER
12: if (Tokens->getCurType() == TOK_IDENTIFIER) {
13:
14: ・・・本来ならここで関数の宣言確認・・・
15:
16: // 関数名取得
17: std::string Callee = Tokens->getCurString();
18: Tokens->nextToken();
19:
20: // LEFT PAREN
21: if (Tokens->getCurType() != TOK_SYMBOL || Tokens->getCurString() != "(") {
22: Tokens->applyTokenIndex(bkup);
23: return NULL;
24: }
25:
26: Tokens->nextToken();
27: std::vector<BaseAST *> args;
28: // 引数を解析
29: BaseAST *assign_expr = visitAssignmentExpression();
30: if (assign_expr) {
31: args.push_back(assign_expr);
32: // ","が続く限り繰り返し
33: while (Tokens->getCurType() == TOK_SYMBOL &&
34: Tokens->getCurString() == ",") {
35: Tokens->nextToken();
36: assign_expr = visitAssignmentExpression();
37: if (assign_expr) {
38: args.push_back(assign_expr);
39: } else {
40: break;
41: }
42: } // end while
43: }
44:
45: ・・・本来ならここで引数の数を確認・・・
46:
47: // RIGHT PAREN
48: if (Tokens->getCurType() == TOK_SYMBOL && Tokens->getCurString() == ")") {
49: Tokens->nextToken();
50: return new CallExprAST(Callee, args);
51: } else {
52: for (int i = 0; i < args.size(); i++)
53: SAFE_DELETE(args[i]);
54: Tokens->applyTokenIndex(bkup);
55: return NULL;
56: }
57: } else {
58: return NULL;
59: }
60: }
visitPostfixExpressionには関数呼び出しの解析が含まれます.意味的な部分の確認するのであれば,12行目の識別子が宣言,もしくは定義された関数名であるか確認すべきですし,また,関数呼び出しの引数の数が関数の宣言もしくは定義とあっているかどうかの確認も必要です.しかし,ここでもこれまで同様意味解析は実装しないため,これらの処理は未実装とします.
次は四則演算の解析を説明します.これは構文規則どおりに実装すればよい部分ではありますが,少々ややこしいのでコードを載せて説明しておきます.なお,四則演算の解析メソッドはvisitAdditiveExpressionとvisitMultiplicativeExpressionにわかれますが,基本的に同様の処理を行うため今回はvisitAdditiveExpressionについてのみ説明します.まず図5.8にadditive_expressionの構文図を記載します.
図5.8: additive_expressionの構文図
additive_expressionは加減算に関する規則で,加算および減算の繰り返しを受け入れます.リスト5.21にvisitAdditiveExpressionの実装を示します.
リスト5.21: 四則演算
1: /**
2: * AdditiveExpression用構文解析メソッド
3: * @param lhs(左辺),初回呼び出し時はNULL
4: * @return 解析成功:AST 解析失敗:NULL
5: */
6: BaseAST *Parser::visitAdditiveExpression(BaseAST *lhs) {
7: // indexを保存
8: int bkup = Tokens->getCurIndex();
9:
10: if (!lhs)
11: lhs = visitMultiplicativeExpression(NULL);
12:
13: if (!lhs)
14: return NULL;
15:
16: BaseAST *rhs;
17: // +
18: if (Tokens->getCurType() == TOK_SYMBOL && Tokens->getCurString() == "+") {
19: Tokens->nextToken();
20: rhs = visitMultiplicativeExpression(NULL);
21: if (rhs) {
22: return visitAdditiveExpression(new BinaryExprAST("+", lhs, rhs));
23: } else {
24: SAFE_DELETE(lhs);
25: Tokens->applyTokenIndex(bkup);
26: return NULL;
27: }
28:
29: // -
30: } else if (Tokens->getCurType() == TOK_SYMBOL &&
31: Tokens->getCurString() == "-") {
32: Tokens->nextToken();
33: rhs = visitMultiplicativeExpression(NULL);
34: if (rhs) {
35: return visitAdditiveExpression(new BinaryExprAST("-", lhs, rhs));
36: } else {
37: SAFE_DELETE(lhs);
38: Tokens->applyTokenIndex(bkup);
39: return NULL;
40: }
41: }
42: return lhs;
43: }
四則演算の解析はvisitAssignmnetExpressionからvisitAdditiveExpressionを呼び出すことで開始します.visitAdditiveExpressionは引数として演算の左辺値(lhs)を受け取りますが,初回呼び出し時はNULLを指定して呼び出されます.lhsがNULLの場合はvisitMultiplicativeExpressionを呼び出して左辺値を解析します.lhsが解析できれば演算子の種類と右辺値の解析を実施します.これで該当演算の解析自体は完了します.しかし,この演算を左辺値とする演算が更に続く可能性があります.そのため,この演算を表すBinaryExprASTを引数として,visitAdditiveExpressionを再帰的に呼び出します.これにより,後続の演算についても解析可能となります.visitMultiplicativeExpressionについても構造は同じですので説明は省略します.
Parserクラスの各メソッドは基本的にASTを返すようにしています.ここで問題となるのが,expression_statementの解析です.まずexpression_statementの構文図を図5.9に示します.
図5.9: expression_statementの構文図
図で見ると一目瞭然ですが,expression_statementは“;”のみの文を受け入れます.“;”のみの文は何もしない文ですのでNULLを返したいところですが,NULLは今回の仕様上エラーを意味します.そこで,今回は特別に“;”を表すNullExprASTを追加してやることにします.先ほど構文上意味のない記号はASTから除くという話をした手前少々まずい気もしますが,今回は楽にわかりやすく実装を進めたいので少々ごり押しで対応します.そんなわけで,下記の様にAstIDを修正し,NullExprASTを追加します.
リスト5.22: AstIDの修正とNullExprAST
1: /**
2: * ASTの種類
3: */
4: enum AstID {
5: BaseID,
6: VariableDeclID,
7: BinaryExprID,
8: NullExprID,
9: CallExprID,
10: JumpStmtID,
11: VariableID,
12: NumberID
13: };
14:
15: /**
16: * ";"を表すAST
17: */
18: class NullExprAST : public BaseAST {
19: public:
20: NullExprAST() : BaseAST(NullExprID) {}
21: static inline bool classof(NullExprAST const *) { return true; }
22: static inline bool classof(BaseAST const *base) {
23: return base->getValueID() == NullExprID;
24: }
25: };
また,expression_statementの解析メソッドvisitExpressionStatementを以下のように定義します.
リスト5.23: visitfExpressionStatement
1: /**
2: * ExpressionStatement用構文解析メソッド
3: * @return 解析成功:AST 解析失敗:NULL
4: */
5: BaseAST *Parser::visitExpressionStatement() {
6: BaseAST *assign_expr;
7:
8: // NULL Expression
9: if (Tokens->getCurString() == ";") {
10: Tokens->nextToken();
11: return new NullExprAST();
12: } else if (assign_expr = visitAssignmentExpression()) {
13: if (Tokens->getCurString() == ";") {
14: Tokens->nextToken();
15: return assign_expr;
16: }
17: }
18: return NULL;
19: }
先ほどは単に構文規則に則っているかのみを見ていましたが,本節では更に意味解析の機能を追加します.
意味解析の実装方法は下記の二つの方法があります.
どちらを採用するかは自由ですが,今回は特に複雑な構文でもないので構文解析と同時に実施してしまいます*5.
意味解析では構文解析で実装しなかった関数の宣言,引数チェックや変数の二重宣言のチェックを実装します.本節で実装する機能を以下に示します.
上記の機能を実装するため,Parserクラスに表5.8のメンバ変数を追加します.
表5.8: Parserクラスに追加するメンバ変数
| クラス名 | メンバ変数 | 補足 |
|---|---|---|
| Parser | std::vector<std::string> VariableTable | 宣言済の変数名を登録する |
| std::map<std::string, int> PrototypeTable | 宣言済関数の関数名と引数の数のマップ | |
| std::map<std::string, int> FunctionTable | 定義済関数の関数名と引数の数のマップ |
上記メンバを追加した,修正後のParserクラスの宣言をリスト5.24に示します.
リスト5.24: 修正したparserクラスの宣言
1: /**
2: * 構文解析・意味解析クラス
3: */
4: class Parser {
5: private:
6: TokenStream *Tokens;
7: TranslationUnitAST *TU;
8:
9: // 意味解析用各種識別子表
10: std::vector<std::string> VariableTable;
11: std::map<std::string, int> PrototypeTable;
12: std::map<std::string, int> FunctionTable;
13:
14: public:
15: Parser(std::string filename) { Tokens = LexicalAnalysis(filename); }
16: ~Parser() {
17: SAFE_DELETE(TU);
18: SAFE_DELETE(Tokens);
19: }
20: bool doParse();
21: TranslationUnitAST &getAST();
22:
23: ・・・省略・・・
24: };
先ほど用意したテーブルを利用して,各解析メソッドの必要な箇所に意味解析の機能を追加していきます.
最初に,関数の再定義が行われていないかのチェックをvisitFunctionDeclarationとvisitFunctionDefinitionに追加します.まず,visitFuncDeclarationから修正します.visitFuncDeclarationに追加したい処理は以下の通りです.
上記の処理を追加したテーブルを用いて実装します.修正後のvisitFunctionDeclarationをリスト5.25に示します.
リスト5.25: 意味解析を実装したvisitFunctionDeclaration
1: /**
2: * FunctionDclaration用構文解析メソッド
3: * @return 解析成功:PrototypeAST 解析失敗:NULL
4: */
5: PrototypeAST *Parser::visitFunctionDeclaration() {
6: int bkup = Tokens->getCurIndex();
7: PrototypeAST *proto = visitPrototype();
8: if (!proto) {
9: return NULL;
10: }
11:
12: // prototype;
13: if (Tokens->getCurString() == ";") {
14:
15: // ここで再定義されていないか確認
16: if (PrototypeTable.find(proto->getName()) != PrototypeTable.end() ||
17: (FunctionTable.find(proto->getName()) != FunctionTable.end() &&
18: FunctionTable[proto->getName()] != proto->getParamNum())) {
19:
20: // エラーメッセージを出してNULL返却
21: fprintf(stderr, "Function:%s is redefined", proto->getName().c_str());
22: SAFE_DELETE(proto);
23: return NULL;
24: }
25: // (関数名,引数)のペアをプロトタイプ宣言テーブル(Map)に追加
26: PrototypeTable[proto->getName()] = proto->getParamNum();
27: Tokens->nextToken();
28: return proto;
29: } else {
30: SAFE_DELETE(proto);
31: Tokens->applyTokenIndex(bkup);
32: return NULL;
33: }
34: }
まずvisitFunctionDeclarationでは,16行目でPrototypeTableを参照し新しく解析した関数が既に宣言されているか確認しています.17,18行目では同様にFunctionTableを参照しており,関数が定義済であるか,定義済の場合は引数の数があっているかを確認しています.この処理によって再定義と判断された場合はエラーメッセージを出力してNULLを返却します.新規に宣言された関数であった場合は,26行目でPrototypeTableに関数名,引数の数のペアを追加します.
次に,visitFunctionDefinitionを修正します.visitFunctionDefinitionに追加したい処理は以下の通りです.
上記の処理を追加したテーブルを用いて実装します.修正後のvisitFunctionDefinitionをリスト5.26に示します.
リスト5.26: 意味解析を実装したvisitFunctionDefinition
1: /**
2: * FunctionDefinition用構文解析メソッド
3: * @return 解析成功:FunctionAST 解析失敗:NULL
4: */
5: FunctionAST *Parser::visitFunctionDefinition() {
6: int bkup = Tokens->getCurIndex();
7:
8: PrototypeAST *proto = visitPrototype();
9: if (!proto) {
10: return NULL;
11:
12: // ここでプロトタイプ宣言と違いがないか,
13: // 既に関数定義が行われていないか確認
14: } else if ((PrototypeTable.find(proto->getName()) != PrototypeTable.end() &&
15: PrototypeTable[proto->getName()] != proto->getParamNum()) ||
16: FunctionTable.find(proto->getName()) != FunctionTable.end()) {
17:
18: // エラーメッセージを出してNULL返却
19: fprintf(stderr, "Function:%s is redefined", proto->getName().c_str());
20: SAFE_DELETE(proto);
21: return NULL;
22: }
23:
24: VariableTable.clear();
25: FunctionStmtAST *func_stmt = visitFunctionStatement(proto);
26: if (func_stmt) {
27: // ここで(関数名,引数の数)のペアを関数テーブル(Map)に追加
28: FunctionTable[proto->getName()] = proto->getParamNum();
29:
30: return new FunctionAST(proto, func_stmt);
31: }
32: ・・・省略・・・
33: }
visitFunctionDefinitionでは,14行目,15行目でPrototypeTableを参照し,新しく解析した関数が宣言済であるか,宣言されている場合は引数の数があっているかを確認しています.16行目ではFunctionTableを参照し,関数が定義済みでないかを確認しています.14行目〜16行目の条件文によって関数の再定義であると判断された場合,エラーメッセージを出力してNULLを返却します.新規に定義された関数であった場合は,28行目でFunctionTableに関数名,引数の数のペアを追加します.なお,関数の再定義とは関係ありませんが,24行目でVariableTableをクリアしていることに注意してください.VariableTableは関数ごとの宣言済み変数を登録するベクタですので,function_statementの解析に入る前に毎回クリアする必要があります.
変数の二重宣言や変数参照時の変数宣言の確認も構文解析を実装します.上記の処理を含むのはvisitPrototype,visitFunctionStatement,visitAssignmentExpression,visitPostfixExpression,visitPrimaryExpressionです.
まず,意味解析を実装したvisitPrototypeをリスト5.27に示します.
リスト5.27: 意味解析を実装したvisitPrototype
1: /**
2: * Prototype用構文解析メソッド
3: * @return 解析成功:PrototypeAST 解析失敗:NULL
4: */
5: PrototypeAST *Parser::visitPrototype() {
6: // indexを保存
7: int bkup = Tokens->getCurIndex();
8: ・・・省略・・・
9: std::vector<std::string> param_list;
10: bool is_first_param = true;
11: while (true) {
12: if (!is_first_param && Tokens->getCurType() == TOK_SYMBOL &&
13: Tokens->getCurString() == ",") {
14: Tokens->nextToken();
15: }
16: if (Tokens->getCurType() == TOK_INT) {
17: Tokens->nextToken();
18: } else {
19: break;
20: }
21:
22: if (Tokens->getCurType() == TOK_IDENTIFIER) {
23: // 引数の変数名に被りがないか確認
24: if (std::find(param_list.begin(), param_list.end(),
25: Tokens->getCurString()) != param_list.end()) {
26: Tokens->applyTokenIndex(bkup);
27: return NULL;
28: }
29: param_list.push_back(Tokens->getCurString());
30: Tokens->nextToken();
31: } else {
32: Tokens->applyTokenIndex(bkup);
33: return NULL;
34: }
35: }
36: ・・・省略・・・
37: }
19行目,20行目のfind関数でparam_list(引数名を格納しているリスト)に新しく読み取った識別子が存在しないか確認しています.同名の識別子が既に存在した場合はNULLを返し,そうでなければリストに識別子を追加します.この処理を行うことで同一の引数名の出現を防いでいます.
次に,意味解析を実装したvisitFunctionStatementは下記の様になります.
リスト5.28: 意味解析を実装したvisitFunctionStatement
1: /**
2: * FunctionStatement用構文解析メソッド
3: * @param 関数名,引数を格納したPrototypeクラスのインスタンス
4: * @return 解析成功:FunctionStmtAST 解析失敗:NULL
5: */
6: FunctionStmtAST *Parser::visitFunctionStatement(PrototypeAST *proto) {
7: int bkup = Tokens->getCurIndex();
8:
9: if (Tokens->getCurString() == "{") {
10: Tokens->nextToken();
11: } else {
12: return NULL;
13: }
14:
15: FunctionStmtAST *func_stmt = new FunctionStmtAST();
16:
17: ・・・省略・・・
18:
19: } else if (var_decl = visitVariableDeclaration()) {
20: while (var_decl) {
21: var_decl->setDeclType(VariableDeclAST::local);
22: // 変数名に被りがないか確認
23: if (std::find(VariableTable.begin(), VariableTable.end(),
24: var_decl->getName()) != VariableTable.end()) {
25: SAFE_DELETE(var_decl);
26: SAFE_DELETE(func_stmt);
27: return NULL;
28: }
29: // 変数名テーブルに新しく読み取った変数名を追加
30: VariableTable.push_back(var_decl->getName());
31: func_stmt->addVariableDeclaration(var_decl);
32: var_decl = visitVariableDeclaration();
33: }
34:
35: if (stmt = visitStatement()) {
36: while (stmt) {
37: last_stmt = stmt;
38: func_stmt->addStatement(stmt);
39: stmt = visitStatement();
40: }
41: }
42:
43: // other
44: } else {
45: SAFE_DELETE(func_stmt);
46: Tokens->applyTokenIndex(bkup);
47: return NULL;
48: }
49:
50: // 最後のStatementがjump_statementであるか確認
51: if (!last_stmt || !isa<JumpStmtAST>(last_stmt)) {
52: SAFE_DELETE(func_stmt);
53: Tokens->applyTokenIndex(bkup);
54: return NULL;
55: }
56:
57: if (Tokens->getCurString() == "}") {
58: Tokens->nextToken();
59: return func_stmt;
60: } else {
61: SAFE_DELETE(func_stmt);
62: Tokens->applyTokenIndex(bkup);
63: return NULL;
64: }
65: }
23行目でのfind関数でVariableTableに新しく読み取った変数宣言の変数名が登録されているか確認しています.VariableTableは解析中の関数の変数名を登録しておくベクタですので,この処理で変数が二重宣言されないようにしています.変数名に被りがあった場合はNULLを返却し,そうでなければ30行目でVariableTableに読み取った変数名を追加します.
次に,意味解析を実装したvisitAssignmentExpressionは下記の様になります.
リスト5.29: 意味解析を実装したvisitAssignmentExpression
1: /**
2: * AssignmentExpression用構文解析メソッド
3: * @return 解析成功:AST 解析失敗:NULL
4: */
5: BaseAST *Parser::visitAssignmentExpression() {
6: int bkup = Tokens->getCurIndex();
7:
8: BaseAST *lhs;
9: if (Tokens->getCurType() == TOK_IDENTIFIER) {
10: // 変数が宣言されているか確認
11: if (std::find(VariableTable.begin(), VariableTable.end(),
12: Tokens->getCurString()) != VariableTable.end()) {
13: lhs = new VariableAST(Tokens->getCurString());
14: Tokens->nextToken();
15: BaseAST *rhs;
16: if (Tokens->getCurType() == TOK_SYMBOL && Tokens->getCurString() == "=") {
17: Tokens->nextToken();
18: if (rhs = visitAdditiveExpression(NULL)) {
19: return new BinaryExprAST("=", lhs, rhs);
20: } else {
21: SAFE_DELETE(lhs);
22: Tokens->applyTokenIndex(bkup);
23: }
24: } else {
25: SAFE_DELETE(lhs);
26: Tokens->applyTokenIndex(bkup);
27: }
28: } else {
29: Tokens->applyTokenIndex(bkup);
30: }
31: }
32:
33: BaseAST *add_expr = visitAdditiveExpression(NULL);
34: if (add_expr) {
35: return add_expr;
36: }
37: return NULL;
38: }
visitAssignmentExpressionでは代入文の左辺値が宣言済みの変数であることを確認する必要があります.それを確認しているのが11行目,12行目のfind関数を用いた処理であり,VariableTableに変数名が登録されていることを確認しています.
次に,意味解析を実装したvisitPrimaryExpressionは下記の様になります.
リスト5.30: 意味解析を実装したvisitPrimaryExpression
1: /**
2: * PrimaryExpression用構文解析メソッド
3: * @return 解析成功時:AST 失敗時:NULL
4: */
5: BaseAST *Parser::visitPrimaryExpression() {
6: // recored index
7: int bkup = Tokens->getCurIndex();
8:
9: // 変数が宣言されていることを確認
10: // VARIABLE_IDENTIFIER
11: if (Tokens->getCurType() == TOK_IDENTIFIER &&
12: (std::find(VariableTable.begin(), VariableTable.end(),
13: Tokens->getCurString()) != VariableTable.end())) {
14: std::string var_name = Tokens->getCurString();
15: Tokens->nextToken();
16: return new VariableAST(var_name);
17:
18: // integer
19: } else if (Tokens->getCurType() == TOK_DIGIT) {
20: int val = Tokens->getCurNumVal();
21: Tokens->nextToken();
22: return new NumberAST(val);
23:
24: // integer(-)
25: } else if (Tokens->getCurType() == TOK_SYMBOL &&
26: Tokens->getCurString() == "-") {
27:
28: ・・・省略・・・
29: }
30: return NULL;
31: }
visitPrimaryExpressionには変数参照の解析が含まれますので,これまでと同様にfind関数を利用して解析した変数が宣言済みであることを確認します.12行目,13行目のfind関数を用いた条件文が上記の処理に該当します.実行している内容はこれまでと同じで,VariableTableに読み取った変数名が含まれている事を確認しています.
関数呼び出しの解析時には,該当の関数が宣言されていることや引数の数が同じであることを確認する必要があります.意味解析では構文解析では実装しなかったこれらの処理を実装します.以上の処理を実装したvisitPostfixExpressionは下記の様になります.
リスト5.31: 意味解析を実装したvisitPostfixExpression
1: /**
2: * PostfixExpression用構文解析メソッド
3: * @return 解析成功:AST 解析失敗:NULL
4: */
5: BaseAST *Parser::visitPostfixExpression() {
6: // get index
7: int bkup = Tokens->getCurIndex();
8:
9: ・・・省略・・・
10:
11: // FUNCTION_IDENTIFIER
12: if (Tokens->getCurType() == TOK_IDENTIFIER) {
13: int param_num;
14: // プロトタイプ宣言されているか確認,引数の数をテーブルから取得
15: if (PrototypeTable.find(Tokens->getCurString()) != PrototypeTable.end()) {
16: param_num = PrototypeTable[Tokens->getCurString()];
17:
18: // 関数定義済みであるか確認,引数の数をテーブルから取得
19: } else if (FunctionTable.find(Tokens->getCurString()) !=
20: FunctionTable.end()) {
21: param_num = FunctionTable[Tokens->getCurString()];
22: } else {
23: return NULL;
24: }
25:
26: // 関数名取得
27: std::string Callee = Tokens->getCurString();
28: Tokens->nextToken();
29:
30: // LEFT PAREN
31: if (Tokens->getCurType() != TOK_SYMBOL || Tokens->getCurString() != "(") {
32: Tokens->applyTokenIndex(bkup);
33: return NULL;
34: }
35:
36: Tokens->nextToken();
37: std::vector<BaseAST *> args;
38: BaseAST *assign_expr = visitAssignmentExpression();
39: if (assign_expr) {
40: args.push_back(assign_expr);
41: while (Tokens->getCurType() == TOK_SYMBOL &&
42: Tokens->getCurString() == ",") {
43: Tokens->nextToken();
44:
45: // IDENTIFIER
46: assign_expr = visitAssignmentExpression();
47: if (assign_expr) {
48: args.push_back(assign_expr);
49: } else {
50: break;
51: }
52: } // end while
53: }
54:
55: // 引数の数を確認
56: if (args.size() != param_num) {
57: for (int i = 0; i < args.size(); i++)
58: SAFE_DELETE(args[i]);
59: Tokens->applyTokenIndex(bkup);
60: return NULL;
61: }
62:
63: ・・・省略・・・
64: }
65: }
まず,15行目〜25行目までの処理でPrototypeTableとFunctionTableを参照し,読み取った関数名が宣言/定義されているか確認しています.既に宣言/定義済であった場合,引数の数を変数param_numに取得しておきます.関数が宣言されていることを確認できれば,visitAssignmentExpressionによる引数の解析を繰り返し,解析に成功した場合はargs(引数を格納するベクタ)に格納しておきます.その後,57行目でargsのサイズと最初に取得したparam_numを比較し,解析中の関数呼び出しが関数定義とあっていることを確認します.
構文解析/意味解析でASTの生成が出来るようになりましたので,次はコード生成を実装します.ここでいうコード生成とはLLVM IRの出力であり,アセンブリやバイナリの出力ではないので注意してください.なお,ここにきてようやくLLVMが絡んできますので気合を入れていきましょう.
コード生成部の実装に先立ち,LLVM IRの生成手順を示しておきます.LLVM IRの生成は以下の手順で行います.
LLVM IRではメインコンテナであるModuleが一つのモジュール(翻訳単位)に相当するものなので,Moduleの生成は最初の一回のみです.その後のFunction,BasicBlock,Instructionに関してはソースコード(AST)の内容に応じて適宜生成します.グローバル変数に対応していればGlobalVariable等も挿入する必要がありますが,DummyCでは考慮しないので今回は実装しません.最後のファイルへの出力に関しては次節にて説明しますので,コード生成では手順5までを実装することにします.
なお,LLVMではModule,Function,BasicBlock,Instructionに対応するクラスとしてそれぞれModuleクラス,Functionクラス,BasicBlockクラス,Instructionクラスというクラスが用意されています.また,Instructionに関しては,派生クラスとしてAllocaInstやStoreInstなど,固有の命令を表すクラスが用意されています.最初の手順で示したLLVM IRの生成というのはこれらのクラスのインスタンスを生成するということを意味します.また,LLVMにはこれらFunctionやInstructionを簡単に生成する便利なクラスやメソッドも用意されています.特にInstructionに関してはIRBuilderという一つのクラスで様々なInstructionを生成できるため,非常に容易に実装可能です.本節ではこれらLLVMで用意されているクラスやメソッドの使用方法を中心に,LLVM IRの生成方法を説明します.
コード生成では構文解析/意味解析で生成したASTを頂点から辿り,必要に応じて関数や命令の生成を行います.これまでと同じようにコード生成用にCodeGenクラスを作成していきます.以下にCodeGenクラスの実装方針を示します.
以上をまとめたCodeGenクラスの宣言例をリスト5.32に示します.
リスト5.32: CodeGenクラスの宣言
1: /**
2: * コード生成クラス
3: */
4: class CodeGen {
5: private:
6: Function *CurFunc; // 現在コード生成中のFunction
7: Module *Mod; // 生成したModuleを格納
8: IRBuilder<> *Builder; // LLVM-IRを生成するIRBuilderクラス
9:
10: public:
11: CodeGen();
12: ~CodeGen();
13: bool doCodeGen(TranslationUnitAST &tunit, std::string name);
14: Module &getModule();
15:
16: private:
17: bool generateTranslationUnit(TranslationUnitAST &tunit, std::string name);
18: Function *generateFunctionDefinition(FunctionAST *func, Module *mod);
19: Function *generatePrototype(PrototypeAST *proto, Module *mod);
20: Value *generateFunctionStatement(FunctionStmtAST *func_stmt);
21: Value *generateVariableDeclaration(VariableDeclAST *vdecl);
22: Value *generateStatement(BaseAST *stmt);
23: Value *generateBinaryExpression(BinaryExprAST *bin_expr);
24: Value *generateCallExpression(CallExprAST *call_expr);
25: Value *generateJumpStatement(JumpStmtAST *jump_stmt);
26: Value *generateVariable(VariableAST *var);
27: Value *generateNumber(int value);
28: };
ここで,CodeGenクラス内にIRBuilderという型のメンバ変数が宣言されています.llvm::IRBuilderはLLVMで用意されたクラスでその名の通りLLVM IRの各種Instruction(命令)を生成し,BasicBlockに挿入するAPIを提供するクラスです.LLVM 3.2ではIRBuilderは“llvm/IRBuilder.h”にその宣言が記述されています*6.IRBuilderはコード生成の中核をなす重要なクラスですので,以降のコード生成メソッドの実装ではIRBuilderの使用方法を中心に話を進めます.また,同様にメンバ変数としてModule型のポインタが宣言されていますが,これは生成したLLVM IRを保存する変数です.第4章でも少し触れましたが,Doxygenの言葉を借りるとModuleは内部にグローバル変数や関数等の他の全てのLLVM IRオブジェクトを含む最上位のコンテナです.つまり,Moduleクラスがソースコードを解析/生成したLLVM IR全体の情報を持つと言えます.CodeGenクラスでは,生成したModuleのインスタンスをメンバとして保存しておき,外部からの要求に応じてModuleを返却することにします.
前節に記載したCodeGenクラスの各メソッドの実装について説明します.
外部から呼び出されるコンストラクタ,doCodeGen,getModuleの説明から開始します.
まずはコンストラクタです.今回のコード生成クラスではコンストラクタでIRBuilderを生成し,各コード生成メソッドで使用します.IRBuilderには複数のコンストラクタが用意されていますが,最も簡単なものを以下に示します.
llvm::IRBuilderのコンストラクタ
IRBuilder(LLVMContext &C, MDNode *FPMathTag = 0)
上記コンストラクタは第1引数としてLLVMContextを受け取ります.LLVMContextの説明は非常に難しいのですが,LLVM Coreのグローバルデータを管理,提供するクラスらしいです.何だか難しそうですが,実はllvm::getGlobalContext()によりLLVMのデフォルトのグローバルコンテキストが得られます.シングルスレッドである限りはgetGlobalContextを指定しておけばよかったりするので,とりあえずはおまじないみたいなものだと思ってgetGlobalContext()を使用していれば良いと思います.第2引数にはMDNodeを指定します.これは4章でお話したfpmath Metadata(ULP)を指定する引数で,デフォルト引数として0が与えられています.しかし今回はすべてint型とするので,今のところは明示的に指定しなくても問題ありません.なお,LLVMContextおよびllvm::getGlobalContextの宣言は“llvm/LLVMContext.h”に記載されています.以上から,IRBuilderの生成を行うCodeGenクラスのコンストラクタをリスト5.33に示します.
リスト5.33: CodeGenクラスのコンストラクタ
1: /**
2: * コンストラクタ
3: */
4: CodeGen::CodeGen(){
5: Builder = new IRBuilder<>(getGlobalContext());
6: Mod = NULL;
7: }
次にコード生成のトリガとなるdoCodeGenと生成したModuleを取得するgetModuleのコードを載せます.
リスト5.34: doCodeGen
1: /**
2: * コード生成実行
3: * @param TranslationUnitAST Module名(入力ファイル名)
4: * @return 成功時:true 失敗時:false
5: */
6: bool CodeGen::doCodeGen(TranslationUnitAST &tunit, std::string name) {
7: return generateTranslationUnit(tunit, name);
8: }
リスト5.35: getModule
1: /**
2: * Module取得
3: */
4: Module &CodeGen::getModule(){
5: if(Mod)
6: return *Mod;
7: else
8: return *(new Module("null", getGlobalContext()));
9: }
この二つのメソッドは特にLLVMのライブラリは関係してきませんので,そういう風に実装したんだなー程度に見てもらえれば良いです.doCodeGenは内部でgenerateTranslationUnitを呼び出し,その戻り値をそのまま返却します.getModuleはdoCodeGenにより生成したModule(LLVM IR)への参照を返却します.Moduleの生成に失敗している場合,もしくは未生成の場合は空のModuleを生成して返却します.Moduleクラスの生成については次の段落で説明しますので,ここでの説明は省略します.
generateTranslationUnitの役割はModuleを生成し,内包する関数のプロトタイプ宣言と関数定義の生成メソッドを呼び出すことです.
Module生成はllvm::Moduleクラスのコンストラクタを呼び出すことで実行します.Moduleクラスの宣言は“llvm/Module.h”に記載されており,コンストラクタは下記の様になっています.
llvm::Moduleのコンストラクタ
Module(StringRef ModuleID, LLVMContext &C)
第一引数のModuleIDにはModuleの名前を指定します.基本的に何を指定してもいいはずですが,とりあえずClangに倣って入力ソースコードのファイル名を入れておくのが良いと思います.第二引数はLLVMのコンテキストで,これはIRBuilderと同じくgetGlobalContext()を使用すれば良いです.Moduleを生成した後はTranslationUnitAST内に存在するPrototypeASTとFunctionASTをループで探索し,それぞれのコード生成メソッドを呼び出すだけです.Module生成を含めたgenerateTranslationUnitのコードを以下に示します.
リスト5.36: generateTranslationUnit
1: /**
2: * Module生成メソッド
3: * @param TranslationUnitAST Module名(入力ファイル名)
4: * @return 成功時:true 失敗時:false
5: */
6: bool CodeGen::generateTranslationUnit(TranslationUnitAST &tunit,
7: std::string name) {
8: // Moduleを生成
9: Mod = new Module(name, getGlobalContext());
10:
11: // funtion declaration
12: for (int i = 0;; i++) {
13: PrototypeAST *proto = tunit.getPrototype(i);
14: if (!proto)
15: break;
16: else if (!generatePrototype(proto, Mod)) {
17: SAFE_DELETE(Mod);
18: return false;
19: }
20: }
21:
22: // function definition
23: for (int i = 0;; i++) {
24: FunctionAST *func = tunit.getFunction(i);
25: if (!func)
26: break;
27: else if (!generateFunctionDefinition(func, Mod)) {
28: SAFE_DELETE(Mod);
29: return false;
30: }
31: }
32: return true;
33: }
generatePrototypeではPrototypeASTの情報からFunctionを生成しModuleに追加します.generatePrototypeはFunctionのボディは生成しませんので,空のFunction,つまりプロトタイプ宣言の生成を行います.
Functionの生成には“llvm/Function.h”に宣言されたFunctionクラスのCreateメソッドを使用します.Function::Createのインタフェースは下記の通りです.
llvm::Function::Create
static Function *Create(FunctionType *Ty, LinkageTypes Linkage, const Twine &N = "", Module *M = 0)
第1引数のFunctionTypeはFunctionの戻り値や引数リストを表すクラスです.FunctionTypeの生成については後述しますので,Function::Createの説明を続けます.第2引数はLinkageTypesを指定します.LinkageTypesはGlobalValueクラスに定義されている列挙型で様々な要素を含みますが,ここではExternalLinkageを指定します.第3引数は関数名の指定ですので,PrototypeASTのgetName()メソッドで関数名を取得して与えます.第4引数は生成したFunctionを追加するModuleクラスを指定します.
さて,先ほど後回しにしたFunctionTypeの説明に移ります.FunctionTypeの生成にはFunctionTypeクラスのgetメソッドを使用します.FunctionTypeの宣言は“llvm/DerivedTypes.h”にあり,FunctionType::getのインタフェースは下記の様になっています.
llvm::FunctionType::get
static FunctionType *get(Type *Result, ArrayRef<Type *> Params, bool isVarArg)
第1引数は戻り値の型をTypeで指定します.第2引数には引数の型を示すTypeを格納したベクタを与えます.第3引数は可変長引数であるかをbool型で指定します.なお,Typeの生成には複数の方法があるのですが,本書では以下に示すTypeクラスのメソッドを使用します.
llvm::Type::getInt32Ty
static IntegerType *getInt32Ty(LLVMContext &C)
Typeクラスの宣言は“llvm/Type.h”にあります.なお,DummyCは全てint型という制約を設けているのでTypeの生成は全てgetInt32Ty()メソッドを用いれば良いのですが,void型の場合はgetVoidTy,float型の場合はgetFloatPtrTyなどの他のメソッドを使用しなければなりません.以上をまとめたgeneratePrototypeをリスト5.37に示します.
リスト5.37: generatePrototype
1: /**
2: * 関数宣言生成メソッド
3: * @param PrototypeAST, Module
4: * @return 生成したFunctionのポインタ
5: */
6: Function *CodeGen::generatePrototype(PrototypeAST *proto, Module *mod) {
7: // already declared?
8: Function *func = mod->getFunction(proto->getName());
9: if (func) {
10: if (func->arg_size() == proto->getParamNum() && func->empty()) {
11: return func;
12: } else {
13: fprintf(stderr, "error::function %s is redefined",
14: proto->getName().c_str());
15: return NULL;
16: }
17: }
18:
19: // create arg_types
20: std::vector<Type *> int_types(proto->getParamNum(),
21: Type::getInt32Ty(getGlobalContext()));
22:
23: // create func type
24: FunctionType *func_type =
25: FunctionType::get(Type::getInt32Ty(getGlobalContext()), int_types, false);
26: // create function
27: func = Function::Create(func_type, Function::ExternalLinkage,
28: proto->getName(), mod);
29:
30: // set names
31: Function::arg_iterator arg_iter = func->arg_begin();
32: for (int i = 0; i < proto->getParamNum(); i++) {
33: arg_iter->setName(proto->getParamName(i).append("_arg"));
34: arg_iter++;
35: }
36:
37: return func;
38: }
冒頭でModuleクラスのgetFunctionメソッドを呼び出していますが,このメソッドは引数に与えた名前のFunctionがModule内に存在した場合,当該Functionを返却してくれます.このメソッドでFunctionが取得でき,なおかつModuleのemptyメソッドがtrueの時,つまりプロトタイプ宣言のみでまだボディがない時には取得したFunctionを返却するようにしています.それ以外のときは関数の再定義に該当するのでエラーメッセージを出力してNULLを返却します.末尾でFunctionの引数のイテレータarg_iterを辿り,setNameを行っていますが,この処理は各引数に対する変数名の設定です.本書の実装では引数に設定する名前は,PrototypeASTのgetParamnameメソッドで取得した変数名に明示的に“_arg”という接尾語をつけることにしています.
generateFunctionDefinitionは関数を生成します.Functionの生成は前述のgeneratePrototypeに任せ,生成されたFunctionに対してBasicBlockの追加,およびgenerateFunctionStatementによるボディの生成を行います.
BasicBlockはBasicBlockクラスのCreateメソッドで生成します.BasicBlockクラスの宣言は“llvm/BasicBlock.h”に記載されており,BasicBlock::Createのインタフェースは以下の様になっています.
llvm::BasicBlock::Create
static BasicBlock *
Create(LLVMContext &Context, const Twine &Name = "", Function *Parent = 0,
BasicBlock *InsertBefore = 0)
第1引数はLLVMContextです.これまで通りllvm:getGlobalContext()を使用すれば良いでしょう.第2引数はBasicBlockにつけるラベル名です.これは任意の名前をつければよいですが,Functionに最初に挿入するBasicBlockなので“entry”としておくのが良いでしょう.第3引数はBasicBlockを挿入するFunctionを指定します.第4引数はBasicBlockを挿入するFunction内の位置を指定します.デフォルトで0が指定されており,0の場合はFunctionの最後に,特定のBasicBlockが指定された場合はそのBasicBlockの前に挿入されます.
generateFunctionDefinitionでは,BasicBlockを生成した後,IRBuilderクラスのSetInsertPointメソッドで命令の挿入位置を指定しておきます.SetInsertPointはIRBuilderのスーパークラスであるIRBuilderBaseクラスで定義されているメソッドで,引数の異なる複数のSetInsertPointが存在します.今回使用するSetInsertPointメソッドのインタフェースは下記の通りです.
llvm::IRBuilderBase::SetInsertPoint
void SetInsertPoint(BasicBlock *TheBB)
SetInsertPointは引数で指定したBasicBlockの末尾に命令の挿入位置を設定します.以降,IRBuilderで命令を生成した場合ここで指定したBasicBlockの最後に命令が追加されていくことになります*7.
以上をまとめたgenerateFunctionDefinitionをリスト5.38に示します.
リスト5.38: generateFunctionDefinition
1: /**
2: * 関数定義生成メソッド
3: * @param FunctionAST Module
4: * @return 生成したFunctionのポインタ
5: */
6: Function *CodeGen::generateFunctionDefinition(FunctionAST *func_ast,
7: Module *mod) {
8: Function *func = generatePrototype(func_ast->getPrototype(), mod);
9: if (!func) {
10: return NULL;
11: }
12: CurFunc = func;
13: BasicBlock *bblock = BasicBlock::Create(getGlobalContext(), "entry", func);
14: Builder->SetInsertPoint(bblock);
15: // Functionのボディを生成
16: generateFunctionStatement(func_ast->getBody());
17:
18: return func;
19: }
generateFunctionStatementではFunctionのボディを生成します.とはいえ,実際にLLVM IRを生成するのは内部で呼び出すgenerateVariableDeclarationとgenerateStatementです.generateFunctionStatementで行うことは,引数で与えられたFunctionStmtASTが保持するVariableDeclASTと各種StatementのASTを探索し,上記コード生成メソッドの呼び出すことだけです.ただし,NullExprASTについてはコード生成をする必要がないので何もしないことにします.以上の内容を実装したgenerateFunctionStatementをリスト5.39に示します.
リスト5.39: generateFunctionStatement
1: /**
2: * 関数生成メソッド
3: * 変数宣言、ステートメントの順に生成
4: * @param FunctionStmtAST
5: * @return 最後に生成したValueのポインタ
6: */
7: Value *CodeGen::generateFunctionStatement(FunctionStmtAST *func_stmt) {
8: // insert variable decls
9: VariableDeclAST *vdecl;
10: Value *v=NULL;
11: for (int i = 0;; i++) {
12: // 最後まで見たら終了
13: if (!func_stmt->getVariableDecl(i))
14: break;
15:
16: // create alloca
17: vdecl = dyn_cast<VariableDeclAST>(func_stmt->getVariableDecl(i));
18: v = generateVariableDeclaration(vdecl);
19: }
20:
21: // insert expr statement
22: BaseAST *stmt;
23: for (int i = 0;; i++) {
24: // 最後まで見たら終了
25: stmt = func_stmt->getStatement(i);
26: if (!stmt)
27: break;
28: else if (!isa<NullExprAST>(stmt))
29: v = generateStatement(stmt);
30: }
31: return v;
32: }
generateVariableDeclarationではBasicBlockに変数宣言を追加します.
変数宣言の生成とはつまりAlloca命令の生成であり,IRBuilderクラスのCreateAllocaを用いて行います.CreateAllocaのインタフェースは下記の通りです.
llvm::IRBuilder::CreateAlloca
AllocaInst *CreateAlloca(Type *Ty, Value *ArraySize = 0, const Twine &Name = "")
第1引数は生成する変数の型を示すTypeです.今回は先ほど同様Type::getInt32Tyを使用します.第2引数は恐らく配列長を示すValueですが,DummyCでは配列をサポートしないので0を指定しておけば問題ありません.第3引数は変数名を指定しますので,VariableADeclASTのgetNameメソッドで変数名を取得して与えます.
なお,ASTの定義でも触れましたが,本書では関数の引数に対して,関数内の他の変数と同様にアクセスするために,引数とは別に関数内に引数を示す変数を宣言します.そのため,generateVariableDeclarationでは,VariableDeclASTの宣言種別(DeclType)がparamであったとき,つまり引数を明示的に関数内部に宣言する場合は,CreateAllocaで確保した変数に該当の引数の値を保存します.Alloca命令で確保した変数に対する値の保存はStore命令を使用します.IRBuilderのStore命令生成メソッドは下記の通りです.
llvm::IRBuilder::CreateStore
StoreInst *CreateStore(Value *Val, Value *Ptr, bool isVolatile = false)
CreateStoreの第1引数は格納する値を示すValueです.また,第2引数は格納先を示すValueです.ValueとはInstructionやGlobalValue,Functionの基底クラスで,LLVMの全てのInstructionは基本的にValueクラスの派生クラスとなっているはずです*8.ですので,ここでは第1引数にFunctionの引数を,第2引数にCreateAllocaで生成したAllocaInstを指定します.第3引数はvolatileかどうかを示すbool値ですので,基本的にはfalseで問題ないと思います.なお,引数を示すValueを取得する際にはFunctionのgetValueSymbolTableメソッド,およびValueSymbolTableのlookupメソッドを使用しています.FunctionのgetValueSymbolTableメソッドはその名の通りValueSymbolTable型の参照として該当Functionの識別子表を返却します.Functionから取得したValueSymbolTableのlookupメソッドは引数として変数名を与えてやることで,Function内の該当するValueを返却してくれます.
以上をまとめたgenerateVariableDeclarationをリスト5.40に示します.
リスト5.40: generateVariableDeclaration
1: /**
2: * 変数宣言(alloca命令)生成メソッド
3: * @param VariableDeclAST
4: * @return 生成したValueのポインタ
5: */
6: Value *CodeGen::generateVariableDeclaration(VariableDeclAST *vdecl) {
7: // create alloca
8: AllocaInst *alloca = Builder->CreateAlloca(
9: Type::getInt32Ty(getGlobalContext()), 0, vdecl->getName());
10:
11: // if alloca is used for function arguments
12: if (vdecl->getType() == VariableDeclAST::param) {
13: // store args
14: ValueSymbolTable &vs_table = CurFunc->getValueSymbolTable();
15: Builder->CreateStore(vs_table.lookup(vdecl->getName().append("_arg")),
16: alloca);
17: }
18: return alloca;
19: }
generateStatementではステートメントを生成します.generateStatementの処理は引数として与えられたBaseASTの種類を判別し,他のコード生成メソッドを呼び出すだけです.主な処理は外部メソッドなので,コードのみをリスト5.41に示して説明は省略します.
リスト5.41: generateStatement
1: /**
2: * ステートメント生成メソッド
3: * 実際にはASTの種類を確認して各種生成メソッドを呼び出し
4: * @param JumpStmtAST
5: * @return 生成したValueのポインタ
6: */
7: Value *CodeGen::generateStatement(BaseAST *stmt) {
8: if (isa<BinaryExprAST>(stmt)) {
9: return generateBinaryExpression(dyn_cast<BinaryExprAST>(stmt));
10: } else if (isa<CallExprAST>(stmt)) {
11: return generateCallExpression(dyn_cast<CallExprAST>(stmt));
12: } else if (isa<JumpStmtAST>(stmt)) {
13: return generateJumpStatement(dyn_cast<JumpStmtAST>(stmt));
14: } else {
15: return NULL;
16: }
17: }
generateBinaryExpressionは二項演算のコードを生成します.generateBinaryExpressionはBinaryExprASTを引数として受け取り,左辺値,右辺値のコード生成を順に実施し,最終的に代入文,もしくは四則演算命令を生成します.
まず,IRBuilderでの各種四則演算の生成メソッドを以下に示します.
llvm::IRBuilderの四則演算生成メソッド
Value *CreateAdd(Value *LHS, Value *RHS, const Twine &Name = "", bool HasNUW = false,
bool HasNSW = false)
Value *CreateSub(Value *LHS, Value *RHS, const Twine &Name = "", bool HasNUW = false,
bool HasNSW = false)
Value *CreateMul(Value *LHS, Value *RHS, const Twine &Name = "", bool HasNUW = false,
bool HasNSW = false)
Value *CreateSDiv(Value *LHS, Value *RHS, const Twine &Name= "",
bool isExact = false)
上から順に,加算命令,減算命令,乗算命令,除算命令の生成です.第1引数〜第3引数が示すものは全て同一で,それぞれ左辺値,右辺値,名前となっています.ここで指定した名前は命令の結果を格納するレジスタ名となります.加算命令〜乗算命令の第4引数,第5引数はNUW,NSWを有効にするかどうかのフラグで,除算命令の第4引数はExactを有効にするかのフラグです.NUW,NSWは4章で説明した様に,それぞれUnsigned,Signedでオーバーフローした時にPoison Valueになるというフラグで,ExactはLHSがRHSの倍数でないときにPoison Valueになるというフラグだそうです.
generateBinaryExpressionに渡されたBinaryExprASTが代入文であった場合,Store命令により代入文を生成します.Store命令は前述のメソッドで生成できますので,ここでの説明は省略します.なお,generateBinaryExpressionは左辺値,および右辺値を表すBaseASTの種類によって適宜他のコード生成メソッドを呼び出して必要な命令を生成するものとします.
以上の内容を利用したgenerateBinaryExpressionメソッドをリスト5.42に示します.
リスト5.42: generateBinaryExpression
1: /**
2: * 二項演算生成メソッド
3: * @param JumpStmtAST
4: * @return 生成したValueのポインタ
5: */
6: Value *CodeGen::generateBinaryExpression(BinaryExprAST *bin_expr) {
7: BaseAST *lhs = bin_expr->getLHS();
8: BaseAST *rhs = bin_expr->getRHS();
9:
10: Value *lhs_v;
11: Value *rhs_v;
12:
13: // assignment
14: if (bin_expr->getOp() == "=") {
15: printf("genStore\n");
16: // lhs is variable
17: VariableAST *lhs_var = dyn_cast<VariableAST>(lhs);
18: ValueSymbolTable &vs_table = CurFunc->getValueSymbolTable();
19: lhs_v = vs_table.lookup(lhs_var->getName());
20:
21: // other operand
22: } else {
23: // lhs=?
24: // Binary?
25: if (isa<BinaryExprAST>(lhs))
26: lhs_v = generateBinaryExpression(dyn_cast<BinaryExprAST>(lhs));
27:
28: // Variable?
29: else if (isa<VariableAST>(lhs))
30: lhs_v = generateVariable(dyn_cast<VariableAST>(lhs));
31:
32: // Number?
33: else if (isa<NumberAST>(lhs)) {
34: NumberAST *num = dyn_cast<NumberAST>(lhs);
35: lhs_v = generateNumber(num->getNumberValue());
36: }
37: }
38:
39: // create rhs value
40: if (isa<BinaryExprAST>(rhs)) {
41: rhs_v = generateBinaryExpression(dyn_cast<BinaryExprAST>(rhs));
42:
43: // CallExpr?
44: } else if (isa<CallExprAST>(rhs)) {
45: rhs_v = generateCallExpression(dyn_cast<CallExprAST>(rhs));
46:
47: // Variable?
48: } else if (isa<VariableAST>(rhs)) {
49: rhs_v = generateVariable(dyn_cast<VariableAST>(rhs));
50:
51: // Number?
52: } else if (isa<NumberAST>(rhs)) {
53: NumberAST *num = dyn_cast<NumberAST>(rhs);
54: rhs_v = generateNumber(num->getNumberValue());
55: }
56:
57: if (bin_expr->getOp() == "=") {
58: // store
59: return Builder->CreateStore(rhs_v, lhs_v);
60: } else if (bin_expr->getOp() == "+") {
61: // add
62: return Builder->CreateAdd(lhs_v, rhs_v, "add_tmp");
63: } else if (bin_expr->getOp() == "-") {
64: // sub
65: return Builder->CreateSub(lhs_v, rhs_v, "sub_tmp");
66: } else if (bin_expr->getOp() == "*") {
67: // mul
68: return Builder->CreateMul(lhs_v, rhs_v, "mul_tmp");
69: } else if (bin_expr->getOp() == "/") {
70: // div
71: return Builder->CreateSDiv(lhs_v, rhs_v, "div_tmp");
72: }
73: }
generateCallExpressionは関数呼び出し命令を生成するコード生成メソッドです.
関数呼び出し命令の生成はIRBuilderのCreateCallメソッドを使用します.CreateCallメソッドのインタフェースを以下に示します.
llvm::IRBuilder::CreateCall
CallInst *CreateCall(Value *Callee, ArrayRef<Value *> Args, const Twine &Name = "")
第1引数は呼び出し対象のFunctionを指定します.この値はModuleクラスのgetFunctionメソッドに関数名を指定することで取得できます.第2引数は引数として渡すValueです.std::vector型のベクタに引数のValueを詰め込んだものを渡します.第3引数には名前を指定します.ここで指定した名前は関数呼び出しの戻り値を格納するレジスタの名前に使用されます.なお,引数として渡すValueはCallExprASTから取得したBaseASTの種類によって適宜他のコード生成メソッドを呼び出して生成するものとします.
以上の内容を利用したgenerateCallExpressionをリスト5.43に示します.
リスト5.43: generateCallExpression
1: /**
2: * 関数呼び出し(Call命令)生成メソッド
3: * @param CallExprAST
4: * @return 生成したValueのポインタ
5: */
6: Value *CodeGen::generateCallExpression(CallExprAST *call_expr) {
7: std::vector<Value *> arg_vec;
8: BaseAST *arg;
9: Value *arg_v;
10: ValueSymbolTable &vs_table = CurFunc->getValueSymbolTable();
11: for (int i = 0;; i++) {
12: if (!(arg = call_expr->getArgs(i)))
13: break;
14:
15: // isCall
16: if (isa<CallExprAST>(arg))
17: arg_v = generateCallExpression(dyn_cast<CallExprAST>(arg));
18:
19: // isBinaryExpr
20: else if (isa<BinaryExprAST>(arg)) {
21: BinaryExprAST *bin_expr = dyn_cast<BinaryExprAST>(arg);
22: arg_v = generateBinaryExpression(dyn_cast<BinaryExprAST>(arg));
23: if (bin_expr->getOp() == "=") {
24: VariableAST *var = dyn_cast<VariableAST>(bin_expr->getLHS());
25: arg_v = Builder->CreateLoad(vs_table.lookup(var->getName()), "arg_val");
26: }
27: }
28:
29: // isVar
30: else if (isa<VariableAST>(arg))
31: arg_v = generateVariable(dyn_cast<VariableAST>(arg));
32:
33: // isNumber
34: else if (isa<NumberAST>(arg)) {
35: NumberAST *num = dyn_cast<NumberAST>(arg);
36: arg_v = generateNumber(num->getNumberValue());
37: }
38: arg_vec.push_back(arg_v);
39: }
40: return Builder->CreateCall(Mod->getFunction(call_expr->getCallee()), arg_vec,
41: "call_tmp");
42: }
generateJumpStatementではreturn文の生成を行います*9.generateJumpStatementは渡されたJumpStmtASTが含む戻り値を示すASTに応じてreturn文を生成します.
リターン文の生成はIRBuilderクラスのCreateRetメソッドを使用します.CreateRetメソッドのインタフェースは下記の通りです.
IRBuilder::CreateRet
ReturnInst *CreateRet(Value *V)
CreateRetは引数として戻り値を示すValueを与えるだけの簡単なメソッドです.CreateRetに渡すValueはJumpStmtASTが保持する戻り値を示すASTに応じて他のコード生成メソッドを呼び出して生成するものとします.
リスト5.44: generateJumpStatement
1: /**
2: * ジャンプ(今回はreturn命令のみ)生成メソッド
3: * @param JumpStmtAST
4: * @return 生成したValueのポインタ
5: */
6: Value *CodeGen::generateJumpStatement(JumpStmtAST *jump_stmt) {
7: BaseAST *expr = jump_stmt->getExpr();
8: Value *ret_v;
9: if (isa<BinaryExprAST>(expr)) {
10: ret_v = generateBinaryExpression(dyn_cast<BinaryExprAST>(expr));
11: } else if (isa<VariableAST>(expr)) {
12: VariableAST *var = dyn_cast<VariableAST>(expr);
13: ret_v = generateVariable(var);
14: } else if (isa<NumberAST>(expr)) {
15: NumberAST *num = dyn_cast<NumberAST>(expr);
16: ret_v = generateNumber(num->getNumberValue());
17: }
18: Builder->CreateRet(ret_v);
19: }
generateVariableは変数の参照に対するコード生成を行います.変数の参照とはすなわちLoad命令の生成です.
IRBuilderのLoad命令生成メソッドのインタフェースを以下に示します.
llvm::IRBuilder::CreateLoad
LoadInst *CreateLoad(Value *Ptr, const Twine &Name = "")
第1引数はLoadの対象となるValueです.ここにはValueSymbolTableからAllocaInstを取得して指定します.第2引数は名前を指定します.ここで指定した名前はLoadした値を格納するレジスタの名前になります.generateVariableは上記メソッドを使用してLoad命令を生成して返却するだけのメソッドです.実際のgenerateVariableの定義をリスト5.45に示します.
リスト5.45: generateVariable
1: /**
2: * 変数参照(load命令)生成メソッド
3: * @param VariableAST
4: * @return 生成したValueのポインタ
5: */
6: Value *CodeGen::generateVariable(VariableAST *var) {
7: ValueSymbolTable &vs_table = CurFunc->getValueSymbolTable();
8: return Builder->CreateLoad(vs_table.lookup(var->getName()), "var_tmp");
9: }
generateNumberは定数を表すValueを生成するメソッドです.引数に与えられた値を示すValueを生成して返却します.
LLVM IRの整数値の生成にはConstantIntクラスのgetメソッドを使用します.ConstantInt::getメソッドのインタフェースを以下に示します.
llvm::ConstantInt::get
static ConstantInt *get(LLVMContext &Context, const APInt &V)
第1引数はLLVMContextですので,今まで通りllvm:getGlobalContextを指定してやれば良いです.第2引数には生成する整数値の値を指定します.APIntという見慣れない型になっていますが,実際のところint型の値を与えてやれば問題ありません.
上記メソッドを使用したgenerateNumberの定義をリスト5.46に示します.
リスト5.46: generateNumber
1: /**
2: * 定数生成メソッド
3: * @param 生成する定数の値
4: * @return 生成したValueのポインタ
5: */
6: Value *CodeGen::generateNumber(int value) {
7: return ConstantInt::get(Type::getInt32Ty(getGlobalContext()), value);
8: }
ここまでの内容で字句解析〜コード生成までの各種モジュールを作成しました.しかしこれらを順序だてて呼び出すためのドライバ部分の実装が完了していません.本節では標準入力から入力ソースコード名を受け取り,これまでに作成した各種モジュールを呼び出す機能を実装します.さほど長い処理ではないので,今回はmain関数内に上記処理を記述していくことにします.
まず,標準入力から入力ソースコード,および出力ソースコードを受け取る機能を実装します.これはLLVMのコマンドライン ライブラリを使用しても良いのですが,今回は自分で実装することにしました.LLVMのコマンドライン ライブラリについては本章の最後で説明しますが,特に詳しく使用方法が知りたいという方はドキュメントのCommandLine 2.0 Library Manualを読むことをお勧めします.
本書で作成するDummyCコンパイラは以下の方針で入力ソースコード名と出力ソースコード名を受け取ることにします.
OptionParserクラスの宣言を以下に示します.OptionParserクラスのコンストラクタはmain関数のargcとargvをそのまま受け取るものとしています.オプションの切出しは外部からParseOptionメソッドを実行することで開始し,入出力ファイル名をメンバ変数に格納します.また,OptionParserクラスは切出した入出力ファイル名を取得するgetterメソッドを提供します.
リスト5.47: OptionParser
1: /**
2: * オプション切り出し用クラス
3: */
4: class OptionParser {
5: private:
6: std::string InputFilename;
7: std::string OutputFilename;
8: int Argc;
9: char **Argv;
10:
11: public:
12: OptionParser(int argc, char **argv) : Argc(argc), Argv(argv) {}
13: void printHelp() {
14: // ヘルプ表示
15: fprintf(stdout,
16: "Compiler for DummyC...\n 試作中なのでバグがあったらご報告を\n");
17: }
18: void printHelp();
19: std::string getInputFileName() { return InputFilename; } //入力ファイル名取得
20: std::string getOutputFileName() { return OutputFilename; } //出力ファイル名取得
21: bool parseOption(); //オプション切出しメソッド
22: };
OptionParserクラスのオプション切出しメソッドであるParseOptionの実装をリスト5.48に示します.
リスト5.48: ParseOption
1: /**
2: * オプション切り出し
3: * @return 成功時:true 失敗時:false
4: */
5: bool OptionParser::parseOption() {
6: if (Argc < 2) {
7: fprintf(stderr, "引数が足りません\n");
8: return false;
9: }
10:
11: for (int i = 1; i < Argc; i++) {
12: if (Argv[i][0] == '-' && Argv[i][1] == 'o' && Argv[i][2] == '\0') {
13: // output filename
14: OutputFilename.assign(Argv[++i]);
15: } else if (Argv[i][0] == '-' && Argv[i][1] == 'h' && Argv[i][2] == '\0') {
16: printHelp();
17: reutrn false;
18: } else if (Argv[i][0] == '-') {
19: fprintf(stderr, "%s は不明なオプションです\n", Argv[i]);
20: return false;
21: } else {
22: // inputfilename
23: InputFilename.assign(Argv[i]);
24: }
25: }
26:
27: // OutputFilename
28: std::string ifn = InputFilename;
29: int len = ifn.length();
30: if (OutputFilename.empty() && (len > 2) && ifn[len - 3] == '.' &&
31: ((ifn[len - 2] == 'd' && ifn[len - 1] == 'c'))) {
32: OutputFilename = std::string(ifn.begin(), ifn.end() - 3);
33: OutputFilename += ".ll";
34: } else if (OutputFilename.empty()) {
35: OutputFilename = ifn;
36: OutputFilename += ".ll";
37: }
38:
39: return true;
40: }
特に難しい処理はありませんが,念のために説明しておきます.ParseOptionは冒頭でまず引数の数を確認し,足りなければエラーメッセージを出力してfalseを返却します.次に,argvの値を一つずつ確認し,入力ファイル名,出力ファイル名,ヘルプ表示の何れかに該当すれば該当の処理を行います.最後に,出力ファイル名が指定されていなかった場合は,入力ファイル名から出力ファイル名を生成します.なお,DummyCファイルの拡張子は.dcとなっていると想定しています.末尾が.dcで終わるファイルについては入力ファイルの拡張子を.llに変更したものを出力ファイル名とし,それ以外のファイルは入力ファイル名の末尾に.llを付加したものを出力ファイル名とします.
ここでは,OptionParserの呼び出しを含む各種初期化と構文解析やコード生成の各種クラスの生成/メソッド呼び出しによるコンパイル,ファイル出力を実装します.ここでは初期化,作成した各種クラスのメソッド呼び出し,ファイル出力の3段階にわけて説明を進めます.
まず,各種初期化処理からはじめます.リスト5.49にmain関数の初期化部分のコードを記載します.
リスト5.49: 初期化
1: /**
2: * main関数
3: */
4: int main(int argc, char **argv) {
5: InitializeNativeTarget();
6: sys::PrintStackTraceOnErrorSignal();
7: PrettyStackTraceProgram X(argc, argv);
8: EnableDebugBuffering = true;
9:
10: OptionParser opt(argc, argv);
11: if (!opt.parseOption())
12: exit(1);
13:
14: ・・・省略・・・
15: }
5行目〜8行目は何れもLLVMのライブラリ関数や変数です.InitializeNativeTargetはプログラムを実行したホスト環境に合わせてネイティブターゲットを初期化します.sys::PrintStackTraceOnErrorSignalは致命的なシグナルが発生した時に,スタックトレースを出力します.PrettyStackTraceProgramはプログラムがクラッシュした際に指定された引数をスタックトレースとしてストリームに出力します.EnebleDebugBufferingはtrueにすると,バッファリングされたデバッグ出力をダンプするためにデバッグストリームにシグナルハンドラがインストールされるそうです*10.その後,先ほど作成したOptionParserクラスのインスタンスを生成して引数をパースしています.ちなみに,InitalizeNativeTarget,sys::PrintStackTraceOnErrorSignal,PrettyStackTraceProgram EnableDebugBufferingはそれぞれ,“llvm/Support/TargetSelect.h”,“lvm/Support/Signals.h”,“llvm/Support/PrettyStackTrace.h”,“llvm/Support/Debug.h”に記載されています.
次に,本書で作成した各種クラスを用いたコンパイル実行部分に移ります.リスト5.50にmain関数のコンパイル実行部分のコードを記載します.
リスト5.50: 作成した各種クラスによるコンパイル実行
1: /**
2: * main関数
3: */
4: int main(int argc, char **argv) {
5: ・・・初期化処理・・・
6:
7: // check
8: if (opt.getInputFileName().length() == 0) {
9: fprintf(stderr, "入力ファイル名が指定されていません\n");
10: exit(1);
11: }
12:
13: // lex and parse
14: Parser *parser = new Parser(opt.getInputFileName());
15: if (!parser->doParse()) {
16: fprintf(stderr, "err at parser or lexer\n");
17: SAFE_DELETE(parser);
18: exit(1);
19: }
20:
21: // get AST
22: TranslationUnitAST &tunit = parser->getAST();
23: if (tunit.empty()) {
24: fprintf(stderr, "TranslationUnit is empty");
25: SAFE_DELETE(parser);
26: exit(1);
27: }
28:
29: CodeGen *codegen = new CodeGen();
30: if (!codegen->doCodeGen(tunit, opt.getInputFileName())) {
31: fprintf(stderr, "err at codegen\n");
32: SAFE_DELETE(parser);
33: SAFE_DELETE(codegen);
34: exit(1);
35: }
36:
37: // get Module
38: Module &mod = codegen->getModule();
39: if (mod.empty()) {
40: fprintf(stderr, "Module is empty");
41: SAFE_DELETE(parser);
42: SAFE_DELETE(codegen);
43: exit(1);
44: }
45:
46: ・・・ファイル出力と終了処理・・・
47: }
最初にParserクラスのインスタンスを生成し,doParseメソッドで構文解析/意味解析を実行します.doParseが失敗した場合はエラーメッセージを出力して終了します.doParseが成功した場合はgetASTメソッドでASTを取得します.ここで,念のためにemptyメソッドでASTが空でない事を確認しておきます.ASTが取得できればCodeGenクラスのdoCodeGenメソッドでコード生成を実行します.doCodeGenが失敗した場合はエラーメッセージを出力して終了します.成功した場合はCodeGenクラスからgetModuleメソッドでModuleを取得し,Moduleが空でない事を確認します.
最後に,生成したLLVM IRのファイル出力を説明します.ファイル出力にはPrintModulePassクラスを用います.PrintModulePassは“llvm/Assembly/PrintModulePass.h”に宣言されているllvm::createPrintModulePass関数で生成します.llvm::createPrintModulePassのインタフェースは以下の通りです.
llvm::CreatePrintModulePass
ModulePass *llvm::createPrintModulePass(raw_ostream * OS,
bool DeleteStream = false,
const std::string &Banner = "")
まず,第1引数は出力ストリームの指定です.第2引数はこのパスを実行後に第一引数で渡したストリームをdeleteするかを指定するフラグで,trueを指定した場合はPrintModulePassのデストラクタにてストリームがdeleteされます.第3引数のBannerに文字列を指定すると,ストリームの先頭,つまり出力対象のModuleの前にBannerに指定した文字列が出力されます.ただし,第2,第3引数にはデフォルト引数が設定されていますので,通常は第1引数のストリームの指定だけで良いと思います.なお,第1引数に指定できるraw_ostreamの派生クラスは複数あるのですが,今回はファイル出力なのでraw_fd_streamを使用します.raw_fd_streamは“llvm/raw_ostream.h”に宣言されており,コンストラクタは以下の様になっています.
raw_fd_ostream
raw_fd_ostream(const char *Filename, std::string &ErrorInfo, unsigned Flags = 0)
第1引数は出力ファイル名を指定し,第2引数にはエラー情報を格納するstringを渡します.第3引数のFlagsでファイルをどのように開くかのオプションを指定できます.
生成したPrintModulePassはPassManagerのaddメソッドで登録し,runメソッドで適用します.以上の処理を記述したmain関数のファイル出力部分のコードをリスト5.51に記載します.
リスト5.51: ファイル出力と終了処理
1: /**
2: * main関数
3: */
4: int main(int argc, char **argv) {
5:
6: ・・・初期化処理とコンパイル・・・
7:
8: PassManager pm;
9:
10: //出力
11: std::string error;
12: raw_fd_ostream raw_stream(opt.getOutputFileName().c_str(), error);
13: pm.add(createPrintModulePass(&raw_stream));
14: pm.run(mod);
15: raw_stream.close();
16:
17: // delete
18: SAFE_DELETE(parser);
19: SAFE_DELETE(codegen);
20:
21: return 0;
22: }
本節ではここまで生成したファイルのコンパイルと動作確認を行います.なお,本節ではここまで説明してきたクラスや関数が表5.9に示すファイルに収められているものとして話を進めます.また,拡張子がcppのファイルはsrcディレクトリ,hppのファイルはincディレクトリに配置されているものとします.
表5.9: DummyCフロントエンドのファイル構成
| ファイル名 | 内容 |
|---|---|
| APP.hpp | 全ソースファイルで共通して使用するマクロが記述されたファイル |
| lexer.hpp | TokenクラスやTokenStreamクラスのクラス宣言等,字句解析関係の宣言が記述されたヘッダファイル |
| lexer.cpp | TokenクラスやTokenStreamクラスのメソッド定義およびLexicalAnalysisの定義が記述されたファイル |
| AST.hpp | 各種ASTクラスのクラス宣言が記述されたヘッダファイル |
| AST.cpp | 各種ASTクラスのメソッド定義が記述されたファイル |
| parser.hpp | Parserクラスのクラス宣言が記述されたヘッダファイル |
| parser.cpp | Parserクラスのメソッド定義が記述されたファイル |
| codegen.hpp | CodeGenクラスのクラス宣言が記述されたヘッダファイル |
| codegen.cpp | CodeGenクラスのメソッド定義が記述されたファイル |
| dcc.cpp | main関数およびOptionParserクラスが記述されたファイル |
ここまで作成したソースコードをコンパイルし,実行ファイルを生成します.ここで使用するのが第3章で紹介したllvm-configです.llvm-configは前述の通り,LLVMのライブラリを使用したプログラムのコンパイルに必要なオプションを出力するツールです.llvm-configを利用したコンパイル方法の例として,実際に投入するコンパイルコマンドを次に示します.
# githubからソースコードを取得 $ git clone git@github.com:Kmotiko/DummyCCompiler.git DummyCCompiler $ cd DummyCCompiler # チェックアウト $ git checkout llvm-3.2_ch5.9 # 一時ファイル用ディレクトリを作成 $ mkdir -p ./obj # dcc.cppを-cオプションでコンパイル $ g++ -g ./src/dcc.cpp -I./inc `llvm-config --cxxflags --ldflags --libs` \ -c -o ./obj/dcc.o # lexer.cppを-cオプションでコンパイル $ g++ -g ./src/lexer.cpp -I./inc `llvm-config --cxxflags --ldflags --libs` \ -c -o ./obj/lexer.o # AST.cppを-cオプションでコンパイル $ g++ -g ./src/AST.cpp -I./inc `llvm-config --cxxflags --ldflags --libs` \ -c -o ./obj/AST.o # parser.cppを-cオプションでコンパイル $ g++ -g ./src/parser.cpp -I./inc `llvm-config --cxxflags --ldflags --libs` \ -c -o ./obj/parser.o # codegen.cppを-cオプションでコンパイル $ g++ -g ./src/codegen.cpp -I./inc `llvm-config --cxxflags --ldflags --libs` \ -c -o ./obj/codegen.o # 実行ファイル用ディレクトリを作成 $ mkdir -p ./bin # 各オブジェクトファイルをリンクして実行ファイルdccを生成 $ g++ -g ./obj/dcc.o ./obj/lexer.o ./obj/AST.o ./obj/parser.o ./obj/codegen.o \ `llvm-config --cxxflags --ldflags --libs` -ldl -o ./bin/dcc
最終的に生成されるdccというファイルがDummyCフロントエンドの実行ファイルです.なお,DummyCCompilerをcloneするとMakefileも取得できますので,面倒な方はmakeコマンドを実行していただければコンパイルできるはずです.
生成したDummyCフロントエンドの動作確認を行います.以下のサンプルコードに対してコンパイルを実行します.
リスト5.52: DummyCのサンプルコード
1: int test(int j) {
2: int i;
3: i = j * 10;
4: return i;
5: }
6:
7: int main() {
8: int i;
9: i = 10;
10: test(i);
11: return 0;
12: }
DummyCCompilerディレクトリにて下記コマンドを投入して,DummyCフロントエンドでLLVM IRを生成します.なお,以降サンプルコードはsampleディレクトリ配下に配置されているものとします.
# dccでサンプルコードをコンパイル $ ./bin/dcc ./sample/test.dc -o ./sample/test.ll
出力としてtest.llにリスト5.53示す様なLLVM IRが得られていれば成功です.
リスト5.53: サンプルコードのLLVM IR表現
1: ; ModuleID = './sample/test.dc'
2:
3: define i32 @test(i32 %j_arg) {
4: entry:
5: %j = alloca i32
6: store i32 %j_arg, i32* %j
7: %i = alloca i32
8: %var_tmp = load i32* %j
9: %mul_tmp = mul i32 %var_tmp, 10
10: store i32 %mul_tmp, i32* %i
11: %var_tmp1 = load i32* %i
12: ret i32 %var_tmp1
13: }
14:
15: define i32 @main() {
16: entry:
17: %i = alloca i32
18: store i32 10, i32* %i
19: %var_tmp = load i32* %i
20: %call_tmp = call i32 @test(i32 %var_tmp)
21: ret i32 0
22: }
本節では,機能拡張としてmem2reg Passの適用とシステム関数の導入を行います.
前節でコード生成の確認は出来ましたが,生成されるLLVM IRはallocaやload,store命令を多用しているため少々汚いです.そこでmem2reg使ってメモリアクセスをレジスタに移動させます.mem2regという名前で指定するPassの実体は実はPromotePassクラスであり,PromotePassはllvm::createPromoteMemoryToRegisterPassで生成します.そこで,main関数内の処理を以下のように変更します.
リスト5.54: PromotePassの追加
1: /**
2: * main関数
3: */
4: int main(int argc, char **argv) {
5:
6: ・・・初期化処理・コード生成・・・
7:
8: PassManager pm;
9:
10: // mem2regをPassManagerに登録
11: pm.add(createPromoteMemoryToRegisterPass());
12:
13: // 出力
14: std::string error;
15: raw_fd_ostream raw_stream(opt.getOutputFileName().c_str(), error);
16: pm.add(createPrintModulePass(&raw_stream));
17: pm.run(mod);
18: raw_stream.close();
19:
20: ・・・終了処理・・・
21: }
追加した処理は11行目のPromotePassクラスを生成してPassManagerにaddする処理のみです.これでallocaやload,storeなどのメモリアクセス命令を可能な限りレジスタへ移動させることが出来ます.上記修正を施したファイルを再度コンパイルしてサンプルコードのコンパイルを実行してみてください.最終的にリスト5.55に示す様なコードが生成されていれば成功です.
リスト5.55: mem2reg適用後のLLVM
1: ; ModuleID = './sample/test.dc'
2:
3: declare i32 @printnum(i32)
4:
5: define i32 @test(i32 %j_arg) {
6: entry:
7: %mul_tmp = mul i32 %j_arg, 10
8: ret i32 %mul_tmp
9: }
10:
11: define i32 @main() {
12: entry:
13: %call_tmp = call i32 @test(i32 10)
14: ret i32 0
15: }
なお,createPromoteMemoryToRegisterPassの宣言は“llvm/Transforms/Scalar.h”に記載されています.
前節まででコード生成が出来ることを確認し,メモリアクセスをレジスタへ移動させました.しかし,出力がないので演算結果が正しいのかいまいちわかりません.そこで組込み関数として,数値を出力するprintnumを実装してみます.ここで,printnumはC言語の標準ライブラリ関数であるprintf関数をラップしただけの簡単な関数とします.printnumの実装方法は幾つか考えられましたが,今回は最も簡単に実装できそうな下記の方法を採用しました.
それでは,上記方針に従ってprintnumを実装します.
まず,printfをラップしたprintnum関数をC言語で記述し,ClangでLLVM IRに変換します.printnum関数の定義をリスト5.56に示します.
リスト5.56: printnumの定義
1: #include <stdio.h>
2:
3: int printnum(int i){
4: return printf("%d\n",i);
5: }
Clang でこれまでと同様にLLVM IRを生成します.折角フロントエンドが生成するLLVM IRにmem2regを適用しているので,今回はClangにも最適化オプションを使用します
$ clang -emit-llvm -S -O -o printnum.ll printnum.c
print.llとしてリスト5.57に示す様なLLVM IRが生成されているはずです.
リスト5.57: printnumのLLVM IR
1: ; ModuleID = 'lib/src/printnum.c'
2: target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32-n8:16:32-S128"
3: target triple = "i386-pc-linux-gnu"
4:
5: @.str = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1
6:
7: define i32 @printnum(i32 %i) nounwind {
8: entry:
9: %call = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([4 x i8]* @.str, i32 0, i32 0), i32 %i) nounwind
10: ret i32 %call
11: }
12:
13: declare i32 @printf(i8* nocapture, ...) nounwind
printnumの宣言を構文解析/コード生成で参照できるようvisitTranslationUnit内の処理をlist[fixvisitTranslationUnit]の様に修正し,printnumの宣言を表すASTを追加します.これにより,ASTにprintnumの宣言が追加され,最終的にDummyCフロントエンドによって生成されるLLVM IRにprintnumの宣言が追加されます.
リスト5.58: システム関数導入によるvisitTranslationUnitの修正
1: /**
2: * TranslationUnit用構文解析メソッド
3: * @return 解析成功:true 解析失敗:false
4: */
5: bool Parser::visitTranslationUnit() {
6: // 最初にprintnumの宣言追加
7: TU = new TranslationUnitAST();
8: std::vector<std::string> param_list;
9: param_list.push_back("i");
10: TU->addPrototype(new PrototypeAST("printnum", param_list));
11: PrototypeTable["printnum"] = 1;
12:
13: ・・・省略・・・
14: }
Parserクラスの修正が完了したら再度コンパイルしてフロントエンドを生成しなおして下さい.その上で,リスト5.59に示すDummyCのサンプルコードをコンパイルしましょう.
リスト5.59: printnumを含むDummyCのサンプルコード
1: int test(int j) {
2: int i;
3: i = j * 10;
4: return i;
5: }
6:
7: int main() {
8: int i;
9: i = 10;
10: printnum(test(i));
11: return 0;
12: }
出力としてリスト5.60の様なLLVM IRが生成されると思います.
リスト5.60: printnumを含むDummyCのLLVM IR
1: ; ModuleID = './sample/test.dc'
2:
3: declare i32 @printnum(i32)
4:
5: define i32 @test(i32 %j_arg) {
6: entry:
7: %mul_tmp = mul i32 %j_arg, 10
8: ret i32 %mul_tmp
9: }
10:
11: define i32 @main() {
12: entry:
13: %call_tmp = call i32 @test(i32 10)
14: %call_tmp1 = call i32 @printnum(i32 %call_tmp)
15: ret i32 0
16: }
このLLVM IRとprintnumのLLVM IRをllvm-linkでリンクさせ,lliで実行します.投入するコマンドを以下に示します.
$ llvm-link test.ll printnum.ll -S -o link_test.ll $ lli link_test.ll 100
100という出力が得られれば成功です.ちなみに,llvm-linkで生成したLLVM IRはリスト5.61の様になっています.
リスト5.61: リンク後のLLVM IR
1: ; ModuleID = 'test.ll'
2: target datalayout = "e-p:32:32:32-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:32:64-f32:32:32-f64:32:64-v64:64:64-v128:128:128-a0:0:64-f80:32:32-n8:16:32-S128"
3: target triple = "i386-pc-linux-gnu"
4:
5: @.str = private unnamed_addr constant [4 x i8] c"%d\0A\00", align 1
6:
7: define i32 @test(i32 %j_arg) {
8: entry:
9: %mul_tmp = mul i32 %j_arg, 10
10: ret i32 %mul_tmp
11: }
12:
13: define i32 @main() {
14: entry:
15: %call_tmp = call i32 @test(i32 10)
16: %call_tmp1 = call i32 @printnum(i32 %call_tmp)
17: ret i32 0
18: }
19:
20: define i32 @printnum(i32 %i) nounwind {
21: entry:
22: %call = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([4 x i8]* @.str, i32 0, i32 0), i32 %i) nounwind
23: ret i32 %call
24: }
25:
26: declare i32 @printf(i8* nocapture, ...) nounwind
なお,下記の様にllcを使用してアセンブリに変換して実行することも可能です.
$ llc -o link_test.s link_test.ll $ gcc -o link_test link_test.s $ ./link_test 100
これまでのお話で,ソースコードから中間表現を生成するというフロントエンドとしての最低限の機能を実装しました.ここでは一歩進んでフロントエンドにJITコンパイル機能を導入してみたいと思います.
JITコンパイルとはJust In Timeコンパイルのことで,実行時にコンパイルを行うことを意味します.JITはフロントエンドに実装すべき機能というわけではないですが,折角なのでここでその実装方法を確認しておきたいと思います.
LLVMでのJITコンパイルにはllvm::ExecutionEngineクラスを使用します.ただし,ExecutionEngineクラスはJITのインターフェースを定義した抽象クラスなので実際にはそのサブクラスであるllvm::Interpreter,llvm::JIT,llvm::MCJITの何れかのインスタンスを生成します.llvm::Interpreterはインタプリタ,llvm::JITはJIT,llvm::MCJITはMC(MachineCode)-JITのクラスです.MC(MachineCode)の話は主にバックエンドに関係するため,詳細は7章にて説明しますが,簡単に言うとMCはLLVM内で使用する中間表現の一種でLLVM IRに比べてよりターゲットアーキテクチャに近い表現になっています.MC-JITの概要についてはLLVM 3.4のドキュメント*11に含まれており,MCについてはLLVMのBlogエントリ*12に記載がありますので,興味のある方は一度読んでみることをお勧めします.なお,MLやBlogエントリの内容からすると,MC-JITはLazyモードをサポートしていないなど,既存のJIT(エントリ内ではOld JITなどと呼ばれています)の全ての機能を提供出来ているわけではない様です.
ExecutionEngineの生成はllvm::EngineBuilderクラスのcreateメソッドを使用すると容易です.EnginBuilder::createメソッドは実行するプラットフォームで利用可能なJITがあればそれを作成し,なければインタプリタを生成します.なお,lli内でもEngineBuilderを利用してExecutionEngineを生成し,コードを実行しています.
まずはEngineBuilderのコンストラクタを以下に示します.
llvm::ExecutionEngineのコンストラクタ
llvm::EngineBuilder::EngineBuilder(Module *m)
コンストラクタの引数はModuleクラスのポインタですので,JITを利用して実行したいModuleのポインタを渡せばよいです.
次に,EnginBuilder::createメソッドの定義を以下に示します.
llvm::EngineBuilderのcreateメソッド
ExecutionEngine *llvm::EngineBuilder::create()
createメソッドは引数をとらず,自動的にJIT,もしくはインタプリタを生成してExecutionEngine型のポインタとして返却します.EngineBuilderにはTargetMachineを引数にとるcreateメソッドも存在しますが,引数がないcreateメソッドは内部で自動的にターゲットを選択して引数ありのcreateメソッドを呼びますので基本的には引数なしのメソッドを使用すればよいでしょう.なお,ExecutionEngineおよびEngineBuilderは“llvm/ExecutionEngine/ExecutionEngine.h”に宣言されています.また,Interpreter/JIT/MCJITを使用する場合は,それぞれ“llvm/ExecutionEngine/Interpreter.h”,“llvm/ExecutionEngine/JIT.h”,“llvm/ExecutionEngine/MCJIT.h”をインクルードしておく必要があります.
ちなみにデフォルトではcreateメソッドは自動的にInterpreter,JITの何れかを生成しようとしますが,createメソッド呼び出し前にEngineBuilderクラスに用意されているsetEngineKindを使用することでExecutionEngineの種類を明示的に指定できます.setEngineKindの定義を以下に示します.
llvm::EngineBuilderのsetEngineKindメソッド
EngineBuilder &setEngineKind(EngineKind::Kind w)
EngineKind::Kindはenumで,要素にはJITとInterpreterがあります.createメソッドはInterpreterが指定されていた場合はInterpreterを,JITが指定されていた場合はJITの生成を試みます.(デフォルトではEitherとなっており,JIT→Interpreterの順で生成を試みます)
さらに,EngineBuilderのsetUseMCJITを使用することでMCJITの利用を明示的に指定できます.
llvm::EngineBuilderのsetUseMCJITメソッド
EngineBuilder &setUseMCJIT(bool Value)
setUseMCJITでtrueが指定され,EngineKindにJIT(もしくはEither)が指定されていた場合,createメソッドはMCJITの生成を試みるようです.
それではDummyCコンパイラへのJIT機能追加の第一歩として,ExecutionEngineの生成処理を追加してみましょう.今回はCodeGenクラスのdoCodeGenメソッドを改良してJITを実装することにします.
リスト5.62: ExecutionEngine生成を追加したdoCodeGen
1: /**
2: * 引数を追加してJITが出来るように変更
3: */
4: bool CodeGen::doCodeGen(TranslationUnitAST &tunit, std::string name,
5: bool with_jit = false) {
6: // Module生成に失敗したら終了
7: if (!generateTranslationUnit(tunit, name)) {
8: return false;
9: }
10:
11: // JITのフラグが立っていたらJIT
12: if (with_jit) {
13: // ExecutionEngine生成
14: ExecutionEngine *EE = EngineBuilder(Mod).create();
15:
16: ・・・最終的にこのあたりで関数を実行・・・
17: }
18:
19: return true;
20: }
これまではdoCodeGen内ではgenerateTranslationUnitを実行してModuleを生成し,その返り値をそのまま返却していました.しかし,今回はJITの実装に伴い,引数とExecutionEngineの生成処理を追加しています.
まず,第3引数にbool型の変数(with_jit)を追加し,JITを行うかのフラグを指定できるようにしました.デフォルトはfalseですが,trueを指定した場合にJITの処理を実行します.ExecutionEngineの生成は非常に簡単で,先ほど紹介したEngineBuilderをModuleを引数に与えて生成し,createメソッドを呼び出すだけです.createメソッドを利用してExecutionEngineを生成してしまえば既にJITの準備はできていますので,ExecutionEngineに用意されているメソッドを利用してプログラムを実行することができます.
ExecutionEngineには様々なメソッドが用意されていますが,今回はgetPointerToFunctionメソッドを使用することにします.ExecutionEngine::getPointerToFucntionによりJITコンパイル済みの関数へのポインタを取得できます.ExecutionEngine::getPointerToFucntionの定義は以下のとおりです.
llvm::ExecutionEngineのgetPointerToFunctionメソッド
void *JIT::getPointerToFunction(Function *F)
getPointerToFunctionの引数はFunction型のポインタです.実行したいFunctionのポインタを引数として渡すと,そのFunction(関数)を(必要があれば)コンパイルしてそのアドレスを返してくれます.帰ってきたポインタを関数ポインタに受けることで,目的のFunctionを実行できるというわけです.getPointerToFunctionメソッドを利用すれば任意の関数へのポインタを取得できるわけですが,今回はmain関数を取得して実行することにしたいと思います.
実際に使用する際のコードを見てみましょう.doCodeGenメソッドに関数ポインタ取得~実行までのコードを追加したものを以下に示します.
リスト5.63: ExecutionEngine生成を追加したdoCodeGen
1: /**
2: * 引数を追加してJITが出来るように変更
3: */
4: bool CodeGen::doCodeGen(TranslationUnitAST &tunit, std::string name,
5: bool with_jit = false) {
6: // Module生成に失敗したら終了
7: if (!generateTranslationUnit(tunit, name)) {
8: return false;
9: }
10:
11: // JITのフラグが立っていたらJIT
12: if (with_jit) {
13: llvm::ExecutionEngine *EE = llvm::EngineBuilder(Mod).create();
14: llvm::Function *F;
15:
16: // main関数へのポインタを取得
17: if (!(F = Mod->getFunction("main")))
18: return false;
19:
20: // JIT済みmain関数のポインタを取得
21: int (*fp)() = (int (*)())EE->getPointerToFunction(F);
22: fprintf(stderr, "%d\n", fp());
23: }
24:
25: return true;
26: }
13行目でExecutionEngineを生成した後,17行目でModuleクラスのgetFunctionメソッドを利用してmain関数のFunctionを検索します.getFunctionメソッドは引数としてあたえた文字列に一致する名前のFunctionをModule内から検索して返してくれます.このメソッドによりmain関数が見つかった場合は処理を継続し,取得したFunctionへのポインタを引数としてgetPointerToFunctionを実行します.結果を関数ポインタへ受け取ることでmain関数を実行できます.今回のmain関数は引数なしで返り値はint型なので,“int (*fp)()”としています.後は通常の関数のように実行するだけですが,今回はfprintfを利用してmain関数の返り値を出力しています.
doCodeGen側の対応は完了していますが,doCodeGenにJITコンパイルを指示するために呼び出し側であるmain関数にも手を加えます.実行時に“-jit”を指定するとJITコンパイルを行うようにしてみたいと思います.
リスト5.64: OptionParserクラスとmain関数の修正(JITの追加)
1: /**
2: * オプション切り出し
3: * @return 成功時:true 失敗時:false
4: */
5: bool OptionParser::parseOption() {
6: ・・・省略・・・
7:
8: for (int i = 1; i < Argc; i++) {
9: // 出力ファイル名取得
10: if (Argv[i][0] == '-' && Argv[i][1] == 'o' && Argv[i][2] == '\0') {
11: // output filename
12: OutputFilename.assign(Argv[++i]);
13:
14: // ヘルプ
15: } else if (Argv[i][0] == '-' && Argv[i][1] == 'h' && Argv[i][2] == '\0') {
16: printHelp();
17: reutrn false;
18:
19: // -jitが指定されたときはJITの有効フラグをtrueにする
20: } else if (Argv[i][0] == '-' && Argv[i][1] == 'j' && Argv[i][2] == 'i' &&
21: Argv[i][3] == 't' && Argv[i][4] == '\0') {
22: WithJit = true;
23:
24: // 不明なオプション
25: } else if (Argv[i][0] == '-') {
26: fprintf(stderr, "%s は不明なオプションです\n", Argv[i]);
27: return false;
28:
29: // 入力ファイル名取得
30: } else {
31: // inputfilename
32: InputFileName.assign(Argv[i]);
33: }
34: }
35:
36: ・・・省略・・・
37:
38: return true;
39: }
40:
41: /**
42: * JIT実行フラグ取得
43: */
44: bool OptionParser::getWithJit() { return WithJit; }
45:
46: /**
47: * main関数
48: */
49: int main(int argc, char **argv) {
50: ・・・省略・・・
51:
52: // コード生成実行時の引数を追加
53: CodeGen *codegen = new CodeGen();
54: if (!codegen->doCodeGen(tunit, opt.getInputFileName(), opt.getWithJit())) {
55: fprintf(stderr, "err at codegen\n");
56: SAFE_DELETE(parser);
57: SAFE_DELETE(codegen);
58: exit(1);
59: }
60:
61: ・・・省略・・・
62: }
まず,OptionParserクラスのparseOptionメソッドにJITの実行有無を取得する処理を追加しています.“-jit”が指定されたときはWithJitというbool型のメンバ変数にtrueを設定し,WithJitの値はgetWithJitメソッドで取得できるようにしました.main関数の変更点は,doCodeGen呼び出し時にこのメソッドの返り値を渡すようにしている点だけです.
追加した機能を確認するため,次のコマンドを投入し、リスト5.65 のコードをJIT コンパイルして実行します。GitHubからソースコードを取得している場合は、事前に“llvm-3.2_all.r2”をチェックアウトし、コンパイルしておいてください。“llvm-3.2_all.r2”のソースコードはJITと後述するLLVM IRのリンクに対応したものになっています。
リスト5.65: DummyCのサンプルコード
1: int main(){
2: return 0;
3: }
下記コマンドを投入し,リスト5.65のコードをJITコンパイルして実行します.
# dccでサンプルコードをコンパイル $ ./bin/dcc ./sample/test.dc -o ./sample/test.ll -jit 0
リスト5.65は0を返すだけの何もしないプログラムですので,出力として0が得られれば成功です.
第2章にてJITにはEagerとLazyという二つのモードがあるとお話しましたが, ここでExecuttionEgineのEagerとLazyの指定方法を説明しておきます.ExecutionEngineにはDisableLazyCompilationというメソッドが用意されており,このメソッドを使用することでEagerとLazyの切り替えが可能です.DisableLazyCompilationの定義を以下に示します.
llvm::ExecutionEngine::DisableLazyCompilation
void DisableLazyCompilation(bool Disabled = true)
引数にtrueを指定するとEagerとなり,falseを指定するとLazyになります. なお,デフォルトではtrue,つまりEagerとなっています.
前節までの内容でJITの機能を実装し,フロントエンドの中で入力ファイルのmain関数を呼び出すことに成功しました.しかし,今の状態ではprintnumのLLVM IRがリンクされていないためprintnumを含むプログラムをJITを利用して実行することができません.そこで,今度はJITコンパイルを行う前に外部ファイルに記述されたLLVM IRを結合してみることにします.
外部ファイルのLLVM IRをプログラム中で利用するには,まずファイルからLLVM IRを読み込む必要があります.LLVM IRの読み込みにはllvm::ParseIRFile関数を使用します.llvm::ParseIRFileは指定したファイルからLLVM IRを読み込む関数です.llvm::ParseIRFileの定義を以下に示します.
llvm::ParseIRFile
Module *ParseIRFile(const std::string &Filename, SMDiagnostic &Err, LLVMContext &Context)
ParseIRFileは第1引数にファイル名,第2引数にSMDiagnostic,第3引数にLLVMContextを渡します.SMDiagnosticクラスはエラー時の診断情報を格納するクラスのようですので,エラーが起きたときにはこのクラスの各種メソッドを利用して情報を取得できます.ParseIRFileは読み込みに成功すると読み込んだModuleへのポインタを返却しますので,自分で生成したModuleと同じようにプログラム中で利用可能になります.
プログラム中で複数のModuleを合成するにはllvm::LinkerクラスのLinkModulesメソッドを使用します.llvm::Linker::LinkModulesの定義は以下のとおりです.
llvm::Linker::LinkModulesメソッド
static bool LinkModules(Module *Dest, Module *Src, unsigned Mode, std::string *ErrorMsg)
LinkModulesの第1引数と第2引数はModule型のポインタとなっており,第1引数のModule(Dest)で指定されたModuleに第2引数のModule(Src)を結合します.第3引数は結合時のモードです.Linker::DestroySourceとLinker::PreserveSourceが指定でき,モードによってSrcで与えるModuleの扱いが変わるようです.第4引数にはエラーメッセージを格納するstringを渡します.なお,先ほどprintnumのLLVM IRをリンクするのに使用したllvm-linkも内部でllvm::Linkder::LinkModulesを利用しています.
それではDummyCフロントエンド内でLLVM IRを結合する機能を追加してみましょう.方針としては,実行時の引数でリンクしたいファイルを指定し,doCodeGenメソッドで実際にリンクを行うことにします.また,リンク用のメソッドをCodeGenクラスに用意し,doCodeGenメソッド内ではそのメソッドを呼び出すことにします.
それではまずはリンク用のメソッドをリスト5.66に示します.メソッド名はlinkModuleにしてみました.
リスト5.66: linkModule
1: /**
2: * Module結合用メソッド
3: */
4: bool CodeGen::linkModule(llvm::Module *dest, std::string file_name) {
5: SMDiagnostic err;
6: // Moduleの読み込み
7: llvm::Module *link_mod =
8: llvm::ParseIRFile(file_name, err, llvm::getGlobalContext());
9: if (!link_mod)
10: return false;
11:
12: // Moduleの結合
13: std::string err_msg;
14: if (llvm::Linker::LinkModules(dest, link_mod, llvm::Linker::DestroySource,
15: &err_msg))
16: return false;
17:
18: SAFE_DELETE(link_mod);
19:
20: return true;
21: }
linkModuleは結合先のModuleと結合対象のLLVM IRが書かれたファイルのパスを引数としてとることにします.linkModuleメソッドではまず結合対象のLLVM IRファイルをllvm::ParseIRFile関数を利用して読み込みます.読み込みに失敗した場合はfalseを返却しますが,成功した場合は8行目でLinkModulesメソッドを利用してModuleの結合を行います.
次にlinkModuleメソッドの呼び出しを追加したdoCodeGenメソッドをリスト5.67に示します.
リスト5.67: ExecutionEngine生成を追加したdoCodeGen
1: /**
2: * 外部Module結合機能を追加したdoCodeGenメソッド
3: */
4: bool CodeGen::doCodeGen(TranslationUnitAST &tunit, std::string name,
5: std::string link_file, bool with_jit = false) {
6: if (!generateTranslationUnit(tunit, name)) {
7: return false;
8: }
9:
10: // LinkFileの指定があったらModuleをリンク
11: if (!link_file.empty() && !linkModule(link_file))
12: return false;
13:
14: // JITのフラグが立っていたらJIT
15: if (with_jit) {
16: // ExecutionEngine生成
17: EE = EngineBuilder(Mod).create();
18: Function *F;
19:
20: // main関数のFunction取得
21: if (!(F = Mod->getFunction("main")))
22: return false;
23:
24: //コンパイル済みmain関数を取得して実行
25: int (*fp)() = (int (*)())EE->getPointerToFunction(F);
26: fprintf(stderr, "%d\n", fp());
27: }
28:
29: return true;
30: }
Moduleの結合を行うため,JIT実装後のdoCodeGenメソッドにさらに引数を追加し,結合対象のLLVM IRのファイル名を渡せるようにしています.8行目でファイル名が空でなければlinkModuleメソッドを呼び出してModuleの結合を試みます.結果が失敗だったときはfalseを返却して処理を終了しますが,成功した場合はさらにJITの処理に進むようにしています.
最後にOptionParserクラスとmain関数に修正を加えてリンクするファイルを実行時に指定できるように変更しましょう.実行時に引数として“-l”が指定された場合は,その次の単語を結合するLLVM IRのファイル名として認識することにします.修正後のソースコードをリスト5.68に示します.
リスト5.68: OptionParserクラスとmain関数の修正(リンク機能の追加)
1: /**
2: * オプション切り出し
3: * @return 成功時:true 失敗時:false
4: */
5: bool OptionParser::parseOption() {
6: ・・・省略・・・
7:
8: for (int i = 1; i < Argc; i++) {
9: // 出力ファイル名取得
10: if (Argv[i][0] == '-' && Argv[i][1] == 'o' && Argv[i][2] == '\0') {
11: // output filename
12: OutputFilename.assign(Argv[++i]);
13:
14: //ヘルプ
15: } else if (Argv[i][0] == '-' && Argv[i][1] == 'h' && Argv[i][2] == '\0') {
16: printHelp();
17: reutrn false;
18:
19: // -jitが指定されたときはJITの有効フラグをtrueにする
20: } else if (Argv[i][0] == '-' && Argv[i][1] == 'j' && Argv[i][2] == 'i' &&
21: Argv[i][3] == 't' && Argv[i][4] == '\0') {
22: WithJit = true;
23:
24: // -lオプションが指定されたときは次のワードをリンクするファイル名として取得
25: } else if (Argv[i][0] == '-' && Argv[i][1] == 'l' && Argv[i][2] == '\0') {
26: LinkFileName.assign(Argv[++i]);
27:
28: // 不明なオプション
29: } else if (Argv[i][0] == '-') {
30: fprintf(stderr, "%s は不明なオプションです\n", Argv[i]);
31: return false;
32:
33: // 入力ファイル名
34: } else {
35: // inputfilename
36: InputFileName.assign(Argv[i]);
37: }
38: }
39:
40: ・・・省略・・・
41:
42: return true;
43: }
44:
45: /**
46: *リンクするファイルを取得
47: */
48: std::string OptionParser::getLinkFileName() { return LinkFileName; }
49:
50: /**
51: * main関数
52: */
53: int main(int argc, char **argv) {
54: ・・・省略・・・
55:
56: // コード生成実行時の引数を追加
57: CodeGen *codegen = new CodeGen();
58: if (!codegen->doCodeGen(tunit, opt.getInputFileName(), opt.getLinkFileName(),
59: opt.getWithJit())) {
60: fprintf(stderr, "err at codegen\n");
61: SAFE_DELETE(parser);
62: SAFE_DELETE(codegen);
63: exit(1);
64: }
65:
66: ・・・省略・・・
67: }
まず,OptionParserクラスのparseOptionメソッドに結合対象のファイル名を取得する処理を追加しています.“-l”が指定されたときは次の単語をLinkFileNameに代入しています.LinkFileNameはstd::string型のメンバ変数で,OptionParser::getLinkFileNmaeメソッドで外部から取得できるようにしています.main関数で変更する箇所はdoCodeGenの引数のみです.第3引数に上記のgetLinkFileNameメソッドの返り値を与えています.これでdoCodeGenメソッドに必要な情報が渡り,LLVM IRの結合をしてくれるはずです.
printnumのリンクに成功していることを確認するため,リスト5.69のサンプルコードを使用して動作確認を行います.
リスト5.69: DummyCのサンプルコード
1: int test(int j){
2: printnum(i);
3: return 0;
4: }
5:
6: int main() {
7: int i;
8: i = 10;
9: test(i);
10: return 0;
11: }
下記コマンドを投入し,JITコンパイルを行います.
# dccにprintnumのLLVM IRを結合させてJITの処理をさせる # -l ./lib/printnum.llでリンクするLLVM IRのパスを指定 $ ./bin/dcc -o ./sample/test.ll -l ./lib/printnum.ll ./sample/test.dc -jit 10 0
今回はtest関数内でprintnumによる出力がありますので,10と0が出力されればリンクとJITコンパイルに成功しています.
DummyCには特にMetadataを埋め込む予定はありませんが,ここでMetadataの作成と命令への付与方法を紹介しておきます.
第4章にてMetadataにはMetadataStringとMetadataNodeがあると説明しましたが,LLVMのAPIではllvm::MDStringとllvm::MDNodeがそれに相当します.これらのMetadataのクラスの生成はllvm::MDBuilderクラスを利用すると簡単です.
llvm::MDBuilderクラスは“llvm/MDBuilder.h”に定義されているクラスで,fpmath Metadataやrange Metadataなど様々なMetadataを生成するメソッドを提供します.まずはMDBuilderクラスのコンストラクタを以下に示します.
llvm::MDBuilderのコンストラクタ
MDBuilder(LLVMContext &context)
MDBuilderクラスのコンストラクタはLLVMContextへの参照を渡すだけの非常に簡単なものです.いつも通りllvm::getGlobalContextを利用すればよいでしょう.
次に,MDBuilderで提供されているMetadata生成用のメソッドを紹介します.LLVM 3.2のMDBuilderクラスでは下記のメソッドが利用可能となっています.
LLVM 3.2のllvm::MDBuilderのMetadata生成メソッド
MDString *createString(StringRef Str) MDNode *createFPMath(float Accuracy) MDNode *createRange(const APInt &Lo, const APInt &Hi) MDNode *createAnonymousTBAARoot() MDNode *createTBAARoot(StringRef Name) MDNode *createTBAANode(StringRef Name, MDNode *Parent, bool isConstant = false) MDNode *createTBAAStructNode(ArrayRef<TBAAStructField> Fields) MDNode *createBranchWeights(uint32_t TrueWeight, uint32_t FalseWeight) MDNode *createBranchWeights(ArrayRef<uint32_t> Weights)
createXXという形式ですので,おおよそ何を生成するものかはメソッド名から推測可能だとは思いますが,簡単に概要を記載しておきます.上記のメソッドは上から順に,MetadataString,fpmath Metadata,range Metadata,tbaa MetadataのRootとtbaa MetadataのNode,tbaa.struct Metadata,そしてBranchWeight Metadataの生成用メソッドとなっています(TBAAは本書では説明していませんが,とりあえず紹介程度に記載しています).なお,Branch Weight Metadata は分岐命令に付与される Metadata で,分岐先の重み付けに使用する Metadata の様です.また,createRangeの引数となっているAPIntというクラスは任意の精度の整数定数を表現するクラスです.
ここで,MDBuilderクラスのメソッドを使用してMetadataを生成する際の簡単なコードを紹介します.まずは例としてrange Metadataの生成を行います.
リスト5.70: range Metadata生成のサンプルコード
1: //MDBuilderの生成 2: MDBuilder *MDB = new llvm::MDBuilder(llvm::getGlobalContext()); 3: 4: //0~100をあらわすrange Metadataの生成 5: llvm::MDNode *mdn = MDB->createRange(llvm::APInt(32,0),llvm::APInt(32,100));
まず,2行目でLLVMContextを渡してMDBuilderクラスを生成しています.そして5行目でcreateRangeメソッドを使用してrange Metadataを生成しています.なお,createRangeメソッドの引数となっているAPIntクラスのコンストラクタは下記のようになっています.
llvm::APIntのコンストラクタ
APInt(unsigned numBits, uint64_t val, bool isSigned = false)
第一引数で精度を指定し,第二引数で整数の値を指定します.また第三引数は符号あり/なしを指定する真偽値を指定しますが,デフォルトはfalseとなっています.今回の例では第一引数は32を指定して32bitの整数とし,第二引数には0と100をそれぞれ指定することで32bitの0および100をあらわす整数定数を生成しています.非常に簡単なコードですが,Metadataを生成するだけであれば上記のコードだけで完了です.
次にfpmath Metadata生成のコードをリスト5.71に示します.
リスト5.71: fpmath Metadata生成のサンプルコード
1: //MDBuilderの生成 2: MDBuilder *MDB = new llvm::MDBuilder(llvm::getGlobalContext()); 3: 4: //0.5 ULPのfpmath Metadataの生成 5: llvm::MDNode *mdn = MDB->createFPMath(0.5);
fpmathの生成ですのでcreateFPMathを使用するのですが,createFPMathの引数はULPの値を示す単精度浮動小数点数のみです.上記の例では0.5 ULPのfpmath Metadataを生成しています.なお,生成したMetadataは別のAPIを利用して命令へと付与するのですが,その詳細は次節以降に記述しています.
生成したMetadataを命令に付与する方法はいくつかありますが,llvm::InstructionクラスのsetMetadataメソッドを利用するのが最も基本的な方法です.LLVMの各種命令はllvm::Instructionクラスのサブクラスですので,任意のInstructionのインスタンスのsetMetadataを呼び出すことでMetadataを付与できます.setMetadataメソッドの定義は以下のとおりです.
llvm::Instruction::setMetadata
void setMetadata(unsigned KindID, MDNode *Node) void setMetadata(StringRef Kind, MDNode *Node)
llvm::Instructionには第1引数の型が異なる二つのsetMetadataがありますが,まず第1引数がunsignedのメソッドから説明します.このメソッドの第1引数のKindIDにはMetadataの種類を示す数値を指定し,第2引数には付与するMDNodeのポインタを渡します.第1引数のKindIDはLLVMContextクラスのgetMDKindメソッドで取得できます.
llvm::LLVMContext::getMDKind
unsigned LLVMContext::getMDKindID(StringRef Name)
getMDKindメソッドは引数としてMetadataの種類をあらわす文字列を受け取り,該当の種類のMetadataを示すIDを返却します.getMDKindで指定できる文字列は,dbg,tbaa,tbaa.struct,prof,fpmath,rangeの5種類となっています*14.次に第1引数がStringRef型の二つ目のsetMetadataですが,実はこのメソッドは内部でKindに与えられた文字列でgetMDKindメソッドを使用し,一つ目のsetMetadataを呼び出しているだけです.ですので,特に問題がなければこちらを利用するほうが簡単かもしれません.
setMetadataを利用して命令にMetadataを付与する際のサンプルコードをリスト5.72に示します.
リスト5.72: Metadata生成のサンプルコード
1: //MDBuilderの生成
2: MDBuilder *MDB = new llvm::MDBuilder(llvm::getGlobalContext());
3:
4: //range Metadataの生成
5: llvm::MDNode *mdn = MDB->createRange(llvm::APInt(32,0),llvm::APInt(32,100));
6:
7: //load_valをロードするload命令を生成
8: llvm::Instruction *inst = Builder->CreateLoad(load_val, "sample_var");
9:
10: //load命令へMetadataを付与
11: inst->setMetadata(llvm::getGlobalContext().getMDKindID("range"), mdn);
5行目までは前節で紹介したコードと同様ですが,8行目でCreateLoad命令を使用してload_valという変数名で示されるValueをロードするload命令を生成し,11行目で生成したload命令のsetMetadataを呼び出しています.第一引数でLLVMContextクラスのgetMDKindIDメソッドを利用しrange Metadataであることを指定し,第二引数で生成済みのMetadataクラスのインスタンスを渡しています.このコードを実行した場合,リスト5.73のようなLLVM IRが生成されます.
リスト5.73: 生成されるMetadata付きのLLVM IR
1: ・・・省略・・・
2: %sample_var = load i32* %i, !range !0
3: ・・・省略・・・
4: !0 = metadata !{i32 0, i32 100}
前節ではInstructionクラスのsetMetadataメソッドによるMetadataの付与方法を紹介しましたが,fpmath MetadataについてはIRBuilderのCreateFXX系のメソッドでMetadataを設定することができます.以下にfpmath Metadataを設定可能なIRBuilderクラスのメソッドを記載します.
fpmath Metadataを付与可能なllvm::IRBuilderクラスのメソッド
Value *CreateFAdd(Value *LHS, Value *RHS, const Twine &Name = "", MDNode *FPMathTag = 0) Value *CreateFSub(Value *LHS, Value *RHS, const Twine &Name = "", MDNode *FPMathTag = 0) Value *CreateFMul(Value *LHS, Value *RHS, const Twine &Name = "", MDNode *FPMathTag = 0) Value *CreateFDiv(Value *LHS, Value *RHS, const Twine &Name = "", MDNode *FPMathTag = 0) Value *CreateFRem(Value *LHS, Value *RHS, const Twine &Name = "", MDNode *FPMathTag = 0) Value *CreateFNeg(Value *V, const Twine &Name= "" , MDNode *FPMathTag = 0)
fpmath Metadataを付与するため上記はすべて浮動小数点演算に関する命令の生成メソッドで,上から順番に浮動小数点の加算,減算,乗算,除算,剰余,符号反転を行う命令を作成します.CreateFAddからCreateFremまではそれぞれfadd,fsub,fmul,fdiv,frem命令が生成されますが,FNegに関しては実際にはfsubによって符号逆転を行う命令となります.
なお,LLVM 3.2ではBranchWeights Metadataを生成するために,下記のメソッドもIRBuilderに用意されています.
BranchWeights Metadataを付与可能なllvm::IRBuilderクラスのメソッド
BranchInst *CreateCondBr(Value *Cond, BasicBlock *True, BasicBlock *False,
MDNode *BranchWeights = 0)
SwitchInst *CreateSwitch(Value *V, BasicBlock *Dest, unsigned NumCases = 10,
MDNode *BranchWeights = 0)
それでは今度はIRBuilderクラスのcreateFXX系メソッドを利用してMetadataを付与する際のサンプルコードを記載します.今回はIRBuilderクラスのcreateFSubメソッドを利用してfpmath Metadataを付与したfsub命令を生成します.
リスト5.74: createFXXメソッドを使用する際のサンプルコード
1: // MDBuilderの生成 2: MDBuilder *MDB = new llvm::MDBuilder(llvm::getGlobalContext()); 3: 4: // 0.5 ULPのfpmath Metadataの生成 5: llvm::MDNode *mdn = MDB->createFPMath(0.5); 6: 7: // float型のValueをロードするload命令を生成(BuilderはIRBuilderクラスのインスタンス) 8: llvm::Value *arg_v = Builder->CreateLoad(alloca, "float_tmp"); 9: 10: // Metadataを付与するfsub命令を生成(BuilderはIRBuilderクラスのインスタンス) 11: llvm::Value *tmp = Builder->CreateFSub( 12: arg_v, llvm::ConstantFP::get( 13: llvm::Type::getFloatTy(llvm::getGlobalContext()), 1.0), 14: "fpmath_sample", mdn);
今回も5行目までは前節のコードと同様ですが,8行目から処理が異なります.8行目ではCreateLoadメソッドでfloat型のValueをロードするload命令を生成しています.11行目ではIRBuilder::CreateFSubメソッドを利用して8行目でloadした値から1.0を引くためのfsub命令を生成しています.
最後に,リスト5.74のコードを実行した際に得られるLLVM IRをリスト5.75に示します.
リスト5.75: 生成されるfsub命令
1: %float = alloca float
2: %float_tmp = load float* %float
3: %fpmath_sample = fsub float %float_tmp, 1.000000e+00, !fpmath !0
4: ・・・省略・・・
5: !0 = metadata !{float 5.000000e-01}
これまで,DummyCフロントエンドのオプションは自前でOptionParserクラスを生成,実装してきたわけですが,実はLLVMにはコマンドラインオプションを取得するためのCommandLineライブラリという便利なライブラリが存在します.CommandLineライブラリの各種APIは“llvm/Support/CommandLine.h”に定義されており,ここで用意されているAPIを使用することで任意のオプションを非常に簡単にプログラムに追加することができます.
まずはCommandLineライブラリを使用したコマンドラインオプション実装の最も基本的な使い方を説明します.CommandLineライブラリでオプションを実装する場合,まずllvm::cl::optテンプレートやllvm::cl::listを利用してプログラムがどの様なオプションを受け付けるのかを宣言する必要があります.本節ではllvm::cl::opt,llvm::cl::listについて説明を行いますが,その使用方法は多岐に渡るため,実際のコード例とその実行結果を軸に紹介していきたいと思います.
最初に最も基本的な例として,“-o ファイル名”として出力ファイルを指定するオプションをllvm::cl::optで実装する方法を紹介したいと思います.リスト5.76に実装例を示します.
リスト5.76: llvm::cl::optの使用例
1: #include "llvm/Support/CommandLine.h"
2: #include <cstdio>
3:
4: namespace {
5: llvm::cl::opt<std::string> OutputFile(
6: "o",
7: llvm::cl::desc("出力ファイルを指定します"),
8: llvm::cl::value_desc("filename"),
9: llvm::cl::init("-")
10: );
11: }
12:
13: /**
14: * main関数
15: */
16: int main(int argc, char **argv) {
17: // 引数のパース
18: llvm::cl::ParseCommandLineOptions(argc, argv, "CommandLine Sample\n");
19:
20: // OutputFileの取得
21: fprintf(stderr, "output file : %s\n", OutputFile.c_str());
22:
23: return 0;
24: }
リスト5.76において,OutputFileはcl::optテンプレートにstd::stringを渡しています.これによりOutputFileオプションでパースするデータがstd::string型であることをライブラリに伝えています.ここにはstd::string以外も指定可能で,例えば何らかのフラグの有効無効を指定するようなオプションであれば,ここにbool型を指定することで真偽値として値を受け取ることができます.
次にOutputFileの宣言に記述する引数を確認します.まず第一引数に“o”を指定していますが,これはOuputFileオプションは“-o”で指定することを意味しています.指定する文字列を変更すれば異なる文字列をオプション名に使用することができます.
次に,第二引数としてllvm::cl::desc構造体を渡しています.これはoptで宣言するオプションに指定する属性の一つで,helpを表示した際に出力されるオプションの説明を定義しています.上記の例ではOutputFileは出力ファイルを指定するオプションですのでその旨を記載しています.
第三引数はllvm::cl::value_descとなっています.llvm::cl::value_desc構造体もオプションに指定する属性で,help表示においてこのオプションにどのような値を入力するかを示すのに利用します.今回は“-o”オプションではファイル名を指定するため,“filename”としています.後ほど実行結果を記載しますので,そこでどのような出力になるかを確認するとvalue_descの意味がわかりやすいと思います.最後に第四引数はllvm::cl::init構造体です.これもここまで同様オプションに指定する属性で,オプションの初期値を指定します.仮に実行時にこのオプションの値が指定されなかったときはここで指定した値が使用されることになります.リスト5.76のOutputFileでは“-”が初期値になります.
なお,引数の中で必須なのは第一引数で指定したオプションで使用する文字だけです.第二引数以降の指定は任意ですので,指定がなくても動作します.また,引数の順番についても特に制限はないため,記述する順番を入れ替えても同じように動作します.
llvm::cl::optの宣言によりオプションの定義はしましたが,実際にオプションとして値を受け取るためにはコマンドライン入力をパースしてやる必要があります.コマンドライン入力をパースするにはllvm::cl::ParseCommandLineOptions関数を使用します.llvm::cl::ParseCommandLineOptionsのインターフェースを以下に示します.
llvm::cl::ParseCommandLineOptions
void ParseCommandLineOptions(int argc, const char * const *argv, const char *Overview = 0);
引数名を見ればわかると思いますが,llvm::cl::ParseCommandLineOptionsにはmain関数で受け取るargcとargvを渡します.第三引数にはhelpを表示した際に出力される概要を指定できますが,デフォルト引数として0が設定されています.llvm::cl::ParseCommandLineOptionsを使用すると,パースして得られた値がllvm::cl::optで宣言しておいた各オプションに代入されます.
llvm::cl::ParseCommandLineOptionsで取得した各オプションの値にアクセスする方法は非常に簡単で,llvm::cl::optテンプレートに渡した型の通常の変数と同じようにllvm::cl::optの変数にアクセスするだけです.今回はllvm::cl::optにstd::stringを指定しておいたので,一般的なC++のstringと同じようにOutputFileを使用することができます.実際にリスト5.76の21行目でOutputFileに対してc_strメソッドを呼び,値を出力しています.
実行結果を確認するため,このコードをcltest.cppという名前で保存してコンパイル・実行してみます.
# cltest.cppという名前のソースコードだったとした場合のコンパイル $ g++ -g cltest.cpp `llvm-config --cxxflags --ldflags --libs` -ldl -pthread \ -o cltest # # -oオプションの確認 $ ./cltest -o test output file : test # # helpを表示 $ ./cltest -help OVERVIEW: CommandLine Sample USAGE: cltest [options] OPTIONS: -help - Display available options (-help-hidden for more) -o=<filename> - 出力ファイルを指定します -version - Display the version of this program
-oオプションで指定した値が正しく得られていることが確認できます.また,-helpオプションを指定して実行すると-oオプションのヘルプが表示され,OVERVIEWにはParseCommandLineOptionsの第三引数の値を出力されています.なお,helpを出力した際にあらわれるhelp,versionというオプションはライブラリ側で用意されているもので,ユーザプログラム側で特に指定しない場合もオプションとして用意されます.
出力ファイルのオプションができたので,今度は入力ファイルを受け取ることを考えてみましょう.入力ファイルも出力ファイルと同様入力から文字列を受け取るので基本的にOutputFileと変わりません.しかし入力ファイルは“-o”の様に明示的なオプションを指定するわけではありません.そんな時はllvm::cl::Positionalを使用します.実際の例をリスト5.77に示します.
リスト5.77: llvm::cl::optの使用例(入力ファイルを受け取る場合)
1: #include "llvm/Support/CommandLine.h"
2: #include <cstdio>
3: using namespace llvm;
4:
5: namespace {
6: llvm::cl::opt<std::string> InputFile(llvm::cl::Positional,
7: llvm::cl::desc("<input file>"),
8: llvm::cl::init("-"));
9: }
10:
11: /**
12: * main関数
13: */
14: int main(int argc, char **argv) {
15: // 引数のパース
16: llvm::cl::ParseCommandLineOptions(argc, argv, "CommandLine Sample\n");
17:
18: // InputFileの取得
19: fprintf(stderr, "input file : %s\n", InputFile.c_str());
20:
21: return 0;
22: }
InputFileの宣言時に第一引数としてllvm::cl::Positionalを指定していることに注目してください.llvm::cl::Positionalは該当オプションが“-o”の様なオプション名をとらず,単独で出現する場合に指定します.次ページに実行結果を記載しますので,llvm::cl::Positionalがどの様に扱われるかを確認しましょう.
# cltest.cppという名前のソースコードだったとした場合のコンパイル $ g++ -g cltest.cpp `llvm-config --cxxflags --ldflags --libs` -ldl -pthread \ -o cltest # # -oオプションの確認 $ ./cltest test input file : test # # helpを表示 $ ./cltest -help OVERVIEW: CommandLine Sample USAGE: cltest [options] <input file> OPTIONS: -help - Display available options (-help-hidden for more) -version - Display the version of this program
OutputFileではhelpを表示した際にOPTIONSの欄に概要が出力されていましたが,InputFileではllvm::cl::Positionalを使用したことでUSAGEに記述が追加されています.USAGEは使用方法を説明する項目で,ここではcltestを使用する際の構文はOPTIONS欄に記載されたオプションを指定し,最後にinput fileを指定することを表しています.ちなみに,USAGEに現れる<input file>はllvm::cl::descで指定した文字列です.
なお,コンパイラにおいて入力ファイルは実行に必要不可欠です.そんな時はllvm::cl::initで初期値を指定する代わりにllvm::cl::Requiredを指定することができます.llvm::cl::Requiredを指定するとそのオプションは必須項目として扱われ,必ず一度だけ指定されるオプションとしてライブラリに認識されます.llvm::cl::Requiredを指定したオプションにパラメータが与えられなかった場合はParseCommandLineOption実行時に自動的にエラー終了してくれます.
なお,CommandLineライブラリではllvm::cl::Required以外にもオプションの出現回数がenumで定義されています.指定可能な出現回数のenum値一覧を表5.10に示します.
表5.10: 指定可能なオプション出現回数
| 名前 | 概要 |
|---|---|
| llvm::cl::Optional | 0回もしくは1回指定される.llvm::cl::optのデフォルト |
| llvm::cl::ZeroOrMore | 0回以上指定される.後述するllvm::cl::listのデフォルト |
| llvm::cl::Required | 必ず1度だけ指定される. |
| llvm::cl::OneOrMore | 1回以上指定される. |
| llvm::cl::ConsumeAfter | 最後に出現したcl::Positionalなオプションより後ろに出現する. |
ここまでで文字列を受け取るオプションについて説明をしましたが,DummyCフロントエンドのJIT有効/無効のフラグの様にオプションで真偽値の管理を行いたい場合があります.
オプションで真偽値を管理するのは文字列を扱うよりも非常に単純に実装できます.例としてJITの有効/無効を指定するオプションの実装例をリスト5.78に示します.
リスト5.78: llvm::cl::optの使用例(真偽値を管理する場合)
1: #include "llvm/Support/CommandLine.h"
2: #include <cstdio>
3:
4: namespace {
5: // JIT有効無効を示すオプション
6: llvm::cl::opt<bool> WithJit("jit",
7: llvm::cl::desc("JIT コンパイルを有効にします"),
8: llvm::cl::init("false"));
9: }
10:
11: /**
12: * main関数
13: */
14: int main(int argc, char **argv) {
15: // 引数のパース
16: llvm::cl::ParseCommandLineOptions(argc, argv, "CommandLine Sample\n");
17:
18: // JITフラグの取得
19: if (WithJit)
20: fprintf(stderr, "enabled jit \n");
21:
22: return 0;
23: }
真偽値を管理する場合はllvm::cl::optテンプレートにbool型を指定します.また,オプションで指定されなかった時のためにllvm::cl::initで初期値を決めておいた方が良いでしょう.あとはこれまでと同様で,オプション名やhelpで表示する説明などを属性として指定するだけです.真偽値へのアクセス方法もこれまで同様で,通常のbool型変数と同じように使用できます.今回はオプション名を“jit”としているのため,“-jit”が指定されているとWithJitの値がtrueとなります.
念のため,実行結果を確認しておきましょう.
# cltest.cppという名前のソースコードだったとした場合のコンパイル $ g++ -g cltest.cpp `llvm-config --cxxflags --ldflags --libs` -ldl -pthread \ -o cltest # # -jitオプションの確認 $ ./cltest -jit enabled jit # # helpの表示 $ ./cltest -help OVERVIEW: CommandLine Sample USAGE: cltest [options] OPTIONS: -help - Display available options (-help-hidden for more) -jit - JITコンパイルを有効にします -version - Display the version of this program
OPTIONSに“-jit”オプションが追加されていることが確認できます.また,“-jit”を指定した場合にはWithJitがtrueとなっている様です.
最適化レベルを指定する際など,複数の値からどれか一つを選択してもらう様なオプションをとりたい場合があります.そんな時はllvm::cl::valuesとclEnumVal,clEnumValNマクロを使用します.
複数のパラメータからオプションを選択する場合の例として,最適化レベルオプションの実装例をリスト5.79に示します.
リスト5.79: llvm::cl::optの使用例(複数パラメータから選択する場合)
1: #include "llvm/Support/CommandLine.h"
2: #include <cstdio>
3:
4: namespace {
5:
6: //最適化レベルを示すenum
7: enum OptLevel {
8: NONE,
9: O1,
10: O2,
11: O3
12: };
13:
14: //オプション定義
15: llvm::cl::opt<OptLevel>
16: OLevel(llvm::cl::values(clEnumValN(NONE, "O0", "最適化しません"),
17: clEnumVal(O1, "基本的な最適化"),
18: clEnumVal(O2, "積極的な最適化"),
19: clEnumVal(O3, "さらに積極的な最適化"), clEnumValEnd),
20: llvm::cl::desc("最適化レベルの選択"));
21: }
22:
23: /**
24: * main関数
25: */
26: int main(int argc, char **argv) {
27: // 引数のパース
28: llvm::cl::ParseCommandLineOptions(argc, argv, "CommandLine Sample\n");
29:
30: // 最適化レベルの取得
31: if (OLevel == O0)
32: fprintf(stderr, "disable Optimization\n");
33:
34: return 0;
35: }
複数のパラメータから一つを選択するようなオプションを実装する場合,まずその選択肢を示すenumを定義しておく必要があります.上記の例ではOptLevelというenumで最適化レベルの種類を定義しています.ここで定義したenumをllvm::cl::optテンプレートで指定することで値の選択が可能になります.
さらにllvm::cl::optに渡す引数もこれまでとは異なり,llvm::cl::valuesを使用します.llvm::cl::valuesは入力文字列を値に結びつけるためのテンプレートです.llvm::cl::valuesの引数はclEnumValマクロ,もしくはclEnumValNマクロで記述します.clEnumValマクロは第一引数にenumを指定し,第二引数にそのオプションの説明をあらわす文字列を渡します.clEnumValでは第一引数で指定したenumの名前がオプション名として使用されます.clEnumValNオプションもclEnumValとよく似たマクロですが,こちらはenumの名前とオプション名が異なる際に使用します.clEnumValNは第一引数にenumをとり,第二引数にオプション名を指定します.第三引数は該当オプションの説明となります.なお,llvm::cl::valuesの引数の最後には必ずclEnumValEndを指定しなければならないことに注意してください.
それでは実行結果の確認をしておきましょう.
# cltest.cppという名前のソースコードだったとした場合のコンパイル
$ g++ -g cltest.cpp `llvm-config --cxxflags --ldflags --libs` -ldl -pthread \
-o cltest
#
# 最適化オプションの確認
$ ./cltest -O0
disable Optimization
#
# helpの表示
$ ./cltest -help
OVERVIEW: CommandLine Sample
USAGE: cltest [options]
OPTIONS:
最適化レベルの選択
-O0 - 最適化しません
-O1 - 基本的な最適化
-O2 - 積極的な最適化
-O3 - さらに積極的な最適化
-help - Display available options (-help-hidden for more)
-version - Display the version of this program
llvm::cl::valuesを使用した場合,llvm::cl::optに渡したllvm::cl::descの内容がトップレベルに表示され,その下にllvm::cl::valuesに渡した各オプション名とその説明が出力されます.今回の例では各最適化オプションのオプション名と説明が記載されていることがわかると思います.
DummyCにおいては入力ファイルは一つのみとしていますが,場合によっては複数のファイルを受け取りたい場合があるでしょう.そのような,一つのオプションに複数の値を受け取りたい場合のCommandLineライブラリの使用方法を紹介します.
一つのオプションに複数の値を受け取るときはllvm::cl::listを使用します.例として入力ファイルを複数受け取る場合の実装例をリスト5.80に示します.
リスト5.80: llvm::cl::optの使用例(複数パラメータから選択する場合)
1: #include "llvm/Support/CommandLine.h"
2: #include <cstdio>
3:
4: namespace {
5: //入力ファイルのオプション定義(複数バージョン)
6: llvm::cl::list<std::string> InputFiles(llvm::cl::Positional,
7: llvm::cl::desc("<input files>"),
8: llvm::cl::OneOrMore);
9: }
10:
11: /**
12: * main関数
13: */
14: int main(int argc, char **argv) {
15: // 引数のパース
16: llvm::cl::ParseCommandLineOptions(argc, argv, "CommandLine Sample\n");
17:
18: // 入力ファイル名の取得
19: for (int i = 0; i < InputFiles.size(); i++)
20: fprintf(stderr, "input file : %s\n", InputFiles[i].c_str());
21:
22: return 0;
23: }
llvm::cl::listの使い方は基本的にllvm::cl::optと同様です.異なる点は,llvm::cl::OneOrMoreを使用しているところでしょうか.llvm::cl::OneOrMoreはこのオプションに必ず一つ以上のパラメータが渡されることを意味しています.llvm::cl:listは複数の値を扱うため,llvm::cl::Requiredの代わりにllvm::cl::OneOrMoreを使用しています.
オプションの定義はllvm::cl::optと似ていますが,入力をパースして得られる結果が少し違います.llvm::cl::listはその名の通り複数のパラメータを受け取るため,パースした結果にも複数の値が格納されています.例えば今回の例ではInputFilesには複数の値が格納されているため,“vector<std::string>”のように振舞います.実際に上記のコード例ではInputFilesのサイズを確認し,i番目に格納された値を取得する処理を行っています.
それでは今回も実行結果を確認しておきましょう.
# cltest.cppという名前のソースコードだったとした場合のコンパイル $ g++ -g cltest.cpp `llvm-config --cxxflags --ldflags --libs` -ldl -pthread \ -o cltest # # 入力ファイルオプションの確認 $ ./cltest foo bar input file : foo input file : bar # # helpの表示 $ ./cltest -help OVERVIEW: CommandLine Sample USAGE: cltest [options] <input files> OPTIONS: -help - Display available options (-help-hidden for more) -version - Display the version of this program
helpの出力内容には変化がありませんが,複数の入力ファイルが認識されていることが確認できます.
本節では実例をもとにCommandLineライブラリを使用したオプションの実装方法を紹介しました.最後に,CommandLineライブラリのオプションに指定できる属性を表5.11にまとめておきます.本書では紹介していないものもありますが,詳細についてはLLVMの公式ドキュメント,CommandLine 2.0 Library Manual*15をご覧下さい.
表5.11: cl::optに指定可能な属性
| 属性 | 概要 |
|---|---|
| llvm::cl::desc | 該当オプションにhelpにて出力する説明を付与するために使用する. |
| llvm::cl::value_desc | 該当オプションにhelpにて出力するオプションで指定する値の説明を付与するために使用する. |
| llvm::cl::init | 該当オプションの初期値をあらわす. |
| llvm::cl::location | 入力をパースして得た該当オプションのパラメータを保存する変数を指定するために使用する. |
| llmv::cl::aliasopt | 他のオプションへのエイリアスを設定するために使用する. |
| llvm::cl::values | 複数のパラメータをオプションに受け取る際に使用する. clEnumValマクロとclEnumValNマクロでオプションを設定する. |
| llvm::cl::multi_val | 一つのオプションに複数の値をとりたい時(-オプション名 値1 値2 値3 の様な形)に使用する. llvm::cl::listでのみ使用可能. |
[*1] とは言えコード生成の前提となる話を意味解析まででやっているので簡単に目を通した方が良いかもしれません
[*2] http://llvm.org/docs/tutorial/index.html
[*3] EBNFの規則守れてなかったらごめんなさい!
[*4] LLVM Tutorialではdynamic_castを使っていたりしますが
[*5] そもそも構文解析の時点でそういう前提で話を進めていましたが
[*6] LLVM 3.1ではllvm/Support/IRBuilder.hになっています
[*7] 他にもInstruction単位で命令の挿入位置を設定するSetInsertPointもあります
[*8] 少々曖昧な表現をしているのは筆者も全てのInstructionの定義を覚えているわけではないからです
[*9] 一応今後の拡張で分岐命令なども考えてはいるのですが
[*10] このあたりはおまじないと思ってもよいでしょう
[*11] http://llvm.org/releases/3.4/docs/MCJITDesignAndImplementation.html
[*12] http://blog.llvm.org/search/label/MC
[*13] 正確には0と1
[*14] BranchWeights Metadataはprofに分類されるようです
[*15] http://llvm.org/docs/CommandLine.html