编译原理 第一章 词法分析

词法分析器的做用

词法分析器(scanner), 在第〇章咱们已经说过它的做用: 识别全部标识符并对其标记属性信息. 这篇内容讲解这个过程是如何实现的. git

首先, 先引入三个概念: , 单词模式. 就是咱们一般说的字符串, 是一个一维的逻辑结构, 按照某个特定顺序存储了一些符号; 单词就是符合某一模式的串; 那么什么是模式呢? 简单的说就是字符的造成规则. 好比在 C 中, 咱们知道变量的名字由字母, 数字, 以及下划线组成, 且数字不能够出如今首位. 除此以外, 变量名不能够与保留字相同(好比不能够"int for = 3;"). 这就是一条规则. 根据这个模式(规则)咱们能够识别出给定字符串的若干字串, 这些字串即是知足这一模式的单词. 正则表达式

有了上面的概念, 咱们即可以进一步描述词法分析器的任务: 根据约定的模式, 识别出串中的单词, 并标记其属性信息(好比识别出一个数字常量, 它的属性多是它的存储类型(可能涉及类型转换)以及数字的值, 识别出一个操做符; 它的属性多是它表明的操做含义;).编程

举个例子, 咱们有串"float aaa = bb 12.23". 咱们有这样的几个规则(并不是C标准), "float 是类型名, 指的是浮点数", "变量名由字母组成", " 是操做符, 指的是乘法", "数字常量由数字(+ "." + 数字)". 通过词法分析器, 咱们应该能够获得这样的记号流: "<TYPE, FLOAT> <ID, "aaa"> <OP, SET> <ID, "bbb"> <OP, MUL> <NUM, FLOAT, "12.23">". 编程语言

示例1.1

一般在识别单词的时候, 可能会发现有些串有二义性. 也就是说同一个串可能符合多个模式. 好比, "while"是 C 的保留字, 可是个人变量名能够定义为"while_count_ge_0". 顺便说一句, 词法分析器在分析串的时候是一个字符一个字符读入的. 那么它就可能在读取到"while"后立刻标记它为"保留字,循环语句标志"; 亦或是在识别串中的">="时, 可能在识别">"后立刻断定这个是"操做符, 大于"而不是"大于或等于". 这个时候就须要超前搜索, 即知足某一模式后, 仍继续读入字符, 直到不知足任何模式才作标记, 而不是在第一次知足模式后就立刻标记.函数

读到这, 你可能以为词法分析器很强大. 不知你有没有"debug 弹出的 warning 和 error 都是词法分析器给出的" 这样的错觉. 事实上词法分析器只能区分不多的错误. 好比在识别某一模式时, 不能准确地落在某一标记上. 好比在 C 中写了这样的东西 "a = 12.ab3". 在词法分析器读取小数点"."后, 它期待下一个字符要么是数字, 要么是空格分号之类的断句符号. 但接下来读入的字母令它始料未及, 它无法继续作标记, 这个时候就会抛出错误. 词法分析器也只能给出这样的错误信息. 至于函数未声明, 变量未定义等其余语法文法的错误, 词法分析器是不会明白的. 工具

示例1.2

词法分析器的实现

逻辑上,咱们要实现的词法分析器其实是对咱们对于语言模式要求的实现. 这句话看着有点绕, 简单的解释就是, 咱们想要根据某些模式对串中的单词作出区别与标记, 词法分析器就是要帮咱们完成这项工做. 为了形式化地描述模式, 咱们须要了解正规式(正则表达式).spa

正规式

坊间有个段子: "没有人能学会正则表达式". 咱们在这里介绍的也只是正规式最基本的语法成分, 不过用来实现词法分析器是足够的.翻译

首先咱们有一张字母表, 所谓字母表, 就是咱们须要匹配的符号的集合, 用 Σ 表示. 下面介绍一些必要的正规式知识. debug

ε 表示空串, 匹配的结果什么都没有的空串.
a (假设 a ∈ Σ, a 在字母表中) 表示匹配 a 这个字母
a? 表示匹配 a 或不匹配 a
a* 表示匹配 a 0次或若干次, 等价于{ε, a, aa, aaa, aaaa, ...}
a+ 表示匹配 a 1次或屡次, 和 a* 惟一的区别在于它不接受空串 ε
a|b 表示这一位匹配的字符是 a 或 b
ab 表示按顺序匹配两个字符 a 和 b, 有时候也不省略中间的点乘符号 a·b设计

有了这些知识, 咱们就能够用正规式构造逻辑上的词法分析器了. 好比, 咱们要描述 C 语言中对数字的表示模式.

示例1.3

这个表达式定义了 C 中对于十进制数字常量的模式要求(假定不会有空串). digit 是数字符号集合, dot 是小数点. e 就是字母 e. 先看表达式前半部分的含义: (有或没有(正号或者负号))(0个或多个数字)(有或没有(小数点及(0个或多个数字))). 用人话来讲, 就是能够匹配多位数字, 也能够匹配带小数点的浮点数. 能够没符号, 也能够有正负号. 因为 C 中容许将 "0.123" 写成 ".123", 因此前面的 digit 用的是 "*" 而不是 "+". 同理, C 中一样容许 "123." 表示 "123", 故小数点后面也能够没有数字.

后半部分就是科学计数法的匹配, 要匹配一个字母 e, 剩下的和前面同样. 一样是由于数字常量能够不使用科学计数法, 所以这部分也无关紧要.

若是要匹配变量名:

