【编译原理】词法分析

时间:2020-12-08 19:54:11

词法分析



词法分析概述

从源程序的第一个字符开始,从左到右扫描源程序,一次读一个字符,根据词法规则将有关字符组合成单词,并识别各类单词,当确定单词类别后,将单词输出。在词法分析过程中还要完成其它任务,如:

  • 过滤掉源程序中的注释和空白;
  • 记录读入字符的行号,以便发现错误后能报告出错位置;
  • 进行预编译工作(对宏进行展开等工作);
  • 符号表操作;
  • 错误处理等;

词法分析器的输出一般为二元式(class,value)。
class代表它的种类,如标识符、数字、保留字、常数等等,一般用数字表示。
vlaue就是它具体的值。

正规文法与状态转换图

正规文法

回忆一下:

正规文法就是Chomsky分类中的3型文法: 
若文法中的规则都具有如下形式:A→ α 或A → B α (左线性)或 A→ α B(右线性)
其中 A,B∈ Vn, α ∈ Vt
规则右部至多含有一个非终结符号。也称线性文法、正则文法或正规文法。
3型文法描述的语言为3型语言(正则语言、正规语言),用L3表示。大多数程序设计语言的单词 (标识符、无符号整数) 的文法都是3型文法。

许多程序设计语言的单词,可以用正规文法来描述。对于这样的语言,使用状态转换图可以设计词法分析程
序(扫描器)。

状态转移图

状态转换图TG(简称状态图或转换图)是一张定义在字母表Σ上的有限方向图。在状态转换图中 :

  • 结点代表状态,用圆圈表示;
  • 状态之间用有向弧连结;
  • 有向弧上的标记(字符)表示在射出结点(有向弧的开始结点)所代表的状态下可能出现的输入符号或符号串。
  • 用带有符号“ ”的圆圈表示状态转换图的初始状态
  • 表示终止状态。
    例如:
    【编译原理】词法分析

一个状态转换图可用于接受(或识别)一定的符号串。在状态转换图中从初始状态到某一终止状态的序列为路径。对于某一符号串β,在状态转换图中,若存在一条路径产生β,则称状态转换图接受(或识别)该符号串β,否则符号串β不能被接受。

能被状态转换图TG接受的符号串的集合记为L(TG),称为状态转换图所能识别的语言。

正规文法的状态转换图表示

许多程序设计语言的单词可以用正规文法来表示,而对于正则文法所描述的语言又可以用状态转换图来非形式的表示。

对于右线性文法G[S]:U→xV|y V →z,状态转换图的表示方法如下:
1. 用状态表示G[S]中的非终结符,G[S]的开始符号S对应状态转换图的开始状态S;
2. 增加一个新状态Z,作为状态转换图的终止状态;
3. 对于G[S]中形如U→xV的每条产生式,画一条从状态U到状态V的方向弧,弧上的标记为x;
4. 对于G[S]中形如U→y的每条产生式,画一条从状态U到终态Z的方向弧,弧上的标记为y 。

例: 给出与正则文法G[S]等价的状态转换图。
G[A0]:
A0→0A0 | 1A0 | 1A1
A1→1A2
A2→ε
【编译原理】词法分析

对于左线性文法G[Z]:U→Vx|y V →z,状态转换图的表示方法如下:
1. 用状态表示G[Z]中的非终结符,G[Z]的开始符号Z对应状态转换图的终止状态Z;
2. 增加一个新状态S,作为状态转换图的初始状态;
3. 对于G[Z]中形如U→Vx的每条产生式,画一条从状态V到状态U的方向弧,弧上的标记为x;
4. 对于G[Z]中形如U→y的每条产生式,画一条从初态S到状态U的方向弧,弧上的标记为y 。

例:给出与正规文法G[Z]等价的状态转换图。
G[Z]:
Z→U0 | V1
U→Z1 | 1
V→Z0 | 0
【编译原理】词法分析

右线性文法状态图的识别过程,是为串w建立一个推导S w的过程。
左线性文法状态图的识别过程,是从串w出发的归约过程,最后归约为开始符号S。

