这是关于编译原理的第三篇笔记。
编译有五大步骤,本篇笔记将会讲解编译的第一步:词法分析。
词法分析的任务是:从左往右逐个字符地扫描源程序,产生一个个的单词符号。也就是说,它会对输入的字符流进行处理,再输出单词流。执行词法分析的程序即词法分析器,或者说扫描器。闭包
词法分析的成果就是由一系列单词符号构成的单词流。单词符号其实就是 token,通常有如下五大类:函数
while
,if
,int
等100
,'text'
,TRUE
等+
,*
,/
等具体来讲,一个单词符号在形式上是这样的一个二元式:(单词种别,单词符号的属性值)
。编码
单词种别:spa
单词种别一般用整数编码。一个语言的单词符号如何分种,分红几种,怎样编码是一个技术问题。它取决于处理上的方便。设计
a
和 b
,可能咱们都只用 1
做为它们的单词种别。2
表示,布尔值可能用 3
表示。单词符号的属性值3d
由上面的单词种别能够知道,关键字、运算符、界符基本都是一字(或者一符)对应一个种别,因此只依靠单词种别便可确切地判断出具体是哪种单词符号了。可是标识符和常数却不是这样,一个种别可能对应好几个单词符号。因此咱们须要借助单词符号的属性值作进一步的区分。指针
对于标识符类型的单词符号,它的属性值一般是一个指针,这个指针指向符号表的某个表项,这个表项包含了该单词符号的相关信息;对于常数类型的单词符号,它的属性值也是一个指针,这个指针指向常数表的某个表项,这个表项包含了该单词符号的相关信息。code
以 while(i>=j)i++
为例,它的单词符号流大概以下:blog
<while,-> <(,-> <id,pointer1> <>=,-> <id,pointer2> <),-> <id,pointer3> <++,-> <;,->
注意:实际上,对于关键字、界符这些,应该用整数表示单词种别,不过这里为了便于区分,直接用对应的单词符号表示了。对于标识符,因为 id 这个单词种别可能对应多个标识符,因此能够看到咱们用不一样的指针进行了标识。其它不须要标识的,则统一用短横线代替。递归
按照咱们常规的想法,应该是词法分析器扫描整个源程序,产生单词流,以后再由语法分析器分析生成的单词。若是是这样,那么就说词法分析器独立负责了一趟的扫描。但其实,更多的时候咱们认为词法分析器并不负责独立的一趟,而是做为语法分析器的子程序被调用。也就是说,一上来就准备对源程序进行语法分析,可是语法分析没法处理字符流,因此它又回过头调用了词法分析器,将字符流转化成单词流,再去分析它的语法。以此类推,后面每次遇到字符串流,都是这样的一个过程。
字符流输入后首先到达输入缓冲区,在词法分析器正式对它进行扫描以前,还得先作一些预处理的工做。预处理子程序会对必定长度的字符流进行处理,包括去除注释、合并多个空白符、处理回车符和换行符等。处理完以后再把这部分字符流送到扫描缓冲区。此时,词法分析器才正式开始拆分字符流的工做。
词法分析器对扫描缓冲区进行扫描时通常使用两个指示器:起点指示器指向当前正在识别单词的开始位置,搜索指示器用于向前搜索以寻找单词的终点。问题在于,就算缓冲区再大,也难保不会出现突破缓冲区长度的单词符号。也就是说,输入缓冲区把处理好的一段字符流送到扫描缓冲区时,扫描缓冲区可能装不下这段字符流,在这种状况下,若是依然只用一个缓冲区存放字符流,可能会致使某个过长的单词符号没法被正确读取。所以,扫描缓冲区最好使用以下一分为二的区域:
这样,在搜索指示器向前搜索到 A 半区边缘时,若是发现尚未找到单词符号的终点,那么就会调用预处理程序把剩下的部分送到 B 半区,搜索指示器再来到 B 半区扫描。这样就能够避免截断,从而将这个过长的单词符号顺利衔接起来。若是单词符号实在太长,两个半区都没法解决,那就没辙了。因此应该对单词符号的长度加以限制。
像 FORTRAN 这样的语言,关键字不加保护(只要不引发矛盾,用户能够用它们做为普通标识符),关键字和用户自定义的标识符或标号之间没有特殊的界符做间隔。这使得关键字的识别变得很麻烦。好比 DO99K=1,10
和 DO99K=1.10
。前者的意思是,K 从 1 变到 10 以后,跳转到第 99 行执行;后者的意思是,为变量 DO99K 赋值 1.10。问题在于,咱们并不能在扫描到 DO
的时候就确定这是一个关键字,事实上,它既有多是关键字,也有可能做为标识符的一部分。而具体是哪种,只有在咱们扫描到 =1
后面才能肯定 —— 若是后面是逗号,则这是关键字,若是是点号,则是标识符的一部分。
也就是说,咱们须要超前扫描到达第一个界符 =
,可是 =
还不能肯定,再继续超前扫描到达第二个界符(逗号或者点号),这时候才能彻底肯定。
状态转换图是设计词法分析程序的一种模型,咱们能够借助这个模型体会识别某个特定字符串的过程。它是一张有限方向图,结点表示状态,结点之间的箭弧上有字符,表示遇到该字符就将其读进来,而且转换到另外一个状态。如下面这张图为例,在状态 0 下若是输入的是字母,则将字母读进来,并进入状态 1 ;在状态 1 下若是输入的是字母或者数字,则将其读进来并从新进入状态 1 。不断重复,直到输入的不是字母和数字,这时候也将其读进来,并进入状态 2。状态 2 是终态,有一个 *
做为标记,标记着多读进来一个不属于目标的符号,应该把它退还给原输入串。这张图实际表示的是标识符类型的输入串。
状态转换图的结点(状态)个数是有限的,其中有一个初态,以及至少一个终态(同心圆表示)。
左图是 FORTRAN 语言的一些单词符号,右图是对应的状态转换图:
状态转换图的实现:
好比上面的状态转换图,它的词法分析器大概以下:
状态转换图是制造词法分析器的模型,不过这个模型过于具体,咱们应该想个办法,用一种更接近数学的、更为形式化的方法来表示状态转换图。而这种状态转化图的形式化表达,就是有限自动机。因为有限自动机涉及到了正规式、正规集等其它概念,因此咱们这里先普及一下这些概念。
推导
正规式和正规集都是相对于字母表来讲的概念,一般说“xx 字母表的正规式是......,字母表的正规集是......”。对于正规式和正规集,咱们采用递归的方式进行定义。即,对于某个给定的字母表 ∑
,规定:
ε
和 Ø
都是该字母表的正规式,这两个正规式分别表示了 {ε}
和 Ø
这两个正规集a
都是字母表的正规式,它表示了 {a}
这个正规集a
和 b
都是字母表上的正规式,且分别表示了 L(a)
和 L(b)
这两个正规集。那么,(a|b)
,(ab)
,(a)*
也都是正规式,它们分别表明了 L(a)∪L(b)
,L(a)L(b)
和 (L(a))*
这几个正规集。(笛卡尔积和闭包)根据上面这四条规则,咱们能够递归列举出某个字母表的正规式和对应的正规集
例如对于给定的字母表 ∑ = {a,b}
,咱们能够像下面这样推导出它的正规式和对应的正规集:
ba*
:a
是正规式,因此 a*
也是正规式(规则二),因此 ba*
也是正规式(规则二)。a
表示 {a}
这个集合,加上星号则表示该集合的闭包,b
表示 {b}
这个集合,因此并排放在一块儿表示两个集合做笛卡尔积运算
等价的正规式
若是两个正规式 U
和 V
表示的正规集相同,则认为这两个正规式等价,记做 U = V
。例如,b(ab)*
和 (ba)*b
就是等价的两个正规式。它们表示的集合形如 {ba,bb,bab,babab,babababab,......}
。能够看出这个集合的元素特色是,以 b
开头,后面跟着 a 和 b 自由组合的符号串。在没有引入正规式的概念以前,要表示这样的集合是比较麻烦的,但如今则方便不少。
正规式运算规则
对于正规式 U
,V
,W
,它们知足下面的运算规律:
一、交换律:U|V=V|U
二、结合律:U|(V|W)=(U|V)|W
三、结合律:(UV)W=U(VW)
四、分配律:U(V|W) = UV|UW
五、分配律: (V|W)U = VU|WU
六、零一概:εU=U
七、零一概:Uε=U
最后再来看一道题:
令 ∑={d,. ,e,+,-},其中d为 0~9 中的数字,则 ∑ 上的正规式
d*(.dd*|ε)(e(+|-|ε)dd*|ε)
表示的是?
先来划分结构,以 d*
开头,说明第一个部分是一个整数,第二个部分是 (.dd*|ε)
,能够取空,第三个部分是 (e(+|-|ε)dd*|ε)
,一样能够取空。若是后面两个部分都取空,则确定表明一个整数;若是第二个部分不取空,则会出现小数点,代表这时候会是一个小数;若是第三个部分不取空,则会出现 e
,代表这是一个用科学计数法表示的数字。综上,这个正规式表示的是全部无符号数构成的集合。
有个须要注意的地方是,d*
已经能够表示全部整数了,为何小数点后使用的是 dd*
而不是 d*
呢?这里实际上是起到一个占位的做用,由于单纯用 d*
的话,其实也包括了空符号串,可是既然出现了小数点,后面至少要跟一位数,不能为空。因此这里用 dd*
。对于 e
后面也是同理,既然出现了 e
,后面就不能为空了。
1. 肯定有限自动机的结构
咱们先来回顾一下这副状态转换图:
考虑到要用形式化的方法来表示它,咱们得先考虑转换图的一些重要组成因素。
那么,咱们能够构造一个有限的状态集合 S ,用以保存该转换图的全部状态;构造一个有限的字母集合 ∑,用以保存每个输入的字符;构造包括多个单值映射对 的 δ,每一对都表示从“当前状态和输入字符”到“跳转状态”的映射关系。具体地说,用 δ(s,a) = a'
表示,当前状态为 s
且输入字符为 a
时,跳转到状态 a'
;此外,须要用来自于状态集合 S 的 s0 做为惟一的初态;最后,构造一个终态集合 F,它是 S 的子集,可取空。
这样,咱们就有了 S,∑,δ,s0,F。这五个元素在一块儿就构成了咱们要讲的是肯定有限自动机。即,肯定有限自动机 DFA 可用以下的五元式表示:
M = {S,∑,δ,s0,F}
2. 肯定有限自动机的其它表示
正如咱们所说的,有限自动机是抽象层面上的形式化表达,而它在具体层面上的表达就是以前所讲的状态转换图。另外,肯定有限自动机还能够用一个矩阵来表示,这样的矩阵即 状态转换矩阵。它的行表示当前状态,列表示输入字符,而矩阵元素则表示跳转状态,也就是 δ(s,a)
的值。
以 DFA M = ({0,1,2,3,4},{a,b},δ,0,{3})
为例,若是它的映射以下:
δ(0,a) = 1 δ(0,b) = 2 δ(1,a) = 3 δ(1,b) = 2 δ(2,a) = 1 δ(2,b) = 3 δ(3,a) = 3 δ(3,b) = 3
那么它的状态转换矩阵以下所示:
当前状态 | a | b |
---|---|---|
0 | 1 | 2 |
1 | 3 | 2 |
2 | 1 | 3 |
3 | 3 | 3 |
3. 肯定有限自动机的做用
肯定有限自动机是状态转换图的形式化表达,它能够用于识别(或者说读出、接受)正规集。
对于 ∑* 中的任何一个字 a,若存在一条从初态结点到某一终态结点的通路,且这条通路上全部箭弧的标记符链接成的字等于 a,则称 a 为 DFA M 所识别(读出或接受)。
若是 M 的初态结点同时也是终态结点,那么就说空符号串能够被 M 所识别。
DFA M 能够识别的字的全体记为 L(M)。
看下面的例子:
这是某个肯定有限自动机对应的状态转换图,那么这个 DFA M 能够识别什么样的正规集呢?咱们能够先走几条路线看看(假定在遇到状态 3 就中止),不难发现它能够识别出诸如 aa
,bb
,abb
,baa
这样的符号串。这样的符号串的特色是,中间要么是 aa
,要么是 bb
,因此首先肯定中间是 (aa|bb)
。因为 aa
和 bb
均可以独立存在,说明 (aa|bb)
的前面和后面必须能够是空符号串,说到空符号串,咱们会想到闭包,因此它的前面后面一定会分别出现一个闭包。考虑前面,能够出现 a
或者 b
,因此前面应该是 (a|b)*
;考虑后面,咱们在遇到状态 3 的时候就中止了,但实际上,在这以后遇到 a
或者 b
,状态变化会循环往复,也就是说,无论遇到什么样的 ab
组合符号串,都可以被识别并循环转换到状态 3,这里说明后面的状态是任意的,因此肯定后面是 (a|b)*
。
结合起来,这个有限自动机能够识别的正规集能够用正规式 (a|b)*(aa|bb)(a|b)*
表示。
1. “肯定”和“不肯定”指的是什么?
“肯定”指的是,五元式中的映射是一个单值函数,也就是说,在当前状态下,面对某个输入字符,其跳转状态是惟一肯定的,即只会跳转到某一个值。可是,有的时候映射是多值函数,也就是说,在某个输入字符下有多个跳转状态可供选择。具备这样特色的有限自动机,就叫作非肯定有限自动机。
2. 非肯定有限自动机的结构
非肯定有限自动机能够用以下的五元式表示:
M = {S,∑,δ,s0,F}
3. 非肯定有限自动机的做用
非肯定有限自动机一样能够用于识别(或者说读出、接受)正规集。
对于 ∑* 中的任何一个字 a,若存在一条从初态结点到某一终态结点的通路,且这条通路上全部箭弧的标记符链接成的字等于 a,则称 a 为 NFA M 所识别(读出或接受)。
若是 M 的初态结点同时也是终态结点,或者存在一条从某个初态结点到某个终态结点的 ε 通路,那么就说空符号串 ε 能够被 M 所识别。(由于输入符号来自于集合闭包,因此输入符号接受空符号串 ε)
看下面的例子:
假设有非肯定有限自动机 NFA M=({0,1,2,3,4},{a,b},δ,{0},{2,4})
,其中,
δ(0,a)={0,3} δ(2,b)={2} δ(0,b)={0,1} δ(3,a)={4} δ(1,b)={2} δ(4,a)={4} δ(2,a)={2} δ(4,b)={4}
能够看到,有很多 δ 是被映射到 S 的一个子集,而不是像肯定 DFA 那样映射到一个输入字符。这个 NFA 对应的状态转换图以下:
这里会发现,这个 NFA 所能识别的正规集和以前的 DFA 是同样的,都是含有相继两个 a 或者相继两个 b 的符号串。事实上,尽管 DFA 是 NFA 的特例,可是对于每一个 NFA M,都会有一个 DFA M‘ 与之对应,使得 L(M) = L(M')
。这时候,咱们就说 NFA M 等价于 DFA M’。
非肯定有限自动机的肯定化,指的就是将非肯定有限自动机转换为一个与之等价的肯定有限自动机。总的来讲分为两步,第一步是利用等价转换规则细化 NFA 状态转换的过程;第二步是利用子集法对第一步转化获得的 NFA 进行肯定化。因为第二步又涉及到了一些概念,因此这里咱们先来对这些概念进行解释。
(1)空闭包集合
若 I
是一个状态集合的子集,那么 I
会有一个空闭包集合,记做 ε-closure(I)
。这个空闭包集合一样是一个状态集合,它的元素符合如下几点:
I
的全部元素都是空闭包集合的元素I
中的每个元素,从该元素出发通过任意条 ε 弧可以到达的状态,都是空闭包集合的元素如下面这张图为例:
ε-closure({5,3,4})
会等于多少呢?这里的 I
是 {5,3,4}
,因此空闭包集合必定包含了5,3,4。从 5 出发,通过一条 ε 弧到达 6,两条 ε 弧到达 2,因此 6 和 2 也是闭包集合的元素;从 3 出发,通过一条 ε 弧到达 8,因此 8 也是;从 4 出发,通过一条 ε 弧 7,因此 7 也是。综上,ε-closure({5,3,4}) = {5,3,4,6,2,8,7}
。
(2)Ia
若 I
是一个状态集合的子集,那么它相对于状态 a
的 Ia
等于 ε-closure(J)
。其中,J
表示的是,从 I
中每一个状态出发,通过标记为 a 的单条弧而到达的状态的集合。也就是说,Ia
表示的是从 I
中每一个状态出发,通过标记为 a 的弧而到达的状态,再加上从这些状态出发,通过任意条 ε 弧可以到达的状态。
仍是以这幅图为例:
当 I
是 {1,2}
的时候,Ia
等于多少呢?
Ia
。从 5,4 出发,通过 ε 弧可以到达 6,2,7,因此 6,2,7 属于 Ia
Ia
。从 3 出发,通过 ε 弧可以到达 8,2,7,因此 8 属于 Ia
综上,Ia = {5,4,6,2,7,3,8}
下面,介绍具体的肯定化过程。
第一条和第二条都好理解,重点在第三条规则。为何右边的图能够等价于左边的图呢?A*
其实表示的是相似 {ε,A,AA,AAA,AAAA,......}
这样的集合,由于 A
自由组合造成的符号串是能够用一个 A
的自循环来表示的,因此中间有一个自循环,而 ε 则能够用 εε 来表示,因此考虑在先后各加一个 ε,对于 A
的符号串不影响。
子集法的核心是,针对上面规则转换后获得的 NFA,画出它的状态转换矩阵,这个矩阵的矩阵元素是映射的子集,不是单值,而咱们要作的事情就是把这个子集用一个单值来表示。也就是说,对于 NFA 的每一组映射状态集,都用一个来自 DFA 的映射单值与之对应,从而求出等价的 DFA。
假设通过第一步,咱们已经获得下面的 NFA:
选取 NFA 的初态集合的空闭包做为初始集合 I
,这个集合 I
将是 ε-closure({i}) = {i,1,2}
。同时因为输入符号只有 a 和 b,因此第二列为 Ia
,第三列为 Ib
。获得以下这个表:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
根据前面的说法求解 Ia
和 Ib
。从 i 出发没有 a 弧,无视之;从 1 出发通过 a 弧 到达 1,从 2 出发通过 a 弧到达 3;从 1 出发通过 ε 弧到达 2,从 1 出发没有 ε 弧。因此,Ia = {1,2,3}
。从 i 出发没有 b 弧,无视之;从 1 出发通过 b 弧到达 1,从 2 出发通过 b 弧到达 4。从 1 出发通过 ε 弧到达 2,从 4 出发没有 ε 弧,因此 Ib = {1,2,4}
。记新获得的两个
集合为 A 和 B,获得下面的表:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
将新获得的集合 A 和 B 做为第一列的元素,获得下面的表:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
A:{1,2,3} |
||
B:{1,2,4} |
分别对 A 集合和 B 集合求解对应的 Ia
和 Ib
,获得下表(对于一样形式的集合仍采起以前命名,仅对新出现集合给定新的命名):
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
A:{1,2,3} |
C:{1,2,3,5,6,f} |
B:{1,2,4} |
B:{1,2,4} |
A:{1,2,3} |
D:{1,2,4,5,6,f} |
将新获得的 C、D 集合做为第一列的元素,一样求解 Ia
和 Ib
,获得下面的表:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
A:{1,2,3} |
C:{1,2,3,5,6,f} |
B:{1,2,4} |
B:{1,2,4} |
A:{1,2,3} |
D:{1,2,4,5,6,f} |
C:{1,2,3,5,6,f} |
C:{1,2,3,5,6,f} |
E:{1,2,4,6,f} |
D:{1,2,4,5,6,f} |
F:{1,2,3,6,f} |
D:{1,2,4,5,6,f} |
同理,继续推导,直到再也没有新集合出现:
Ia |
Ib |
|
---|---|---|
{i,1,2} |
A:{1,2,3} |
B:{1,2,4} |
A:{1,2,3} |
C:{1,2,3,5,6,f} |
B:{1,2,4} |
B:{1,2,4} |
A:{1,2,3} |
D:{1,2,4,5,6,f} |
C:{1,2,3,5,6,f} |
C:{1,2,3,5,6,f} |
E:{1,2,4,6,f} |
D:{1,2,4,5,6,f} |
F:{1,2,3,6,f} |
D:{1,2,4,5,6,f} |
E:{1,2,4,6,f} |
F:{1,2,3,6,f} |
D:{1,2,4,5,6,f} |
F:{1,2,3,6,f} |
C:{1,2,3,5,6,f} |
E:{1,2,4,6,f} |
如今,用字母命名代替全部的集合(初始集合给定名字 S
),获得下面的矩阵:
Ia |
Ib |
|
---|---|---|
S | A | B |
A | C | B |
B | A | D |
C | C | E |
D | F | D |
E | F | D |
F | C | E |
这个矩阵实际上已是一个 DFA 矩阵。咱们再以初始集合 S
将做为初态,包含原始 NFA 终态的集合(即 C、D、E、F)做为终态,画出它对应的状态转换图,以下:
那么,这个转换图实际上就是与最初 NFA 等价的 DFA 所对应的转换图了,到这里,咱们就完成了对非肯定有限自动机进行肯定化的工做了。
最后咱们再对这篇笔记涉及的知识点作一下回顾。首先咱们解释了词法分析的结果,也就是单词符号,以后讲解了一些词法分析过程当中的要点(预处理、超前扫描),最后则是本篇笔记的重点,词法分析的模型,包括状态转换图以及它的形式化表达 —— 有限自动机。
到这里,词法分析的内容尚未结束。剩下的内容咱们将在下一篇笔记中继续讲解。