文章简单翻译自LLVM totorial
原始链接
该教程需要有C++语言的编程基础,不过不需要有编译器相关的经验(要是有更好)
这里完整的介绍了一个简单的语言实现,这个教程会展示的使用LLVM生成代码的一个具体例子。
教程介绍了一个简单的语言,叫做“Kaleidoscope”,以迭代的方式在后面的几个教程章节中不断完事这个语言的构建。
这样可以让我们涵盖大量的语言设计和LLVM的技术思想,随着搭建程序的过程不断展示和实验程序,并且减少大量的细节问题。
注意:为了专注于编译器技术和LLVM,该教程不会展示一些关于软件工程的最佳实现原理。如,代码使用满天飞的全局变量,不会使用visitor设计模式等等(这个还是得看看)。
本章会介绍我们要做什么,以及要构建的基本功能。通常lexer是构建一个语言的parser(解析器)的第一步,这里使用一个好理解的简单的C++程序实现的lexer。
原文链接
该教程以一个叫做“Kaleidoscope ”的玩具语言来展示编译器的构建过程。
Kaleidoscope 是一个过程语言,用户可以使用这个语言定义函数、使用条件判断和数学表达式等等。
通过这个教程,我们将拓展Kaleidoscope,使其可以支持if/then/else ,for循环, 用户定义运算符,使用命令行的方式进行即时编译(JIT),debug等等功能。
为了使语言简单一些,在Kaleidoscope 语言中只支持一种数据类型,该数据类型为一个64-bit的浮点数类型。
这样所有的变量都会以双精度的形式出现,并且该语言不需要类型声明。这会使语言的语法非常简单。例如下面计算斐波那契的程序。
def fib(x)
if x < 3 then
1
else
fib(x-1)+fib(x-2)
# This expression will compute the 40th number.
fib(40)
Kaleidoscope 语言可以调用便准库函数,LLVM的JIT会使这个操作非常简单。可以使用extern关键字定义一个函数(可以互相调用和递归使用)如下面:
extern sin(arg);
extern cos(arg);
extern atan2(arg1, arg2);
atan2(sin(.4), cos(42))
更多的例子在第六章,可以写一个小型的Kaleidoscope 应用程序,如写一个Mandelbrot Set (曼德勃罗集合)。
下面进入Kaleidoscope 语言的设计阶段。
实现一个编程语言,首先需要做的就是就是处理文本文件和识别它表达的是什么。传统办法是使用"lexer"(也叫"scanner")将输入数据拆分成"token"。 每个token由lexer返回的信息包括token code和一些隐藏元信息(metadata),(例如:数字的数值) 首先定义所有可能的出现。
// The lexer returns tokens [0-255] if it is an unknown character, otherwise one
// of these for known things.
enum Token {
tok_eof = -1,
// commands
tok_def = -2,
tok_extern = -3,
// primary
tok_identifier = -4,
tok_number = -5,
};
static std::string IdentifierStr; // Filled in if tok_identifier
static double NumVal; // Filled in if tok_number
每个token 由lexer 要么返回一个Token 的枚举值,要么返回一个未知字符的(如‘+’)的ascII码。如果当前的token是一个标识符,那么这个标识符字符串的全局变量会保存该标识符的名称。如果当前的token是一个数字(如1.0),NumVal会保存它的的值。为了简化实现,我们使用全局变量,但是在一个真正的语言实现程序中,这不是一个好的方法。
真正执行lexer是一个名为gettok的函数。这个函数被调用并从当前的输入信息返回下一个token。
定义如下:
/// gettok - Return the next token from standard input.
static int gettok() {
static int LastChar = ' ';
// Skip any whitespace.
while (isspace(LastChar))
LastChar = getchar();
gettok函数通过调用C语言的getchar()函数从标准的输入中读取一个字符。识别并存储这些字符直到读取到最后一个字符为止,该过程只进行读取,并不进行处理。
首先要做的事事去掉所有token之间的空格。这个过程可以通过一个循环搞定。
接下来gettok函数要做的事识别表示和特殊的关键字,比如"def"。Kaleidoscope 语言实现这些功能使用简单的循环遍历的方式实现。
if (isalpha(LastChar)) {
// identifier: [a-zA-Z][a-zA-Z0-9]*
IdentifierStr = LastChar;
while (isalnum((LastChar = getchar())))
IdentifierStr += LastChar;
if (IdentifierStr == "def")
return tok_def;
if (IdentifierStr == "extern")
return tok_extern;
return tok_identifier;
}
注意,代码中将“IdentifierStr”变量设置成了全局的变量,并使用它来识别一个标识符。同样,一个语言的关键字同样由循环来进行匹配,识别数字变量也是同样的方法。
if (isdigit(LastChar) || LastChar == '.') {
// Number: [0-9.]+
std::string NumStr;
do {
NumStr += LastChar;
LastChar = getchar();
} while (isdigit(LastChar) || LastChar == '.');
NumVal = strtod(NumStr.c_str(), 0);
return tok_number;
}
上面代码非常直观的处理输入信息。当读入一个数值变量时,我们使用C语言中的strtod函数将这个数值变量的字符串形式转换成数值,并粗出到NumVal中。 注意,这里没有有做充分的错误检查,可能会有不正确的输入信息,比如“1.23.56.67",会被当成”1.23"来处理(可以自己加个逻辑改正掉)
接下来处理注释信息:
if (LastChar == '#') {
// Comment until end of line.
do
LastChar = getchar();
while (LastChar != EOF && LastChar != '\n' && LastChar != '\r');
if (LastChar != EOF)
return gettok();
}
遇到注释信息直接跳过该行并返回下一个token。最后,如果输入没有匹配上任何一个上面的标识符或者关键字,而且也不是一个操作符字符比如"+",又不是读到文件尾部,那么用下面的代码处理:
// Check for end of file. Don't eat the EOF.
if (LastChar == EOF)
return tok_eof;
// Otherwise, just return the character as its ascii value.
int ThisChar = LastChar;
LastChar = getchar();
return ThisChar;
}
上面的代码就是完整的Kaleidoscope语言的一个lexer,完整代码如下:
enum Token {
tok_eof = -1,
// commands
tok_def = -2,
tok_extern = -3,
// primary
tok_identifier = -4,
tok_number = -5
};
static std::string IdentifierStr; // Filled in if tok_identifier
static double NumVal; // Filled in if tok_number
/// gettok - Return the next token from standard input.
static int gettok() {
static int LastChar = ' ';
// Skip any whitespace.
while (isspace(LastChar))
LastChar = getchar();
if (isalpha(LastChar)) {
// identifier: [a-zA-Z][a-zA-Z0-9]*
IdentifierStr = LastChar;
while (isalnum((LastChar = getchar())))
IdentifierStr += LastChar;
if (IdentifierStr == "def")
return tok_def;
if (IdentifierStr == "extern")
return tok_extern;
return tok_identifier;
}
if (isdigit(LastChar) || LastChar == '.') {
// Number: [0-9.]+
std::string NumStr;
do {
NumStr += LastChar;
LastChar = getchar();
} while (isdigit(LastChar) || LastChar == '.');
NumVal = strtod(NumStr.c_str(), nullptr);
return tok_number;
}
if (LastChar == '#') {
// Comment until end of line.
do
LastChar = getchar();
while (LastChar != EOF && LastChar != '\n' && LastChar != '\r');
if (LastChar != EOF)
return gettok();
}
// Check for end of file. Don't eat the EOF.
if (LastChar == EOF)
return tok_eof;
// Otherwise, just return the character as its ascii value.
int ThisChar = LastChar;
LastChar = getchar();
return ThisChar;
}
就是一个简单的字符串处理程序,自己配上所需要的头文件,可以输入一些测试用的文本试试这个lexer的效果,查看一下有什么bug(bug超多,主要是为了理解lexer的功能)