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

项目的完整代码在 C2j-Compiler

写在前面

这个系列算作为我自己在学习写一个编译器的过程的一些记录,算法之类的都没有记录原理性的东西,想知道原理的在龙书里都写得非常清楚,但是我自己一开始是不怎么看得下来,到现在都还没有完整的看完,它像是一本给已经有基础的人写的书。

在parse包里一共有8个文件,就是语法分析阶段写的所有东西啦

  • Symbols.java
  • Production.java
  • SyntaxProductionInit.java
  • FirstSetBuilder.java
  • ProductionManager.java
  • ProductionsStateNode.java
  • StateNodeManager.java
  • LRStateTableParser.java

项目的完整代码在 C2j-Compiler

SyntaxProductionInit 语法初始化

在上一篇说了,竟然要验证句子正确与否,自然就需要语法,也就是给出相应的语法推导式

所有语法的初始化工作都在SyntaxProductionInit里完成

1
2
3
4
5
///EXT_DECL_LIST ->EXT_DECL_LIST COMMA EXT_DECL
right = getProductionRight(new int[]{Token.EXT_DECL_LIST.ordinal(), Token.COMMA.ordinal(), Token.EXT_DECL.ordinal()});
production = new Production(productionNum, Token.EXT_DECL_LIST.ordinal(), 0, right);
productionNum++;
addProduction(production, false);

比如下面这个就对应C语言的变量声明语句的推导式,PROGRAM是整个推导式的开始符号,EXT_DEF_LIST就是声明列表,这里的EXT_DEF_LIST -> EXT_DEF_LIST EXT_DEF需要注意一下是左递归情况,LR语法是可以处理的,关于这个可以看之前的博文。

比如EXT_DECL_LIST -> EXT_DECL -> VAR_DECL 多个变量名称声明可以推导是一个变量名称声明或者多个变量名称声明 + 逗号 + 变量名称声明

VAR_DECL 则可以是一个标识符或者一个多重指针

即一个从叶子节点不断的推导,读入终结符,最后推导到开始符号,且输入流也已经读完

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* PROGRAM -> EXT_DEF_LIST
*
* EXT_DEF_LIST -> EXT_DEF_LIST EXT_DEF
*
* EXT_DEF -> OPT_SPECIFIERS EXT_DECL_LIST SEMI
* | OPT_SPECIFIERS SEMI
*
*
* EXT_DECL_LIST -> EXT_DECL
* | EXT_DECL_LIST COMMA EXT_DECL
*
* EXT_DECL -> VAR_DECL
*
* OPT_SPECIFIERS -> CLASS TTYPE
* | TTYPE
* | SPECIFIERS
* | EMPTY?
*
* SPECIFIERS -> TYPE_OR_CLASS
* | SPECIFIERS TYPE_OR_CLASS
*
*
* TYPE_OR_CLASS -> TYPE_SPECIFIER
* | CLASS
*
* TYPE_SPECIFIER -> TYPE
*
* NEW_NAME -> NAME
*
* NAME_NT -> NAME
*
* VAR_DECL -> | NEW_NAME
*
* | START VAR_DECL
*
*/

在语法推导式初始化的过程一共要构建三个数据结构

1
2
3
private HashMap<Integer, ArrayList<Production>> productionMap = new HashMap<>();
private HashMap<Integer, Symbols> symbolMap = new HashMap<>();
private ArrayList<Symbols> symbolArray = new ArrayList<>();
  • ProductionMap的key就是推导式的左边,value即对应的一个或多个产生式
  • SymbolMap类似ProductionMap,key是推导式的左边,value是一个或者多个产生式的右边

Symbol类似Production,也是用来表示产生式的,但是稍有点不同,也还包括终结符,在后面也会有不同的作用,

1
2
3
4
5
//Symbols
public int value;
public ArrayList<int[]> productions;
public ArrayList<Integer> firstSet = new ArrayList<>();
public boolean isNullable;

如果一个非终结符,它可以推导出空集,那么这样的非终结符我们称之为nullable的非终结符

  • symbolArray存储着每一个Symbols对象,在后面也会有不一样的作用

Production 产生式类

在上一篇里说到验证语法的过程就是在一堆对应语法产生式中推导出答案,Production类就是来表示一个产生式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private int dotPos = 0;
private int left;
private ArrayList<Integer> right;
private ArrayList<Integer> lookAhead = new ArrayList<>();
private int productionNum = -1;

public Production(int productionNum, int left, int dot, ArrayList<Integer> right) {
this.left = left;
this.right = right;
this.productionNum = productionNum;
lookAhead.add(Token.SEMI.ordinal());

if (dot >= right.size()) {
dot = right.size();
}
this.dotPos = dot;
}
  1. left和right就是产生式的左边和右边,都是用之前Token的值来表示
  2. lookahead向前看集合,后面用到再提
  3. dos就像构造这个自动机的辅助工具,之后用到详细说
  4. productionNum是这个产生式对应编号,这个编号是在初始化语法的时候给定的

小结

这一篇主要介绍了几个数据结构,这几个数据结构都是之后构建有限状态自动机的基础。本来想把状态机的构建也写在这一篇,但是如果加上去之后,篇幅太长,加一部分会感觉不成模块,有点分散,所以自动机的构建就写在下一篇。