词法与文法简介

喜欢而已 提交于 2020-01-15 19:27:05

https://zhuanlan.zhihu.com/p/19878087

https://segmentfault.com/a/1190000015568992

词法简介

所谓的源代码,我们可以将其视为一段长长的字符串。所谓字符串,即是字符的有序集。但是,字符本身作为编译器的输入单位,粒度实在太小了,因此,我们往往需要对编译器进行第一层封装,即分割出一个称之为 Tokenizer (词法分析器)的部分。

Tokenizer 的作用即是将字符序列翻译成 Token(单词)的一个过程,这一过程称之为单词化(Tokenization)。很容易理解单词化这一步骤在整个编译过程中的价值,举个例子,如下这么一个英语句子。

It’s understandable that we share some common values as we are living in the same world.

实际上,这个句子倘若以字符串的形式存在,即以字符作为最小单位来解析,则看起来形式如下。实际上,我们很难从中提取出有价值的信息。

['I', 't', '\'', 's', ' ', 'u', 'n', 'd', 'e', 'r', 's', 't', 'a', 'n',
 'd', 'a', 'b', 'l', 'e', ' ', 't', 'h', 'a', 't', ' ', 'w', 'e', ' ', 
's', 'h', 'a', 'r', 'e', ' ', 's', 'o', 'm', 'e', ' ', 'c', 'o', 'm', 
'm', 'o', 'n', ' ', 'v', 'a', 'l', 'u', 'e', 's', ' ', 'a', 's', ' ', 
'w', 'e', ' ', 'a', 'r', 'e', ' ', 'l', 'i', 'v', 'i', 'n', 'g', ' ', 
'i', 'n', ' ', 't', 'h', 'e', ' ', 's', 'a', 'm', 'e', ' ', 'w', 'o', 
'r', 'l', 'd' ]

而分词话的作用,则是将上面这段东西,变成(至少)下面这段东西。

["It", "is", "understandable", "that", "we", "share", "some",
 "common", "values", "as", "we", "are", "living", "in", "the",
"same", "world", "."]

看起来是不是更加顺眼了呢?实际上我们都几乎能读出这个单词数组所代表句子的意思了。

词法分析器 Tokenizer 的另一个功能在于,将单词分类。考虑源代码中这么一行。

private int index = 27;

会被拆分为如下形式。

["private", " ", "int", " ", "index", " ", "=", " ", "27", ";"]

但仅仅是把源代码的字符分割成段,这些字符串尚不能称之为完整的单词Token,而只能作为单词的语素。实际上,词法分析器还对将单词分类。因此,读到的语素,分析出的类型,两者才构成一个完整的单词。

实际上,词法分析器会为这行代码生成如下形式。

语素 private int index = 27 ;
类型 关键字 空白 数值类型 空白 标识符 空白 运算符 空白 整数 分隔符

每一列代表一个单词Token,而单词包含两个属性,语素、类型。

词法分析阶段会用到以下几个数学工具:

  • 正则表达式(Regular Expression, RE)
  • 确定性有限状态自动机(DFA)
  • 非确定性有限状态自动机(NFA)

文法简介

所谓文法,即是用于描述语言的一种工具

例如,一个赋值语句可能写成如下形式:

variable = 1 + 3

如何充分定义这个赋值语句的形式呢?若用自然语言描述,我可以说,

赋值语句最左边是一个标示符,然后紧接一个“=”符号,然后再接一个表达式。满足这个条件的,即是赋值语句。

S → abE

用符号来描述的话,就是如上形式,这种形式称之为 S 的产生式

S:赋值语句

a:一个标示符,对应一个token,token类型为identifier(标识符)

b:是“=”符号,对应一个 token,token类型为=

E:是一个表达式,无token与之对应,非终结符,需要进一步描述表达式

这里用到了S、a、b、E 四个不同的字母。

等等,似乎还有什么没说完,因为标示符(字母a表示)与“=”符号(字母b表示)都与 Tokenizer 生成的 Token 对应,但是表达式(字母E表示)却没有对应的 Token 呀。

于是,我还要进一步描述表达式。这里为了不让问题变得过于繁琐,我先假定表达式只出现加减号和数字。那么表达式的定义如下。

E → d | E+d | E-d

