编译原理-2.扫描/词法分析


扫描,词法分析将源程序视为字符流读取,并将它们分成多个记号。

记号与自然语言中的单词类似:每一个记号都是表示源程序中信息单元的字符序列。 典型的有关键字(keyword),标识符(identifier);

由于扫描程序的任务是格式匹配一种特殊情况,所以要研究在扫描过程中的格式说明和识别方法。主要方法是:正则表达式(regular expression)和有穷自动机(finite automata)

2.1扫描处理

扫描程序的任务是从源程序中读取字符并形成由编译器的以后部分(通常是分析程序)处理的逻辑单元。由扫描程序生成的逻辑单元称为记号(token),将字符组合成记号与在一个英语句子中将字幕构成单词并确定单词的含义很像。

记号可以分为四类:
1.保留字:if,then等;
2.特殊符号:+,-等;
3.数字和标识符:3.14,myName,myGrade等;
4.文本:“hello world”,“a”,“b”等;
因此,记号常被定义为枚举类型
typedef enum{IF,THEN,ELSE,PLUS,MINUS,NUM,ID,…}TokenTpye;

记号属性
1.记号必须跟它们所表示的字符串完全区分开来,例如保留字记号IF必须与它表示的两个字符“if”串相区别。为了使这个区别更明显,由记号表示的字符串有时乘坐它的串值(string value)或者词义(lexeme)。
2.某些记号只有一个词义,例如保留字就具有这个特性。但记号可以表示无限多个语义,例如标识符全由单个记号 ID表示,然而标识符有许多不同的串值来表示它们的单个的名字。
3.任何与记号相关的值都是记号的属性(attribute),而串值就是属性的示例。

由于扫描程序必须计算每一个记号的若干属性,所以将所有的属性收集到一个单独构造的数据结构中是很有用的,这种数据类型称为记号记录(token record)。

记号的功能:
扫描器根据需要通过一个函数从输入中返回下一个标记。
TokenType getToken( void );
这个方式中声明的getToken函数在调用时从输入中返回下一个记号,并计算诸如记号串值这 样的附加属性。输入字符串通常并不给这个函数提供参数,但参数却被保存在缓冲区中或由系 统输入设备提供。
在这里插入图片描述

2.2正则表达式

正则表达式表示字符串的格式。
正则表达式r完全由它所匹配的串集来定义。这个集合称为有正则表达式生成的语言,写作L(r)。

该语言首先依赖于适用的字符集合,一般是ASCLL字符集合或者其子集,有时甚至更普通,此处集合的元素称为符号,这个正规符号的集合乘坐字母表(alphabet),写作Σ(sigma)。

正则表达式r还包括字母表中的字符,但这些字符具有不同的含义:在正则表达式中,所有的符号指的都是模式。

最后,正则表达式r还可包括有特殊含义的字符。这样的字符称作元字符( m e t a c h a r a c t e r) 或元符号(m e t a s y m b o l)。

2.2.1正则表达式的定义

1.基本正则表达式:
它们是字母表中某个单个字符且自身匹配。假设a是字母表Σ中任一字符,则指定正则表达式a通过书写L(a)={a}来匹配a字符,而特殊情况还要用到另外两个字符。又是需要指出空串的匹配,空串就是不包括任何字符的串,用 ε表示,由L(ε)={ε}定义,偶尔也需要一个不与任何串匹配的符号,也就是空集,写作{ }。用符号φ表示,写作L(φ)={}。它与空串不同,空串包含一个不含任何字符的串,空集则不包括任何东西。

2.正则表达式的运算:
有三种基本运算:
1.从选择对象中选择:用|表示
2.连结,用并置表示
3.重复或者“闭包”,用元字符*表示。

3.从各选择对象中选择:
如果r 和s 是正则表达式,那么正则表达式r | s 可匹配被r 或s 匹 配的任意串。从语言方面来看,r | s 语言是r 语言和s 语言的联合(u n i o n),或L (r | s) = L ® ∪L (s)。以下是一个简单例子:正则表达式a | b匹配了a 或b 中的任一字符,即L (a | b) = L (a) ∪L (b) = {a}∪{b} = {a, b}。又例如表达式a | 匹配单个字符a 或空串(不包括任何字符),也 就是L (a | ) = {a, }。 还可在多个选择对象中选择,因此L(a | b | c | d) = {a, b, c, d} 也成立。有时还用点号表示 选择的一个长序列,如a | b |…| z,它表示匹配a~z 的任何小写字母。

