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

项目的完整代码在 C2j-Compiler

通过上一篇对几个构造自动机的基础数据结构的描述,现在就可以正式来构造有限状态自动机

我们先用一个小一点的语法推导式来描述这个过程

s -> e
e -> e + t
e -> t
t -> t * f
t -> f
f -> ( e )
f -> NUM

初始化

状态0是状态机的初始状态,它包含着语法表达式中的起始表达式,也就是编号为0的表达式:

0: s -> . e

这里的点也就是之前Production类中的dosPos

负责这个操作的方法在StateNodeManager类中,前面先判断当前目录下是不是已经构建好语法分析表了,如果有的话就不需要再次构建了。

productionManager.buildFirstSets();可以先略过,后面会讲到。

ProductionsStateNode就是用来描述状态节点的

public static int stateNumCount = 0;
/** Automaton state node number */
public int stateNum;
/** production of state node */
public ArrayList<Production> productions;

接着就是放入开始符号作为第一个状态节点,也就是这一步的初始化

public void buildTransitionStateMachine() {
    File table = new File("lrStateTable.sb");
    if (table.exists()) {
        return;
    }
    ProductionManager productionManager = ProductionManager.getInstance();
    productionManager.buildFirstSets();
    ProductionsStateNode state = getStateNode(productionManager.getProduction(Token.PROGRAM.ordinal()));

    state.buildTransition();

    debugPrintStateMap();
}

对起始推导式做闭包操作

注意之前的 . ,也就是Production里的dosPos,这一步就有用了,利用这个点来做闭包操作

对.右边的符号做闭包操作,也就是说如果 . 右边的符号是一个非终结符,那么肯定有某个表达式,->左边是该非终结符,把这些表达式添加进来

s -> . e
e -> . e + t
e -> . t

对新添加进来的推导式反复重复这个操作,直到所有推导式->右边是非终结符的那个所在推导式都引入,这也就是ProductionsStateNode里的makeClosure方法

主要逻辑就是先将这个节点中的所有产生式压入堆栈中,再反复的做闭包操作。closureSet是每个节点中保存闭包后的产生式

private void makeClosure() {
    Stack<Production> productionStack = new Stack<Production>();
    for (Production production : productions) {
        productionStack.push(production);
    }

    if (Token.isTerminal(production.getDotSymbol())) {
        ConsoleDebugColor.outlnPurple("Symbol after dot is not non-terminal, ignore and process next item");
        continue;
    }

    while (!productionStack.empty()) {
        Production production = productionStack.pop();
        int symbol = production.getDotSymbol();
        ArrayList<Production> closures = productionManager.getProduction(symbol);
        for (int i = 0; closures != null && i < closures.size(); i++) {
            if (!closureSet.contains(closures.get(i))) {
                closureSet.add(closures.get(i));
                productionStack.push(closures.get(i));
            }
        }
    }
}

对引入的产生式进行分区

把 . 右边拥有相同非终结符的表达式划入一个分区,比如

s -> . e
e -> . e + t

就作为同一个分区。最后把每个分区中的表达式中的 . 右移动一位,形成新的状态节点

s -> e .
e -> e . + t

分区操作就在ProductionsStateNode类中的partition方法中

主要逻辑也很简单,遍历当前的closureSet,如果分区不存在,就以产生式点的右边作为key,产生式列表作为value,并且如果当前产生式列表里不包含这个产生式,就把这个产生式加入当前的产生式列表

private void partition() {
    ConsoleDebugColor.outlnPurple("==== state begin make partition ====");

    for (Production production : closureSet) {
        int symbol = production.getDotSymbol();
        if (symbol == Token.UNKNOWN_TOKEN.ordinal()) {
            continue;
        }

        ArrayList<Production> productionList = partition.get(symbol);
        if (productionList == null) {
            productionList = new ArrayList<>();
            partition.put(production.getDotSymbol(), productionList);
        }

        if (!productionList.contains(production)) {
            productionList.add(production);
        }
    }

    debugPrintPartition();
    ConsoleDebugColor.outlnPurple("==== make partition end ====");
}

对所有分区节点构建跳转关系

根据每个节点 . 左边的符号来判断输入什么字符来跳入该节点

比如, . 左边的符号是 t, 所以当状态机处于状态0时,输入时 t 时, 跳转到状态1。

