最近在对LLVM IR起了兴趣,应为它可以将它支持的所有编程语言都转换成同一个IR语言。LLVM IR生成步骤有三步,首先是词法解析,然后是语法分析生成AST,然后AST可以通过代码生成,得到IR。我是跟着LLVM的官方教程学习的,这个教程通过一个很简单的,虚构的编程语言Kaleidoscope来教你一步步实现自己的编程语言以及对应的编译器。
Kaleidoscope语言
Kaleidoscope是一个程序性语言,简单起见,我们规定Kaleidoscope中的数字类型仅有双精度浮点数类型,因此也无需进行类型声明。这样Kaleidoscope就有了很简单的语法,下面的例子是Kaleidoscope写成的斐波那契数列函数。
1 2 3 4 5 6 7 8 9
| # Compute the x'th fibonacci number. def fib(x) if x < 3 return 1 else return fib(x-1)+fib(x-2)
# This expression will compute the 40th number. fib(40)
|
此外,我们还允许Kaleidoscope调用标准库中的函数,之后的LLVM JIT会使实现这个功能变得非常简单。Kaleidoscope通过关键字extern
在使用之前定义一个函数(在手动定义递归函数中也有用),下面是一个例子。
1 2 3 4 5 6
| # 声明三个标准库中的函数 extern sin(arg); extern cos(arg); extern atan2(arg1 arg2);
atan2(sin(.4), cos(42))
|
这里我们可以基本上理解了Kaleidoscope语言的基本语法,下面就开始实现这个语言的第一步,词法分析器。
词法分析器,Lexer
首先介绍一下编译器编译代码的第一步,词法分析器,Lexer。
词法分析器处理编程语言的代码文本,并理解这些文本表达了什么。词法分析器lexer(也称为scanner),将代码文本识别成一系列token,每个token被识别为一个编程语言中的定义,例如关键字,标识符,literal等。每个token包含一个token的代码以及可能的一些信息(例如,如果该token是一个数字,那么包含的信息还有这个数字的值)。
下面,我们将所有可能出现的token类型进行枚举。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
enum Token { tok_eof = -1,
tok_def = -2, tok_if = -3, tok_else = -4, tok_return = -5,
tok_identifier = -6, tok_number = -7,
tok_comment = -8 };
static std::string identifier_str; static double num_val; static std::string comment_line;
|
Lexer返回的每个token只有两种情况,一是上面枚举类型中存在的负数,二是不能识别的字符,例如”+”,这时lexer返回的是该字符的ASCII码值,范围为0-255。
如果识别到标识符identifier,那么我们将该标识符的名称赋值给identifier_str
。如果识别到数字,那么我们将该数字表示的值赋值给num_val
。如果识别到注释行,那么我们将注释的字符串赋值给comment_line
。注意这里我们简单起见,使用的是全局变量来储存这些值,但这并不是一个好的选择,在真实的编译器实现中,显然不会用这个函数。
Lexer的实现是通过一个函数get_tok
实现的,它持续读取代码文本的下一个字符,直到识别到下一个token并返回。下面我们通过一个简单的实现对这个函数进行解释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| std::ifstream fin;
int get_tok() { static int last_char = ' '; while (isspace(last_char)) last_char = fin.get();
if (isalpha(last_char)) { identifier_str = last_char; last_char = fin.get(); while (isalnum(last_char) || last_char == '_') { identifier_str += last_char; last_char = fin.get(); }
if (identifier_str == "def") return tok_def; else if (identifier_str == "if") return tok_if; else if (identifier_str == "else") return tok_else; else if (identifier_str == "return") return tok_return; return tok_identifier; }
if (isdigit(last_char) || last_char == '.') { std::string num_str; do { num_str += last_char; last_char = fin.get(); } while (isdigit(last_char) || last_char == '.'); num_val = strtod(num_str.c_str(), nullptr); return tok_number;
}
if (last_char == '#') { comment_line = last_char; do { last_char = fin.get(); comment_line += last_char; } while (last_char != EOF && last_char != '\n' && last_char != '\r'); return tok_comment; }
if (last_char == EOF) return tok_eof;
int this_char = last_char; last_char = fin.get(); return this_char; }
|
到这里我们就完成了一个简单的lexer的核心,get_tok函数。我们在外面再写一个驱动,用于循环调用get_tok函数,并接受它的输出,对它识别到的每一个token进行我们需要的处理,下面我们写一个简单的函数,将get_tok函数读取到的token输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| int main() { fin.open("fibonacci.ks", std::ios::in); if (!fin.is_open()) { std::cout << "Cannot open file 'fibonacci.ks'." << std::endl; return 1; } while (true) { int res = get_tok(); if (res == tok_def || res == tok_if || res == tok_else || res == tok_return) { std::cout << "Keywords: " << identifier_str << std::endl; } else if (res == tok_identifier) { std::cout << "Identifier: " << identifier_str << std::endl; } else if (res == tok_number) { std::cout << "Number: " << num_val << std::endl; } else if (res == tok_comment) { std::cout << "Comment line: " << comment_line << std::endl; } else if (res == tok_eof) { std::cout << "End of file" << std::endl; } else { std::cout << "Unknown char: " << char(res) << ", ASCII code: " << res << std::endl; }
if (res == EOF) { fin.close(); return 0; } } }
|
这时我们将上面的斐波那契数列的Kaleidoscope代码储存在fibonacci.ks
文件中,就可以得到下面的输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| Comment line:
Keywords: def Identifier: fib Unknown char: (, ASCII code: 40 Identifier: x Unknown char: ), ASCII code: 41 Keywords: if Identifier: x Unknown char: <, ASCII code: 60 Number: 3 Keywords: return Number: 1 Keywords: else Keywords: return Identifier: fib Unknown char: (, ASCII code: 40 Identifier: x Unknown char: -, ASCII code: 45 Number: 1 Unknown char: ), ASCII code: 41 Unknown char: +, ASCII code: 43 Identifier: fib Unknown char: (, ASCII code: 40 Identifier: x Unknown char: -, ASCII code: 45 Number: 2 Unknown char: ), ASCII code: 41 Comment line:
Identifier: fib Unknown char: (, ASCII code: 40 Number: 40 Unknown char: ), ASCII code: 41 End of file
|
到这里我们就实现了一个简单的词法分析器,接下来,就到了parser和AST的实现了。