概述

课程:编译原理之美
源码路径:https://gitee.com/richard-gong/PlayWithCompiler/tree/master/lab/craft

项目目录分析

image.png

词法分析

  • Token.java : 此文件定义了 Token 接口。
  • TokenReader.java : 此文件定义了 TokenReader 接口
  • TokenType.java : 此文件定义了可识别的 Token 类别
  • SimpleLexer.java : 此文件定义了一个简单的手写的词法分析器,其中包括 一个 TokenReader 实现、状态机状态定义和一个根据字符串解析 Token 的函数。

两个接口的定义可以有效隔离类与类之间的耦合,同时也只将必要的方法进行暴露。

语法解析

语法解析目前处理的是上下文无关的文法

  • ASTNode.java : 此文件定义了 ASTNode 接口。 [ AST : 抽象语法树 ]
  • ASTNodeType.java : 此文件定义了 ASTNode 的类别。
  • SimpleCalculator.java : 此文件定义了一个可以做简单四则运算并给出结果.
  • SimpleParser.java : 此文件定义了一个简单的语法解析器, 支持表达式, 变量声明和初始化语句, 赋值语句.

    语义识别

  • SimpleScript.java : 此文件定义了一个简单的脚本解释器, 其支持的语法在 SimpleParser.java 中.

    词法分析器

    Token.java

    Token 需要包括 类型原始值 两个属性。 原始值 存储为 String 类型。 ```java /**

    • 一个简单的Token。
    • 只有类型和文本值两个属性。 */ public interface Token{

      /**

      • Token的类型
      • @return */ public TokenType getType();

      /**

      • Token的文本值
      • @return */ public String getText();

}

  1. <a name="Lvuvv"></a>
  2. ### TokenReader.java
  3. 利用 **TokenReader **对 **Token **的去取逻辑进行封装,方便后面增加必要的控制函数和逻辑。此类中的返回值都利用了 **Token **接口,实现对 Token 相关类的隔离。
  4. ```java
  5. /**
  6. * 一个Token流。由Lexer生成。Parser可以从中获取Token。
  7. */
  8. public interface TokenReader{
  9. /**
  10. * 返回Token流中下一个Token,并从流中取出。 如果流已经为空,返回null;
  11. */
  12. public Token read();
  13. /**
  14. * 返回Token流中下一个Token,但不从流中取出。 如果流已经为空,返回null;
  15. */
  16. public Token peek();
  17. /**
  18. * Token流回退一步。恢复原来的Token。
  19. */
  20. public void unread();
  21. /**
  22. * 获取Token流当前的读取位置。
  23. * @return
  24. */
  25. public int getPosition();
  26. /**
  27. * 设置Token流当前的读取位置
  28. * @param position
  29. */
  30. public void setPosition(int position);
  31. }

TokenType.java

可以解析的 Token 类型定义为“四则运算”,“大小判断”,“表达式符号”,“赋值”,“条件”,“变量定义”,“字面量”。

  1. /**
  2. * Token的类型
  3. */
  4. public enum TokenType{
  5. Plus, // +
  6. Minus, // -
  7. Star, // *
  8. Slash, // /
  9. GE, // >=
  10. GT, // >
  11. EQ, // ==
  12. LE, // <=
  13. LT, // <
  14. SemiColon, // ;
  15. LeftParen, // (
  16. RightParen,// )
  17. Assignment,// =
  18. If,
  19. Else,
  20. Int,
  21. Identifier, //标识符
  22. IntLiteral, //整型字面量
  23. StringLiteral //字符串字面量
  24. }

SimpleLexer.java

