上篇文章记录了一个简单的计算器,但是只能计算一个表达式,比如计算8+3*5,得到值23.这次在其基础上添加了支持语句的功能,并且支持表达式中存在变量。比如下面:
num1 := 5;
num2 := num1+3*5;
num3 := num1 * (num2 - 20/5);
最后计算并返回的值是num3的值80.
根据这个例子,可以看出相比于上次那个简单的计算器,添加的特性包括1、支持赋值语句 2、支持变量 3、支持多条赋值语句,也就是语句块。其中语句之间使用分号分隔,赋值符号为”:=”,变量名的规则和c语言规则一致,以字母或者下划线开头,能包含数字、字母、下划线。
下面是新增的语法规则:
stmt -> id := expr;
stmts -> stmts stmt | stmt
id -> (letter|_) (letter | num)*
其中stmt代表一条语句,目前的语句只有一种,就是赋值语句,id代表一个标识符,在这里仅代表变量,id的定义使用正则表达式定义,letter代表字母。
另外一个值得关注的是新增了符号表,它是一个结构体数组,包含的结构体如下:
typedef struct sym
{
char *name; /*符号名字*/
int val; /*值*/
}Sym;
因为在这个简单的计算器中,所有变量只有一种值,即32位正数。所以Sym结构体的值val就是int。在我们的计算器中,变量可以不用声明而直接使用,如果事先没有赋值的话,那么变量的值将会是0。
符号表的填充是在生成了语法树之后,对其进行求值时进行的。比如单条语句num1 := 5; 在生成语法树
:=
/ \
num1 5
之后。开始调用calc函数对其递归求值。在对num1求值时,先检查符号表中是否存在符号num1,如果不存在则将其加入到符号表,并对它赋值为0。最后对赋值操作符“:=”求值时,将其右边表达式的值5赋给左边标识符num1,并且返回num1的值作为赋值操作符的返回值。
对于多条语句的求值,比如num1 := 5; num2 := num1 + 10; 生成的语法树如下:
:= ―> :=
/ \ / \
num1 5 num2 +
/ \
num1 10
可见对于两条语句分别生成了两棵语法树,其中第二棵语法树是作为第一棵语法树的兄弟节点存在的,下面是树节点的结构体TreeNode:
typedef struct treenode
{
struct treenode *child[CHILDNUM];/*子结点*/
struct treenode *brother;/*兄弟节点*/
NodeKind kind;/*节点类型:1. 语句 2.表达式*/
union
{
ExpK e;/*表达式子类型:常数、变量、数学表达式*/
StmtK s;/*语句类型:目前只有赋值语句一种*/
}attr;
union
{
int num;
TokenType tt;
char *name;
}val;/*属性对应的值*/
}TreeNode;
其中兄弟节点的存在就是为了将多条语句连接起来,以便在语法分析和求值的时候方便找到。
下面看下在代码中所做的相应改变,首先是在语法规则中新增的stmt和stmts规则所对应的两个函数:
TreeNode *stmt()
{
TreeNode *node;
TreeNode *lnode, *rnode;
/*目前就只有赋值语句这一种,以一个ID类型值开头*/
switch (token)
{
case ID:
/*赋值左边的值为左节点*/
lnode = newIdNode();
match(ID);
/*赋值号为根节点*/
node = newNode();
node->kind = Stmt;
node->attr.s = AssignK;
node->val.tt = ASSIGN;
match(ASSIGN);
/*赋值右边的表达式为右节点*/
rnode = exp();
node->child[0] = lnode;
node->child[1] = rnode;
/*匹配分号*/
match(SEMI);
break;
default:
printf("
stmt: Unknown token.\n");
exit(1);
break;
}
return node;
}
由于目前语句只有赋值语句这一种,所以stmt函数就只需要解析语句就行了,赋值语句以一个ID类型的token开头,接着是赋值操作符,最后是一个exp表达式,解析表达式的函数exp()与上一篇文章中的一致。
下面是解析语句块的函数stmts()
TreeNode *stmts()
{
TreeNode *node, *brother;
TreeNode *head;
head = node = stmt();
while (ENDFILE != token)
{
brother = stmt();
node->brother = brother;
node = brother;
}
return head;
}
可以看到,stmts函数中先调用stmt解析一条语句,也就是说,源文件中至少得有一条语句。接着是一个while循环,不断调用stmt函数进行语句的解析,直到文件结束就退出while循环。此时返回一棵语法树,该树对应第一条语句,语法树的兄弟节点指向它后面的语句。
接下来需要关注的就是calc求值函数,
/*计算语法树的值*/
int calc(TreeNode *node)
{
int val;
int val1, val2;
Sym *s;
if (NULL == node)
{
printf("
calc: syntax error.\n");
exit(1);
}
/*根据节点的属性返回相应的值,目前节点有两种
属性:数字或者操作符*/
switch (node->kind)
{
case Exp:
switch (node->attr.e)
{
/*数字属性节点直接返回值*/
case ConstK:
return node->val.num;
break;
/*操作符属性节点值需要先计算两个操作数的值,
再根据操作符来计算最后的结果*/
case OpK:
val1 = calc(node->child[0]);
if (NULL != node->child[1])
{
val2 = calc(node->child[1]);
}
switch (node->val.tt)
{
case ADD:
val = val1 + val2;
break;
case MINUS:
val = val1 - val2;
break;
case MUL:
val = val1 * val2;
break;
case DIV:
val =