正规表达式与有限自动机

正规式和正规集

为了识别正则语言,我们引入了状态转换图和有限自动机,有限自动机所接受的语言正是正规文法产生的语言(正规语言),程序设计语言中的单词也大多是由正规文法产生的。作为单词的语法除了用正规文法描外,我们还可以用一种更有效的工具——正规式加以描述。
采用正规式有以下几个原因:
1. 词法规则简单,无需上下文无关文法那样强有力的表示法,用正规式表示法去理解正被定义的是什么样的符号集合比领会由产生式集合定义的语言更为容易;
2. 借助正规式构造高效的识别程序比上下文无关文法更容易;
3. 可以从某个正规式自动构造识别程序,它识别的正是用该正规式表示的字符串集合中的字符串,从而减轻实现词法分析时工作的单调乏味程度。

多数程序设计语言的单词的语法都能用正规文法来表示。即文法G=( VN VT ,S,P)中的规则P都有下述形式:
A→aB或A→a,其中A,B∈ VN ,a∈ VT
实际上,正规文法所描述的是字汇表V( VN VT )上的一些特殊子集,称为正规集。

正规式也称正规表达式,是用于描述单词的另外一个工具,也是表示正规集的工具。下面我们给出正规式和它所表示的正规集的递归定义:
设有字母表为Σ,辅助字母表Σ’={ф,ε, | , . , * , ( , ) }
其中: “|” 读为 “或”
“.” 读为 “连接”
“*” 读为 “闭包”(即,任意有限次的自重复连接)。
算符的优先级为先“ * ”,再“ . ”,最后“ | ” ,都是左结合的,它们满足结合律,规定了算符的优先级后,可以省去一些不必要的括号。
定义:
1. ε和ф是Σ上的正规式,它们所表示的正规集分别为{ε}和ф;
2. 若a∈Σ,则a是Σ上的正规式,它所表示的正规集为{a};
3. 若e1和e2都是Σ上的正规式,且它们所表示的正规集分别为L(e1)和L(e2),那么:

  • (e1)是正规式,表示的正规集为L(e1);
  • e1|e2 是正规式,表示的正规集为L(e1)∪L(e2) ;
  • e1.e2 是正规式,表示的正规集为L(e1)L(e2) ;
  • e1* 是正规式,表示的正规集为(L(e1))*。

4.仅由有限次使用上述三步骤而定义的表达式才是Σ上的正规式,仅由这些正规式所表示的字集(符号串集合)才是Σ上的正规集。

没错就是这么抽象,就是这么难以理解。

如果两个正规式r和s表示同样的正规集,我们称两个正规式r和s等价,写作r=s。
而对某个正规式来说,可以对其进行变换并使它表示的正规集不变,这样的变换称为等价变换,在等价变
换的过程中,正规式服从以下代数定律:

r|s = s|r “ | ”运算满足交换律 
r|(s|t) = (r|s)|t “ | ”运算的可结合律
(rs)t = r(st) “连接”运算的可结合律
r(s|t) = rs|rt
(s|t) r = sr|tr 分配律
r = r rε= r ε是“连接”的恒等元素
r* = (r|ε)* ε和 * 的关系
(r*)* = r* * 是幂等的

三个概念之间的关系:
一个正规语言可以用正规文法定义,也可以用正规式定义,对任意一个正规文法,存在一个定义同一个语言的正规式;
同样,对每个正规式,存在一个生成同一语言的正规文法;
有些正规语言很容易用文法定义,有些则用正规式定义更容易;两者之间是可以转换的,结构上具有等价性.
由正规文法或正规式定义的正规语言的集合构成正规集.

有限自动机

有限自动机是一种识别装置,它能准确地识别正规集。它为词法分析程序的构造提供了方法和工具。
有限自动机是具有离散输入输出的数学模型。它具有有限数目的内部状态,系统可以根据当前所处的状态和面临的输入字符决定系统的后继行为。其当前状态概括了过去输入处理的信息。

