第二章 词法分析

词法分析是编译的第一个阶段,它的主要任务是扫描输入字符流,产生用于语法分析的词法记号序列。

2.1 词法记号

词法记号(tokentokentoken): 是由记号名(又称种别码)和属性值构成的二元组,属性值不是必须项。记号名是语法分析的输入符号。

关于tokentokentoken的介绍见“第一章 概述”。

模式: 一个记号的模式描述属于该记号的词法单元的形式。

在一个关键字作为一个记号的情况下,它的模式就是构成该关键字的字符序列。对于标识符和其他的一些记号,它们的模式有更为复杂的结构并且有很多字符串可以匹配它们。

简单来说,就是一种格式,这种格式规定了一种或多种字符序列,比如在一个关键字作为一个记号的情况下,它的模式只规定了一种序列,就是构成该关键字的字符序列;又比如标识符的模式规定了多种更复杂的字符序列,有很多匹配的字符串,abcA123都是合法的标识符,即满足标识符的模式规定),在C语言中标识符必须是以字母或下划线开头的字符串,这样的规则就是一种模式。

词法单元: 又称单词,是源程序中匹配一个记号模式的字符序列,它由词法分析器识别为该记号的一个实例。

能够识别为一个记号的字符串就是一个词法单元。

举例:printf("Tital = %d\n", score); 中,printfscore 是匹配 id 模式的词法单元,"Total = %d\n" 是匹配 literal 模式的词法单词。其中 idliteral 为不同的记号名,对应模式的非形式描述分别为“由字母开头的字母数字串”和“引号之间任意不含本身的字符串”


※ 对于记号 relation\textbf{relation}relation (>>>、<<<、===、>=>=>=、…)而言,从程序的语法是否正确的角度看,使用哪一个关系算符都一样。但是从翻译成目标代码的角度来考虑,不同的关系算符,其翻译结果不一样。因此词法分析器需要给记号以属性,用属性来记住记号的附加信息,以便需要时使用它们。概括地说,记号名影响语法分析的决策,属性影响记号的翻译。

2.2 正规式(正则表达式)

字符串集合由称为模式的规则来描述,而正规式是表示这些规则的一种重要方法。

2.2.1 术语解释

字母表: 表示符号的有限集合。符号可以是字母、数字、标点符号。

集合{0,1}\{0,1\}{0,1}是二进制字母表。

串: 字母表中的字符组成的一个有穷序列。

011101110111是一个在二进制字母上的串。空串是长度为0的特殊串,用 ε\varepsilonε 表示。

语言: 表示字母表上的一个串集。

{0111,1000,1010,0101}\{0111, 1000, 1010,0101\}{0111,1000,1010,0101}就是在二进制字母表上的一种语言。

语言的运算:

  • 并:L∪M={s∣s∈L或  s∈M}L∪M=\{s|s∈L\space\space或\space\space s∈M\}L∪M={s∣s∈L  或  s∈M}
  • 连接:LM={st∣s∈L且  t∈M}LM=\{st|s∈L\space\space 且 \space\space t∈M\}LM={st∣s∈L  且  t∈M}
  • 幂:L0={ε},Li=Li−1LL^0=\{\varepsilon\},\space\space L^i=L^{i-1}LL0={ε},  Li=Li−1L
  • 闭包:L∗=⋃i=0∞LiL^*=\bigcup_{i=0}^∞L^iL∗=⋃i=0∞​Li,L∗L^*L∗ 表示零个或多个 LLL 连接的并集
  • 正闭包:L+=⋃i=1∞LiL^+=\bigcup_{i=1}^∞L^iL+=⋃i=1∞​Li,L+L^+L+ 表示一个或多个 LLL 连接的并集

**句子(字):**属于某个语言的串称为该语言的句子或字。

对于上面的语言,011101110111就是该语言的句子。

注意:区别于文法中的“句子”。文法中的“句子”是一种特殊的“句型”,“句型”是由终结符和非终结符构成字符序列,而“句子”是仅由终结符构成的字符序列。

2.2.2 正规式运算

正规式(又称正规表达式、正则表达式)是语言的一种形式化表达。

在引入正规式之前,我们是通过列举语言中的串的方式来描述语言,但是这样能描述的串非常有限。正规式可以用于描述某些由无限多的串构成的语言。

正规式表示的语言叫做正规语言或正规集。

正规式的运算:

  1. 闭包运算(算符是∗^*∗):有最高优先级并且是左结合的运算。正规式a∗a^*a∗表示仅由字母aaa构成的所有串的集合,包括空串。
  2. 连接运算(两个正规式并列):优先级次之且也是左结合的运算。正规式ababab表示集合{ab}\{ab\}{ab}。
  3. 选择运算(算符是 ∣|∣):优先级最低且仍然是左结合的运算。正规式a∣ba\space|\space ba ∣ b表示集合{a,b}\{a,b\}{a,b}。

2.2.3 正规定义

可以对正规式命名,并用这些名字来引用相应的正规式,以求得表示上的简洁。这些名字也可以像符号一样,出现在正规式中。

如果Σ\SigmaΣ是基本符号的字母表,那么正规定义是形式为
d1→r1d2→r2...dn→rnd_1\rightarrow r_1 \\ d_2\rightarrow r_2 \\ ...\\ d_n\rightarrow r_n \\ d1​→r1​d2​→r2​...dn​→rn​
的定义序列,各个did_idi​的名字都不同,每个rir_iri​都是Σ∪{d1,...,di−1}\Sigma\space∪\space\{d_1,...,d_{i-1}\}Σ ∪ {d1​,...,di−1​}上的正规式。

例如:C语言的标识符是由字母、数字和下划线组成的串,下面是C语言标识符的正规定义。
letter_→A∣B∣...∣Z∣a∣b∣...∣z∣_digit→0∣1∣...∣9id→letter_(litter_∣digit)∗\textbf{letter\_}\rightarrow A\space|\space B\space|\space ...\space | \space Z \space | \space a \space |\space b\space | \space ...\space | \space z\space | \space \_\\ \textbf{digit}\rightarrow 0\space |\space 1 \space|\space ...\space |\space 9 \\ \textbf{id}\rightarrow \textbf{letter}\_(\textbf{litter}\_\space|\space\textbf{digit})^*\\ letter_→A ∣ B ∣ ... ∣ Z ∣ a ∣ b ∣ ... ∣ z ∣ _digit→0 ∣ 1 ∣ ... ∣ 9id→letter_(litter_ ∣ digit)∗

注意区别正规定义和文法:

正规定义要求,后出现的名字允许使用先出现的名字,但反之不允许;而正则文法中的名字(非终结符)没有这样的要求。

正则文法本质上与正规式(正则表达式)是等价的,表达单词的能力相同,二者都可以描述同一个语言,且可以相互转化。正规式比正则文法更加直观,有时首选正则表达式来表示正则语言。

2.2.4 状态转换图

状态转换图描述词法分析器呗语法分析器调用时,词法分析器为返回下一个记号所做的动作。

上图是记号 relop\textbf{relop}relop 的转换图,可以用这张图来解释有关转换图的概念。转换图上圆圈表示状态,状态由有向边连接,边上有指示输入字符的标记,标记通常是一个字符。若离开状态 sss 的某个边上有标记 other\textbf{other}other,则它表示离开 sss 的其他边所指示的字符以外的任意字符。

在状态转换图中,有一个状态标记为开始状态,这是转换图的初始状态。当开始识别记号时,控制进入开始状态。当控制进入某个状态时,读输入串的下一个字符,如果离开这个状态的一条边上的标记和该输入字符匹配,控制就进入由这条边指向的状态,否则识别过程失败。某些状态有两个圆圈,表示它们是接受状态,控制进入这样的状态表明识别了一个记号。接受状态可以有动作,控制到达接受状态时执行它的动作。

对于上图的状态转换图,如果输入串是 <=…<=…<=…,那么控制从开始状态0到达接受状态2,读出词法单元 <=<=<=,执行动作 return(relop, LE)

补充:

注意,如果到达接受状态4,意味着 <<< 和另一个字符已被读过,由于这第二个字符不是关系算符 <<< 的一部分,因此必须把输入串上指示下一个字符的指针回退一个字符。用 ∗*∗ 表示输入指针必须回退的状态。

后续讲到的有限自动机都不会涉及other\textbf{other}other、“补充”部分的内容和到达接受状态执行语句的情况。

2.3 有限自动机

注意:下面讲到的转换图可能与上述转换图存在一定的不同,上述转换图的介绍是为了起到内容过渡的作用,方便理解后续的有限自动机。

语言的识别器是一个程序,它取串 xxx 作为输入,当 xxx 是语言的句子时,它回答“是”,否则回答“不是”。可以通过构造称为有限自动机的更一般的转换图,把正规式翻译成识别器。

可见:

  1. 识别器本质是程序;
  2. 有限自动机本质是转换图/表(描述状态转换)。

有限自动机分成确定的和不确定的两种情况。“不确定”的含义是,存在某个状态,对于某个输入符号,它存在不止一种转换。

确定的和不确定的有限自动机都正好能识别正规集,也就是它们能识别的语言正好是正规式所能表达的语言。但是,它们之间存在着时空权衡问题:从确定的有限自动机得到识别器,比从等价的不确定的有限自动机得到识别器要快得多;但是,确定的有限自动机可能比等价的不确定有限自动机占用更多的空间。

