语义分析和代码生成.ppt

上传人:牧羊曲112 文档编号:6060964 上传时间:2023-09-19 格式:PPT 页数:49 大小:342.61KB
返回 下载 相关 举报
语义分析和代码生成.ppt_第1页
第1页 / 共49页
语义分析和代码生成.ppt_第2页
第2页 / 共49页
语义分析和代码生成.ppt_第3页
第3页 / 共49页
语义分析和代码生成.ppt_第4页
第4页 / 共49页
语义分析和代码生成.ppt_第5页
第5页 / 共49页
点击查看更多>>
资源描述

《语义分析和代码生成.ppt》由会员分享,可在线阅读,更多相关《语义分析和代码生成.ppt(49页珍藏版)》请在三一办公上搜索。

1、第9章 语义分析和代码生成,9.1 语义分析的概念程序语言是用上下文无关文法描述的。在语法分析中,我们严格按文法来检查语句的语法是否正确,但有些语句,单看语法结构并没有错误,但和该语句所处的上下文联系考虑就有错误,那么,我们把这种错误就称为语义错误。语义分析就是处理这种语义错误。,考虑下段程序:int a;int b;real a;d=a+b;,第3行语句real a,c;为声明语句,单看这一行没有语法错误,但和上一行语句比较,发现变量a属于重复定义。第4行语句为表达式语句,单看这一行也没有错误,但查询前面的声明语句,发现变量d没有声明。因此,我们可看到语句的正确与否还与上下文密切相关。,第9

2、章 语义分析和代码生成,语义分析主要借助符号表记录的信息来实现语义分析动作。常见的语义分析动作有:1)对表达式中的操作数进行类型的一致性检查。对于强类型语言,要求表达式中的各个操作数、赋值语句左部的变量和右部的表达式的类型应该相同;若不相同则报告语义错误,并要求程序员作显式转换。对于无此项要求的语言,编译程序也要进行类型检查,当发现类型不一致时,则自动作相应的类型转换。2)语义分析的另一个重要功能是要分析由语法分析所识别出来的语句的意义并作相应的语义处理。例如,对声明语句,用户通过这类语句声明程序中要使用的变量,并说明其种类和类型等特性。语义分析程序就要将变量名及其有关属性填入符号表,以备后面

3、使用。对于程序中的可执行语句,则要根据该语句的语义生成相应的中间代码或目标代码。,9.2 中间代码,源程序的中间形式(也称中间代码)是在编译程序将高级语言程序翻译为汇编语言或机器代码的过程中产生的,在编译过程中起着桥梁的作用。如果不使用中间代码,若有M种语言要在N种机器上运行,则需要编制M*N套编译程序;而如果采用中间代码,则只需编制M+N套编译程序。使用中间代码有如下优点:在生成中间代码时,可以不考虑机器的特性,使得编制生成中间代码的编译程序变得较为简单。由于中间代码形式与具体机器无关,所以,生成中间代码的编译程序很方便地被移植到别的机器上,只需为该中间代码开发一个解释器或者将中间代码翻译为

4、目标机指令就能在目标机上运行。在中间代码上更便于做优化处理。使用中间代码的主要缺点是编译的效率比直接产生机器码编译的效率要低一些。中间代码的选择也是一个重要的研究课题。人们期望能找到这样的一种中间语言,它即适合于将各种高级程序设计语言翻译成这种中间语言,又能比较方便地将这种中间语言翻译成各种类型机器的目标语言。目前,中间代码有很多种,下面介绍几种常见的中间代码。,波兰后缀表示,波兰后缀表示是使用较早并且流行至今的一种中间代码形式。对于波兰后缀表达式,处理起来比较容易。在将中缀表达式转换成波兰后缀表示的算法中,设置一个操作符栈,当扫描到操作数时,就立即输出该操作数。当遇到操作符时,则要与栈顶操作

5、符比较其优先级,若栈顶操作符优先级高于栈外操作符,则输出该栈顶操作符;反之,则栈外操作符入栈。而对于赋值表达式,只需定义赋值操作符“=”的优先级低于可在表达式中出现的其他操作符,那么可使用同一算法将其转换成波兰后缀表示。例如,对于算术表达式 F*3.1416*R*(H+R)可转换成波兰后缀表示:F3.1416*R*HR+*对于赋值表达式:S=F*3.1416*R*(H+R)可转换为:SF3.1416*R*HR+*=,波兰后缀表示,波兰后缀表示除可用来表示表达式类的语言结构以外,也能够通过操作符的扩充来表示其他的语言结构。增加相应的操作符就可以表示条件转移、下标变量语法单位的语言结构。例如,条件

