上一节中,我们实现了函数的定义,在这一节中,我们会实现调用函数并返回一个值。具体来说:
- 定义一个函数
- 调用一个目前无法使用的单一值的函数
- 从函数返回一个值
- 将函数调用用作语句和表达式
- 确保void函数永远不会返回值和非void函数必须返回一个值
函数调用的SysY语法定义:
语句: Stmt → Ident ‘(’ [FuncRParams] ‘)’ ‘;’ | ‘return’ [Exp] ‘;’
该函数有一个名称,后跟一对括号,在括号中必须只有一个参数。它既可以作为表达式又可以作为独立语句。
修改词法分析
我们现在的词法分析存在这样一个问题:
x= a + b;
x= a(5) + b;
我们不能正确分辨出a是变量还是函数。
首先,我们增加了标识符的属性,结构类型,表示这个标识符是一个函数还是变量。
// 结构类型
enum
{
S_VARIABLE, S_FUNCTION
};
// 数据类型
enum
{
P_VOID, P_INT
};
// 符号表结构体
struct symbol_table
{
char *name; // 符号名
int stype; // 结构类型
int type; // 类型void或int
int endlabel; // 函数的结束标签
};
同时修改增加全局变量函数为
// 将全局变量添加到符号表,并返回符号表中的位置
int add_global(char *name, int stype, int endlabel)
{
int y;
// 如果已经在符号表中,则返回现有位置
if ((y = find_global(name)) != -1)
return y;
// 获得一个新的位置,并填入信息和返回位置
y = new_global();
Tsym[y].name = strdup(name);
Tsym[y].stype = stype;
Tsym[y].endlabel = endlabel;
return y;
}
调用add_global()
的var_declaration()
函数也要修改
// 分析变量声明
void var_declaration()
{
// 检查当前单词是否为“int”,后跟一个标识符和一个分号
match(T_KEYINT, "int");
ident();
add_global(Text, S_VARIABLE, 0);
Tsym[t].type = P_INT;
arm_global_sym(Text);
semi();
}
接着回来考虑我们刚才的那个问题,如何区分变量和函数。我的方法是向前看一个符号,看看是否有“(”。如果有,则是函数调用。但是这样做的话,我们会失去当前的单词。为了解决这个问题,我修改了scan()函数,这样我们就可以放回不需要的单词:当我们获得下一个单词是丢弃单词的时候,它将返回。
// 被丢弃单词的指针
struct token *Rejtoken = NULL;
// 丢弃刚刚扫描的单词
void reject_token(struct token *t)
{
if (Rejtoken != NULL)
{
fprintf(stderr, "Can't reject token twice on line %d\n", Line);
exit(1);
}
Rejtoken = t;
}
// 扫描并返回在输入中找到的下一个单词。
// 如果标记有效则返回 1,如果没有标记则返回 0
int scan(struct token *t)
{
int c, tokentype;
// 如果有被丢弃的单词,将其返回
if (Rejtoken != NULL)
{
t = Rejtoken;
Rejtoken = NULL;
return 1;
}
/*继续正常扫描... ...*/
}
接下来,我们考虑另一个问题,如何分析return语句。首先,添加“T_T_RETURN”单词类型。
然后加入对return的分析在match_keyword()
函数中。
case 'r': if(!strcmp(s, "return")) return (T_RETURN);
break;
修改语法分析
分析函数调用
// 分析有单个参数的函数调用并返回其AST树
struct ASTnode *functioncall()
{
struct ASTnode *tree;
int id;
// 检查标识符是否已定义,然后为其创建一个叶节点
if ((id = find_global(Text)) == -1)
{
fprintf(stderr, "Undeclared function on line %d\n", Line);
exit(1);
}
// 匹配'('
lparen();
// 分析接下来的表达式
tree = binexpr(0);
// 构建函数调用AST节点,将函数的返回类型存储为此节点的类型,
// 同时记录函数的符号ID
tree = mkastunary(A_FUNCTIONCALL, tree, id);
// 匹配')'
rparen();
return tree;
}
将函数作为表达式调用
我们在primary()
中区分变量名和函数调用,并把函数作为表达式调用。
// 解析一个整数单词并返回表示它的AST节点
struct ASTnode *primary()
{
struct ASTnode *n;
int id;
switch (Token.token)
{
// 对于整数单词,为其生成一个AST叶子节点
case T_INT:
n = mkastleaf(A_INT, Token.intvalue);
break;
// 对于标识符,检查存在并为其生成一个AST叶子节点
case T_IDENT:
// 扫描下一个字符判断这单词是变量还是函数调用
scan(&Token);
// 如果是'(',那这是函数调用
if (Token.token == T_LPAREN) return functioncall();
// 如果不是函数调用,则丢弃新单词
reject_token(&Token);
// 检查单词是否存在
id = find_global(Text);
if (id == -1)
{
fprintf(stderr, "Unknown variable %s on line %d\n", Text, Line);
exit(1);
}
n = mkastleaf(A_IDENT, id);
break;
default:
fprintf(stderr, "syntax error, token %s on line %d\n", Token.token, Line);
exit(1);
}
// 扫描下一个单词,并返回左节点
scan(&Token);
return n;
}
将函数作为语句调用
当我们尝试将函数作为语句调用时,我们遇到了本质上与把函数作为表达式调用相同的问题。
考虑下面的代码:
x = 20;
x(21);
我们要区分变量赋值语句和函数调用语句,这与区分变量和函数的方法类似:
// 分析赋值语句
struct ASTnode * assignment_statement()
{
struct ASTnode *left, *right, *tree;
int id;
// 检查标识符
ident();
// 如果下一个单词是'(',则这是函数调用
if (Token.token == T_LPAREN) return functioncall();
/*不是函数调用,进行赋值操作... ...*/
}
分析RETURN语句
分析很简单:‘return’, ‘(’, call binexpr()
, ‘)’。困难的是类型的检查。
首先,当我们到达 return 语句时,我们需要知道我们实际上在哪个函数中。为此,我们定义一个全局变量表示当前所在的函数。
int Functionid; // 当前函数的符号id
修改函数定义的函数,使每次输入函数声明时都设置了Functionid
,我们可以重新解析和检查return语句的语义。
// 分析简单函数声明
struct ASTnode *function_declaration()
{
struct ASTnode *tree, *finalstmt;
int nameslot, endlabel;
// 匹配'void'或'int'、函数名和'(' ')',
// 但不做任何处理
if (Token.token == T_VOID || Token.token == T_KEYINT)
{
scan(&Token);
}
else
{
fprintf(stderr, "Void or int expected on line %d\n", Line);
exit(1);
}
ident();
// 获取结束标签的label-id,
// 将函数添加到符号表中,
// 将Functionid设置为函数的符号id
endlabel = label();
nameslot = add_global(Text, S_FUNCTION, endlabel);
Functionid = nameslot;
lparen();
rparen();
// 获得代码块的AST树
tree = Block_statement();
// 如果函数类型不是VOID,
// 检查语句块中的最后一个AST操作是否为return语句
if (Token.token != T_VOID)
{
Tsym[nameslot].type = P_INT;
finalstmt = (tree->op == A_GLUE) ? tree->right : tree;
if (finalstmt == NULL || finalstmt->op != A_RETURN)
{
fprintf(stderr, "No return for function with non-void type on line %d\n", Line);
exit(1);
}
}
else
{
Tsym[nameslot].type = P_VOID;
}
// 返回具有函数符号位置和语句块子树的A_FUNCTION节点
return mkastunary(A_FUNCTION, tree, nameslot);
}
这里用了A_RETURN一个新的AST操作类型,它返回子节点中的表达式树。
// 分析return语句,并返回其AST树
static struct ASTnode *return_statement()
{
struct ASTnode *tree;
int functype;
// 如果是void函数,不返回值,报错
if (Tsym[Functionid].type == P_VOID)
{
fprintf(stderr, "Can't return from a void function on line %d\n", Line);
exit(1);
}
// 匹配'return'
match(T_RETURN, "return");
// 分析接下来的语句
tree = binexpr(0);
// 添加一个A_RETURN节点
tree = mkastunary(A_RETURN, tree, 0);
// 匹配";"
semi();
return tree;
}
修改代码生成器
在code_generator()
中增加对return和函数调用的分析:
case A_RETURN: arm_return(leftreg, Functionid); return NOREG;
case A_FUNCTIONCALL: return arm_call(leftreg, n->v.id);
A_RETURN不返回值,因为它不是表达式。A_FUNCTIONCALL当然是表达式,需要返回值。
修改汇编
新增汇编代码如下:
// 使用给定寄存器中的一个参数调用函数,返回带有结果的寄存器
int arm_call(int r, int id)
{
int outr = arm_alloc_register();
fprintf(Outfile, "\tmov\tr0, r%d\n", r);
fprintf(Outfile, "\tbl\t%s\n", Tsym[id].name);
fprintf(Outfile, "\tmov\tr%d, r0\n", outr);
arm_free_register(r);
return outr;
}
// 生成return语句代码
void arm_return(int reg, int id)
{
if(Tsym[id].type == P_INT)
{
fprintf(Outfile, "\tmov\tr0, r%d\n", reg);
}
else
{
fprintf(stderr, "Bad function type in cgreturn:%d on line %d\n", Tsym[id].type, Line);
exit(1);
}
if(strcmp(Tsym[id].name,"main"))
{
arm_function_postamble();
fprintf(Outfile, "\tbx lr\n");
}
}
代码重构
正如前文所讲的,我们是一边想一边做的,前面的结构总会不符合后面的需求,所以会面临代码重构的问题。这次重构的目的是为了让代码更加贴合语言定义。
语句块: Block → ‘{’ { BlockItem } ‘}’
语句块项: BlockItem → Decl | Stmt
语句: Stmt → LVal ‘=’ Exp ‘;’ | [Exp] ‘;’ | Block
| ‘if’ '( Cond ‘)’ Stmt [ ‘else’ Stmt ]
| ‘while’ ‘(’ Cond ‘)’ Stmt
| ‘return’ [Exp] ‘;’
我们可以看到语法中定义一个语句块,由语句块调用定义变量和单个语句,然后单个语句实现IF、WHILE、RETURN等语句。所以,我们也把代码结构改成类似的,但我们把定义变量放在单个语句中实现。
先定义一个分析单个语句的函数
// 分析一条语句,并返回其AST树
struct ASTnode *single_statement()
{
switch (Token.token)
{
case T_PRINT: return print_statement();
case T_KEYINT: var_declaration(); return NULL; // 没有AST树生成
case T_IDENT: return assignment_statement();
case T_IF: return if_statement();
case T_WHILE: return while_statement();
case T_RETURN: return return_statement();
default: fprintf(stderr, "Syntax error, token:%d on line %d\n", Token.token, Line);
exit(1);
}
}
然后修改Block_statement()
函数,使其调用single_statement()
函数来分析
// 分析语句块并返回其AST
struct ASTnode *Block_statement()
{
struct ASTnode *left = NULL;
struct ASTnode *tree;
// 匹配"{"
lbrace();
while (1)
{
// 分析单个语句
tree = single_statement();
// 对于每个新树,如果左子树为空,则将其保存在左子树中,
// 否则将左子树和新树合并
if (tree)
{
if (left == NULL) left = tree;
else left = mkastnode(A_GLUE, left, NULL, tree, 0);
}
// 遇到"}"时,跳过并返回AST
if (Token.token == T_RBRACE)
{
rbrace();
return left;
}
}
}
测试结果
输入:
int ant()
{
return 20;
}
int main()
{
int i;
i=1;
print 10;
i = ant(15);
print i;
print ant(15) + 10;
return 0;
}
输出(out.s):
.text
.global __aeabi_idiv
.section .rodata
.align 2
.LC0:
.ascii "%d\012\000"
.text
.align 2
.globl ant
.type ant, %function
ant:
push {fp, lr}
add fp, sp, #4
mov r4, #20
mov r0, r4
pop {fp, pc}
bx lr
pop {fp, pc}
.text
.comm i,4,4
.text
.align 2
.globl main
.type main, %function
main:
push {fp, lr}
add fp, sp, #4
mov r5, #1
ldr r3, .L2+8
str r5, [r3]
mov r4, #10
mov r1, r4
ldr r0, .L3
bl printf
mov r4, #15
mov r0, r4
bl ant
mov r5, r0
ldr r3, .L2+8
str r5, [r3]
ldr r3, .L2+8
ldr r4, [r3]
mov r1, r4
ldr r0, .L3
bl printf
mov r4, #15
mov r0, r4
bl ant
mov r5, r0
mov r4, #10
add r5, r5, r4
mov r1, r5
ldr r0, .L3
bl printf
mov r4, #0
mov r0, r4
pop {fp, pc}
.L3:
.word .LC0
.L2:
.word ant
.word main
.word i
输出(out):
10
20
30
总结
在这一节中,我们实现了一个简单版本的函数调用,函数返回。这不是微不足道的,但我认为变化大多是明智的。我们可以看到,在这里我们的全局变量变成了局部变量,但是实现的方式仍是全局变量。在下一节中,我们将正确实现全局变量。