4.连结:
正则表达式r 和正则表达式s 的连结可写作r s,它匹配两串连结的任何一个串,其 中第1个匹配r,第2个匹配s。例如:正则表达式a b只匹配a b,而正则表达式( a | b ) c 则匹配 串ac 和b c(下面将简要介绍括号在这个正则表达式中作为元字符的作用)。

5.重复:(这里特别强调a可以与空串匹配)
在这里插入图片描述
6.优先级和括号的使用:
前面的内容忽略了选择、连结和重复运算的优先问题。例如 对于正则表达式a | b ,是将它解释为( a | b ) 还是a | ( b
) 呢(这里有一个很大的差别,因 为L (( a | b )) = { , a, b, aa, ab, ba, bb, . . . },但L ( a | ( b )) = { , a, b, bb, bbb, . . . } )?标准 惯例是重复的优先权较高,所以第 2个解释是正确的。实际上,在这 3个运算中,*优先权最高,连结其次,| 最末。
因此,a | b c * 就可解释为a | ( b ( c* )),而a b | c * d 却解释为( a b ) | (( c* ) d )。 当需指出与上述不同的优先顺序时,就必须使用括号。这就是为什么用 ( a | b ) c 能表示 选择比连结有更高的优先权的原因。而a | b c 则被解释为与a 或bc 匹配。类似地,没有括号的 ( a | b b )* 应解释为a | b b*,它匹配a、b、b b、b b b. . . 。此处括号的用法与其在算术中类似: (3 + 4) * 5 =35,而3 + 4 * 5 = 23,这是因为* 的优先权比+ 的高。

例子1:在仅有字母表中的三个字符组成的简单字母表∑ = {a, b, c}中,考虑在这个字母表上的仅有包括一个b的集合,用正则表达式表示。

答案:(a|c)*b(a|c)*,尽管b出现在正则表达式中间,但b无需位于被匹配串的正中间。实际上在b之前或之后的a或c的重复或发生不同的次数,如b,abc,abaca,baaaaac等。

例子2:在与上面相同的字母表中,如果集合是包括了最多一个b的所有串,则用正则表达式该如何表示?

答案:(a|c)*(b|ε)(a|c)*;其实答案并不唯一,如也可以写为(a|c)*|(a|c)*b(a|c) ;

例子3:在字母表∑= {a, b}上的串S的集合是由一个b及在其前后有相同数目的a 组成: S = { b, aba, aabaa, aaabaaa, . . . } = { an b an | n≠0 } ,用正则表达式并不能描述这个集合,其原因在于重复运算只能算闭包运算*的一种,它允许有任意次数的重复,如果写出a*ba*,就无法保证a的数量是否相等,它通常表示为“不能计算的正则表达式”。但若要给出一个数学论证,则需要使用有关正则表达式的Pumping引理,这里不再介绍。

例子4:使用正则表达式表达这样的串:在字母表∑= {a, b, c}上的串S中,任意两个b 都不相连,所以在任何两个 b 之间都至 少有一个a 或c。

解答:** 注意(( r * | s * ) *与( r | s ) *所匹配的串相同) **
或许有人会给出这个答案( ( a | c ) | ( b ( a | c ) ) ) * 或者化简得到(a|c|ba|bc)*但这并不是正确的答案,这个正则表达式实际描述的是:没有两个相邻的b,这个答案中并不包括符合要求的b,ab,cb这三个答案,可以为其添加一个可选的结尾来修改:
( a | c | ba | bc ) *(b|ε)

例子5: 本例给出了一个正则表达式,要求用英语简要地描述它生成的语言。若有字母表 ∑ = {a, b, c},则正则表达式:
( ( b | c ) * a ( b | c ) * a ) * ( b | c ) *
生成了所有包括偶数个a 的串的语言。为了看清它,可考虑外层左重复之中的表达式:
( b | c ) * a ( b | c ) * a 它生成的串是以a 结尾且包含了两个a(在这两个a 之前或之间可有任意个b 和c)。重复这些串 则得到所有以a 结尾的串,且a 的个数是2的倍数(即偶数)。在最后附加重复(b | c)(如前 例所示)则得到所需结果。 这个正则表达式还可写作:
(not a
a not a* a)* not a*

