跳到主要内容

2 词法分析

实验要求

使用C/C++/Java语言编写,设计、编制一个识别一简单语言单词的词法分析程序。程序能够识别字符串表中的基本字、标识符、无符号整数、浮点数、运算符和界符。实现编译过程中的词法分析过程。程序最终的使用功能是输入源程序(测试),输出单词符号信息。

词法分析是整个编译过程的基础,为之后的语法分析、语义分析提供可输入的结果。

使用C/C++语言编写PL/0编译程序的词法分析程序。 需要注意的点:

  1. 识别非法字符:如 @ 、 & 和 ! 等;
  2. 识别非法单词:数字开头的数字字母组合;
  3. 标识符和无符号整数的长度不超过8位;
  4. 能自动识别并忽略/**/及//格式的注释信息;
  5. 词法分析过程中遇到错误后能继续往下识别,并输出错误信息。

实验分析

根据PL/0的单词可以划分为5个大类:

  1. 保留字: const , var , procedure , begin , end , odd , if , then , call , while , do , read , write
  2. 标识符:是字母开头的字母数字序列,字母包括大小写英文字母: a , b , ..., z , A , B , …, Z
  3. 运算符:共有11个,包括4个整型算数运算符号 + 、 - 、 * 和 / ,6个比较运算符号 < 、 <= 、 > 、 >= 、 # 和 = ,1个赋值运算符 :=
  4. 无符号整数:是由一个或多个数字组成,数字为 0 , 1 , 2 , … , 9
  5. 界符:共有5个,包括 (   )   ,   ;   .

其基本思想是根据每一行的输入,在分析每一行代码时,跳过空行和注释行(以"//"开头的行),并将代码行按照空白字符进行拆分。然后,遍历拆分后的每个词法单元,并使用正则表达式和预定义的模式进行匹配。

以下是分析完成的状态转移图:

词法分析状态转移图

实现

根据状态转移图代码的执行流程算法设计如下:

  1. 通过读取控制台输入获取代码内容,并将代码逐行存储到一个字符串构建器对象中。
  2. 对存储的代码内容进行词法分析,首先按行将代码分割成字符串数组。
  3. 遍历每一行的代码,并去除首尾的空白字符。遍历字符串数组完成后结束代码
  4. 检查当前行是否进入或退出注释区域,根据行开头是否为"/"或行结尾是否为"/"来确定。
  5. 如果当前行在注释区域内或为注释行(以"//"开头),则跳过该行的分析。
  6. 对于非注释行,按字符逐个扫描当前行的代码内容。扫描一行完成后回到步骤3。
  7. 根据字符的特征判断其可能的类型:字母表示保留字或标识符、数字表示无符号整数、特定字符表示运算符或界符、其他字符可能为非法字符。
  8. 如果识别出保留字,则输出"(保留字, 值)"的格式;如果识别出标识符,则输出"(标识符, 值)"的格式;如果标识符长度超过限制,则输出"(标识符长度超长, 值, 行号)"的格式。字符序列加一回到6步骤。
  9. 如果识别出无符号整数,则输出"(无符号整数, 值)"的格式;如果无符号整数超过范围,则输出"(无符号整数越界, 值, 行号)"的格式。字符序列加一回到6步骤。
  10. 如果识别出运算符,则输出"(运算符,token)"的格式。字符序列加一回到6步骤。
  11. 如果识别出界符,则输出(界符,token)的格式。字符序列加一回到6步骤。
  12. 除了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;
    }
}