您的位置:首页 > 编程语言

解读FLEX代码之生成NFA

2008-12-04 20:04 260 查看

1. 生成NFA

1.1. 基本数据结构

【state context】Lex源文件中用%x或%s定义的状态正文,一个state context包含多条规则。默认的state context为INITIAL,如果一个规则没有指定state context,则属于INITIAL。%s表示共享,如果一个规则没有指定state context,则会加入每个共享的state context。%x表示互斥,没有指定state context的规则不会加入互斥的state context。每个state context包含两个状态机,一个对应非顶行的规则集,另一个对应顶行的规则集。
【rule】Lex中的每个正则表达式,每个规则对应一个NFA状态机。
【machine】NFA的状态机,flex中没有明显的状态机对象,状态机隐含在状态的数据结构中。state context的状态机有一个起始状态,多个接受状态。rule的状态机有一个起始状态,一个接受状态。正在构造中的状态机,有一个头状态和一个尾状态,没有起始状态和接受状态。
【state】NAF状态机中的节点,属性如下:
firstst:该state所属machine的参数,该状态机的头状态,也是编号最小的状态。
finalst:该state所属machine的参数,该状态机中编号最大的状态。
lastst:该state所属machine的参数,该状态机的尾状态。
transchar:从该状态出来的边上的字符,负数表示一个字符集合,正数表示一个字符,零表示ε。
trans1:从该状态转移到的状态编号。
trans2:从该状态转移到的状态编号,只有transchar为零时才有意义。
accptnum:非接受状态该值为零,普通接受状态该值为rule的编号,向前看接受状态该值为rule的编号加YY_TRAILING_HEAD_MASK。
assoc_rule:该状态所属rule的编号。
state_type:状态类型,区分普通状态和向前看状态。

正在构造的状态机的特点:永远只有一个头状态、一个尾状态;状态机的头状态是编号最小的状态,状态机的编号是连续的,不跳过一个编号。
状态的特点:transchar不为零时有且只有一条出边;transchar为零时,可以有一条、两条、或没有出边,接受状态没有出边。

1.2. 最终的状态机




起始状态
接受状态
复杂的状态变迁
一个rule的NFA
rule1
rule2为空
rule3
多个rule的NFA,红线为ε转移
空rule的NFA
为处理方便增加的没用的状态

flex在语法分析过程中生成NFA。每扫描一个字符生成一个NFA状态,遇到规则尾部时生成接受状态,扫描完规则后就已经生成对应的状态机了。
规则的NFA:一个起始状态,一个接受状态,中间是复杂的状态变迁。空规则不识别任何字符,通常不会出现空规则,空规则只有一个接受状态,起始状态和接受状态重合。
规则集的NFA:flex增加一些没有用的状态,将多个rule的NFA组合成二叉树结构,或者称之为链表结构。
起始状态的编号能唯一标识一个NFA。

处理词法源文件的过程如下:
1. 为INITIAL state context生成两个NFA,每个NFA包含一个非接受状态,这个状态是没有用的,仅仅为了处理方便。第一个NFA对应非顶行的规则集,第二个NFA对应顶行的规则集,顶行就是^打头的规则。
2. 识别%s和%x定义的所有state context,为每个state context生成两个NFA,同INITIAL。
3. 逐条识别规则。
a) 识别该规则属于哪个state context,每写明属于哪个state context的为公有规则,公有规则属于所有共享state context。
b) 为该规则生成一个NFA,用起始状态的编号标识该NFA。
c) 如果是非顶行规则,则加入所属state context的第一个NFA;如果是顶行规则,则加入所属state context的第二个NFA。
4. 扫描完所有规则后,生成一条缺省规则,将加入所有state context的第一个NFA。该规则接收任意字符,将其输出到屏幕上,用来处理不能识别的字符。flex的运行参数-s用于禁止生成该缺省规则,此时遇到不能识别的字符后会退出程序。
5. 将每个state context的第一个NFA加到第二个NFA上,结果做为第二个NFA。这个过程不是在扫描结束时做的,而是在讲NFA转换成DFA时做的。查看flex生成的词法分析程序,可知,在输入流行首时使用第二个NFA,不在行首时使用第一个NFA,准确地讲使用的是根据NFA生成的DFA。这段代码为yy_current_state = yy_start; yy_current_state += YY_AT_BOL()。