此文件是词法分析器的核心组件,由于源文件内容比较多下面先总分析,再分别分析各个主要功能的实现逻辑。

  1. /**
  2. * 一个简单的手写的词法分析器。
  3. * 能够为后面的简单计算器、简单脚本语言产生Token。
  4. */
  5. public class SimpleLexer {
  6. //测试解析器的能力
  7. public static void main(String args[]) {
  8. ...
  9. }
  10. //下面几个变量是在解析过程中用到的临时变量,如果要优化的话,可以塞到方法里隐藏起来
  11. private StringBuffer tokenText = null; //临时保存token的文本
  12. private List<Token> tokens = null; //保存解析出来的Token
  13. private SimpleToken token = null; //当前正在解析的Token
  14. //是否是字母
  15. private boolean isAlpha(int ch) {
  16. ...
  17. }
  18. //是否是数字
  19. private boolean isDigit(int ch) {
  20. ...
  21. }
  22. //是否是空白字符
  23. private boolean isBlank(int ch) {
  24. ...
  25. }
  26. /**
  27. * 有限状态机进入初始状态。
  28. * 这个初始状态其实并不做停留,它马上进入其他状态。
  29. * 开始解析的时候,进入初始状态;某个Token解析完毕,也进入初始状态,在这里把Token记下来,然后建立一个新的Token。
  30. */
  31. private DfaState initToken(char ch) {
  32. ...
  33. }
  34. /**
  35. * 解析字符串,形成Token。
  36. * 这是一个有限状态自动机,在不同的状态中迁移。
  37. */
  38. public SimpleTokenReader tokenize(String code) {
  39. ...
  40. }
  41. /**
  42. * Token的一个简单实现。只有类型和文本值两个属性。
  43. */
  44. private final class SimpleToken implements Token {
  45. ...
  46. }
  47. /**
  48. * 打印所有的Token
  49. */
  50. public static void dump(SimpleTokenReader tokenReader){
  51. ...
  52. }
  53. /**
  54. * 有限状态机的各种状态。
  55. */
  56. private enum DfaState {
  57. ...
  58. }
  59. /**
  60. * 一个简单的Token流。是把一个Token列表进行了封装。
  61. */
  62. private class SimpleTokenReader implements TokenReader {
  63. ...
  64. }
  65. }

SimpleToken

这是一个对 Token 接口的实现。由于此类定义在 SimpleLexer 内部,因此在 SimpleLexer 中可以直接调用 SimpleToken 中的私有变量进行赋值。根据设计分析,在词法解析器之外是不需要也不应该修改 **type****text** 参数。

  1. /**
  2. * Token的一个简单实现。只有类型和文本值两个属性。
  3. */
  4. private final class SimpleToken implements Token {
  5. //Token类型
  6. private TokenType type = null;
  7. //文本值
  8. private String text = null;
  9. @Override
  10. public TokenType getType() {
  11. return type;
  12. }
  13. @Override
  14. public String getText() {
  15. return text;
  16. }
  17. }

SimpleTokenReader

这是一个对 TokenReader 接口的实现。利用 List 存储 Token,并利用 List 特性覆盖接口函数。

  1. private class SimpleTokenReader implements TokenReader {
  2. List<Token> tokens = null;
  3. int pos = 0; // 记录当前的读取位置
  4. public SimpleTokenReader(List<Token> tokens) {
  5. this.tokens = tokens;
  6. }
  7. @Override
  8. public Token read() {
  9. // 这里并没有真的将 Token 移出数组,只是让标志为后移
  10. if (pos < tokens.size()) {
  11. return tokens.get(pos++);
  12. }
  13. return null;
  14. }
  15. @Override
  16. public Token peek() {
  17. if (pos < tokens.size()) {
  18. return tokens.get(pos);
  19. }
  20. return null;
  21. }
  22. @Override
  23. public void unread() {
  24. if (pos > 0) {
  25. pos--;
  26. }
  27. }
  28. @Override
  29. public int getPosition() {
  30. return pos;
  31. }
  32. @Override
  33. // 设置读取位置
  34. public void setPosition(int position) {
  35. if (position >=0 && position < tokens.size()){
  36. pos = position;
  37. }
  38. }
  39. }

DfaState

根据状态图定义的状态值

  1. /**
  2. * 有限状态机的各种状态。
  3. */
  4. private enum DfaState {
  5. Initial,
  6. If, Id_if1, Id_if2, Else, Id_else1, Id_else2, Id_else3, Id_else4, Int, Id_int1, Id_int2, Id_int3, Id, GT, GE,
  7. Assignment,
  8. Plus, Minus, Star, Slash,
  9. SemiColon,
  10. LeftParen,
  11. RightParen,
  12. IntLiteral
  13. }

tokenize

