上篇文章记录了一个简单的计算器。可是仅仅能计算一个表达式,比方计算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
有问题欢迎交流~~~~~