# 引论
# 什么是编译程序
# 编译程序
编译程序是翻译程序的一种,它将高级语言程序翻译成等价的目标程序。编译程序的输入是高级语言程序,输出是目标程序。编译程序的主要任务是将源程序翻译成目标程序,使得目标程序能够在计算机上正确运行。
# 解释程序
以源程序为输入,直接执行源程序,不生成目标程序。解释程序的主要任务是解释源程序,使得源程序能够在计算机上正确运行。
# 编译程序与解释程序的区别
根本区别:是否生成目标程序。
# 编译系统的组成
- 编译程序
- 运行系统
- 支持环境
- 运行库
# 编译过程与编译程序的组织结构
# 编译过程
自然语言的翻译 | 编译 |
---|---|
识别出句子中的单词 | 词法分析 |
确定单词之间的语法关系 | 语法分析 |
根据句子含义初步翻译 | 语义分析 |
对译文进行修饰 | 中间代码生成 |
将译文翻译成目标语言 | 目标代码生成 |
# 词法分析
输入源程序;扫描、分解字符串,识别出单词;输出单词流。
词法分析由词法分析器完成,词法分析器又叫扫描器。
词法分析器从左到右扫描源程序,并将其转换成单词(Token)串;并检查词法错误,进行标识符的登记 —— 符号表管理。
输入:字符串
输出:(种别码,属性值)
# 语法分析
语法分析由语法分析器完成,语法分析器又叫解析器。
功能:
- 实现组词成句
- 构造分析树
- 检查语法错误
- 指导翻译
输入:Token 序列
输出:语法成分
# 语义分析
语义分析一般和语法分析一起进行,称为语法制导翻译。
功能:
- 获取标识符的属性
- 语义检查
- 子程序静态绑定
- 变量的静态绑定
# 中间代码生成
生成四元式、三地址码、P - 代码等。
# 目标代码生成
任务:将中间代码变换成特定机器上的低级语言代码
目标代码形式:绝对指令、可重定位指令、汇编指令
# 表格与表格管理
- 符号名表
- 常数表
- 标号表
- 入口名表
- 过程引用表
- 循环表
- 等价名表
- 公用链表
- 格式表
- 中间代码表
# 出错处理
- 语法错误:不符合语法(或词法)规则
- 语义错误:不符合语义规则
# 遍
遍:编译程序对源程序的从头到尾扫描一次,并作有关的加工处理,生成新的中间结果或目标程序。
# 前端与后端
- 前端:词法分析、语法分析、语义分析、中间代码生成,与源语言有关
- 后端:目标代码生成、优化,与目标机器有关
# 自举技术
编译程序的编译程序完全是由这个语言本身编写的,而且能够编译这门语言的任何代码。
# 自展技术
先对语言的核心部分构造一个小小的编译程序,然后用这个编译程序编译语言的其他部分。
# 移植技术
编译程序的移植性是指编译程序能够在不同的计算机上运行。
# 形式语言与自动机
# 形式语言
# 语言
- 语言(Language):满足一定条件的句子的集合
- 句子(Sentence):满足一定规则的单词的序列
- 单词(Token):满足一定规则的字符串
语言就是字和组合字的规则所产生的字的序列。
# 文法、语言、自动机
- 0 型文法:递归可枚举语言,图灵机
- 1 型文法:上下文相关语言,线性有界自动机
- 2 型文法:上下文无关语言,下推自动机,例如
- 3 型文法:正规语言,有限自动机,例如
# 程序语言
程序语言是一个记号系统
# 语法
- 任何程序语言都可以看成一定字符集上的字符串
- 语法保障了这串字符的合法性
- 语法 = 词法规则 + 语法规则
# 词法
- 单词符号:语言中具有意义的最基本结构
- 词法规则
- 规定了字母表中的字符如何组成单词符号
- 程序语言的单词符号包括:常数、标识符、关键字、运算符、分节符等
- 使用正规式和有限自动机理论描述词法结构和词法分析
# 语法规则
- 语法单位:表达式、子句、语句、函数、过程、程序
- 语法规则
- 规定了如何从单词符号构成语法单位
- 使用上下文无关文法描述语法结构和语法分析
- 语言的词法规则和语法规则共同定义了程序的形式结构,是判断输入字符串是否都成一个形式上正确的程序的依据
# 语义
- 程序中单词符号和语法单位的含义
- 不同语言形式上语法单位相似,但含义可能不同
- 对于某种语言,可以定义一个程序的意义的一组规则,称为语义规则
- 目前大多数编译程序使用基于属性文法的语法制导翻译来分析语义
# 字母表
字母表是符号的非空有限集合,通常用 表示。
# 字母表的运算
字母表 的运算:
- 连接 / 乘积:
- 幂:,其中 时,
- 正闭包,记作:
- Kleene 闭包,记作:
# 符号串
由字母表中的符号所组成的任何有穷序列称为符号串。也叫做字母表上的句子。
# 符号串的形式定义
- 字母表 上的字符是 上的符号串
- 若 是 上的符号串, 是 上的字符,则 是 上的符号串
- 是 上的符号串,当且仅当它是由上述规则生成的
# 符号串的术语
- 前缀:移走 尾部的零个或多个字符得到的串称为 的前缀
- 后缀:移走 头部的零个或多个字符得到的串称为 的后缀
- 子串:从 中删去一个前缀和一个后缀得到的串称为 的子串
- 真前缀、真后缀、真子串:不等于 和 的前缀、后缀、子串
- 子序列:从 中任意删除零个或多个字符得到的串称为 的子序列
- 长度:符号串的字符个数称为该串的长度,记作
# 句子和语言
- 句子:字母表上符合某种规则构成的符号串
- 语言:确定字母表上字符串的集合,即句子的集合,即
# 符号串的运算
- 连接:设 是 上的符号串,它们的连接 是 后接 的符号串
- 方幂:设 是 上的符号串, 是非负整数, 是 的 次方幂,即 连接自身 次
# 语言的运算
- 并:
- 连接:
- 方幂:
- Kleene 闭包:
- 正闭包:
# 文法
字和组合的字规则,是描述语言的语法结构的形式化规则。
# BNF 范式
巴科斯 - 诺尔范式(Backus-Naur Form,BNF)是一种用于表示上下文无关文法的语言。
用 ::=
或 ->
表示 “定义为”,用 |
表示 “或”, |
两边的成分叫做候选式,用 <>
表示非终结符。
# 语法规则的推导
由规则推导句子:从一个要识别的符号开始,用相应规则的右部替换左部,直到推导出句子,每次仅用一个规则。推导一直进行到句子中不再有非终结符为止。
# 文法的形式定义
- 文法 是一个四元组
- :非空有穷终结符集合
- :非空有穷非终结符集合且
- :开始符号,
- :产生式的非空有穷集合,每个产生式是一个形如 的规则,其中,开始符号 必须至少出现在一个产生式的左部
# 直接推导和推导
令,
- 若,则称 直接推导出,记作,同时称 直接由 推导出,或称 直接规约为
- 如果存在一个直接推导的有限序列,表示成,则称从 到 的长度为 的推导。 表示从 的经 0 步或多步推导得到
# 最左推导和最右推导
- 最左推导:,每次推导都是用最左边的非终结符推导
- 最右推导:,每次推导都是用最右边的非终结符推导,也称为规范推导
# 语言的形式定义
设文法,如果,则称 是一个句型。仅含终结符的句型称为句子。
语言 是文法 的所有句子的集合:
# 短语、直接短语和句柄
令 是一个文法,如果有,则称 是句型 的相对于非终结符 的短语
特别是,如果有,则称 是句型 的相对于 的直接短语,一个句型的最左直接短语称为句型的句柄
# 分析树
设 是一个上下文无关文法, 的一个分析树是一个有根树,满足以下条件:
- 每个节点有个标记,是 的元素
- 根节点标记为
- 如果节点是内部节点,则其标记 必在 中
- 如果编号为 的节点标记为 是节点 从左到右的儿子,并分别标有,则 必须是 中的一个产生式
- 如果节点 有标记,则 必须是叶节点,且是他父亲的唯一儿子,其他叶子节点是 中的元素(终结符)
# 子树
一棵分析树中一个特有节点连同他的全部后裔,连接这些节点的边和边上的标记称为这个节点的子树。
# 从语法树看短语、直接短语和句柄
- 短语:一棵子树的所有叶子从左至右排列起来形成一个相对于该子树根节点的串。
- 直接短语:如果一棵子树只有两代,它的叶子从左至右排列起来形成的串。
- 句柄:一个句型的分析树中,最左那棵两代子树的叶子从左至右的排列。
找到句型的短语:找分析树的所有子树
- 最左推导和最右推导的语法树是否相同?
尽管推导顺序不同,两者生成的语法树是一样的,因为语法树表示的是推导过程中应用的规则和它们的结构关系,而不是推导的具体顺序。因此,不管是最左推导还是最右推导,最终语法树的形状和结构都相同,表示句子中的层次关系。
# 二义性
如果一个文法存在两个不同的分析树,那么这个文法是二义的。如果一个文法包含二义性句子,那么这个文法是二义的。
# 自动机
# 有限自动机
# 有限自动机基本组成
- 有限自动机(FA)是一种识别装置,能准确识别正规文法,为词法分析程序提供了方法和工具。
- 离散输入输出系统,具有有限数目的内部状态
- 根据当前所处的状态和面临的输入字符决定系统的后继行为。
# 有限自动机工作原理
- 状态为初始状态、中间状态和终止状态。
- 可以有若干个终止状态。
- 只能有一个初始状态。
# 状态转换图
状态用节点表示,输入符号用边表示,两个圈表示终止状态。
# 确定的有限自动机(DFA)
# DFA 的定义
- 一个确定的有限自动机是一个五元组
- :输入字母表
- :有限状态集合
- :初始状态
- :终止状态集合
- :状态转移函数,从 到 的映射, 表示从状态 经过输入 转移到状态, 称为状态 的后继状态
# DFA M 接受的语言
- 如果对所有的输入字符串,以下述方式递归地对 DFA M 扩充 的定义:
# DFA M 所识别的语言
从状态转换图看,从初态出发,沿任一条路径到达终结状态,这条路径上的弧上的标记符号连接起来构成的符号串是 DFA M 所识别的语言。
DFA M 所识别的符号串的全体记作,即
# 非确定的有限自动机(NFA)
# NFA 的定义
- 一个非确定的有限自动机是一个五元组
- :输入字母表
- :有限状态集合
- :初始状态
- :终止状态集合
- :状态转移函数,从 到 的映射,其中 表示 的幂集,即 的所有子集的集合。
# NFA M 接受的语言
如果,则从状态 经过输入 可以转移到状态 中的任意一个。
# NFA M 所识别的语言
# FA 的等价定理
对于任何 NFA M,都存在一个 DFA M',使得。
NFA 和 DFA 的区别:
比较项 | DFA | NFA |
---|---|---|
初始状态 | 唯一 | 多个 |
弧上的标记 | 输入符号 | 输入符号或 |
转换关系 | 确定 | 非确定 |
# 具有 - 转移的 NFA 构造 DFA 的方法
集合 的 - 闭包定义:
- 令 是一个状态集的子集,定义 的
- 若,则
- 若,则从 出发的所有 - 转移的状态都在 中
设 是 中的一个符号,定义,其中
# 构造方法
确定化:不失一般性,设字母表 中的符号是,
- 首先,置第一行第一列为,求出这一列的 和,
- 然后,检查这两个 和 是否在第一列中,如果不在,就加入到第一列中,然后求出这两个 和 的 和,如此循环下去,直到没有新的 加入为止。
_ | _ | |
_ | _ | _ |
# 等价分析
- 把表看成状态转移矩阵,子集视为状态
- 转换表唯一刻画了一个 DFA M
- 初态是
- 终态是包含原终态 的子集
# DFA 的最小化
# DFA 的最小化定义
DFA M= 的化简是指寻找一个状态数比较少的 DFA M',使
一个 DFA M 的最小化是指他没有多余状态并且没有相互等价的状态。
# DFA 的多余状态(无关状态)
- 从开始状态出发,任何输入串也不能到达的那个状态
- 从该状态出发没有通向终态的道路
# DFA 的等价状态
设,如果对于任何输入符号, 和 同时到达终态或同时不到达终态,则称状态 和 是等价的。
判断 和 不等价:只要存在一个输入符号,使得 而,或者 而。
若 为自动机 的一个状态集,我们把从 出发能导出的所有符号串集合记为,若有,则称 和 等价。
# 可区别状态
自动机的两个状态 和,如果他们不等价,则称他们是可区别的。
# 分割法
把一个 DFA(不含多余状态)的状态分割成一些不相关的子集,使得任何不同的两个子集状态都是可区别的,而同一个子集中的任何状态都是等价的。在各个子集中任取一个状态做代表,删去子集的其余状态。
- 首先将终态和非终态分开,分为
- 对 中的每个集合, 中任意两个状态 和 在同一组中,当且仅当对于, 和 在同一组中,否则将 和 分开,用 替换原来的集合
- 重复第 2 步,直到不能再分为止
- 合并等价状态,在每个 中任取一个状态作为代表,删去 中其余状态
- 删去多余状态,从其他状态到无关状态到转换都成为无定义
# NFA 的最小化
NFA 化为 DFA,然后再对 DFA 进行最小化。
# 词法分析
# 词法分析的功能
对源程序进行从左到右的顺序扫描,按照词法规则从中识别出一个个具有独立意义的单词符号并把源程序转换为等价的内部表示形式(属性字流)。
- 识别单词符号
- 过滤掉源程序中的注释和空白符
- 对数字字符串转换为二进制数
- 记录读入字符的行号和列号,以便编译程序输出错误信息时能够准确指出错误位置
- 进行预编译
- 符号表操作和错误处理
# 实现词法分析的方案
- 词法分析单独作为一遍(优点:结构清晰,各遍功能单一;缺点:生成中间文件,效率低)
- 词法分析程序作为单独的子程序
# 预处理 —— 扫描缓冲区
- 两个半区互补使用:搜索指针从单词起点开始搜索,如果遇到半区域的边界,但尚未到达单词的终点时,则可将后续的输入字符装入该缓冲区的另一半
If f at end of first half { | |
reload second half; | |
F++; | |
} Else if F at end of second half { | |
reload first half; | |
move F to beginning of first half; | |
} Else { | |
F++; | |
} |
# 单词的描述和识别
# 单词的种类
- 保留字
- 标识符
- 常数
- 运算符
- 界限符
# 词法分析程序的输出形式
常用的单词内部表示形式:
- 按单词种类分类
- 保留字和分界符一符一类
- 标识符和长处的单词属性值作为符号表入口指针(标识符、常数相关属性很多)
# 出错处理
- 非法字符检查
- 关键字拼写错误检查
- 不封闭错误检查
- 重复说明错误检查
- 错误恢复与续编译
# 正规式(Regular Expression)
正则表达式是一种描述字符串特征的表达式,是一种用来描述字符串的方法。
正则表达式可以有较小的正则表达式递归地构建,每个正则表达式 都可以用来描述一个语言。
# 正规集
- 和 都是 上的正规表达式,他们所表示的语言分别为 和
- 对于, 是 上的正规表达式,他所表示的语言为
- 如果 和 是 上的正规表达式,那么,, 都是 上的正规表达式,他们所表示的语言分别为,,
一个由正规表达式定义的语言称为正规集或正规语言。
# 正规式的代数性质
- 交换律:
- 结合律:
- 分配律:
- 幂等律:,,
# 正则定义
- 正则定义:正规表达式是由正规集上的运算符和正规集中的元素组成的表达式
- 其中 是新符号,都不在 中, 是 上的正规表达式
# 正规文法 正规式
规则 | 文法产生式 | 正规式 |
---|---|---|
1 | ||
2 | ||
3 |
# 正规式 正规文法
- 对任何正规表达式,选择一个非终结符 作为识别符号。并构建正规定义式
- 若 是正规表达式, 重写为, 重写为,其中 是新的非终结符
# 正规式与有限自动机
定理:设 是 上一个正规表达式,则存在一个 FA 接受。反之亦然。
正规文法,正规表达式和有限自动机三者之间在表示语言的意义下是等价的。
字母表 上的一个正规语言,既可以由某一个正规文法产生,也可以由某一个正规表达式所表示,还可以由某一个有限自动机识别。
# 转换法实现正规式与有限自动机的转换
# 从正规式到 NFA 的转换规则
对于 上任一正规表达式,一定能构造一个 NFA ,使得。
- 首先把转换图的概念拓广,每条弧上可以用一个正规式标记,对 NFA 构造一个广义的转换图。其中只有开始状态 和终止状态,连接 和 的有向弧上是正规式。
- 然后按照下面的替换规则对正规式不断进行分解,不断加入状态结点和箭弧,直到转换图上的所有弧标记都是 上的元素或 为止。
# 从 NFA 到正规式的替换规则
对于 上任一 NFA ,能构造 上一个正规表达式 ,使得。
- 首先,在 的转换图上加进, 两个节点。从 用 弧连接到 的所有初态结点,从 的所有终结(接受)结点用 弧连接到,从而构成一个新的 NFA ,。
- 反复使用下面的替换规则逐步消去 NFA 中的状态结点和弧,直至剩下, 为止。在消去结点的过程中,逐步用正规式标记弧。
# 正规文法与 FA 的等价性
如果对于某个正规文法 和某个有限自动机,有(正规文法 所产生的语言和某个有限自动机所识别的语言相同),则称 和 是等价的。
定理 对每一个右线性正规文法或左线性正规文法 ,都存在一个 FA ,使
定理 对于每一个 FA ,都存在一个右线性正规文法 和一个左线性正规文法 ,使 .
# 有限自动机 正规文法(右线性正规文法)
-
对转换函数,可写成一个产生式:
-
对可接受状态,增加一个产生式:
-
有限自动机的初态对应于文法的开始符号,有限自动机的字母表文法的终结符号集.
# 正规文法(右线性) 有限自动机
- 字母表与 的终结符号相同。
- 为 中的每个非终结符生成 的一个状态, 的开始符号 是开始状态 ;
- 增加一个新状态 ,作为 NFA 的终态。
- 对 中的形如 ,其中 为终结符或 , 和 为非终结符的产生式,构造 的一个转换函数 。
- 对 中的形如 的产生式,构造 的一个转换函数
# 词法分析程序的实现
- 构造识别单词的状态转换图
- 对程序语言的单词按种类分别构造对应的状态转换图.
- 对各类转换图合并,构成一个能识别语言所有单词的状态转换图.
- 编程实现状态转换图
# 限制条件
- 所有基本字都是保留字;用户不能用它们作自己的标识
符 - 基本字作为特殊的标识符来处理,使用保留字表如果基本字、标识符和常数(或标号)之间没有确定的运算符或界符作间隔,则必须使用一个空白符作间隔
# 状态转换图的代码实现
每个状态节点对应一小段程序
# 不含回路的分叉节点
GetChar(); | |
if (IsLetter()) { | |
// 状态 j | |
} else if (IsDigit()) { | |
// 状态 k | |
} else if (ch == '\\') { | |
// 状态 m | |
} else { | |
// 错误处理 | |
} |
# 含回路的状态节点
GetChar(); | |
while (IsLetter() || IsDigit()) { | |
GetChar(); | |
} | |
// 状态 j |
# 终态节点
直接返回单词符号
return (C, VAL); |
C
是单词种别, VAL
是单词的属性值
# 全局变量与过程
ch
字符变量,存放最新读入的源程序字符strToken
字符数组,存放构成单词符号的字符串GetChar
子程序过程,把下一个字符读入到ch
中GetBC
子程序过程,跳过空白符,直至ch
中读入一非空白符Concat
子程序,把ch
中的字符连接到strToken
IsLetter
和IsDisgital
布尔函数,判断ch
中字符是否为字母和数字Reserve
整型函数,对于strToken
中的字符串查找保留字表,若它是保留字则给出它的编码,否则回送 0Retract
子程序,把搜索指针回调一个字符位置Insertld
整型函数,将strToken
中的标识符插入符号表,返回符号表指针InsertConst
整型函数过程,将strToken
中的常数插入常数表,返回常数表指针
# 状态图代码一般化
curState = 初态; | |
GetChar(); | |
while (stateTrans[curState][ch] 有定义) { | |
// 存在后继状态,读入,拼接 | |
Concat(); | |
// 转换入下一个状态,读入下一个字符 | |
curState = stateTrans[curState][ch]; | |
if (curState == 终态) { | |
// 返回单词符号 | |
return (C, VAL); | |
} | |
GetChar(); | |
} |
# 词法分析程序的设计过程
词法规则 -> 正规式 -> 状态转换图 -> 合并状态转换图 -> 编程实现
# 词法分析阶段的错误处理
# 可检测的错误类型
- 单词拼写错误
- 非法字符
# 词法错误检测
如果当前状态与当前输入符号在转换表对应项中的信息为空,而当前状态又不是终止状态,则调用错误处理程序
# 词法错误处理的方法
查找已扫描字符串中最后一个对应于某终态字符
- 如果找到了,将该字符与其前面的字符识别成一个单词。然后将输入指针退回到该字符,扫描器重新回到初始状态,继续识别下一个单词
- 如果没找到,则确定出错,采用错误恢复策略
# 错误恢复策略
最简单的错误恢复策略!恐慌模式(panic mode)恢复
从剩余的输入中不断删除字符,直到词法分析器能够在剩余输入的开头发现一个正确的字符为止
# 自顶向下语法分析
# 词法、语法的关系
-
源语言的文法可以分解词法和语法
- 词法:描述语言单词符号构成的文法
- 语法:描述语言结构的文法
-
用正规文法或正规表达式描述单词符号的结构
-
用上下文无关文法描述语言的结构
-
词法产生式的终结符号是单个字符
-
语法产生式的终结符号是单词符号
# 语法分析器的作用
- 语法分析任务:按照语言规定的语法规则,对词法分析形成的记号(单词)串进行语法检查,识别出相应的语法成分,采用语法树的形式输出,如果不符合语法要求则给出出错的原因。
- 语法分析程序:执行语法分析的程序,也称语法分析器.
- 语法分析的关键:句型的识别
# 书写格式约定
- 终结符号
- 次序靠前的小写字母,如, ,
- 标点符号
- 黑体的字串
- 非终结符号
- 次序靠前的大写字母和
- 小写的斜体的名字
- 文法符号:次序靠后的大写字母 , ,
- 终结符号串:次序靠后的小写字母,, , ...
- 文法符号串:小写的希腊字母,, ,
# 推导
推导:就是用产生式的右部的串来代替左部的非终结符,事实上推导给出了自顶向下构成分析树过程的精确描述。
# 消除二义性
- 引入新的语法成分,使得原来的文法成为无二义的文法。
- 增加就近原则,使得文法成为无二义的文法。
# 下推自动机
# 消除左递归
一个文法,若存在推导,则称 是左递归的。
消除直接左递归的方法:
若:,其中 不以 开头,,则可将 的产生式分为两部分:
消除间接左递归的方法:
-
非终结符号按照某种次序排列
-
消除算法
for i = 1 to n
for j = 1 to i-1
用A_i的产生式替换A_j的产生式中的A_i
end for
消除A_i的直接左递归
end for
-
去除开始符号无法到达的非终结符号的产生式
# 提取左因子
文法 的产生式,若,, 和 有一个最长的公共左因子
若,则可将 的产生式分为两部分:
# 递归下降分析法
# 包含回溯的递归下降分析法
思想:从语法的开始符号出发,试探使用不同产生式,寻找匹配于输入符号串的推导。或者说,从对应文法开始符号的根结点出发,自顶向下为输入符号串建立一棵分析树。
这种分析方法实质上是一种试探过程,是因为文法中,对于某个非终结符号的规则其右部有多个选择,并根据所面临的输入符号不能准确的确定所要选择时,就可能出现回溯。
上述分析称为带回溯的自顶向下分析。对于 w,从文法的开始符号出发,反复使用不同的产生式谋求匹配输入串。当用某个非终结符号的候选式进行匹配失败时,删除这个失败分析建立的分析树分支并回头查输入符号,以便与其它候选式匹配。
这种带回溯的自顶向下分析技术,效率很低,代价极高,因此,它只有理论上的意义。
# 不包含回溯的递归下降分析法
在不含左递归和每个非终结符的所有候选式的终结首符集都两两不相交(不含左因子)条件下,构造一个不带回溯的自上而下分析程序,该分析程序由一组递归过程组成,每个过程对应文法的一个非终结符。这样的一个分析程序称为递归预测分析器。
在推导过程中,根据当前的向前看符号,决定选择哪个产生式往下推导,因此,分析过程是完全确定的。这种分析称为自顶向下的预测分析。
# 终结首符集辅助确定候选式
若当前符号 =,对 展开,而,那么,要知道哪一个 是获得以 开头的串的唯一替换式。
即:选择哪一个,亦即通过查看第一个(当前)符号来发现合适的替换式。
怎样选择?
- 以 为头的;
- 如有多个 以 为头的,这是文法的问题
定义 为串 的首符号集合;
若,则
若有产生式,且,当遇到输入字符 时,选择 进行推导。
若一个 有许多,如果 当任何两个候选式 和 均有
意味着 每一候选式 推导后所得的串的第一个 符号都不相同。
于是对输入符号,如果,则,因此对于输入符号,可以唯一确定 的候选式。
# 递归的预测分析法
递归的预测分析法是指:在递归下降分析中,对文法中的每个非终结符编写一个函数或子程序,其功能是识别由该非终结符所表示的语法成分,由于语言的文法常常是递归定义的,因此这组函数或子程序必须以递归方式调用。
void A() { | |
选择一个A的产生式,A->X1X2...Xn | |
for i = 1 to n | |
if (Xi是终结符号) | |
调用过程Xi() | |
else if (Xi等于当前输入符号) | |
GetChar() | |
else | |
出错处理 | |
} |
# 递归预测分析器的构造
- 向前看符号决定了选择哪个产生式右部的文法符号串与输入符号串匹配,在程序中由向前看符号可以确定一段程序完成与某一产生式右部的匹配。
- 每一个非终结符号对应一个函数,函数的功能是根据向前看符号扩展分析树,同时用该非终结符号的儿子结点匹配输入符号串。
# 非递归预测分析法
# 预测分析表
递归分析表是矩阵,其中行标 是文法的非终结符号,列标 是文法的终结符号或串终结符, 是 的一个候选式,指出当前栈顶符号是,输入符号是 时,应该用 进行推导,或者存放一个错误标记,指出 不该面临输入符号。
# 符号栈
把 和文法开始符号 推进栈,并读入输入串的第一个单词 ,重复下述过程直到正常结束或出错。
根据栈顶文法符号 和当前单词符号 的关系进行不同的处理:
- 若 ,分析成功,停止。匹配输入串成功。
- 若 ,把 从栈顶弹出,再读入下一个符号。
- 若 ,查分析表 。
# 预测分析控制程序
置ip指向输入串w#的第一个符号 | |
REPEAT | |
令X为栈顶符号,a为ip所指符号 | |
IF X in VT | X == # | |
IF X == a | |
POP | |
ip++ | |
ELSE | |
ERROR | |
ENDIF | |
ELSE | |
IF M[X, a] == X -> Y1Y2...Yk | |
POP | |
PUSH YkYk-1...Y1 | |
ELSE | |
ERROR | |
ENDIF | |
ENDIF | |
UNTIL X == # |
- 输出的产生式对应了最左推导
- 栈中:
残缺规范句型<未被匹配的句型>
- :指出 按哪一条产生式扩展,依赖于 表指出 时,如何扩充语法树,出错了可以立即发现。
# 预测分析表的构造
# 什么时候使用 产生式
如果当前某非终结符 与当前输入符 不匹配时,若存在 ,可以通过检查 是否可以出现在 的后面,来决定是否使用产生式 。若文法中无 ,则应报错。
# FIRST 集
令 是一个文法,则对任意文法串 ,定义
若 ,则规定 。
是 所有可能推导出的开头终结符号的集合或可能为空串 所组成的集合。
# FIRST 集的构造
# 单个符号的 FIRST 集
当 时,连续使用下面的规则,直至每个 集不再增大为止:
- 若 ,则
- 若 ,且有产生式 ,则将 加入到 中, 若 ,则将 加入到 中。
# 多个符号的 FIRST 集
当 时,把 的所有非 元素加入到,若 在 中,把 的所有非 元素也加入到,若 既在 中也在 中,把 的所有非 元素也加入到,...,若对所有的,,都含有,把 加入到。
# FOLLOW 集
令 是一个文法,则
若,则规定,作为输入串的右结束符号。
是所有句型中 后面可能出现的终结符号的集合或串终结符号所组成的集合。
# FOLLOW 集的构造
对于文法 的每个非终结符号 ,构造 的办法是,连续使用下面的规则,直至每个 集不再增大为止:
-
对于文法的开始符号 ,将符号 放入 中;
-
若 是一个产生式,则将 加入到 中;
-
若 是一个产生式,或 是一个产生式且 (即),则将 加入到 中。
# 预测分析表的构造方法
在对文法 的每个非终结符号 及其任意候选 都构造出 和 之后,可以构造 的分析表。
算法:非递归预测分析表的构造
- 对文法 的每个产生式 执行第 2 步和第 3 步;
- 对每个终结符号,把 加至 中;
- 若,则对任何,把 加至 中;
- 把所有无定义的 标上错误标记。
# LL (1) 文法
一个文法,若他的分析表 不含多重定义入口,则称 是 LL (1) 文法。
- L:从左向右扫描输入串
- L:用最左推导
- 1:在栈顶符号和输入符号的组合下,只有一个产生式可以被选择
一个文法 是 LL (1) 的,当且仅当对于 的每一个非终结符号 的任何两个不同的产生式 和,满足:
- 若,则
# 预测分析法实现步骤
- 构造文法
- 改造文法:消除二义性、消除左递归、消除回溯
- 求每个变量的 FIRST 集和 FOLLOW 集,从而求得每个候选式的 SELECT 集
- 检查是不是 LL (1) 文法。若是,构造预测分析表对于递归的预测分析,根据预测分析表为每一个非终结符编写一个过程;对于非递归的预测分析,实现表驱动的预测分析算法。
# 递归的预测分析法 vs. 非递归的预测分析法
比较项 | 递归的预测分析法 | 非递归的预测分析法 |
---|---|---|
程序规模 | 程序规模较大,不需载入分析表 | 主控程序规模较小,需载入分析表(表较小) |
直观性 | 较好 | 较差 |
效率 | 较低 | 分析时间大约正比于待分析程序的长度 |
自动生成 | 较难 | 较易 |
# 预测分析的错误检测
- 栈顶的终结符和当前输入符号不匹配
- 栈顶非终结符与当前输入符号在预测分析表对应项中的信息为空
# 预测分析中的错误恢复
恐慌模式
- 忽略输入中的一些符号,直到输入中出现由设计者选定的同步词法单元(synchronizing token)集合中的某个词法单元
- 其效果依赖于同步集合的选取。集合的选取应该使得语法分析器能从实际遇到的错误中快速恢复,例如可以把 FOLLOW(A)中的所有终结符放入非终结符的同步记号集合
- 如果终结符在栈顶而不能匹配,一个简单的办法就是弹出此终结符
synch
表示根据相应非终结符的 FOLLOW 集得到的同步词法单元
- 如果
M[A,a]
是空,表示检测到错误,根据恐慌模式,忽略输入符号a
- 如果
M[A,a]
是synch
,报错,弹出栈顶的非终结符A
,试图继续分析后面的语法成分 - 如果栈顶的终结符和输入符号不匹配,则弹出栈顶的终结符
# 自底向上语法分析
- 从分析树的底部(叶节点)向顶部(根节点)方向构造分析树
- 可以看成是将输入串 归约为文法开始符号 的过程
- 自顶向下的语法分析采用最左推导方式
- 自底向上的语法分析采用最左归约方式(反向构造最右推导)
- 自底向上语法分析的通用框架:移入 - 归约分析(Shift-Reduce Parsing)
# 规约
# 最右推导和最左归约
- 由最右推导得到的句型称为右句型, 最右推导也称为规范推导。
- 最左规约是最右推导的逆过程,即由最右推导的句型的某个右部规约到左部的过程,也称为规范规约。
# 规范规约
假定 是文法 的一个句子。称句型序列 是 的一个规范归约,如果此序列满足
- ;
- , 是把 中的句柄替换为相应产生式的左部符号得到的。
如果文法 是无二义的,那么,规范推导(最右推导)的逆过程必是规范归约(最左归约)。
# 移入 - 归约分析
把一个输入符号串逐步归约到文法的开始符号。
实现这种方法的大致过程是,用一个栈,把输入符号一个一个地移进到栈里,反复查看栈顶符号串,当栈顶形成某个产生式的右部(句柄)时,把栈顶的这一部分替换成(归约为)它的左部符号。直到移进所有的输入符号串时,若栈内是开始符号,则符合语法,否则不是文法的句子,这种分析称作 “移进一归约” 分析。
# 移入 - 归约分析的栈实现
“移进一归约” 分析器使用一个栈和一个存放输入符号串 的缓冲器。分析器的初始状态为:
工作过程:自左至右把串 的符号一一移进栈里,一旦栈顶形成句柄时,就进行归约。这种归约可能持续多次,直至栈顶不再呈现句柄为止。然后,继续向栈里移进符号,重复这个过程,直至最终形成如下格局:
# 移入 - 归约分析的动作
- 移入:将下一个输入符号移到栈的顶端
- 归约:被归约的符号串的右端必然处于栈顶。语法分析器在栈中确定这个串的左端,并决定用哪个非终结符来替换这个串
- 接收:宣布语法分析过程成功完成
- 报错:发现一个语法错误,并调用错误恢复子例
程
# 规范句型的活前缀
设有文法,若 是文法 的一个规范推导,其中,,,若存在 是 的一个前缀,则称 是文法的一个活前缀,如果 是含句柄的活前缀,则称 是文法的可归前缀(特殊活前缀)。
# 规范句型的活前缀的识别
对于一个文法,可以构造一个 DFA,它能够识别 的所有活前缀。如果将经过此 DFA 的弧上的文法符号依次入栈,那么栈内就一定是活前缀。
# 规范句型的可归活前缀的识别算法
产生式的右部加上一个圆点指示位置,用于标识产生式右部有多少成分被识别了,并且找到该右部正好被识别的时刻,才能保证此时被识别的部分仅仅包含一个句柄。
如果将产生式右部圆点不同的位置表示上述 DFA 的不同状态,那么就可以确定,经过某个弧后,是否在栈顶正好形成句柄。如果当前状态下圆点在产生式右部的末端,此时栈顶必然出现了句柄,此时的活前缀可以规约操作。
# LR 分析法
LR 分析法是一种有效的自底向上的语法分析技术,一般叫 LR(k)分析技术,其中的 “L” 是指从左至右扫描输入符号串,“R” 是指构造一个最右推导的逆过程,“k” 是指为了作出分析决定而向前看的输入符号的个数。
LR 分析方法是当前最广义的无回溯的 “移进 - 归约” 方法。根据栈中的符号串和向右查看输入串的 k(k≥0)个符号,就能唯一确定分析器的动作是移进还是归约,以及用哪个产生式进行归约。
# LR 分析法的优缺点
- 适合文法类足够大,适用于绝大多数上下文无关文法
- 分析效率高
- 报错及时
- 可以自动生成
- 手工实现工作量大
# LR 分析法的本质 —— 状态法
由于句柄是逐步形成的,用状态来描述形成句柄到达什么程度。
采用此方法,语法分析程序根据当前分析状态就可以确定句柄的头和尾,从而进行正确的归约。
LR 分析方法的关键就是根据上述状态,设计状态转换规则,从而实现识别活前缀的 NFA,进而实现识别活前缀的 DFA。
# 项目与状态
定义:对于文法 G,其产生式的右部添加一个特殊符号 ·
就构成文法的一个项目。后面也称为 LR(0)项目。
书写文法中所有产生式的项目,而每个项目都为识别活前缀的 NFA 的一个状态。
# 项目的分类
- 移进项目:,这类 LR(0)项目表示 出现在符号栈中,活前缀包含句柄的部分符号,为构成可归前缀,需继续将圆点后的终结符号 移进符号栈。因此这种形式的 LR(0)项目称为移进项目,它对应的状态为移进状态。
- 待约项目:,这类 LR(0)项目表示 出现在符号栈中,活前缀包含句柄的部分符号,为构成可归前缀,期待着把当前输入字符串中的相应内容先归约到。因此这种形式的 LR(0)项目称为待约项目,它对应的状态为待约状态。
- 归约项目:,这类 LR(0)项日表示句柄 a 刚好出现在符号栈栈顶,活前缀刚好包含句柄,即当前符号栈中的符号串正好为可归前缀,应按 进行归约。因此这种形式的 LR(0)项目称归约项目,它对应的状态为归约状态。
- 接受项目:对于拓广文法 ,有项目 ,它是一个特殊的归约项目,我们称它为接受项目,它对应的状态为接受状态。
# 状态的转移规则
- 若文法中有项目,项目,那么从状态 到状态 连接一条有向弧,标记为。
- 若项目,项目,那么从状态 到状态 连接一条有向弧,标记为。
# 拓广文法
对任何文法 ,在 中加进一个新的符号 和一个产生式 ,并以 代替 作为文法的开始符号,这样得到的一个与 等价的文法 称为 的拓广文法。
# 构造识别活前缀 NFA 的原则
- NFA 的状态集:由每个项目所对应的状态组成;
- 输入字符集合:由文法的文法符号组成包括终结符、非终结符和 ;
- 初态:对于文法 的拓广文法 ,有项目 ,由于 仅在第一个产生式的左部出现,所以规定它为初态;
- 终态:对于拓广文法 ,有项目 (即圆点在最后的项目),作为 NFA 的终态;
- 设计转换规则
# LR 分析器的实现
LR 分析器是带有下推栈的确定的有限状态自动机。将所设计的识别活前缀的 NFA,转换成等价的 DFA,并根据 DFA 设计分析表,并通过主控程序,查找分析表,完成了移进规约的分析过程。
由于分析表来自 NFA,LR 分析过程 结合输入的符号,执行分析表中涵盖的移进、规约等动作,保障了符号栈内一定是某个规范句型的活前缀,且能够判定当前活前缀尾部是否包含句柄。
# LR 分析器的逻辑结构
- 总控程序:LR 分析器在总控程序的控制下自左至右扫描输人串,根据当前分析栈顶所存放的文法符号状态及当前扫描读人的符号,查询 LR 分析表完成分析动作。
- 分析表:分析表是 LR 分析器的核心,它跟具体文法有关,包括动作表(action)和状态转换表(goto)两部分,均使用二维数组表示,为总控程序提供决策依据。
- 分析栈:包括文法符号栈
X[i]
和相应的状态栈S[i]
两部分(状态是指能识别活前缀的自动机状态),LR 分析器通过判断栈顶元素和输入符号查分析表确定下步分析动作。栈顶状态符号概括了在栈中位于它下边的全部信息,即从分析开始直到某一归约阶段的全部历史和展望
# LR 分析表的组成
# action 表
分析动作表以一个二维数组表示,行代表分析栈栈顶状态,列代表当前的输入符号,数组元素表示当前栈顶为状态为 ,而输入符号为 时,所执行的分析动作 。
- 移进:新状态 进状态栈,输入符号 进符号栈
- 归约:用相应的产生式进行归约 ;
- 接受:当文法符号归约到只剩下开始符号,且输入串结束时(当前输入为 ),分析成功;
- 报警:当状态栈顶为某一状态下,出现了不该出现的文法符号时,报错。
# goto 表
状态转换表也用一个二维数组表示,行代表分析栈栈顶状态,列代表文法符号(非终结符),数组元素表示当前栈顶为状态为;面对文法符号为 时,应转去的新状态为。
# LR 分析算法
- 将初始状态,和输入串的左边界()分别进分析栈;
- 根据状态栈栈顶和当前输入符号查动作表进行如下工作:
- 移进:若动作表中对应 “移进”,那么当前输入符号移入符号栈,并将所对应的新的状态进状态栈,继续扫描,即下一个输入符号变成当前的输入符号;
- 归约:若动作表中对应 “归约”,则按指定产生式进行归约,若产生式右部的符号串长度,则符号栈栈顶的 个符号为句柄,所以符号栈栈顶 个符号出栈,同时,状态栈顶的 个元素也出栈,归约后的文法符号(非终结符)进符号栈,并根据状态转换表查归约后的文法符号所对应的新状态进状态栈;
- 接受:若动作表中对应 “acc”,则分析成功;
- 出错:若动作表中对应空白,则报告错误信息。
- 重复以上(2)的工作直到接受或出错为止。
# LR 文法
泛泛地讲,一个上下文无关文法,若构造出无冲突的 LR 分析表,则说明该文法是 LR 文法。
具体说,一个上下文无关文法,若构造出无冲突的 LR(k)分析表,则说明该文法是 LR(k)文法。(k=0,1 )
LR 分析法是一种基本分析思想,LR 文法是一种泛泛的文法,当 k 具体有确定值时,其文法和分析方法就具体了。
# 构造识别文法所有活前缀的 DFA
- 手工操作:根据文法的所有项目,构造一个 NFA,然后将 NFA 转换成 DFA
- 机器操作:直接构造 LR(0)项目集规范族,然后根据项目集规范族构造 DFA
# 闭包和转移函数
# 闭包 - 形成项目集
定义 设 是文法 的一个 LR (0) 项目集合, 是从 出发用下面三个规则构造的项目集:
- 中每一个项目都属于 。
- 若项目 且 ,则将所有 的项目加入到 中。
- 重复执行(2)直到 不再增大为止。
# 转移函数 - 后继项目集
若 是文法 的一个 LR (0) 项目集,,
其中 , 称为转移函数。项目 称为项目 的后继项目。
# LR (0) 项目集规范族的构造算法
PROCEDURE items(G'); | |
BEGIN | |
C := {closure({S' -> .S})}; | |
REPEAT | |
FOR 任意 I in C and 任意 X in {V_T ∪ V_N} | |
把go(I, X)加入到C中; | |
UNTIL C 不再增大; | |
END; |
# LR (0) 分析表的构造
- 对于 ,若 ,则置 ;若 ,则置 。
- 对于 ,若 是 的第 个产生式,则对于所有 (包括 ),置 。
- 若 ,则置 。
- 其他情况均置错
# LR (0) 文法
-
项目集的相容性:我们已知,项目有 4 种类型,但在一个项目集中,不能有以下情况存在:
- 移进项目和归约项目并存;
- 多个归约项目并存;
若项目集能满足以上条件,则称相容的项目集。
-
冲突:项目集中含有的项目若符合以上两个条件,则称为项目集中含有冲突。若移进项目和归约项目并存,则称为移进 — 归约冲突,若多个归约项目并存,则称为归约一归约冲突。
-
LR(0)文法:若一个文法 G 的项目集规范族中的所有项目都是相容的,则称文法 G 为 LR(0)文法。
# SLR (1) 分析法
SLR (1) 分析法是对 LR (0) 分析法的改进,增加了 集的信息,减少了规约的数量,以解决 LR (0) 分析法中的移进 - 归约冲突和归约 - 归约冲突。
# SLR (1) 分析表的构造
为了构造 SLR(1)分析表,将刚才例中的情况一般化:根据不同的向前看符号将状态 中的各个项目所对应的动作加以区分,从而解决冲突动作。
设有一个 LR(0)的规范族中有如下项目集(状态)
项目集中含有一个移进项目和两个归约项目。就是说 是不相容的项目集,如果用 LR(0)分析表的构造规则来构造分析表,将出现动作冲突,可用下面方法解决:
对于归约项目: 和 ,分别求 和 。若 , 和 互不相交,则冲突可以解决。
构造规则如下
- 对于 ,若 ,则置 ;若 ,则置 。
- 对于规约项目:,若 是 的第 个产生式,则对于任意输入符号 ,置 。
- 若 ,则置 。
- 其他情况均置错
# LR (1) 分析法
对于当前分析状态 ;
注意 为开始符号
把 作为用产生式 进行归约的搜索符,用于替代 SLR(1)分析法中的 ,并把搜索符号的集合放在项目的后面( )。这种处理方法称为 LR(1)方法。
一般的 LR(1)方法的功能在于集合 可能是 的一个怡当的子集。
# LR (1) 项目
在 LR(0)项目中放置一个向前搜索的符号 ,形成
是一个 LR(0)项目, 是一个终结符号,这种形式的项目称为 LR(1)项目, 称向前搜索符。
向前搜索符仅对归约项目 有意义,当当前输入符号是 时,才能用 进行归约。
# LR (1) 项目集的构造
LR(1)分析过程中的每个状态,就是包含若干 LR(1)项目的一个 LR(1)项目集,特殊的, 属于初始项目集中的一个 LR(1)项目,即,它表示:归约到最后一步,必须面临输入符号才行。所以文法的所有项目集的获得,就是从初始项目 出发,通过求其闭包再用转换函数逐步求出整个文法的 LR(1)项目集。
# LR (1) 项目集的闭包
算法:设 是文法 的一个 LR(1)项目集, 是通过以下三个规则构造出的项目集:
- 中的每个项目都属于 。
- 若有项目 属于 ,且 是一个产生式,则对于任意 ,将 加入到 中。
- 重复执行(2),直到 不再增大为止。
# LR (1) 项目集的转移函数
设 是文法 的一个 LR(1)项目集,,,其中 , 称为转移函数。项目 称为项目 的后继项目。
# LR (1) 分析表的构造
对于文法 ,其 LR(1) 分析表的构造规则为:
-
对于每个项目 ,如果 ,则设置 。
-
对于每个项目 ,如果该产生式是 的第 个产生式,则设置 。(其中 )
-
如果 ,则设置 。
-
对于每个非终结符号 ,如果 ,则设置 。
-
分析表中的空白处填入 error。
# LR (1) 文法
按上述算法构造的分析表,如果每个入口是唯一确定的,则称它为文法 G 的一张 LR(1)分析表。具有 LR(1)分析表的文法称为 LR(1)文法。使用 LR(1)分析表的分析器称作一个 LR(1)分析器。
# LALR (1) 分析法
LALR(1)分析的思想是对 LR(1)中能够合并的项目集进行合并,从而减少状态,对同一个文法,合并 LR(1)项目集之后得到的 LALR(1)分析表比 LR(1)分析表状态数少,具有和 SLR 分析表相同数目的状态,但却能胜任 SLR(1)所不能解决的问题。
# 同心集
若 LR(1)的两个项目集的 LR(0)项目全部相同,则称两个 LR(1)项目集具有相同的心。具有相同心的项目集称为同心集。
# LALR (1) 文法
若合并同心集后项目集规范族无分析动作冲突,则此表称为 LALR(1)分析表。相应的文法称为 LALR(1)文法。
# LR 分析的错误处理
# 恐慌模式
- 从栈顶向下扫描,直到发现某个状态 ,它有一个对应于某个非终结符 的 GOTO 目标,可以认为从这个 推导出的串中包含错误。
- 然后丢弃 0 个或多个输入符号,直到发现一个可能合法地跟在 之后的符号 为止。
- 之后将 压入栈中,继续进行正常的语法分析。
# 短语层次
- 检查 LR 分析表中的每一个报错条目,并根据语言的使用方法来决定程序员所犯的何种错误最有可能引起这个语法错误
- 然后构造出适当的恢复过程
# LR 和 LL 的比较
-
LL() 根据 和 确定使用哪条产生式;而 LR () 是在识别出整个 后,再往后看 个符号,然后确定使用哪条产生式。
LL() 文法都是 LR () 文法。
-
都能用形式化方法实现;
-
LR() 分析用手工构造是不可能的。
# 语法制导翻译与中间代码生成
# 语法制导翻译
# 语法制导翻译的基本思想
- 为上下文无关文法中的文法符号设置语义属性,用来表示语法成分对应的语义信息
- 文法符号是用与文法符号所在产生式(语法规则)相关联的语义属性值的计算规则来计算的
- 对于给定的输入串,构建 的语法分析树,并利用与产生式(语法规则)相关联的语义规则来计算分析树中各结点对应的语义属性值
# 语法制导翻译的处理方法
# 语法制导定义(SDD)
对上下文无关文法的推广
- 将每个文法符号和一个语义属性集合相关联
- 将每个产生式和一组语义规则相关联,这些规则用于计算该产生式中各文法
符号的属性值如果 X
是一个文法符号, a
是 X
的一个属性,则用 X.a
表示属性 a
在某个标号为 X
的分析树结点上的值
# 语法制导翻译方案(SDT)
语法制导翻译方案是在产生式右部嵌入了若干组程序片段的上下文无关文法,这些程序片段称为语义动作。按照惯例,语义动作放在花括号内。
# 语法制导定义
# 文法中的属性
- 属性通常分为两类:综合属性和继承属性。
- 综合属性用于 “自下而上” 传递信息
- 继承属性用于 “自上而下” 传递信息。
- 另一类属性:固有属性:词法分析器得到的值,如常量 5 的值
# 语法制导定义的属性
在一个 SDD 中, 都有与之相关联的一套语义规则,规则形式为
是一个函数,而且或者
- 是 的一个综合属性并且 是 中文法符号的属性,或者
- 是 中文法符号的一个继承属性并且 是 或 中的任何文法符号的属性。
在两种情况下,都说属性 b 依赖于属性。
# 属性文法
- 属性文法是在上下文无关文法的基础上,为文法每个终结符和非终结符配备若干相关的属性及其计算规则
- 没有副作用的语法制导定义,有时又称属性文法
- 副作用:除了属性计算的语义动作
# 综合属性
在分析树中,一个结点的综合属性值是从其子结点的属性值计算出来的;结点属性值的计算正好和自底向上分析建立分析树结点同步进行。
只使用综合属性的语法制导定义称为 S - 属性定义,也叫 S - 属性文法。
# 继承属性
每个文法符号可以和一个继承属性值联系,属性值的设置和语法结构的语义以及翻译程序的需要有关。
一个结点的继承属性值是由该结点兄弟结点和 / 或父结点的属性值计算出来的。
# 综合属性和继承属性的区别
- 非终结符既可有综合属性也可有继承属性,文法开始符号的所有继承属性作为属性计算前的初始值(或者开始符号没有继承属性)
- 终结符只有综合属性,它们由词法程序提供.
# 语义规则
- 对出现在产生式右边的继承属性和出现在产生式左边的综合属性都必须提供一个计算规则。属性计算规则中只能使用相应产生式中的文法符号的属性。
- 出现在产生式左边的继承属性和出现在产生式右边的综合属性不由所给的产生式的属性计算规则进行计算,由其它产生式的属性规则计算或者由属性计算器的参数提供。
- 语义规则所描述的工作可以包括属性计算、静态语义检查、符号表操作、代码(中间)生成等等。有些语义规则可能产生副作用(如产生代码),通常把这样的语义规则写成函数调用。
# SDD 的属性计算
# 依赖图
for 分析树中每一结点 n do | |
for 结点 n 的文法符号的每一个属性 a do | |
为 a 在依赖图中建立一个结点; | |
for 分析树中每一个结点 n do | |
for 结点 n 所用产生式对应的每一条语义规则 b := f(c1, c2, ..., ck) do | |
for i := 1 to k do | |
从结点 ci 到结点 b 构造一条有向边; |
若依赖图无环,则可以按照拓扑排序的顺序计算属性值。
# 树遍历
- 通过树遍历的方法计算属性的值
- 假设语法树已建立,且树中已带有开始符号的继承属性和终结符的综合属性
- 以某种次序遍历语法树,直至计算出所有属性
- 深度优先,从左到右的遍历
- 如果需要的话,可使用多次遍历。
While 还有未被计算的属性 do | |
VisitNode(S) | |
/* S是开始符号 */ | |
procedure VisitNode (N:Node); | |
begin | |
if N是一个非终结符 then | |
/* 假设其产生式为 N -> X1 X2 ⋯ Xm */ | |
for i := 1 to m do | |
if Xi ∈ Vn then | |
/* 即 Xi 是非终结符 */ | |
begin | |
计算 Xi 的所有能够计算的继承属性; | |
VisitNode (Xi) | |
end | |
计算 N 的所有能够计算的综合属性 | |
end |
# 一遍扫描
- 与树遍历的属性计算文法不同,一遍扫描的处理方法是在语法分析的同时计算属性值,而不是语法分析构造语法树之后进行属性的计算,而且并无需构造实际的语法树。
- 语义规则被计算的时机:
- 在自上而下的分析中,一个产生式匹配输入串成功时计算
- 在自下而上的分析中,一个产生式被用于归约时计
# 抽象语法树(AST)
抽象语法树(Abstract Syntax Tree,AST),在语法树中去掉那些对翻译不必要的信息,从而获得更有效的源程序中间表示。
mknode(op, left, right)
:建立一个运算符号结点,标号是op
,两个域left
和right
分别指向左子树和右子树。mkleaf(id, entry)
:建立一个标识符结点,标号为id
,一个域entry
指向标识符在符号表中的入口。mkleaf(num, val)
:建立一个数结点,标号num
,一个域val
用于存放数的值。
# S - 属性文法与自底向上翻译
- S - 属性文法,它只含有综合属性(S-SDD)。
- 综合属性可以在分析输入符号串的同时自下而上计算。
- 分析器可以保存与栈中文法符号有关的综合属性值,每当进行归约时,新的属性值就由栈中正在归约的产生式右边符号的属性值来计算。
- S - 属性文法的翻译器通常可借助于 LR 分析器实现。
- 在 S - 属性文法的基础上,LR 分析器可以改造为一个翻译器,在对输入串进行语法分析的同时对属性进行计算。
# L - 属性文法与语法制导定义
一个属性文法是 L - 属性文法(L-SDD),如果对于产生式 ,其每一个语义规则中的每一个属性都是一个综合属性,或者()的一个继承属性仅依赖于:
- 的继承属性(不能是 的综合属性);
- 产生式 左边符号 的属性。
# 语法制导翻译方案
语法制导翻译方案是语法制导定义的一种便于翻译的书写形式。其中语义规则或语义动作用花括号{} 括起来,可被插入到产生式右部的任何合适的位置上。
这是一种语法分析和语义动作交错的表示法,它表达在按深度优先遍历分析树的过程中何时执行语义动作。
翻译模式给出了使用语义规则进行计算的顺序。可看成是分析过程中翻译的注释。
# 设计翻译模式(根据属性文法)
前提:语法制导定义是 L - 属性文法 / 定义保证语义动作不会引用还没有计算的属性值。
# 只有综合属性的情况
为每一个语义规则建立一个包含赋值的动作,并把这个动作放在相应的产生式右边的末尾。
# 既有综合属性又有继承属性
- 产生式右边的符号的继承属性必须在这个符号以前的动作中计算出来,放在紧靠该符号的前面位置。
- 一个动作不能引用这个动作右边符号的综合属性。
- 产生式左边非终结符号的综合属性只有在它所引用的所有属性都计算出来以后才能计算。计算这种属性的动作通常可放在产生式右端的末尾。
# 翻译方案的设计
- 只有综合属性的语法制导翻译方案
- 适用于自底向上
- 有左递归,可以改造成下一类 L 属性文法
- 既有综合属性又有继承属性的语法制导翻译方案
- 适用于自顶向下
- 可以通过改造文法自底向上
# S - 属性文法与语法制导翻译
# S - 属性文法与自底向上的翻译
# 实现方法
LR 分析器增加语义栈,在对输入串进行语法分析的同时对属性进行计算。
定义式 归约时对应的翻译是 val[ntop] = f(val[top-2]
, val[top-1], val[top])
(具体可执行代码)。
在执行代码段之前找到归约后新的栈顶:
ntop := top - r + 1 |
其中, r
是句柄的长度。
执行代码后:
top = ntop |
# S - 属性文法与自顶向下的翻译
- 消除左递归、提取公因子
- 将语义动作看成终结符,在其处于栈顶将要被弹出时执行该动作。
# 关于左递归翻译模式更一般化的讨论
-
左递归 SDD
每一个文法符号都有一个综合属性,用相应的小写字母表示, 和 是任意函数。
-
消除左递归,文法转换成
-
再考虑语义动作,SDT 变为:
经过转换的 SDT 引入了 的继承属性 和综合属性。
# L - 属性文法与语法制导翻译
# 递归下降翻译器的设计
L - 属性文法允许一次遍历就计算出所有属性值。
自上而下分析文法的分析过程,从概念上说可以看成是深度优先遍历语法树的过程,因此,可以在自上而下语法分析的同时实现 L 属性文法的计算。
# 在递归的预测分析过程中进行语义翻译
- 为每个非终结符 A 构造一个函数,A 的每个继承属性对应该函数的一个形参,函数的返回值是 A 的综合属性值
- 对出现在 A 产生式右部中的每个文法符号的每个属性都设置一个局部变量
- 对于每个动作,将其代码复制到语法分析器,并把对属性的引用改为对相应变量的引用
# 在非递归的预测分析过程中进行语义翻译
- 与 S 属性文法的自顶向下翻译类似
- 注意文法具有左递归的处理
- 处理方法
- L 属性文法本身具有的继承属性,不做处理
- 对综合属性,消除左递归时,计算顺序发生变化,因此要增加继承属性来辅助传递结果
- 改成 SDT,进行分析(见 “S 属性文法与自顶向下的翻译” 一节)
- 注意:语义动作看成终结符,并当其在栈顶将要弹出时,执行语义动作。属性计算结果放在另一个栈内。
- 此时,语法栈和语义属性栈不同步。
# LR 分析过程中的语义翻译
一种可以自下而上的 L-SDD 的语法制导翻译方案
- 可以实现任何基于 LL(1)文法的 L 属性文法
- 也能实现许多(但不是所有)基于 LR(1)文法 L 属性文
法 - 给定一个以 LL 文法为基础的 L 属性文法,可以修改这个文法,并在 LR 语法分析过程中计算这个新文法之上的语义属性。
- 修改后的属性文法,要求所有语义动作都位于产生式末尾(保证在归约的同时进行语义动作处理)
# 中间代码生成
- “中间代码生成” 程序的任务是:把经过语法分析和语义分析而获得的源程序中间表示翻译为中间代码表示。
- 方法:语法制导翻译。
- 采用独立于机器的中间代码的好处:
- 便于进行与机器无关的代码优化;
- 使编译程序改变目标机更容易;易于编译器的移植
- 使编译程序的结构在逻辑上更为简单明确,以中间语言为界面,编译前端和后端的接口更清晰。
# 中间代码的表示
# 后缀式(逆波兰式)
- 后缀式:把操作数写在前面,把算符写在后面。
- 后缀式的构造方法:
- 如果 是一个变量或常量,则 的后缀式是 自身。
- 如果 是 形式的表达式,这里 是任何二元操作符,则 的后缀式为 ,这里 和 分别为 和 的后缀式。
- 如果 是 形式的表达式,则 的后缀式就是 的后缀式。
# 图表示法
- 图表示法包括 DAG(有向无环图)与 AST(抽象语法树)。
- 相同点:对于表达式中的每个子表达式,图中都有一个结点。一个内部结点代表一个操作符,它的孩子代表操作数。
- 不同点:在 DAG 图中代表公共子表达式的结点具有多个父结点,而在一棵抽象语法树中公共子表达式被表示为重复的子树。
# 三地址指令
- 三地址代码是由下面一般形式的语句构成的序列:
x:=y op z
- 每条语句通常包含三个地址:两个操作数地址,一个操作结果地址
- 地址可以是变量名、常量和编译器生成的临时变量
# 三地址指令的基本思想
- 给每个中间变量和计算结果命名
- 没有复合表达式
- 只有最基本的控制流
- 没有各种控制结构
- 只有
goto
,call
等
- 所以三地址码可以看成是抽象的指令集
- 通用的 RISC
# 常用三地址码
- 形如
x := y op z
或x := op z
的赋值指令; - 形如
x := y
的赋值指令; - 无条件跳转指令
goto L
; - 形如
if x goto L
的条件跳转指令; - 形如
if x relop y goto L
的条件跳转指令; - 过程调用和返回:
param x
用来指明参数;- 过程调用
call p, n
和函数调用y = call p, n
; return y
函数返回;
- 形如
x := y[i]
和x[i] := y
的变址赋值指令; - 形如
x := &y
、x := *y
、*x := y
的地址和指针赋值指令。
# 三地址码的具体形式
- 四元式:
[op, arg1, arg2, result]
- 三元式:
[op, arg1, arg2]
- 间接三元式:间接码表 + 三元式表
# 声明语句
- 对于声明语句,语义分析的主要任务就是收集标识符的类型等属性信息,并为每一个名字分配一个相对地址
- 相对地址:相对静态数据区基址或活动记录中局部数据区基址的一个偏移量。
- 名字的类型和相对地址信息保存在相应的符号表记录中
- 过程中的说明语句:一个过程中的所有说明语句作为一组来处理。用一个全程变量
Offset
来记录下一个数据在活动记录中的位置。
# 赋值语句
表达式的类型可以是整型、实型、字符型、数组。
lookup(id.name)= id.entry | nil
,检查是否在符号表中存在相应此名字的表项,返回值为空或标识符在符号表的入口.emit
,将生成的三地址代码送到输出文件上。
# 布尔表达式
-
布尔表达式:用布尔运算符号(
and
,or
,not
)作用到布尔变量或关系表达式上而组成。 -
布尔表达式的作用:
- 用作计算逻辑值。
- 用作控制流语句如
if-then
,if-then-else
和while-do
等之中的条件表达式。
-
本节考虑由如下文法生成的布尔表达式:
E -> E or E
| E and E
| not E
| (E)
| id relop id
| true
| false
# 数值表示布尔值
用数值 1 和 0 分别表示真和假,从而对布尔表达式的求值可以像对算术表达式的求值那样一步一步地来计算
- 用 1 和 0 表示真和假
a < b
改成if a < b then 1 else 0
# 布尔表达式的数值表示法的 SDT
emit
用于将一个三地址语句输送到文件中nextstat
给出下一条即将生成的三地址语句在输出序列中的序号- 每执行一次
emit
后,nextstat
自动加 1
# 用真假出口表示的翻译
作为其他语句的条件改变控制流程时使用,隐含着程序的位置。例: A or B
如果 A
为真,那么 B
的值就不必计算,此时 A or B
的值已定,为真。同理, A and B
如果 A
为假,那么 B
的值就不必计算,此时 A and B
的值已定,为假。用于翻译控制流语句中的布尔表达式尤其方便。
-
条件语句
if E then S1 else S2
,赋予E
两个出口,一个为真,一个为假
-
if a > c or b < d then S1 else S2 翻译成三地址代码
if a > c goto L2 // “真”出口 | |
goto L1 | |
L1: if b < d goto L2 // “真”出口 | |
goto L3 // “假”出口 | |
L2: (关于 S1 的三地址代码序列) | |
goto Lnext | |
L3: (关于 S2 的三地址代码序列) | |
Lnext: |
# 真假出口布尔表达式三地址代码的属性文法
- 语义函数
newlabel
,返回一个新的符号标号 - 对于一个布尔表达式
E
,设置两个继承属性E.true
是E
为‘真’时控制流转向的标号E.false
是E
为‘假’时控制流转向的标号
E.code
记录E
生成的三地址代码序列S.next
记录S
紧跟在后面的三地址码指令的标号
a < b
如何翻译?
if a < b goto E.true | |
goto E.false |
以下是用 ^^ 替代空单元格的 Markdown 表格版本:
产生式 | 语义规则 |
---|---|
E → id₁ relop id₂ |
E.code := gen('if ' id₁.place relop.op id₂.place 'goto' E.true) + gen('goto' E.false) |
E → E₁ or E₂ |
E₁.true := E.true; |
E₁.false := newlabel; |
|
E₂.true := E.true; |
|
E₂.false := E.false; |
|
E.code := E₁.code + gen(E₁.false ':') + E₂.code |
|
E → E₁ and E₂ |
E₁.true := newlabel; |
E₁.false := E.false; |
|
E₂.true := E.true; |
|
E₂.false := E.false; |
|
E.code := E₁.code + gen(E₁.true ':') + E₂.code |
# 回填
- 两遍扫描:
- 从给定的输入构造出一棵语法树;
- 对语法树按深度优先遍历,来进行定义中给出的翻译。
- 一遍扫描:
- 先产生暂时没有填写目标标号的转移指令。
- 对于每一条这样的指令作适当的记录,一旦目标标号被确定下来,再将它 “回填” 到相应的指令中。
# 使用一遍扫描的布尔表达式的翻译模式
布尔表达式文法:
8 M -> ε { M.quad := nextquad } | |
1 E -> E1 or M E2 | |
{ backpatch(E1.falselist, M.quad); | |
E.truelist := merge(E1.truelist, E2.truelist); | |
E.falselist := E2.falselist; } | |
2 E -> E1 and M E2 | |
{ backpatch(E1.truelist, M.quad); | |
E.truelist := E2.truelist; | |
E.falselist := merge(E1.falselist, E2.falselist); } | |
3 E -> not E1 | |
{ E.truelist := E1.falselist; | |
E.falselist := E1.truelist; } | |
4 E -> (E1) | |
{ E.truelist := E1.truelist; | |
E.falselist := E1.falselist; } | |
5 E -> id1 relop id2 | |
{ E.truelist := makelist(nextquad); | |
E.falselist := makelist(nextquad + 1); | |
emit('if' + id1.place + relop.op + id2.place + 'goto - '); | |
emit('goto - '); } | |
6 E -> true | |
{ E.truelist := makelist(nextquad); | |
emit('goto - '); } | |
7 E -> false | |
{ E.falselist := makelist(nextquad); | |
emit('goto - '); } |
# 控制流语句
# if-then
语句
S → if E then S1 | |
{ E.true := newlabel; | |
E.false := S.next; | |
S1.next := S.next; | |
S.code := E.code || emit(E.true, ':') || S1.code } |
# if-then-else
语句
S → if E then S1 else S2 | |
{ E.true := newlabel; | |
E.false := newlabel; | |
S1.next := S.next; | |
S2.next := S.next; | |
S.code := E.code || emit(E.true, ':') || S1.code | |
|| emit('goto', S.next) || emit(E.false, ':') || S2.code } |
# while-do
语句
S -> while E do S1 | |
{ S.begin := newlabel; | |
E.true := newlabel; | |
E.false := S.next; | |
S1.next := S.begin; | |
S.code := emit(S.begin, ':') || E.code || emit(E.true, ':') || S1.code || emit('goto', S.begin) } |