这里出现的“|”表示“或”,这表明表达式(字母E)可以展开成三种不同的式子。同时,E 展开后的式子中可能再次出现 E 本身,这种递归形式足以涵盖任意长度的表达式形式。

于是,我们又得到字母 d,d 表示一个数字(也与某种 Token 对应)。

至此,我们一共得到了 S、a、b、E、d 五个不同的字母,其中 a、b、d 都与 Token 对应。然而,虽然 S、E 却没有对应的 Token,但它们都有至少有一个属于自己的产生式。

对于 a、b、d,称之为终结符。即它们不会再有自己的产生式了。而 S、E,称之为非终结符

当我们为式子中某个非终结符挑选一个特定的产生式,并用产生式的右边部分代替这个非终结符在式子中的位置,那么我们将这个过程称之为非终结符的展开

考虑下面这行代码:

index = 15 + 7 - 3

其形如 abd+d-d(a 为 “index”、b 为"="、d 为"15", “7”, “3”)

对于 S 有如下展开方式:

S → abE (S:赋值语句,a为变量标识符,b为=标识符,E为表达式)

→ abE-d (展开 E → E-d)

→ abE+d-d (展开 E → E+d)

→ abd+d-d (展开 E → d)

其中 S 表示一个赋值语句。既然 S 存在某种展开方式,其形式与这行代码完全相同,我们说,这行代码与 S 是匹配的。对于 Parser 而言,即可断定这行语句是一个赋值语句。

因此,Parser 读取语言的文法定义。然后,通过找到一个展开方案以匹配源代码。而这个展开方案中对各个非终结符产生式的选择过程,即是对源代码中每一个部分的定性过程。这个过程让 Parser 能够理解源代码各个部分表示的含义,并以此生成对应的语法树(Syntax Tree)

LL(1)文法

在简单介绍 LL(1) 文法之前,我还要说明一种比较特别的产生式。

A → ε

希腊字母 ε 表示“空”,这个产生式表明非终结符 A 可以产生一个空。

具体来说,如果有如下文法。

S → αAβ

A → ε

那么:

S → αβ

非终结符可以产生空这一特性,令文法的形式更加复杂,但是却是必不可少的。

此外,对于一个文法之中的非终结符,还有 FIRST 集、FOLLOW 集的概念。

  • 对于一个非终结符 A 而言,它的 FIRST 集指 A 可能展开的各种形式中,位于第一的所有终结符所组成的集合。记为 FIRST(A)。
  • 而 FOLLOW 集,指在整个文法中,非终结符 A 后面可能接的所有终结符组成的集合。记为 FOLLOW(A)。

这么描述可能有点绕,细节先不管,但有一点很重要,即无论是FIRST集还是FOLLOW集,它们都只能包含终结符

那么,LL(1) 又是怎样一种文法呢?

对于一个文法而言,如果它的每一个非终结符的产生式

A → α | β | γ ……

都满足如下三个条件,则将这个文法称之为 LL(1) 文法。

对于所有不能导出 ε 的表达式 α、β、γ……,都有,FIRST(α)、FIRST(β)、FIRST(γ)……两两互不相交。

最多只有一个表达式可以导出 ε

如果有一个表达式可以导出 ε,那么对于其他不可以导出 ε 的表达式 ξ,有,FIRST(ξ) ∩ FOLLOW(A) = Φ

α:Alpha,中文读音为“阿尔法”,音标/lf/

β:beta,中文读音为“贝塔” ,音标/'beit/

δ:delta,中文读音为“得尔塔”,音标/'delt/

ε:epsilon,中文读音为“艾普西隆",音标/ep’silon/

为什么要引入FIRST集的概念?

因为有公共左因子的问题,公共左公因子是指在文法的产生式集合中,某个非终结符的多个候选式具有相同的前缀。

一般来说,公共左公因子的产生式为

A→αβ1│αβ2A→αβ1│αβ2

如果有公共左因子的问题,那么只能采取试探的方法来分析每一个候选式,分析的过程很可能产生回溯,回溯分析法是一种不确定的方法。

若所有候选式都没有公共左因子就可以选择惟一匹配的候选式,不会产生(公共左公因子引起的)回溯。

为了消除回溯,对任何一个非终结符和当前的待匹配符号,期望

  • 要么只有一个候选式可用
  • 要么没有候选式可用

编译原理之first集,follow集,select集解析

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!