6、语句:if then else 可转换成波兰后缀表示:BZBR在该波兰表示中,引入了BZ和BR结构。BZ是二目操作符,如果的计算结果为0(false),则产生到的转移,而是的入口地址。BR则是一个单目操作符,它产生到的转移,而是一个紧跟在后面的语句的入口地址。,9.2.2 N-元表示,N-元表示是一种常见的中间代码形式。N-元表示中,每一条指令由n个域所组成。第一个域说明操作符,剩下的n-1个域则用来表示操作数。可以将N-元表示的标准格式翻译为寄存器机器的代码,因为在该表示中,操作数常常是作为某些前步计算的结果列出的。N-元表示中最常见的有三元式和四元式。1、三元式三元式的每条指令只有3个域。

7、如算术表达式X+Y,可用一个三元式(+,X,Y)来描述。三元式的第一个域是操作符(+),第二和第三个域分别是两个操作数(Y和Z)。三元式的缺点是优化较困难。三元式的一般表示如下:,例如,表达式W*X+(Y+Z)可用如下三元式序列表示:(1)*,W,X(2)+,Y,Z(3)+,(1),(2),9.2.2 N-元表示,对三元式序列中的每一个三元式都编上号码,且对前面三元式的计算结果的引用可用在括号内加上三元式的编号来表示。例如,条件语句if(XY)Z=X;else Z=Y+1;可以用如下三元式序列表示:(1),X,Y(2)BMZ,(1),(5)(3)=,Z,X(4)BR,(7)(5)+,Y,1(6

8、)=,Z,(5)(7)其中,操作符BMZ和BR分别表示零转移和无条件转移。,9.2.2 N-元表示,2、四元式四元式是另一种N-元表示。该表示法的形式如下:,其中,和是运算数;表示操作符操作的结果,该结果通常是一临时变量,在以后可以由编译程序分配给一个寄存器或者一个主存地址。四元式的优点是便于进行优化处理。例如,表达式(A+B)*(C+D)-E能够由下列四元式序列表示:+,A,B,T1+,C,D,T2*,T1,T2,T3-,T3,E,T4上面四元式序列中,T1、T2、T3和T4均是临时变量。,9.2.2 N-元表示,3、抽象机代码编译程序设计的重点是要开发出可移植又适用的编译程序。一种方法是使

9、编译程序产生一种作为源程序中间形式的抽象机的代码。而该抽象机的指令应当尽可能模仿所编译的源语言的结构,且具备下列特点:可移植性:如果花很小的代价,就能将一个程序移植到另一台机器上,那么称该程序是可移植的。移植的开销在本质上要小于当初编制程序时的开销。可适应性:如果一个程序能够容易地进行修改就能满足不同的用户和系统的需求,那么称该程序是可适应的。假定需要将一个给定的编译程序从X机移植到Y机,为了实现这种移植,需要产生Y机的代码,所以,必须重写现有编译程序的代码生成程序。如果原来的编译程序已分成两部分,而且这两部分之间定义有良好接口,那么完成改写的任务将是很容易的。第一部分(前端)分析源语言,第二

10、部分(后端)处理目标机。如果在前后端之间有一个良好的接口,那么移植的主要工作仅仅是改变目标机部分。一种较理想的接口形式是汇编程序。编译程序两部分之间的信息流在一端为源语言结构形式,在另一端为目标机信息。两者的接口能够通过抽象机来实现,即能够将源语言的各种语法结构映射到该抽象机的伪操作上。,9.2.2 N-元表示,对每一种特定的高级程序设计语言都可以为其设计抽象机。构建抽象机模型的基本原则如下:1)与源语言的操作和数据的良好对应:根据源语言中的原始操作和数据模式来建立抽象机模型。编译程序的前端将源程序中的每一个原始操作和原始数据模式翻译成抽象机指令。所以,抽象机的原始操作应该与组成源程序的最简单

11、和最直接的操作语句相对应;另外抽象机的原始数据模式也应该能与源语言中的最简单的数据类型,或是更复杂的数据模式建立起对应关系。2)在目标机上的高效实现:抽象机的伪操作能够迅速转换成目标机的机器指令。3)虚拟体系结构:需要为抽象机体系构建一个运行环境,以便在该环境中模拟语言的数据模式和操作的相互作用。,9.2.3 栈式抽象机及其汇编指令,为了提高可读性、简化代码生成过程,我们的目标平台所采用的机器是一台抽象的栈式计算机,它用一个栈来保存操作数,并有足够的内存空间,该抽象机的常用汇编指令如下:LOAD D 将D中的内容加载到操作数栈。LOADI 常量 将常量压入操作数栈STO D 将操作数栈栈顶单元

