Lab3 词法分析实验
文章目录
1、实验目的
(1)熟悉 C 语言的词法规则,了解编译器词法分析器的主要功能和实现技术,掌握典型词法分析器构造方法,设计并实现 C 语言词法分析器;
(2)了解 Flex 工作原理和基本思想,学习使用工具自动生成词法分析器;
(3)掌握编译器从前端到后端各个模块的工作原理,词法分析模块与其他模块之间的交互过程。
2、实验内容
根据C语言词法规则,设计识别C语言所有单词类的词法分析器的确定有限状态自动机,并使用Java,采用数据中心法设计并实现词法分析器。词法分析器的输入为C语言源程序,输出为属性字流。
3、实验过程
3.1 DFA设计
3.1.1 状态设计
状态主要相关以下方面:1、字符常量,字符串常量,标识符识别。2、运算符,界限符识别。3、整数常量,浮点数常量识别。3、特殊状态BACK, EOF, NULL, INITIAL。下面为状态设计。
public static enum STATE{
//0、各种终止态
INITIAL, //初始态
IDENTIFIER, //标识符(终止态)
KEYWORD, //关键字(终止态)
INTEGER_CONSTANT, //整型常量(终止态)
FLOATING_CONSTANT, //浮点常量(终止态)
CHARACTER_CONSTANT, //字符常量(终止态)
STRING_LITERAL, //字符串常量(终止态)
OPERATORS, //操作符,界符(终止态)
EOF, //结束符号(终止态)
NULL, //无意义符号,用于空格等状态
ERROR, //错误态,识别到错误的情况
BACK, //回退一次
//以下为各个中间态
//1、识别标识符,字符常量,字符串常量使用的状态
MidIdentifier, //标识符中间态
UPrefix, //以U|u标识的字/字符串前缀
LPrefix, //以L|l标识的字/字符串前缀
U8Prefix, //以u8开头的字符串前缀
MidChar, //字符常量中间态
CHAR_WAIT, //字符常量阻塞态
EscapeChar, //含有转义\的字符常量中间态
EscapeStr, //含有转义\的字符串常量中间态
MidStr, //字符串常量中间态
STR_WAIT, //字符串常量阻塞态
//2、识别整形常量、浮点常量使用的状态
NoZeroDecInt, //非0十进制整数
Zero, //以0开头,可能是十进制/十六进制小数,也可能是10,8,16进制整数
OctInt, //8进制整数
HexInt, //16进制整数
DecFloatFrac, //十进制浮点数小数部分
IntSuffix, //整数后缀部分
HexFloatFrac, //16进制小数部分
Exponent, // e|E|p|P标识处
OrderPart, //阶码部分
FLSuffix, //浮点数的F/L后缀
// 2.1 识别u, l, ul, ull, lu, llu ll 这7种整数后缀的状态
ULSuffix,
ULLSuffix,
LUSuffix,
LLUSuffix,
USuffix,
LSuffix,
LLSuffix,
//3、识别运算符,界限符使用的状态
MidSymbol
}
根据设置好的状态绘制相应的DFA图。这里需要说明的是,由于状态过多,难以在一张图中绘制完成,并且字符、字符串以及常量的识别部分和数字产量识别部分没有交集,故以此为界限分为两个主要部分。一些特殊状态在下面说明。
这一部分对应字符常量、字符串常量、标识符以及操作符部分。黄色字体为终止态。
这一部分对应于整数常量,浮点数常量的识别部分。这里使用了两个特殊的状态 IntSuffix 和 BACK,对应于整形常量可能存在的 u/l 后缀识别以及特殊的需要程序进行控制的BACK状态。IntSuffix 则拓展(左边)如下。首先会根据当前字符选择迁移到 LSuffix 或者 USuffix 状态。后续再与进入的字符进行匹配。
右半部分对应的是两个特殊状态 ‘EOF’ 和 ‘NULL’,当读取到 ‘\u001a’ 时为EOF,读取到任何无法转移的字符时转移到NULL状态。
特别的是,考虑到类似如字符,字符串产量的识别当识别到单引号/双引号就可以终止,而类似于标识符则需要识别到other才能终止并回退剔除的原因。我添加了一些中间态,以便在扫描过程中所有的终止态能够统一用回退一次的操作进行控制。
3.1.2 DFA编码实现
使用JAVA编码实现DFA。还有一个状态集合STATE,终止态集合Set endStateSet,当前状态curState,字符集通过若干函数判断进行实现,初始状态为STATE.INITIAL,映射关系由成员函数 public void stateTransition(char curCh, String curStr)实现。
通过以上部分完成了DFA的构造,为了使用该DFA进行识别还需要构造一些数据类以及工具类。
3.2 Token
3.2.1 数据类Token
根据实验要求,最终输出到文件中的内容是具有一定格式的Token字符串,这里构造一个Token类,并重写.toString()方法实现自动根据内容产生Token串。
Token类的标识通过一个枚举 enum TokenTypes 实现。
//属性字类型
public static enum TokenTypes
{
IDENTIFIER,
KEYWORD,
INTEGER_CONSTANT,
FLOATING_CONSTANT,
CHARACTER_CONSTANT,
STRING_LITERAL,
OPERATORS,
EOF,
ERROR
}
Token具有内容,类型,id号,起始编号,结束编号,起始行号,起始列号这些属性。
private String content; //token原内容
private TokenTypes type; //token类型
private Integer id; //token序号
private Integer stNum; //token起始编号
private Integer edNum; //token结束编号
private Integer stRow; //token起始行。
private Integer stCol; //token起始列。
由于DFA会将关键字识别为标识符,在构造Token的时候需要判断标识符是否为关键字,将其转化为对应的关键字类型。此外输出时,KWYWORD类型和OPERATOR类型需要标记为其具体的标记。这两步使用两个函数实现:
//将关键字和运算符映射成具体的类型。
private static String mappingTokenTypeToActualType(Token token)
//将DFA终止状态映射为对应的Token类型
public static TokenTypes mappingDFAEndStateToTokenType(DFA.STATE state)
3.2.2 Tokens工具类TokensBlock
由于一组源代码对应于一组Token,为了便于管理,构造一个TokensBlock集合管理属于同一组源代码的Token。
TokensBlock记录对应源代码的文件路径地址,输出tokens标记的文件路径地址,提供添加Token方法。
3.3 标准元素StdElements
静态类 StdElements 记录所有C语言关键字以及相应的运算符,界符。提供对应的判别方法:
public static boolean isKeyWords(String str)
public static boolean isSymbol(String str)
3.4 扫描器C_CodeScanner
C_CodeScanner 能够扫描C语言源代码并生成相应tokens。成员变量如下:
private String srcPath;
private String[] src;
private DFA dfa;
private int curRowIndex;
private int curColIndex;
private int forePointer;//前向指针
private int backPointer;//后向指针
private TokensBlock tokensBlock;
这里读取文件部分本应使用框架提供的方法,但经过测试,框架提供的 this.srcLines=MiniCCUtil.readFile(iFile); 方法无法读取到文件行末尾的 ‘/r’, ‘/n’ 这类字符,而框架提供的默认词法分析器 antlr 在读取源文件时会读取到这些控制符,并且况且提供的pp在运行时会在每一行的行尾自动添加 “/r/n”(由反编译得到的字节码分析得到),因此,如果使用框架本身提供的读取方法使用DFA识别的结果会导致最终token定位标签(开始编号,结束编号)与 antlr 结果不一致。因此我在 C_CodeScanner 中使用 FileInputStream 的方式来读取文件而不是使用框架提供的 BufferReader 来进行文件读取。这笔部分代码如下:
public String[] readFile(String fPath)
{
List<String> strList = new ArrayList<>();
FileInputStream in = null;
StringBuilder strb = new StringBuilder();
try{
in =new FileInputStream(fPath);
}catch(FileNotFoundException e) {
System.out.println("can not find it");
System.exit(1);
}
try{
int cur = 0;
while((cur = in.read()) != -1){
//System.out.println(count);
strb.append((char)cur);
if((char)cur == '\n') {
strList.add(strb.toString());
strb = new StringBuilder();
}
}
strList.add("\u001a");
}catch(IOException e) {
e.printStackTrace();
}
String[] srcArray = new String[strList.size()];
for(int i = 0; i < strList.size(); i++) {
srcArray[i] = strList.get(i);
}
return srcArray;
}
扫描器按照行进行扫描。每次扫描过程中先读取当前指示的字符,然后进行状态转移。转移完成后进行一系列判定并更新各个变量。具体说明如下:
1、终态非空:(1)后向指针退回一格,计数器i减一。(2)添加Token,根据DFA状态以及各个指针变量信息构造Token并添加到tokensBlock中。(3)状态重置,调用方法重置状态机,清空Token字符串构造器,让前向指针移动到后向指针+1即: forePointer = backPointer + 1,重设列指针,与forePointer同步。这样在一次扫描后后向指针前移以同步指针。
2、非空且为EOF:完成终止态非空后将计数器回复即可。EOF状态到达文末,不需要回退扫描。
3、终态为空(NULL):状态为NULL时,为识别到一些不具备识别意义的单字符。这里需要调整前向指针来同步。forePointer = baackPointer +1;
4、终态为BACK(转移后):转移后为back状态需要让字符串构造器删除最后一个字符,并且后向指针和计数器i -2 (实际是后退1,但下次循环开始时会自增,这里需要先-2)。
5、终态为BACK(转移前):由于回退了一个字符,这里需要替换被会退掉的字符为空字符来让状态机从前一个状态进行空转移,看是否能转移到一个终止态,如果不行状态机会再次到BACK状态,前移。类似于回溯的做法。
6、其他状态:其他状态只需要字符构造器添加当前的字符即可。
扫描器这一部分和DFA是实现的关键。共同实现了自动代码识别以及分类,完成了词法分析的主要工作。其他数据,方法都为这一目的提供支持。具体的扫描器扫描过程代码如下:
private void rowAnalyse(String code)
{
StringBuilder tokenContent = new StringBuilder();
curColIndex = 0;
for(int i = 0; i < code.length(); i++)
{
char curCh = code.charAt(i);
if(CDfa.getCurState() == DFA.STATE.BACK)
curCh = '\0';
curColIndex = i;
CDfa.stateTransition(curCh, tokenContent.toString());
if(CDfa.isEndState() && CDfa.getCurState() != DFA.STATE.NULL && CDfa.getCurState() != DFA.STATE.EOF) //转移后达到终止态
{
backPointer -= 1;
i--; //需要回退一次重新确认状态
//1、添加token
Token.TokenTypes type = Token.mappingDFAEndStateToTokenType(CDfa.getCurState());
Token token = new Token(tokenContent.toString(), type, tokensBlock.getTokensSize(), forePointer, backPointer, curRowIndex, curColIndex - (backPointer - forePointer) - 1 );
tokensBlock.addToken(token);
//System.out.println(token.toString());
//2、重置变量
tokenContent = new StringBuilder();
forePointer = backPointer + 1;
CDfa.initDFA();
}
else if(CDfa.isEndState() && CDfa.getCurState() == DFA.STATE.EOF)
{
backPointer -= 1;
Token.TokenTypes type = Token.mappingDFAEndStateToTokenType(CDfa.getCurState());
Token token = new Token(tokenContent.toString(), type, tokensBlock.getTokensSize(), forePointer, backPointer, curRowIndex, curColIndex - (backPointer - forePointer) - 1 );
tokensBlock.addToken(token);
CDfa.initDFA();
}
else if(CDfa.isEndState() && CDfa.getCurState() == DFA.STATE.NULL)
{
forePointer = backPointer + 1;
CDfa.initDFA();
}
else if(CDfa.getCurState() == DFA.STATE.BACK)
{
tokenContent.deleteCharAt(tokenContent.length()-1); //回退
backPointer -= 2;
i -= 2; //需要回退一次重新确认状态
}
else {
tokenContent.append(curCh);
}
backPointer += 1;
}
}
4、实验运行及结果
4.1 框架装入
完成代码编写和测试后,需要将其装入框架。下图为我的包结构:
将 MyScanner 包整个装入框架的src文件夹下,形成如下结构:
为了在框架中便于使用,在bit.minisys.minicc.scanner下创建类 C_JavaScanner ,该类实现框架中的 IMiniCCScanner 接口。将 run 方法重写如下:
这里输出,创建文件的方式使用的仍然是框架提供的方法。
为了在框架中调用 C_JavaScanner 类,修改 config.xml ,结果如下:
由于考虑到框架中存在预处理程序,我也并没有写处理注释的情况,这里选择使用默认的预处理程序,在第二步将路径调整为所写的类路径。