词法分析器原理简介
词法分析器读取有字符串组成的输入流,并产生包含单词的输出流,每个单词都标记了其语法范畴(syntactic category)或类型,等效于英文单词的词类。为了完成这种聚集和分类操作,词法分析器会应用一组描述输入程序设计语言的词法结构(也称微语法,microsyntax)的规则。程序设计语言的微语法规定了如何将字符组合为单词,以及反过来如何分开混合在一起的各个单词。
如何识别单词的
考虑识别关键字new的问题,首先检查第一个字符是否为n,再次检查是否后接e,最后e是否后接w。在每一步,未能匹配到适当的字符都将导致不能识别单词,编译器此时会输出一个错误信息并返回失败。整个对new关键字的识别可用转移图(transition diagram)来表示。
该转移图表示了一个识别器。每个圆圈都表示计算中的一个抽象状态(status)。为了方便起见,每个状态都标记起来。初始状态或起始状态为,我们始终将起始状态标记为。状态是接受状态,仅当输入为new时,识别器才会到达。接受状态以双层圆圈绘制,如上图所示。箭头表示根据输入字符的不同,所发生的状态之间的转移。如果识别器始于状态,然后读取了字符n、e和w,那么最终会转移到状态。
使用同样的方法为while和not构建识别器,将产生的识别器合并后的转移图,如下:
识别器的形式化
转移图还可以看做是形式化的数学对象,成为有限自动机,它定义了识别器的规格。形式上,有限自动机(FA)是一个五元组,其中各分量的含义如下:
- 是识别器中的有限状态集,以及一个错误状态。
- 是识别器使用的有限字母表。通常,是转移图中边的标签的合集。
- 是识别器的转移函数。它将每个状态和每个字符的组合映射到下一个状态。在状态遇到输入字符,FA将采用转移转移函数。
- 是指定的起始状态。
- 是接受状态的集合,。中的每个状态都在转移图中表示为双层圈。
我们可以将识别new/not/while的FA形式如下:
对于状态和输入字符的所有其他组合,我们定义,是指定的错误状态。这种五元组与转移图是等价的,给出一种表示,我们可以轻易地重建另一种表示。转移图就是描绘了对应FA的一副图画。
并且仅当字符串从开始时,FA接受字符串Str,该字符串中的字符序列可以使FA经历一系列状态转移,在处理整个字符串后FA到达某个接受状态。
更形式化地讲,如果字符串Str有字符组成,那么FA 接受Str的充分必要条件是:
确定性有限自动机(Deterministic Finite Automaton,DFA)和非确定性有限自动机(Nondeterministic Finite Automaton,NFA)
手工构建的都不包括(空字符串),但一些正则表达式(Regular Expression,RE)确实用到了。在中发挥什么作用?我们可以使用针对输入的转移来合并,并组成用于更复杂RE的FA。例如,假定我们有用于m和n的两个RE的,分别称为和。如下图所示:
在的接受状态添加一个针对输入的的转移,转移到的初始状态,把各个状态重新编号,然后使用的接受状态作为最新的接受状态,这样构建用于处理的。如下图所示:
随着转移的引入,“接受”的定义必须稍微改变一下,以允许输入字符串中任何两个字符串之间出现一次或多次转移。例如,在状态,该会采用转移,而不会消耗任何字符。这是一个较小的变更,但看起来颇为合乎直觉。目测显示我们可以合并和,以消除转移。如下图:
利用转移合并两个,可能会使我们关于工作方式的模型复杂化。考虑用于语言和的。
我们可以用转移合并它们,形成一个处理的。
实际上,转移的引入,使得在状态遇到输入字符时,可以选择两种不同的转移。它可以采用转移,或采用两个转移:和 。哪种转移是正确的?考虑字符串和。这个应该可以接受这两个字符串。
对于,它应该采用的转移是:
对于,应该采用的转移是:
正如这两个字符串所说明的,从状态采取的正确转移,取决于之后的字符。在每一步,都会检查当前字符。的状态隐含了左上下文(left context),即它已经处理过了的那些字符。因为必须在检查下一个字符之前进行转移,所以诸如这样的状态,背离了我们对顺序算法行为的观念。如果一个包含了这样的状态,即对单个输入字符有多种可能的转移,则称为非确定性有限自动机(Nondeterministic Finite Automaton,NFA
。相反如果中每个状态对任一输入字符都具有唯一可能的转移,则称为确定性有限自动机(Deterministic Finite Automaton,DFA)
。
Note:
- NFA,允许在空串输入上进行转移的,其状态对同一个字符输入可能存在多种转移。
- DFA,转移函数在单值的称为。不允许转移。
为了更加清楚NFA的语义,我们需要一组规则来描述其行为。在历史上,针对NFA的行为,已经给出了两个不同的模型。
- 每次NFA必须进程非确定性选择时,如果有使得输入字符串转向接受状态的转移存在,则采用这样的转移。使用“全知”NFA的这种模型颇有吸引力,因为它(表面上)维护了DFA那种定义明确的接受机制。本质上,NFA在每个状态都需要猜测正确的转移。
- 每次NFA必须进行非确定性选择时,NFA都克隆自身,以追踪每个可能的转移。因而,对于一个给定的输入字符,NFA实际上是处于一个特定的状态集合,其中每个状态都是NFA的某个可能来处理。在这种模型中,NFA并发地追踪所有转移路径。在任一时刻,存在NFA克隆副本活动状态的那些集合称为NFA的配置。当NFA到达一个配置,此时NFA已经耗尽输入字符串,且配置中的一个或多个克隆副本处于某个接受状态,则NFA接受该输入字符串。
Note:
- NFA的配置:NFA上并发活动状态的集合。
在上述任一模型中,接受一个输入字符串的充分必要条件是:至少存在一条穿越转移图的路径,起始于状态,结束于某个接受状态,且从头至尾沿该路径前进时,其上各个转移边的标签能够与输入字符串匹配(忽略标签为的边)。换言之,第条边的标签必须是。这种定义与NFA行为的两种模型都是一致的。
NFA和DFA的等价性
NFA和DFA在表达力上是等价的。任何DFA都是某个NFA的一个特例。因而,NFA至少像DFA一样强大。任何NFA都可以通过一个DFA模拟,通过子集构造法可以确立的。
与NFA等价的DFA包含的状态可能是NFA的指数倍。虽然DFA中状态的集合可能比较庞大,但它是有限的。此外这个DFA对每个输入符号任然只进行一次状态转移。因而,模拟NFA的DFA,其运行实践任然与输入字符串的长度成正比。在DFA上模拟NFA可能有潜在的空间问题,而不是时间问题。
因为NFA和DFA是等价的,我们可以为构建一个DFA,如下图:
这依赖于下述事实:与所定义的单词集合是相同的。
参考
《Engineering a Compiler》