本次实验之前看了很多学长的代码,给了我很多的思路和解决方法,灰常感谢。
编译原理实验一词法分析器实验报告
一、实验目的
学习和掌握词法分析程序手工构造状态图及其代码实现方法。
二、实验任务
⑴阅读已有编译器的经典词法分析源程序;
⑵用C或C++语言编写一门语言的词法分析器。
三、实验内容
① 我选择的编译器以及需要分析的语言是TINY
TINY语言的详细定义:
TINY的程序结构很简单,它在语法上与 Ada或Pascal的语法相似:仅是一个由分号分隔开的语句序列。另外,它既无过程也无声明。所有的变量都是整型变量,通过对其赋值可较轻易地声明变量(类似FORTRAN或BASIC)。
它只有两个控制语句:if语句和repeat语句,这两个控制语句本身也可包含语句序列。if语句有一个可选的else部分且必须由关键字end结束。除此之外,read语句和write语句完成输入/输出。在花括号中可以有注释,但注释不能嵌套。TINY的表达式也局限于布尔表达式和整型算术表达式。布尔表达式由对两个算术表达式的比较组成,该比较使用<与=比较算符。算术表达式可以包括整型常数、变量、参数以及 4个整型算符+、-、、/,此外还有一般的数学属性。布尔表达式可能只作为测试出现在控制语句中——而没有布尔型变量、赋值或I / O。
TINY语言特点总结:
a.语句序列用分号隔开
b.所有变量都是整形变量,且不需要声明
c.只有两个控制语句,if和repeat
d.if判断语句必须以end结束,且有可选的else语句
e.read和write完成输入输出
f.花括号表示注释,但不允许嵌套注释
g.有<和=两个比较运算符h.有+、-、、/简单运算符
关键字表:
TINY可识别的词法单元有{ID,单词字母},{NUM,数字},{COMMENT,注释},{ASSIGN,特殊符号:=},{SYMBOL,特殊符号±*/=<();},{ERROR,错误符号}。其中识别的单词字母中有可能有TINY保留字
{if,then,else,end,repeat,until,read,write},于是TINY的关键字表如下图所示
② 依据TINY关键字表画出的DFA状态转换图如下图所示:
- START状态
当读入空格或者缩进符时状态不变,当读入数字时进入INNUM,读入字母时进入INID,读入:时进入识别:=的INASSIGN,读入{时进入注释INCOMMENT,读入特殊字符±*/=<();直接进入DONE识别完成。 - INID、INNUM状态
当读入字母、数字时,INID和INNUM分别保持现有状态,但输入其他字符时都进入DONE识别完成。 - INASSIGN状态
当读入=时就进入DONE识别出特殊字符:=,但是读入其他字符时进入ERROR错误状态。 - INCOMMENT状态
当读入}时表示注释完毕,重新进入START,读入其他任何字符时都识别为注释内容,状态不变。 - ERROR状态
当读入 字母 数字 : + - * / = < ( ) ; { ‘ ’ ‘\t’ 这些字符时表示需要进入DONE完成错误字符的识别,因为这些都能被TINY识别。但是当输入其他任何字符都不能被TINY识别,保持在ERROR状态。 - DONE状态
进入这个状态表示一次TINY标记token识别完毕,需要开始输出识别的词法单元。
③ 编写的TINY词法分析器程序的重要变量及关键函数 - 重要变量的声明
- codeLoading() 代码加载函数
首先打开test.txt文件,assert()函数检测test.txt是否打开。然后进入一个循环,不断取出文件中的一行代码字符,并用line字符串存储,lineno存储当前行号。随后输出当前行号及代码字符串。判定
line.empty(),如果不为null调用scanToken()函数进行词法扫描。最后当文件读取到EOF表示文件读取完毕时发现状态state仍然为
INCOMMENT说明TINY代码存在一个错误“注释未完成”,于是打印提示信息。
3. isID()识别一个字母 isNUM()识别一个数字
isOperator()识别一个除了:=的TINY特殊符号
isWhiteSpace()识别一个空格或者缩进符
这些函数就不做分析了,因为实现原理很简单就是if 判断。
4. identifyReserved()识别一个字符串是否为保留字并转换标记
实现过程也很简单,输入识别的字符串和它的标记,利用if else识别,如果字符串为TINY保留字则将其标记转换为对应的保留字标记。否则不改变其标记。
5. showWord()输出当前代码行识别出的词法单元<标记token,字符串string>
利用switch分支根据标记分别输出对应的词法分析语句,保留字输出“reserved word:” ,单词输出“ID,name=”,数字输出“NUM,val=”,错误输出“ERROR,error=”,特殊字符直接输出。
6. scanToken()词法扫描函数
每次扫描一行代码的字符串,所以主循环是while(linepos < linesize)取字符分析直到line的最后一个字符被分析完毕。使用switch()实现一个状态转换函数,case的情况就是当前状态state的情况,每个case表示的state状态又因为当前识别字符串的不同又进行分支。总的分析情况就不赘述了,完全与上面给出的DFA状态转换图一致。其中saveflag = true表示识别的字符需要加入识别字符串ans中去,每进入一次DONE状态都代表一个标记被识别,就要给token赋对应的值,而且对应的识别字符串ans输出后需要ans.clear()清空初始化以迎接下一次识别。
还需要注意的地方有,当INID、INNUM、INASSIGN、ERROR状态分别识别了非字母字符、非数字字符、非 = 字符、非字母 数字 空格 缩进 特殊符号 左花括号字符后都需要linepos-- 表示当前字符虽然未被识别但是已经被匹配过,可能是其他状态的可识别字符,需要回溯。最后每到一个代码行的末尾必须强制进入DONE状态,因为除了INCOMMENT状态外没有哪一个字符串可以换行表示,所以也需要一个switch()来分析当前状态强制进入DONE后的情况。最后再根据标记token和识别字符串ans调用showWord()输出。
④ 测试用例
-
test1.txt //正确无错误的代码
-
test2.txt //出现错误字符,并且测试注释范围的代码
-
test3.txt //出现“未完成注释”错误的代码
四、思考题
在充分理解状态转换图代码化思想的基础上,思考不同的程序设计语言从词法角度有什么区别,可利用增量编程的思想提高编程效率。
答:
对于不同的程序设计语言,词法角度脱离不了几个大块:
保留字、特殊符号、和主要类型(ID, NUM, COMMENT,……)
在保持原有框架基础上,可以将主要类型的DFA构建出来,实现各种状态间的跳转与接受。
对于特殊符号,较复杂的特殊符号可以同理使用DFA构造。
五、学习心得
通过本次TINY词法分析器的实验,感觉对于编译原理课上讲解的NFA、DFA的各种转换图的理解更深入了。对于真正的编译器词法分析有了一定的认识,而且状态转换函数的编写让自己的思维更加的缜密,因为需要考虑到所有可能出现的情况并编写相应的语句进行操作的执行。总的来说,本次试验思维过程不是很难,难的是情况的考虑,理论课上感觉理解了,实际操作才发现有各种各样的问题,全部解决了也受益良多。
六、完整源码
#include <iostream>
#include <fstream>
#include <assert.h>
using namespace std;
enum TokenType{ //规定TINY语言可能出现的标记
ERR,NONE, //错误,空词
IF,THEN,ELSE,END,REPEAT,UNTIL,READ,WRITE, //TINY的保留词
ID,NUM, //单词,数字
ASSIGN,PLUS,MINUS,MULTI,DIV,LESS,LPAR,RPAR,COLON,EQ //TINY的特殊符号
};
enum StateType{ //规定状态机的所有状态
START,DONE,ERROR, //开始状态,结束状态,错误状态
INID,INNUM,INASSIGN,INCOMMENT //单词状态,数字状态,:=符号状态,注释状态
};
int lineno = 0; //当前代码行
int linepos = 0; //当前代码行字符位置
int linesize = 0; //当前代码行长度
bool saveflag = true; //当前字符是否保存到当前识别字符串标志
string ans = ""; //当前识别字符串
string line; //当前代码行
StateType state = START; //当前状态机状态
TokenType token; //当前TINY标记
bool isID(char c){ //识别一个字符是否为字母
if((c >= 'a' && c <= 'z')||(c >= 'A' && c <= 'Z'))
return true;
return false;
}
bool isNUM(char c){ //识别一个字符是否为数字
if(c >= '0' && c <= '9')
return true;
return false;
}
bool isOperator(char c){ //识别一个字符是否为TINY特殊运算符(除了:=)
if(c == '+'||c == '-'||c == '*'||c == '/'||c == '='||c == '<'||c == '('||c == ')'||c == ';')
return true;
return false;
}
bool isWhiteSpace(char c){ //识别一个字符是否为空格或者缩进符
if(c == ' ' || c == '\t')
return true;
return false;
}
TokenType identifyReserved(TokenType tok,string s){ //识别一个字符串是否为保留字并进行转换
if(s == "if") return IF;
else if(s == "else") return ELSE;
else if(s == "then") return THEN;
else if(s == "end") return END;
else if(s == "repeat") return REPEAT;
else if(s == "until") return UNTIL;
else if(s == "read") return READ;
else if(s == "write") return WRITE;
else return tok;
}
void showWord(TokenType tok,string s){ //输出当前代码行识别出的标记以及识别字符串
if(tok == NONE)
return;
switch(tok){
case IF:
case THEN:
case ELSE:
case END:
case REPEAT:
case UNTIL:
case READ:
case WRITE:
cout << " " << lineno << ": " << "reserved word: " << s << endl;break; //以上标记为TINY保留字
case ID:
cout << " " << lineno << ": " << "ID, name= " << s << endl;break; //标记为单词
case NUM:
cout << " " << lineno << ": " << "NUM, val= " << s << endl;break; //标记为数字
case ERR:
cout << " " << lineno << ": " << "ERROR, error= " << s << endl;break; //标记为错误
case ASSIGN:
case PLUS:
case MINUS:
case MULTI:
case DIV:
case LESS:
case LPAR:
case RPAR:
case COLON:
case EQ:
cout << " " << lineno << ": " << s << endl;break; //以上标记为TINY特殊字符
default: break;
}
}
void scanToken(){ //词法扫描当前代码行
ans.clear(); //新的代码行开始初始化识别字符串
linepos = 0; //当前代码行字符位置初始化
linesize = (int)line.length(); //获得当前代码行长度
while(linepos < linesize){ //代码行字符位置小于代码行长度时状态机不断识别字符
saveflag = true; //字符是否保存到当前识别字符串标志初始化
char ch = line[linepos]; //ch存储当前字符
switch(state){ //状态机
case START://开始状态
if(isWhiteSpace(ch)) saveflag = false; //状态不变,字符不保存
else if(isID(ch)) state = INID; //进入单词状态,字符保存
else if(isNUM(ch)) state = INNUM; //进入数字状态,字符保存
else if(ch == ':') state = INASSIGN; //及进入:=符号状态,字符保存
else if(ch == '{') {saveflag = false;state = INCOMMENT;} //进入注释状态,字符不保存
else if(isOperator(ch)){
state = DONE; //识别TINY特殊符号,进入结束状态
if(ch == '+') token = PLUS; // +标记为PLUS
else if(ch == '-') token = MINUS; // -标记为MINUS
else if(ch == '*') token = MULTI; // *标记为MULTI
else if(ch == '/') token = DIV; // /标记为DIV
else if(ch == '=') token = EQ; // =标记为EQ
else if(ch == '<') token = LESS; // <标记为LESS
else if(ch == '(') token = LPAR; //(标记为LPAR
else if(ch == ')') token = RPAR; // )标记为RPAR
else token = COLON; // ;标记为COLON
}
else state = ERROR; //其余字符进入错误状态
break;
case INID://单词状态
//字符不为字母时,进入结束状态,字符位置返回,字符不保存,标记为ID
if(!isID(ch)) {state = DONE;linepos--;saveflag = false;token = ID;}
break;
case INNUM://数字状态
//字符不为数字时,进入结束状态,字符位置返回,字符不保存,标记为NUM
if(!isNUM(ch)) {state = DONE;linepos--;saveflag = false;token = NUM;}
break;
case INASSIGN://:=符号状态
if(ch == '=') {state = DONE;token = ASSIGN;} //字符为 = 时,进入结束状态,字符保存,标记为ASSIGN
else {state = ERROR;linepos--;saveflag = false;} //其余字符进入错误状态,字符位置返回,字符不保存
break;
case INCOMMENT://注释状态
if(ch == '}') {state = START;saveflag = false;} //字符为 } 时,进入开始状态,字符不保存
else saveflag = false; //其余字符状态不变,字符不保存
break;
case ERROR://错误状态
//字符为数字、字母、空格、缩进、特殊符号、花括号时进入结束状态,字符位置返回,字符不保存,标记为ERR
if(isNUM(ch)||isID(ch)||isWhiteSpace(ch)||isOperator(ch)||ch == ':'||ch == '{')
{state = DONE;linepos--;saveflag = false;token = ERR;}
break;
default:break;
}
linepos++; //字符位置向后偏移
if(saveflag) //当前字符ch保存标记为真时,将ch加入识别字符串ans
ans += ch;
if(linepos == linesize){ //字符位置等于代码行长度,溢出
switch(state){
case START: state = DONE;token = NONE;break; //开始 -> 结束,标记为NONE
case INID: state = DONE;token = ID;break; //单词 -> 结束,标记为ID
case INNUM: state = DONE;token = NUM;break; //数字 -> 结束,标记为NUM
case INASSIGN: state = DONE;token = ERR;break; // := -> 结束,标记为ERR
case ERROR: state = DONE;token = ERR;break; //错误 -> 结束,标记为ERR
default: break;
}
}
// 当前为结束状态时,进入开始状态,输出当前代码行识别出的标记以及识别字符串,清空识别字符串
if(state == DONE) {state = START;showWord(identifyReserved(token,ans),ans);ans.clear();}
}
}
void codeLoading(){ //代码加载函数
fstream myfile("test1.txt",ios::in); //打开 test1.txt test2.txt test3.txt
assert(myfile.is_open()); //检测 test.txt 是否正常打开
while(!myfile.eof()){ //文件输入未检测到字符EOF结束
line.clear(); //清空之前的代码行字符串
getline(myfile,line); //将当前代码行字符串(包含空格、缩进,不包含\n)赋予line
cout << "LINE" << ++lineno << ":" << line << endl; //输出当前代码行号及代码行字符串
if(!line.empty()) //当前代码行字符串非空时进行词法扫描
scanToken();
if(myfile.eof() && state == INCOMMENT) //当文件输入检测到字符EOF结束且状态仍为注释状态时报告 注释未完成 错误
cout << " " << lineno << ": " << "ERROR, error= Incomplete comment";
}
}
int main()
{
codeLoading();
return 0;
}