辣鸡的我终于在一个已经保研的小哥哥(萌似泰迪)的帮助下完成了解释器!!(VS2013)
分为3步:词法分析器、语法分析器、语义分析器
代码大部分出自《编译原理基础-习题与上机解答》(西安电子科技大学出版社)中的附录
下面会上所有代码附带(超级)大量详细注释和理解,以及很多处理细节。因为在这些在高手看来顺理成章的过程才是新手很大的障碍。
step 1
安装Virsual Stidio 2013
经过我的实践和另一个小哥哥的经验:windos7只能安装vs2013版的,否则就会出现下面这种2015版装了3个小时进度条还没前进的情况
不要装在C盘,这样如果系统崩了,C盘会全丢失,而且放进C盘电脑会运行变慢。
step 2
上代码之前,先来说说词法分析器。
“sacanner”是词法分析器,输入流是序列,输出流是“字典”。
我们需要
1、设计记号:词法分析器读取一个序列并根据构词规则把序列转化为记号流
2、定义一个字典:把所有符合一个模式的保留字、常量名、参数名、函数名等放进字典。字典是个数组,其元素的类型和记号的类型相同
3、设计程序的结构,具体见下面的代码
step3(重头戏)
新建一个词法分析器的项目
把已经编写好的代码扔进去:scanner.h scanner.c scannermain.c
1 //-----------------------scanner.h-------------------- 2 #pragma once 3 //#ifndef SCANNER_H 4 //#define SCANNER_H 5 #define _CRT_SECURE_NO_WARNINGS 6 7 #include<stdio.h> 8 #include<string.h> 9 #include<stdlib.h> 10 #include<ctype.h> 11 #include<stdarg.h> 12 #include<math.h> 13 14 enum Token_Type//枚举记号的类别 15 { 16 ORIGIN,SCALE,ROT,IS,TO,STEP,DRAW,FOR,FROM,//保留字 17 T,//参数 18 SEMICO,L_BRACKET, R_BRACKET,COMMA,//分隔符 19 PLUS,MINUS,MUL,DIV,POWER,//运算符 20 FUNC,//函数 21 CONST_ID,//常数 22 NONTOKEN,//空记号 23 ERRTOKEN//出错记号 24 }; 25 26 typedef double(*MathFuncPtr) (double); 27 28 struct Token//记号的数据结构 记号由类别和属性组成 29 { 30 Token_Type type;//记号的类别 31 char *lexeme;//属性,原始输入的字符串 是个字符指针,当需要记号的字符串时,就会引用这个指针,但是字符串保留在TokenBuffer中,所以要指向TokenBuffer 32 double value; //为常数设置,是常数的值 33 double(*FuncPtr)(double); //为函数设置,是函数的指针 34 }; 35 //正规式个数越少越利于程序编写,所以把相同模式的记号共用一个正规式描述,要设计出一个预定义的符号表(就是一个数组),进行区分~ 36 static Token TokenTab[]=//符号表(字典):数组元素的类型于记号的类型相同 37 {//当识别出一个ID时,通过此表来确认具体是哪个记号 38 { CONST_ID, "PI", 3.1415926, NULL }, 39 { CONST_ID, "E", 2.71828, NULL }, 40 { T, "T", 0.0, NULL }, 41 { FUNC, "SIN", 0.0, sin }, 42 { FUNC, "COS", 0.0, cos }, 43 { FUNC, "TAN", 0.0, tan }, 44 { FUNC, "LN", 0.0, log }, 45 { FUNC, "EXP", 0.0, exp }, 46 { FUNC, "SQRT", 0.0, sqrt }, 47 { ORIGIN, "ORIGIN", 0.0, NULL }, 48 { SCALE, "SCALE", 0.0, NULL }, 49 { ROT, "ROT", 0.0, NULL }, 50 { IS, "IS", 0.0, NULL }, 51 { FOR, "FOR", 0.0, NULL }, 52 { FROM, "FROM", 0.0, NULL }, 53 { TO, "TO", 0.0, NULL }, 54 { STEP, "STEP", 0.0, NULL }, 55 { DRAW, "DRAW" , 0.0, NULL } 56 }; 57 58 extern unsigned int LineNo; //跟踪记好所在源文件行号 59 extern int InitScanner(const char*); //初始化词法分析器 60 extern Token GetToken(void); //获取记号函数 61 extern void CloseScanner(void); //关闭词法分析器 62 63 //#endif
1 #include"scanner.h" 2 #ifndef MSCANNER_H 3 #define MSCANNER_H 4 #define TOKEN_LEN 100//设置一个字符缓冲区,这是他的大小用来保留记号的字符串 5 unsigned int LineNo;//记录字符所在行的行号-》词法分析器对每个记号的字符串进行分析时必须记住该字符串在源程序的位置 6 static FILE *InFile;//打开绘图语言源程序时,指向该源程序的指针 7 static char TokenBuffer[TOKEN_LEN];//设置一个字符缓冲区,用来保留记号的字符串,当需要记号的字符串时,char*lexeme指针会指向TokenBuffer 8 9 //--------------------初始化词法分析器 10 extern int InitScanner(const char *FileName)//输入要分析的源程序文件名 11 { 12 LineNo = 1; 13 InFile = fopen(FileName, "r"); 14 if (InFile != NULL) 15 return 1; //如果存在,打开文件,并初始化lineNO的值为1,返回true 16 else 17 return 0;//不存在返回0 18 } 19 20 //---------------------关闭词法分析器 21 extern void CloseScanner(void) 22 { 23 if (InFile != NULL) 24 fclose(InFile); 25 } 26 27 //--------------------从输入源程序中读入一个字符 28 static char GetChar(void) 29 { 30 int Char = getc(InFile); 31 return toupper(Char);//输出源程序的一个字符,没有输入 32 } 33 34 //--------------------把预读的字符退回到输入源程序中,分析的过程中需要预读1、2...个字符,预读的字符必须回退,以此保证下次读时不会丢掉字符 35 static void BackChar(char Char)//输入:回退一个字符, 没有输出 36 { 37 if (Char != EOF) 38 ungetc(Char, InFile); 39 } 40 41 //--------------------加入字符到TokenBuffer-----把已经识别的字符加到TokenBuffer 42 static void AddCharTokenString(char Char)//输入源程序的一个字符 没有输出 43 { 44 int TokenLength = strlen(TokenBuffer);//设定好长度 45 if (TokenLength + 1 >= sizeof(TokenBuffer)) 46 return;//此时字符串的长度超过最大值,返回错误 47 TokenBuffer[TokenLength] = Char;//添加一个字符 48 TokenBuffer[TokenLength + 1] = '