第9部分:While循环
在这部分的学习旅程中,我们将向我们的语言中添加WHILE循环。在某种意义上,WHILE循环非常类似于没有’else’子句的IF语句,只是我们总是跳回到循环的顶部。
所以,这样的代码:
while (条件为真) {
语句;
}
应该被翻译为:
Lstart: 计算条件
如果条件为假则跳转到Lend
语句
跳转到Lstart
Lend:
这意味着我们可以借用我们在IF语句中使用的扫描、解析和代码生成结构,并对其进行一些小的修改,以便也能处理WHILE语句。
让我们看看如何实现这一点。
新的令牌
我们需要一个新的令牌,T_WHILE,用于新的’while’关键字。对defs.h和scan.c的修改是显而易见的,所以在这里我将省略它们。
解析While语法
WHILE循环的BNF语法是:
// while_statement: 'while' '(' true_false_expression ')' compound_statement ;
我们需要在stmt.c中添加一个函数来解析这个。这是该函数;注意这与解析IF语句相比的简单性:
// 解析一个WHILE语句
// 并返回其AST
struct ASTnode *while_statement(void) {
struct ASTnode *condAST, *bodyAST;
// 确保我们有'while' '('
match(T_WHILE, "while");
lparen();
// 解析接下来的表达式
// 并且解析随后的')'。确保
// 树的操作是比较。
condAST = binexpr(0);
if (condAST->op < A_EQ || condAST->op > A_GE)
fatal("比较操作符错误");
rparen();
// 获取复合语句的AST
bodyAST = compound_statement();
// 构建并返回这个语句的AST
return (mkastnode(A_WHILE, condAST, NULL, bodyAST, 0));
}我们需要一个新的AST节点类型,A_WHILE,它已经被添加到defs.h中。这个节点有一个左子树来评估条件,和一个右子树来作为WHILE循环的复合语句的主体。
通用代码生成
我们需要创建一个开始和结束标签,计算条件,并插入适当的跳转以退出循环并返回到循环的顶部。再次说明,这比生成IF语句的代码要简单得多。在gen.c中:
// 为WHILE语句生成代码
// 和一个可选的ELSE子句
static int genWHILE(struct ASTnode *n) {
int Lstart, Lend;
// 生成开始和结束标签
// 并输出开始标签
Lstart = label();
Lend = label();
cglabel(Lstart);
// 生成条件代码,紧随其后
// 是跳转到结束标签。
// 我们通过将Lfalse标签作为寄存器发送来作弊。
genAST(n->left, Lend, n->op);
genfreeregs();
// 生成主体的复合语句
genAST(n->right, NOREG, n->op);
genfreeregs();
// 最后输出回到条件的跳转,
// 和结束标签
cgjump(Lstart);
cglabel(Lend);
return (NOREG);
}我必须做的一件事是认识到比较运算符的父AST节点现在可以是A_WHILE,所以在genAST()中,比较运算符的代码看起来像:
case A_EQ:
case A_NE:
case A
_LT:
case A_GT:
case A_LE:
case A_GE:
// 如果父AST节点是A_IF或A_WHILE,生成
// 比较后跟随一个跳转。否则,比较寄存器
// 并根据比较设置一个为1或0。
if (parentASTop == A_IF || parentASTop == A_WHILE)
return (cgcompare_and_jump(n->op, leftreg, rightreg, reg));
else
return (cgcompare_and_set(n->op, leftreg, rightreg));这总的来说,就是实现WHILE循环所需要的全部!
测试新的语言添加
我已经将所有的输入文件移动到了一个test/目录中。如果你现在执行make test,它将进入这个目录,编译每个输入文件,并将输出与已知的良好输出进行比较:
cc -o comp1 -g cg.c decl.c expr.c gen.c main.c misc.c scan.c stmt.c
sym.c tree.c
(cd tests; chmod +x runtests; ./runtests)
input01: OK
input02: OK
input03: OK
input04: OK
input05: OK
input06: OK
你还可以执行make test6。这将编译tests/input06文件:
{ int i;
i=1;
while (i <= 10) {
print i;
i= i + 1;
}
}这将打印出从1到10的数字:
cc -o comp1 -g cg.c decl.c expr.c gen.c main.c misc.c scan.c
stmt.c sym.c tree.c
./comp1 tests/input06
cc -o out out.s
./out
1
2
3
4
5
6
7
8
9
10这里是编译产生的汇编输出:
.comm i,8,8
movq $1, %r8
movq %r8, i(%rip) # i= 1
L1:
movq i(%rip), %r8
movq $10, %r9
cmpq %r9, %r8 # i是否小于等于10?
jg L2 # 大于,跳转到L2
movq i(%rip), %r8
movq %r8, %rdi # 打印出i
call printint
movq i(%rip), %r8
movq $1, %r9
addq %r8, %r9 # i加1
movq %r9, i(%rip)
jmp L1 # 然后回到循环
L2:
结论和接下来的内容
一旦我们已经完成了IF语句,添加WHILE循环就变得容易了,因为它们有很多相似之处。
我认为我们现在也拥有了一个图灵完备的语言:
- 无限量的存储,即无限数量的变量
- 基于存储值做出决策的能力,即IF语句
- 改变方向的能力,即WHILE循环
所以我们现在可以停止了,我们的任务完成了!不,当然不是。我们仍在努力使编译器能够编译自身。
在我们的编译器编写旅程的下一部分,我们将向语言中添加FOR循环。下一步