12、内容存入D,且栈顶单元内容保持不变。STI D 将操作数栈栈顶单元内容存入D,且栈顶单元出栈。ADD 将次栈顶单元与栈顶单元内容出栈并相加,和置于栈顶。SUB 将次栈顶单元减去栈顶单元内容并出栈,差置于栈顶。MULT 将次栈顶单元与栈顶单元内容出栈并相乘,积置于栈顶。DIV 将次栈顶单元与栈顶单元内容出栈并相除,商置于栈顶。BR lab 无条件转移到labBRF lab 检查栈顶单元逻辑值,若为假(0)则转移到labEQ 将栈顶两单元做等于比较,并将结果真或假(1或0)置于栈顶,9.2.3 栈式抽象机及其汇编指令,NOTEQ 将栈顶两单元做不等于比较,并将结果真或假(1或0)置于栈顶GT 次栈

13、顶大于栈顶操作数,则栈顶置1,否则置0LES 次栈顶小于栈顶操作数,则栈顶置1,否则置0GE 次栈顶大于等于栈顶操作数,则栈顶置1,否则置0LE 次栈顶小于等于栈顶操作数,则栈顶置1,否则置0AND 将栈顶两单元做逻辑与运算,并将结果真或假(1或0)置于栈顶OR 将栈顶两单元做逻辑或运算,并将结果真或假(1或0)置于栈顶NOT 将栈顶的逻辑值取反IN 从标准输入设备(键盘)读入一个整型数据,并入操作数栈OUT 将栈顶单元内容出栈,并输出到标准输出设备上(显示器)STOP 停止执行,9.2 栈式抽象机及其汇编指令,例如,有下段程序:int a,b;a=10;b=20*a;假设记录a、b属性信息符

14、号表为:,则相对应的抽象机汇编程序如下:LOADI 10STO 0LOADI 20LOAD 0MULTSTO 2,这台抽象机称作TEST机。TEST机的指令仅能作为TEST语言的目标。实际上,TEST机具有精减指令集计算机的一些特性。TEST机的模拟程序直接从一个文件中读取汇编代码并执行它,因此避免了由汇编语言翻译为机器代码的过程。此外为了避免与外部的输入/输出例程连接的复杂性,TEST机有内部整型的I/O设备;在模拟时,它们都对标准设备读写。附录E列出了用C语言编写的TEST机模拟程序。,9.3 声明的处理,从代码生成和语义分析的要求出发,处理声明时编译程序的主要任务有两个1)分离出每一个被

15、声明的实体,并将它的名字填入符号表。2)尽可能多地将要保留在符号表中的有关该实体的信息填入符号表。一旦声明了一个实体,编译器就可使用符号表中的信息进行如下的分析和处理:1)检查对所声明实体的引用是否正确。2)利用已声明实体的属性信息,例如类型和已分配的目标地址,为其它执行语句生成正确的目标代码。,9.3 声明的处理,不同的程序设计语言,声明语句的结构也不一样。有的语言类型说明在实体前,有的在实体后。有的语言要求每一个实体都要用一个独立的声明语句进行声明(Ada语言即属于此类),有的语言在一个独立的声明语句中可声明多个类型相同的实体。C语言声明语句的类型说明是在实体前,而且,允许一条声明语句可声

16、明多个类型相同的实体,如“Int a,b,c;”。在自左向右扫描和处理C语言声明语句时,编译器首先知道类型,在扫描到后面的实体后,就可为该实体建立符号表的记录,并可将类型及其它信息填入符号表中。而PASCAL声明语句的类型说明是在实体后且允许一次声明多个实体,如“VAR age,day:integer”,那么,在自左向右扫描和处理这样的声明语句时,编译器分离出实体后,不知道该实体的类型,填符号表时,无法填写类型信息,因此,必须记住这些未填类型的实体在符号表中的位置,以便在扫描到类型之后,将声明语句中有关实体的全部信息回填到符号表中。,9.3.1 符号常量,符号常量在程序的执行期间不发生改变,其

17、优点是:符号常量名声明一次,在程序中就可以多次使用;若要改变符号常量的值只需修改符号常量声明中符号常量的定义值,而不必去修改程序中该符号常量的每一次出现。在大多数有符号常量声明的程序设计语言中,符号常量只能在任何独立的可编译模块中声明一次。符号常量标识符被看作是全局的,因此要存放在符号表的全局部分。例如PASCAL语言的符号常量定义:const SYMBSIZE=1024下面以PASCAL语言的符号常量为示例文法,讲解符号常量的语义处理:const n=c,s插入n,c,sc,sc,s|c,s|c,s常量声明的处理流程如下:1)首先识别常量的名字,将其赋给属性n;2)识别综合常量表达式,将其放