. 左边的符号是e, 所以当状态机处于状态 0 ,且输入时符号e时,跳转到状态2:
0 – e -> 2

这个操作的实现再ProductionsStateNode的makeTransition方法中

主要逻辑是遍历所有分区,每个分区都是一个新的节点,所以拿到这个分区的跳转关系,也就是partition的key,即之前产生式的点的右边。然后构造一个新的节点和两个节点之间的关系

private void makeTransition() {
    for (Map.Entry<Integer, ArrayList<Production>> entry : partition.entrySet()) {
        ProductionsStateNode nextState = makeNextStateNode(entry.getKey());

        transition.put(entry.getKey(), nextState);

        stateNodeManager.addTransition(this, nextState, entry.getKey());
    }

    debugPrintTransition();

    extendFollowingTransition();
}

makeNextStateNode的逻辑也很简单,就是拿到这个分区的产生式列表,然后返回一个新节点

private ProductionsStateNode makeNextStateNode(int left) {
    ArrayList<Production> productions = partition.get(left);
    ArrayList<Production> newProductions = new ArrayList<>();

    for (int i = 0; i < productions.size(); i++) {
        Production production = productions.get(i);
        newProductions.add(production.dotForward());
    }

    return stateNodeManager.getStateNode(newProductions);
}

stateNodeManager已经出现很多次了,它是类StateNodeManager,它的作用是管理节点,分配节点,统一节点。之后对节点的压缩和语法分析表的最终构建都在这里完成,这是后话了。

上面用到的两个方法:

transitionMap相当于一个跳转表:key是起始节点,value是一个map,这个map的key是跳转关系,也就是输入一个终结符或者非终结符,value则是目标节点

public void addTransition(ProductionsStateNode from, ProductionsStateNode to, int on) {
        HashMap<Integer, ProductionsStateNode> map = transitionMap.get(from);
        if (map == null) {
            map = new HashMap<>();
        }

        map.put(on, to);
        transitionMap.put(from, map);
}

getStateNode先从判断如果这个节点没有创建过,创建过的节点都会加入stateList中,就创建一个新节点。如果存在就会返回这个原节点

public ProductionsStateNode getStateNode(ArrayList<Production> productions) {
    ProductionsStateNode node = new ProductionsStateNode(productions);

    if (!stateList.contains(node)) {
        stateList.add(node);
        ProductionsStateNode.increaseStateNum();
        return node;
    }

    for (ProductionsStateNode sn : stateList) {
        if (sn.equals(node)) {
            node = sn;
        }
    }

    return node;
}

对所有新生成的节点重复构建

这时候的第一轮新节点才刚刚完成,到等到所有节点都完成节点的构建才算是真正的完成,在makeTransition中调用的extendFollowingTransition正是这个作用

private void extendFollowingTransition() {
    for (Map.Entry<Integer, ProductionsStateNode> entry : transition.entrySet()) {
        ProductionsStateNode state = entry.getValue();
        if (!state.isTransitionDone()) {
            state.buildTransition();
        }
    }
}

小结

创建有限状态自动机的四个步骤

  • makeClosure
  • partition
  • makeTransition
  • 最后重复这些步骤直到所有的节点都构建完毕

至此我们对

public void buildTransition() {
    if (transitionDone) {
        return;
    }
    transitionDone = true;

    makeClosure();
    partition();
    makeTransition();
}

的四个过程都已经完成,自动机的构建也算完成,应该进行语法分析表的创建了,但是这个自动机还有些问题,下一篇会来改善它。

另外我的github博客:https://dejavudwh.cn/

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

时间: 2024-08-28 11:30:46

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

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

项目的完整代码在 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 前言 上一篇已经正式的完成了有限状态自动机的构建和足够判断reduce的信息,接下来的任务就是根据这个有限状态自动机来完成语法分析表和根据这个表来实现语法分析 reduce信息 在完成语法分析表之前,还差最后一个任务,那就是描述reduce信息,来指导自动机是否该进行reduce操作 reduce信息在ProductionsStateNode各自的节点里完成,只要遍历节点里的产生式,如果符号"."位于表达式的末尾,那么该节点即可根据该表达式以

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

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

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

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

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

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

从零写一个编译器(十三):代码生成之遍历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. ===================