词法分析
如果要手动地实现词法分析器,首先建立起每个词法单元的词法结构图或其他描述会有所帮助。然后,我们可以编写代码来识别输入中出现的每个词素,并返回识别到的词法单元的有关信息。
词法分析器的作用
词法分析是编译的第一阶段。
词法分析器的主要任务是读入源程序的输入字符、将它们组成词素,生成并输出一个词法单元序列,每个词法单元对应于一个词素。
这个词法单元序列输出到语法分析器进行语法分析。词法分析器通常还要和符号表进行交互。当词法分析器发现一个标识符的词素时,它要将这个词素添加到符号表中。在某些情况下,词法分析器会从符号表中读取有关标识符种类的信息,以确定向语法分析器传送哪个词法单元。
通常,交互是由语法分析器调用词法分析器来实现的。在交互时,语法分析器通过调用词法分析器的 getNextToken 命令从它的输入中不断读取字符,直到它识别出一个词素为止。词法分析器根据这个词素生成下一个词法单元并返回给语法分析器。
除了识别词素之外,词法分析器还可以与源程序做交互。
交互一:过滤掉源程序中的注释和空白(空格、换行符、制表符以及在输入中用于分隔词法单元的其他字符);
交互二:将编译器生成的错误消息与源程序的位置联系起来。
词法分析可以分为两个级联的处理阶段:
扫描阶段主要负责完成一些不需要生成词法单元的简单处理,比如删除注释和将多个连续的空白字符压缩成一个字符。
词法分析阶段:处理扫描阶段的输出并生成词法单元。
词法分析和语法分析
编译过程的分析部分划分为词法分析和语法分析阶段有以下几个原因:
最主要是简化编译器的设计。将词法分析和语法分析分离通常使我们至少可以简化其中一项任务。
提高编译器的效率。把词法分析器独立出来使我们能够使用专用于词法分析任务、不进行语法分析的技术。此外,我们可以使用专门的用于读取输入字符的缓冲技术来显著提高编译器的速度。
增强编译器的可移植性。输入设备相关的特殊性可以被限制在词法分析器中。
词法单元、模式和词素
词法单元由一个词法单元名和一个可选的属性组成。词法单元名是一个表示某种词法单位的抽象符号,比如一个特定的关键字,或者代表一个标识符的输入字符序列。
模式描述了一个词法单元的词素可能具有的形式。当词法单元是一个关键字时,它的模式就是组成这个关键字的字符序列。对于标识符和其他词法单元,模式是一个更加复杂的结构,它可以和很多符号串匹配。
词素是源程序中的一个字符序列,它和某个词法单元的模式匹配,并被词法分析器识别为该词法单元的一个实例。
在很多程序设计语言中,下面的类别覆盖了大部分词法单元:
1.每个关键字有一个词法单元。一个关键字的模式就是该关键字本身。
2.表示运算法的词法单元。它可以表示单个运算法,也可以表示一类运算符。
3.一个表示所有标识符的词法单元。
4.一个或多个表示常量的词法单元,比如数字和字面值字符串。
5.每一个标点符号有一个词法单元,比如左右括号、逗号和分号。
词法单元的属性
如果有多个词素可以和一个模式匹配,那么词法分析器必须向编译器的后续阶段提供有关被匹配词素的附加信息。
词法错误
假设出现所有词法单元的模式都无法和剩余输入的某个前缀相匹配的情况,此时词法分析器就不能继续处理输入。当出现这种情况时,最简单的错误恢复策略是“恐慌模式”恢复。我们从剩余的输入中不断删除字符,知道词法分析器能够在剩余输入的开头发现一个正确的词法单元为止。
可能采取的其他错误恢复动作包括:
1.从剩余的输入中删除一个字符
2.向剩余的输入中插入一个遗漏的字符
3.用一个字符来替换另一个字符
4.交换两个相邻的字符
输入缓冲
我们将介绍一种双缓冲区方案。这种方案能够安全地处理向前看多个符号的问题。然后我们将考虑一种改进方法。这种方法使用“哨兵标记”来节约用于检查缓冲区末端的时间。
缓冲区对
由于在编译一个大型源程序时需要处理大量的字符,处理这些字符需要很多的时间,因此开发了一些特殊的缓冲技术来减少用于处理单个输入字符的时间开销。一种重要的机制就是利用两个交替读入的缓冲区。
每个缓冲区的容量都是 N 个字符,通常 N 是一个磁盘块的大小,如 4096 字节。我们可以用系统读取命令一次将 N 个字符读入到缓冲区中,而不是每读入一个字符调用一次系统读取命令。如果输入文件中的剩余字符不足 N 个,那么就会有一个特殊字符(用 eof 表示)来标记源文件的结束。这个特殊字符不同于任何可能出现在源程序中的字符。
程序为输入维护了两个指针:
1.lexemeBegin 指针:该指针指向当前词素的开始处。当前我们正试图确定这个词素的结尾。
2.forward 指针:它一直向前扫描,直到发现某个模式被匹配为止。
哨兵标记
我们每次向前移动 forward 指针时,都必须检查是否到达了缓冲区的末尾。若是,那么我们必须加载另一个缓冲区。因此每读入一个字符,我们需要做两次检测:一次是检查是否到达缓冲区的末尾,另一次是确定读入的字符是什么。如果我们扩展每个缓冲区,使他们在末尾包含一个“哨兵”字符,我们就可以把对缓冲区末端的测试和对当前字符的测试合二为一。这个哨兵符合必须是一个不会在源程序中出现的特殊字符,一个自然的选择就是字符 eof。
词法单元的规约
正则表达式是一种用来描述词素模式的重要表示方法。虽然正则表达式不能表达出所有可能的模式,但是它们可以高效的描述在处理词法单元时要用到的模式类型。
串和语言
字母表是一个有限的符号集合。符号的典型例子包括字母、数位和标点符号。
某个字母表上的一个串(string)是该字母表中符号的一个有穷序列。在语言理论中,术语“句子”和“字”常常被当做“串”的同义词。串 s 的长度,通常记作 |s|,是指 s 中符号出现的次数。空串是长度为 0 的串。
语言(language)是某个给定字母表上一个任意的可数的串集合。
串的各部分的术语
串的前缀(prefix)是从 s 的尾部删除 0 个或多个符号后得到的串。
串的后缀(suffix)是从 s 的开始处删除 0 个或多个符号后得到的串。
串 s 的子串(substring)是删除 s 的某个前缀和后缀之后得到的串。
串 s 的真前缀、真后缀、真子串分别是 s 的既不等于 € 也不等于 s 本身的前缀、后缀和子串。
串 s 的子序列(subsequence)是从 s 中删除 0 个或者多个符号后得到的串,这些被删除的符号可能不相邻。例如:baan是banana的一个子序列。
语言上的运算
在词法分析中,最重要的语言上的运算是并、连接和闭包运算。
运算 定义和表示
L 和 M 的并 L ∪ M = { s | s 属于 L 或者 s 属于 M }
L 和 M 的连接 LM = { st | s 属于 L 且 t 属于M }
L的 Kleene 闭包
L 的正闭包
正则表达式
正则表达式可以描述所有通过对某个字母表上的符号应用这些运算符而得到的语言。
正则表达式可以由较小的正则表达式按照如下规则递归地构建。每个正则表达式 r 表示一个语言L(r)这个语言也是根据 r 的子表达式所表示的语言递归地定义的。下面的规则定义了某个字母表 ∑ 上的正则表达式以及这些表达式所表示的语言。
归纳基础:如下两个规则构成了归纳基础:
1.€ 是一个正则表达式,L(€) = {€},即该语言只包含空串。
2.如果 a 是 ∑ 上的一个符号,那么 a 是一个正则表达式。并且L(a) = { a }。也就是说,这语言仅包含一个长度为 1 的符号串 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。
正则定义
为方便表示,我们可能希望给给某些正则表达式命名,并在之后的正则表达式中像使用符合一样使用这些名字。
d1 → r1 d2 → r2 ... dn → rn
其中
每个 di 都是一个新符号,它们都不在 ∑ 中,并且各不相同。
每个 ri 是字母表 ∑ ∪ { d1,d2,...,di-1 } 上的正则表达式。
正则表达式的扩展
1.一个或多个实例。单目后缀运算符 + 表示一个个正则表达式及其语言的正闭包。
2.零个或一个实例。单目后缀运算符 ? 的意思是“零个或一个出现”。
3.字符类。一个正则表达式 a | b | c | ... | z 可以缩写为[abc...z],也就是说[abc]是a|b|c的缩写,[a-z]是a|b|...|z的缩写。