2 词法分析
实验要求
使用C/C++/Java语言编写,设计、编制一个识别一简单语言单词的词法分析程序。程序能够识别字符串表中的基本字、标识符、无符号整数、浮点数、运算符和界符。实现编译过程中的词法分析过程。程序最终的使用功能是输入源程序(测试),输出单词符号信息。
词法分析是整个编译过程的基础,为之后的语法分析、语义分析提供可输入的结果。
使用C/C++语言编写PL/0编译程序的词法分析程序。 需要注意的点:
- 识别非法字符:如 @ 、 & 和 ! 等;
- 识别非法单词:数字开头的数字字母组合;
- 标识符和无符号整数的长度不超过8位;
- 能自动识别并忽略
/**/
及//格式的注释信息; - 词法分析过程中遇到错误后能继续往下识别,并输出错误信息。
实验分析
根据PL/0的单词可以划分为5个大类:
- 保留字: const , var , procedure , begin , end , odd , if , then , call , while , do , read , write
- 标识符:是字母开头的字母数字序列,字母包括大小写英文字母: a , b , ..., z , A , B , …, Z
- 运算符:共有11个,包括4个整型算数运算符号 + 、 - 、 * 和 / ,6个比较运算符号
< 、 <= 、 > 、 >= 、 #
和 = ,1个赋值运算符 := - 无符号整数:是由一个或多个数字组成,数字为 0 , 1 , 2 , … , 9
- 界符:共有5个,包括 ( ) , ; .
其基本思想是根据每一行的输入,在分析每一行代码时,跳过空行和注释行(以"//"开头的行),并将代码行按照空白字符进行拆分。然后,遍历拆分后的每个词法单元,并使用正则表达式和预定义的模式进行匹配。
以下是分析完成的状态转移图:
实现
根据状态转移图代码的执行流程算法设计如下:
- 通过读取控制台输入获取代码内容,并将代码逐行存储到一个字符串构建器对象中。
- 对存储的代码内容进行词法分析,首先按行将代码分割成字符串数组。
- 遍历每一行的代码,并去除首尾的空白字符。遍历字符串数组完成后结束代码
- 检查当前行是否进入或退出注释区域,根据行开头是否为"/"或行结尾是否为"/"来确定。
- 如果当前行在注释区域内或为注释行(以"//"开头),则跳过该行的分析。
- 对于非注释行,按字符逐个扫描当前行的代码内容。扫描一行完成后回到步骤3。
- 根据字符的特征判断其可能的类型:字母表示保留字或标识符、数字表示无符号整数、特定字符表示运算符或界符、其他字符可能为非法字符。
- 如果识别出保留字,则输出"(保留字, 值)"的格式;如果识别出标识符,则输出"(标识符, 值)"的格式;如果标识符长度超过限制,则输出"(标识符长度超长, 值, 行号)"的格式。字符序列加一回到6步骤。
- 如果识别出无符号整数,则输出"(无符号整数, 值)"的格式;如果无符号整数超过范围,则输出"(无符号整数越界, 值, 行号)"的格式 。字符序列加一回到6步骤。
- 如果识别出运算符,则输出"(运算符,token)"的格式。字符序列加一回到6步骤。
- 如果识别出界符,则输出(界符,token)的格式。字符序列加一回到6步骤。
- 除了8,9,10,11判断外,其他情况是非法字符,输出(非法字符,字符,行号)的格式。字符序列加一回到6步骤。
以下是词法分析的类图:
具体代码
import java.util.Scanner; //用于从控制台读取输入
import java.util.regex.Matcher; //用于匹配正则表达式
import java.util.regex.Pattern; //用于编译正则表达式
public class LexicalAnalysis {
private static final String[] KEYWORDS = { //用于存储保留字
"const", "var", "procedure", "begin", "end", "if", "then", "while", "do", "call", "read", "write"
};
private static final String[] OPERATORS = { //用于存储运算符
"+", "-", "*", "/", "=", "<", "<=", ">", ">=", "<>" ,"#" ,":="
};
private static final String[] DELIMITERS = { //用于存储界符
",", ";", ".", "(", ")"
};
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[a-zA-Z]\\w{0,7}$"); //以字母开头,后面跟0到7个字母或数字或下划线
private static final Pattern INTEGER_PATTERN = Pattern.compile("^\\d{1,8}$"); //(1到8位数字)
private static final Pattern COMMENT_PATTERN = Pattern.compile("(\\/\\*([\\s\\S]*?)\\*\\/)|(\\/\\/.*$)"); //用于编译注释的正则表达式(以/*开头,以*/结尾,或以//开头)
private static final Pattern ILLEGAL_CHAR_PATTERN = Pattern.compile("[^\\w\\s]"); //用于编译非法字符的正则表达式(除了字母、数字、下划线和空白字符之外的字符)
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
StringBuilder code = new StringBuilder(); //创建一个StringBuilder对象code,用于存储输入的代码
while (scanner.hasNextLine()) { //当控制台还有下一行输入时执行以下操作:
String line = scanner.nextLine(); //读取一行输入并赋值给字符串变量line
code.append(line).append("\n"); //将line追加到code中,并加上换行符
if (line.equals("end.")) { //如果line等于"end.",则表示输入结束
break;
}
}
scanner.close();
analyze(code.toString());
}
private static void analyze(String code) { //用于对输入的代码进行词法分析,并输出词法单元及其类型和值
String[] lines = code.split("\\n"); //将输入的代码按换行符分割成字符串数组lines
boolean insideComment = false; //定义一个布尔变量insideComment,用于记录当前是否在注释内,默认为false
for (int lineNumber = 0; lineNumber < lines.length; lineNumber++) { //遍历每一行line,使用整数变量lineNumber 记录当前行号
String line = lines[lineNumber].trim(); //获取当前行的内容,并去除首尾空白字符,赋值给字符串变量line
if (line.isEmpty()) { //如果line为空,则跳过
continue;
}
if (line.startsWith("/*")) { //如果line以/*开头,则表示进入注释
insideComment = true; //设置insideComment为true
}
if (line.endsWith("*/")) { //如果line以*/结尾,则表示退出注释
insideComment = false; //设置insideComment为false
continue; //跳过当前行
}
if (insideComment || isComment(line)) { //如果insideComment为true或者line是注释,则跳过当前行
continue;
}
StringBuilder token = new StringBuilder(); //创建一个StringBuilder对象token,用于存储当前识别出的词法单元
int index = 0; //创建一个整数变量index,用于记录当前扫描到的字符位置,默认为0
int lineLength = line.length(); //创建一个整数变量lineLength,用于记录当前行的长度
while (index < lineLength) { //当index小于lineLength时执行以下操作:
char currentChar = line.charAt(index); //获取当前字符currentChar
if (Character.isLetter(currentChar)) { //如果currentChar是字母,则表示可能是保留字或标识符
token.append(currentChar); //将currentChar追加到token中
index++; //将index加一
while (index < lineLength && (Character.isLetterOrDigit(line.charAt(index)) || line.charAt(index) == '_')) { //继续向后扫描,直到遇到非字母或数字或下划线的字符为止
token.append(line.charAt(index)); //将扫描到的字符追加到token中
index++; //将index加一
}
if (isKeyword(token.toString())) { //判断token是否是保留字
System.out.println("(保留字," + token.toString() + ")"); //如果是,则输出(保留字,token)的格式
} else if (isValidIdentifier(token.toString())) { //否则判断token是否是合法标识符
System.out.println("(标识符," + token.toString() + ")"); //如果是,则输出(标识符,token)的格式
} else { //否则表示标识符长度超长
System.out.println("(标识符长度超长," + token.toString() + ",行号:" + (lineNumber + 1) + ")"); //输出(标识符长度超长,token,行号)的格式
}
token.setLength(0); //清空token,准备下一次识别
} else if (Character.isDigit(currentChar)) { //如果currentChar是数字,则表示可能是无符号整数
token.append(currentChar); //将currentChar追加到token中
index++; //将index加一
boolean flag = false;
while (index < lineLength && Character.isLetter(line.charAt(index)) && line.charAt(index) != ' ') { //继续向后扫描,直到遇到非数字的字符为止
while (index < lineLength && line.charAt(index) != ' '){
token.append(line.charAt(index));
index++;
}
System.out.println("(非法字符(串)," + token.toString() + ",行号:" + (lineNumber + 1) + ")");
flag = true;
break;
}
if(!flag){
while (index < lineLength && Character.isDigit(line.charAt(index))) { //继续向后扫描,直到遇到非数字的字符为止
token.append(line.charAt(index)); //将扫描到的字符追加到token中
index++; //将index加一
}
if (isValidInteger(token.toString())) { //判断token是否是合法无符号整数
System.out.println("(无符号整数," + token.toString() + ")"); //如果是,则输出(无符号整数,token)的格式
} else { //否则表示无符号整数越界
System.out.println("(无符号整数越界," + token.toString() + ",行号:" + (lineNumber + 1) + ")"); //输出(无符号整数越界,token,行号)的格式
}
}
token.setLength(0); //清空token,准备下一次识别
} else if (isOperator(currentChar)) { //如果currentChar是运算符,则表示可能是单个或两个字符组成的运算符
token.append(currentChar); //将currentChar追加到token中
index++; //将index加一
if (index < lineLength && isTwoCharOperator(token.toString() + line.charAt(index))) { //判断是否存在两个字符组成的运算符(如<=,>=等)
token.append(line.charAt(index)); //如果存在,则将下一个字符也追加到token中
index++; //将index加一
}
System.out.println("(运算符," + token.toString() + ")"); //输出(运算符,token)的格式
token.setLength(0); //清空token,准备下一次识别
} else if (isDelimiter(currentChar)) { //如果currentChar是界符,则表示是单个字符的界符
token.append(currentChar); //将currentChar追加到token中
index++; //将index加一
System.out.println("(界符," + token.toString() + ")"); //输出(界符,token)的格式
token.setLength(0); //清空token,准备下一次识别
} else { //否则表示可能是非法字符
Matcher matcher = ILLEGAL_CHAR_PATTERN.matcher(String.valueOf(currentChar)); //使用ILLEGAL_CHAR_PATTERN匹配currentChar
if (matcher.find()) { //如果匹配成功,则表示是非法字符
System.out.println("(非法字符(串)," + currentChar + ",行号:" + (lineNumber + 1) + ")"); //输出(非法字符,currentChar,行号)的格式
}
index++; //将index加一
}
}
}
}
private static boolean isComment(String line) {
Matcher matcher = COMMENT_PATTERN.matcher(line);
return matcher.find();
}
private static boolean isKeyword(String token) {
for (String keyword : KEYWORDS) {
if (keyword.equals(token)) {
return true;
}
}
return false;
}
private static boolean isValidIdentifier(String token) {
Matcher matcher = IDENTIFIER_PATTERN.matcher(token);
return matcher.matches();
}
private static boolean isValidInteger(String token) {
Matcher matcher = INTEGER_PATTERN.matcher(token);
if (matcher.matches()) {
try {
int value = Integer.parseInt(token);
return value >= 0;
} catch (NumberFormatException e) {
return false;
}
}
return false;
}
private static boolean isOperator(char ch) {
for (String operator : OPERATORS) {
if (operator.indexOf(ch) != -1) {
return true;
}
}
return false;
}
private static boolean isTwoCharOperator(String token) {
for (String operator : OPERATORS) {
if (operator.equals(token)) {
return true;
}
}
return false;
}
private static boolean isDelimiter(char ch) {
for (String delimiter : DELIMITERS) {
if (delimiter.indexOf(ch) != -1) {
return true;
}
}
return false;
}
}