编译原理:词法分析

    在词法分析的过程中还有一个过称是不能少的,就是在分析的时候一次读入多少代码。编译一个程序的时候,往往需要进行大量的字符串读入。前人做了比较多的优化,其中一项就是采用来个交替读入的缓冲区。每个缓冲区大概能有4096的字节,读一句话是足够的。读入程序中维护了两个指针:分别是lexemeBegin 指针,顾名思义,就是当前词素的开始处。以及forward 指针,就是试图判断词素的结尾是什么。这个很复杂我们会在接下来的章节中详细介绍。可以想象,一旦确定了当前词素的位置,那我们就把forward的位置+1之后赋值给lexemeBegin,然后继续上述的过程。但是简单地做上面的工作会有一个小小的问题,就是如果恰好一个词素被分开了怎么办,这就涉及到了哨兵标记。

    当我们移动forward指针的时候,实际上同时做了两件事情,第一件事情是判断是否已经能够完成词素的匹配。并且要同时检查我们是否到了缓冲区的结尾(如果到了结尾自然要选择是不是要重新装载缓冲区,是不是要大幅度移动forward指针),这个问题被 eof 很好的解决了。

一、词法分析与正则表达式

    程序语言可以认为是一个字符集,字符串是一个有限的符号集,符号本身是有限的字母。当使用正则表达式的符号表示法时,每个正则表达式就代表一个字符串集。一个词法分析器的输入是源代码,本质上可以看做是一个字符串。而它的输出是“记号”流。一个“记号”由一个模式来描述。凡是匹配该模式的字符串都被标识成相同的记号。而记号的模式通常是一个正则表达式,或者说正则文法。也就是说正则表达式是用来描述词素的。是一种用来描述词素模式的重要方法。

    单词的本质是字符串,在词法分析中为了用正则表达式描述单词,使用语义函数为正则表达式和字符串集合建立一种映射关系,使得正则表达式的语义解释被描述成字符串的形式。

    (1) (r) | (s) 是一个正则表达式,其所对应的语言为 L(r)∪L(s)

    (2) (r) (s) 是一个正则表达式,其所对应的语言为 L(r)L(s)

    (3) (r)* 是一个正则表达式,其所对应的语言为 (L(r))∗

    (4) (r) 是一个正则表达式,只是说明在表达式左右加个括号是没有影响的。

    词法分析生成器中常使用的规则:

    最长匹配:选择可以匹配正则表达式、最长的初始输入字符串作为下一个记号。

    优先规则:对于特定的初始字符串,最先与之匹配的正则表达式决定了这个字串的记号类型。

    这时先介绍一下关于正则表达式的数学原理:首先是因为集合论的出现导致世界可以由集合表示,同时集合是多种多样的,所以正则表达式之中有几种重要的集合:符号,逻辑预算法(| . )其中.可以被省略,空(e),重复(*)或者说幂或者说克林。有了这些概念之后,在来看一下正则集合(0|1)*.0,首先*得含义表示的是括号呢内部的多次出现,而.的含义是指连接,指多个字符的连接,|表示两个钟出现一个就可以,所以他的含义是前面的数字是前面是多个0或者1组成的字符串,最后以0结尾的,表示含义为 以2的倍数组成的二进制数。在举一个例子b*(abb*)*(a|e),可以发现a始终没有连续的出现,因为指的是a和b组成。但是如果正则表达式每次都要使用各种符号来描述,那未免太过繁琐,于是出现更简洁的描述方式用[abcd]描述[a|b|c|d],甚至当字母连续时可以这样[a-d]进行表示,当同时匹配几段不同的字母时也可以[a-d1-9],m+可以表示m乘以m*。

