语义分析与代码生成
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
第七章语义分析与代码生成
7.1 语法制导翻译
编译程序的实质性工作是翻译,即为源程序生成目标代码。
为此,我们必须知道程序的含义是什么(语义分析)?应该翻译成什么(代码生成)?
在三、四章,我们主要讨论了源程序的识别,即判定一个源程序是否符合源语言的文法。
在讨论语法分析时曾说过,上下文无关文法不足以描述编程语言的全部语法特征。
为了说明这一点,让我们来看一个例子:
V AR
i:integer;
BEGIN
j:=i*i
END;
如果j没有在外层块中说明,那么赋值语句中出现的j就是非法的。
这是一种上下文敏感的成分。
为了清楚地说明这一点,假定j是在自顶向下分析过程中由非终极符<变量>导出的,在这次推导之前的句型为
αVARj:integer;β<变量>γ
其中α,β,γ为符号串。
推导后的句型为
αVARj:integer;βjγ
为了保证变量在使用前必须说明,需要有如下形式的规则:
V AR j:integer;β<变量>→V AR j:integer;βj
而不是
<变量>→j
即<变量>只有在一定的上下文中才可以展开成j。
上下文敏感成分的分析实质上是语法分析的内容。
但是,因为我们的语法分析是以上下文无关文法为基础的,没有考虑上下文敏感成分的处理,所以必须在语义分析时加以考虑。
这相当于把语言的敏感成分划归为语言的语义范畴(尽管在概念上并非如此)。
比如说在处理说明
V AR j:integer
时,语义分析应该将这个说明所提供的信息填入符号表,即完成对符号表的
插入操作;当然也需要完成其它的语义分析工作,而后在确定是否可用规则<变量>→j
进行推导时,就可通过对符号表的检索操作来完成。
如果符号表中有标识符j,而且j又是个变量标识符,就可以用此规则进行推导。
除了敏感成分的处理之外,为了生成目标代码,所需要完成的一切操作都属于语义分析的范畴。
考虑如下条件语句:
IF E THEN S1 ELSE S2
为它生成的目标代码应具有图7.1的结构(称为它的目标结构),其中,计算E的目标指令、S1和S2的目标指令是在处理表达式和语句时生成的,处理条件语句时所生成的指令就是“jumpf l0”和“jump l1”。
前者的含义为表达式E的值为false时,程序转向l0的指令继续执行,后者为无条件转到l1的指令执行。
问题在于编译程序在处理条件语句时是从左向右进行的。
因此,当要生成jumpf指令时,不知道l0的值,因为S1的目标这时尚未生成,不知道它究竟有多少条指令。
在生成jump这条指令时也有同样问题。
为了解决这个问题,在生成jumpf和jump指令时,应先记录这两条指令本身的位置,等以后再回填它们的转向目标。
假设当前要生成的指令位置为l c,条件语句的处理算法如下:
IF sy=ifsyTHEN
insymbol;
expression;{处理表达式}
IF表达式类型< >boolsTHEN
error (n)
ENDIF;
lc1:=lc;
生成jumpf指令;lc:=lc+1;
IF sy=thensy THEN
insymbol;
statement;{处理语句}
IF sy=elsesy THEN
lc2:=lc;
生成jump指令;
lc:=lc+1;
回填jumpf指令的转向目标;
insymbol; 图7.1
statement;{处理语句}
回填jump指令的转向目标;
ELSE
回填jumpf指令的转向目标
ENDIF
ENDIF
ENDIF;
可以看出,除了检查表达式类型外(敏感成分的处理),语义分析工作还包括转向目标的回填等操作。
与第四章给出的条件语句的语法分析算法相比,上述算法只是增加了如下几个操作:
<op1>:IF表达式类型< >bools THEN
error(n);
ENDIF;
lc1:=lc;
生成jumpf指令;
lc:=lc+1;
<op2>:lc2:=lc;
生成jump指令;
lc:=lc+1;
回填jumpf指令的转向目标;
<op3>:回填jump指令的转向目标;
<op4>:回填jumpf指令的转向目标;
这相当于说上面的处理算法是根据如下文法规则写成的:
<IF语句>→IF<表达式><op1>THEN<语句>ELSE<op2><语句><op3>
<IF语句>→IF<表达式><op1>THEN<语句><op4>
即在文法规则中嵌入了相应的语义加工操作。
于是,语义分析及代码生成可以随着语法分析的进行,通过嵌入相应的语义加工操作来完成。
这种方法称为语法制导翻译,因为语言的文法规则确定了相应的语义分析类型及生成代码的性质,而且分析算法的主体控制是相应的文法规则。
本章后面将结合实例讨论各种典型的语言结构的语义分析及代码生成。
7.2 目标机
为了完成代码生成工作,必须有一个提供运行环境的目标机。
最直接的方法是,在哪个机器上运行的编译程序就生成那个机器的目标代码,或生成那个机器的汇编语言程序,然后经过汇编程序汇编成可以执行的机器语言程序。
汇编后产生的目标代码可以具有绝对地址,从而可以装到内存的固定区域去执行;也可以具有浮动的(相应的)地址,再由装入程序(或者是连接装配程序)来为地址代真(即转换成绝对地址),即可用于执行。
无论是哪
一种情况,都需要知道机器的硬件,诸如有多少个累加器、特殊寄存器、地址空间的大小等。
但事实上,代码生成也可先脱离特定的硬件环境。
一种逐渐流行的方法是为一个抽象机生成代码,然后,在特定的机器上写一个解释程序来解释抽象机的指令。
下面我们将介绍一个抽象机,它是专为PASCAL-S设计的,与任何特定的计算机无关,不妨称为PASCAL-S处理机。
尽管PASCAL-S处理机在硬件上并不存在,但它的指令不难落实到任何特定的计算机上。
PASCAL-S处理机上有如下一些寄存器和一个存贮区:
ps:程序状态字
ir:指令寄存器
pc:指令计数器
t:栈顶寄存器
b:基地址寄存器
display:地址寄存器组
存贮区分为程序存贮区、表格区和栈区,如图7.2所示。
程序存贮区CODE用于存放目标代码,这部分存贮区在目标代码的执行期间保持不变,可看作只读存贮(ROM)。
表格区用来存放程序的静态信息。
栈区用作程序执行的数据空间。
栈区由一系列数据段组成,每个数据段包括如下内容:
(1)标记部分。
(2)参数部分(可能为空)。
(3)局部量。
(4)处理语句时所需要的临时工作空间。
对应PASCAL-S中过程或函数的数据区,
标记部分用来存放
(1)函数的返回结果。
(2)过程或函数的返回地址。
(3)静态链。
(4)动态链。
(5)过程或函数标识符在tab中的位置。
其中静态链与动态链指向栈区S的其它单
元,返回地址指向代码区CODE中的单元,第
图7.2
(5)项则指向表格区中的单元。
PASCAL-S处理机是一个栈式的机器,它没有传统的累加器,所有对数据的操作均在栈顶进行。
例如,加法指令是把栈顶两个单元的内容相加,并把结果留在栈顶;条件转向指令根据栈顶单元的内容决定是否转向,等等。
下面我们来介绍PASCAL-S机的指令系统。
因为这个指令系统是根据源语言PASCAL-S的特点而设计的,所以为了深刻理解各指令的意义,需
要与后面将讨论的目标结构结合起来学习。
1.双操作指令(4条)
LODA:
将x(层号)和y
LODV:
根据x(层号)和y(位移量)确定单元地址,把单元的内容装到栈顶。
LOD*:
根据x(层号)和y(位移量)确定存放地址的单元,把那个地址所指单元的内容装到栈顶。
UPD:
x,y均为层号,根据静态链更新display从第x+1层到第y层的内容。
2.单操作指令(23条)
STAD:
调用y
栈顶。
表7.1中给出了编号y与标准函数的对应关系。
表7.1
ADDL:
JUMP:
转到y所指的指令继续执行。
JUMPF:
JUMPX:
y
ENTRY:
ENTRY总是成对出现。
第一条中的y为情况标号的值,第二条中的y为相
ENTRY组成。
FORIUP:
FOR1UP
FOR i:=E1 TO E2 DOS S
当执行到FOR1UP指令时,运行时栈的状态如图7.3所示。
图7.3
指令FOR1UP在初值小于终值时,为循环变量i赋初值,否则退掉栈顶三个单元并转到y
FOR2UP:
FOR2UP与FOR1UP配对使用。
FOR2UP用于循环的重复条件测试。
如果循环变量的值小于终值,则循环变量的值加上1,并转循环入口y;否则退掉栈顶三个单元。
FOR1DOWN:
FOR2DOWN:
FOR i:=E1
MARK:
这条指令为过程语句或函数调用的第一条指令,y为过程或函数标识符在符号表的位置。
该指令在栈顶分配5个单元作为标记部分,并将y填入所分配的第5个单元中。
CALL:
—1;由于栈顶指针t指向参数区最后一个单元,所以t-y恰为本层数据区的开始位置。
在MARK与CALL之间的指令用于为形参分配存贮。
CALL指令用于填写本层display内容(t-y);填写标记部分(静态链,动态链,返回地址);为局部量分配存贮,根据标识符表中的入口地址转过程体。
INDEX1:
INDEX:
这两条指令是为计算数组元素的地址而设计的,y是指向数组表的指针。
它的功能是将(栈顶单元内容—数组下界)*L加到次栈顶单元的内容上并退掉栈顶单元。
在。
LODB:
将一串相连单元的内容装到栈顶,这串单元的个数由y指定,第一个单元的
COPYB:
址为开始的y
LODL:
把y装到栈顶。
LODR:
y
FLOAT:
READ:
将y并退掉栈顶单元。
WRITE0:
y为指向串表stab(输出字
WRITE1:
WRITE2:
WRITE1输出栈顶单元的内容并退掉栈顶单元(输出域宽为缺省值)。
WRITE2按栈顶单元所示域宽输出次栈顶的内容并退掉栈顶两个单元。
3.无操作数指令(33条)
HALT:
EXITP:
MARK前的值;
EXITF:
MARK前的值加1(栈顶为函数结果);根据动态链恢复基地址寄存器的内容;按返回地址返回。
FETCH:
以栈顶单元的内容作地址,取这个地址所指单元的内容回送到栈顶。
NOT:
NEGATE:
WRITER:
这条指令的意义是用如下语句打印实数x:
write (x:y:z);
其中z在栈顶单元中,y在次栈顶单元中,而x在栈顶第三个单元中。
STORE:
READLN:
WRITELN:
语句。
以下指令均是在栈顶两个单元间进行的数据操作,功能是
<次栈顶> <次栈顶><运算><栈顶>
并退掉栈顶单元。
表7.2给出了各种运算所对应的操作码及助记符。
我们之所以这样设计目标机,一方面是因为可以避免考虑特定计算机的具体细节,另一方面是因为可以简化编译程序的代码生成。
然而,由于这个目标机在实际上并不存在,因此,为了使目标程序可以真正执行,必须设法实现这个目标机。
我们的办法是用PASCAL语言写一个解释程序来执行这个目标机的指令。
目标机的各寄存器及存贮区可以用PASCAL的各种变量来模拟,比如:CONST
maxlevel =7;
tabsize =100;
atabsize =30;
btabsize =20;
rconstsize =20;
stabsize =600;
codesize =800;
stacksize =1450;
TYPE
object=(konstant,vvariable,typel,prozedure,funktion);
types=(notyp,ints,reals,bools,chars,arrays,records);
order=PACKEDRECORD
f:0..63;
x:0..maxlevel;
y:integer
END;
V AR
ps : (run,finish,error); {程序状态字}
ir : order; {指令寄存器}
pc : integer; {指令计数器}
t : integer; {栈顶寄存器}
b : integer; {基地址寄存器}
display : ARRAY [0..maxlevel] OF integer;{地址寄存器组} code : ARRAY [0..codesize] OF order;{代码区}
tab : ARRAY [0..tabsize] OF
PACKED RECORD
name :PACKED ARRAY [1..10] OF char;
link :0..tabsize;
obj :object;
typ :types;
ref :integer;
normal :boolean;
lev :0..maxlevel;
adr :integer
END;
atab: ARRAY [1..atabsize] OF PACKED RECORD
inxtyp :types;
eltyp :types;
elref :integer;
low :integer;
hig :integer;
elsize :integer;
size :integer
END;
btab:ARRAY [1..btabsize] OF PACKED RECORD
last,lastpar:0..tabsize;
psize,vsize:integer
END;
rconst :ARRAY [1..rconstsize] OF real;
stab :PACKED ARRAY [0..stabsize] OF char;
s :ARRAY [1..stacksize] OF {栈区}
RECORD
CASE cn:types OF
ints : (I:integer);
reals : (r:real);
bools : (b:boolean);
chars : (c:char)
END;
目标程序的执行则可以用如下语句模拟:
各寄存器及栈区初始化;
REPEAT
ir :=code [pc];
pc :=pc+1;
CASE ir.f OF
LODA :t:=t+1;
s[t].i:=display [ir.x]+ir.y;
LODV :t:=t+1;
s[t].i:=s[display[ir.x]+ir.y];
LOD* :t:=t+1;
s[t]:=s[s[display[ir.x]+ir.y].i];
UPD :h1:=ir.y;
h2:=ir.x;
h3:=b;
REPEAT
display[h1]:=h3;
h1:=h1-1;
h3:=s[h3+2].i
UNTIL h1=h2;
STAD :CASE ir.y OF
0:s[t].i:=abs(s[t].i);
1:s[t].r:=abs(s[t].r);
16:s[t].r:=atan(s[t].r);
17:s[t].b:=eof(prd);
18:s[t].b:=eoln(prd)
ENDCASE;
ADDL :s[t].i:=s[t].i+ir.y;
JUMP :pc:=ir.y;
JUMPF :IF NOT s[t].b THEN
pc:=ir.y
ENDIF;
t:=t-1;
JUMPX :h1:=s[t].i;
t:=t-1;
h2:=ir.y;
h3:=0;
REPEAT
IF code[h2].f<>ENTRY THEN
h3:=1;
ps:=caschk{情况语句出错}
ELSE
IF code[h2].y=h1 THEN
h3:=1;
pc:=code[h2+1].y
ELSE
h2:=h2+2
ENDIF
ENDIF
UNTIL h3<>0;
FOR1UP :h1:=S[t-1].i;
IF h1<=s[t].i THEN
s[s[t-2].i].i:=h1
ELSE
t:=t-3;
pc:=ir.y
ENDIF
FOR2UP :h2:=s[t-2].i;
h1:=s[h2].i+1;
IF h1<=s[t].i THEN
s[h2].i:=h1;
pc:=ir.y
ELSE
t:=t-3
ENDIF;
FOR1DOWN :h1:=s[t-1].i;
IF h1>=s[t].i THEN
s[s]t-2[.i].i:=h1
ELSE
pc:=ir.y;
t:=t-3
ENDIF;
FOR2DOWN :h2:=s[t-2].i;
h1:=s[h2].i-1
IF h1>=s[t].i THEN
s[h2].i:=h1;
pc:=ir.y
ELSE
t:=t-3
ENDIF;
MARK :h1:=btab[tab[ir.y].re].vsize;
t:=t+5;
s[t-1].i:=h1-1;
s[t].i:=ir.y;
CALL :h1:=t-ir.y;
h2:=s[h1+4].i
h3:=tab[h2].lev;
display[h3+1]:=h1;
h4:=s[h1+3].i+h1
s[h1+1].i:=pc;
s[h1+2].i:=display[h3];
s[h1+3].i:=b;
FOR h3:=t+1 TO h4 DO
s[h3].i:=0
ENDFOR;
b:=h1;
t:=h4;
pc:=tab[h2].adr;
INDEX1 :h1:=ir.y;
h2:=atab[h1].low;
h3:=s[t].i;
IF h3<h2 THEN
ps:=inxchk {下标越界错}
ELSE
IF h3>atab[h1].high THEN
ps:=inxchk
ELSE
t:=t-1;
s[t].i:=s[t].i+(h3-h2)
ENDIF
ENDIF;
INDEX :h1:=ir.y;
h2:=atab[h1].low;
h3:=s[t].i;
IF h3<h2 THEN
ps:=inxchk
ELSE
IF h3>atab[h1].high THEN
ps:=inxchk
ELSE
t:=t-1;
s[t].i:=s[t].i+(h3-h2)*atab[h1].elsize
ENDIF
ENDIF;
LODB :h1:=s[t].i;
t:=t-1;
h2:=ir.y+t;
WHILE t<h2 DO
BEGIN
t:=t+1;
s[t]:=s[h1]
h1:=h1+1
END;
COPYB :h1:=s[t-1].i;
h2:=s[t].i;
h3:=h1+ir.y;
WHILE h1<h3 DO
BEGIN
s[h1]:=s[h2];
h1:=h1+1;
h2:=h2+1
END;
t:=t-2;
LODL :t:=t+1;
s[t].i:=ir.y;
LODR :t:=t+1;
s[t].r:=rconst[ir.y]; FLOAT :h1:=t-ir.y;
s[h1].r:=s[h1].i;
READ :IF eof(prd) THEN
ps:=redchk {读错}
ELSE
CASE ir.y OF
ints:read(prd,s[s[t].i].i);
reals:read(prd,s[s[t].i].r);
chars:read(prd,s[s[t].i].c);
ENDCASE;
t:=t-1;
WRITE0 :h1:=s[t].i;
h2:=ir.y;
t:=t-1;
REPEAT
write(prr,stab[h2]);
h1:=h1-1;
h2:=h2+1
UNTIL h1=0;
WRITE1 :CASE ir.y OF
ints:write(prr,s[t].i:fld[1]);
reals:write(prr,s[t].r:fld[2]);
bools:IF s[t].b THEN
write(″true″)
ELSE
write(″false″)
ENDIF;
chars:write(prr,chr(s[t].i))
ENDCASE;
t:=t-1;
WRITE2 :CASE ir.y OF
ints:write(prr,s[t-1].i:s[t].i);
reals:write(prr.s[1].r:s[t].i);
bools:IF s[t] THEN
write(″true″)
ELSE
write(″false″)
ENDIF;
chars:write(prr,chr(s[t-1].i:s[t].i))
ENDCASE;
t:=t-2;
HALT :ps:=finish; {正常停机} EXITP :t:=b-1;
pc:=s[b+1].i;
b:=s[b+3].i;
EXITF :t:=b;
pc:=s[b+1].i;
b:=s[b+3].i;
FETCH :s[t]:=s[s[t].i];
NOT :s[t].b:=NOTs[t].b;
NEGATE :s[t].i:=-s[t].i;
WRITER :write(prr,s[t-2].r:s[t-1].i);
t:=t-3;
STORE :s[s[t-1].i]:=s[t];
t:=t-2;
EQR :t:=t-1;
s[t].b:=s[t].r=s[t+1].r;
NEQR :t:=t-1;
s[t].b:=s[t].r< >s[t+1].r;
DIVR :t=t-1;
s[t].r:=s[t].r/s[t+1].r;
READLN :readln;
WRITELN :writeln;
ENDCASE;
UNTIL ps< >run;
IF ps< >finish THEN
运行时错误处理;
这就是解释执行的过程。
这个解释程序的结构非常简单,几乎就是一个情况语句。
事实上,高级语言的解释程序结构基本都是这样。
7.3 说明部分的处理
说明部分给语言带来了一定的冗余度。
有些语言(如LISP)没有说明语句;有些语言虽然有说明语句(如BASIC,FORTRAN),但很少需要用;而其它语言(如PASCAL,ADA),则要求程序中所使用的每个实体都必须说明,说明的作用是把程序中的实体与某个标识符联系起来,用标识符作为这个实体的名字。
通过缺省的命名规则(如FORTRAN中以I,J,K,L,M,N开头的名字若不特别说明则是用来标识整型变量的)或根据名字所出现的上下文(如在SNOBOL中,TEXT=‘STRINGA’意味着TEXT是一个字符串的名字),说明的使用是可以避免的。
然而,人们并不总是希望避免使用说明,特别是当软件开发的主要目标是生产可靠的软件产品时更是这样。
因为说明的作用是明确地声明程序员打算怎样使用所说明的实体,而编译程序根据某个实体的上下文可以推断它的使用情况。
如果后者与前者不一致,编译程序就可以检查出来并通知给程序员,而如果不使用说明,这类错误就无法检查出来。
因此,需要说明部分的语言可以编出较为可靠的程序。
从语义分析及代码生成的角度看,编译程序在处理说明部分时的主要任务是:
(1)分离出说明语句中所说明的每一个实体,并把它填到符号表中。
(2)在符号表中填入尽可能多的有关实体的属性。
从而,编译程序可以根据符号表中的信息来检查将来对某个实体的引用是否正确,并根据有关属性为源程序生成正确的目标代码。
下面我们将结合PASCAL-S的说明部分加以讨论。
7.3.1 常量说明
常量说明的作用在于提高程序的可读性与修改方便。
例如在上一节,我们利用常量说明定义了栈区S的大小:
stacksize=1450;
由此可以很容易看出,
IF t>stacksize THEN…
是在测试栈是否溢出。
如果需要修改栈区大小,只需修改常量说明,程序中所有用到stacksize的地方(如测试栈溢出的地方)就都同时得到了修改,而无需逐一进行修改。
PASCAL-S中常量说明的定义为
<常量说明>→CONST<标识符><op1>=<常量><op2>
{;<标识符><op1>=<常量><op2>}
请注意,文法中已嵌入了语义加工操作。
处理完标识符后,可以知道常量的名字;处理完常量后可以知道常量的类型(整型、实型、字符型等)及其值。
设id定义为
id:PACKED ARRAY[1..10] OF char;
用来存放所拼出的标识符,c定义为
c:RECORD
CASE tp:types OF
ints,chars,bools: (i:integer);
reals: (r:real)
END;
用来存放常量的类型及其值(字符型及布尔型常量的值是用其序号代表的),则嵌入的语义操作为:
<op1>: enter(id,konstant);
<op2>: tab[t].typ:=c.tp;
IF c.tp=reals THEN
enterreal (c.r);
tab[t].adr:=c1
ELSE
tab[t].adr:=c.i
ENDIF;
这两个操作对符号表完成了一次完整的插入(见5.2节)。
相应的,常量的定义为
<常量>→<字符常量><op1>|
[+|-]<op2><无符号数><op3>|
[+|-]<op2><常量标识符><op4>
嵌入的语义操作如下:
<op1>: c.tp:=chars;
c.i:=inum;{字符值的序号}
<op2>: sign:=1;
IF sy=minus THEN
sign:=-1
ENDIF;
<op3>: IF sy=intcon THEN
c.tp:=ints;
c.i:=sign*inum
ELSE
IF sy=realcon THEN
c.tp:=reals;
c.r:=sign*rnum
ENDIF
ENDIF;
<op4>: x:=loc<id>;{检索标识符表}
IF (x< >0) AND (tab[x].obj=konstant) THEN
c.tp:=tab[x].typ;
IF c.tp=reals THEN
c.r:=sign*rconst[tab[x].adr]
ELSE
IF c.tp=ints THEN
c.i:=sign*tab[x].adr
ENDIF
ENDIF
ENDIF;
根据这两个嵌入语义操作的文法规则,不难写出常量说明的处理算法。
7.3.2 类型说明
为数据规定类型可以更自然地描述数据,因而适于对实际问题的抽象。
类型实质上代表着一组值以及可在这组值上施加的一组操作。
例如,integer 表示整数类型,它与集合
{…,-2,-1,0,1,2,…}
{+,-,*,div,:=,<,…}
有关,即说明为integer的变量i可以取第一个集合中的值,可以参与第二个集合中的运算。
当然,类型可以在定义数据对象时引入,而不是非有名字不可。
但利用类型说明为类型赋以名字更能反映类型概念本身,而且用有名类型来定义数据更能反映对数据的抽象。
比较下面两个说明:
1.V AR
r1,r2,r3:RECORD
name:PACKED ARRAY[1..20]OF char;
age:integer;
sex: (male,female)
END;
2.TYPE
personal message
=RECORD
name:PACKED ARRAY[1..20]OF char;
age:integer;
sex: (male,female)
END;
V AR
r1,r2,r3:personal message;
第一个说明定义了r1,r2,r3为一个记录,它们将来可存放这种记录的值。
第二个说明先定义了personal message(个人信息)为一个记录类型,然后定义r1,r2,r3为个人信息,它们将来可用于存放有关个人的信息。
显然第二个说明更自然些。
PASCAL-S中,类型说明的定义为
<类型说明>→TYPE<标识符><op1>=<类型><op2>
{;<标识符><op1>=<类型><op2>}
处理完标识符后,需要将这个标识符(类型的名字)填到标识符表中,其它一些属性如typ,ref及adr等,则需在处理完类型后回填。
但处理类型的时候,可能会对标识符表完成其它的插入操作,如对记录类型就需要将各域标识符插入标识符表中。
因此,为了在标识符表中回填属性,需要记下类型名字在标识符表中的位置。
于是,嵌入的语义操作为
<op1> :enter(id,type1);
t1:=t;
<op2> :tab[t1].typ:=tp;
tab[t1].ref:=rf;
tab[t1].adr:=sz;
其中tp,rf,sz由处理类型的过程返回。
关于类型的处理,我们将在讨论变量说明时一起介绍。
7.3.3 变量说明
变量说明是显式地为程序中所使用的变量命名并规定属性。
由于变量在程序运行时拥有值,编译程序必须为变量分配存贮空间,并赋予变量一个地址,且当存贮分配是动态进行时,这个地址只能是相对地址。
为变量分配存贮空间的大小取决于变量的数据类型。
在PASCAL-S中,由于允许过程的递归调用,所以存贮分配是动态进行的,编译时所做的工作是为变量分配位移量。
变量说明的语法规则如下:
<变量说明>→V AR<标识符><op1>{,<标识符><op2>}:<op3><类型
><op4>
{;<标识符><op1>{,<标识符><op2>}:<op3><类型
><op4>}
当处理完逗号分隔的标识符时,尚不知其类型,所以无法填类型及地址属性,必须等处理完类型后回填。
因此,需要记录这些标识符在标识符表tab中的位置(注意,这些标识符在标识符表tab中是连续存放的)。
如果dx作为位移量分配的计数器(它在处理一个块的开始时被初始化),则各语义操作如下:
<op1>: t0:=t;
enter(id,vvariable);
<op2>: enter(id,vvariable);
<op3>: t1:=t;
<op4>: WHILE t0<t1 DO
BEGIN
t0:=t0+1;
WITH tab[t0]DO
typ:=tp;
ref:=rf;
normal:=true;
adr:=dx;
dx:=dx+sz
ENDWITH
END;
其中tp、rf、sz由处理类型的过程返回。
现在我们来讨论类型的处理。
首先考虑最简单的情况
<类型>→<类型标识符><op1>
此时,类型的处理算法很简单,只需执行对符号表的检索操作并取出相应的属性即可。
<op1>: x:=loc(id);
IF(x< >0)AND(tab[x].obj=typel)THEN
tp:=tab[x].typ;
rf:=tab[x].ref;
sz:=tab[x].adr;
ENDIF;
其中各属性是在处理类型说明时填入的(或预定义的)。
当引入新的数组或记录类型时,相应的算法要复杂些,它必须将这种新类型的信息填到数组表
或块表中,而且,在处理当前的新类型时,又必须递归地调用它本身去处理数组的基类型或记录域的类型。
下面我们先讨论记录类型的处理。
<类型>→RECORD<op1>{<标识符><op2>{,<标识符><op3>}:
<op4><类型>;<op5>END<op6>
处理记录时,需要将记录的信息填到块表中,而记录的各个域则需填进标识符表tab,有些属性需要回填。
由于各个域标识符是局部于它所在的记录的,所以在处理各个域之前需要执行符号表的进层操作,而各个域处理完后要执行符号表的复位操作。
算法如下:
<op1>: enterblock;
level:=level+1;
display[level]:=b;
offset:=0;
tp:=records;
rf:=b;
<op2>: t0:=t;
enter(id,vvariable);
<op3>: enter(id,vvariable);
<op4>: t1:=t;
调用处理类型的过程;{返回eltp,elrf,elsz}
<op5>: WHILE t0<t1 DO
BEGIN
t0:=t0+1;
tab[t0].typ:=eltp;
tab[t0].ref:=elrf;
tab[t0].normal:=true;
tab[t0].adr:=offset;
offset:=offset+elsz;
END;
<op6>: sz:=offset;
btab[rf].vsize:=offset;
btab[rf].psize:=0;
level:=level-1;
最后,我们讨论数组类型的处理。
<类型>→ARRAY[<op1><常量><op2>..<常量><op3>
{,<op4><常量><op2>..<常量><op3>}]OF<类型><op5>
它要求在数组表中建立如图7.4的串记录并返回rf及size,其中
elsize n=elsz {elsz由处理类型的过程返回}
elsize i=size i+1{1<=i<n}
size i=(h i-l i+1)*elsize i {1<=i<=n}
可以看出,这一串记录的建立是一个递归的过程,相当于把给定的多维数组看作是
ARRAY [l1..h1] OF
ARRAY [l2..h2] OF
…
ARRAY [ln..hn] OF<类型>
图7.4
这样处理的好处是使数组元素的地址计算得到了简化。
具体算法如下:<op1>: tp:=arrays;
<op2>: IF low.tp=reals THEN
error(n)
ENDIF;{low由处理常量的过程返回}
<op3>: IF high.tp < > low.tp THEN
error(n)
ENDIF;{high由处理常量的过程返回}
enterarray(low.tp,low.i,high.i);
rf:=a;
<op4>: eltp:=ARRAY;
递归地调用处理数组的过程;
<op5>: atab[rf].eltyp:=eltp;
atab[rf].elref:=elrf;
atab[rf].elsize:=elsz;
atab[rf].size:=(high.i-low.i+1)*elsz;
sz:=atab[rf].size;
7.3.4 子程序说明
子程序使我们能利用语言所提供的基本结构来定义更高级的操作,从而,在编译时可以不受语言的基本结构的束缚,尽量使用问题本身的术语来描述它的解,就好象语言是可扩充的一样。
PASCAL-S中有两种形式的子程序——过程与函数。
它们都是在更高的层次上为一组操作命名,函数区别于过程的主要点是它需要返回一个值。
子程序说明是用来定义子程序的,包括两个部分:一部分定义子程序与
外界的接口,或称调用约定;这部分包括子程序名、参数的数目、类型及传递方式(对函数来说,返回值的类型也在这个接口中提供)。
另一部分定义了子程序的操作,称为子程序体;这部分包括一些局部量的定义以及实现子程序操作的一些语句。
子程序定义后要在调用时执行。
为了实现这一点,编译程序在处理子程序说明时必须为子程序生成目标代码,以便在调用子程序时执行。
子程序的目标结构为
入口地址:
此外,为了检查对子程序的调用是否正确,处理子程序说明时还必须将有关调用约定的信息保存起来。
以过程说明为例,在扫描到过程标识符时,应将它登记在标识符表中,并在块表中建立相应记录。
但有些信息在此时是无法填入的,因此要记住标识符及块表中相应记录的位置,以便回填。
在处理参数表时,将各参数的属性如参数类型,传递方式(值参或变量参数)等插入标识符表中。
参数表处理完后,需要回填块表中相应记录的lastpar及psize域。
过程的各局部量说明及定义过程操作的语句部分要由各种说明的处理过程及语句的处理过程分别处理,结果是为各种局部量分配存贮(包括在表格区的分配及位移量分配),为过程体的语句生成代码。
在此期间,需要回填块表中相应记录的vsize域以及标识符表中相应记录的adr域(过程入口地址)。
当整个过程说明处理完后,要生成返回指令EXITP。
注意,过程的形式参数及局部量是局部于过程的,所以在处理参数表之前要执行对符号表的进层操作,而在过程说明处理完后要执行复位操作。
嵌入语义操作的文法规则为
<过程说明>→PROCEDURE<标识符><op1>{<形参表><op2>};
<说明部分>;<op3><复合语句>;<op4>
各语义操作为
<op1>: enter(id,prozedure);
enter block;
level:=level+1;
display[level]:=b;
prb :=b;
prt :=t;
dx :=5;
<op2>: btab[prb].lastpar:=t;
btab[prb].psize:=dx;
<op3>: tab[prt].adr:=lc;{入口地址}
<op4>: 生成EXITP指令;
level:=level-1;
对函数说明来说,除了还要处理函数的类型之外(这可以通过检索标识符表来完成),所需要的加工操作与过程基本相同,这里就不讨论了。
7.4 语句部分的处理
语句部分完成所需要的计算,每个语句都导致一组计算机动作的执行。
编译程序在处理语句时的主要工作是将语句所表达的计算翻译成一组目标指令,即代码生成。
当然,在进行代码生成时,必须检查语句中所使用的实体与说明中的定义是否一致。
在PASCAL-S的编译程序中,代码生成就是向代码区CODE发送所生成的指令。
令lc为一个指示器,它始终指向要生成的下一条指令在代码区CODE中的位置,则发送代码的工作可由如下三个过程完成:
PROCEDURE emit(operator:integer);
{发送无操作数指令}
code[lc].f:=operator;
lc:=lc+1
END-PROCEDURE;
PROCEDURE emit1(operator,operand:integer);
{发送单操作数指令}
code[lc].f:=operator
code[lc].y:=operand;
lc:=lc+1
END- PROCEDURE;
PROCEDURE emit2(operator,operand1,operand2:integer);
{发送双操作数指令}
code[lc].f:=operator;
code[lc].x:=operand1;
code[lc].y:=operand2;
lc:=lc+1
END-PROCEDURE;
每一种语句应该翻译成什么样的目标指令(称为目标结构),是由它的语义所规定的。
在PASCAL-S中,基本语句包括:赋值、分支(IF与CASE)、循环(REPEAT,WHILE与FOR)、过程调用。
下面我们分别讨论这些语句的目标结构及翻译算法。
7.4.1 表达式与赋值语句。