编译原理-第三章 词法分析

Lex词法分析器生成工具(它的一个最新的变体称为Flex)。向一个词法分析器生成工具(lexical-analyzer generator)描述出词素的模式,然后将这个模式编译为具有词法分析器功能的代码。

1. 词法分析器的作用

词法分析器的主要任务是读入源程序的输入字符、将它们组成词素,生成并输出一个词法单元序列,每个词法单元对应于一个词素。这个词法单元序列被输出到语法分析器进行语法分析。当词法分析器发现了一个标识符的词素时,它要将这个词素添加到符号表中。

image

 词法分析器过滤掉源程序中的注释和空白,记录行号。宏扩展也可以由词法分析器完成。

词法分析器可以分成两个级联的处理阶段:

1. 扫描阶段主要负责完成一些不需要生成词法单元的简单处理,比如删除注释和将多个连续的空白字符压缩成一个字符。

2. 词法分析阶段是较为复杂的部分,它处理扫描阶段的输出并生成词法单元。

1.1 词法分析及语法分析

把编译过程的分析部分划分为词法分析和语法分析阶段有如下几个原因:

1. 简化编译器设计。

2. 提供编译器效率。

3. 增强编译器的可移植性。

1.2 词法单元、模式和词素

词法单元由一个词法单元名和一个可选的属性值组成。词法单元名是一个表示某种词法单位的抽象符号。词法单元名字是由语法分析器处理的输入符号。

模式描述了一个词法单元的词素可能具有的形式。

词素是源程序中的一个字符序列。

1.3 词法单元的属性

一般来说,和一个标识符有关的信息-例如它的词素、类型、它第一次出现的位置(在发出一个有关该标识符的错误信息时需要使用这个信息)-都保存在符号表中。一个标识符的属性值是一个指向符号表中该标识符对应条目的指针。

1.4 词法错误

错误恢复动作:

1. 从剩余的输入中删除一个字符。

2. 剩余的输入中插入一个遗漏的字符。

3. 用一个字符来替换另一个字符。

4. 交换两个相邻的字符。

2. 输入缓冲

双缓冲区方案,这种方案能够安全地处理向前看多个符号的问题。使用哨兵标记来节约用于检查缓冲区末端的时间。

2.1 缓冲区对

利用两个交替读入的缓冲区处理输入字符。

每个缓冲区的容量都是N个字符,通常N是一个磁盘块的大小。如果输入文件中剩余字符不足N个,那么就会有一个特殊字符(用eof表示)来标记源文件的结束。

2.2 哨兵标记

如果我们扩展每个缓冲区,使它们在末尾包含了一个“哨兵”(sentinel)字符,我们就可以把对缓冲区末端的测试和对当前字符的测试合而为一。

3. 词法单元的规约

正则表达式是一种用来描述词素模式的重要表示方法。

如果字符串包含很多行,那么我们就有可能面临单个词素的长度超过N的情况。

3.1 串和语言

字母表(alphabet)是一个有限的符号集合。

某个字母表上的一个串(string)是该字母表中符号的一个有穷序列。串s的长度,通常记作|s|,是指s中符号出现的次数。空串(empty string)是长度为0的串,用∈表示。

语言是某个给定字母表上一个任意的可数的串集合。

串相关的常用术语:

1)串s的前缀(prefix)是从s的尾部删除0个或多个符号后得到的串。

2)串s的后缀(suffix)是从s的开始处删除0个或多个符号后得到的串。

3)串s的子串(substring)是删除s的某个前缀和某个后缀之后得到的串。

4)串s的真(true)前缀、真后缀、真子串分别是s的既不等于∈,也不等于s本身的前缀、后缀和子串。

5)串s的子序列(subsequence)是从s中删除0个或多个符号后得到的串,这些被删除的符号可能不相邻。

如果x和y是串,那么x和y的连接(concatenation)(记作xy)是把y附加到x后面而形成的串。空串是连接运算的单位元,也就是说,对于任何串s都有,s∈=∈s=s。

如果把两个串的连接看成是这两个串的“乘积”,我们可以定义串的“指数”运输如下:定位s0为∈,并且对于i>0,si为si-1s。因为∈s=s,由此可知s1=s, s2=ss, s3=sss,以此类推。

3.2 语言上的运算

在词法分析中,最重要的语言上的运输是并、连接和闭包运算。一个语言L的Kleene闭包(closure),记作L*,就是将L连接0次或多次后得到的串集。L0,即将L连接0次得到的集合,被定义为{∈},并且Li被归纳地定义为Li-1L。L的正闭包和Kleene闭包基本相同,但是不包含L0

