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/
因为有公共左因子的问题,公共左公因子是指在文法的产生式集合中,某个非终结符的多个候选式具有相同的前缀。
一般来说,公共左公因子的产生式为
A→αβ1│αβ2A→αβ1│αβ2
如果有公共左因子的问题,那么只能采取试探的方法来分析每一个候选式,分析的过程很可能产生回溯,回溯分析法是一种不确定的方法。
若所有候选式都没有公共左因子就可以选择惟一匹配的候选式,不会产生(公共左公因子引起的)回溯。
为了消除回溯,对任何一个非终结符和当前的待匹配符号,期望
- 要么只有一个候选式可用
- 要么没有候选式可用
来源:CSDN
作者:ljhabc1982
链接:https://blog.csdn.net/LJHABC1982/article/details/103991298