概述:
本次实验作为编译技术课程实验的第一次正式实验(正式开始编译器的代码实现),任务量并不大。词法分析气的主要作用是将源代码输入进行划分与识别,删除掉其中的空格与注释,并从中提取出一个个Token,以便后续实验使用。
一、问题描述
1.1任务概览
1.2 单词类别码
二、实现思路
本次实验需要我们从原文件输入中识别出各个单词类别码并保存。我们需要完成一个Lexer类,并完成该类中最主要的一个方法next () 。该方法的实现思路如下:
1、首先我们应该把原文件输入中的所有空格符和换行符清除得到一个仅包含有效信息的字符串。
2、然后,按照单词类别码,我们可以将其大致分为四类:标识符及保留字、数字、运算符、注释
3、针对每一个类别码编写相应的判断逻辑即可
三、实现细节
3.1 标识符及保留字:
这类字符串的判断逻辑为:我们先把所有的保留字也视作标识符,当读取出一个完整的标识符后,去保留字表中查找该标识符是否为保留字,如果是则返回对应保留字的类别码,否则,返回标识符的类别码。进一步的,识别标识符的判断逻辑为,如果当前读入的字符是一个字母,则进行连续读入,直到读到非数字字母及下划线的字符终止(因为标识符定义规定是以非数字开头,仅包含字母数字下划线)。其部分代码实现如下:
if (isNonDigit(c)) { // 标识符或保留字
token += c;
while (curPos < source.length() && (isNonDigit(source.charAt(curPos)) || Character.isDigit(source.charAt(curPos)))) {
// 下一个字符为数字或字母
c = source.charAt(curPos++);
token += c;
}
reserve(); // 查关键字表
}
3.2 无符号整数
这类字符串的识别逻辑为:(在识别完上一个token之后)读取一个新字符,若该字符是数字,则进行连续读入,直到读入非数字为止。其实现代码如下:
else if (Character.isDigit(c)) { // 无符号整数
token += c;
while (curPos < source.length() && Character.isDigit(source.charAt(curPos))) {
// 下一个符号是数字
c = source.charAt(curPos++);
token += c;
}
tokenType = TokenType.INTCON; // 设置单词类别
number = Integer.valueOf(token); // 转化为数值
}
3.3 运算符
这类字符串在读如以后直接判断即可,以 "+" 为例,代码如下:
if (c == '+' || c == '-' || c == '*' || c == '%' || c == ';' || c == ',' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}') {
token += c;
reserve();
}
3.4 注释
注释的判断比较复杂,其中需要识别出单行注释"//" 和多行注释 "/**/" 两种注释,这里我是参考指导PPT,使用有限状态机进行判断的,其实现代码如下:
else if (c == '/') { // 第一个 /
token += c;
if (curPos < source.length() && source.charAt(curPos) == '/') { //
// 第二个 /
c = source.charAt(curPos++); //
token += c;
while (curPos < source.length() && source.charAt(curPos) != '\n') {
// 非换行字符
c = source.charAt(curPos++);
token += c;
}
if (curPos < source.length()) { // \n 或 直接结束
c = source.charAt(curPos++);
token += c;
lineNum++; // 单行注释末尾的\n
}
return next();
} else if (curPos < source.length() && source.charAt(curPos) == '*') {
// /* 跨行注释 用状态机判断
c = source.charAt(curPos++);
token += c;
while (curPos < source.length()) { // 状态转换循环(直至末尾)
while (curPos < source.length() && source.charAt(curPos) != '*') {
// 非*字符 对应状态q5
c = source.charAt(curPos++);
token += c;
if (c == '\n') lineNum++; // 多行注释中 每行最后的回车
}
// *
while (curPos < source.length() && source.charAt(curPos) == '*') {
// *字符 对应状态q6 如果没有转移到q7,则会在循环中转移到q5
c = source.charAt(curPos++);
token += c;
}
if (curPos < source.length() && source.charAt(curPos) == '/') {
// /字符 对应状态q7
c = source.charAt(curPos++);
token += c;
return next();
}
}
} else {
reserve();
}
}
3.5 其它细节
对于next () 函数的设计,我使用了一个curPos变量来指向当前待判断的字符,如果判断source[curPos]属于当前正在判断的Token,则将其加入token,并把curPos++(source是完整记录源文件内容的字符串)。类似于预读判断。此外我把Lexer设置为一个单例模式,方便管理。Lexer的属性如下。
private static Lexer instance; // 单例实例
private TokenType tokenType; // 解析单词类型
private String source; // 源程序字符串
private int curPos; // 当前字符串位置指针
private String token; // 解析单词值
private int lineNum; // 当前行号
private int number; // 解析数值
private Map<String, TokenType> reserveWords; // 保留字表
private ArrayList<Token> tokens = new ArrayList<>(); // 词法单元序列
四、其他
Lexer词法分析器作为我们实现的编译器前端中的重要组成部分,负责源文件的第一遍分析,在之后的实验中,我们只需要调用Lexer.getTokens即可获得一个装有全部token的ArrayList,以便后续实验的进行