编译原理之美
实现脚本语言
- 词法分析:把程序分割成一个个 Token 的过程,可以通过构造有限自动机来实现
- 语法分析:把程序的结构识别出来,并形成一棵便于由计算机处理的抽象语法树。可以用递归下降的算法来实现
- 语义分析:消除语义模糊,生成一些属性信息,让计算机能够依据这些信息生成目标代码
词法分析器
实现一个词法分析器,首先需要写出每个词法的正则表达式,并画出有限自动机,之后,只要用代码表示这种状态迁移过程就可以了
有限状态自动机 ,分别是字符集、状态集、初始状态、终结状态(集)、转移函数
- 确定状态有限自动机 DFA:对任意字符,最多有一个状态可以转移, 是终结状态
- 非确定状态有限自动机 NFA:对任意字符,有多于一个状态可以转移, 是终结状态集
1Thompson 算法 子集构造算法 Hopcraft 最小化算法2RE ------------> NFA ----------> DFA -----------------> 最小化 DFA ------> 词法分析器代码
RE -> NFA
e -> 𝛆
e -> c
e -> e1 e2
e -> e1 | e2
e -> e1*
由上面构造 a(b|c)*
得
NFA -> DFA
1// eps_closure 可用 BFS 或 DFS 实现2q0 <- eps_closure(n0) // q0 = {n0}3Q <- {q0} // Q = {q0}4workList <- q0 // workList = [q0, ...]5while(workList != []) 6 remove q from workList // workList = [...]7 foreach(character c) // c = a8 t <- e-closure(delta(q,c)) // delta(q0, a) = {n1}, t = {n1, n2, n3, n4, n6, n9}9 D[q,c] <- t // q1 = t10 if(t not in Q) // Q = {q0, q1} , workList = [q1]11 add t to Q and workList
a(b|c)*
进行转换可得
q1 = {n1, n2, n3, n4, n6, n9}
q2 = {n5, n8, n9, n3, n4, n6}
q3 = {n7, n8, n9, n3, n4, n6}
图像:
Hopcraft 最小化算法将 DFA 转化为最小化 DFA
1//基于等价类的思想2split(S)3 foreach(character c)4 if(c can split s)5 split s into T1, ..., Tk67hopcroft()8 split all nodes into N, A9 while(set is still changes)10 split(s)
先分为非终结状态 N: {q0, q1, q2, q4}
和终结状态 A: {q3, q5}
,在 N 中 q0 和 q1 在接受 e 的条件下最终得到的状态还是在 N 的内部。所以可以将其根据 e 拆分成 {q0, q1}
,{q2, q4}
,{q3, q5}
。q0 和 q1 ,在接受 e 的时候,q0 最终得到还是在 {q0, q1}
这个状态的结合中,q1 却会落在 {q2, q4}
的状态中,所以可以将 q0 和 q1 分为 {q0}
,{q1}
语法分析器
上下文无关文法
上下文无关文法 G 是一个四元组:
- T 是终结符集合
- N 是非终结符集合
- P 是一组产生式规则集合
- S 是唯一的开始符号(非终结符,S ∈ N)
1E -> num2 | id3 | E + E4 | E * E56T = {num, id, +, *}7N = {E}8P = {E -> num, E -> id, E -> E + E, E -> E * E}9S = E
推导
- 最左推导:每次总是选择最左侧的符号进行替换
- 最右推导:每次总是选择最右侧的符号进行替换
语法分析器的任务:语法分析器从记号流中获取句子 S,再接受使用上下文无关文法 G 描述的语言的语言规则,回答在 G 中是否存在对 S 的推导。是或者否。并给程序员反馈精确的出错信息
分析树
- 推导可以表达成树状结构
- 和推导所用的顺序无关(最左、最右、其它)
- 特点:
- 树中的每个内部节点代表非终结符
- 每个叶子节点代表终结符
- 每一步推导代表如何从双亲节点生成它的直接孩子节点
1E -> num2 | id3 | E + E4 | E * E56推导 3 + 4 * 578E -> E + E E9 -> 3 + E E + E10 -> 3 + E * E 3 E * E11 -> 3 + 4 * e 4 512 -> 3 + 4 * 51314E -> E * E E15 -> E + E * E E * E16 -> 3 + E * E E + E 517 -> 3 + 4 * E 3 418 -> 3 + 4 * 51920得到两种树的结构,分析树的含义取决于树的**后序遍历**的顺序,所以第一个是先乘后加,第二个是先加后乘,导致二义性
二义性文法
重写解决
1E -> E + T2 | T3T -> T * F4 | F5F -> num6 | id78推导 3 + 4 * 5 如下910E -> E + T E11 -> T + T E + T12 -> F + T T T * F13 -> 3 + T F F 514 -> 3 + T * F 3 415 -> 3 + F * F16 -> 3 + 4 * F17 -> 3 + 4 * 51819推导 3 + 4 + 5 如下2021E -> E + T E22 -> E + T + T E + T23 -> T + T + T E + T F24 -> F + T + T T F 525 -> 3 + T + T F 426 -> 3 + F + T 327 -> 3 + 4 + T28 -> 3 + 4 + F29 -> 3 + 4 + 53031使用了左递归的方式(E -> E + T,T -> T * F ),从而使得该句子的计算顺序是 (3+4)+5,保证了加法的左结合性
算法
- 回溯算法(自顶向下)
- 递归下降分析算法:O(n),容易实现(方便手工编码),错误定位和诊断信息准确,GCC4、LLVM 使用
1S -> N V N2N -> s3 | t4 | g5 | w6V -> e7 | d89parse_S()10 parse_N()11 parse_V()12 parse_N()1314parse_N()15 token = tokens[i++]16 if(token == s || token == t || token == g || token == w)17 return;18 error("...")1920parse_V()21 token = tokens[i++]22 ... //
一般框架:
1X -> β11 ... β1i2 | β21 ... β2j3 | β31 ... β3k4 | ...56parse_X()7 token = nextToken()8 switch(token)9 case ...: // β11 ... β1i10 case ...: // β21 ... β2j11 case ...: // β31 ... β3k12 ...13 default: error("...");
LL(1) 分析算法
从左(L)向右读入程序,最左(L)推导,采用一个(1)前看符号
与回溯算法不同的在于,又一个分析表进行优化,时间复杂度 O(n)
如何构造分析表就是关键,分别求出 NULLABLE 集合、FIRST 集合、FOLLOW 集合,然后推出 SELECT 集合,即预测分析表
NULLABLE 集合
1NULLABLE = {}2while (NULLABLE is still changing)3 for (production p: X -> β)4 if (β == 𝛆)5 NULLABLE ⋃= {X}6 if (β == Y1Y2...Yn)7 if (Y1 ∈ NULLABLE && ... && Yn ∈ NULLABLE)8 NULLABLE ⋃= {X}FIRST 集合
1for (noterminal N)2 FIRST(N) = {}34while (some set is still changing)5 for (production p: N -> β1β2...βn)6 for (βi from β1 upto βn)7 if (βi == a)8 FIRST(N) ⋃= {a}9 break10 if (βi == M)11 FIRST(N) ⋃= FIRST(M)12 if (M is not in NULLABLE)13 breakFOLLOW 集合
1for (noterminal N)2 FOLLOW(N) = {}34while (some set is still changing)5 for (production p: N -> β1β2...βn)6 tmp = FOLLOW(N)7 for (βi from βn downto β1)8 if (βi == a)9 tmp = {a}10 if (βi == M)11 FOLLOW(M) ⋃= tmp12 if (M is not in NULLABLE)13 tmp = FIRST(M)14 else15 tmp ⋃= FIRST(M)SELECT 集合
1for (production p)2 SELECT(p) = {}34for (production p: N -> β1β2...βn)5 for (βi from β1 upto βn)6 if (βi == a)7 SELECT(p) ⋃= {a}8 return9 if (βi == M)10 SELECT(p) ⋃= FIRST(M)11 if (M is not in NULLABLE)12 return13 SELECT(p) ⋃= FOLLOW(N)
例子:
1p0: Z -> d2p1: | X Y Z3p2: Y -> c4p3: | 𝛆5p4: X -> Y6p5: | a78NULLABLE = {Y, X}910N\FIRST 0 1 211Z {} {d} {d, c, a}12Y {} {c} {c}13X {} {c, a} {c, a}1415N\FOLLOW 0 116Z {} {}17Y {} {d, c, a}18X {} {d, c, a}1920 p0 p1 p2 p3 p4 p521SELECT {d} {d, c, a} {c} {d, c, a} {d, c, a} {a}2223预测分析表24 a c d25Z p1 p1 p0,p126Y p3 p2,p3 p327X p4,p5 p4 p4
得到的预测分析表有的会有多个,引起冲突,导致算法的不确定性
1p0: E -> T E'2p1: E' -> + T E'3p2: | 𝛆4p3: T -> F T'5p4: T' -> * F T'6p5: | 𝛆7p6: F -> ( E )8p7: | a910NULLABLE = {E', T'}1112FIRST(E) = FIRST(T) = FIRST(F) = {(, a}13FIRST(E') = {+, 𝛆}14FIRST(T') = {*, 𝛆}1516FOLLOW(E) = FOLLOW(E') = {), #}17FOLLOW(T) = FOLLOW(T') = {+, ), #}18FOLLOW(F) = {*, +, ), #}1920预测分析表21 a + * ( ) #22E p0 p023E' p1 p2 p224T p3 p325T' p5 p4 p5 p526F p7 p6
解决冲突
消除左递归
1E -> E + T // 左递归情况下,E 能推导出 E 开头的,导致冲突2 | T34// T + T + T ... + T,把 `+ T` 看作一体,重新构建成右递归的形式56E -> T E'7E' -> + T E'提取左公因子
1X -> a Y2 | a Z // 都是 a 开头,导致冲突34X -> a X'5X' -> Y6 | Z