编译原理

时间:2024-04-11 17:55:09

文章目录

一、绪论

什么是编译

计算机程序设计语言及编译

编译原理

编译器在语言处理系统中的位置

编译原理

编译系统的结构

编译原理

词法分析概述

词法分析/扫描(Scanning)

词法分析的主要任务:从左向右逐行扫描源程序的字符,识别出各个单词,确定单词的类型。 将识别出的单词转换成统一的机内表示——词法单元(token)形式

编译原理

语法分析概述

语法分析器(parser)从词法分析器输出的token序列中识别出各类短语,并构造语法分析树(parse tree)

语义分析概述

语义分析的主要任务

  • 收集标识符的属性信息
    • 种属 (Kind)
      • 简单变量、复合变量(数组、记录、…)、过程、…
    • 类型 (Type)
      • 整型、实型、字符型、布尔型、指针型、…
    • 存储位置、长度
    • 作用域
    • 参数和返回值信息
      • 参数个数、参数类型、参数传递方式、返回值类型、…
  • 语义检查
    • 变量或过程未经声明就使用
    • 变量或过程名重复声明
    • 运算分量类型不匹配
    • 操作符与操作数之间的类型不匹配
      • 数组下标不是整数
      • 对非数组变量使用数组访问操作符
      • 对非过程名使用过程调用操作符
      • 过程调用的参数类型或数目不匹配
      • 函数返回类型有误

中间代码生成及编译器后端概述

常用的中间表示形式

  • 三地址码 (Three-address Code)
    • 三地址码由类似于汇编语言的指令序列组成,每个指令最多有三个操作数(operand)
  • 语法结构树/语法树 (Syntax Trees)

三地址指令的表示

  • 四元式 (Quadruples)
    • (op, y, z, x)
  • 三元式 (Triples)
  • 间接三元式 (Indirect triples)

目标代码生成器

  • 目标代码生成器以源程序的中间 表示形式作为输入,并把它映射到目标语言
  • 目标代码生成的一个重要任务是为程序中使用的变量合理分配寄存器

代码优化

为改进代码所进行的等价程序变换,使其运行得更快一些、占用空间更少一些,或者二者兼顾

二、语言及其文法

基本概念

字母表 (Alphabet)

字母表∑是一个有穷符号集合

字母表上的运算

  1. 字母表∑1和∑2的乘积( product) ∑1∑2 ={ab|a ∈ ∑1, b ∈ ∑2}

  2. 字母表∑的n次幂( power)

    • ∑0 ={ ε }

    • ∑n =∑n-1 ∑ , n ≥ 1

      字母表的n次幂:长度为n的符号串构成的集合

  3. 字母表∑的正闭包( positive closure)

    ∑+ = ∑∪∑2∪∑3∪…

    字母表的正闭包:长度正数的符号串构成的集合

  4. 字母表∑的克林闭包(Kleene closure)

    ∑ 克林闭包= ∑0∪∑+ = ∑0∪∑∪∑2∪∑3∪…

    字母表的克林闭包:任意符号串(长度可以为零)构成的集合

串 (String)

设∑是一个字母表,任意x∈∑克林闭包,x称为是∑上的一个串

串是字母表中符号的一个有穷序列

串s的长度,通常记作|s|,是指s中符号的个数

空串是长度为0的串,用 ε(epsilon)表示

串上的运算——连接

如果 x和y是串,那么x和y的连接(concatenation)是把y附加到x后面而形成的串,记作xy

空串是连接运算的单位元( identity),即,对于任何串s都有,εs = sε = s

设x,y,z是三个字符串,如果 x=yz, 则称y是x的前缀,z是x的后缀

串上的运算——幂

串s的幂运算

  • s0= ε
  • sn = sn-1 s , n ≥1

串s的n次幂:将n个s连接起来

文法的定义

文法的形式化定义

G=(VT ,VN ,P,S)

VT :终结符集合

VN:非终结符集合

P :产生式集合

S :开始符号

符号约定

  • 终结符
  1. 字母表中排在前面的小写字母,如a、b、c
  2. 运算符,如+、乘等
  3. 标点符号,如括号、逗号等
  4. 数字0、1、. . . 、9
  5. 粗体字符串,如id、if等
  • 非终结符
  1. 字母表中排在前面的大写字母,如A、B、C
  2. 字母S。通常表示开始符号
  3. 小写、斜体的名字,如expr、stmt等
  4. 代表程序构造的大写字母。如E(表达式)、T(项) 和F(因子)

编译原理

语言的定义

推导 (Derivations)和归约(Reductions)

句子的推导(派生)-从生成语言的角度

句子的归约 - 从识别语言的角度

句型和句子

如果 S ->star α,α∈(VT∪VN)star ,则称α是G的一个句型 (sentential form)

  • 一个句型中既可以包含终结符,又可以包含非终 结符,也可能是空串

如果 S ->star w,w ∈VTstar,则称w是G的一个句子(sentence)

  • 句子是不包含非终结符的句型

语言的形式化定义

由文法G的开始符号S推导出的所有句子构成 的集合称为文法G生成的语言,记为L(G )。 即

L(G )= {w | S ->star w, w∈ VT star }

语言上的运算

编译原理

文法的分类

Chomsky 文法分类体系

前提:α →β

0型文法:α中至少包含1个非终结符

1型文法(CSG) :|α|≤|β|

  • 上下文有关文法(Context-Sensitive Grammar , CSG )