18、在c中,并将表达式的类型赋给属性s。3)调用动作程序插入,其功能是调用符号表管理程序,将名字n、类型s及表达式的值c填入符号表中。,9.3.1 符号常量,注意,许多语言有符号常量,如 PASCAL语言。我们都知道,如果一个标识符声明为常量,在程序中不能对该标识符进行赋值,只能引用。因为符号常量名虽然也保存在符号表中,但符号表中记录了符号常量的名字、类型及符号常量的值,并没有为其分配内存地址,这是符号常量与变量的关键区别。C语言没有符号常量的概念,但C语言提供了宏定义,如#DEFINE PI 3.14159,其功能与符号常量差不多,但概念不一样。C语言的宏定义是在预处理中完成的,在预处理中将C源

19、程序中的所有PI替换成3.14159,因此,C编译系统实际编译的源程序并没有PI这个符号,自然PI也不会出现在符号表中。,9.3.2 简单变量,简单变量是一种保存单个数据实体的数据区,该数据实体在程序中通常声明有指定的类型。遇到简单变量声明时,除了将其名字、类型、维数等信息填入符号表外,还要对变量分配存贮空间。对于实型、整型、逻辑型和固定长度字符串类型的变量,根据所声明的变量类型就可以确定在运行时必须为变量分配的存贮空间的大小。而对于动态数据类型,如可变长度的字符串、特殊类型、类数据类型等存贮空间大小不定的数据类型,则需要作特殊的考虑。考虑下列简单变量声明的属性翻译说明:t,in svarde

20、ft,i,n,i;t,irealt,i|intt,i|chart,I动作符号svardeft,i,n,i:查询符号表,若没有,将类型及变量名存入符号表为该变量名分配存储空间,并将存储空间地址存入符号表中存入符号表;若有,报告错误:变量重复定义。符合上述文法的变量声明的例子如下:real x;int j;char s;,9.3.2 简单变量,约定:文法中所有规则中的符号有3个继承属性vartablep,datap,labelp。但为了规则表示简洁,我们将3个继承属性省略。假设有一块大的数据空间,且该数据区的指针为datap。这个数据空间或者在编译时就分配好(静态分配),或者是在目标程序运行时动态

21、地分配。由于TEST语言只有一种数据类型(整型),且没有数组,所以,我们设计的符号表保存两个属性,分别为名字和分配的地址,省略了类型和维数。属性vartablep指出符号表的最后一个记录的下一个位置,即第一个空白记录位置。为简化起见,我们采用无序符号表,并用一个结构数组做符号表。TEST语言简单变量声明的属性翻译文法如下:-int IDnname-defn,t;动作解释(注意所有符号有继承属性vartablep,datap,labelp):vartablep指出符号表的最后一个记录的下一个位置,即第一个空白记录位置。每当有一个记录加入符号表,该值加1;datap表示已经分配的地址空间,它开始时

22、为0,每声明一个变量,该值则根据变量类型累加,如整型加2,实型加4等等。但TEST语言只有整型,目标机又是抽象机,所以,每声明一个变量,datap加1,即增加一个存储整型的单元。,9.3.2 简单变量,name-defn,t的动作:查询符号表,从vartablep所指的前一个位置起往回查直到第一个记录,若没有,将标识符名n及类型1、datap的值填入符号表vartablep所指的位置,然后vartablep加1;若有,报告错误:变量重复定义。在具体程序实现时,可将代表vartablep,datap,lablep这3个继承属性的变量定义成同名的整型变量,并设为全局变量,初值均为0。用结构数组做符

23、号表,名为vartablemaxvartablep,maxvartablep为符号表的最大容量,如下所示:struct char name8;int address;vartablemaxvartablep;int vartablep=0,datap=0,labelp=0;,9.3.2 简单变量,name-defn,t的程序如下:int name_def(char*name)/查符号表int i,es=0;if(vartablep=maxvartablep)return(es=21);for(i=vartablep-1;i=0;i-)if(strcmp(vartablei.name,name)

24、=0)es=22;/22表示变量重复定义break;if(es0)return(es);strcpy(vartablevartablep.name,name);vartablevartablep.address=datap;datap+;vartablep+;return(es);,在过程name_def中,所要做的工作是将简单变量名类型等信息填入符号表。如果该名字在表中已存在,则返回值22,表示变量重复定义的错误信息。返回值为21则说明符号表已满,这时编译失败,并停止编译,如进一步编译将会导致一连串的语义错误。,9.3.2 简单变量,处理声明语句的程序如下:int declaration_s

