编译器概述
编译器的核心功能
编译器的核心功能是把源代码翻译成目标代码:
翻译!!!目标代码!!!
- 理解源代码:词法分析、语法分析、语义分析
- 转化为等价的目标代码:中间代码生成、目标代码生成
- 更好:优化方法
编译器的结构
每个阶段将源程序从一种表示转换成另一种表示。
随着编译器各个阶段的进展,源程序的内部表示不断地发生变化。
- 词法分析器:字符流->单词流
- 语法分析器:单词流->语法树
- 语义分析器:
- 收集标识符的属性信息:
- 类型(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 是 ∑ 上的一个串。
语言的定义和运算、句子的定义
设 ∑ 是一个字母表,任意L 属于 ∑*,L 称为字母表 ∑ 上的一个语言。
任意 x∈L,x 叫做 L 的一个句子。
文法的定义
文法是用于描述语言的语法结构的形式规则。
任何一种语言都有它自己的文法,不管它是机器语言还是自然语言。
- 自然语言的文法:主 谓 宾
- 机器语言也有描述它语言构成的特定文法
文法可以定义为一个四元组(VT, VN, S, P)
- 一个终结符号集合VT
- 一个非终结符号集合VN
- 一个产生式集合P,定义语法范畴。产生式:A → α,产生式:描述了将终结符和非终结符组合成串的方法
- 一个特定的非终结符――开始符号S
文法的四种类型
- 0型文法:也称为短语文法
- 任何0型语言都是递归可枚举的;反之,递归可枚举集必定是一个0型语言。
- 1型文法:上下文有关文法context-sensitive
- 产生式的形式描述:α1Aα2 → α1βα2 (α1, α2,β∈(VN∪VT ) *,β≠ε,A∈ VN)
- 即:A只有出现在α1, α2的上下文中,才允许用β替换。
- 2型文法:上下文无关文法, context-free grammar. CFG
- 产生式的形式描述:A →β (A∈VN)
- 即:β取代A时,与A所处的上下文无关。
- 3型文法:regular grammar, RG,也称正则文法
- 每个产生式均为 “A→aB”或“A→a” ―― 右线性
- 或 “A→Ba”或“A→a” ―― 左线性
- (A、B∈VN,a∈VT*)
推导和归约
推导:用产生式的右部替换产生式的左部
句子、句型和语言的定义
- 如果,则称α是G的一个句型
- 如果,则称w是G的一个句子
- 由文法G的开始符号S推导出的所有句子构成的集合称为文法G生成的语言
语法生成树
用图形的方式展示推导过程
二义性:多个语法分析树生成同一个终结符号串
程序语言定义
任何语言实现的基础是语言定义
语言的定义决定了该语言具有什么样的语言功能、 什么样的程序结构、以及具体的使用形式等细节问题。
- 词法:是指单词符号的形成规则。(状态转换图、正则表达式和有穷自动机)
- 语法:是指一组规则,用它可产生一个程序。(下推自动机理论和上下文无关文法是讨论语法分析的理论基础)
- 规则:词法规则 + 语法规则
- 语义:定义语言的单词符号和语法单位的意义。(属性方法和基于属性文法的语法制导翻译方法)
- 形式语言:上述的定义是用文字来描述的,当设计编译程序时,就要把它用形式的方式描述出来,就要用到形式语言。
参数传递方法:
- 传值调用
- 引用调用
- 复制恢复(传值结果)
- 传名调用
词法分析
词法分析器的作用
- 识别源程序中的单词是否有误
- 读入源程序字符流、组成词素,输出词法单元序列
- 过滤空白、换行、制表符、注释等
- 将词素添加到符号表中
词法分析器的角色
编译过程的分析部分:词法分析 + 语法分析
- 简化编译器的设计:词法分析器可以首先完成一些简单的处理工作
- 提高编译器的效率:相对于语法分析,词法分析过程简单,可高效实现增强编译器的可移植性(输入设备无关)
输入缓冲
三种实现方式
- 自动生成工具――Lex,生成工具提供读取输入和缓冲的函数
- 高级语言手工编码,利用高级语言提供的I/O函数
- 汇编语言编程,直接访问磁盘
- 唯一读取文件的阶段,值得优化
- 方案:
- 双缓冲方案
- 哨兵标记
词法单元的描述:正则表达式定义规则
- 能被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)?
- 不包含连续的0的01串:((ε|0)1)*0?
词法单元的识别
状态转换图(注意初态和终态)
不确定有限自动机
数学模型,表示为五元组M = {S,∑,δ,s0,F},其中
- S:有限状态集
- ∑:有穷字母表,其元素为输入符号
- δ:S×∑到S的子集的映射,即δ: S×(∑∪{ε}) → ,状态转换函数。
- s0∈S 是唯一的初态
正则表达式(描述单词)
NFA(定义语言):存在缺点,同一符号或ε和其它符号 → 多义性,难以实现
DFA(适于计算机实现)
NFA 和 DFA 都可识别正则表达式,时-空折衷
词法分析器可用一组NFA来描述,每个NFA表示一个单词
设计自动机
词法分析器的构造
正则表达式 → 构造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()
LL 和 LR 都不能表示所有 CFG。
LR分析表种类:
- LR(0):局限性大,但它是建立其它分析表的基础
- SLR (1) :易实现,功能比LR(0)稍强些
- LR(1) :分析能力最强,但实现代价高
- LALR (1) :功能介于SLR(1)和LR(1)之间,适用于大多数程序设计语言的结构,并且可以比较有效地实现。同心集。
语法分析器的作用
- 利用语法检查单词流的语法结构
- 构造语法分析树
- 语法错误和修正
- 识别正确语法
- 报告错误
语法错误处理
不同层次的错误
- 词法:拼写错误(then → them)
- 语法:单词漏掉、顺序错误(花括号不配对)
- 语义:类型错误(声明void f()和调用aa = f())
- 逻辑:无限循环/递归调用( == → = )
错误处理目标
- 清楚、准确地检测、报告错误及其发生位置
- 快速恢复,继续编译,以便发现后续错误
- 不能对正确程序的编译速度造成很大影响
LL,LR,可最快速度发现错误:可行前缀特性,viable-prefix property,当一个输入前缀不是语言中任何符号串前缀――发生错误。
错误恢复策略
- 错误恢复策略
- 丢弃单词,直到发现 “同步”单词,设计者指定同步单词集,{end, “;”, “}”, …}
- 缺点:丢弃输入导致遗漏定义,造成更多错误;遗漏错误
- 优点:简单 → 适合每个语句一个错误的情况
- 短语层次的恢复
- 局部修正,继续分析,同样由设计者指定修正方法。e.g. “,” → “;”,删除“,”,插入“;”
- 与恐慌模式相结合,避免丢弃过多单词
- 错误产生式
- 理解、描述错误模式,文法添加生成错误语句的产生式,拓广文法 → 语法分析器程序。( 错误检测信息+自动修正)
- e.g. 对C语言赋值语句,为“:=”添加规则报告错误,但继续编译
- 全局纠正
- 错误程序 → 正确程序,寻找最少修正步骤,插入、删除、替换
- 过于复杂,时空效率低
上下文无关文法
推导:描述文法定义语言的过程,自顶向下构造语法分析树的精确描述。
两个CFG生成相同语言,两个CFG等价。
最左推导,最右推导。
语法树:推导的图示,但不体现推导过程的顺序。
一棵语法树对应多个推导,但是有唯一的最左推导和最右推导。
二义性文法存在某个句子对应多个语法树,多个最左(右)推导。
证明CFG G生成语言L:(数学归纳法)
- G生成的每个符号串都在L中
- L中每个符号串都可由G生成
正则表达式可描述的语言CFG均可描述。
RG 都可用 NFA 表示,CFG 都是 RG,所以 CFG 都可用 NFA 表示。
为什么还需要正则表达式?
- 词法规则很简单,正则表达式描述能力足够
- 正则表达式更简洁、更容易理解
- 能更自动构造更高效的词法分析器
- 使编译器前端更模块化
设计 CFG:
CFG 的修改
- 消除二义性
- 消除左递归(间接左递归):见附录
- 消除空产生式:利用产生式进行代入
- 消除回路:保证每个产生式都加入终结符(开始符号的空产生式除外)
- 提取左公因子:预测分析方法要求――向前搜索一个单词,即可确定产生式
优点:
- 给出精确的,易于理解的语法说明
- 自动产生高效的分析器
- 可以给语言定义出层次结构
- 以文法为基础的语言的实现便于语言的修改
问题:文法只能描述编程语言的大部分语法。(无法表示标识符必须在使用前定义,函数形参和实参数目应匹配)
递归下降分析法
缺点:
- 不能处理左递归
- 复杂的回溯技术
- 回溯导致语义工作推倒重来
- 难以报告出错的确切位置
- 效率低
产生回溯的原因:进行推导时,若产生式存在多个候选式,选择哪个候选式进行推导存在不确定性。
消除回溯的基本原则:对文法的任何非终结符,若能根据当前读头下的符号,准确的选择一个候选式进行推导,那么回溯就可以消除。
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
LL(1)文法
若文法G的预测分析表M中不含有多重定义项,则称G为 LL(1)文法。且无二义性,无左递归。
计算 First 和 Follow 习题
预测分析法的错误恢复
恐慌模式恢复策略
考虑非终结符的同步单词集
- FOLLOW(A)――略过A
- 将高层结构的开始符号作为低层结构的同步集
- FIRST(A)――重新开始分析A
- Follow 集为 sync,弹出一个非终结符
- 其他的空位则跳过输入符号
短语层次错误恢复
预测分析表空位填入错误处理函数
自顶向下分析:预测分析法
预测分析法实现步骤
- 构造文法
- 改造文法:消除二义性、消除左递归、消除回溯
- 求每个变量的FIRST集和FOLLOW集,构造预测分析表
- 检查是不是LL(1) 文法
- 对于递归的预测分析,为每一个非终结符编写一个过程;对于非递归的预测分析,实现表驱动的预测分析算法
缺点:不是所有文法满足LL(1)要求
自底向上分析方法
自底向上语法分析,是从输入符号串出发,试图把它归约成识别符号。
从图形上看,自底向上分析过程是以输入符号串作为末端结点符号串,向着根结点方向往上构造语法树,使识别符号正是该语法树的根结点。
归约:某产生式体相匹配的特定子串被替换为该产生式头部的非终结符号
自底向上分析是一个不断进行直接归约的过程。任何自底向上分析方法的关键是要找出这种句柄。
符号串的句柄是与某个产生式右部匹配的子串,应该归约为产生式左部(最右推导的逆过程)。
一个句型可有多个不同句柄,但是非多义性文法的最右句型有唯一句柄。
基本操作:移进、归约、接受、错误。
LR分析方法:当前最广义的无回溯的“移进- 归约”方法。
优点:适用范围广;分析速度快;报错准确。
从逻辑上说,一个LR分析器包括两部分:一个总控程序和一张分析表。
构造 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 |
LR(1)
SLR(1)也存在不足,即如果冲突项目的非终结符FOLLOW集与有关集合相交时,就不能用SLR(1)方法解决。
SLR只是简单地考察下一个输入符号b是否属于与归约项目A→α相关联的FOLLOW(A),但b∈FOLLOW(A)只是归约α的一个必要条件,而非充分条件。
对于产生式A→α的归约,在不同的使用位置,A会要求不同的后继符号。在特定位置,A的后继符集合是FOLLOW(A)的子集。
**任何二义性文法都不是LR(k)文法。 **
LALR
LALR分析法与SLR相类似,但功能比SLR(1)强,比LR(1)弱。
首先构造LR(1)项目集,如果它不存在冲突,就把同心集合并在一起,若合并后项目集规范族不存在“归约―归约”冲突,就按照这个集族构造分析表。``
语法制导翻译
语法制导翻译概述
语法制导翻译使用CFG来引导对语言的翻译,是一种面向文法的翻译技术。
语义分析器的任务:
① 检查各个语法结构的静态语义 (静态语义分析或静态检查 )
② 执行所规定的语义动作:如表达式的求值、符号表的填写、中间代码的生成
将静态检查和中间代码生成结合到语法分析中进行的技术称为语法制导翻译。
语法制导的翻译:一种形式化的语义描述方法,包括两种具体形式:
- 语法制导定义(Syntax-Directed Definitions, SDD):定义翻译所必须的语义属性和语义规则,一般不涉及计算顺序。
- 翻译模式(translation schemes): 给出语义规则的计算顺序。
语法制导定义(SDD)
- 一个上下文无关文法。
- 每个属性与文法的一个终结符或非终结符相关联
- 每一个产生式和一个语义规则集合相关联。描述产生式中各文法符号的属性之间的依赖关系。通常用函数或程序语句的形式表示
语法制导定义是一种接近形式化的语义描述方法。语法制导定义的表示分两部分:
- 先针对语义为文法符号设置属性,
- 然后为每个产生式设置语义规则,来描述各属性间的关系。
继承属性和综合属性
属性的特点:
- 一个结点的综合属性的值通过分析树中它的子结点的属性值和自己的属性值计算的;
- 继承属性的值由结点的父结点、兄弟结点来计算的;
- 非终结符号即可有综合属性也可有继承属性,但文法开始符号没有继承属性;
- 终结符号只有综合属性,由词法分析器提供,即记号的属性;
- 每个文法符号的综合属性和继承属性的交集为空。
判断一个属性是继承属性还是综合属性
综合属性(synthesized attribute):
- 在分析树结点 N 上的非终结符 A 的综合属性只能通过 N 的子结点或 N 本身的属性值来定义。
- 终结符可以具有综合属性。终结符的综合属性值是由词法分析器提供的词法值,因此在SDD中没有计算终结符属性值的语义规则。
继承属性(inherited attribute)
- 在分析树结点 N 上的非终结符 A 的继承属性只能通过 N 的父结点、N 的兄弟结点或 N 本身的属性值来定义。
- 终结符没有继承属性。终结符从词法分析器处获得 的属性值被归为综合属性值。
继承属性和综合属性的计算在产生式中所在的位置
- 将计算某个非终结符号A的继承属性的动作插入到产生式右部中紧靠在A的本次出现之前的位置上
- 将计算一个产生式左部符号的综合属性的动作放置在这个产生式右部的最右端
副作用
副作用:一般属性值计算(基于属性值或常量进行的)之外的功能,如,打印出一个结果;将信息加入符号表。
注释分析树
注释分析树:将属性附着在分析树对应文法符号上(属性作为分析树的注释)。
SDD的求值顺序
- 对单词符号串进行语法分析,构造语法分析树;
- 根据需要构造属性依赖图;(综合属性在右,继承属性在左)
- 遍历语法树,并在语法树各结点处按语义规则进行计算。
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
示例4:给出下列文法的 SDT
为文法
S → ( L ) | a
L → L , S | S
写一个翻译方案,它打印出每个a在句子中是第几个字符。例如,当句子是( a , ( a , ( a , a ) , (a) ) )时,打印的结果是2 5 8 10 14。
S’ → {S. in = 0 } S
S → {L. in = S. in + 1 } ( L )
{S. out = L. out + 1 }
S → a {S. out = S. in + 1; print (S. out) }
L → {L1. in = L. in }L1 ,
{S. in = L1. out + 1 } S
{L. out = S. out }
L → {S. in = L. in }S {L. out = S. out }
示例5:给出下列文法的 SDT
为文法
S → ( L ) | a
L → L , S | S
写一个翻译方案,它输出括号的对数。
S’ → S print (S. num)
S → ( L ) S. num = L.num + 1
S → a S. num = 0
L → L1 , S L. num = L1. num + S. num
L → S L. num = S.num
中间代码生成
中间代码生成概述
“中间代码生成”程序的任务是:把经过语法分析和语义分析而获得的源程序中间表示翻译为中间代码表示。
方法:语法制导翻译。
采用独立于机器的中间代码的好处:
- 便于编译系统建立和编译系统的移植;
- 便于进行独立于机器的代码优化工作。
中间表示
优点: 容易为不同目标机器开发不同后端
缺点: 编译过程变慢 (因为中间步骤)
中间表示:
- 语法树:抽象语法树
- 反映了抽象的语法结构,而分析树反映的是具体的语法结构。语法树是分析树的抽象形式或压缩形式。
- 语法规则中包含的某些符号可能起标点符号作用,也可能起解释作用。
- 三地址代码表示
- 四元式
- 三元式
- 间接三元式
语法分析树,抽象语法树,有向无环图(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)
类型等价
类型等价:结构等价 + 名等价
结构等价:满足以下条件之一:(类型名被类型表达式所代替,if 替换所有名字后,两个类型表达式结构上等价即结构等价)
- 相同的基本类型
- 将相同类型构造算子应用于等价的类型而构建的
- 一个类型是另一个类型表达式的名字
名等价:满足前两个条件(将每个类型名看作是可区分的类型,名字完全相同即名等价)
计算类型及其宽度的语法制导翻译
T → B { t = B.type; w = B.width;}
C { T.type = C.type; T.width = C.width;}
B → int { B.type = integer; B.width = 4;}
B → float { B.type = float; B.width = 8;}
C → ε { C.type = t; C.width = w;}
C → [num] C1 { C.type = array( num.vale, C1.type);
C.width = num.value × C1.width;}
习题
运行存储分配
运行存储分配概述
运行时刻环境
- 为源程序中命名的对象分配安排存储位置
- 确定目标程序访问变量时使用的机制
- 过程之间的连接
- 参数传递
主题
- 存储管理:栈分配、堆管理、垃圾回收
- 对变量、数据的访问
静态和动态存储
-
静态分配
- 编译器在编译时刻就可以做出存储分配决定,不需要考虑程序运行时刻的情形
- 全局变量
- 静态分配给语言带来限制
- 递归过程不被允许
- 数据对象的长度和它在内存中位置的限制,必须是在编译时可以知道的
- 数据结构不能动态建立
-
动态分配
- 栈式存储:和过程的调用/返回同步进行分配和回收,值的生命期和过程生命期相同
- 堆存储:数据对象比创建它的过程调用更长寿
堆空间:存储管理器
堆空间,用于存放生命周期不确定、或生存到被明确删除为止的数据对象。
存储管理器基本功能
- 分配:为每个内存请求分配一段连续的、适当大小的堆空间。
- 首先从空闲的堆空间分配;
- 如果不行则从操作系统中获取内存、增加堆空间。
- 回收:把被回收的空间返回空闲空间缓冲池,以满足其他内存需求。
评价存储管理器的特性:
- 空间效率:使程序需要的堆空间最小,即减小碎片
- 程序效率:充分运用内存系统的层次,提高效率
- 低开销:使分配/收回内存的操作尽可能高效
两大问题:
- 内存泄露:未能删除不可能再被引用的数据
- 悬空指针引用:引用已被删除的数据
其他问题
- 空指针访问/数组越界访问
代码生成
代码生成器概述
- 根据中间表示生成代码
- 代码生成器之前可能有一个优化组件
- 代码生成器的三个任务
- 指令选择:选择适当的指令实现IR语句
- 寄存器分配和指派:把哪个值放在哪个寄存器中
- 指令排序:按照什么顺序安排指令执行
静态/栈式数据区分配
活动记录静态分配:每个过程静态地分配一个数据区域,开始位置用staticArea表示。
活动记录栈式分配:
- 寄存器SP指向栈顶
- 第一个过程(main)初始化栈区
- 过程调用指令序列
- 返回指令序列
基本块相关的代码生成
中间代码的流图表示法
- 中间代码划分成为基本块(basic block),其特点是单入口单出口,即:
- 控制流只能从第一个指令进入
- 除了基本块最后一个指令,控制流不会跳转/停机
- 流图中结点是基本块,边指明了哪些基本块可以跟在一个基本块之后运行
流图可作为优化的基础
- 它指出了基本块之间的控制流
- 可根据流图了解一个值是否会被使用等信息
划分基本块的算法:
确定首指令(基本块的第一个指令)符合以下任一条:
- 中间代码的第一个三地址指令
- 任意一个条件或无条件转移指令的目标指令
- 紧跟在一个条件/无条件转移指令之后的指令
确定基本块
- 每个首指令对应于一个基本块:从首指令(包含)开始到下一个首指令(不含)
基本块划分示例:
窥孔优化
- 消除冗余指令
- 消除不可达指令
- 控制流优化
- 代数化简/强度消减
- 机器特有指令的使用
简单的代码生成算法
重要子函数:getReg(I)
- 为三地址指令I选择寄存器;
- 根据寄存器描述符和地址描述符、以及数据流信息来分配最佳的寄存器;
- 得到的机器指令的质量依赖于getReg函数选取寄存器的算法
代码生成时必须更新寄存器描述符和地址描述符。
代码优化
代码优化概述
通过一些相对低层的语义等价转换来优化代码。
- 公共子表达式消除
- 复制传播
- 死代码消除
- 代码移动
- 归纳变量和强度消减
- 常量折叠