2型文法(CFG) :α ∈ VN

  • 上下文无关文法 (Context-Free Grammar, CFG )

3型文法(RG):A→wB 或 A→w (A→Bw 或A→w)

编译原理

CFG的分析树

CFG 的分析树

编译原理

  • 根节点的标号为文法开始符号
  • 内部结点表示对一个产生式A→β的应用,该结点的标号是此产生式左部A 。该结点的子结点的标号从左到右构成了产生式的右部β
  • 叶结点的标号既可以是非终结符,也可以是终结符。从左到右排列叶节点得到的符号串称为是这棵树的产出( yield )或边缘(f rontier)

分析树是推导的图形化表示

给定一个推导 S -> α1-> α2->…-> αn ,对于推导 过程中得到的每一个句型αi,都可以构造出一个 边缘为αi的分析树

编译原理

(句型的)短语

给定一个句型,其分析树中的每一棵子树的边缘称为该句型的一个短语(phrase)

  • 如果子树只有父子两代结点,那么这棵子树的边缘称为该句型的一个直接短语(immediate phrase)

编译原理

二义性文法 (Ambiguous Grammar)

如果一个文法可以为某个句子生成多棵分析树, 则称这个文法是二义性的

二义性文法的判定

对于任意一个上下文无关文法,不存在一个算法, 判定它是无二义性的;但能给出一组充分条件, 满足这组充分条件的文法是无二义性的

  • 满足,肯定无二义性
  • 不满足,也未必就是有二义性的

三、词法分析

正则表达式

正则表达式 (Regular Expression,RE ) 是一种用来描述正则语言的更紧凑的表示方法

正则表达式可以由较小的正则表达式按照特定规则递归地 构建。每个正则表达式 r定义(表示)一个语言,记为L(r )。 这个语言也是根据r 的子表达式所表示的语言递归定义的。

正则表达式的定义

  • ε是一个RE,L(ε) = {ε}
  • 如果 a∈∑,则a是一个RE,L(a) = {a}
  • 假设 r和 s都是 RE,表示的语言分别是 L( r)和L(s),则
    • r|s 是一个RE,L( r|s ) = L (r )∪L(s)
    • rs 是一个RE,L( rs ) = L(r ) L(s)
    • r star是一个RE,L( r star)= (L(r ))star
    • (r ) 是一个RE,L( (r ) ) = L(r )

运算的优先级:*、连接、|

正则语言

可以用RE定义的语言叫做正则语言(regular language)或正则集合(regular set)

正则文法与正则表达式等价

对任何正则文法 G,存在定义同一语言的 正则表达式 r

对任何正则表达式 r,存在生成同一语言的 正则文法 G

正则定义(Regular Definition)

编译原理

有穷自动机 ( Finite Automata,FA )

  • 一类处理系统 建立的数学模型
  • 具有一系列离散的输入输出信息和有穷数目的内部状态(状态:概括了对过去输入信息处理的状况)
  • 系统只需要根据当前所处的状态和当前面临的输入信息就可以决定系统的后继行为。每当系统处理了当前的输入后,系统的内部状态也将发生改变

FA模型

编译原理

FA的表示

转换图 (Transition Graph)

  • 结点:FA的状态
    • 初始状态(开始状态):只有一个,由start箭头指向
    • 终止状态(接收状态):可以有多个,用双圈表示
  • 带标记的有向边:如果对于输入a,存在一个从状态p到状 态q的转换,就在p、q之间画一条有向边,并标记上a

编译原理

FA定义(接收)的语言

给定输入串x,如果存在一个对应于串x的从初始状态到某个终止状态的转换序列,则称串x被该FA接收

由一个有穷自动机M接收的所有串构成的集合称为是该FA定义(或接收)的语言,记为L(M )

最长子串匹配原则 (Longest String Matching Principle)

当输入串的多个前缀与一个或多个模式匹配时, 总是选择最长的前缀进行匹配

在到达某个终态之后,只要输入带上还有符号, DFA就继续前进,以便寻找尽可能长的匹配

有穷自动机的分类

确定的FA (Deterministic finite automata, DFA)

编译原理

非确定的FA (Nondeterministic finite automata, NFA)

编译原理

DFA和NFA的等价性

  • 对任何非确定的有穷自动机N ,存在定义同一语言的确定的有穷自动机D
  • 对任何确定的有穷自动机D ,存在定义同一语言的 非确定的有穷自动机N

从正则表达式到有穷自动机

编译原理

从NFA到DFA的转换

子集构造法(subset construction)

编译原理

计算 ε-closure(T)

编译原理

确定性有限自动机的简化

DFA化简

概念:指寻找一个状态数比DFA M少的DFA M’,使得L(M)=L(M’)

一个确定有限自动机M是通过①消除多余状态和②合并等价状态而转换成一个最小的与之等价的有限自动机来实现其化简的。

多余状态(无用状态)

概念:从该自动机的初始状态出发,任何输入串也不能到达的那个状态。

对于给定的DFA M,若它含有多余状态,可以非常简单地将多余状态消除,而得到与之等价的有穷自动机。

两个状态S和T等价的条件

  1. 如果从S出发能读出某个字w而停于终态,那么从T出发也能读出同样的字而停于终态;
  2. 如果从T出发能读出某个字w而停于终态,那么从S出发也能读出同样的字而停于终态。

在DFA M里如果两个状态若不等价,则称这两个状态是可区别的。

DFA的状态最少化算法

子集分割法:把一个DFA M的状态分割成一些不相交的子集,使得任何不同的两个子集的状态都是可区别的,而同一子集中的两个状态都是等价的。最后,在每个子集中选出一个代表,同时消去其他等价的状态。如果在从M′中删除所有的无用状态(多余状态),则M′便是最简的(状态最少),从而得到把DFA M状态简化了的DFA M’。

编译原理

编译原理

四、语法分析

自顶向下的分析 (Top-Down Parsing)

每一步推导中,都需要做两个选择

  • 替换当前句型中的哪个非终结符
  • 用该非终结符的哪个候选式进行替换

最左推导 (Left-most Derivation)

在最左推导中,总是选择每个句型的最左非终结符进行替换

如果S->* α,则称α是当前文法的最左句型 (left-sentential form)

自顶向下的语法分析采用最左推导方式

最右推导 (Right-most Derivation)

在最右推导中,总是选择每个句型的最右非终结符进行替换

在自底向上的分析中,总是采用最左归约的方式,因此把最左归约称为规范归约,而最右推导相应地称为规范推导

递归下降分析 (Recursive-Descent Parsing)

  • 由一组过程组成,每个过程对应一个非终结符
  • 从文法开始符号S对应的过程开始,其中递归调用文法中其它非终结符对应 的过程。如果S对应的过程体恰好扫描了整个输入串,则成功完成语法分析
  • 可能需要回溯 (backtracking), 导致效率较低

预测分析 (Predictive Parsing)

预测分析是递归下降分析技术的一个特例,通过 在输入中向前看固定个数(通常是一个)符号来选 择正确的A-产生式。

可以对某些文法构造出向前看k个输入符号的预测分析 器,该类文法有时也称为LL(k) 文法类

预测分析不需要回溯,是一种确定的自顶向下分析方法

文法转换

左递归

含有A→Aα形式产生式的文法称为是直接左递归的 (immediate left recursive)

如果一个文法中有一个非终结符A使得对某个串α存在一个推导A->+Aα ,那么这个文法就是左递归的

经过两步或两步以上推导产生的左递归称为是间接左递归的

消除直接左递归

编译原理
事实上,这种消除过程就是把左递归转换成了右递归

消除间接左递归

编译原理

提取左公因子 (Left Factoring )

通过改写产生式来推迟决定,等读入了足够多的输入,获得足够信息后再做出正确的选择

编译原理

LL(1) 文法

S _ 文 法-简单的确定性文法

  • 每个产生式的右部都以终结符开始
  • 同一非终结符的各个候选式的首终结符都不同

S_文法不含ε产生式

预测分析法的工作过程:从文法开始符号出发,在每一步推导过程中根据当前句型 的最左非终结符A和当前输入符号a,选择正确的 A- 产生式。为保证分析的确定性,选出的候选式必须是唯一的。

非终结符的后继符号集

可能在某个句型中紧跟在A后边的终结符a的集合,记为FOLLOW(A),FOLLOW(A)={a| S->αAaβ, a∈VT,α,β∈(VT ∪ VN)*}

如果 A是某个句型的的最右符号, 则将结束符 “$” 添加到 FOLLOW(A) 中

产生式的可选集

产生式A→β的可选集是指可以选用该产生式进行推导时对应的输入符号的集合,记为SELECT( A→β )

SELECT( A→aβ ) = { a }

SELECT( A→ε )=FOLLOW( A )

q_ 文法
  • 每个产生式的右部或为ε ,或以终结符开始
  • 具有相同左部的产生式有不相交的可选集

q_文法不含右部以非终结符打头的产生式

串首终结符集

串首终结符

串首第一个符号,并且是终结符,简称首终结符

给定一个文法符号串α, α的串首终结符集FIRST(α)被定义为可以从α推导出的所有串首终结符构成的集合。

  • 如果α ->* ε, 那么ε也在FIRST(α)中
  • 对于任意α∈(VT ∪ V N)+, FIRST(α)={ a | α->* aβ,a∈ VT,β∈(VT ∪ VN)*};

产生式A→α的可选集SELECT

  • 如果ε不属于FIRST(α), 那么SELECT(A→α)=FIRST(α)
  • 如果 ε∈FIRST(α), 那么SELECT(A→α)=( FIRST(α)-{ε} )∪FOLLOW(A)

LL(1) 文法

文法G是LL(1)的,当且仅当G的任意两个具有相同左部的产生式A → α | β 满足下面的条件

  • 不存在终结符a使得α 和β都能够推导出以a开头的串
  • α 和β至多有一个能推导出ε
  • 如果 β -> ε,则FIRST (α)∩FOLLOW(A) =Φ;如果 α -> ε,则FIRST (β)∩FOLLOW(A) =Φ

sum=同一非终结符的各个产生式的可选集互不相交

  • 第一个“L” 表示从左向右扫描输入
  • 第二个“ L” 表示产生最左推导
  • “1” 表示在每一步中只需要向前看一个输入符号来决定语法分析动作

FIRST集 和 FOLLOW集 的计算

编译原理

计算文法符号X的FIRST(X )