2.2.2正则表达式的拓展

1.一个或多个重复

加入有一个正则表达式r,r 的重复是通过标准的闭包运算来描述的,并写作r*。它允许有0个或多个r重复,但0次并非最典型的情况,一次或者多次才是,只要求至少有一个串匹配r,但空串ε不行,这里我们就发明+来代替*完成要求的操作,例如自然数中需要有一个数字序列,且至少要出现一个数字,可以写为( 0 | 1 ) ( 0 | 1 ) * 等价为(0|1)+。

2.任意字符

为字母表中的任意字符进行匹配需要一个通常的状况:无需特别运算,它只要求字母表中的每个字符都列在一个解中。句号“.”表示任意字符匹配的典型元字符,他不要求真正将字母表写出来。利用这个元字符就可以为所有包含了至少一个b的串写出一个正则表达式,如下:
. * b . *

3.字符范围

我们经常需要写出字符的范围,例如所有的小写字母或者所有数字,常见的表示法是利用方括号和一个连字符,如[a~z]表示所有的小写字母,[0-9]表示数字,a|b|c可以写作[a,b,c]。

4.不在给定集合中的任意字符

正如前面的,能够使要匹配的字符集中不包括单个字符很有用,这个可以用“非”来表示,即~,如 ~a在{a,b,c}中就可以表示(b|c)

5.可选的子表达式

有关串的最后一个常见的情况是在特定的串中包括既可能出现又可能不出现的可选部分,例如,数字前的+、-可以没有,就可以用解来表示。
例如:
natural = [0-9]+
signedNatural = natural | + natural | - natural
但这会很快变得麻烦,引入r?来表示由r匹配的串是可选的(或显示r的0个或1个拷贝),上述例子就可以写为:
natural = [0-9]+
signedNatural = (+|-)? n a t u r a l

2.2.3程序设计语言记号的正则表达式

众多程序设计语言汇总记号可以分为以下若干类:
1.保留字,或者叫做关键,3.141,keyword)
2.特殊符号,例如算术运算符,赋值和等式。可以是单个的字符如=,也可以是多个字符++
3.标识符,通常是以字母开头的字母和数字的序列
4.常量或者文字,如43, 3.141592653

1.数

nat = [0-9]+
signedNat=(+|-)? nat
number = signedNat (".",nat) ? (E signedNat) ?(2.71E - 2表示数.027)

2.保留字和标识符

正则表达式中最简单的其实是保留字,它们都是由固定的序列表示的。
例如:
reserved = if | while | do | …

3.注释

注释在扫描过程中一般会被忽略,然而扫描程序必须识别并舍弃它们,例如:
{ this is a Pascal comment }
/* this is a C comment */
为某些语言的注释编写正则表达式并不难,例如Pascal的注释的正则表达式可以是:{ (~} ) * } ,但是例如c++或c的注释/* */并不容易写出正则表达式,这个问题可以通过后续的有穷自动机解决。

4.二义性,空格和先行

在程序设计语言记号使用正则表达式的描述汇总,有一些串经常可被不同的正则表达式匹配。例如:if和while的串,既可以是标识符又可以是关键字,类似的串< >可以被看作大于号小于号,或者单一符号不等于,此时设计语言必须根据无二义性规则回答每一种情况下的含义。

规则:
1.当串可以被认作标识符也可以被当做关键字的时候,认为它是关键字,也就是我们在使用程序设计语言时不能用关键字作为标识符。
2.当串可以是单个记号也可以是若干记号的序列时,通常认为是单个记号,这被称作最长子串原理。
3.使用最长子串原理时出出现的记号分隔符的问题,即表示那些在某时不能代表记号的长串的字符。分隔符应是肯定为其他记号一部分的字符。
例如 xtemp=ytemp中,等号将xtemp分开,这是因为=不能作为标识符的一部分出现,通常空格,新的一行,制表位是记号分隔符,因此 while x… 就解释为两个记号 while 和 x,这是因为空格分隔了while 和 x。在此情况下,定义空白格伪记号非常有用,他与注释伪记号类似,但注释伪记号仅仅是在扫描程序内部区分其它记号,实际上注释本身也会被用来作为分隔符。

程序设计语言中的空白格伪记号的典型定义是:
whitespace = (newline | blank | tab | c o m m e n t) +
请注意:空白格通常不是作为记号分隔符,而 是被忽略掉。指定这个行为的语言叫作自由格式语言( free format)

分隔符结束记号串,但它们并不是记号本身的一部分。因此,扫描程序必须处理先行 (l o o k a h e a d)问题:当它遇到一个分隔符时,它必须作出安排分隔符不会从输入的其他部分中 删除,方法是将分隔符返回到输入串(“备份”)或在将字符从输入中删除之前先行。在大多数 情况下,只有单个字符才需要这样做(“单个字符先行”)。例如在串x t e m p = y t e m p中,当遇 到=时,就可找到标识符x t e m p的结尾,且=必须保留在输入中,这是因为它表示要识别下一 个记号。还应注意,在识别记号时可能不需要使用先行。例如,等号可能是以 =开头的唯一字 符,此时无需考虑下一个字符就可立即识别出它了

2.3有穷自动机

有穷自动机,或者又穷状态机器,是描述特定类型算法的数学方法,特别的,有穷自动机可以用作描述在输入串中识别模式的过程。因此也可以用作构造扫描程序,当然有穷自动机和正则表达式之间有很密切的关系。

下面给出一个实例:
正则表达式为:identifier = letter ( letter | d i g i t) * 它代表以一个字母开头且其后为任意字母和 / 或数字序列的串。
他可以描述为下面的有穷自动机:
在这里插入图片描述
其中圆圈1,2都是表示状态(state),它们表示其中记录已被发现的模式的数量在识别过程中的位置。带有箭头的线表示由一个状态转到另一个状态,该转换依赖于所标字符的匹配。在简单的图示中,1是初始状态,2是接受状态,一旦位于2状态,就可以看到任何数量的字母或数字,实际上接受状态可能不止一个。

2.3.1 确定性有穷自动机的定义

确定性又穷自动机的定义
(这里我们在图示中省略错误匹配的情况,如果考虑出错的有穷自动机应为下列图示)
在这里插入图片描述
如何将正则表达式转换为DFA呢?
在这里插入图片描述
有了有穷自动机对于前面很难用正则表达式表示的c++的注释(//)就可以用有穷自动机表示:
在这里插入图片描述

2.3.2先行,回溯和非确定性自动机

作为根据模式接受字符串的表示算法的一种方法,我们已经学习了 D FA。正如同读者可能 早已猜到的一样,模式的正则表达式与根据模式接受串的 D FA之间有很密切的关系,下一节我 们将探讨这个关系。但首先需要更仔细地学习 D FA表示的精确算法,这是因为希望最终能将这 些算法变成扫描程序的代码。

我们早已注意到 D FA的图表并不能表示出 D FA所需的所有东西而仅仅是给出其运算的要 点。实际上,我们发现数学定义意味着D FA必须使每个状态和字符都具有一个转换,而且这些 导致出错的转换通常是不在D FA的图表中。但即使是数学定义也不能描述出 D FA算法行为的所 有方面。例如在出错时,它并不指出错误是什么。在程序将要到达接受状态时或甚至是在转换 中匹配字符时,它也不指出该行为。

进行转换时进行的典型动作是:将字符从输入串中移到属于单个记号(记号串值或记号词)累积字符的字符串中,在到达某个接收状态时的典型动作则是将刚被识别的记号及相关属性返回,遇到出错状态的典型动作则是在输入中备份(回溯)或生成错误记号。

在关于标识符最早的示例中有许多这里将要描述的行为,所以我们再次回到图 2 - 4中。由 于某些原因,该图中的D FA并没有如希望的那样来自扫描程序的动作。首先,出错状态根本就 不是一个真正的错误,而是表示标识符将不被识别(如来自于初始状态)或是已看到的一个分 隔符,且现在应该接受并生成标识符记号。我们暂时假设(实际这是正确的操作)有其他的转 换可表示来自初始状态的非字母转换。接着指出可看到来自状态 i n _ i d的分隔符,以及应被生成 的一个标识符记号,如图2 - 5所示。

在这里插入图片描述
在该图中,o t h e r转换前后都带有方括号,它表示了应先行考虑分隔字符,也就是:应先 将其返回到输入串并且不能丢掉。此外在该图中,出错状态已变成接受状态,且没有离开接受 状态的转换。因为扫描程序应一次识别一个记号并在每一个记号识别之后再一次从它的初始状 态开始,所以这正是所需要的。

这个新的图示还表述了在2 . 2 . 4节中谈到的最长子串原理:D FA将一直(在状态i n _ i d中) 匹配字母和数字直到找到一个分隔符。与在读取标识符串时允许 D FA在任何地方接受的旧图相 反,我们确实不希望发生某些事情。

现在将注意力转向如何在一开始就到达初始状态的问题上。典型的程序设计语言中都有许 多记号,且每一个记号都能被其自己的D FA识别出来。如果这每一个记号都以不同的字符开头, 则只需通过将其所有的初始状态统一到一个单独的初始状态上,就能很便利地将它们放在一起 了。例如,考虑串:=、< =和=给出的记号。其中每一个都是一个固定串,它们的D FA可写作:

在这里插入图片描述
因为每一个记号都是以不同的字符开始的,故只需通过标出它们的初始状态就可得出以下 的D FA:
在这里插入图片描述
相反地,我们必须做出安排,以便在每一个状态中都有一个唯一的转换。例如下图:

在这里插入图片描述
在理论上是应该能够将所有的记号都合并为具有这种风格的一个巨大的 D FA,但是它非常复杂, 在使用一种非系统性的方法时尤为如此。

解决这个问题的一个方法是将有穷自动机的定义扩展到包括了对某一特定字符一个状态存 在有多个转换的情况,并同时为系统地将这些新生成的有穷自动机转换成 N F A开发一个算法。 这里会讲解到这些生成的自动机,但有关转换算法的内容要在下一节才能提到。

新的有穷自动机称作非确定性有穷自动机( nondeterministic finite automaton)或简称为 N FA。在对它下定义之前,还需要为在扫描程序中应用有穷自动机再给出一个概括的讲法: 转换的概念。

ε- t r a n s i t i o n(ε-转换)是无需考虑输入串(且无需消耗任何字符)就有可能发生的转换,它可以看做是一个空串的“匹配”,空串在前面已讲过是写作ε的,ε-转换在图中的表示就好像ε是一个真正的字符。

不过这不能跟输入中的字符ε的匹配相混淆,如果字母表包括了这样一个字符,就必须与使用ε作为表示ε-转换的元字符相区别。

ε-转换往往与直觉有些矛盾,因为它们可以同时发生,换句话说,就是无需先行和改变到输入串,但它们在两方面很有用,首先,它们可以不用合并状态就表述另一个选择。

例如:记号: =、< =和=的选择可表述为:为每一个记号合并自动机,如下所示:在这里插入图片描述
另外一个优点是它们可以清晰地表示空串的匹配:
在这里插入图片描述
当然,这也就等同于
在这里插入图片描述
下面给出非确定性自动机的定义:它与 D FA的定义很相似,但有一点不同:根据上面所 讨论的,需要将字母表∑扩展到包括了 。将原来写作∑的地方(这假设 最初并不属于∑)改 写成∑∪{ }(即∑和 的并集)。此外还需要扩展T(转换函数)的定义,这样每一个字符都可 以导致多个状态,通过令T的值是状态的一个集合而不是一个单独的状态就可以做到它。例如 下示的图表:
在这里插入图片描述
在这里插入图片描述

2.3.3用代码实现有穷自动机(略)

2.4从正则表达式到DFA

这里的内容教材上描述较为繁琐,确实概括起来就只有三点:
1.由正则表达式到NFA;
2.从NFA到DFA;
3.DFA的最小化;
也就分别涉及三种算法,比起教材的刻板定义描述,我在另一篇博客中找到了更好理解的部分,网址如下:
https://www.jianshu.com/p/de84d27264cc

2.5TINY扫描程序的实现(略)

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值