此为词法解析器对外暴露的唯一方法,够建一个解析器后可以用此方法去解析所有的数据。

  • 其接受一个字符串进行 Token 解析。
  • 此函数实现了状态机的切换和 Token 数组的构建。
  • 其中临时变量的初始化还可以提取函数。
  • 它也存在最后一个 Token 的存储问题。
  • 每个地方都使用 initToken 进行处理,我个人认为并不是很妥当。

    1. public SimpleTokenReader tokenize(String code) {
    2. tokens = new ArrayList<Token>(); // Token 临时数组复位
    3. CharArrayReader reader = new CharArrayReader(code.toCharArray());
    4. tokenText = new StringBuffer(); // 临时值复位
    5. token = new SimpleToken(); // 临时 Token 复位
    6. int ich = 0;
    7. char ch = 0;
    8. DfaState state = DfaState.Initial; // 状态装换为初始态
    9. try {
    10. while ((ich = reader.read()) != -1) {
    11. ch = (char) ich;
    12. switch (state) {
    13. case Initial:
    14. state = initToken(ch); //重新确定后续状态
    15. break;
    16. case Id:
    17. if (isAlpha(ch) || isDigit(ch)) {
    18. tokenText.append(ch); //保持标识符状态
    19. } else {
    20. state = initToken(ch); //退出标识符状态,并保存Token
    21. }
    22. break;
    23. case GT:
    24. if (ch == '=') {
    25. token.type = TokenType.GE; //转换成GE
    26. state = DfaState.GE;
    27. tokenText.append(ch);
    28. } else {
    29. state = initToken(ch); //退出GT状态,并保存Token
    30. }
    31. break;
    32. case GE:
    33. case Assignment:
    34. case Plus:
    35. case Minus:
    36. case Star:
    37. case Slash:
    38. case SemiColon:
    39. case LeftParen:
    40. case RightParen:
    41. state = initToken(ch); //退出当前状态,并保存Token
    42. break;
    43. case IntLiteral:
    44. if (isDigit(ch)) {
    45. tokenText.append(ch); //继续保持在数字字面量状态
    46. } else {
    47. state = initToken(ch); //退出当前状态,并保存Token
    48. }
    49. break;
    50. case Id_int1:
    51. if (ch == 'n') {
    52. state = DfaState.Id_int2;
    53. tokenText.append(ch);
    54. }
    55. else if (isDigit(ch) || isAlpha(ch)){
    56. state = DfaState.Id; //切换回Id状态
    57. tokenText.append(ch);
    58. }
    59. else {
    60. state = initToken(ch);
    61. }
    62. break;
    63. case Id_int2:
    64. if (ch == 't') {
    65. state = DfaState.Id_int3;
    66. tokenText.append(ch);
    67. }
    68. else if (isDigit(ch) || isAlpha(ch)){
    69. state = DfaState.Id; //切换回id状态
    70. tokenText.append(ch);
    71. }
    72. else {
    73. state = initToken(ch);
    74. }
    75. break;
    76. case Id_int3:
    77. if (isBlank(ch)) {
    78. token.type = TokenType.Int;
    79. state = initToken(ch);
    80. }
    81. else{
    82. state = DfaState.Id; //切换回Id状态
    83. tokenText.append(ch);
    84. }
    85. break;
    86. default:
    87. }
    88. }
    89. // 把最后一个token送进去
    90. if (tokenText.length() > 0) {
    91. initToken(ch);
    92. }
    93. } catch (IOException e) {
    94. e.printStackTrace();
    95. }
    96. return new SimpleTokenReader(tokens);
    97. }

    initToken

    此函数处理状态的初始化以及 Token 数据的保存。我个人认为此函数中功能并不单一,应该将保存的功能单独列出来。

    1. /**
    2. * 有限状态机进入初始状态。
    3. * 这个初始状态其实并不做停留,它马上进入其他状态。
    4. * 开始解析的时候,进入初始状态;某个Token解析完毕,也进入初始状态,在这里把Token记下来,然后建立一个新的Token。
    5. * @param ch
    6. * @return
    7. */
    8. private DfaState initToken(char ch) {
    9. if (tokenText.length() > 0) {
    10. token.text = tokenText.toString();
    11. tokens.add(token);
    12. tokenText = new StringBuffer();
    13. token = new SimpleToken();
    14. }
    15. DfaState newState = DfaState.Initial;
    16. if (isAlpha(ch)) { //第一个字符是字母
    17. if (ch == 'i') {
    18. newState = DfaState.Id_int1;
    19. } else {
    20. newState = DfaState.Id; //进入Id状态
    21. }
    22. token.type = TokenType.Identifier;
    23. tokenText.append(ch);
    24. } else if (isDigit(ch)) { //第一个字符是数字
    25. newState = DfaState.IntLiteral;
    26. token.type = TokenType.IntLiteral;
    27. tokenText.append(ch);
    28. } else if (ch == '>') { //第一个字符是>
    29. newState = DfaState.GT;
    30. token.type = TokenType.GT;
    31. tokenText.append(ch);
    32. } else if (ch == '+') {
    33. newState = DfaState.Plus;
    34. token.type = TokenType.Plus;
    35. tokenText.append(ch);
    36. } else if (ch == '-') {
    37. newState = DfaState.Minus;
    38. token.type = TokenType.Minus;
    39. tokenText.append(ch);
    40. } else if (ch == '*') {
    41. newState = DfaState.Star;
    42. token.type = TokenType.Star;
    43. tokenText.append(ch);
    44. } else if (ch == '/') {
    45. newState = DfaState.Slash;
    46. token.type = TokenType.Slash;
    47. tokenText.append(ch);
    48. } else if (ch == ';') {
    49. newState = DfaState.SemiColon;
    50. token.type = TokenType.SemiColon;
    51. tokenText.append(ch);
    52. } else if (ch == '(') {
    53. newState = DfaState.LeftParen;
    54. token.type = TokenType.LeftParen;
    55. tokenText.append(ch);
    56. } else if (ch == ')') {
    57. newState = DfaState.RightParen;
    58. token.type = TokenType.RightParen;
    59. tokenText.append(ch);
    60. } else if (ch == '=') {
    61. newState = DfaState.Assignment;
    62. token.type = TokenType.Assignment;
    63. tokenText.append(ch);
    64. } else {
    65. newState = DfaState.Initial; // skip all unknown patterns
    66. }
    67. return newState;
    68. }

    辅助函数

    ```java //是否是字母 private boolean isAlpha(int ch) { return ch >= ‘a’ && ch <= ‘z’ || ch >= ‘A’ && ch <= ‘Z’; }

//是否是数字 private boolean isDigit(int ch) { return ch >= ‘0’ && ch <= ‘9’; }

//是否是空白字符 private boolean isBlank(int ch) { return ch == ‘ ‘ || ch == ‘\t’ || ch == ‘\n’; }

/**

  1. * 打印所有的Token
  2. * @param tokenReader
  3. */

public static void dump(SimpleTokenReader tokenReader){ System.out.println(“text\ttype”); Token token = null; while ((token= tokenReader.read())!=null){ System.out.println(token.getText()+”\t\t”+token.getType()); } }

<a name="DtP0A"></a>
## 语法解析器
<a name="ASTNode.java"></a>
### ASTNode.java
**ASTNode **接口提供子节点,父节点以及自身类型和文本值的方法。
```java
/**
 * AST的节点。
 * 属性包括AST的类型、文本值、下级子节点和父节点
 */