25、tat()int es=0;fscanf(fp,%s%sn,9.3.3 数组,数组是一个有公共名字且类型相同的元素所组成的数据实体,在数组名字上附加一个下标就能唯一地引用一个数组元素。数组有两种类型:一种是数组的大小在编译时是已知的,即静态数组。编译程序在处理数组声明时,将建立一个模板,以便在执行期间能够间接引用该数组的元素。对于静态数组,该模板在编译期间建立。另一种数组的大小是在运行时动态地确定,即动态数组。对于动态数组,在编译时仅为模板分配一个空间,而模板本身将在运行时建立。,1、数组模板内容在介绍模板内容前,我们先看一下数组通常是如何存放在存贮器中的。假设有一个二维数组A(1:3,-1:

26、1),表示第一维下界为1上界为3,第二维下界为-1上界为1,那么,该数组有9个数组元素:A(1,-1)A(1,0)A(1,1)A(2,-1)A(2,0)A(2,1)A(3,-1)A(3,0)A(3,1)数组的存贮有按行方式和按列方式。按行方式是指数组元素从第一行开始一行一行地顺序存贮,见图9.1(a)。按列方式是指数组元素从第一列开始一列一列地存贮,见图9.1(b)。多数语言采用按行存贮方式,如C语言、PASCAL语言,而FORTRAN语言采用按列存贮方式。设数组维数为n,L(i)为第i维的下界,U(i)为第i维的上界,那么数组维数n及每一维的上、下界值应存放在模板中,以便用来检测引用的数组元

27、素下标是否越界以及下标数目是否与数组声明的维数相一致。,9.3.3 数组,2、数组元素的地址计算假设数组按行存贮,数组维数为n,L(i)为第i维的下界,U(i)为第i维的上界,LOC为分配给数组的数据空间的开始地址,那么可用如下公式来计算数组元素的绝对地址。假定数组元素的下标为V(1),V(2),V(n)。n绝对地址=LOC+V(i)-L(i)P(i)i=1上式中:1)当i=n时,P(i)=1 n2)当1in时,P(i)=U(j)-L(j)+1 j=i+1 n令RC=-L(i)P(i)i=1 n绝对地址=LOC+RC+V(i)P(i)i=1,(a)按行方式(b)按列方式 图9-1 数组的存储方

28、式,9.3.3 数组,C语言规定数组下界必须为0,实际可用的上界为声明的上界减一,而且引用数组元素时如果越界,并不报错,为什么呢?观查上面的绝对地址计算公式可知,如果下界为0,则RC=0,这样绝对地址的计算公式为:n绝对地址=LOC+RC+V(i)P(i)i=1而上式中:1)当i=n时,P(i)=1 n2)当1in时,P(i)=U(j)+1 j=i+1 U(j)为第j维实际可用的上界,但如果实际可用的上界为声明的上界减一,那么P(i)的计算公式为:1)当i=n时,P(i)=1 n 2)当1in时,P(i)=U(j)j=i+1此时,U(j)就是声明的上界,由于它比实际可用的上界多一,所以计算时就

29、不必再加1,9.3.3 数组,C语言规定数组下界必须为0,实际可用的上界为声明的上界减一简化了绝对地址的计算。从上面的公式可分析出,如果是静态数组,编译时就知道维数和上下界,而RC和P的计算只和上下界有关,与引用元素的下标无关,而引用元素绝对地址的计算要使用RC和各维的P值,那么为了提高运行时绝对地址的计算速度,可在编译时先算出RC及各维的P值,并保存在模板中,见图9.3(a)。如果不做越界检查,那么模板中就没有必要保存各维的上下界了,如果规定下界为0,则RC=0,也不用保存,这样模板的内容可大大简化,由此,可猜测C语言的模板只保存了各维的P值,没有保存各维的上下界,如图9.3(b)所示。,图

30、9.3(a)数组模板 图9.3(b)C数组模板,9.3.3 数组,3、数组属性翻译文法数组在声明时,无论什么格式,都必须指出数组的维数及每维的上下界。下面是某语言的数组声明文法:t niniti ijsymbinsertj,n,tijdimentij|,ijdimentijbounds|:lowerbndupperbnd1)动作init的功能是在分配给数组的模板的数据区中保留两个存贮单元(即为n和RC),并将保存数组维数计数的属性i初始化为0。2)动作dimen简单地继承i并将其综合为i+1,以反映在说明中遇到一个多维数组。3)动作bounds生成一个存贮命令,该命令是取与有关的值,该值一般存

