原文地址:http://zserge.com/blog/cucu-part2.html
到目前为止,我们已经定义了我们语言的语法并编写了一个词法分析器。在本篇文章中,我们将为我们的语言写解析器。但在开始之前,我们先需要一些辅助函数:
int peek(char *s) {
return (strcmp(tok, s) == 0);
}
int accept(char *s) {
if (peek(s)) {
readtok();
return 1;
}
return 0;
}
int expect(char *s) {
if (accept(s) == 0) {
error("Error: expected '%s'\n", s);
}
}
peek()
函数若下一个符号与传入的字符串相等,则返回非零值。 accept()
函数读取下一个符号,如果其与传入参数相同,否则返回0。expect()
h帮助我们检查语言的语法。
较为困难的部分
从语言的语法中我们可以得知,语句和表达式是相互掺杂在一起的。因此这就意味着一旦我们开始写解析器,我们必须时刻记住这些递归生成规则。让我们从顶至底来进行分析。下面是最高层的函数compiler():
static int typename();
static void statement();
static void compile() {
while (tok[0] != 0) { /* until EOF */
if (typename() == 0) {
error("Error: type name expected\n");
}
DEBUG("identifier: %s\n", tok);
readtok();
if (accept(";")) {
DEBUG("variable definition\n");
continue;
}
expect("(");
int argc = 0;
for (;;) {
argc++;
typename();
DEBUG("function argument: %s\n", tok);
readtok();
if (peek(")")) {
break;
}
expect(",");
}
expect(")");
if (accept(";") == 0) {
DEBUG("function body\n");
statement();
}
}
}
这个函数首先尝试读取类型名,其次是标识符。如果在此之后紧跟一个分号,则说明是一个变量声明。如果跟着括号,说明是一个函数。如果是函数,就接着去逐一搜索参数,再次之后如果没有分号,则说明是一个函数定义(有函数体),否则就只是一个函数声明(只有函数名和类型)。
这里, typename()
是一个用来让我们跳过类型名的函数。 我们指接受int类型、char类型以及其指针(char*):
static int typename() {
if (peek("int") || peek("char")) {
readtok();
while (accept("*"));
return 1;
}
return 0;
}
最有趣的大概就是 statement()
函数了。它可以分析一个单独的语句,而这个语句可以是一个块、一个局部变量的定义/声明、一个return语句等。
现在让我们来看看它的样子:
static void statement() {
if (accept("{")) {
while (accept("}") == 0) {
statement();
}
} else if (typename()) {
DEBUG("local variable: %s\n", tok);
readtok();
if (accept("=")) {
expr();
DEBUG(" :=\n");
}
expect(";");
} else if (accept("if")) {
/* TODO */
} else if (accept("while")) {
/* TODO */
} else if (accept("return")) {
if (peek(";") == 0) {
expr();
}
expect(";");
DEBUG("RET\n");
} else {
expr();
expect(";");
}
}
如果遇到的是一个“块”,即{...}的部分,就继续尝试在块中解析语句直到块结束。如果以变量名开头,则说明是一个局部变量的定义。条件语句(if/then/else)和循环语句在这里没有列出,留给读者去思考根据我们的语法,这些部分应当如何去实现。
当然,大部分语句里都包含着表达式,因此我们需要写一个函数取分析表达式。表达式解析器是一个向下递归的解析器,因此很多的表达式解析函数会互相调用直到找到主表达式为止。所谓的主表达式,根据我们的语法,是指一个数字(常量)或者一个标识符(变量或者函数)。
static void prim_expr() {
if (isdigit(tok[0])) {
DEBUG(" const-%s ", tok);
} else if (isalpha(tok[0])) {
DEBUG(" var-%s ", tok);
} else if (accept("(")) {
expr();
expect(")");
} else {
error("Unexpected primary expression: %s\n", tok);
}
readtok();
}
static void postfix_expr() {
prim_expr();
if (accept("[")) {
expr();
expect("]");
DEBUG(" [] ");
} else if (accept("(")) {
if (accept(")") == 0) {
expr();
DEBUG(" FUNC-ARG\n");
while (accept(",")) {
expr();
DEBUG(" FUNC-ARG\n");
}
expect(")");
}
DEBUG(" FUNC-CALL\n");
}
}
static void add_expr() {
postfix_expr();
while (peek("+") || peek("-")) {
if (accept("+")) {
postfix_expr();
DEBUG(" + ");
} else if (accept("-")) {
postfix_expr();
DEBUG(" - ");
}
}
}
static void shift_expr() {
add_expr();
while (peek("<<") || peek(">>")) {
if (accept("<<")) {
add_expr();
DEBUG(" << ");
} else if (accept(">>")) {
add_expr();
DEBUG(" >> ");
}
}
}
static void rel_expr() {
shift_expr();
while (peek("<")) {
if (accept("<")) {
shift_expr();
DEBUG(" < ");
}
}
}
static void eq_expr() {
rel_expr();
while (peek("==") || peek("!=")) {
if (accept("==")) {
rel_expr();
DEBUG(" == ");
} else if (accept("!=")) {
rel_expr();
DEBUG("!=");
}
}
}
static void bitwise_expr() {
eq_expr();
while (peek("|") || peek("&")) {
if (accept("|")) {
eq_expr();
DEBUG(" OR ");
} else if (accept("&")) {
eq_expr();
DEBUG(" AND ");
}
}
}
static void expr() {
bitwise_expr();
if (accept("=")) {
expr();
DEBUG(" := ");
}
}
上面是一大段的代码,但是不需要感到头疼,因为它们都简单的很。每一个分析表达式的函数首先都尝试调用一个更高优先级的表达式分析函数。接着,如果找到了这个函数期望的符号,则它继续调用高优先级的函数。然后当它分析完了一个二元表达式(如x+y、x&y、x==y)的两部分之后,就将值返回。有些表达式可以链式连接(如a+b+c+d),因此需要循环的分析它们。
我们在分析每一个表达式的时候都会输出一些调试信息,这些信息会给我们带来一些有趣的结果。例如,若我们分析以下代码片段:
int main(int argc, char **argv) {
int i = 2 + 3;
char *s;
func(i+2, i == 2 + 2, s[i+2]);
return i & 34 + 2;
}
我们将会得到如下的输出:
identifier: main
function argument: argc
function argument: argv
function body
local variable: i
const-2 const-3 + :=
local variable: s
var-func var-i const-2 + FUNC-ARG
var-i const-2 const-2 + == FUNC-ARG
var-s var-i const-2 + [] FUNC-ARG
FUNC-CALL
var-i const-34 const-2 + AND RET
所有的表达式都会被写成逆波兰式(比如2+3变成23+)。而这对于有堆栈的计算机来说,是更为方便合理的形式,当操作数在栈顶的时候,函数能够执行出栈操作并取得操作数,之后将结果压栈。
虽然对于现在的以寄存器为基础的CPU,这或许不是一个最优的方法,但这个方法很简单并且能够满足我们编译器的需要。
symbols
现在,我们已经完成了很多工作了,我们使用不到300行的代码写了一个词法分析器和解析器。接下来我们要做的事情是添加以下函数,以便让这些符号(比如变量名、函数名)能够正确的工作。一个编译器应该有一个符号表以便能够很快的找到这些符号的地址,所以当你在代码中写“i=0"的时候,实际上你是将0这个值放入了内存的0x1234的位置(假设变量i在内存的位置就是0x1234)。相似的,当我们调用函数”func()"时,实际上做的是跳转到0x5678继续执行而已(假设func这个符号的的值是0x5678)。
我们需要一个数据结构来存放符号:
struct sym {
char type;
int addr;
char name[];
};
这里type
有不同的含义。 我们用一个单独的字母来标示不同的类型:
L
- 局部变量。addr
存储变量在堆栈里的地址A
- 函数参数。addr
存储参数在堆栈里的地址U
- 未定义的全局变量。addr
存储其在内存中的绝对地址。D
- 定义过的全局变量。 其余同上。
So far, I've added two functions: sym_find(char *s)
to find symbol by its name, andsym_declare()
to add a new symbol.
到此为止,我们还需要增加两个函数:: sym_find(char *s)
来根据符号名查找符号, sym_declare()
来加入一个新的符号。
现在我们已经可以去设计后端的架构了,详情见下篇文章。
如果你忘了前面的信息你可以到part1部分去查阅。