示例1.4

若是咱们不考虑保留字的状况, 这就是变量名的模式. underscore 是下划线, letter 是英文字母集合, unl 就是带有下划线的字母集. digit 一样表示数字集. 这样的话, 变量名的模式就是, 首字母必定不能是数字(至少匹配一个 unl 中的字母), 后面的字母既能够来自 unl, 又能够来自 digit. 且数目能够为 0, 1, 也能够不少(不考虑变量名长度的限制).

有限状态自动机

前文说过, 词法分析器每次仅处理串中的一个字母. 这不由让咱们联想到状态自动机模型: 由初态出发, 每次根据读入的一个字符判断下一个状态位置, 以此类推, 直到停机, 落在的那个状态表示什么就意味着这个串序列表示的是什么. 若是没法正常停在某个终态, 则发生错误. (好比前面的 "12.ab3", 读到字母 a, 自动机会不知道下一个状态是什么, 形成非正常停机.)

不肯定状态自动机 NFA

为了进一步用更具体的手段实现词法分析器, 咱们须要将正规式表示成自动机. 所幸正规式能够机械化地翻译成不肯定状态自动机(NFA). 具体实现方法叫作 "Thopmson 方法". 翻译的规则以下:

感谢画图工具的友情支持

咱们用单圈表示状态, 双圈表示终态. 初始状态只能有一个, 通常标记为 S, 而终态可能有不少.

示例1.6

对于第一个, 空串的模式表示为"什么都没有就能够从初态到终态", 由于初态即终态. 第二个表示识别一个字母 a, 在初态接受这个字母就能够到达终态, 完成识别.

我知道是很丑啦...

a|b 就意味着不管是 a 仍是 b 都能到达终态; a* 就是能够不少 a 或者没有. 同理 a+ 指的是至少有一个 a. (没有写 a?, 由于 a? 等价于 a|ε)

举个例子, 好比对于正规式 0(0|1)1+, 意思是, 首位必须是 0, 第二位必须是 0 或 1, 后面至少有一个 1 的单词. 画成 NFA 是这样:

忽然学会曲线怎么画

不知道你对 NFA 的名字是否感受很困惑, 为何叫"不肯定状态"呢? 下面举这样的例子.

正规式 0+(0|1)1 的 NFA :

示例1.9

经过观察这个 NFA, 你会发现一个很是诡异的现象: 假设你在状态 1, 接下来读进一个数字符号'0', 你是继续停留在状态 1, 仍是转移到状态 2 ? 若是再下一个符号是 1, 你可能知道须要转移到状态 2. 但别忘了, 咱们的词法分析器每次仅读进一个字符, 若是这个例子中的(0+)匹配特别长的0串, 咱们是否是须要使用缓冲区, 预先读取特别多的字符, 仅仅用来判断第二步跳转! 这显然是很是不现实的. 还有就是NFA 中可能涉及空串匹配, 也就是说一个状态可能平白无故就跳到其余状态. 固然你能够作出约定, 要求你们不要写这么奇怪的正规式. 但咱们有更好的解决方案: 肯定状态自动机(DFA).

(注: 真正的 NFA 比这个复杂, 由于他要自行添加不少空串匹配, 为了避免影响阅读, 这里的 NFA 作了适当的简化.)

肯定状态自动机 DFA

知道了 NFA 的缺陷, 咱们但愿咱们设计的 DFA, 在每读取一个字符后就能够找到本身的下一个状态. 所以, 咱们须要将 NFA 翻译成 DFA. 这里用的方法叫作 "子集构造法".

简单来说, 咱们先将初态以及它用空串匹配就能够到达的一切状态, 做为初始状态集. 将这个状态集看作一个独立的状态, 它根据本身集合中元素的全部状态的匹配规则找到规则和目标状态, 获得的目标状态再根据"可由空串匹配到达的一切状态", 找到这样的状态集合. 以此类推, 作出新的状态迁移图. 新的状态图中, 若某状态表示的老状态集合中包含终态, 那么这个状态也是终态(显然能够有不少).

上面的文字可能不是很好理解, 咱们举个例子.

如今咱们有这样的 NFA :

画这张图的时候, 买平板的欲望如此强烈(惋惜没钱)

咱们能够看出, 这张图表示的正规式是 ((a(a|b)b)?a)+

先构造初态集合, 从初态 S 咱们能够用空串匹配走到状态 1, 4, 因此初态集合是{S, 1, 4}.由初态集合的每一个顶的转移规则, 咱们先看初态集合根据匹配 a, 能够到达状态 {1, 5}, 再用空串匹配, 找到初态集合匹配 a 后到达的状态集合 {1, 5, F}. 以此类推, {S, 1, 4} 根据匹配 b 能够获得空集. 因而咱们再从 {1, 5, F} 出发, 使用 a 匹配获得 {2}, 使用 b 匹配获得空集. 再从 {2} 出发, 经过 a 或 b 获得 {3}; 再从 {3} 出发经过 b 获得 {4}; 再从 {4} 经过 a 获得 {1, 5, F}.

画成 DFA :

示例1.11

固然, 你能够将状态集合更名, 改为新的状态, 而不是一个个集合.

到这里, 咱们由正规式拿到了肯定状态自动机 DFA. DFA 用编程语言实现起来就简单多了.

(后续工做还有 DFA 的简化, 将 DFA 用相似于子集构造法查找能够合并的状态, 减小复杂度)

2017.9.13Osinovsky

相关文章
相关标签/搜索