2.3.1 不确定的有限自动机(NFA)

转换图表示

NFA可以用带标记的有向图表示,即状态转换图,结点表示状态,有标记的边代表转换函数。这种转换图和上一节所讲的略有区别,在这里,可以把同样的符号标记在出自同一个状态的多条边上。另外,边可以由输入符号(输入符号的集合为字母表 Σ\SigmaΣ)标记,也可以由特殊符号 ε\varepsilonε 标记。

可以识别语言 (a∣b)∗ab(a\space|\space b)^*ab(a ∣ b)∗ab 的 NFA 的转换图如下图所示。这个NFA的状态集合是{0,1,2}\{0,1,2\}{0,1,2},输入符号表{a,b}\{a,b\}{a,b},状态0是开始状态,接受状态2用双圈表示。

转换表表示

在NFA对应的转换表中,每个状态一行,每个**输入符号和 ε\varepsilonε (如果需要的话)**各占一列,表的第 iii 行中对应某个符号 aaa 的条目是一个状态集合,表示NFA在输入是 aaa 时,状态 iii 所能到达的状态集合。下图是语言 (a∣b)∗ab(a\space|\space b)^*ab(a ∣ b)∗ab 对应的转换表。

转换表的优点是可以快速访问给定状态和字符的状态集。它的缺点是,当输入字母表较大,并且大多数转换是空集时,占用了大量空间。

NFA接受输入串 xxx ,当且仅当转换图中存在从开始状态到某个接受状态的路径,该路径各边上的标记可拼成 xxx。

2.3.2 确定的有限自动机(DFA)

确定的有限自动机是不确定的有限自动机的特殊情况。在NFA的基础上,确定的有限自动机还要求:

  1. 任何状态下都没有 ε\varepsilonε 转换,即任何状态必须进行输入符号的匹配才能进入下一个状态;(没有空转换的不一定是DFA)
  2. 对任何状态 sss 和任何输入符号 aaa,最多只有一条标记为 aaa 的边离开 sss,即从任何状态出发,对于任何输入符号,最多只有一个转换。体现在转换表中,每个条目最多只有一个状态。

对于某个输入串,从开始状态起,最多只有一条到达某个终态的路径可以由这个串标记。也就是说,如果DFA可以识别某个输入串,那么路径一定唯一。

2.3.3 从正规式到 NFA

介绍两种方法。


方法一:

如果比较熟悉这部分,完全可以只看看图片,不去阅读文字。

将正规式分解成子表达式,使用规则(1)和(2)为正规式中的每个基本符号(ε\varepsilonε 和字母表符号)构造NFA。要注意,如果符号 aaa 在正规式中出现多次,那么要为它的每次出现构造一个NFA。

简而言之,将分解后的每个子表达式都创建一个NFA,最后会将这些NFA通过规则进行合并的。

然后,根据正规式正规式的语法结构,用下面的规则(3)归纳地组合这些NFA,直到获得整个正规式的 NFA为止。在构造过程中所产生的中间NFA有一些重要的性质:只有一个终态,没有边进入开始状态,也没有边离开终态。

(1)对于 ε\varepsilonε ,构造下图所示的NFA,其中 iii 是开始状态,fff 是接受状态。很显然这个NFA识别 {ε}\{\varepsilon\}{ε}。

(2)对 Σ\SigmaΣ 中的每个符号 aaa ,构造下图所示的NFA。同样,iii 是开始状态,fff 是接受状态。这个NFA识别 {a}\{a\}{a}。

(3)如果 N(s)N(s)N(s) 和 N(t)N(t)N(t) 分别是正规式 sss 和 ttt 的NFA,则:

​ ① 对于正规式 s∣ts | ts∣t,构造合成的NFA N(s∣t)N ( s | t)N(s∣t) ,结果如下图所示。这里 iii 是新的开始状态,fff 是新的接受状态。从 iii 到 N(s)N(s)N(s) 和 N(t)N(t)N(t) 的开始状态有 ε\varepsilonε 转换,从 N(s)N( s )N(s) 和 N(t)N(t)N(t) 的接受状态到 fff 也有 ε\varepsilonε 转换。N(s)N(s )N(s) 和 N(t)N(t)N(t) 的开始和接受状态不是 N(s∣t)N(s | t)N(s∣t) 的开始和接受状态。这样,从 iii 到 fff 的任何路径必须排他地通过 N(s)N(s)N(s) 或 N(t)N(t)N(t) 。这个合成的 NFA 识别 L(s)∪L(t)L(s)∪L( t)L(s)∪L(t)。

​ ② 对于正规式 ststst,构造合成的NFA N(st)N( st )N(st) ,结果如下图所示。N(s)N(s)N(s) 的开始状态成为合成后的 NFA 的开始状态,N(t)N(t)N(t) 的接受状态成为合成后的 NFA 的接受状态,N(s)N(s)N(s) 的接受状态和 N(t)N(t)N(t) 的开始状态合并,也就是 N(t)N(t)N(t) 开始状态的所有转换成为 N(s)N(s )N(s) 的接受状态的转换。合并后的这个状态不作为合成后的 NFA 的接受状态或开始状态。从 iii 到 fff 的路径必须首先经过 N(s)N(s )N(s) ,然后经过 $N(t) $,所以这种路径上的标记拼成 L(s)L(t)L(s)L(t)L(s)L(t) 的串。因为没有边进入N(t)N(t)N(t) 的开始状态或离开 N(s)N(s)N(s) 的接受状态,所以在 iii 到 fff 的路径中不存在从 N(t)N(t)N(t) 回到 N(s)N(s )N(s) 的现象,故合成的 NFA 识别 L(s)L(t)L( s)L( t)L(s)L(t)。

​ ③ 对于正规式 s∗s^*s∗,构造合成的 NFA N(s∗)N(s^*)N(s∗),结果如下图所示。同样,iii 和 fff 分别是新的开始状态和接受状态。在这个合成的NFA中,可以沿着 ε\varepsilonε 边直接从 iii 到 fff,这代表 ε\varepsilonε 属于 (L(s))∗(L(s))^*(L(s))∗,也可以从 iii 经过 N(s)N(s)N(s) 一次或多次。显然,这个NFA识别 (L(s))∗(L(s))^*(L(s))∗。

​ ④ 对于外加括号的正规式 (s)(s)(s),对应 sss 的 N(s)N(s)N(s) 就作为 (s)(s)(s) 的NFA。

对于每次构造的新状态都赋予不同的名字。这样,所有的状态都有不同的名字。

如此产生的NFA具有下列性质:

  • NFA的状态数最多是正规式中符号和算符总数的两倍。因为构造的每一步最多引入两个新的状态。
  • NFA只有一个接受状态,接受状态没有向外的转换。
  • NFA的每个状态有一个用 Σ\SigmaΣ 的符号标记的指向其他状态的转换,或者最多两个指向其他状态的 ε\varepsilonε 转换。

方法二:

以构造正规式 (a∣b)∗ab(a\space|\space b)^*ab(a ∣ b)∗ab 的NFA为例。

2.3.4 从 NFA 到 DFA

通常采用子集构造法从NFA构造出可以识别同样语言的DFA。

子集构造法的思想:让新构造的DFA的每个状态对应到该NFA的一个状态集,即这个DFA在读取输入 a1a2...ana_1a_2...a_na1​a2​...an​ 后到的状态,对应于该NFA从开始状态沿着那些标有 a1a2...ana_1a_2...a_na1​a2​...an​ 的路径能到达的所有状态的集合。

下图是识别 (a∣b)∗ab(a\space|\space b)^*ab(a ∣ b)∗ab 的NFA。

先找开始状态0通过若干次空转换能够到达的状态,即 ε−colsure(0)\varepsilon-colsure(0)ε−colsure(0),记为 A={0,1,2,4,7}A = \{0,1,2,4, 7\}A={0,1,2,4,7}。

输入字母表为 {a,b}\{a, b\}{a,b}。

查找新状态 AAA 经过一次 aaa 转换能够到达的状态,即 ε−colsure(move(A,a))\varepsilon-colsure(move(A, a))ε−colsure(move(A,a)) ,由于在 A={0,1,2,4,7}A=\{0,1, 2, 4, 7\}A={0,1,2,4,7} 中,只有状态2和7能发生 aaa 转换,分别转变为状态3和8,因此 move(A,a)={3,8}move(A, a)=\{3, 8\}move(A,a)={3,8}。所以 ε−colsure(move(A,a))=ε−colsure({3,8})={1,2,3,4,6,7,8}\varepsilon-colsure(move(A, a))=\varepsilon-colsure(\{3,8\})=\{1,2, 3,4, 6, 7, 8\}ε−colsure(move(A,a))=ε−colsure({3,8})={1,2,3,4,6,7,8}。显然这个状态集是全新的,给它命名为状态 BBB。

查找新状态 AAA 经过一次 bbb 转换能够到达的状态,在状态 AAA 中只有状态4发生 bbb 转换到达状态5,所以状态 AAA 的 bbb 转换到达,故 ε−closure(move(A,b))=ε−closure({5})={1,2,4,5,6,7}\varepsilon-closure( move( A, b)) = \varepsilon-closure( \{5 \})= \{1,2,4,5,6,7\}ε−closure(move(A,b))=ε−closure({5})={1,2,4,5,6,7},显然这个状态集是全新的,给它命名为状态 CCC。

对 BBB 和 CCC 重复上面的过程,又会生成一个全新的状态 D={1,2,4,5,6,7,9}D=\{1,2,4,5,6,7,9\}D={1,2,4,5,6,7,9},由于状态 DDD 中包含状态9,即接受状态,故状态 DDD 为构造出的DFA的接受状态。DFA的转换表和转换图如下:

这部分只要学会做法了就行,记住做法,多做几道就可以完全掌握。练习题可以看我做的课后题,这里就不展开讲题了。

2.3.5 DFA 的化简

化简后的DFA包含的状态数最少,并且识别同样的语言。

化简步骤:

  1. 化简所用的方法基于转换函数是全函数(下面有介绍全函数)。如果一个DFA的转换函数不是全函数,可以引入一个“死状态” sds_dsd​ ,sds_dsd​ 对所有输入符号都转换到 sds_dsd​ 本身。如果状态 sss 对符号 aaa 没有转换,那么加上从 sss 到 sds_dsd​ 的 aaa 的转换。显然,加入死状态后的DFA和原来的DFA等价。

    全函数:通过DFA的转换图来理解,每个状态都有对应的每个输入符号(字母表)的输出;通过DFA的转换表来理解,转换表每一项都不是空集。

  2. 构造状态集合的初始划分 ΠΠΠ:分成两个子集,接受状态子集(由全部接受状态构成的状态集)和非接受状态子集(由全部非接受状态构成的状态集)

  3. 应用下面的过程对 ΠΠΠ 构造新的划分 ΠnewΠ_{new}Πnew​ :

    for(Π中的每个子集 G){\textbf{for}\space(Π \space 中的每个子集\space G)\space\space \{for (Π 中的每个子集 G)  {

    把 G划分成若干子集,G的两个状态 s和 t在同一子集中,当且仅当对任意输入符号 a, s和 t的 a转换是到 Π的同一子集中\space\space\space\space\space\space\space\space 把 \space G\space 划分成若干子集,G\space 的两个状态\space s\space 和 \space t\space 在同一子集中,当且仅当对任意输入符号 \space a\space ,\space s\space 和\space t\space 的\space a\space 转换是到\space Π\space 的同一子集中        把 G 划分成若干子集,G 的两个状态 s 和 t 在同一子集中,当且仅当对任意输入符号 a , s 和 t 的 a 转换是到 Π 的同一子集中

    在 Πnew中,用 G的划分代替 G\space\space\space\space\space\space\space\space在\spaceΠ_{new}\space中,用\space G\space的划分代替\space G\space        在 Πnew​ 中,用 G 的划分代替 G

    }\}}

    不知道你是否考虑过,同一次循环中,如果循环开始前 ΠΠΠ 存在两个子集分别记为 G1G_1G1​ 和 G2G_2G2​,现在执行循环中的第一条语句,假设将 G1G_1G1​ 划分成了 G1−sub1G_{1-sub_1}G1−sub1​​ 和 G1−sub2G_{1-sub_2}G1−sub2​​ ,那么如果对 G2G_2G2​ 进行划分时其中一组状态遇到输入字符 aaa 转移到了 G1−sub1G_{1-sub_1}G1−sub1​​ 中,而另一组同样是遇到输入字符 aaa 但是转移到了 G1−sub2G_{1-sub_2}G1−sub2​​ 中,那么是否要对 G2G_2G2​ 进行划分?

    答案是否,不需要划分。紧扣上面的算法过程,将 G1G_1G1​ 划分成 G1−sub1G_{1-sub_1}G1−sub1​​ 和 G1−sub2G_{1-sub_2}G1−sub2​​ 后“在ΠnewΠ_{new}Πnew​中,用 GGG 的划分代替 GGG”,即 G1−sub1G_{1-sub_1}G1−sub1​​ 和 G1−sub2G_{1-sub_2}G1−sub2​​ 代替 GGG ,但是主要是对 ΠnewΠ_{new}Πnew​ 进行的操作,也就是说 ΠΠΠ 是没有变化的,仍然是两个子集 G1G_1G1​ 和 G2G_2G2​。看完下面的步骤就会知道后续会将 ΠΠΠ 修改为 ΠnewΠ_{new}Πnew​。

  4. 如果 ΠnewΠ_{new}Πnew​ 与 ΠΠΠ 相等,则让 Πfinal=ΠΠ_{final}=ΠΠfinal​=Π ,再执行步骤5,否则,令 Π=ΠnewΠ= Π_{new}Π=Πnew​ ,转到步骤3。

  5. 在 ΠnewΠ_{new}Πnew​ 的每个状态子集中选一个状态代表它,这些代表就是最简DFA M′M'M′ 的状态。如果 sss 是这样的一个代表,在 DFA MMM 中,若 sss 的 aaa 转换到 ttt,并且 ttt 所在子集的代表是 rrr( rrr 可能就是 ttt ),那么,在 M′M'M′ 中,sss 的 aaa 转换到 rrr。包含 s0s_0s0​ 的状态子集的代表是 M′M'M′ 的开始状态,M′M'M′ 的接受状态是那些原先属于 FFF 集合的代表。注意, ΠnewΠ_{new}Πnew​ 的每个子集或者仅含 FFF 中的状态,或者不含 FFF 中的状态。

  6. 如果 M′M'M′ 有死状态,则去掉它。从开始状态不可达的状态也要删除。从任何其他状态到死状态的转换都改成无定义。