public interface ASTNode{
    //父节点
    public ASTNode getParent();

    //子节点
    public List<ASTNode> getChildren();

    //AST类型
    public ASTNodeType getType();

    //文本值
    public String getText();
}

ASTNodeType.java

此处理的类型定义将一些标识符进行了简化

/**
 * AST节点的类型。
 */
public enum ASTNodeType{
    Programm,           //程序入口,根节点

    IntDeclaration,     //整型变量声明
    ExpressionStmt,     //表达式语句,即表达式后面跟个分号
    AssignmentStmt,     //赋值语句

    Primary,            //基础表达式
    Multiplicative,     //乘法表达式
    Additive,           //加法表达式

    Identifier,         //标识符
    IntLiteral          //整型字面量
}

SimpleCalculator.java

语法规则:

 * additive -> multiplicative | multiplicative + additive
 * multiplicative -> primary | primary * multiplicative
 * primary -> IntLiteral | ( additive )

这里实现了一个根据语言分析进行四则运算的语法,语法本身存在结合性问题即会先算后面的结果再算前面的内容

/**
 * 实现一个计算器,但计算的结合性是有问题的。因为它使用了下面的语法规则:
 *
 * additive -> multiplicative | multiplicative + additive
 * multiplicative -> primary | primary * multiplicative    //感谢@Void_seT,原来写成+号了,写错了。
 *
 * 递归项在右边,会自然的对应右结合。我们真正需要的是左结合。
 */
public class SimpleCalculator {

    public static void main(String[] args) {
        ...
    }

    /**
     * 执行脚本,并打印输出AST和求值过程。
     * @param script
     */
    public void evaluate(String script){
        ...
    }

    /**
     * 解析脚本,并返回根节点
     * @param code
     * @return
     * @throws Exception
     */
    public ASTNode parse(String code) throws Exception {
        ...
    }

    /**
     * 对某个AST节点求值,并打印求值过程。
     * @param node
     * @param indent  打印输出时的缩进量,用tab控制
     * @return
     */
    private int evaluate(ASTNode node, String indent) {
       ...
    }

    /**
     * 语法解析:根节点
     * @return
     * @throws Exception
     */
    private SimpleASTNode prog(TokenReader tokens) throws Exception {
        ...
    }

    /**
     * 整型变量声明语句,如:
     * int a;
     * int b = 2*3;
     *
     * @return
     * @throws Exception
     */
    private SimpleASTNode intDeclare(TokenReader tokens) throws Exception {
        ...
    }

    /**
     * 语法解析:加法表达式
     * @return
     * @throws Exception
     */
    private SimpleASTNode additive(TokenReader tokens) throws Exception {
        ...
    }

    /**
     * 语法解析:乘法表达式
     * @return
     * @throws Exception
     */
    private SimpleASTNode multiplicative(TokenReader tokens) throws Exception {
        ...
    }

    /**
     * 语法解析:基础表达式
     * @return
     * @throws Exception
     */
    private SimpleASTNode primary(TokenReader tokens) throws Exception {
        ...
    }

    /**
     * 一个简单的AST节点的实现。
     * 属性包括:类型、文本值、父节点、子节点。
     */
    private class SimpleASTNode implements ASTNode {
          ...
    }

    /**
     * 打印输出AST的树状结构
     * @param node
     * @param indent 缩进字符,由tab组成,每一级多一个tab
     */
    private void dumpAST(ASTNode node, String indent) {
        ...
    }
}

SimpleASTNode

ASTNode 接口的实现,利用 List 存储必要的数据。

private class SimpleASTNode implements ASTNode {
        SimpleASTNode parent = null;
        List<ASTNode> children = new ArrayList<ASTNode>();
        List<ASTNode> readonlyChildren = Collections.unmodifiableList(children);
        ASTNodeType nodeType = null;
        String text = null;


        public SimpleASTNode(ASTNodeType nodeType, String text) {
            this.nodeType = nodeType;
            this.text = text;
        }

        @Override
        public ASTNode getParent() {
            return parent;
        }

        @Override
        public List<ASTNode> getChildren() {
            return readonlyChildren;
        }

        @Override
        public ASTNodeType getType() {
            return nodeType;
        }

        @Override
        public String getText() {
            return text;
        }

        public void addChild(SimpleASTNode child) {
            children.add(child);
            child.parent = this;
        }

    }

ASTNode 接口实现,用简单的数据结构存储

/**
 * 一个简单的AST节点的实现。
 * 属性包括:类型、文本值、父节点、子节点。
 */
private class SimpleASTNode implements ASTNode {
    SimpleASTNode parent = null;
    List<ASTNode> children = new ArrayList<ASTNode>();
    List<ASTNode> readonlyChildren = Collections.unmodifiableList(children);
    ASTNodeType nodeType = null;
    String text = null;

    public SimpleASTNode(ASTNodeType nodeType, String text) {
        this.nodeType = nodeType;
        this.text = text;
    }

