在上一节中,我们实现了注释和变量初始化的功能;在这一节中,我们来实现数组。
先来看看,我们要实现的具体功能:
int ary[5]; // 定义数组
ary[3]= 63; // 表达式给数组元素赋值
int a;
a = ary[4]; // 数组元素赋值变量
具体地讲,我们将实现:
- 具有固定大小但没有初始化列表的数组声明
- 数组索引作为赋值中的右值
- 数组索引作为赋值中的左值
数组的SysY语法是
数组定义 VarDef → Ident ‘[’ ConstExp ‘]’
同时,我们暂时只实现一维数组。
修改词法分析
我们的符号表中有标量变量(只有一个值)和函数,现在添加数组类型。同时,要在符号表中增加长度属性,表示数组的长度。
// 结构类型
enum
{
S_VARIABLE, S_FUNCTION, S_ARRAY
};
// 符号表结构体
struct symbol_table
{
char *name; // 符号名
int type; // 类型void或int
int stype; // 结构类型
int endlabel; // 函数的结束标签
int size; // 数组大小
};
同时,往add_global()
函数中添加size属性。
// 将全局变量添加到符号表,并返回符号表中的位置
int add_global(char *name, int type, int stype, int endlabel, int size)
{
/*......其余代码......*/
Tsym[y].endlabel = endlabel;
Tsym[y].size = size;
return y;
}
接下来,我们开始识别‘[’和’]’,先增加两种单词类型T_LBRACKET, T_RBRACKET
,然后在scan()
函数中,加入对它们的解析。
// 单词类型
enum
{
/*......其余类型......*/
T_COMMA, T_LBRACKET, T_RBRACKET
};
// 扫描并返回在输入中找到的下一个单词。
// 如果标记有效则返回 1,如果没有标记则返回 0
int scan(struct token *t)
{
/*......其他代码......*/
case '[': t->token = T_LBRACKET; break;
case ']': t->token = T_RBRACKET; break;
/*......其他代码......*/
}
修改语法分析
修改了add_global()
函数之后,也要在function_declaration()
函数中修改
nameslot = add_global(Text, type, S_FUNCTION, endlabel, 0);
我们通过在var_declaration()
中查看下一个单词是什么来处理标量变量声明或数组声明:
// 分析变量声明
void var_declaration(int type)
{
int id;
if(type != P_INT)
{
fprintf(stderr, "Error token != T_INT on line %d\n", Line);
exit(1);
}
while(1)
{
// 现在是标识符,如果下一个标记是"["
if (Token.token == T_LBRACKET)
{
// 跳过"["
scan(&Token);
// 获得数组的大小
if (Token.token == T_INT)
{
// 将此添加为已知数组并在汇编中生成其空间
id = add_global(Text, P_INT, S_ARRAY, 0, Token.intvalue);
arm_global_sym(id);
}
// 检查下一个"]"
scan(&Token);
match(T_RBRACKET, "]");
}
else
{
// 将其添加为已知标量并在汇编中生成其空间
id = add_global(Text, P_INT, S_VARIABLE, 0, 1);
// 如果下一个是"=",为变量赋初值
if(Token.token == T_EQU)
{
scan(&Token);
if(Token.token != T_INT)
{
fprintf(stderr, "Error token != T_INT on line %d\n", Line);
exit(1);
}
arm_global_symassign(Text, Token.intvalue);
scan(&Token);
}
else
{
arm_global_sym(id);
}
}
// 如果下一个单词是";",则返回
if(Token.token == T_SEM)
{
scan(&Token);
return ;
}
// 如果下一个是",",生成下一个变量
if(Token.token == T_COMMA)
{
scan(&Token);
ident();
continue;
}
fprintf(stderr, "Missing , or ; after identifier on line %d\n", Line);
exit(1);
}
}
修改函数使其能解析数组变量。
// 解析一个整数单词并返回表示它的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();
// 如果是'[',那是数组变量
if (Token.token == T_LBRACKET) return array_access();
// 如果不是函数调用,则丢弃新单词
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;
case T_LPAREN:
// 括号表达式的开头,跳过'(',扫描表达式和右括号
scan(&Token);
n = binexpr(0);
rparen();
return n;
default:
{
fprintf(stderr, "syntax error, token %d on line %d\n", Token.token, Line);
exit(1);
}
}
// 扫描下一个单词,并返回左节点
scan(&Token);
return n;
}
可以看到,我在这里增加了一个T_LPAREN
情况,来解析表达式中的括号。
我在这里定义了一个新的函数来处理数组变量
// 分析数组并为其返回其AST树
struct ASTnode *array_access()
{
struct ASTnode *left, *right;
int id;
// 检查标识符是否已定义为数组,
// 然后为其创建一个指向基部的叶节点
if ((id = find_global(Text)) == -1 || Tsym[id].stype != S_ARRAY)
{
fprintf(stderr, "Undeclared array:%s on line %d\n", Text, Line);
exit(1);
}
left = mkastleaf(A_ADDRESS, id);
// 跳过'['
scan(&Token);
// 解析接下来的表达式
right = binexpr(0);
// 匹配']'
match(T_RBRACKET, "]");
// 返回一个AST树,其中数组的基数添加了偏移量,并取消引用该元素
left = mkastnode(A_LOGADD, left, NULL, right, 0);
return left;
}
可以看到,我在这里新定义了两种AST节点类型,A_ADDRESS,A_LOGADD
前者表示这是一个数组首地址,后者表示左子树加右子树得到数组元素地址。
对应的我们也要修改binexpr()
函数,使之能解析’[‘和’]'之中的表达式,以下代码在函数中有两段要修改
// 如果遇到';'或者')'或者']',则返回左节点
// 此时的左节点已经更新为合并后的树的根节点
if (tokentype == T_SEM || tokentype == T_RPAREN || tokentype == T_RBRACKET)
{
left->rvalue = 1;
return left;
}
修改代码生成器
我们在代码生成函数code_generator()
中加入对A_ADDRESS,A_LOGADD
的对应处理。
// 给定一个 AST、一个可选标签和父级的AST操作,
// 递归生成汇编代码,返回带有树的最终值的寄存器id
int code_generator(struct ASTnode *n, int label, int parentASTop)
{
/*......其他代码......*/
case A_IDENT: // 如果标识符是右值,则加载它的值
if (n->rvalue || parentASTop== A_ADDRESS) return arm_load_global(n->v.id);
else return NOREG;
case A_ASSIGN: // 判断分配的标量变量还是数组变量
switch (n->right->op)
{
case A_IDENT: return arm_stor_global(leftreg, n->right->v.id);
case A_LOGADD: return arm_stor_address(leftreg, rightreg);
default:
fprintf(stderr, "Can't A_ASSIGN in code_generator(), op:%d on line %d\n", n->op, Line);
exit(1);
}
/*......其他代码......*/
case A_FUNCTIONCALL: return arm_call(leftreg, n->v.id);
case A_ADDRESS: return arm_address(n->v.id);
case A_LOGADD: // 如果是右值,返回地址所在的值;如果是左值,返回逻辑地址给A_ASSIGN
return arm_load_address(leftreg, rightreg, n->rvalue);
/*......结尾代码......*/
}
修改汇编代码
现在我们知道数组的大小,我们可以将 arm_global_sym()
修改为在汇编程序中分配此空间:
// 生成全局变量符号表
void arm_global_sym(int id)
{
fprintf(Outfile, "\t.text\n\t.comm\t%s,%d,4\n", Tsym[id].name, 4 * Tsym[id].size);
}
我们还需要一个获得变量地址的函数arm_address()
,从给定地址加载值的函数arm_load_address()
来获得数组元素,把值写入给定数组地址的函数arm_stor_address()
,来给数组元素赋值。
// 生成代码将标识符的地址加载到寄存器中并返回
int arm_address(int id)
{
int r = arm_alloc_register();
// 获得变量偏移地址
set_var_offset(id);
fprintf(Outfile, "\tmov\tr%d, r3\n", r);
return r;
}
// 生成以下代码,
// 如果为右值,将寄存器中首地址和偏移地址
// 生成逻辑地址的值并返回;如果为左值,
// 将逻辑地址的值加载到寄存器中并返回
int arm_load_address(int l, int r, int flag)
{
fprintf(Outfile, "\tlsl\tr%d, r%d, #2\n", r, r);
fprintf(Outfile, "\tadd\tr%d, r%d, r%d\n", r, l, r);
arm_free_register(l);
if(flag)
{
fprintf(Outfile, "\tldr\tr%d, [r%d]\n", r, r);
}
return r;
}
// 生成代码把寄存器中的值写入地址
int arm_stor_address(int l, int r)
{
fprintf(Outfile, "\tstr\tr%d, [r%d]\n", l, r);
return l;
}
除此之外,还需要修改arm_postamble()
函数,使之打印变量符号表
// 汇编尾代码
void arm_postamble()
{
// 打印大整数
fputs(
".L3:\n"
"\t.word .LC0\n",
Outfile);
for (int i = 0; i < Intslot; i++)
{
fprintf(Outfile, "\t.word %d\n", Intlist[i]);
}
// 打印全局变量
fprintf(Outfile, ".L2:\n");
for (int i = 0; i < Globals; i++)
{
if (Tsym[i].stype != S_FUNCTION)
{
fprintf(Outfile, "\t.word %s\n", Tsym[i].name);
}
}
}
修复bug
修改mkastnode()
函数,右值属性不为零,因为没有初始化
// 生成并返回一个通用的AST节点
struct ASTnode *mkastnode(int op, struct ASTnode *left, struct ASTnode *mid, struct ASTnode *right, int intvalue)
{
struct ASTnode *n;
n = (struct ASTnode *) malloc(sizeof(struct ASTnode));
if (n == NULL)
{
fprintf(stderr, "Unable to malloc in mkastnode()\n");
exit(1);
}
n->op = op;
n->left = left;
n->mid = mid;
n->right = right;
n->v.intvalue = intvalue;
n->rvalue = 0; // 初始化为零
return n;
}
测试结果
输入:
int a = 4, b = 3; // a = 4, b = 3
int any[50];
/*计算a+b的值,
并打印在频幕上*/
int main()
{
any[10] = 480;
any[(1 + 3) * 2] = (any[a + b * 2] + 20) * 2;
print any[8];
return any[8];
}
输出(out.s):
.text
.global __aeabi_idiv
.section .rodata
.align 2
.LC0:
.ascii "%d\012\000"
.text
.global a
.data
.align 2
.type i, %object
.size i, 4
a: .word 4
.text
.global b
.data
.align 2
.type i, %object
.size i, 4
b: .word 3
.text
.comm any,200,4
.text
.align 2
.globl main
.type main, %function
main:
push {fp, lr}
add fp, sp, #4
sub sp, sp, #8
str r0, [fp, #-8]
mov r4, #480
ldr r3, .L2+8
mov r5, r3
mov r6, #10
lsl r6, r6, #2
add r6, r5, r6
str r4, [r6]
ldr r3, .L2+8
mov r4, r3
ldr r3, .L2+0
ldr r5, [r3]
ldr r3, .L2+4
ldr r6, [r3]
mov r7, #2
mul r6, r6, r7
add r5, r5, r6
lsl r5, r5, #2
add r5, r4, r5
ldr r5, [r5]
mov r4, #20
add r5, r5, r4
mov r4, #2
mul r5, r5, r4
ldr r3, .L2+8
mov r4, r3
mov r6, #1
mov r7, #3
add r6, r6, r7
mov r7, #2
mul r6, r6, r7
lsl r6, r6, #2
add r6, r4, r6
str r5, [r6]
ldr r3, .L2+8
mov r4, r3
mov r5, #8
lsl r5, r5, #2
add r5, r4, r5
ldr r5, [r5]
mov r1, r5
ldr r0, .L3
bl printf
ldr r3, .L2+8
mov r4, r3
mov r5, #8
lsl r5, r5, #2
add r5, r4, r5
ldr r5, [r5]
mov r0, r5
b L1
L1:
sub sp, fp, #4
pop {fp, pc}
.align 2
.L3:
.word .LC0
.L2:
.word a
.word b
.word any
输出(out):
1000
总结
在这节中,我们添加基本数组声明和数组表达式在处理语法方面的解析,然后得到数组元素偏移地址并乘以4,添加到基地址,并设置为左值或者右值。最终,成功实现数组的基础功能。在下一节中,我们将实现逻辑表达式。