对上面得到的DFA进行化简。初始划分 ΠΠΠ 包括两个子集:接受状态子集 {D}\{D\}{D} 和非接受状态子集 {A,B,C}\{A,B,C\}{A,B,C}。为了构造 ΠnewΠ_{new}Πnew​ ,首先考虑 {D}\{D\}{D},因为这个子集只包含一个状态,它不能再划分,所以在 ΠnewΠ_{new}Πnew​ 中仍是 {D}\{D\}{D}。然后考虑 {A,B,C}\{A,B,C\}{A,B,C} ,对于输入 aaa ,这些状态都转换到 BBB,但对于输入 bbb,AAA 和 CCC 都转换到状态子集 {A,B,C}\{A,B,C\}{A,B,C} 的一个成员,而 BBB 转换到 DDD ,是另一个子集的成员。于是,在 ΠnewΠ_{new}Πnew​ 中,状态子集 {A,B,C}\{A,B,C\}{A,B,C} 必须分成两个新子集 {A,C}\{A,C\}{A,C} 和 {B}\{B\}{B}, ΠnewΠ_{new}Πnew​ 成了 {A,C}\{A,C\}{A,C}、{B}\{B\}{B} 和 {D}\{D\}{D}。

再次扫描,只有 {A,C}\{A,C\}{A,C} 有划分的可能。但是对于输入 aaa 和 bbb,它们都分别转换到 BBB 和 CCC,因而不必再划分。即这一遍扫描后,Πnew=ΠΠ_{new}=ΠΠnew​=Π。所以 $ Π_{final}$ 是 {A,C}\{A,C\}{A,C}、{B}\{B\}{B} 和 {D}\{D\}{D}。

如要选择 AAA 作为 {A,C}\{A,C\}{A,C} 的代表,选择 BBB 和 DDD 作为其他单状态子集的代表,可以得到最简自动机。它的转换表如下图所示,状态 AAA 是开始状态,状态 DDD 是唯一的接受状态。


注意:一个正规式对应的DFA不唯一,但是最简DFA(或者说是状态数最少的DFA)是唯一的。

比如:对于 (a∣b)∗ab(a\space|\space b)^*ab(a ∣ b)∗ab

是我们由NFA通过子集构造法得到的DFA,也就是上面出现过的。

这是另一种该正规式的转换图表示,同样也是一个DFA,但是与上面的DFA显然不是同一个,因此一个正规式对应的DFA不唯一。

我们还应该注意到第二个转换图正是最简DFA的转换图。

2.4 词法分析器

词法分析器对源程序采取非常局部的观点。

