你的位置:首页 > Java教程

[Java教程]PL/0语言编译器的设计与实现


 

一、设计任务

1.1程序实现要求

PL/0语言可以看成PASCAL语言的子集,它的编译程序是一个编译解释执行系统。PL/0的目标程序为假想栈式计算机的汇编语言,与具体计算机无关。

PL/0的编译程序和目标程序的解释执行程序都是用JAVA语言书写的,因此PL/0语言可在配备JDK的任何机器上实现。

其编译过程采用一趟扫描方式,以语法分析程序为核心,词法分析和代码生成程序都作为一个独立的过程,当语法分析需要读单词时就调用词法分析程序,而当语法分析正确需要生成相应的目标代码时,则调用代码生成程序。

用表格管理程序建立变量、常量和过程标示符的说明与引用之间的信息联系。

用出错处理程序对词法和语法分析遇到的错误给出在源程序中出错的位置和错误性质。

当源程序编译正确时,PL/0编译程序自动调用解释执行程序,对目标代码进行解释执行,并按用户程序的要求输入数据和输出运行结果。

1.2 PL/0语言的BNF描述(扩充的巴克斯范式表示法)

<prog> → program <id>;<block>

<block> → [<condecl>][<vardecl>][<proc>]<body>

<condecl> → const <const>{,<const>};

<const> → <id>:=<integer>

<vardecl> → var <id>{,<id>};

<proc> → procedure <id>([<id>{,<id>}]);<block>{;<proc>}

<body> → begin <statement>{;<statement>}end

<statement> → <id> := <exp>

               |if <lexp> then <statement>[else <statement>]

               |while <lexp> do <statement>

               |call <id>([<exp>{,<exp>}])

               |<body>

               |read (<id>{,<id>})

               |write (<exp>{,<exp>})

<lexp> → <exp> <lop> <exp>|odd <exp>

<exp> → [+|-]<term>{<aop><term>}

<term> → <factor>{<mop><factor>}

<factor>→<id>|<integer>|(<exp>)

<lop> → =|<>|<|<=|>|>=

<aop> → +|-

<mop> → *|/

<id> → l{l|d}   (注:l表示字母)

<integer> → d{d}

注释:

<prog>:程序 ;<block>:块、程序体 ;<condecl>:常量说明 ;<const>:常量;<vardecl>:变量说明 ;<proc>:分程序 ; <body>:复合语句 ;<statement>:语句;<exp>:表达式 ;<lexp>:条件 ;<term>:项 ; <factor>:因子 ;<aop>:加法运算符;<mop>:乘法运算符; <lop>:关系运算符。

1.3假想目标机的代码

LIT 0 ,a 取常量a放入数据栈栈顶

OPR 0 ,a 执行运算,a表示执行某种运算

LOD L ,a 取变量(相对地址为a,层差为L)放到数据栈的栈顶

STO L ,a 将数据栈栈顶的内容存入变量(相对地址为a,层次差为L)

CAL L ,a 调用过程(转子指令)(入口地址为a,层次差为L)

INT 0 ,a 数据栈栈顶指针增加a

JMP 0 ,a无条件转移到地址为a的指令

JPC 0 ,a 条件转移指令,转移到地址为a的指令

RED L ,a 读数据并存入变量(相对地址为a,层次差为L)

WRT 0 ,0 将栈顶内容输出

代码的具体形式:

其中:F段代表伪操作码

      L段代表调用层与说明层的层差值

      A段代表位移量(相对地址)

进一步说明:

INT:为被调用的过程(包括主过程)在运行栈S中开辟数据区,这时A段为所需数据单元个数(包括三个连接数据);L段恒为0。

CAL:调用过程,这时A段为被调用过程的过程体(过程体之前一条指令)在目标程序区的入口地址。

LIT:将常量送到运行栈S的栈顶,这时A段为常量值。

LOD:将变量送到运行栈S的栈顶,这时A段为变量所在说明层中的相对位置。

STO:将运行栈S的栈顶内容送入某个变量单元中,A段为变量所在说明层中的相对位置。

JMP:无条件转移,这时A段为转向地址(目标程序)。

JPC:条件转移,当运行栈S的栈顶的布尔值为假(0)时,则转向A段所指目标程序地址;否则顺序执行。

OPR:关系或算术运算,A段指明具体运算,例如A=2代表算术运算“+”;A=12代表关系运算“>”等等。运算对象取自运行栈S的栈顶及次栈顶。

1.4假想机的结构

两个存储器:存储器CODE,用来存放P的代码数据存储器STACK(栈)用来动态分配数据空间

四个寄存器:

一个指令寄存器I:存放当前要执行的代码

一个栈顶指示器寄存器T:指向数据栈STACK的栈顶

一个基地址寄存器B:存放当前运行过程的数据区在STACK中的起始地址

一个程序地址寄存器P:存放下一条要执行的指令地址该假想机没有供运算用的寄存器。所有运算都要在数据栈STACK的栈顶两个单元之间进行,并用运算结果取代原来的两个运算对象而保留在栈顶

1.5活动记录:

RA:返回地址

SL:保存该过程直接外层的活动记录首地址

DL:调用者的活动记录首地址

过程返回可以看成是执行一个特殊的OPR运算

注意:层次差为调用层次与定义层次的差值

二、功能结构设计

2.1综述

本PL/0编译器共包括词法分析、语法分析、语义分析(包括符号表管理和目标代码生成)、活动记录的组织(解释执行程序)、错误处理四大部分组成。

2.2详细说明

2.2.1词法分析

根据所给的PL/0语言的BNF描述,该语言的组成单词包括以下元素:

关键字(程序保留字){program,const,var,procedure,begin,end,if,else,then,call,while,do,read,write}

运算符:{+,-,*,/,odd}

界符:{“,”,“;”,“(”,“)”}

关系运算符:{=,<,<=,>,>=,<>}

数字:只能为整型,且常量不可以是负数

标识符:由用户定义,以字母开头,由数字和字母组成

词法分析程序读取源文件,识别出上述关键字、界符、关系运算符、数字、标识符五种元素,输出到lex.txt文件中,供后续的语法分析程序使用。其中,

(1)除标识符外,剩余的每一种字符可以用数字表示;

(2)而标识符则统一用一个数字表示其为标识符,再将标识符本身存储起来;

(3)数字的存储和标识符存储类似。

例如:

将关键字、界符、关系运算符共29中符号从1到29依次编号,1表示关键字program,2表示关键字const,以此类推,到29表示关系运算符<>,

为了和下面标识符、数字的存储统一起来,将识别出的关键字以如下形式

保存在lex.txt文件中。如program则存储为(1,-)。

用30表示是标识符,如定义了一个变量v1,则其存储为(30,v1)。

用31表示是数字,如12,其存储为(31,13)。

2.2.2语法分析

语法分析结合BNF产生式,利用递归下降的方法实现。具体实现方式是,为每一个非终结符编写一个子程序,以<prog> → program <id>;<block>此产生式为列,用伪码描述其子程序如下:

void vardecl(){

if(当前指向的字符==program){

           指向下一个字符;

           if(当前指向的字符==id){  //是标识符

    指向下一个字符;

    if(当前指向的字符==;){

    block();

}else{

    error;

}

}

}else{

    error

}                       

}

说明:由于之前的词法分析使用数字来描述的,故此处的program,id(即标识符),分号等都可以用相应的数字代替。

2.2.3语义分析

语义分析在语法分析的基础上完成,涉及到的操作有符号表的管理和目标代码的生成,分别对应说明语句和处理语句,下面分开来说。

(1)符号表管理

符号表中存储以下数据:定义的变量、定义的常量、定义的过程;

定义的变量需要存储:变量的标识符,变量定义所在层次,相对于该层次基地址的偏移量(对于基地址将在后面活动记录中详细说明)

定义的常量需要存储:常量的标识符,常量的值,定义所在层次

定义的过程需要存储:过程名,过程处理语句的开始地址(处理语句不是说明语句,说明语句中涉及到符号表的操作,而处理语句中涉及到产生目标代码的操作),过程定义所在层次;

具体操作用伪码描述:

a.常量说明的翻译:

void condecl(){

    if(当前指向的字符==const){

       指向下一个字符;

       const();

       while(当前指向的字符==,){

           指向下一个字符;

           const();

       }

    }

}

void const(){

    if(当前指向的字符==id){

       指向下一个字符;

       用name记录下字符;

       if(当前指向的字符== := ){

           指向下一个字符;

           if(当前指向的字符==数字){

              用value记录下值;

              enter(name,value,level,addr);//将其记录到符号表

           }

       }

    }

}

b.变量说明的翻译:

void vardecl(){

    if(当前指向的字符==id){

       用name记录下字符;

       enter(name,level,addr);

       指向下一个字符;

       while(当前指向的字符==,){

           指向下一个字符;

           if(当前指向的字符==id){

              用name记录下字符;

              enter(name,level,addr);

              指向下一个字符;

           }

       }

    }

}

c.过程说明的翻译:

proc的登录符号表的操作与上述方法类似,不再赘述。

d.补充

但是上面我们并没有涉及如何求得层次差和偏移量,获得层次差和偏移量的思路大体如下,在整个语法分析函数中,定义两个全局变量level和address,level初始化为1,其中主程序是第一层,每次遇到过程嵌套定义就将level加1,过程定义结束后再将其减1,这样下来就实现了获得说明语句所在层次的功能;address初始化为0,每次在符号表中登录变量后就将address加1,其实address的作用是,结合level,可以获得每一层有几个变量,这样在活动记录的分配(也叫运行时存储空间的划分,运行时只为变量事先划分存储空间),可以根据该层变量的个数划分相应的存储空间。

(2)目标代码的产生

这部分涉及到翻译模式相关的知识,由于说明语句是不产生目标代码的,在结合PL/0的BNF描述,会产生目标代码的只有<block>、<body>、<statement>、<lexp>、<exp>、<term>、<factor>、<lop>、<mop>,下面一次用伪码说明其翻译模式:

a.<block>:

分析可以发现,主程序除去program<id>;,过程除去procedure<id>([id{,id}]),剩下的部分都是block。则进入block有两种情况:(1)从主程序进入,此时符号表中没有任何元素,也没有产生任何目标代码;(2)从过程进入,此时符号表中有了该子过程所有外层过程定义的变量,常量和过程,并且还产生了一部分目标代码;对于第一种情况比较简单,不再赘述,而对于第二种情况,则比较复杂。现在我们将主程序处理语句开始的地址叫做程序的入口,由于嵌套过程的存在,导致寻找程序入口有些麻烦,在这里我们用到了回填技术:

l  在进入block的第一步就先产生一个无条件跳转指令,由于中间子过程嵌套还会产生目标代码 ,导致不知道跳转到哪里才是主程序的入口,这里我们先记下这个无条件跳转指令在目标代码中的位置,当程序分析完变量说明、常量说明和过程说明即将进入语句处理的分析时,这时目标代码的地址就是主程序的入口,我们根据刚才记下的无条件跳转指令的位置将这个地址回填到跳转指令的目标地址上,从而目标代码第一句就是跳转到主程序入口处的指令,对于程序中嵌套程序,用这个方法依然是可行的;

l  除此之外,在进行其他任何分析之前,还要记录下主程序或者当前过程的数据量,即符号表中的变量数目,也即address,以便后面活动记录的划分可以在结束过程后返回到原来的数据栈的位置;

l  另外,如果是从过程进入到block,在产生过程调用指令需要用到这个入口地址,所以这个过程入口地址也要记录下来,用来填写过程调用指令的目标位置。则在进行其他任何分析之前,此时符号表中最新的一项必定是该过程的相关信息,我们可以将本过程的value值设为其入口地址,因为value值对于符号表procedure来说是无用的,即将开始地址登入到符号表,从而在做任何分析前我们也要记录下符号表中的最新项(记录其序号),在经过变量说明分析、常量说明分析、过程分析后即将进入语句处理分析时,便得到了该过程的入口地址,将其登录到刚刚记录的那个符号表最新项的value域;

可以通过判断符号表中的最新项的序号是否为0来判断是从过程进入<block>还是从过程进入<block>

 

进行完上述三步操作后即可进入语句的处理部分;

在语句的处理部分完成后,生成目标代码的退出过程或主程序的语句,然后恢复address和符号表的序号。

b.<body>

此部分不直接产生目标代码

c.<statement>

<statement>中有赋值语句(<id>:=<exp>)、if语句、while语句、call语句、read语句、write语句,下面分别说明其翻译模式:

l  赋值语句:此部分涉及到活动记录的组织,先大致说一下:本目标机是哈佛架构,数据存储器和代码存储器是分开的,这里活动记录的组织或者说运行时存储空间的组织是指数据存储器的组织,(代码存储器不需要什么组织,目标代码顺序存放,解释程序按目标代码的意义进行相关控制流的操作等),对于每一个过程或主程序,都要开辟3+变量个数的存储空间,其中3个是用来存放SL,DL,RA的,这里我们遇到一个变量就生成STO L,A目标代码,其中L和A据可由符号表中存储的相关信息获得,意思是将此时数据栈栈顶的元素,通过层差L(调用位置和定义位置之差)和偏移地址A找到这个变量在其定义的过程开辟的空间中为该变量开辟的存储空间,然后将栈顶元素存到这个位置,具体的生成看代码;

 

