上篇文章记录了一个简单的计算器,但是只能计算一个表达式,比如计算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("<Error>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("<Error>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 = val1 / val2;
break;
case NEGATIVE:
val = val1*(-1);
break;
default:
printf("<Error>cal: Unknown operation.\n");
exit(1);
break;
}
break;
case IdK:
s = findSym(node->val.name);
if (NULL == s)
{
/*未声明的id默认值为0*/
addSym(node->val.name, 0);
val = 0;
}
else
{
val = s->val;
}
break;
default:
printf("<Error>calc: Unknown expression type.\n");
exit(1);
break;
}
break;
case Stmt:
<span style="white-space:pre"> </span>/*赋值语句的计算*/
switch (node->attr.s)
{
case AssignK:
val1 = val = calc(node->child[1]);
s = findSym(node->child[0]->val.name);
if (NULL == s)
{
addSym(node->child[0]->val.name, val1);
}
else
{
s->val = val1;
}
break;
default:
printf("<Error>calc: Unknown stmt kind.\n");
exit(1);
break;
}
break;
}
if (NULL != node->brother)
{
val = calc(node->brother);
}
return val;
}
可以看到该函数有两层的switch嵌套,第一层switch判断节点的类型是语句还是表达式,第二层switch判断其子类型,比如表达式可以是常数、标识符、数学表达式,语句目前只有赋值语句一种,将来可以能有if语句、while语句等等。在计算计算赋值语句分支中,先计算赋值符号右边表达式的值,这个值将会赋给左边标识符,并且作为赋值符号的值而返回。对于左边标识符,先判断其是否在符号表中,如果不在就将其添加到符号表,并把右边表达式的值赋给它,以便后面引用到该标识符时使用。
在计算switch的表达式标识符的分支中,也是先查找其是否在符号表中,如果在的话,就直接返回符号表中保存的该标识符的值。如果不在,那么就将该标识符添加到符号表,并初始化为0。
另外的一些改变是getToken函数,添加了识别标识符的分支,下面仅列出其添加的代码:
default:
/*首字符是字母或者下划线*/
if (isalpha(c) || ('_' == c))
{
/*可以是下划线字母或者数字*/
while (isalnum(c) || ('_' == c))
{
tval[index++] = c;
c = nextChar();
}
pushBack();
token = ID;
state = DONE;
}
else
{
printf("<Error>getToken: Unknown character.\n");
exit(1);
}
break;
添加的代码是在switch的default分支中,如果取出的字符c是一个字母或者下划线,那么说明是一个标识符的开始,然后while循环将标识符从缓冲区中取出放到另一个存储标识符字符串的小缓冲区中,直到遇到一个非字母和下划线的字符。
下面是向符号表添加符号和查找符号的两个函数:
void addSym(char *name, int val)
{
if (symidx >= SYMNUM)
{
printf("<Error>addSym: too much symbol.\n");
exit(1);
}
symTbl[symidx] = (Sym *)malloc(sizeof(Sym));
symTbl[symidx]->name = name;
symTbl[symidx]->val = val;
symidx++;
}
Sym *findSym(char *name)
{
int i;
for (i = 0; i < symidx; i++)
{
if (!strcmp(symTbl[i]->name, name))
{
return symTbl[i];
}
}
return NULL;
}
这两个函数比较简单,就是用一个数组和一个下标来管理,查找使用顺序查找,简直是简单到不能忍了,不过目前来说,还是够用就好,够用就好,呵呵。
好了,这个带变量,带语句版本的计算器就完成了。下面看看效果。
建立一个测试文件test.txt,输入以下内容:
num1 := 5;
num2 := num1 + 10;
num3 := num2 * num1 - 20 / num1;
接下来编译计算器
gcc -fno-builtin mycomplier.c -o mycpl
执行
./mycpl test.txt
将会输出 The result is 71.
看到这一条条带变量的语句,还能计算出正确的结果,真是觉点有点像那么回事 Y(^_^)Y。
源代码下载路径 http://download.csdn.net/detail/luo3532869/8039017
Email: robin.long.219@gmail.com
有问题欢迎交流~~~~~