算法:不断应用下列规则,直到没有新的终结符或ε可以被 加入到任何FIRST集合中为止

  • 如果X是一个终结符,那么FIRST ( X ) = { X }
  • 如果X是一个非终结符,且 X→Y1…Yk∈P (k≥1),那么如果对于某个i,a在FIRST (Yi ) 中且ε 在所有的FIRST(Y1) , … , FIRST(Yi-1)中(即Y1…Yi-1->* ε ),就把a加入 到FIRST( X )中。如果对于所有的 j = 1,2, . . . , k,ε在 FIRST(Yj)中,那么将ε加入到FIRST( X )
  • 如果 X→ε∈P,那么将ε加入到FIRST( X ) 中

计算串X1X2 …Xn的FIRST 集合

编译原理

编译原理

计算非终结符A的FOLLOW(A)

算法:不断应用下列规则,直到没有新的终结符可以被加 入到任何FOLLOW集合中为止

  • 将$放入FOLLOW( S )中,其中S是开始符号,$ 是输入右 端的结束标记
  • 如果存在一个产生式A→αBβ,那么FIRST ( β )中除ε 之外 的所有符号都在FOLLOW( B )中
  • 如果存在一个产生式A→αB,或存在产生式A→αBβ且 FIRST ( β ) 包含ε,那么 FOLLOW( A ) 中的所有符号都在 FOLLOW( B ) 中

递归的预测分析法

递归的预测分析法是指:在递归下降分析中,根据预测分析表进行产生式的选择

根据每个非终结符的产生式和LL(1)文法的预测分析表,为每个非终结符编写对应的过程

编译原理

非递归的预测分析法

非递归的预测分析不需要为每个非终结符编写递归下降过程,而是根据预测分析表构造一个自动机,也叫表驱动的预测分析

编译原理

编译原理

表驱动的预测分析法

编译原理

预测分析法实现步骤

  1. 构造文法
  2. 改造文法:消除二义性、消除左递归、消除回溯
  3. 求每个变量的FIRST集和FOLLOW集,从而求得每个候选式的SELECT集
  4. 检查是不是 LL(1) 文法,若是,构造预测分析表
  5. 对于递归的预测分析,根据预测分析表为每一个非终结符编写一个过程;对于非递归的预测分析,实现表驱动 的预测分析算法

自底向上分析

自底向上分析概述

自底向上的语法分析

  • 自顶向下的语法分析采用最左推导方式
  • 自底向上的语法分析采用最左归约方式(反向构造最右推导)

自底向上语法分析的通用框架:移入 - 归约分析(Shift-Reduce Parsing)

编译原理

移入-归约分析器可采取的4种动作

  • 移入:将下一个输入符号移到栈的顶端
  • 归约:被归约的符号串的右端必然处于栈顶。语法 分析器在栈中确定这个串的左端,并决定用哪个非 终结符来替换这个串
  • 接收:宣布语法分析过程成功完成
  • 报错:发现一个语法错误,并调用错误恢复子例程

LR分析法概述

LR 分析法

LR文法是最大的、可以构造出相应移入 - 归约语法分析器的文法类

  • L: 对输入进行从左到右的扫描
  • R: 反向构造出一个最右推导序列

LR(k)分析:需要向前查看k个输入符号的LR分析

k = 0和k = 1这两种情况具有实践意义,当省略 (k) 时,表示 k =1

LR 分析法的基本原理

自底向上分析的关键问题是什么? ans:如何正确地识别句柄

句柄是逐步形成的,用“状态”表示句柄识别的进展程度

编译原理

LR 分析器(自动机)的总体结构

编译原理

LR(0)分析

LR(0) 项目

右部某位置标有圆点的产生式称为相应文法的一个LR(0) 项目(简称为项目)

  • S → ·bBB 移进项目
  • S → b·BB 待约项目
  • S → bB·B 待约项目
  • S → bBB· 归约项目

项目描述了句柄识别的状态

产生式A→ε 只生成一个项目A→ ·

增广文法 (Augmented Grammar)

如果G 是一个以S为开始符号的文法,则G的增广文法 G’ 就是在G中加上新开始符号S’ 和产生式S’ → S而得到的文法

引入这个新的开始产生式的目的是使得文法开始符号仅出现在一个产生式的左边,从而使得分析器只有一个接受状态

文法中的项目

后继项目(Successive Item)

同属于一个产生式的项目,但圆点的位置只相差一个符号,则称后者是前者的后继项目

eg A→α· Xβ的后继项目是A→αX·β

可以把等价的项目组成一个项目集( I ) ,称为项目集闭包 (Closure of Item Sets),每个项目集闭包对应着自动机的一个状态

编译原理

LR(0)分析表构造算法

CLOSURE( ) 函数

计算给定项目集I的闭包 CLOSURE(I) =I ∪{B→·γ|A→α·Bβ∈CLOSURE(I),B→γ∈P}

GOTO ( ) 函数

返回项目集I对应于文法符号X的后继项目集闭包GOTO( I, X )=CLOSURE({A→αX·β | A→α·Xβ∈I })

构造 LR(0) 自动机的状态集

规范LR(0) 项集族(Canonical LR(0) Collection)C={I0}∪{I | 存在J∈C, X∈VN∪ VT , I=GOTO(J , X) }

LR(0) 分析表构造算法

编译原理

LR(0) 自动机的形式化定义

编译原理

SLR分析

SLR 分析

编译原理

SLR 分析表构造算法

编译原理

LR(1)分析

LR(1)分析法的提出

SLR分析存在的问题:SLR只是简单地考察下一个输入符号b是否属于与归约项目A→α相关联的FOLLOW(A),但b∈FOLLOW(A) 只是归约α的一个必要条件,而非充分条件

对于产生式 A→α的归约,在不同的使用位置,A会要求不同的后继符号

在特定位置,A的后继符集合是 FOLLOW(A) 的子集

规范LR(1)项目

将一般形式为 [A→α· β, a]的项称为 LR(1) 项,其中A→αβ 是 一个产生式,a 是一个终结符(这里将$视为一个特殊的终结符) 它表示在当前状态下,A后面必须紧跟的终结符,称为该项的展望符

LR(1) 中的1指的是项的第二个分量的长度

在形如[A→α·β, a]且β ≠ ε的项中,展望符a没有任何作用,但是一个形如 [A→α·, a]的项在只有在下一个输入符号等于a时才可 以按照A→α 进行归约

这样的a的集合总是FOLLOW(A)的子集,而且它通常是一个真子集

等价LR(1)项目

编译原理

b∈FIRST(βa)

LR(1)项目集闭包

CLOSURE( I ) = I ∪ { [B→·γ, b] | [A→α·Bβ,a] ∈ CLOSURE( I ), B→γ∈P, b∈FIRST(βa) }

GOTO 函数

GOTO( I, X ) = CLOSURE({[A→αX·β,a]|[A→α·Xβ, a]∈I })

LR(1)自动机的形式化定义

编译原理

LR分析表构造算法

编译原理

LALR分析法

LALR ( lookahead-LR )分析的基本思想

  1. 寻找具有相同核心的LR (1) 项集,并将这些项集合并为一个项集。 所谓项集的核心就是其第一分量的集合。
  2. 根据合并后得到的项集族构造语法分析表
  3. 如果分析表中没有语法分析动作冲突,给定的文 法就称为LALR (1) 文法,就可以根据该分析表 进行语法分析

合并同心项集后,虽然不产生冲突,但可能会推迟错误的发现

合并同心项集时会产生归约-归约冲突

编译原理

合并同心项集不会产生移进 - 归约冲突

存在的小问题

合并同心项集后,虽然不产生冲突,但可能会推迟错误的发现

编译原理

LALR分析法可能会作多余的归约动作, 但绝不会作错误的移进操作,因为合并同心项集不会影响移进

LALR(1)的特点

  • 形式上与 LR(1) 相同
  • 大小上与 LR(0)/SLR 相当
  • 分析能力介于SLR 和 LR(1) 二者之间:SLR<LALR(1)<LR(1)
  • 合并后的展望符集合仍为FOLLOW 集的子集

五、语法制导翻译

语法制导翻译概述

什么是语法制导翻译

编译原理

语法制导翻译的基本思想

如何表示语义信息?

  • 为CFG中的文法符号设置语义属性,用来表示语法成分对应的语义信息

如何计算语义属性?

  • 文法符号的语义属性值是用与文法符号所在产生式 (语法规则)相关联的语义规则来计算的
  • 对于给定的输入串x ,构建x的语法分析树,并利用与 产生式(语法规则)相关联的语义规则来计算分析树 中各结点对应的语义属性值

两个概念

将语义规则同语法规则(产生式)联系起来要 涉及两个概念

  • 语法制导定义(Syntax-Directed Definitions, SDD)
  • 语法制导翻译方案 (Syntax-Directed Translation Scheme , SDT )

语法制导定义 (SDD)

SDD 是对CFG的推广

  • 将每个文法符号和一个语义属性集合相关联
  • 将每个产生式和一组语义规则相关联,这些规则用于计算该产生式中各文法符号的属性值

编译原理

语法制导翻译方案( SDT )

SDT是在产生式右部嵌入了程序片段的CFG,这些程序片段称为语义动作。

编译原理

一个语义动作在产生式中的位置决定了这个动作的执行时间

SDD与SDT

SDD

  • 是关于语言翻译的高层次规格说明
  • 隐蔽了许多具体实现细节,使用户不必显式地说明翻译发生的顺序

SDT

  • 可以看作是对SDD的一种补充,是SDD的具体实施方案
  • 显式地指明了语义规则的计算顺序,以便说明某些实现细节

语法制导定义SDD

综合属性(synthesized attribute)

在分析树结点 N上的非终结符A的综合属性只能通过N的子结点或 N本身的属性值来定义

终结符可以具有综合属性:终结符的综合属性值是由词法分析器提供的词法值,因此在SDD中没有计 算终结符属性值的语义规则

继承属性(inherited attribute)

在分析树结点 N上的非终结符A的继承属性只能通过N的父结点、N的兄弟结点或 N本身的属性值来定义

终结符没有继承属性:终结符从词法分析器处获得 的属性值被归为综合属性值

属性文法 (Attribute Grammar)

一个没有副作用的SDD有时也称为属性文法

属性文法的规则仅仅通过其它属性值和常量来定义一个属性值

SDD的求值顺序

SDD的求值顺序

按照什么顺序计算属性值?

  • 语义规则建立了属性之间的依赖关系,在对语法分析树节点的一个属性求值之前,必须首先求出 这个属性值所依赖的所有属性值

依赖图(Dependency Graph)

  • 依赖图是一个描述了分析树中结点属性间依赖关系的有向图
  • 分析树中每个标号为X的结点的每个属性a都对应 着依赖图中的一个结点
  • 如果属性X.a的值依赖于属性Y.b的值,则依赖图 中有一条从Y.b的结点指向X.a的结点的有向边

属性值的计算顺序