    @Override
    public ASTNode getParent() {
        return parent;
    }

    @Override
    public List<ASTNode> getChildren() {
        return readonlyChildren;
    }

    @Override
    public ASTNodeType getType() {
        return nodeType;
    }

    @Override
    public String getText() {
        return text;
    }

    public void addChild(SimpleASTNode child) {
        children.add(child);
        child.parent = this;
    }

}

parse

对外公开的第一个函数,用于将输入的字符串语句解析为语法树节点。也是类的主要功能函数。

  • 调用 SimpleLexer 解析出 Token 数组
  • 利用 Token 数组生成语法树【调用 prog

    /**
    * 解析脚本,并返回根节点
    */
    public ASTNode parse(String code) throws Exception {
      SimpleLexer lexer = new SimpleLexer();
      TokenReader tokens = lexer.tokenize(code);
    
      ASTNode rootNode = prog(tokens);
    
      return rootNode;
    }
    

    prog

    此函数创建语法树根节点,并根据自定义的语法规则进行

    /**
    * 语法解析:根节点
    * @return
    * @throws Exception
    */
    private SimpleASTNode prog(TokenReader tokens) throws Exception {
      SimpleASTNode node = new SimpleASTNode(ASTNodeType.Programm, "Calculator");
    
      SimpleASTNode child = additive(tokens);
    
      if (child != null) {
          node.addChild(child);
      }
      return node;
    }
    

    语法解析

    下面三个函数是解析器的核心算法部分,用于对定义的语法规则进行解析。

  • additive : 加法表达式解析 `` ```java /**

    • 语法解析:加法表达式
    • @return
    • @throws Exception */ private SimpleASTNode additive(TokenReader tokens) throws Exception { SimpleASTNode child1 = multiplicative(tokens); SimpleASTNode node = child1;

      Token token = tokens.peek(); if (child1 != null && token != null) {

      if (token.getType() == TokenType.Plus || token.getType() == TokenType.Minus) {
          token = tokens.read();
          SimpleASTNode child2 = additive(tokens);
          if (child2 != null) {
              node = new SimpleASTNode(ASTNodeType.Additive, token.getText());
              node.addChild(child1);
              node.addChild(child2);
          } else {
              throw new Exception("invalid additive expression, expecting the right part.");
          }
      }
      

      } return node; }

/**

  • 语法解析:乘法表达式
  • @return
  • @throws Exception */ private SimpleASTNode multiplicative(TokenReader tokens) throws Exception { SimpleASTNode child1 = primary(tokens); SimpleASTNode node = child1;

    Token token = tokens.peek(); if (child1 != null && token != null) {

     if (token.getType() == TokenType.Star || token.getType() == TokenType.Slash) {
         token = tokens.read();
         SimpleASTNode child2 = multiplicative(tokens);
         if (child2 != null) {
             node = new SimpleASTNode(ASTNodeType.Multiplicative, token.getText());
             node.addChild(child1);
             node.addChild(child2);
         } else {
             throw new Exception("invalid multiplicative expression, expecting the right part.");
         }
     }
    

    } return node; }

/**

  • 语法解析:基础表达式
  • @return
  • @throws Exception */ private SimpleASTNode primary(TokenReader tokens) throws Exception { SimpleASTNode node = null; Token token = tokens.peek(); if (token != null) {
     if (token.getType() == TokenType.IntLiteral) {
         token = tokens.read();
         node = new SimpleASTNode(ASTNodeType.IntLiteral, token.getText());
     } else if (token.getType() == TokenType.Identifier) {
         token = tokens.read();
         node = new SimpleASTNode(ASTNodeType.Identifier, token.getText());
     } else if (token.getType() == TokenType.LeftParen) {
         tokens.read();
         node = additive(tokens);
         if (node != null) {
             token = tokens.peek();
             if (token != null && token.getType() == TokenType.RightParen) {
                 tokens.read();
             } else {
                 throw new Exception("expecting right parenthesis");
             }
         } else {
             throw new Exception("expecting an additive expression inside parenthesis");
         }
     }
    
    } return node; //这个方法也做了AST的简化,就是不用构造一个primary节点,直接返回子节点。因为它只有一个子节点。 } ```

    REPL界面