首先呢,是定义了FLEX的版本号
针对不同平台,不同编译器的问题进行处理
定义了在不同C标准下的各种整数类型以供后续使用。这其中为了防止类型被用户代码冲突,此处typedef了一些类型
而后定义了buffer的结构
#buffer
在action匹配前的工作
#input
虽然DFA在状态转移的过程中一次前进一个字符,但是为了提高IO效率,实际从文件读取的时候一般是批量往缓冲区读入的。如果有需要微调这个读入策略的需求,可以通过定义YY_INPUT
宏来实现。当然,在此之前还会依据上面buffer结构中的yy_is_interactive来判断输入源。
此处为从文件中读取
此处为从交互界面中读取
DFSA分析
#DFA状态转移表结构分析
首先给出我们的test.l。
1 | %% |
也就是空编译状态,我们选择flex test.l
进行默认的编译状态,会看到如下的状态表
1 | static yyconst flex_int16_t yy_accept[6] |
此处是经过压缩的表信息,默认选项中,flex会输出压缩后的状态转移表,因为完整版本的矩阵是Nx128大小(其中N是自动机的状态数,128则是字符集大小),如果不经压缩的话,会带来不必要的空间开销。
因此,为了输出完整版表,我们在调用flex的时候需要增加-Cf
参数。
1 | flex -Cf test.l |
就会得到如下的表,详情可以参考https://pastebin.com/VYqCwCh5
1 | static yyconst flex_int16_t yy_nxt[][128]; |
同时,使用 -vC
能够显示DFA的分析数据
这里使用的是空编译的文件,可以发现,初始状态下就存在了6个NFA的状态和4个DFA的状态。
此处不讨论NFA状态,因为flex会把规则转换为NFA,再转换为DFA,中间会有状态化简。
具体转化规则如下:
nfa状态机起始状态, 在flex中就是scset和scbol.
使用epsclosure函数找出所有nfa通过epsilon可到达的状态.
使用snstods把上述的nfa集合生成一个新的dfa状态或者返回相同的旧的dfa状态
扫描dfa状态集中的每一个的状态, 使用sympartition区分出dfa的转换字符
使用symfollowset收集dfa状态的相应输出字符的nfa状态集合
在使用epsclosure函数找出上述集合中所有nfa通过epsilon可到达的状态.
使用snstods把上述的nfa集合生成一个新的dfa状态或者返回相同的旧的dfa状态
使用dfa状态和输出字符集以及输出状态集生成表格.
base为相应dfa状态的字符集起始nxt为字符集转换表 chk为检查表 def为默认转换表, 即输入字符不在字符集转换表中时,dfa如何转.
重复步骤4.
这里的四个基础DFA状态分别是 开始状态(start) , 接收状态(accept) , 错误状态(error) , 停机状态。
前三个状态都好理解,而第四个停机状态的含义是,一旦状态机到达这个状态就“死”了,它再也不能离开这个状态。
举个例子
用flex -vC test.l
编译后有8个DFA,也就基础四个加上a(b|c)d*e+
规则中的4个状态,产生了如下图示
以及如下选择表
在状态1的时候,如果遇到(b|c)以外的字符,就全部跳到停机状态。这意味着在状态1,接收这些未定义的字符,会导致DFA死掉。
#DFA工作流程分析
接下来就是scanner的主入口
1 | YY_DECL |
其DFA的伪代码可以抽象成
1 | state= 0; |
总结一下,也就是从start状态,通过匹配每个读入的字符结合转移表来跳转状态,然后在无法转移或者字符已经读完的时候,判断一下是否停在了接收状态,然后进行对应的用户定义的代码规则
状态转移矩阵的压缩
上文中已经提到,如果不加-Cf
参数,flex会生成压缩版本的状态转移矩阵。
1 | yyconst flex_int16_t yy_accept[10] |
此时的状态转移循环如下所示:
1 | yy_current_state = (yy_start); |
总结
flex最核心的就是yylex()函数,自动从输入文件读入数据,进行匹配,并返回对应token。不断的循环调用yylex()来匹配输入流来实现规则->行为
的模式。通过压缩表信息来达到优化的效果。
reference
http://www.cs.man.ac.uk/~pjj/cs211/ho/node6.html
https://www.cnblogs.com/ninputer/archive/2011/06/12/2078671.html