正规文法可以用状态转换图非形式的进行表示,这就表明正规文法所对应的语言(正则语言)可以用状态转换图来接受(识别)。我们将看到,有限自动机正是对状态转换图进一步形式化的结果,它对于扫描器的构造,特别是扫描器自动生成将带来很大的方便。

确定的有限自动机(Deterministic Finite Automata)

定义:一个确定有限自动机(DFA)M是一个五元组:M=(S,Σ,f,S0,F),其中
1. S是一个非空有限集,它的每个元素称为一个状态;
2. Σ是一个有限的输入字母表,它的每个元素称为一个 输入字符;
3. f是转换函数,它是从S ×Σ到S的单值部分映射,即如果f(ki,a)=kj(ki∈S, kj∈S,a∈Σ)就意味着,当前状态为ki,输入字符为a时,将转换到下一个状态kj,kj成为新的当前状态,我们把kj称为ki的一个后继状态;
4. S0∈S,是唯一的初始状态;
5. F S,是终止状态集合。

例子:为下图所示的状态转移图构造确定的有限自动机。
【编译原理】词法分析
DFA M=({S,U,V,O},{a,b},f,S,{O}),其中f为:
f(S,a)=U f(S,b)=V f(V,a)=U f(V,b)=O
f(U,a)=O f(U,b)=V f(O,a)=O f(O,b)=O

事实上,状态转换图是有限自动机的一种表示形式,假定DFA M含有m个状态,n个输入字符,那么这个状态转换图含有m个状态(结点),每个结点最多有n个弧射出,整个图含有唯一一个初态结点(冠以“ ” )和若干个终态结点(用双圈表示),若有f(ki,a)=kj(ki∈K,kj∈K,a∈Σ),则从状态结点ki到状态结点kj画标记为a的弧。

一个DFA还可以用一个矩阵(状态矩阵)表示:矩阵的行表示状态,列表示输入字符,矩阵元素表示相应状态行和输入字符列下的新状态。

状态\字符 a b
S U V
U O V
V U O
O O O

对于Σ*中的任何字符串α,若DFA M中存在一条从初态结点到某一终态结点的路,且这条路上所有弧的标记连接成的字符串等于α,则称α可以被DFA M所接受(识别)。
若M的初态结点同时又是终态结点,则空串ε可被M所接受(识别)。
若α∈Σ*,f(S, α)=P,其中S为DFA M的初始状态,P∈Z,Z为终态集,则称字符串α可以被DFA M所接受(识别) 。
DFA M所能识别的所有字符串的全体(字的全体)称为DFA M所能接受的语言,记为L(M) 。

DFA的确定性表现在转换函数f: S×Σ→S是一个单值函数,也就是说对任何状态k∈S,和输入符号a∈Σ,f(k,a)唯一地确定了下一个状态。
从状态转换图来看,若字母表含有n个输入字符,那么任何一个状态结点最多有n条弧射出,而且每条弧以一个不同的输入字符进行标记。

非确定的有限自动机(Nondeterministic Finite Automata)

一个非确定的有限自动机(NFA) M是一个五元组:M=(S ,Σ,f,S0,F),其中
1. S是一个非空有限集,它的每个元素称为一个状态;
2. Σ是一个有限的输入字母表,它的每个元素称为一个输入字符;
3. f是转换函数,它是从S ×Σ*到2S的子集的映象 ;(也就是说对于某个输入可以达到很多个状态)
4. S0 S是一个非空的初始状态集; (也就是说可能有多个初态)
5. F S,是终止状态集合。
所以,一个含有m个状态和n个输入字符的NFA可表示成如下的一张状态转换图:这张状态转换图含有m个状态结点,每个结点可射出若干条箭弧与别的结点相连接,每条弧用Σ*中的一个串作标记(可相同),整个图至少含有一个初态结点以及若干个终态结点。

与DFA有相似的结论:对于Σ*中的任何一个串α,若NFA M中存在一条从某一初态结点到某一终态结点的路,且这条路上所有弧的标记依次连接成的串(不理睬那些标记为ε的弧)等于α,则称α可以被NFA M所接受(识别)。若M的某些结点既是初态结点又是终态结点,或者存在一条从某个初态结点到某个终态结点的ε路,那么空串ε可被M所接受(识别)。

