CH2 词法分析
词 法 分 析 的 含 义 { 构 词 规 则 或 者 词 法 规 则 — — 立 法 识 别 输 入 序 列 , 词 法 分 析 — — 执 法 词法分析的含义\begin{cases}构词规则或者词法规则——立法\\识别输入序列,词法分析——执法\end{cases} 词法分析的含义{构词规则或者词法规则——立法识别输入序列,词法分析——执法
2.1 词法分析的若干问题
2.1.1 记号,模式和单词
单词的基本分类:
类别 | 符号表示 |
---|---|
关键字(保留字) | kw(key word or reserved word) |
标识符 | id(identifier) |
字面量 | literal |
特殊符号 | ks(key symbol, or special symbol) |
例:
position := initial + rate * 60;
识别结果: id ks id ks id ks literal
根据什么识别单词?
三个术语:
- 模式:产生和识别单词的规则
- 记号:按照某个模式识别出来的元素
- 单词:被识别元素的自身的值,也称为词值
记号的分类:
记号的类别 | 单词举例 | 模式的非形式化描述 |
---|---|---|
CONST(01) | const | const |
IF(03) | if | if |
RELATION(81) | <,>,=,<>,<=,>= | <或<=或=或… |
ID(81) | pi,count,D2 | 字母打头的字母数字串 |
NUM(83) | 3.14,0,6.02E23 | 任何数值常数 |
LITERAL(84) | “core dumped” | 双引号之间的任意字符串 |
Comment | {x is an integer} | 括号之间的任意字符串 |
2.1.2 记号的属性
再考查赋值语句position := initial + rate * 60;
position、initial和rate均为标识符,即它们的种类均是ID。
当识别出一个ID时,如何区分?
同样,当识别出一个RELATION时,究竟是=还是<?
- 记号=记号的类别+记号的属性
记号的类别标识一类记号,记号的类别加上记号的属性标识一个记号实例
一般情况下,记号类别可以用整型编码/枚举类型
记号类别 | 编码 |
---|---|
ID | 81 |
CONST | 01 |
… |
2.1.3 词法分析器的作用与工作方式
特征:唯一与源程序打交道的部分
任务:
- 识别记号,并交给语法分析器
- 滤掉源程序中的无用的部分,如,注释,回车,空格等
- 处理与具体平台有关的输入(如文件结束符的不同表示的等)
- 调用符号表或者出错处理器,进行相关处理
工作方式:
-
单独一遍扫描——串行结构
-
作为语法分析器的子程序——按需调用
-
并行方式
2.2 模式的形式化描述
2.2.1 字符串与语言
(1)定义2.1 语言的定义
语言 L 是有限字母表∑上有限长度字符串的集合。
- 字符串中的所有字符取自该字母表
- 两个有限:
- 字母表中的元素是有限的
- 字符串的长度是有限的,即字符串中字符的个数是有限多的
(2)字符串与字符串集合相关的概念与运算
- 字符串的相关概念
表示,术语 | 举例 |
---|---|
|S| | |“abc”| = 3 |
ε | |ε|= 0 |
S1S2**(字符串上的连接运算)** | “abc”“efg” = “abcefg” |
S n S^n Sn(拼接若干次) | “abc”2 = “abcabc”, “abc”3 = “abcabcabc” |
S的前缀**(从首部开始连续取)** | “abc”的前缀有:ε, “a”, “ab”, “abc” |
S的后缀**(从首部开始连续去掉)** | “abc”的后缀有:ε, “c”, “bc”, “abc” |
S的子串 | “abc”的子串有:ε, “a”, “b”, “c”, … |
S的真前缀**(不能为空,不能为原串)** | “abc”的真前缀: “a”, “ab” |
S的真后缀(不能为空,不能为原串) | |
S的真子串(不能为空,不能为原串) | |
S的子序列 | “abcdef”的一个子序列: “abdf” |
S中去掉0个或者多个不一定连续的字符后形成的字符串
Note:
S 0 = ϵ S 1 = S S n = S S n − 1 S^0 = \epsilon\\ S^1 = S\\S^n = SS^{n-1} S0=ϵS1=SSn=SSn−1
- 字符串集合的相关概念
EXAMPLE:
若L = {a,b},M={c,d}
LM = {ac,ad,bc,bd}——这里注意字符串和集合的区别
L* :L中的任意个元素拼接任意次,同一元素可选择。
2.2.2 正规式与正规集
定义2.2 正规式和正规集的定义
1.ε是正规式,它表示集合 L(ε) = {ε}
-
若a是Σ上的字符,则a是正规式,它表示集合L(a)={a}
-
若正规式r和s分别表示集合L®和L(s),则
(a) r|s 是正规式,表示集合 L®∪L(s),
(b) rs 是正规式,表示集合 L®L(s),
(c) r* 是正规式,表示集合 (L®)*,
(d) ® 是正规式,表示的集合仍然是 L®。括弧用来改变运算的先后次序!
可用正规式描述(其结构)的语言称为正规语言或正规集。
(1)运算的优先级和结合性
-
三种运算均有左结合的性质
-
优先级从高到低顺序排列为:闭包运算、连接运算、或运算。
-
正规式中不必要的括号可以被省略
(a)|(((b)*)©)可以简化成: a|b*c
(2)正规式的等价
定义2.3 正规式的等价
若正规式P和Q表示了同一个正规集,则称P和Q是等价的,记为P=Q。
这意味着:不同正规式也可以表示同一个正规集,
即:正规式与正规集之间是多对一的关系。
(3)正规式等价的证明
正规式的等价性判定可以采用两种方法:
- 根据定义2.2/2.3,证明不同的正规式表示同一集合(如例2.4)
- 根据下述正规式的代数性质进行运算
r|s = s|r | (rs)t = r(st) |
---|---|
r|(s|t) = (r|s)|t | ϵ \epsilon ϵr = r ϵ \epsilon ϵ = r |
r(s|t) = rs|rt | r* = (r+| ϵ \epsilon ϵ) |
(s|t)r = sr|tr | r** = r* |
2.2.3 记号的说明
用自然语言对模式进行非形式化描述,存在精度问题
- 正规式是严格的数学表达式,可以解决该问题
用正规式说明记号的公式:记号 = 正规式
记号指的是记号的种类名
意为:左边的记号定义为右边的正规式
例如:id = a(a|b)*,可以读作id定义为a(a|b)*
(1)正规式的简化表示
-
正闭包
若r是表示L®的正规式,则r+是表示(L®)+的正规式,且下述等式成立:
r+ = rr* = r*r,r* = r+|ε
+与*具有相同的运算结合性和优先级。
例如:
(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)*
可以化简为:
(0|1|2|3|4|5|6|7|8|9)+
-
可缺省
若r是正规式,则r?是表示L®∪{ε}的正规式,且下述等式成立:
r? = r|ε
? 与 * 具有相同的运算结合性和优先级。
例如: E**(**+|-|ε) 可以改写为:E(+|-)?
-
串
若r是若干字符进行连接运算构成的正规式,则: “r” = r ,
且: ε= “”, a = “a”(a是Σ的任一字符)
-
字符组
若r是若干字符进行“或”运算构成的正规式,则可改写为 [r’],其中 r’ 可以有如下书写形式:
- 枚举: 如 a|b|e|h , 可写为 [abeh],顺序无关
- 分段: 如 0|1|2|3|4|5 ,可写为 [0-5]
- 混合: 如 0|1|2|3|4|5|6|7|8|9|a|b|c|d|x 可写为: [0-9a-dx]
注意:左边界小于右边界,字符范围要连续
-
非字符组
若[r]是一个字符组形式的正规式,则[**^**r]是表示∑- L([r])的正规式。
例如:若 ∑={a, b, c, d, e, f, g},则:
L([abc]) = L(a|b|c) = { a,b,c}
L([^abc]) = L(d|e|f|g) = {d,e,f,g}
(2)引入辅助定义
辅助定义的作用是为复杂的或重复出现的正规式命名,并在以后的使用中用名字代替该正规式。
辅助定义的形式与正规式一样:
名
字
=
正
规
式
名字 = 正规式
名字=正规式
但是:辅助定义并不表示任何模式。
即:作为辅助定义的正规式仅供内部使用,而不用于说明记号。
2.3 记号的识别——有限自动机
模式的描述——正规式
记号识别——有限自动机(确定,不确定)
2.3.1 不确定的有限自动机NFA
定义2.4 NFA
NFA是一个五元组:M = (S, ∑ \sum ∑,move, s0,F)
- S是有限个状态的集合
- ∑ \sum ∑是有限个输入字符(包括ε)的集合
- move是一个状态转移函数,move(si, ch) = sj,表示当前状态si下如果遇到字符ch,那么转移到状态sj
- s0是唯一的初态——有且仅有一个
- F是终态集(也成接受状态集),他是S的子集,包含了所有终态
(1)直观的表示方式
-
状态转化图:用一个有向图来表示NFA
-
NFA中的每个状态,对应转换图中的一个节点;
-
NFA中的每个move(si, a)=sj,对应转换图中的一条有向边;
表示从节点si出发进入节点sj,字符a是边上的标记。(注意:a可以是ε)
-
-
状态转移矩阵
用一个矩阵来直观表示NFA。 矩阵中,状态对应行,字符对应列;
每个矩阵元素M[si,a]中的内容,是从状态si出发,经字符a 所到达的下一状态sj;(注意:a可以是ε)
在转换矩阵中,一般以矩阵第一行所对应的状态为初态,而终态需要特别指出。
没有转移的状态时用"-"表示
(2)记号在NFA中的表现
对字符串,从初态开始,经一系列状态转移到达终态。
Example:abb
定义:
move(0,a) = 1, move(1,b) = 2, move(2,b) = 3}
转换图:
状态转移矩阵:
(3)NFA识别记号的特点
NFA识别记号的最大特点是它的不确定性,即在当前状态下对同一字符有多于一个的下一状态转移。
具体体现:
- 定义:move函数是一对多的
- 状态转移图:从同一个状态出发可以通过多于一条标记相同字符的边转移到不同的状态
- 状态转移矩阵:M[si,a]是一个状态的集合
(4)NFA识别输入序列的一般方法
反复试探所有路径,直到到达终态,或者到达不了终态。
-
从NFA的初态开始(边扫描边转移);
-
对于输入的每个字符,寻找其下一状态转移,直到到达一个终态,或没有下一状态转移为止。
①如果此时处于终态,则:NFA接受该记号;
②否则,回朔 + 试探:沿原路返回,并对于遇到的每个状态,寻找其可能的下一状态转移;
a. 若能够到达一个终态,则:NFA接受该记号;
b. 若一直返回到初态也没有遇到终态,则NFA不接受该输入(即输入序列不是语言的合法记号)。
(5)NFA识别记号存在的问题
1.只有尝试了全部可能的路径,才能确定一个输入序列不被接受,而这些路径的条数随着路径长度的增长成指数增长。
2.识别过程中需要进行大量回朔,时间复杂度升高且算法复杂。
2.3.2 确定的有限自动机DFA
定义2.5 DFA的定义
DFA是NFA的一个特例,其中:
(1)没有状态具有ε状态转移(ε-transition),即状态转换图中没有标记ε的边;
(2)对每个状态s和每个字符a,最多有一个下一状态。
与NFA相比,DFA的特征:确定性,其表现形式有:
-
定义: move(si, a)函数都是 1对1 的;
-
转换图: 从一个状态出发的任2条边上的标记均不同;
-
转换矩阵:M[si,a]是一个状态。
且字母表不包括ε。
算法2.1 模拟DFA
输入 DFA D和输入字符串x(eof)。D的初态为s0,终态集为F。
输出 若D接受x,回答“yes”,否则回答“no”。
方法 用下述过程识别x:
s:=s0; ch = nextchar(); // 准备初值
while ch≠eof // 循环
loop s:=move(s,ch); ch := nextchar();
end loop;
if s∈F
then return "yes";
else return "no";
end if;
2.4 从正规式到词法分析器
构造词法分析器的一般方法和步骤:
1.用正规式描述模式(为记号设计正规式);
2.为每个正规式构造一个NFA,它识别正规式所表示的正规集;
3.将构造的NFA转换成等价的DFA,这一过程也被称为确定化;
4.优化DFA,使其状态数最少,这一过程也被称为最小化;
5.根据优化后的DFA构造词法分析器。
2.4.1 从正规式到NFA
算法2.2 Thompson 算法
输入:字母表∑上的正规式r
输出 :接受L®的NFA N
方法:首先分解r,然后根据下述步骤构造NFA:
- 对于 ϵ \epsilon ϵ,构造NFA N( ϵ \epsilon ϵ)如下,初态为S0,F为终态,此NFA接受{ ϵ \epsilon ϵ}
- 对∑上的每个字符a,构造NFA N(a)如右上,它接受{a} ;
- 若N§和N(Q)是正规式P和Q的NFA,则
- 对正规式P|Q,构造NFA N(P|Q)如下。
- 对正规式PQ,构造NFA N(PQ)如下。其中s0为初态,f为终态,此NFA接受L§L(Q);
- 对正规式P*,构造NFA N(P*)如下。其中,s0为初态,f为终态,此NFA接受L(P*)(等价于(L§)*);
- 对于正规式§,使用P本身的NFA,不再构造新的NFA。
例2.11 用Thompson算法构造正规式r=(a|b)*abb的NFA N®
(1)分解NFA
(2)构造NFA
2.4.2 从NFA到DFA
(1)NFA识别的并行算法
并行的方法,核心思想是将不确定的下一状态确定化: 在每试探一步时,考虑了所有的下一状态转移,因此所走的每一步都是确定的。
因此,用NFA识别记号,常用并行方法,而不采用串行的方法(算法不易构造,复杂度高且回溯)
NFA上识别记号的确定化方法:
- 确定化的两个方面
- 计算下一状态转移时:
- 消除 ϵ \epsilon ϵ状态: ϵ \epsilon ϵ-闭包(T)
- 消除多余一个的下一状态转移:smove(S,a)
- smove(S,a):从状态集S出发,经过a可以直接到达的下一状态全体。与move(S,a)的唯一区别:状态->状态集
- ϵ \epsilon ϵ-闭包(T):从状态集T出发,不经过任何字符或者经过 ϵ \epsilon ϵ可到达的状态全体。
定义2.7 ε-闭包(T)
状态集T的ε-闭包(T)是一个状态集,且满足:
(1) T中所有状态属于ε-闭包(T);
(2) 任何smove(ε-闭包(T),ε)属于ε-闭包(T);
(3) 再无其他状态属于ε-闭包(T)。
- 总结:经过0次或者任意次空转移可以到达的状态集合
算法2.3 模拟NFA
输入 NFA N,x(eof), s0, F
输出 若N接受x,回答"yes",否则"no"
方法 用下面的过程对x进行识别。S是一个状态的集合。
S := ε-闭包({S0}); // 所有可能的初态的集合
ch := nextchar();
while ch ≠ eof loop
S:= ε-闭包(smove(S, ch));
ch := nextchar();
end loop;
if S ∩ F ≠ ∅
then return "yes";
else return "no";
end if;
例:在NFA上识别输入序列abb和abab
识别abb:
1.计算初态集: S = ε-闭包({0}) = {0,1,2,4,7} A
2.A出发经过a到达:S = ε-闭包(smove(A, a)) = {3,8,6,7,1,2,4} B
3.B出发经过b到达:S = ε-闭包(smove(B, b)) = {9,5,6,7,1,2,4} C
4.C出发经过b到达:S = ε-闭包(smove(C, b)) = {10,5,6,7,1,2,4} D
5.结束且D ∩ {10} = {10},接受
识别的路径为:AaBbCbD
(2)子集法构造DFA
“并行”模拟NFA的弱点:每次动态计算下一状态转移的集合,效率低。
改进方法:将NFA上的全部路径均确定化并且记录下来,得到与NFA等价的DFA。
NFA->DFA,消除不确定性
算法2.5 从NFA构造DFA(子集法)
输入:NFA N
输出 :等价的DFA D。其初态是NFA初态的ε-闭包,其终态是含有NFA终态的状态集合。
两个数据结构:Dstates(状态),Dtran(状态转移)
方法:用下述过程构造DFA
ε-闭包({s0})是Dstates仅有的状态,且尚未标记。// D的初态
while Dstates 有尚未标记的状态T
loop 标记T
for 每一个字符a // 来自字母表
loop U:= ε-闭包(smove(T,a));
if U非空
then Dtran[T,a]:=U;
if U不在Dstates中
then U作为尚未标记的状态加入Dstates;
end if;
end if;
end loop;
end loop;
例2.15 用算法2.5构造(a|b)*abb的DFA
a | b | |
---|---|---|
ε-闭包({0}) = {0,1,2,4,7} 初态A | {3, 8, 6, 7, 1, 2, 4} B | {5, 6, 7, 1, 2, 4} C |
B | {3, 8, 6, 7, 1, 2, 4} B | {9, 5, 6, 7, 1, 2, 4} D |
C | {3, 8, 6, 7, 1, 2, 4} B | {5, 6, 7, 1, 2, 4} C |
D | {3, 8, 6, 7, 1, 2, 4} B | {10, 5, 6, 7, 1, 2, 4} E |
E | {3, 8, 6, 7, 1, 2, 4} B | {5, 6, 7, 1, 2, 4} C |
画出状态转移表:
a | b | |
---|---|---|
A | B | C |
B | B | D |
C | B | C |
D | B | E |
E | B | C |
包含状态10的为终态,所以E为终态。
状态转移图:
定义2.8 可区分的概念
对于任何两个状态t和s,若:
-
从一状态出发接受输入字符串ω,而从另一状态出发不接受ω,或者
-
∃ω, 从t出发和从s出发到达不同的接受状态,
则称ω对状态t和s是可区分的。
反方向思考该定义:
设想任何输入序列ω对s和t均是不可区分的,则说明从s出发和从t出发,分析任何输入序列ω均得到相同结果。
因此,s和t可以合并成一个状态。
算法2.6 最小化DFA的状态数
输入 DFA D={S,∑,move,s0,F}。
输出 等价的D’={S’,∑,move’,s0’,F’}(D’状态数最少)
方法 执行如下步骤:
1. 初始划分 π = {S-F, F};
2. 应用下述过程构造新的划分π_{new}:
令π_{new} = π
for π的每一个组G
loop 划分G,G的两个状态s和t仍在一个组中的充要条件是:
任意a.(move(s,a)∈Gi∧move(t,a)∈Gi);-- Gi是Π中的组
用新划分的组代替π_new的G,形成新的划分
end loop;
3. if π_new = π then π_final := π 且转4;
else π := π_new 且转2;
4. 在Πfinal每个组Gi中选一个代表si',
使得D中从Gi所有状态出发的状态转移在D'中均从si'出发,D中所有转向Gi中的状态转移在D'中均转向si'。
含有D中s0的状态组G0的代表s0' 成为 D’的初态, π_final所有含F中状态的组Gk 的代表 sk'构成D'的终态集F';
// 选代表,改转移
5. 删除D’的死状态(即不是终态且对所有输入字符均转向其自身),并删除从初态不可到达的状态。
最小化DFA算法的主要步骤:
1.初始划分:终态组 , 非终态组;
2.利用可区分的概念,反复分裂划分中的组Gi,直到不可再分裂;
3.由最终划分构造D’,关键是选代表和修改状态转移;
4.消除可能的死状态和不可达状态。
例2.17 用算法2.6化简DFA
-
初始划分{ABCD, E}
-
∵ m o v e ( D , b ) = E , ∴ Π 1 = { A B C , D , E } ∵ m o v e ( B , b ) = D , ∴ Π 1 = { A C , B , D , E } 且 A C 不 可 区 分 , 所 以 Π f i n a l = { A C , B , D , E } ∵ move(D,b) = E, ∴ \Pi_1 = \{ABC,D,E\}\\ ∵ move(B,b) = D, ∴ \Pi_1 = \{AC,B,D,E\}\\ 且AC不可区分,所以\Pi_{final} = \{AC,B,D,E\} ∵move(D,b)=E,∴Π1={ABC,D,E}∵move(B,b)=D,∴Π1={AC,B,D,E}且AC不可区分,所以Πfinal={AC,B,D,E}
-
根据 Π f i n a l \Pi_{final} Πfinal构造D’
- 选代表:用A代表AC
- 修改状态转移
----选代表,改转移---->-----状态转移图------>
2.4.5 构造词法分析器的步骤
1.用正规式描述模式
2.正规式à NFA:Thompson 算法(算法2.2)
3.从NFA构造DFA(算法2.5)-子集构造法
4.最小化DFA(算法2.6)
5.从DFA构造词法分析器(模拟DFA(算法2.1))
(1)表驱动型词法分析器
(2)直接编码的词法分析器
在表驱动的词法分析器中,DFA是数据,用于指导驱动器(模拟DFA 的代码) 对输入序列进行分析。直接编码的词法分析器,将 DFA 和 识别输入序列的过程合并在一起,直接用程序代码模拟DFA识别输入序列的过程。
问题:如何用程序模拟DFA的状态和它的状态转移?