1.3. 临时的状态机

头状态
尾状态
临时状态机
出边
a
单个字符a生成的临时状态机
r的NFA
s的NFA
r·s运算
r的NFA
s的NFA
r | s运算
r的NFA
r?运算
r的NFA
r*运算
r的NFA
r+运算
说明:红线表示ε转移
r的NFA
r的NFA
出边不为ε时增加一个状态做为接收状态
出边为ε时将最后一个状态设为接收状态


扫描规则的过程中会生成临时状态机。每扫描一个字符会生成只有一个状态的状态机,遇到 * | ? + 等符号时对状态机做运算。
临时状态机的模型如上图所示,一个头状态,一个尾状态,一条出边。出边的transchar属性是有值的,但没有填转移到哪个状态;有两种类型的出边,transchar为ε或具体的字符。
临时状态机的运算有:r·s、r | s、r*、r+、r?、增加接收状态。上图描述了这些操作的结果。
r{2,4}、r{2,}、r{4}这种记号表示将r重复多次,例如,a{2,4}表示两个、或三个、或四个连续的字符a。flex将r{2,4}拆成r·r·r?·r?,将r{2,}拆成r·r·r*。此时需要将r的状态机复制很多份。复制过程比想像的要简单很多,用到临时状态机的特点:临时状态机中的状态编号是连续的,每个状态的trans1、trans2字段永远指向该状态机内部的某个状态,不会指向状态机外部的状态。

1.4. 向前看问题

向前看规则的形式为r/s。flex期望r或s能接收定长的字符串,例如a*/bcd或abc/d*,这样处理起来很简单,生成的词法分析器也很简单。如果r和s接收的字符串都不是定长的,例如a*/b*,处理起来很复杂。
对于规则a*/bcd,生成的状态机同abcd,只是在识别出该模式后,先回退三个字符,再执行自定义动作。
对于规则abc/d*,生成的状态机同abcd,只是在识别出该模式后,回退足够多的字符知道剩下三个字符,再执行自定义动作。
以a*/b*c为例,描述一下处理方法。

3
4
5
6
7
8
9
10
11
12
13
14
15
16
a
b
c
all
规则1
规则1
NFA:红线表示ε转移,灰色为接收状态,虚线为标记为TRAILING的状态
规则2,容错
1
6
2
3
4
5
7
8
other
DFA
蓝线表示字符a
红线表示字符b
黑线表示字符c


DFA中,各状态接收的规则如下
state 1:规则1(trailing_head)
state 2:规则1(trailing_head),规则2
state 3:规则2
state 4:规则1(trailing),规则2
state 5:规则2
state 6:规则1(trailing_head)
state 7:不是接收状态
state 8:规则1(trailing)

生成的词法分析器在扫描输入串时,将遇到的每个DAF状态存在栈中,直到不能匹配更长的字符串。
然后遍历状态栈中每个状态s,遍历状态s能接受的每个规则r。如果最先看到的是普通的规则x,即没有标记trailing_head或trailing,按规则x处理。如果最先看到的规则y标记了trailing,则继续找标记为trailing_head的规则y,按规则y处理。如果最先看到的规则标记了trailing_head,则跳过它,当它不存在。

再来看生成的代码。
系统有很多状态,每个状态能够接收零个或多个规则,系统将其压缩在数组yy_accept和yy_acclist中,yy_acclist[yy_accept[s]]到yy_acclist[yy_accept[s+1]-1]存储状态s能接受的规则。了每个状态能够接受的规则的编号,有些加了YY_TRAILING_MASK标记,有些加了YY_TRAILING_HEAD_MASK标记。
yy_state_buf存储了扫描输入串时遇到的DFA状态,yy_state_ptr指向栈顶。
find_rule标号后面的代码,就是遍历状态栈yy_state_buf中的每个状态s,遍历s能接受的每个规则r。

总之,为了处理向前看问题,在NFA的接受状态上加上特殊的标记,转换为DFA后,DFA的状态也带有特殊标记。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: