本文中内容整理西安交通大学软件学院吴晓军老师的ppt中,仅供学习使用,请勿转载或他用
参考教材:《程序设计语言 编译原理》(第3版) 陈火旺等 国防工业出版社
这一章分数在35左右,两个大题
- 数组的引用四元式生成
- 控制语句当中布尔表达式的翻译
- 考试时候一定是嵌套的
- if单双分支和while,三选二进行混合
语义分析和中间代码产生
-
词法分析和语法分析之后的中间代码产生是编译第三阶段的工作。本章介绍几种典型的中间代码形式以及产生他们的算法
-
中间代码的形式很多,如逆波兰记号(后缀表达式)、树形表示(抽象语法树)、三元式、四元式。他们都是介于单词与目标指令之间的“中间产品”
-
困难:目前还不存在一种广泛接受的方式来描述为典型程序语言产生中间代码所需的语义动作。原因是代码生成依赖于对语义的解释,而语义刻画的形式化系统尚未诞生
-
解决办法:为每一个产生式配一个翻译子程序(语义子程序、动作),在语法分析的同时执行它。这样,配上语义动作之后,既指定了串的意义,同时又按这种意义规定了产生某种中间代码应做的基本动作
本章内容
- 语法制导翻译
- 中间语言
- 逆波兰表示法(后缀式)
- 树
- 三地址代码
- 说明语句
- 赋值语句的翻译
- 简单算术表达式及赋值语句的翻译
- 数组元素的引用
- 布尔表达式的翻译
- 控制语句的翻译
- 过程调用的处理
语法制导翻译
基础说明
概念
在语法分析过程中,随着分析的步步进展,根据每个产生式所对应的语义子程序(语义动作)进行翻译(产生中间代码)的办法
标记说明
- 描述语义动作时,需要赋予每个文法符号X(终结符或者非终结符)以种种不同方面的值,如 X . t y p e X.type X.type(类型), X . v a l X.val X.val(值)等
- 一个产生式中同一符号出现多次,用下标来区分
- 例如 E → E + E E\to E + E E→E+E表示为 E → E 1 + E 2 E\to E_1 + E_2 E→E1+E2
- 每个产生式的语义动作,写在该产生式之后的花括号之内(右部的末尾)
- 这种情形下应该选择自下而上的分析方法来完成翻译
语法制导的一个具体实现
先对LR分析器的栈做一些改进,加入语义值
栈底 S 0 S_0 S0是DFA的初态
- state:实际上为一个指示器,指向分析表的某一行
- val:需要保存某些语义信息
- SYM:文法符号,无需进栈,让其进栈只是为了醒目
文法及其语义动作
LR文法(不确定是不是,完了看看前面的确定一下)需要进行文法拓广,满足初态惟一性的要求
上述的语义动作等于给出了计算由 + 、 ∗ +、* +、∗组成的正数算术式的过程。其相应的程序段如下:
LEXVAL是从词法分析器得到的属性
若把语义动作改为中间代码的动作,就能随着分析的进展逐步产生中间代码
中间语言
中间代码的必要性:
大部分的编译器都不直接产生目标代码,虽然这是可以实现的,但是产生的代码不是最优的,因为这涉及到寄存器的分配问题。在语义分析阶段,很难有效地分配他们
逆波兰表达式(后缀式)
概念
一般,若 e 1 , e 2 e_1,e_2 e1,e2为任意的后缀表达式, Θ \Theta Θ为任意双目运算符,则用 Θ \Theta Θ作用于 e 1 , e 2 e_1,e_2 e1,e2所代表的结果用后缀式 e 1 e 2 Θ e_1e_2\Theta e1e2Θ表示
推而广之, Θ \Theta Θ为K目运算符,则 Θ \Theta Θ作用于 e 1 e 2 ⋯ e k e_1e_2\cdots e_k e1e2⋯ek的结果用 e 1 e 2 ⋯ e k Θ e_1e_2\cdots e_k\Theta e1e2⋯ekΘ来表示
问题: Θ \Theta Θ为K目运算符,将其作用于 e 1 e 2 ⋯ e k e_1e_2\cdots e_k e1e2⋯ek的结果用 Θ e 1 e 2 ⋯ e k \Theta e_1e_2\cdots e_k Θe1e2⋯ek来表示。请问,这样的表示叫什么?
- 后缀式
- 波兰表示法
- 逆波兰表示法
- 前缀式
答案:BD
示例
a ∗ ( b + c ) → a b c + ∗ ( a + b ) ∗ ( c + d ) → a b + c d + ∗ a*(b+c)\to a\quad bc+\quad *\\ (a+b)*(c+d) \to ab+ \quad cd+ \quad *\\ a∗(b+c)→abc+∗(a+b)∗(c+d)→ab+cd+∗
若用 ? ? ?表示if-then-else,则 If a then if c-d then a+c else a*c else a+b → a c d − a c + a c ∗ ? a b + ? \text{If a then if c-d then a+c else a*c else a+b} \to a\quad cd- \quad ac+\quad ac * \quad ? \quad ab+ \quad ? If a then if c-d then a+c else a*c else a+b→acd−ac+ac∗?ab+?
后缀式求值
使用一个栈(软件栈或者硬件栈)来求值
求值过程:从左到右扫描后缀式,没碰到操作数就把他推进栈,如果碰到K目运算符就把它作用于栈顶的K个项,并用计算结果来代替这K个项(重新压栈)
控制流的后缀式
前面讲到,if-then-else运算符的实现 e x y ? → e不等于0,取x,否则取y exy?\to \text{e不等于0,取x,否则取y} exy?→e不等于0,取x,否则取y
这种表示法要求在任何情况下都要把x,y都计算出来,尽管只用到其中一个
如果运算量无定义或者有副作用,则后缀表示法不仅无效,而且可能是错误的
解决方法
-
引入标号,在后缀式中加入条件转移,无条件转移算符
-
存储方式:后缀式存放在以为数组 P O S T [ 1.. N ] POST[1..N] POST[1..N]中,每个元素是运算符或者分量(指向符号表)
-
转移算符
p junp -> 转到POST[p] e1 e2 p jlt -> e1<e2时,转到POST[p] e p jez -> 若e=0,转到POST[p]
例子
在数组POST中出现的后缀式
语法制导生成后缀式
产生式所带的语义动作,由以下翻译模式描述
这里的是属性文法
/ 隔开的意思是表示两个不同方面的,一个是属性本身的,一个是打印在屏幕上的
例子
这里还需要继续移入,因为*的优先级比+的优先级高,为啥知道的,又没有多取一个符号看
翻译 a + b ∗ c a+b*c a+b∗c
- 首先移入a,然后根据第三个产生式,打印并且规约a为Ea
- 然后移入符号+
- 移入符号b,然后根据第三个产生式,打印b并且规约b为Eb
- 读入下一个符号*,优先级比+高,继续移入
- 读入符号c,然后根据第三个产生式,打印c并且规约c为Ec
- 这时候进行规约,将 E b ∗ E c Eb *Ec Eb∗Ec规约为E,并且打印 *
- 然后再次进行规约,将 E a + E Ea+E Ea+E规约为E,并且打印+
树
用树形结构来表示一个表达式或者语句
简单变量或者常数的数就是该变量或者常数自身。一般的,叶子表示运算量,内部结点表示OP。例如,已知 e 1 , e 2 e_1,e_2 e1,e2的树为 T 1 , T 2 T_1,T_2 T1,T2,则 e 1 + e 2 , e 1 ∗ e 2 和 − e 1 e_1+e_2,e_1*e_2和-e_1 e1+e2,e1∗e2和−e1的树分别为
例子
语法制导产生树
NODE
:函数过程,建立一个以OP为结点, E 1 . V A L 和 E 2 . V A L E_1.VAL和E_2.VAL E1.VAL和E2.VAL为左右枝的子树,回送新子树根的指针UNARY
:与NODE相仿,但是他只有一个分支LEAF
:函数过程,建立一个以 i . L E X V A L i.LEXVAL i.LEXVAL为标志的结点,并回送此结点的地址,该节点是个端末结点(即叶节点)
三地址代码
三元式
三元式的构成: O P A R G 1 A R G 2 OP\ ARG_1\ ARG_2 OP ARG1 ARG2
- A R G 1 , A R G 2 ARG_1, ARG_2 ARG1,ARG2都是指示器,或者指向符号表的某项,或者是三元式表自身的某项(用的前面计算的结果)
- O P OP OP通常用整数编码,来源:词法分析中算符,一符一种编号
如果要对计算顺序做优化,那么整个表就会发生改变,所有的三元式都会发生改变,所以三元式并不是常用的表示方法
语法制导生成三元式的语义动作
E.val
是综合属性,是一个指示器,指向符号表汇总的某一项,或者三元式表中的某一项ENTRY(id) :是一个函数过程,在符号表中查找id代表的标识符以获知它在表中的位置,如果不在的话就添加一条新记录
TRIP:语义过程,产生新的三元式,回送新三元式在三元式表中的位置
第一个符号表示取反的动作,第二个表示值,第三个是空
两个语义过程
LOOKUP(NAME)
:在符号表中查NAME,如果查到则返回入口值,否则返回NULL。(出错处理,调用FILLSYM)FILLSYM(NAME)
:在符号表中开辟新项目,并返回入口值
问题:三元式中指示器连接,不易更改,不利于优化
间接三元式
重复的计算在三元式表中不重复登记,在间接三元式中会按顺序登记
解决办法:用一张间接码表辅以三元表来表示中间代码
间接码表体现了顺序。在进行代码优化时,如果需要调整顺序,只需要重新安排间接码表,而无需改动三元式表。同时,相同的三元式无需重复填入三元式表。
语义动作
对于间接三元式表示,产生三元式表时,应增添产生间接码表的语义动作。并且,在向三元式表填进一个三元式之前,必须先查看一下此式是否已经在其中,如果已经在其中则补充再添加
四元式
只要遇到新的算术算符或者是逻辑算符就可以引入新的临时变量
表示方法: O P A R G 1 A R G 2 R E S U L T OP \ ARG_1\ ARG_2 \ RESULT OP ARG1 ARG2 RESULT
运算量和运算结果有时指向用户自定义的变量,有时指编译程序引进的临时变量。如果OP是算术或者逻辑算符,则RESULT总是一个新引进的临时变量,用于存放中间结果。注意,可以不加限制地使用临时变量,在优化时再进行压缩
- O P OP OP:算符的整数码
- A R G ARG ARG和 R E S U L T RESULT RESULT:符号表入口或者临时变量的整数码
- R E S U L T RESULT RESULT为 T 1 T_1 T1时的处理:可以填入符号表,通过符号表入口进行引用,也可以不填,用某种整数编码代替它们
在用到前面的式子结果时不通过序号引用,而是通过前一个式子的临时变量来传递值
三元式和四元式的差异在于:表示式中有多少间接表示的问题。
- 三元式对于结果用指示器指向式子表示。
- 四元式则使用临时变量表示结果。相应的,计算和使用的联系不那么直接了,允许重排顺序,从而利于优化
问题:表达式中的间接表示越多,越有利于后期优化。请问,下列哪种中间代码的间接表示最多?
- 间接四元式
- 间接三元式
- 四元式
- 三元式
答案:C
没有间接四元式这个东西
说明语句
假设典型语句的翻译用的都是自下而上的方法
程序的说明语句如:integer L, M, N; array A;
,语义动作是把L、M、N、A
登记到符号表中,并在相应位置填入整型等性质
文法
对于如下文法
D -> integer namelist | real namelist
namelist -> namelist, i | i
存在如下问题:该文法要求把完整的namelist
读完才能做语义动作(在符号表中登记性质)。这样,就必须用栈、队列来保存所有这些名字
文法改造
为了解决上面的问题,对文法进行改造
从文法中发现引入了左递归,但是我们使用自下而上的分析对左递归是不敏感的
D.ATT是综合属性
改进后的文法如下:
S -> D,i | integer i | real i
语义动作如下:
数组说明
a a a:整个数组的首地址
l l l:某一项的起始地址
u u u:某一项的结束地址
d d d:某一项的宽度, d i = u 1 − l 1 + 1 d_i = u_1-l_1+1 di=u1−l1+1
array A[l1:u1, l2:u2, ..., ln:un]
需要做的事情有:
- 填向量
- 申请空间
- 计算下标地址
a:数组的首地址
C:计算其他元素的地址的一些参数,eg:Conspart=a-C,计算数组元素下标的式子,Conspart是数组元素的下标
这里不太懂。。。完了再看看
关于向量填写
侧重点在确定数组,可变数组看看就行
-
A为确定数组
- 每维的上下限 l i , u i l_i,u_i li,ui都是常数,长度 d i d_i di、体积A都可以计算。编译时可以填所有元素
- 由于下标变量地址计算所涉及的 l i , u i l_i,u_i li,ui都是已知量,所以运行时可以不要信息向量,理论上只要保留工作单元和一些常数就可以了
-
A为可变数组
-
l i , u i l_i,u_i li,ui是变量,体积要在运行时计算确定
-
编译时分配向量区,是空架子
-
编译时产生计算 l i , u i l_i,u_i li,ui的指令组,填入向量区中
生成的指令组的功能为:计算上下标 l 1 , u 1 l_1,u_1 l1,u1,申请存储空间
-
根据 l i , u i l_i,u_i li,ui申请内存区的指令
-
可变数组分配子程序
- 输入:维数n,界限序列 l 1 , u 1 , l 2 , u 2 , ⋯ , l n , u n l_1,u_1,l_2,u_2,\cdots,l_n,u_n l1,u1,l2,u2,⋯,ln,un,类型type以及内情向量表区地址
- 功能:建立内情向量被分配数组空间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RqVBLVH2-1678364969151)(http://cdn.Hydrion-qlz.top/blog/202204181920177.png#pic_center)]
问题:对于可变数组,编译程序应该怎样建立内情向量?
- 填向量
- 申请存储空间
- 生成响应指令组
- 计算下标地址
答案:AC
简单算数表表达式及赋值语句的翻译
下面要讨论的是只含整型变量的简单赋值句的翻译,它的文法描述是:
A -> i:=E
E -> E+E | E*E | -E | (E) | i
非终结符A代表“赋值句”。该文法虽然是一个二义性文法,但接受通常对于算符的结合性和优先级的规定,即二义性可以克服
这里的二义性指的是啥?有两颗语法树
几个语义变量和过程
NEWTEMP
:函数过程。每次调用时,他都回送一个代表新临时变量名的整数码作为函数值。ENTRY(i)
:函数过程,查变量i的符号表入口地址,返回变量i的语义值E.PLACE
:和非终结符E相联系的语义变量,它表示存放E值的变量名在符号表的入口或者整数码(若为临时变量)GEN(OP, ARG1, ARG2, RESULT)
:语义过程,把四元式 ( O P , A R G 1 , A R G 2 , R E S U L T ) (OP, ARG_1, ARG_2, RESULT) (OP,ARG1,ARG2,RESULT)填入四元式表
翻译算法的语义动作描述
E.PLACE
可以指向符号表,也可以指向变量表lyn:E.place理解为综合属性
类型转换
前面假定了所有类型 i i i都是整型。实际上,在一个表达式中可能出现各种不同类型的变量和常数。编译程序或者拒绝混合运算,或者产生有关类型转换的指令。
拒绝混合运算就是直接报错
例如:令文法7.1允许混合类型
那么在进行混合运算时,首先要将整形量转化为实型量。而要进行转换,其前提是对每一个 V N V_N VN必须有类型信息语义变量----E.TYPE
因此,对应的产生式要附加关于E.TYPE
的语义规则
语义动作的增加,意味着语义子程序的修改,必要时能够产生对运算量进行类型转换的四元式。
例如(itr, A1, _, T)
表示将整形量
A
1
A_1
A1转换成实型量T
示例
输入串为$ X:=Y+I*J$,其中,X,Y为实型,I,J为整型
关于产生式 E → E 1 o p E 2 E\to E_1\ op\ E_2 E→E1 op E2
在上述语义规则中,非终结符E的语义值**E.TYPE
必须保存在翻译栈中**,如果运算量类型增多,语义子程序必须区别的情形很快增多,从而使语义子程序累赘不堪
数组元素引用的翻译
考试必考
本小节讨论数组元素的表达式和赋值句的翻译
由于数组元素较简单变量有一定的特殊性,分几个方面来介绍
本小节内容
- 地址计算公式
- 四元式中数组元素的表达形式(数组元素引用和中间代码)
- 赋值语句中数组元素的翻译
地址计算公式
假设有数组 A [ l 1 . . u 1 , l 2 . . u 2 ] A[l_1..u_1,l_2..u_2] A[l1..u1,l2..u2],按行存放,则有
-
l 1 − 第一维的下界, u 1 − 第一维的上界 l_1-第一维的下界,u_1-第一维的上界 l1−第一维的下界,u1−第一维的上界
-
l 2 − 第一维的下界, u 2 − 第一维的上界 l_2-第一维的下界,u_2-第一维的上界 l2−第一维的下界,u2−第一维的上界
-
d 1 = u 1 − l 1 + 1 − 第一维的宽度 d_1 = u_1 - l_1 + 1-第一维的宽度 d1=u1−l1+1−第一维的宽度
-
d 2 = u 2 − l 2 + 1 − 第一维的宽度 d_2 = u_2 - l_2 + 1-第一维的宽度 d2=u2−l2+1−第一维的宽度
-
地址计算公式:
A [ i 1 , i 2 ] = A [ l 1 , l 2 ] + ( i 1 − l 1 ) × d 2 + ( i 2 − l 2 ) = A [ i 1 , i 2 ] + i 1 × d 2 − l 1 × d 2 + i 2 − l 2 = i 1 × d 2 + i 2 + A [ l 1 , l 2 ] − ( l 1 × d 2 + l 2 ) \begin{aligned} A[i_1,i_2] &= A[l_1,l_2] + (i_1 -l_1)\times d_2 + (i_2 -l_2)\\ &= A[i_1,i_2]+i_1 \times d_2 -l_1\times d_2 + i_2 -l_2\\ &= i_1\times d_2 + i_2 + A[l_1,l_2]-(l_1\times d_2 + l_2) \end{aligned} A[i1,i2]=A[l1,l2]+(i1−l1)×d2+(i2−l2)=A[i1,i2]+i1×d2−l1×d2+i2−l2=i1×d2+i2+A[l1,l2]−(l1×d2+l2)
简化假定
数组元素按行存放,每维的下限都为都为1,每个元素只占一个机器字,目标机器存储器是以字编址的。
注意
CONSPART
只依赖于数组各维的宽度d和数组的首址a,与数组元素各维的下标 i 1 , i 2 , ⋯ , i n i_1,i_2,\cdots,i_n i1,i2,⋯,in无关。因此,对确定数组而言,计算数组元素的地址时,无需重复计算CONSPART
VARPART
是一个可变部分,它的值随着各维下标 i 1 , i 2 , ⋯ , i n i_1,i_2,\cdots,i_n i1,i2,⋯,in的不同而不同- 计算数组元素的地址主要计算
VARPART
四元式中数组元素的表达形式
数组元素的引用和中间代码
这里只讨论确定数组(编译时可静态确定体积的数组,也称静态数组)的翻译
简单变量可以在符号表中查到它的地址,而数组元素却不行,在符号表中只有它们的总代表----数组名的入口
因此,当下标变量在语句中出现时,如 X : = A [ . . . ] X:=A[...] X:=A[...],在目标指令中必须有计算 A [ . . . ] A[...] A[...]地址的指令
下标变量的表示形式
- 不变部分
CONSPART
:在编译时,可以产生 { T 1 : = a − C } \{T_1:=a-C\} {T1:=a−C},将其存放在临时单元 T 1 T_1 T1中; - 在运行时计算下标变量
A
[
i
1
,
i
2
,
.
.
.
,
i
n
]
A[i_1,i_2,...,i_n]
A[i1,i2,...,in]的可变部分,产生计算
VARPART
的四元式。令 { T : = V A R P A R T } \{T:=VARPART\} {T:=VARPART},则 a d d r ( A [ i 1 , . . . , i n ] ) = T + T 1 addr(A[i_1,...,i_n])=T+T_1 addr(A[i1,...,in])=T+T1
这样,四元式有如下形式(以对数组元素为例): { = [ ] , T + T 1 , , X } \{=[\ ],T+T_1,_,X\} {=[ ],T+T1,,X}
在四元式中出现 T + T 1 T+T_1 T+T1不够理想,不够简洁,可以参照计算机的变址指令,考虑使用 T 1 [ T ] T_1[T] T1[T]
如此,四元式的形式如下:
赋值语句中数组元素的翻译
关键问题是下标表达式的计算
例如对于以下文法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Hz0UqBG-1678364969157)(http://cdn.Hydrion-qlz.top/blog/202204182004271.png#pic_center)]
定义要点:
-
文法允许数组元素嵌套定义,如 A [ B , C 2 + 1 ] A[B,C_{2}+1] A[B,C2+1]
如果数组元素为 A [ B , C [ D , E [ F , G ] ] ] A[B,C[D,E[F,G]]] A[B,C[D,E[F,G]]],那么,在按上面的文法规约下标表达式串时,无法获得数组的内情向量,对每一位的下标都需要保存下来。在该表达式中,就要保存BDF等中间结果,如果规模进一步扩大的话,要保存的中间量就会迅速增加,很是繁琐
所以,要寻求能够及时计算下标的方法
这样就能够在整个下标串
elist
的翻译过程中随时知道数组名 i i i的入口,从而获取登记在符号表中的数组信息 -
为了在规约时完成VARPART的计算,需要修改V的文法
这样就能够在整个下标串elist的翻译过程中随时知道数组名i的入口,从而获取登记在符号表中的数组信息
回顾一下VARPART的计算公式,它是一个乘加式: ( ( ⋯ ( i 1 ∗ d 2 + i 2 ) d 3 + i 3 ) ⋯ + i n − 1 ) d n + i n ((\cdots(i_1 * d_2 + i_2)d_3+i_3)\cdots+i_{n-1})d_n+i_n ((⋯(i1∗d2+i2)d3+i3)⋯+in−1)dn+in
语义变量和过程
elist.ARRAY
:数组名的符号表入口elist.DIM
:数组维数计数器elist.PLACE
:寄存已经形成的VARPART的中间结果名字在符号表中的位置,或者是一个临时变量的整数码limit(ARRAY, k)
:函数过程,数组ARRAY的第k维宽度 d k d_k dk
现在要考虑的变量有两类,每个变量V有两项语义值:
V.PLACE
:- 简单变量:变量名的符号表入口
- 下标变量:保存CONSPART的临时变量的整数码
V.OFFSET
:- 简单变量:NULL(用来区分简单变量和下标变量)
- 下标变量:保存VARPART的临时变量的整数码
语义动作
4为什么引入T?
这步不是必要的,可以直接把
E.place
放在GEN的第一个元中5一定要引入T是为什么?
因为
V.place
在其他地方用到了
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YOjzZkYV-1678364969159)(http://cdn.Hydrion-qlz.top/blog/202204231357207.png#pic_center)]
问题:在前述属性文法第7条产生式的语义动作中,第2条产生式生成语句是否可以使用新临时变量?
- 不可以
- 可以
临时变量只要是做运算就可以引入新的临时变量(无论算术还是逻辑运算),但是也可以复用前面的临时变量
答案:B
变址存数不会引入新的临时变量,变址取数会引入新的临时变量
数组在左侧是变址存数,数组在右侧是变址取数
C A = d 2 + 1 C_A=d_2+1 CA=d2+1
布尔表达式的翻译
基础知识
布尔表达式E是由布尔算符(KaTeX parse error: Undefined control sequence: \and at position 1: \̲a̲n̲d̲,\or,¬)作用于布尔变量或关系表达式而形成的
关系表达式: E 1 r o p E 2 E_1\ rop\ E_2 E1 rop E2
rop是关系算符, E 1 E 2 E_1E_2 E1E2是算数表达式
rop典型的有六种,大于,小于,等于,大于等于,小于等于,不等于
文法
KaTeX parse error: Undefined control sequence: \or at position 8: E\to E\̲o̲r̲ ̲E | E\and E |¬ …
- 布尔算符的优先顺序:KaTeX parse error: Undefined control sequence: \and at position 3: ¬,\̲a̲n̲d̲,\or
- KaTeX parse error: Undefined control sequence: \and at position 1: \̲a̲n̲d̲、\or服从左结合
- 所有关系算符的优先级相同,高于任何布尔算符,低于任何算数算符
关系算符不得结合,如 A > B > C A>B>C A>B>C不合法
布尔表达式E在语言中的用途
- 计算逻辑值:KaTeX parse error: Undefined control sequence: \or at position 7: X := A\̲o̲r̲ ̲B <D
- 条件表达式:KaTeX parse error: Undefined control sequence: \or at position 9: WHILE\ A\̲o̲r̲ ̲B < D\ DO\ S
布尔表达式的求值
- 通常算法:与算术表达式的计算过程一样,一步一步地计算出各部分的值,进而计算出整个表达式的值
- 采用优化策略:
- KaTeX parse error: Undefined control sequence: \or at position 2: A\̲o̲r̲ ̲B,
if A then true else B
- KaTeX parse error: Undefined control sequence: \and at position 2: A\̲a̲n̲d̲ ̲B,
if A then B else false
-
¬
A
¬A
¬A,
if A then false else true
- KaTeX parse error: Undefined control sequence: \or at position 2: A\̲o̲r̲ ̲B,
说明:
上述两种计算方法对于不包含布尔函数调用的式子是没有什么差别的
仅当遇到布尔函数调用并且这种函数调用会引起副作用时,上述两种算法等价
问题:关于布尔算符(3种)、关系算符(6种)、算术算符的优先级,下列哪些说法正确?
- 算术算符的优先级最高
- 布尔算符的优先级高于关系算符
- 关系算符的优先级低于算术算符
- 所有的关系算符的优先级相同
答案:ACD
本节内容
- IF语句的四元式结构
- 翻译的困难和解决办法
- E的文法和语义子程序
- 例题
IF语句的四元式结构
条件语句if E then S1 else S2
,赋予E两种出口:一真一假
红线划着的表示为真的时候转到哪里
( j , _ , _ , p + 1 ) (j,\_,\_,p+1) (j,_,_,p+1)表示无条件跳转语句
翻译的困难和解决办法
困难
所有的转移目标都是在对它的引用之后才出现的。
如果不知道目前转向哪里则对四元式的第四项填写0,即: ( j , _ , _ , 0 ) (j,\_,\_,0) (j,_,_,0)
E(条件表达式)可以很复杂,上面的情况可以频繁出现,因此确定修改的时机很重要
解决办法
现有目标定义,后有目标引用怎么解决
队列法
如果转移的目标不止一个就需要设置多个队列
拉链-回填法
0是链尾的标志
如果需要跳转到同一个目标,则更新链头,并且把当前表达式的第四元设置为上一个链头
一旦找到之后,则从链头往回找,从第四元中拿出上一元,直到第四元为0
有多条链的情况
布尔表达式文法定义及语义动作
文法定义
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JaqLDND6-1678364969164)(http://cdn.Hydrion-qlz.top/blog/202204231428399.png#pic_center)]
语义动作
NXQ
是记录下一个四元式的编号BACKPATCH
回填MERG
合并链M.quad
:表示的后面元素的序号
问题:当转移的目标在对它的引用之后才出现,如何解决这个问题?
- 符号表法
- 队列法
- 数组法
- 拉链-回填法
答案:BD
例题
用自下而上语法分析方法,语法制导翻译生成KaTeX parse error: Undefined control sequence: \and at position 2: A\̲a̲n̲d̲ ̲B \or ¬C的四元式
其中ABC都是一个布尔变量
对于每个布尔表达式来说都有两个出口,但是作为一个整体,使用拉链可以优化成两个出口
这个布尔表达式总共有6个四元表达式
第四元最初都是0,随着不停地
在处理完成之后一定只有两个第四元为0
真值表考试时候不做要求
控制语句的翻译
本节讨论控制语句的翻译,只讨论四元式的产生
本节内容
- 控制流语句
- 标号与goto语句
- CASE语句的翻译
控制流语句
引入
问题
较为复杂的程序控制语句常常是嵌套的
S1后有一条无条件转移指令,跳转到本语句之后。这里,与上一节不同的是,在翻译S2翻译之后,也不能确定其跳转地址,它要跨越S2和S3.
所以,转移地址的确定与语句所处的环境有关
解决办法
令每个非终结符S附带一项语义值S.nextlist
,它指出一条链的头,该链由所有期待翻译完S后填目标的四元式所组成
注意:回填值可能是 { S } \{S\} {S}的下一条四元式,也可能不是。真正的回填,要在处理完S的外层后进行
例子
- 考虑语句
WHILE E1 DO S1
,将其译为代码结构:
绿色表示为假时的跳出位置,由于语句的嵌套,WHILE翻译完了也未必知道假出口的转移目标,所以作为S1.nextlist
保留下来,以便伺机回填
-
对于下面的代码也是一样,在WHILE处理完了之后不知道该跳往何处,也需要将其保存到
nextlist
上存下来
文法
改进后的文法
为了能及时回填有关四元式的转移目标,如同处理布尔表达式一样,需要对文法(7.5)进行改写:
if语句的文法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HqDDQ6uj-1678364969171)(http://cdn.Hydrion-qlz.top/blog/202204302224037.png#pic_center)]
while语句的文法
问题:前述对文法中的if、while、符合语句均进行了改写。请问,这种改写的目的是什么?
- 使文法称为S-属性文法
- 一次扫描构成语法树
- 一次扫描完成所有翻译
- 使文法称为L-属性文法
答案:C
没有定义动作就不能区分L属性文法还是S属性文法
语义动作
回填可以理解为一个过程,但是没有返回值
MERGE(拉链)返回的是这个链的链头
if语句的翻译模式
while-do语句的翻译模式
复合语句的文法
对于布尔表达式来说,无论有多么复杂,一定只有两个出口
例子
双分支的if语句
总共有九个四元式,每一个布尔表达式有两条四元式,计算一个四元式,赋值一个四元式,then的末尾有一个跳转语句
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n7wLHh2l-1678364969175)(http://cdn.Hydrion-qlz.top/blog/202204302237940.png#pic_center)]
while和单分支if
四元式从100开始写起
对于布尔算符来说,四元式的第一项为
jnz
(jump when xxx not zero)或者j(jump)对于关系算符来说,四元式的第一项为
jop
(例如 j < , j > , j = j<,j>,j= j<,j>,j=)共有七个四元式,关系表达式有两句,while语句结束后还有一个跳转语句
处理完控制流语句后,待回填的链有几个?
- 3个
- 2个
- 1个
- 0个
答案:C
布尔表达式翻译完了需要回填的有两条链,控制流语句待回填的链只有一个
标号与goto语句
这里的L是Label,不是List
标号的两种使用方式
L
:
S
G
o
t
o
L
L:S\\ Goto\ L
L:SGoto L
语言中允许标号先定义后使用,也允许先使用后定义
先定义
先使用
遇到goto L2
,填符号表,“未定义”,把NXQ填入L2的地址部分,作为链头。产生(k, _, _, 0)
又遇到goto L2
,查到为定义,取符号表中L2的地址q1填入四元式q2:(j, _, _, q1)
,将q2填入符号表
遇到L2:S2
,就可以回填。(假设S2对应的第一个四元式的标号是q3)
一般而言,带标号语句产生式为:
S
→
l
a
b
e
l
S
l
a
b
e
l
→
i
:
S \to label \ S \\ label \to i:
S→label Slabel→i:
$Label \to i: $的语义动作
- 若 i i i所指的表示符(假定为L)不在符号表中,则把它填入,置类型为“标号”,“定义否”为已,地址为NXQ
- 若L已在符号表中,但“类型”不为“标号”或者“定义否”为“已”,则报告出错
- 若L已在符号表中,则把标志“未”改为“已”,然后,把地址栏中的链头(记为q)取出,同时把NXQ填在其中,最后,执行
BACKPATCH(q,NXQ)
问题:布尔表达式、控制流语句、标号/转移语句等的翻译都会涉及拉链-回填法。请问,下列哪些说法正确?
- 几种语句的链都在四元式表中
- 布尔表达式的链在四元式表中
- 标号/转移语句的链在符号表中
- 控制流语句的链在四元式表中
答案:ABD
转移语句的链头是在符号里面的,其余两个的链头都在属性的定义里
CASE语句的翻译
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Liw2DyZn-1678364969179)(http://cdn.Hydrion-qlz.top/blog/202205041755370.png#pic_center)]
实现方法
翻译为条件转移语句
分叉只有10个左右时,可以翻译为条件转移语句
开关表
- 编译程序构造下面的开关表
- 产生将E值送到该表末项的指令组,以及一个对E值查找开关表的程序
- 运行时,循环程序对E值查开关表,当E与某个 C i C_i Ci匹配就执行 S i S_i Si
S n S_n Sn是缺省分支,所以最后一个的开关为E,如果前面都不匹配就会进入该项中
杂凑表
如果case的分支情况比较多,例如在10以上,最好建立杂凑表。求出 H ( C i ) H(C_i) H(Ci),在杂凑表中填入 C i C_i Ci和 S i S_i Si,类似于哈希表
编译时,对CASE构造该表,有的表项为空。运行时,求 H ( E ) H(E) H(E)值,找对应表项 ( 1 ≤ H ( E ) ≤ M ) (1\le H(E) \le M) (1≤H(E)≤M);如空白,则执行 S n S_n Sn(即缺省分支)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nFBPIxbn-1678364969180)(http://cdn.Hydrion-qlz.top/blog/202205041800204.png#pic_center)]
数组实现
选择子E在基本连续的一个范围(可通过变换)内变化,如0到127,只有少数几个值不作为 C i C_i Ci,则可以建立一个数组 B [ 0 : 127 ] B[0:127] B[0:127],每个元素 B [ C i ] B[C_i] B[Ci]中存放着 S i S_i Si的地址
翻译
下面讨论一种便于语法分析制导实现的翻译法。
问题:当产生末尾的转移语句时, C i C_i Ci和 L i L_i Li的地址 P i P_i Pi无法查找
解决:应在每一个 L i L_i Li出现时,将这两方面的内容存放到队列中
产生代码过程
注意
-
末尾的多项转移目标指令组,视不同情况生成,可优化处理
-
如果 S i S_i Si又是一个 c a s e case case语句,怎么办?
应该建立嵌套队列,要解决队列嵌套、栈嵌套的底标记问题
-
在产生完指令之后,队列可以不要,大那是符号表仍然存在,这样可以灵活地优化
问题:分叉语句的翻译比较灵活,有多重方法可以供选择。请问,下列哪些方法可以?
- 数组
- 开关表
- Hash表
- 条件语句
答案:ABCD
过程调用
过程调用或者说转子,本质上是把控制权转移给子程序
几个问题
- 转移目标
- 返回地址
- 参数传递
一般方案:
- 主程序:实参 → \to →约定单元
- 子程序:约定单元 → \to →形参 → \to →访问形参
关于地址传递
一个简单的方法,由指令携带参数地址,把实参地址统一放在转子指令前
Par:parameter
过程调用的四元式产生
- 困难:如何在处理参数串的过程中记住每个实参的地址,以便最后将他们排列在转子指令的前面
- 解决:第一个实参建立队列,后面的循序记录,要保持队列头
语义动作
例子
写法二义性
对于 X : = A ( I , J ) X := A(I,J) X:=A(I,J)来说,其可能表示过程调用或者数组引用,两者难以区分。而语法制导翻译是按语法规则(产生式)进行的,上下文无法区分,这就造成了翻译的困难。
解决方案:
- 查符号表
- 词法分析器在发送A之前先查表确定其特性
- 规定数组用[],过程调用用(),避免冲突
- 先说明后引用,则使用两边扫描
后记
早期的编译程序中,语法分析和语义翻译往往混杂在一起。把语法分析和语义翻译分开不仅可以是语法分析形式化和自动化,而且可以使整个编译程序的结构更合乎逻辑和更加系统化。语法制导翻译技术就是为了这个目标产生的。该方法在六十年代出现之后很快就得到了推广应用。
考试题目
数组元素引用
2021 A卷
第二题几分就有几条四元式,顺序和个数都需要正确
左边有八个四元式,右边有一个变址取数,和一个变址存数,下标还有一个运算,总共11个
乘加的临时变量复用了
2021 B卷
12条
从第二条开始乘加,然后碰到]有一个减法
A有3个,B有7个(两个乘加,一个加法,一个]的减法,一个变址取数),乘法一个,变址存数一个
这里到时候注意一下顺序
控制流语句
2021 A卷
backpatch和merge的数量和是9
这个题是双分支if+while+单分支if
while的最后有一个,if的最后有一个,逻辑表达式有两个
双分支是两次回填,一个merge,单分支一次回填一个merge,布尔运算也有一个回填一个merge,while语句也有一个回填
merge有返回值,不能忘了