词法分析器原理简介

词法分析器读取有字符串组成的输入流,并产生包含单词的输出流,每个单词都标记了其语法范畴(syntactic category)或类型,等效于英文单词的词类。为了完成这种聚集和分类操作,词法分析器会应用一组描述输入程序设计语言的词法结构(也称微语法,microsyntax)的规则。程序设计语言的微语法规定了如何将字符组合为单词,以及反过来如何分开混合在一起的各个单词。

如何识别单词的

考虑识别关键字new的问题,首先检查第一个字符是否为n,再次检查是否后接e,最后e是否后接w。在每一步,未能匹配到适当的字符都将导致不能识别单词,编译器此时会输出一个错误信息并返回失败。整个对new关键字的识别可用转移图(transition diagram)来表示。

词法分析器原理简介

该转移图表示了一个识别器。每个圆圈都表示计算中的一个抽象状态(status)。为了方便起见,每个状态都标记起来。初始状态或起始状态为S0S_0,我们始终将起始状态标记为S0S_0。状态S3S_3是接受状态,仅当输入为new时,识别器才会到达S3S_3。接受状态以双层圆圈绘制,如上图所示。箭头表示根据输入字符的不同,所发生的状态之间的转移。如果识别器始于状态S0S_0,然后读取了字符n、e和w,那么最终会转移到状态S3S_3

使用同样的方法为while和not构建识别器,将产生的识别器合并后的转移图,如下:

词法分析器原理简介

识别器的形式化

转移图还可以看做是形式化的数学对象,成为有限自动机,它定义了识别器的规格。形式上,有限自动机(FA)是一个五元组(SΣδS0SA)( S,\Sigma,\delta,S_0,S_A),其中各分量的含义如下:

  • SS是识别器中的有限状态集,以及一个错误状态SeS_e
  • Σ\Sigma是识别器使用的有限字母表。通常,Σ\Sigma是转移图中边的标签的合集。
  • δ(si,c)\delta(s_i, c)是识别器的转移函数。它将每个状态siSs_i \in S和每个字符cΣc \in \Sigma的组合(si,c)(s_i, c)映射到下一个状态。在状态sis_i遇到输入字符cc,FA将采用转移δ(si,c)\delta(s_i, c)转移函数。
  • S0SS_0 \in S是指定的起始状态。
  • SAS_A是接受状态的集合,SASS_A \subseteq SSAS_A中的每个状态都在转移图中表示为双层圈。

我们可以将识别new/not/while的FA形式如下:

S=S0,S1,S2,S3,S4,S5,S6,S7,S8,S9,S10,SeS = {S_0, S_1, S_2, S_3, S_4, S_5, S_6, S_7, S_8, S_9, S_{10}, S_e}

Σ=e,h,i,l,n,o,t,w\Sigma = {e, h, i, l, n, o, t, w}

δ={\delta = \{
S0(n)S1,S_0(n) \rightarrow S_1,
S0(w)S6,S_0(w) \rightarrow S_6,
S1(e)S2,S_1(e) \rightarrow S_2,
S1(o)S4,S_1(o) \rightarrow S_4,
S2(w)S3,S_2(w) \rightarrow S_3,
S4(t)S5,S_4(t) \rightarrow S_5,
S6(h)S7,S_6(h) \rightarrow S_7,
S7(i)S8,S_7(i) \rightarrow S_8,
S8(l)S9,S_8(l) \rightarrow S_9,
S9(e)S10S_9(e) \rightarrow S_{10}
}\}

S0=S0S_0=S_0

SA={S3,S5,S10}S_A=\{ S_3, S_5, S_{10} \}

对于状态sis_i和输入字符cc的所有其他组合,我们定义δ(si,c)=Se\delta(s_i, c)=S_eSeS_e是指定的错误状态。这种五元组与转移图是等价的,给出一种表示,我们可以轻易地重建另一种表示。转移图就是描绘了对应FA的一副图画。

并且仅当字符串从S0S_0开始时,FA接受字符串Str,该字符串中的字符序列可以使FA经历一系列状态转移,在处理整个字符串后FA到达某个接受状态。

更形式化地讲,如果字符串Str有字符c1,c2,c3,...,cnc_1, c_2, c_3, ..., c_n组成,那么FA (S,Σ,δ,S0,SA)(S, \Sigma, \delta, S_0, S_A)接受Str的充分必要条件是:

δ(...δ(δ(δ(S0,c1),c2),c3)...,cn)SA\delta( ... \delta(\delta(\delta(S_0, c_1), c_2), c_3)..., c_n) \in S_A

确定性有限自动机(Deterministic Finite Automaton,DFA)和非确定性有限自动机(Nondeterministic Finite Automaton,NFA)

手工构建的FAFA都不包括ϵ\epsilon(空字符串),但一些正则表达式(Regular Expression,RE)确实用到了ϵ\epsilon。在FAFAϵ\epsilon发挥什么作用?我们可以使用针对ϵ\epsilon输入的转移来合并FAFA,并组成用于更复杂RE的FA。例如,假定我们有用于m和n的两个RE的FAFA,分别称为FAmFA_mFAnFA_n。如下图所示:

词法分析器原理简介

FAmFA_m的接受状态添加一个针对输入的ϵ\epsilon的转移,转移到FAnFA_n的初始状态,把各个状态重新编号,然后使用FAnFA_n的接受状态作为最新FAFA的接受状态,这样构建用于处理mnmnFAFA。如下图所示:

词法分析器原理简介

随着ϵ\epsilon转移的引入,“接受”的定义必须稍微改变一下,以允许输入字符串中任何两个字符串之间出现一次或多次ϵ\epsilon转移。例如,在状态s1s_1,该FAFA会采用转移s1(ϵ)s2s_1(\epsilon) \rightarrow s_2,而不会消耗任何字符。这是一个较小的变更,但看起来颇为合乎直觉。目测显示我们可以合并s1s_1s2s_2,以消除ϵ\epsilon转移。如下图:

词法分析器原理简介

利用ϵ\epsilon转移合并两个FAFA,可能会使我们关于FAFA工作方式的模型复杂化。考虑用于语言aa^*ababFAFA

词法分析器原理简介

我们可以用ϵ\epsilon转移合并它们,形成一个处理aaba^*abFAFA

词法分析器原理简介

实际上,ϵ\epsilon转移的引入,使得在状态s0s_0遇到输入字符aa时,FAFA可以选择两种不同的转移。它可以采用转移s0(a)s0s_0(a) \rightarrow s_0,或采用两个转移:s0(ϵ)s1s_0(\epsilon) \rightarrow s_1s1(a)s2s_1(a) \rightarrow s_2。哪种转移是正确的?考虑字符串aabaababab。这个FAFA应该可以接受这两个字符串。

对于aabaab,它应该采用的转移是:
s0(a)s0s0(ϵ)s1s1(a)s2s2(b)s3 s_0(a) \rightarrow s_0,s_0(\epsilon) \rightarrow s_1,s_1(a) \rightarrow s_2,s_2(b) \rightarrow s_3
对于abab,应该采用的转移是:
s0(ϵ)s1s1(a)s2s2(b)s3 s_0(\epsilon) \rightarrow s_1,s_1(a) \rightarrow s_2,s_2(b) \rightarrow s_3,
正如这两个字符串所说明的,从状态s0s_0采取的正确转移,取决于aa之后的字符。在每一步,FAFA都会检查当前字符。FAFA的状态隐含了左上下文(left context),即它已经处理过了的那些字符。因为FAFA必须在检查下一个字符之前进行转移,所以诸如s0s_0这样的状态,背离了我们对顺序算法行为的观念。如果一个FAFA包含了s0s_0这样的状态,即对单个输入字符有多种可能的转移,则称为非确定性有限自动机(Nondeterministic Finite Automaton,NFA。相反如果FAFA中每个状态对任一输入字符都具有唯一可能的转移,则称为确定性有限自动机(Deterministic Finite Automaton,DFA)

Note:

  1. NFA,允许在空串输入ϵ\epsilon上进行转移的FAFA,其状态对同一个字符输入可能存在多种转移。
  2. DFA,转移函数在单值的FAFA称为DFADFADFADFA不允许ϵ\epsilon转移。

为了更加清楚NFA的语义,我们需要一组规则来描述其行为。在历史上,针对NFA的行为,已经给出了两个不同的模型。

  1. 每次NFA必须进程非确定性选择时,如果有使得输入字符串转向接受状态的转移存在,则采用这样的转移。使用“全知”NFA的这种模型颇有吸引力,因为它(表面上)维护了DFA那种定义明确的接受机制。本质上,NFA在每个状态都需要猜测正确的转移。
  2. 每次NFA必须进行非确定性选择时,NFA都克隆自身,以追踪每个可能的转移。因而,对于一个给定的输入字符,NFA实际上是处于一个特定的状态集合,其中每个状态都是NFA的某个可能来处理。在这种模型中,NFA并发地追踪所有转移路径。在任一时刻,存在NFA克隆副本活动状态的那些集合称为NFA的配置。当NFA到达一个配置,此时NFA已经耗尽输入字符串,且配置中的一个或多个克隆副本处于某个接受状态,则NFA接受该输入字符串。

Note:

  1. NFA的配置:NFA上并发活动状态的集合。

在上述任一模型中,NFA(S,Σ,δ,s0,SA)NFA(S, \Sigma, \delta, s_0, S_A)接受一个输入字符串x1,x2,x3...xkx_1, x_2, x_3...x_k的充分必要条件是:至少存在一条穿越转移图的路径,起始于状态s0s_0,结束于某个接受状态skSAs_k \in S_A,且从头至尾沿该路径前进时,其上各个转移边的标签能够与输入字符串匹配(忽略标签为ϵ\epsilon的边)。换言之,第ii条边的标签必须是xix_i。这种定义与NFA行为的两种模型都是一致的。

NFA和DFA的等价性

NFA和DFA在表达力上是等价的。任何DFA都是某个NFA的一个特例。因而,NFA至少像DFA一样强大。任何NFA都可以通过一个DFA模拟,通过子集构造法可以确立的。

与NFA等价的DFA包含的状态可能是NFA的指数倍。虽然DFA中状态的集合SDFAS_{DFA}可能比较庞大,但它是有限的。此外这个DFA对每个输入符号任然只进行一次状态转移。因而,模拟NFA的DFA,其运行实践任然与输入字符串的长度成正比。在DFA上模拟NFA可能有潜在的空间问题,而不是时间问题。

因为NFA和DFA是等价的,我们可以为aaba^*ab构建一个DFA,如下图:

词法分析器原理简介

这依赖于下述事实:aaba*abaabaa*b所定义的单词集合是相同的。

参考

《Engineering a Compiler》

相关文章: