编译原理LLVM

基于LLVM的编译原理简明教程 (2) - 词法与语法分析初步

2017-03-07  本文已影响442人  Jtag特工

递归 - 词法分析与语法分析的分界

一般来说,决定词法分析和语法分析的界限是是否需要递归。
词法分析是将输入的符号流转换成一个个独立的token。比如说,996是个数值型或者更精确一些整型的token。
这个token解析的过程,它前面是什么符号,后面是什么符号,完全没有关系。
token也不存在递归的可能性,token之间相互独立,不可能是嵌套的关系。
所以,词法分析可以用正则表达式来实现。只要一个串符合[0-9]+,我们就可以确定地认为,这是一个整数。
词法分析可以从左到右,完全线性的方式实现。它不需要树的结构,自然没有递归的需求。

而语法分析就有不同,比如一个表达式,它可能是"7 * 24",也可能是"(8+1) * 5",或者更复杂的组合。这样的表达式就需要用一棵树的结构来表示。
比如我们这样定义表达式:

表达式 = 数字
表达式 = 表达式 + 表达式
表达式 = 表达式 - 表达式

这样,表达式"1+2+3+4"就可以表示成下图这样的一棵树:

statement

自顶向下和自底向上

针对上面的图,我们有两种分析的办法,一种是自顶向下,一种是自底向上。

为了大家看起来方便,我们不妨把上面的式子改成前缀表达式:

表达式 = 数字
表达式 = + 表达式 表达式
表达式 = - 表达式 表达式

自顶向下就是,先扫描到一个"+"的token,然后分别对它后面的两个token进行分析。第一个token是数字,不需要递归了。然后去看第二个表达式,发现还是一个"+",于是递归去分析+,以此类推。

总结起来,自顶向下的思想是,先预测是个什么,然后按照期待去找下一个token。

而自底向上的思想不是这样,它是从左到右把token都读进来,然后去尝试找目前已经读进来的token们符合哪个式子的定义。
以上面的例子为例:
第一步,读到"+" 不符合上面三个式子的任何一个,继续向右读token。这个过程叫做shift,中文译成“移进”。
第二步,"+ 1",数字1匹配了第一条,变成+ 表达式,不能继续匹配了,继续读token
第三步,"+ 表达式 +",什么鬼,不匹配,继续读。
...
第五步,读到"+ 表达式 + 表达式 +",还是找不到可以匹配的式子,继续向右读。
...
第七步,读到"+ 表达式 + 表达式 + 表达式 4",4匹配了第一条,变成表达式, "+ 表达式 表达式"匹配了第二条,也变成表达式。这种操作叫做“归约”-reduce。这一步归约了"+ 3 4"
第八步,归约"+2 表达式"
第九步,归约"+1 表达式"

LR分析器

自底向上的方法的重要方法是LR方法,LR分析器的构造一般如下图所示:

LR分析

LR分析器是以一个状态机的方向来运作的。
这其中有两个重要的表:

Action表的输入有两项:

Action表的输出有4种情况:

我们看一个龙书上的例子,构造含有二元运算符+和*的算术表达式的文法:

1. E -> E + T
2. E -> T
3. T -> T * F
4. T -> F
5. F -> (E)
6. F -> id

我把龙书上用符号表示的表用文字标注上颜色,使大家更加容易记忆和理解。
对应的action表如下:

action table

goto表如下:

goto table

下面我们尝试分析一下"id * id $".

  1. 初始状态是0. 输入为id. 我们查0行id列的action表,是将id移进栈,同时,状态栈顶转为5. 完成这一步后,栈中内容[0 id 5]
  2. 状态5,输入为。查action表5行列,是使用公式6(F->id)进行归约。此时,状态5和id输入,都被从栈中归约掉,变成F。这时的栈为[0 F],因为产生了归约,所以要再去查goto表,根据目前栈中的值,去查0行F列,查到操作是转到状态3. 于是将3压入栈中,现在栈中的值是[0 F 3]
  3. 状态3下,还是遇到刚才的输入“*”。查action表,要做的是使用公式4(T->F)来归约。同样,F和状态3出栈,T入栈。现在栈中的内容是[0 T],又产生了归约,于是再查goto表,0行T列是转到状态2. 现在栈中的值是[0 T 2]
  4. 状态2下,刚才输入的还在,继续查表。action表的2行列是移进,下一状态是7。终于把这个*移进去了。现在的栈中的内容是:[0 T 2 * 7]
  5. 状态7下,遇到id。查action表,7行id列,移进,下一状态是5. 现在栈中内容为:[0 T 2 * 7 id 5]
  6. 状态5下,遇到结束符$。查action表,5行$列,归约,使用公式6(F->id). id和状态5出栈,F入栈。现在的栈是[0 T 2 * 7 F],再去goto表中查7行F列,状态为10。这一步的最终栈是:[0 T 2 * 7 F 10]
  7. 状态10下,输入还是$。查action表,10行$列:使用公式3(T->T*F)归约。请注意,除去状态不计的话,[0 T 2 * 7 F 10]的值正是[T * F],于是将[T 2 * 7 F 10]全部出栈,将T入栈。归约之后再查goto表,[0 T],查0行T列,状态是2. 这一步最终栈结果:[0 T 2]
  8. 状态2下,输入还是$。查action表,归约,使用公式2(E -> T). T和2出栈,E入栈。现在的栈为[0 E],再查goto表,0行E列,状态为1。这一步最终结果是[0 E 1]
  9. 状态1下,输入仍然是$没变。查action表,1行$列,接受,解析成功!

下面的问题就变成如何能够构造action表和goto表。LR下面的不同方法,就是如何生成这两张表的过程。

子集构造算法

子集构造算法是将不确定的有穷自动机NFA转换成确定的有穷自动机的算法。

从不确定的有穷自动机转换成确定的有穷自动机的基本思想是将确定有穷自动机的一个状态对应于不确定有穷自动机的一个状态集合。

子集构造算法

状态集合初值为初始状态的空闭包(ε-closure),且不作标记
while (状态集合中还有未标记的状态T){
    标记这个状态T;
    for 每个输入符号a in 输入集合 {
        U = 空闭包(move(T,a));
        if(U不在状态集合中){
            U添加到状态集合中;
            U的状态为未标记;
        }
        Dtran[T,a]=U;
    }
}

其中,构造子集算法使用到了求空闭包(ε-closure)的算法。

求ε-closure的算法用人话讲就是,从起点或者起点的集合,计算出走ε路径可以到达的所有状态。我们可以把a,b这些值理解为大于0的权值,而ε为权值为0. 求ε闭包的算法就是求从指定起点的权值之和为0的所有路径的集合。

求ε-closure空闭包的算法

将T中所有的状态压入栈中; //这是所有的起点的集合
空闭包集合初始化为T; //清空栈
while (栈不空){
    栈顶元素t弹出栈; //取一个起点出来
    for 状态u in 从t到u有一条标记为ε的边{ //起点和状态之间有ε的边
        if (u不在空闭包集合中){
            将u添加到空闭包集合中; //u是符合条件的值
            将u压入栈中; //如果u下面还可以继续传导,后面还可以有ε的边
        }
    }
}

我们看下面的龙书上的例子:

epsilon-closure

ε-closure(0)就是从0开始距离为ε的所有状态,直接跟0相连的有1和7。1又可以通达2,4. 所以ε-closure(0)为{0,1,2,4,7}

下面我们开始应用到子集构造算法的例子中:
初始状态0的空闭包集合为{0,1,2,4,7},我们用A来表示。
move({0,1,2,4,7},a) = {3,8}。这一步是从{0,1,2,4,7},指定输入为a时可以到达的状态,move(2,a)=3, move(7,a)=8,其他都不能到达。
ε-closure({3,8})= {1,2,3,4,6,7,8},用B来表示.
move({1,2,3,4,6,7,8},a)={3,8},跟B重复
于是a的情况完成了,我们再遍历输入为b的情况。
move({0,1,2,4,7},b)={5}
ε-closure({5})={1,2,4,5,6,7} = C
ε-closure(move(B,b))=ε-closure({5,9})={1,2,4,5,6,7,9} = D
ε-closure(move(C,a))=ε-closure({3,8}) = B
ε-closure(move(C,b))=ε-closure({5}) = C
ε-closure(move(D,a))=ε-closure({3,8}) = B
ε-closure(move(D,b))=ε-closure({5}) = C

最后生成的是这样的状态转换图:

state machine

SLR方法

拓广文法

如果文法G的开始符号是S,那么文法G的拓广文法G'是在G的基础上增加一个新的开始符号S'和产生式S->S'。新产生式的目的是用做归约的终点。

闭包运算

闭包是:

例:

E' -> E
E -> E+T | T
T -> T*F | F
F -> (E) | id

closure{[E' -> .E]}包含:

根据规则1,E' -> .E 本身在闭包里
根据规则2,
E -> .E+T
E -> .T
T -> .T*F
T -> .F
F -> .(E)
F -> .id

goto函数

我们终于开始看到如何生成goto函数了。
goto(I,X)函数的定义为A->αX.β的闭包。

例:若I是两个项目的集合{[E'->.E],[E->E.+T]},则goto(I,+)包括:

E -> E + .T
T -> .T + F
T -> .F
F -> .(E)
F -> .id

项目集的构造算法

算法:

C = {closure([S'->.S])};
do{
    for 项目集I in C, 文法符号X in C {
        if(goto(I,X)!=nullptr && inC(goto(I,X))
        C.add(goto(I,X));
    }
}while(还有更多的项目可以加入C);

SLR语法分析表的构造

算法:

  1. 构造G'的LR(0)项目集规范族。采用上面介绍的项目集构造算法。C={I0,I1,I2,...}
  2. 从Ii构造状态i,它的分析动作确定如下:
    2.1. 如果[A->α.aβ]在Ii中,并且 goto(Ii,a)=Ij,则置action[i,a]为"移进j",这里的a必须是终结符
    2.2. 如果[A->α.]在Ii中,则对FOLLOW(A)中的所有a,置action[i,a]="归约A->α",这里的A不能是S'.
    2.3. 如果[S'->.S]在Ii中,则置action[i,$]为“接受”
  3. 对所有的非终结符A,使用下面的规则构造状态i的goto函数:如果goto(Ii,A)=Ij,则goto[i,A]=j
  4. 不能由2和3构造出来的表项都置为出错。

构造规范LR语法分析表

SLR对于某些情况是无法归约的,我们可以通过重新定义项目,把更多的信息并入状态中,变成[A->α.β,a], 其中A->α.β是产生式,a是终结符或$。
这样的对象叫做LR(1)项目。

构造LALR语法分析表

LALR是(look-ahead-LR)的缩写。它的优点是比LR(1)的分析表要小得多。

乔姆斯基的文法分类

我们先看个乔姆斯基文法分类的示例图:


chmosky

类似于我们上面所讲的生成式,可以对应到乔姆斯基的文法分类上。
关于终结符和非终结符,我们就不需要做严谨的数学定义了吧。像数字一样不能推导出其他式子的,就是终结符。像表达式这样可以继续推导的就是非终结符。

乔姆斯基将文法分成4类:

上一篇下一篇

猜你喜欢

热点阅读