从零写一个编译器(六):语法分析之表驱动语法分析

项目的完整代码在 C2j-Compiler

前言

上一篇已经正式的完成了有限状态自动机的构建和足够判断reduce的信息,接下来的任务就是根据这个有限状态自动机来完成语法分析表和根据这个表来实现语法分析

reduce信息

在完成语法分析表之前,还差最后一个任务,那就是描述reduce信息,来指导自动机是否该进行reduce操作

reduce信息在ProductionsStateNode各自的节点里完成,只要遍历节点里的产生式,如果符号“.”位于表达式的末尾,那么该节点即可根据该表达式以及表达式对应的lookAhead set得到reduce信息

reduce信息用一个map来表示,key是可以进行reduce的符号,也就是lookahead sets中的符合,value则是进行reduce操作的产生式

public HashMap<Integer, Integer> makeReduce() {
      HashMap<Integer, Integer> map = new HashMap<>();
      reduce(map, this.productions);
      reduce(map, this.mergedProduction);

      return map;
  }

  private void reduce(HashMap<Integer, Integer> map, ArrayList<Production> productions) {
      for (int i = 0; i < productions.size(); i++) {
          if (productions.get(i).canBeReduce()) {
              ArrayList<Integer> lookAhead = productions.get(i).getLookAheadSet();
              for (int j = 0; j < lookAhead.size(); j++) {
                  map.put(lookAhead.get(j), (productions.get(i).getProductionNum()));
              }
          }
      }
  }

语法分析表的构建

语法分析表的构建主要在StateNodeManager类里,可以先忽略loadTable和storageTableToFile的逻辑,这一部分主要是为了储存这张表,能够多次使用

主要逻辑从while开始,遍历所有节点,先从跳转信息的Map里拿出跳转关系和跳转的目的节点,然后把这个跳转关系(这个本质上对应的是一开始Token枚举的标号)和目的节点的标号拷贝到另一个map里。接着拿到reduce信息,找到之前对应在lookahead set里的符号,把它们的value改写成- (进行reduce操作的产生式编号),之所以写成负数,就是为了区分shift操作。

所以HashMap<Integer, HashMap<Integer, Integer>>这个数据结构作为解析表表示:

  1. 第一个Integer表示当前节点的编号
  2. 第二个Integer表示输入字符
  3. 第三个Integer表示,如果大于0则是做shift操作,小于0则根据推导式做reduce操作
public HashMap<Integer, HashMap<Integer, Integer>> getLrStateTable() {
      File table = new File("lrStateTable.sb");
      if (table.exists()) {
          return loadTable();
      }

      Iterator it;
      if (isTransitionTableCompressed) {
          it = compressedStateList.iterator();
      } else {
          it = stateList.iterator();
      }

      while (it.hasNext()) {
          ProductionsStateNode state = (ProductionsStateNode) it.next();
          HashMap<Integer, ProductionsStateNode> map = transitionMap.get(state);
          HashMap<Integer, Integer> jump = new HashMap<>();

          if (map != null) {
              for (Map.Entry<Integer, ProductionsStateNode> item : map.entrySet()) {
                  jump.put(item.getKey(), item.getValue().stateNum);
              }
          }

          HashMap<Integer, Integer> reduceMap = state.makeReduce();
          if (reduceMap.size() > 0) {
              for (Map.Entry<Integer, Integer> item : reduceMap.entrySet()) {

                  jump.put(item.getKey(), -(item.getValue()));
              }
          }

          lrStateTable.put(state.stateNum, jump);
      }

      storageTableToFile(lrStateTable);

      return lrStateTable;
  }

表驱动的语法分析

语法分析的主要过程在LRStateTableParser类里,由parse方法启动.

和第二篇讲的一样需要一个输入堆栈,节点堆栈,其它的东西现在暂时不需要用到。在初始化的时候先把开始节点压入堆栈,当前输入字符设为EXT_DEF_LIST,然后拿到语法解析表

public LRStateTableParser(Lexer lexer) {
    this.lexer = lexer;
    statusStack.push(0);
    valueStack.push(null);
    lexer.advance();
    lexerInput = Token.EXT_DEF_LIST.ordinal();
    lrStateTable = StateNodeManager.getInstance().getLrStateTable();
}