image

3.3 正则表达式

正则表达式可以由较小的正则表达式按照如下规则递归地构建。每个正则表达式r表示一个语言L(r),这个语言也是根据r的子表达式所表示的语言递归地定义的。下面的规则定义了某个字母表∑上的正则表达式以及这些表达式所表示的语言。

归纳基础:

1)∈是一个正则表达式,L(∈)={∈},即该语言只包含空串。

2)如果a是∑上的一个符号,那么a是一个正则表达式,并且L(a) = {a}。

归纳步骤:由小的正则表达式构造较大的正则表达式的步骤有四个部分。假定r和s都是正则表达式,分别表示语言L(r)和L(s),那么:

1)(r) | (s) 是一个正则表达式,表示语言L(r) ∪ L(s)。

2)(r)(s)是一个正则表达式,表示语言L(r)L(s)。

3)(r)*是一个正则表达式,表示语言(L(r))*

4)(r)是一个正则表达式,表示语言L(r)。最后这个规则是说在表达式的两边加上括号并不影响表达式所表示的语言。

约定:

1)一元运算符*具有最高的优先级,并且是左结合的。

2)连接具有次高的优先级,它也是左结合的。

3)| 的优先级最低,并且也是左结合的。

可以用一个正则表达式定义的语言叫做正则集合(regular set)。如果两个正则表达式r和s表示同样的语言,则称r和s等价(equivalent),记作r=s。

正则表达式遵守一些代数定律:

image

3.4 正则定义

为方便表示,给某些正则表达式命名,并在之后的正则表达式中像使用符号一样使用这些名字。如果∑是基本符号的集合,那么一个正则定义(regular definition)是具有如下形式的定义序列:

d1 -> r1

d2 -> r2

   ...

dn -> rn

其中:

每个di都是一个新符号,它们都不在∑中,并且各不相同。

每个ri是字母表∑∪{d1, d2, ..., di-1}上的正则表达式。

3.5 正则表达式的扩展

1)一个或多个实例。单目后缀运算符+表示一个正则表达式及其语言的正闭包。也就是说,如果r是一个正则表达式,那么(r)+就表示语言(L(r))+。运算符+和运算符*具有同样的优先级和结合性。r* = r| ∈。r+ = rr* = r*r。

2)零个或一个实例。单目后缀运算符?的意思是零个或一个出现。r?等价于r | ∈,L(r?) = L(r) ∪ {∈}。运算符?与运算符+和运算符*具有相同的优先级和结合性。

3)字符类。一个正则表达式a1 | a2 | ... | an(其中ai是字母表中的各个符号)可以缩写为[a1a2an]。当a1, a2, ..., an形成一个逻辑上连续的序列时,可以把它们表示成a1 - a2

4. 词法单元的识别

image

 

image

 空白符词法单元ws:

ws -> (blank | tab | newline)+

image

4.1 状态转换图

状态转换图(transition diagram)有一组被称为“状态”(state)的结点或圆圈。词法分析器在扫描输入串的过程中寻找和某个模式匹配的词素,而转换图中的每个状态代表一个可能在这个过程中可能出现的情况。

状态图中的边(edge)从图的一个状态指向另一个状态。每条边的标号包含了一个或多个符号。

约定:

1)某些状态称为接受状态或最终状态。表明已经找到了一个词素。用双层的圈来表示一个接受状态。

2)如果需要将forward回退一个位置(即相应的词素并不包含那个在最后一步是我们到达接受状态的符号),那么我们将在该接受状态的附近加上一个*。如果需要回退多个位置,将为接受状态附加相应数目的*。

3)有一个状态被指定为开始状态,也称为初始状态,该状态由一条没有出发结点的、标号为“start”的边指明。

image

4.2 保留字和标识符的识别

可以使用两种方法来处理那些看起来很像标识符的保留字:

1)初始化时就将各个保留字填入符号表中。符号表条目的某个字段会指明这些串并不是普通的标识符,并指出它们所代表的词法单元。

2)为每个关键字建立单独的状态转换图。设定词法单元之间的优先级,使得当一个词素同时匹配id的模式和关键字的模式时,优先识别保留字词法单元。

4.3 完成我们的例子

词法单元number的状态转换图。

image

 空白符的状态转换图

image

4.4 基于状态转换图的词法分析器的体系结构

