现在研究2.2)根据正则表达式规则rule生成NFA(非确定有穷自动机)。
从spec 文件(rule)区域读取/解析输入的函数为CLexGen.userRules(), 该函数中其实包含了
2.2)-2.6)的多步,为突出研究各个生成步骤,分解为几个小步骤分别看。实际userRules()
函数中解析(parse)正则表达式和生成NFA是在一起的,函数名为CMakeNfa.thompson()。
函数CMakeNfa.thompson(),顾名思义,使用的是thompson构造法来从regex中构造出NFA,
参见龙书算法3.3。
thompson()函数中调用machine()函数来解析rule,生成NFA,machine()是一个递归向下
正则表达式的解析器的实现,我们以生成式的方式写出其对应的生成式如下:
(1) machine -> rule while(rule) 一部NFA状态机由多条rule构成,while(rule)表示
这是使用尾递归方式实现的rule*。一个spec可以没有任何规则。
(2) rule -> state expr accept 一条rule前面是lex state,后面是accept action。
实际上 state部分在machine产生式(函数)中处理的,写在这里是方便看。
(3) expr -> cat_expr while(cat_expr) 一个expr是多个cat_expr OR 或构成的。
例如 a|b, 则cat_expr1=a, cat_expr2=b
(4) cat_expr -> factor while(factor) 一个cat_expr是多个factor CONCAT连接构成的。
例如 ab, 则factor1=a, factor2=b
(5) factor -> term[*+?] 一个factor是一个term加上可选的Kleene算符*构成,+?是*
的变化形式。
(6) term -> normal_char 任意普通字符是一个term
| '.' '.' 符号匹配任意字符
| '[' char_class ']' 字符类匹配该类的任意字符
| '(' expr ')' 括号里面的expr是一个独立term,括号常用来改变算符优先级
以上产生式的左侧名字也即CMakeNfa类的函数名,通过这组递归向下正则表达式解析
处理,正则表达式被按照thompson算法构造为一个内部的树结构。下面用龙书上的例子
来举例,生成正则表达式(a|b)*abb的内部表示。
NFA 的一个状态在JLex表示一个CNfa类的一个实例(instance),CNfa类定义如下:
class CNfa
m_edge -- 如果>=0表示是输入的值;也即当前状态在此输入下将转移(下述)
=CNfa.CCL=-1 表示是一组输入,使用m_set保存该组输入(如[0-9])
=CNfa.EMPTY=-2 表示此状态没有转移(无接受任何输入)
=CNfa.EPSILON(ε)=-3 表示是空串ε输入。只有此类型才可能有两个转移目标状态
m_set -- 如果输入边的类型为字符类(m_edge==CNfa.CCL), CCL=Character CLass时,使用
m_set保存是哪些输入,如[0-9]
m_next -- 输入之后的转移状态。EMPTY时为null
m_next2 -- 输入为 ε 时才可能有,如果没有则为 null.
m_accept -- 如果是终态,则保存用户给出的action代码。
m_anchor -- 和$^匹配行首行尾有关的标志。
m_label -- 此状态的编号。
重申一下,每个CNfa的instance都表示一个NFA的状态,以及从该状态出发的所有边。
在CNfa中只有m_next, m_next2最多2个边,相当于用2叉树来表示更复杂的树,下面我们会看到。
从和上面所述产生式相反的顺序我们研究正则表达式是如何构造为NFA(CNfa的相互连接)的:
term() 函数我们可以认为其返回值为CNfaPair{start, end}对,其中start表示开始节点,end表示
结束节点,参见龙书中途
(6.1) term -> normal_char
略去语法分析部分,一旦找到一个普通字符,例如'a',构造的NFA如下图示例:
解释一下该图,标号为1的CNfa 节点为start,2为end,start实例的m_edge='a'表示输入为字符'a',
在图上标记在边上,边上的箭头表示从1到2转移,start.m_next=end。start.m_next我们画在
上面,如果有m_next2我们画在下面。end没有任何出边,实际上其m_edge=EMPTY。
此图表示正则表达式a的状态NFA。
(6.2) term -> . 任意字符。
图类似于上面,只是start.m_edge=CCL(Character CLass),m_set为所有字符除了'\r\n'。
(6.3) term -> [ char_class ] 类似于 (6.2) m_set 为char_class包含的所有字符。
(6.4) term -> '(' expr ')' 根据 thompson算法,直接返回expr表示的NFA即可。
(5) factor -> term[*+?] 实现在函数 CMakeNfa.factor()
term返回{start, end} CNfa节点对,对于:
(5.1) term* 构造为相似于龙书图 3-42 的NFA。通过ε边实现*算符。如下图示:
(5.2) term+ 构造类似于*,少一条从i到f的ε转移边。表示1次到任意次。
(5.3) term? 构造类似于*,少内部term的end回到start的ε转移边。表示0-1次。
(5.4) term后面无算符,则返回原term pair{start,end}即可。
(5.5) 这里没有实现term{m,n}这样的正则语法扩展。
在*算符的实现中,start节点m_edge就是EPSILON(ε),并且有两条出的边。
(4) cat_expr -> factor while(factor)
两个子factor连接起来,设两个factor 为first, second,则将first.end和second.start合并即可。
参见书上的图。
(3) expr -> cat_expr while(cat_expr)
两个cat_expr以OR(|)算符连接,创建两个新的start,end,通过分支将两者并联在一起,构成或
关系,参见书上的图。(这种新的start有两个ε边)。
(2) rule -> state expr accept
将state,accept信息合并到expr的NFA中,accept作为终态的CNfa.m_accept.
state部分我研究的少,未曾详细看。
(1) machine -> rule while(rule)
一个NFA machine由多条rule 构成,通过CNfa.m_next2字段构成树结构。
生成的NFA状态转换图,画出来和龙书上图3-57很类似。不同之处在于Jlex为其伪输入BOL,EOF
生成了一个空的NFA分支,以“吃掉”未处理的BOL,EOF的输入。
以下研究 2.3)简化2.2)中生成的NFA中的字符输入为字符类。