l  if语句:用伪码描述

if <lexp>

产生条件转移指令“JPC,0,0”;

用记录cx1上面那条条件转移指令的位置;

then

<statement>

产生无条件转移指令“JMP,0,0”;

用cx2记录上面那条无条件转移指令的位置;

cx3为即将产生的目标代码的位置;

用cx3回填cx1和cx2记录位置的指令

else

<statement>

cx4为即将产生的目标代码的位置;

用cx4回填cx2记录位置的指令

l  while语句:

while<lexp>do

产生田间转移指令“JPC,0,0”

用cx1记录下上面那条条件转移指令的位置

<statement>

产生无条件转移指令“JMP,0,0”

cx2为即将产生的目标代码的位置

用cx2回填cx1记录位置的指令

 

l  call语句:

产生“CAL,L,A”,意思是通过层差L和过程入口地址A,找到过程的入口地址,这里A可以有该过程在符号表中的value域的值获得(原因见<block>的翻译)

 

l  read语句:

首先从命令行获取一个值放在栈顶,然后将栈顶值赋值给相应变量(赋值过程见前面赋值语句的翻译)

 

l  write语句:

直接输出栈顶值

其他运算的翻译较为简单,不再赘述.

 

2.2.4活动记录的组织

此部分主要是活动记录的组织,具体如下:

为每一个过程开辟的新的存储空间,最低层三个用来存储SL:保存该过程直接外层的活动首地址;DL:调用者的活动记录首地址;RA:返回地址;

具体的执行方式见代码注释。

2.2.5难点说明

l  变量(常数、过程)作用域的确定,分为两步:一步是定义变量时,我们要查询在本层有无同名的变量已经定义,若有,则报错;二步是变量使用时,首先查询本层有无定义该变量,若有,则使用本层定义的变量,若无,则寻找本过程直接外层有无定义同名变量,若仍无此变量,则继续向下一个直接外层寻找,以此类推。若有,则使用最靠近本层的那个定义,若无,则报错。由于本符号表采用的是数组管理,在实现第一步时,在变量(常数、过程)登录符号表时记录其所在的层次和所在层次的名字,若符号表中存在变量(常量、过程)的名字、所在层次和所在层次的名字均相同,则报错,若无,则将定义的变量加到符号表中。在实现第二步时,结合语法的特点分析,首先在本调用过程所在的过程进行查找有无定义该变量,若有,则就使用本过程的变量,若无,则根据前面在符号表中记录的本调用过程所在的直接外层的名字查找该直接外层有无定义此变量,以此类推,若最终有定义,则使用该变量,否则保存。

l  过程传递参数:我把在过程定义时的形式参数,当作在本过程定义变量进行处理,在调用过程中出传入的实在参数当作对相应的形式参数进行赋值,具体说来就是用该过程在符号表中的size域存储该过程中的形参个数,再用一个变量记录过程中定义参数的个数(具体做法是定义一个变量,每次进入block时置为3,用来存储SL、DL、RA,然后遇到变量定义就加一来记录)当遇到过程定义时,将其说明的形参按照在本过程中定义变量的相关属性登录到符号表中,并且他们在活动记录的存储位置是紧接着SL,DL,RA;在遇到调用过程时,首先根据栈顶、栈顶-1、栈顶-2、栈顶-形参个数+1根据所存储的位置进行赋值,然后再分配相应的活动存储空间(如果刚开始就分配,则会导致栈顶更新,失去了原来的栈顶元素),该活动存储空间的大小应该是参数个数+本过程定义的变量个数+3。

 

三、函数说明

3.1类的作用的说明

l  RValue+LexAnalysis+chTable完成词法分析。

l  AllPcode+PerPcode完成目标代码生成。

l  SymTable+TableRow完成符号表管理。

l  Interpreter完成解释执行程序。

l  MpgAnalysis的一个函数showError完成错误处理程序。

l  MpgAnalysis综合上述类完成编译功能。

3.2具体函数功能的说明

四、源代码

PL/0 Compiler