编译器概述
编译器的核心功能是把源代码翻译成目标代码。
- 编译器结构
- 一个典型的编译程序通常由哪些部分组成?
- 画出编译程序的总体结构图。
- 各部分的主要功能是什么?
每个阶段将源程序从一种表示转换成另一种表示:
- 词法分析器:字符流->单词流
- 语法分析器:单词流->语法树
- 语义分析器:
- 收集标识符的属性信息:
- 类型(Type)
- 种属(Kind)
- 存储位置、长度
- 值
- 作用域
- 参数和返回值信息
- 语义检查:
- 变量或过程未经声明就使用
- 变量或过程名重复声明
- 运算分量类型不匹配
- 操作符与操作数之间的类型不匹配
- 收集标识符的属性信息:
- 中间代码生成器:抽象语法树->中间表示(与平台无关的抽象程序):
- 易于产生
- 易于翻译成目标程序
- 三地址码:temp1=c*d;temp2=b+temp1;a=temp2
- 四元式:(op, arg1, arg2, result);(* , c , d , temp1);(+ , b, temp1 , temp2);(= , temp2 , - , a)
- 代码优化器:试图改进中间代码,以产生执行速度较快的机器代码:
- temp1=c*d;temp2=b+temp1;a=temp2
- change to:temp1=c*d;a=b+temp1
- 代码生成器:生成可重定位的机器代码或汇编代码:
- temp1=c*d;a=b+temp1
- change to:Mov R2,c;Mul R2, d;Mov R1, b;Add R2, R1;Mov a, R2
- 一个重要任务是为程序中使用的变量合理分配寄存器
- 符号管理表:
- 基本功能是记录源程序中使用的标识符,
- 并收集与每个标识符相关的各种属性信息,
- 并将它们记载到符号表中。
- 错误处理器:
- 处理方式:报告错误,应继续编译
- 大部分错误在语法分析、语义分析阶段检测出来
- 词法分析:字符无法构成合法单词
- 语法分析:单词流违反语法结构规则
- 语义分析:语法结构正确,但无实际意义
程序设计语言及其文法
符号串及其运算
串:是字母表中符号的一个有穷序列。
∑ 是一个字母表,任意 x∈∑*,x 是 ∑ 上的一个串。
串的长度、空串、连接、幂。
文法定义
- 文法是用于描述语言的语法结构的形式规则
- 任何一种语言都有它自己的文法
- 一个文法所描述的语言是唯一的
- 文法可以定义为一个四元组(VT, VN, S, P)
- 一个终结符号集合VT
- 一个非终结符号集合VN
- 一个产生式集合P ,定义语法范畴产生式:A → α
- 一个特定的非终结符——开始符号S
- 文法所描述的语言是由文法的开始符号推出的所有终结符号串的集合
- 设计语言的正则表达式
- 能被5整除的10进制整数:[1-9][0-9]*(0|5)|0|5(!!!)
- C 语言标识符:[_a-zA-Z][_a-zA-Z0-9]*
- C 语言无符号数的集合:
- digit → [0-9]
- digits → digit+
- number → digits (.digits)? (E(+|-)? digits)?
词法分析
任务
- 识别源程序中的单词是否有误
- 读入源程序字符流、组成词素,输出词法单元序列
- 过滤空白、换行、制表符、注释等
- 将词素添加到符号表中
主要内容
- 正则表达式:描述词素模式的重要表示方法
- 状态转换图是正则表达式的一种表示
- 将正则表达式转换为状态转换图
- 词法分析器的构造方法
- 有限自动机(NFA/DFA, 五元组、转换图、转换表)
- 从正则表达式到自动机(RE -> NFA、RE->DFA、NFA->DFA)
- 编写词法分析程序(RE -> NFA <Thompson构造法>、NFA->DFA <子集构造法>)
- 优化词法分析程序
- 正则表达式直接构造DFA
- DFA状态最小
- 转换表压缩
词法分析器的构造
正则表达式 → 构造NFA(Thompson构造法)
构造规则:注意开始的箭头和结束的双圈
- N(ε)
- N(a)
- N(s|t)
- N(st)
- N(s*)
NFA → 转换DFA(子集构造法)
- ε-closure(0)={0,1,2,4,7}=A
- ε-closure(δ(A,a)) = ε-closure(δ({0,1,2,4,7},a))
= ε-closure({3,8})
= {1,2,3,4,6,7,8} = B
∴ Dtran[A,a] = B
ε-closure(δ(A,b)) = ε-closure(δ({0,1,2,4,7},b))
= ε-closure({5}) = {1,2,4,5,6,7} = C
∴ Dtran[A,b] = C - ε-closure(δ(B,a)) = ε-closure(δ({1,2,3,4,6,7,8},a))
= ε-closure({3,8})
= {1,2,3,4,6,7,8} = B
∴ Dtran[B,a] = B
ε-closure(δ(B,b)) = ε-closure(δ({1,2,3,4,6,7,8},b))
= ε-closure({5,9})
= {1,2,4,5,6,7,9} = D
∴ Dtran[B,b] = D - ε-closure(δ(C,a)) = ε-closure(δ({1,2,4,5,6,7},a))}
= ε-closure({3,8})
= {1,2,3,4,6,7,8} = B
∴ Dtran[C,a] = B
ε-closure(δ(C,b)) = ε-closure(δ({1,2,4,5,6,7},b))
= ε-closure({5})
= {1,2,4,5,6,7} = C
∴ Dtran[C,b] = C - ε-closure(δ(D,a)) = ε-closure(δ({1,2,4,5,6,7,9},a))}
= {1,2,3,4,6,7,8} = B
∴ Dtran[D,a] = B
ε-closure(δ(D,b)) = ε-closure(δ({1,2,4,5,6,7,9},b))}
= {1,2,4,5,6,7,10} = E
∴ Dtran[D,b] = E - ε-closure(δ(E,a)) = ε-closure(δ({1,2,4,5,6,7,10},a))}
= {1,2,3,4,6,7,8} = B
∴ Dtran[E,a] = B
ε-closure(δ(E,b)) = ε-closure(δ({1,2,4,5,6,7,10},b))}
= {1,2,4,5,6,7} = C
∴ Dtran[E,b] = C - 最终结果:
最小化DFA状态
- 初始,两个组:终态与非终态
- 划分方法,对于状态组A={s1,s2,…,sk}
- 对符号a,得到其转换状态t1,t2,…,tk
- 若t1,t2,…,tk属于不同状态组,则需将A对应划分为若干组
语法分析
任务
- 构造语法分析树
- 语法错误和修正
主要内容
- 上下文无关文法(描述语言的语法结构的形式规则)
- 推导(最左推导和最右推导)
- 语法分析树
- 语法分析器的类型
- 自顶向下分析器(递归下降分析,LL(1))
- 自底向上分析器(LR(K)分析法:LR(0) ,SLR,规范LR,LALR)
推导和归约
- 推导:描述文法定义语言的过程
- 归约:某产生式体相匹配的特定子串被替换为该产生式头部的非终结符号
- 推导:将产生式用作重写规则
- 由开始符号起始
- 每个步骤将符号串转换为另一个符号串
- 转换规则:利用某个产生式,将符号串中出现的其左部NT替换为其右部符号串
- 最左推导:总替换最左边的NT
- 最右推导:总替换最右边的NT
- 例题:给出句子id+id的最右推导过程
- 文法
- E → TE’
- E’ → +TE’|ε
- T → FT’
- T’ → *FT’|ε
- F → ( E ) | id
- 最右推导过程
- E → TE’ → T+TE’ → T+T → T+FT’ → T+F → T+id → FT’+id → F+id → id+id
语法分析器的类型
在自顶向上的语法分析方法中,分析的关键是:选择候选式
在自底向上的语法分析方法中,分析的关键是:寻找句柄
自顶向下分析器:递归下降法,LL(1)
自底向上分析器:LR()
LL(1):构造预测分析表
计算FIRST和FOLLOW函数
- FIRST
FIRST(X1 X2 … Xn ) = FIRST (X1) “+”
FIRST(X2) if ε is in FIRST(X1) “+”
FIRST(X3) if ε is in FIRST(X2) “+”
…
FIRST(Xn) if ε is in FIRST(Xn-1)
注意:仅当对所有i,e∈FIRST(Xi),才将e加入FIRST(X1 X2 … Xn)
- FOLLOW
为输入串结束标记
若A → αBβ,则FIRST(β)中符号除ε外,均加入FOLLOW(B)
若A → αB 或 A → αBβ 且 β →* e,FOLLOW(A)中所有符号加入FOLLOW(B)
应用构造算法
- 对所有的终结符a ∈ FIRST(α),将 A->α 加入M[A,a]
- 若ε ∈ FIRST(α),对所有的终结符b ∈ FOLLOW(A),将 A->α 加入M[A,b]
- 所有未定义的表项设置为错误
e.g. id+id*id
LR
自底向上分析器LR(k):能识别几乎所有上下文无关文法描述的程序设计语言结构。
LR分析表种类:
- LR(0):局限性大,但它是建立其它分析表的基础
- SLR (1) :易实现,功能比LR(0)稍强些
- LR(1) :分析能力最强,但实现代价高
- LALR (1) :功能介于SLR(1)和LR(1)之间,适用于大多数程序设计语言的结构,并且可以比较有效地实现。同心集
构造 LR(0)分析表
- 写出给定文法G的增广文法G’并编号
- 构造识别可行前缀的DFA
- 根据DFA构造LR(0)分析表
LR(0)示例
- 示例文法
- 示例文法的分析表
构造 SLR(1)分析表
解决同一项目集中的移进-归约冲突。
对于冲突项目集 Ii ={A→β1·bβ2,B→β·,C→β·},如果集合FOLLOW(B)和FOLLOW(C)不相交,而且不包含b,那么,当状态Ii面临任何输入符号a时,可采用如下“移进—归约”的决策。
①当a=b时,则移进,置ACTION[i,a]=Sj
②当a∈FOLLOW(B)时,置ACTION [i,a]=rj
③当a∈FOLLOW(C)时,置ACTION [i,a]=rm
④当a不属于三种情况之一,置ACTION [i,a]=“ERROR”
一般地,若一个项目集 Ii 含有多个移进项目和归约项目,例如
Ii={A1→α·a1β1,A2→α·a2β2,…,Am→α·amβm,B1→α·,B2→α·,…,Bn→α·}
如果集合{a1,a2,…,am},FOLLOW(B1),FOLLOW(B2),… FOLLOW(Bn)两两不相交时,可类似地根据不同的当前符号,对状态为i中的冲突动作进行区分。这种解决“移进—归约”冲突的方法称作SLR方法。
SLR(1)文法:对于给定的文法G,若按上述方法构造的分析表不含多重定义的元素,则称文法G是SLR(1)文法。
SLR(1)示例
设有文法G
E→E+T|T
T→T*F|F
F→(E)|id
构造该文法SLR(1)分析表。
① 将文法G增广为G′,同时对每一产生式进行编号
(0)S′→E
(1)E→E+T
(2)E→T
(3)T→T*F
(4)T→F
(5)F→(E)
(6)F→id
②对G′构造文法LR(0)项目集规范族如下:
③ 取这些项目集作为各状态,并根据转换函数GO画出识别文法G′的有穷自动机,
④ 用SLR方法解决“移进—归约”冲突。
在十二个项目集中, I1、 I2 和 I9 都含有“移进—归约”冲突,其解决办法是:
对于项目集 I1 ={S′→E·,E →E·+T},由于集合 FOLLOW(S′)={$}与集合{+}不相交,所以当状态为1时,面临着输入符号为+时便移进,而面临着输入符号为$时,则按产生式S′→E归约。对于项目集 I2 ={E→T·,T→T·*F},由于集合 FOLLOW(E)={+,),$}与集合{*}不相交,因此状态2面临输入符号为*时移进,而面临输入符号为+或)或$时,按产生式E→T归约。对于项目集I9 ={E →E+T·,T →T·*F},同样由于 FOLLOW(E) = { +, ), $ }与集合{*}不相交,因此状态9面临着输入符号为*时移进,面临着输入符号为+或)或$ 时,按产生式E→E+T归约。
⑤ 构造SLR(1)分析表
- 输入串为id+id*id为例,给出LR分析器的分析过程如下表:
步骤 | 状态栈 | 符号栈 | 输入串 | 分析动作 | 下一状态 |
---|---|---|---|---|---|
1 | 0 | $ | id+id*id$ | S5 | 5 |
2 | 05 | $id | +id*id$ | r6 | GOTO[0,F]=3 |
3 | 03 | $F | +id*id$ | r4 | GOTO[0,T]=2 |
4 | 02 | $T | +id*id$ | r2 | GOTO[0,E]=1 |
5 | 01 | $E | +id*id$ | S6 | 6 |
6 | 016 | $E+ | id*id$ | S5 | 5 |
7 | 0165 | $E+id | *id$ | r6 | GOTO[6,F]=3 |
8 | 0163 | $E+F | *id$ | r4 | GOTO[6,T]=9 |
9 | 0169 | $E+T | *id$ | S7 | 7 |
10 | 01657 | $E+T* | id$ | S5 | 5 |
11 | 016575 | $E+T*id | $ | r6 | GOTO[7,F]=10 |
12 | 0165710 | $E+T*F | $ | r3 | GOTO[6,T]=9 |
13 | 0169 | $E+T | $ | r1 | GOTO[0,E]=1 |
14 | 01 | $E | $ | acc |
语法制导翻译
- 语法制导的翻译:一种形式化的语义描述方法
- 语法制导定义(SDD) :每个文法产生式有一组基于文法符号的属性的语义规则。
- 属性:综合属性与继承属性
- 语法制导的翻译方案(SDT):是SDD的实现。在文法中嵌入语义动作,当归约时,执行语义动作
- S-属性的SDT,L-属性的SDT
继承属性和综合属性
判断一个属性是继承属性还是综合属性
综合属性(synthesized attribute):
- 在分析树结点 N 上的非终结符 A 的综合属性只能通过 N 的子结点或 N 本身的属性值来定义。
- 终结符可以具有综合属性。终结符的综合属性值是由词法分析器提供的词法值,因此在SDD中没有计算终结符属性值的语义规则。
继承属性(inherited attribute)
- 在分析树结点 N 上的非终结符 A 的继承属性只能通过 N 的父结点、N 的兄弟结点或 N 本身的属性值来定义。
- 终结符没有继承属性。终结符从词法分析器处获得 的属性值被归为综合属性值。
继承属性和综合属性的计算在产生式中所在的位置
- 将计算某个非终结符号A的继承属性的动作插入到产生式右部中紧靠在A的本次出现之前的位置上
- 将计算一个产生式左部符号的综合属性的动作放置在这个产生式右部的最右端
语法制导定义
语法制导定义是一种接近形式化的语义描述方法。语法制导定义的表示分两部分:
- 先针对语义为文法符号设置属性,
- 然后为每个产生式设置语义规则,来描述各属性间的关系。
S-属性定义与L-属性定义
仅仅使用综合属性的 SDD 称为 S 属性的 SDD。
如果一个SDD是S属性的,可以按照语法分析树节点的任何自底向上顺序来计算它的各个属性值。
L-属性定义的直观含义:在一个产生式所关联的各属性之间,依赖图的边可以从左到右,但不能从右到左。即 A->X1X2……Xn 中 Xi 的每个继承属性仅依赖于:
- A 的继承属性
- Xi 左边的符号X1,……Xi-1 的属性
- Xi 本身的属性,但是不能在依赖图中形成环路。
每个S-属性定义都是L-属性定义。
把L-属性定义变换成翻译模式,在构造翻译程序的过程中前进了一大步。
示例
示例1:实现桌上计算器的后缀SDT
L → En { print (E.val); }
E → E1 + T { E.val = E1 .val + T.val;}
E → T { E.val = T.val;}
T → T1 * F { T.val = T1.val * F.val ;}
T → F { T.val = F.val; }
F → (E) { F.val = E.val; }
F → digit { F.val = digit.lexval; }
示例2:给出下列文法的SDT
为文法
S → ( L ) | a
L → L , S | S
写一个翻译方案,它输出每个a的嵌套深度。例如,对于( a , ( a , a) ),输出的结果是1 2 2。
S’→ {S. depth = 0 } S
S → {L. depth = S. depth + 1 } ( L )
S → a {print (S. depth) }
L → {L1. depth = L. depth }L1 , {S. depth = L. depth }S
L → {S. depth = L. depth }S
示例3:根据下列文法的SDT
有文法G及其语法制导翻译如下所示:(语义规则中的*和+为运算符乘号和加号)
E → E1^T{E.val=E1.val * T.val}
E → T{E.val= T.val}
T → T1#n{T.val=T1.val +n.val}
T → n{T.val= n.val}
采用自底向下分析时,句子3∧3#4其值为:21
中间代码生成
任务
- 把经过语法分析和语义分析而获得的源程序中间表示翻译为中间代码表示。
主要内容
- 中间表示(语法树和三地址码)
- 类型和声明
- 表达式的翻译、控制语句的翻译、布尔表达式的翻译
语法分析树,抽象语法树,有向无环图(DAG)
抽象语法树(或者简称为语法树)反映了抽象的语法结构,而语法分析树(分析树)反映的是具体的语法结构。语法树是分析树的抽象形式或压缩形式。
- ((a)+(b))的语法分析树和抽象语法树
- 有向无环图
三地址码的实现
四元式 op, arg1, arg2, result
三元式 op, arg1, arg2
三元式可以避免引入临时变量,使用获得变量值的位置来引用前面的运算结果。
间接三元式 间接码表+三元式表
包含一个指向三元式的指针列表,而不是列出三元式序列本身。
类型表达式示例
int[2][3] array(2,array(3,integer))
int *f(char a, char b) (char×char) → pointer(integer)
typedef struct person={
char name[8];
int sex;
int age;
}
struct person table[50];
person record( (name × array(8,char)) × (sex × integer) × (age × integer))
table array(50,person)
运行环境
任务
- 为源程序中命名的对象分配安排存储位置
- 确定目标程序访问变量时使用的机制
- 过程之间的连接
- 参数传递
主要内容
- 存储管理:静态分配、动态分配(栈分配、堆管理)
- 对变量、数据的访问
静态和动态存储
- 静态分配
- 编译器在编译时刻就可以做出存储分配决定,不需要考虑程序运行时刻的情形
- 全局变量
- 动态分配
- 栈式存储:和过程的调用/返回同步进行分配和回收,值的生命期和过程生命期相同
- 堆存储:数据对象比创建它的过程调用更长寿
代码生成
任务
- 根据中间表示生成代码
- 指令选择
- 寄存器分配和指派
- 指令排序
主要内容
- 静态/栈式数据区分配
- 基本块相关的代码生成
- 窥孔优化
- 简单的代码生成算法
代码优化
任务
- 通过一些相对低层的语义等价转换来优化代码
主要内容
- 公共子表达式消除
- 复制传播
- 死代码消除
- 常量折叠
机器无关优化(中间代码的优化)
- 局部优化:基本块范围内优化(常量合并、公共子表达式删除、计算强度削弱、无用代码删除等)
- 全局优化:循环优化(循环不变量代码外提、删除归纳变量、计算强度削弱)
符号表
记录与每个标识符相关的各种属性信息
错误处理器
- 各阶段均会遇到错误
- 处理方式:报告错误,继续编译