用C语言写解释器(四)——语句分析

声明

为提高教学质量,我所在的学院正在筹划编写C语言教材。《用C语言写解释器》系列文章经整理后将收入书中“综合实验”一章。因此该系列的文章主要阅读对象定为刚学完C语言的学生(不要求有数据结构等其他知识),所以行文比较罗嗦,请勿见怪。本人水平有限,如有描述不恰当或错误之处请不吝赐教!特此声明。

语句

在前面的章节中已经成功实现了内存管理和表达式求值模块。之所以称表达式求值是解释器的核心部分,是因为几乎所有语句的操作都伴随着表达式求值。也许你已经迫不及待地给 eval 传值让它执行复杂的运输了,但目前来讲它充其量只是一个计算器。要想成为一门语言,还需要一套自成体系的语法,包括输入输出语句和控制语句。但在进行语法分析之前,首先需要将 BASIC 源码载入到内存中。

BASIC 源码载入

在《用C语言写解释器(一)》中附了一段 BASIC 参考代码,每一行的结构是一个行号+一条语句。其中行号为 1-9999 之间的正整数,且当前行号大于前面的行号;语句则由以下即将介绍的 3 条 I/O 语句和 8 条控制语句组成。为方便编码,程序中采用静态数组来保存源代码,读者可以尝试用链表结构实现动态申请的版本。下面是代码结构的定义。

  1. // in basic_io.h  
  2. #define PROGRAM_SIZE (10000)  
  3.   
  4. typedef struct {  
  5.     int ln;     // line number  
  6.     STRING line;  
  7. } CODE;  
  8.   
  9. extern CODE code[PROGRAM_SIZE];  
  10. extern int cp;  
  11. extern int code_size;  

其中 code_size 的作用顾名思义:记录代码的行数。cp (0 ≤ cp < code_size)记录当前行的下标(比如 cp 等于5时表明执行到第5行)。下面是载入 BASIC 源码的参考代码,在载入源码的同时会去除两端的空白字符。

  1. // in basic_io.c  
  2. void load_program ( STRING filename )  
  3. {  
  4.     FILE *fp = fopen ( filename, “r” );  
  5.     int bg, ed;  
  6.   
  7.     if ( fp == NULL ) {  
  8.         fprintf ( stderr, ”文件 %s 无法打开!/n”, filename );  
  9.         exit ( EXIT_FAILURE );  
  10.     }  
  11.   
  12.     while ( fscanf ( fp, “%d”, &code[cp].ln ) != EOF ) {  
  13.         if ( code[cp].ln <= code[cp-1].ln ) {  
  14.             fprintf ( stderr, ”Line %d: 标号错误!/n”, cp );  
  15.             exit ( EXIT_FAILURE );  
  16.         }  
  17.   
  18.         fgets ( code[cp].line, sizeof(code[cp].line), fp );  
  19.         for ( bg = 0; isspace(code[cp].line[bg]); bg++ );  
  20.         ed = (int)strlen ( code[cp].line + bg ) - 1;  
  21.         while ( ed >= 0 && isspace ( code[cp].line[ed+bg] ) ) {  
  22.             ed–;  
  23.         }  
  24.         if ( ed >= 0 ) {  
  25.             memmove ( code[cp].line, code[cp].line + bg, ed + 1 );  
  26.             code[cp].line[ed + 1] = 0;  
  27.         } else {  
  28.             code[cp].line[0] = 0;  
  29.         }  
  30.   
  31.         cp++;  
  32.         if ( cp >= PROGRAM_SIZE ) {  
  33.             fprintf ( stderr, ”程序%s太大,代码空间不足!/n”, filename );  
  34.             exit ( EXIT_FAILURE );  
  35.         }  
  36.     }  
  37.   
  38.     code_size = cp;  
  39.     cp = 1;  
  40. }  

语法分析

源码载入完成后就要开始逐行分析语句了,程序中总共能处理以下 11 种语句:

  1. // in main.c  
  2. typedef enum {  
  3.     key_input = 0,  // INPUT  
  4.     key_print,      // PRINT  
  5.     key_for,        // FOR .. TO .. STEP  
  6.     key_next,       // NEXT  
  7.     key_while,      // WHILE  
  8.     key_wend,       // WEND  
  9.     key_if,         // IF  
  10.     key_else,       // ELSE  
  11.     key_endif,      // END IF  
  12.     key_goto,       // GOTO  
  13.     key_let         // LET  
  14. } keywords;  

用C语言写解释器(一)》中详细描述了每个语句的语法,本程序中所谓的语法其实就是字符串匹配,参考代码如下:

  1. // in main.c  
  2. keywords yacc ( const STRING line )  
  3. {  
  4.     if ( !strnicmp ( line, “INPUT ”, 6 ) ) {  
  5.         return key_input;  
  6.     } else if ( !strnicmp ( line, “PRINT ”, 6 ) ) {  
  7.         return key_print;  
  8.     } else if ( !strnicmp ( line, “FOR ”, 4 ) ) {  
  9.         return key_for;  
  10.     } else if ( !stricmp ( line, “NEXT” ) ) {  
  11.         return key_next;  
  12.     } else if ( !strnicmp ( line, “WHILE ”, 6 ) ) {  
  13.         return key_while;  
  14.     } else if ( !stricmp ( line, “WEND” ) ) {  
  15.         return key_wend;  
  16.     } else if ( !strnicmp ( line, “IF ”, 3 ) ) {  
  17.         return key_if;  
  18.     } else if ( !stricmp ( line, “ELSE” ) ) {  
  19.         return key_else;  
  20.     } else if ( !stricmp ( line, “END IF” ) ) {  
  21.         return key_endif;  
  22.     } else if ( !strnicmp ( line, “GOTO ”, 5 ) ) {  
  23.         return key_goto;  
  24.     } else if ( !strnicmp ( line, “LET ”, 4 ) ) {  
  25.         return key_let;  
  26.     } else if ( strchr ( line, ‘=’ ) ) {  
  27.         return key_let;  
  28.     }  
  29.   
  30.     return -1;  
  31. }  

每个语句对应有一个执行函数,在分析出是哪种语句后,就可以调用它了!为了编码方便,我们将这些执行函数保存在一个函数指针数组中,请看下面的参考代码:

  1. // in main.c  
  2. void (*key_func[])( const STRING ) = {  
  3.     exec_input,  
  4.     exec_print,  
  5.     exec_for,  
  6.     exec_next,  
  7.     exec_while,  
  8.     exec_wend,  
  9.     exec_if,  
  10.     exec_else,  
  11.     exec_endif,  
  12.     exec_goto,  
  13.     exec_assignment  
  14. };  
  15.   
  16. int main ( int argc, char *argv[] )  
  17. {  
  18.     if ( argc != 2 ) {  
  19.         fprintf ( stderr, ”usage: %s basic_script_file/n”, argv[0] );  
  20.         exit ( EXIT_FAILURE );  
  21.     }  
  22.   
  23.     load_program ( argv[1] );  
  24.   
  25.     while ( cp < code_size ) {  
  26.         (*key_func[yacc ( code[cp].line )]) ( code[cp].line );  
  27.         cp++;  
  28.     }  
  29.   
  30.     return EXIT_SUCCESS;  
  31. }  

以上代码展示的就是整个程序的基础框架,现在欠缺的只是每个语句的执行函数,下面将逐个详细解释。

I/O语句

输入输出是一个宽泛的概念,并不局限于从键盘输入和显示到屏幕上,还包括操作文件、连接网络、进程通信等。《我们的目标》中指出只需实现从键盘输入(INPUT)和显示到屏幕上(PRINT),事实上还应该包括赋值语句,只不过它属于程序内部的I/O。

INPUT 语句

INPUT 语句后面跟着一堆变量名(用逗号隔开)。因为变量是弱类型,你可以输入数字或字符串。但C语言是强类型语言,为实现这个功能就需要判断一下 scanf 的返回值。我们执行 scanf ( “%lf”, &memory[n].i ),如果你输入的是一个数字,就能成功读取一个浮点数,函数返回 1、否则就返回 0;不能读取时就采用 getchar 来获取字符串!参考代码如下:

  1. // in basic_io.c  
  2. void exec_input ( const STRING line )  
  3. {  
  4.     const char *s = line;  
  5.     int n;  
  6.   
  7.     assert ( s != NULL );  
  8.     s += 5;  
  9.   
  10.     while ( *s ) {  
  11.         while ( *s && isspace(*s) ) {  
  12.             s++;  
  13.         }  
  14.         if ( !isalpha(*s) || isalnum(*(s+1)) ) {  
  15.             perror ( ”变量名错误!/n” );  
  16.             exit ( EXIT_FAILURE );  
  17.         } else {  
  18.             n = toupper(*s) - ’A’;  
  19.         }  
  20.   
  21.         if ( !scanf ( “%lf”, &memory[n].i ) ) {  
  22.             int i;  
  23.             // 用户输入的是一个字符串  
  24.             memory[n].type = var_string;  
  25.             if ( (memory[n].s[0] = getchar()) == ‘”’ ) {  
  26.                 for ( i = 0; (memory[n].s[i]=getchar())!=‘”’; i++ );  
  27.             } else {  
  28.                 for ( i = 1; !isspace(memory[n].s[i]=getchar()); i++ );  
  29.             }  
  30.             memory[n].s[i] = 0;  
  31.         } else {  
  32.             memory[n].type = var_double;  
  33.         }  
  34.   
  35.         do {  
  36.             s++;  
  37.         } while ( *s && isspace(*s) );  
  38.         if ( *s && *s != ‘,’ ) {  
  39.             perror ( ”INPUT 表达式语法错误!/n” );  
  40.             exit ( EXIT_FAILURE );  
  41.         } else if ( *s ) {  
  42.             s++;  
  43.         }  
  44.     }  
  45. }  

输出相对简单些,PRINT 后面跟随的是一堆表达式,表达式只需委托给 eval 来求值即可,因此 PRINT 要做的仅仅是按照值的类型来输出结果。唯一需要小心的就是类似 PRINT “hello, world” 这样字符串中带有逗号的情况,以下是参考代码:

  1. // in basic_io.c  
  2. void exec_print ( const STRING line )  
  3. {  
  4.     STRING l;  
  5.     char *s, *e;  
  6.     VARIANT v;  
  7.     int c = 0;  
  8.   
  9.     strcpy ( l, line );  
  10.     s = l;  
  11.   
  12.     assert ( s != NULL );  
  13.     s += 5;  
  14.   
  15.     for (;;) {  
  16.         for ( e = s; *e && *e != ‘,’; e++ ) {  
  17.             // 去除字符串  
  18.             if ( *e == ‘”’ ) {  
  19.                 do {  
  20.                     e++;  
  21.                 } while ( *e && *e != ‘”’ );  
  22.             }  
  23.         }  
  24.         if ( *e ) {  
  25.             *e = 0;  
  26.         } else {  
  27.             e = NULL;  
  28.         }  
  29.   
  30.         if ( c++ ) putchar ( ‘/t’ );  
  31.         v = eval ( s );  
  32.         if ( v.type == var_double ) {  
  33.             printf ( ”%g”, v.i );  
  34.         } else if ( v.type == var_string ) {  
  35.             printf ( v.s );  
  36.         }  
  37.   
  38.         if ( e ) {  
  39.             s = e + 1;  
  40.         } else {  
  41.             putchar ( ’/n’ );  
  42.             break;  
  43.         }  
  44.     }  
  45. }  

LET 语句