对于任何两个有限自动机M和M’,如果 L(M)=L(M’),则称M与M’是等价的。
可以看出,DFA是NFA的一个特例。并且可以证明,对于每个NFA M存在一个DFA M’使L(M)=L(M’)。

非确定的有限自动机的确定化

子集构造法

NFA到相应的DFA的构造的基本想法是让DFA的每个状态代表NFA的一个状态集合。
即在转换后的DFA中,使用它的一个状态去记录在NFA中读入一个输入符号后可能到达的所有状态。
也就是将NFA的一个状态子集在DFA中用一个状态表示出来。

三个重要运算:
1. 状态集的ε-闭包:状态集I中的任何状态s及经任意条( 0)ε弧而能到达的所有状态的集合,定义为状态集I的ε-闭包,表示为ε-closure(I)。
2. 状态集的a弧转换:状态集I中的任何状态s经过**一条**a弧而能到达的所有状态的集合,定义为状态集I的a弧转换,表示为move(I, a)。
对于任意 NFA M=(K,Σ,f,S,F),I K,a∈Σ,不妨设I={s1,s2,…,sj },则move(I,a)=f(s1,a)∪f(s2,a) ∪…∪f(sj,a)。
3. 状态集的a弧转换的闭包:Ia=ε-closure(move(I,a))。

使用下面的例子来理解上面3个运算:
【编译原理】词法分析
对于I={0},ε-closure(I)=ε-closure({0})={0,1,2,4,7}
若I={2,3},ε-closure(I)=ε-closure({2,3})=={1,2,3,4,6,7}
令I ={0,1,2,4,7},则move(I,a)={3,8},move(I,b)={5}
Ia={1,2,3,4,6,7,8}
Ib={1,2,4,5,6,7}

确定化算法:
构造NFA N的状态集K的子集从而与DFA M的状态对应,即构造若干个状态集T1,T2, …,Ti,且Ti K,这样就可以构成由若干个状态集组成的子集族C ,C=(T1,T2, …,Ti) ,具体如下:

