词法单元,模式和词素
1.词法单元由一个词法单元名,一个可选的属性值组成.
2.模式描述了一个词法单元的词素可能具有的形式.
3.词素是源程序中的一个字符序列,和某个词法单元的模式匹配,
并被词法分析器识别为该词法单元的一个实例.
正则表达式
归纳基础:
1.ε是一个正则表达式,L(ε)={ε},即该语言只包含空串.
2.如a是∑上一个符号,则a是一个正则表达式,且L(a)={a}.
归纳步骤:
由小的正则表达式构造较大的正则表达式的步骤有四个部分.
假定r和s都是正则表达式,分别表示语言L(r)和L(s),那么:
1.(r) | (s)是一个正则表达式,表示语言L(r)UL(s).
2.(r)(s)是一个正则表达式,表示语言L(r)L(s).
3.(r)^{*}是一个正则表达式,表示语言(L(r))^{*}.
4.(r)是一个正则表达式,表示语言L(r).
a.一元运算符*具有最高的优先级,且是左结合的.
b.连接具有次高的优先级,也是左结合的.
c.|的优先级最低,且也是左结合的.
正则定义
如果∑是基本符号的集合,则一个正则定义是具有如下形式的定义序列:
d_{1}-->r_{1}
...
d_{n}-->r_{n}
其中:
1.每个d_{i}都是一个新符号,它们都不在∑中,且各不相同.
2.每个r_{i}是字母表∑ U {d_{1}, ..., d_{i-1}}上的正则表达式.
正则表达式的扩展
1.一个或多个实例.
如r是一个正则表达式,则(r)^{+}就表示语言(L(r))^{+}.
2.零个或一个实例.
类似+,符号为?
3.字符类
类似[a-d]可以表示a|b|c|d
状态转换图
一个正则表达式可以用一个状态转换图模拟.
一个起始位置,每次接受∑中一个字符,由当前状态切换到下一状态.
接受状态为从开始到此状态经历的边构成的字符串恰好符合正则表达式.
词法分析辅助工具--Lex
// 一个Lex中输入例子
%{
/* definitions of manifest constans
LT,LE,EQ,NE,GT,GE,
IF,THEN,ELSE,ID,NUMBER,RELOP */
}%
/* regular definitions */
delim [ \t\n]
ws {delim}+
letter [A-Za-z]
digit [0-9]
id {letter}({letter}|{digit})*
number {digit}+(\.{digit}+)?(E[+-]?{digit}+)?
%%
{ws} {/* no action and no return */}
if {return(IF);}
then {return(THEN);}
else {return(ELSE);}
{id} {yylval = (int)installID();return (ID);}
{number}{yylval = (int)installNum();return (NUMBER);}
"<" {yylval = LT;return (RELOP);}
"<=" {yylval = LE;return (RELOP);}
"=" {yylval = EQ;return (RELOP);}
"<>" {yylval = NE;return (RELOP);}
">" {yylval = GT;return (RELOP);}
">=" {yylval = GE;return (RELOP);}
%%
int installID(){**}
int installNum(){**}
不确定的有穷自动机
一个不确定的有穷自动机[NFA]由以下几个部分组成:
1.一个有穷的状态集合S.
2.一个输入符合集合∑,即输入字母表.
假设代表空串的ε不是∑中的元素.
3.一个转换函数,
它为每个状态和∑ U {ε}中的每个符号都给出了相应的后继状态的集合.
4.S中的一个状态s_{0}被指定为开始状态,或者说初始状态.
5.S的一个子集F被指定为接受状态或者说终止状态的集合.
a.从一个状态,通过一个符号表示的边,可以到达多个不同的下一状态.
b.边上的标号不仅可以是输入字母表中的符号,也可以是空符号串ε.
自动机中输入字符串的接受
一个NFA接受输入字符串x,
当且仅当对应的转换图中存在一条从开始状态到某个接受状态的路径,
使得该路径中各条边上的标号组成符号串x.
路径中的ε将被忽略.
确定的有穷自动机
确定的有穷自动机[DFA]是不确定有穷自动机的一个特例,其中:
a.从一个状态,通过一个符号表示的边,可以到达1个下一状态.
b.边上的标号是输入字母表中的符号,不可以是空符号串ε.
基于DFA的串的识别
输入:一个以文件结束符eof结尾的字符串x.
DFA D的开始状态为s_{0},接受状态集为F,
转换函数为move.
输出:如果D接受x,则回答"yes",否则回答"no".
方法:
s = s_{0};
c = nextChar();
while(c != eof)
{
s = move(s, c);
c = nextChar();
}
if(s在F中)
return "yes";
else
return "no";
从NFA到DFA的转换
由NFA构造DFA的子集构造算法
输入:一个NFA N
输出:一个接受同样语言的DFA D
方法:
// s表示N的单个状态
// T代表N的一个状态集
操作 描述
ε-closure(s) 能够从NFA的状态s开始只通过ε转换到达的NFA状态集合
ε-closure(T) 能够从T中某个NFA状态s开始
只通过ε转换到达的NFA状态集合.
U_{s->T}ε-closure(s)
move(T, a) 能够从T中某个状态s出发通过标号为a的转换
到达的NFA状态的集合
算法:
一开始,ε-closure(s_{0})是Dstates中的唯一状态,且它未加标记;
while(在Dstates中有一个未标记状态T)
{
给T加上标记;
for(每个输入符号a)
{
U = ε-closure(move(T, a));
if(U不在Dstates中)
将U加入到Dstates中,且不加标记;
Dtran[T, a] = U;
}
}
// 计算ε-closure(T)
将T的所有状态压入stack中;
将ε-closure(T)初始化为T;
while(stack非空) {
将栈顶元素t弹出栈中;
for(每个满足如下条件的u:从t出发有一个标号为ε的转换到达状态u)
if(u不在ε-closure(T)中){
将u加入到ε-closure(T)中;
将u压入栈中;
}
}
D的接受状态是所有至少包含了N的一个接受状态的状态集合.
NFA的模拟
模拟一个NFA的执行
输入:一个以文件结束符eof结尾的输入串x.
一个NFA N,其开始状态为s_{0},接受状态集为F,转换函数为move.
输出:如果N接受x则返回"yes",否则返回"no".
方法:
1.S = ε-closure(s_{0});
2.c = nextChar();
3.while(c != eof){
4. S = ε-closure(move(S, c));
5. c = nextChar();
6.}
7.if(S ∩ F != Φ) return "yes";
8.else return "no";
NFA模拟的一种高效实现
1.两个堆栈,其中每一个堆栈都存放了一个NFA状态集合.
其中的一个堆栈oldStates存放"当前状态集合".
另一个堆栈newStates存放了"下一个"状态集合.
2.一个以NFA状态为下标的布尔数组alreadyOn.
它指示出哪个状态已经在newStates中.
同时存在alreadyOn和newStates,因为alreadyOn[s]可以获得常量查询耗时.
3.一个二维数组move[s, a],它保存这个NFA的转换表.
这个表中的条目是状态的集合,用链表表示.
实现S = ε-closure(s_{0});
a.将alreadyOn数组中的所有条目都设置为FALSE
b.对ε-closure(s_{0})中的每个状态s,
将s压入newStates
设置alreadyOn[s]为TRUE
使用move[s, ε]递归地调用自身
// 加入一个不在newStates中的新状态s-->ε-closure(s_{0})
9.addState(s){
10. 将s压人栈newStates中;
11. alreadyOn[s] = TRUE;
12. for(t on move[s, ε])
13. if(!alreadyOn[t])
14. addState(t);
}
实现S = ε-closure(move(S, c));
1.找出状态集合move[s, c],
其中,c是下一个输入字符.
对于那些不在newStates栈中的状态,
应用函数addState.
addState还计算了一个状态的ε-closure值,
把其中的状态一起加入到newStates中[针对之前不在newStates中的状态]
// ε-closure(move(S, c));
16.for(oldStates上的每个s){
17. for(move[s, c]中的每个t)
18. if(!alreadyOn[t])
19. addState(t);
20. 将s弹出oldStates栈;
21.}
22.for(newStates中的每个s){
23. 将s弹出newStates栈;
24. 将s压入oldStates栈;
25. alreadyOn[s] = FALSE;
26.}
从正则表达式构造NFA
将任何正则表达式转变为接受相同语言的NFA,
这个算法是语法制导的,即它沿着正则表达式的语法分析树自底向上递归地进行处理
对每个子表达式,该算法构造一个只有一个接受状态的NFA.
输入:字母表∑上的一个正则表达式r.
输出:一个接受L(r)的NFA N.
方法:
1.对r进行语法分析,分解出组成它的子表达式.
构造一个NFA的规则分为基本规则和归纳规则.
基本规则处理不包含运算符的子表达式,
归纳规则根据一个给定表达式的直接子表达式的NFA构造出这个表达式的NFA.
基本规则:
1.对表达式ε,构造下面的NFA
--start--> State_S --ε--> State_E
2.对字母表∑中的子表达式a,构造下面的NFA
--start--> State_S --a--> State_E
归纳规则:
假设正则表达式s和t的NFA分别为N(s)和N(t).
1.假设r=s|t,r的NFA,基于s的NFA,t的NFA构造得到.图示略.
2.假设r=st, r的NFA,基于s的NFA,t的NFA构造得到.图示略.
3.假设r=t^{*},r的NFA,基于s的NFA,t的NFA构造得到.图示略.
4.r=(s),r的NFA,可直接用s的NFA得到,图示略.
基于NFA的模式匹配
模式 动作
a {模式p_{1}的动作A_{1}}
abb {模式p_{2}的动作A_{2}}
a^{*}b^{+} {模式p_{3}的动作A_{3}}
输入字符串:aaba....
对aaba...输入,
前缀aab是最长的使我们到达某个接受状态的前缀.
我们选择abb作为被识别的词素,执行A_{3}.
这个动作应包含一个返回语句,
向语法分析器指明已经找到了一个模式为p_{3}=a^{*}b^{+}的词法单元.
基于DFA的模式匹配
基于3.52得到的DFA.
6,8同时存在于状态集合时,状态集合按匹配长度一致下,模式在前优先原则,
认为匹配的是6对应的模式.
模拟此DFA的运行,直到在某一点上没后续状态为止.
回头查找进入过的状态序列,一旦找到接受状态,就执行状态对应的模式关联的动作
实现向前看运算符
Lex模式r_{1}/r_{2}中的Lex向前看运算符/是必不可少的.
在将模式r_{1}/r_{2}转化为NFA时,
把/看成ε.
如NFA发现输入缓冲区的一个前缀xy和这个正则表达式匹配时,
这个词素的末尾不在NFA进入接受状态的地方.
这个末尾是在此NFA进入满足如下条件的状态s的地方:
1.s在/上有一个ε转换
2.有一条从NFA的开始状态到状态s[标号序列为x]的路径
3.有一条从状态s到NFA的接受状态[标号序列为y]的路径
4.在所有满足条件1~3的xy中,x尽可能长.
基于DFA的模式匹配器的优化
NFA的重要状态
如一个NFA状态有一个标号非ε的离开转换,称这个状态是重要状态.
在子集构造法的应用过程中,
两个NFA状态集合可以被认为是一致的条件是它们:
1.具有相同的重要状态
2.要么都包含接受状态,要么都不包含接受状态.
根据抽象语法树计算得到的函数
要从一个正则表达式直接构造出DFA
1.构造它的抽象语法树
2.计算如下四个函数:nullable,firstpos,lastpos,followpos.
每个函数的定义都用到了一个特定增广正则表达式(r)#的抽象语法树.
a.nullable(n)对一个抽象语法树结点n为真,
当且仅当此结点代表的子表达式的语言中包含空串ε.
b.firstpos(n)定义了以结点n为根的子树中的位置集合.
这些位置对应于以n为根的子表达式的语言中某个串的第一个符号.
c.lastpos(n)定义了以节点n为根的子树中的位置集合.
这些位置对应于以n为根的子表达式的语言中某个串的最后一个符号.
d.followpos(p)定义了一个和位置p相关的,抽象语法树中的某些位置的集合.
一个位置q在followpos(p)中当且仅当存在L((r)#)中的某个串x=a_{1}...a_{n},
使得我们在解释为什么x属于L((r)#)时,
可以将x中的某个a_{i}和抽象语法树中的位置p匹配,
且将位置a_{i+1}和位置q匹配.
应用实例
n对应于(a|b)^{*}a的cat结点
nullable(n) = false
firstpos(n) = {1,2,3}
lastpos(n) = {3}
followpos(1) = {1,2,3}
计算nullable,firstpos及lastpos
结点n nullable(n) firstpos(n)
一个标号为ε的叶子结点 true Φ
一个位置为i的叶子结点 false {i}
一个or-结点n=c_{1}|c_{2} nullable(c_{1}) or nullable(c_{2}) firstpos(c_{1}) ∪ firstpos(c_{2})
一个cat-结点n=c_{1}c_{2} nullable(c_{1}) and nullable(c_{2}) if(nullable(c_{1}))
firstpos(c_{1}) ∪ firstpos(c_{2})
else
firstpos(c_{1})
一个star-结点n=c_{1}^{*} true firstpos(c_{1})
计算followpos
只有两种情况会使得一个正则表达式的某个位置会跟在另一个位置之后:
1.如果n是一个cat结点,且其左右子结点分别为c_{1},c_{2}.
则对于lastpos(c_{1})中的每个位置i,
firstpos(c_{2})中的所有位置都在followpos(i)中.
2.如n是star结点,且i是lastpos(n)中的一个位置,
则firstpos(n)中的所有位置都在followpos(i)中.
基于图3-59得到.
把3-60所示有向图转化为NFA.
1.将根结点的firstpos中的所有位置设为开始状态.
2.在每条从i到j的有向边上添加位置i上的符号作为标号.
3.把和结尾#相关的位置作为唯一的接受状态.
根据正则表达式构建DFA
输入:一个正则表达式r
输出:一个识别L(r)的DFA D.
方法:
1.根据扩展的正则表达式(r)#构造出一棵抽象语法树T
2.得到T的函数nullable, firstpos, lastpos和followpos.
3.用如下过程,构造出D的状态集Dstates和D的转换函数Dtran.
D的状态是T中的位置集合.
每个状态最初都是"未标记的",
开始考虑某个状态的离开转换时,变为"已标记的".
D的开始状态是firstpos(n_{0}),
结点n_{0}是T的根结点.
这个DFA接受状态集合是那些包含了和结束标记#对应的位置的状态.
// 从一个正则表达式直接构造一个DFA
初始化Dstates,使之只包含未标记的状态firstpos(n_{0})
其中n_{0}是(r)#的抽象语法树的根结点;
while(Dstates中存在未标记的状态S){
标记S;
for(每个输入符号a){
令∪为S中和a对应的所有位置p的followpos(p)的并集;
if(∪不在Dstates中)
将∪作为未标记的状态加入到Dstates中;
Dtran[S, a] = ∪;
}
}
最小化一个DFA的状态数
输入串如何区分各个状态.
如果分别从状态s和t出发,
沿着标号为x的路径到达的两个状态中只有一个是接受状态.
说串x区分状态s和t.
如存在某个能区分状态s和t的串,
则它们就是可区分的.
DFA状态最小化算法的工作原理是将一个DFA的状态集合分划成多个组,
每个组中的各个状态之间相互不可区分.
将每个组中的状态合并成状态最少DFA的一个状态.
算法执行中,维护了状态集合的一个分划,
分划中的每个组内的各个状态尚不能区分,
但来自不同组的任意两个状态是可区分的.
任意一个组都不能再被分解为更小的组时,就得到状态最少的DFA.
最小化一个DFA的状态数量:
输入:一个DFA D,其状态集合为S,输入字母表为∑,开始状态为s_{0},接受状态集为F
输出:一个DFA D',它和D接受相同的语言,且状态数最少.
方法:
1.构造包含两个组F和S-F的初始划分∏,
这两个组分别是D的接受状态组和非接受状态组
2.应用以下构造新的分划∏_{new}
最初,令∏_{new} = ∏;
for(∏中的每个组G){
将G分划为更小的组,使得两个状态s和t在同一小组中
当且仅当对于所有的输入符号a,状态s和t在a上的转换都到达∏中的同一组;
在∏_{new}中将G替换为对G进行分划得到的那些小组
}
3.如∏_{new} = ∏,令∏_{final} = ∏,执行4.
否则,用∏_{new}替换∏并重复步骤2;
4.在分划∏_{final}的每个组中选取一个状态作为该组的代表.
这些代表构成了状态最少DFA D'的状态.
D'的其他部分按如下步骤构建:
a.D'的开始状态是包含了D的开始状态的组的代表
b.D'的接受状态是那些包含了D的接受状态的组的代表
c.令s是∏_{final}中某个组G的代表,
并令DFA D中在输入a上离开s的转换到达状态t.
令r为t所在组H的代表.
则D'中存在一个从s到r在输入a上的转换
词法分析器的状态最小化
将识别某个特定词法单元的所有状态放到对应于此词法单元的一个组中.
把不识别任何词法单元的状态放到另一个组.