创造一门程序设计语言,写一个解释器
本文参考《用C语言写解释器》http://blog.csdn.net/redraiment/article/details/4693952 一文写成。
本文的目的是创造一门自己的程序设计语言,并为这门语言编写一个解释器。解释器是一个程序,它能将我们编写的代码一条一条的解释执行。
文章《用C语言写解释器》中说“要做到“可编程”,程序至少应该具备“输入/输出”、“表达式运算”、“内存管理”和“按条件跳转”四个功能”,我们就从这四个功能入手设计自己的语言。
1. 语法
1.1 输入/输出
输入/输出是人机交互的基本要求。我们设计的语言拥有输入语句 INPUT 和输出语句 PRINT 。
INPUT 语句的语法如下:
INPUT VAR
VAR 代表变量名(下同)。这条语句的作用是从键盘上获得一个数字或者字符串,并将这个数字或字符串存储在变量 VAR 中。
PRINT 语句的语法如下:
PRINT exp 或者 PRINT "str"
exp 表示表达式,str 表示字符串。这条语句的作用是在屏幕上输出表达式的值或者字符串。为了满足格式化的需求,PRINT 语句还能使用“\n”换行,使用“\t”输出制表符。
1.2 表达式运算
在计算机中,表达式运算不仅仅是算数运算,还要有关系运算和逻辑运算。我们设计的语言要实现以下几种运算符:
名称 符号
加 +
减 -
乘 *
除 /
取余 %
小于 <
大于 >
等于 ==
不等于 !=
不大于 <=
不小于 >=
且 &
或 |
非 !
1.3 内存管理
在我们设计的简单解释器中,不需要复杂的内存管理,只要实现简单的变量内存管理。我们默认提供A-Z共26字母作为变量名的变量。这些变量能存储浮点数或者字符串。变量要求赋值后才能使用,否则解释器将会提示出错。
赋值语句的的语法如下:
LET VAR = exp
这个语句中的关键字 LET 是必须的,这个语句的作用是将表达式 exp 的值赋值给变量 VAR。
1.4 跳转语句
我们要创造的这门语言可以实现 FOR 语句、WHILE 语句、IF ELSE 语句。它们的语法如下:
1) FOR 语句
FOR VAR = exp1 TO exp2 [ STEP exp3 ]
......
ENDFOR
STEP exp3 部分是步长,可以省略,默认步长为1 . FOR 语句的作用是首先将表达式 exp1 的值赋值给变量 VAR,之后执行下一条语句,直至执行到 ENDFOR 语句,遇到 ENDFOR 语句后变量 VAR 的值加上步长,若此时变量的值小于表达式 exp2 的值,则跳回循环开始处执行循环内容,否则执行下面的语句。
2)WHILE 语句
WHILE exp
.......
ENDW
WHILE 语句首先计算表达式 exp 的值,若表达式的值非零,则执行循环,若表达式的值为零,则跳到ENDW以下的语句执行。
3)IF ELSE 语句
IF exp
......
[ ELSE
......
]
ENDIF
ELSE 部分可以省略。IF 语句首先计算表达式 exp 的值,若表达式的值不为零,则执行IF至ELSE部分的语句,否则执行ELSE至ENDIF 部分的语句。若没有ELSE部分则不执行。
1.5 解释器的使用
这门语言每一行只能写一条语句,最多只能有1000行。
编写一个txt文件,内容如下:
PRINT "LINE = " INPUT A IF A < 10 FOR B = 1 TO A FOR C = 1 TO A - B PRINT " " ENDFOR FOR D = 1 TO 2*B-1 PRINT "*" ENDFOR PRINT "\n" ENDFOR ELSE PRINT "NO\n" ENDIF
将文件保存在D盘,文件名为mycode.txt。用解释器打开,则会出现以下内容,输入5,则会出现这样的图形。
1. 6 编写解释器的基本思想
我们这个解释器主要有两个部分,分别是表达式求值和语句分析。表达式求值目前有很多种方法,例如说先将中缀表达式转为后缀表达式,然后求值,也可以将中缀表达式直接求值。我们的解释器使用的是直接将中缀表达式求值。
语句分析就是匹配字符串,分析是何种语句,然后执行相应的指令。
2. 表达式求值
2.1 内存管理及变量
这门语言的每一个值可以是双精度数也可以是一个字符串,所以值的定义如下:
typedef enum { var_null = 0, //未赋值 var_double, //数值 var_string //字符串 } var_type; //字符串类型,一个字符数组 //最大长度为128 typedef char STRING[128]; //值的结构体 typedef struct { var_type type; //值的类型 union { double i; STRING s; }; } VARIANT; //开一个26个元素的值数组 //保存自带的26个变量 extern VARIANT memory[MEMORY_SIZE] = {0}; //换个名字 操作数 typedef VARIANT OPERAND;
在这里我们开了一个26个元素的数组用于储存我们的变量。
2.2 表达式的标记
表达式中有操作符和操作数,例如一个表达式 “1+1”,”1“ 是操作数,”+“ 是操作符。在这里我们要定义表达式的标记,如下:
//运算符 typedef enum { /* 算数运算 */ oper_lparen = 0, // 左括号 oper_rparen, // 右括号 oper_plus, // 加 oper_minus, // 减 oper_multiply, // 乘 oper_divide, // 除 oper_mod, // 取余 /* 关系运算 */ oper_lt, // 小于 oper_gt, // 大于 oper_eq, // 等于 oper_ne, // 不等于 oper_le, // 不大于 oper_ge, // 不小于 /* 逻辑运算 */ oper_and, // 且 oper_or, // 或 oper_not, // 非 oper_min // 栈底 } operator_type; //结合性枚举类 typedef enum { left2right, right2left } associativity; //操作符结构体 typedef struct { int numbers; // 操作数 int icp; // 优先级 int isp; // 优先级 associativity ass; // 结合性 operator_type oper; // 操作符 } OPERATOR; //操作符表 static const OPERATOR operators[] = { /* 算数运算 */ {2, 17, 1, left2right, oper_lparen}, // 左括号 {2, 17, 17, left2right, oper_rparen}, // 右括号 {2, 12, 12, left2right, oper_plus}, // 加 {2, 12, 12, left2right, oper_minus}, // 减 {2, 13, 13, left2right, oper_multiply}, // 乘 {2, 13, 13, left2right, oper_divide}, // 除 {2, 13, 13, left2right, oper_mod}, // 取余 /* 关系运算 */ {2, 10, 10, left2right, oper_lt}, // 小于 {2, 10, 10, left2right, oper_gt}, // 大于 {2, 9, 9, left2right, oper_eq}, // 等于 {2, 9, 9, left2right, oper_ne}, // 不等于 {2, 10, 10, left2right, oper_le}, // 不大于 {2, 10, 10, left2right, oper_ge}, // 不小于 /* 逻辑运算 */ {2, 5, 5, left2right, oper_and}, // 且 {2, 4, 4, left2right, oper_or}, // 或 {1, 15, 15, right2left, oper_not}, // 非 /* 最小优先级 */ {2, 0, 0, right2left, oper_min} // 栈底 }; //表达式 //标记类型 typedef enum { token_operand = 1, token_operator } token_type; //标记 typedef struct { token_type type; union { OPERAND var; OPERATOR ator; }; } TOKEN; typedef struct tlist { TOKEN token; struct tlist *next; } TOKEN_LIST, *PTLIST;
这里的左括号有两个优先级,左括号入栈前使用高优先级,入栈后使用低优先级。
2.3. 中缀表达式求值的方法
我们日常使用的表达式称为中缀表达式,它将操作符放在操作数的中间,所以称为中缀表达式。
中 缀表达式求值得方法是:初始化两个栈,分别是操作数栈和操作符栈。从左往右逐个扫描标记,如果遇到操作数,则将操作数入栈;如果遇到操作符,如果操作符栈 为空则入操作符栈,否则将遇到的操作符与操作符栈栈顶元素比较优先级,如果优先级高于栈顶操作符,则入栈,如果低于或等于栈顶操作符,则弹出栈顶操作符, 根据操作符的操作数个数弹出操作数进行计算,并将结果入操作数栈。如果遇到左括号则入操作符栈,如果遇到右括号,就弹出操作符进行计算,直至遇到左括号。 最后将剩余的操作符出栈计算。
举个例子,例如求表达式 1+2*(1+2-1).开始扫描表达式,遇到操作数“1”,入操作数栈;操作符“+”,入操作符栈;操作数“2”入操作数栈。此时两个栈的情况如下:
操作数栈:
栈顶 2
1
操作符栈:
栈顶 +
此时遇到操作符“*”,其优先级高于栈顶元素,入操作符栈,接着遇到左括号,入操作符栈。两个栈的情况如下:
操作数栈:
栈顶 2
1
操作符栈:
栈顶 (
*
+
接着继续扫描标记,遇到“1”,入操作数栈,遇到“+”号,因为左括号入栈后使用低优先级,所以“+”号的优先级高于左括号,因此入栈。遇到操作数“2”,入操作数栈。此时两个栈的情况如下:
操作数栈:
栈顶 2
1
2
1
操作符栈:
栈顶 +
(
*
+
继 续扫描,遇到操作符“-”,因为它的优先级等于栈顶元素,所以要取出操作符栈栈顶的操作符“+”进行运算,因为“+”号有两个操作数,所以在操作数栈取出 两个操作数进行计算,1 + 2 = 3,将“3”入操作数栈。此时“-”号的优先级低于栈顶元素,入栈。继续扫描,到“1”,也将其入操作数栈。此时两个堆栈的情况如下:
操作数栈:
栈顶 1
3
2
1
操作符栈:
栈顶 -
(
*
+
继续扫描,到“)”,将操作符栈栈顶操作符和操作数栈的操作数出栈进行计算,3 - 1 = 2,因为“-”号的结合性是从左往右,所以用先入栈的元素作为被减数。最后将 “2” 入栈,并将左括号“(”弹出。此时表达式已经结束,两个栈的情况如下:
操作数栈:
栈顶 2
2
1
操作符栈:
栈顶 *
+
最后取出操作符“*”进行计算,2*2 = 4,将“4”入操作数栈。取出操作符“+”进行计算,1 + 4 = 5,这样便得到了这个表达式的结果 5.
2.4 栈的实现
从表达式求值得过程中可以看到多次使用了栈这个数据结构。为了方便下面的编写,我们有必要实现栈的一些操作。
//表达式求值时用到的堆栈操作 //栈的初始化 PTLIST stack_ini() { PTLIST p; p = ( PTLIST )malloc( sizeof( TOKEN_LIST ) ); if( p == NULL) { return NULL; } else { p -> next = NULL; return p; } } //将一个标记压入堆栈 void stack_push( PTLIST st, TOKEN x ) { PTLIST t; t = ( PTLIST )malloc( sizeof( TOKEN_LIST ) ); if( t == NULL ) { exit(-1); } else { t -> token = x; t -> next = st -> next; st -> next = t; } } //弹出一个元素 TOKEN stack_pop( PTLIST st) { PTLIST t; TOKEN e; t = st -> next; e = t->token; st -> next = t -> next; free(t); return e; } //返回栈顶元素 TOKEN stack_top(PTLIST st) { TOKEN t={0}; if(st->next != NULL) { t = (st->next)->token; return t; } else { return t; } } //释放堆栈空间 void stack_free(PTLIST st) { while(st -> next != NULL) { stack_pop(st); } free(st); }
2.5 取出表达式中的标记
我们在对表达式求值时,要逐个扫描表达式的标记,为了方便后面程序的编写,要编写一个函数,取出表达式的标记。
字符指针 e 指向表达式字符串开始的那个字符,当函数取出一个标记之后,会将指针 e 前移相应的距离。函数的实现如下:
//内部函数 //取出字符串开头的一个标记 //同时将字符串指针前移 //全局变量 char *e; static TOKEN next_token() { //变量声明 TOKEN token = {0}; int i; STRING str_t; //去除前面的空格 while(*e && isspace(*e)) { e++; } //如果字符串结束 if(*e == '\0') { return token; } //一个标记有 4 种可能 //1.字符串 //2.变量 //3.数字 //4.操作符/运算符 //如果是字符串 if(*e == '"') { //标记类型为操作数 token.type = token_operand; //操作数类型为字符串 token.var.type = var_string; e++; //字符串赋值 for(i = 0; *e && *e != '"'; i++) { token.var.s[i] = *e; e++; } token.var.s[i] = '\0'; e++; } //如果是变量 else if( isalpha(e[0]) && !isalpha(e[1]) ) { token.type = token_operand; token.var = memory[*e - 'A']; e++; if(token.var.type == var_null) { printf("变量%c未赋值\n", *e); exit(-1); } } //如果是数字 else if(isdigit(*e)) { token.type = token_operand; token.var.type = var_double; for(i = 0; *e && ( isdigit(*e) || *e == '.'); i++) { str_t[i] = *e; e++; } str_t[i] = '\0'; if( sscanf(str_t, "%lf", &(token.var.i)) != 1) { printf("数字读取失败\n"); } } //如果是运算符 else { token.type = token_operator; switch(*e) { //左括号 case '(': token.ator = operators[oper_lparen]; break; //右括号 case ')': token.ator = operators[oper_rparen]; break; //加 case '+': token.ator = operators[oper_plus]; break; //减 case '-': token.ator = operators[oper_minus]; break; //乘 case '*': token.ator = operators[oper_multiply]; break; //除 case '/': token.ator = operators[oper_divide]; break; //取余 case '%': token.ator = operators[oper_mod]; break; case '<': //小于或等于 if(*(e + 1) == '=') { token.ator = operators[oper_le]; e++; break; } //小于 else { token.ator = operators[oper_lt]; break; } case '>': //大于或等于 if(*(e + 1) == '=') { token.ator = operators[oper_ge]; e++; break; } //大于 else { token.ator = operators[oper_gt]; break; } //等于 case '=': if(e[1] == '=') { token.ator = operators[oper_eq]; e++; break; } else { printf("表达式错误11\n"); exit(-1); } //不等于 case '!': if(*(e + 1) == '=') { token.ator = operators[oper_ne]; e++; break; } //非 else { token.ator = operators[oper_not]; break; } //与 case '&': token.ator = operators[oper_and]; break; //或 case '|': token.ator = operators[oper_or]; break; //其它字符 default: printf("表达式错误2\n"); } e++; } return token; }
2.6 表达式求值
最后,我们就可以进行表达式求值得操作了。这个函数需要传入一个指针,这个指针指向表达式字符串。函数返回一个值类型,为表达式的值。具体代码如下:
//表达式求值的函数 //传入一个表达式字符串 //函数返回字符串的值 VARIANT eval(char *eval) { //变量声明和初始化 PTLIST s_num; //操作数栈 PTLIST s_ator; //操作符栈 TOKEN token; //标记的变量 TOKEN t; //用做中间变量 TOKEN a, b, c = {0}; //计算用的标记 //初始化字符串 e = eval; //初始化栈 s_num = stack_ini(); s_ator = stack_ini(); //设置操作符栈的栈底 token.type = token_operator; token.ator = operators[oper_min]; stack_push(s_ator, token); //主循环 while(*e) { //取出一个标记 token = next_token(); //如果标记是操作数则入操作数栈 if(token.type == token_operand) { stack_push(s_num, token); continue; } else if(token.type == token_operator) { //如果是操作符 //判断操作符与栈顶元素的优先级 //取出栈顶元素 不弹栈 t = stack_top(s_ator); while(token.ator.icp <= t.ator.isp || token.ator.oper == oper_rparen) { //如果小于栈顶元素 //取出栈顶运算符进行计算 t = stack_pop(s_ator); switch(t.ator.oper) { //加 case oper_plus : a = stack_pop(s_num); b = stack_pop(s_num); if(a.var.type == var_string && b.var.type == var_string) { c.type = token_operand; c.var.type = var_string; strcat(b.var.s, a.var.s); strcpy(c.var.s, b.var.s); stack_push(s_num, c); } else if(a.var.type == var_double && b.var.type == var_double) { c.var.i = b.var.i + a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); } else { printf("表达式错误\n"); } break; //减 case oper_minus : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i - a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //乘 case oper_multiply : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i * a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //除 case oper_divide : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i / a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //小于 case oper_lt : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i < a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //大于 case oper_gt : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i > a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //等于 case oper_eq : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i == a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //小于等于 case oper_le : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i <= a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //大于等于 case oper_ge : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i >= a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //不等于 case oper_ne : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i != a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //与 case oper_and : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i && a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //或 case oper_or : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i || a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //非 case oper_not : b = stack_pop(s_num); c.var.i = !b.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //右括号 case oper_lparen : if(*e == '\0') { goto con; } token = next_token(); break; } t = stack_top(s_ator); } stack_push(s_ator, token); con: continue; } else { printf("表达式错误3\n"); } } t = stack_top(s_ator); while(t.ator.oper != oper_min) { t = stack_pop(s_ator); switch(t.ator.oper) { //加 case oper_plus : a = stack_pop(s_num); b = stack_pop(s_num); if(a.var.type == var_string && b.var.type == var_string) { c.type = token_operand; c.var.type = var_string; strcat(b.var.s, a.var.s); strcpy(c.var.s, b.var.s); stack_push(s_num, c); } else if(a.var.type == var_double && b.var.type == var_double) { c.var.i = b.var.i + a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); } else { printf("表达式错误\n"); } break; //减 case oper_minus : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i - a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //乘 case oper_multiply : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i * a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //除 case oper_divide : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i / a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //小于 case oper_lt : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i < a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //大于 case oper_gt : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i > a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //等于 case oper_eq : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i == a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //小于等于 case oper_le : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i <= a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //大于等于 case oper_ge : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i >= a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //不等于 case oper_ne : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i != a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //与 case oper_and : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i && a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //或 case oper_or : a = stack_pop(s_num); b = stack_pop(s_num); c.var.i = b.var.i || a.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; //非 case oper_not : b = stack_pop(s_num); c.var.i = !b.var.i; c.type = token_operand; c.var.type = var_double; stack_push(s_num, c); break; } t = stack_top(s_ator); } c = stack_pop(s_num); if(s_num->next == NULL) { return c.var; } else { printf("表达式错误4\n"); } //释放堆栈空间 stack_free(s_num); stack_free(s_ator); }
2.7 小结
至此,表达式求值得部分就完成了。表达式求值是这个解释器中很重要的一部分,在接下来的很多地方都会用到表达式求值得函数。接下来就可以写语句分析的部分了。
3. 语句分析
在这里,语句分析就是通过字符串匹配,判断输入的是何种语句,然后执行相应的操作。由于我们创造的语言中每一条语句都有相应开始的标记,例如赋值语句是 LET 开头。所以要识别输入的是何种语句只要判断一行代码开头的几个字符就可以。这里还有一点需要注意,那就是我们创造的语言一行只能写一条指令。
3.1 代码载入
开始,我们要设计一个结构体来存储语句,同时开一个数组来存储所有的代码。全局变量 cp 是下一条要执行的代码的行数,在执行循环或者分支的语句时,就是通过改变 cp 的值来控制程序执行。
//最多能读取的代码行数 #define PROGRAM_SIZE 1000 //存储每一行代码的结构体 typedef struct { int ln; //代码行号 STRING line; //代码 } CODE; //开一个数组存储代码 CODE code[PROGRAM_SIZE]; //下一条要执行的代码的行号 int cp; //总代码行数 int code_size;
函数 input_code 的作用是从文本文件中载入代码,并将代码存入数组中。由于在编写代码时为了代码的缩进和美观,在每一行的开头和结尾都有可能输入多余的空格,这个函数还应该经这些空格去掉。
//读取代码的函数 //将文件中的代码读取出来 //放入一个数组中 void input_code(STRING filename) { FILE *fp = fopen(filename, "r"); int bg, ed; if(fp == NULL) { printf("文件打开错误\n"); exit(-1); } cp = 0; //逐行读取字符串 while(fgets(code[cp].line, PROGRAM_SIZE, fp) != NULL && !feof(fp)) { //去掉字符串两边的空格 for(bg = 0; isspace(code[cp].line[bg]); bg++) ; //空循环 此时bg为前面空格的个数 ed = (int)strlen(code[cp].line + bg) - 1; //ed + bg 此时为字符串末尾的下标 //从字符串末尾开始循环 while(ed >= 0 && isspace(code[cp].line[bg + ed])) { ed--; } //ed+1 此时为去掉两边空格后中间字符的个数 if(ed >= 0) { memmove(code[cp].line, code[cp].line + bg, ed + 1); code[cp].line[ed + 1] = '\0'; } else { code[cp].line[0] = '\0'; } //代码行号 code[cp].ln = cp + 1; cp++; if(cp >= PROGRAM_SIZE) { printf("代码量太大,无法打开\n"); exit(-1); } } code_size = cp + 1; cp = 1; }
3.2 语句分析
语句分析就是匹配字符串,然后执行相应的函数。不同的语句相应的定义如下:
typedef enum { key_null = 0, //空语句 key_let, //赋值语句 key_input, //INPUT key_print, //PRINT key_for, //FOR key_endfor, //END FOR key_while, //WHILE key_endw, //END WHILE key_if, //IF key_else, //ELSE key_endif //END IF }keywords; 函数 yacc 的作用是匹配字符串,返回相应的语句: keywords yacc(const STRING line) { if(!strnicmp(line, "INPUT ", 6)) { return key_input; } else if(!strnicmp(line, "PRINT ", 6)) { return key_print; } else if(!strnicmp(line, "FOR ", 4)) { return key_for; } else if(!strnicmp(line, "ENDFOR", 6)) { return key_endfor; } else if(!strnicmp(line, "WHILE", 5)) { return key_while; } else if(!strnicmp(line, "ENDW", 4)) { return key_endw; } else if(!strnicmp(line, "IF ", 3)) { return key_if; } else if(!strnicmp(line, "ELSE", 4)) { return key_else; } else if(!strnicmp(line, "ENDIF", 5)) { return key_endif; } else if(!strnicmp(line, "LET ", 4)) { return key_let; } else if(*line == '\0') { return key_null; } else { printf("语法错误yacc\n"); exit(-1); } }
为了方便后面的编写,定义一个函数指针数组。
//声明一个函数指针数组 //方便调用 void (*key_func[])( const STRING ) = { exec_null, exec_let, exec_input, exec_print, exec_for, exec_endfor, exec_while, exec_endw, exec_if, exec_else, exec_endif };
解释语句的函数如下,该函数通过 yacc 函数识别出是何语句,然后调用相应的函数,执行相应的操作。
void exp_code(const char *filename) { input_code(filename); for(cp = 0; cp < code_size; cp++) { (*key_func[yacc(code[cp].line)])(code[cp].line); } }
3.3 INPUT 语句
input 语句是一个输入语句,输入有可能是字符串,也有可能是变量。
//input语句的函数 void exec_input(const STRING line) { const char *cs = line; int n; //变量的序号 //跳过 input_ cs += 5; while(*cs && isspace(*cs)) { //忽略空格 cs++; } if(isalpha(*cs) && *(cs + 1) == '\0') { //只能有一个变量 //转换为大写 n = toupper(*cs) - 'A'; } else { printf("input语法错误\n"); exit(-1); } if(!scanf("%lf", &(memory[n].i))) { //输入的是字符串 memory[n].type = var_string; scanf("%s", memory[n].s); } else { memory[n].type = var_double; } }
3.4 PRINT 语句
PRINT 语句是输出语句,有可能输出一个变量的值,也有可能是字符串或者是表达式。代码如下:
void exec_print(const STRING line) { const char *s = line; VARIANT v; s += 6; if(*s == '"') { //是字符串 s++; while(*s != '"') { if(s[0] == '\\' && s[1] == 'n') { //输出换行符 printf("\n"); s += 2; } else if(s[0] == '\\' && s[1] == 't') { //输出tab符 printf("\t"); s += 2; } else { printf("%c", *s); s++; } } } else { v = eval(s); if(v.type == var_string) { printf("%s", v.s); } else if(v.type == var_double) { printf("%lf", v.i); } } }
3.5 FOR 语句
FOR 语句的实现思路是:开始执行 FOR 语句时,将 FOR 语句的变量、行号、终止值、步长记录下来,当执行到 ENDFOR 语句时,检查变量的值是否小于终止值,若小于,则将变量加上步长后将 cp 的值变为 FOR 语句的起始行号,重新执行循环体;若不小于,则执行接下来的语句。
存储 FOR 语句信息的结构体定义如下,由于考虑到 FOR 语句可以循环,所以将用一个数组来保存 FOR 语句的信息。
//用于保存for循环中的信息 static struct { int id; //变量id int ln; //起始行号 double target; //终止值 double step; //步长 }stack_for[MEMORY_SIZE]; static int top_for = -1; //FOR循环函数 void exec_for(const STRING line) { //变量声明 STRING sl; int top; char *et = NULL; char *s; //line是地址,对ling的操作会改变它本身的内容 //所以要复制line的内容 strcpy(sl, line); s = sl; top = top_for + 1; s += 4; if(top >= MEMORY_SIZE) { printf("for循环过深\n"); exit(-1); } //去空格 while(*s && isspace(*s)) s++; if(isalpha(*s) && !isalnum(s[1])) { //如果s是字母且下一个字符不是字母和数字 stack_for[top].id = toupper(*s) - 'A'; stack_for[top].ln = cp; s++; } else { printf("语法错误1\n"); exit(-1); } //去空格 while(*s && isspace(*s)) s++; if(*s == '=') { s++; } else { printf("语法错误2\n"); exit(-1); } //去空格 while(*s && isspace(*s)) s++; et = strstr(s, " TO "); if(et != NULL) { *et = '\0'; //表达式求值 memory[stack_for[top].id] = eval(s); s = et + 4; } else { printf("语法错误X\n"); exit(-1); } et = strstr(s, " STEP "); if(et != NULL) { //有步长 *et = '\0'; //终止值 stack_for[top].target = eval(s).i; s = et + 5; //步长 stack_for[top].step = eval(s).i; if(fabs(stack_for[top].step) < 1E-6) { printf("步长太小\n"); exit(-1); } } else { //步长为1 //终止值 stack_for[top].target = eval(s).i; stack_for[top].step = 1; } if( (stack_for[top].step >= 0 && memory[stack_for[top].id].i > stack_for[top].target) || (stack_for[top].step <= 0 && memory[stack_for[top].id].i < stack_for[top].target) ) { while(yacc(code[cp].line) != key_endfor) { cp++; } return; } else { top_for++; } }
ENDFOR 语句只需要变量的值是否超过终止值,然后相应的改变 cp 的值即可。代码如下:
//ENDFOR语句函数 void exec_endfor(const STRING line) { if(stricmp(line, "ENDFOR")) { printf("语法错误21\n"); exit(-1); } if(top_for < 0) { printf("没有匹配的FOR语句\n"); } memory[stack_for[top_for].id].i += stack_for[top_for].step; if( stack_for[top_for].step > 0 && memory[stack_for[top_for].id].i > stack_for[top_for].target) { top_for--; } else if( stack_for[top_for].step < 0 && memory[stack_for[top_for].id].i < stack_for[top_for].target ) { top_for--; } else { cp = stack_for[top_for].ln; } }
3.6 WHILE 语句
WHILE 语句的实现思路与 FOR 语句类似,只是在保存语句信息时时保存语句的起始行号和逻辑表达式,若表达式为真则执行 WHILE 至 ENDW 之间的语句,否则不执行。当执行到 ENDW 语句时,再次判断表达式的值,若为真,则改变 cp 的值,从 WHILE 以下继续执行。代码如下:
//while语句函数 void exec_while(const STRING line) { char *s = line; double v; int top; top = top_while + 1; s += 5; if(top >= MEMORY_SIZE) { printf("循环过深\n"); exit(-1); } //复制表达式 strcpy(stack_while[top].eval, s); //求值 v = eval(stack_while[top].eval).i; if(v) { //如果表达式的值非零 stack_while[top].ln = cp; top_while += 1; return; } else { //如果表达式的值非零 //不执行循环 while(yacc(code[cp].line) != key_endw) { cp++; } return; } } //endw语句的函数 void exec_endw(const STRING line) { double v; v = eval(stack_while[top_while].eval).i; if(v) { cp = stack_while[top_while].ln; return; } else { top_while -= 1; return; } }
3.7 IF ELSE 语句
IF ELSE 语句的实现就相对比较简单,代码如下:
//IF语句函数 void exec_if(const STRING line) { char *s = line; s += 2; if(eval(s).i) { return; } else { while(yacc(code[cp].line) != key_else && yacc(code[cp].line) != key_endif) { cp++; if(cp > MEMORY_SIZE) { printf("line %d : 没有对应的else或endif语句\n", cp); exit(-1); } } } } void exec_else(const STRING line) { while(yacc(code[cp].line) != key_endif) { cp++; if(cp > MEMORY_SIZE) { printf("line %d : 没有对应的endif语句\n", cp); } } } //endif语句函数 void exec_endif(const STRING ling) { return; }
3.8 赋值语句
赋值语句也相对简单,代码如下:
//赋值语句函数 void exec_let(const STRING line) { char *s = line; int n; int i; s += 3; //去空格 while(*s && isspace(*s)) s++; if(isalpha(*s) && !isalpha(s[1])) { n = toupper(*s) - 'A'; s++; } else { printf("语法错误let 1\n"); exit(-1); } while(*s && isspace(*s)) s++; if(*s == '=') { s++; } else { printf("语法错误let 2"); exit(-1); } while(*s && isspace(*s)) s++; if(*s == '"') { //字符串赋值 memory[n].type = var_string; s++; for(i = 0; *s && *s != '"'; i++) { (memory[n].s)[i] = *s; s++; } if(*s != '"') { printf("语法错误let 3\n"); exit(-1); } } else { //表达式赋值 memory[n].type = var_double; memory[n] = eval(s); } }
3.9 空语句
在编写代码时经常出现空行,如果没有相应的函数处理空行将会出现错误。处理这个问题的方法就是:遇到空行,直接返回。
//空语句 void exec_null(const STRING line) { return; }
至此,整个解释器就算完成了。
4. 总结
写这个解释器的最初目的是想提高自己 C 语言编程的的能力。但是当这个解释器完成后,发现自己 C 语言的水平并没有多大的提高,但是编程的能力,阅读代码的能力还是有所提高的。整个解释器的代码超过 1000 行,这对自己还是有所提高的。至少以后有勇气面对 1000 行的代码了,也有勇气阅读别人的项目了。
为什么想要练习 C 语言,而不是练习更高级的语言,是因为我觉得编程最重要的是思想,不是用何种语言。语言终归到底只是一种工具。编程的主要目的是解决问题,用何种工具来解决问题应该要看哪种工具更加合适。但是解决问题的思路是独立于工具的,我希望我能培养起解决问题的思路,而不仅仅是熟练使用某一种工具。
在写这个编译器的过程中,最重要的思路是将大问题转化为小问题,将小问题的解决方法独立出来,作为一个模块。例如在这里的表达式求值,就能够独立的使用。语句分析中,将语句分析简化为字符串匹配,将各种语句用函数实现,这也有体现将大问题转化为小问题的思路。我觉得这个思路应该可以利用在方方面面,有待我的开发。
最后,这篇文章参考了《用 C 语言写解释器》中大量的内容,但本文由本人完成,转载请务必注明出处!