一、概述
1.1 必要性
设计一门语言的时候,首先需要编写它的文法产生式,然后根据文法产生式得到词法、语法的解析规则。
编译器工作的头两个步骤:词法解析、语法解析的功能如下:
词法解析:解析输入字符串,将其转换为一个个有意义的符号(Token)。
语法分析:解析上一步得到的符号,根据一定的规则,转换成一颗抽象语法树。
而本文要做的,是这两步之前的一个动作:根据文法产生式,生成对应的编译器。
至于为啥不对照文法产生式,硬编对应的代码,有以下原因:
- 在设计过程中,我的文法产生式可能会随时改变
- 复杂度较高,而且容易出错
- 我想设计一个通用的工具
1.2 本文想要解析的文法产生式
下面是一个计算器的文法:
cal:
exec| cal exec
;
exec:
assign '\n'|'\n'
;
assign:
ID '=' assign
| newExp
;
newExp:
'(' newExp ')'
| prim
| newExp ('*'|'/') newExp
| newExp ('+'|'-') newExp
;
prim:
Integer|Decimal|ID
这个文法可以匹配下列语句:
a=2
b=3
a*2+5-6/b
(a+b)*9
我的想法的是用户每输入一行表达式,就可以计算出对应的值并输出.
1.3 产生式的处理步骤
本文将使用LL1算法做语法分析。
对产生式的处理过程如下:
- 解析文法文件,得到 rule -> produce 的规则映射关系
- 将括号中的内容提取为子规则(subRule)
- 消除左递归
- 提取左公因子
- 计算First集合
- 计算Follow集合
- 利用以上处理结果,生成对应文法的Lexer(词法解析)、Parser(语法解析)类
然后我需要另外开发一个类,依赖上一步生成的Lexer、Parser,解析该文法所表示的源码,生成对应的语法树
二、功能实现
2.1 定义几个实体类
Rule:表示语法规则
Produce: 具体的产生式
这里对于每一个终结符、非终结符,都会分配一个short类型的ID,这样可以方便计算、存储。
因为,本质上文法的解析更专注于各个符号之间的关系,符号的名字并不重要,一般只是在解析过程中想直观的输出时,才会考虑展现各个符号的名字。
public class Rule {
public short id;
public String name;
public List<Produce> produces = new ArrayList<>();
public Set<Short> first = new HashSet<>(), follow = new HashSet<>();
public boolean isDigui = false, isSub = false;
public Rule(short id, String name) {
this.id = id;
this.name = name;
this.produces.add(new Produce(id));
}
}
public class Produce {
public short id;
public List<Short> nodes = new ArrayList<>();
public Set<Short> first = new HashSet<>();
public Produce(short ruleId ) {
this.id = ruleId;
}
}
以上代码展示的:
- 一个Rule(语法规则)由多个Produce(产生式)组成
- 一个Produce(产生式)由多个节点(node)组成,这里只存储节点的ID(short类型)
- 每个rule都有自己的first、follow集合,用于判断是否匹配当前的词法符号(Token)
- 每个Produce也有自己的first集合,用于判断当前Token是否适用这个Produce
- First和Follow集合如何计算、具体怎么用,在接下来的内容会详细解释。
2.2 解析文法文件
第一步当然是做词法解析,将文法文件的内容解析为一个个的词法符号(Token)。
第二步是遍历这些词法符号,将其转换为语法规则(Rule)的集合。
所以先定义一个GmLexer类:
public class GmLexer extends Lexer {
public static final short StrStart = Lexer.StrTermStart;
public static final short RuleEnd= StrStart, SubStart= StrStart +1,
SubEnd= StrStart +2, Or= StrStart +3, RuleSplit = StrStart + 4;
public static final String[] Constant = {";","(",")","|",":"};
public GmLexer(Reader reader) {
super(reader);
}
@Override
public String[] constantList() {
return Constant;
}
}
再然后就是重头戏了,遍历上一步得到的词法符号,转换为Rule集合,这里定义一个GmParser类用于干这些事情:
public class GmParser {
static String[] defaultSymbol = "ID,STR1,Integer,Decimal,STR2,NewLine,NullTerm,EndTerm".split(",");
Map<Short, Rule> ruleMap = new HashMap<>();
GmLexer lexer;
List<String> termList = new ArrayList<>();
String startRule;
List<Token> allTokens = new ArrayList<>();
short termIndex = Lexer.StrTermStart, ruleIndex = Lexer.RuleStart;
Map<String, Short> symbolMap = new HashMap<>();
public GmParser(GmLexer lexer) {
this.lexer = lexer;
short id = 1;
for (String s : defaultSymbol ) {
symbolMap.put(s, id++);
}
symbolMap.put("T_\\n", Lexer.NewLine);
}
}
然后我需要加载所有符号:
void loadAllSymbol() {
Token token ;
while ((token = lexer.nextToken()) != null) {
allTokens.add(token);
switch (token.type) {
case GmLexer.RuleSplit :
Token lastToken = allTokens.get(allTokens.size() - 2);
Rule rule = newRule(lastToken.text);
symbolMap.put(rule.name, rule.id);
if (startRule == null) {
startRule = rule.name;
}
break;
case Lexer.STR1:
if (symbolMap.containsKey("T_"+token.text)) {
break;
}
symbolMap.put("T_"+token.text, termIndex++);
termList.add(token.text);
break;
}
}
}
这里对于每一个终结符、非终结符,都会分配一个short类型的ID,用symbolMap登记名字与ID之间的关系,第一次扫描到符号的时候,会给它生成一个新的ID,后面就之间从symbolMap取了。
另外为了防止字符串常量与其他符号冲突,所以在前面统一增加"T_"前缀。
termList用于存储所有的字符串常量,为后面的Lexer代码生成做准备。
2.3 提取子规则
这一步是完整的遍历整个文法文件,根据各个产生式的内容,创建对应的Rule、Produce。
从这一步开始,包括后面的消除左递归、提取左公因子,目的都是重组各个产生式:
- 使之达到LL1文法的标准
- 每个产生式尽量简洁好处理
最好每个产生式的格式都是 a → b c ∣ d e a\to b\ c|d\ e a→b c∣d e这样的格式,一方面用程序好表示,另一方面也方便计算一些集合。
当前的这个步骤涉及到了子规则的处理,比如说,对于下面的产生式:
a
→
b
c
(
d
(
e
∣
f
)
∣
(
g
∣
h
)
)
a \to b\ c\ ( d\ \color{red}( e|f )\color{black}\ |\ \color{green}(g|h)\color{black})
a→b c (d (e∣f) ∣ (g∣h))
我希望得到的Rule及Produce为:
a
→
b
c
a
1
a
1
→
d
a
2
∣
a
3
a
2
→
e
∣
f
a
3
→
g
∣
h
a \to b \quad c\quad a_1\\ a_1 \to d \quad a_2 | a_3\\ a_2 \to e|f \\ a_3 \to g|h
a→bca1a1→da2∣a3a2→e∣fa3→g∣h
所以解析的时候,我需要定义一个produceStack
栈结构。每次解析的时候,将当前Produce压入栈,解析完弹出。 解析中碰到小括号,则创建一个新的subRule,压入栈顶,同时作为当前Produce的一个符号。
void createRule() {
Stack<Produce> produceStack = new Stack<>();
for (Token token : allTokens) {
if (produceStack.empty() && token.type == Lexer.ID) {
Rule rule = ruleMap.get(symbolMap.get(token.text));
produceStack.add(rule.produces.get(0));
} else switch (token.type) {
case GmLexer.SubStart:
Rule rule = newRule("subRule@");
rule.isSub = true;
produceStack.peek().nodes.add(rule.id);
produceStack.add(rule.produces.get(0));
break;
case GmLexer.RuleEnd:
case GmLexer.SubEnd:
produceStack.pop();
break;
case GmLexer.Or:
Produce pop = produceStack.pop();
Produce produce = new Produce(pop.id);
ruleMap.get(pop.id).produces.add(produce);
produceStack.add(produce);
break;
case GmLexer.STR1:
produceStack.peek().nodes.add(symbolMap.get("T_"+token.text));
break;
case GmLexer.ID:
produceStack.peek().nodes.add(symbolMap.get(token.text));
break;
default:
break;
}
}
}