①令T=ε-closure(K0)作为C中的唯一成员(开始以前C中为空),并且它是未标记的,其中K0为NFA N的初态。
while(C中存在尚未标记的子集T
{ 标记T
while(每个输入字母a)
{ U= ε-closure(move(T,a));
if(U不在C中)
{
将U作为未标记的子集加入C中;
}
}
}

对下面的这个NFA进行确定化,帮助我们理解整个过程。
【编译原理】词法分析

状态集K={0,1,2,3,4,5,6,7,8,9,10},初态集 K0={0},开始前C不包含任何状态集
1. T0=ε-closure (K0)=ε-closure({0})={0,1,2,4,7},C=(T0) 。
2. T1=ε-closure(move(T0,a))={1,2,3,4,6,7,8}
T2=ε-closure(move(T0,b))={1,2,4,5,6,7}
C= (T0,T1,T2)。
T0被标记。
3. T3=ε-closure(move(T1,a))={1,2,3,4,6,7,8}=T1 ,舍弃 。
T3=ε-closure(move(T1,b))={1,2,4,5,6,7,9}。
C= (T0,T1,T2,T3)。
T1被标记。
4. T4=ε-closure(move(T2,a))={1,2,4,6,7,8}=T1,舍弃。
T4=ε-closure(move(T2,b))={1,2,4,5,6,7}=T2,舍弃。
T2被标记。
5. T4=ε-closure(move(T3,a))={1,2,3,4,6,7,8}=T1,舍弃。
T4=ε-closure(move(T3,b))={1,2,4,5,6,7,10}。
C= (T0,T1,T2,T3,T4)。
T3被标记。
6. T5=ε-closure(move(T4,a))={1,2,3,4,6,7,8}=T1,舍弃。
T5=ε-closure(move(T4,b))={1,2,4,5,6,7}=T2,舍弃。
T4被标记。

C中所有状态子集都已经被标记,算法结束

状态转移表:
实际上,对于Σ={a1,a2,…,ak}我们构造一张表,此表的每一行含有k+1列。置该表的首行首列为ε-closure(K0)。一般而言,如果某一行的第一列的状态子集已经确定,例如,记为I,那么,置该行的i+1列为Iai(i=1,2,…,k)。然后,检查该行上的所有状态子集,看它们是否已在表的第一列中出现,将未曾出现者填入后面空行的第一列。重复上述过程直至出现在第i+1列的所有状态子集均已在第一列上出现。

DFA的最简化(最小化)

定义和相关概念

对任意一个DFA M构造另一个DFA M’,使L(M)=L(M’),并且M’的状态个数不多于M的状态个数。

几个有关的概念:
1. 多余状态:对于一个状态Si,若从开始状态出发,不可能到达该状态Si,则Si为多余(无用)状态 。
【编译原理】词法分析
上图中S4,S5,S6均为多余状态。
2. 死状态:对于一个状态Si,对任意输入符号a,若转到它本身后,不可能从它到达终止状态,则称Si为死状态。 上图中的S2就是一个死状态。
多余状态和死状态又称为无关状态。
3. 等价状态:若Si为自动机的一个状态,我们把从Si出发能导出的所有符号串集合记为L(Si) 。设有两个状态Si和Sj,若有L(Si)=L(Sj),则称Si和Sj是等价状态。
【编译原理】词法分析
上图S1和S2就是等价状态。从S1和S2能导出相同的符号串集合:L(S1)=L(S2)={b},所以S1和S2等价。
4. 可区别状态:自动机中两个状态Si和Sj,如果它们不等价,则称它们是可区别的。

  • 状态Si和Sj必须同时为终止状态或同时为非终止状态。即终止状态和非终止状态是可区别的。
  • 状态Si和Sj对于任意输入符号a∈Σ,必须转到等价的状态里,否则Si和Sj是可区别的。

上图中,S0,S1,S2和S3是可区别的,S0和S2也是可区别的。

具体步骤

  1. 首先将DFA的状态集进行初始化,分成Π=(Z,S-Z),即终态和非终态。
  2. 用下面的过程对Π构造新的划分Π new:
    对Π中每个组G,把G划分成小组,G中的任意两个状态Si和Sj在同一组中,当且仅当对于Σ中任意输入符号a ,Si和Sj的a转换是到同一组中,move(Si, a) ∈Gi ,move(Sj, a) ∈Gi。这样,只要Si和Sj的a转换是到不同的组中,则说明Si和Sj是可区别的,可进行划分。在Π new中用刚完成的对G的划分代替原来的G。
  3. 重复执行2,直到Π中每个状态集不能再划分 (Π new= Π)为止;
  4. 合并等价状态,在每个G中,取任意状态作为代表,删去其它状态;
  5. 删去无关状态,从其它状态到无关状态的转换都成为无定义。

具体例子:对下图DFA进行最小化。
【编译原理】词法分析
1.首次划分: Π0=({A,B,C,D},{E})
2.在G={A,B,C,D}中:
f(A,a)=B,f(B,a)=B,
f(C,a)=B,f(D,a)=B,
f(A,b)=C,f(B,b)=D,
f(C,b)=C,f(D,b)=E(终态)
故{A,B,C,D}可划分为{A,B,C}和{D},
Π1=({A,B,C},{D},{E})
3.在G={A,B,C}中:
f(A,a)=B,f(B,a)=B,f(C,a)=B
f(A,b)=C,f(B,b)=D,f(C,b)=C
故{A,B,C}可划分为{A, C},{B},
Π2=({A,C},{B},{D},{E})
4. 在G={A,C}中:不可再分将A、C作为一个状态
最小化就变成了这样:
【编译原理】词法分析

正规式R与NFA的相互转换

正规式R转化为NFA:

三个替换规则:
【编译原理】词法分析

例子:构造正规式(a|b)* (aa|bb)(a|b)*的DFA。
通过上述3个替换规则可以得到:
【编译原理】词法分析
于是状态转移图为:
【编译原理】词法分析
写出状态转移矩阵:

I Ia Ia
{1,5,2} {5,2,7} {5,2,8}
{5,2,7} {5,2,7,3,6,4} {5,2,8}
{5,2,8} {5,2,7} {5,2,8,3,6,4}
{5,2,7,3,6,4} {5,2,7,3,6,4} {5,2,8,6,4}
{5,2,8,3,6,4} {5,2,7,6,4} {5,2,8,3,6,4}
{5,2,8,6,4} {5,2,7,6,4} {5,2,8,3,6,4}
{5,2,7,6,4} {5,2,7,3,6,4} {5,2,8,6,4}

重命名的状态转换矩阵:

I Ia Ia
0 1 2
1 3 2
2 1 4
3 3 5
4 6 4
5 6 4
6 3 5

从而可以得到未化简得DFA:
【编译原理】词法分析

接下来进行化简:
1.首次划分: Π0=({0,1,2},{3,4,5,6})
2.在G={0,1,2}中:
f(0,a)=1,f(2,a)=1(非终态)
f(1,a)=3(终态)
故{0,1,2}可划分为{0,2}和{1}
Π1=({0,2},{1},{3,4,5,6})
3.在G={0,2}中:
f(0,b)=2(非终态)
f(2,b)=4(终态)
故{0,2}可划分为{0}和{2}
Π2=({0},{2},{1},{3,4,5,6})
4.在G={3,4,5,6}中,同上做法,发现不可再分。
所以最后的划分为:Π=({0},{1},{2},{3,4,5,6})

所以最终化简后的DFA:
【编译原理】词法分析

NFA转化为正规式R:

  1. 在NFA M的状态转换图上加进两个结点x、y,从x结点用ε弧连接到M的所有初态结点,同时从M的所有终态结点用ε弧连接到y结点,形成一个与M等价的NFA M’,M’只有一个初态结点x,一个终态结点y ;
  2. 逐步消去M’的结点,直至只剩下x和y两个结点为止。在消结过程中,逐步使用正规式来标记弧。使用的规则如下:
    【编译原理】词法分析

  3. 按以上规则消结直到最后成为:
    【编译原理】词法分析

例子:对下图所示的NFA M求正规式R,使L(R)=L(M)。
【编译原理】词法分析

  1. 添加初态x,终态y。
    【编译原理】词法分析
  2. 消除节点
    【编译原理】词法分析
    【编译原理】词法分析
    【编译原理】词法分析
  3. 即可得到正规式R=(a|b)* (aa|bb)(a|b)*。

词法分析器的自动生成

把一个正规式编译(或称转换)为一个NFA进而转换为DFA,而这个NFA或DFA正是识别该正规式所表示的语言(正规集)的识别器。基于这种方法可以构造出词法分析程序(扫描器),这就是扫描器的自动生成原理。LEX是一个广泛使用的工具,它用于构造各种各样语言的词法分析程序。LEX编译系统的作用如图:
【编译原理】词法分析

LEX程序由三部分组成:说明部分、转换规则和辅助过程,它们之间用%%做间隔,其一般格式为:

{辅助定义部分}
%%
{识别规则部分}
%
%
{用户子程序部分}
  1. 辅助定义部分包括变量的说明、常量说明和正规式定义,所谓正规式定义是形如如下形式的一系列定义:
    d1→r1
    d2→r2

    dn→rn
    其中, di是代表这个正规式的简名,ri是Σ∪{d1,d2, …,di-1}上的正规式,即在ri中允许出现字母表Σ中的字符和前面定义的简名(d1,d2, …,di-1)
  2. 识别规则部分是LEX源程序的核心。它是一张表,左边一列是正规式,右边一列是相应的动作。
    P1 {action1}
    P2 {action2}

    Pm {actionm}
    其中,每个Pi是Σ∪{d1,d2, …,di-1}上的正规式,称之为词形。
  3. 用户子程序部分是在action的执行过程中用到的过程或函数 。