image

一个状态转换图已经找到了一个与它的模式相匹配的词素,但另外的一个或多个状态转换图仍然可以继续处理输入。解决这个问题的常见策略是取最长的和某个模式相匹配的输入前缀。

5. 词法分析器生成工具Lex

介绍一个名为Lex的工具。在最近的实现中它也称为Flex。它支持使用正则表达式来描述各个词法单元的模式,由此给出一个词法分析器规约。Lex编译器将输入的模式转换成一个状态转换图,并生成相应的实现代码,并存放到文件lex.yy.c中。

5.1 Lex的使用

image

 a.out,通常是一个被语法分析器调用的子例程,这个子例程返回一个整数值,即可能出现的某个词法单元名的编码。而词法单元的属性值,不管它是一个数字编码,还是一个指向符号表的指针,或者什么都没有,都保存在全局变量yylval中。这个变量由词法分析器和语法分析器共享。

5.2 Lex程序的结构

一个Lex程序具有如下形式:

声明部分

%%

转换规则

%%

辅助函数

声明部分包括变量和明示常量(manifest constant,被声明的表示一个常量的标识符,如一个词法单元的名字)的声明和正则定义。

转换规则形式:

模式 { 动作 }

其中,每个模式是一个正则表达式,它可以使用声明部分中给出的正则定义。动作部分是代码片段。

Lex程序的第三部分包含各个动作需要使用的所有辅助函数。

由Lex创建的词法分析器和语法分析器按照如下方式协同工作。当词法分析器被语言分析器调用时,词法分析器开始从余下的输入中逐个读取字符,直到它发现了最长的与某个模式Pi匹配的前缀。然后,词法分析器执行相关的动作Ai。通常Ai会将控制返回给语法分析器。词法分析器只向语法分析器返回一个值,即词法单元名。但在需要时可以利用共享的整型变量yylval传递有关这个词素的附加信息。

image

5.3 Lex中的冲突解决

当输入的多个前缀与一个或多个模式匹配时,Lex用如下规则选择正确的词素:

1)总是选择最长的前缀。

2)如果最长的可能前缀与多个模式匹配,总是选择在Lex程序中先被列出的模式。

5.4 向前看运算符

可以在模式中用斜线来指明该模式中和词素实际匹配的部分的结尾,斜线/之后的内容表示一个附加的模式,只有附加模式和输入匹配之后,我们才可以确定已经看到了要寻找的词法单元的词素,但是和第二个模式匹配的字符并不是这个词素的一部分。

6. 有穷自动机

有穷自动机(finite automata)。自动机与状态转换图的不同:

1)有穷自动机是识别器(recognizer),它们只能对每个可能的输入串简单的回答“是”或“否”。

2)有穷自动机分为两类:

1. 不确定的有穷自动机(Nondeterministic Finite Automata, NFA)对其边上的标号没有任何限制。一个符号标记离开同一状态的多条边,并且空串∈也可以作为标号。

2. 对于每个状态及自动机输入字母表中的每个符号,确定的有穷自动机(Deterministic Fnite Automata, DFA)有且只有一条离开该状态、以该符号为标号的边。

6.1 不确定的有穷自动机

一个不确定的有穷自动机(NFA)由以下几个部分组成:

1)一个有穷的状态集合S。

2)一个输入符号集合∑,即输入字母表(input alphabet)。假设代表空串的∈不是∑中的元素。

3)一个转换函数(transition function),它为每个状态和∑ ∪ { ∈ }中的每个符号都给出了相应的后继状态(next state)的集合。

4)S中的一个状态s0被指定为开始状态,或者说初始状态。

5)S的一个子集F被指定为接受状态(或者说终止状态的)集合。

不管是NFA还是DFA,我们都可以将它表示为一张转换图(transition graph)。

image

6.2 转换表

我们也可以将一个NFA表示为一张转换表(transition table),表的各行对应于状态,各列对应于输入符号和∈。对应于一个给定状态和给定输入的条目是将NFA的转换函数应用于这些参数后得到的值。

image

6.3 自动机中输入字符串的接受

一个NFA接受输入字符串x,当且仅当对应的转换图中存在一条从开始状态到某个接受状态的路径,使得该路径中各条边上的标号组成符号串x。路径中的∈标号将被忽略。

只要存在某条其标号序列为某符号串的路径能够从开始状态到达某个接受状态,NFA就接受这个符号串。

6.4 确定的有穷自动机