二、词法分析与有限自动机

    由于正则表达式对于确定词法记号很方便,但还是需要一个将其实现的计算机程序方法。所以需要引入有限自动机,从正则表达式到有限自动机有两种方法自顶向下逐步分解法和自下而上组合方法,所以正则表达式就是建立在自动机的理论基础上的:用户写完正则表达式之后,正则引擎会按照这个表达式构建相应的自动机(可能是NFA,也可能是DFA,但它们必定是等价的),若输入一串文本之后,自动机抵达了接受状态,则这串文本可以“匹配”用户指定的正则表达式。有穷状态自动机按照转移函数的不同,也就是有穷自动机容许转移函数不确定,分成了“确定有穷状态自动机”(DFA)和“非确定有穷自动机”(NFA)。NFA是基于表达式的,而DFA是基于文本的。两类引擎要顺利工作,都必须有一个正则式和一个文本串。DFA通过文本串去比较正则式,看到一个子正则式就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是通过正则式去比文本,读入一个字符就把它跟正则式比较,匹配就记下来:“某时间在某处匹配上了”,然后接着往下寻找。一旦一直不匹配,就把刚吃的这个字符退出出来,一个个的退出,直到回到上一次匹配的地方。

    DFA与NFA机制上的不同带来5个影响:

    1. DFA对于文本串里的每一个字符只需扫描一次,比较快,但特性较少;NFA要翻来覆去吃字符、吐字符,速度慢,但是特性丰富,所以反而应用广泛,当今主要的正则表达式引擎都是NFA的。

    2. 只有NFA才支持lazy和backreference等特性;

    3. NFA急于邀功请赏,所以最左子正则式优先匹配成功,因此偶尔会错过最佳匹配结果;DFA则是“最长的左子正则式优先匹配成功”。

    4. NFA缺省采用greedy量词

    5. NFA可能会陷入递归调用的陷阱而表现得性能极差。

    有限自动机包含一个有限的状态集,状态由箭头连接,称为边。从一个状态指向到另外一个状态,每个边都有一个标记,其中包含一个初始状态和若干个终结状态。词法记号的有限自动机,圆圈表示状态,双圆圈表示终结状态,开始状态是一个无输入的边。在不能确定是否正确读人一个单词的时候,只能将一个字母作为分析目标并根据下一个字母来判断是否应该判定该字母所属类型,所以通过有限自动机来判断字母所属类型。在确定的有限自动机中,不存在两个从同一个状态下出发且标记完全相同的边。

    确定的有限自动机的工作过程:从初始状态出发,对于每个输入字符串中的字符,自动机都沿着相应的边到达另一状态,边上必须标有输入字符,对于有n个字符的字符串,若在n次状态转换之后,自动机到达了终结状态,那么自动机就接受了字符串,若未达到终结状态,或者找不到与输入字符匹配的边,自动机就拒绝接受这个字符串。每个终结状态都要标明有限自动机所接收的记号类型。同时需要遵守最长匹配原则以及优先规则,因为词法分析器的工作是找到原始的合法子字符串的最长匹配,在解释转换过程中词法分析器一直追踪最长匹配的路径和匹配的位置,追踪最长匹配路径就是用两个变量保存自动机上一次的结束状态(Last-Final以及Input-Position-at-Last-Final)每次到达终结状态时都需要更新这两个变量,当到达停滞状态时这些变量会记录相匹配的记号和结束的位置。

    可以将自动机表示为一个转换矩阵,一个二维数组下标表示状态号和输入字符,还有一个停滞状态0,其任何输入字符都指回自身状态,用该状态来代替不存在的边,还有一个结束数组,其作用是将状态号与动作一一对应。

    非确定有限自动机则存在多条从一个状态出发并且标记相同符号的边,也可能存在标有空字符串的边即在不接收输入字符时可进行状态转换。从初始状态开始自动机就必须选择一个转换方向,若存在可以接收的字符串的选择路径,自动机就应该接受该字符串。NFA是一个很有用的概念,它会将一个正则表达式转换成一个仿真的NFA,可以将任何一个正则表达式转换为含有一个尾和一个头的NFA。

    用计算机程序实现DFA是比较容易的但实现NFA就不容易了,因为计算机不能进行很好的猜测。将NFA转换为DFA时,每个NFA的状态集都对应着DFA的一个状态。DFA构造完成之后状态数组可能被丢弃,而转换数组则用于词法分析。对于一个已经形成的NFA,我们需要定义新的状态来形成DFA,从NFA转化到DFA的重点是重新组合NFA状态。解决以下的两种情形,就能将NFA转为DFA:

    (1)边上的ε:ε闭包:遇到这种情况,后面的三个状态完全可以合并,如果将后面的三个状态合为一个,那这个转换图里就没有ε边,也就满足了DFA的条件。 对应的解决方法是找出ε闭包,也就是先找出该状态的ε边推出的所有状态,再找那些状态的ε边推出的状态,是一个迭代的过程,直到找出一个状态的ε闭包。

   (2)不确定的后续状态:子集构造法:遇到这种情况,如果能将状态2和状态3合并到一起,就不会出现“面对相同的输入状态可能跳转到多种状态的情形了”,合并状态2和状态3这样类似的状态的方法叫做子集构造法。

猜你喜欢

转载自blog.csdn.net/ZytheMoon/article/details/86746633