语法解析的步骤:

  • 拿到当前节点和当前字符所对应的下一个操作,也就是action > 0是shift操作,action < 0是reduce操作
  • 如果进入action > 0,也就是shift操作
    1. 把当前状态节点和输入字符分别压入堆栈
    2. 这里要区分如果当前的字符是终结符,这时候就可以直接读入下一个字符
    3. 但是这里如果是非终结符,就应该直接用当前字符跳转到下一个状态。这里是一个需要注意的一个点,这里需要把当前的这个非终结符,放入到下一个节点的对应输入堆栈中,这样它进行reduce操作时弹出退栈的符号才是正确的
  • 如果action > 0,也就是reduce操作
    1. 拿到对应的产生式
    2. 把产生式右边对应的状态节点弹出堆栈
    3. 把完成reduce的这个符号放入输入堆栈
public void parse() {
      while (true) {
          Integer action = getAction(statusStack.peek(), lexerInput);

          if (action == null) {
              ConsoleDebugColor.outlnPurple("Shift for input: " + Token.values()[lexerInput].toString());
              System.err.println("The input is denied");
              return;
          }

          if (action > 0) {
              statusStack.push(action);
              text = lexer.text;

              // if (lexerInput == Token.RELOP.ordinal()) {
              //     relOperatorText = text;
              // }

              parseStack.push(lexerInput);

              if (Token.isTerminal(lexerInput)) {
                  ConsoleDebugColor.outlnPurple("Shift for input: " + Token.values()[lexerInput].toString() + "   text: " + text);

                  // Object obj = takeActionForShift(lexerInput);

                  lexer.advance();
                  lexerInput = lexer.lookAhead;
                  // valueStack.push(obj);
              } else {
                  lexerInput = lexer.lookAhead;
              }
          } else {
              if (action == 0) {
                  ConsoleDebugColor.outlnPurple("The input can be accepted");
                  return;
              }

              int reduceProduction = -action;
              Production product = ProductionManager.getInstance().getProductionByIndex(reduceProduction);
              ConsoleDebugColor.outlnPurple("reduce by product: ");
              product.debugPrint();

              // takeActionForReduce(reduceProduction);

              int rightSize = product.getRight().size();
              while (rightSize > 0) {
                  parseStack.pop();
                  // valueStack.pop();
                  statusStack.pop();
                  rightSize--;
              }

              lexerInput = product.getLeft();
              parseStack.push(lexerInput);
              // valueStack.push(attributeForParentNode);
          }
      }
  }

  private Integer getAction(Integer currentState, Integer currentInput) {
      HashMap<Integer, Integer> jump = lrStateTable.get(currentState);
      return jump.get(currentInput);
  }

歧义性语法

到现在已经完成了语法分析的所有内容,接下来就是语义分析了,但是在这之前还有一个需要说的是,我们当前构造的有限状态自动机属于LALR(1)语法,即使LALR(1)语法已经足够强大,但是依旧有LALR(1)语法处理不了的语法,如果给出的推导式不符合,那么这个有限状态自动机依旧不能正确解析,但是之前给出的语法都是符合LALR(1)语法的

小结

这一篇主要就是

  • 利用有限状态自动机和reduce信息完成语法解析表
  • 利用语法解析表实现表驱动的语法解析

原文地址:https://www.cnblogs.com/secoding/p/11371502.html

时间: 2024-10-09 23:39:17

从零写一个编译器(六):语法分析之表驱动语法分析的相关文章

从零写一个编译器(三):语法分析之几个基础数据结构

项目的完整代码在 C2j-Compiler 写在前面 这个系列算作为我自己在学习写一个编译器的过程的一些记录,算法之类的都没有记录原理性的东西,想知道原理的在龙书里都写得非常清楚,但是我自己一开始是不怎么看得下来,到现在都还没有完整的看完,它像是一本给已经有基础的人写的书. 在parse包里一共有8个文件,就是语法分析阶段写的所有东西啦 Symbols.java Production.java SyntaxProductionInit.java FirstSetBuilder.java Prod

从零写一个编译器(五):语法分析之自动机的缺陷和改进

项目的完整代码在 C2j-Compiler 前言 在上一篇,已经成功的构建了有限状态自动机,但是这个自动机还存在两个问题: 无法处理shift/reduce矛盾 状态节点太多,导致自动机过大,效率较低 这一节就要解决这两个问题 shift/reduce矛盾 看上一节那个例子的一个节点 e -> t . t -> t . * f 这时候通过状态节点0输入t跳转到这个节点,但是这时候状态机无法分清是根据推导式1做reduce还是根据推导式2做shift操作,这种情况就称之为shift / redu