举例:难以发现 fi(a==f(x) ... 存在的错误,直到没有 fi 函数的定义 。

2.4.1 词法分析器简述

**词法分析器的主要任务:**把构成源程序的字符流翻译成词法记号流。

词法分析器的作用:

  1. 去除注释和空白;
  2. 把来自编译器各个阶段的错误信息和源程序联系起来;
  3. 宏定义替换。

其中,词法分析器具有“将错误信息与源程序联系起来”的作用,是因为通常在词法记号流中已经没有行的概念,因此如果出现错误,则必须要从源程序(或字符流)中分析出错误的位置。

词法分析器和语法分析器的一种典型关系,即把词法分析器作为语法分析器的一个子程序来实现。当收到来自语法分析器的“取下一个记号”命令时,词法分析器扫描剩余输入字符,直到它能够确认一个词法记号为止。

2.4.2 识别空白

假定词法单元之间在必要时用空格(或制表符、换行符)分开,词法分析器通过把剩余输入的前缀和下面的正规定义ws\textbf{ws}ws相比较来完成忽略词法单元之间的空白。
delim→blank∣tab∣newlinews→delim+\textbf{delim}\rightarrow \textbf{blank}\space |\space \textbf{tab}\space |\space \textbf{newline}\\ \textbf{ws}\rightarrow \textbf{delim}^+\\ delim→blank ∣ tab ∣ newlinews→delim+
如果剩余输入的前缀能由ws\textbf{ws}ws匹配,词法分析器不返回记号给分析器,它继续去寻找空白后面的记号,然后返回到分析器。

词法分析器输出的是<记号名,属性值><记号名, 属性值><记号名,属性值>二元组序列,正规式ws\textbf{ws}ws没有对应的记号。


2.4.3 识别关键字与标识符

关键字的识别一般与标识符的识别使用同一个转换图,而不为识别关键字单独创建转换图。这也就意味着,如果到达接受状态时,需要执行某段代码,以判定到达接受状态的词法单元是关键字还是标识符。

把关键字从标识符中分离出来的简单办法是建立一张关键字表。在扫描任何字符前,把构成关键字的串,如while和do等都置入该表,把与它们对应的记号名也加入该表,以便识别出这些串时返回。

※ 完整的识别过程:

识别器对源文件进行识别,如果识别器能识别出当前扫描到的字符串,则执行相应的语句(调用函数),以获取要返回的记号名和属性值。函数首先查看符号表中的关键字部分,如果当前词法单元构成关键字,则返回相应的记号(关键字无属性值);否则该词法单元是标识符。该函数再查符号表中的标识符部分,如果在表中发现该词法单元则返回相应的条目指针,如果没有找到,则把该词法单元填入标识符表,并返回新建条目的指针(很多编译器在语法分析阶段才将标识符填入标识符表,这时 id\textbf{id}id 的属性是它的拼写形式)。识别完当前关键字或标识符后,如果遇到空白,则词法分析器不返回记号给分析器,它继续去寻找空白后面的记号,然后返回到分析器。

如果要识别的关键字有所变化,无须修改转换图,只需给符号表的关键字部分重新置初值即可。为关键字单独构造转换图是可能的。对典型的编程语言来说,这么做会使词法分析器的状态数多达几百个。而用上面的方法,不到100个状态可能就够了。

* 2.5 词法分析器的生成器

Lex编译器是一种依据基于正规式的描述来构造词法分析器的特殊工具。

Lex的使用方式:

Lex通常按下图描绘的方式使用。

首先,词法分析器的说明用Lex语言表达在程序 lex.l 中,然后 lex.l 通过Lex编译器,产生C语言程序 lex.yy.c。程序lex.yy.c 包括从 lex.l 的正规式构造出的转换图(用表格形式表示)和使用这张转换图识别词法单元的标准子程序。在 lex.l 中,和正规式相关联的动作是用C语言代码表示的,它们被直接复制到 lex.yy.c 中。最后, lex.yy.c 被编译成目标程序 a.out,它就是把输人串识别成记号序列的词法分析器。

Lex程序的三部分:

声明部分包括变量声明、常量定义和正规定义。

每条翻译规则的形式如下:
模式    {动作}模式\space\space\space\space \{动作\} 模式    {动作}
其中,每个模式是一个正规式,每个动作描述该模式匹配词法单元时,词法分析器应执行的程序段。在Lex中,动作是用C语言写的代码段。

第三部分包括了动作所用到的辅助函数。


Lex建立的词法分析器执行过程:

由Lex建立的词法分析器通常作为语法分析器的一个子程序。词法分析器每次被语法分析器调用时,逐个字符地读它的剩余输人,直到它在剩余输入中发现能和某个模式 РРР 匹配的最长前缀为止。然后执行和 РРР 对应的动作 AAA。典型地,动作A将把控制返回语法分析器。如果不是这样,例如 РРР 描述空白或注释,则词法分析器就继续寻找后继的词法单元,直到有一个动作引导控制回到语法分析器为止。

词法分析器仅返回一个值(记号名)给语法分析器,记号的属性值通过共享整型变量 yylvalyylvalyylval 传递。

注意:这里说的词法分析器仅返回记号名,是针对Lex建立的词法分析器而言的。一般如果我们只问词法分析器为语法分析器提供什么,则回答记号名和属性值。


最长前缀匹配原则:

当输入串的多个前缀与一个或多个模式匹配时,总选择最长的前缀进行匹配。

输入 <=<=<= 虽然识别到 === 时就已经到达了接受状态,但是根据最长前缀匹配原则,还需要继续识别 === 进入接受状态2;识别 ++++++ 的过程类似。

在到达某个接受状态后,只要还有要输入的符号,有限自动机就继续前进,以便寻找尽可能长的匹配。

【编译原理】词法分析相关推荐

  1. 编译原理 词法分析 算符优先分析法

    编译原理 词法分析 算符优先分析法 实验目的 加深对语法分析器工作工程的理解,加强对算符优先分析法实现语法分析程序的掌握:能够采用一种编程语言实现简单的语法分析程序:能够使用自己辨析的分析程序对简单的 ...

  2. 编译原理词法分析程序设计

    编译原理词法分析程序设计 1.   课程设计目的: 结合讲授内容,设计与实现一个简单词法分析器,通过设计编制调试一个具体的词法分析程序,加深对词法分析程序的功能及实现方法的理解.并掌握在对程序设计语言 ...

  3. java实现词法分析_编译原理(词法分析) Java 实现

    编译原理(词法分析) Java 实现 编译原理(词法分析) Java 实现 1. 项目目录 2. 需要解释的源代码 PROGRAM SOURCE; /*定义变量*/ VAR X, Y, Z:INTEG ...

  4. c++实现编译原理词法分析实验(含代码)

    c++实现编译原理词法分析实验(含代码) 一.实验目的: 通过设计编制调试一个具体的词法分析程序,加深对词法分析原理的理解.并掌握在对程序设计语言源程序进行扫描过程中将其分解为各类单词的词法分析方法. ...

  5. 编译原理---词法分析

    词法分析的原理 词法分析是编译程序进行编译时第一个要进行的任务,主要是对源程序进行编译预处理之后,对整个源程序进行分解,分解成一个个单词,这些单词有且只有五类,分别时标识符.关键字(保留字).常数.运 ...

  6. 编译原理——词法分析

    根据上课内容顺序写的博客,并不是按照书的目录来的 使用龙书以及编译程序设计原理(第二版)金成植.金英编著 老师的PPT是英文的,我自己随便翻的,不一定对 文章目录 词法分析(scanning) 概述 ...

  7. 编译原理词法分析实验

    目录 实验内容描述 实验设计 输入输出形式 样例输入和样例输出 实验设计原理(步骤) 主要函数和辅助函数 核心代码截图 实验结果 可以找我代做,包满分.QQ1975728171可以写完整实验报告 实验 ...

  8. 编译原理 - 词法分析

    词法分析 词法分析器 作用 编译过程划分为词法分析和语法分析两个阶段的原因 语法分析中的三个概念 词法分析的实现 如何区分兼容性的标识符 词法分析算法 词法单元 词法单元例子 词法单元的模式 正则表达 ...

  9. 编译原理——词法分析(1)

    在我们学习词法分析时,就会思考如何构造一个词法分析器? 一个词法分析器可以通过手工构造:也可以通过以下方式自动生成一个词法分析器:向一个词法分析器生成工具描述出词素的模式,然后将这些模式编译为具有动词 ...

  10. 编译原理——词法分析(3)有穷自动机中DFA与NFA的理解

    1.1词法分析器生成工具Lex 虽然在学习上,我们学习的是Lex,但是最近经常使用的是词法分析器生成工具是Flex,它可以为C语言生成代码,Vern Paxson于1987年以C语言写作了Flex,他 ...

最新文章

  1. 为啥看恐怖片老是忘不掉?最新研究:恐惧记忆的形成方式更利于稳定存储
  2. NDO中的ActiveRecord 简介 2——强类型的活动记录
  3. CVE-2018-8120 Windows权限提升
  4. Yolov5身份证检测——C++ OpenCV DNN推理
  5. override,final的使用,两者都是针对虚函数,也就是说要有virtual关键字
  6. JavaScript算法与数据结构——字典详解
  7. Java 7和Java 8之间的细微自动关闭合同更改
  8. 13亿美元的思想实验
  9. springmvc5中设计模式
  10. IT优秀书籍收集下载
  11. 在PHP中2中特殊数据类型是,@PHP中的数据类型(2)
  12. Net设计模式实例之桥接模式( Bridge Pattern)(4)
  13. Corel Painter 2022 for Mac(初学者可驾驭的绘画软件)
  14. 程序员的生活照,最后一个绝了!有同感吗?
  15. SPSS多元统计分析【009期】
  16. 打造史上最容易使用的Tab指示符——Indicator
  17. 2022系统分析师--案例必备知识点汇总
  18. 你真的知道如何正确清除 DNS 缓存吗?( 附全平台详细教程 )
  19. 人工智能急需变革?比起英特尔的焦虑,互联网企业更想蹚这一池芯片水...
  20. An overview of color constancy algorithms论文笔记

热门文章

  1. python斐波那契数列for循环_Python编程题、for循环和列表推导式的用法题(阿凡提与国王比赛下棋、 斐波那契数列等)...
  2. 【数论】排列组合问题
  3. 计算机互联网行业和地产行业哪个好,互联网行业毕业生起薪最高 互联网、金融、房地产人才很紧缺...
  4. html js居中,javascript居中怎么表示
  5. win7一直显示正在关机_电脑win7系统怎么优化,可以提高电脑运行速度?
  6. 浅析highchart
  7. 快速查询德邦物流信息,并标记未签收单号
  8. centos锁定与解锁账户
  9. 网页版phpMyAdmin账号密码
  10. 热门列表的实现思路整理