在 BASIC 中,“赋值”和“等号”都使用“=”,因此不能像 C 语言中使用 A = B = C 这样连续赋值,在 BASIC 中它的意思是判断 B 和 C 的值是否相等并将结果赋值给 A 。而且关键字 LET 是可选的,即 LET A = 1 和 A = 1 是等价的。剩下的事情那个就很简单了,只要将表达式的值赋给变量即可。以下是参考代码:

  1. // in basic_io.c  
  2. void exec_assignment ( const STRING line )  
  3. {  
  4.     const char *s = line;  
  5.     int n;  
  6.   
  7.     if ( !strnicmp ( s, “LET ”, 4 ) ) {  
  8.         s += 4;  
  9.     }  
  10.     while ( *s && isspace(*s) ) {  
  11.         s++;  
  12.     }  
  13.     if ( !isalpha(*s) || isalnum(*(s+1)) ) {  
  14.         perror ( ”变量名错误!/n” );  
  15.         exit ( EXIT_FAILURE );  
  16.     } else {  
  17.         n = toupper(*s) - ’A’;  
  18.     }  
  19.   
  20.     do {  
  21.         s++;  
  22.     } while ( *s && isspace(*s) );  
  23.     if ( *s != ‘=’ ) {  
  24.         fprintf ( stderr, ”赋值表达式 %s 语法错误!/n”, line );  
  25.         exit ( EXIT_FAILURE );  
  26.     } else {  
  27.         memory[n] = eval ( s + 1 );  
  28.     }  
  29. }  

控制语句

现在是最后一个模块——控制语句。控制语句并不参与交互,它们的作用只是根据一定的规则来改变代码指针(cp)的值,让程序能到指定的位置去继续执行。限于篇幅,本节只介绍 for、next 以及 goto 三个控制语句的实现方法,读者可以尝试自己完成其他函数,也可以参看附带的完整代码。

FOR 语句

先来看一下 FOR 语句的结构:

FOR var = expression1 TO expression2 [STEP expression3]

它首先要计算三个表达式,获得 v1、v2、v3 三个值,然后让变量(var)从 v1 开始,每次迭代都加 v3,直到超出 v2 的范围位置。因此,每一个 FOR 语句,我们都需要保存这四个信息:变量名、起始值、结束值以及步长。另外,不要忘记 FOR 循环等控制语句可以嵌套使用,因此需要开辟一组空间来保存这些信息,参考代码如下:

  1. // in grammar.h  
  2. static struct {  
  3.     int id;           // memory index  
  4.     int ln;           // line number  
  5.     double target;    // target value  
  6.     double step;  
  7. } stack_for[MEMORY_SIZE];  
  8. static int top_for = -1;  

分析的过程就是通过 strstr 在语句中搜索“=”、“TO”、“STEP”等字符串,然后将提取的表达式传递给 eval 计算,并将值保存到 stack_for 这个空间中。参考代码如下:

  1. // in grammar.c  
  2. void exec_for ( const STRING line )  
  3. {  
  4.     STRING l;  
  5.     char *s, *t;  
  6.     int top = top_for + 1;  
  7.   
  8.     if ( strnicmp ( line, “FOR ”, 4 ) ) {  
  9.         goto errorhandler;  
  10.     } else if ( top >= MEMORY_SIZE ) {  
  11.         fprintf ( stderr, ”FOR 循环嵌套过深!/n” );  
  12.         exit ( EXIT_FAILURE );  
  13.     }  
  14.   
  15.     strcpy ( l, line );  
  16.   
  17.     s = l + 4;  
  18.     while ( *s && isspace(*s) ) s++;  
  19.     if ( isalpha(*s) && !isalnum(s[1]) ) {  
  20.         stack_for[top].id = toupper(*s) - ’A’;  
  21.         stack_for[top].ln = cp;  
  22.     } else {  
  23.         goto errorhandler;  
  24.     }  
  25.   
  26.     do {  
  27.         s++;  
  28.     } while ( *s && isspace(*s) );  
  29.     if ( *s == ‘=’ ) {  
  30.         s++;  
  31.     } else {  
  32.         goto errorhandler;  
  33.     }  
  34.   
  35.     t = strstr ( s, ” TO ” );  
  36.     if ( t != NULL ) {  
  37.         *t = 0;  
  38.         memory[stack_for[top].id] = eval ( s );  
  39.         s = t + 4;  
  40.     } else {  
  41.         goto errorhandler;  
  42.     }  
  43.   
  44.     t = strstr ( s, ” STEP ” );  
  45.     if ( t != NULL ) {  
  46.         *t = 0;  
  47.         stack_for[top].target = eval ( s ).i;  
  48.         s = t + 5;  
  49.         stack_for[top].step = eval ( s ).i;  
  50.         if ( fabs ( stack_for[top].step ) < 1E-6 ) {  
  51.             goto errorhandler;  
  52.         }  
  53.     } else {  
  54.         stack_for[top].target = eval ( s ).i;  
  55.         stack_for[top].step = 1;  
  56.     }  
  57.   
  58.     if ( (stack_for[top].step > 0 &&   
  59.          memory[stack_for[top].id].i > stack_for[top].target)||  
  60.          (stack_for[top].step < 0 &&   
  61.          memory[stack_for[top].id].i < stack_for[top].target)) {  
  62.         while ( cp < code_size && strcmp(code[cp].line, “NEXT”) ) {  
  63.             cp++;  
  64.         }  
  65.         if ( cp >= code_size ) {  
  66.             goto errorhandler;  
  67.         }  
  68.     } else {  
  69.         top_for++;  
  70.     }  
  71.   
  72.     return;  
  73.   
  74. errorhandler:  
  75.     fprintf ( stderr, ”Line %d: 语法错误!/n”, code[cp].ln );  
  76.     exit ( EXIT_FAILURE );  
  77. }  

NEXT 语句

NEXT 的工作就简单得多了。它从 stack_for 这个空间中取出最后一组数据,让变量的值累加上步长,并判断循环是否结束。如果结束就跳出循环执行下一条语句;否则就将代码指针移回循环体的顶部,继续执行循环体。下面是参考代码。

  1. // in grammar.c  
  2. void exec_next ( const STRING line )  
  3. {  
  4.     if ( stricmp ( line, “NEXT” ) ) {  
  5.         fprintf ( stderr, ”Line %d: 语法错误!/n”, code[cp].ln );  
  6.         exit ( EXIT_FAILURE );  
  7.     }  
  8.     if ( top_for < 0 ) {  
  9.         fprintf ( stderr, ”Line %d: NEXT 没有相匹配的 FOR!/n”, code[cp].ln );  
  10.         exit ( EXIT_FAILURE );  
  11.     }  
  12.   
  13.     memory[stack_for[top_for].id].i += stack_for[top_for].step;  
  14.     if ( stack_for[top_for].step > 0 &&   
  15.          memory[stack_for[top_for].id].i > stack_for[top_for].target ) {  
  16.         top_for–;  
  17.     } else if ( stack_for[top_for].step < 0 &&   
  18.          memory[stack_for[top_for].id].i < stack_for[top_for].target ) {  
  19.         top_for–;  
  20.     } else {  
  21.         cp = stack_for[top_for].ln;  
  22.     }  
  23. }  

GOTO 语句

也许你认为 GOTO 语句只是简单的将 cp 的值设置为指定的行,但事实上它比想象中的要复杂些。考虑下面的 BASIC 代码:

0010 I = 5
0020 GOTO 40
0030 FOR I = 1 TO 10
0040   PRINT I
0050 NEXT

像这类代码,直接跳到循环体内部,如果只是简单地将 cp 移动到指定位置,当代码继续执行到 NEXT 时就会报告没有对应的 FOR 循环!跳到其他的控制结构,如 WHILE、IF 等,也会出现相同的问题。以下是参考代码(有删减)。

  1. // in grammar.c  
  2. void exec_goto ( const STRING line )  
  3. {  
  4.     int ln;  
  5.   
  6.     if ( strnicmp ( line, “GOTO ”, 5 ) ) {  
  7.         fprintf ( stderr, ”Line %d: 语法错误!/n”, code[cp].ln );  
  8.         exit ( EXIT_FAILURE );  
  9.     }  
  10.   
  11.     ln = (int)eval ( line + 5 ).i;  
  12.     if ( ln > code[cp].ln ) {  
  13.         // 往下跳转  
  14.         while ( cp < code_size && ln != code[cp].ln ) {  
  15.             if ( !strnicmp ( code[cp].line, “IF ”, 3 ) ) {  
  16.                 top_if++;  
  17.                 stack_if[top_if] = 1;  
  18.             } else if ( !stricmp ( code[cp].line, “ELSE” ) ) {  
  19.                 stack_if[top_if] = 1;  
  20.             } else if ( !stricmp ( code[cp].line, “END IF” ) ) {  
  21.                 top_if–;  
  22.             } else if ( !strnicmp ( code[cp].line, “WHILE ”, 6 ) ) {  
  23.                 top_while++;  
  24.                 stack_while[top_while].isrun = 1;  
  25.                 stack_while[top_while].ln = cp;  
  26.             } else if ( !stricmp ( code[cp].line, “WEND” ) ) {  
  27.                 top_while–;  
  28.             } else if ( !strnicmp ( code[cp].line, “FOR ”, 4 ) ) {  
  29.                 int i = 4;  
  30.                 VARIANT v;  
  31.                 while ( isspace(code[cp].line[i]) ) i++;  
  32.                 v = memory[toupper(code[cp].line[i])-’A’];  
  33.                 exec_for ( code[cp].line );  
  34.                 memory[toupper(code[cp].line[i])-’A’] = v;  
  35.             } else if ( !stricmp ( code[cp].line, “NEXT” ) ) {  
  36.                 top_for–;  
  37.             }  
  38.             cp++;  
  39.         }  
  40.     } else if ( ln < code[cp].ln ) {  
  41.         // 往上跳转  
  42.         // 代码类似,此处省略  
  43.     } else {  
  44.         // 我不希望出现死循环,你可能有其他处理方式  
  45.         fprintf ( stderr, ”Line %d: 死循环!/n”, code[cp].ln );  
  46.         exit ( EXIT_FAILURE );  
  47.     }  
  48.   
  49.     if ( ln == code[cp].ln ) {  
  50.         cp–;  
  51.     } else {  
  52.         fprintf ( stderr, ”标号 %d 不存在!/n”, ln );  
  53.         exit ( EXIT_FAILURE );  
  54.     }  
  55. }  

总结

本章介绍了源码载入、语法分析以及部分语句的实现,WHILE 和 IF 等控制语句方法和 FOR、NEXT 类似,有兴趣的读者请尝试自己实现(或者参看附带的完整源码)。这样一个解释器的四个关键部分“内存管理”、“表达式求值”、“输入输出”和“控制语句”就全部介绍完了,希望你也能写出自己的解释器。下一篇我将总结一下我个人对编程语言的一些思考,如果你也有兴趣请继续关注《用C语言写解释器(五)》!


版权声明

请尊重原创作品。转载请保持文章完整性,并以超链接形式注明原始作者“redraiment”和主站点地址,方便其他朋友提问和指正。

联系方式

我的邮箱,欢迎来信(redraiment@gmail.com
我的Blogger(子清行):http://redraiment.blogspot.com/
我的Google Sites(子清行):https://sites.google.com/site/redraiment
我的CSDN博客(梦婷轩):http://blog.csdn.net/redraiment
我的百度空间(梦婷轩):http://hi.baidu.com/redraiment

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值