确定的有穷自动机是不确定的有穷自动机的一个特例,其中:

1)没有输入∈之上的转换动作。

2)对每个状态s和每个输入符号a,有且只有一条标号为a的边离开s。

image

7. 从正则表达式到自动机

对NFA的模拟不如对DFA的模拟直接。

7.1 从NFA到DFA的转换

子集构造法的基本思想是让构造得到的DFA的每个状态对应于NFA的一个状态集合。DFA在读入输入a1a2...an之后到达的状态对应于相应的NFA从开始状态出发,沿着以a1a2...an为标号的路径能够到达的状态的集合。

image

 

image

 

image

 

7.2 NFA的模拟

根据一个正则表达式构造出相应的NFA,然后使用on-the-fly(即边构造边使用的)的子集构造法来模拟这个NFA的执行。

这个算法保存了一个当前的集合S,即那些可以从s0开始沿着标号为当前已读入输入部分的路径到达的状态的集合。如果c是函数nextChar()读到的下一个输入字符,那么我们首先计算move(S, c),然后使用∈-closure求出这个集合的闭包。

image

 

7.3 NFA模拟的效率

所需时间和输入串的长度和转换图的大小(结点数加上边数)的乘积成正比。

7.4 从正则表达式构造NFA

将正则表达式转换为一个NFA的McMaughton-Yamada-Thompson算法。

image

 

image

image

 

image

 该算法构造NFA具有的性质:

1)N(r)的状态数最多为r中出现的运算符和运算分量的总数的2倍。

2)N(r)有且只有一个开始状态和一个接受状态。接受状态没有出边,开始状态没有入边。

3)N(r)中除接受状态之外的每个状态要么有一条其标号为∑中符号的出边,要么有两条标号为∈的出边。

7.5 字符串处理算法的效率

image

 

8. 词法分析器生成工具的设计

8.1 生成的词法分析器的结构

image

 Lex组件:

1)表示自动机的一个转换表

2)由Lex编译器从Lex程序中直接拷贝到输出文件的函数

3)输入程序定义的动作。

将多个NFA合并为1个NFA。合并的方法是引入一个新的开始状态,从这个新开始状态到各个对应于模式Pi的NFA Ni的开始状态各有一个∈转换。

8.2 基于NFA的模式匹配

在这个模拟NFA运行的过程中,最终会到达一个没有后续状态的输入点。于是,我们就可以判定最长前缀(与某个模式匹配的词素)是什么。

我们沿着状态集的顺序回头寻找,直到找到一个包含一个或多个接受状态的集合为止。如果集合中有多个接受状态,我们就选择和在Lex程序中位置最靠前的模式相关联的那个接受状态Pi。

8.3 词法分析器使用的DFA

用子集构造法将NFA转换为等价的DFA。在DFA的每个状态中,如果该状态包含一个或多个NFA的接受状态,那么就要确定哪些模式的接受状态出现在此DFA状态中,并找出第一个这样的模式。

我们模拟这个DFA的运行,直到在某一点上没有后续状态为止(严格地说应该是下一个状态为∅,即对应于空的NFA状态集合的死状态)。此时,我们回头查找我们进入过的状态序列,一旦找到接受状态就执行与该状态对应的模式相关联的动作。

8.4 实现向前看运算符

在将模式r1/r2转化为NFA时,我们先把/看成∈,因此我们实际上不会在输入中查找/。词素的末尾是在此NFA进入满足如下条件的状态s的地方:

1)s在(假想的)/上有一个∈转换。

2)有一条从NFA的开始状态到状态s(相应标号序列为x)的路径。

3)有一条从状态s到NFA的接受状态的(相应标号序列为y)路径。

4)在所有满足条件1-3的xy中,x尽可能长。

如果这个NFA中只有一个在假想的/上的∈转换状态,词素的末尾出现在最后一次进入该状态的地方。如果NFA在假想的/上有多个∈转换状态,那么如何寻找正确的状态s的问题就会变得困难得多。

9. 基于DFA的模式匹配器的优化

9.1 NFA的重要状态

9.2 根据抽象语法树计算得到的函数

9.3 计算nullable、firstpos及lastpos

9.4 计算followpos

9.5 根据正则表达式构造DFA

9.6 最小化一个DFA的状态数

9.7 词法分析器的状态最小化

9.8 DFA模拟中的时间和空间权衡

 

posted @ 2025-04-07 22:44  明er  阅读(12)  评论(0)    收藏  举报