@parser-generator/core
Version:
A Parser Generator that supports LL,SLR,LR1,LALR
469 lines (343 loc) • 12.1 kB
Markdown
[TOC]
# LR项与展望集的构造
名词约定:
- 展望集,LR中每个项都有自己的展望符集,一个项的展望符集决定何时可以归约(后称lookaheadSet,有时简称lookSet)
- 展开符号,例如项S->·E是可展开的,展开符号为E
## 1. 一个项何时应该展开
对于何时应该展开 需要考虑如下几种情况
- 1.1 没有后继符号「点」已经到了产生式的最末尾,形如`A-> X·`,不展开
- 1.2 有后继符号,但展开符是终结符,形如 `A-> ·a`,不展开
- 1.3 有后继符号,且为非终结符
对于情况 1.3,又可分为如下两种情况:
1. 由别的项展开自己
```
S->A
A->B | B𝜶
B->b
I0
S->·A
A->·B
A->·B𝜶
B->·b $
B->·b 𝜶
```
如上状态集中,B被A的两个项分别引用,因此展开将两次:
- 由`A->·B` 可展开得到 `B->·b`,展望集为`$`
- 由`A->·B𝜶` 可展开得到 `B->·b`,展望集为`𝜶`
由于每一次展开,展望集都不同,此种情况允许同一符号被多次展开✅(且这两个项应该被合并)
2. 自己展开自己
示例1,在项 A->·AAb 中,由于为左递归产生式,且「点」位于「左递归符号」前,如果展开将重复产生相同的项,因此不能再展开A.❌
```
S->A
A->AAb | a
I0
S->·A
A->·AAb
A->·a
```
示例2,在项A->A·Ab中,虽然左递归,但「点」在左递归符号之后,因此可以展开A. I1继续加入如下项:✅
```
I1
S->A·
A->A·Ab
```
示例3,此时,在项A->·AAb中,「点」位于左递归符号之前,因此不能再展开A❌
```
A->·AAb
A->·a
```
**结论**
0. 存在展开符号,且展开符号为非终结符
1. 一个符号被不同的多个个项引用,可以重复展开多次
2. 左递归 且 展开符为最左符号 不得展开 , 形如 A->·AA𝜶
3. 左递归 但 展开符不是最左符号,可以展开,形如 A->A·A𝜶
## 2.如何展开(LR展望符集计算)
对展望符的计算可分为2种情况
例:
```
S->E
E->E+F | E*F | F
F->id | (E)
```
### 2.1. 针对闭包项
初始状态I0中的核心项S->·E产生如下4个闭包项:
```
I0
S->·E $
E->·E+F ?
E->·E*F ?
E->·F ?
F->·id ?
```
#### 2.1.1 展开符是左递归的
核心项`S->·E `的展开符为E,按照期望,E应该展开如下3个项:
```
E->·E+F ?
E->·E*F ?
E->·F ?
```
由于E是左递归的,在展开E时,需要对其子产生式产生的项做特殊处理.
`E->·E+F`、`E->·E*F`、`E->·F` 均为左递归产生式,它们可能处于末端,也可能被嵌入另一个左递归产生式
例1 对于项E->·E+F而言
- 如果它处于末端,那么 lookSet(E·E+F)就应该有`$` (继承自 lookSet(S->·E))
- 如果它被嵌入E->·E*F,那么 lookSet(E->·E+F)就应该有`*`
- 如果它被嵌入E->·E+F,那么 lookSet(E->·E+F)就应该有`+`
- 要注意,它不可能被嵌入E->·F,所以 lookSet(E->·E+F)不包含`id`
例2 对于E->·F而言,虽然其本身没有左递归,但是由于它的兄弟项中存在左递归项,因此它仍然可能被嵌入其左递归的兄弟项
- 如果E->·F 直接作为S->·E的右侧,那么LookSet(E->·F)应包含`$` (继承自 lookSet(S->·E))
- 如果E->·F被嵌入E->·E+F,那么LookSet(E->·F)应包含`+`
- 如果E->·F被嵌入E->·E*F,那么LookSet(E->·F)应包含`*`
更一般的讲,一个左递归项的LookSet应该包含它的「左递归兄弟项及其本身」的「最左非左递归符号」,以及继承的LookSet
在本例中,E->·E+F的「左递归兄弟项」是 E->·E*F,这个兄弟项的「最左非左递归符号」是`*`, 且继承了来自于S->·E的lookSet`$`.
```
E->·E+F $,+,*
E->·E*F $,+,*
E->·F $,+,*
```
### 2.1.2 展开符不是左递归的
可分为如下两种情况:
- 展开符位于末尾,项形如A->𝜶·B,由B展开的项应继承A的展望集.
`E->·F`,的展开符F位于产生式末尾,符合第一种情况,因此F展开的项应该继承E:
```
F->·id $,+,*
F->·(E) $,+,*
```
- 展开符不位于末尾,项形如A->·B𝜶𝜷
- 若𝝴∉First(𝜶) 由展开符B得到的项的lookSet为First(𝜶)
- 否则,应当继续加入First(B),重复以上过程,直到末尾时,如果仍包含𝝴,则加入lookSet(A)
`First->(·E)`,的展开符后面仍有符号,所以E展开的项的lookSet将包含`)`
```
E->·E+F ),+,*
E->·E*F ),+,*
E->·F ),+,*
```
> 由于E是左递归的,因此lookSet会包含`+,*`
### 2.2 针对前进项
如果只是单纯的项的「点」前进,那么可以直接继承.
例如,项E->·E+F 前进后得到项E->E·+F, 后者的lookSet继承前者.
## 3. 展开后的处理
3.1 如果项集中存在产生式相同,且「点」的位置相同的项, 应将它们合并.
例
```
S->A
A->B | B𝜶
B->b
```
以上文法的初始状态:
```
S->·A
```
展开A,得到如下两个闭包项:
```
A->·B $
A->·B𝜶 $
```
由于B会被展开两次,因此分别得到如下两个项:
```
B->·b $
B->·b 𝜶
```
由于两者是同心项的(注意,此同心是指项,并非LALR中针对项集的同心),一个状态(项集)中存在两个同心项是无意义的,因此应当将其合并:
```
B->·b $,𝜶
```
---
# 状态机的构造
构造状态机的过程就是构造有向图的过程.
一个项集对应一个状态,每个状态维护了与后继状态的边 ,对每个状态输入某一个符号可到达下一个状态.
```
S->E
E->E+T | T
T->T*F | F
F->(E) | id
```
在初始状态,内核项为`S->·E`, 对其求闭包,可得到第一个状态I0:
```
I0
S->·E
E->·E+T
E->·T
T->·T*F
T->·F
F->·(E)
F->·id
```
可知,I0的后继符号集合为: {E,T,F,(,id}, 遍历该集合,可得到后继状态,以及I0与这些后继状态的边:
```
𝛂=E
E->E·+T
𝛂=T
E->T·
T->T·*F
𝛂=F
T->F·
𝛂=(
F->(·E)
𝛂=id
F->id·
```
对新产生的状态求闭包,可得到:
```
I1
E->E·+T
I2
E->T·
T->T·*F
I3
T->F·
I4
F->(·E)
E->·E+T
E->·T
T->·T*F
T->·F
F->·(E)
F->·id
I5
F->id·
```
重复上面的过程,直到没有新的状态产生.
# LR分析表
LR分析表描述了「什么状态」 对 「什么符号」 应该执行「什么操作」.
计算LR分析表的本质就是,确定如下3点:
- 什么状态: 确定分析表的行数,状态可通过自动机获得.
- 什么符号: 确定了每一行的列数,可通过状态中的项
- 什么操作: 可为 Shift、GOTO、Reduce、Accept 之一
**什么状态**,通过遍历自动机即可得确定分析表的每一行.
**什么符号**,即lookahead,该符号确定了每一行的列数
- 对于一个形如A->𝜶·𝜷的项,其lookahead为𝜷
- Reduce/Accept,取决于具体分析算法:
- SLR,lookhead存在于「项对应的产生式的Follow集」
- LR,lookhead存在于项的展望集(lookaheadSet)
**什么操作** (关于Shift、GOTO、Reduce、Accept的产生时机)
- 每当遇到形如A->𝜶·𝜷的项时,如果𝜷是非终结符则产生GOTO动作,否则为Shift
- 每当遇到形如A->𝜶·的项时,如果A是开始符号则Accpet,否则为Reduce
伪代码如下:
```
for(自动机中的每一个状态S){
for(S中的每一个项I){
if(I形如A->𝜶·𝜷){
如果𝜷是非终结符则产生GOTO动作,否则为Shift
}
if(I形如A->𝜶·){
如果A是开始符号则Accpet,否则为Reduce
}
}
}
```
## 关于冲突检查
### 1 移入-归约冲突
1.1 允许多个项移入同一符号,考虑如下两个项:
```
S->·E
E->·E+T
```
它们的 nextSymbol 都是 E, 在处理第一个项时,已经计算了当前状态对输入E的后继状态,所以在当前状态第二次遇到E时,不必再重新处理.
1.2 对同一lookahead,存在移入、归约操作,考虑如下项集:
```
E->E+E·,{+,*,EOF}
E->E·+E,{+,*,EOF}
E->E·*E,{+,*,EOF}
```
该项集对于输入{+,*,EOF},同时存在移入、归约操作.
### 2 归约-归约冲突
原则上同一个状态对同一符号只能有一个操作,若个一个状态中的多个归约项的Follow集存在交集,那么分为如下两种情况进行处理:
2.1 允许状态中多个项归约为同一个符号. 如下例子中,虽然两个项的Follow集都为Follow(A),但是归约动作/结果都是相同的(都是A),因此允许该情况存在.
```
A->𝜶·
A->𝜶·
```
2.2 不允许状态中多个项归约为不同符号. 如下例子中,Follow(A) ∩ Follow(B)不为空的情况下,无法预知归约为A还是B,因此如下情况不允许存在.
```
A->𝜶·
B->𝜶·
```
### 3 其他冲突
3.1 允许GOTO-GOTO冲突, 即: A->a·C B->b·C
3.2 Accept和 GOTO、Shift、Reduce都不可能冲突,因为Accept具备以下特征:
- EOF才可能触发Accept (Shift、GOTO不具备)
- 归约项必须为开始符号 (Reduce不具备)
## 关于优先级与结合性
每一个项都具有优先级(prec)、结合性(assoc)属性.
```
E->E+E·,{+,*,EOF}
E->E·+E,{+,*,EOF}
E->E·*E,{+,*,EOF}
+: prec=1,assoc=left
*: prec=2,assoc=left
```
### 如何确定优先级、结合性
**产生式的优先级与结合性**
一个产生式的prec、assoc由产生式中的符号决定. 上例中,`E->E+E`的prec、assoc继承`+`,即: `prec=1,assoc=left`.
如果产生式中存在多个符号具有prec、assoc,则以prec最大的符号为准,例如 `E->E+E*`中,`*`的优先级高于`+`,因此该产生式的prec、assoc以`*`为准,即`*: prec=2,assoc=left`.
**项**
项的优先级、结合性继承于对应的产生式.
**操作的优先级与结合性**
操作的prec、assoc取决于产生该操作的项. 例如: E->E·+E,会产生式一个对`+`的`shift`操作,该操作的优先级、结合性取决于项`E->E·+E`;类似的,`E->E+E·`会产生式一个`reduce`操作,该操作的prec、assoc取决于`E->E+E·`
### 如何根据优先级、结合性决定解决冲突
通过比较两个操作的prec、assoc
**移入归约冲突**
```
如果相同优先级
结合方向相同
- 左结合,优先归约
- 右结合,优先移入
结合方向不同
抛出异常
不同优先级
以高优先级的操作的prec、assoc为准
```
例1,E->E·+E对`+`产生一个`shift`,而E->E+E·对`+`产生一个`reduce`,两个操作的优先级相同,且为左结合,因此选择`reduce`
例2,E->E·*E对`*`产生一个`shift`, 而E->E+E·对`*`产生一个`reduce`,前者的优先级更高,因此选择`shift`
**归约归约冲突**
```
优先级相同,抛出异常
优先级不同,执行高优先级的操作
```
例3,下例中,两个规约项的优先级同,无法得知应该规约为A、还是B,将抛出异常.
```
S->A | B
A->a+b
B->a+b
+ left 1
A->a+b· $
B->a+b· $
```
# LR Parser 实现
```
parse(lexer: ILexer): ASTree {
let startState = this.stateAutomata.satrtState();
声明 stateStack ,令状态栈的栈顶为0(初始状态)
声明 astStack
令lookahead为输入流的第一个符号
while (1) {
topS = stateStack.peek()
action = parsingTable[topSta][lookahead]; //查找分析表,得到 topS 对 lookahead 的动作 action
if (action is Shift) {
astStack.push(lexer.next()); //lookahead放入符号栈
stateStack.push(op.nextState); //状态装入后继状态
lookahead = lexer.peek(); //令lookahead为下一个符号
}
else if (action is Goto) {
stateStack.push(op.nextState);
lookahead = lexer.peek();
}
else if (op is Reduce) {
let eles: ASTElement[] = []; /* 此次归约产生的AST节点所需的元素 */
for (规约产生式中的每一个符号x) {
从stateStack弹出x对应的状态 // 每个状态都由输入一个符号得到 因此每个状态都一个对应的符号 详见:P158
}
astStack.push(op.prod.postAction(eles, astStack)); //issue ASTNode的元素的顺序问题
debug(`reduce ${op.prod}, make ast: ${astStack.peek()}`);
//将向前看符号指定为归约符号
lookahead = op.prod.head;
}
else if (action is Accept {
debug(`accept!`);
break;
}
debug(`${astStack.join(" ")}`);
}
return astStack.pop() as ASTree;
}
```