从零写一个编译器(四):语法分析之构造有限状态自动机

项目的完整代码在 C2j-Compiler 通过上一篇对几个构造自动机的基础数据结构的描述,现在就可以正式来构造有限状态自动机 我们先用一个小一点的语法推导式来描述这个过程 s -> e e -> e + t e -> t t -> t * f t -> f f -> ( e ) f -> NUM 初始化 状态0是状态机的初始状态,它包含着语法表达式中的起始表达式,也就是编号为0的表达式: 0: s -> . e 这里的点也就是之前Production类中的

从零写一个编译器(二):语法分析之前置知识

前言 在之前完成了词法分析之后,得到了Token流,那么接下来就是实现语法分析器来输入Token流得到抽象语法树 (Abstract Syntax Tree,AST).但是在完成这个语法分析器不像词法分析器,直接手撸就好了,还是需要一些前置的知识. 这些前置知识在之前的博文都有提起过 之前的博文目录 项目的完整代码在 C2j-Compiler 什么是语法分析? 如果我们把词法分析看成是组合单词,输出单词流,那么语法分析就可以看作是检查这些单词是不是符合语法的过程.在词法分析的时候用正则或者手工比

从零写一个编译器(十):编译前传之直接解释执行

项目的完整代码在 C2j-Compiler 前言 这一篇不看也不会影响后面代码生成部分 现在经过词法分析语法分析语义分析,终于可以进入最核心的部分了.前面那部分可以称作编译器的前端,代码生成代码优化都是属于编译器后端,如今有关编译器的工作岗位主要都是对后端的研究.当然现在写的这个编译器因为水平有限,并没有优化部分. 在进行代码生成部分之前,我们先来根据AST来直接解释执行,其实就是对AST的遍历.现代解释器一般都是生成一个比较低级的指令然后跑在虚拟机上,但是简单起见我们就直接根据AST解释执行的

从零写一个编译器(七):语义分析之符号表的数据结构

项目的完整代码在 C2j-Compiler 前言 有关符号表的文件都在symboltable包里 前面我们通过完成一个LALR(1)有限状态自动机和一个reduce信息来构建了一个语法解析表,正式完成了C语言的语法解析.接下来就是进入语义分析部分,和在第二篇提到的一样,语义分析的主要任务就是生成符号表来记录变量和变量的类型,并且发现不符合语义的语句 描述变量 在C语言里对变量声明定义里,主要有两种描述 说明符(Specifier) 说明符也就是对应C语言的一些描述变量类型或者像static,ex

从零写一个编译器(十三):代码生成之遍历AST

项目的完整代码在 C2j-Compiler 前言 在上一篇完成对JVM指令的生成,下面就可以真正进入代码生成部分了.通常现代编译器都是先把生成IR,再经过代码优化等等,最后才编译成目标平台代码.但是时间水平有限,我们没有IR也没有代码优化,就直接利用AST生成Java字节码 入口 进行代码生成的入口在CodeGen,和之前解释器一样:先获取main函数的头节点,从这个节点开始,先进入函数定义,再进入代码块 函数定义节点 在进入函数定义节点的时候,就要生成一个函数定义对应的Java字节码,即一个静

从零写一个编译器(十一):代码生成之Java字节码基础

项目的完整代码在 C2j-Compiler 前言 第十一篇,终于要进入代码生成部分了,但是但是在此之前,因为我们要做的是C语言到字节码的编译,所以自然要了解一些字节码,但是由于C语言比较简单,所以只需要了解一些字节码基础 JVM的基本机制 JVM有一个执行环境叫做stack frame 这个环境有两个基本数据结构 执行堆栈:指令的执行,都会围绕这个堆栈来进行 局部变量数组,参数和局部变量就存储在这个数组. 还有一个PC指针,它指向下一条要执行的指令. 举一个例子 int f(int a, int

学了编译原理能否用 Java 写一个编译器或解释器?

16 个回答 默认排序? RednaxelaFX JavaScript.编译原理.编程 等 7 个话题的优秀回答者 282 人赞同了该回答 能.我一开始学编译原理的时候就是用Java写了好多小编译器和解释器.其实用什么语言来实现编译器并不是最重要的部分(虽然Java也不是实现编译器最方便的语言),最初用啥语言都可以. 我在大学的时候,我们的软件工程和计算机科学的编译原理课的作业好像都是可以用Java来写的.反正我印象中我给这两门课写的作业都是用的Java. ===================