31、放在操作栈的栈顶或一个专用的寄存器中,并将它赋给数组模板数据区中的U(i),同时将缺省的下界1赋给L(i),然后计算P(i)。注意可使用递归关系p(i)=U(i+1)-L(i+1)+1P(i+1)以减少与P(i)相关的计算。4)动作lowerbnd生成将L(i)的存入模板中的代码。5)动作upperbnd生成计算P(i)的代码和将P(i)和U(i)的存入模板中的代码。6)动作symbinsert将数组名n、类型t和维数j填入符号表的合适位置中,并生成调用运行程序的代码,该运行程序为给定的模板计算RC并将计算结果连同数组名n一起存入模板区内。,9.3.4 过程声明,使用过程声明的原因:1是对非嵌

32、套过程定义的语言,如C,因为引用在定义之前,故为了正确生成调用语句的代码,需要知道参数类型,顺序和个数。2是通过使用程序库和模块的分别编译,可以隐藏过程或模块的定义。Ada是支持这一观点的最好的例子,其基本思想是将模块说明的声明部分与模块体分开。声明部分的内容只需要为使用该模块的程序段可见,模块说明的实现细节则被隐藏在程序库中。程序库管理系统可利用各级权限,对模块体无访问权的模块实行限制。要实现这一目标,要求与每一个独立编译模块有关的符号表必须处于程序库管理系统的控制之下。,9.4 表达式语句,TEST语言的表达式语句文法为::=;|;从文法可看出,所谓表达式语句就是在表达式后面加上了分号。表

33、达式在大多数程序设计语言中是用得最多的语法成分。分析表达式的主要任务是生成能正确计算表达式值的目标代码。实现这个目标的基本思路是:1)首先将表达式的操作数装载到操作数栈栈顶或某个寄存器中;2)然后执行表达式所指定的操作。3)将结果保留在操作数栈或寄存器中。我们以栈式抽象机作为目标机,因此,当调用add过程时,需要生成一条无操作数的add指令,假定两个操作数均已存放在操作数栈的栈顶,执行动作后两个操作数均已出栈,结果则入栈。,TEST语言的表达式与其它语言的表达式区别在于表达式中含有赋值运算符,其属性翻译文法如下::=IDnLOOKndASSIGN=STOd|:=|GT|LES|=GE|LE|=

34、EQ|!=NOTEQ:=(+ADD|-SUB):=(*MULT|/DIV):=()|IDnLOOKndLOADd|NUMiLOADIi,9.4 表达式语句,9.4 表达式语句,文法中的动作符号解释如下:LOOKnd:查符号表n,给出变量地址d;没有,变量没定义ASSIGN:超前读一个符号,如果是=,则表示进入赋值表达式,如果不是=,则选择,然后还要将超前读的这个符号退回。STOd:输出指令代码STO dLOADIi:输出指令代码LOADI i LOADd:输出指令代码LOAD d GT、ADD等:输出后的指令代码GT、ADD等在设计程序时要注意,对于规则:=IDnLOOKnd=STOd|ID与

35、的FIRST集合可能相交,可能都是标识符,因此添加了一个动作符号ASSIGN,超前读一个符号,如果是=,则表示进入赋值表达式,如果不是=,则选择,然后还要将超前读的这个符号退回。,表达式的处理程序,多数程序设计语言的表达式都不含有赋值运算符,也没有表达式语句,但有赋值语句。一般赋值语句的文法为::=nLOOKnd=STOd,9.5 if 语句,多数语言的IF语句文法为::=if THEN else 而TEST的IF语句文法为::=if()else IF语句的处理思路是处理所生成的目标代码是计算该表达式的值(真或假),并将结果置于操作数栈栈顶。如果的值是假,则抽象机指令“BRF labA”将控制

36、转到labA;否则控制传给所生成的抽象机指令序列中的下一条指令。BR指令是抽象机的无条件转移指令。TEST的IF语句属性翻译文法为::=if()BRFlabel1 BRlabel2 SETlabellabel1 else SETlabellabel2 BRFlabel1:输出 BRF label1 BRlabel2:输出 BR label2 SETlabellabel1:设置标号label1 SETlabellabel2:设置标号label2,IF语句的处理程序,9.5 if 语句,例如,有TEST程序语句:If(a5)a=1;else a=2;按照IF语句翻译文法,设LABELP=0,则应产

37、生下列目标代码:LOAD 0/表达式a5的代码LOADI 5GTBRF LABEL0/执行动作符号BRFlabel1所产生的LOADI 1/a=1;的代码STO 0BR LABEL1/执行动作符号BRlabel2所产生的LABEL0:/执行动作符SETlabellabel1设置标号label1LOADI 2STO 0LABEL1:/执行动作符SETlabellabel2设置标号label2,9.6 while语句,TEST的WHILE文法为::=while()当表达式成立时,执行语句。其属性翻译文法为::=while SETlabellabel1()BRFlabel2BRlabel1 SETl

38、abellabel2动作解释如下:SETlabellabel1:设置标号label1BRFlabel2:输出 BRF label2BRlabel1:输出 BR label1SETlabellabel2:设置标号label2,while语句的处理程序 例如,有TEST语句:While(a3)a=a+2;,假设目前的标号记数的当前值为:labelp=2,则属性翻译文法应产生代码:Label2:/label1LOAD 0LOADI 3LEBRF label3/label2LOAD 0LOADI 2ADDSTO 0BR label2Label3:,9.7 for循环语句,FOR语句的属性翻译文法如下:

39、:=for(;SETlabellabel1BRFlabel2BRlabel3;SETlabellabel4 BRlabel1)SETlabellabel3 BRlabel4SETlabellabel2 动作解释:1.SETlabellabel1:设置标号label12.BRFlabel2:输出 BRF label23.BRlabel3:输出 BR label34.SETlabellabel4:设置标号label45.BRlabel1:输出 BR label16.SETlabellabel3:设置标号label37.BRlabel4:输出 BR label48.SETlabellabel2:设置

40、标号label2,FOR语句的处理程序,9.7 for循环语句,例如,有TEST语句:For(i=1;i3;i=i+1)a=a+10;假设目前的标号记数的当前值为:labelp=6则属性翻译文法应产生下列代码:,LOAD 2/i的地址是2LOADI 1STO 2Label6:/label1LOAD 2LOADI 3LEBRF label7:/label2BR label8/label3Label9:/label4LOAD 2LOADI 1ADDSTO 2BR label6/label1Label8:/label3LOAD 0/a的地址是0LOADI 10ADDSTO 0BR label9/la

41、bel4Label7:,9.8 write_语句,TEST语言的输出语句极为简单,其属性文法如下::=write OUT;动作解释:OUT:输出 OUT程序如下:,int write_stat()int es=0;fscanf(fp,%s%sn,9.9 read_语句,TEST语言的输入语句与输出语句一样极为简单,其属性文法如下::=read IDn LOOKnd IN STId;,int read_stat()int es=0,address;fscanf(fp,%s%sn,动作解释:LOOKnd:查符号表n,给出变量地址d;没有,变量没定义IN:输出INSTId:输出指令代码 STO d程

42、序如下:,9.10 过程调用和返回,处理过程调用和返回时,主要涉及下列基本问题:1)实现程序中过程调用和返回的控制逻辑。2)处理实在参数和形式参数之间的数据传递问题。在过程调用时,要将实在参数传递给形式参数。在执行过程或从过程返回时,要将过程的处理结果返回给相应的主调过程。在过程调用时不同语言、不同性质的形参将采用不同的数据传递方式,而不同的数据传递方法又将对过程和过程调用的语义分析和代码生成产生影响。,9.10.1 参数的基本传递形式,常见的参数传递有值传递、引用传递(地址传递)、值结果传递和名字传递。,1、值传递值传递又称为值调用,它是最简单的数据传递方式。在这种参数传递机制下,调用程序段

43、要把实在参数的值计算出来并存放在操作数栈或寄存器或被调过程能够取得的数据单元中;而在被调用的子程序或函数中,首先要将实在参数的值送进相应的形式参数的数据单元中。编译程序对过程体中形式参数的处理就象处理一般实在参数标识符那样,即像实在参数那样生成目标代码。这种形式参数与实在参数的结合方式,数据传递是单方向进行的,即调用段将实在参数的值传递到被调用段相应的形式参数的数据单元中,而在执行被调用段的过程中,不可能将赋值形参的数据单元中的内容再传回调用段的相应实在参数单元中去。C语言就是采用这种参数传递机制。,9.10.1 参数的基本传递形式,2、引用传递所谓引用传递就是传地址,又叫引用调用。在这种参数

44、传递机制下,调用程序段要将实在参数的地址传给相应的形式参数。对于实际参数的各种情况,处理方法如下:若实在参数是简单变量,则编译程序要生成将它们的地址保存在操作数栈或寄存器或某个被调过程能够取得的数据单元的代码;若实在参数是数组元素,则除上述代码外,在调用段中还应先有计算该数组元素地址的代码;若实在参数是表达式或常量,则编译程序应先分配一个临时数据单元,在调用程序中先要有计算表达式的值送进临时数据单元中的代码,还要有将该临时数据单元的地址保存在操作数栈或寄存器或某个被调过程能够取得的数据单元的目标代码。而在被调程序段,对形参的处理方法是:首先将实在参数的地址抄进自己相应的形式参数的数据单元中。过

45、程体中对形式参数的引用或赋值都将被处理成对形参数据单元的间接访问。这种处理方式当被调用过程执行完毕返回时,形式参数数据单元所指的实在参数的值会随着被调程序对形参的改变而变化,也就是我们通常说的被调用过程的处理结果被送回调用过程。,9.10.2 过程调用,与过程调用有关的主要语义动作包括:检查该过程是否已定义;与所定义的过程的类型是否相一致;实在参数的数量及类型与过程定义中的形式参数的说明是否相一致。装载实在参数,如果是引用传递,则装载的是实参的地址。装载返回地址。转移到相应过程体。例如,有如下函数调用语句:x=fun(a,b,c);则实现该调用语句要产生的目标代码指令如下:LOAD,a的地址L

46、OAD,b的地址LOAD,c的地址JSR,函数fun的目标指令地址(将下条指令地址压入操作栈,转向被调函数)STI,x的地址,9.10.2 过程调用,在所生成的代码中,假定采用值传递的参数传递机制。前三条指令,将实参入操作数栈,如果实参是表达式,那么显然应该产生表达式的计算代码,但表达式的计算结果应正好位于操作数栈。接下来发送转子指令,这个指令将控制转移到被调用过程体的指令代码的起始地址,并将下一条指令(STO,x的地址)的地址即返回地址压入操作数栈。我们以C语言的过程调用为例,其过程调用属性翻译文法如下::=nLOOKnd,i=0(t,i=i+1CHKTYPEi,t,t,i=i+1CHKTY

47、PEi,t)JSRd动作解释:LOOKnd,i=0:查符号表,取指令地址,实参数目i=0CHKTYPEi,t:查符号表,检查表达式与第i个形参是否匹配JSRd:生成JSR d 指令,转到被调过程。,9.10.3 过程定义的处理,编译过程定义时,有关该过程和它的形参的信息都存放在符号表中。编译程序首先检查要填入的过程名是否合法,确定后填入名字、类型等信息,还要统计形参数目,接下来生成申请新的活动记录、存贮返回地址和形参的值所必须的目标代码。很明显,在对过程处理完后,可能才能确定活动记录的大小,因此,申请空间的代码指令生成时,缺少具体的大小,要在处理完过程的所有声明语句后回填。,在过程调用语句的讨

48、论中,我们仅描述了如何按值传递的参数传递形式。参数传递的另一种常用方法是引用传递,它可以用非常类似于按值传递的方法来实现,不是加载一个变元的值,而是加载传递变元的地址。于是形参保存的是实参的地址,这样被调用过程中的形式参数值的改变将自动地改变主调过程的变元的值。如果实参不是变量时,可为实参设置一个哑单元,该单元放实参的值,且该单元的地址传递给形参。很显然,改变对应哑单元的形参对被调过程的外部将无任何影响。,下面以C语言为例,说明过程定义的主要任务。假设被调函数形式为 int fun(int x,int y).则在过程的开始处,必须生成下列指令:ALLOCATE 4+x STI retaddrS

49、TI y的地址STI x的地址,(x是隐式参数和DISPLAY区大小),过程声明处理:遇到过程名填符号表,将此时的指令记数作为过程的地址。维数一栏标记为过程或函数。过程定义属性翻译文法如下::=(,):=tn INSERTn,allop,i=0,vartable1,ardi,ard,i,ard)INSERTIi,vartable1ALLOCATEallopSTOI ardALLOallop,ardi,ard:=tnINARDn,t,ardINSERTn,allop,i=0:将过程名及函数指令的起始位置插入符号表,为过程的返回值开辟数据空间,形参数,符号栈升级,初始化活动记录ard,用allop

50、记住函数指令的起始位置。INSERTIi,vartable1:回填形参数。ALLOCATE:产生申请活动记录的指令,但大小此时不能填,所以前面用allop记住指令的位置。ALLOallop,ard:回填活动记录大小。STOi:按形参声明的逆序产生存贮形参的指令。,INARDn,t,ard:将形参n填符号表,根据类型t分配空间,并计算活动记录ard的大小。,9.10.3 过程定义的处理,9.10.4 返回语句和过程终止,不管是通过返回语句或是通过过程体的出口来返回主调程序,与过程结束有关的语义动作有下列几种:1)如果过程是函数过程,则发送一条指令将返回值存入操作数栈或运行中为该函数预先分配的结果

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 生活休闲 > 在线阅读


备案号:宁ICP备20000045号-2

经营许可证:宁B2-20210002

宁公网安备 64010402000987号