可行的求值顺序是满足下列条件的结点序列N1, N2, … , Nk :如果依赖图中有一条从结点Ni到 Nj 的边(Ni→Nj), 那么i < j(即:在节点序列中,Ni 排在Nj 前面)

这样的排序将一个有向图变成了一个线性排序, 这个排序称为这个图的拓扑排序(topological sort)

拓扑排序存在性
  • 对于只具有综合属性的SDD ,可以按照任何自 底向上的顺序计算它们的值
  • 对于同时具有继承属性和综合属性的SDD,不能保证存在一个顺序来对各个节点上的属性进行求值
    • 如果图中没有环,那么至少存在一个拓扑排序
    • 从计算的角度看,给定一个SDD,很难确定是否存在某棵语法分析树,使得SDD的属性之间存在循环依赖关系;幸运的是,存在一个SDD的有用子类,它们能够保证对每棵语法分析树都存在一个求值顺序,因为它们不允许产生带有环的依赖图

S-属性定义与L-属性定义

S-属性定义

仅仅使用综合属性的SDD称为S属性的SDD,或S-属性定义、S-SDD

  • 如果一个SDD是S属性的,可以按照语法分析树节点的任何 自底向上顺序来计算它的各个属性值
  • S-属性定义可以在自底向上的语法分析过程中实现

L-属性定义

一个SDD是L-属性定义,当且仅当它的每个属性要 么是一个综合属性,要么是满足如下条件的继承属 性:假设存在一个产生式A→X1X2…Xn,其右部符 号Xi (1<= i <= n)的继承属性仅依赖于下列属性:

  • A的继承属性(如果是综合属性,可能会形成环)
  • 产生式中Xi左边的符号 X1, X2, … , Xi-1 的属性
  • Xi本身的属性,但Xi 的全部属性不能在依赖图中形成环路

注:每个S-属性定义都是L-属性定义

语法制导翻译方案SDT

语法制导翻译方案SDT

语法制导翻译方案(SDT )是在产生式右部中嵌入了程序片段(称为语义动作)的CFG

SDT可以看作是SDD的具体实施方案

  • 基本文法可以使用LR分析技术,且SDD是S属性的
  • 基本文法可以使用LL分析技术,且SDD是L属性的

将S-SDD转换为SDT

将一个S-SDD转换为SDT的方法:将每个语义动作都放在产生式的最后

S-属性定义的SDT 实现

如果一个S-SDD的基本文法可以使用LR分析技术,那么它的SDT可以在LR语法分析过程中实现

将L-SDD转换为SDT

将L-SDD转换为SDT的规则

  • 将计算某个非终结符号A的继承属性的动作插入 到产生式右部中紧靠在A的本次出现之前的位置上
  • 将计算一个产生式左部符号的综合属性的动作放 置在这个产生式右部的最右端

L-属性定义的SDT 实现

如果一个L-SDD的基本文法可以使用LL分析技术,那么它的SDT可以在LL或LR语法分析过程中实现

  • 在非递归的预测分析过程中进行语义翻译
  • 在递归的预测分析过程中进行语义翻译
  • 在LR分析过程中进行语义翻译

在非递归的预测分析过程中进行翻译

扩展语法分析栈

编译原理

分析栈中的每一个记录都对应着一段执行代码

  • 综合记录出栈时,要将综合属性值复制给后面特定的语义动作
  • 变量展开时(即变量本身的记录出栈时),如果其含有继承属性,则要将继承属性值复制给后面特定的语义动作

在递归的预测分析过程中进行翻译

算法

  • 为每个非终结符A构造一个函数,A的每个继承属性对应该函数的一个形参,函数的返回值是A的综 合属性值。对出现在A产生式中的每个文法符号的 每个属性都设置一个局部变量
  • 非终结符A的代码根据当前的输入决定使用哪个产生式
  • 与每个产生式有关的代码执行如下动作:从左到右考虑 产生式右部的词法单元、非终结符及语义动作
    • 对于带有综合属性x的词法单元 X,把x的值保存在局部变量 X.x中;然后产生一个匹配 X的调用,并继续输入
    • 对于非终结符B,产生一个右部带有函数调用的赋值语句c := B(b1 , b2 , …, bk),其中, b1 , b2 , …, bk是代表B的继承属性的变 量,c是代表B的综合属性的变量
    • 对于每个动作,将其代码复制到语法分析器,并把对属性的引 用改为对相应变量的引用

L-属性定义的自底向上翻译

给定一个以LL文法为基础的L-SDD,可以修改这个文法,并在LR语法分析过程中计算这个新文法之上的SDD

  • 首先构造SDT,在各个非终结符之前放置语义动作来计算它的继承属性, 并在产生式后端放置语义动作计算综合属性
  • 对每个内嵌的语义动作,向文法中引入一个标记非终结符来替换它。每个这样的位置都有一个不同的标记,并且对于任意一个标记M都有一个产生 式M→ε
  • 如果标记非终结符M在某个产生式A→α{a}β中替换了语义动作a,对a进行 修改得到a’ ,并且将a’关联到M→ε 上。动作a’
    1. 将动作a需要的A或α中符号的任何属性作为M的继承属性进行复制
    2. 按照a中的方法计算各个属性,但是将计算得到的这些属性作为M的综合属性

六、中间代码生成

类型表达式

类型表达式 (Type Expressions)

  • 基本类型是类型表达式
  • 可以为类型表达式命名,类型名也是类型表达式
  • 将类型构造符(type constructor)作用于类型表达式可以构成新的类型表达式
    • 数组构造符array
      • 若T是类型表达式,则array ( I, T )是类型表达式( I是一个整数)
    • 指针构造符pointer
      • 若T 是类型表达式,则 pointer ( T ) 是类型表达式,它表示一个指针类型
    • 笛卡尔乘积构造符
      • 若T1 和T2是类型表达式,则笛卡尔乘积T1 x T2 是类型表达式
    • 函数构造符→
      • 若T1、T2、…、Tn 和R是类型表达式,则T1xT2 x…xTn→ R是类型表达式
    • 记录构造符record
      • 若有标识符N1 、N2 、…、Nn 与类型表达式T1 、T2 、…、Tn , 则 record((N1 xT1)x(N2xT2)x…x(Nn xTn))是一个类型表达式

声明语句的翻译

局部变量的存储分配

  • 对于声明语句,语义分析的主要任务就是收集标识符的类型等属性信息,并为每一个名字分配一个相对地址
    • 从类型表达式可以知道该类型在运行时刻所需的存储单元数量称为类型的宽度(width)
    • 在编译时刻,可以使用类型的宽度为每一个名字分配一个相对地址
  • 名字的类型和相对地址信息保存在相应的符号表记录中

编译原理

编译原理

简单赋值语句的翻译

赋值语句翻译的任务

编译原理

赋值语句翻译的主要任务:生成对表达式求值的三地址码

编译原理

数组引用的翻译

数组引用的翻译

编译原理

将数组引用翻译成三地址码时要解决的主要问题是确定数组元素的存放地址,也就是数组元素的寻址

数组元素寻址 (Addressing Array Elements )

编译原理

编译原理

带有数组引用的赋值语句的翻译

编译原理

数组引用的SDT

编译原理

编译原理

控制流语句及其SDT

控制流语句的基本文法

编译原理

布尔表达式及其SDT

布尔表达式的基本文法

编译原理

在跳转代码中,逻辑运算符&&、|| 和 ! 被翻译成跳转指令。运算符本身 不出现在代码中,布尔表达式的值是通过代码序列中的位置来表示的

编译原理

SDT的通用实现方法

任何SDT都可以通过下面的方法实现:首先建立一棵语法分析树,然后按照从左到右的深度优先顺序来执行这些动作

回填 (Backpatching)

基本思想

生成一个跳转指令时,暂时不指定该跳转指令的目标标 号。这样的指令都被放入由跳转指令组成的列表中。同一个列表中的所有跳转指令具有相同的目标标号。等到能够确定正确的目标标号时,才去填充这些指令的目标标号。

七、运行存储分配

运行存储分配概述

运行存储分配策略

编译器在工作过程中,必须为源程序中出现的一些数据对象分配运行时的存储空间

  • 对于那些在编译时刻就可以确定大小的数据对象,可以在 编译时刻就为它们分配存储空间,这样的分配策略称为静态存储分配
  • 如果不能在编译时完全确定数据对象的大小,就要 采用动态存储分配的策略。即在编译时仅产生各种必要的信息,而在运行时刻,再动态地分配数据对象的存储空间
    • 栈式存储分配
    • 堆式存储分配

注:静态和动态分别对应编译时刻和运行时刻

运行时内存的划分

编译原理

活动记录

  • 使用过程(或函数、方法)作为用户自定义动作的单元的 语言,其编译器通常以过程为单位分配存储空间
  • 过程体的每次执行称为该过程的一个活动(activation)
  • 过程每执行一次,就为它分配一块连续存储区,用来管 理过程一次执行所需的信息,这块连续存储区称为活动 记录( activation record )
活动记录的一般形式

编译原理

静态存储分配

静态存储分配

在静态存储分配中,编译器为每个过程确定其活动记 录在目标程序中的位置,这样,过程中每个名字的存储位置就确定了。因此,这些名字的存储地址可以被编译到目标代码,过程每次执行时,它的名字都绑定到同样的存储单元。

静态存储分配的限制条件

适合静态存储分配的语言必须满足以下条件

  • 数组上下界必须是常数
  • 不允许过程的递归调用
  • 不允许动态建立数据实体

满足这些条件的语言有BASIC和FORTRAN等

常用的静态存储分配方法

  • 顺序分配法

    • 按照过程出现的先后顺序逐段分配存储空间
    • 各过程的活动记录占用互不相交的存储空间

    编译原理

  • 层次分配法

    • 通过对过程间的调用关系进行分析,凡属无相互调用 关系的并列过程,尽量使其局部数据共享存储空间

    编译原理

    • 层次分配法方向:自底向上

栈式存储分配

栈式存储分配

有些语言使用过程、函数或方法作为用户自定义动作的单 元,几乎所有针对这些语言的编译器都把它们的(至少一 部分的)运行时刻存储以栈的形式进行管理,称为栈式存 储分配

当一个过程被调用时,该过程的活动记录被压入栈;当过程结束时,该活动记录被弹出栈

这种安排不仅允许活跃时段不交叠的多个过程调用之间共 享空间,而且允许以如下方式为一个过程编译代码:它的非局部变量的相对地址总是固定的,和过程调用序列无关

活动树

用来描述程序运行期间控制进入和离开各个活动的情况的树称为活动树

  • 树中的每个结点对应于一个活动。根结点是启动程序执行的main过程的活动
  • 在表示过程p的某个活动的结点上,其子结点对应于被p 的这次活动调用的各个过程的活动。按照这些活动被调用的顺序,自左向右地显示它们,一个子结点必须在其 右兄弟结点的活动开始之前结束
  • 每个活跃的活动都有一个位于控制栈中的活动记录
  • 活动树的根的活动记录位于栈底
  • 程序控制所在的活动的记录位于栈顶
  • 栈中全部活动记录的序列对应于在活动树中到达当前控制所在的活动结点的路径

设计活动记录的一些原则

编译原理

调用序列和返回序列

过程调用和过程返回都需要执行一些代码来管理活动记录 栈,保存或恢复机器状态等

  • 调用序列

    • 实现过程调用的代码段。为一个活动记录在栈中分配空间,并在此记录的字段中填写信息

      编译原理

  • 返回序列

    • 恢复机器状态,使得调用过程能够在调用结束之后继续执行

    编译原理

  • 一个调用代码序列中的代码通常被分割到调用过程(调用者) 和被调用过程(被调用者)中。返回序列也是如此。

    编译原理

变长数据的存储分配

  • 在现代程序设计语言中,在编译时刻不能确定大小的对象 将被分配在堆区。但是,如果它们是过程的局部对象,也 可以将它们分配在运行时刻栈中。尽量将对象放置在栈区 的原因:可以避免对它们的空间进行垃圾回收,也就减少了相应的开销。
  • 只有一个数据对象局部于某个过程,且当此过程结束时它 变得不可访问,才可以使用栈为这个对象分配空间。

非局部数据的访问

非局部数据的访问

一个过程除了可以使用过程自身定义的局部数据以 外,还可以使用过程外定义的非局部数据

语言可以分为两种类型

  • 支持过程嵌套声明的语言
    • 可以在一个过程中声明另一个过程
    • 例: Pascal:一个过程除自身定义的局部数据和全局定义的数据以外,还可以使用外围过程中声明的对象
  • 不支持过程嵌套声明的语言
    • 不可以在一个过程中声明另一个过程
    • 例:C:过程中使用的数据要么是自身定义的局部数据,要么是在所有过程之外定义的全局数据

无过程嵌套声明时的数据访问

变量的存储分配和访问

  • 全局变量被分配在静态区,使用静态确定的地址访问它们
  • 其它变量一定是栈顶活动的局部变量。可以通过 运行时刻栈的top_sp指针访问它们

有过程嵌套声明时的数据访问

嵌套深度

  • 过程的嵌套深度
    • 不内嵌在任何其它过程中的过程,设其嵌套深度为1
    • 如果一个过程p在一个嵌套深度为i的过程中定义,则设定p的嵌套深度为i +1
  • 变量的嵌套深度
    • 将变量声明所在过程的嵌套深度作为该变量的嵌套深度

访问链 (Access Links)

静态作用域规则:只要过程b的声明嵌套在过程a的声明中,过程b就可以访问过程a中声明的对象

  • 可以在相互嵌套的过程的活动记录之间建立一种称 为访问链(Access link)的指针,使得内嵌的过程可以访 问外层过程中声明的对象
    • 如果过程b在源代码中直接嵌套在过程a中(b的嵌套深度 比a的嵌套深度多1),那么b的任何活动中的访问链都指向最近的a的活动

访问链的建立

建立访问链的代码属于调用序列的一部分

假设嵌套深度为nx的过程x调用嵌套深度为ny的过程y (x→y)

  • nx < ny 的情况(外层调用内层)
    • y一定是直接在x中定义的 (例如:s→q, q→p) ,因此,ny=nx +1
    • 在调用代码序列中增加一个步骤:在y的访问链 中放置一个指向x的活动记录的指针
  • n =n的情况(本层调用本层)
    • 递归调用(例如: q→q )
    • 被调用者的活动记录的访问链与调用者的活 动记录的访问链是相同的,可以直接复制
  • nx > ny的情况(内层调用外层,如: p→e )
    • 过程x必定嵌套在某个过程z中,而z中直接定义了过程y
    • 从x的活动记录开始,沿着访问链经过nx - ny + 1 步就可以找到离栈顶最近的z的活动记录。y的访问链必须指向z的这个活动记录

堆式存储分配

堆式存储分配

  • 堆式存储分配是把连续存储区域分成块,当活动记录或其它对象需要时就分配
  • 块的释放可以按任意次序进行,所以经过一段时间后, 对可能包含交错的正在使用和已经释放的区域

符号表

符号表的组织:为每个作用域(程序块)建立一个独立的符号表

根据符号表进行数据访问

编译原理

实际上,这种为每个过程或作用域建立的符号表与编译时的活动记录是对应的。一个过程的非局部名字的信息可以通过扫描外围过程的符号表而得到

标识符的基本处理方法

  • 当在某一层的声明语句中识别出一个标识符(id的定义性出 现)时,以此标识符查相应于本层的符号表
    • 如果查到,则报错并发出诊断信息“id重复声明”
    • 否则,在符号表中加入新登记项,将标识符及有关信息填入
  • 当在可执行语句部分扫视到标识符时( id的应用性出现)
    • 首先在该层符号表中查找该id,如果找不到,则到直接外层 符号表中去查,如此等等,一旦找到,则在表中取出有关信 息并作相应处理
    • 如果查遍所有外层符号表均未找到该id,则报错并发出诊断 信息“id未声明”

符号表的建立

编译原理

红色箭头在建表的时候完成,蓝色箭头在表建好之后完成。